Initial commit

This commit is contained in:
PhaseOfficial 2026-06-04 14:13:19 +02:00
parent ab1dc1da83
commit 35be3c029b
128 changed files with 35795 additions and 30 deletions

65
.gitignore vendored
View File

@ -1,35 +1,44 @@
# ---> Android # Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files
# Gradle files
.gradle/
build/
# Local configuration file (sdk path, etc) # dependencies
local.properties node_modules/
# Log/OS Files # Expo
*.log .expo/
dist/
web-build/
expo-env.d.ts
# Android Studio generated files and folders # Native
captures/ .kotlin/
.externalNativeBuild/ *.orig.*
.cxx/
*.apk
output.json
# IntelliJ
*.iml
.idea/
misc.xml
deploymentTargetDropDown.xml
render.experimental.xml
# Keystore files
*.jks *.jks
*.keystore *.p8
*.p12
*.key
*.mobileprovision
# Google Services (e.g. APIs or Firebase) # Metro
google-services.json .metro-health-check*
# Android Profiling # debug
*.hprof npm-debug.*
yarn-debug.*
yarn-error.*
# macOS
.DS_Store
*.pem
# local env files
.env
.env*.local
# typescript
*.tsbuildinfo
app-example
# generated native folders
/ios
/android

1
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1 @@
{ "recommendations": ["expo.vscode-expo-tools"] }

7
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit",
"source.sortMembers": "explicit"
}
}

107
README.md
View File

@ -1,3 +1,106 @@
# nextgenmobile # Atomic Habits AI - Habit Tracker & Personal Library
This is the starting point for Mobile App Development for NextGen Atomic Habits AI is a cross-platform mobile and web application designed to help users build lasting habits through a science-backed tracking system and a personalized AI-powered library. It combines local-first performance with cloud synchronization and AI insights.
---
## 🏗 Architecture
The application follows a **Local-First, Cloud-Sync** architecture.
### Frontend
- **Framework:** [Expo](https://expo.dev/) (React Native) with [Expo Router](https://docs.expo.dev/router/introduction/) for file-based routing.
- **Language:** TypeScript.
- **State Management:** Local SQLite database for offline-first capabilities, synced with Supabase.
- **UI/UX:** Custom design system built with `Vanilla CSS` (via React Native Stylesheets), utilizing `Lucide React Native` for iconography and `Reanimated` for smooth transitions.
### Backend & Cloud
- **Platform:** [Supabase](https://supabase.com/)
- **Authentication:** Supabase Auth (Email/Password, JWT).
- **Database:** PostgreSQL (Cloud) + SQLite (Local).
- **Storage:** Supabase Storage for PDF/Epub books.
- **Edge Functions:** Supabase Edge Functions (Deno) for AI processing and book parsing.
### AI Integration
- **Models:** Gemini (via Supabase Edge Functions).
- **Features:** Automated book metadata extraction, AI-generated reading insights/synthesis, and interactive habit coaching.
---
## 🔐 Security
Security is integrated at every layer of the stack:
1. **Authentication:** Secure session management using `AsyncStorage` and Supabase JWTs.
2. **Row Level Security (RLS):** Supabase RLS policies ensure users can only access their own habits, books, and logs.
3. **Secure Storage:** sensitive files are accessed via **Signed URLs** with limited expiration windows (1 hour) rather than public links.
4. **Local Encryption:** (Optional/Roadmap) Local SQLite data protection.
5. **Environment Variables:** All sensitive keys (Supabase URL, Anon Key) are managed via `.env` files and `EXPO_PUBLIC_` prefixes.
---
## 🗄 Database Schema
The system uses a mirrored schema between local SQLite and cloud PostgreSQL.
### Core Tables:
- **`books`**: Stores library items (`id`, `user_id`, `title`, `author`, `file_uri`, `current_page`, `status`).
- **`habits`**: Core habit tracking logic (`id`, `user_id`, `name`, `frequency`, `streak`, `last_completed`).
- **`reading_logs`**: Tracks progress over time (`id`, `book_id`, `duration_seconds`, `pages_read`).
- **`chat_history`**: Persists AI interactions for context-aware coaching.
---
## 🛠 Running the Project
### Prerequisites
- Node.js (v18+)
- Expo Go app (for physical device testing) or Android Studio / Xcode (for emulators).
### Installation
```bash
# Clone the repository
git clone <repo-url>
cd atomichabitsai
# Install dependencies
npm install
```
### Development
```bash
# Start the Expo development server
npx expo start
```
- Press **`a`** for Android emulator.
- Press **`i`** for iOS simulator.
- Press **`w`** for web.
---
## 🐞 Debugging Details
### Logging
The application uses structured logs for critical paths:
- **`[PdfReader]`**: WebView lifecycle and file writing logs.
- **`[downloadBook]`**: Sync and storage fetch status.
- **`[resolveFileUri]`**: Path resolution debugging for iOS container changes.
### Common Troubleshooting
1. **Stale iOS Paths:** If files don't open on iOS after an update, the `resolveFileUri` utility automatically corrects absolute paths that contain outdated container UUIDs.
2. **Sync Failures:** Check the `performMutation` calls in `sync.ts`. Ensure the device has internet access for Supabase connectivity.
3. **PDF Reader Crashes:** Ensure the library `@bildau/rn-pdf-reader` is correctly patched via `patch-package` to handle the Legacy FileSystem API in Expo 54.
---
## 🧑‍💻 Backend Developer Notes
### Edge Functions
Located in `/supabase/functions/`:
- **`process-book-ai`**: Parses uploaded filenames to suggest titles/authors and generates reading summaries.
- **`chat-ai`**: The core engine for the AI habit coach.
### Storage
- Bucket name: `books`
- Access: Private (accessed via `createSignedUrl` in `src/lib/file-utils.ts`).

68
app.json Normal file
View File

@ -0,0 +1,68 @@
{
"expo": {
"name": "RCS BatsirAI",
"slug": "atomichabitsai",
"version": "1.0.0",
"orientation": "portrait",
"icon": "./assets/images/icon.png",
"scheme": "atomichabitsai",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"ios": {
"supportsTablet": true,
"bundleIdentifier": "com.phaseofficial.atomichabitsai",
"infoPlist": {
"ITSAppUsesNonExemptEncryption": false
}
},
"android": {
"adaptiveIcon": {
"backgroundColor": "#E6F4FE",
"foregroundImage": "./assets/images/android-icon-foreground.png",
"backgroundImage": "./assets/images/android-icon-background.png",
"monochromeImage": "./assets/images/android-icon-monochrome.png"
},
"edgeToEdgeEnabled": true,
"predictiveBackGestureEnabled": false,
"package": "com.phaseofficial.atomichabitsai"
},
"web": {
"output": "static",
"favicon": "./assets/images/favicon.png"
},
"plugins": [
[
"expo-router",
{
"origin": "https://atomichabits.ai/",
"root": "src/app"
}
],
[
"expo-splash-screen",
{
"image": "./assets/images/favicon.png",
"imageWidth": 200,
"resizeMode": "contain",
"backgroundColor": "#ffffff",
"dark": {
"backgroundColor": "#000000"
}
}
]
],
"experiments": {
"typedRoutes": true,
"reactCompiler": true
},
"extra": {
"router": {
"origin": "https://atomichabits.ai/",
"root": "src/app"
},
"eas": {
"projectId": "5f2929a1-ae47-4616-a893-8c7161eaafe9"
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
assets/images/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
assets/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

230
assets/ui/mobile/ai.html Normal file
View File

@ -0,0 +1,230 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Serene Path | AI Assistant</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&amp;family=Plus_Jakarta_Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"inverse-surface": "#0b0f0f",
"primary-container": "#cbe9db",
"on-secondary-container": "#40536d",
"on-primary-fixed": "#2a443b",
"inverse-on-surface": "#9b9d9d",
"surface-container": "#eaefee",
"error-container": "#fa746f",
"surface-container-lowest": "#ffffff",
"surface-variant": "#dde4e3",
"secondary-fixed": "#d3e3ff",
"tertiary-fixed-dim": "#e5d8f0",
"secondary-container": "#d3e3ff",
"on-primary-fixed-variant": "#466156",
"surface-bright": "#f8faf9",
"on-tertiary": "#fef6ff",
"on-secondary-fixed-variant": "#4a5d77",
"surface-container-highest": "#dde4e3",
"on-surface": "#2d3433",
"surface-tint": "#4a655a",
"tertiary": "#655b6f",
"error": "#a83836",
"tertiary-fixed": "#f4e6ff",
"surface-container-low": "#f1f4f3",
"on-background": "#2d3433",
"on-secondary": "#f8f8ff",
"secondary": "#4e607b",
"error-dim": "#67040d",
"surface": "#f8faf9",
"on-primary-container": "#3d574d",
"surface-dim": "#d4dbda",
"on-secondary-fixed": "#2e405a",
"on-error-container": "#6e0a12",
"outline": "#757c7b",
"on-tertiary-fixed": "#4a4154",
"secondary-dim": "#42546f",
"on-surface-variant": "#596060",
"primary": "#4a655a",
"on-error": "#fff7f6",
"surface-container-high": "#e4e9e8",
"secondary-fixed-dim": "#c2d6f5",
"tertiary-dim": "#594f63",
"primary-fixed-dim": "#bedbce",
"on-tertiary-fixed-variant": "#675d71",
"outline-variant": "#acb3b2",
"on-tertiary-container": "#5c5367",
"primary-dim": "#3e594e",
"tertiary-container": "#f4e6ff",
"inverse-primary": "#d7f5e7",
"on-primary": "#e5fff2",
"background": "#f8faf9",
"primary-fixed": "#cbe9db"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Plus Jakarta Sans"]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.pb-safe { padding-bottom: env(safe-area-inset-bottom); }
.glass-panel {
background: rgba(248, 250, 249, 0.8);
backdrop-filter: blur(20px);
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-surface font-body text-on-surface selection:bg-primary-container selection:text-on-primary-container">
<!-- Top Navigation -->
<header class="bg-[#f8faf9] dark:bg-slate-950 flex justify-between items-center w-full px-6 py-4 docked full-width top-0 sticky z-50 no-line-rule bg-[#f1f4f3] dark:bg-slate-900">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full overflow-hidden bg-surface-container-highest">
<img alt="user profile picture" class="w-full h-full object-cover" data-alt="close-up portrait of a woman with a serene expression in natural morning light" src="https://lh3.googleusercontent.com/aida-public/AB6AXuB0QWWVOvRdrj_Vfv5qQoDF-mEaziKJXhCbqzC06NAQq0cpzmhkRJn1Ed4n_T_3QWCAnmigxZO4JzN9jrfrm_XSVAxXYyTUJezo-_y3z9mYRoNr0kJ4y08raLXinTZHGYzgUYq2SxUhQWNiZBz56x7EPeLpl-wUywiTEoLA0UjZgzjuFd8HLfyOjrsXS9lxWEwusrtPSZxIIH7O_sK1mC9COn1Gz_Ha-evCwLjMsr32-bYQxsGuERtyJyPuTEXAqvxDZLBQazO6yAWp"/>
</div>
<h1 class="text-xl font-bold text-[#4a655a] dark:text-[#cbe9db] font-['Manrope'] tracking-tight">Serene Path</h1>
</div>
<div class="flex items-center gap-4">
<button class="text-[#acb3b2] dark:text-slate-500 hover:opacity-80 transition-opacity duration-300">
<span class="material-symbols-outlined" data-icon="settings">settings</span>
</button>
</div>
</header>
<main class="max-w-4xl mx-auto px-6 pt-8 pb-48 min-h-screen">
<!-- Status Indicator -->
<div class="flex justify-center mb-8">
<div class="inline-flex items-center gap-3 px-4 py-2 bg-surface-container-low rounded-full">
<div class="relative flex h-2 w-2">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
<span class="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
</div>
<span class="font-label text-label-sm text-primary font-medium tracking-wide">AI is syncing your plan...</span>
</div>
</div>
<!-- Chat History -->
<div class="space-y-8">
<!-- AI Message -->
<div class="flex flex-col items-start gap-3 max-w-[85%]">
<div class="flex items-center gap-2 px-1">
<span class="font-label text-label-sm font-bold text-primary uppercase tracking-widest">Serene AI</span>
<span class="font-label text-[10px] text-outline-variant">10:02 AM</span>
</div>
<div class="bg-surface-container-low p-6 rounded-2xl rounded-tl-none">
<p class="text-body-lg text-on-surface leading-relaxed">
Good morning. I've analyzed your sleep data and upcoming tasks. You seem to have a productive window between 10 AM and 12 PM. Would you like me to block this time for your deep work session?
</p>
</div>
</div>
<!-- User Message -->
<div class="flex flex-col items-end gap-3 max-w-[85%] ml-auto">
<div class="flex items-center gap-2 px-1">
<span class="font-label text-[10px] text-outline-variant">10:05 AM</span>
<span class="font-label text-label-sm font-bold text-secondary uppercase tracking-widest">You</span>
</div>
<div class="bg-primary text-on-primary p-6 rounded-2xl rounded-tr-none shadow-sm">
<p class="text-body-lg leading-relaxed">
Yes, please. Also, can you check if I have any reading assignments for tonight's book club?
</p>
</div>
</div>
<!-- AI Message -->
<div class="flex flex-col items-start gap-3 max-w-[85%]">
<div class="flex items-center gap-2 px-1">
<span class="font-label text-label-sm font-bold text-primary uppercase tracking-widest">Serene AI</span>
<span class="font-label text-[10px] text-outline-variant">10:05 AM</span>
</div>
<div class="bg-surface-container-low p-6 rounded-2xl rounded-tl-none">
<p class="text-body-lg text-on-surface leading-relaxed mb-4">
Of course. I've scheduled your deep work block. Regarding your book club: you have to finish Chapter 4 of "The Art of Stillness".
</p>
<div class="bg-surface-container-highest/50 p-4 rounded-xl flex items-center gap-4">
<div class="w-12 h-16 bg-surface-container rounded shadow-sm overflow-hidden flex-shrink-0">
<img alt="book cover" class="w-full h-full object-cover" data-alt="minimalist book cover with abstract blue shapes on a cream background" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBcWYI5cWHVTopKWhKJ7aSqKjnVBMPazwJ3XxcalI1000uiyX91i6Y42-JlBlkvq-VMh6SetBPeJMBGepDkoEqF-81CffQd3gkgsv5PCP7tEFoNL2HPvgr58fPUQW5BP7qNnfkgLi8cqJ9tnfGQAtqeipABtPM4Tqx8kIFlVYKxtH5rM5kwhxc7Y3qo0YjpET_9q2aAQAeww_gNQHC4Xyk_SfRVOkwZIb-6ktMF-mwjiCa79fvimJGzgi90EULOoYwahb3-dje6BtEP"/>
</div>
<div>
<p class="font-headline font-bold text-sm">Chapter 4: The Inner Journey</p>
<p class="font-label text-xs text-on-surface-variant">12 pages remaining • 15 min read</p>
</div>
</div>
</div>
</div>
<!-- Empty space for layout rhythm -->
<div class="h-12"></div>
</div>
</main>
<!-- Bottom Input & Controls Container -->
<div class="fixed bottom-0 left-0 w-full z-40 bg-surface/80 backdrop-blur-xl">
<div class="max-w-4xl mx-auto px-6 py-6">
<!-- Suggested Prompts -->
<div class="flex gap-3 mb-6 overflow-x-auto no-scrollbar pb-2">
<button class="whitespace-nowrap px-5 py-2.5 bg-surface-container-high rounded-full font-label text-sm font-semibold text-on-surface-variant hover:bg-primary-container hover:text-on-primary-container transition-all duration-300">
Update my schedule
</button>
<button class="whitespace-nowrap px-5 py-2.5 bg-surface-container-high rounded-full font-label text-sm font-semibold text-on-surface-variant hover:bg-primary-container hover:text-on-primary-container transition-all duration-300">
Set a reminder for reading
</button>
<button class="whitespace-nowrap px-5 py-2.5 bg-surface-container-high rounded-full font-label text-sm font-semibold text-on-surface-variant hover:bg-primary-container hover:text-on-primary-container transition-all duration-300">
Summarize my week
</button>
</div>
<!-- Chat Input Bar -->
<div class="relative group">
<div class="absolute inset-0 bg-primary/5 rounded-[2rem] blur-xl opacity-0 group-focus-within:opacity-100 transition-opacity duration-500"></div>
<div class="relative flex items-center gap-2 bg-surface-container-low rounded-[2rem] p-2 pr-4 transition-all duration-300 focus-within:bg-surface-container-lowest focus-within:shadow-xl">
<button class="w-12 h-12 flex items-center justify-center text-primary rounded-full hover:bg-primary/10 transition-colors">
<span class="material-symbols-outlined" data-icon="mic">mic</span>
</button>
<input class="flex-1 bg-transparent border-none focus:ring-0 text-body-lg placeholder:text-outline-variant px-2" placeholder="Message Serene AI..." type="text"/>
<button class="w-12 h-12 flex items-center justify-center bg-primary text-on-primary rounded-full hover:scale-105 active:scale-95 transition-all duration-300 shadow-lg shadow-primary/20">
<span class="material-symbols-outlined" data-icon="send">send</span>
</button>
</div>
</div>
</div>
<!-- Global Bottom Nav -->
<nav class="bg-[#f8faf9]/80 dark:bg-slate-950/80 backdrop-blur-xl fixed bottom-0 w-full z-50 pb-safe no-line-rule tonal-layering-top shadow-[0_-10px_40px_rgba(45,52,51,0.05)] dark:shadow-none flex justify-around items-center w-full px-4 py-3">
<a class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db]" href="#">
<span class="material-symbols-outlined mb-1" data-icon="dashboard">dashboard</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider">Focus</span>
</a>
<a class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db]" href="#">
<span class="material-symbols-outlined mb-1" data-icon="calendar_today">calendar_today</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider">Plan</span>
</a>
<a class="flex flex-col items-center justify-center bg-[#cbe9db] dark:bg-[#4a655a]/30 text-[#4a655a] dark:text-[#cbe9db] rounded-[1.5rem] px-5 py-2.5 transition-all duration-500" href="#">
<span class="material-symbols-outlined mb-1" data-icon="smart_toy" style="font-variation-settings: 'FILL' 1;">smart_toy</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider">AI</span>
</a>
<a class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db]" href="#">
<span class="material-symbols-outlined mb-1" data-icon="auto_graph">auto_graph</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider">Habits</span>
</a>
<a class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db]" href="#">
<span class="material-symbols-outlined mb-1" data-icon="menu_book">menu_book</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider">Library</span>
</a>
</nav>
</div>
</body></html>

BIN
assets/ui/mobile/ai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,267 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Serene Path - Daily Plan</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;700;800&amp;family=Plus_Jakarta_Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"inverse-surface": "#0b0f0f",
"primary-container": "#cbe9db",
"on-secondary-container": "#40536d",
"on-primary-fixed": "#2a443b",
"inverse-on-surface": "#9b9d9d",
"surface-container": "#eaefee",
"error-container": "#fa746f",
"surface-container-lowest": "#ffffff",
"surface-variant": "#dde4e3",
"secondary-fixed": "#d3e3ff",
"tertiary-fixed-dim": "#e5d8f0",
"secondary-container": "#d3e3ff",
"on-primary-fixed-variant": "#466156",
"surface-bright": "#f8faf9",
"on-tertiary": "#fef6ff",
"on-secondary-fixed-variant": "#4a5d77",
"surface-container-highest": "#dde4e3",
"on-surface": "#2d3433",
"surface-tint": "#4a655a",
"tertiary": "#655b6f",
"error": "#a83836",
"tertiary-fixed": "#f4e6ff",
"surface-container-low": "#f1f4f3",
"on-background": "#2d3433",
"on-secondary": "#f8f8ff",
"secondary": "#4e607b",
"error-dim": "#67040d",
"surface": "#f8faf9",
"on-primary-container": "#3d574d",
"surface-dim": "#d4dbda",
"on-secondary-fixed": "#2e405a",
"on-error-container": "#6e0a12",
"outline": "#757c7b",
"on-tertiary-fixed": "#4a4154",
"secondary-dim": "#42546f",
"on-surface-variant": "#596060",
"primary": "#4a655a",
"on-error": "#fff7f6",
"surface-container-high": "#e4e9e8",
"secondary-fixed-dim": "#c2d6f5",
"tertiary-dim": "#594f63",
"primary-fixed-dim": "#bedbce",
"on-tertiary-fixed-variant": "#675d71",
"outline-variant": "#acb3b2",
"on-tertiary-container": "#5c5367",
"primary-dim": "#3e594e",
"tertiary-container": "#f4e6ff",
"inverse-primary": "#d7f5e7",
"on-primary": "#e5fff2",
"background": "#f8faf9",
"primary-fixed": "#cbe9db"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Plus Jakarta Sans"]
}
},
},
}
</script>
<style>
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.tonal-layering-top {
background: linear-gradient(to bottom, rgba(241, 244, 243, 0.8), rgba(248, 250, 249, 0));
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-surface font-body text-on-surface min-h-screen">
<!-- TopAppBar -->
<header class="bg-[#f8faf9] dark:bg-slate-950 flex justify-between items-center w-full px-6 py-4 docked full-width top-0 sticky z-50 no-line-rule bg-[#f1f4f3] dark:bg-slate-900 flat no shadows">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full overflow-hidden">
<img class="w-full h-full object-cover" data-alt="portrait of a calm professional woman in soft natural lighting with a clean minimal background" src="https://lh3.googleusercontent.com/aida-public/AB6AXuD49eKVuQZyJyYBJcOmRRo39IaSMMjWKxi2ZPiDnfuKD_xnuNSc0pphkXzgRSltlM50Wo9bnXaIO5nhBWtx8tK8wi7TOm7a_4VTcHKEpjoMXPyJ0jayRV6uJgv5FVBdjxKuGqXRxMbr1jiZK4yKxjKlPJbZqwy1ODOShkyUi5MBJpz-pG9fLdGBO91RwkLxV_0dXiVXGY-IG32s1MTF5OxH2AePThaKuOgFQR81fRjTssWXg4g0KpoXrylm0q-QtXZ5YrNa0JBc2wLJ"/>
</div>
<h1 class="text-xl font-bold text-[#4a655a] dark:text-[#cbe9db] font-['Manrope'] tracking-tight">Serene Path</h1>
</div>
<div class="flex items-center gap-4">
<button class="text-[#acb3b2] dark:text-slate-500 hover:opacity-80 transition-opacity duration-300">
<span class="material-symbols-outlined">settings</span>
</button>
</div>
</header>
<main class="max-w-5xl mx-auto px-6 pt-6 pb-32">
<!-- View Toggle & Date Header -->
<section class="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-12">
<div>
<p class="font-label text-sm text-secondary font-semibold uppercase tracking-[0.15em] mb-2">Tuesday</p>
<h2 class="font-headline text-5xl font-extrabold tracking-tight text-on-surface">Oct 24</h2>
</div>
<div class="flex bg-surface-container-low p-1.5 rounded-full items-center">
<button class="px-6 py-2.5 rounded-full bg-surface-container-lowest shadow-sm font-label text-sm font-bold text-primary transition-all">Day</button>
<button class="px-6 py-2.5 rounded-full text-outline font-label text-sm font-medium hover:text-on-surface transition-all">Week</button>
</div>
</section>
<!-- Timeline Grid -->
<div class="relative grid grid-cols-[80px_1fr] gap-x-8">
<!-- Time Markers -->
<div class="space-y-[80px] pt-4">
<div class="font-label text-xs font-bold text-outline-variant text-right pr-4">08:00</div>
<div class="font-label text-xs font-bold text-outline-variant text-right pr-4">09:00</div>
<div class="font-label text-xs font-bold text-outline-variant text-right pr-4">10:00</div>
<div class="font-label text-xs font-bold text-outline-variant text-right pr-4">11:00</div>
<div class="font-label text-xs font-bold text-outline-variant text-right pr-4">12:00</div>
<div class="font-label text-xs font-bold text-outline-variant text-right pr-4">13:00</div>
<div class="font-label text-xs font-bold text-outline-variant text-right pr-4">14:00</div>
<div class="font-label text-xs font-bold text-outline-variant text-right pr-4">15:00</div>
<div class="font-label text-xs font-bold text-outline-variant text-right pr-4">16:00</div>
<div class="font-label text-xs font-bold text-outline-variant text-right pr-4">17:00</div>
<div class="font-label text-xs font-bold text-outline-variant text-right pr-4">18:00</div>
<div class="font-label text-xs font-bold text-outline-variant text-right pr-4">19:00</div>
<div class="font-label text-xs font-bold text-outline-variant text-right pr-4">20:00</div>
</div>
<!-- Content Area -->
<div class="relative bg-surface-container-low/30 rounded-3xl min-h-[1040px] border border-surface-container-high/50">
<!-- Grid Horizontal Lines -->
<div class="absolute inset-0 flex flex-col pointer-events-none">
<div class="h-[92px] border-b border-surface-container-high/40"></div>
<div class="h-[92px] border-b border-surface-container-high/40"></div>
<div class="h-[92px] border-b border-surface-container-high/40"></div>
<div class="h-[92px] border-b border-surface-container-high/40"></div>
<div class="h-[92px] border-b border-surface-container-high/40"></div>
<div class="h-[92px] border-b border-surface-container-high/40"></div>
<div class="h-[92px] border-b border-surface-container-high/40"></div>
<div class="h-[92px] border-b border-surface-container-high/40"></div>
<div class="h-[92px] border-b border-surface-container-high/40"></div>
<div class="h-[92px] border-b border-surface-container-high/40"></div>
<div class="h-[92px] border-b border-surface-container-high/40"></div>
<div class="h-[92px] border-b border-surface-container-high/40"></div>
</div>
<!-- Task Blocks -->
<!-- Sleep Data (Apple Health Integration) -->
<div class="absolute top-0 left-4 right-4 h-[120px] bg-tertiary-container/30 backdrop-blur-md rounded-2xl p-4 flex flex-col justify-between border-l-4 border-tertiary shadow-sm">
<div class="flex justify-between items-start">
<h3 class="font-headline font-bold text-tertiary">Sleep Cycle</h3>
<span class="material-symbols-outlined text-tertiary text-lg" style="font-variation-settings: 'FILL' 1;">favorite</span>
</div>
<div class="flex items-center gap-2">
<span class="font-label text-xs font-bold text-on-tertiary-container uppercase tracking-wider">Health Data • 7h 20m</span>
</div>
</div>
<!-- Morning Routine -->
<div class="absolute top-[140px] left-4 right-4 h-[80px] bg-primary-container rounded-2xl p-4 flex items-center justify-between shadow-sm">
<div class="flex items-center gap-4">
<div class="w-10 h-10 bg-on-primary-container/10 rounded-full flex items-center justify-center">
<span class="material-symbols-outlined text-on-primary-container">light_mode</span>
</div>
<div>
<h3 class="font-headline font-bold text-on-primary-container">Morning Routine</h3>
<p class="font-label text-xs text-on-primary-container/70">Meditation &amp; Journaling</p>
</div>
</div>
<span class="font-label text-xs font-bold text-on-primary-container/60">09:30 - 10:15</span>
</div>
<!-- Deep Work Block -->
<div class="absolute top-[240px] left-4 right-4 h-[220px] bg-primary rounded-2xl p-6 shadow-xl flex flex-col justify-between">
<div>
<div class="flex justify-between items-start mb-2">
<h3 class="font-headline text-2xl font-bold text-on-primary">Deep Work</h3>
<button class="w-8 h-8 rounded-full bg-on-primary/10 flex items-center justify-center text-on-primary">
<span class="material-symbols-outlined text-sm">more_horiz</span>
</button>
</div>
<p class="font-body text-on-primary/80 max-w-sm">Focusing on the Product Strategy Deck and Quarter 4 planning. Phone in DND mode.</p>
</div>
<div class="flex items-center gap-4">
<span class="px-3 py-1 bg-on-primary/10 rounded-full font-label text-[10px] font-bold text-on-primary uppercase tracking-widest">Focus Mode</span>
<div class="h-1 w-1 rounded-full bg-on-primary/30"></div>
<span class="font-label text-xs font-medium text-on-primary/70">10:45 - 12:45</span>
</div>
</div>
<!-- Lunch Break -->
<div class="absolute top-[480px] left-4 right-4 h-[70px] bg-surface-container-high rounded-2xl px-6 flex items-center justify-between border-dashed border-2 border-outline-variant/30">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-secondary">restaurant</span>
<h3 class="font-headline font-bold text-on-surface-variant">Lunch Break</h3>
</div>
<span class="font-label text-xs font-medium text-outline">13:00 - 13:45</span>
</div>
<!-- Client Meeting -->
<div class="absolute top-[570px] left-4 right-4 h-[90px] bg-secondary-container rounded-2xl p-4 flex items-center justify-between shadow-sm">
<div class="flex items-center gap-4">
<div class="flex -space-x-2">
<img class="w-8 h-8 rounded-full border-2 border-secondary-container object-cover" data-alt="headshot of a smiling man with glasses" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAz7ESSe-84qXiBDvD4uadJMFf9550mrVSy0fTyFvnsBteXsKz_0dhRXy3PXY3emgvqKUhp4BL1ma3ttDbU8YaDUOaAprKhuWHK3P-J9w0OMu4cNy1jXB-O0SZUCzDcHW07VqM1d7UpUdfAUfyUEzYgotxvw2lOJpNl84rJiqgiTSEehqmZefB52e0oaPiDKrCof7FzSvvdTHsI5RwWAobb_iupZqghGgyaENH4GY5ghsbRjPztlJocL3N--zFd2iopmyOT_RRnDs97"/>
<img class="w-8 h-8 rounded-full border-2 border-secondary-container object-cover" data-alt="headshot of a confident woman in a blazer" src="https://lh3.googleusercontent.com/aida-public/AB6AXuBqrlyGgX_5i_lRONU78TaViRxqEky5ORJslxtzjoKmQEvhQwUe3QJbNzfwAai-moc6IaD5fkTwcLBDBOOa8PiXqUO49dV3ViRpJX1eGp_rX0PAXrzPXmwCCgTsRbd-Uowb1BxhW25VYWVIRveFP1VWfobcRQJYoMJxrGb9t3P6SwfMF2aRzmLI4kxLpWQMHpUi5bObYCGCcd1ZbLOTHve7fHicEc99qXbuvAKmLG1AVTS0etWXgVYEnu8QcYhi-ldGRlrY7F7xHFbx"/>
</div>
<div>
<h3 class="font-headline font-bold text-on-secondary-container">Creative Review</h3>
<p class="font-label text-xs text-on-secondary-container/70">With Design Team</p>
</div>
</div>
<span class="font-label text-xs font-bold text-on-secondary-container/60">14:00 - 15:00</span>
</div>
<!-- Afternoon Walk -->
<div class="absolute top-[750px] left-4 right-4 h-[60px] bg-tertiary-fixed-dim/40 rounded-2xl px-6 flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-tertiary">directions_walk</span>
<h3 class="font-headline font-bold text-on-tertiary-container">Mindful Walk</h3>
</div>
<span class="font-label text-xs font-medium text-on-tertiary-fixed-variant">16:30 - 17:15</span>
</div>
<!-- FAB Trigger Line (Visual Current Time Indicator) -->
<div class="absolute top-[680px] left-0 right-0 h-0.5 bg-error/40 flex items-center">
<div class="w-3 h-3 rounded-full bg-error ml-[-6px] shadow-sm shadow-error/40"></div>
</div>
</div>
</div>
</main>
<!-- Floating Action Button -->
<button class="fixed bottom-24 right-6 w-14 h-14 bg-primary text-on-primary rounded-full shadow-[0_10px_30px_rgba(74,101,90,0.3)] flex items-center justify-center scale-100 hover:scale-105 active:scale-95 transition-all z-40">
<span class="material-symbols-outlined text-3xl">add</span>
</button>
<!-- BottomNavBar -->
<nav class="bg-[#f8faf9]/80 dark:bg-slate-950/80 backdrop-blur-xl fixed bottom-0 w-full z-50 pb-safe no-line-rule tonal-layering-top shadow-[0_-10px_40px_rgba(45,52,51,0.05)] dark:shadow-none flex justify-around items-center w-full px-4 py-3">
<div class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db]">
<span class="material-symbols-outlined mb-1">dashboard</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider">Focus</span>
</div>
<div class="flex flex-col items-center justify-center bg-[#cbe9db] dark:bg-[#4a655a]/30 text-[#4a655a] dark:text-[#cbe9db] rounded-[1.5rem] px-5 py-2.5 transition-all duration-500 scale-90">
<span class="material-symbols-outlined mb-1">calendar_today</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider">Plan</span>
</div>
<div class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db]">
<span class="material-symbols-outlined mb-1">smart_toy</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider">AI</span>
</div>
<div class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db]">
<span class="material-symbols-outlined mb-1">auto_graph</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider">Habits</span>
</div>
<div class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db]">
<span class="material-symbols-outlined mb-1">menu_book</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider">Library</span>
</div>
</nav>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,239 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Serene Path</title>
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
<style>
:root {
--bg-color: #ffffff;
--secondary-bg: #f8f9fa;
--text-primary: #1a1c1e;
--text-secondary: #5f6368;
--accent-blue: #e8f0fe;
--border-color: #dadce0;
}
body {
font-family: 'Google Sans', Roboto, Helvetica, Arial, sans-serif;
background-color: var(--secondary-bg);
margin: 0;
display: flex;
justify-content: center;
color: var(--text-primary);
}
.app-shell {
width: 100%;
max-width: 480px;
background: var(--bg-color);
min-height: 100vh;
padding: 24px;
box-sizing: border-box;
display: flex;
flex-direction: column;
padding-bottom: 100px;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 32px;
}
.logo { font-weight: 500; font-size: 1.1rem; }
.greeting-container { margin-bottom: 32px; }
.label-caps {
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 1.5px;
color: var(--text-secondary);
text-transform: uppercase;
}
.greeting-text {
font-size: 1.5rem;
font-weight: 400;
margin-top: 8px;
line-height: 1.3;
}
.section-title {
font-size: 0.75rem;
font-weight: 700;
color: var(--text-secondary);
letter-spacing: 1px;
margin: 24px 0 12px;
text-transform: uppercase;
}
.focus-card {
background: var(--accent-blue);
border-radius: 24px;
padding: 24px;
margin-bottom: 24px;
}
.focus-title { font-weight: 700; font-size: 1rem; margin-bottom: 8px; }
.focus-desc { font-size: 0.9rem; color: #444; line-height: 1.5; margin-bottom: 20px; }
.btn-group { display: flex; gap: 12px; }
.btn {
border-radius: 100px;
padding: 10px 20px;
font-size: 0.85rem;
font-weight: 500;
border: none;
cursor: pointer;
display: flex;
align-items: center;
gap: 8px;
}
.btn-primary { background: var(--text-primary); color: white; }
.btn-secondary { background: white; border: 1px solid var(--border-color); }
.progress-row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 0;
}
.percentage { font-size: 1.5rem; font-weight: 700; }
.habit-item {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 0;
}
.icon-done { color: #1e8e3e; }
.icon-todo { color: var(--border-color); }
.energy-grid {
display: flex;
gap: 8px;
margin-top: 12px;
}
.energy-chip {
flex: 1;
padding: 12px;
border: 1px solid var(--border-color);
border-radius: 16px;
text-align: center;
font-size: 0.85rem;
}
.management-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-top: 12px;
}
.tool-card {
border: 1px solid var(--border-color);
border-radius: 16px;
padding: 16px 8px;
text-align: center;
font-size: 0.7rem;
}
.tool-card .material-icons { margin-bottom: 8px; color: #1a73e8; }
.nav-bar {
position: fixed;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 100%;
max-width: 480px;
background: white;
display: flex;
justify-content: space-around;
padding: 12px 0;
border-top: 1px solid var(--border-color);
}
.nav-item {
display: flex;
flex-direction: column;
align-items: center;
font-size: 0.7rem;
color: var(--text-secondary);
text-decoration: none;
}
.nav-item.active { color: var(--text-primary); }
</style>
</head>
<body>
<div class="app-shell">
<header>
<div class="logo">Serene Path</div>
<span class="material-icons">settings</span>
</header>
<div class="greeting-container">
<div class="label-caps">GOOD MORNING, ALEX</div>
<div class="greeting-text">Today is a fresh canvas. Take a deep breath.</div>
</div>
<div class="section-title">Today's Focus 9:30 AM — 11:00 AM</div>
<div class="focus-card">
<div class="focus-title">DEEP WORK: INTERFACE REFINEMENT</div>
<div class="focus-desc">Focus on the typography hierarchy and tonal layering transitions for the mobile navigation shell.</div>
<div class="btn-group">
<button class="btn btn-primary"><span class="material-icons">play_arrow</span> Continue Session</button>
<button class="btn btn-secondary">Reschedule</button>
</div>
</div>
<div class="section-title">Reading Progress</div>
<div class="progress-row">
<div>
<div style="font-weight: 700;">Building a Second Brain</div>
<div style="font-size: 0.85rem; color: var(--text-secondary);">Tiago Forte • 42 pages left</div>
</div>
<div class="percentage">65%</div>
</div>
<div class="section-title">KEYSTONE HABITS</div>
<div style="font-size: 0.85rem; color: var(--text-secondary); margin-bottom: 8px;">2/3 Done</div>
<div class="habit-item">
<span class="material-icons icon-done">check_circle</span>
<span>Morning Meditation</span>
</div>
<div class="habit-item">
<span class="material-icons icon-done">check_circle</span>
<span>Deep Work Session</span>
</div>
<div class="habit-item">
<span class="material-icons icon-todo">radio_button_unchecked</span>
<span>20min Evening Walk</span>
</div>
<div class="section-title">ENERGY CHECK-IN</div>
<div style="font-size: 0.9rem;">How are you feeling right now? Listen to your body.</div>
<div class="energy-grid">
<div class="energy-chip">Drained</div>
<div class="energy-chip">Balanced</div>
<div class="energy-chip">Radiant</div>
</div>
<div class="section-header section-title">MANAGEMENT DECK</div>
<div class="management-grid">
<div class="tool-card"><span class="material-icons">ads_click</span><br>Google Ads</div>
<div class="tool-card"><span class="material-icons">social_leaderboard</span><br>Facebook</div>
<div class="tool-card"><span class="material-icons">analytics</span><br>Analytics</div>
<div class="tool-card"><span class="material-icons">mail</span><br>Inbox</div>
<div class="tool-card"><span class="material-icons">payments</span><br>Finance</div>
<div class="tool-card"><span class="material-icons">add</span><br>Add Tool</div>
</div>
<nav class="nav-bar">
<a href="#" class="nav-item active"><span class="material-icons">dashboard</span>Focus</a>
<a href="#" class="nav-item"><span class="material-icons">calendar_today</span>Plan</a>
<a href="#" class="nav-item"><span class="material-icons">smart_toy</span>AI</a>
<a href="#" class="nav-item"><span class="material-icons">auto_graph</span>Habits</a>
<a href="#" class="nav-item"><span class="material-icons">menu_book</span>Library</a>
</nav>
</div>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,323 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Serene Path | Habit &amp; Identity Tracker</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&amp;family=Plus+Jakarta+Sans:wght@300..800&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"inverse-surface": "#0b0f0f",
"primary-container": "#cbe9db",
"on-secondary-container": "#40536d",
"on-primary-fixed": "#2a443b",
"inverse-on-surface": "#9b9d9d",
"surface-container": "#eaefee",
"error-container": "#fa746f",
"surface-container-lowest": "#ffffff",
"surface-variant": "#dde4e3",
"secondary-fixed": "#d3e3ff",
"tertiary-fixed-dim": "#e5d8f0",
"secondary-container": "#d3e3ff",
"on-primary-fixed-variant": "#466156",
"surface-bright": "#f8faf9",
"on-tertiary": "#fef6ff",
"on-secondary-fixed-variant": "#4a5d77",
"surface-container-highest": "#dde4e3",
"on-surface": "#2d3433",
"surface-tint": "#4a655a",
"tertiary": "#655b6f",
"error": "#a83836",
"tertiary-fixed": "#f4e6ff",
"surface-container-low": "#f1f4f3",
"on-background": "#2d3433",
"on-secondary": "#f8f8ff",
"secondary": "#4e607b",
"error-dim": "#67040d",
"surface": "#f8faf9",
"on-primary-container": "#3d574d",
"surface-dim": "#d4dbda",
"on-secondary-fixed": "#2e405a",
"on-error-container": "#6e0a12",
"outline": "#757c7b",
"on-tertiary-fixed": "#4a4154",
"secondary-dim": "#42546f",
"on-surface-variant": "#596060",
"primary": "#4a655a",
"on-error": "#fff7f6",
"surface-container-high": "#e4e9e8",
"secondary-fixed-dim": "#c2d6f5",
"tertiary-dim": "#594f63",
"primary-fixed-dim": "#bedbce",
"on-tertiary-fixed-variant": "#675d71",
"outline-variant": "#acb3b2",
"on-tertiary-container": "#5c5367",
"primary-dim": "#3e594e",
"tertiary-container": "#f4e6ff",
"inverse-primary": "#d7f5e7",
"on-primary": "#e5fff2",
"background": "#f8faf9",
"primary-fixed": "#cbe9db"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Plus Jakarta Sans"]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
display: inline-block;
line-height: 1;
text-transform: none;
letter-spacing: normal;
word-wrap: normal;
white-space: nowrap;
direction: ltr;
}
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-background text-on-background font-body selection:bg-primary-container selection:text-on-primary-container">
<!-- Top Navigation Bar -->
<header class="bg-[#f8faf9] dark:bg-slate-950 flex justify-between items-center w-full px-6 py-4 docked full-width top-0 sticky z-50 no-line-rule bg-[#f1f4f3] dark:bg-slate-900 flat no shadows">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full overflow-hidden">
<img class="w-full h-full object-cover" data-alt="professional portrait of a calm woman in a soft-lit studio with neutral earth tones" src="https://lh3.googleusercontent.com/aida-public/AB6AXuC54N1fh97FMb60y8DmfYnS3DaiEd8HINjSzIXwOna5i3kvq6CA5IiGlkBUXxAs_R537Hgy7Z7bmlp4DA_UUxINVOXOe850ADHPnhSD1N7yAqlQJPLdsESO2TSah8tLoaNTEXlx54QJVzHS77oW7hO8ToCZnim4t1-JU0FcNzHPbwpE0vAIQ7oz4ObhTOuyjePph_dIFrYJTxaz1mKCrqgPC1rsb0yrz0hC4MyhdcLqXuonW7l5P6h7zwJ4D0luoBY5_YyYwpkoEZw8"/>
</div>
<span class="text-xl font-bold text-[#4a655a] dark:text-[#cbe9db] font-headline tracking-tight">Serene Path</span>
</div>
<div class="flex items-center gap-4">
<button class="p-2 hover:opacity-80 transition-opacity duration-300 scale-95 duration-300 ease-in-out text-[#4a655a] dark:text-[#cbe9db]">
<span class="material-symbols-outlined" data-icon="settings">settings</span>
</button>
</div>
</header>
<main class="max-w-5xl mx-auto px-6 py-10 pb-32">
<!-- Identity Header: "Who I am becoming" -->
<section class="mb-12">
<div class="flex flex-col md:flex-row md:items-end justify-between gap-6 mb-8">
<div class="max-w-2xl">
<span class="font-label text-label-sm text-tertiary uppercase tracking-[0.2em] mb-3 block">Foundation</span>
<h1 class="font-headline text-5xl font-extrabold tracking-tight text-on-surface mb-4">Who I am becoming</h1>
<p class="text-on-surface-variant text-lg leading-relaxed">Focusing on identity-based growth rather than just checking boxes.</p>
</div>
<div class="hidden md:block">
<div class="bg-surface-container-low rounded-xl p-6 flex items-center gap-4">
<div class="w-12 h-12 rounded-full bg-primary-container flex items-center justify-center text-primary">
<span class="material-symbols-outlined" data-icon="auto_awesome">auto_awesome</span>
</div>
<div>
<p class="font-label text-[10px] uppercase tracking-wider text-outline">Current Focus</p>
<p class="font-bold text-on-surface">The Disciplined Reader</p>
</div>
</div>
</div>
</div>
<!-- Identity Cards (Bento Style) -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="md:col-span-2 bg-gradient-to-br from-primary to-primary-dim rounded-xl p-8 text-on-primary relative overflow-hidden">
<div class="relative z-10">
<span class="bg-white/20 backdrop-blur-sm px-3 py-1 rounded-full text-xs font-bold uppercase tracking-widest mb-6 inline-block">Primary Identity</span>
<h2 class="text-3xl font-bold mb-4 leading-tight italic">"I am a person who prioritizes deep knowledge over superficial distraction."</h2>
<div class="flex items-center gap-2 mt-8">
<span class="material-symbols-outlined text-sm" data-icon="verified" style="font-variation-settings: 'FILL' 1;">verified</span>
<span class="font-label text-sm tracking-wide">Rooted in 12 consistent days</span>
</div>
</div>
<div class="absolute top-0 right-0 w-64 h-64 bg-primary-container/10 rounded-full -mr-20 -mt-20 blur-3xl"></div>
</div>
<div class="bg-surface-container-lowest rounded-xl p-8 border-outline-variant/15 border flex flex-col justify-between">
<div>
<h3 class="font-headline text-xl font-bold text-on-surface mb-2">Secondary Shift</h3>
<p class="text-on-surface-variant italic">"I am a healthy athlete who treats my body with respect."</p>
</div>
<div class="mt-6 flex items-center justify-between">
<div class="flex -space-x-2">
<div class="w-8 h-8 rounded-full bg-secondary-container flex items-center justify-center text-xs border-2 border-white">M</div>
<div class="w-8 h-8 rounded-full bg-secondary-container flex items-center justify-center text-xs border-2 border-white">T</div>
<div class="w-8 h-8 rounded-full bg-primary flex items-center justify-center text-xs border-2 border-white text-white">W</div>
</div>
<span class="text-primary font-bold text-sm">Active</span>
</div>
</div>
</div>
</section>
<!-- Burnout Alert Section (Conditional UI) -->
<section class="mb-12">
<div class="bg-error-container/10 border-l-4 border-error-container p-6 rounded-r-xl flex flex-col md:flex-row items-center gap-6">
<div class="w-14 h-14 bg-error-container rounded-full flex items-center justify-center text-on-error-container animate-pulse shrink-0">
<span class="material-symbols-outlined text-3xl" data-icon="warning" style="font-variation-settings: 'FILL' 1;">warning</span>
</div>
<div class="flex-grow">
<h4 class="font-headline text-lg font-bold text-on-error-container mb-1">Burnout Alert: Momentum is Fading</h4>
<p class="text-on-error-container/80 text-sm">You've missed 4 habits in the last 24 hours. The path to growth requires rhythm, but also recovery.</p>
</div>
<button class="bg-error-container text-on-error-container font-label text-xs font-bold px-6 py-3 rounded-full hover:brightness-95 transition-all shrink-0">
REST &amp; RECHARGE
</button>
</div>
</section>
<!-- Keystone Habits & Progress -->
<section class="grid grid-cols-1 lg:grid-cols-12 gap-10">
<!-- Habit List -->
<div class="lg:col-span-8">
<div class="flex items-center justify-between mb-6">
<h3 class="font-headline text-2xl font-bold">Keystone Habits</h3>
<button class="text-primary font-label text-sm font-bold flex items-center gap-2">
<span class="material-symbols-outlined text-lg" data-icon="add_circle">add_circle</span>
NEW HABIT
</button>
</div>
<div class="space-y-4">
<!-- Habit Item 1 -->
<div class="group bg-surface-container-lowest hover:bg-surface-bright transition-all p-6 rounded-2xl flex items-center gap-6 shadow-sm border border-outline-variant/5">
<button class="w-14 h-14 rounded-full border-4 border-primary-container flex items-center justify-center text-primary-container hover:text-primary transition-colors">
<span class="material-symbols-outlined text-2xl" data-icon="check_circle">check_circle</span>
</button>
<div class="flex-grow">
<div class="flex items-center gap-3 mb-1">
<h4 class="font-headline font-bold text-lg text-on-surface">Morning Meditation</h4>
<span class="bg-secondary-container/50 text-on-secondary-container px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-tighter">Identity: Calm Observer</span>
</div>
<p class="text-on-surface-variant text-sm">"I don't react, I observe my thoughts without judgment."</p>
</div>
<div class="text-right shrink-0">
<p class="text-xs font-label text-outline uppercase tracking-widest">Streak</p>
<p class="text-2xl font-bold text-primary">12d</p>
</div>
</div>
<!-- Habit Item 2 -->
<div class="group bg-surface-container-lowest hover:bg-surface-bright transition-all p-6 rounded-2xl flex items-center gap-6 shadow-sm border border-outline-variant/5">
<button class="w-14 h-14 rounded-full bg-primary flex items-center justify-center text-on-primary">
<span class="material-symbols-outlined text-2xl" data-icon="done_all" style="font-variation-settings: 'FILL' 1;">done_all</span>
</button>
<div class="flex-grow opacity-60">
<div class="flex items-center gap-3 mb-1">
<h4 class="font-headline font-bold text-lg text-on-surface line-through">Read 20 Pages</h4>
<span class="bg-primary-container text-on-primary-container px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-tighter">Identity: Lifelong Learner</span>
</div>
<p class="text-on-surface-variant text-sm">"Knowledge is the compound interest of my identity."</p>
</div>
<div class="text-right shrink-0">
<p class="text-xs font-label text-outline uppercase tracking-widest">Streak</p>
<p class="text-2xl font-bold text-primary">45d</p>
</div>
</div>
<!-- Habit Item 3 -->
<div class="group bg-surface-container-lowest hover:bg-surface-bright transition-all p-6 rounded-2xl flex items-center gap-6 shadow-sm border border-outline-variant/5">
<button class="w-14 h-14 rounded-full border-4 border-primary-container flex items-center justify-center text-primary-container hover:text-primary transition-colors">
<span class="material-symbols-outlined text-2xl" data-icon="check_circle">check_circle</span>
</button>
<div class="flex-grow">
<div class="flex items-center gap-3 mb-1">
<h4 class="font-headline font-bold text-lg text-on-surface">Evening Reflection</h4>
<span class="bg-tertiary-container text-on-tertiary-container px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-tighter">Identity: Intentional Soul</span>
</div>
<p class="text-on-surface-variant text-sm">"I review my day to refine my tomorrow."</p>
</div>
<div class="text-right shrink-0">
<p class="text-xs font-label text-outline uppercase tracking-widest">Streak</p>
<p class="text-2xl font-bold text-primary">0d</p>
</div>
</div>
</div>
</div>
<!-- Visualization / Weekly Summary -->
<div class="lg:col-span-4">
<div class="bg-surface-container rounded-3xl p-8 sticky top-24">
<h3 class="font-headline text-xl font-bold mb-6">Weekly Rhythm</h3>
<div class="space-y-6">
<!-- Chart Mock -->
<div class="flex items-end justify-between h-32 gap-2 mb-8">
<div class="flex flex-col items-center gap-2 flex-1">
<div class="w-full bg-primary-fixed-dim rounded-t-lg h-[60%]"></div>
<span class="text-[10px] font-label text-outline">M</span>
</div>
<div class="flex flex-col items-center gap-2 flex-1">
<div class="w-full bg-primary-fixed-dim rounded-t-lg h-[85%]"></div>
<span class="text-[10px] font-label text-outline">T</span>
</div>
<div class="flex flex-col items-center gap-2 flex-1">
<div class="w-full bg-primary rounded-t-lg h-[100%]"></div>
<span class="text-[10px] font-label text-outline font-bold">W</span>
</div>
<div class="flex flex-col items-center gap-2 flex-1">
<div class="w-full bg-surface-container-highest rounded-t-lg h-[20%]"></div>
<span class="text-[10px] font-label text-outline">T</span>
</div>
<div class="flex flex-col items-center gap-2 flex-1">
<div class="w-full bg-surface-container-highest rounded-t-lg h-[15%]"></div>
<span class="text-[10px] font-label text-outline">F</span>
</div>
<div class="flex flex-col items-center gap-2 flex-1">
<div class="w-full bg-surface-container-highest rounded-t-lg h-[10%]"></div>
<span class="text-[10px] font-label text-outline">S</span>
</div>
<div class="flex flex-col items-center gap-2 flex-1">
<div class="w-full bg-surface-container-highest rounded-t-lg h-[5%]"></div>
<span class="text-[10px] font-label text-outline">S</span>
</div>
</div>
<div class="space-y-4">
<div class="flex justify-between items-center">
<span class="text-sm font-label text-on-surface-variant">Weekly Completion</span>
<span class="text-sm font-bold text-primary">78%</span>
</div>
<div class="h-3 w-full bg-surface-container-highest rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-primary to-primary-fixed-dim w-[78%]"></div>
</div>
</div>
<div class="pt-6 border-t border-outline-variant/10">
<p class="font-label text-xs text-outline leading-relaxed mb-4">Identity reinforcement suggests you are 14 days away from these habits feeling "automatic." Keep going.</p>
<button class="w-full py-4 bg-surface-container-lowest rounded-2xl text-on-surface font-bold text-sm hover:bg-white transition-all shadow-sm">
View Deep Analytics
</button>
</div>
</div>
</div>
</div>
</section>
</main>
<!-- Bottom Navigation Bar -->
<nav class="fixed bottom-0 w-full z-50 pb-safe bg-[#f8faf9]/80 dark:bg-slate-950/80 backdrop-blur-xl no-line-rule tonal-layering-top shadow-[0_-10px_40px_rgba(45,52,51,0.05)] dark:shadow-none">
<div class="flex justify-around items-center w-full px-4 py-3">
<a class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] scale-90 duration-500 ease-in-out" href="#">
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider mt-1">Focus</span>
</a>
<a class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] scale-90 duration-500 ease-in-out" href="#">
<span class="material-symbols-outlined" data-icon="calendar_today">calendar_today</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider mt-1">Plan</span>
</a>
<a class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] scale-90 duration-500 ease-in-out" href="#">
<span class="material-symbols-outlined" data-icon="smart_toy">smart_toy</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider mt-1">AI</span>
</a>
<a class="flex flex-col items-center justify-center bg-[#cbe9db] dark:bg-[#4a655a]/30 text-[#4a655a] dark:text-[#cbe9db] rounded-[1.5rem] px-5 py-2.5 transition-all duration-500 scale-90 duration-500 ease-in-out" href="#">
<span class="material-symbols-outlined" data-icon="auto_graph" style="font-variation-settings: 'FILL' 1;">auto_graph</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider mt-1">Habits</span>
</a>
<a class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] scale-90 duration-500 ease-in-out" href="#">
<span class="material-symbols-outlined" data-icon="menu_book">menu_book</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider mt-1">Library</span>
</a>
</div>
</nav>
</body></html>

BIN
assets/ui/mobile/habits.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View File

@ -0,0 +1,287 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Serene Path - Library</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&amp;family=Plus_Jakarta_Sans:wght@500;600;700&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"inverse-surface": "#0b0f0f",
"primary-container": "#cbe9db",
"on-secondary-container": "#40536d",
"on-primary-fixed": "#2a443b",
"inverse-on-surface": "#9b9d9d",
"surface-container": "#eaefee",
"error-container": "#fa746f",
"surface-container-lowest": "#ffffff",
"surface-variant": "#dde4e3",
"secondary-fixed": "#d3e3ff",
"tertiary-fixed-dim": "#e5d8f0",
"secondary-container": "#d3e3ff",
"on-primary-fixed-variant": "#466156",
"surface-bright": "#f8faf9",
"on-tertiary": "#fef6ff",
"on-secondary-fixed-variant": "#4a5d77",
"surface-container-highest": "#dde4e3",
"on-surface": "#2d3433",
"surface-tint": "#4a655a",
"tertiary": "#655b6f",
"error": "#a83836",
"tertiary-fixed": "#f4e6ff",
"surface-container-low": "#f1f4f3",
"on-background": "#2d3433",
"on-secondary": "#f8f8ff",
"secondary": "#4e607b",
"error-dim": "#67040d",
"surface": "#f8faf9",
"on-primary-container": "#3d574d",
"surface-dim": "#d4dbda",
"on-secondary-fixed": "#2e405a",
"on-error-container": "#6e0a12",
"outline": "#757c7b",
"on-tertiary-fixed": "#4a4154",
"secondary-dim": "#42546f",
"on-surface-variant": "#596060",
"primary": "#4a655a",
"on-error": "#fff7f6",
"surface-container-high": "#e4e9e8",
"secondary-fixed-dim": "#c2d6f5",
"tertiary-dim": "#594f63",
"primary-fixed-dim": "#bedbce",
"on-tertiary-fixed-variant": "#675d71",
"outline-variant": "#acb3b2",
"on-tertiary-container": "#5c5367",
"primary-dim": "#3e594e",
"tertiary-container": "#f4e6ff",
"inverse-primary": "#d7f5e7",
"on-primary": "#e5fff2",
"background": "#f8faf9",
"primary-fixed": "#cbe9db"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Plus Jakarta Sans"]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.pb-safe { padding-bottom: env(safe-area-inset-bottom); }
.tonal-layering-top { box-shadow: 0 -1px 0 0 rgba(0,0,0,0.02); }
.no-scrollbar::-webkit-scrollbar { display: none; }
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-surface font-body text-on-surface antialiased">
<!-- TopAppBar -->
<header class="bg-[#f8faf9] dark:bg-slate-950 docked full-width top-0 sticky z-50 flex justify-between items-center w-full px-6 py-4 no-line-rule bg-[#f1f4f3] dark:bg-slate-900 flat no shadows">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full overflow-hidden">
<img class="w-full h-full object-cover" data-alt="softly lit portrait of a calm woman with a neutral background, high-end editorial photography style" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDU25RSnEY9tY1-qYqlM11PIxf7jEh2sXopp1_idFCTCkKrRigPSMDoS39foFoEPDAO6pFMF1HiWceeP7jmtcsEBagEfh2bJfnP4Sbm7ZeKi4IcaEXzTCs_d14hftSuBjjBlIZMNd8AutmAVMpRujwqrFiHBqSUe0cJZgwg54QWAdxD4uoS4mKqrasws6E1dWIo8JgYtD0odEL8pj0qCtELbtrxaL-AiHRg_moyvFXMGyPFRIztJtubtuqblMp1dHIDEt0lq9hXtOeC"/>
</div>
<span class="text-xl font-bold text-[#4a655a] dark:text-[#cbe9db] font-['Manrope'] tracking-tight">Serene Path</span>
</div>
<button class="text-[#4a655a] dark:text-[#cbe9db] hover:opacity-80 transition-opacity duration-300 transform active:scale-95">
<span class="material-symbols-outlined" data-icon="settings">settings</span>
</button>
</header>
<main class="max-w-5xl mx-auto px-6 pt-8 pb-32">
<!-- Hero Section: Daily Goal -->
<section class="mb-12">
<div class="grid grid-cols-1 md:grid-cols-12 gap-8 items-end">
<div class="md:col-span-8">
<h1 class="text-4xl md:text-5xl font-headline font-extrabold tracking-tight text-on-surface mb-2">Library</h1>
<p class="text-on-surface-variant font-label text-sm tracking-wide uppercase">Curate your mind's expansion</p>
</div>
<div class="md:col-span-4">
<div class="bg-surface-container-low p-6 rounded-xl relative overflow-hidden">
<div class="relative z-10">
<div class="flex justify-between items-center mb-4">
<span class="font-label text-xs font-bold text-primary tracking-widest uppercase">Daily Goal</span>
<span class="font-label text-xs font-bold text-on-surface">45 / 60 min</span>
</div>
<div class="w-full h-3 bg-surface-container-highest rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-primary to-primary-fixed-dim rounded-full w-3/4"></div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Continue Reading Widget (Asymmetric Layout) -->
<section class="mb-16">
<div class="bg-primary text-on-primary rounded-xl overflow-hidden flex flex-col md:flex-row shadow-2xl shadow-primary/10">
<div class="md:w-1/3 h-64 md:h-auto overflow-hidden">
<img class="w-full h-full object-cover" data-alt="abstract minimalist book cover with muted sage and cream organic shapes, soft paper texture" src="https://lh3.googleusercontent.com/aida-public/AB6AXuALRJjEAYLwO1uKsH4jwYuenBVv3BvFF52ZW-vJsBjHgn-GS1nGUK1Kznxt6ArYAQumRl0zYWB2gdmaUCGSrT7ziB6662hVhW0K8tw5wJw80T6U-Cz3VecK9XGa0GYO0dCHEFc0XeT5UiqP6MCb90hpBQMX56HWmtYhb966RULfbQ7J7m5lDWYpRPbpE8Zo1kgB5DdVjuPVg_qOOe6gWv2QKTTk89xrzmNRMiKNTgdMTV8ErisxMsABf9h5NQIFO4A1yvOwmkBzb6Vv"/>
</div>
<div class="md:w-2/3 p-8 flex flex-col justify-between">
<div>
<div class="flex items-center gap-2 mb-4">
<span class="material-symbols-outlined text-sm" data-icon="auto_stories">auto_stories</span>
<span class="font-label text-[10px] font-bold uppercase tracking-[0.2em]">Continue Reading</span>
</div>
<h2 class="text-3xl font-headline font-bold mb-2">The Architecture of Stillness</h2>
<p class="text-on-primary/70 text-sm mb-8 leading-relaxed max-w-md">Discover the profound impact of intentional physical environments on mental clarity and spiritual peace.</p>
</div>
<div class="flex flex-col sm:flex-row sm:items-center gap-6">
<button class="bg-on-primary text-primary px-8 py-3 rounded-full font-label text-xs font-bold tracking-wider hover:opacity-90 transition-opacity">
RESUME (65%)
</button>
<div class="flex items-center gap-4">
<div class="w-10 h-10 rounded-full bg-on-primary/10 flex items-center justify-center cursor-pointer hover:bg-on-primary/20 transition-colors">
<span class="material-symbols-outlined text-xl" data-icon="bookmark">bookmark</span>
</div>
<div class="w-10 h-10 rounded-full bg-on-primary/10 flex items-center justify-center cursor-pointer hover:bg-on-primary/20 transition-colors">
<img class="w-5 h-5 opacity-80" data-alt="Apple Books app icon on a white square background" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDKakrxK95LkAvHeedsPl4oou9-M5jn1SgDuYpf0MFlddJiNiVpSro_hVxjhqVxmge6P_jzQBoW_sRikxylyKR9QWfhUF5rNY4If3Z1RD_JMUCA2FhhBl2DCk0YpUSHHrgGYcswi7MXiC1CO8g23zXhASHHBHgdaTn8a8BsdPfPUGW2z-fshnFRGTZX7sh6_gbwpd1ebq3nCu9yKr_dCFwK1sy_kb7ySVtarDpc7hUio4-MLahyM8wRJCrFwKYryXllRv3oEmt_0gG2"/>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- The Shelf (Bento Grid) -->
<section class="mb-16">
<div class="flex justify-between items-baseline mb-8">
<h3 class="text-2xl font-headline font-bold text-on-surface">Your Collection</h3>
<button class="text-primary font-label text-xs font-bold tracking-widest uppercase hover:underline">View All</button>
</div>
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
<!-- Book 1 -->
<div class="group cursor-pointer">
<div class="relative aspect-[3/4] rounded-xl overflow-hidden bg-surface-container-high mb-4 transition-transform duration-500 group-hover:scale-[1.02]">
<img class="w-full h-full object-cover" data-alt="aesthetic hardback book cover with simple typography and natural linen texture" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAkMDYviMy3jBSuSlwLl39b84QGALZJh1oU3qPe1sxyVj0Io3uS1m8z9FObax_LXbABst3EvYPAe-UqjS4y_-Gfs3TWh-bA4rqccIhsAyIwFr68Gl0fn_CRQtHB2jSgKZLvgP3RVqBnXtXbqr1VTS17KGL4fjn9FOxsd0_u0rK0KmzUCgfbnWAshsjOb9bskPvbinnhqeDepBBIcexi_f54gzgDM_xZfJwoc9mKc34Av2MyCVkzGHZ99R68ph2607fFP5ddj5qw1Juf"/>
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex items-end p-4">
<span class="text-white text-xs font-label font-bold uppercase tracking-widest">Open PDF</span>
</div>
</div>
<h4 class="font-headline font-bold text-sm mb-1 line-clamp-1">Mindful Breathing</h4>
<p class="text-[10px] font-label font-semibold text-on-surface-variant uppercase tracking-wider mb-3">Dr. Aris Thorne</p>
<div class="flex items-center gap-3">
<div class="flex-1 h-1 bg-surface-container-highest rounded-full overflow-hidden">
<div class="h-full bg-primary rounded-full w-[82%]"></div>
</div>
<span class="text-[9px] font-label font-bold text-primary">82%</span>
</div>
</div>
<!-- Book 2 -->
<div class="group cursor-pointer">
<div class="relative aspect-[3/4] rounded-xl overflow-hidden bg-surface-container-high mb-4 transition-transform duration-500 group-hover:scale-[1.02]">
<img class="w-full h-full object-cover" data-alt="red book cover with gold foil accents, scholarly and elegant visual style" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCgNxP_SYl7o4L0gL6iaFDlW4K1_qwrnX-gsKyOzi1rAebIY7UUUjnt-NyZ94LlcGHAxFBLM0ZP_ru6guRzopt-TulwwqcbWdXJTqgJSJcYrQzy1v3N2IZX8eaoDV8V9c1pUXgTyHAttpoLYObLXh--NvvpHd4f11nJWUyWV616isPNDLj0YWhLLiKPLvdgEcpFuBcImRVz-31e7aLqjYhlkkx3xUYRv0DRmG2XFPNaoccrehn_ICWZ_ct-p16INBzr_I7Z9_dAh1rk"/>
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex items-end p-4">
<span class="text-white text-xs font-label font-bold uppercase tracking-widest">Open PDF</span>
</div>
</div>
<h4 class="font-headline font-bold text-sm mb-1 line-clamp-1">Quietude</h4>
<p class="text-[10px] font-label font-semibold text-on-surface-variant uppercase tracking-wider mb-3">Elena Vance</p>
<div class="flex items-center gap-3">
<div class="flex-1 h-1 bg-surface-container-highest rounded-full overflow-hidden">
<div class="h-full bg-primary rounded-full w-[24%]"></div>
</div>
<span class="text-[9px] font-label font-bold text-primary">24%</span>
</div>
</div>
<!-- Book 3 -->
<div class="group cursor-pointer">
<div class="relative aspect-[3/4] rounded-xl overflow-hidden bg-surface-container-high mb-4 transition-transform duration-500 group-hover:scale-[1.02]">
<img class="w-full h-full object-cover" data-alt="illustrated book cover showing stars and mountains in a minimalist woodcut style" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAk1adUlUMova9E31Mv0wNIMuNic-DgJ19IfldCDNG7Iek6SYtga6uiIl3HWZg2UYVu0Jf9tFp6G22Ie7oCoCX5mCdPilvqySx80OHHlWr0luTN7H8ybLeNcAZykr_leSwnFGWiNBZiS9XdI-QOSq2lws7oSjTzzDMtj6aJe2O9mvYUMHhYEDIITQJmMxjYmG5Lr-j9xu0DTtbnB2FpcggcU-PNwwje-FYsfvwVKj1YtJG4IgB3-Ex-8zD48Z31S_VK47VgLiWBwe3s"/>
<div class="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent opacity-0 group-hover:opacity-100 transition-opacity flex items-end p-4">
<span class="text-white text-xs font-label font-bold uppercase tracking-widest">Open PDF</span>
</div>
</div>
<h4 class="font-headline font-bold text-sm mb-1 line-clamp-1">Nature's Rythm</h4>
<p class="text-[10px] font-label font-semibold text-on-surface-variant uppercase tracking-wider mb-3">Marcus Sol</p>
<div class="flex items-center gap-3">
<div class="flex-1 h-1 bg-surface-container-highest rounded-full overflow-hidden">
<div class="h-full bg-primary rounded-full w-[45%]"></div>
</div>
<span class="text-[9px] font-label font-bold text-primary">45%</span>
</div>
</div>
<!-- Add New -->
<div class="group cursor-pointer">
<div class="relative aspect-[3/4] rounded-xl border-2 border-dashed border-outline-variant/30 flex flex-col items-center justify-center gap-3 hover:bg-surface-container-low transition-colors duration-300">
<span class="material-symbols-outlined text-4xl text-outline-variant" data-icon="add_circle">add_circle</span>
<span class="font-label text-[10px] font-bold text-on-surface-variant uppercase tracking-widest">Upload PDF</span>
</div>
</div>
</div>
</section>
<!-- Secondary Collection (Asymmetric List) -->
<section class="mb-16">
<h3 class="text-2xl font-headline font-bold text-on-surface mb-8">Recent Articles</h3>
<div class="space-y-4">
<div class="bg-surface-container-lowest p-5 rounded-xl flex items-center gap-6 hover:shadow-sm transition-shadow cursor-pointer">
<div class="w-16 h-16 rounded-lg bg-surface-container-high flex-shrink-0 flex items-center justify-center">
<span class="material-symbols-outlined text-primary text-3xl" data-icon="description">description</span>
</div>
<div class="flex-1">
<h5 class="font-headline font-bold text-base mb-1">Journal of Cognitive Rest</h5>
<p class="text-xs text-on-surface-variant line-clamp-1">Exploring the synaptic benefits of 15-minute digital detoxes.</p>
</div>
<div class="text-right hidden sm:block">
<p class="text-[10px] font-label font-bold uppercase tracking-widest text-primary mb-1">Completed</p>
<p class="text-[10px] font-label text-on-surface-variant">2 days ago</p>
</div>
</div>
<div class="bg-surface-container-lowest p-5 rounded-xl flex items-center gap-6 hover:shadow-sm transition-shadow cursor-pointer">
<div class="w-16 h-16 rounded-lg bg-surface-container-high flex-shrink-0 flex items-center justify-center">
<span class="material-symbols-outlined text-secondary text-3xl" data-icon="article">article</span>
</div>
<div class="flex-1">
<h5 class="font-headline font-bold text-base mb-1">The Art of Soft Focus</h5>
<p class="text-xs text-on-surface-variant line-clamp-1">Balancing deep work with peripheral awareness for creativity.</p>
</div>
<div class="text-right hidden sm:block">
<p class="text-[10px] font-label font-bold uppercase tracking-widest text-secondary mb-1">12 min left</p>
<p class="text-[10px] font-label text-on-surface-variant">Today</p>
</div>
</div>
</div>
</section>
</main>
<!-- BottomNavBar -->
<nav class="bg-[#f8faf9]/80 dark:bg-slate-950/80 backdrop-blur-xl fixed bottom-0 w-full z-50 pb-safe no-line-rule tonal-layering-top shadow-[0_-10px_40px_rgba(45,52,51,0.05)] dark:shadow-none flex justify-around items-center w-full px-4 py-3">
<div class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db] cursor-pointer transform hover:scale-90 active:scale-90">
<span class="material-symbols-outlined mb-1" data-icon="dashboard">dashboard</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider">Focus</span>
</div>
<div class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db] cursor-pointer transform hover:scale-90 active:scale-90">
<span class="material-symbols-outlined mb-1" data-icon="calendar_today">calendar_today</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider">Plan</span>
</div>
<div class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db] cursor-pointer transform hover:scale-90 active:scale-90">
<span class="material-symbols-outlined mb-1" data-icon="smart_toy">smart_toy</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider">AI</span>
</div>
<div class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db] cursor-pointer transform hover:scale-90 active:scale-90">
<span class="material-symbols-outlined mb-1" data-icon="auto_graph">auto_graph</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider">Habits</span>
</div>
<div class="flex flex-col items-center justify-center bg-[#cbe9db] dark:bg-[#4a655a]/30 text-[#4a655a] dark:text-[#cbe9db] rounded-[1.5rem] px-5 py-2.5 transition-all duration-500 transform scale-90">
<span class="material-symbols-outlined mb-1" data-icon="menu_book">menu_book</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider">Library</span>
</div>
</nav>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -0,0 +1,259 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Serene Path | Focus Sprint</title>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;600;700;800&amp;family=Plus+Jakarta+Sans:wght@500;600;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"inverse-surface": "#0b0f0f",
"primary-container": "#cbe9db",
"on-secondary-container": "#40536d",
"on-primary-fixed": "#2a443b",
"inverse-on-surface": "#9b9d9d",
"surface-container": "#eaefee",
"error-container": "#fa746f",
"surface-container-lowest": "#ffffff",
"surface-variant": "#dde4e3",
"secondary-fixed": "#d3e3ff",
"tertiary-fixed-dim": "#e5d8f0",
"secondary-container": "#d3e3ff",
"on-primary-fixed-variant": "#466156",
"surface-bright": "#f8faf9",
"on-tertiary": "#fef6ff",
"on-secondary-fixed-variant": "#4a5d77",
"surface-container-highest": "#dde4e3",
"on-surface": "#2d3433",
"surface-tint": "#4a655a",
"tertiary": "#655b6f",
"error": "#a83836",
"tertiary-fixed": "#f4e6ff",
"surface-container-low": "#f1f4f3",
"on-background": "#2d3433",
"on-secondary": "#f8f8ff",
"secondary": "#4e607b",
"error-dim": "#67040d",
"surface": "#f8faf9",
"on-primary-container": "#3d574d",
"surface-dim": "#d4dbda",
"on-secondary-fixed": "#2e405a",
"on-error-container": "#6e0a12",
"outline": "#757c7b",
"on-tertiary-fixed": "#4a4154",
"secondary-dim": "#42546f",
"on-surface-variant": "#596060",
"primary": "#4a655a",
"on-error": "#fff7f6",
"surface-container-high": "#e4e9e8",
"secondary-fixed-dim": "#c2d6f5",
"tertiary-dim": "#594f63",
"primary-fixed-dim": "#bedbce",
"on-tertiary-fixed-variant": "#675d71",
"outline-variant": "#acb3b2",
"on-tertiary-container": "#5c5367",
"primary-dim": "#3e594e",
"tertiary-container": "#f4e6ff",
"inverse-primary": "#d7f5e7",
"on-primary": "#e5fff2",
"background": "#f8faf9",
"primary-fixed": "#cbe9db"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Plus Jakarta Sans"]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.zen-gradient {
background: linear-gradient(135deg, #f8faf9 0%, #eaefee 100%);
}
.timer-glow {
box-shadow: 0 0 50px rgba(74, 101, 90, 0.05);
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-background text-on-surface font-body selection:bg-primary-container">
<!-- TopAppBar -->
<header class="bg-[#f8faf9] dark:bg-slate-950 text-[#4a655a] dark:text-[#cbe9db] docked full-width top-0 sticky z-50 no-line-rule bg-[#f1f4f3] dark:bg-slate-900 flat no shadows flex justify-between items-center w-full px-6 py-4">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-full overflow-hidden bg-surface-container-high">
<img alt="user profile picture" class="w-full h-full object-cover" data-alt="minimalist professional avatar of a calm individual with clean aesthetic and soft lighting" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDFm9flLrERRx-ZlB3atcJy4lX2S5YFwrUTAsRSQQGILoGrAyH48H1wbTv14TNdVe1I0WwnthXrxvEmtpyyhNNdgSZW8ad-toXlA1aGMGo0Jx7StW7NpDTlTu7rpbEKeY1zH0H8CkA0oQVkr-1QoxevD6Entr_QcRCyCRkhiVlQ6vm_ilqIdWrLZ3ULV8JYpkJy6Il8VPwOx-BS5hAiQl69pr-oao_rlDuLfkgLRL1JnzpHqM-iZJbhrhuC1pghVknyCpLsuElLiQGR"/>
</div>
<h1 class="font-['Manrope'] font-bold tracking-tight text-[#2d3433] dark:text-slate-100 text-xl font-bold text-[#4a655a] dark:text-[#cbe9db]">Serene Path</h1>
</div>
<button class="hover:opacity-80 transition-opacity duration-300 active:scale-95 duration-300 ease-in-out">
<span class="material-symbols-outlined text-2xl">settings</span>
</button>
</header>
<main class="max-w-7xl mx-auto px-6 py-8 pb-32">
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8 items-start">
<!-- Left Column: Timer & Zen Mode -->
<section class="lg:col-span-7 flex flex-col gap-8">
<div class="bg-surface-container-lowest rounded-[2rem] p-12 flex flex-col items-center justify-center relative overflow-hidden timer-glow">
<!-- Subtle Background Decoration -->
<div class="absolute top-0 left-0 w-full h-full opacity-5 pointer-events-none">
<svg height="100%" preserveaspectratio="none" viewbox="0 0 100 100" width="100%">
<circle class="text-primary" cx="50" cy="50" fill="none" r="40" stroke="currentColor" stroke-width="0.5"></circle>
</svg>
</div>
<div class="z-10 text-center">
<p class="font-label text-label-md text-primary uppercase tracking-[0.2em] mb-4">Current Sprint: Deep Work</p>
<h2 class="font-headline text-[8rem] md:text-[10rem] font-extrabold leading-none tracking-tighter text-on-surface">
24:59
</h2>
<div class="flex items-center justify-center gap-8 mt-12">
<button class="w-14 h-14 rounded-full flex items-center justify-center bg-surface-container-high text-primary hover:bg-primary-container transition-all">
<span class="material-symbols-outlined text-3xl">skip_previous</span>
</button>
<button class="w-24 h-24 rounded-full flex items-center justify-center bg-primary text-on-primary shadow-xl hover:scale-105 transition-transform">
<span class="material-symbols-outlined text-5xl" style="font-variation-settings: 'FILL' 1;">pause</span>
</button>
<button class="w-14 h-14 rounded-full flex items-center justify-center bg-surface-container-high text-primary hover:bg-primary-container transition-all">
<span class="material-symbols-outlined text-3xl">skip_next</span>
</button>
</div>
</div>
</div>
<!-- Session Progress -->
<div class="bg-surface-container-low rounded-xl p-6">
<div class="flex justify-between items-center mb-4">
<span class="font-label text-label-sm text-secondary font-bold uppercase tracking-wider">Daily Goal: 4 Sprints</span>
<span class="font-label text-label-sm text-on-surface">2 / 4 Completed</span>
</div>
<div class="h-3 w-full bg-surface-container-highest rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-primary to-primary-fixed-dim w-1/2 rounded-full"></div>
</div>
</div>
</section>
<!-- Right Column: Planning & Tasks -->
<section class="lg:col-span-5 flex flex-col gap-6">
<!-- Micro-break Settings -->
<div class="bg-surface-container-low rounded-xl p-6">
<div class="flex items-center gap-3 mb-6">
<span class="material-symbols-outlined text-primary">eco</span>
<h3 class="font-headline text-lg font-bold">Focus Strategy</h3>
</div>
<div class="flex gap-4">
<div class="flex-1 bg-surface-container-lowest p-4 rounded-xl">
<p class="font-label text-[10px] text-outline uppercase tracking-wider mb-1">Work</p>
<p class="font-headline text-xl font-bold text-on-surface">25 <span class="text-sm font-normal text-outline">min</span></p>
</div>
<div class="flex-1 bg-surface-container-lowest p-4 rounded-xl">
<p class="font-label text-[10px] text-outline uppercase tracking-wider mb-1">Break</p>
<p class="font-headline text-xl font-bold text-on-surface">5 <span class="text-sm font-normal text-outline">min</span></p>
</div>
<button class="w-14 bg-primary-container text-on-primary-container rounded-xl flex items-center justify-center hover:opacity-80 transition-opacity">
<span class="material-symbols-outlined">tune</span>
</button>
</div>
</div>
<!-- Sprint Tasks -->
<div class="bg-surface-container-lowest rounded-xl p-8 shadow-sm">
<div class="flex justify-between items-center mb-8">
<h3 class="font-headline text-xl font-bold">Sprint Tasks</h3>
<button class="text-primary font-label text-label-md font-bold flex items-center gap-1 hover:underline">
<span class="material-symbols-outlined text-sm">add</span>
Batch New Task
</button>
</div>
<div class="space-y-4">
<!-- Task Item: Active -->
<div class="flex items-start gap-4 p-4 rounded-xl bg-primary-container/30 border-l-4 border-primary">
<button class="mt-1 w-6 h-6 rounded-full border-2 border-primary flex items-center justify-center">
<div class="w-3 h-3 bg-primary rounded-full animate-pulse"></div>
</button>
<div class="flex-1">
<p class="font-body text-body-lg font-bold text-on-primary-container">Refine Brand Identity Guidelines</p>
<p class="font-label text-label-sm text-on-primary-fixed-variant mt-1">Priority focus for this session</p>
</div>
<span class="font-label text-label-sm text-primary font-bold">Current</span>
</div>
<!-- Task Item: Queued -->
<div class="flex items-start gap-4 p-4 rounded-xl hover:bg-surface-container transition-colors group">
<button class="mt-1 w-6 h-6 rounded-full border-2 border-outline-variant group-hover:border-primary transition-colors"></button>
<div class="flex-1">
<p class="font-body text-body-lg text-on-surface">Review Q3 Performance Metrics</p>
<div class="flex gap-2 mt-2">
<span class="px-2 py-0.5 rounded-full bg-secondary-container text-on-secondary-container font-label text-[10px] uppercase">Analytics</span>
</div>
</div>
</div>
<!-- Task Item: Queued -->
<div class="flex items-start gap-4 p-4 rounded-xl hover:bg-surface-container transition-colors group">
<button class="mt-1 w-6 h-6 rounded-full border-2 border-outline-variant group-hover:border-primary transition-colors"></button>
<div class="flex-1">
<p class="font-body text-body-lg text-on-surface">Client Proposal Draft</p>
<div class="flex gap-2 mt-2">
<span class="px-2 py-0.5 rounded-full bg-tertiary-container text-on-tertiary-container font-label text-[10px] uppercase">Sales</span>
</div>
</div>
</div>
</div>
</div>
<!-- Deep Work Insight Card -->
<div class="bg-tertiary text-on-tertiary rounded-xl p-6 relative overflow-hidden">
<div class="absolute right-[-20px] bottom-[-20px] opacity-10">
<span class="material-symbols-outlined text-[120px]" style="font-variation-settings: 'FILL' 1;">lightbulb</span>
</div>
<p class="font-label text-label-sm uppercase tracking-widest opacity-80 mb-2">Deep Work Insight</p>
<p class="font-headline text-lg font-bold leading-relaxed z-10 relative">
"Your focus is the most valuable currency you possess. Don't spend it on trivial distractions."
</p>
<div class="mt-4 flex items-center gap-2 opacity-80">
<span class="material-symbols-outlined text-sm">auto_stories</span>
<span class="font-label text-label-sm italic">Strategy: Task Batching</span>
</div>
</div>
</section>
</div>
</main>
<!-- BottomNavBar -->
<nav class="bg-[#f8faf9]/80 dark:bg-slate-950/80 backdrop-blur-xl fixed bottom-0 w-full z-50 pb-safe shadow-[0_-10px_40px_rgba(45,52,51,0.05)] dark:shadow-none flex justify-around items-center w-full px-4 py-3">
<div class="flex flex-col items-center justify-center bg-[#cbe9db] dark:bg-[#4a655a]/30 text-[#4a655a] dark:text-[#cbe9db] rounded-[1.5rem] px-5 py-2.5 transition-all duration-500 active:scale-90 duration-500 ease-in-out">
<span class="material-symbols-outlined" data-icon="dashboard" style="font-variation-settings: 'FILL' 1;">dashboard</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider mt-1">Focus</span>
</div>
<div class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db] active:scale-90 duration-500 ease-in-out">
<span class="material-symbols-outlined" data-icon="calendar_today">calendar_today</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider mt-1">Plan</span>
</div>
<div class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db] active:scale-90 duration-500 ease-in-out">
<span class="material-symbols-outlined" data-icon="smart_toy">smart_toy</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider mt-1">AI</span>
</div>
<div class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db] active:scale-90 duration-500 ease-in-out">
<span class="material-symbols-outlined" data-icon="auto_graph">auto_graph</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider mt-1">Habits</span>
</div>
<div class="flex flex-col items-center justify-center text-[#acb3b2] dark:text-slate-500 px-5 py-2.5 transition-all duration-500 hover:text-[#4a655a] dark:hover:text-[#cbe9db] active:scale-90 duration-500 ease-in-out">
<span class="material-symbols-outlined" data-icon="menu_book">menu_book</span>
<span class="font-['Plus_Jakarta_Sans'] text-[10px] font-semibold uppercase tracking-wider mt-1">Library</span>
</div>
</nav>
</body></html>

BIN
assets/ui/mobile/sprint.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

316
assets/ui/web/ai.html Normal file
View File

@ -0,0 +1,316 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Serene Path AI Assistant</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&amp;family=Plus+Jakarta+Sans:wght@400;500;600;700&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary-fixed": "#f4e6ff",
"on-primary-container": "#3d574d",
"on-error-container": "#6e0a12",
"on-secondary-fixed": "#2e405a",
"on-tertiary-fixed-variant": "#675d71",
"on-surface": "#2d3433",
"surface-tint": "#4a655a",
"surface": "#f8faf9",
"on-secondary": "#f8f8ff",
"secondary-fixed-dim": "#c2d6f5",
"surface-variant": "#dde4e3",
"primary-fixed-dim": "#bedbce",
"on-tertiary-fixed": "#4a4154",
"primary-dim": "#3e594e",
"primary": "#4a655a",
"on-primary": "#e5fff2",
"inverse-surface": "#0b0f0f",
"on-error": "#fff7f6",
"background": "#f8faf9",
"error": "#a83836",
"outline-variant": "#acb3b2",
"secondary-container": "#d3e3ff",
"on-tertiary": "#fef6ff",
"primary-container": "#cbe9db",
"tertiary-dim": "#594f63",
"tertiary-container": "#f4e6ff",
"surface-container-high": "#e4e9e8",
"tertiary-fixed-dim": "#e5d8f0",
"error-container": "#fa746f",
"on-primary-fixed": "#2a443b",
"surface-container": "#eaefee",
"primary-fixed": "#cbe9db",
"on-primary-fixed-variant": "#466156",
"error-dim": "#67040d",
"on-secondary-container": "#40536d",
"surface-container-highest": "#dde4e3",
"tertiary": "#655b6f",
"on-tertiary-container": "#5c5367",
"surface-bright": "#f8faf9",
"on-secondary-fixed-variant": "#4a5d77",
"inverse-primary": "#d7f5e7",
"surface-container-low": "#f1f4f3",
"outline": "#757c7b",
"secondary-fixed": "#d3e3ff",
"on-background": "#2d3433",
"secondary-dim": "#42546f",
"inverse-on-surface": "#9b9d9d",
"surface-dim": "#d4dbda",
"surface-container-lowest": "#ffffff",
"on-surface-variant": "#596060",
"secondary": "#4e607b"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Plus Jakarta Sans"]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
body {
font-family: 'Manrope', sans-serif;
background-color: #f8faf9;
color: #2d3433;
}
</style>
</head>
<body class="overflow-hidden">
<!-- SIDE NAV BAR -->
<aside class="h-screen w-64 fixed left-0 top-0 bg-stone-50 dark:bg-stone-950 flex flex-col h-full py-8 px-6 font-manrope text-stone-800 dark:text-stone-200">
<div class="mb-10 flex flex-col gap-1">
<h1 class="text-xl font-bold tracking-tight text-emerald-900 dark:text-emerald-200">Serene Path</h1>
<p class="text-xs text-stone-500 font-label">The Curated Breath</p>
</div>
<button class="mb-8 w-full py-3 px-4 rounded-full bg-primary text-on-primary font-bold flex items-center justify-center gap-2 hover:opacity-90 transition-all">
<span class="material-symbols-outlined" data-icon="add">add</span>
New Entry
</button>
<nav class="flex-1 flex flex-col gap-2">
<a class="flex items-center gap-4 py-3 px-4 rounded-xl text-stone-500 dark:text-stone-400 hover:text-emerald-700 dark:hover:text-emerald-300 hover:bg-stone-200/50 dark:hover:bg-stone-800/50 transition-colors duration-300" href="#">
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
Dashboard
</a>
<a class="flex items-center gap-4 py-3 px-4 rounded-xl text-stone-500 dark:text-stone-400 hover:text-emerald-700 dark:hover:text-emerald-300 hover:bg-stone-200/50 dark:hover:bg-stone-800/50 transition-colors duration-300" href="#">
<span class="material-symbols-outlined" data-icon="calendar_today">calendar_today</span>
Calendar
</a>
<a class="flex items-center gap-4 py-3 px-4 rounded-xl text-emerald-800 dark:text-emerald-400 font-bold border-r-4 border-emerald-700 bg-stone-100 dark:bg-stone-900" href="#">
<span class="material-symbols-outlined" data-icon="smart_toy">smart_toy</span>
AI Assistant
</a>
<a class="flex items-center gap-4 py-3 px-4 rounded-xl text-stone-500 dark:text-stone-400 hover:text-emerald-700 dark:hover:text-emerald-300 hover:bg-stone-200/50 dark:hover:bg-stone-800/50 transition-colors duration-300" href="#">
<span class="material-symbols-outlined" data-icon="auto_awesome_motion">auto_awesome_motion</span>
Habits
</a>
<a class="flex items-center gap-4 py-3 px-4 rounded-xl text-stone-500 dark:text-stone-400 hover:text-emerald-700 dark:hover:text-emerald-300 hover:bg-stone-200/50 dark:hover:bg-stone-800/50 transition-colors duration-300" href="#">
<span class="material-symbols-outlined" data-icon="library_books">library_books</span>
Library
</a>
</nav>
<div class="mt-auto flex flex-col gap-2 pt-6 border-t border-stone-200/50 dark:border-stone-800/50">
<a class="flex items-center gap-4 py-3 px-4 rounded-xl text-stone-500 dark:text-stone-400 hover:text-emerald-700 dark:hover:text-emerald-300 transition-colors" href="#">
<span class="material-symbols-outlined" data-icon="settings">settings</span>
Settings
</a>
<a class="flex items-center gap-4 py-3 px-4 rounded-xl text-stone-500 dark:text-stone-400 hover:text-emerald-700 dark:hover:text-emerald-300 transition-colors" href="#">
<span class="material-symbols-outlined" data-icon="help">help</span>
Support
</a>
<div class="mt-4 flex items-center gap-3 px-4">
<img alt="User profile" class="w-8 h-8 rounded-full object-cover" data-alt="Close up portrait of a calm woman with natural look in soft daylight, minimalist and peaceful aesthetic" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAR0tdnMdZsJEqW3xObOpP8RaXycu3OrWKpSl-3j6PJkhJA4U6BxpyAi0iOSkRCwhaRudEmhQ58jmdOAKu3oz9WnlxTvBNqQWv1Y-FCS9WqJsgd-xuB5OdS9ZOpUJfCyecu_tQW6q4eack9Nqs6u8rz4CC4-UtpEheRp7zBVkdu6FcyQALU1XqYlAFapOho5xLUQLtXKeKJq1ZAtx16ga1L1vjIP8VhyE5ZvB_QJKhfPJpGNqGc8dw4Ifl7UlI51nFDZEq39HFUE9z6"/>
<span class="text-sm font-medium">Elena Thorne</span>
</div>
</div>
</aside>
<!-- MAIN CONTENT AREA -->
<main class="ml-64 flex h-screen bg-surface">
<!-- CENTER CHAT INTERFACE -->
<div class="flex-1 flex flex-col h-full bg-surface-container-low">
<!-- Top App Bar -->
<header class="flex items-center justify-between px-12 py-4 bg-transparent sticky top-0 z-10">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-primary-container flex items-center justify-center text-primary">
<span class="material-symbols-outlined" data-icon="smart_toy">smart_toy</span>
</div>
<div>
<h2 class="text-lg font-bold text-on-surface">AI Assistant</h2>
<span class="text-xs font-label text-on-surface-variant flex items-center gap-1">
<span class="w-2 h-2 rounded-full bg-emerald-500"></span> Online
</span>
</div>
</div>
<div class="flex items-center gap-4">
<button class="p-2 text-on-surface-variant hover:text-primary transition-all">
<span class="material-symbols-outlined" data-icon="notifications">notifications</span>
</button>
<button class="p-2 text-on-surface-variant hover:text-primary transition-all">
<span class="material-symbols-outlined" data-icon="account_circle">account_circle</span>
</button>
</div>
</header>
<!-- Message Area -->
<div class="flex-1 overflow-y-auto px-12 py-8 space-y-8">
<!-- AI Message -->
<div class="flex items-start gap-4 max-w-2xl">
<div class="w-8 h-8 rounded-lg bg-primary-container flex items-center justify-center text-primary shrink-0 mt-1">
<span class="material-symbols-outlined text-sm" data-icon="smart_toy">smart_toy</span>
</div>
<div class="space-y-2">
<div class="p-5 bg-surface-container-lowest rounded-2xl rounded-tl-none shadow-sm">
<p class="text-body-lg text-on-surface leading-relaxed">
Good morning, Elena. I've analyzed your sleep patterns and today's calendar. You have a few open windows this afternoon. Shall we plan a focused reflection session or a nature walk to keep your serenity score high?
</p>
</div>
<span class="text-[10px] font-label text-outline uppercase tracking-wider ml-1">Assistant • 09:12 AM</span>
</div>
</div>
<!-- User Message -->
<div class="flex items-start gap-4 max-w-2xl ml-auto flex-row-reverse">
<div class="w-8 h-8 rounded-lg overflow-hidden shrink-0 mt-1">
<img alt="Elena" class="w-full h-full object-cover" data-alt="Close up portrait of a calm woman with natural look in soft daylight, minimalist and peaceful aesthetic" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAil22lpK92TzEl-VXOypx4-IEON39DZ5X3naTyQHbq3UtnMCokMmvk4hOw73y9rDdho0BG6JaSCm9XB_2zWaEafPrKnNmDz9GBGpiTexrkJWAGzebVlRdP6Rh1tl9NO2_ROHAdME8FVdfQ8DuKsQ0GVbick2-wZO1nIt0Y2sKlbdliOd_jBtHcHZF7WV7go-d6uqsWm78Xs9KjAnGGTn_Lt1LuHCI3gkqBnOj4JpIvVNWVOzLv1T4vV3saSA6gdyBNJpK_xf3CRiuZ"/>
</div>
<div class="space-y-2 flex flex-col items-end">
<div class="p-5 bg-primary text-on-primary rounded-2xl rounded-tr-none shadow-md">
<p class="text-body-lg leading-relaxed">
I'd like the nature walk around 3:00 PM. Also, please remind me to finish my meditation log before dinner at 7:00 PM.
</p>
</div>
<span class="text-[10px] font-label text-outline uppercase tracking-wider mr-1">You • 09:14 AM</span>
</div>
</div>
<!-- AI Message with Dynamic Action -->
<div class="flex items-start gap-4 max-w-2xl">
<div class="w-8 h-8 rounded-lg bg-primary-container flex items-center justify-center text-primary shrink-0 mt-1">
<span class="material-symbols-outlined text-sm" data-icon="smart_toy">smart_toy</span>
</div>
<div class="space-y-4">
<div class="p-5 bg-surface-container-lowest rounded-2xl rounded-tl-none shadow-sm border border-primary/5">
<p class="text-body-lg text-on-surface leading-relaxed">
Perfect choice. I've updated your schedule for today.
</p>
<div class="mt-4 p-4 bg-surface-container-high rounded-xl flex items-center gap-4">
<span class="material-symbols-outlined text-primary" data-icon="check_circle">check_circle</span>
<div>
<p class="text-sm font-bold">Action Recorded</p>
<p class="text-xs text-on-surface-variant">Nature Walk scheduled for 15:00</p>
</div>
</div>
</div>
<span class="text-[10px] font-label text-outline uppercase tracking-wider ml-1">Assistant • 09:15 AM</span>
</div>
</div>
</div>
<!-- Input Bar Area -->
<div class="px-12 py-8 bg-gradient-to-t from-surface-container-low to-transparent">
<div class="max-w-4xl mx-auto flex items-center gap-4 bg-surface-container-lowest p-3 rounded-full shadow-lg shadow-on-surface/5 border border-outline-variant/10">
<button class="p-2 text-outline hover:text-primary transition-colors">
<span class="material-symbols-outlined" data-icon="add_circle">add_circle</span>
</button>
<input class="flex-1 bg-transparent border-none focus:ring-0 text-on-surface placeholder:text-outline/60 font-body" placeholder="Type a message or ask for advice..." type="text"/>
<button class="w-10 h-10 rounded-full bg-primary flex items-center justify-center text-on-primary hover:opacity-90 transition-all">
<span class="material-symbols-outlined text-sm" data-icon="send" data-weight="fill">send</span>
</button>
</div>
<div class="mt-4 flex justify-center gap-6">
<button class="text-[10px] font-label font-bold text-primary flex items-center gap-1 uppercase tracking-widest hover:opacity-70">
<span class="material-symbols-outlined text-xs" data-icon="psychology">psychology</span>
Summarize Day
</button>
<button class="text-[10px] font-label font-bold text-primary flex items-center gap-1 uppercase tracking-widest hover:opacity-70">
<span class="material-symbols-outlined text-xs" data-icon="eco">eco</span>
Mindfulness Tips
</button>
</div>
</div>
</div>
<!-- RIGHT SIDEBAR: PLAN SUMMARY -->
<aside class="w-80 h-full bg-surface-container border-l border-outline-variant/10 p-8 flex flex-col gap-8 overflow-y-auto">
<div>
<h3 class="text-headline-sm font-bold text-on-surface mb-6">Plan Summary</h3>
<div class="space-y-6">
<!-- Date Header -->
<div class="flex items-center justify-between">
<div class="flex flex-col">
<span class="text-xs font-label text-outline uppercase tracking-widest">Today</span>
<span class="text-lg font-bold text-on-surface">Friday, May 24</span>
</div>
<div class="w-12 h-12 rounded-full border-2 border-primary-container flex items-center justify-center">
<span class="text-xs font-bold text-primary">65%</span>
</div>
</div>
<!-- Tasks Timeline -->
<div class="relative space-y-4 pt-4">
<div class="absolute left-4 top-0 bottom-0 w-px bg-outline-variant/30"></div>
<!-- Task Item -->
<div class="relative pl-10">
<div class="absolute left-3 top-2 w-2 h-2 rounded-full bg-primary ring-4 ring-surface-container"></div>
<div class="p-4 bg-surface-container-lowest rounded-xl shadow-sm">
<span class="text-[10px] font-label text-primary font-bold">10:00 AM</span>
<p class="text-sm font-bold text-on-surface mt-1">Project Sync</p>
<p class="text-xs text-on-surface-variant">Focus on core deliverables</p>
</div>
</div>
<!-- Scheduled Task Item -->
<div class="relative pl-10">
<div class="absolute left-3 top-2 w-2 h-2 rounded-full bg-secondary-dim ring-4 ring-surface-container"></div>
<div class="p-4 bg-secondary-container/30 rounded-xl border border-secondary-container/50">
<span class="text-[10px] font-label text-secondary font-bold">03:00 PM</span>
<p class="text-sm font-bold text-on-secondary-container mt-1">Nature Walk</p>
<p class="text-xs text-on-secondary-container/70">Reflect on weekly progress</p>
</div>
</div>
<!-- Reminder Item -->
<div class="relative pl-10">
<div class="absolute left-3 top-2 w-2 h-2 rounded-full bg-tertiary-dim ring-4 ring-surface-container"></div>
<div class="p-4 bg-tertiary-container/30 rounded-xl">
<span class="text-[10px] font-label text-tertiary font-bold">07:00 PM</span>
<p class="text-sm font-bold text-on-tertiary-container mt-1">Meditation Log</p>
<p class="text-xs text-on-tertiary-container/70">Complete reflection journal</p>
</div>
</div>
</div>
</div>
</div>
<!-- Health/Wellness Widget -->
<div class="mt-auto">
<div class="p-6 bg-gradient-to-br from-primary to-primary-dim rounded-2xl text-on-primary">
<div class="flex items-center justify-between mb-4">
<span class="material-symbols-outlined" data-icon="self_improvement">self_improvement</span>
<span class="text-[10px] font-label font-bold uppercase tracking-widest opacity-80">Serenity Index</span>
</div>
<div class="flex items-end gap-2">
<span class="text-3xl font-extrabold leading-none">8.4</span>
<span class="text-xs font-label opacity-70 mb-1">/ 10</span>
</div>
<p class="text-xs mt-4 opacity-90 leading-relaxed font-body">
"Each breath is a new beginning." You're doing great today, Elena.
</p>
</div>
<div class="mt-6 flex items-center justify-between text-on-surface-variant px-2">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-sm" data-icon="schedule">schedule</span>
<span class="text-xs font-label">Sync 2m ago</span>
</div>
<button class="text-xs font-label font-bold text-primary hover:underline">Edit Plan</button>
</div>
</div>
</aside>
</main>
</body></html>

364
assets/ui/web/calendar.html Normal file
View File

@ -0,0 +1,364 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@200..800&amp;family=Plus+Jakarta+Sans:wght@200..800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary-fixed": "#f4e6ff",
"on-primary-container": "#3d574d",
"on-error-container": "#6e0a12",
"on-secondary-fixed": "#2e405a",
"on-tertiary-fixed-variant": "#675d71",
"on-surface": "#2d3433",
"surface-tint": "#4a655a",
"surface": "#f8faf9",
"on-secondary": "#f8f8ff",
"secondary-fixed-dim": "#c2d6f5",
"surface-variant": "#dde4e3",
"primary-fixed-dim": "#bedbce",
"on-tertiary-fixed": "#4a4154",
"primary-dim": "#3e594e",
"primary": "#4a655a",
"on-primary": "#e5fff2",
"inverse-surface": "#0b0f0f",
"on-error": "#fff7f6",
"background": "#f8faf9",
"error": "#a83836",
"outline-variant": "#acb3b2",
"secondary-container": "#d3e3ff",
"on-tertiary": "#fef6ff",
"primary-container": "#cbe9db",
"tertiary-dim": "#594f63",
"tertiary-container": "#f4e6ff",
"surface-container-high": "#e4e9e8",
"tertiary-fixed-dim": "#e5d8f0",
"error-container": "#fa746f",
"on-primary-fixed": "#2a443b",
"surface-container": "#eaefee",
"primary-fixed": "#cbe9db",
"on-primary-fixed-variant": "#466156",
"error-dim": "#67040d",
"on-secondary-container": "#40536d",
"surface-container-highest": "#dde4e3",
"tertiary": "#655b6f",
"on-tertiary-container": "#5c5367",
"surface-bright": "#f8faf9",
"on-secondary-fixed-variant": "#4a5d77",
"inverse-primary": "#d7f5e7",
"surface-container-low": "#f1f4f3",
"outline": "#757c7b",
"secondary-fixed": "#d3e3ff",
"on-background": "#2d3433",
"secondary-dim": "#42546f",
"inverse-on-surface": "#9b9d9d",
"surface-dim": "#d4dbda",
"surface-container-lowest": "#ffffff",
"on-surface-variant": "#596060",
"secondary": "#4e607b"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Plus Jakarta Sans"]
}
},
},
}
</script>
<style>
body { font-family: 'Manrope', sans-serif; background-color: #f8faf9; color: #2d3433; }
.material-symbols-outlined { font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24; }
.calendar-grid { grid-template-columns: 80px repeat(7, 1fr); }
.no-scrollbar::-webkit-scrollbar { display: none; }
</style>
</head>
<body class="bg-background overflow-hidden">
<!-- Shared SideNavBar -->
<aside class="h-screen w-64 fixed left-0 top-0 bg-stone-50 dark:bg-stone-950 flex flex-col py-8 px-6 font-manrope text-stone-800 dark:text-stone-200">
<div class="mb-10">
<div class="flex items-center gap-3 mb-6">
<img alt="User profile" class="w-10 h-10 rounded-full object-cover" data-alt="Close-up portrait of a woman in soft natural lighting, minimal aesthetic, serene expression" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAGbn0o4qJqoKdsK0dDVFGY7TZ1WDmYrE8gkUpb7PG0hBvf41AX8BAQGScbG7RKgMvxbic2RTaGRAZl0ITsTRdsw1AtenE2CVHtZyDfzn9U0PqefZ7m_HEvu7b44-N95aY9NpDsRctqvmcqEoP9ccPFXwj-gFaAWNneB8PsQPHQ1jfm8iiSDIwa2wZabnEfiNs6kF02TUixrZFLh1KxqGHg9iBGKkLUwkdFiF4AsOY0xDein1h1vqf-olMcwyvAlX0x7Uoe_NsxOtrP"/>
<div>
<h1 class="text-xl font-bold tracking-tight text-emerald-900 dark:text-emerald-200">Serene Path</h1>
<p class="text-xs font-label text-stone-500 uppercase tracking-widest">The Curated Breath</p>
</div>
</div>
<button class="w-full bg-primary py-3 rounded-full text-on-primary font-bold flex items-center justify-center gap-2 hover:opacity-80 transition-all active:scale-95">
<span class="material-symbols-outlined">add</span>
New Entry
</button>
</div>
<nav class="flex-1 space-y-2">
<a class="flex items-center gap-3 px-4 py-3 rounded-xl transition-colors duration-300 hover:bg-stone-200/50 dark:hover:bg-stone-800/50 text-stone-500 dark:text-stone-400" href="#">
<span class="material-symbols-outlined" data-icon="dashboard">dashboard</span>
Dashboard
</a>
<a class="flex items-center gap-3 px-4 py-3 rounded-xl transition-colors duration-300 text-emerald-800 dark:text-emerald-400 font-bold border-r-4 border-emerald-700 bg-stone-100 dark:bg-stone-900" href="#">
<span class="material-symbols-outlined" data-icon="calendar_today">calendar_today</span>
Calendar
</a>
<a class="flex items-center gap-3 px-4 py-3 rounded-xl transition-colors duration-300 hover:bg-stone-200/50 dark:hover:bg-stone-800/50 text-stone-500 dark:text-stone-400" href="#">
<span class="material-symbols-outlined" data-icon="smart_toy">smart_toy</span>
AI Assistant
</a>
<a class="flex items-center gap-3 px-4 py-3 rounded-xl transition-colors duration-300 hover:bg-stone-200/50 dark:hover:bg-stone-800/50 text-stone-500 dark:text-stone-400" href="#">
<span class="material-symbols-outlined" data-icon="auto_awesome_motion">auto_awesome_motion</span>
Habits
</a>
<a class="flex items-center gap-3 px-4 py-3 rounded-xl transition-colors duration-300 hover:bg-stone-200/50 dark:hover:bg-stone-800/50 text-stone-500 dark:text-stone-400" href="#">
<span class="material-symbols-outlined" data-icon="library_books">library_books</span>
Library
</a>
</nav>
<footer class="mt-auto space-y-1">
<a class="flex items-center gap-3 px-4 py-3 rounded-xl transition-colors duration-300 hover:bg-stone-200/50 text-stone-500" href="#">
<span class="material-symbols-outlined" data-icon="settings">settings</span>
Settings
</a>
<a class="flex items-center gap-3 px-4 py-3 rounded-xl transition-colors duration-300 hover:bg-stone-200/50 text-stone-500" href="#">
<span class="material-symbols-outlined" data-icon="help">help</span>
Support
</a>
</footer>
</aside>
<!-- Main Workspace -->
<main class="ml-64 flex min-h-screen">
<!-- Weekly Calendar Center -->
<section class="flex-1 flex flex-col bg-surface overflow-hidden">
<!-- Top Nav Bar Shared -->
<header class="w-full h-16 flex items-center justify-between px-12 sticky top-0 z-10 bg-transparent font-manrope">
<div class="flex items-center gap-8">
<div class="flex items-center gap-4">
<button class="p-2 hover:bg-surface-container-low rounded-full transition-all">
<span class="material-symbols-outlined">chevron_left</span>
</button>
<h2 class="text-xl font-bold tracking-tight text-on-surface">September 11 — 17</h2>
<button class="p-2 hover:bg-surface-container-low rounded-full transition-all">
<span class="material-symbols-outlined">chevron_right</span>
</button>
</div>
<div class="flex bg-surface-container-low p-1 rounded-full">
<button class="px-4 py-1 text-sm font-label text-on-surface-variant hover:text-on-surface">Day</button>
<button class="px-6 py-1 text-sm font-label bg-surface-container-lowest text-primary font-bold rounded-full shadow-sm">Week</button>
<button class="px-4 py-1 text-sm font-label text-on-surface-variant hover:text-on-surface">Month</button>
</div>
</div>
<div class="flex items-center gap-4">
<div class="relative group">
<div class="flex items-center bg-surface-container-low rounded-full px-4 py-2 w-64">
<span class="material-symbols-outlined text-outline text-sm">search</span>
<input class="bg-transparent border-none focus:ring-0 text-sm font-label ml-2 w-full" placeholder="Search focus moments..." type="text"/>
</div>
</div>
<button class="p-2 text-on-secondary-container hover:text-emerald-600 transition-all">
<span class="material-symbols-outlined">notifications</span>
</button>
<button class="p-2 text-on-secondary-container hover:text-emerald-600 transition-all">
<span class="material-symbols-outlined">account_circle</span>
</button>
</div>
</header>
<!-- Calendar Canvas -->
<div class="flex-1 px-8 pb-8 overflow-y-auto no-scrollbar">
<div class="bg-surface-container-lowest rounded-[2rem] h-full shadow-sm border border-outline-variant/10 flex flex-col">
<!-- Day Headers -->
<div class="calendar-grid grid border-b border-surface-container-low">
<div class="p-4"></div>
<div class="p-4 text-center border-l border-surface-container-low">
<span class="block text-xs font-label text-on-surface-variant mb-1">MON</span>
<span class="text-xl font-headline font-bold">11</span>
</div>
<div class="p-4 text-center border-l border-surface-container-low bg-primary-container/10">
<span class="block text-xs font-label text-primary font-bold mb-1">TUE</span>
<span class="text-xl font-headline font-bold text-primary">12</span>
</div>
<div class="p-4 text-center border-l border-surface-container-low">
<span class="block text-xs font-label text-on-surface-variant mb-1">WED</span>
<span class="text-xl font-headline font-bold">13</span>
</div>
<div class="p-4 text-center border-l border-surface-container-low">
<span class="block text-xs font-label text-on-surface-variant mb-1">THU</span>
<span class="text-xl font-headline font-bold">14</span>
</div>
<div class="p-4 text-center border-l border-surface-container-low">
<span class="block text-xs font-label text-on-surface-variant mb-1">FRI</span>
<span class="text-xl font-headline font-bold">15</span>
</div>
<div class="p-4 text-center border-l border-surface-container-low">
<span class="block text-xs font-label text-on-surface-variant mb-1">SAT</span>
<span class="text-xl font-headline font-bold">16</span>
</div>
<div class="p-4 text-center border-l border-surface-container-low">
<span class="block text-xs font-label text-on-surface-variant mb-1">SUN</span>
<span class="text-xl font-headline font-bold text-on-surface-variant">17</span>
</div>
</div>
<!-- Time Grid -->
<div class="flex-1 relative overflow-y-auto no-scrollbar">
<div class="calendar-grid grid min-h-max">
<!-- Time Column -->
<div class="bg-surface-container-low/30">
<div class="h-20 flex items-start justify-end pr-4 pt-2 text-[10px] font-label text-outline uppercase tracking-tighter">08:00 AM</div>
<div class="h-20 flex items-start justify-end pr-4 pt-2 text-[10px] font-label text-outline uppercase tracking-tighter">09:00 AM</div>
<div class="h-20 flex items-start justify-end pr-4 pt-2 text-[10px] font-label text-outline uppercase tracking-tighter border-t border-surface-container-low">10:00 AM</div>
<div class="h-20 flex items-start justify-end pr-4 pt-2 text-[10px] font-label text-outline uppercase tracking-tighter border-t border-surface-container-low">11:00 AM</div>
<div class="h-20 flex items-start justify-end pr-4 pt-2 text-[10px] font-label text-outline uppercase tracking-tighter border-t border-surface-container-low">12:00 PM</div>
<div class="h-20 flex items-start justify-end pr-4 pt-2 text-[10px] font-label text-outline uppercase tracking-tighter border-t border-surface-container-low">01:00 PM</div>
<div class="h-20 flex items-start justify-end pr-4 pt-2 text-[10px] font-label text-outline uppercase tracking-tighter border-t border-surface-container-low">02:00 PM</div>
<div class="h-20 flex items-start justify-end pr-4 pt-2 text-[10px] font-label text-outline uppercase tracking-tighter border-t border-surface-container-low">03:00 PM</div>
<div class="h-20 flex items-start justify-end pr-4 pt-2 text-[10px] font-label text-outline uppercase tracking-tighter border-t border-surface-container-low">04:00 PM</div>
</div>
<!-- Day Columns -->
<div class="relative border-l border-surface-container-low"><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div></div>
<div class="relative border-l border-surface-container-low bg-primary-container/5">
<!-- Calendar Block: Focus -->
<div class="absolute top-[80px] left-1 right-1 h-[140px] bg-primary text-on-primary p-3 rounded-xl shadow-sm z-20">
<div class="flex items-center gap-2 mb-1">
<span class="material-symbols-outlined text-[14px]">psychology</span>
<p class="text-[10px] font-label uppercase font-bold tracking-widest opacity-80">Deep Work</p>
</div>
<p class="text-sm font-bold leading-tight">UI Design Language</p>
<p class="text-[10px] mt-1 opacity-70">Focus Zone</p>
</div>
<div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div>
</div>
<div class="relative border-l border-surface-container-low">
<!-- Calendar Block: Wellness -->
<div class="absolute top-[240px] left-1 right-1 h-[60px] bg-tertiary-container text-on-tertiary-container p-2 rounded-xl border border-tertiary/20">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-[14px]">spa</span>
<p class="text-xs font-bold">Guided Meditation</p>
</div>
</div>
<div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div>
</div>
<div class="relative border-l border-surface-container-low">
<!-- Calendar Block: Collaboration -->
<div class="absolute top-[160px] left-1 right-1 h-[80px] bg-secondary-container text-on-secondary-container p-3 rounded-xl border border-secondary/10">
<p class="text-xs font-bold">Stakeholder Review</p>
<p class="text-[10px] mt-1 opacity-80">Design Sync</p>
</div>
<div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div>
</div>
<div class="relative border-l border-surface-container-low"><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div></div>
<div class="relative border-l border-surface-container-low bg-surface-container-low/20"><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div></div>
<div class="relative border-l border-surface-container-low bg-surface-container-low/20"><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div><div class="h-20 border-b border-surface-container-low/50"></div></div>
</div>
</div>
</div>
</div>
</section>
<!-- Right Context Sidebar -->
<aside class="w-80 bg-surface-container-low p-6 flex flex-col gap-8 overflow-y-auto no-scrollbar">
<!-- Mini Month View -->
<div class="bg-surface-container-lowest p-6 rounded-3xl shadow-sm">
<div class="flex items-center justify-between mb-4">
<h3 class="text-sm font-bold font-headline">September 2023</h3>
<div class="flex gap-1">
<span class="material-symbols-outlined text-sm cursor-pointer">chevron_left</span>
<span class="material-symbols-outlined text-sm cursor-pointer">chevron_right</span>
</div>
</div>
<div class="grid grid-cols-7 gap-y-2 text-center">
<span class="text-[10px] font-label text-outline">S</span>
<span class="text-[10px] font-label text-outline">M</span>
<span class="text-[10px] font-label text-outline">T</span>
<span class="text-[10px] font-label text-outline">W</span>
<span class="text-[10px] font-label text-outline">T</span>
<span class="text-[10px] font-label text-outline">F</span>
<span class="text-[10px] font-label text-outline">S</span>
<span class="text-xs text-outline py-1">27</span>
<span class="text-xs text-outline py-1">28</span>
<span class="text-xs text-outline py-1">29</span>
<span class="text-xs text-outline py-1">30</span>
<span class="text-xs text-outline py-1">31</span>
<span class="text-xs py-1">1</span>
<span class="text-xs py-1">2</span>
<span class="text-xs py-1">3</span>
<span class="text-xs py-1">4</span>
<span class="text-xs py-1">5</span>
<span class="text-xs py-1">6</span>
<span class="text-xs py-1">7</span>
<span class="text-xs py-1">8</span>
<span class="text-xs py-1">9</span>
<span class="text-xs py-1">10</span>
<span class="text-xs py-1 bg-primary text-on-primary rounded-full font-bold">11</span>
<span class="text-xs py-1">12</span>
<span class="text-xs py-1">13</span>
<span class="text-xs py-1">14</span>
<span class="text-xs py-1">15</span>
<span class="text-xs py-1">16</span>
</div>
</div>
<!-- Health Data Integration -->
<div class="space-y-4">
<div class="flex items-center justify-between">
<h3 class="text-sm font-bold font-headline uppercase tracking-wider text-on-surface-variant">Vitals Sync</h3>
<span class="material-symbols-outlined text-emerald-600 text-sm">sync</span>
</div>
<!-- Sleep Card -->
<div class="bg-surface-container-lowest p-5 rounded-3xl border-l-4 border-secondary shadow-sm">
<div class="flex items-center gap-3 mb-3">
<div class="w-8 h-8 rounded-full bg-secondary-container flex items-center justify-center">
<span class="material-symbols-outlined text-secondary text-sm">bedtime</span>
</div>
<div>
<p class="text-[10px] font-label text-outline uppercase">Sleep Score</p>
<p class="text-lg font-bold font-headline">84/100</p>
</div>
</div>
<div class="w-full h-1.5 bg-surface-container-high rounded-full overflow-hidden">
<div class="w-[84%] h-full bg-secondary"></div>
</div>
<p class="text-[10px] text-on-surface-variant mt-2 font-label">Recommended bedtime: 10:30 PM</p>
</div>
<!-- Activity Card -->
<div class="bg-surface-container-lowest p-5 rounded-3xl border-l-4 border-primary shadow-sm">
<div class="flex items-center gap-3 mb-3">
<div class="w-8 h-8 rounded-full bg-primary-container flex items-center justify-center">
<span class="material-symbols-outlined text-primary text-sm">directions_run</span>
</div>
<div>
<p class="text-[10px] font-label text-outline uppercase">Movement</p>
<p class="text-lg font-bold font-headline">6,240 <span class="text-xs font-normal opacity-60">steps</span></p>
</div>
</div>
<div class="w-full h-1.5 bg-surface-container-high rounded-full overflow-hidden">
<div class="w-[62%] h-full bg-primary"></div>
</div>
</div>
</div>
<!-- Categories / Tags -->
<div class="mt-auto">
<h3 class="text-xs font-bold font-label uppercase tracking-widest text-outline mb-4">Focus Categories</h3>
<div class="flex flex-wrap gap-2">
<span class="px-3 py-1 bg-primary/10 text-primary text-[10px] font-bold rounded-full border border-primary/20">DEEP WORK</span>
<span class="px-3 py-1 bg-secondary/10 text-secondary text-[10px] font-bold rounded-full border border-secondary/20">STRATEGY</span>
<span class="px-3 py-1 bg-tertiary/10 text-tertiary text-[10px] font-bold rounded-full border border-tertiary/20">RECOVERY</span>
<span class="px-3 py-1 bg-surface-container-highest text-on-surface-variant text-[10px] font-bold rounded-full">ADMIN</span>
</div>
</div>
<!-- Insight Quote -->
<div class="mt-4 p-4 rounded-3xl bg-gradient-to-br from-primary-container/30 to-tertiary-container/30">
<p class="text-xs italic text-on-primary-container/80 leading-relaxed">
"The way you spend your hours is the way you spend your life."
</p>
</div>
</aside>
</main>
</body></html>

View File

@ -0,0 +1,350 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Serene Path | Dashboard</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&amp;family=Plus+Jakarta+Sans:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary-fixed": "#f4e6ff",
"on-primary-container": "#3d574d",
"on-error-container": "#6e0a12",
"on-secondary-fixed": "#2e405a",
"on-tertiary-fixed-variant": "#675d71",
"on-surface": "#2d3433",
"surface-tint": "#4a655a",
"surface": "#f8faf9",
"on-secondary": "#f8f8ff",
"secondary-fixed-dim": "#c2d6f5",
"surface-variant": "#dde4e3",
"primary-fixed-dim": "#bedbce",
"on-tertiary-fixed": "#4a4154",
"primary-dim": "#3e594e",
"primary": "#4a655a",
"on-primary": "#e5fff2",
"inverse-surface": "#0b0f0f",
"on-error": "#fff7f6",
"background": "#f8faf9",
"error": "#a83836",
"outline-variant": "#acb3b2",
"secondary-container": "#d3e3ff",
"on-tertiary": "#fef6ff",
"primary-container": "#cbe9db",
"tertiary-dim": "#594f63",
"tertiary-container": "#f4e6ff",
"surface-container-high": "#e4e9e8",
"tertiary-fixed-dim": "#e5d8f0",
"error-container": "#fa746f",
"on-primary-fixed": "#2a443b",
"surface-container": "#eaefee",
"primary-fixed": "#cbe9db",
"on-primary-fixed-variant": "#466156",
"error-dim": "#67040d",
"on-secondary-container": "#40536d",
"surface-container-highest": "#dde4e3",
"tertiary": "#655b6f",
"on-tertiary-container": "#5c5367",
"surface-bright": "#f8faf9",
"on-secondary-fixed-variant": "#4a5d77",
"inverse-primary": "#d7f5e7",
"surface-container-low": "#f1f4f3",
"outline": "#757c7b",
"secondary-fixed": "#d3e3ff",
"on-background": "#2d3433",
"secondary-dim": "#42546f",
"inverse-on-surface": "#9b9d9d",
"surface-dim": "#d4dbda",
"surface-container-lowest": "#ffffff",
"on-surface-variant": "#596060",
"secondary": "#4e607b"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"2xl": "1.5rem",
"full": "9999px"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Plus Jakarta Sans"]
}
}
}
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.glass-panel {
background: rgba(248, 250, 249, 0.8);
backdrop-filter: blur(20px);
}
</style>
</head>
<body class="bg-surface text-on-surface font-body overflow-hidden">
<!-- SideNavBar -->
<aside class="h-screen w-64 fixed left-0 top-0 bg-stone-50 flex flex-col py-8 px-6 font-manrope text-stone-800 z-20">
<div class="mb-10 flex items-center gap-3">
<div class="w-10 h-10 rounded-xl bg-primary flex items-center justify-center text-on-primary">
<span class="material-symbols-outlined">spa</span>
</div>
<div>
<h1 class="text-xl font-bold tracking-tight text-emerald-900">Serene Path</h1>
<p class="text-[10px] font-label uppercase tracking-widest text-stone-500">The Curated Breath</p>
</div>
</div>
<button class="mb-8 w-full py-3 px-4 rounded-full bg-primary text-on-primary flex items-center justify-center gap-2 font-semibold shadow-xl shadow-primary/10 hover:opacity-90 transition-all active:scale-95">
<span class="material-symbols-outlined">add</span>
New Entry
</button>
<nav class="flex-1 space-y-1">
<!-- Active State: Dashboard -->
<a class="flex items-center gap-3 py-3 px-4 rounded-xl text-emerald-800 font-bold border-r-4 border-emerald-700 bg-stone-200/50 transition-colors duration-300" href="#">
<span class="material-symbols-outlined">dashboard</span>
<span>Dashboard</span>
</a>
<a class="flex items-center gap-3 py-3 px-4 rounded-xl text-stone-500 hover:text-emerald-700 hover:bg-stone-200/50 transition-colors duration-300" href="#">
<span class="material-symbols-outlined">calendar_today</span>
<span>Calendar</span>
</a>
<a class="flex items-center gap-3 py-3 px-4 rounded-xl text-stone-500 hover:text-emerald-700 hover:bg-stone-200/50 transition-colors duration-300" href="#">
<span class="material-symbols-outlined">smart_toy</span>
<span>AI Assistant</span>
</a>
<a class="flex items-center gap-3 py-3 px-4 rounded-xl text-stone-500 hover:text-emerald-700 hover:bg-stone-200/50 transition-colors duration-300" href="#">
<span class="material-symbols-outlined">auto_awesome_motion</span>
<span>Habits</span>
</a>
<a class="flex items-center gap-3 py-3 px-4 rounded-xl text-stone-500 hover:text-emerald-700 hover:bg-stone-200/50 transition-colors duration-300" href="#">
<span class="material-symbols-outlined">library_books</span>
<span>Library</span>
</a>
</nav>
<div class="mt-auto space-y-1 pt-6">
<a class="flex items-center gap-3 py-3 px-4 rounded-xl text-stone-500 hover:text-emerald-700 hover:bg-stone-200/50 transition-colors duration-300" href="#">
<span class="material-symbols-outlined">settings</span>
<span>Settings</span>
</a>
<a class="flex items-center gap-3 py-3 px-4 rounded-xl text-stone-500 hover:text-emerald-700 hover:bg-stone-200/50 transition-colors duration-300" href="#">
<span class="material-symbols-outlined">help</span>
<span>Support</span>
</a>
<div class="pt-6 mt-6 border-t border-stone-200 flex items-center gap-3">
<img alt="User profile" class="w-10 h-10 rounded-full object-cover border-2 border-primary-container" data-alt="portrait of a calm professional woman in minimalist studio lighting with soft natural shadows" src="https://lh3.googleusercontent.com/aida-public/AB6AXuDQQNVeWqTZ7WCzww1HpSw6l-0NNBRLiUbydzCC5CTty-V-sVlANbx1tGCU9-bnWKvSud6PzBZc8qNg3Y3kjpD8hbB1R7YGq06sZ9YVXmkF9j6tybOTGKXyWJdTs27o-j71sWJLmaT012WS8Oj5vGtiuKUmC4ic9wYcZ4k1WlyREij59AMBmXqJjJfPiJ1ea4X6KdF1-m63ShK6fJAXfZAeFM85zOQncxqto8SGUqWEzKfM2V9GqfVC6PtewSDZNvyhg_auivVy5OdQ"/>
<div class="flex flex-col">
<span class="text-sm font-bold text-on-surface">Elena Vance</span>
<span class="text-[10px] font-label text-stone-500">Premium Member</span>
</div>
</div>
</div>
</aside>
<!-- Main Content Area -->
<main class="ml-64 flex-1 h-screen overflow-y-auto bg-surface flex">
<!-- Center Content: Productivity Feed -->
<div class="flex-1 px-12 py-8 overflow-y-auto">
<!-- TopAppBar Internal Mapping -->
<header class="flex items-center justify-between mb-12 sticky top-0 bg-surface/80 backdrop-blur-md z-10 py-4">
<div class="flex flex-col">
<h2 class="text-3xl font-bold tracking-tight text-on-surface">Good Morning, Elena</h2>
<p class="text-on-surface-variant font-label mt-1">Your path today is clear and focused.</p>
</div>
<div class="flex items-center gap-6">
<div class="relative group">
<span class="material-symbols-outlined text-on-surface-variant group-hover:text-primary transition-all cursor-pointer">search</span>
</div>
<div class="flex items-center gap-4 text-on-surface-variant">
<span class="material-symbols-outlined hover:text-primary cursor-pointer">notifications</span>
<span class="material-symbols-outlined hover:text-primary cursor-pointer">account_circle</span>
</div>
</div>
</header>
<!-- Grid Layout -->
<div class="grid grid-cols-12 gap-8">
<!-- Current Focus Card (Bento Style) -->
<section class="col-span-12 lg:col-span-8 bg-surface-container-low rounded-2xl p-8 relative overflow-hidden flex flex-col justify-between min-h-[340px]">
<div class="relative z-10">
<div class="flex items-center gap-3 mb-6">
<span class="px-4 py-1.5 rounded-full bg-primary-container text-on-primary-fixed text-xs font-label font-semibold">Current Focus</span>
<span class="text-xs font-label text-on-surface-variant">45 mins remaining</span>
</div>
<h3 class="text-4xl font-extrabold text-on-surface mb-4 leading-tight">Serene Path UI Refinement</h3>
<p class="text-lg text-on-surface-variant max-w-md">Deep work session focused on architectural patterns and spatial hierarchy.</p>
</div>
<div class="relative z-10 flex items-center gap-4 mt-8">
<button class="bg-primary text-on-primary px-8 py-3 rounded-full font-bold flex items-center gap-2 hover:opacity-90 active:scale-95 transition-all">
<span class="material-symbols-outlined">pause_circle</span>
Pause Session
</button>
<button class="text-primary font-bold hover:underline">Complete Task</button>
</div>
<!-- Visual Accent -->
<div class="absolute right-0 bottom-0 top-0 w-1/3 bg-gradient-to-l from-primary-fixed-dim/20 to-transparent pointer-events-none"></div>
<img alt="decorative" class="absolute right-8 top-1/2 -translate-y-1/2 w-48 h-48 object-contain opacity-40 mix-blend-multiply" data-alt="minimalist abstract sculpture of a white sphere on a pedestal in a sunlit art gallery" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCr3gmjwbNL74MbEER826FpgoKx20vqr_WCjcfnBI62zgNQBYG75qaPADzZY-_S-ia7ulrmF3JQhTbtQDXJc-wrdUDzLGQEcokZQ0FY1fybn2daumbJHgjo--1Xcib_81JclVRT8u0K7IPwYYioHlNZ6LaVdoiiiGneq3G-qqRoT9B3ppdwLdBaKS8RhSzh9V3f-67Kl3ttIg3GzQYQ3KA23dGQ0K30PE4kbiWwyAJRe_EdIURq1nGV3jh9RKmdT770ifWCxkeKiMuS"/>
</section>
<!-- Daily Progress Widget -->
<section class="col-span-12 lg:col-span-4 bg-surface-container-lowest rounded-2xl p-8 border border-outline-variant/15 flex flex-col justify-between">
<div>
<h4 class="text-xl font-bold text-on-surface mb-6">Daily Habits</h4>
<div class="space-y-6">
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-tertiary">mindfulness</span>
<span class="font-medium">Meditation</span>
</div>
<span class="material-symbols-outlined text-primary" style="font-variation-settings: 'FILL' 1;">check_circle</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-secondary">water_drop</span>
<span class="font-medium">Hydration</span>
</div>
<span class="material-symbols-outlined text-outline-variant">radio_button_unchecked</span>
</div>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-primary">local_library</span>
<span class="font-medium">Reading</span>
</div>
<span class="material-symbols-outlined text-primary" style="font-variation-settings: 'FILL' 1;">check_circle</span>
</div>
</div>
</div>
<div class="mt-8">
<div class="flex justify-between items-end mb-2">
<span class="text-sm font-label font-bold">Total Progress</span>
<span class="text-sm font-label text-primary">68%</span>
</div>
<div class="w-full h-2 bg-surface-container-highest rounded-full overflow-hidden">
<div class="h-full bg-gradient-to-r from-primary to-primary-fixed-dim" style="width: 68%;"></div>
</div>
</div>
</section>
<!-- Quick Links Grid -->
<section class="col-span-12 grid grid-cols-1 md:grid-cols-4 gap-6">
<div class="bg-surface-container-low p-6 rounded-2xl flex flex-col items-center justify-center text-center hover:bg-surface-container-high transition-colors group cursor-pointer">
<div class="w-12 h-12 rounded-full bg-surface-container-lowest flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
<span class="material-symbols-outlined text-primary">auto_awesome</span>
</div>
<span class="text-sm font-bold">Inspiration</span>
</div>
<div class="bg-surface-container-low p-6 rounded-2xl flex flex-col items-center justify-center text-center hover:bg-surface-container-high transition-colors group cursor-pointer">
<div class="w-12 h-12 rounded-full bg-surface-container-lowest flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
<span class="material-symbols-outlined text-secondary">history</span>
</div>
<span class="text-sm font-bold">Recent Logs</span>
</div>
<div class="bg-surface-container-low p-6 rounded-2xl flex flex-col items-center justify-center text-center hover:bg-surface-container-high transition-colors group cursor-pointer">
<div class="w-12 h-12 rounded-full bg-surface-container-lowest flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
<span class="material-symbols-outlined text-tertiary">menu_book</span>
</div>
<span class="text-sm font-bold">Quick Journal</span>
</div>
<div class="bg-surface-container-low p-6 rounded-2xl flex flex-col items-center justify-center text-center hover:bg-surface-container-high transition-colors group cursor-pointer">
<div class="w-12 h-12 rounded-full bg-surface-container-lowest flex items-center justify-center mb-4 group-hover:scale-110 transition-transform">
<span class="material-symbols-outlined text-on-surface-variant">settings_ethernet</span>
</div>
<span class="text-sm font-bold">Integrations</span>
</div>
</section>
</div>
</div>
<!-- Right Content: Day at a Glance Sidebar -->
<aside class="w-96 bg-surface-container-low px-10 py-12 flex flex-col gap-10 overflow-y-auto">
<!-- Energy Check-in -->
<section>
<h4 class="text-lg font-bold text-on-surface mb-6">Energy Check-in</h4>
<div class="bg-surface-container-lowest p-8 rounded-2xl border border-outline-variant/10">
<div class="flex justify-between mb-8">
<span class="material-symbols-outlined text-on-surface-variant opacity-50">battery_horiz_000</span>
<span class="material-symbols-outlined text-primary">battery_charging_80</span>
<span class="material-symbols-outlined text-on-surface-variant opacity-50">battery_full</span>
</div>
<input class="w-full h-1.5 bg-surface-container-high rounded-lg appearance-none cursor-pointer accent-primary" max="100" min="0" type="range" value="75"/>
<div class="flex justify-between mt-4">
<span class="text-[10px] font-label text-on-surface-variant">Low</span>
<span class="text-[10px] font-label font-bold text-primary">Focused</span>
<span class="text-[10px] font-label text-on-surface-variant">Peak</span>
</div>
</div>
</section>
<!-- Schedule -->
<section class="flex-1">
<div class="flex items-center justify-between mb-6">
<h4 class="text-lg font-bold text-on-surface">Schedule</h4>
<span class="text-xs font-label text-primary font-bold">Today</span>
</div>
<div class="space-y-4">
<!-- Morning -->
<div class="flex gap-4 relative">
<div class="flex flex-col items-center">
<div class="w-2.5 h-2.5 rounded-full bg-primary ring-4 ring-primary/10"></div>
<div class="w-0.5 flex-1 bg-outline-variant/20 my-2"></div>
</div>
<div class="pb-8">
<span class="text-[10px] font-label text-on-surface-variant">08:00 AM</span>
<p class="font-bold text-on-surface">Deep Focus: Project Alpha</p>
</div>
</div>
<!-- Midday -->
<div class="flex gap-4 relative">
<div class="flex flex-col items-center">
<div class="w-2.5 h-2.5 rounded-full bg-secondary ring-4 ring-secondary/10"></div>
<div class="w-0.5 flex-1 bg-outline-variant/20 my-2"></div>
</div>
<div class="pb-8">
<span class="text-[10px] font-label text-on-surface-variant">12:30 PM</span>
<p class="font-bold text-on-surface">Lunch &amp; Mindful Walk</p>
</div>
</div>
<!-- Afternoon (Current) -->
<div class="flex gap-4 relative">
<div class="flex flex-col items-center">
<div class="w-2.5 h-2.5 rounded-full bg-primary animate-pulse ring-4 ring-primary/20"></div>
<div class="w-0.5 flex-1 bg-outline-variant/20 my-2"></div>
</div>
<div class="pb-8">
<div class="flex items-center gap-2">
<span class="text-[10px] font-label text-primary font-bold">NOW</span>
<span class="text-[10px] font-label text-on-surface-variant">02:00 PM</span>
</div>
<p class="font-bold text-on-surface">Design System Audit</p>
</div>
</div>
<!-- Evening -->
<div class="flex gap-4 relative">
<div class="flex flex-col items-center">
<div class="w-2.5 h-2.5 rounded-full bg-tertiary"></div>
</div>
<div>
<span class="text-[10px] font-label text-on-surface-variant">05:00 PM</span>
<p class="font-bold text-on-surface">Reflection &amp; Wrap-up</p>
</div>
</div>
</div>
</section>
<!-- Decorative Card -->
<section class="mt-auto">
<div class="bg-gradient-to-br from-tertiary to-tertiary-dim p-6 rounded-2xl text-on-tertiary">
<span class="material-symbols-outlined mb-4">format_quote</span>
<p class="italic text-sm leading-relaxed mb-4">"The soul usually knows what to do to heal itself. The challenge is to silence the mind."</p>
<p class="text-[10px] font-label opacity-80">— Caroline Myss</p>
</div>
</section>
</aside>
</main>
<!-- FAB Suppression: On Settings/Profile/Details, hide. On Home/Dashboard, show if relevant. -->
<button class="fixed bottom-8 right-8 w-14 h-14 rounded-full bg-primary text-on-primary shadow-2xl flex items-center justify-center hover:scale-105 active:scale-95 transition-all z-30 lg:hidden">
<span class="material-symbols-outlined">add</span>
</button>
</body></html>

347
assets/ui/web/habits.html Normal file
View File

@ -0,0 +1,347 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Serene Path | Habits &amp; Identity</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&amp;family=Plus+Jakarta+Sans:wght@400;500;600;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary-fixed": "#f4e6ff",
"on-primary-container": "#3d574d",
"on-error-container": "#6e0a12",
"on-secondary-fixed": "#2e405a",
"on-tertiary-fixed-variant": "#675d71",
"on-surface": "#2d3433",
"surface-tint": "#4a655a",
"surface": "#f8faf9",
"on-secondary": "#f8f8ff",
"secondary-fixed-dim": "#c2d6f5",
"surface-variant": "#dde4e3",
"primary-fixed-dim": "#bedbce",
"on-tertiary-fixed": "#4a4154",
"primary-dim": "#3e594e",
"primary": "#4a655a",
"on-primary": "#e5fff2",
"inverse-surface": "#0b0f0f",
"on-error": "#fff7f6",
"background": "#f8faf9",
"error": "#a83836",
"outline-variant": "#acb3b2",
"secondary-container": "#d3e3ff",
"on-tertiary": "#fef6ff",
"primary-container": "#cbe9db",
"tertiary-dim": "#594f63",
"tertiary-container": "#f4e6ff",
"surface-container-high": "#e4e9e8",
"tertiary-fixed-dim": "#e5d8f0",
"error-container": "#fa746f",
"on-primary-fixed": "#2a443b",
"surface-container": "#eaefee",
"primary-fixed": "#cbe9db",
"on-primary-fixed-variant": "#466156",
"error-dim": "#67040d",
"on-secondary-container": "#40536d",
"surface-container-highest": "#dde4e3",
"tertiary": "#655b6f",
"on-tertiary-container": "#5c5367",
"surface-bright": "#f8faf9",
"on-secondary-fixed-variant": "#4a5d77",
"inverse-primary": "#d7f5e7",
"surface-container-low": "#f1f4f3",
"outline": "#757c7b",
"secondary-fixed": "#d3e3ff",
"on-background": "#2d3433",
"secondary-dim": "#42546f",
"inverse-on-surface": "#9b9d9d",
"surface-dim": "#d4dbda",
"surface-container-lowest": "#ffffff",
"on-surface-variant": "#596060",
"secondary": "#4e607b"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Plus Jakarta Sans"]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.no-scrollbar::-webkit-scrollbar { display: none; }
.no-scrollbar { -ms-overflow-style: none; scrollbar-width: none; }
</style>
</head>
<body class="bg-background font-body text-on-surface">
<!-- SideNavBar -->
<aside class="h-screen w-64 fixed left-0 top-0 bg-stone-50 flex flex-col py-8 px-6 font-manrope text-stone-800">
<div class="mb-10">
<h1 class="text-xl font-bold tracking-tight text-emerald-900">Serene Path</h1>
<p class="text-[10px] uppercase tracking-widest text-stone-500 mt-1">The Curated Breath</p>
</div>
<nav class="flex-1 space-y-2">
<a class="flex items-center gap-3 py-3 px-4 rounded-xl text-stone-500 hover:bg-stone-200/50 transition-colors duration-300 transition-all active:opacity-80 active:scale-95" href="#">
<span class="material-symbols-outlined">dashboard</span>
<span class="font-medium">Dashboard</span>
</a>
<a class="flex items-center gap-3 py-3 px-4 rounded-xl text-stone-500 hover:bg-stone-200/50 transition-colors duration-300 transition-all active:opacity-80 active:scale-95" href="#">
<span class="material-symbols-outlined">calendar_today</span>
<span class="font-medium">Calendar</span>
</a>
<a class="flex items-center gap-3 py-3 px-4 rounded-xl text-stone-500 hover:bg-stone-200/50 transition-colors duration-300 transition-all active:opacity-80 active:scale-95" href="#">
<span class="material-symbols-outlined">smart_toy</span>
<span class="font-medium">AI Assistant</span>
</a>
<a class="flex items-center gap-3 py-3 px-4 rounded-xl text-emerald-800 font-bold border-r-4 border-emerald-700 bg-stone-200/50 transition-all active:opacity-80 active:scale-95" href="#">
<span class="material-symbols-outlined">auto_awesome_motion</span>
<span class="font-medium">Habits</span>
</a>
<a class="flex items-center gap-3 py-3 px-4 rounded-xl text-stone-500 hover:bg-stone-200/50 transition-colors duration-300 transition-all active:opacity-80 active:scale-95" href="#">
<span class="material-symbols-outlined">library_books</span>
<span class="font-medium">Library</span>
</a>
</nav>
<div class="mt-8">
<button class="w-full bg-primary text-on-primary py-4 rounded-full font-bold flex items-center justify-center gap-2 shadow-lg shadow-primary/10 hover:opacity-90 transition-all">
<span class="material-symbols-outlined">add</span>
New Entry
</button>
</div>
<div class="mt-auto space-y-2 border-t border-stone-100 pt-6">
<a class="flex items-center gap-3 py-2 px-4 rounded-xl text-stone-500 hover:bg-stone-200/50 transition-all" href="#">
<span class="material-symbols-outlined">settings</span>
<span class="font-medium text-sm">Settings</span>
</a>
<a class="flex items-center gap-3 py-2 px-4 rounded-xl text-stone-500 hover:bg-stone-200/50 transition-all" href="#">
<span class="material-symbols-outlined">help</span>
<span class="font-medium text-sm">Support</span>
</a>
<div class="flex items-center gap-3 px-4 py-4 mt-2">
<img alt="User profile" class="w-10 h-10 rounded-full object-cover" data-alt="Close up portrait of a serene woman with natural lighting and a minimalist background" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAVnnwyoVrgeaS7kEziuuiiE0cyJVfnLCVTECD43Eubqxmhdty131e0GjGvyqXKP69ecEDZ8dvj5AEXagStK65HneJZTjZSgT4s4VfHmGHcb7Xxd9JvCocKDFViC93aGBn_sLlaO1oACISJ8OdgZrKvGUK_vbCOa6Hmx9Zihje4agEikfe2cpM3xg0G3tmJ65QnW8aY5InbDMKg_RVj3ZALN-83Vino_oskJPntq_zV6aNJFRs0O4ZWHr3AHTuM4IJ6UiKLNHIqWE3o"/>
<div class="overflow-hidden">
<p class="text-sm font-bold text-on-surface truncate">Elena Thorne</p>
<p class="text-[10px] text-stone-500 uppercase tracking-tighter">Pro Member</p>
</div>
</div>
</div>
</aside>
<!-- Main Content Area -->
<main class="ml-64 min-h-screen flex">
<!-- Center Canvas -->
<div class="flex-1 p-12 overflow-y-auto no-scrollbar">
<!-- TopNavBar (Contextual) -->
<header class="flex items-center justify-between mb-12">
<div>
<h2 class="text-3xl font-extrabold tracking-tight text-on-surface font-headline">Habit Synthesis</h2>
<p class="text-on-surface-variant font-label text-sm mt-1">Nurturing your daily keystone rituals.</p>
</div>
<div class="flex items-center gap-6">
<div class="relative">
<span class="absolute left-3 top-1/2 -translate-y-1/2 material-symbols-outlined text-outline">search</span>
<input class="bg-surface-container-low border-none rounded-full py-2 pl-10 pr-4 text-sm focus:ring-2 focus:ring-primary/20 w-64" placeholder="Search rituals..." type="text"/>
</div>
<div class="flex gap-4">
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer hover:text-primary transition-colors">notifications</span>
<span class="material-symbols-outlined text-on-surface-variant cursor-pointer hover:text-primary transition-colors">account_circle</span>
</div>
</div>
</header>
<!-- Keystone Habits Grid -->
<section class="space-y-8">
<!-- Highlight Cards: Identity Based -->
<div class="grid grid-cols-2 gap-6">
<div class="bg-gradient-to-br from-primary to-primary-dim p-8 rounded-[2rem] text-on-primary relative overflow-hidden group">
<div class="relative z-10">
<span class="bg-on-primary/20 text-on-primary text-[10px] font-bold uppercase tracking-widest px-3 py-1 rounded-full">Identity Anchor</span>
<h3 class="text-2xl font-bold mt-4 mb-2">"I am a focused creator."</h3>
<p class="text-on-primary/80 text-sm max-w-[240px]">Connected to Deep Work habit. You've honored this identity for 14 consecutive days.</p>
<div class="mt-8 flex items-center gap-4">
<div class="h-1.5 flex-1 bg-on-primary/20 rounded-full overflow-hidden">
<div class="h-full bg-on-primary w-[82%]"></div>
</div>
<span class="text-xs font-bold font-label">82% Mastery</span>
</div>
</div>
<span class="material-symbols-outlined absolute -bottom-4 -right-4 text-[120px] opacity-10 group-hover:scale-110 transition-transform duration-500">draw</span>
</div>
<div class="bg-surface-container-low p-8 rounded-[2rem] relative overflow-hidden">
<span class="bg-secondary-container text-on-secondary-container text-[10px] font-bold uppercase tracking-widest px-3 py-1 rounded-full font-label">Active Goal</span>
<h3 class="text-2xl font-bold mt-4 mb-2 text-on-surface">The Mindful Athlete</h3>
<p class="text-on-surface-variant text-sm max-w-[240px]">Integrated with Movement rituals. 4/5 weekly milestones achieved.</p>
<div class="mt-8 flex -space-x-2">
<div class="w-8 h-8 rounded-full border-2 border-surface bg-primary-container flex items-center justify-center text-[10px] font-bold">M</div>
<div class="w-8 h-8 rounded-full border-2 border-surface bg-primary-container flex items-center justify-center text-[10px] font-bold">T</div>
<div class="w-8 h-8 rounded-full border-2 border-surface bg-primary-container flex items-center justify-center text-[10px] font-bold">W</div>
<div class="w-8 h-8 rounded-full border-2 border-surface bg-primary-container flex items-center justify-center text-[10px] font-bold">T</div>
<div class="w-8 h-8 rounded-full border-2 border-surface bg-surface-container-highest flex items-center justify-center text-[10px] font-bold text-outline">F</div>
</div>
</div>
</div>
<!-- Detailed Habit Tracking -->
<div class="bg-surface-container-lowest p-8 rounded-[2.5rem] shadow-sm">
<div class="flex items-center justify-between mb-10">
<h4 class="text-xl font-bold tracking-tight">Keystone Habit Performance</h4>
<div class="flex gap-2">
<button class="px-4 py-1.5 rounded-full bg-surface-container-high text-xs font-bold font-label">Weekly</button>
<button class="px-4 py-1.5 rounded-full hover:bg-surface-container-high text-xs font-bold font-label transition-colors">Monthly</button>
</div>
</div>
<div class="space-y-12">
<!-- Habit Row 1 -->
<div class="grid grid-cols-[1fr_2fr_1fr] items-center gap-8">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-2xl bg-tertiary-container flex items-center justify-center text-tertiary">
<span class="material-symbols-outlined">self_improvement</span>
</div>
<div>
<p class="font-bold text-on-surface">Morning Stillness</p>
<p class="text-xs text-on-surface-variant font-label">15 mins • 07:00 AM</p>
</div>
</div>
<div class="flex justify-between items-end h-16 gap-2">
<div class="w-full bg-primary-container/30 rounded-t-lg h-[40%]"></div>
<div class="w-full bg-primary-container/30 rounded-t-lg h-[65%]"></div>
<div class="w-full bg-primary-container/30 rounded-t-lg h-[45%]"></div>
<div class="w-full bg-primary-container/30 rounded-t-lg h-[80%]"></div>
<div class="w-full bg-primary rounded-t-lg h-[95%]"></div>
<div class="w-full bg-primary/40 rounded-t-lg h-[60%]"></div>
<div class="w-full bg-surface-container-highest rounded-t-lg h-[20%]"></div>
</div>
<div class="text-right">
<p class="text-2xl font-extrabold text-on-surface">12 <span class="text-xs font-label text-on-surface-variant">Day Streak</span></p>
<p class="text-[10px] uppercase font-bold text-primary tracking-widest">+2 from avg</p>
</div>
</div>
<!-- Habit Row 2 -->
<div class="grid grid-cols-[1fr_2fr_1fr] items-center gap-8">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-2xl bg-secondary-container flex items-center justify-center text-secondary">
<span class="material-symbols-outlined">menu_book</span>
</div>
<div>
<p class="font-bold text-on-surface">Curated Reading</p>
<p class="text-xs text-on-surface-variant font-label">20 pages • Afternoon</p>
</div>
</div>
<div class="flex justify-between items-end h-16 gap-2">
<div class="w-full bg-secondary-container/30 rounded-t-lg h-[90%]"></div>
<div class="w-full bg-secondary-container/30 rounded-t-lg h-[85%]"></div>
<div class="w-full bg-secondary-container/30 rounded-t-lg h-[95%]"></div>
<div class="w-full bg-secondary rounded-t-lg h-[100%]"></div>
<div class="w-full bg-secondary/40 rounded-t-lg h-[70%]"></div>
<div class="w-full bg-secondary/20 rounded-t-lg h-[30%]"></div>
<div class="w-full bg-surface-container-highest rounded-t-lg h-[10%]"></div>
</div>
<div class="text-right">
<p class="text-2xl font-extrabold text-on-surface">31 <span class="text-xs font-label text-on-surface-variant">Day Streak</span></p>
<p class="text-[10px] uppercase font-bold text-secondary tracking-widest">Milestone: +3d</p>
</div>
</div>
<!-- Habit Row 3 -->
<div class="grid grid-cols-[1fr_2fr_1fr] items-center gap-8">
<div class="flex items-center gap-4">
<div class="w-12 h-12 rounded-2xl bg-primary-container flex items-center justify-center text-primary">
<span class="material-symbols-outlined">forest</span>
</div>
<div>
<p class="font-bold text-on-surface">Nature Exposure</p>
<p class="text-xs text-on-surface-variant font-label">10 mins • Sunlight</p>
</div>
</div>
<div class="flex justify-between items-end h-16 gap-2">
<div class="w-full bg-primary-container/30 rounded-t-lg h-[20%]"></div>
<div class="w-full bg-primary-container/30 rounded-t-lg h-[35%]"></div>
<div class="w-full bg-primary-container/30 rounded-t-lg h-[15%]"></div>
<div class="w-full bg-primary-container/30 rounded-t-lg h-[50%]"></div>
<div class="w-full bg-primary rounded-t-lg h-[40%]"></div>
<div class="w-full bg-primary/40 rounded-t-lg h-[55%]"></div>
<div class="w-full bg-surface-container-highest rounded-t-lg h-[15%]"></div>
</div>
<div class="text-right">
<p class="text-2xl font-extrabold text-on-surface">4 <span class="text-xs font-label text-on-surface-variant">Day Streak</span></p>
<p class="text-[10px] uppercase font-bold text-tertiary tracking-widest">In Focus</p>
</div>
</div>
</div>
</div>
</section>
</div>
<!-- Right Side Panel: Analysis & Affirmation -->
<aside class="w-[400px] bg-surface-container-low border-l border-outline-variant/10 p-10 flex flex-col gap-8">
<!-- Identity Affirmation -->
<div class="space-y-6">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-tertiary" style="font-variation-settings: 'FILL' 1;">auto_awesome</span>
<h5 class="text-sm font-bold uppercase tracking-wider font-label text-tertiary">Identity Affirmation</h5>
</div>
<div class="bg-surface-container-lowest p-6 rounded-3xl shadow-sm border border-outline-variant/5">
<p class="text-lg font-medium leading-relaxed italic text-on-surface">
"Your habits are how you embody your identity. Every action is a vote for the person you wish to become."
</p>
<div class="mt-4 pt-4 border-t border-surface-container-high flex items-center justify-between">
<span class="text-xs font-label text-on-surface-variant">Identity: The Conscious Architect</span>
<span class="material-symbols-outlined text-sm text-outline">more_horiz</span>
</div>
</div>
</div>
<!-- Burnout Analysis -->
<div class="space-y-6">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-error" style="font-variation-settings: 'FILL' 1;">monitoring</span>
<h5 class="text-sm font-bold uppercase tracking-wider font-label text-error">Burnout Analysis</h5>
</div>
<div class="space-y-4">
<div class="bg-surface-container-lowest p-6 rounded-3xl">
<div class="flex items-center justify-between mb-4">
<span class="text-sm font-bold">Cognitive Load</span>
<span class="text-xs font-label bg-error-container text-on-error-container px-2 py-0.5 rounded-full">Elevated</span>
</div>
<div class="h-2 w-full bg-surface-container-high rounded-full overflow-hidden">
<div class="h-full bg-error w-[74%]"></div>
</div>
<p class="mt-4 text-xs text-on-surface-variant leading-relaxed">
Research suggests your late-night sessions are impacting habit consistency by <span class="text-error font-bold">14%</span>.
</p>
</div>
<div class="bg-tertiary-container/30 p-6 rounded-3xl border border-tertiary/10">
<h6 class="text-sm font-bold text-tertiary mb-2">Restorative Suggestions</h6>
<ul class="space-y-3">
<li class="flex gap-3 text-xs text-on-tertiary-container items-start">
<span class="material-symbols-outlined text-sm mt-0.5">check_circle</span>
<span>Shift 'Curated Reading' to 30 mins before bed to lower cortisol.</span>
</li>
<li class="flex gap-3 text-xs text-on-tertiary-container items-start">
<span class="material-symbols-outlined text-sm mt-0.5">check_circle</span>
<span>Add 5-minute Non-Sleep Deep Rest (NSDR) at 2 PM.</span>
</li>
</ul>
</div>
</div>
</div>
<!-- Visual Context -->
<div class="mt-auto rounded-3xl overflow-hidden h-48 relative">
<img alt="Quiet desk with notebook and plant" class="w-full h-full object-cover" data-alt="A clean minimalist workspace with a notebook, a small green plant, and soft morning sunlight casting shadows" src="https://lh3.googleusercontent.com/aida-public/AB6AXuCMV31Y42a6hLY9oVtwic2jxzWmx2tD4OdAMuWR9ykSRKDT3CMeUZuz-4ur9C3BAENKHV1HcybZ4Iq6L7fqgL9Tn_MAXY5ClZ0Lo0HHX9j0fkrgT960_ZtBzD2fAC5KrVpk8QvnP3uOd4Nh46A6i3Y4brfel3ijzrdC5BxOODdE0B-YIXoBFLjOUiYqxac7oryruCkAPdkjRHd7FMfN9j-o6NQBgSWc-zNe-eUyQ4JIj6c9JhHRczs9ZjB30OlYKMUwwBzjDu8SfVeA"/>
<div class="absolute inset-0 bg-gradient-to-t from-black/40 to-transparent flex items-end p-6">
<p class="text-white text-xs font-medium">Keep your environment as intentional as your actions.</p>
</div>
</div>
</aside>
</main>
</body></html>

303
assets/ui/web/sprint.html Normal file
View File

@ -0,0 +1,303 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Digital Sanctuary | Sprint Planner</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700;800&amp;family=Plus_Jakarta_Sans:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"surface-tint": "#4a655a",
"error-dim": "#67040d",
"surface": "#f8faf9",
"secondary-fixed-dim": "#c2d6f5",
"error": "#a83836",
"surface-container-high": "#e4e9e8",
"primary-fixed": "#cbe9db",
"secondary-dim": "#42546f",
"on-error-container": "#6e0a12",
"on-tertiary-container": "#5c5367",
"on-background": "#2d3433",
"on-surface-variant": "#596060",
"tertiary": "#655b6f",
"on-primary-fixed-variant": "#466156",
"primary-dim": "#3e594e",
"on-primary-fixed": "#2a443b",
"error-container": "#fa746f",
"surface-container": "#eaefee",
"outline-variant": "#acb3b2",
"tertiary-dim": "#594f63",
"primary-fixed-dim": "#bedbce",
"on-surface": "#2d3433",
"on-primary": "#e5fff2",
"on-secondary-fixed-variant": "#4a5d77",
"tertiary-fixed": "#f4e6ff",
"tertiary-container": "#f4e6ff",
"inverse-primary": "#d7f5e7",
"on-secondary": "#f8f8ff",
"surface-container-low": "#f1f4f3",
"surface-bright": "#f8faf9",
"on-primary-container": "#3d574d",
"background": "#f8faf9",
"surface-dim": "#d4dbda",
"tertiary-fixed-dim": "#e5d8f0",
"on-tertiary": "#fef6ff",
"inverse-surface": "#0b0f0f",
"primary": "#4a655a",
"primary-container": "#cbe9db",
"on-tertiary-fixed-variant": "#675d71",
"on-secondary-container": "#40536d",
"surface-container-highest": "#dde4e3",
"secondary": "#4e607b",
"inverse-on-surface": "#9b9d9d",
"surface-variant": "#dde4e3",
"outline": "#757c7b",
"surface-container-lowest": "#ffffff",
"on-tertiary-fixed": "#4a4154",
"on-error": "#fff7f6",
"on-secondary-fixed": "#2e405a",
"secondary-fixed": "#d3e3ff",
"secondary-container": "#d3e3ff"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"fontFamily": {
"headline": ["Manrope"],
"body": ["Manrope"],
"label": ["Plus Jakarta Sans"]
}
},
},
}
</script>
<style>
body { font-family: 'Manrope', sans-serif; }
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.tonal-shift { background-color: #f1f4f3; }
.glass-panel {
background: rgba(248, 250, 249, 0.8);
backdrop-filter: blur(20px);
}
.active-nav-border { border-right: 4px solid #4a655a; }
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #dde4e3; border-radius: 10px; }
</style>
</head>
<body class="bg-surface text-on-background antialiased overflow-hidden">
<!-- SideNavBar (Left Column) -->
<aside class="h-screen w-64 fixed left-0 top-0 border-r-0 tonal-shift flex flex-col py-8 px-6 z-50">
<div class="mb-12">
<h1 class="text-xl font-bold tracking-tight text-primary">Digital Sanctuary</h1>
<p class="text-xs font-label text-secondary opacity-70 tracking-wider uppercase mt-1">Stay Focused</p>
</div>
<nav class="flex-1 space-y-2">
<!-- Deep Work (Active) -->
<a class="flex items-center gap-4 px-4 py-3 rounded-lg text-primary font-bold active-nav-border bg-surface-container-high transition-all duration-300 scale-[0.98x]" href="#">
<span class="material-symbols-outlined" data-icon="timer">timer</span>
<span class="font-medium text-sm">Deep Work</span>
</a>
<!-- Task Batching -->
<a class="flex items-center gap-4 px-4 py-3 rounded-lg text-secondary opacity-70 hover:bg-surface-container-high hover:opacity-100 transition-colors duration-300" href="#">
<span class="material-symbols-outlined" data-icon="layers">layers</span>
<span class="font-medium text-sm">Task Batching</span>
</a>
<!-- Productivity Stats -->
<a class="flex items-center gap-4 px-4 py-3 rounded-lg text-secondary opacity-70 hover:bg-surface-container-high hover:opacity-100 transition-colors duration-300" href="#">
<span class="material-symbols-outlined" data-icon="insights">insights</span>
<span class="font-medium text-sm">Productivity Stats</span>
</a>
<!-- Settings -->
<a class="flex items-center gap-4 px-4 py-3 rounded-lg text-secondary opacity-70 hover:bg-surface-container-high hover:opacity-100 transition-colors duration-300" href="#">
<span class="material-symbols-outlined" data-icon="settings">settings</span>
<span class="font-medium text-sm">Settings</span>
</a>
</nav>
<div class="mt-auto pt-8">
<button class="w-full py-4 px-6 rounded-full bg-gradient-to-br from-primary to-primary-container text-on-primary font-bold shadow-lg shadow-primary/10 hover:opacity-90 transition-opacity">
Start Sprint
</button>
<div class="mt-8 flex items-center gap-3 px-2">
<img alt="User profile" class="w-10 h-10 rounded-full object-cover grayscale opacity-80" data-alt="Close-up portrait of a calm professional man in a minimalist office setting, soft natural lighting" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAMRs-teYoIX1gsBikcDvjkoxnFng7xCzSQY88Mz1RqZU1ahQ14UyQ7r7_ahzmMEibEuy97mNP3M5KuPnHwU0pQVK-VFlhisOndALt0GJ-UL5_u_s_RpRWlGjPSrf-TW7V7KDNjqRuSrcYuitAnTb8k3zCvMmQE2-FVRNtRu3qhSf7kD7uA6zDyIwExLVXAqnSiwMteoIW8qNtV0UqLjW2uAo-jab1jcF5fXamGLR6WbQlRAINGn-m5VUVTZBku68aBGoq02Ax30TOW"/>
<div>
<p class="text-sm font-bold text-primary">Julian V.</p>
<p class="text-[10px] font-label text-secondary uppercase tracking-widest">Focus Mode On</p>
</div>
</div>
</div>
</aside>
<!-- Main Content Cluster -->
<main class="ml-64 flex min-h-screen">
<!-- Center Column (Timer & Tasks) -->
<section class="flex-1 px-12 py-10 overflow-y-auto max-h-screen">
<!-- TopAppBar Internal -->
<header class="flex justify-between items-center mb-16 h-16 sticky top-0 bg-surface/80 backdrop-blur-md z-40">
<div class="flex items-center gap-3">
<span class="material-symbols-outlined text-primary" data-icon="bolt">bolt</span>
<h2 class="text-lg font-semibold text-primary">Sprint Planner</h2>
</div>
<div class="flex items-center gap-6">
<div class="relative">
<span class="material-symbols-outlined absolute left-3 top-1/2 -translate-y-1/2 text-outline text-sm" data-icon="search">search</span>
<input class="pl-10 pr-4 py-2 bg-surface-container-low rounded-full border-none focus:ring-1 focus:ring-primary/20 text-sm w-64" placeholder="Search focus logs..." type="text"/>
</div>
<div class="flex items-center gap-4 text-secondary">
<span class="material-symbols-outlined cursor-pointer hover:text-primary transition-colors" data-icon="notifications">notifications</span>
<span class="material-symbols-outlined cursor-pointer hover:text-primary transition-colors" data-icon="account_circle">account_circle</span>
</div>
</div>
</header>
<!-- Large Deep Work Timer -->
<div class="flex flex-col items-center justify-center mb-20 text-center">
<div class="relative w-80 h-80 flex items-center justify-center rounded-full border-[12px] border-surface-container-highest">
<!-- Progress Circle Overlay -->
<svg class="absolute inset-0 w-full h-full -rotate-90">
<circle cx="160" cy="160" fill="none" r="148" stroke="url(#timerGradient)" stroke-dasharray="929" stroke-dashoffset="230" stroke-linecap="round" stroke-width="12"></circle>
<defs>
<lineargradient id="timerGradient" x1="0%" x2="100%" y1="0%" y2="100%">
<stop offset="0%" stop-color="#4a655a"></stop>
<stop offset="100%" stop-color="#bedbce"></stop>
</lineargradient>
</defs>
</svg>
<div class="flex flex-col">
<span class="text-7xl font-extrabold tracking-tighter text-on-background">24:59</span>
<span class="text-xs font-label text-secondary uppercase tracking-[0.2em] mt-2">Deep Focus</span>
</div>
</div>
<div class="mt-10 flex items-center gap-6">
<button class="p-4 rounded-full bg-surface-container-low text-secondary hover:bg-surface-container-high transition-colors">
<span class="material-symbols-outlined" data-icon="replay">replay</span>
</button>
<button class="w-20 h-20 rounded-full bg-primary flex items-center justify-center text-white shadow-xl shadow-primary/20 hover:scale-105 transition-transform">
<span class="material-symbols-outlined text-4xl" data-icon="play_arrow" data-weight="fill" style="font-variation-settings: 'FILL' 1;">play_arrow</span>
</button>
<button class="p-4 rounded-full bg-surface-container-low text-secondary hover:bg-surface-container-high transition-colors">
<span class="material-symbols-outlined" data-icon="skip_next">skip_next</span>
</button>
</div>
</div>
<!-- Sprint Tasks Section -->
<div class="max-w-3xl mx-auto">
<div class="flex items-center justify-between mb-8">
<h3 class="text-2xl font-bold tracking-tight text-on-background">Sprint Tasks</h3>
<button class="text-sm font-label font-semibold text-primary flex items-center gap-1">
<span class="material-symbols-outlined text-sm" data-icon="add">add</span>
Batch Task
</button>
</div>
<div class="space-y-4">
<!-- Task Item -->
<div class="group flex items-center gap-4 p-5 rounded-xl bg-surface-container-lowest transition-all hover:bg-surface-container-low border border-transparent hover:border-primary/5">
<div class="w-6 h-6 rounded-md border-2 border-primary/20 flex items-center justify-center cursor-pointer group-hover:border-primary">
<span class="material-symbols-outlined text-primary text-lg hidden group-hover:block" data-icon="check">check</span>
</div>
<div class="flex-1">
<p class="font-medium text-on-background">Audit design system color tokens</p>
<div class="flex gap-4 mt-1">
<span class="text-[10px] font-label text-secondary bg-surface-container-high px-2 py-0.5 rounded uppercase tracking-wider">High Energy</span>
<span class="text-[10px] font-label text-outline uppercase tracking-wider">15 mins</span>
</div>
</div>
<span class="material-symbols-outlined text-outline opacity-0 group-hover:opacity-100 cursor-grab" data-icon="drag_indicator">drag_indicator</span>
</div>
<div class="group flex items-center gap-4 p-5 rounded-xl bg-surface-container-lowest transition-all hover:bg-surface-container-low">
<div class="w-6 h-6 rounded-md border-2 border-primary/20 flex items-center justify-center cursor-pointer"></div>
<div class="flex-1">
<p class="font-medium text-on-background">Review feedback for navigation shell</p>
<div class="flex gap-4 mt-1">
<span class="text-[10px] font-label text-secondary bg-surface-container-high px-2 py-0.5 rounded uppercase tracking-wider">Batch: Email</span>
<span class="text-[10px] font-label text-outline uppercase tracking-wider">25 mins</span>
</div>
</div>
<span class="material-symbols-outlined text-outline opacity-0 group-hover:opacity-100 cursor-grab" data-icon="drag_indicator">drag_indicator</span>
</div>
<div class="group flex items-center gap-4 p-5 rounded-xl bg-surface-container-lowest transition-all hover:bg-surface-container-low opacity-60">
<div class="w-6 h-6 rounded-md border-2 border-primary bg-primary flex items-center justify-center">
<span class="material-symbols-outlined text-on-primary text-lg" data-icon="check" data-weight="fill" style="font-variation-settings: 'FILL' 1;">check</span>
</div>
<div class="flex-1 line-through">
<p class="font-medium text-on-background">Update component JSON schema</p>
<p class="text-[10px] font-label text-outline uppercase tracking-wider mt-1">Completed at 09:45 AM</p>
</div>
</div>
</div>
</div>
</section>
<!-- Right Column (Insights & Strategy) -->
<aside class="w-80 h-screen overflow-y-auto px-8 py-10 tonal-shift">
<!-- Daily Progress -->
<div class="mb-12">
<h4 class="text-sm font-label font-bold text-secondary uppercase tracking-[0.15em] mb-6">Daily Progress</h4>
<div class="p-6 rounded-2xl bg-surface-container-lowest">
<div class="flex justify-between items-end mb-4">
<p class="text-3xl font-bold text-on-background">2/4</p>
<p class="text-xs font-label text-secondary">Sprints Finished</p>
</div>
<div class="w-full h-3 bg-surface-container-highest rounded-full overflow-hidden">
<div class="h-full w-1/2 bg-gradient-to-r from-primary to-primary-fixed-dim rounded-full"></div>
</div>
<p class="text-[11px] text-outline mt-4 leading-relaxed italic">"Slow progress is still progress. Stay breathing."</p>
</div>
</div>
<!-- Deep Work Insights -->
<div class="mb-12">
<h4 class="text-sm font-label font-bold text-secondary uppercase tracking-[0.15em] mb-6">Deep Work Insights</h4>
<div class="space-y-4">
<div class="p-5 rounded-2xl bg-surface-container-lowest group hover:scale-[1.02] transition-transform cursor-default">
<div class="flex items-center gap-3 mb-2">
<span class="material-symbols-outlined text-tertiary" data-icon="flare">flare</span>
<p class="text-xs font-bold text-on-background">Peak Focus Time</p>
</div>
<p class="text-lg font-bold text-primary">09:00 — 11:30</p>
<p class="text-[10px] text-secondary mt-1">Based on last 7 days activity</p>
</div>
<div class="p-5 rounded-2xl bg-surface-container-lowest group hover:scale-[1.02] transition-transform cursor-default">
<div class="flex items-center gap-3 mb-2">
<span class="material-symbols-outlined text-secondary" data-icon="speed">speed</span>
<p class="text-xs font-bold text-on-background">Avg. Task Completion</p>
</div>
<p class="text-lg font-bold text-primary">22 mins</p>
<p class="text-[10px] text-secondary mt-1">8% faster than previous sprint</p>
</div>
</div>
</div>
<!-- Focus Strategy Widget -->
<div>
<h4 class="text-sm font-label font-bold text-secondary uppercase tracking-[0.15em] mb-6">Focus Strategy</h4>
<div class="relative rounded-2xl overflow-hidden aspect-[4/5] p-6 flex flex-col justify-end">
<!-- Abstract Background Image -->
<div class="absolute inset-0 z-0">
<div class="absolute inset-0 bg-gradient-to-t from-primary/90 to-transparent z-10"></div>
<img alt="Atmospheric plant silhouette" class="w-full h-full object-cover" data-alt="Abstract silhouette of a minimalist plant against a soft morning fog with ethereal lighting and organic textures" src="https://lh3.googleusercontent.com/aida-public/AB6AXuAr83QAjEcihyZZ1-FWloPAjOdORsmWV-VeuuXmgx3nrdUoNp49dmhbdQxpaQklhxfa71hm2OrmIS90CIqtoUb-q9tCGJL5aobe44rCbBzOp5k3b2VMqg6jNLiTumi0d2_S7As27BVcp45_ESzb-LDDxcqT38K2K88UKGFWrfOHugOem8uSui4CiyatZ0YfIKBI1Qo0wnn0qIp4GPqYw-tYmXa_EXGMRV3BlLh8kdnvRnFKqYJBJxH0Icg7hJQtbTWR6CTaeb2oz2A6"/>
</div>
<div class="relative z-20">
<h5 class="text-on-primary font-bold text-lg leading-tight mb-2">Monastic Mode</h5>
<p class="text-on-primary/80 text-xs leading-relaxed mb-4">Minimize all external sensory input. No notifications. Pure output.</p>
<button class="text-[11px] font-label font-extrabold text-on-primary uppercase tracking-widest border border-on-primary/30 rounded-full px-4 py-2 hover:bg-on-primary hover:text-primary transition-colors">
Apply Tactic
</button>
</div>
</div>
</div>
</aside>
</main>
<!-- Contextual FAB (Only on Home/Dashboard, not settings/details) -->
<button class="fixed bottom-10 right-10 w-16 h-16 rounded-full bg-primary-container text-on-primary-container shadow-2xl flex items-center justify-center hover:scale-110 transition-transform active:scale-95 group z-50">
<span class="material-symbols-outlined text-3xl" data-icon="add">add</span>
<span class="absolute right-20 bg-primary text-on-primary px-4 py-2 rounded-xl text-sm font-bold opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap">Quick Task</span>
</button>
</body></html>

6
babel.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
};
};

View File

@ -0,0 +1,25 @@
import { Href, Link } from 'expo-router';
import { openBrowserAsync, WebBrowserPresentationStyle } from 'expo-web-browser';
import { type ComponentProps } from 'react';
type Props = Omit<ComponentProps<typeof Link>, 'href'> & { href: Href & string };
export function ExternalLink({ href, ...rest }: Props) {
return (
<Link
target="_blank"
{...rest}
href={href}
onPress={async (event) => {
if (process.env.EXPO_OS !== 'web') {
// Prevent the default behavior of linking to the default browser on native.
event.preventDefault();
// Open the link in an in-app browser.
await openBrowserAsync(href, {
presentationStyle: WebBrowserPresentationStyle.AUTOMATIC,
});
}
}}
/>
);
}

18
components/haptic-tab.tsx Normal file
View File

@ -0,0 +1,18 @@
import { BottomTabBarButtonProps } from '@react-navigation/bottom-tabs';
import { PlatformPressable } from '@react-navigation/elements';
import * as Haptics from 'expo-haptics';
export function HapticTab(props: BottomTabBarButtonProps) {
return (
<PlatformPressable
{...props}
onPressIn={(ev) => {
if (process.env.EXPO_OS === 'ios') {
// Add a soft haptic feedback when pressing down on the tabs.
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}
props.onPressIn?.(ev);
}}
/>
);
}

19
components/hello-wave.tsx Normal file
View File

@ -0,0 +1,19 @@
import Animated from 'react-native-reanimated';
export function HelloWave() {
return (
<Animated.Text
style={{
fontSize: 28,
lineHeight: 32,
marginTop: -6,
animationName: {
'50%': { transform: [{ rotate: '25deg' }] },
},
animationIterationCount: 4,
animationDuration: '300ms',
}}>
👋
</Animated.Text>
);
}

View File

@ -0,0 +1,80 @@
import type { PropsWithChildren, ReactElement } from 'react';
import { StyleSheet } from 'react-native';
import Animated, {
interpolate,
useAnimatedRef,
useAnimatedStyle,
useScrollOffset,
} from 'react-native-reanimated';
import { ThemedView } from '@/components/themed-view';
import { useColorScheme } from '@/hooks/use-color-scheme';
import { useThemeColor } from '@/hooks/use-theme-color';
const HEADER_HEIGHT = 250;
type Props = PropsWithChildren<{
headerImage: ReactElement;
headerBackgroundColor: { dark: string; light: string };
}>;
export default function ParallaxScrollView({
children,
headerImage,
headerBackgroundColor,
}: Props) {
const backgroundColor = useThemeColor({}, 'background');
const colorScheme = useColorScheme() ?? 'light';
const scrollRef = useAnimatedRef<Animated.ScrollView>();
const scrollOffset = useScrollOffset(scrollRef);
const headerAnimatedStyle = useAnimatedStyle(() => {
'worklet';
return {
transform: [
{
translateY: interpolate(
scrollOffset.value,
[-HEADER_HEIGHT, 0, HEADER_HEIGHT],
[-HEADER_HEIGHT / 2, 0, HEADER_HEIGHT * 0.75]
),
},
{
scale: interpolate(scrollOffset.value, [-HEADER_HEIGHT, 0, HEADER_HEIGHT], [2, 1, 1]),
},
],
};
});
return (
<Animated.ScrollView
ref={scrollRef}
style={{ backgroundColor, flex: 1 }}
scrollEventThrottle={16}>
<Animated.View
style={[
styles.header,
{ backgroundColor: headerBackgroundColor[colorScheme] },
headerAnimatedStyle,
]}>
{headerImage}
</Animated.View>
<ThemedView style={styles.content}>{children}</ThemedView>
</Animated.ScrollView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
header: {
height: HEADER_HEIGHT,
overflow: 'hidden',
},
content: {
flex: 1,
padding: 32,
gap: 16,
overflow: 'hidden',
},
});

View File

@ -0,0 +1,60 @@
import { StyleSheet, Text, type TextProps } from 'react-native';
import { useThemeColor } from '@/hooks/use-theme-color';
export type ThemedTextProps = TextProps & {
lightColor?: string;
darkColor?: string;
type?: 'default' | 'title' | 'defaultSemiBold' | 'subtitle' | 'link';
};
export function ThemedText({
style,
lightColor,
darkColor,
type = 'default',
...rest
}: ThemedTextProps) {
const color = useThemeColor({ light: lightColor, dark: darkColor }, 'text');
return (
<Text
style={[
{ color },
type === 'default' ? styles.default : undefined,
type === 'title' ? styles.title : undefined,
type === 'defaultSemiBold' ? styles.defaultSemiBold : undefined,
type === 'subtitle' ? styles.subtitle : undefined,
type === 'link' ? styles.link : undefined,
style,
]}
{...rest}
/>
);
}
const styles = StyleSheet.create({
default: {
fontSize: 16,
lineHeight: 24,
},
defaultSemiBold: {
fontSize: 16,
lineHeight: 24,
fontWeight: '600',
},
title: {
fontSize: 32,
fontWeight: 'bold',
lineHeight: 32,
},
subtitle: {
fontSize: 20,
fontWeight: 'bold',
},
link: {
lineHeight: 30,
fontSize: 16,
color: '#0a7ea4',
},
});

View File

@ -0,0 +1,14 @@
import { View, type ViewProps } from 'react-native';
import { useThemeColor } from '@/hooks/use-theme-color';
export type ThemedViewProps = ViewProps & {
lightColor?: string;
darkColor?: string;
};
export function ThemedView({ style, lightColor, darkColor, ...otherProps }: ThemedViewProps) {
const backgroundColor = useThemeColor({ light: lightColor, dark: darkColor }, 'background');
return <View style={[{ backgroundColor }, style]} {...otherProps} />;
}

View File

@ -0,0 +1,45 @@
import { PropsWithChildren, useState } from 'react';
import { StyleSheet, TouchableOpacity } from 'react-native';
import { ThemedText } from '@/components/themed-text';
import { ThemedView } from '@/components/themed-view';
import { IconSymbol } from '@/components/ui/icon-symbol';
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
export function Collapsible({ children, title }: PropsWithChildren & { title: string }) {
const [isOpen, setIsOpen] = useState(false);
const theme = useColorScheme() ?? 'light';
return (
<ThemedView>
<TouchableOpacity
style={styles.heading}
onPress={() => setIsOpen((value) => !value)}
activeOpacity={0.8}>
<IconSymbol
name="chevron.right"
size={18}
weight="medium"
color={theme === 'light' ? Colors.light.icon : Colors.dark.icon}
style={{ transform: [{ rotate: isOpen ? '90deg' : '0deg' }] }}
/>
<ThemedText type="defaultSemiBold">{title}</ThemedText>
</TouchableOpacity>
{isOpen && <ThemedView style={styles.content}>{children}</ThemedView>}
</ThemedView>
);
}
const styles = StyleSheet.create({
heading: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
},
content: {
marginTop: 6,
marginLeft: 24,
},
});

View File

@ -0,0 +1,32 @@
import { SymbolView, SymbolViewProps, SymbolWeight } from 'expo-symbols';
import { StyleProp, ViewStyle } from 'react-native';
export function IconSymbol({
name,
size = 24,
color,
style,
weight = 'regular',
}: {
name: SymbolViewProps['name'];
size?: number;
color: string;
style?: StyleProp<ViewStyle>;
weight?: SymbolWeight;
}) {
return (
<SymbolView
weight={weight}
tintColor={color}
resizeMode="scaleAspectFit"
name={name}
style={[
{
width: size,
height: size,
},
style,
]}
/>
);
}

View File

@ -0,0 +1,41 @@
// Fallback for using MaterialIcons on Android and web.
import MaterialIcons from '@expo/vector-icons/MaterialIcons';
import { SymbolWeight, SymbolViewProps } from 'expo-symbols';
import { ComponentProps } from 'react';
import { OpaqueColorValue, type StyleProp, type TextStyle } from 'react-native';
type IconMapping = Record<SymbolViewProps['name'], ComponentProps<typeof MaterialIcons>['name']>;
type IconSymbolName = keyof typeof MAPPING;
/**
* Add your SF Symbols to Material Icons mappings here.
* - see Material Icons in the [Icons Directory](https://icons.expo.fyi).
* - see SF Symbols in the [SF Symbols](https://developer.apple.com/sf-symbols/) app.
*/
const MAPPING = {
'house.fill': 'home',
'paperplane.fill': 'send',
'chevron.left.forwardslash.chevron.right': 'code',
'chevron.right': 'chevron-right',
} as IconMapping;
/**
* An icon component that uses native SF Symbols on iOS, and Material Icons on Android and web.
* This ensures a consistent look across platforms, and optimal resource usage.
* Icon `name`s are based on SF Symbols and require manual mapping to Material Icons.
*/
export function IconSymbol({
name,
size = 24,
color,
style,
}: {
name: IconSymbolName;
size?: number;
color: string | OpaqueColorValue;
style?: StyleProp<TextStyle>;
weight?: SymbolWeight;
}) {
return <MaterialIcons color={color} size={size} name={MAPPING[name]} style={style} />;
}

53
constants/theme.ts Normal file
View File

@ -0,0 +1,53 @@
/**
* Below are the colors that are used in the app. The colors are defined in the light and dark mode.
* There are many other ways to style your app. For example, [Nativewind](https://www.nativewind.dev/), [Tamagui](https://tamagui.dev/), [unistyles](https://reactnativeunistyles.vercel.app), etc.
*/
import { Platform } from 'react-native';
const tintColorLight = '#0a7ea4';
const tintColorDark = '#fff';
export const Colors = {
light: {
text: '#11181C',
background: '#fff',
tint: tintColorLight,
icon: '#687076',
tabIconDefault: '#687076',
tabIconSelected: tintColorLight,
},
dark: {
text: '#ECEDEE',
background: '#151718',
tint: tintColorDark,
icon: '#9BA1A6',
tabIconDefault: '#9BA1A6',
tabIconSelected: tintColorDark,
},
};
export const Fonts = Platform.select({
ios: {
/** iOS `UIFontDescriptorSystemDesignDefault` */
sans: 'system-ui',
/** iOS `UIFontDescriptorSystemDesignSerif` */
serif: 'ui-serif',
/** iOS `UIFontDescriptorSystemDesignRounded` */
rounded: 'ui-rounded',
/** iOS `UIFontDescriptorSystemDesignMonospaced` */
mono: 'ui-monospace',
},
default: {
sans: 'normal',
serif: 'serif',
rounded: 'normal',
mono: 'monospace',
},
web: {
sans: "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif",
serif: "Georgia, 'Times New Roman', serif",
rounded: "'SF Pro Rounded', 'Hiragino Maru Gothic ProN', Meiryo, 'MS PGothic', sans-serif",
mono: "SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
},
});

24
eas.json Normal file
View File

@ -0,0 +1,24 @@
{
"cli": {
"version": ">= 18.12.2",
"appVersionSource": "remote"
},
"build": {
"development": {
"developmentClient": true,
"distribution": "internal"
},
"preview": {
"distribution": "internal",
"android": {
"buildType": "apk"
}
},
"production": {
"autoIncrement": true
}
},
"submit": {
"production": {}
}
}

10
eslint.config.js Normal file
View File

@ -0,0 +1,10 @@
// https://docs.expo.dev/guides/using-eslint/
const { defineConfig } = require('eslint/config');
const expoConfig = require('eslint-config-expo/flat');
module.exports = defineConfig([
expoConfig,
{
ignores: ['dist/*'],
},
]);

View File

@ -0,0 +1 @@
export { useColorScheme } from 'react-native';

View File

@ -0,0 +1,21 @@
import { useEffect, useState } from 'react';
import { useColorScheme as useRNColorScheme } from 'react-native';
/**
* To support static rendering, this value needs to be re-calculated on the client side for web
*/
export function useColorScheme() {
const [hasHydrated, setHasHydrated] = useState(false);
useEffect(() => {
setHasHydrated(true);
}, []);
const colorScheme = useRNColorScheme();
if (hasHydrated) {
return colorScheme;
}
return 'light';
}

21
hooks/use-theme-color.ts Normal file
View File

@ -0,0 +1,21 @@
/**
* Learn more about light and dark modes:
* https://docs.expo.dev/guides/color-schemes/
*/
import { Colors } from '@/constants/theme';
import { useColorScheme } from '@/hooks/use-color-scheme';
export function useThemeColor(
props: { light?: string; dark?: string },
colorName: keyof typeof Colors.light & keyof typeof Colors.dark
) {
const theme = useColorScheme() ?? 'light';
const colorFromProps = props[theme];
if (colorFromProps) {
return colorFromProps;
} else {
return Colors[theme][colorName];
}
}

21
metro.config.js Normal file
View File

@ -0,0 +1,21 @@
// Learn more https://docs.expo.io/guides/customizing-metro
const { getDefaultConfig } = require("expo/metro-config");
const path = require("path");
/** @type {import('expo/metro-config').MetroConfig} */
const config = getDefaultConfig(__dirname);
const { transformer, resolver } = config;
config.transformer = {
...transformer,
babelTransformerPath: require.resolve("react-native-svg-transformer"),
};
config.resolver = {
...resolver,
assetExts: [...resolver.assetExts.filter((ext) => ext !== "svg"), "wasm"],
sourceExts: [...resolver.sourceExts, "svg", "wasm"],
};
module.exports = config;

14482
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

78
package.json Normal file
View File

@ -0,0 +1,78 @@
{
"name": "rcs-batsirai",
"main": "expo-router/entry",
"version": "1.0.0",
"scripts": {
"start": "expo start",
"reset-project": "node ./scripts/reset-project.js",
"android": "expo run:android",
"ios": "expo run:ios",
"web": "expo start --web",
"lint": "expo lint",
"postinstall": "patch-package"
},
"dependencies": {
"@bildau/rn-pdf-reader": "^4.2.7",
"@expo-google-fonts/inter": "^0.4.2",
"@expo-google-fonts/manrope": "^0.4.2",
"@expo-google-fonts/newsreader": "^0.4.1",
"@expo-google-fonts/plus-jakarta-sans": "^0.4.2",
"@expo-google-fonts/space-grotesk": "^0.4.1",
"@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/netinfo": "11.4.1",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"@supabase/supabase-js": "^2.103.1",
"base-64": "^1.0.0",
"deprecated-react-native-prop-types": "^5.0.0",
"expo": "~54.0.34",
"expo-asset": "~12.0.13",
"expo-blur": "~15.0.8",
"expo-constants": "~18.0.13",
"expo-document-picker": "~14.0.8",
"expo-file-system": "~19.0.21",
"expo-font": "~14.0.11",
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-linking": "~8.0.12",
"expo-modules-core": "~3.0.30",
"expo-router": "~6.0.23",
"expo-splash-screen": "~31.0.13",
"expo-sqlite": "~16.0.10",
"expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8",
"expo-system-ui": "~6.0.9",
"expo-web-browser": "~15.0.11",
"lucide-react": "^1.16.0",
"lucide-react-native": "^1.16.0",
"or": "^0.2.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-blob-util": "^0.24.7",
"react-native-drax": "^1.1.0",
"react-native-gesture-handler": "~2.28.0",
"react-native-pdf": "^7.0.4",
"react-native-pdf-renderer": "^2.3.0",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
"react-native-web": "~0.21.0",
"react-native-webview": "13.15.0",
"react-native-worklets": "0.5.1"
},
"devDependencies": {
"@types/react": "~19.1.0",
"babel-preset-expo": "^55.0.22",
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",
"patch-package": "^8.0.1",
"postinstall-postinstall": "^2.1.0",
"react-native-svg-transformer": "^1.5.3",
"typescript": "~5.9.2"
},
"private": true
}

View File

@ -0,0 +1,58 @@
{
"name": "rcs-batsirai",
"main": "expo-router/entry",
"version": "1.0.0",
"scripts": {
"start": "expo start",
"reset-project": "node ./scripts/reset-project.js",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web",
"lint": "expo lint"
},
"dependencies": {
"@expo-google-fonts/inter": "^0.4.2",
"@expo-google-fonts/manrope": "^0.4.2",
"@expo-google-fonts/newsreader": "^0.4.1",
"@expo-google-fonts/plus-jakarta-sans": "^0.4.2",
"@expo-google-fonts/space-grotesk": "^0.4.1",
"@expo/vector-icons": "^15.0.3",
"@react-native-async-storage/async-storage": "^3.0.2",
"@react-native-community/netinfo": "^12.0.1",
"@react-navigation/bottom-tabs": "^7.4.0",
"@react-navigation/elements": "^2.6.3",
"@react-navigation/native": "^7.1.8",
"@supabase/supabase-js": "^2.103.1",
"expo": "~54.0.33",
"expo-constants": "~18.0.13",
"expo-font": "~14.0.11",
"expo-haptics": "~15.0.8",
"expo-image": "~3.0.11",
"expo-linking": "~8.0.11",
"expo-router": "~6.0.23",
"expo-splash-screen": "~31.0.13",
"expo-sqlite": "^55.0.15",
"expo-status-bar": "~3.0.9",
"expo-symbols": "~1.0.8",
"expo-system-ui": "~6.0.9",
"expo-web-browser": "~15.0.10",
"lucide-react-native": "^0.473.0",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-gesture-handler": "~2.28.0",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-svg": "15.12.1",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1"
},
"devDependencies": {
"@types/react": "~19.1.0",
"eslint": "^9.25.0",
"eslint-config-expo": "~10.0.0",
"typescript": "~5.9.2"
},
"private": true
}

View File

@ -0,0 +1,186 @@
diff --git a/node_modules/@bildau/rn-pdf-reader/lib/index.js b/node_modules/@bildau/rn-pdf-reader/lib/index.js
index 2483bd2..c2c49a5 100644
--- a/node_modules/@bildau/rn-pdf-reader/lib/index.js
+++ b/node_modules/@bildau/rn-pdf-reader/lib/index.js
@@ -1,7 +1,7 @@
import * as React from 'react';
import { View, ActivityIndicator, Platform, StyleSheet } from 'react-native';
import { WebView } from 'react-native-webview';
-import * as FileSystem from 'expo-file-system';
+import * as FileSystem from 'expo-file-system/legacy';
const { cacheDirectory, writeAsStringAsync, deleteAsync, getInfoAsync, EncodingType, } = FileSystem;
function viewerHtml(base64, customStyle, withScroll = false, withPinchZoom = false, maximumPinchZoomScale = 5) {
return `
@@ -48,10 +48,14 @@ const bundleJsPath = `${cacheDirectory}bundle.js`;
const htmlPath = `${cacheDirectory}index.html`;
const pdfPath = `${cacheDirectory}file.pdf`;
function writeWebViewComponentFile(container, fileName, callback) {
+ console.log(`[PdfReader] writing component file: ${fileName}`);
writeAsStringAsync(`${cacheDirectory}${fileName}`, container.getBundle()).then(() => {
+ console.log(`[PdfReader] finished writing: ${fileName}`);
if (typeof callback === 'function') {
callback();
}
+ }).catch(err => {
+ console.error(`[PdfReader] error writing ${fileName}:`, err);
});
}
function writeWebViewComponentFiles() {
@@ -66,15 +70,19 @@ function writeWebViewComponentFiles() {
});
}
async function writeWebViewReaderFileAsync(data, customStyle, withScroll, withPinchZoom, maximumPinchZoomScale) {
+ console.log('[PdfReader] writeWebViewReaderFileAsync start');
writeWebViewComponentFiles();
+ console.log('[PdfReader] checking bundle.js exists...');
const { exists, md5 } = await getInfoAsync(bundleJsPath, { md5: true });
const bundleContainer = require('./bundleContainer');
if (__DEV__ || !exists || bundleContainer.getBundleMd5() !== md5) {
+ console.log('[PdfReader] writing bundle.js...');
await writeAsStringAsync(bundleJsPath, bundleContainer.getBundle());
}
+ console.log('[PdfReader] writing index.html...');
await writeAsStringAsync(htmlPath, viewerHtml(data, customStyle, withScroll, withPinchZoom, maximumPinchZoomScale));
-}
-async function writePDFAsync(base64) {
+ console.log('[PdfReader] writeWebViewReaderFileAsync finished');
+}async function writePDFAsync(base64) {
await writeAsStringAsync(pdfPath, base64.replace('data:application/pdf;base64,', ''), { encoding: EncodingType.Base64 });
}
export async function removeFilesAsync() {
@@ -150,6 +158,7 @@ class PdfReader extends React.Component {
data: undefined,
renderedOnce: false,
};
+ this.cachedWebviewSource = null;
this.validate = () => {
const { onError: propOnError, source } = this.props;
const { renderType } = this.state;
@@ -176,33 +185,40 @@ class PdfReader extends React.Component {
};
this.init = async () => {
try {
+ console.log('[PdfReader] init start');
const { source, customStyle, withScroll, withPinchZoom, maximumPinchZoomScale, } = this.props;
const { renderType } = this.state;
+ console.log('[PdfReader] renderType:', renderType);
switch (renderType) {
case 'GOOGLE_DRIVE_VIEWER': {
break;
}
case 'URL_TO_BASE64': {
+ console.log('[PdfReader] fetching PDF...');
const data = await fetchPdfAsync(source);
+ console.log('[PdfReader] writing files...');
await writeWebViewReaderFileAsync(data, customStyle, withScroll, withPinchZoom, maximumPinchZoomScale);
break;
}
case 'DIRECT_BASE64': {
+ console.log('[PdfReader] writing files (direct base64)...');
await writeWebViewReaderFileAsync(source.base64, customStyle, withScroll, withPinchZoom, maximumPinchZoomScale);
break;
}
case 'BASE64_TO_LOCAL_PDF': {
+ console.log('[PdfReader] writing local PDF...');
await writePDFAsync(source.base64);
break;
}
default:
break;
}
+ console.log('[PdfReader] init finished, setting ready: true');
this.setState({ ready: true });
}
catch (error) {
alert(`Sorry, an error occurred. ${error.message}`);
- console.error(error);
+ console.error('[PdfReader] init error:', error);
}
};
this.getRenderType = () => {
@@ -232,23 +248,33 @@ class PdfReader extends React.Component {
this.getWebviewSource = () => {
const { renderType } = this.state;
const { source: { uri, headers }, onError, } = this.props;
+ let result;
switch (renderType) {
case 'GOOGLE_READER':
- return { uri: getGoogleReaderUrl(uri) };
+ result = { uri: getGoogleReaderUrl(uri) };
+ break;
case 'GOOGLE_DRIVE_VIEWER':
- return { uri: getGoogleDriveUrl(uri) };
+ result = { uri: getGoogleDriveUrl(uri) };
+ break;
case 'DIRECT_BASE64':
case 'URL_TO_BASE64':
- return { uri: htmlPath };
+ result = { uri: htmlPath };
+ break;
case 'DIRECT_URL':
- return { uri: uri, headers };
+ result = { uri: uri, headers };
+ break;
case 'BASE64_TO_LOCAL_PDF':
- return { uri: pdfPath };
+ result = { uri: pdfPath };
+ break;
default: {
onError('Unknown RenderType');
return undefined;
}
}
+ if (!this.cachedWebviewSource || this.cachedWebviewSource.uri !== result.uri) {
+ this.cachedWebviewSource = result;
+ }
+ return this.cachedWebviewSource;
};
}
componentDidMount() {
@@ -262,6 +288,7 @@ class PdfReader extends React.Component {
if (prevProps.source.uri !== this.props.source.uri ||
prevProps.source.base64 !== this.props.source.base64) {
this.setState({ ready: false, renderType: this.getRenderType() });
+ this.cachedWebviewSource = null;
this.validate();
this.init();
}
@@ -294,20 +321,33 @@ class PdfReader extends React.Component {
const isAndroid = Platform.OS === 'android';
if (ready) {
const source = this.getWebviewSource();
+ console.log('[PdfReader] rendering WebView with source:', source);
return (React.createElement(View, { style: [styles.container, containerStyle] },
React.createElement(WebView, { ...{
originWhitelist,
onLoad: (event) => {
- this.setState({ renderedOnce: true });
+ console.log('[PdfReader] WebView onLoad called');
+ if (!this.state.renderedOnce) {
+ this.setState({ renderedOnce: true });
+ }
if (onLoad) {
onLoad(event);
}
},
- onLoadEnd,
- onError,
- onHttpError: onError,
+ onLoadEnd: () => {
+ console.log('[PdfReader] WebView onLoadEnd called');
+ if (onLoadEnd) onLoadEnd();
+ },
+ onError: (error) => {
+ console.error('[PdfReader] WebView onError:', error);
+ if (onError) onError(error);
+ },
+ onHttpError: (error) => {
+ console.error('[PdfReader] WebView onHttpError:', error);
+ if (onError) onError(error);
+ },
style,
- source: renderedOnce || !isAndroid ? source : undefined,
+ source: source,
}, allowFileAccess: isAndroid, allowFileAccessFromFileURLs: isAndroid, allowUniversalAccessFromFileURLs: isAndroid, scalesPageToFit: Platform.select({ android: false }), mixedContentMode: isAndroid ? 'always' : undefined, sharedCookiesEnabled: false, startInLoadingState: !noLoader, renderLoading: () => (noLoader ? React.createElement(View, null) : React.createElement(Loader, null)), ...webviewProps })));
}
return !noLoader && !ready && React.createElement(Loader, null);

View File

@ -0,0 +1,26 @@
diff --git a/node_modules/@supabase/supabase-js/dist/index.cjs b/node_modules/@supabase/supabase-js/dist/index.cjs
index 56a9c3a..c57ea9d 100644
--- a/node_modules/@supabase/supabase-js/dist/index.cjs
+++ b/node_modules/@supabase/supabase-js/dist/index.cjs
@@ -624,7 +624,7 @@ var require_extract = /* @__PURE__ */ __commonJSMin(((exports) => {
let otelModulePromise = null;
const OTEL_PKG = "@opentelemetry/api";
function loadOtel() {
- if (otelModulePromise === null) otelModulePromise = Promise.resolve(`${OTEL_PKG}`).then((s) => tslib_1.__importStar(require(s))).catch(() => null);
+ otelModulePromise = Promise.resolve(null);
return otelModulePromise;
}
/**
diff --git a/node_modules/@supabase/supabase-js/dist/index.mjs b/node_modules/@supabase/supabase-js/dist/index.mjs
index 0b33345..fc81723 100644
--- a/node_modules/@supabase/supabase-js/dist/index.mjs
+++ b/node_modules/@supabase/supabase-js/dist/index.mjs
@@ -68,7 +68,7 @@ function __awaiter(thisArg, _arguments, P, generator) {
let otelModulePromise = null;
const OTEL_PKG = "@opentelemetry/api";
function loadOtel() {
- if (otelModulePromise === null) otelModulePromise = import(/* webpackIgnore: true */ /* turbopackIgnore: true */ /* @vite-ignore */ OTEL_PKG).catch(() => null);
+ if (otelModulePromise === null) otelModulePromise = Promise.resolve(null);
return otelModulePromise;
}
/**

112
scripts/reset-project.js Normal file
View File

@ -0,0 +1,112 @@
#!/usr/bin/env node
/**
* This script is used to reset the project to a blank state.
* It deletes or moves the /app, /components, /hooks, /scripts, and /constants directories to /app-example based on user input and creates a new /app directory with an index.tsx and _layout.tsx file.
* You can remove the `reset-project` script from package.json and safely delete this file after running it.
*/
const fs = require("fs");
const path = require("path");
const readline = require("readline");
const root = process.cwd();
const oldDirs = ["app", "components", "hooks", "constants", "scripts"];
const exampleDir = "app-example";
const newAppDir = "app";
const exampleDirPath = path.join(root, exampleDir);
const indexContent = `import { Text, View } from "react-native";
export default function Index() {
return (
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
}}
>
<Text>Edit app/index.tsx to edit this screen.</Text>
</View>
);
}
`;
const layoutContent = `import { Stack } from "expo-router";
export default function RootLayout() {
return <Stack />;
}
`;
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
});
const moveDirectories = async (userInput) => {
try {
if (userInput === "y") {
// Create the app-example directory
await fs.promises.mkdir(exampleDirPath, { recursive: true });
console.log(`📁 /${exampleDir} directory created.`);
}
// Move old directories to new app-example directory or delete them
for (const dir of oldDirs) {
const oldDirPath = path.join(root, dir);
if (fs.existsSync(oldDirPath)) {
if (userInput === "y") {
const newDirPath = path.join(root, exampleDir, dir);
await fs.promises.rename(oldDirPath, newDirPath);
console.log(`➡️ /${dir} moved to /${exampleDir}/${dir}.`);
} else {
await fs.promises.rm(oldDirPath, { recursive: true, force: true });
console.log(`❌ /${dir} deleted.`);
}
} else {
console.log(`➡️ /${dir} does not exist, skipping.`);
}
}
// Create new /app directory
const newAppDirPath = path.join(root, newAppDir);
await fs.promises.mkdir(newAppDirPath, { recursive: true });
console.log("\n📁 New /app directory created.");
// Create index.tsx
const indexPath = path.join(newAppDirPath, "index.tsx");
await fs.promises.writeFile(indexPath, indexContent);
console.log("📄 app/index.tsx created.");
// Create _layout.tsx
const layoutPath = path.join(newAppDirPath, "_layout.tsx");
await fs.promises.writeFile(layoutPath, layoutContent);
console.log("📄 app/_layout.tsx created.");
console.log("\n✅ Project reset complete. Next steps:");
console.log(
`1. Run \`npx expo start\` to start a development server.\n2. Edit app/index.tsx to edit the main screen.${
userInput === "y"
? `\n3. Delete the /${exampleDir} directory when you're done referencing it.`
: ""
}`
);
} catch (error) {
console.error(`❌ Error during script execution: ${error.message}`);
}
};
rl.question(
"Do you want to move existing files to /app-example instead of deleting them? (Y/n): ",
(answer) => {
const userInput = answer.trim().toLowerCase() || "y";
if (userInput === "y" || userInput === "n") {
moveDirectories(userInput).finally(() => rl.close());
} else {
console.log("❌ Invalid input. Please enter 'Y' or 'N'.");
rl.close();
}
}
);

View File

@ -0,0 +1,71 @@
import FloatingAIDock from "@/src/components/FloatingAIDock";
import GlassBottomTabBar from "@/src/components/GlassBottomTabBar";
import { FONTS } from "@/src/constants/Theme";
import { TabBarVisibilityProvider } from "@/src/hooks/useAutoHideTabBar";
import { useTheme } from "@/src/hooks/useTheme";
import { Tabs } from "expo-router";
import { Calendar, LayoutDashboard, Library } from "lucide-react-native";
import React from "react";
export default function TabLayout() {
const { colors } = useTheme();
const screenOptions = {
tabBarActiveTintColor: colors.primary,
tabBarInactiveTintColor: colors.outline,
headerShown: false,
tabBarStyle: {
position: "absolute",
borderTopWidth: 0,
elevation: 0,
},
tabBarLabelStyle: {
fontFamily: FONTS.label,
fontSize: 10,
letterSpacing: 0.5,
},
} as any;
return (
<TabBarVisibilityProvider>
<Tabs
tabBar={(props: any) => <GlassBottomTabBar {...props} />}
screenOptions={screenOptions}
>
<Tabs.Screen
name="index"
options={{
title: "Day",
tabBarIcon: ({ color }) => (
<LayoutDashboard size={24} color={color} strokeWidth={1.5} />
),
}}
/>
<Tabs.Screen
name="calendar"
options={{
title: "Schedule",
tabBarIcon: ({ color }) => (
<Calendar size={24} color={color} strokeWidth={1.5} />
),
}}
/>
<Tabs.Screen
name="library"
options={{
title: "Library",
tabBarIcon: ({ color }) => (
<Library size={24} color={color} strokeWidth={1.5} />
),
}}
/>
</Tabs>
<FloatingAIDock />
{/* Extra screens still exist in the app, but are intentionally excluded from the bottom glass tab bar.
Keep these routes reachable via menu, buttons, or a future drawer/More screen.
*/}
</TabBarVisibilityProvider>
);
}

3
src/app/(tabs)/aa_ai.tsx Normal file
View File

@ -0,0 +1,3 @@
import AIScreen from '@/src/screens/AIScreen';
export default AIScreen;

2982
src/app/(tabs)/calendar.tsx Normal file

File diff suppressed because it is too large Load Diff

1081
src/app/(tabs)/hh_habits.tsx Normal file

File diff suppressed because it is too large Load Diff

646
src/app/(tabs)/history.tsx Normal file
View File

@ -0,0 +1,646 @@
import AutoHideScrollView from "@/src/components/AutoHideScrollView";
import { FONTS, ROUNDNESS, SPACING } from "@/src/constants/Theme";
import { useAuth } from "@/src/hooks/useAuth";
import { useData } from "@/src/hooks/useData";
import { useTheme } from "@/src/hooks/useTheme";
import { getLocalDateString } from "@/src/lib/date-utils";
import { performMutation } from "@/src/lib/sync";
import * as Haptics from "expo-haptics";
import { useFocusEffect, useRouter } from "expo-router";
import {
CheckCircle2,
Clock,
Copy,
History as HistoryIcon,
Layout,
Menu,
Settings,
BookOpen
} from "lucide-react-native";
import React, { useCallback, useMemo, useState } from "react";
import {
ActivityIndicator,
Alert,
Image,
StyleSheet,
Text,
TouchableOpacity,
View
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
type TabType = "History" | "Repository" | "Reading";
type FilterType = "Day" | "Week" | "Month" | "Year";
interface Task {
id: string;
title: string;
status: "todo" | "doing" | "done";
completed_sessions: number;
estimated_sessions: number;
tag: string;
updated_at: string;
todos?: string;
}
interface ScheduleBlock {
start: string;
end: string;
task: string;
type?: string;
todos?: any[];
date: string;
}
interface ReadingLog {
id: string;
book_id: string;
book_title: string;
start_page: number;
end_page: number;
pages_read: number;
duration_seconds: number;
logged_at: string;
}
export default function HistoryScreen() {
const { colors } = useTheme();
const { user } = useAuth();
const router = useRouter();
const styles = useMemo(() => createStyles(colors), [colors]);
const [activeTab, setActiveTab] = useState<TabType>("History");
const [activeFilter, setActiveFilter] = useState<FilterType>("Month");
const userId = user?.id || "guest";
const today = getLocalDateString();
// 1. Fetch Completed Tasks History
const {
data: completedTasks,
loading: tasksLoading,
refresh: refreshTasks,
} = useData<Task>(
`SELECT * FROM tasks
WHERE (user_id = ? OR user_id IS NULL)
AND status = 'done'
ORDER BY updated_at DESC LIMIT 200`,
[userId],
);
// 2. Fetch Past Schedules
const {
data: pastSchedules,
loading: schedulesLoading,
refresh: refreshSchedules,
} = useData<{ date: string; time_blocks: string }>(
`SELECT date, time_blocks FROM schedules
WHERE (user_id = ? OR user_id IS NULL)
AND date <= ?
ORDER BY date DESC`,
[userId, today],
);
// 3. Fetch Reading History
const {
data: readingLogs,
loading: readingLoading,
refresh: refreshReading,
} = useData<ReadingLog>(
`SELECT rl.*, b.title as book_title
FROM reading_logs rl
JOIN books b ON rl.book_id = b.id
WHERE (rl.user_id = ? OR rl.user_id IS NULL)
ORDER BY rl.logged_at DESC LIMIT 100`,
[userId],
);
// Filtering Logic
const filteredHistory = useMemo(() => {
const now = new Date();
const allHistory = pastSchedules
.flatMap((s) => {
try {
const blocks = JSON.parse(s.time_blocks) as any[];
return blocks.map((b) => ({ ...b, date: s.date }));
} catch {
return [];
}
})
.filter((b) => b.type !== "break" && b.task !== "Break");
return allHistory.filter((item) => {
const itemDate = new Date(item.date);
const diffTime = now.getTime() - itemDate.getTime();
const diffDays = Math.floor(diffTime / (1000 * 3600 * 24));
if (activeFilter === "Day") return diffDays <= 0;
if (activeFilter === "Week") return diffDays <= 7;
if (activeFilter === "Month") return diffDays <= 31;
if (activeFilter === "Year") return diffDays <= 365;
return true;
});
}, [pastSchedules, activeFilter]);
const filteredInventory = useMemo(() => {
const now = new Date();
return completedTasks.filter((task) => {
const taskDate = new Date(task.updated_at);
const diffTime = now.getTime() - taskDate.getTime();
const diffDays = Math.floor(diffTime / (1000 * 3600 * 24));
if (activeFilter === "Day") return diffDays <= 0;
if (activeFilter === "Week") return diffDays <= 7;
if (activeFilter === "Month") return diffDays <= 31;
if (activeFilter === "Year") return diffDays <= 365;
return true;
});
}, [completedTasks, activeFilter]);
const filteredReading = useMemo(() => {
const now = new Date();
return readingLogs.filter((log) => {
const logDate = new Date(log.logged_at);
const diffTime = now.getTime() - logDate.getTime();
const diffDays = Math.floor(diffTime / (1000 * 3600 * 24));
if (activeFilter === "Day") return diffDays <= 0;
if (activeFilter === "Week") return diffDays <= 7;
if (activeFilter === "Month") return diffDays <= 31;
if (activeFilter === "Year") return diffDays <= 365;
return true;
});
}, [readingLogs, activeFilter]);
useFocusEffect(
useCallback(() => {
refreshTasks();
refreshSchedules();
refreshReading();
}, [refreshTasks, refreshSchedules, refreshReading]),
);
const formatDuration = (seconds: number) => {
const mins = Math.floor(seconds / 60);
if (mins < 1) return "< 1 min";
return `${mins} min${mins > 1 ? "s" : ""}`;
};
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleDateString(undefined, {
month: "short",
day: "numeric",
year: "numeric",
});
};
return (
<View style={styles.container}>
<SafeAreaView style={styles.safeArea} edges={["top"]}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity
style={styles.menuBtn}
onPress={() => router.push("/menu")}
>
<Menu size={24} color={colors.primary} strokeWidth={1.5} />
</TouchableOpacity>
<View style={styles.logoContainer}>
<Image
source={require("@/assets/images/Artboard 1 logo.png")}
style={styles.logoImage}
tintColor={colors.primary}
resizeMode="contain"
/>
</View>
<TouchableOpacity
style={styles.ghostBtn}
onPress={() => router.push("/modal")}
>
<Settings size={20} color={colors.primary} strokeWidth={1.5} />
</TouchableOpacity>
</View>
{/* Tab Switcher */}
<View style={styles.tabContainer}>
<TouchableOpacity
style={[styles.tab, activeTab === "History" && styles.activeTab]}
onPress={() => {
setActiveTab("History");
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
>
<HistoryIcon
size={16}
color={activeTab === "History" ? colors.primary : colors.outline}
/>
<Text
style={[
styles.tabText,
activeTab === "History" && { color: colors.primary },
]}
>
Plans
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, activeTab === "Repository" && styles.activeTab]}
onPress={() => {
setActiveTab("Repository");
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
>
<Layout
size={16}
color={
activeTab === "Repository" ? colors.primary : colors.outline
}
/>
<Text
style={[
styles.tabText,
activeTab === "Repository" && { color: colors.primary },
]}
>
Tasks
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, activeTab === "Reading" && styles.activeTab]}
onPress={() => {
setActiveTab("Reading");
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
>
<BookOpen
size={16}
color={activeTab === "Reading" ? colors.primary : colors.outline}
/>
<Text
style={[
styles.tabText,
activeTab === "Reading" && { color: colors.primary },
]}
>
Reading
</Text>
</TouchableOpacity>
</View>
<AutoHideScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<View style={styles.section}>
{/* Filter Bar */}
<View style={styles.filterRow}>
{["Day", "Week", "Month", "Year"].map((f) => (
<TouchableOpacity
key={f}
style={[
styles.filterBtn,
activeFilter === f && {
backgroundColor: colors.primary + "1A",
borderColor: colors.primary,
},
]}
onPress={() => setActiveFilter(f as FilterType)}
>
<Text
style={[
styles.filterText,
activeFilter === f && {
color: colors.primary,
fontFamily: FONTS.labelSm,
},
]}
>
{f}
</Text>
</TouchableOpacity>
))}
</View>
{activeTab === "History" ? (
<View style={styles.historyList}>
{schedulesLoading ? (
<ActivityIndicator
color={colors.primary}
style={{ marginTop: 40 }}
/>
) : filteredHistory.length > 0 ? (
filteredHistory.map((block, idx) => (
<TouchableOpacity
key={idx}
style={styles.historyCard}
onPress={() => handleReuseTask(block)}
>
<View style={styles.cardHeader}>
<View style={styles.dateBadge}>
<Text style={styles.dateText}>
{block.date === today ? "TODAY" : block.date}
</Text>
</View>
<Text style={styles.timeRange}>
{block.start} - {block.end}
</Text>
</View>
<View style={styles.cardBody}>
<Text style={styles.taskTitle}>{block.task}</Text>
<View style={styles.cardActions}>
<Copy size={16} color={colors.primary} />
<Text style={styles.reuseText}>REUSE</Text>
</View>
</View>
</TouchableOpacity>
))
) : (
<View style={styles.emptyState}>
<Clock size={40} color={colors.outlineVariant} />
<Text style={styles.emptyText}>
No historical data for this period.
</Text>
</View>
)}
</View>
) : activeTab === "Reading" ? (
<View style={styles.historyList}>
{readingLoading ? (
<ActivityIndicator
color={colors.primary}
style={{ marginTop: 40 }}
/>
) : filteredReading.length > 0 ? (
filteredReading.map((log) => (
<View key={log.id} style={styles.historyCard}>
<View style={styles.cardHeader}>
<View style={styles.dateBadge}>
<Text style={styles.dateText}>{formatDate(log.logged_at)}</Text>
</View>
<Text style={styles.timeRange}>{formatDuration(log.duration_seconds)}</Text>
</View>
<View style={styles.cardBody}>
<View style={{ flex: 1 }}>
<Text style={styles.taskTitle}>{log.book_title}</Text>
<Text style={styles.inventoryMeta}>
Read {log.pages_read} pages (p. {log.start_page} {log.end_page})
</Text>
</View>
</View>
</View>
))
) : (
<View style={styles.emptyState}>
<BookOpen size={40} color={colors.outlineVariant} />
<Text style={styles.emptyText}>No reading history for this period.</Text>
</View>
)}
</View>
) : (
<View style={styles.inventoryList}>
{tasksLoading ? (
<ActivityIndicator
color={colors.primary}
style={{ marginTop: 40 }}
/>
) : filteredInventory.length > 0 ? (
filteredInventory.map((task) => (
<TouchableOpacity
key={task.id}
style={styles.inventoryCard}
onPress={() => handleReuseTask(task)}
>
<View style={styles.inventoryMain}>
<View
style={[
styles.tagBadge,
{ backgroundColor: colors.secondaryContainer },
]}
>
<Text
style={[
styles.tagText,
{ color: colors.onSecondaryContainer },
]}
>
{task.tag || "General"}
</Text>
</View>
<Text style={styles.inventoryTitle}>{task.title}</Text>
<Text style={styles.inventoryMeta}>
Completed{" "}
{getLocalDateString(new Date(task.updated_at))}
</Text>
</View>
<View style={styles.reuseIconBtn}>
<Copy size={20} color={colors.primary} />
</View>
</TouchableOpacity>
))
) : (
<View style={styles.emptyState}>
<CheckCircle2
size={48}
color={colors.outlineVariant}
strokeWidth={1}
/>
<Text style={styles.emptyText}>
No completed tasks for this period.
</Text>
</View>
)}
</View>
)}
</View>
</AutoHideScrollView>
</SafeAreaView>
</View>
);
}
const createStyles = (colors: any) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
safeArea: {
flex: 1,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
backgroundColor: colors.background,
height: 60,
},
logoContainer: {
position: "absolute",
left: 0,
right: 0,
alignItems: "center",
justifyContent: "center",
zIndex: -1,
},
logoImage: { height: 40, width: 160 },
menuBtn: { padding: 8 },
ghostBtn: { padding: 8 },
tabContainer: {
flexDirection: "row",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
backgroundColor: colors.surface,
borderBottomWidth: 1,
borderBottomColor: colors.outlineVariant + "33",
},
tab: {
flexDirection: "row",
alignItems: "center",
gap: 8,
paddingVertical: 8,
paddingHorizontal: 16,
marginRight: 12,
borderRadius: ROUNDNESS.full,
},
activeTab: {
backgroundColor: colors.primaryContainer,
},
tabText: {
fontFamily: FONTS.labelSm,
fontSize: 14,
color: colors.outline,
},
scrollContent: {
paddingBottom: 40,
},
section: {
padding: SPACING.lg,
},
filterRow: {
flexDirection: "row",
gap: 8,
marginBottom: SPACING.xl,
flexWrap: "wrap",
},
filterBtn: {
paddingVertical: 6,
paddingHorizontal: 12,
borderRadius: ROUNDNESS.md,
borderWidth: 1,
borderColor: "transparent",
minWidth: 60,
alignItems: "center",
},
filterText: {
fontFamily: FONTS.label,
fontSize: 12,
color: colors.onSurfaceVariant,
},
historyList: { gap: 16 },
historyCard: {
backgroundColor: colors.surface,
borderRadius: ROUNDNESS.lg,
padding: 16,
borderWidth: 1,
borderColor: colors.outlineVariant + "33",
},
cardHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 12,
},
dateBadge: {
backgroundColor: colors.surfaceVariant,
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
},
dateText: {
fontFamily: FONTS.label,
fontSize: 10,
color: colors.onSurfaceVariant,
},
timeRange: {
fontFamily: FONTS.labelSm,
fontSize: 11,
color: colors.primary,
},
cardBody: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
taskTitle: {
fontFamily: FONTS.headline,
fontSize: 18,
color: colors.onSurface,
flex: 1,
marginRight: 16,
},
cardActions: {
flexDirection: "row",
alignItems: "center",
gap: 6,
backgroundColor: colors.primary + "1A",
paddingHorizontal: 10,
paddingVertical: 6,
borderRadius: 6,
},
reuseText: {
fontFamily: FONTS.labelSm,
fontSize: 11,
color: colors.primary,
},
inventoryList: { gap: 12 },
inventoryCard: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.surface,
padding: 16,
borderRadius: ROUNDNESS.lg,
borderWidth: 1,
borderColor: colors.outlineVariant + "33",
},
inventoryMain: { flex: 1 },
tagBadge: {
alignSelf: "flex-start",
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
marginBottom: 6,
},
tagText: { fontFamily: FONTS.label, fontSize: 9 },
inventoryTitle: {
fontFamily: FONTS.labelSm,
fontSize: 16,
color: colors.onSurface,
},
inventoryMeta: {
fontFamily: FONTS.body,
fontSize: 12,
color: colors.outline,
marginTop: 4,
},
reuseIconBtn: {
padding: 12,
backgroundColor: colors.primaryContainer,
borderRadius: ROUNDNESS.md,
},
emptyState: {
alignItems: "center",
justifyContent: "center",
paddingVertical: 60,
gap: 16,
},
emptyText: {
fontFamily: FONTS.body,
fontSize: 14,
color: colors.outline,
textAlign: "center",
paddingHorizontal: 40,
},
});

843
src/app/(tabs)/index.tsx Normal file
View File

@ -0,0 +1,843 @@
import AutoHideScrollView from "@/src/components/AutoHideScrollView";
import { FONTS, ROUNDNESS, SPACING } from "@/src/constants/Theme";
import { useAuth } from "@/src/hooks/useAuth";
import { useData } from "@/src/hooks/useData";
import { useTheme } from "@/src/hooks/useTheme";
import { performMutation } from "@/src/lib/sync";
import * as Haptics from "expo-haptics";
import { useFocusEffect, useRouter } from "expo-router";
import {
CheckCircle2,
Globe,
History,
Menu,
Play,
Plus,
Settings,
Sparkles,
Trash2
} from "lucide-react-native";
import React, { useCallback, useMemo } from "react";
import {
ActivityIndicator,
Alert,
Image,
Linking,
StyleSheet,
Text,
TouchableOpacity,
View
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { getLocalDateString } from "@/src/lib/date-utils";
export default function DashboardScreen() {
const { colors, focusGoal, displayName } = useTheme();
const { user } = useAuth();
const styles = useMemo(() => createStyles(colors), [colors]);
const router = useRouter();
const userId = user?.id || "guest";
const today = getLocalDateString();
const { data: habitStats, loading: habitsLoading } = useData<{
count: number;
}>(
"SELECT COUNT(*) as count FROM habits WHERE is_active = 1 AND (user_id = ? OR user_id IS NULL)",
[userId],
);
const { data: logStats, loading: logsLoading } = useData<{ count: number }>(
"SELECT COUNT(DISTINCT l.habit_id) as count FROM logs l JOIN habits h ON l.habit_id = h.id WHERE date(l.logged_at, 'localtime') = date('now', 'localtime') AND (h.user_id = ? OR h.user_id IS NULL)",
[userId],
);
const { data: sessionStats } = useData<{ count: number }>(
"SELECT SUM(completed_sessions) as count FROM tasks WHERE (user_id = ? OR user_id IS NULL) AND date(updated_at, 'localtime') = date('now', 'localtime')",
[userId],
);
const {
data: habits,
loading: listLoading,
refresh: refreshHabits,
} = useData<{ id: string; title: string; is_done_today: number }>(
`SELECT h.id, h.title,
(SELECT COUNT(*) FROM logs l WHERE l.habit_id = h.id AND date(l.logged_at, 'localtime') = date('now', 'localtime')) as is_done_today
FROM habits h
WHERE h.is_active = 1 AND (h.user_id = ? OR h.user_id IS NULL) LIMIT 3`,
[userId],
);
const {
data: shortcuts,
loading: shortcutsLoading,
refresh: refreshShortcuts,
} = useData<{ id: string; title: string; url: string; icon: string }>(
"SELECT * FROM shortcuts WHERE user_id = ? OR user_id IS NULL",
[userId],
);
const { data: identitySettings } = useData<{ value: string }>(
"SELECT value FROM settings WHERE key = 'identity_anchor'",
[],
);
const { data: latestTask } = useData<{ title: string }>(
"SELECT title FROM tasks WHERE (user_id = ? OR user_id IS NULL) AND status != 'done' ORDER BY updated_at DESC LIMIT 1",
[userId],
);
const identityAnchor =
identitySettings?.[0]?.value || "The Disciplined Creator";
const currentFocus = latestTask?.[0]?.title || "Daily Discipline";
// Refresh data when screen is focused
useFocusEffect(
useCallback(() => {
refreshHabits();
refreshShortcuts();
}, []),
);
const handleToggleHabit = async (habitId: string, isDone: boolean) => {
if (isDone) return;
try {
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
await performMutation("logs", "INSERT", {
id: Math.random().toString(36).substring(7),
habit_id: habitId,
status: "completed",
logged_at: new Date().toISOString(),
});
refreshHabits();
} catch (err) {
console.error("Failed to log habit:", err);
}
};
const activeHabits = habitStats?.[0]?.count || 0;
const completedToday = logStats?.[0]?.count || 0;
const sessionsDone = sessionStats?.[0]?.count || 0;
const userName = useMemo(() => {
if (displayName) return displayName;
if (user?.email) return user.email.split("@")[0];
return "Guest";
}, [displayName, user]);
const greeting = useMemo(() => {
const hour = new Date().getHours();
if (hour < 12) return "Good morning";
if (hour < 17) return "Good afternoon";
return "Good evening";
}, []);
const isLoading =
habitsLoading || logsLoading || listLoading || shortcutsLoading;
const handleOpenLink = async (url: string) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
try {
const supported = await Linking.canOpenURL(url);
if (supported) {
await Linking.openURL(url);
} else {
Alert.alert("Error", "Don't know how to open this URL: " + url);
}
} catch (error) {
Alert.alert("Error", "An error occurred while trying to open the link");
}
};
const handleDeleteShortcut = async (id: string) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
Alert.alert(
"Delete Shortcut",
"Are you sure you want to remove this shortcut?",
[
{ text: "Cancel", style: "cancel" },
{
text: "Delete",
style: "destructive",
onPress: async () => {
try {
await performMutation("shortcuts", "DELETE", { id });
refreshShortcuts();
Haptics.notificationAsync(
Haptics.NotificationFeedbackType.Success,
);
} catch (error) {
Alert.alert("Error", "Failed to delete shortcut");
}
},
},
],
);
};
return (
<View style={styles.container}>
<SafeAreaView style={styles.safeArea} edges={["top"]}>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity
style={styles.menuBtn}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.push("/menu");
}}
>
<Menu size={24} color={colors.primary} strokeWidth={1.5} />
</TouchableOpacity>
<View style={styles.logoContainer}>
<Image
source={require("@/assets/images/Artboard 1 logo.png")}
style={styles.logoImage}
tintColor={colors.primary}
resizeMode="contain"
/>
</View>
<View style={styles.headerRight}>
<TouchableOpacity
style={styles.ghostBtn}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.push("/history");
}}
>
<History size={20} color={colors.primary} strokeWidth={1.5} />
</TouchableOpacity>
<TouchableOpacity
style={styles.ghostBtn}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.push("/modal");
}}
>
<Settings size={20} color={colors.primary} strokeWidth={1.5} />
</TouchableOpacity>
</View>
</View>
<AutoHideScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
bounces={true}
>
{/* Greeting */}
<View style={styles.greetingContainer}>
<Text style={styles.labelCaps}>DAILY OVERVIEW</Text>
<Text style={styles.greetingText}>
{greeting}, {userName}. Let's find your flow today.
</Text>
</View>
{/* Stats Section */}
<View style={styles.statsContainer}>
<View style={styles.statsGrid}>
<TouchableOpacity
style={[
styles.statCard,
{ borderLeftColor: colors.primary, borderLeftWidth: 4 },
]}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.push("/hh_habits");
}}
>
<Text style={styles.statLabel}>Active Habits</Text>
<Text style={styles.statValue}>
{isLoading ? "..." : activeHabits}
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.statCard,
{ borderLeftColor: colors.secondary, borderLeftWidth: 4 },
]}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.push("/calendar");
}}
>
<Text style={styles.statLabel}>Done Today</Text>
<Text style={styles.statValue}>
{isLoading ? "..." : completedToday}
</Text>
</TouchableOpacity>
</View>
</View>
{/* Current Focus Card */}
<View style={styles.focusContainer}>
<View style={styles.focusCard}>
<View style={styles.focusHeader}>
<View style={styles.focusIconBg}>
<Sparkles size={20} color={colors.onPrimary} />
</View>
<Text style={styles.focusBadge}>
{identityAnchor.toUpperCase()}
</Text>
</View>
<Text style={styles.focusTitle}>{currentFocus}</Text>
<Text style={styles.focusDesc}>
Refining your presence through focused intention and consistent
action.
</Text>
<TouchableOpacity
style={styles.primaryBtn}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
router.push("/sprint");
}}
>
<Text style={styles.primaryBtnText}>Resume Session</Text>
<Play size={16} color={colors.primary} fill={colors.primary} />
</TouchableOpacity>
</View>
</View>
{/* Focus Capacity Slider */}
<View style={styles.section}>
<Text style={styles.sectionLabel}>FOCUS CAPACITY</Text>
<View style={styles.sliderContainer}>
<View style={styles.sliderTrack}>
<View
style={[
styles.sliderFill,
{
width: `${Math.min((sessionsDone / focusGoal) * 100, 100)}%`,
backgroundColor: colors.primary,
},
]}
/>
<View
style={[
styles.sliderThumb,
{
left: `${Math.min((sessionsDone / focusGoal) * 100, 100)}%`,
borderColor: colors.primary,
backgroundColor: colors.surface,
},
]}
/>
</View>
<View style={styles.sliderLabels}>
<Text style={styles.sliderLevel}>
{sessionsDone} sessions done
</Text>
<Text style={styles.sliderLevel}>Goal: {focusGoal}</Text>
</View>
</View>
</View>
{/* Keystone Habits */}
<View style={styles.section}>
<View style={styles.rowBetween}>
<Text style={styles.sectionTitle}>Daily Habits</Text>
<View style={styles.rowAlign}>
<TouchableOpacity
style={{ marginRight: 16 }}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.push("/add-habit");
}}
>
<Plus size={20} color={colors.primary} />
</TouchableOpacity>
<TouchableOpacity
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.push("/hh_habits");
}}
>
<Text style={styles.sectionSubtitle}>View All</Text>
</TouchableOpacity>
</View>
</View>
{isLoading ? (
<ActivityIndicator
color={colors.primary}
style={{ marginVertical: 20 }}
/>
) : habits.length > 0 ? (
<View style={styles.habitList}>
{habits.map((habit) => (
<HabitItem
key={habit.id}
title={habit.title}
done={habit.is_done_today > 0}
colors={colors}
onToggle={() =>
handleToggleHabit(habit.id, habit.is_done_today > 0)
}
/>
))}
</View>
) : (
<TouchableOpacity
style={styles.emptyStateCard}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.push("/add-habit");
}}
>
<Plus size={24} color={colors.primary} />
<Text style={styles.emptyStateText}>
Create your first habit to start tracking
</Text>
</TouchableOpacity>
)}
</View>
{/* Tools / Shortcuts */}
<View style={styles.toolsContainer}>
<View style={styles.rowBetween}>
<Text style={styles.sectionLabel}>ECOSYSTEM TOOLS</Text>
<TouchableOpacity
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.push("/add-shortcut");
}}
>
<Plus size={16} color={colors.primary} />
</TouchableOpacity>
</View>
<View style={styles.toolsGrid}>
{shortcuts.length > 0 ? (
shortcuts.map((shortcut) => (
<ToolCard
key={shortcut.id}
icon={<Globe size={20} color={colors.primary} />}
label={shortcut.title}
colors={colors}
onPress={() => handleOpenLink(shortcut.url)}
onDelete={() => handleDeleteShortcut(shortcut.id)}
/>
))
) : (
<View style={styles.emptyStateContainer}>
<Text style={styles.emptyStateTextSmall}>
No shortcuts added yet.
</Text>
</View>
)}
</View>
</View>
</AutoHideScrollView>
</SafeAreaView>
</View>
);
}
function HabitItem({
title,
done,
colors,
onToggle,
}: {
title: string;
done: boolean;
colors: any;
onToggle: () => void;
}) {
const router = useRouter();
return (
<TouchableOpacity style={stylesHabit.habitItem} onPress={onToggle}>
<View
style={[
stylesHabit.checkbox,
{ borderColor: colors.primary },
done && { backgroundColor: colors.primary },
]}
>
{done && <CheckCircle2 size={14} color={colors.onPrimary} />}
</View>
<Text
style={[
stylesHabit.habitText,
{ color: colors.onSurface },
done && {
color: colors.onSurfaceVariant,
textDecorationLine: "line-through",
},
]}
>
{title}
</Text>
</TouchableOpacity>
);
}
const stylesHabit = StyleSheet.create({
habitItem: {
flexDirection: "row",
alignItems: "center",
gap: 12,
paddingVertical: 8,
},
checkbox: {
width: 24,
height: 24,
borderRadius: 6,
borderWidth: 2,
alignItems: "center",
justifyContent: "center",
},
habitText: {
fontFamily: FONTS.body,
fontSize: 16,
},
});
function ToolCard({
icon,
label,
colors,
onPress,
onDelete,
}: {
icon: React.ReactNode;
label: string;
colors: any;
onPress?: () => void;
onDelete?: () => void;
}) {
return (
<View style={stylesTool.container}>
<TouchableOpacity
style={[
stylesTool.toolCard,
{ backgroundColor: colors.surfaceVariant },
]}
onPress={
onPress ||
(() =>
Alert.alert(
"External Tool",
`Launching integrated ${label} dashboard.`,
))
}
>
<View
style={[
stylesTool.iconContainer,
{ backgroundColor: colors.surface },
]}
>
{icon}
</View>
<Text
style={[stylesTool.toolLabel, { color: colors.onSurface }]}
numberOfLines={1}
>
{label}
</Text>
</TouchableOpacity>
{onDelete && (
<TouchableOpacity
style={[
stylesTool.deleteBtn,
{ backgroundColor: colors.error + "1A" },
]}
onPress={onDelete}
>
<Trash2 size={14} color={colors.error} />
</TouchableOpacity>
)}
</View>
);
}
const stylesTool = StyleSheet.create({
container: {
width: "47%",
marginBottom: SPACING.md,
position: "relative",
},
toolCard: {
width: "100%",
padding: SPACING.md,
borderRadius: ROUNDNESS.lg,
alignItems: "center",
justifyContent: "center",
gap: 10,
},
deleteBtn: {
position: "absolute",
top: 6,
right: 6,
padding: 6,
borderRadius: 12,
},
iconContainer: {
padding: 10,
borderRadius: ROUNDNESS.md,
},
toolLabel: {
fontFamily: FONTS.labelSm,
fontSize: 12,
letterSpacing: 0.5,
},
});
const createStyles = (colors: any) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
safeArea: {
flex: 1,
},
scrollContent: {
paddingBottom: 40,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
backgroundColor: colors.background,
height: 60,
},
logoContainer: {
position: "absolute",
left: 0,
right: 0,
alignItems: "center",
justifyContent: "center",
zIndex: -1,
},
menuBtn: {
padding: 8,
},
logoImage: {
height: 40,
width: 160,
},
headerRight: {
flexDirection: "row",
alignItems: "center",
gap: 8,
},
ghostBtn: {
padding: 8,
},
greetingContainer: {
paddingHorizontal: SPACING.lg,
paddingBottom: SPACING.lg,
paddingTop: SPACING.md,
},
labelCaps: {
fontFamily: FONTS.labelSm,
fontSize: 11,
letterSpacing: 1.5,
color: colors.primary,
marginBottom: 4,
},
greetingText: {
fontFamily: FONTS.headline,
fontSize: 28,
color: colors.onSurface,
lineHeight: 34,
},
statsContainer: {
paddingHorizontal: SPACING.lg,
marginBottom: SPACING.lg,
},
statsGrid: {
flexDirection: "row",
gap: SPACING.md,
},
statCard: {
flex: 1,
backgroundColor: colors.surface,
padding: SPACING.md,
borderRadius: ROUNDNESS.lg,
borderWidth: 1,
borderColor: colors.outlineVariant + "4D",
},
statLabel: {
fontFamily: FONTS.label,
fontSize: 12,
color: colors.onSurfaceVariant,
marginBottom: 4,
},
statValue: {
fontSize: 32,
fontFamily: FONTS.headline,
color: colors.onSurface,
},
focusContainer: {
paddingHorizontal: SPACING.lg,
marginBottom: SPACING.xl,
},
focusCard: {
backgroundColor: colors.primary,
padding: SPACING.lg,
borderRadius: ROUNDNESS.xl,
},
focusHeader: {
flexDirection: "row",
alignItems: "center",
gap: 12,
marginBottom: SPACING.md,
},
focusIconBg: {
padding: 8,
backgroundColor: "rgba(255,255,255,0.2)",
borderRadius: ROUNDNESS.md,
},
focusBadge: {
fontFamily: FONTS.labelSm,
fontSize: 11,
color: colors.onPrimary,
letterSpacing: 1,
},
focusTitle: {
fontFamily: FONTS.headline,
fontSize: 24,
color: colors.onPrimary,
marginBottom: 8,
},
focusDesc: {
fontFamily: FONTS.body,
fontSize: 15,
color: colors.onPrimary,
opacity: 0.8,
lineHeight: 22,
marginBottom: SPACING.lg,
},
primaryBtn: {
backgroundColor: colors.onPrimary,
paddingHorizontal: 20,
paddingVertical: 12,
borderRadius: ROUNDNESS.full,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 10,
},
primaryBtnText: {
color: colors.primary,
fontFamily: FONTS.labelSm,
fontSize: 14,
},
section: {
padding: SPACING.lg,
},
sectionLabel: {
fontFamily: FONTS.labelSm,
fontSize: 11,
color: colors.outline,
letterSpacing: 1.5,
marginBottom: SPACING.md,
},
sectionTitle: {
fontFamily: FONTS.headline,
fontSize: 24,
color: colors.onSurface,
},
sectionSubtitle: {
fontFamily: FONTS.label,
fontSize: 13,
color: colors.primary,
},
rowBetween: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "baseline",
marginBottom: SPACING.md,
},
rowAlign: {
flexDirection: "row",
alignItems: "center",
},
sliderContainer: {
backgroundColor: colors.surface,
padding: SPACING.lg,
borderRadius: ROUNDNESS.lg,
borderWidth: 1,
borderColor: colors.outlineVariant + "4D",
},
sliderTrack: {
height: 8,
backgroundColor: colors.surfaceVariant,
borderRadius: 4,
position: "relative",
marginBottom: 12,
},
sliderFill: {
height: "100%",
borderRadius: 4,
},
sliderThumb: {
width: 20,
height: 20,
borderRadius: 10,
position: "absolute",
top: -6,
marginLeft: -10,
borderWidth: 2,
},
sliderLabels: {
flexDirection: "row",
justifyContent: "space-between",
},
sliderLevel: {
fontFamily: FONTS.label,
fontSize: 11,
color: colors.onSurfaceVariant,
},
habitList: {
gap: 4,
},
toolsContainer: {
padding: SPACING.lg,
},
toolsGrid: {
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "space-between",
},
emptyStateContainer: {
width: "100%",
padding: SPACING.lg,
backgroundColor: colors.surfaceVariant + "4D",
borderRadius: ROUNDNESS.lg,
borderWidth: 1,
borderStyle: "dashed",
borderColor: colors.outlineVariant,
alignItems: "center",
},
emptyStateTextSmall: {
fontFamily: FONTS.body,
fontSize: 12,
color: colors.onSurfaceVariant,
},
emptyStateCard: {
backgroundColor: colors.surface,
padding: SPACING.xl,
borderRadius: ROUNDNESS.lg,
borderWidth: 1,
borderStyle: "dashed",
borderColor: colors.primary,
alignItems: "center",
justifyContent: "center",
gap: 12,
marginTop: 10,
},
emptyStateText: {
fontFamily: FONTS.body,
fontSize: 14,
color: colors.onSurfaceVariant,
textAlign: "center",
},
});

371
src/app/(tabs)/library.tsx Normal file
View File

@ -0,0 +1,371 @@
import AutoHideScrollView from "@/src/components/AutoHideScrollView";
import { SPACING } from "@/src/constants/Theme";
import { useAuth } from "@/src/hooks/useAuth";
import { useData } from "@/src/hooks/useData";
import { useTheme } from "@/src/hooks/useTheme";
import { supabase } from "@/src/lib/supabase";
import { performMutation } from "@/src/lib/sync";
import { getDb } from "@/src/db/database";
import { decode } from "base-64";
import * as DocumentPicker from "expo-document-picker";
import * as FileSystem from "expo-file-system/legacy";
import { resolveFileUri, getRelativePath, getDocumentDirectory } from "@/src/lib/file-utils";
import * as Haptics from "expo-haptics";
import { useFocusEffect, useRouter } from "expo-router";
import React, { useCallback, useMemo, useState } from "react";
import {
Alert,
StyleSheet,
View,
TouchableOpacity,
} from "react-native";
import { GestureHandlerRootView } from "react-native-gesture-handler";
import { SafeAreaView } from "react-native-safe-area-context";
import { Plus } from "lucide-react-native";
import { ROUNDNESS } from "@/src/constants/Theme";
// Import extracted components
import LibraryHeader from "@/src/components/Library/LibraryHeader";
import LibraryStats from "@/src/components/Library/LibraryStats";
import BookGrid from "@/src/components/Library/BookGrid";
import AddBookModal from "@/src/components/Library/AddBookModal";
import BookDetailsModal from "@/src/components/Library/BookDetailsModal";
interface Book {
id: string;
title: string;
author: string;
total_pages: number;
current_page: number;
file_uri: string;
cover_uri: string;
status: "reading" | "finished" | "want_to_read";
updated_at: string;
synthesis?: string;
}
export default function LibraryScreen() {
const { colors, isDark } = useTheme();
const { user } = useAuth();
const router = useRouter();
const userId = user?.id || "guest";
// State
const [showAddModal, setShowAddModal] = useState(false);
const [loading, setLoading] = useState(false);
// Form State
const [bookTitle, setBookTitle] = useState("");
const [bookAuthor, setBookAuthor] = useState("");
const [totalPages, setTotalPages] = useState("");
const [selectedFile, setSelectedFile] = useState<any>(null);
// Data
const {
data: books,
loading: booksLoading,
refresh: refreshBooks,
} = useData<Book>(
"SELECT * FROM books WHERE (user_id = ? OR user_id IS NULL) ORDER BY updated_at DESC",
[userId],
);
const stats = useMemo(() => {
const reading = books.filter((b) => b.status === "reading");
const finished = books.filter((b) => b.status === "finished");
const totalPagesRead = books.reduce(
(acc, b) => acc + (b.current_page || 0),
0,
);
return {
reading: reading.length,
finished: finished.length,
pages: totalPagesRead,
};
}, [books]);
useFocusEffect(
useCallback(() => {
refreshBooks();
}, [refreshBooks]),
);
const handlePickDocument = async () => {
try {
const result = await DocumentPicker.getDocumentAsync({
type: ["application/pdf", "application/epub+zip"],
copyToCacheDirectory: true,
});
if (!result.canceled) {
const file = result.assets[0];
setSelectedFile(file);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
try {
const { data, error } = await supabase.functions.invoke(
"process-book-ai",
{
body: { filename: file.name },
},
);
if (!error && data) {
setBookTitle(data.title || file.name.replace(/\.[^/.]+$/, ""));
setBookAuthor(data.author || "");
setTotalPages(data.totalPages?.toString() || "");
} else {
setBookTitle(file.name.replace(/\.[^/.]+$/, ""));
}
} catch (e) {
setBookTitle(file.name.replace(/\.[^/.]+$/, ""));
} finally {
setShowAddModal(true);
}
}
} catch (err) {
Alert.alert("Error", "Failed to pick document");
}
};
const handleAddBook = async () => {
if (!bookTitle.trim()) return;
setLoading(true);
try {
let finalUri = "";
if (selectedFile) {
const fileExt = selectedFile.name.split(".").pop();
const fileName = `${userId}/${Date.now()}.${fileExt}`;
const filePath = `${fileName}`;
const docDir = getDocumentDirectory();
const localUri = `${docDir}books/${fileName}`;
const dirPath = `${docDir}books/${userId}`;
await FileSystem.makeDirectoryAsync(dirPath, { intermediates: true });
await FileSystem.copyAsync({
from: selectedFile.uri,
to: localUri,
});
const base64 = await FileSystem.readAsStringAsync(selectedFile.uri, {
encoding: FileSystem.EncodingType.Base64,
});
supabase.storage
.from("books")
.upload(filePath, decode(base64), {
contentType: selectedFile.mimeType || "application/pdf",
upsert: true,
})
.then(({ error }) => {
if (error) console.error("Cloud sync error:", error);
});
finalUri = getRelativePath(localUri);
}
const id = Math.random().toString(36).substring(7);
await performMutation("books", "INSERT", {
id,
user_id: userId,
title: bookTitle.trim(),
author: bookAuthor.trim() || "Unknown",
total_pages: parseInt(totalPages) || 0,
current_page: 0,
file_uri: finalUri,
status: "reading",
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
});
setShowAddModal(false);
setBookTitle("");
setBookAuthor("");
setTotalPages("");
setSelectedFile(null);
refreshBooks();
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} catch (e) {
console.error(e);
Alert.alert("Upload Error", "Failed to add book to library.");
} finally {
setLoading(false);
}
};
const [showDetailsModal, setShowDetailsModal] = useState(false);
const [selectedBookForDetails, setSelectedBookForDetails] =
useState<Book | null>(null);
const openDetails = (book: Book) => {
setSelectedBookForDetails(book);
setShowDetailsModal(true);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
};
const openReader = (book: Book) => {
setShowDetailsModal(false);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
router.push(`/reader/${book.id}`);
};
const handleDeleteBook = async (id: string, fileUri?: string) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
Alert.alert(
"Remove Book",
"Are you sure you want to delete this book from your library?",
[
{ text: "Cancel", style: "cancel" },
{
text: "Delete",
style: "destructive",
onPress: async () => {
try {
await performMutation("books", "DELETE", { id });
if (fileUri) {
const resolvedUri = resolveFileUri(fileUri);
const fileInfo = await FileSystem.getInfoAsync(resolvedUri);
if (fileInfo.exists) {
await FileSystem.deleteAsync(resolvedUri, { idempotent: true });
}
}
const db = await getDb();
await db.runAsync("DELETE FROM reading_sessions WHERE book_id = ?", [id]);
refreshBooks();
Haptics.notificationAsync(
Haptics.NotificationFeedbackType.Success,
);
} catch (e) {
console.error("Delete error:", e);
Alert.alert("Error", "Failed to delete book");
}
},
},
],
);
};
const [isGeneratingAI, setIsGeneratingAI] = useState(false);
const generateAIInsights = async (book: Book) => {
setIsGeneratingAI(true);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
try {
const { data, error } = await supabase.functions.invoke(
"process-book-ai",
{
body: { title: book.title },
},
);
if (error) throw error;
await performMutation("books", "UPDATE", {
id: book.id,
synthesis: data.synthesis,
updated_at: new Date().toISOString(),
});
refreshBooks();
Alert.alert(
"AI Synthesis Complete",
"Insights have been generated from your reading session.",
);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} catch (e) {
console.error(e);
Alert.alert("AI Error", "Failed to generate insights.");
} finally {
setIsGeneratingAI(false);
}
};
return (
<GestureHandlerRootView style={styles.container}>
<View style={styles.container}>
<SafeAreaView style={styles.safeArea} edges={["top"]}>
<LibraryHeader onSettingsPress={() => router.push("/modal")} />
<AutoHideScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<LibraryStats stats={stats} />
<BookGrid
books={books as any}
loading={booksLoading}
onBookPress={openDetails}
onEmptyPress={handlePickDocument}
/>
</AutoHideScrollView>
<AddBookModal
visible={showAddModal}
onClose={() => setShowAddModal(false)}
bookTitle={bookTitle}
onTitleChange={setBookTitle}
bookAuthor={bookAuthor}
onAuthorChange={setBookAuthor}
totalPages={totalPages}
onPagesChange={setTotalPages}
onAddBook={handleAddBook}
loading={loading}
/>
<BookDetailsModal
visible={showDetailsModal}
onClose={() => setShowDetailsModal(false)}
book={selectedBookForDetails as any}
onContinueReading={() => selectedBookForDetails && openReader(selectedBookForDetails)}
onGenerateAI={() => selectedBookForDetails && generateAIInsights(selectedBookForDetails)}
onDelete={() => selectedBookForDetails && handleDeleteBook(selectedBookForDetails.id, selectedBookForDetails.file_uri)}
isGeneratingAI={isGeneratingAI}
/>
<TouchableOpacity
style={[styles.fab, { backgroundColor: colors.primary }]}
onPress={handlePickDocument}
activeOpacity={0.8}
>
<Plus size={28} color={colors.onPrimary} />
</TouchableOpacity>
</SafeAreaView>
</View>
</GestureHandlerRootView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
safeArea: {
flex: 1,
},
scrollContent: {
paddingBottom: 100,
},
fab: {
position: "absolute",
right: 20,
bottom: 90,
width: 60,
height: 60,
borderRadius: 30,
justifyContent: "center",
alignItems: "center",
elevation: 8,
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.3,
shadowRadius: 4,
zIndex: 10,
},
});

1408
src/app/(tabs)/sprint.tsx Normal file

File diff suppressed because it is too large Load Diff

139
src/app/_layout.tsx Normal file
View File

@ -0,0 +1,139 @@
import { DarkTheme, DefaultTheme, ThemeProvider } from '@react-navigation/native';
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import { useEffect, useState, useRef } from 'react';
import * as SplashScreen from 'expo-splash-screen';
import { useFonts } from 'expo-font';
import { Manrope_700Bold, Manrope_400Regular } from '@expo-google-fonts/manrope';
import { PlusJakartaSans_500Medium, PlusJakartaSans_700Bold } from '@expo-google-fonts/plus-jakarta-sans';
import { initDatabase } from '@/src/db/database';
import { useTheme, BatsirThemeProvider } from '@/src/hooks/useTheme';
import { useSync } from '@/src/hooks/useSync';
import { AnimatedSplashScreen } from '@/src/components/animated-splash-screen';
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { configureReanimatedLogger, ReanimatedLogLevel } from 'react-native-reanimated';
// Disable strict mode for Reanimated logger to suppress warnings about reading value during render,
// which can be triggered by the React Compiler or third-party libraries in Reanimated 4.
configureReanimatedLogger({
level: ReanimatedLogLevel.warn,
strict: false,
});
export {
// Catch any errors thrown by the Layout component.
ErrorBoundary,
} from 'expo-router';
export const unstable_settings = {
// Ensure that reloading on `/modal` keeps a back button present.
initialRouteName: '(tabs)',
};
// Prevent the splash screen from auto-hiding before asset loading is complete.
SplashScreen.preventAutoHideAsync().catch(() => {
/* Reloading in development can sometimes cause this to fail, ignore it */
});
export default function RootLayout() {
const [dbLoaded, setDbLoaded] = useState(false);
const [fontsLoaded, fontError] = useFonts({
Manrope_700Bold,
Manrope_400Regular,
PlusJakartaSans_500Medium,
PlusJakartaSans_700Bold,
});
useEffect(() => {
initDatabase()
.then(() => setDbLoaded(true))
.catch((err) => console.error('Database initialization failed:', err));
}, []);
useEffect(() => {
if (fontError) throw fontError;
}, [fontError]);
if (!fontsLoaded || !dbLoaded) {
return null;
}
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<BatsirThemeProvider>
<SyncWrapper />
<RootLayoutContent />
</BatsirThemeProvider>
</GestureHandlerRootView>
);
}
function SyncWrapper() {
useSync();
return null;
}
function RootLayoutContent() {
const { isLoaded, colors } = useTheme();
const [animationFinished, setAnimationFinished] = useState(false);
const splashHidden = useRef(false);
useEffect(() => {
if (isLoaded && !splashHidden.current) {
const hideSplash = async () => {
try {
splashHidden.current = true;
// Verify if we can actually hide it
await SplashScreen.hideAsync();
} catch (e) {
// If it fails, it usually means it's already hidden or not registered
console.log("Splash hide safely ignored:", e);
}
};
const timer = setTimeout(hideSplash, 200);
return () => clearTimeout(timer);
}
}, [isLoaded]);
if (!isLoaded) {
return null;
}
return (
<>
<RootLayoutNav />
{!animationFinished && (
<AnimatedSplashScreen
onAnimationFinish={() => setAnimationFinished(true)}
backgroundColor={colors.background}
/>
)}
</>
);
}
function RootLayoutNav() {
const { colorScheme } = useTheme();
return (
<ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
<Stack>
<Stack.Screen name="index" options={{ headerShown: false }} />
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: 'modal', headerShown: false }} />
<Stack.Screen name="menu" options={{ presentation: 'modal', headerShown: false }} />
<Stack.Screen name="add-habit" options={{ presentation: 'modal', headerShown: false }} />
<Stack.Screen name="add-shortcut" options={{ presentation: 'modal', headerShown: false }} />
<Stack.Screen name="add-task" options={{ presentation: 'modal', headerShown: false }} />
{/* Secondary Screens now in root stack */}
<Stack.Screen name="(tabs)/history" options={{ headerShown: false, title: 'History' }} />
<Stack.Screen name="(tabs)/sprint" options={{ headerShown: false, title: 'Sprint' }} />
<Stack.Screen name="(tabs)/aa_ai" options={{ headerShown: false, title: 'Assistant' }} />
<Stack.Screen name="(tabs)/hh_habits" options={{ headerShown: false, title: 'Habits' }} />
</Stack>
<StatusBar style={colorScheme === 'dark' ? 'light' : 'dark'} />
</ThemeProvider>
);
}

571
src/app/add-habit.tsx Normal file
View File

@ -0,0 +1,571 @@
import { FONTS, ROUNDNESS, SPACING } from "@/src/constants/Theme";
import { useAuth } from "@/src/hooks/useAuth";
import { useTheme } from "@/src/hooks/useTheme";
import { useData } from "@/src/hooks/useData";
import { performMutation } from "@/src/lib/sync";
import * as Haptics from "expo-haptics";
import { useRouter } from "expo-router";
import {
Calendar,
Clock,
Repeat,
Save,
Sparkles,
X,
MapPin,
Anchor,
ChevronDown,
} from "lucide-react-native";
import React, { useMemo, useState } from "react";
import {
ActivityIndicator,
Alert,
ScrollView,
StyleSheet,
Switch,
Text,
TextInput,
TouchableOpacity,
View,
Modal,
KeyboardAvoidingView,
Platform
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { TimeInput } from "@/src/components/ui/TimeInput";
const FREQUENCIES = [
{ label: "Daily", value: "daily" },
{ label: "Weekly", value: "weekly" },
{ label: "Monthly", value: "monthly" },
];
const TIME_PRESETS = [
{ label: "Morning", time: "08:00" },
{ label: "Noon", time: "12:00" },
{ label: "Evening", time: "18:00" },
{ label: "Night", time: "22:00" },
];
export default function AddHabitScreen() {
const { colors } = useTheme();
const { user } = useAuth();
const styles = useMemo(() => createStyles(colors), [colors]);
const router = useRouter();
const userId = user?.id || 'guest';
const { data: existingHabits } = useData<{id: string, title: string}>(
'SELECT id, title FROM habits WHERE is_active = 1 AND (user_id = ? OR user_id IS NULL)',
[userId]
);
const [title, setTitle] = useState("");
const [frequency, setFrequency] = useState("daily");
const [preferredTime, setPreferredTime] = useState("08:00");
const [location, setLocation] = useState("");
const [twoMinuteVersion, setTwoMinuteVersion] = useState("");
const [anchorHabitId, setAnchorHabitId] = useState<string | null>(null);
const [weekendFlexibility, setWeekendFlexibility] = useState(false);
const [loading, setLoading] = useState(false);
const [showAnchorModal, setShowAnchorModal] = useState(false);
const selectedAnchor = useMemo(() =>
existingHabits.find(h => h.id === anchorHabitId),
[existingHabits, anchorHabitId]
);
const handleSave = async () => {
if (!title) {
Alert.alert("Error", "Please provide a title for your habit");
return;
}
setLoading(true);
try {
await performMutation("habits", "INSERT", {
id: Math.random().toString(36).substring(7),
user_id: userId,
title,
frequency,
preferred_time: preferredTime,
location: location,
two_minute_version: twoMinuteVersion,
anchor_habit_id: anchorHabitId,
weekend_flexibility: weekendFlexibility ? 1 : 0,
is_active: 1,
});
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
router.back();
} catch (error) {
console.error("Failed to save habit:", error);
Alert.alert("Error", "Failed to save habit");
} finally {
setLoading(false);
}
};
return (
<View style={styles.container}>
<SafeAreaView style={styles.safeArea}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()}>
<X size={24} color={colors.onSurface} />
</TouchableOpacity>
<Text style={styles.headerTitle}>New Habit</Text>
<TouchableOpacity onPress={handleSave} disabled={loading}>
{loading ? (
<ActivityIndicator size="small" color={colors.primary} />
) : (
<Save size={24} color={colors.primary} />
)}
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled">
<View style={styles.section}>
<Text style={styles.sectionLabel}>HABIT ARCHITECT</Text>
{/* Title */}
<View style={styles.inputGroup}>
<Text style={styles.label}>WHAT IS THE HABIT?</Text>
<View style={styles.inputWrapper}>
<Sparkles
size={20}
color={colors.primary}
style={styles.inputIcon}
/>
<TextInput
style={styles.input}
placeholder="e.g. Meditate"
placeholderTextColor={colors.outline}
value={title}
onChangeText={setTitle}
/>
</View>
</View>
{/* Implementation Intentions: Location */}
<View style={styles.inputGroup}>
<Text style={styles.label}>WHERE WILL YOU DO IT? (LOCATION)</Text>
<View style={styles.inputWrapper}>
<MapPin
size={20}
color={colors.primary}
style={styles.inputIcon}
/>
<TextInput
style={styles.input}
placeholder="e.g. My study, the gym, on the sofa"
placeholderTextColor={colors.outline}
value={location}
onChangeText={setLocation}
/>
</View>
</View>
{/* Small Start: Two-Minute Version */}
<View style={styles.inputGroup}>
<Text style={styles.label}>THE TWO-MINUTE VERSION (START SMALL)</Text>
<View style={styles.inputWrapper}>
<Sparkles
size={20}
color={colors.secondary}
style={styles.inputIcon}
/>
<TextInput
style={styles.input}
placeholder="e.g. Close my eyes for 1 minute"
placeholderTextColor={colors.outline}
value={twoMinuteVersion}
onChangeText={setTwoMinuteVersion}
/>
</View>
<Text style={styles.hintText}>"Optimize for the starting line, not the finish line."</Text>
</View>
{/* Habit Stacking: Anchor Habit */}
<View style={styles.inputGroup}>
<Text style={styles.label}>STACK IT: AFTER I...</Text>
<TouchableOpacity
style={styles.inputWrapper}
onPress={() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
setShowAnchorModal(true);
}}
>
<Anchor
size={20}
color={colors.primary}
style={styles.inputIcon}
/>
<Text style={[styles.input, { textAlignVertical: 'center', paddingTop: 14 }]}>
{selectedAnchor ? selectedAnchor.title : "Choose an anchor habit"}
</Text>
<ChevronDown size={20} color={colors.outline} />
</TouchableOpacity>
</View>
{/* Time */}
<View style={styles.inputGroup}>
<TimeInput
label="WHAT TIME?"
value={preferredTime}
onChange={setPreferredTime}
/>
<View style={styles.presetsGrid}>
{TIME_PRESETS.map((p) => (
<TouchableOpacity
key={p.time}
style={[
styles.presetBtn,
preferredTime === p.time && {
backgroundColor: colors.primaryContainer,
},
]}
onPress={() => {
setPreferredTime(p.time);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
>
<Text
style={[
styles.presetText,
preferredTime === p.time && {
color: colors.primary,
fontFamily: FONTS.labelSm,
},
]}
>
{p.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* Frequency */}
<View style={styles.inputGroup}>
<Text style={styles.label}>FREQUENCY</Text>
<View style={styles.frequencyGrid}>
{FREQUENCIES.map((f) => (
<TouchableOpacity
key={f.value}
style={[
styles.frequencyOption,
frequency === f.value && {
backgroundColor: colors.primary,
},
]}
onPress={() => setFrequency(f.value)}
>
<Repeat
size={16}
color={
frequency === f.value
? colors.onPrimary
: colors.onSurfaceVariant
}
/>
<Text
style={[
styles.frequencyText,
frequency === f.value && {
color: colors.onPrimary,
fontFamily: FONTS.labelSm,
},
]}
>
{f.label}
</Text>
</TouchableOpacity>
))}
</View>
</View>
{/* Weekend Flexibility */}
<View style={styles.switchRow}>
<View style={styles.switchContent}>
<View style={styles.rowAlign}>
<Calendar
size={18}
color={colors.primary}
style={{ marginRight: 8 }}
/>
<Text style={styles.switchTitle}>Weekend Flexibility</Text>
</View>
<Text style={styles.switchDesc}>
Allow skipping on weekends without breaking streaks.
</Text>
</View>
<Switch
value={weekendFlexibility}
onValueChange={setWeekendFlexibility}
trackColor={{
false: colors.surfaceVariant,
true: colors.primary + "80",
}}
thumbColor={
weekendFlexibility ? colors.primary : colors.outline
}
/>
</View>
</View>
<View style={styles.infoCard}>
<Text style={styles.infoText}>
"Every action you take is a vote for the type of person you wish to become."
</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
{/* Anchor Habit Modal */}
<Modal
visible={showAnchorModal}
animationType="slide"
transparent={true}
onRequestClose={() => setShowAnchorModal(false)}
>
<View style={styles.modalOverlay}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={[styles.modalContent, { backgroundColor: colors.surface }]}
>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Select Anchor Habit</Text>
<TouchableOpacity onPress={() => setShowAnchorModal(false)}>
<X size={24} color={colors.onSurface} />
</TouchableOpacity>
</View>
<ScrollView style={styles.anchorList}>
<TouchableOpacity
style={[styles.anchorItem, !anchorHabitId && { backgroundColor: colors.primaryContainer }]}
onPress={() => {
setAnchorHabitId(null);
setShowAnchorModal(false);
}}
>
<Text style={[styles.anchorItemText, !anchorHabitId && { color: colors.primary, fontFamily: FONTS.labelSm }]}>
No Anchor (Independent)
</Text>
</TouchableOpacity>
{existingHabits.map((habit) => (
<TouchableOpacity
key={habit.id}
style={[styles.anchorItem, anchorHabitId === habit.id && { backgroundColor: colors.primaryContainer }]}
onPress={() => {
setAnchorHabitId(habit.id);
setShowAnchorModal(false);
}}
>
<Text style={[styles.anchorItemText, anchorHabitId === habit.id && { color: colors.primary, fontFamily: FONTS.labelSm }]}>
{habit.title}
</Text>
</TouchableOpacity>
))}
</ScrollView>
</KeyboardAvoidingView>
</View>
</Modal>
</View>
);
}
const createStyles = (colors: any) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
safeArea: {
flex: 1,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: SPACING.lg,
borderBottomWidth: 1,
borderBottomColor: colors.outlineVariant + "4D",
},
headerTitle: {
fontFamily: FONTS.headline,
fontSize: 20,
color: colors.onSurface,
},
content: {
padding: SPACING.lg,
},
section: {
gap: SPACING.xl,
},
sectionLabel: {
fontFamily: FONTS.labelSm,
fontSize: 11,
color: colors.primary,
letterSpacing: 1.5,
marginBottom: 4,
},
inputGroup: {
gap: 8,
},
label: {
fontFamily: FONTS.labelSm,
fontSize: 11,
color: colors.outline,
},
inputWrapper: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.surface,
borderRadius: ROUNDNESS.md,
borderWidth: 1,
borderColor: colors.outlineVariant + "4D",
paddingHorizontal: 12,
},
inputIcon: {
marginRight: 10,
},
input: {
flex: 1,
height: 52,
fontFamily: FONTS.body,
fontSize: 16,
color: colors.onSurface,
},
hintText: {
fontFamily: FONTS.body,
fontSize: 12,
color: colors.onSurfaceVariant,
fontStyle: 'italic',
marginTop: 2,
},
presetsGrid: {
flexDirection: "row",
gap: 8,
marginTop: 4,
},
presetBtn: {
flex: 1,
paddingVertical: 8,
borderRadius: ROUNDNESS.sm,
backgroundColor: colors.surfaceVariant + "4D",
alignItems: "center",
borderWidth: 1,
borderColor: colors.outlineVariant + "33",
},
presetText: {
fontFamily: FONTS.label,
fontSize: 10,
color: colors.onSurfaceVariant,
},
frequencyGrid: {
flexDirection: "row",
gap: 10,
},
frequencyOption: {
flex: 1,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 8,
backgroundColor: colors.surface,
paddingVertical: 12,
borderRadius: ROUNDNESS.md,
borderWidth: 1,
borderColor: colors.outlineVariant + "4D",
},
frequencyText: {
fontFamily: FONTS.label,
fontSize: 13,
color: colors.onSurfaceVariant,
},
switchRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
backgroundColor: colors.surface,
padding: SPACING.lg,
borderRadius: ROUNDNESS.lg,
borderWidth: 1,
borderColor: colors.outlineVariant + "4D",
},
switchContent: {
flex: 1,
paddingRight: 16,
},
rowAlign: {
flexDirection: "row",
alignItems: "center",
marginBottom: 4,
},
switchTitle: {
fontFamily: FONTS.headline,
fontSize: 16,
color: colors.onSurface,
},
switchDesc: {
fontFamily: FONTS.body,
fontSize: 13,
color: colors.onSurfaceVariant,
lineHeight: 18,
},
infoCard: {
backgroundColor: colors.primaryContainer + "40",
padding: SPACING.lg,
borderRadius: ROUNDNESS.lg,
borderWidth: 1,
borderColor: colors.primaryContainer,
marginTop: SPACING.xxl,
alignItems: "center",
},
infoText: {
fontFamily: FONTS.body,
fontSize: 14,
color: colors.onSurfaceVariant,
fontStyle: "italic",
textAlign: "center",
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'flex-end',
},
modalContent: {
borderTopLeftRadius: ROUNDNESS.xl,
borderTopRightRadius: ROUNDNESS.xl,
paddingBottom: 40,
maxHeight: '70%',
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: SPACING.lg,
borderBottomWidth: 1,
borderBottomColor: colors.outlineVariant + '4D',
},
modalTitle: {
fontFamily: FONTS.headline,
fontSize: 18,
color: colors.onSurface,
},
anchorList: {
padding: SPACING.md,
},
anchorItem: {
padding: SPACING.lg,
borderRadius: ROUNDNESS.lg,
marginBottom: 8,
},
anchorItemText: {
fontFamily: FONTS.body,
fontSize: 16,
color: colors.onSurface,
},
});

192
src/app/add-shortcut.tsx Normal file
View File

@ -0,0 +1,192 @@
import React, { useState, useMemo } from 'react';
import { StyleSheet, View, Text, TextInput, TouchableOpacity, ScrollView, Alert, ActivityIndicator, KeyboardAvoidingView, Platform } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { X, Save, Link as LinkIcon, Type, Sparkles } from 'lucide-react-native';
import { SPACING, FONTS, ROUNDNESS } from '@/src/constants/Theme';
import { useTheme } from '@/src/hooks/useTheme';
import { performMutation } from '@/src/lib/sync';
import { useRouter } from 'expo-router';
import { useAuth } from '@/src/hooks/useAuth';
export default function AddShortcutScreen() {
const { colors } = useTheme();
const { user } = useAuth();
const styles = useMemo(() => createStyles(colors), [colors]);
const router = useRouter();
const [title, setTitle] = useState('');
const [url, setUrl] = useState('');
const [loading, setLoading] = useState(false);
const handleSave = async () => {
if (!title || !url) {
Alert.alert('Error', 'Please provide both a title and a URL');
return;
}
setLoading(true);
try {
await performMutation('shortcuts', 'INSERT', {
id: Math.random().toString(36).substring(7),
user_id: user?.id || 'guest',
title,
url: url.startsWith('http') ? url : `https://${url}`,
icon: 'sparkles',
});
router.back();
} catch (error) {
console.error('Failed to save shortcut:', error);
Alert.alert('Error', 'Failed to save shortcut');
} finally {
setLoading(false);
}
};
return (
<View style={styles.container}>
<SafeAreaView style={styles.safeArea}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()}>
<X size={24} color={colors.onSurface} />
</TouchableOpacity>
<Text style={styles.headerTitle}>Add Shortcut</Text>
<TouchableOpacity onPress={handleSave} disabled={loading}>
{loading ? (
<ActivityIndicator size="small" color={colors.primary} />
) : (
<Save size={24} color={colors.primary} />
)}
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
<View style={styles.section}>
<Text style={styles.sectionLabel}>SHORTCUT DETAILS</Text>
<View style={styles.inputGroup}>
<Text style={styles.label}>DISPLAY NAME</Text>
<View style={styles.inputWrapper}>
<Type size={20} color={colors.outline} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="e.g. Project Dashboard"
placeholderTextColor={colors.outline}
value={title}
onChangeText={setTitle}
/>
</View>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>URL OR APP LINK</Text>
<View style={styles.inputWrapper}>
<LinkIcon size={20} color={colors.outline} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="e.g. linear.app/my-team"
placeholderTextColor={colors.outline}
value={url}
onChangeText={setUrl}
autoCapitalize="none"
keyboardType="url"
/>
</View>
</View>
</View>
<View style={styles.infoCard}>
<Sparkles size={20} color={colors.primary} />
<Text style={styles.infoText}>
Shortcuts will appear in your Ecosystem Tools section for quick access to your external workspaces.
</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
</View>
);
}
const createStyles = (colors: any) => StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
safeArea: {
flex: 1,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: SPACING.lg,
borderBottomWidth: 1,
borderBottomColor: colors.outlineVariant + '4D',
},
headerTitle: {
fontFamily: FONTS.headline,
fontSize: 20,
color: colors.onSurface,
},
content: {
padding: SPACING.lg,
},
section: {
gap: SPACING.lg,
},
sectionLabel: {
fontFamily: FONTS.labelSm,
fontSize: 11,
color: colors.primary,
letterSpacing: 1.5,
marginBottom: 4,
},
inputGroup: {
gap: 8,
},
label: {
fontFamily: FONTS.labelSm,
fontSize: 11,
color: colors.outline,
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.surface,
borderRadius: ROUNDNESS.md,
borderWidth: 1,
borderColor: colors.outlineVariant + '4D',
paddingHorizontal: 12,
},
inputIcon: {
marginRight: 10,
},
input: {
flex: 1,
height: 52,
fontFamily: FONTS.body,
fontSize: 16,
color: colors.onSurface,
},
infoCard: {
flexDirection: 'row',
backgroundColor: colors.primaryContainer + '40',
padding: SPACING.lg,
borderRadius: ROUNDNESS.lg,
gap: 16,
borderWidth: 1,
borderColor: colors.primaryContainer,
marginTop: SPACING.xxl,
},
infoText: {
flex: 1,
fontFamily: FONTS.body,
fontSize: 14,
color: colors.onSurfaceVariant,
lineHeight: 20,
},
});

280
src/app/add-task.tsx Normal file
View File

@ -0,0 +1,280 @@
import React, { useState, useMemo } from 'react';
import { StyleSheet, View, Text, TextInput, TouchableOpacity, ScrollView, Alert, ActivityIndicator, KeyboardAvoidingView, Platform } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { X, Save, Type, Tag, Target } from 'lucide-react-native';
import { SPACING, FONTS, ROUNDNESS } from '@/src/constants/Theme';
import { useTheme } from '@/src/hooks/useTheme';
import { performMutation } from '@/src/lib/sync';
import { useRouter } from 'expo-router';
import { useAuth } from '@/src/hooks/useAuth';
import { getDb } from '@/src/db/database';
import * as Haptics from 'expo-haptics';
import { getLocalDateString } from '@/src/lib/date-utils';
const toMinutes = (time: string) => {
const [h, m] = time.split(':').map(Number);
return h * 60 + m;
};
const toTimeString = (minutes: number) => {
const h = Math.floor(minutes / 60);
const m = minutes % 60;
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
};
export default function AddTaskScreen() {
const { colors, sprintDuration } = useTheme();
const { user } = useAuth();
const styles = useMemo(() => createStyles(colors), [colors]);
const router = useRouter();
const [title, setTitle] = useState('');
const [tag, setTag] = useState('');
const [sessions, setSessions] = useState('1');
const [loading, setLoading] = useState(false);
const handleSave = async () => {
if (!title.trim()) {
Alert.alert('Error', 'Please provide a title for your task');
return;
}
const estSessions = parseInt(sessions) || 1;
const taskId = Math.random().toString(36).substring(7);
const userId = user?.id || 'guest';
const today = getLocalDateString();
setLoading(true);
try {
// 1. Save the Task
await performMutation('tasks', 'INSERT', {
id: taskId,
user_id: userId,
title: title.trim(),
status: 'todo',
estimated_sessions: estSessions,
completed_sessions: 0,
tag: tag.trim() || 'General',
todos: '[]',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
});
// 2. Automatically Add to Schedule
const db = await getDb();
const existingSchedule = await db.getFirstAsync<{id: string, time_blocks: string}>(
"SELECT id, time_blocks FROM schedules WHERE date = ? AND (user_id = ? OR user_id IS NULL)",
[today, userId]
);
let blocks = [];
let scheduleId = Math.random().toString(36).substring(7);
if (existingSchedule) {
try {
blocks = JSON.parse(existingSchedule.time_blocks);
scheduleId = existingSchedule.id;
} catch (e) { blocks = []; }
}
// Find first available slot after 9 AM
let currentStart = toMinutes("09:00");
const duration = sprintDuration;
const sortedBlocks = [...blocks].sort((a, b) => a.start.localeCompare(b.start));
for (const block of sortedBlocks) {
const blockStart = toMinutes(block.start);
if (blockStart >= currentStart + duration) {
break;
}
currentStart = Math.max(currentStart, toMinutes(block.end));
}
const newBlock = {
start: toTimeString(currentStart),
end: toTimeString(currentStart + duration),
task: title.trim(),
type: 'deep-work',
todos: []
};
const updatedBlocks = [...blocks, newBlock].sort((a, b) => a.start.localeCompare(b.start));
await performMutation('schedules', existingSchedule ? 'UPDATE' : 'INSERT', {
id: scheduleId,
user_id: userId,
date: today,
time_blocks: JSON.stringify(updatedBlocks)
});
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
router.back();
} catch (error) {
console.error('Failed to save task/schedule:', error);
Alert.alert('Error', 'Failed to save task');
} finally {
setLoading(false);
}
};
return (
<View style={styles.container}>
<SafeAreaView style={styles.safeArea}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
<View style={styles.header}>
<TouchableOpacity onPress={() => router.back()}>
<X size={24} color={colors.onSurface} />
</TouchableOpacity>
<Text style={styles.headerTitle}>New Task</Text>
<TouchableOpacity onPress={handleSave} disabled={loading}>
{loading ? (
<ActivityIndicator size="small" color={colors.primary} />
) : (
<Save size={24} color={colors.primary} />
)}
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={styles.content} keyboardShouldPersistTaps="handled">
<View style={styles.section}>
<Text style={styles.sectionLabel}>TASK DETAILS</Text>
<View style={styles.inputGroup}>
<Text style={styles.label}>WHAT ARE YOU WORKING ON?</Text>
<View style={styles.inputWrapper}>
<Type size={20} color={colors.outline} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="e.g. Audit Typography Hierarchy"
placeholderTextColor={colors.outline}
value={title}
onChangeText={setTitle}
/>
</View>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>TAG / CATEGORY</Text>
<View style={styles.inputWrapper}>
<Tag size={20} color={colors.outline} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="e.g. Design, Coding, Admin"
placeholderTextColor={colors.outline}
value={tag}
onChangeText={setTag}
/>
</View>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>ESTIMATED SESSIONS (25m each)</Text>
<View style={styles.inputWrapper}>
<Target size={20} color={colors.outline} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="1"
placeholderTextColor={colors.outline}
value={sessions}
onChangeText={setSessions}
keyboardType="number-pad"
/>
</View>
</View>
</View>
<View style={styles.infoCard}>
<Text style={styles.infoText}>
Break large tasks into small, actionable chunks to maintain flow and momentum.
</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
</View>
);
}
const createStyles = (colors: any) => StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
safeArea: {
flex: 1,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
padding: SPACING.lg,
borderBottomWidth: 1,
borderBottomColor: colors.outlineVariant + '4D',
},
headerTitle: {
fontFamily: FONTS.headline,
fontSize: 20,
color: colors.onSurface,
},
content: {
padding: SPACING.lg,
},
section: {
gap: SPACING.xl,
},
sectionLabel: {
fontFamily: FONTS.labelSm,
fontSize: 11,
color: colors.primary,
letterSpacing: 1.5,
marginBottom: 4,
},
inputGroup: {
gap: 8,
},
label: {
fontFamily: FONTS.labelSm,
fontSize: 11,
color: colors.outline,
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.surface,
borderRadius: ROUNDNESS.md,
borderWidth: 1,
borderColor: colors.outlineVariant + '4D',
paddingHorizontal: 12,
},
inputIcon: {
marginRight: 10,
},
input: {
flex: 1,
height: 52,
fontFamily: FONTS.body,
fontSize: 16,
color: colors.onSurface,
},
infoCard: {
backgroundColor: colors.primaryContainer + '40',
padding: SPACING.lg,
borderRadius: ROUNDNESS.lg,
borderWidth: 1,
borderColor: colors.primaryContainer,
marginTop: SPACING.xxl,
alignItems: 'center',
},
infoText: {
fontFamily: FONTS.body,
fontSize: 14,
color: colors.onSurfaceVariant,
textAlign: 'center',
lineHeight: 20,
},
});

23
src/app/index.tsx Normal file
View File

@ -0,0 +1,23 @@
import { Redirect } from 'expo-router';
import { useAuth } from '../hooks/useAuth';
import { View, ActivityIndicator } from 'react-native';
import { useTheme } from '../hooks/useTheme';
export default function Index() {
const { isAuthenticated, loading } = useAuth();
const { colors } = useTheme();
if (loading) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: colors.background }}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
);
}
if (!isAuthenticated) {
return <Redirect href="/login" />;
}
return <Redirect href="/(tabs)/" />;
}

313
src/app/login.tsx Normal file
View File

@ -0,0 +1,313 @@
import React, { useState, useMemo } from 'react';
import { StyleSheet, View, Text, TextInput, TouchableOpacity, ScrollView, Image, ActivityIndicator, Alert, KeyboardAvoidingView, Platform } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Mail, Lock, LogIn, UserPlus, Sparkles, ArrowRight } from 'lucide-react-native';
import { SPACING, FONTS, ROUNDNESS } from '@/src/constants/Theme';
import { useTheme } from '@/src/hooks/useTheme';
import { supabase } from '@/src/lib/supabase';
import { useRouter } from 'expo-router';
export default function LoginScreen() {
const { colors } = useTheme();
const styles = useMemo(() => createStyles(colors), [colors]);
const router = useRouter();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const [isSignUp, setIsSignUp] = useState(false);
async function handleAuth() {
if (!email || !password) {
Alert.alert('Error', 'Please fill in all fields');
return;
}
setLoading(true);
try {
if (isSignUp) {
const { error } = await supabase.auth.signUp({
email,
password,
});
if (error) throw error;
Alert.alert('Success', 'Check your email for the confirmation link!');
} else {
const { data: authData, error: signInError } = await supabase.auth.signInWithPassword({
email,
password,
});
if (signInError) throw signInError;
// Ensure profile exists
if (authData?.user) {
const { error: profileError } = await supabase
.from('profiles')
.upsert({
id: authData.user.id,
email: authData.user.email,
updated_at: new Date().toISOString()
});
if (profileError) {
console.error('Error ensuring profile exists:', profileError.message);
}
}
router.replace('/(tabs)/');
}
} catch (error: any) {
Alert.alert('Auth Error', error.message);
} finally {
setLoading(false);
}
}
return (
<View style={styles.container}>
<SafeAreaView style={styles.safeArea}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
<ScrollView contentContainerStyle={styles.scrollContent} keyboardShouldPersistTaps="handled">
<View style={styles.header}>
<View style={styles.logoContainer}>
<Image
source={require('@/assets/images/Artboard 1 logo.png')}
style={[styles.logoImage, { tintColor: colors.primary }]}
resizeMode="contain"
/>
</View>
<Text style={styles.title}>{isSignUp ? 'Join Batsir' : 'Welcome Back'}</Text>
<Text style={styles.subtitle}>
{isSignUp ? 'Start your journey to atomic efficiency.' : 'Find your flow state and continue building.'}
</Text>
</View>
<View style={styles.form}>
<View style={styles.inputGroup}>
<Text style={styles.label}>EMAIL ADDRESS</Text>
<View style={styles.inputWrapper}>
<Mail size={20} color={colors.outline} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="name@example.com"
placeholderTextColor={colors.outline}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
/>
</View>
</View>
<View style={styles.inputGroup}>
<Text style={styles.label}>PASSWORD</Text>
<View style={styles.inputWrapper}>
<Lock size={20} color={colors.outline} style={styles.inputIcon} />
<TextInput
style={styles.input}
placeholder="Your secure password"
placeholderTextColor={colors.outline}
value={password}
onChangeText={setPassword}
secureTextEntry
/>
</View>
</View>
<TouchableOpacity
style={[styles.primaryBtn, loading && { opacity: 0.7 }]}
onPress={handleAuth}
disabled={loading}
>
{loading ? (
<ActivityIndicator color={colors.onPrimary} />
) : (
<>
<Text style={styles.primaryBtnText}>{isSignUp ? 'Create Account' : 'Sign In'}</Text>
{isSignUp ? <UserPlus size={18} color={colors.onPrimary} /> : <LogIn size={18} color={colors.onPrimary} />}
</>
)}
</TouchableOpacity>
<TouchableOpacity
style={styles.switchBtn}
onPress={() => setIsSignUp(!isSignUp)}
>
<Text style={styles.switchBtnText}>
{isSignUp ? 'Already have an account? Sign In' : "Don't have an account? Sign Up"}
</Text>
</TouchableOpacity>
</View>
{!isSignUp && (
<View style={styles.featureHighlight}>
<View style={styles.featureIcon}>
<Sparkles size={20} color={colors.primary} />
</View>
<View style={styles.featureContent}>
<Text style={styles.featureTitle}>Batsirai Integration</Text>
<Text style={styles.featureDesc}>Your personal flow assistant is ready to help you optimize your schedule.</Text>
</View>
</View>
)}
<TouchableOpacity style={styles.guestBtn} onPress={() => router.replace('/(tabs)/')}>
<Text style={styles.guestBtnText}>Continue as Guest</Text>
<ArrowRight size={14} color={colors.outline} />
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
</View>
);
}
const createStyles = (colors: any) => StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
safeArea: {
flex: 1,
},
scrollContent: {
padding: SPACING.xl,
flexGrow: 1,
justifyContent: 'center',
},
header: {
marginBottom: SPACING.xxl,
alignItems: 'center',
},
logoContainer: {
marginBottom: SPACING.lg,
},
logoImage: {
height: 48,
width: 180,
},
title: {
fontFamily: FONTS.headline,
fontSize: 32,
color: colors.onSurface,
textAlign: 'center',
marginBottom: 8,
},
subtitle: {
fontFamily: FONTS.body,
fontSize: 16,
color: colors.onSurfaceVariant,
textAlign: 'center',
lineHeight: 22,
paddingHorizontal: 20,
},
form: {
gap: SPACING.lg,
marginBottom: SPACING.xl,
},
inputGroup: {
gap: 8,
},
label: {
fontFamily: FONTS.labelSm,
fontSize: 11,
color: colors.primary,
letterSpacing: 1.5,
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: colors.surface,
borderRadius: ROUNDNESS.md,
borderWidth: 1,
borderColor: colors.outlineVariant + '4D',
paddingHorizontal: 12,
},
inputIcon: {
marginRight: 10,
},
input: {
flex: 1,
height: 52,
fontFamily: FONTS.body,
fontSize: 16,
color: colors.onSurface,
},
primaryBtn: {
backgroundColor: colors.primary,
height: 56,
borderRadius: ROUNDNESS.full,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 12,
marginTop: 10,
elevation: 2,
shadowColor: colors.primary,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.2,
shadowRadius: 8,
},
primaryBtnText: {
color: colors.onPrimary,
fontFamily: FONTS.labelSm,
fontSize: 16,
},
switchBtn: {
alignItems: 'center',
padding: 10,
},
switchBtnText: {
fontFamily: FONTS.label,
fontSize: 14,
color: colors.primary,
},
featureHighlight: {
flexDirection: 'row',
backgroundColor: colors.primaryContainer + '40',
padding: SPACING.lg,
borderRadius: ROUNDNESS.lg,
gap: 16,
borderWidth: 1,
borderColor: colors.primaryContainer,
marginBottom: SPACING.xl,
},
featureIcon: {
width: 40,
height: 40,
borderRadius: 20,
backgroundColor: colors.surface,
alignItems: 'center',
justifyContent: 'center',
},
featureContent: {
flex: 1,
},
featureTitle: {
fontFamily: FONTS.headline,
fontSize: 16,
color: colors.onSurface,
marginBottom: 2,
},
featureDesc: {
fontFamily: FONTS.body,
fontSize: 13,
color: colors.onSurfaceVariant,
lineHeight: 18,
},
guestBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
padding: 10,
},
guestBtnText: {
fontFamily: FONTS.label,
fontSize: 14,
color: colors.outline,
},
});

260
src/app/menu.tsx Normal file
View File

@ -0,0 +1,260 @@
import { FONTS, ROUNDNESS, SPACING } from "@/src/constants/Theme";
import { useTheme } from "@/src/hooks/useTheme";
import * as Haptics from "expo-haptics";
import { useRouter } from "expo-router";
import {
Activity,
ChevronRight,
Clock,
Save,
Sparkles,
Target,
X,
History
} from "lucide-react-native";
import React, { useMemo, useState } from "react";
import {
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
export default function MenuModal() {
const {
colors,
identityAnchor,
updateIdentityAnchor,
focusGoal,
updateFocusGoal,
} = useTheme();
const router = useRouter();
const styles = useMemo(() => createStyles(colors), [colors]);
const [newAnchor, setNewAnchor] = useState(identityAnchor);
const [newGoal, setNewGoal] = useState(focusGoal.toString());
const handleSaveSettings = async () => {
try {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
await updateIdentityAnchor(newAnchor);
const goalNum = parseInt(newGoal);
if (!isNaN(goalNum)) {
await updateFocusGoal(goalNum);
}
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
} catch (e) {
console.error(e);
}
};
const menuItems = [
{
title: "Sprint",
subtitle: "Start a focused work session",
icon: <Clock size={22} color={colors.primary} />,
route: "/sprint" as const,
},
{
title: "History",
subtitle: "Browse past activities & inventory",
icon: <History size={22} color={colors.primary} />,
route: "/history" as const,
},
{
title: "Assistant",
subtitle: "Chat with Batsirai AI",
icon: <Sparkles size={22} color={colors.primary} />,
route: "/aa_ai" as const,
},
{
title: "Habits",
subtitle: "Manage your daily evolution",
icon: <Activity size={22} color={colors.primary} />,
route: "/hh_habits" as const,
},
];
const navigateTo = (route: any) => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
router.replace(route);
};
return (
<View style={styles.container}>
<SafeAreaView style={styles.safeArea}>
<View style={styles.header}>
<Text style={styles.headerTitle}>Navigation</Text>
<TouchableOpacity
onPress={() => router.back()}
style={styles.closeBtn}
>
<X size={24} color={colors.onSurface} />
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={styles.content}>
<View style={styles.section}>
<Text style={styles.sectionLabel}>MAIN TOOLS</Text>
{menuItems.map((item, index) => (
<TouchableOpacity
key={index}
style={styles.menuItem}
onPress={() => navigateTo(item.route)}
>
<View style={styles.menuItemLeft}>
<View style={styles.iconBg}>{item.icon}</View>
<View>
<Text style={styles.menuItemTitle}>{item.title}</Text>
<Text style={styles.menuItemSubtitle}>{item.subtitle}</Text>
</View>
</View>
<ChevronRight size={18} color={colors.outline} />
</TouchableOpacity>
))}
</View>
<View style={styles.footer}>
<Text style={styles.footerText}>BATSIRAI / ECOSYSTEM</Text>
</View>
</ScrollView>
</SafeAreaView>
</View>
);
}
const createStyles = (colors: any) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
safeArea: {
flex: 1,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: SPACING.lg,
borderBottomWidth: 1,
borderBottomColor: colors.outlineVariant + "4D",
},
headerTitle: {
fontFamily: FONTS.headline,
fontSize: 20,
color: colors.onSurface,
},
closeBtn: {
padding: 4,
},
content: {
padding: SPACING.lg,
},
section: {
gap: 8,
},
sectionLabel: {
fontFamily: FONTS.labelSm,
fontSize: 11,
color: colors.primary,
letterSpacing: 1.5,
marginBottom: 8,
},
settingsCard: {
backgroundColor: colors.surface,
padding: 16,
borderRadius: ROUNDNESS.lg,
borderWidth: 1,
borderColor: colors.outlineVariant + "33",
gap: 16,
},
inputGroup: {
gap: 8,
},
rowAlign: {
flexDirection: "row",
alignItems: "center",
gap: 8,
},
inputLabel: {
fontFamily: FONTS.labelSm,
fontSize: 10,
color: colors.outline,
},
input: {
backgroundColor: colors.surfaceVariant + "40",
borderRadius: ROUNDNESS.md,
paddingHorizontal: 12,
paddingVertical: 10,
fontFamily: FONTS.body,
fontSize: 16,
color: colors.onSurface,
borderWidth: 1,
borderColor: colors.outlineVariant + "26",
},
saveBtn: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 10,
backgroundColor: colors.primary,
paddingVertical: 12,
borderRadius: ROUNDNESS.md,
marginTop: 8,
},
saveBtnText: {
color: colors.onPrimary,
fontFamily: FONTS.labelSm,
fontSize: 14,
},
menuItem: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
padding: 16,
backgroundColor: colors.surface,
borderRadius: ROUNDNESS.lg,
borderWidth: 1,
borderColor: colors.outlineVariant + "33",
marginBottom: 8,
},
menuItemLeft: {
flexDirection: "row",
alignItems: "center",
gap: 16,
},
iconBg: {
width: 44,
height: 44,
borderRadius: ROUNDNESS.md,
backgroundColor: colors.primaryContainer + "40",
alignItems: "center",
justifyContent: "center",
},
menuItemTitle: {
fontFamily: FONTS.headline,
fontSize: 16,
color: colors.onSurface,
},
menuItemSubtitle: {
fontFamily: FONTS.body,
fontSize: 12,
color: colors.onSurfaceVariant,
marginTop: 2,
},
footer: {
marginTop: 40,
alignItems: "center",
paddingBottom: 40,
},
footerText: {
fontFamily: FONTS.label,
fontSize: 10,
color: colors.outline,
letterSpacing: 2,
},
});

662
src/app/modal.tsx Normal file
View File

@ -0,0 +1,662 @@
import { AccentKey, FONTS, ROUNDNESS, SPACING } from "@/src/constants/Theme";
import { ThemeMode, useTheme } from "@/src/hooks/useTheme";
import * as Haptics from "expo-haptics";
import { useRouter } from "expo-router";
import {
Check,
Clock,
Edit3,
LogOut,
Monitor,
Moon,
RefreshCw,
Save,
Sun,
Target,
X,
Shield,
Database,
Fingerprint
} from "lucide-react-native";
import React, { useMemo, useState } from "react";
import {
ActivityIndicator,
Alert,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
KeyboardAvoidingView,
Platform
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { useAuth } from "../hooks/useAuth";
import { pullFromServer, syncWithSupabase } from "../lib/sync";
export default function SettingsModal() {
const {
colors,
accentKey,
updateAccent,
availableAccents,
themeMode,
updateThemeMode,
focusGoal,
updateFocusGoal,
sprintDuration,
updateSprintDuration,
displayName,
updateDisplayName,
identityAnchor,
updateIdentityAnchor,
} = useTheme();
const [newName, setNewName] = useState(displayName);
const [newIdentity, setNewIdentity] = useState(identityAnchor);
const [newGoal, setNewGoal] = useState(focusGoal.toString());
const [newSprintDuration, setNewSprintDuration] = useState(
sprintDuration.toString(),
);
const [isEditingAccount, setIsEditingAccount] = useState(false);
const [isEditingIdentity, setIsEditingIdentity] = useState(false);
const [isEditingGoals, setIsEditingGoals] = useState(false);
const accountDirty = newName.trim() !== displayName.trim();
const identityDirty = newIdentity.trim() !== identityAnchor.trim();
const goalsDirty =
newGoal.trim() !== focusGoal.toString() ||
newSprintDuration.trim() !== sprintDuration.toString();
const { signOut, user } = useAuth();
const router = useRouter();
const styles = useMemo(() => createStyles(colors), [colors]);
const [syncing, setSyncing] = useState(false);
const handleSaveIdentity = async () => {
if (!identityDirty) return;
try {
await updateIdentityAnchor(newIdentity);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
setIsEditingIdentity(false);
} catch (e) {
Alert.alert("Error", "Unable to save identity anchor.");
}
};
const handleSaveAccount = async () => {
if (!accountDirty) return;
try {
await updateDisplayName(newName);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
setIsEditingAccount(false);
} catch (e) {
Alert.alert("Error", "Unable to save display name.");
}
};
const handleSaveGoals = async () => {
if (!goalsDirty) return;
const goalNum = parseInt(newGoal, 10);
const sprintNum = parseInt(newSprintDuration, 10);
if (isNaN(goalNum) || isNaN(sprintNum)) {
Alert.alert("Error", "Please enter valid numbers.");
return;
}
try {
await updateFocusGoal(goalNum);
await updateSprintDuration(sprintNum);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
setIsEditingGoals(false);
} catch (e) {
Alert.alert("Error", "Unable to save goals.");
}
};
const handleSignOut = async () => {
Alert.alert("Sign Out", "Are you sure you want to sign out?", [
{ text: "Cancel", style: "cancel" },
{
text: "Sign Out",
style: "destructive",
onPress: async () => {
await signOut();
router.replace("/login");
},
},
]);
};
const handleManualSync = async () => {
setSyncing(true);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
try {
await syncWithSupabase();
await pullFromServer();
Alert.alert("Cloud Sync Successful", "Your flow is now synchronized across all devices.");
} catch (e) {
Alert.alert("Sync Error", "Could not connect to the cloud architect.");
} finally {
setSyncing(false);
}
};
const handleResetData = () => {
Alert.alert(
"Hard Reset",
"This will clear all local data. Cloud data will remain safe. Proceed?",
[
{ text: "Cancel", style: "cancel" },
{
text: "Reset",
style: "destructive",
onPress: async () => {
Alert.alert("Reset Complete", "Local data has been purged.");
}
}
]
);
};
return (
<View style={styles.container}>
<SafeAreaView style={styles.safeArea} edges={["bottom"]}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={{ flex: 1 }}
>
<View style={styles.header}>
<Text style={styles.title}>System Configuration</Text>
<TouchableOpacity
style={styles.closeBtn}
onPress={() => router.back()}
>
<X size={24} color={colors.onSurface} />
</TouchableOpacity>
</View>
<ScrollView contentContainerStyle={styles.content} showsVerticalScrollIndicator={false} keyboardShouldPersistTaps="handled">
{/* Identity Section */}
<View style={styles.section}>
<View style={styles.sectionHeaderRow}>
<Fingerprint size={16} color={colors.primary} />
<Text style={styles.sectionLabel}>IDENTITY PRINCIPLE</Text>
</View>
<Text style={styles.sectionDesc}>
This is the "Identity Anchor" that guides your habits. Define who you are becoming.
</Text>
<View style={styles.inputGroup}>
<TextInput
style={[
styles.input,
{ minHeight: 80, textAlignVertical: 'top', paddingTop: 12 },
!isEditingIdentity && styles.inputDisabled,
]}
value={newIdentity}
onChangeText={setNewIdentity}
placeholder="I am the type of person who..."
placeholderTextColor={colors.outline}
editable={isEditingIdentity}
multiline
/>
</View>
<View style={styles.actionRow}>
{!isEditingIdentity ? (
<TouchableOpacity style={styles.actionBtn} onPress={() => setIsEditingIdentity(true)}>
<Edit3 size={16} color={colors.onSurface} />
<Text style={styles.actionBtnText}>Refine Identity</Text>
</TouchableOpacity>
) : (
<>
<TouchableOpacity style={styles.actionBtn} onPress={() => { setIsEditingIdentity(false); setNewIdentity(identityAnchor); }}>
<X size={16} color={colors.error} />
<Text style={[styles.actionBtnText, { color: colors.error }]}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionBtn, identityDirty && styles.actionBtnActive]}
onPress={handleSaveIdentity}
disabled={!identityDirty}
>
<Save size={16} color={identityDirty ? colors.onPrimary : colors.outline} />
<Text style={[styles.actionBtnText, identityDirty && styles.actionBtnTextActive]}>Save Anchor</Text>
</TouchableOpacity>
</>
)}
</View>
</View>
{/* Account Section */}
<View style={styles.section}>
<View style={styles.sectionHeaderRow}>
<Shield size={16} color={colors.primary} />
<Text style={styles.sectionLabel}>ACCOUNT SECURITY</Text>
</View>
<View style={styles.accountInfo}>
<View style={styles.accountDetails}>
<Text style={styles.menuItemText}>{user?.email || "Guest User"}</Text>
<TextInput
style={[styles.nameInput, !isEditingAccount && styles.inputDisabled]}
value={newName}
onChangeText={setNewName}
editable={isEditingAccount}
placeholder="Your display name"
/>
</View>
<TouchableOpacity style={styles.signOutBtn} onPress={handleSignOut}>
<LogOut size={18} color={colors.error} />
</TouchableOpacity>
</View>
<View style={styles.actionRow}>
{!isEditingAccount ? (
<TouchableOpacity style={styles.actionBtn} onPress={() => setIsEditingAccount(true)}>
<Edit3 size={16} color={colors.onSurface} />
<Text style={styles.actionBtnText}>Update Profile</Text>
</TouchableOpacity>
) : (
<TouchableOpacity
style={[styles.actionBtn, accountDirty && styles.actionBtnActive]}
onPress={handleSaveAccount}
disabled={!accountDirty}
>
<Save size={16} color={accountDirty ? colors.onPrimary : colors.outline} />
<Text style={[styles.actionBtnText, accountDirty && styles.actionBtnTextActive]}>Save Profile</Text>
</TouchableOpacity>
)}
</View>
</View>
{/* Productivity Goals */}
<View style={styles.section}>
<View style={styles.sectionHeaderRow}>
<Target size={16} color={colors.primary} />
<Text style={styles.sectionLabel}>PRODUCTIVITY ARCHITECTURE</Text>
</View>
<View style={styles.goalRow}>
<View style={styles.goalInfo}>
<Target size={20} color={colors.primary} />
<View>
<Text style={styles.goalTitle}>Daily Focus Goal</Text>
<Text style={styles.goalDesc}>Number of sessions per day</Text>
</View>
</View>
<TextInput
style={[styles.goalInput, !isEditingGoals && styles.inputDisabled]}
value={newGoal}
onChangeText={setNewGoal}
keyboardType="number-pad"
editable={isEditingGoals}
/>
</View>
<View style={styles.goalRow}>
<View style={styles.goalInfo}>
<Clock size={20} color={colors.primary} />
<View>
<Text style={styles.goalTitle}>Sprint Duration</Text>
<Text style={styles.goalDesc}>Minutes per session</Text>
</View>
</View>
<TextInput
style={[styles.goalInput, !isEditingGoals && styles.inputDisabled]}
value={newSprintDuration}
onChangeText={setNewSprintDuration}
keyboardType="number-pad"
editable={isEditingGoals}
/>
</View>
<View style={styles.actionRow}>
{!isEditingGoals ? (
<TouchableOpacity style={styles.actionBtn} onPress={() => setIsEditingGoals(true)}>
<Edit3 size={16} color={colors.onSurface} />
<Text style={styles.actionBtnText}>Modify Goals</Text>
</TouchableOpacity>
) : (
<TouchableOpacity
style={[styles.actionBtn, goalsDirty && styles.actionBtnActive]}
onPress={handleSaveGoals}
disabled={!goalsDirty}
>
<Save size={16} color={goalsDirty ? colors.onPrimary : colors.outline} />
<Text style={[styles.actionBtnText, goalsDirty && styles.actionBtnTextActive]}>Apply Goals</Text>
</TouchableOpacity>
)}
</View>
</View>
{/* Visual Style */}
<View style={styles.section}>
<Text style={styles.sectionLabel}>VISUAL INTERFACE</Text>
<View style={styles.themeToggleGrid}>
{(['light', 'dark', 'system'] as ThemeMode[]).map((mode) => (
<TouchableOpacity
key={mode}
style={[
styles.themeOption,
themeMode === mode && { backgroundColor: colors.primary }
]}
onPress={() => {
updateThemeMode(mode);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
>
{mode === 'light' ? <Sun size={20} color={themeMode === mode ? colors.onPrimary : colors.onSurfaceVariant} /> :
mode === 'dark' ? <Moon size={20} color={themeMode === mode ? colors.onPrimary : colors.onSurfaceVariant} /> :
<Monitor size={20} color={themeMode === mode ? colors.onPrimary : colors.onSurfaceVariant} />}
<Text style={[styles.themeLabel, themeMode === mode && { color: colors.onPrimary }]}>
{mode.charAt(0).toUpperCase() + mode.slice(1)}
</Text>
</TouchableOpacity>
))}
</View>
<View style={styles.accentGrid}>
{(Object.keys(availableAccents) as AccentKey[]).map((key) => {
const accent = availableAccents[key];
const isSelected = accentKey === key;
return (
<TouchableOpacity
key={key}
style={[styles.accentCard, isSelected && { borderColor: colors.primary, borderWidth: 2 }]}
onPress={() => {
updateAccent(key);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
>
<View style={[styles.accentPreview, { backgroundColor: accent.primary }]}>
{isSelected && <Check size={20} color="#fff" />}
</View>
<Text style={[styles.accentLabel, isSelected && { color: colors.primary, fontFamily: FONTS.labelSm }]} numberOfLines={1}>
{accent.label}
</Text>
</TouchableOpacity>
);
})}
</View>
</View>
{/* System & Sync */}
<View style={styles.section}>
<View style={styles.sectionHeaderRow}>
<Database size={16} color={colors.primary} />
<Text style={styles.sectionLabel}>CORE ENGINE</Text>
</View>
<View style={styles.systemCard}>
<TouchableOpacity style={styles.systemItem} onPress={handleManualSync} disabled={syncing}>
<View style={styles.systemItemInfo}>
<Text style={styles.menuItemText}>Cloud Architecture Sync</Text>
<Text style={styles.menuItemValue}>Last verified: Just now</Text>
</View>
{syncing ? <ActivityIndicator size="small" color={colors.primary} /> : <RefreshCw size={18} color={colors.primary} />}
</TouchableOpacity>
<TouchableOpacity style={styles.systemItem} onPress={handleResetData}>
<View style={styles.systemItemInfo}>
<Text style={styles.menuItemText}>Purge Local Cache</Text>
<Text style={styles.menuItemValue}>Hard reset database</Text>
</View>
<Database size={18} color={colors.error} opacity={0.7} />
</TouchableOpacity>
</View>
</View>
<View style={styles.footer}>
<Text style={styles.versionText}>Batsirai Productivity Planner v1.2.0</Text>
<Text style={styles.versionText}>Engineered for Focus</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
</View>
);
}
const createStyles = (colors: any) =>
StyleSheet.create({
container: {
flex: 1,
backgroundColor: colors.background,
},
safeArea: {
flex: 1,
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: SPACING.lg,
borderBottomWidth: 1,
borderBottomColor: colors.outlineVariant + "4D",
},
title: {
fontFamily: FONTS.headline,
fontSize: 22,
color: colors.onSurface,
},
closeBtn: {
padding: 4,
},
content: {
padding: SPACING.lg,
},
section: {
marginBottom: SPACING.xxl,
},
sectionHeaderRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginBottom: 8,
},
sectionLabel: {
fontFamily: FONTS.labelSm,
fontSize: 11,
color: colors.primary,
letterSpacing: 1.5,
},
sectionDesc: {
fontFamily: FONTS.body,
fontSize: 13,
color: colors.onSurfaceVariant,
lineHeight: 18,
marginBottom: 12,
},
accountInfo: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: colors.surface,
padding: 16,
borderRadius: ROUNDNESS.lg,
borderWidth: 1,
borderColor: colors.outlineVariant + "33",
},
accountDetails: {
flex: 1,
},
nameInput: {
fontFamily: FONTS.labelSm,
fontSize: 15,
color: colors.primary,
marginTop: 4,
},
signOutBtn: {
padding: 8,
},
inputGroup: {
gap: 8,
},
input: {
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.outlineVariant + "33",
borderRadius: ROUNDNESS.md,
paddingHorizontal: 16,
paddingVertical: 12,
fontFamily: FONTS.body,
color: colors.onSurface,
fontSize: 15,
},
inputDisabled: {
opacity: 0.6,
backgroundColor: colors.surfaceVariant + '40',
},
actionRow: {
flexDirection: 'row',
gap: 12,
marginTop: 12,
},
actionBtn: {
flex: 1,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 8,
paddingVertical: 12,
borderRadius: ROUNDNESS.md,
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.outlineVariant + "80",
},
actionBtnActive: {
backgroundColor: colors.primary,
borderColor: colors.primary,
},
actionBtnText: {
fontFamily: FONTS.label,
color: colors.onSurface,
fontSize: 13,
},
actionBtnTextActive: {
color: colors.onPrimary,
},
goalRow: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
backgroundColor: colors.surface,
padding: 14,
borderRadius: ROUNDNESS.lg,
marginBottom: 8,
borderWidth: 1,
borderColor: colors.outlineVariant + "33",
},
goalInfo: {
flexDirection: "row",
alignItems: "center",
gap: 12,
},
goalTitle: {
fontFamily: FONTS.headline,
fontSize: 15,
color: colors.onSurface,
},
goalDesc: {
fontFamily: FONTS.body,
fontSize: 12,
color: colors.onSurfaceVariant,
},
goalInput: {
width: 60,
height: 40,
backgroundColor: colors.surfaceVariant + "80",
borderRadius: ROUNDNESS.sm,
textAlign: "center",
fontFamily: FONTS.labelSm,
color: colors.primary,
fontSize: 18,
},
themeToggleGrid: {
flexDirection: "row",
gap: 10,
marginBottom: SPACING.xl,
},
themeOption: {
flex: 1,
paddingVertical: 16,
alignItems: "center",
justifyContent: "center",
borderRadius: ROUNDNESS.lg,
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.outlineVariant + "33",
gap: 8,
},
themeLabel: {
fontFamily: FONTS.label,
fontSize: 12,
color: colors.onSurfaceVariant,
},
accentGrid: {
flexDirection: "row",
flexWrap: "wrap",
gap: 10,
},
accentCard: {
width: "31%",
backgroundColor: colors.surface,
padding: 10,
borderRadius: ROUNDNESS.lg,
borderWidth: 1,
borderColor: colors.outlineVariant + "33",
alignItems: "center",
gap: 8,
marginBottom: 4,
},
accentPreview: {
width: 36,
height: 36,
borderRadius: 18,
alignItems: "center",
justifyContent: "center",
},
accentLabel: {
fontFamily: FONTS.label,
fontSize: 11,
color: colors.onSurfaceVariant,
},
systemCard: {
backgroundColor: colors.surface,
borderRadius: ROUNDNESS.lg,
borderWidth: 1,
borderColor: colors.outlineVariant + "33",
overflow: 'hidden',
},
systemItem: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 16,
borderBottomWidth: 1,
borderBottomColor: colors.outlineVariant + "33",
},
systemItemInfo: {
flex: 1,
},
menuItemText: {
fontFamily: FONTS.body,
fontSize: 15,
color: colors.onSurface,
},
menuItemValue: {
fontFamily: FONTS.label,
fontSize: 12,
color: colors.onSurfaceVariant,
marginTop: 2,
},
footer: {
alignItems: "center",
marginTop: SPACING.xl,
paddingBottom: 60,
},
versionText: {
fontFamily: FONTS.label,
fontSize: 11,
color: colors.outline,
letterSpacing: 0.5,
textTransform: 'uppercase',
marginBottom: 4,
},
});

169
src/app/reader/[id].tsx Normal file
View File

@ -0,0 +1,169 @@
import React, { useEffect, useState, useCallback, useRef } from 'react';
import { View, ActivityIndicator, StyleSheet, Alert } from 'react-native';
import { useLocalSearchParams, useRouter, Stack } from 'expo-router';
import { getDb } from '@/src/db/database';
import { performMutation } from '@/src/lib/sync';
import { resolveFileUri, downloadBook } from '@/src/lib/file-utils';
import PdfReader from '@/src/components/Library/PdfReader';
import { useTheme } from '@/src/hooks/useTheme';
import { ThemedText } from '@/components/themed-text';
import * as Haptics from 'expo-haptics';
interface Book {
id: string;
title: string;
file_uri: string;
current_page: number;
total_pages: number;
status: string;
}
export default function ReaderScreen() {
const { id } = useLocalSearchParams<{ id: string }>();
const router = useRouter();
const { colors } = useTheme();
const [book, setBook] = useState<Book | null>(null);
const [loading, setLoading] = useState(true);
const [downloading, setDownloading] = useState(false);
const [localUri, setLocalUri] = useState<string | null>(null);
const [currentPage, setCurrentPage] = useState(0);
const [startPage, setStartPage] = useState(0);
const [totalPages, setTotalPages] = useState(0);
const [sessionSeconds, setSessionSeconds] = useState(0);
const sessionStartTime = useRef<number>(Date.now());
useEffect(() => {
loadBook();
}, [id]);
const loadBook = async () => {
try {
const db = await getDb();
const bookData = await db.getFirstAsync<Book>(
'SELECT * FROM books WHERE id = ?',
[id]
);
if (bookData) {
setBook(bookData);
setCurrentPage(bookData.current_page || 0);
setStartPage(bookData.current_page || 0);
setTotalPages(bookData.total_pages || 0);
// Ensure file is local
try {
setDownloading(true);
const uri = await downloadBook(bookData.file_uri);
setLocalUri(uri);
} catch (e) {
console.error('Failed to ensure local file:', e);
// Fallback to resolved URI (might be a remote URL if docDir was null)
setLocalUri(resolveFileUri(bookData.file_uri));
} finally {
setDownloading(false);
}
} else {
Alert.alert('Error', 'Book not found');
router.back();
}
} catch (error) {
console.error('Failed to load book:', error);
Alert.alert('Error', 'Failed to load book');
router.back();
} finally {
setLoading(false);
}
};
const handleClose = async () => {
if (!book) return;
const durationSeconds = Math.floor((Date.now() - sessionStartTime.current) / 1000);
const pagesRead = Math.max(0, currentPage - startPage);
try {
// Update book progress
await performMutation('books', 'UPDATE', {
id: book.id,
current_page: currentPage,
total_pages: totalPages,
status: totalPages > 0 && currentPage >= totalPages - 1 ? 'finished' : 'reading',
updated_at: new Date().toISOString(),
});
// Log reading session
if (pagesRead > 0 || durationSeconds > 30) {
await performMutation('reading_logs', 'INSERT', {
id: Math.random().toString(36).substring(7),
book_id: book.id,
start_page: startPage,
end_page: currentPage,
pages_read: pagesRead,
duration_seconds: durationSeconds,
duration_minutes: durationSeconds / 60,
logged_at: new Date().toISOString(),
});
}
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
router.back();
} catch (error) {
console.error('Failed to save progress:', error);
router.back();
}
};
const handlePageChange = useCallback((page: number, total: number) => {
setCurrentPage(page);
if (total > 0 && total !== totalPages) {
setTotalPages(total);
}
}, [totalPages]);
if (loading || (downloading && !localUri)) {
return (
<View style={[styles.loadingContainer, { backgroundColor: '#000' }]}>
<ActivityIndicator size="large" color={colors.primary} />
{downloading && (
<ThemedText style={{ color: '#fff', marginTop: 16 }}>
Downloading your book...
</ThemedText>
)}
</View>
);
}
if (!book || !localUri) return null;
return (
<View style={styles.container}>
<Stack.Screen options={{ headerShown: false, animation: 'fade' }} />
<PdfReader
uri={localUri}
title={book.title}
initialPage={currentPage}
onClose={handleClose}
onPageChange={handlePageChange}
onSessionUpdate={setSessionSeconds}
onAddNote={(page) => {
// Future: Open note editor
console.log('Add note for page:', page);
}}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#000',
},
loadingContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
},
});

View File

@ -0,0 +1,127 @@
import { useTheme } from "@/src/hooks/useTheme";
import { BlurView } from "expo-blur";
import * as Haptics from "expo-haptics";
import { LayoutDashboard } from "lucide-react-native";
import React, { useCallback, useMemo, useRef } from "react";
import { Animated, Platform, Pressable, StyleSheet, View } from "react-native";
type Props = {
onPress: () => void;
accentColor: string;
label?: string;
position?: "center" | "right";
};
export default function AutoHideFloatingActionButton({
onPress,
accentColor,
label = "Day",
position = "center",
}: Props) {
const { isDark, colors } = useTheme();
const scaleAnim = useRef(new Animated.Value(1)).current;
const onPressIn = useCallback(() => {
// Requirement: Animated scale on press (0.95 -> 1.0)
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
Animated.spring(scaleAnim, {
toValue: 0.95,
useNativeDriver: true,
friction: 8,
tension: 100,
}).start();
}, [scaleAnim]);
const onPressOut = useCallback(() => {
Animated.spring(scaleAnim, {
toValue: 1,
useNativeDriver: true,
friction: 8,
tension: 100,
}).start();
}, [scaleAnim]);
const animatedStyle = useMemo(
() => ({ transform: [{ scale: scaleAnim }] }),
[scaleAnim],
);
return (
<Animated.View
style={[
styles.wrapper,
animatedStyle,
position === "right" ? styles.positionRight : styles.positionCenter,
]}
>
<Pressable
accessibilityRole="button"
accessibilityLabel={`Go to ${label}`}
onPress={onPress}
onPressIn={onPressIn}
onPressOut={onPressOut}
style={({ pressed }) => [
styles.pressable,
{ opacity: pressed ? 0.95 : 1 },
]}
>
<BlurView
// Requirement: Blur amount 15-20
intensity={Platform.OS === "ios" ? 30 : 0}
tint={isDark ? "dark" : "light"}
style={[
styles.fab,
{
backgroundColor: isDark ? "rgba(22,22,24,0.72)" : "rgba(248,250,249,0.85)",
borderColor: isDark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.05)",
},
Platform.OS === "android" && {
backgroundColor: isDark ? "rgba(28,28,30,0.96)" : colors.surface,
},
]}
>
<View style={[styles.iconContainer]}>
<LayoutDashboard size={24} color={accentColor} strokeWidth={2.2} />
</View>
</BlurView>
</Pressable>
</Animated.View>
);
}
const styles = StyleSheet.create({
wrapper: {
// Requirement: Large border radius (24-28px) -> 28 for 56x56
borderRadius: 30,
// Requirement: Slight elevation/shadow
shadowColor: "#000",
shadowOpacity: 0.2,
shadowRadius: 16,
shadowOffset: { width: 0, height: 8 },
elevation: 10,
},
pressable: {
borderRadius: 30,
overflow: "hidden",
},
positionCenter: {
alignSelf: "center",
},
positionRight: {
alignSelf: "flex-end",
marginRight: 20,
},
fab: {
// Requirement: Circular (56x56 points)
width: 60,
height: 60,
borderRadius: 30,
borderWidth: 1,
justifyContent: "center",
alignItems: "center",
},
iconContainer: {
justifyContent: "center",
alignItems: "center",
},
});

View File

@ -0,0 +1,25 @@
import { useAutoHideOnScroll } from "@/src/hooks/useAutoHideTabBar";
import React from "react";
import { ScrollView, ScrollViewProps } from "react-native";
const AutoHideScrollView = React.forwardRef<ScrollView, ScrollViewProps>(
(props, ref) => {
const { onScroll, scrollEventThrottle } = useAutoHideOnScroll();
return (
<ScrollView
ref={ref}
{...props}
scrollEventThrottle={scrollEventThrottle}
onScroll={(event) => {
onScroll(event);
if (props.onScroll) {
props.onScroll(event);
}
}}
/>
);
},
);
export default AutoHideScrollView;

View File

@ -0,0 +1,70 @@
import { FONTS, SPACING, ThemeColors } from "@/src/constants/Theme";
import React from "react";
import { StyleSheet, Text, View } from "react-native";
interface ErrorBoundaryProps {
children: React.ReactNode;
colors: ThemeColors;
}
interface ErrorBoundaryState {
hasError: boolean;
message: string;
}
export class ErrorBoundary extends React.Component<
ErrorBoundaryProps,
ErrorBoundaryState
> {
constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false, message: "" };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, message: error.message };
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error("Chat error boundary caught:", error, errorInfo);
}
render() {
const { hasError, message } = this.state;
const { colors, children } = this.props;
if (!hasError) {
return children;
}
return (
<View style={[styles.container, { backgroundColor: colors.background }]}>
<Text style={[styles.title, { color: colors.onBackground }]}>
Something went wrong.
</Text>
<Text style={[styles.details, { color: colors.onSurfaceVariant }]}>
{message}
</Text>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: SPACING.lg,
},
title: {
fontFamily: FONTS.headline,
fontSize: 20,
marginBottom: SPACING.sm,
},
details: {
fontFamily: FONTS.body,
fontSize: 14,
textAlign: "center",
},
});

View File

@ -0,0 +1,141 @@
import { useTheme } from "@/src/hooks/useTheme";
import { BlurView } from "expo-blur";
import * as Haptics from "expo-haptics";
import { usePathname, useRouter } from "expo-router";
import { Sparkles } from "lucide-react-native";
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
import {
Animated,
Keyboard,
Platform,
Pressable,
StyleSheet,
View
} from "react-native";
export default function FloatingAIDock() {
const { colors, isDark } = useTheme();
const router = useRouter();
const pathname = usePathname();
const [isVisible, setIsVisible] = useState(true);
const visibilityAnim = useRef(new Animated.Value(1)).current;
// Keyboard awareness - hide when typing
useEffect(() => {
const showSubscription = Keyboard.addListener(
Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow",
() => setIsVisible(false)
);
const hideSubscription = Keyboard.addListener(
Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide",
() => setIsVisible(true)
);
return () => {
showSubscription.remove();
hideSubscription.remove();
};
}, []);
useEffect(() => {
Animated.spring(visibilityAnim, {
toValue: isVisible ? 1 : 0,
useNativeDriver: true,
friction: 8,
tension: 40,
}).start();
}, [isVisible, visibilityAnim]);
const navigateToAI = useCallback(() => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
router.push("/aa_ai");
}, [router]);
const isActive = pathname === "/aa_ai";
const dockStyle = useMemo(() => [
styles.dockContainer,
{
right: 16,
top: "55%", // Slightly below center
transform: [
{ scale: visibilityAnim },
{ translateX: visibilityAnim.interpolate({
inputRange: [0, 1],
outputRange: [100, 0],
})
},
],
opacity: visibilityAnim,
}
], [visibilityAnim]);
// Don't show the launcher if we're already on the AI screen
if (isActive) return null;
return (
<Animated.View style={dockStyle}>
<Pressable
onPress={navigateToAI}
style={({ pressed }) => [
styles.pressable,
{ transform: [{ scale: pressed ? 0.92 : 1 }] }
]}
>
<BlurView
intensity={Platform.OS === "ios" ? 60 : 0}
tint={isDark ? "dark" : "light"}
style={[
styles.blurContainer,
{
backgroundColor: isDark ? "rgba(22,22,24,0.72)" : "rgba(248,250,249,0.85)",
borderColor: isDark ? "rgba(255,255,255,0.12)" : "rgba(0,0,0,0.08)",
},
Platform.OS === "android" && {
backgroundColor: isDark ? "rgba(28,28,30,0.96)" : colors.surface,
},
]}
>
<View style={styles.iconWrapper}>
<Sparkles size={28} color={colors.primary} strokeWidth={2.2} />
</View>
</BlurView>
</Pressable>
</Animated.View>
);
}
const styles = StyleSheet.create({
dockContainer: {
position: "absolute",
zIndex: 1000,
width: 64,
height: 64,
shadowColor: "#000",
shadowOpacity: 0.2,
shadowRadius: 15,
shadowOffset: { width: 0, height: 8 },
elevation: 20,
},
pressable: {
width: "100%",
height: "100%",
},
blurContainer: {
flex: 1,
borderRadius: 32,
overflow: "hidden",
borderWidth: 1.5,
alignItems: "center",
justifyContent: "center",
},
iconWrapper: {
alignItems: "center",
justifyContent: "center",
// Subtle glow effect
shadowColor: "#FFF",
shadowOpacity: 0.1,
shadowRadius: 10,
}
});

View File

@ -0,0 +1,309 @@
import AutoHideFloatingActionButton from "@/src/components/AutoHideFloatingActionButton";
import { FONTS } from "@/src/constants/Theme";
import { useTabBarVisibility } from "@/src/hooks/useAutoHideTabBar";
import { useTheme } from "@/src/hooks/useTheme";
import { BottomTabBarProps } from "@react-navigation/bottom-tabs";
import { BlurView } from "expo-blur";
import * as Haptics from "expo-haptics";
import React, { useEffect, useMemo, useRef } from "react";
import {
Animated,
Platform,
Pressable,
StyleSheet,
Text,
View,
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
const MAIN_TABS = ["index", "calendar", "library"];
export default function GlassBottomTabBar(props: BottomTabBarProps) {
const { state, descriptors, navigation } = props;
const { colors, isDark } = useTheme();
const { tabBarVisible, showTabBar } = useTabBarVisibility();
const insets = useSafeAreaInsets();
const currentRouteName = state.routes[state.index].name;
const animated = useRef(new Animated.Value(tabBarVisible ? 0 : 1)).current;
useEffect(() => {
Animated.spring(animated, {
toValue: tabBarVisible ? 0 : 1,
damping: 18,
stiffness: 180,
mass: 0.9,
useNativeDriver: true,
}).start();
}, [tabBarVisible, animated]);
const containerStyle = useMemo(
() => [
styles.container,
{
bottom: insets.bottom + 12,
transform: [
{
translateY: animated.interpolate({
inputRange: [0, 1],
outputRange: [0, 140],
}),
},
],
opacity: animated.interpolate({
inputRange: [0, 1],
outputRange: [1, 0],
}),
},
],
[animated, insets.bottom],
);
const fabStyle = useMemo(
() => ({
transform: [
{
translateY: animated.interpolate({
inputRange: [0, 1],
outputRange: [160, 0],
}),
},
{
scale: animated.interpolate({
inputRange: [0, 1],
outputRange: [1, 0.7],
}),
},
],
opacity: animated.interpolate({
inputRange: [0, 1],
outputRange: [0, 1],
}),
}),
[animated],
);
const indexRoute = state.routes.find((route) => route.name === "index");
const onMostImportantPress = () => {
if (currentRouteName === "index" && indexRoute) {
const event = navigation.emit({
type: "tabPress",
target: indexRoute.key,
canPreventDefault: true,
});
if (!event.defaultPrevented) {
navigation.navigate("index");
}
} else {
navigation.navigate("index");
}
showTabBar();
};
if (currentRouteName === "aa_ai") {
return null;
}
const visibleRoutes = state.routes.filter((r) => MAIN_TABS.includes(r.name));
const indexRouteKey = visibleRoutes.find((r) => r.name === "index")?.key;
const indexActiveColor =
(descriptors[indexRouteKey || ""].options
.tabBarActiveTintColor as string) || colors.primary;
return (
<>
<Animated.View
style={containerStyle}
pointerEvents={tabBarVisible ? "auto" : "none"}
>
<BlurView
intensity={Platform.OS === "ios" ? 70 : 0}
tint={isDark ? "dark" : "light"}
style={[
styles.blurContainer,
{
backgroundColor: isDark ? "rgba(22,22,24,0.72)" : "rgba(248,250,249,0.85)",
borderColor: isDark ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.05)",
shadowColor: isDark ? "#000" : colors.outline,
},
Platform.OS === "android" && {
backgroundColor: isDark ? "rgba(28,28,30,0.96)" : colors.surface,
},
]}
>
<View style={styles.inner}>
{visibleRoutes.map((route) => {
const descriptor = descriptors[route.key];
const focused = currentRouteName === route.name;
const activeColor =
(descriptor.options.tabBarActiveTintColor as string) ||
colors.primary;
const inactiveColor =
(descriptor.options.tabBarInactiveTintColor as string) ||
colors.outline;
const color = focused ? activeColor : inactiveColor;
const label = descriptor.options.title || route.name;
const icon = descriptor.options.tabBarIcon?.({
focused,
color,
size: 24,
});
const onPress = () => {
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
const event = navigation.emit({
type: "tabPress",
target: route.key,
canPreventDefault: true,
});
if (!event.defaultPrevented) {
navigation.navigate(route.name);
}
};
return (
<Pressable
key={route.key}
onPress={onPress}
style={styles.tabWrapper}
>
<View
style={[
styles.tabButton,
focused && {
backgroundColor: isDark ? "rgba(255,255,255,0.12)" : "rgba(0,0,0,0.04)",
borderColor: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.02)",
},
focused && styles.activeTabButton,
]}
>
<View style={styles.iconWrapper}>{icon}</View>
<Text
numberOfLines={1}
style={[
styles.label,
{
color,
fontWeight: focused ? "700" : "500",
},
]}
>
{label}
</Text>
</View>
</Pressable>
);
})}
</View>
</BlurView>
</Animated.View>
<Animated.View
style={[
styles.fabHost,
fabStyle,
{
bottom: insets.bottom + 28,
},
]}
pointerEvents={tabBarVisible ? "none" : "auto"}
>
<AutoHideFloatingActionButton
onPress={onMostImportantPress}
accentColor={indexActiveColor}
label="Day"
/>
</Animated.View>
</>
);
}
const styles = StyleSheet.create({
container: {
position: "absolute",
alignSelf: "center",
zIndex: 100,
},
blurContainer: {
borderRadius: 999,
overflow: "hidden",
borderWidth: 1,
paddingHorizontal: 8,
paddingVertical: 8,
shadowOpacity: 0.15,
shadowRadius: 20,
shadowOffset: {
width: 0,
height: 8,
},
elevation: 20,
},
inner: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
gap: 4,
},
tabWrapper: {
borderRadius: 999,
},
tabButton: {
minWidth: 76,
height: 44,
borderRadius: 999,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 12,
},
activeTabButton: {
borderWidth: 1,
},
iconWrapper: {
marginBottom: 2,
alignItems: "center",
justifyContent: "center",
},
label: {
fontFamily: FONTS.label,
fontSize: 10,
letterSpacing: -0.1,
textAlign: "center",
lineHeight: 12,
},
fabHost: {
position: "absolute",
alignSelf: "center",
zIndex: 101,
},
});

View File

@ -0,0 +1,178 @@
import { ROUNDNESS, SPACING } from "@/src/constants/Theme";
import { useTheme } from "@/src/hooks/useTheme";
import { Upload, X } from "lucide-react-native";
import React from "react";
import {
ActivityIndicator,
KeyboardAvoidingView,
Modal,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
interface AddBookModalProps {
visible: boolean;
onClose: () => void;
bookTitle: string;
onTitleChange: (text: string) => void;
bookAuthor: string;
onAuthorChange: (text: string) => void;
totalPages: string;
onPagesChange: (text: string) => void;
loading: boolean;
onAddBook: () => void;
}
export default function AddBookModal({
visible,
onClose,
bookTitle,
onTitleChange,
bookAuthor,
onAuthorChange,
totalPages,
onPagesChange,
loading,
onAddBook,
}: AddBookModalProps) {
const { colors } = useTheme();
const styles = StyleSheet.create({
modalOverlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.5)",
justifyContent: "flex-end",
},
modalContainer: {
maxHeight: "90%",
backgroundColor: colors.surface,
borderTopLeftRadius: ROUNDNESS.lg,
borderTopRightRadius: ROUNDNESS.lg,
},
modalHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
borderBottomWidth: 1,
borderBottomColor: colors.surfaceVariant,
},
modalTitle: {
fontSize: 16,
fontWeight: "700",
color: colors.onSurface,
},
modalField: {
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
},
modalLabel: {
fontSize: 10,
fontWeight: "600",
color: colors.outline,
marginBottom: SPACING.sm,
letterSpacing: 0.5,
},
modalInput: {
borderWidth: 1,
borderColor: colors.surfaceVariant,
borderRadius: ROUNDNESS.md,
paddingHorizontal: SPACING.md,
paddingVertical: SPACING.md,
fontSize: 14,
color: colors.onSurface,
backgroundColor: colors.background,
},
primaryBtn: {
backgroundColor: colors.primary,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
borderRadius: ROUNDNESS.md,
gap: SPACING.sm,
marginHorizontal: SPACING.lg,
marginVertical: SPACING.md,
},
primaryBtnText: {
color: colors.onPrimary,
fontSize: 14,
fontWeight: "600",
letterSpacing: 0.5,
},
});
return (
<Modal visible={visible} transparent animationType="slide">
<View style={styles.modalOverlay}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={styles.modalContainer}
>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Add New Book</Text>
<TouchableOpacity onPress={onClose}>
<X size={24} color={colors.onSurface} />
</TouchableOpacity>
</View>
<ScrollView
showsVerticalScrollIndicator={false}
keyboardShouldPersistTaps="handled"
>
<View style={styles.modalField}>
<Text style={styles.modalLabel}>TITLE</Text>
<TextInput
style={styles.modalInput}
value={bookTitle}
onChangeText={onTitleChange}
placeholder="Book Title"
placeholderTextColor={colors.outline}
/>
</View>
<View style={styles.modalField}>
<Text style={styles.modalLabel}>AUTHOR</Text>
<TextInput
style={styles.modalInput}
value={bookAuthor}
onChangeText={onAuthorChange}
placeholder="Author Name"
placeholderTextColor={colors.outline}
/>
</View>
<View style={styles.modalField}>
<Text style={styles.modalLabel}>TOTAL PAGES</Text>
<TextInput
style={styles.modalInput}
value={totalPages}
onChangeText={onPagesChange}
keyboardType="numeric"
placeholder="Total pages"
placeholderTextColor={colors.outline}
/>
</View>
<TouchableOpacity
style={[styles.primaryBtn, loading && { opacity: 0.6 }]}
onPress={onAddBook}
disabled={loading}
>
{loading ? (
<ActivityIndicator color={colors.onPrimary} />
) : (
<>
<Upload size={20} color={colors.onPrimary} />
<Text style={styles.primaryBtnText}>Add to Library</Text>
</>
)}
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
</View>
</Modal>
);
}

View File

@ -0,0 +1,128 @@
import { ROUNDNESS, SPACING } from "@/src/constants/Theme";
import { useTheme } from "@/src/hooks/useTheme";
import { Book as BookIcon, Play } from "lucide-react-native";
import React from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
interface Book {
id: string;
title: string;
author: string;
total_pages: number;
current_page: number;
status: "reading" | "finished" | "want_to_read";
}
interface BookCardProps {
book: Book;
onPress: () => void;
}
export default function BookCard({ book, onPress }: BookCardProps) {
const { colors } = useTheme();
const styles = StyleSheet.create({
bookCard: {
flex: 1,
backgroundColor: colors.surface,
borderRadius: ROUNDNESS.md,
overflow: "hidden",
marginBottom: SPACING.md,
},
bookCover: {
height: 160,
backgroundColor: colors.surfaceVariant,
justifyContent: "center",
alignItems: "center",
position: "relative",
},
resumeIndicator: {
position: "absolute",
top: 8,
right: 8,
backgroundColor: colors.primary,
width: 32,
height: 32,
borderRadius: 16,
justifyContent: "center",
alignItems: "center",
},
progressOverlay: {
position: "absolute",
bottom: 8,
right: 8,
backgroundColor: "rgba(0,0,0,0.6)",
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
},
progressText: {
color: "#fff",
fontSize: 12,
fontWeight: "600",
},
bookTitle: {
fontSize: 13,
fontWeight: "600",
color: colors.onSurface,
paddingHorizontal: SPACING.sm,
paddingTop: SPACING.sm,
},
bookAuthor: {
fontSize: 11,
color: colors.outline,
paddingHorizontal: SPACING.sm,
},
progressBarBg: {
height: 3,
backgroundColor: colors.surfaceVariant,
marginHorizontal: SPACING.sm,
marginVertical: SPACING.sm,
borderRadius: 1.5,
overflow: "hidden",
},
progressBarFill: {
height: "100%",
borderRadius: 1.5,
},
});
const progress =
book.total_pages > 0
? Math.round((book.current_page / book.total_pages) * 100)
: 0;
return (
<TouchableOpacity style={styles.bookCard} onPress={onPress}>
<View style={styles.bookCover}>
<BookIcon size={40} color={colors.outlineVariant} strokeWidth={1} />
{book.status === "reading" && (
<View style={styles.resumeIndicator}>
<Play size={12} color="#fff" fill="#fff" />
</View>
)}
<View style={styles.progressOverlay}>
<Text style={styles.progressText}>{progress}%</Text>
</View>
</View>
<Text style={styles.bookTitle} numberOfLines={1}>
{book.title}
</Text>
<Text style={styles.bookAuthor} numberOfLines={1}>
{book.author}
</Text>
<View style={styles.progressBarBg}>
<View
style={[
styles.progressBarFill,
{
width: `${Math.min(progress, 100)}%`,
backgroundColor:
book.status === "finished" ? colors.tertiary : colors.primary,
},
]}
/>
</View>
</TouchableOpacity>
);
}

View File

@ -0,0 +1,391 @@
import { ROUNDNESS, SPACING } from "@/src/constants/Theme";
import { useTheme } from "@/src/hooks/useTheme";
import { useData } from "@/src/hooks/useData";
import {
Book as BookIcon,
Clock,
Play,
Sparkles,
Trash2,
TrendingUp,
X,
ChevronRight,
Calendar,
} from "lucide-react-native";
import React, { useMemo } from "react";
import {
ActivityIndicator,
Modal,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
interface Book {
id: string;
title: string;
author: string;
total_pages: number;
current_page: number;
status: "reading" | "finished" | "want_to_read";
file_uri: string;
updated_at: string;
}
interface ReadingLog {
id: string;
start_page: number;
end_page: number;
pages_read: number;
duration_seconds: number;
logged_at: string;
}
interface BookDetailsModalProps {
visible: boolean;
onClose: () => void;
book: Book | null;
isGeneratingAI: boolean;
onContinueReading: () => void;
onGenerateAI: () => void;
onDelete: () => void;
}
export default function BookDetailsModal({
visible,
onClose,
book,
isGeneratingAI,
onContinueReading,
onGenerateAI,
onDelete,
}: BookDetailsModalProps) {
const { colors } = useTheme();
const styles = useMemo(() => createStyles(colors), [colors]);
const { data: logs, loading: logsLoading } = useData<ReadingLog>(
"SELECT * FROM reading_logs WHERE book_id = ? ORDER BY logged_at DESC LIMIT 5",
[book?.id || ""],
);
const formatDuration = (seconds: number) => {
const mins = Math.floor(seconds / 60);
if (mins < 1) return "< 1 min";
return `${mins} min${mins > 1 ? "s" : ""}`;
};
const formatDate = (dateStr: string) => {
const date = new Date(dateStr);
return date.toLocaleDateString(undefined, {
month: "short",
day: "numeric",
});
};
return (
<Modal visible={visible} transparent animationType="slide">
<View style={styles.modalOverlay}>
<View style={styles.modalContainer}>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>Book Details</Text>
<TouchableOpacity onPress={onClose}>
<X size={24} color={colors.onSurface} />
</TouchableOpacity>
</View>
{book && (
<ScrollView showsVerticalScrollIndicator={false} contentContainerStyle={{ paddingBottom: 40 }}>
<View style={styles.detailsHeader}>
<View style={[styles.bookCover, { width: 120, height: 160 }]}>
<BookIcon size={48} color={colors.outlineVariant} />
</View>
<View style={styles.detailsMeta}>
<Text style={styles.detailsTitle}>{book.title}</Text>
<Text style={styles.detailsAuthor}>{book.author}</Text>
<View style={styles.detailsStats}>
<View style={styles.detailStatItem}>
<TrendingUp size={14} color={colors.primary} />
<Text style={styles.detailStatText}>
{book.current_page} / {book.total_pages} pages
</Text>
</View>
<View style={styles.detailStatItem}>
<Clock size={14} color={colors.secondary} />
<Text style={styles.detailStatText}>
Last read:{" "}
{new Date(book.updated_at).toLocaleDateString()}
</Text>
</View>
</View>
</View>
</View>
<TouchableOpacity
style={[styles.primaryBtn, { marginBottom: 12 }]}
onPress={onContinueReading}
>
<Play size={20} color={colors.onPrimary} fill="#fff" />
<Text style={styles.primaryBtnText}>Continue Reading</Text>
</TouchableOpacity>
{/* Reading History Section */}
<View style={styles.historySection}>
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>Reading History</Text>
<TrendingUp size={16} color={colors.outline} />
</View>
{logsLoading ? (
<ActivityIndicator size="small" color={colors.primary} />
) : logs.length > 0 ? (
<View style={styles.logsList}>
{logs.map((log) => (
<View key={log.id} style={styles.logItem}>
<View style={styles.logLeft}>
<View style={styles.dateChip}>
<Calendar size={10} color={colors.primary} />
<Text style={styles.logDateText}>{formatDate(log.logged_at)}</Text>
</View>
<Text style={styles.logPagesText}>
p. {log.start_page} {log.end_page}
</Text>
</View>
<View style={styles.logRight}>
<Text style={styles.logDurationText}>
{formatDuration(log.duration_seconds)}
</Text>
<Text style={styles.logDeltaText}>+{log.pages_read} pages</Text>
</View>
</View>
))}
</View>
) : (
<Text style={styles.emptyLogsText}>No reading sessions logged yet.</Text>
)}
</View>
<View style={styles.actionSection}>
<TouchableOpacity
style={[
styles.primaryBtn,
{
backgroundColor: colors.surfaceVariant,
borderWidth: 1,
borderColor: colors.outlineVariant + "33",
marginHorizontal: 0,
},
]}
onPress={onGenerateAI}
disabled={isGeneratingAI}
>
{isGeneratingAI ? (
<ActivityIndicator size="small" color={colors.primary} />
) : (
<Sparkles size={20} color={colors.primary} />
)}
<Text
style={[styles.primaryBtnText, { color: colors.onSurface }]}
>
Generate AI Insights
</Text>
</TouchableOpacity>
<TouchableOpacity
style={styles.deleteActionBtn}
onPress={onDelete}
>
<Trash2 size={18} color={colors.error} />
<Text style={styles.deleteActionText}>Remove from Library</Text>
</TouchableOpacity>
</View>
</ScrollView>
)}
</View>
</View>
</Modal>
);
}
const createStyles = (colors: any) =>
StyleSheet.create({
modalOverlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.5)",
justifyContent: "flex-end",
},
modalContainer: {
maxHeight: "90%",
backgroundColor: colors.surface,
borderTopLeftRadius: ROUNDNESS.lg,
borderTopRightRadius: ROUNDNESS.lg,
},
modalHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
borderBottomWidth: 1,
borderBottomColor: colors.surfaceVariant,
},
modalTitle: {
fontSize: 16,
fontWeight: "700",
color: colors.onSurface,
},
detailsHeader: {
flexDirection: "row",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.lg,
gap: SPACING.lg,
},
bookCover: {
backgroundColor: colors.surfaceVariant,
borderRadius: ROUNDNESS.md,
justifyContent: "center",
alignItems: "center",
},
detailsMeta: {
flex: 1,
justifyContent: "flex-start",
},
detailsTitle: {
fontSize: 14,
fontWeight: "700",
color: colors.onSurface,
marginBottom: SPACING.sm,
},
detailsAuthor: {
fontSize: 12,
color: colors.outline,
marginBottom: SPACING.md,
},
detailsStats: {
gap: SPACING.sm,
},
detailStatItem: {
flexDirection: "row",
alignItems: "center",
gap: SPACING.sm,
},
detailStatText: {
fontSize: 11,
color: colors.onSurface,
},
primaryBtn: {
backgroundColor: colors.primary,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
borderRadius: ROUNDNESS.md,
gap: SPACING.sm,
marginHorizontal: SPACING.lg,
marginVertical: SPACING.md,
},
primaryBtnText: {
color: colors.onPrimary,
fontSize: 14,
fontWeight: "600",
letterSpacing: 0.5,
},
historySection: {
paddingHorizontal: SPACING.lg,
marginVertical: SPACING.md,
},
sectionHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: SPACING.md,
},
sectionTitle: {
fontSize: 14,
fontWeight: "700",
color: colors.onSurface,
letterSpacing: 0.5,
},
logsList: {
gap: SPACING.sm,
},
logItem: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
backgroundColor: colors.surfaceVariant + "40",
padding: SPACING.md,
borderRadius: ROUNDNESS.md,
borderWidth: 1,
borderColor: colors.outlineVariant + "20",
},
logLeft: {
gap: 4,
},
dateChip: {
flexDirection: "row",
alignItems: "center",
gap: 4,
backgroundColor: colors.primary + "15",
paddingHorizontal: 6,
paddingVertical: 2,
borderRadius: 4,
alignSelf: "flex-start",
},
logDateText: {
fontSize: 9,
fontWeight: "600",
color: colors.primary,
},
logPagesText: {
fontSize: 12,
fontWeight: "600",
color: colors.onSurface,
},
logRight: {
alignItems: "flex-end",
gap: 2,
},
logDurationText: {
fontSize: 11,
fontWeight: "600",
color: colors.onSurface,
},
logDeltaText: {
fontSize: 10,
color: colors.outline,
},
emptyLogsText: {
fontSize: 12,
color: colors.outline,
fontStyle: "italic",
textAlign: "center",
marginVertical: SPACING.md,
},
actionSection: {
paddingHorizontal: SPACING.lg,
marginTop: SPACING.lg,
gap: SPACING.md,
},
deleteActionBtn: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
marginBottom: SPACING.lg,
gap: SPACING.sm,
borderWidth: 1,
borderColor: colors.error + "30",
borderRadius: ROUNDNESS.md,
},
deleteActionText: {
color: colors.error,
fontSize: 14,
fontWeight: "600",
letterSpacing: 0.5,
},
});

View File

@ -0,0 +1,90 @@
import { SPACING } from "@/src/constants/Theme";
import { useTheme } from "@/src/hooks/useTheme";
import { Upload } from "lucide-react-native";
import React from "react";
import {
ActivityIndicator,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
import BookCard from "./BookCard";
interface Book {
id: string;
title: string;
author: string;
total_pages: number;
current_page: number;
status: "reading" | "finished" | "want_to_read";
file_uri: string;
cover_uri: string;
}
interface BookGridProps {
books: Book[];
loading: boolean;
onBookPress: (book: Book) => void;
onEmptyPress: () => void;
}
export default function BookGrid({
books,
loading,
onBookPress,
onEmptyPress,
}: BookGridProps) {
const { colors } = useTheme();
const styles = StyleSheet.create({
bookGrid: {
paddingHorizontal: SPACING.lg,
gap: SPACING.md,
},
emptyBookCard: {
flex: 1,
backgroundColor: colors.surfaceVariant + "40",
borderStyle: "dashed",
borderWidth: 2,
borderColor: colors.outline + "40",
borderRadius: 12,
padding: SPACING.lg,
alignItems: "center",
justifyContent: "center",
minHeight: 200,
gap: SPACING.md,
},
emptyBookText: {
fontSize: 14,
fontWeight: "600",
color: colors.onSurface,
},
emptyBookSub: {
fontSize: 12,
color: colors.outline,
},
});
if (loading) {
return (
<View style={[styles.bookGrid, { alignItems: "center", marginTop: 40 }]}>
<ActivityIndicator color={colors.primary} size="large" />
</View>
);
}
return (
<View style={styles.bookGrid}>
{books.map((book) => (
<BookCard key={book.id} book={book} onPress={() => onBookPress(book)} />
))}
{books.length === 0 && (
<TouchableOpacity style={styles.emptyBookCard} onPress={onEmptyPress}>
<Upload size={32} color={colors.outline} strokeWidth={1.5} />
<Text style={styles.emptyBookText}>Your library is empty</Text>
<Text style={styles.emptyBookSub}>Tap to add your first book</Text>
</TouchableOpacity>
)}
</View>
);
}

View File

@ -0,0 +1,74 @@
import { SPACING } from "@/src/constants/Theme";
import { useTheme } from "@/src/hooks/useTheme";
import { useRouter } from "expo-router";
import { Menu, Settings } from "lucide-react-native";
import React from "react";
import { Image, StyleSheet, TouchableOpacity, View } from "react-native";
interface LibraryHeaderProps {
onSettingsPress?: () => void;
onMenuPress?: () => void;
}
export default function LibraryHeader({
onSettingsPress,
onMenuPress,
}: LibraryHeaderProps) {
const { colors } = useTheme();
const router = useRouter();
const styles = StyleSheet.create({
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
backgroundColor: colors.background,
height: 60,
},
menuBtn: {
padding: 8,
},
logoContainer: {
position: "absolute",
left: 0,
right: 0,
alignItems: "center",
justifyContent: "center",
zIndex: -1,
},
logoImage: {
height: 40,
width: 160,
},
ghostBtn: {
padding: 8,
},
});
return (
<View style={styles.header}>
<TouchableOpacity
style={styles.menuBtn}
onPress={onMenuPress || (() => router.push("/menu"))}
>
<Menu size={24} color={colors.primary} strokeWidth={1.5} />
</TouchableOpacity>
<View style={styles.logoContainer}>
<Image
source={require("@/assets/images/Artboard 1 logo.png")}
style={styles.logoImage}
tintColor={colors.primary}
resizeMode="contain"
/>
</View>
<TouchableOpacity
style={styles.ghostBtn}
onPress={onSettingsPress || (() => router.push("/modal"))}
>
<Settings size={20} color={colors.primary} strokeWidth={1.5} />
</TouchableOpacity>
</View>
);
}

View File

@ -0,0 +1,67 @@
import { SPACING } from "@/src/constants/Theme";
import { useTheme } from "@/src/hooks/useTheme";
import { BookOpen, CheckCircle2, TrendingUp } from "lucide-react-native";
import React, { useMemo } from "react";
import { StyleSheet, Text, View } from "react-native";
interface LibraryStatsProps {
stats: {
reading: number;
finished: number;
pages: number;
};
}
export default function LibraryStats({ stats }: LibraryStatsProps) {
const { colors, isDark } = useTheme();
const styles = useMemo(() => createStyles(colors, isDark), [colors, isDark]);
return (
<View style={styles.statsSection}>
<View style={styles.statCard}>
<TrendingUp size={18} color={colors.primary} />
<Text style={styles.statValue}>{stats.pages}</Text>
<Text style={styles.statLabel}>PAGES READ</Text>
</View>
<View style={styles.statCard}>
<BookOpen size={18} color={colors.secondary} />
<Text style={styles.statValue}>{stats.reading}</Text>
<Text style={styles.statLabel}>ACTIVE</Text>
</View>
<View style={styles.statCard}>
<CheckCircle2 size={18} color={colors.tertiary} />
<Text style={styles.statValue}>{stats.finished}</Text>
<Text style={styles.statLabel}>FINISHED</Text>
</View>
</View>
);
}
const createStyles = (colors: any, isDark: boolean) =>
StyleSheet.create({
statsSection: {
flexDirection: "row",
gap: SPACING.md,
marginVertical: SPACING.lg,
paddingHorizontal: SPACING.lg,
},
statCard: {
flex: 1,
backgroundColor: colors.surfaceVariant,
borderRadius: 12,
padding: SPACING.md,
alignItems: "center",
gap: SPACING.sm,
},
statValue: {
fontSize: 18,
fontWeight: "700",
color: colors.onSurface,
},
statLabel: {
fontSize: 9,
fontWeight: "600",
color: colors.outline,
letterSpacing: 0.5,
},
});

View File

@ -0,0 +1,138 @@
import { ROUNDNESS, SPACING } from "@/src/constants/Theme";
import { useTheme } from "@/src/hooks/useTheme";
import { Save, X } from "lucide-react-native";
import React from "react";
import {
KeyboardAvoidingView,
Modal,
Platform,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from "react-native";
interface NoteEditorModalProps {
visible: boolean;
onClose: () => void;
currentPage: number;
currentNote: string;
onNoteChange: (text: string) => void;
onSave: () => void;
isEditing: boolean;
}
export default function NoteEditorModal({
visible,
onClose,
currentPage,
currentNote,
onNoteChange,
onSave,
isEditing,
}: NoteEditorModalProps) {
const { colors } = useTheme();
const styles = StyleSheet.create({
modalOverlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.5)",
justifyContent: "flex-end",
},
modalContainer: {
maxHeight: "90%",
backgroundColor: colors.surface,
borderTopLeftRadius: ROUNDNESS.lg,
borderTopRightRadius: ROUNDNESS.lg,
},
modalHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
borderBottomWidth: 1,
borderBottomColor: colors.surfaceVariant,
},
modalTitle: {
fontSize: 16,
fontWeight: "700",
color: colors.onSurface,
},
modalField: {
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
},
noteInput: {
minHeight: 120,
textAlignVertical: "top",
paddingTop: SPACING.md,
},
modalInput: {
borderWidth: 1,
borderColor: colors.surfaceVariant,
borderRadius: ROUNDNESS.md,
paddingHorizontal: SPACING.md,
paddingVertical: SPACING.md,
fontSize: 14,
color: colors.onSurface,
backgroundColor: colors.background,
},
primaryBtn: {
backgroundColor: colors.primary,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
borderRadius: ROUNDNESS.md,
gap: SPACING.sm,
marginHorizontal: SPACING.lg,
marginBottom: SPACING.lg,
},
primaryBtnText: {
color: colors.onPrimary,
fontSize: 14,
fontWeight: "600",
letterSpacing: 0.5,
},
});
return (
<Modal visible={visible} transparent animationType="slide">
<View style={styles.modalOverlay}>
<KeyboardAvoidingView
behavior={Platform.OS === "ios" ? "padding" : "height"}
style={styles.modalContainer}
>
<View style={styles.modalHeader}>
<Text style={styles.modalTitle}>
{isEditing ? "Edit Note" : `Add Note - Page ${currentPage}`}
</Text>
<TouchableOpacity onPress={onClose}>
<X size={24} color={colors.onSurface} />
</TouchableOpacity>
</View>
<View style={styles.modalField}>
<TextInput
style={[styles.modalInput, styles.noteInput]}
value={currentNote}
onChangeText={onNoteChange}
placeholder="Write your note here..."
placeholderTextColor={colors.outline}
multiline
numberOfLines={5}
textAlignVertical="top"
/>
</View>
<TouchableOpacity style={styles.primaryBtn} onPress={onSave}>
<Save size={20} color={colors.onPrimary} />
<Text style={styles.primaryBtnText}>
{isEditing ? "Update Note" : "Save Note"}
</Text>
</TouchableOpacity>
</KeyboardAvoidingView>
</View>
</Modal>
);
}

View File

@ -0,0 +1,140 @@
import { SPACING } from "@/src/constants/Theme";
import { useTheme } from "@/src/hooks/useTheme";
import { Pencil, Plus, Trash2 } from "lucide-react-native";
import React, { useMemo } from "react";
import {
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
import Animated from "react-native-reanimated";
interface Note {
id: string;
page: number;
content: string;
timestamp: string;
color?: string;
}
interface NotesPanelProps {
currentPage: number;
notes: Note[];
animatedStyle: any;
onAddNote: () => void;
onEditNote: (note: Note) => void;
onDeleteNote: (noteId: string) => void;
}
export default function NotesPanel({
currentPage,
notes,
animatedStyle,
onAddNote,
onEditNote,
onDeleteNote,
}: NotesPanelProps) {
const { colors } = useTheme();
const styles = useMemo(() => createStyles(colors), [colors]);
const currentPageNotes = notes.filter((note) => note.page === currentPage);
return (
<Animated.View style={[styles.notesPanel, animatedStyle]}>
<View style={styles.notesHeader}>
<Text style={styles.notesTitle}>Notes for Page {currentPage}</Text>
<TouchableOpacity onPress={onAddNote}>
<Plus size={20} color={colors.primary} />
</TouchableOpacity>
</View>
<ScrollView style={styles.notesList}>
{currentPageNotes.map((note) => (
<View key={note.id} style={styles.noteItem}>
<View style={styles.noteContent}>
<Text style={styles.noteText}>{note.content}</Text>
<Text style={styles.noteTimestamp}>
{new Date(note.timestamp).toLocaleTimeString()}
</Text>
</View>
<View style={styles.noteActions}>
<TouchableOpacity onPress={() => onEditNote(note)}>
<Pencil size={16} color={colors.outline} />
</TouchableOpacity>
<TouchableOpacity onPress={() => onDeleteNote(note.id)}>
<Trash2 size={16} color={colors.error} />
</TouchableOpacity>
</View>
</View>
))}
{currentPageNotes.length === 0 && (
<Text style={styles.noNotesText}>No notes for this page</Text>
)}
</ScrollView>
</Animated.View>
);
}
const createStyles = (colors: any) =>
StyleSheet.create({
notesPanel: {
position: "absolute",
bottom: 0,
left: 0,
right: 0,
backgroundColor: colors.surface,
borderTopWidth: 1,
borderTopColor: colors.surfaceVariant,
zIndex: 50,
},
notesHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
borderBottomWidth: 1,
borderBottomColor: colors.surfaceVariant,
},
notesTitle: {
fontSize: 14,
fontWeight: "600",
color: colors.onSurface,
},
notesList: {
maxHeight: 200,
},
noteItem: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "flex-start",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
borderBottomWidth: 1,
borderBottomColor: colors.surfaceVariant,
},
noteContent: {
flex: 1,
marginRight: SPACING.md,
},
noteText: {
fontSize: 13,
color: colors.onSurface,
marginBottom: SPACING.xs,
},
noteTimestamp: {
fontSize: 10,
color: colors.outline,
},
noteActions: {
flexDirection: "row",
gap: SPACING.sm,
},
noNotesText: {
fontSize: 12,
color: colors.outline,
textAlign: "center",
paddingVertical: SPACING.lg,
},
});

View File

@ -0,0 +1,464 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import {
StyleSheet,
View,
Text,
TouchableOpacity,
Dimensions,
ActivityIndicator,
TextInput,
Platform,
} from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
FadeIn,
FadeOut,
} from 'react-native-reanimated';
import { BlurView } from 'expo-blur';
import {
X,
ChevronLeft,
Settings,
BookOpen,
MessageSquare,
Clock,
Play,
Pause,
ArrowRight
} from 'lucide-react-native';
import { ROUNDNESS, SPACING } from '@/src/constants/Theme';
import { useTheme } from '@/src/hooks/useTheme';
import Constants, { ExecutionEnvironment } from 'expo-constants';
import { WebView } from 'react-native-webview';
import PDFReader from '@bildau/rn-pdf-reader';
import * as FileSystem from 'expo-file-system/legacy';
// Safe PDF Component Wrapper
const PdfRendererComponent = React.memo((props: any) => {
const { source, style, onLoad, onPageChange, page } = props;
const [base64Source, setBase64Source] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let isMounted = true;
const loadSource = async () => {
if (Platform.OS === 'android' && source.startsWith('file://')) {
try {
console.log('[PdfReader] Android local file detected, reading as base64...');
const base64 = await FileSystem.readAsStringAsync(source, {
encoding: FileSystem.EncodingType.Base64,
});
if (isMounted) {
setBase64Source(`data:application/pdf;base64,${base64}`);
}
} catch (e: any) {
console.error('[PdfReader] Failed to read local file as base64:', e);
if (isMounted) {
setError(e.message);
}
}
} else {
if (isMounted) {
setBase64Source(null);
}
}
};
loadSource();
return () => { isMounted = false; };
}, [source]);
// Memoize source object to avoid triggering PDFReader's componentDidUpdate unnecessarily
const memoizedSource = useMemo(() => {
if (base64Source) {
return { base64: base64Source };
}
return { uri: source };
}, [source, base64Source]);
if (Platform.OS === 'android' && source.startsWith('file://') && !base64Source && !error) {
return (
<View style={[style, { justifyContent: 'center', alignItems: 'center' }]}>
<ActivityIndicator size="large" color="#fff" />
</View>
);
}
return (
<PDFReader
source={memoizedSource}
style={style}
withScroll={true}
withPinchZoom={true}
noLoader={true}
customStyle={{
readerContainerNavigate: { display: 'none' },
readerContainerNumbers: { display: 'none' },
}}
onLoad={(event: any) => {
// The library doesn't easily expose total pages here,
// but we can signal it's loaded.
onLoad?.(0);
}}
webviewProps={{
onMessage: (event: any) => {
try {
const data = JSON.parse(event.nativeEvent.data);
if (data.type === 'pageChange') {
onPageChange?.(data.page - 1, data.total);
}
} catch (e) {}
}
}}
/>
);
});
const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
interface PdfReaderProps {
uri: string;
title: string;
initialPage?: number;
onClose: () => void;
onPageChange?: (page: number, total: number) => void;
onSessionUpdate?: (seconds: number) => void;
onAddNote?: (page: number) => void;
}
export const PdfReader: React.FC<PdfReaderProps> = ({
uri,
title,
initialPage = 0,
onClose,
onPageChange,
onSessionUpdate,
onAddNote,
}) => {
const { colors, isDark } = useTheme();
const insets = useSafeAreaInsets();
// State
const [currentPage, setCurrentPage] = useState(initialPage);
const [totalPages, setTotalPages] = useState(0);
const [controlsVisible, setControlsVisible] = useState(true);
const [sessionSeconds, setSessionSeconds] = useState(0);
const [isTimerRunning, setIsTimerRunning] = useState(true);
const [jumpToPage, setJumpToPage] = useState('');
const [isLoading, setIsLoading] = useState(true);
// Animations
const controlsOpacity = useSharedValue(1);
const topBarY = useSharedValue(0);
const bottomBarY = useSharedValue(0);
// Timer logic
useEffect(() => {
let interval: NodeJS.Timeout | null = null;
if (isTimerRunning) {
interval = setInterval(() => {
setSessionSeconds(s => s + 1);
}, 1000);
}
return () => {
if (interval) clearInterval(interval);
};
}, [isTimerRunning]);
// Report session time to parent safely
useEffect(() => {
onSessionUpdate?.(sessionSeconds);
}, [sessionSeconds]);
// Auto-hide controls
useEffect(() => {
let timer: NodeJS.Timeout;
if (controlsVisible) {
timer = setTimeout(() => {
toggleControls(false);
}, 5000);
}
return () => clearTimeout(timer);
}, [controlsVisible]);
const toggleControls = useCallback((force?: boolean) => {
const nextVisible = force !== undefined ? force : !controlsVisible;
setControlsVisible(nextVisible);
const targetOpacity = nextVisible ? 1 : 0;
const targetY = nextVisible ? 0 : -100;
const targetBottomY = nextVisible ? 0 : 100;
controlsOpacity.value = withTiming(targetOpacity);
topBarY.value = withTiming(targetY);
bottomBarY.value = withTiming(targetBottomY);
}, [controlsVisible]);
const animatedTopBarStyle = useAnimatedStyle(() => ({
opacity: controlsOpacity.value,
transform: [{ translateY: topBarY.value }],
}));
const animatedBottomBarStyle = useAnimatedStyle(() => ({
opacity: controlsOpacity.value,
transform: [{ translateY: bottomBarY.value }],
}));
const formatTime = (totalSeconds: number) => {
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
};
const handlePageChange = useCallback((page: number, total: number) => {
setCurrentPage(page);
setTotalPages(total);
// Use a small delay to avoid "update while rendering" if called synchronously by PdfRenderer
setTimeout(() => {
onPageChange?.(page, total);
}, 0);
}, [onPageChange]);
const handleLoad = useCallback((total: number) => {
setTotalPages(total);
setIsLoading(false);
}, []);
const handleGoToPage = () => {
const page = parseInt(jumpToPage);
if (!isNaN(page) && page >= 1 && page <= totalPages) {
// In react-native-pdf-renderer, we might need a ref to the component to scroll
// For now, we update the state and assume the component reacts to it or we'll add a ref later
setCurrentPage(page - 1);
setJumpToPage('');
}
};
return (
<View style={[styles.container, { backgroundColor: '#000' }]}>
{/* PDF View */}
<TouchableOpacity
activeOpacity={1}
onPress={() => toggleControls()}
style={styles.pdfContainer}
>
<PdfRendererComponent
source={uri}
style={styles.pdf}
onLoad={handleLoad}
onPageChange={handlePageChange}
page={currentPage}
/>
{isLoading && (
<View style={styles.loadingOverlay}>
<ActivityIndicator size="large" color={colors.primary} />
</View>
)}
</TouchableOpacity>
{/* Top Controls */}
<Animated.View style={[styles.topBarContainer, animatedTopBarStyle, { top: insets.top + SPACING.sm }]}>
<BlurView intensity={isDark ? 40 : 60} tint={isDark ? 'dark' : 'light'} style={styles.glassBar}>
<View style={styles.topBarContent}>
<TouchableOpacity onPress={onClose} style={styles.iconButton}>
<X size={24} color={isDark ? '#fff' : '#000'} />
</TouchableOpacity>
<View style={styles.titleContainer}>
<Text style={[styles.bookTitle, { color: isDark ? '#fff' : '#000' }]} numberOfLines={1}>
{title}
</Text>
<Text style={styles.pageText}>
{currentPage + 1} / {totalPages}
</Text>
</View>
<View style={styles.timerChip}>
<Clock size={14} color={colors.primary} />
<Text style={[styles.timerText, { color: colors.primary }]}>
{formatTime(sessionSeconds)}
</Text>
</View>
<TouchableOpacity
onPress={() => onAddNote?.(currentPage)}
style={styles.iconButton}
>
<MessageSquare size={22} color={isDark ? '#fff' : '#000'} />
</TouchableOpacity>
</View>
</BlurView>
</Animated.View>
{/* Bottom Controls */}
<Animated.View style={[styles.bottomBarContainer, animatedBottomBarStyle, { bottom: insets.bottom + SPACING.md }]}>
<BlurView intensity={isDark ? 40 : 60} tint={isDark ? 'dark' : 'light'} style={styles.glassBar}>
<View style={styles.bottomBarContent}>
<View style={styles.jumpContainer}>
<TextInput
style={[styles.pageInput, { color: isDark ? '#fff' : '#000', borderColor: isDark ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.1)' }]}
placeholder="Page"
placeholderTextColor={isDark ? 'rgba(255,255,255,0.4)' : 'rgba(0,0,0,0.4)'}
keyboardType="numeric"
value={jumpToPage}
onChangeText={setJumpToPage}
onSubmitEditing={handleGoToPage}
/>
<TouchableOpacity onPress={handleGoToPage} style={[styles.goButton, { backgroundColor: colors.primary }]}>
<ArrowRight size={18} color="#fff" />
</TouchableOpacity>
</View>
<TouchableOpacity
onPress={() => setIsTimerRunning(!isTimerRunning)}
style={[styles.sessionButton, { backgroundColor: isTimerRunning ? colors.secondaryContainer : colors.primary }]}
>
{isTimerRunning ? (
<>
<Pause size={18} color={colors.onSecondaryContainer} />
<Text style={[styles.sessionButtonText, { color: colors.onSecondaryContainer }]}>Pause Session</Text>
</>
) : (
<>
<Play size={18} color="#fff" />
<Text style={[styles.sessionButtonText, { color: '#fff' }]}>Resume Session</Text>
</>
)}
</TouchableOpacity>
</View>
</BlurView>
</Animated.View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
},
pdfContainer: {
flex: 1,
},
pdf: {
flex: 1,
width: SCREEN_WIDTH,
height: SCREEN_HEIGHT,
},
loadingOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
alignItems: 'center',
},
topBarContainer: {
position: 'absolute',
left: SPACING.md,
right: SPACING.md,
zIndex: 1000,
},
bottomBarContainer: {
position: 'absolute',
left: SPACING.md,
right: SPACING.md,
zIndex: 1000,
},
glassBar: {
borderRadius: ROUNDNESS.full,
overflow: 'hidden',
padding: SPACING.sm,
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.1)',
},
topBarContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: SPACING.sm,
},
titleContainer: {
flex: 1,
marginHorizontal: SPACING.md,
alignItems: 'center',
},
bookTitle: {
fontSize: 14,
fontWeight: '700',
textAlign: 'center',
},
pageText: {
fontSize: 12,
color: 'rgba(128,128,128,0.8)',
marginTop: 2,
},
iconButton: {
width: 40,
height: 40,
borderRadius: 20,
justifyContent: 'center',
alignItems: 'center',
},
timerChip: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: 'rgba(255,255,255,0.1)',
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 12,
marginRight: SPACING.sm,
},
timerText: {
fontSize: 12,
fontWeight: '600',
marginLeft: 4,
},
bottomBarContent: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: SPACING.sm,
},
jumpContainer: {
flexDirection: 'row',
alignItems: 'center',
flex: 1,
marginRight: SPACING.md,
},
pageInput: {
width: 60,
height: 40,
borderRadius: 20,
borderWidth: 1,
paddingHorizontal: 12,
fontSize: 14,
backgroundColor: 'rgba(255,255,255,0.05)',
},
goButton: {
width: 36,
height: 36,
borderRadius: 18,
justifyContent: 'center',
alignItems: 'center',
marginLeft: 8,
},
sessionButton: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 16,
paddingVertical: 10,
borderRadius: 25,
},
sessionButtonText: {
fontSize: 14,
fontWeight: '700',
marginLeft: 8,
},
});
export default PdfReader;

View File

@ -0,0 +1,125 @@
import React, { useEffect } from 'react';
import { StyleSheet, View, Image, Dimensions, Text } from 'react-native';
import Animated, {
useSharedValue,
useAnimatedStyle,
withTiming,
withSequence,
withDelay,
runOnJS,
Easing
} from 'react-native-reanimated';
const { width, height } = Dimensions.get('window');
interface Props {
onAnimationFinish: () => void;
backgroundColor: string;
}
export function AnimatedSplashScreen({ onAnimationFinish, backgroundColor }: Props) {
const scale = useSharedValue(0.3);
const opacity = useSharedValue(0);
const footerOpacity = useSharedValue(0);
const containerOpacity = useSharedValue(1);
const animatedLogoStyle = useAnimatedStyle(() => {
'worklet';
return {
transform: [{ scale: scale.value }],
opacity: opacity.value,
};
});
const animatedFooterStyle = useAnimatedStyle(() => {
'worklet';
return {
opacity: footerOpacity.value,
transform: [{ translateY: withTiming(footerOpacity.value === 1 ? 0 : 20, { duration: 800 }) }],
};
});
const animatedContainerStyle = useAnimatedStyle(() => {
'worklet';
return {
opacity: containerOpacity.value,
};
});
useEffect(() => {
// Sequence: Fade in and scale up -> Hold -> Zoom in/Fade out container
scale.value = withTiming(1, {
duration: 1000,
easing: Easing.out(Easing.back(1.5))
});
opacity.value = withTiming(1, { duration: 800 });
// Fade in footer slightly later
footerOpacity.value = withDelay(400, withTiming(1, { duration: 800 }));
containerOpacity.value = withDelay(
2500,
withTiming(0, { duration: 500 }, (finished) => {
if (finished) {
runOnJS(onAnimationFinish)();
}
})
);
}, []);
return (
<Animated.View style={[styles.container, { backgroundColor }, animatedContainerStyle]}>
<View style={styles.content}>
<Animated.Image
source={require('@/assets/images/icon.png')}
style={[styles.logo, animatedLogoStyle]}
resizeMode="contain"
/>
</View>
<Animated.View style={[styles.footer, animatedFooterStyle]}>
<Text style={styles.poweredBy}>powered by</Text>
<Image
source={require('@/assets/images/Artboard 1 logo.png')}
style={styles.footerLogo}
resizeMode="contain"
/>
</Animated.View>
</Animated.View>
);
}
const styles = StyleSheet.create({
container: {
...StyleSheet.absoluteFillObject,
alignItems: 'center',
justifyContent: 'center',
zIndex: 9999,
},
content: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
logo: {
width: width * 0.45,
height: width * 0.45,
},
footer: {
position: 'absolute',
bottom: 50,
alignItems: 'center',
gap: 8,
},
poweredBy: {
fontSize: 12,
color: '#94a3b8',
textTransform: 'lowercase',
letterSpacing: 1,
fontFamily: 'Manrope_400Regular',
},
footerLogo: {
width: width * 0.3,
height: 40,
},
});

View File

@ -0,0 +1,77 @@
import { SPACING, ThemeColors } from "@/src/constants/Theme";
import { Menu, Settings } from "lucide-react-native";
import React, { useMemo } from "react";
import { Image, StyleSheet, TouchableOpacity, View } from "react-native";
interface ChatHeaderProps {
colors: ThemeColors;
onMenuPress: () => void;
onSettingsPress: () => void;
}
const ChatHeader = ({
colors,
onMenuPress,
onSettingsPress,
}: ChatHeaderProps) => {
const styles = useMemo(() => createStyles(colors), [colors]);
return (
<View style={styles.header}>
<TouchableOpacity
style={styles.iconButton}
onPress={onMenuPress}
accessibilityRole="button"
accessibilityLabel="Open menu"
>
<Menu size={24} color={colors.primary} strokeWidth={1.5} />
</TouchableOpacity>
<View style={styles.logoContainer}>
<Image
source={require("@/assets/images/Artboard 1 logo.png")}
style={[styles.logoImage, { tintColor: colors.primary }]}
resizeMode="contain"
/>
</View>
<TouchableOpacity
style={styles.iconButton}
onPress={onSettingsPress}
accessibilityRole="button"
accessibilityLabel="Open settings"
>
<Settings size={20} color={colors.primary} strokeWidth={1.5} />
</TouchableOpacity>
</View>
);
};
const createStyles = (_colors: ThemeColors) =>
StyleSheet.create({
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
height: 60,
},
iconButton: {
padding: 8,
},
logoContainer: {
position: "absolute",
left: 0,
right: 0,
alignItems: "center",
justifyContent: "center",
zIndex: -1,
},
logoImage: {
height: 40,
width: 160,
},
});
export default React.memo(ChatHeader);

View File

@ -0,0 +1,106 @@
import { FONTS, ROUNDNESS, SPACING, ThemeColors } from "@/src/constants/Theme";
import { Send } from "lucide-react-native";
import React, { useMemo } from "react";
import {
Platform,
StyleSheet,
TextInput,
TouchableOpacity,
View,
} from "react-native";
interface ChatInputProps {
inputText: string;
onChangeText: (text: string) => void;
onSend: (text: string) => void;
placeholder: string;
colors: ThemeColors;
disabled?: boolean;
}
const ChatInput = ({
inputText,
onChangeText,
onSend,
placeholder,
colors,
disabled = false,
}: ChatInputProps) => {
const styles = useMemo(() => createStyles(colors), [colors]);
const handleSend = () => {
if (inputText.trim() && !disabled) {
onSend(inputText);
}
};
return (
<View style={styles.inputSection}>
<View style={styles.inputBarContainer}>
<TextInput
style={[styles.input, disabled && { opacity: 0.5 }]}
placeholder={placeholder}
placeholderTextColor={colors.onSurfaceVariant + "80"}
value={inputText}
onChangeText={onChangeText}
onSubmitEditing={handleSend}
returnKeyType="send"
blurOnSubmit={false}
multiline={false}
editable={!disabled}
accessibilityLabel="Chat input"
accessibilityHint="Type a message for the Habit Architect"
/>
<TouchableOpacity
style={[
styles.sendButton,
{ backgroundColor: colors.primary },
disabled && { backgroundColor: colors.outlineVariant, opacity: 0.7 },
]}
onPress={handleSend}
disabled={disabled}
accessibilityRole="button"
accessibilityLabel="Send message"
>
<Send size={18} color={disabled ? colors.onSurfaceVariant : colors.onPrimary} />
</TouchableOpacity>
</View>
</View>
);
};
const createStyles = (colors: ThemeColors) =>
StyleSheet.create({
inputSection: {
backgroundColor: colors.surface,
paddingBottom: Platform.OS === "ios" ? 10 : 20,
},
inputBarContainer: {
flexDirection: "row",
alignItems: "center",
backgroundColor: colors.surface,
marginHorizontal: SPACING.lg,
borderRadius: ROUNDNESS.xl,
borderWidth: 1,
borderColor: colors.outlineVariant + "80",
padding: 4,
marginBottom: 10,
},
input: {
flex: 1,
fontFamily: FONTS.body,
fontSize: 15,
paddingHorizontal: 16,
height: 48,
color: colors.onSurface,
},
sendButton: {
width: 40,
height: 40,
borderRadius: 20,
alignItems: "center",
justifyContent: "center",
},
});
export default React.memo(ChatInput);

View File

@ -0,0 +1,155 @@
import TypingIndicator from "@/src/components/chat/TypingIndicator";
import { FONTS, ROUNDNESS, SPACING, ThemeColors } from "@/src/constants/Theme";
import { ChatMessage } from "@/src/types/chat";
import { formatTime } from "@/src/utils/formatTime";
import { CheckCircle2 } from "lucide-react-native";
import React, { useMemo } from "react";
import { StyleSheet, Text, View } from "react-native";
interface MessageBubbleProps {
message: ChatMessage;
isUser: boolean;
colors: ThemeColors;
}
const MessageBubble = ({ message, isUser, colors }: MessageBubbleProps) => {
const styles = useMemo(() => createStyles(colors), [colors]);
const actionLabel = message.action?.replace(/_/g, " ");
const isSending = message.status === "sending";
const hasError = message.status === "error";
return (
<View
style={[
styles.messageWrapper,
isUser ? styles.userMessageWrapper : styles.aiMessageWrapper,
]}
>
<View
style={[
styles.messageBubble,
isUser ? styles.userBubble : styles.aiBubble,
]}
>
{isSending ? (
<TypingIndicator colors={colors} />
) : (
<Text
style={[
styles.messageText,
isUser ? styles.userText : styles.aiText,
]}
>
{message.content}
</Text>
)}
{actionLabel ? (
<View style={styles.actionBadge}>
<CheckCircle2
size={12}
color={isUser ? colors.onPrimary : colors.primary}
/>
<Text
style={[
styles.actionText,
{ color: isUser ? colors.onPrimary : colors.primary },
]}
>
{actionLabel}
</Text>
</View>
) : null}
{hasError ? (
<Text style={styles.errorText} accessibilityRole="text">
Unable to generate response. Tap send again.
</Text>
) : null}
</View>
<Text style={styles.messageTime}>{formatTime(message.createdAt)}</Text>
</View>
);
};
const createStyles = (colors: ThemeColors) =>
StyleSheet.create({
messageWrapper: {
marginBottom: SPACING.lg,
maxWidth: "85%",
},
aiMessageWrapper: {
alignSelf: "flex-start",
},
userMessageWrapper: {
alignSelf: "flex-end",
alignItems: "flex-end",
},
messageBubble: {
padding: 14,
borderRadius: ROUNDNESS.lg,
},
aiBubble: {
backgroundColor: colors.surface,
borderWidth: 1,
borderColor: colors.outlineVariant + "4D",
borderTopLeftRadius: 4,
},
userBubble: {
backgroundColor: colors.primary,
borderTopRightRadius: 4,
},
messageText: {
fontFamily: FONTS.body,
fontSize: 15,
lineHeight: 22,
},
aiText: {
color: colors.onSurface,
},
userText: {
color: colors.onPrimary,
},
actionBadge: {
flexDirection: "row",
alignItems: "center",
gap: 4,
marginTop: 8,
backgroundColor: "rgba(0,0,0,0.05)",
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 4,
alignSelf: "flex-start",
},
actionText: {
fontFamily: FONTS.label,
fontSize: 9,
letterSpacing: 0.5,
textTransform: "uppercase",
},
messageTime: {
fontFamily: FONTS.label,
fontSize: 9,
color: colors.onSurfaceVariant,
marginTop: 6,
},
errorText: {
marginTop: 10,
fontFamily: FONTS.label,
fontSize: 11,
color: colors.error,
},
});
const areEqual = (prev: MessageBubbleProps, next: MessageBubbleProps) =>
prev.message.id === next.message.id &&
prev.message.content === next.message.content &&
prev.message.status === next.message.status &&
prev.message.action === next.message.action &&
prev.isUser === next.isUser &&
prev.colors.primary === next.colors.primary &&
prev.colors.surface === next.colors.surface &&
prev.colors.onSurface === next.colors.onSurface;
export default React.memo(MessageBubble, areEqual);

View File

@ -0,0 +1,87 @@
import { FONTS, ROUNDNESS, SPACING, ThemeColors } from "@/src/constants/Theme";
import React, { useCallback } from "react";
import {
FlatList,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
const suggestions = [
"Audit my habits",
"Suggest a 2-min version",
"Log my workout",
"Help me build a morning routine",
] as const;
interface SuggestionChipsProps {
colors: ThemeColors;
onSuggestionPress: (message: string) => void;
disabled?: boolean;
}
const SuggestionChips = ({
colors,
onSuggestionPress,
disabled = false,
}: SuggestionChipsProps) => {
const styles = createStyles(colors);
const renderSuggestion = useCallback(
({ item }: { item: string }) => (
<TouchableOpacity
style={[styles.suggestionButton, disabled && { opacity: 0.5 }]}
onPress={() => !disabled && onSuggestionPress(item)}
disabled={disabled}
accessibilityRole="button"
accessibilityLabel={`Send suggestion: ${item}`}
>
<Text style={styles.suggestionText}>{item}</Text>
</TouchableOpacity>
),
[onSuggestionPress, styles, disabled],
);
return (
<View style={styles.container}>
<FlatList
horizontal
data={suggestions}
keyExtractor={(item) => item}
renderItem={renderSuggestion}
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.content}
ItemSeparatorComponent={() => <View style={styles.suggestionSpacer} />}
/>
</View>
);
};
const createStyles = (colors: ThemeColors) =>
StyleSheet.create({
container: {
paddingVertical: SPACING.md,
},
content: {
paddingHorizontal: SPACING.lg,
},
suggestionSpacer: {
width: SPACING.sm,
},
suggestionButton: {
backgroundColor: colors.surfaceVariant + "80",
paddingHorizontal: 12,
paddingVertical: 8,
borderRadius: ROUNDNESS.md,
borderWidth: 1,
borderColor: colors.outlineVariant + "33",
},
suggestionText: {
fontFamily: FONTS.labelSm,
fontSize: 11,
color: colors.onSurface,
},
});
export default React.memo(SuggestionChips);

View File

@ -0,0 +1,48 @@
import { FONTS, ROUNDNESS, ThemeColors } from "@/src/constants/Theme";
import React, { useEffect, useMemo, useState } from "react";
import { StyleSheet, Text, View } from "react-native";
interface TypingIndicatorProps {
colors: ThemeColors;
}
const TypingIndicator = ({ colors }: TypingIndicatorProps) => {
const [dots, setDots] = useState(1);
const styles = useMemo(() => createStyles(colors), [colors]);
useEffect(() => {
const interval = setInterval(() => {
setDots((value) => (value % 3) + 1);
}, 420);
return () => clearInterval(interval);
}, []);
return (
<View style={styles.wrapper} accessibilityRole="text">
<Text
style={styles.text}
>{`Architecting response${".".repeat(dots)}`}</Text>
</View>
);
};
const createStyles = (colors: ThemeColors) =>
StyleSheet.create({
wrapper: {
backgroundColor: colors.surface,
paddingVertical: 10,
paddingHorizontal: 12,
borderRadius: ROUNDNESS.lg,
borderWidth: 1,
borderColor: colors.outlineVariant + "4D",
},
text: {
fontFamily: FONTS.body,
fontSize: 15,
lineHeight: 22,
color: colors.onSurface,
},
});
export default React.memo(TypingIndicator);

View File

@ -0,0 +1,390 @@
import React, { useMemo, useRef, useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, ScrollView, StyleSheet, Modal, Platform, FlatList } from 'react-native';
import { FONTS, SPACING, ROUNDNESS } from '@/src/constants/Theme';
import { getLocalDateString } from '@/src/lib/date-utils';
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, X, Check } from 'lucide-react-native';
import * as Haptics from 'expo-haptics';
interface CalendarStripProps {
selectedDate: string; // YYYY-MM-DD
onDateSelect: (date: string) => void;
colors: any;
}
export const CalendarStrip: React.FC<CalendarStripProps> = ({ selectedDate, onDateSelect, colors }) => {
const scrollRef = useRef<ScrollView>(null);
const [showPicker, setShowPicker] = useState(false);
// Constants for scroll calculation
const ITEM_WIDTH = 55;
const GAP = 12;
// Scroll to selected date on mount and when selectedDate changes
useEffect(() => {
// We want the selected item (index 7 in our 15-day range) to be centered
// Since we generate days centered around selectedDate, it's always at index 7
const timer = setTimeout(() => {
if (scrollRef.current) {
// Approximate centering logic
// Each item + gap is ~67px. 7 items before = 469px.
// We want to scroll so the 8th item is in the middle of the screen.
scrollRef.current.scrollTo({
x: (7 * (ITEM_WIDTH + GAP)) - 100, // Offset to bring it toward center
animated: true
});
}
}, 100);
return () => clearTimeout(timer);
}, [selectedDate]);
// Generate days centered around selectedDate
const days = useMemo(() => {
const result = [];
const centerDate = new Date(selectedDate + 'T00:00:00'); // Ensure local time
if (isNaN(centerDate.getTime())) return [];
const start = new Date(centerDate);
start.setDate(centerDate.getDate() - 7);
for (let i = 0; i < 15; i++) {
const d = new Date(start);
d.setDate(start.getDate() + i);
const dateStr = getLocalDateString(d);
result.push({
date: dateStr,
dayName: d.toLocaleDateString('en-US', { weekday: 'short' }),
dayNum: d.getDate().toString(),
isToday: dateStr === getLocalDateString(new Date()),
});
}
return result;
}, [selectedDate]);
const handleDatePress = (date: string) => {
onDateSelect(date);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
};
const openPicker = () => {
setShowPicker(true);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
};
return (
<View style={styles.outerContainer}>
<View style={styles.stripWithButton}>
<TouchableOpacity
onPress={openPicker}
style={[styles.pickerBtn, { backgroundColor: colors.surfaceVariant + '4D', borderColor: colors.outlineVariant + '33' }]}
>
<CalendarIcon size={20} color={colors.primary} />
</TouchableOpacity>
<ScrollView
ref={scrollRef}
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={styles.container}
>
{days.map((day) => {
const isSelected = day.date === selectedDate;
return (
<TouchableOpacity
key={day.date}
onPress={() => handleDatePress(day.date)}
activeOpacity={0.8}
style={[
styles.dayCell,
isSelected && { backgroundColor: colors.primary }
]}
>
<Text style={[
styles.dayName,
isSelected ? { color: colors.onPrimary, fontFamily: FONTS.labelSm } : { color: colors.outline }
]}>
{day.dayName.toUpperCase()}
</Text>
<View style={[
styles.dayNumContainer,
isSelected && { borderWidth: 1, borderColor: colors.onPrimary + '80' }
]}>
<Text style={[
styles.dayNum,
isSelected ? { color: colors.onPrimary } : { color: colors.onSurface }
]}>
{day.dayNum}
</Text>
</View>
{day.isToday && !isSelected && (
<View style={[styles.todayIndicator, { backgroundColor: colors.primary }]} />
)}
</TouchableOpacity>
);
})}
</ScrollView>
</View>
<DatePickerModal
visible={showPicker}
onClose={() => setShowPicker(false)}
selectedDate={selectedDate}
onSelect={(date) => {
onDateSelect(date);
setShowPicker(false);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
}}
colors={colors}
/>
</View>
);
};
const DatePickerModal = ({ visible, onClose, selectedDate, onSelect, colors }: any) => {
const [viewDate, setViewDate] = useState(new Date(selectedDate + 'T00:00:00'));
useEffect(() => {
if (visible) {
setViewDate(new Date(selectedDate + 'T00:00:00'));
}
}, [visible, selectedDate]);
const monthName = viewDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' });
const daysInMonth = useMemo(() => {
const year = viewDate.getFullYear();
const month = viewDate.getMonth();
const firstDay = new Date(year, month, 1).getDay();
const lastDate = new Date(year, month + 1, 0).getDate();
const days = [];
// Padding for start of month
for (let i = 0; i < firstDay; i++) {
days.push(null);
}
// Days of month
for (let i = 1; i <= lastDate; i++) {
days.push(new Date(year, month, i));
}
return days;
}, [viewDate]);
const changeMonth = (offset: number) => {
const next = new Date(viewDate);
next.setMonth(viewDate.getMonth() + offset);
setViewDate(next);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
};
return (
<Modal visible={visible} transparent animationType="fade">
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: colors.surface }]}>
<View style={styles.modalHeader}>
<Text style={[styles.modalTitle, { color: colors.onSurface }]}>Select Date</Text>
<TouchableOpacity onPress={onClose} style={styles.closeBtn}>
<X size={24} color={colors.onSurface} />
</TouchableOpacity>
</View>
<View style={styles.monthNav}>
<TouchableOpacity onPress={() => changeMonth(-1)} style={styles.navIconBtn}>
<ChevronLeft size={20} color={colors.primary} />
</TouchableOpacity>
<Text style={[styles.monthLabel, { color: colors.onSurface }]}>{monthName}</Text>
<TouchableOpacity onPress={() => changeMonth(1)} style={styles.navIconBtn}>
<ChevronRight size={20} color={colors.primary} />
</TouchableOpacity>
</View>
<View style={styles.weekDaysRow}>
{['S', 'M', 'T', 'W', 'T', 'F', 'S'].map((d, i) => (
<Text key={i} style={[styles.weekDayText, { color: colors.outline }]}>{d}</Text>
))}
</View>
<View style={styles.calendarGrid}>
{daysInMonth.map((date, i) => {
if (!date) return <View key={i} style={styles.calendarDayCell} />;
const dateStr = getLocalDateString(date);
const isSelected = dateStr === selectedDate;
const isToday = dateStr === getLocalDateString(new Date());
return (
<TouchableOpacity
key={i}
style={[
styles.calendarDayCell,
isSelected && { backgroundColor: colors.primary, borderRadius: 20 }
]}
onPress={() => onSelect(dateStr)}
>
<Text style={[
styles.calendarDayText,
{ color: isSelected ? colors.onPrimary : colors.onSurface },
isToday && !isSelected && { color: colors.primary, fontFamily: FONTS.labelSm }
]}>
{date.getDate()}
</Text>
</TouchableOpacity>
);
})}
</View>
<TouchableOpacity
style={[styles.todayBtn, { borderColor: colors.primary + '4D' }]}
onPress={() => onSelect(getLocalDateString(new Date()))}
>
<Text style={[styles.todayBtnText, { color: colors.primary }]}>GO TO TODAY</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
};
const styles = StyleSheet.create({
outerContainer: {
paddingVertical: SPACING.md,
backgroundColor: 'transparent',
},
stripWithButton: {
flexDirection: 'row',
alignItems: 'center',
paddingLeft: SPACING.lg,
},
pickerBtn: {
width: 44,
height: 44,
borderRadius: 22,
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
borderWidth: 1,
},
container: {
paddingRight: SPACING.lg,
gap: 12,
alignItems: 'center',
},
dayCell: {
alignItems: 'center',
justifyContent: 'center',
paddingVertical: 12,
paddingHorizontal: 10,
borderRadius: 30,
minWidth: 55,
},
dayName: {
fontFamily: FONTS.label,
fontSize: 10,
marginBottom: 8,
letterSpacing: 0.5,
},
dayNumContainer: {
width: 32,
height: 32,
alignItems: 'center',
justifyContent: 'center',
borderRadius: 16,
},
dayNum: {
fontFamily: FONTS.headline,
fontSize: 15,
},
todayIndicator: {
position: 'absolute',
bottom: 6,
width: 4,
height: 4,
borderRadius: 2,
},
// Modal Styles
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'center',
padding: 24,
},
modalContent: {
borderRadius: ROUNDNESS.xl,
padding: SPACING.lg,
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.25,
shadowRadius: 3.84,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: SPACING.lg,
},
modalTitle: {
fontFamily: FONTS.headline,
fontSize: 20,
},
closeBtn: {
padding: 4,
},
monthNav: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: SPACING.lg,
backgroundColor: 'rgba(0,0,0,0.03)',
borderRadius: ROUNDNESS.md,
padding: 4,
},
navIconBtn: {
padding: 10,
},
monthLabel: {
fontFamily: FONTS.labelSm,
fontSize: 16,
textTransform: 'uppercase',
letterSpacing: 1,
},
weekDaysRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 12,
},
weekDayText: {
width: '14.28%',
textAlign: 'center',
fontFamily: FONTS.label,
fontSize: 12,
},
calendarGrid: {
flexDirection: 'row',
flexWrap: 'wrap',
marginBottom: SPACING.xl,
},
calendarDayCell: {
width: '14.28%',
height: 40,
alignItems: 'center',
justifyContent: 'center',
},
calendarDayText: {
fontFamily: FONTS.body,
fontSize: 15,
},
todayBtn: {
borderWidth: 1,
paddingVertical: 12,
borderRadius: ROUNDNESS.md,
alignItems: 'center',
justifyContent: 'center',
},
todayBtnText: {
fontFamily: FONTS.labelSm,
fontSize: 13,
letterSpacing: 1,
}
});

View File

@ -0,0 +1,285 @@
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TextInput,
StyleSheet,
TouchableOpacity,
Modal,
FlatList,
Platform
} from 'react-native';
import { Clock, X, Check } from 'lucide-react-native';
import { FONTS, ROUNDNESS, SPACING } from '@/src/constants/Theme';
import { useTheme } from '@/src/hooks/useTheme';
import * as Haptics from 'expo-haptics';
interface TimeInputProps {
value: string;
onChange: (time: string) => void;
label?: string;
}
export const TimeInput: React.FC<TimeInputProps> = ({ value, onChange, label }) => {
const { colors } = useTheme();
const [tempValue, setTempValue] = useState(value);
const [showPicker, setShowPicker] = useState(false);
useEffect(() => {
setTempValue(value);
}, [value]);
const formatTime = (text: string) => {
// Remove non-digits
const cleaned = text.replace(/\D/g, '');
if (cleaned.length === 0) return '';
let hours = '00';
let minutes = '00';
if (cleaned.length === 1) {
hours = cleaned.padStart(2, '0');
} else if (cleaned.length === 2) {
hours = cleaned;
} else if (cleaned.length === 3) {
hours = cleaned.slice(0, 1).padStart(2, '0');
minutes = cleaned.slice(1);
} else {
hours = cleaned.slice(0, 2);
minutes = cleaned.slice(2, 4);
}
let h = parseInt(hours);
let m = parseInt(minutes);
if (isNaN(h)) h = 0;
if (isNaN(m)) m = 0;
if (h > 23) h = 23;
if (m > 59) m = 59;
return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
};
const handleBlur = () => {
const formatted = formatTime(tempValue);
if (formatted) {
setTempValue(formatted);
onChange(formatted);
} else {
setTempValue(value);
}
};
// Dial Picker Logic
const HOURS = Array.from({ length: 24 }, (_, i) => String(i).padStart(2, '0'));
const MINUTES = Array.from({ length: 12 }, (_, i) => String(i * 5).padStart(2, '0'));
const [selectedHour, setSelectedHour] = useState(value.split(':')[0] || '08');
const [selectedMinute, setSelectedMinute] = useState(value.split(':')[1] || '00');
const openPicker = () => {
setSelectedHour(value.split(':')[0] || '08');
setSelectedMinute(value.split(':')[1] || '00');
setShowPicker(true);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
};
const confirmPicker = () => {
const newTime = `${selectedHour}:${selectedMinute}`;
onChange(newTime);
setTempValue(newTime);
setShowPicker(false);
Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
};
return (
<View style={styles.container}>
{label && <Text style={[styles.label, { color: colors.outline }]}>{label}</Text>}
<View style={[styles.inputWrapper, { backgroundColor: colors.surface, borderColor: colors.outlineVariant + '4D' }]}>
<TouchableOpacity onPress={openPicker}>
<Clock size={20} color={colors.primary} style={styles.inputIcon} />
</TouchableOpacity>
<TextInput
style={[styles.input, { color: colors.onSurface }]}
value={tempValue}
onChangeText={setTempValue}
onBlur={handleBlur}
placeholder="00:00"
placeholderTextColor={colors.outline}
keyboardType="number-pad"
maxLength={5}
/>
</View>
<Modal visible={showPicker} transparent animationType="slide">
<View style={styles.modalOverlay}>
<View style={[styles.modalContent, { backgroundColor: colors.surface }]}>
<View style={styles.modalHeader}>
<Text style={[styles.modalTitle, { color: colors.onSurface }]}>Select Time</Text>
<TouchableOpacity onPress={() => setShowPicker(false)}>
<X size={24} color={colors.onSurface} />
</TouchableOpacity>
</View>
<View style={styles.pickerContainer}>
<View style={styles.dialWrapper}>
<Text style={[styles.dialLabel, { color: colors.outline }]}>HOUR</Text>
<FlatList
data={HOURS}
keyExtractor={item => item}
showsVerticalScrollIndicator={false}
snapToInterval={40}
renderItem={({ item }) => (
<TouchableOpacity
style={[styles.dialItem, selectedHour === item && { backgroundColor: colors.primaryContainer }]}
onPress={() => {
setSelectedHour(item);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
>
<Text style={[styles.dialText, { color: selectedHour === item ? colors.primary : colors.onSurface }]}>
{item}
</Text>
</TouchableOpacity>
)}
style={styles.dialList}
/>
</View>
<Text style={[styles.separator, { color: colors.onSurface }]}>:</Text>
<View style={styles.dialWrapper}>
<Text style={[styles.dialLabel, { color: colors.outline }]}>MIN</Text>
<FlatList
data={MINUTES}
keyExtractor={item => item}
showsVerticalScrollIndicator={false}
snapToInterval={40}
renderItem={({ item }) => (
<TouchableOpacity
style={[styles.dialItem, selectedMinute === item && { backgroundColor: colors.primaryContainer }]}
onPress={() => {
setSelectedMinute(item);
Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
}}
>
<Text style={[styles.dialText, { color: selectedMinute === item ? colors.primary : colors.onSurface }]}>
{item}
</Text>
</TouchableOpacity>
)}
style={styles.dialList}
/>
</View>
</View>
<TouchableOpacity
style={[styles.confirmBtn, { backgroundColor: colors.primary }]}
onPress={confirmPicker}
>
<Check size={20} color={colors.onPrimary} />
<Text style={[styles.confirmBtnText, { color: colors.onPrimary }]}>Confirm Time</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
</View>
);
};
const styles = StyleSheet.create({
container: {
gap: 8,
},
label: {
fontFamily: FONTS.labelSm,
fontSize: 11,
},
inputWrapper: {
flexDirection: 'row',
alignItems: 'center',
borderRadius: ROUNDNESS.md,
borderWidth: 1,
paddingHorizontal: 12,
},
inputIcon: {
marginRight: 10,
},
input: {
flex: 1,
height: 52,
fontFamily: FONTS.body,
fontSize: 16,
},
modalOverlay: {
flex: 1,
backgroundColor: 'rgba(0,0,0,0.5)',
justifyContent: 'flex-end',
},
modalContent: {
borderTopLeftRadius: ROUNDNESS.xl,
borderTopRightRadius: ROUNDNESS.xl,
padding: SPACING.lg,
paddingBottom: Platform.OS === 'ios' ? 40 : 20,
},
modalHeader: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
marginBottom: SPACING.xl,
},
modalTitle: {
fontFamily: FONTS.headline,
fontSize: 18,
},
pickerContainer: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 20,
height: 200,
marginBottom: SPACING.xl,
},
dialWrapper: {
alignItems: 'center',
width: 80,
},
dialLabel: {
fontFamily: FONTS.label,
fontSize: 10,
marginBottom: 8,
},
dialList: {
height: 160,
width: '100%',
},
dialItem: {
height: 40,
alignItems: 'center',
justifyContent: 'center',
borderRadius: ROUNDNESS.sm,
},
dialText: {
fontFamily: FONTS.labelSm,
fontSize: 20,
},
separator: {
fontFamily: FONTS.headline,
fontSize: 24,
marginTop: 20,
},
confirmBtn: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
height: 56,
borderRadius: ROUNDNESS.full,
},
confirmBtnText: {
fontFamily: FONTS.labelSm,
fontSize: 16,
},
});

132
src/constants/Theme.ts Normal file
View File

@ -0,0 +1,132 @@
export const ACCENTS = {
slate: {
label: "Modern Slate",
primary: "#334155",
primaryContainer: "#f1f5f9",
onPrimaryContainer: "#0f172a",
},
blue: {
label: "Ocean Blue",
primary: "#4e607b",
primaryContainer: "#d3e3ff",
onPrimaryContainer: "#40536d",
},
pink: {
label: "Rose Pink",
primary: "#a8385a",
primaryContainer: "#ffd9df",
onPrimaryContainer: "#3e001a",
},
red: {
label: "Crimson Rust",
primary: "#a84c36",
primaryContainer: "#ffdad2",
onPrimaryContainer: "#3e0a01",
},
purple: {
label: "Royal Amethyst",
primary: "#7c4dff",
primaryContainer: "#e0e0ff",
onPrimaryContainer: "#2c0091",
},
orange: {
label: "Deep Amber",
primary: "#f57c00",
primaryContainer: "#fff3e0",
onPrimaryContainer: "#e65100",
},
teal: {
label: "Deep Teal",
primary: "#00796b",
primaryContainer: "#e0f2f1",
onPrimaryContainer: "#004d40",
},
emerald: {
label: "Vibrant Emerald",
primary: "#2e7d32",
primaryContainer: "#e8f5e9",
onPrimaryContainer: "#1b5e20",
},
};
export type AccentKey = keyof typeof ACCENTS;
export const COLORS = {
light: {
primary: "#334155", // Slate
onPrimary: "#f8faf9",
primaryContainer: "#f1f5f9",
onPrimaryContainer: "#0f172a",
secondary: "#4e607b", // Soft Blue
onSecondary: "#f8f8ff",
secondaryContainer: "#d3e3ff",
onSecondaryContainer: "#40536d",
tertiary: "#655b6f", // Muted Lavender
onTertiary: "#fef6ff",
background: "#f8faf9", // Off-white
onBackground: "#2d3433",
surface: "#f8faf9",
onSurface: "#2d3433",
surfaceVariant: "#eaefee",
onSurfaceVariant: "#596060",
outline: "#757c7b",
outlineVariant: "#acb3b2",
error: "#a83836",
onError: "#fff7f6",
},
dark: {
primary: "#94a3b8", // Light Slate
onPrimary: "#0f172a",
primaryContainer: "#1e293b",
onPrimaryContainer: "#f1f5f9",
secondary: "#c2d6f5", // Tonal lighter Soft Blue
onSecondary: "#2e405a",
secondaryContainer: "#4a5d77",
onSecondaryContainer: "#d3e3ff",
tertiary: "#e5d8f0", // Tonal lighter Lavender
onTertiary: "#4a4154",
background: "#000000", // Pure black
onBackground: "#f1f4f3",
surface: "#000000",
onSurface: "#f1f4f3",
surfaceVariant: "#1a1a1a",
onSurfaceVariant: "#acb3b2",
outline: "#acb3b2",
outlineVariant: "#596060",
error: "#fa746f",
onError: "#6e0a12",
},
};
export type ThemeColors = typeof COLORS.light;
export const SPACING = {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
xxl: 48,
giant: 64,
};
export const ROUNDNESS = {
none: 0,
sm: 4,
md: 8,
lg: 12,
xl: 24,
full: 9999,
};
export const FONTS = {
headline: "Manrope_700Bold",
body: "Manrope_400Regular",
label: "PlusJakartaSans_500Medium",
labelSm: "PlusJakartaSans_700Bold",
};
export const GRID_STYLE = {
ghostBorder: (scheme: "light" | "dark") =>
scheme === "light" ? "rgba(51, 65, 85, 0.1)" : "rgba(148, 163, 184, 0.1)",
};

195
src/db/database.ts Normal file
View File

@ -0,0 +1,195 @@
import * as SQLite from 'expo-sqlite';
export const DATABASE_NAME = 'batsir.db';
let dbInstance: SQLite.SQLiteDatabase | null = null;
let initPromise: Promise<SQLite.SQLiteDatabase> | null = null;
export const initDatabase = async () => {
if (initPromise) return initPromise;
initPromise = (async () => {
if (!dbInstance) {
dbInstance = await SQLite.openDatabaseAsync(DATABASE_NAME);
}
const db = dbInstance;
await db.execAsync('PRAGMA journal_mode = WAL;');
const tables = [
`CREATE TABLE IF NOT EXISTS habits (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT,
title TEXT NOT NULL,
frequency TEXT NOT NULL DEFAULT 'daily',
preferred_time TEXT,
weekend_flexibility INTEGER DEFAULT 0,
is_active INTEGER DEFAULT 1,
current_streak INTEGER DEFAULT 0,
max_streak INTEGER DEFAULT 0,
two_minute_version TEXT,
location TEXT,
anchor_habit_id TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);`,
`CREATE TABLE IF NOT EXISTS schedules (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT,
date TEXT NOT NULL,
time_blocks TEXT NOT NULL DEFAULT '[]',
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);`,
`CREATE TABLE IF NOT EXISTS logs (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT,
habit_id TEXT NOT NULL,
status TEXT NOT NULL,
logged_at TEXT DEFAULT (datetime('now')),
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (habit_id) REFERENCES habits (id) ON DELETE CASCADE
);`,
`CREATE TABLE IF NOT EXISTS sync_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
table_name TEXT NOT NULL,
operation TEXT NOT NULL,
payload TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);`,
`CREATE TABLE IF NOT EXISTS shortcuts (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT,
title TEXT NOT NULL,
url TEXT NOT NULL,
icon TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);`,
`CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY NOT NULL,
value TEXT NOT NULL
);`,
`CREATE TABLE IF NOT EXISTS tasks (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT,
title TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'todo',
estimated_sessions INTEGER DEFAULT 1,
completed_sessions INTEGER DEFAULT 0,
tag TEXT,
todos TEXT NOT NULL DEFAULT '[]',
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);`,
`CREATE TABLE IF NOT EXISTS sync_history (
old_id TEXT PRIMARY KEY NOT NULL,
new_id TEXT NOT NULL,
table_name TEXT NOT NULL,
synced_at TEXT DEFAULT (datetime('now'))
);`,
`CREATE TABLE IF NOT EXISTS books (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT,
title TEXT NOT NULL,
author TEXT,
total_pages INTEGER DEFAULT 0,
current_page INTEGER DEFAULT 0,
last_page_read INTEGER DEFAULT 0,
file_uri TEXT,
cover_uri TEXT,
status TEXT DEFAULT 'want_to_read',
synthesis TEXT,
created_at TEXT DEFAULT (datetime('now')),
updated_at TEXT DEFAULT (datetime('now'))
);`,
`CREATE TABLE IF NOT EXISTS reading_logs (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT,
book_id TEXT NOT NULL,
start_page INTEGER DEFAULT 0,
end_page INTEGER DEFAULT 0,
pages_read INTEGER DEFAULT 0,
duration_minutes REAL DEFAULT 0,
duration_seconds REAL DEFAULT 0,
logged_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (book_id) REFERENCES books (id) ON DELETE CASCADE
);`,
`CREATE TABLE IF NOT EXISTS reading_sessions (
book_id TEXT PRIMARY KEY NOT NULL,
start_time INTEGER NOT NULL,
start_page INTEGER NOT NULL,
last_update_time INTEGER NOT NULL,
accumulated_time INTEGER NOT NULL,
notes TEXT NOT NULL,
FOREIGN KEY (book_id) REFERENCES books (id) ON DELETE CASCADE
);`,
`CREATE TABLE IF NOT EXISTS bookmarks (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT,
book_id TEXT NOT NULL,
page_number INTEGER NOT NULL,
note TEXT,
created_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (book_id) REFERENCES books (id) ON DELETE CASCADE
);`
];
for (const tableSql of tables) {
try {
await db.execAsync(tableSql);
} catch (err) {
console.error('Error creating table:', err, tableSql);
throw err;
}
}
try {
const habitsInfo = await db.getAllAsync(`PRAGMA table_info(habits)`);
const hCols = (habitsInfo as any[]).map(c => c.name);
if (!hCols.includes('preferred_time')) await db.execAsync(`ALTER TABLE habits ADD COLUMN preferred_time TEXT;`);
if (!hCols.includes('weekend_flexibility')) await db.execAsync(`ALTER TABLE habits ADD COLUMN weekend_flexibility INTEGER DEFAULT 0;`);
if (!hCols.includes('current_streak')) await db.execAsync(`ALTER TABLE habits ADD COLUMN current_streak INTEGER DEFAULT 0;`);
if (!hCols.includes('max_streak')) await db.execAsync(`ALTER TABLE habits ADD COLUMN max_streak INTEGER DEFAULT 0;`);
if (!hCols.includes('two_minute_version')) await db.execAsync(`ALTER TABLE habits ADD COLUMN two_minute_version TEXT;`);
if (!hCols.includes('location')) await db.execAsync(`ALTER TABLE habits ADD COLUMN location TEXT;`);
if (!hCols.includes('anchor_habit_id')) await db.execAsync(`ALTER TABLE habits ADD COLUMN anchor_habit_id TEXT;`);
const taskInfo = await db.getAllAsync(`PRAGMA table_info(tasks)`);
const tCols = (taskInfo as any[]).map(c => c.name);
if (!tCols.includes('todos')) await db.execAsync(`ALTER TABLE tasks ADD COLUMN todos TEXT NOT NULL DEFAULT '[]';`);
const readingLogInfo = await db.getAllAsync(`PRAGMA table_info(reading_logs)`);
const rlCols = (readingLogInfo as any[]).map(c => c.name);
if (!rlCols.includes('user_id')) await db.execAsync(`ALTER TABLE reading_logs ADD COLUMN user_id TEXT;`);
if (!rlCols.includes('duration_seconds')) await db.execAsync(`ALTER TABLE reading_logs ADD COLUMN duration_seconds REAL DEFAULT 0;`);
if (!rlCols.includes('start_page')) await db.execAsync(`ALTER TABLE reading_logs ADD COLUMN start_page INTEGER DEFAULT 0;`);
if (!rlCols.includes('end_page')) await db.execAsync(`ALTER TABLE reading_logs ADD COLUMN end_page INTEGER DEFAULT 0;`);
const bookmarksInfo = await db.getAllAsync(`PRAGMA table_info(bookmarks)`);
const bCols = (bookmarksInfo as any[]).map(c => c.name);
if (bCols.length > 0 && !bCols.includes('user_id')) {
await db.execAsync(`ALTER TABLE bookmarks ADD COLUMN user_id TEXT;`);
}
const logsInfo = await db.getAllAsync(`PRAGMA table_info(logs)`);
const lCols = (logsInfo as any[]).map(c => c.name);
if (!lCols.includes('user_id')) await db.execAsync(`ALTER TABLE logs ADD COLUMN user_id TEXT;`);
const booksInfo = await db.getAllAsync(`PRAGMA table_info(books)`);
const bookCols = (booksInfo as any[]).map(c => c.name);
if (!bookCols.includes('last_page_read')) await db.execAsync(`ALTER TABLE books ADD COLUMN last_page_read INTEGER DEFAULT 0;`);
if (!bookCols.includes('synthesis')) await db.execAsync(`ALTER TABLE books ADD COLUMN synthesis TEXT;`);
} catch (error) {
console.error('Migration error:', error);
}
return db;
})();
return initPromise;
};
export const getDb = async () => {
return initDatabase();
};

Some files were not shown because too many files have changed in this diff Show More