Compare commits
15 Commits
2fad5d88ab
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| fe42d42500 | |||
| 087d22f1fd | |||
| a74fd244b0 | |||
| 7fbb789fdd | |||
| 8d6a20ff05 | |||
| 271780f48a | |||
| 264441f83b | |||
| c218162edf | |||
| 886a2f7372 | |||
| 68e11c57e1 | |||
| 50ef40d2e0 | |||
| b3aa1f2992 | |||
| e1484b1723 | |||
| 8207b3626e | |||
| f1af574eb9 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,7 +1,8 @@
|
||||
# Build output
|
||||
# Build output (main.js is tracked for easy installation)
|
||||
*.js.map
|
||||
release/
|
||||
*.zip
|
||||
immerse-*.zip
|
||||
|
||||
# npm
|
||||
node_modules/
|
||||
@@ -22,5 +23,7 @@ data.json
|
||||
# Development/Documentation (not for distribution)
|
||||
RELEASE-GUIDE.md
|
||||
ROADMAP.md
|
||||
ROADMAP_*.md
|
||||
deploy-test.bat
|
||||
.claude/
|
||||
RELEASE_NOTES_*.md
|
||||
|
||||
237
README.md
237
README.md
@@ -4,56 +4,35 @@ A powerful task management and focus timer plugin for [Obsidian](https://obsidia
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## 🎯 Overview
|
||||
|
||||
Immerse brings the power of time-boxed task management directly into your Obsidian vault. Plan your day, track time with the Pomodoro technique, and crush your tasks with satisfying checkoffs - all without leaving your notes.
|
||||
Task management and focus timer for Obsidian. Plan your day, track time with Pomodoro technique, and manage tasks without leaving your vault.
|
||||
|
||||
### Why Immerse?
|
||||
## ✨ Key Features
|
||||
|
||||
- **Stay in Flow**: No need to switch between apps - manage tasks where you take notes
|
||||
- **Time Awareness**: Know exactly how long tasks take vs. your estimates
|
||||
- **Pomodoro Built-in**: Work in focused sprints with automatic break reminders
|
||||
- **Satisfying Feedback**: Celebratory messages and sounds when you complete tasks
|
||||
- **Visual Progress**: See your daily progress with stats and streaks
|
||||
**📋 Task Management**
|
||||
- Create tasks with time estimates and organize into customizable lists
|
||||
- Filter by list, status, or date
|
||||
- Schedule tasks with reminders (5/10/15/30/60 min before due)
|
||||
- Daily note integration with automatic task logging
|
||||
|
||||
## ✨ Features
|
||||
**⏱️ Dual Timer Modes**
|
||||
- **Pomodoro**: Configurable work sessions with automatic breaks
|
||||
- **Stopwatch**: Free-form time tracking with estimate alerts
|
||||
|
||||
### 📋 Task Management
|
||||
- Create tasks with time estimates
|
||||
- Organize tasks into customizable lists (Work, Personal, Learning, etc.)
|
||||
- Add notes and details to tasks
|
||||
- Filter tasks by list, today's tasks, or completed items
|
||||
- Drag-and-drop task reordering
|
||||
**📊 Analytics & Reports**
|
||||
- View productivity metrics (tasks/day, hours/day, streaks)
|
||||
- Pie charts and bar graphs showing activity breakdown
|
||||
- Time tracking by list category
|
||||
- Insights on most productive hours, days, and months
|
||||
|
||||
### ⏱️ Dual Timer Modes
|
||||
|
||||
#### Pomodoro Timer
|
||||
- Configurable work sessions (default: 25 minutes)
|
||||
- Short breaks (default: 5 minutes)
|
||||
- Long breaks after a set number of pomodoros (default: 15 minutes every 4 pomodoros)
|
||||
- Auto-start break option
|
||||
- Visual countdown with progress bar
|
||||
|
||||
#### Stopwatch Mode
|
||||
- Free-form time tracking
|
||||
- Alerts when you exceed your estimate
|
||||
- Track actual time vs. estimated time
|
||||
|
||||
### 📊 Progress Tracking
|
||||
- **Daily Stats**: See tasks completed, focus time, and more
|
||||
- **Streak Counter**: Build momentum with consecutive productive days
|
||||
- **Time Comparison**: Compare estimated vs. actual time to improve planning
|
||||
- **Pomodoro Count**: Track total pomodoros completed
|
||||
- **Daily Note Logging**: Automatically log completed tasks to your daily notes with timestamps and performance metrics
|
||||
|
||||
### 🎨 User Experience
|
||||
- **Status Bar Timer**: Timer that stays visible while you work
|
||||
- **Celebration Messages**: Fun, randomized messages when you complete tasks
|
||||
- **Sound Notifications**: Audio alerts for timer completion and task completion
|
||||
- **Keyboard Shortcuts**: Quick access to common actions
|
||||
- **Responsive Design**: Works great in any panel size
|
||||
**🎨 Polish**
|
||||
- Status bar timer, celebration messages, sound alerts
|
||||
- Keyboard shortcuts, dark mode support
|
||||
- Overdue task detection with visual indicators
|
||||
- Works on desktop and mobile
|
||||
|
||||
## 🚀 Installation
|
||||
|
||||
@@ -88,183 +67,43 @@ npm run build
|
||||
cp main.js manifest.json styles.css /path/to/your/vault/.obsidian/plugins/immerse/
|
||||
```
|
||||
|
||||
## 📖 Usage
|
||||
## 📖 Quick Start
|
||||
|
||||
### Getting Started
|
||||
1. Click ⚡ icon in ribbon or use command palette (`Ctrl/Cmd + P` → "Open Immerse Panel")
|
||||
2. Add a task with "+ Add Task" (description, time estimate, list)
|
||||
3. Start timer: ▶ for Pomodoro or ⏱ for stopwatch
|
||||
4. Complete and enjoy the celebration!
|
||||
|
||||
1. **Open Immerse**: Click the ⚡ icon in the ribbon or use the command palette (`Ctrl/Cmd + P` → "Open Immerse Panel")
|
||||
## ⚙️ Configuration
|
||||
|
||||
2. **Add a Task**: Click "+ Add Task" and fill in:
|
||||
- Task description
|
||||
- Time estimate
|
||||
- List category
|
||||
|
||||
3. **Start Focusing**: Click ▶ on any task to start a Pomodoro session, or ⏱ for stopwatch mode
|
||||
|
||||
4. **Complete Tasks**: Check off tasks when done and enjoy the celebration!
|
||||
|
||||
### Keyboard Shortcuts
|
||||
|
||||
| Action | Command |
|
||||
|--------|---------|
|
||||
| Open Panel | `Ctrl/Cmd + P` → "Open Immerse Panel" |
|
||||
| Quick Add Task | `Ctrl/Cmd + P` → "Quick Add Task" |
|
||||
| Toggle Timer | `Ctrl/Cmd + P` → "Toggle Timer" |
|
||||
| Complete Task | `Ctrl/Cmd + P` → "Complete Current Task" |
|
||||
| Start Focus | `Ctrl/Cmd + P` → "Start Focus Mode on Next Task" |
|
||||
|
||||
### Timer Modes Explained
|
||||
|
||||
#### 🍅 Pomodoro Mode (▶ button)
|
||||
Best for: Maintaining focus on challenging tasks
|
||||
|
||||
1. Timer counts down from your configured work duration
|
||||
2. When time's up, you'll get a notification
|
||||
3. Take a break (short or long, based on your settings)
|
||||
4. Repeat until the task is complete
|
||||
|
||||
#### ⏱️ Stopwatch Mode (⏱ button)
|
||||
Best for: Tracking time on open-ended tasks
|
||||
|
||||
1. Timer counts up from zero
|
||||
2. Get alerted when you exceed your estimate
|
||||
3. Stop whenever the task is complete
|
||||
4. See exactly how long the task took
|
||||
|
||||
## ⚙️ Settings
|
||||
|
||||
### Pomodoro Timer
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| Work Duration | Length of each work session | 25 min |
|
||||
| Short Break | Length of short breaks | 5 min |
|
||||
| Long Break | Length of long breaks | 15 min |
|
||||
| Long Break Interval | Pomodoros before a long break | 4 |
|
||||
| Auto-start Breaks | Automatically start break timer | On |
|
||||
|
||||
### General
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| Default Time Estimate | Default estimate for new tasks | 30 min |
|
||||
| Enable Sounds | Play completion/alert sounds | On |
|
||||
| Enable Celebrations | Show celebration messages | On |
|
||||
| Show Floating Timer | Display draggable timer widget | On |
|
||||
|
||||
### Daily Note Integration
|
||||
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| Log to Daily Note | Automatically log completed tasks to your daily note | Off |
|
||||
|
||||
When enabled, completed tasks are automatically appended to your daily note with:
|
||||
|
||||
- Task name and list category
|
||||
- Time spent vs. estimated time
|
||||
- Completion timestamp
|
||||
- Performance indicator (under/over/on target)
|
||||
|
||||
**Example entry:**
|
||||
|
||||
```
|
||||
- [x] Write project proposal | 💼 Work | ⏱️ 45min / 30min (15min over estimate) | ✅ 14:30
|
||||
```
|
||||
|
||||
**Requirements:** The core "Daily Notes" plugin must be enabled in Obsidian settings. Immerse respects your Daily Notes configuration (folder, date format, and template).
|
||||
|
||||
### Lists
|
||||
Customize your task lists with:
|
||||
- Custom names
|
||||
- Emoji icons
|
||||
- Color coding
|
||||
|
||||
Default lists: Work 💼, Personal 🏠, Learning 📚
|
||||
|
||||
## 🎨 Customization
|
||||
|
||||
### Adding Custom Lists
|
||||
1. Go to Settings → Immerse → Lists
|
||||
2. Click "+ Add List"
|
||||
3. Set the name, emoji, and color
|
||||
4. Click Save
|
||||
|
||||
### Theming
|
||||
Immerse respects your Obsidian theme and adapts to both light and dark modes automatically.
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
```
|
||||
immerse/
|
||||
├── src/
|
||||
│ ├── main.ts # Main plugin class
|
||||
│ ├── types.ts # TypeScript interfaces
|
||||
│ ├── view.ts # Main UI view
|
||||
│ └── modals.ts # Task modals
|
||||
├── styles.css # Plugin styles
|
||||
├── manifest.json # Obsidian plugin manifest
|
||||
├── package.json # npm configuration
|
||||
├── tsconfig.json # TypeScript config
|
||||
├── esbuild.config.mjs # Build configuration
|
||||
└── README.md # This file
|
||||
```
|
||||
|
||||
## 🙏 Inspiration & Credits
|
||||
|
||||
This plugin is **heavily inspired by [Blitzit](https://www.blitzit.app/)**, a fantastic standalone productivity app that combines task management with focused time tracking.
|
||||
|
||||
Blitzit's approach to productivity resonated with me:
|
||||
- Simple, focused interface
|
||||
- Time estimation and tracking
|
||||
- Pomodoro technique integration
|
||||
- Satisfying task completion experience
|
||||
- Progress insights
|
||||
|
||||
I wanted to bring this experience directly into Obsidian, where I already manage my notes and knowledge. Immerse is my attempt to capture the essence of what makes Blitzit great while leveraging Obsidian's powerful ecosystem.
|
||||
|
||||
**If you're looking for a dedicated productivity app, I highly recommend checking out [Blitzit](https://www.blitzit.app/)!**
|
||||
All settings available in Settings → Immerse:
|
||||
- Pomodoro durations (work: 25min, short break: 5min, long break: 15min)
|
||||
- Default time estimates, sounds, celebrations
|
||||
- Daily note integration (auto-log completed tasks)
|
||||
- Custom lists with names, emojis, and colors
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Contributions are welcome! Feel free to:
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
||||
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
||||
4. Push to the branch (`git push origin feature/amazing-feature`)
|
||||
5. Open a Pull Request
|
||||
|
||||
### Development Setup
|
||||
Contributions welcome! Fork, create a feature branch, and open a PR.
|
||||
|
||||
**Dev Setup:**
|
||||
```bash
|
||||
# Clone the repo
|
||||
git clone https://git.cribdev.com/crib/immerse.git
|
||||
cd immerse
|
||||
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Start development build (watches for changes)
|
||||
npm run dev
|
||||
|
||||
# Create symlink to your test vault
|
||||
ln -s $(pwd) /path/to/vault/.obsidian/plugins/immerse
|
||||
```
|
||||
|
||||
## 📜 License
|
||||
|
||||
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
## 🔗 Links
|
||||
|
||||
- **Repository**: [https://git.cribdev.com/crib/immerse](https://git.cribdev.com/crib/immerse)
|
||||
- **Issues**: [https://git.cribdev.com/crib/immerse/issues](https://git.cribdev.com/crib/immerse/issues)
|
||||
- **Inspiration**: [Blitzit App](https://www.blitzit.app/)
|
||||
MIT License - see [LICENSE](LICENSE) file.
|
||||
|
||||
---
|
||||
|
||||
**Links:** [Repository](https://git.cribdev.com/crib/immerse) • [Issues](https://git.cribdev.com/crib/immerse/issues) • [Blitzit](https://www.blitzit.app/)
|
||||
|
||||
<p align="center">
|
||||
Made with ❤️ for the Obsidian community
|
||||
<br>
|
||||
Inspired by <a href="https://www.blitzit.app/">Blitzit</a> ⚡
|
||||
<br><br>
|
||||
<em>✨ Vibe coded with assistance from <a href="https://claude.ai">Claude.ai</a></em>
|
||||
<em>✨ Coded with assistance from <a href="https://claude.ai">Claude.ai</a></em>
|
||||
</p>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"id": "immerse",
|
||||
"name": "Immerse",
|
||||
"version": "1.0.9",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "A Blitzit-inspired task management and focus timer plugin. Plan your day, track time with Pomodoro technique, and crush your tasks with satisfying checkoffs.",
|
||||
"author": "Crib",
|
||||
"authorUrl": "https://git.cribdev.com/crib",
|
||||
"fundingUrl": "",
|
||||
"isDesktopOnly": false
|
||||
"id": "immerse",
|
||||
"name": "Immerse",
|
||||
"version": "1.1.5",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "A Blitzit-inspired task management and focus timer plugin. Plan your day, track time with Pomodoro technique, and crush your tasks with satisfying checkoffs.",
|
||||
"author": "Crib",
|
||||
"authorUrl": "https://git.cribdev.com/crib",
|
||||
"fundingUrl": "",
|
||||
"isDesktopOnly": false
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immerse",
|
||||
"version": "1.0.9",
|
||||
"version": "1.1.5",
|
||||
"description": "A Blitzit-inspired task management and focus timer plugin for Obsidian",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
|
||||
346
src/main.ts
346
src/main.ts
@@ -21,7 +21,8 @@ import {
|
||||
} from './types';
|
||||
|
||||
import { ImmerseView } from './view';
|
||||
import { QuickAddTaskModal } from './modals';
|
||||
import { ReportView, VIEW_TYPE_REPORT } from './reportView';
|
||||
import { QuickAddTaskModal, EmojiPickerModal } from './modals';
|
||||
|
||||
// ============ Main Plugin Class ============
|
||||
|
||||
@@ -34,6 +35,7 @@ export default class ImmersePlugin extends Plugin {
|
||||
currentTimerSeconds: number = 0;
|
||||
isTimerRunning: boolean = false;
|
||||
isBreakMode: boolean = false;
|
||||
isStopwatchMode: boolean = false;
|
||||
activeTaskId: string | null = null;
|
||||
pomodoroCount: number = 0;
|
||||
|
||||
@@ -44,6 +46,7 @@ export default class ImmersePlugin extends Plugin {
|
||||
// Focus time tracking (in seconds for accuracy)
|
||||
private focusSecondsToday: number = 0;
|
||||
private secondsWorkedOnCurrentTask: number = 0;
|
||||
private sessionStartSeconds: number = 0; // Track seconds at start of current session
|
||||
|
||||
// Status bar element
|
||||
statusBarEl: HTMLElement | null = null;
|
||||
@@ -52,25 +55,42 @@ export default class ImmersePlugin extends Plugin {
|
||||
private reminderCheckInterval: number | null = null;
|
||||
private notifiedReminders: Set<string> = new Set(); // Track which reminders have been shown
|
||||
|
||||
// Daily reset check interval
|
||||
private dailyResetCheckInterval: number | null = null;
|
||||
|
||||
async onload() {
|
||||
await this.loadAllData();
|
||||
|
||||
// Check and reset daily stats
|
||||
this.checkDailyReset();
|
||||
|
||||
// Start daily reset check interval (check every 60 seconds)
|
||||
this.dailyResetCheckInterval = window.setInterval(() => {
|
||||
this.checkDailyReset();
|
||||
}, 60000);
|
||||
|
||||
// Handle visibility changes to sync timer when app comes back to foreground
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (!document.hidden && this.isTimerRunning) {
|
||||
this.syncTimerFromTimestamp();
|
||||
if (!document.hidden) {
|
||||
// Check for day change when app becomes visible
|
||||
this.checkDailyReset();
|
||||
if (this.isTimerRunning) {
|
||||
this.syncTimerFromTimestamp();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Register the main view
|
||||
// Register views
|
||||
this.registerView(
|
||||
VIEW_TYPE_IMMERSE,
|
||||
(leaf) => new ImmerseView(leaf, this)
|
||||
);
|
||||
|
||||
this.registerView(
|
||||
VIEW_TYPE_REPORT,
|
||||
(leaf) => new ReportView(leaf, this)
|
||||
);
|
||||
|
||||
// Add ribbon icon
|
||||
this.addRibbonIcon('zap', 'Open Immerse', () => {
|
||||
this.activateView();
|
||||
@@ -107,6 +127,12 @@ export default class ImmersePlugin extends Plugin {
|
||||
callback: () => this.completeActiveTask(),
|
||||
});
|
||||
|
||||
this.addCommand({
|
||||
id: 'view-reports',
|
||||
name: 'View Reports',
|
||||
callback: () => this.activateReportView(),
|
||||
});
|
||||
|
||||
// Add settings tab
|
||||
this.addSettingTab(new ImmerseSettingTab(this.app, this));
|
||||
|
||||
@@ -122,6 +148,12 @@ export default class ImmersePlugin extends Plugin {
|
||||
onunload() {
|
||||
this.stopTimer();
|
||||
this.stopReminderSystem();
|
||||
|
||||
// Stop daily reset check interval
|
||||
if (this.dailyResetCheckInterval) {
|
||||
window.clearInterval(this.dailyResetCheckInterval);
|
||||
this.dailyResetCheckInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
async loadAllData() {
|
||||
@@ -191,6 +223,26 @@ export default class ImmersePlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
async activateReportView() {
|
||||
const { workspace } = this.app;
|
||||
|
||||
// Check if report view is already open
|
||||
const existingLeaves = workspace.getLeavesOfType(VIEW_TYPE_REPORT);
|
||||
|
||||
if (existingLeaves.length > 0) {
|
||||
// If already open, just reveal it
|
||||
workspace.revealLeaf(existingLeaves[0]);
|
||||
} else {
|
||||
// Open in a new tab in the main area
|
||||
const leaf = workspace.getLeaf('tab');
|
||||
await leaf.setViewState({
|
||||
type: VIEW_TYPE_REPORT,
|
||||
active: true,
|
||||
});
|
||||
workspace.revealLeaf(leaf);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Task Management ============
|
||||
|
||||
createTask(text: string, estimatedMinutes: number = this.settings.defaultEstimateMinutes, list: string = 'work'): ImmerseTask {
|
||||
@@ -246,6 +298,18 @@ export default class ImmersePlugin extends Plugin {
|
||||
this.data.completedToday++;
|
||||
this.data.lastActiveDate = new Date().toDateString();
|
||||
|
||||
// Add focus time from this session (only on completion)
|
||||
if (this.activeTaskId === taskId && this.sessionStartSeconds !== undefined) {
|
||||
const sessionTime = this.secondsWorkedOnCurrentTask - this.sessionStartSeconds;
|
||||
this.focusSecondsToday += sessionTime;
|
||||
}
|
||||
|
||||
// Archive task for historical reporting
|
||||
this.archiveCompletedTask(task);
|
||||
|
||||
// Update daily stats
|
||||
this.updateDailyStats(task);
|
||||
|
||||
// Show celebration
|
||||
if (this.settings.enableCelebrations) {
|
||||
this.showCelebration(task);
|
||||
@@ -279,6 +343,215 @@ export default class ImmersePlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Data Archiving & Statistics ============
|
||||
|
||||
archiveCompletedTask(task: ImmerseTask) {
|
||||
// Check if task was overdue
|
||||
const wasOverdue = task.scheduledDate && task.scheduledTime &&
|
||||
new Date(`${task.scheduledDate}T${task.scheduledTime}`).getTime() < (task.completedAt || Date.now());
|
||||
|
||||
const record: import('./types').CompletedTaskRecord = {
|
||||
id: task.id,
|
||||
text: task.text,
|
||||
list: task.list,
|
||||
estimatedMinutes: task.estimatedMinutes,
|
||||
actualMinutes: task.actualMinutes,
|
||||
createdAt: task.createdAt,
|
||||
completedAt: task.completedAt || Date.now(),
|
||||
scheduledDate: task.scheduledDate,
|
||||
wasOverdue: wasOverdue || false,
|
||||
};
|
||||
|
||||
this.data.completedTasksArchive.push(record);
|
||||
}
|
||||
|
||||
updateDailyStats(task: ImmerseTask) {
|
||||
const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
|
||||
// Find or create today's stats
|
||||
let todayStats = this.data.dailyStats.find(s => s.date === today);
|
||||
|
||||
if (!todayStats) {
|
||||
todayStats = {
|
||||
date: today,
|
||||
tasksCompleted: 0,
|
||||
totalMinutes: 0,
|
||||
pomodorosCompleted: 0,
|
||||
tasksByList: {},
|
||||
minutesByList: {},
|
||||
};
|
||||
this.data.dailyStats.push(todayStats);
|
||||
}
|
||||
|
||||
// Update stats
|
||||
todayStats.tasksCompleted++;
|
||||
todayStats.totalMinutes += task.actualMinutes;
|
||||
todayStats.pomodorosCompleted = this.data.pomodorosCompleted;
|
||||
|
||||
// Update list-specific stats
|
||||
todayStats.tasksByList[task.list] = (todayStats.tasksByList[task.list] || 0) + 1;
|
||||
todayStats.minutesByList[task.list] = (todayStats.minutesByList[task.list] || 0) + task.actualMinutes;
|
||||
|
||||
// Keep only last 365 days of stats to prevent data bloat
|
||||
if (this.data.dailyStats.length > 365) {
|
||||
this.data.dailyStats.sort((a, b) => b.date.localeCompare(a.date));
|
||||
this.data.dailyStats = this.data.dailyStats.slice(0, 365);
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Report Generation ============
|
||||
|
||||
generateReport(filters: import('./types').ReportFilters): import('./types').ReportData {
|
||||
const { startDate, endDate, listIds } = filters;
|
||||
|
||||
// Filter daily stats by date range
|
||||
const filteredStats = this.data.dailyStats.filter(stat => {
|
||||
return stat.date >= startDate && stat.date <= endDate;
|
||||
});
|
||||
|
||||
// Filter completed tasks by date range and lists
|
||||
const filteredTasks = this.data.completedTasksArchive.filter(task => {
|
||||
const taskDate = new Date(task.completedAt).toISOString().split('T')[0];
|
||||
const inDateRange = taskDate >= startDate && taskDate <= endDate;
|
||||
const inList = !listIds || listIds.includes(task.list);
|
||||
return inDateRange && inList;
|
||||
});
|
||||
|
||||
// Calculate summary stats
|
||||
const totalTasks = filteredTasks.length;
|
||||
const totalMinutes = filteredTasks.reduce((sum, task) => sum + task.actualMinutes, 0);
|
||||
const totalPomodoros = filteredStats.reduce((sum, stat) => sum + stat.pomodorosCompleted, 0);
|
||||
|
||||
// Calculate number of days with data
|
||||
const daysWithData = filteredStats.length || 1; // Avoid division by zero
|
||||
|
||||
const tasksPerDay = totalTasks / daysWithData;
|
||||
const hoursPerDay = (totalMinutes / 60) / daysWithData;
|
||||
const minsPerTask = totalTasks > 0 ? totalMinutes / totalTasks : 0;
|
||||
|
||||
// Time breakdown by list
|
||||
const timeByListMap: Record<string, { minutes: number; taskCount: number }> = {};
|
||||
filteredTasks.forEach(task => {
|
||||
if (!timeByListMap[task.list]) {
|
||||
timeByListMap[task.list] = { minutes: 0, taskCount: 0 };
|
||||
}
|
||||
timeByListMap[task.list].minutes += task.actualMinutes;
|
||||
timeByListMap[task.list].taskCount++;
|
||||
});
|
||||
|
||||
const timeByList = this.settings.lists.map(list => {
|
||||
const data = timeByListMap[list.id] || { minutes: 0, taskCount: 0 };
|
||||
const percentage = totalMinutes > 0 ? (data.minutes / totalMinutes) * 100 : 0;
|
||||
return {
|
||||
listId: list.id,
|
||||
listName: list.name,
|
||||
listIcon: list.icon,
|
||||
listColor: list.color,
|
||||
minutes: data.minutes,
|
||||
taskCount: data.taskCount,
|
||||
percentage: Math.round(percentage * 10) / 10, // Round to 1 decimal
|
||||
};
|
||||
}).filter(item => item.minutes > 0); // Only show lists with data
|
||||
|
||||
// Daily breakdown
|
||||
const dailyBreakdown = filteredStats.map(stat => ({
|
||||
date: stat.date,
|
||||
tasks: stat.tasksCompleted,
|
||||
hours: Math.round((stat.totalMinutes / 60) * 10) / 10,
|
||||
pomodoros: stat.pomodorosCompleted,
|
||||
}));
|
||||
|
||||
// Productivity insights
|
||||
const mostProductiveHour = this.calculateMostProductiveHour(filteredTasks);
|
||||
const mostProductiveDay = this.calculateMostProductiveDay(filteredStats);
|
||||
const mostProductiveMonth = this.calculateMostProductiveMonth(filteredStats);
|
||||
|
||||
return {
|
||||
totalTasks,
|
||||
totalMinutes,
|
||||
totalPomodoros,
|
||||
tasksPerDay: Math.round(tasksPerDay * 10) / 10,
|
||||
hoursPerDay: Math.round(hoursPerDay * 10) / 10,
|
||||
minsPerTask: Math.round(minsPerTask),
|
||||
currentStreak: this.data.streak,
|
||||
timeByList,
|
||||
dailyBreakdown,
|
||||
mostProductiveHour,
|
||||
mostProductiveDay,
|
||||
mostProductiveMonth,
|
||||
};
|
||||
}
|
||||
|
||||
calculateMostProductiveHour(tasks: import('./types').CompletedTaskRecord[]): number | undefined {
|
||||
if (tasks.length === 0) return undefined;
|
||||
|
||||
const hourCounts: Record<number, number> = {};
|
||||
tasks.forEach(task => {
|
||||
const hour = new Date(task.completedAt).getHours();
|
||||
hourCounts[hour] = (hourCounts[hour] || 0) + 1;
|
||||
});
|
||||
|
||||
let maxHour = 0;
|
||||
let maxCount = 0;
|
||||
for (const [hour, count] of Object.entries(hourCounts)) {
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
maxHour = parseInt(hour);
|
||||
}
|
||||
}
|
||||
|
||||
return maxCount > 0 ? maxHour : undefined;
|
||||
}
|
||||
|
||||
calculateMostProductiveDay(stats: import('./types').DailyStats[]): string | undefined {
|
||||
if (stats.length === 0) return undefined;
|
||||
|
||||
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
const dayCounts: Record<string, number> = {};
|
||||
|
||||
stats.forEach(stat => {
|
||||
const dayOfWeek = new Date(stat.date).getDay();
|
||||
const dayName = dayNames[dayOfWeek];
|
||||
dayCounts[dayName] = (dayCounts[dayName] || 0) + stat.tasksCompleted;
|
||||
});
|
||||
|
||||
let maxDay = '';
|
||||
let maxCount = 0;
|
||||
for (const [day, count] of Object.entries(dayCounts)) {
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
maxDay = day;
|
||||
}
|
||||
}
|
||||
|
||||
return maxCount > 0 ? maxDay : undefined;
|
||||
}
|
||||
|
||||
calculateMostProductiveMonth(stats: import('./types').DailyStats[]): string | undefined {
|
||||
if (stats.length === 0) return undefined;
|
||||
|
||||
const monthNames = ['January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
const monthCounts: Record<string, number> = {};
|
||||
|
||||
stats.forEach(stat => {
|
||||
const month = new Date(stat.date).getMonth();
|
||||
const monthName = monthNames[month];
|
||||
monthCounts[monthName] = (monthCounts[monthName] || 0) + stat.tasksCompleted;
|
||||
});
|
||||
|
||||
let maxMonth = '';
|
||||
let maxCount = 0;
|
||||
for (const [month, count] of Object.entries(monthCounts)) {
|
||||
if (count > maxCount) {
|
||||
maxCount = count;
|
||||
maxMonth = month;
|
||||
}
|
||||
}
|
||||
|
||||
return maxCount > 0 ? maxMonth : undefined;
|
||||
}
|
||||
|
||||
// ============ Timer Management ============
|
||||
|
||||
// Sync timer based on timestamp when app returns from background
|
||||
@@ -297,13 +570,14 @@ export default class ImmersePlugin extends Plugin {
|
||||
const task = this.data.tasks.find(t => t.id === taskId);
|
||||
if (!task) return;
|
||||
|
||||
// Stop any existing timer
|
||||
this.stopTimer();
|
||||
// Stop any existing timer, preserving actual time
|
||||
this.stopTimer(true);
|
||||
|
||||
// Set active task
|
||||
this.activeTaskId = taskId;
|
||||
task.isActive = true;
|
||||
this.isBreakMode = false;
|
||||
this.isStopwatchMode = true;
|
||||
this.currentTimerSeconds = 0;
|
||||
this.isTimerRunning = true;
|
||||
this.secondsWorkedOnCurrentTask = task.actualMinutes * 60;
|
||||
@@ -314,6 +588,7 @@ export default class ImmersePlugin extends Plugin {
|
||||
|
||||
// Store initial values
|
||||
const initialSecondsWorked = this.secondsWorkedOnCurrentTask;
|
||||
this.sessionStartSeconds = this.secondsWorkedOnCurrentTask; // Track session start for focus time
|
||||
let alertShown = false;
|
||||
|
||||
// Full refresh to show the active task card
|
||||
@@ -334,10 +609,6 @@ export default class ImmersePlugin extends Plugin {
|
||||
this.secondsWorkedOnCurrentTask = initialSecondsWorked + elapsedSeconds;
|
||||
task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
|
||||
|
||||
// Update focus time
|
||||
const newFocusSeconds = Math.floor((this.data.totalFocusMinutesToday || 0) * 60) + elapsedSeconds;
|
||||
this.focusSecondsToday = newFocusSeconds;
|
||||
|
||||
// Light update - only timer display, no full refresh
|
||||
this.updateStatusBar();
|
||||
this.updateTimerDisplay();
|
||||
@@ -359,10 +630,11 @@ export default class ImmersePlugin extends Plugin {
|
||||
const task = this.data.tasks.find(t => t.id === taskId);
|
||||
if (!task) return;
|
||||
|
||||
this.stopTimer();
|
||||
this.stopTimer(true);
|
||||
this.activeTaskId = taskId;
|
||||
task.isActive = true;
|
||||
this.isBreakMode = false;
|
||||
this.isStopwatchMode = false;
|
||||
this.currentTimerSeconds = this.settings.pomodoroWorkMinutes * 60;
|
||||
this.isTimerRunning = true;
|
||||
|
||||
@@ -376,6 +648,7 @@ export default class ImmersePlugin extends Plugin {
|
||||
|
||||
// Store the initial seconds worked to calculate delta
|
||||
const initialSecondsWorked = this.secondsWorkedOnCurrentTask;
|
||||
this.sessionStartSeconds = this.secondsWorkedOnCurrentTask; // Track session start for focus time
|
||||
|
||||
// Full refresh to show the active task card
|
||||
this.refreshView();
|
||||
@@ -398,10 +671,6 @@ export default class ImmersePlugin extends Plugin {
|
||||
if (task.actualMinutes !== actualMinutes) {
|
||||
task.actualMinutes = actualMinutes;
|
||||
}
|
||||
|
||||
// Update focus time based on elapsed seconds
|
||||
const newFocusSeconds = Math.floor((this.data.totalFocusMinutesToday || 0) * 60) + elapsedSeconds;
|
||||
this.focusSecondsToday = newFocusSeconds;
|
||||
}
|
||||
|
||||
// Light update - only timer display, no full refresh
|
||||
@@ -519,24 +788,26 @@ export default class ImmersePlugin extends Plugin {
|
||||
const elapsedMs = now - this.timerStartTimestamp;
|
||||
const elapsedSeconds = Math.floor(elapsedMs / 1000);
|
||||
|
||||
// Update timer (countdown from paused position)
|
||||
this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds);
|
||||
// Update timer based on mode
|
||||
if (this.pausedTimeRemaining === 0 || this.isStopwatchMode) {
|
||||
// Stopwatch mode - count up from paused position
|
||||
this.currentTimerSeconds = this.pausedTimeRemaining + elapsedSeconds;
|
||||
} else {
|
||||
// Countdown mode (pomodoro/break) - count down from paused position
|
||||
this.currentTimerSeconds = Math.max(0, this.pausedTimeRemaining - elapsedSeconds);
|
||||
}
|
||||
|
||||
if (task && !this.isBreakMode) {
|
||||
// Update actual time worked
|
||||
this.secondsWorkedOnCurrentTask = initialSecondsWorked + elapsedSeconds;
|
||||
task.actualMinutes = Math.floor(this.secondsWorkedOnCurrentTask / 60);
|
||||
|
||||
// Update focus time
|
||||
const newFocusSeconds = Math.floor((this.data.totalFocusMinutesToday || 0) * 60) + elapsedSeconds;
|
||||
this.focusSecondsToday = newFocusSeconds;
|
||||
}
|
||||
|
||||
// Light update - only timer display
|
||||
this.updateStatusBar();
|
||||
this.updateTimerDisplay();
|
||||
|
||||
if (this.currentTimerSeconds <= 0) {
|
||||
if (this.currentTimerSeconds <= 0 && !this.isStopwatchMode) {
|
||||
this.handlePomodoroEnd();
|
||||
}
|
||||
}, 1000);
|
||||
@@ -549,7 +820,7 @@ export default class ImmersePlugin extends Plugin {
|
||||
this.refreshView();
|
||||
}
|
||||
|
||||
stopTimer() {
|
||||
stopTimer(preserveActualTime: boolean = false) {
|
||||
if (this.timerInterval) {
|
||||
window.clearInterval(this.timerInterval);
|
||||
this.timerInterval = null;
|
||||
@@ -559,15 +830,18 @@ export default class ImmersePlugin extends Plugin {
|
||||
const task = this.data.tasks.find(t => t.id === this.activeTaskId);
|
||||
if (task) {
|
||||
task.isActive = false;
|
||||
// Reset actual time when manually stopping (not after a break)
|
||||
// This allows starting fresh next time
|
||||
task.actualMinutes = 0;
|
||||
// Only reset actual time when manually stopping (not when resuming after break)
|
||||
if (!preserveActualTime) {
|
||||
task.actualMinutes = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.isTimerRunning = false;
|
||||
this.isStopwatchMode = false;
|
||||
this.activeTaskId = null;
|
||||
this.secondsWorkedOnCurrentTask = 0;
|
||||
this.sessionStartSeconds = 0;
|
||||
this.timerStartTimestamp = 0;
|
||||
this.pausedTimeRemaining = 0;
|
||||
this.updateStatusBar();
|
||||
@@ -1137,12 +1411,20 @@ class ImmerseSettingTab extends PluginSettingTab {
|
||||
this.plugin.settings.lists[index].name = value;
|
||||
await this.plugin.saveAllData();
|
||||
}))
|
||||
.addText(text => text
|
||||
.setValue(list.icon)
|
||||
.setPlaceholder('Emoji')
|
||||
.onChange(async value => {
|
||||
this.plugin.settings.lists[index].icon = value;
|
||||
await this.plugin.saveAllData();
|
||||
.addButton(btn => btn
|
||||
.setButtonText(list.icon || '📁')
|
||||
.setTooltip('Choose emoji')
|
||||
.onClick(() => {
|
||||
const modal = new EmojiPickerModal(
|
||||
this.app,
|
||||
list.icon,
|
||||
async (emoji) => {
|
||||
this.plugin.settings.lists[index].icon = emoji;
|
||||
await this.plugin.saveAllData();
|
||||
this.display();
|
||||
}
|
||||
);
|
||||
modal.open();
|
||||
}))
|
||||
.addColorPicker(picker => picker
|
||||
.setValue(list.color)
|
||||
|
||||
649
src/modals.ts
649
src/modals.ts
@@ -8,6 +8,293 @@ import {
|
||||
import { ImmerseTask } from './types';
|
||||
import ImmersePlugin from './main';
|
||||
|
||||
// Emoji search keywords mapping
|
||||
const EMOJI_KEYWORDS: { [key: string]: string } = {
|
||||
'💼': 'briefcase work business office job',
|
||||
'🏠': 'home house',
|
||||
'📚': 'books study read library',
|
||||
'🎯': 'target goal aim dart',
|
||||
'✅': 'check mark done complete checkbox tick',
|
||||
'📝': 'memo note write pencil',
|
||||
'💡': 'light bulb idea',
|
||||
'🔥': 'fire hot flame',
|
||||
'⚡': 'lightning bolt electric zap',
|
||||
'🎨': 'art paint palette',
|
||||
'🏆': 'trophy award win',
|
||||
'💪': 'muscle strong flex',
|
||||
'🚀': 'rocket ship launch',
|
||||
'📊': 'chart graph data',
|
||||
'⏰': 'clock time alarm',
|
||||
'💰': 'money bag cash dollar',
|
||||
'😀': 'smile happy face grin',
|
||||
'😃': 'smile happy grin',
|
||||
'😄': 'smile happy laugh',
|
||||
'😁': 'grin smile happy',
|
||||
'😆': 'laugh smile happy',
|
||||
'😅': 'sweat smile nervous',
|
||||
'🤣': 'laugh rolling floor',
|
||||
'😂': 'tears joy laugh cry',
|
||||
'🙂': 'smile happy slight',
|
||||
'🙃': 'upside down smile',
|
||||
'😉': 'wink smile flirt',
|
||||
'😊': 'blush smile happy',
|
||||
'😇': 'angel halo smile',
|
||||
'🥰': 'love hearts smile',
|
||||
'😍': 'love heart eyes smile',
|
||||
'🤩': 'star eyes excited',
|
||||
'😘': 'kiss love heart',
|
||||
'😗': 'kiss love',
|
||||
'😚': 'kiss love',
|
||||
'😙': 'kiss love smile',
|
||||
'🥲': 'smile tear cry happy',
|
||||
'😋': 'yum delicious smile',
|
||||
'😛': 'tongue playful',
|
||||
'😜': 'wink tongue playful',
|
||||
'🤪': 'crazy wild eyes',
|
||||
'😝': 'tongue eyes squint',
|
||||
'🤑': 'money dollar rich',
|
||||
'🤗': 'hug smile',
|
||||
'🤭': 'hand over mouth giggle',
|
||||
'🤫': 'shush quiet secret',
|
||||
'🤔': 'think hmm wonder',
|
||||
'🤐': 'zipper mouth secret',
|
||||
'🤨': 'eyebrow raised skeptical',
|
||||
'😐': 'neutral meh',
|
||||
'😑': 'expressionless blank',
|
||||
'😶': 'no mouth silent',
|
||||
'😏': 'smirk confident',
|
||||
'😒': 'unamused annoyed',
|
||||
'🙄': 'eye roll annoyed',
|
||||
'😬': 'grimace awkward',
|
||||
'🤥': 'liar lying pinocchio',
|
||||
'😌': 'relieved content',
|
||||
'😔': 'sad pensive',
|
||||
'😪': 'sleepy tired',
|
||||
'🤤': 'drool sleep',
|
||||
'😴': 'sleep zzz',
|
||||
'😷': 'mask sick medical',
|
||||
'🤒': 'sick thermometer',
|
||||
'🤕': 'injured bandage',
|
||||
'🤢': 'nausea sick',
|
||||
'🤮': 'vomit sick',
|
||||
'🤧': 'sneeze sick tissue',
|
||||
'🥵': 'hot sweat',
|
||||
'🥶': 'cold freeze',
|
||||
'😎': 'cool sunglasses',
|
||||
'🤓': 'nerd glasses',
|
||||
'🧐': 'monocle fancy',
|
||||
'😕': 'confused uncertain',
|
||||
'😟': 'worried concerned',
|
||||
'🙁': 'frown sad',
|
||||
'☹️': 'frown sad',
|
||||
'😮': 'wow surprised',
|
||||
'😯': 'surprised shocked',
|
||||
'😲': 'shocked astonished',
|
||||
'😳': 'flushed embarrassed',
|
||||
'🥺': 'pleading puppy eyes',
|
||||
'😦': 'frown worried',
|
||||
'😧': 'anguished worried',
|
||||
'😨': 'fearful scared',
|
||||
'😰': 'anxious sweat',
|
||||
'😥': 'sad sweat',
|
||||
'😢': 'cry tear sad',
|
||||
'😭': 'cry tears sob',
|
||||
'😱': 'scream fear',
|
||||
'😖': 'confounded',
|
||||
'😣': 'persevere struggle',
|
||||
'😞': 'disappointed sad',
|
||||
'😓': 'downcast sweat',
|
||||
'😩': 'weary tired',
|
||||
'😫': 'tired exhausted',
|
||||
'🥱': 'yawn tired',
|
||||
'😤': 'triumph proud',
|
||||
'😡': 'angry mad rage',
|
||||
'😠': 'angry mad',
|
||||
'🤬': 'cursing swearing angry',
|
||||
'😈': 'devil smiling evil',
|
||||
'👿': 'devil angry evil',
|
||||
'💀': 'skull death',
|
||||
'☠️': 'skull crossbones death',
|
||||
'💩': 'poop poo',
|
||||
'🤡': 'clown funny',
|
||||
'👹': 'ogre monster',
|
||||
'👺': 'goblin monster',
|
||||
'👻': 'ghost boo',
|
||||
'👽': 'alien extraterrestrial',
|
||||
'👾': 'alien monster game',
|
||||
'🤖': 'robot bot',
|
||||
'❤️': 'red heart love',
|
||||
'🧡': 'orange heart love',
|
||||
'💛': 'yellow heart love',
|
||||
'💚': 'green heart love',
|
||||
'💙': 'blue heart love',
|
||||
'💜': 'purple heart love',
|
||||
'🤎': 'brown heart love',
|
||||
'🖤': 'black heart love',
|
||||
'🤍': 'white heart love',
|
||||
'💔': 'broken heart sad',
|
||||
'❣️': 'heart exclamation love',
|
||||
'💕': 'two hearts love',
|
||||
'💞': 'revolving hearts love',
|
||||
'💓': 'beating heart love',
|
||||
'💗': 'growing heart love',
|
||||
'💖': 'sparkling heart love',
|
||||
'💘': 'arrow heart love cupid',
|
||||
'💝': 'heart box gift love',
|
||||
'💟': 'heart decoration love',
|
||||
'❤️🔥': 'heart fire love passion',
|
||||
'❤️🩹': 'heart bandage healing',
|
||||
'💌': 'love letter heart',
|
||||
'💋': 'kiss lips',
|
||||
'💑': 'couple love kiss',
|
||||
'💏': 'kiss couple love',
|
||||
'👋': 'wave hand hello goodbye',
|
||||
'🤚': 'raised hand back',
|
||||
'🖐️': 'hand fingers spread',
|
||||
'✋': 'raised hand stop',
|
||||
'🖖': 'vulcan salute spock',
|
||||
'👌': 'ok okay hand',
|
||||
'🤌': 'pinched fingers italian',
|
||||
'🤏': 'pinching hand small',
|
||||
'✌️': 'peace victory hand',
|
||||
'🤞': 'crossed fingers luck',
|
||||
'🤟': 'love you hand',
|
||||
'🤘': 'rock on horns',
|
||||
'🤙': 'call me hang loose',
|
||||
'👈': 'left point finger',
|
||||
'👉': 'right point finger',
|
||||
'👆': 'up point finger',
|
||||
'🖕': 'middle finger rude',
|
||||
'👇': 'down point finger',
|
||||
'☝️': 'up point finger',
|
||||
'👍': 'thumbs up yes good',
|
||||
'👎': 'thumbs down no bad',
|
||||
'✊': 'fist hand',
|
||||
'👊': 'fist bump punch',
|
||||
'🤛': 'left fist bump',
|
||||
'🤜': 'right fist bump',
|
||||
'👏': 'clap applause',
|
||||
'🙌': 'raising hands celebration',
|
||||
'👐': 'open hands',
|
||||
'🤲': 'palms together pray',
|
||||
'🤝': 'handshake deal',
|
||||
'🙏': 'pray please thank',
|
||||
'✍️': 'writing hand',
|
||||
'💅': 'nail polish manicure',
|
||||
'🤳': 'selfie camera phone',
|
||||
'🐶': 'dog puppy pet',
|
||||
'🐱': 'cat kitty pet',
|
||||
'🐭': 'mouse rat',
|
||||
'🐹': 'hamster pet',
|
||||
'🐰': 'rabbit bunny',
|
||||
'🦊': 'fox',
|
||||
'🐻': 'bear',
|
||||
'🐼': 'panda bear',
|
||||
'🐨': 'koala bear',
|
||||
'🐯': 'tiger face',
|
||||
'🦁': 'lion face',
|
||||
'🐮': 'cow face',
|
||||
'🐷': 'pig face',
|
||||
'🐸': 'frog face',
|
||||
'🐵': 'monkey face',
|
||||
'🍎': 'apple red fruit',
|
||||
'🍊': 'orange fruit',
|
||||
'🍋': 'lemon fruit',
|
||||
'🍌': 'banana fruit',
|
||||
'🍉': 'watermelon fruit',
|
||||
'🍇': 'grapes fruit',
|
||||
'🍓': 'strawberry fruit',
|
||||
'🍒': 'cherry fruit',
|
||||
'🍑': 'peach fruit',
|
||||
'🥭': 'mango fruit',
|
||||
'🍍': 'pineapple fruit',
|
||||
'🥥': 'coconut fruit',
|
||||
'🥝': 'kiwi fruit',
|
||||
'🍅': 'tomato vegetable',
|
||||
'🥑': 'avocado fruit',
|
||||
'🍞': 'bread food',
|
||||
'⚽': 'soccer ball football',
|
||||
'🏀': 'basketball ball',
|
||||
'🏈': 'american football',
|
||||
'⚾': 'baseball ball',
|
||||
'🎾': 'tennis ball',
|
||||
'🏐': 'volleyball ball',
|
||||
'🚗': 'car auto vehicle',
|
||||
'🚕': 'taxi car',
|
||||
'🚙': 'suv car vehicle',
|
||||
'🚌': 'bus vehicle',
|
||||
'🚎': 'trolleybus bus',
|
||||
'🏎️': 'racing car fast',
|
||||
'🚓': 'police car cop',
|
||||
'🚑': 'ambulance emergency',
|
||||
'🚒': 'fire truck engine',
|
||||
'🚲': 'bicycle bike',
|
||||
'✈️': 'airplane plane flight',
|
||||
'💻': 'laptop computer',
|
||||
'⌨️': 'keyboard computer',
|
||||
'🖱️': 'mouse computer',
|
||||
'🖥️': 'desktop computer',
|
||||
'🖨️': 'printer',
|
||||
'📱': 'phone mobile iphone',
|
||||
'📞': 'phone telephone',
|
||||
'☎️': 'telephone phone',
|
||||
'📺': 'tv television',
|
||||
'📻': 'radio',
|
||||
'📁': 'folder file',
|
||||
'📂': 'open folder file',
|
||||
'📅': 'calendar date',
|
||||
'📆': 'calendar date',
|
||||
'📈': 'chart up graph',
|
||||
'📉': 'chart down graph',
|
||||
'📌': 'pushpin pin',
|
||||
'📍': 'pin location map',
|
||||
'📎': 'paperclip clip',
|
||||
'🎵': 'music note',
|
||||
'🎶': 'music notes',
|
||||
'🎼': 'musical score',
|
||||
'🎹': 'piano keyboard music',
|
||||
'🎸': 'guitar music',
|
||||
'🎺': 'trumpet music',
|
||||
'🎷': 'saxophone music',
|
||||
'🥁': 'drum music',
|
||||
'🎤': 'microphone mic sing',
|
||||
'🎧': 'headphones music',
|
||||
'🔊': 'speaker loud volume',
|
||||
'❌': 'cross x no cancel',
|
||||
'⚠️': 'warning caution alert',
|
||||
'🔴': 'red circle',
|
||||
'🟢': 'green circle',
|
||||
'🔵': 'blue circle',
|
||||
'🟡': 'yellow circle',
|
||||
'🟣': 'purple circle',
|
||||
'⚫': 'black circle',
|
||||
'⚪': 'white circle',
|
||||
'🟤': 'brown circle',
|
||||
'🔺': 'triangle red up',
|
||||
'🔻': 'triangle red down',
|
||||
'🔸': 'diamond orange small',
|
||||
'🔹': 'diamond blue small',
|
||||
'🔶': 'diamond orange large',
|
||||
'🔷': 'diamond blue large',
|
||||
};
|
||||
|
||||
// Emoji categories for picker
|
||||
const EMOJI_CATEGORIES = {
|
||||
'⭐ Frequently Used': ['💼', '🏠', '📚', '🎯', '✅', '📝', '💡', '🔥', '⚡', '🎨', '🏆', '💪', '🚀', '📊', '⏰', '💰'],
|
||||
'😀 Smileys & Emotion': ['😀', '😃', '😄', '😁', '😆', '😅', '🤣', '😂', '🙂', '🙃', '😉', '😊', '😇', '🥰', '😍', '🤩', '😘', '😗', '😚', '😙', '🥲', '😋', '😛', '😜', '🤪', '😝', '🤑', '🤗', '🤭', '🤫', '🤔', '🤐', '🤨', '😐', '😑', '😶', '😏', '😒', '🙄', '😬', '🤥', '😌', '😔', '😪', '🤤', '😴', '😷', '🤒', '🤕', '🤢', '🤮', '🤧', '🥵', '🥶', '😎', '🤓', '🧐', '😕', '😟', '🙁', '☹️', '😮', '😯', '😲', '😳', '🥺', '😦', '😧', '😨', '😰', '😥', '😢', '😭', '😱', '😖', '😣', '😞', '😓', '😩', '😫', '🥱', '😤', '😡', '😠', '🤬', '😈', '👿', '💀', '☠️', '💩', '🤡', '👹', '👺', '👻', '👽', '👾', '🤖'],
|
||||
'❤️ Hearts & Love': ['❤️', '🧡', '💛', '💚', '💙', '💜', '🤎', '🖤', '🤍', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟', '❤️🔥', '❤️🩹', '💌', '💋', '💑', '💏', '👩❤️👨', '👨❤️👨', '👩❤️👩'],
|
||||
'👤 People & Body': ['👋', '🤚', '🖐️', '✋', '🖖', '👌', '🤌', '🤏', '✌️', '🤞', '🤟', '🤘', '🤙', '👈', '👉', '👆', '🖕', '👇', '☝️', '👍', '👎', '✊', '👊', '🤛', '🤜', '👏', '🙌', '👐', '🤲', '🤝', '🙏', '✍️', '💅', '🤳', '💪', '🦾', '🦿', '🦵', '🦶', '👂', '🦻', '👃', '🧠', '🫀', '🫁', '🦷', '🦴', '👀', '👁️', '👅', '👄', '👶', '🧒', '👦', '👧', '🧑', '👨', '👩', '🧔', '🧑🦰', '👨🦰', '👩🦰', '🧑🦱', '👨🦱', '👩🦱', '🧑🦳', '👨🦳', '👩🦳', '🧑🦲', '👨🦲', '👩🦲', '👱', '👱♂️', '👱♀️', '🧓', '👴', '👵', '🙍', '🙍♂️', '🙍♀️', '🙎', '🙎♂️', '🙎♀️', '🙅', '🙅♂️', '🙅♀️', '🙆', '🙆♂️', '🙆♀️', '💁', '💁♂️', '💁♀️', '🙋', '🙋♂️', '🙋♀️', '🧏', '🧏♂️', '🧏♀️', '🙇', '🙇♂️', '🙇♀️', '🤦', '🤦♂️', '🤦♀️', '🤷', '🤷♂️', '🤷♀️'],
|
||||
'🐶 Animals & Nature': ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮', '🐷', '🐽', '🐸', '🐵', '🙈', '🙉', '🙊', '🐒', '🐔', '🐧', '🐦', '🐤', '🐣', '🐥', '🦆', '🦅', '🦉', '🦇', '🐺', '🐗', '🐴', '🦄', '🐝', '🐛', '🦋', '🐌', '🐞', '🐜', '🦟', '🦗', '🕷️', '🕸️', '🦂', '🐢', '🐍', '🦎', '🦖', '🦕', '🐙', '🦑', '🦐', '🦞', '🦀', '🐡', '🐠', '🐟', '🐬', '🐳', '🐋', '🦈', '🐊', '🐅', '🐆', '🦓', '🦍', '🦧', '🐘', '🦛', '🦏', '🐪', '🐫', '🦒', '🦘', '🐃', '🐂', '🐄', '🐎', '🐖', '🐏', '🐑', '🦙', '🐐', '🦌', '🐕', '🐩', '🦮', '🐕🦺', '🐈', '🐈⬛', '🐓', '🦃', '🦚', '🦜', '🦢', '🦩', '🕊️', '🐇', '🦝', '🦨', '🦡', '🦦', '🦥', '🐁', '🐀', '🐿️', '🦔', '🌲', '🌳', '🌴', '🌱', '🌿', '☘️', '🍀', '🎍', '🎋', '🍃', '🍂', '🍁', '🍄', '🌾', '💐', '🌷', '🌹', '🥀', '🌺', '🌸', '🌼', '🌻', '🌞', '🌝', '🌛', '🌜', '🌚', '🌕', '🌖', '🌗', '🌘', '🌑', '🌒', '🌓', '🌔', '🌙', '🌎', '🌍', '🌏', '🪐', '💫', '⭐', '🌟', '✨', '⚡', '☄️', '💥', '🔥', '🌪️', '🌈', '☀️', '🌤️', '⛅', '🌥️', '☁️', '🌦️', '🌧️', '⛈️', '🌩️', '🌨️', '❄️', '☃️', '⛄', '🌬️', '💨', '💧', '💦', '☔', '☂️', '🌊', '🌫️'],
|
||||
'🍎 Food & Drink': ['🍇', '🍈', '🍉', '🍊', '🍋', '🍌', '🍍', '🥭', '🍎', '🍏', '🍐', '🍑', '🍒', '🍓', '🫐', '🥝', '🍅', '🫒', '🥥', '🥑', '🍆', '🥔', '🥕', '🌽', '🌶️', '🫑', '🥒', '🥬', '🥦', '🧄', '🧅', '🍄', '🥜', '🌰', '🍞', '🥐', '🥖', '🫓', '🥨', '🥯', '🥞', '🧇', '🧀', '🍖', '🍗', '🥩', '🥓', '🍔', '🍟', '🍕', '🌭', '🥪', '🌮', '🌯', '🫔', '🥙', '🧆', '🥚', '🍳', '🥘', '🍲', '🫕', '🥣', '🥗', '🍿', '🧈', '🧂', '🥫', '🍱', '🍘', '🍙', '🍚', '🍛', '🍜', '🍝', '🍠', '🍢', '🍣', '🍤', '🍥', '🥮', '🍡', '🥟', '🥠', '🥡', '🦀', '🦞', '🦐', '🦑', '🦪', '🍦', '🍧', '🍨', '🍩', '🍪', '🎂', '🍰', '🧁', '🥧', '🍫', '🍬', '🍭', '🍮', '🍯', '🍼', '🥛', '☕', '🫖', '🍵', '🍶', '🍾', '🍷', '🍸', '🍹', '🍺', '🍻', '🥂', '🥃', '🥤', '🧋', '🧃', '🧉', '🧊'],
|
||||
'⚽ Activities & Sports': ['⚽', '🏀', '🏈', '⚾', '🥎', '🎾', '🏐', '🏉', '🥏', '🎱', '🪀', '🏓', '🏸', '🏒', '🏑', '🥍', '🏏', '🪃', '🥅', '⛳', '🪁', '🏹', '🎣', '🤿', '🥊', '🥋', '🎽', '🛹', '🛼', '🛷', '⛸️', '🥌', '🎿', '⛷️', '🏂', '🪂', '🏋️', '🏋️♂️', '🏋️♀️', '🤼', '🤼♂️', '🤼♀️', '🤸', '🤸♂️', '🤸♀️', '⛹️', '⛹️♂️', '⛹️♀️', '🤺', '🤾', '🤾♂️', '🤾♀️', '🏌️', '🏌️♂️', '🏌️♀️', '🏇', '🧘', '🧘♂️', '🧘♀️', '🏄', '🏄♂️', '🏄♀️', '🏊', '🏊♂️', '🏊♀️', '🤽', '🤽♂️', '🤽♀️', '🚣', '🚣♂️', '🚣♀️', '🧗', '🧗♂️', '🧗♀️', '🚵', '🚵♂️', '🚵♀️', '🚴', '🚴♂️', '🚴♀️', '🏆', '🥇', '🥈', '🥉', '🏅', '🎖️', '🏵️', '🎗️', '🎫', '🎟️', '🎪', '🤹', '🤹♂️', '🤹♀️', '🎭', '🩰', '🎨', '🎬', '🎤', '🎧', '🎼', '🎹', '🥁', '🪘', '🎷', '🎺', '🪗', '🎸', '🪕', '🎻', '🎲', '♟️', '🎯', '🎳', '🎮', '🎰', '🧩'],
|
||||
'🚗 Travel & Places': ['🚗', '🚕', '🚙', '🚌', '🚎', '🏎️', '🚓', '🚑', '🚒', '🚐', '🛻', '🚚', '🚛', '🚜', '🦯', '🦽', '🦼', '🛴', '🚲', '🛵', '🏍️', '🛺', '🚨', '🚔', '🚍', '🚘', '🚖', '🚡', '🚠', '🚟', '🚃', '🚋', '🚞', '🚝', '🚄', '🚅', '🚈', '🚂', '🚆', '🚇', '🚊', '🚉', '✈️', '🛫', '🛬', '🛩️', '💺', '🛰️', '🚀', '🛸', '🚁', '🛶', '⛵', '🚤', '🛥️', '🛳️', '⛴️', '🚢', '⚓', '⛽', '🚧', '🚦', '🚥', '🚏', '🗺️', '🗿', '🗽', '🗼', '🏰', '🏯', '🏟️', '🎡', '🎢', '🎠', '⛲', '⛱️', '🏖️', '🏝️', '🏜️', '🌋', '⛰️', '🏔️', '🗻', '🏕️', '⛺', '🛖', '🏠', '🏡', '🏘️', '🏚️', '🏗️', '🏭', '🏢', '🏬', '🏣', '🏤', '🏥', '🏦', '🏨', '🏪', '🏫', '🏩', '💒', '🏛️', '⛪', '🕌', '🕍', '🛕', '🕋', '⛩️', '🛤️', '🛣️', '🗾', '🎑', '🏞️', '🌅', '🌄', '🌠', '🎇', '🎆', '🌇', '🌆', '🏙️', '🌃', '🌌', '🌉', '🌁'],
|
||||
'💻 Objects & Technology': ['⌚', '📱', '📲', '💻', '⌨️', '🖥️', '🖨️', '🖱️', '🖲️', '🕹️', '🗜️', '💾', '💿', '📀', '📼', '📷', '📸', '📹', '🎥', '📽️', '🎞️', '📞', '☎️', '📟', '📠', '📺', '📻', '🎙️', '🎚️', '🎛️', '🧭', '⏱️', '⏲️', '⏰', '🕰️', '⌛', '⏳', '📡', '🔋', '🔌', '💡', '🔦', '🕯️', '🪔', '🧯', '🛢️', '💸', '💵', '💴', '💶', '💷', '🪙', '💰', '💳', '💎', '⚖️', '🪜', '🧰', '🪛', '🔧', '🔨', '⚒️', '🛠️', '⛏️', '🪚', '🔩', '⚙️', '🪤', '🧱', '⛓️', '🧲', '🔫', '💣', '🧨', '🪓', '🔪', '🗡️', '⚔️', '🛡️', '🚬', '⚰️', '🪦', '⚱️', '🏺', '🔮', '📿', '🧿', '💈', '⚗️', '🔭', '🔬', '🕳️', '🩹', '🩺', '💊', '💉', '🩸', '🧬', '🦷', '🧪', '🌡️', '🧹', '🪠', '🧺', '🧻', '🚽', '🚰', '🚿', '🛁', '🛀', '🧼', '🪥', '🪒', '🧽', '🪣', '🧴', '🛎️', '🔑', '🗝️', '🚪', '🪑', '🛋️', '🛏️', '🛌', '🧸', '🪆', '🖼️', '🪞', '🪟', '🛍️', '🛒', '🎁', '🎈', '🎏', '🎀', '🪄', '🪅', '🎊', '🎉', '🎎', '🏮', '🎐', '🧧'],
|
||||
'📋 Office & Writing': ['✉️', '📧', '📨', '📩', '📤', '📥', '📦', '📫', '📪', '📬', '📭', '📮', '🗳️', '✏️', '✒️', '🖋️', '🖊️', '🖌️', '🖍️', '📝', '💼', '📁', '📂', '🗂️', '📅', '📆', '🗒️', '🗓️', '📇', '📈', '📉', '📊', '📋', '📌', '📍', '📎', '🖇️', '📏', '📐', '✂️', '🗃️', '🗄️', '🗑️', '🔒', '🔓', '🔏', '🔐', '🔑', '🗝️', '🔨', '🪓', '⛏️', '⚒️', '🛠️', '🗡️', '⚔️', '💣', '🪃', '🏹', '🛡️', '🪚', '🔧', '🪛', '🔩', '⚙️', '🗜️', '⚖️'],
|
||||
'🎵 Music & Sound': ['🎵', '🎶', '🎼', '🎹', '🎸', '🎺', '🎷', '🥁', '🪘', '🎤', '🎧', '📻', '🎙️', '🔊', '🔉', '🔈', '🔇', '📢', '📣', '📯', '🔔', '🔕', '🎚️', '🎛️', '🎖️', '🏆', '🥇', '🥈', '🥉', '⚡', '🔥', '💥'],
|
||||
'⚡ Symbols & Signs': ['❤️', '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '💔', '❣️', '💕', '💞', '💓', '💗', '💖', '💘', '💝', '💟', '☮️', '✝️', '☪️', '🕉️', '☸️', '✡️', '🔯', '🕎', '☯️', '☦️', '🛐', '⛎', '♈', '♉', '♊', '♋', '♌', '♍', '♎', '♏', '♐', '♑', '♒', '♓', '🆔', '⚛️', '🉑', '☢️', '☣️', '📴', '📳', '🈶', '🈚', '🈸', '🈺', '🈷️', '✴️', '🆚', '💮', '🉐', '㊙️', '㊗️', '🈴', '🈵', '🈹', '🈲', '🅰️', '🅱️', '🆎', '🆑', '🅾️', '🆘', '❌', '⭕', '🛑', '⛔', '📛', '🚫', '💯', '💢', '♨️', '🚷', '🚯', '🚳', '🚱', '🔞', '📵', '🚭', '❗', '❕', '❓', '❔', '‼️', '⁉️', '🔅', '🔆', '〽️', '⚠️', '🚸', '🔱', '⚜️', '🔰', '♻️', '✅', '🈯', '💹', '❇️', '✳️', '❎', '🌐', '💠', 'Ⓜ️', '🌀', '💤', '🏧', '🚾', '♿', '🅿️', '🛗', '🈳', '🈂️', '🛂', '🛃', '🛄', '🛅', '🚹', '🚺', '🚼', '⚧️', '🚻', '🚮', '🎦', '📶', '🈁', '🔣', 'ℹ️', '🔤', '🔡', '🔠', '🆖', '🆗', '🆙', '🆒', '🆕', '🆓', '0️⃣', '1️⃣', '2️⃣', '3️⃣', '4️⃣', '5️⃣', '6️⃣', '7️⃣', '8️⃣', '9️⃣', '🔟', '🔢', '#️⃣', '*️⃣', '⏏️', '▶️', '⏸️', '⏯️', '⏹️', '⏺️', '⏭️', '⏮️', '⏩', '⏪', '⏫', '⏬', '◀️', '🔼', '🔽', '➡️', '⬅️', '⬆️', '⬇️', '↗️', '↘️', '↙️', '↖️', '↕️', '↔️', '↪️', '↩️', '⤴️', '⤵️', '🔀', '🔁', '🔂', '🔄', '🔃', '🎵', '🎶', '➕', '➖', '➗', '✖️', '♾️', '💲', '💱', '™️', '©️', '®️', '〰️', '➰', '➿', '🔚', '🔙', '🔛', '🔝', '🔜', '✔️', '☑️', '🔘', '🔴', '🟠', '🟡', '🟢', '🔵', '🟣', '⚫', '⚪', '🟤', '🔺', '🔻', '🔸', '🔹', '🔶', '🔷', '🔳', '🔲', '▪️', '▫️', '◾', '◽', '◼️', '◻️', '🟥', '🟧', '🟨', '🟩', '🟦', '🟪', '⬛', '⬜', '🟫', '🔈', '🔇', '🔉', '🔊', '🔔', '🔕', '📣', '📢', '💬', '💭', '🗯️', '♠️', '♣️', '♥️', '♦️', '🃏', '🎴', '🀄', '🕐', '🕑', '🕒', '🕓', '🕔', '🕕', '🕖', '🕗', '🕘', '🕙', '🕚', '🕛', '🕜', '🕝', '🕞', '🕟', '🕠', '🕡', '🕢', '🕣', '🕤', '🕥', '🕦', '🕧'],
|
||||
'🏁 Flags': ['🏁', '🚩', '🎌', '🏴', '🏳️', '🏳️🌈', '🏳️⚧️', '🏴☠️', '🇦🇨', '🇦🇩', '🇦🇪', '🇦🇫', '🇦🇬', '🇦🇮', '🇦🇱', '🇦🇲', '🇦🇴', '🇦🇶', '🇦🇷', '🇦🇸', '🇦🇹', '🇦🇺', '🇦🇼', '🇦🇽', '🇦🇿', '🇧🇦', '🇧🇧', '🇧🇩', '🇧🇪', '🇧🇫', '🇧🇬', '🇧🇭', '🇧🇮', '🇧🇯', '🇧🇱', '🇧🇲', '🇧🇳', '🇧🇴', '🇧🇶', '🇧🇷', '🇧🇸', '🇧🇹', '🇧🇻', '🇧🇼', '🇧🇾', '🇧🇿', '🇨🇦', '🇨🇨', '🇨🇩', '🇨🇫', '🇨🇬', '🇨🇭', '🇨🇮', '🇨🇰', '🇨🇱', '🇨🇲', '🇨🇳', '🇨🇴', '🇨🇵', '🇨🇷', '🇨🇺', '🇨🇻', '🇨🇼', '🇨🇽', '🇨🇾', '🇨🇿', '🇩🇪', '🇩🇬', '🇩🇯', '🇩🇰', '🇩🇲', '🇩🇴', '🇩🇿', '🇪🇦', '🇪🇨', '🇪🇪', '🇪🇬', '🇪🇭', '🇪🇷', '🇪🇸', '🇪🇹', '🇪🇺', '🇫🇮', '🇫🇯', '🇫🇰', '🇫🇲', '🇫🇴', '🇫🇷', '🇬🇦', '🇬🇧', '🇬🇩', '🇬🇪', '🇬🇫', '🇬🇬', '🇬🇭', '🇬🇮', '🇬🇱', '🇬🇲', '🇬🇳', '🇬🇵', '🇬🇶', '🇬🇷', '🇬🇸', '🇬🇹', '🇬🇺', '🇬🇼', '🇬🇾', '🇭🇰', '🇭🇲', '🇭🇳', '🇭🇷', '🇭🇹', '🇭🇺', '🇮🇨', '🇮🇩', '🇮🇪', '🇮🇱', '🇮🇲', '🇮🇳', '🇮🇴', '🇮🇶', '🇮🇷', '🇮🇸', '🇮🇹', '🇯🇪', '🇯🇲', '🇯🇴', '🇯🇵', '🇰🇪', '🇰🇬', '🇰🇭', '🇰🇮', '🇰🇲', '🇰🇳', '🇰🇵', '🇰🇷', '🇰🇼', '🇰🇾', '🇰🇿', '🇱🇦', '🇱🇧', '🇱🇨', '🇱🇮', '🇱🇰', '🇱🇷', '🇱🇸', '🇱🇹', '🇱🇺', '🇱🇻', '🇱🇾', '🇲🇦', '🇲🇨', '🇲🇩', '🇲🇪', '🇲🇫', '🇲🇬', '🇲🇭', '🇲🇰', '🇲🇱', '🇲🇲', '🇲🇳', '🇲🇴', '🇲🇵', '🇲🇶', '🇲🇷', '🇲🇸', '🇲🇹', '🇲🇺', '🇲🇻', '🇲🇼', '🇲🇽', '🇲🇾', '🇲🇿', '🇳🇦', '🇳🇨', '🇳🇪', '🇳🇫', '🇳🇬', '🇳🇮', '🇳🇱', '🇳🇴', '🇳🇵', '🇳🇷', '🇳🇺', '🇳🇿', '🇴🇲', '🇵🇦', '🇵🇪', '🇵🇫', '🇵🇬', '🇵🇭', '🇵🇰', '🇵🇱', '🇵🇲', '🇵🇳', '🇵🇷', '🇵🇸', '🇵🇹', '🇵🇼', '🇵🇾', '🇶🇦', '🇷🇪', '🇷🇴', '🇷🇸', '🇷🇺', '🇷🇼', '🇸🇦', '🇸🇧', '🇸🇨', '🇸🇩', '🇸🇪', '🇸🇬', '🇸🇭', '🇸🇮', '🇸🇯', '🇸🇰', '🇸🇱', '🇸🇲', '🇸🇳', '🇸🇴', '🇸🇷', '🇸🇸', '🇸🇹', '🇸🇻', '🇸🇽', '🇸🇾', '🇸🇿', '🇹🇦', '🇹🇨', '🇹🇩', '🇹🇫', '🇹🇬', '🇹🇭', '🇹🇯', '🇹🇰', '🇹🇱', '🇹🇲', '🇹🇳', '🇹🇴', '🇹🇷', '🇹🇹', '🇹🇻', '🇹🇼', '🇹🇿', '🇺🇦', '🇺🇬', '🇺🇲', '🇺🇳', '🇺🇸', '🇺🇾', '🇺🇿', '🇻🇦', '🇻🇨', '🇻🇪', '🇻🇬', '🇻🇮', '🇻🇳', '🇻🇺', '🇼🇫', '🇼🇸', '🇽🇰', '🇾🇪', '🇾🇹', '🇿🇦', '🇿🇲', '🇿🇼', '🏴', '🏴', '🏴'],
|
||||
};
|
||||
|
||||
// ============ Quick Add Task Modal ============
|
||||
|
||||
export class QuickAddTaskModal extends Modal {
|
||||
@@ -297,3 +584,365 @@ export class EditTaskModal extends Modal {
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Report Modal ============
|
||||
|
||||
export class ReportModal extends Modal {
|
||||
plugin: ImmersePlugin;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
selectedListIds: string[] = [];
|
||||
|
||||
constructor(app: App, plugin: ImmersePlugin) {
|
||||
super(app);
|
||||
this.plugin = plugin;
|
||||
|
||||
// Default to last 7 days
|
||||
const today = new Date();
|
||||
this.endDate = today.toISOString().split('T')[0];
|
||||
const weekAgo = new Date(today);
|
||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||
this.startDate = weekAgo.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.addClass('immerse-modal', 'immerse-report-modal');
|
||||
contentEl.empty();
|
||||
|
||||
// Header
|
||||
const header = contentEl.createEl('div', { cls: 'immerse-report-header' });
|
||||
header.createEl('h2', { text: '📊 Reports', cls: 'immerse-report-title' });
|
||||
|
||||
// Filters section
|
||||
this.renderFilters(contentEl);
|
||||
|
||||
// Generate button
|
||||
const generateBtn = contentEl.createEl('button', {
|
||||
text: '🔄 Generate Report',
|
||||
cls: 'immerse-btn immerse-btn-primary immerse-report-generate-btn'
|
||||
});
|
||||
generateBtn.addEventListener('click', () => this.renderReport(contentEl));
|
||||
|
||||
// Initial report render
|
||||
this.renderReport(contentEl);
|
||||
}
|
||||
|
||||
renderFilters(container: Element) {
|
||||
const filtersSection = container.createEl('div', { cls: 'immerse-report-filters' });
|
||||
|
||||
// Date range
|
||||
const dateRow = filtersSection.createEl('div', { cls: 'immerse-report-filter-row' });
|
||||
|
||||
new Setting(dateRow)
|
||||
.setName('Start Date')
|
||||
.addText(text => {
|
||||
text.setValue(this.startDate)
|
||||
.onChange(value => this.startDate = value);
|
||||
text.inputEl.type = 'date';
|
||||
});
|
||||
|
||||
new Setting(dateRow)
|
||||
.setName('End Date')
|
||||
.addText(text => {
|
||||
text.setValue(this.endDate)
|
||||
.onChange(value => this.endDate = value);
|
||||
text.inputEl.type = 'date';
|
||||
});
|
||||
|
||||
// Quick filters
|
||||
const quickFilters = filtersSection.createEl('div', { cls: 'immerse-report-quick-filters' });
|
||||
quickFilters.createEl('span', { text: 'Quick select: ', cls: 'immerse-filter-label' });
|
||||
|
||||
const filters = [
|
||||
{ label: 'Today', days: 0 },
|
||||
{ label: 'Last 7 days', days: 7 },
|
||||
{ label: 'Last 30 days', days: 30 },
|
||||
{ label: 'Last 90 days', days: 90 },
|
||||
];
|
||||
|
||||
filters.forEach(filter => {
|
||||
const btn = quickFilters.createEl('button', {
|
||||
text: filter.label,
|
||||
cls: 'immerse-quick-filter-btn'
|
||||
});
|
||||
btn.addEventListener('click', () => {
|
||||
const today = new Date();
|
||||
this.endDate = today.toISOString().split('T')[0];
|
||||
const startDate = new Date(today);
|
||||
startDate.setDate(startDate.getDate() - filter.days);
|
||||
this.startDate = startDate.toISOString().split('T')[0];
|
||||
this.renderReport(container);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderReport(container: Element) {
|
||||
// Remove old report if exists
|
||||
const oldReport = container.querySelector('.immerse-report-content');
|
||||
if (oldReport) oldReport.remove();
|
||||
|
||||
// Generate report data
|
||||
const filters: import('./types').ReportFilters = {
|
||||
startDate: this.startDate,
|
||||
endDate: this.endDate,
|
||||
listIds: this.selectedListIds.length > 0 ? this.selectedListIds : undefined,
|
||||
};
|
||||
|
||||
const reportData = this.plugin.generateReport(filters);
|
||||
|
||||
// Create report content
|
||||
const reportContent = container.createEl('div', { cls: 'immerse-report-content' });
|
||||
|
||||
// Check if we have data
|
||||
if (reportData.totalTasks === 0) {
|
||||
reportContent.createEl('div', {
|
||||
text: 'No data available for the selected period. Complete some tasks to see your stats!',
|
||||
cls: 'immerse-no-data-message'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Summary stats
|
||||
this.renderSummaryStats(reportContent, reportData);
|
||||
|
||||
// Time by list (donut chart)
|
||||
if (reportData.timeByList.length > 0) {
|
||||
this.renderTimeByList(reportContent, reportData);
|
||||
}
|
||||
|
||||
// Productivity insights
|
||||
this.renderInsights(reportContent, reportData);
|
||||
|
||||
// Daily breakdown (bar chart - simplified text version)
|
||||
if (reportData.dailyBreakdown.length > 0) {
|
||||
this.renderDailyBreakdown(reportContent, reportData);
|
||||
}
|
||||
}
|
||||
|
||||
renderSummaryStats(container: Element, data: import('./types').ReportData) {
|
||||
const statsGrid = container.createEl('div', { cls: 'immerse-stats-grid' });
|
||||
|
||||
const stats = [
|
||||
{ label: 'TASKS DONE', value: data.totalTasks.toString(), icon: '✓' },
|
||||
{ label: 'TASKS PER DAY', value: data.tasksPerDay.toFixed(1), icon: '📅' },
|
||||
{ label: 'HOURS PER DAY', value: data.hoursPerDay.toFixed(1), icon: '⏰' },
|
||||
{ label: 'MINS PER TASK', value: data.minsPerTask.toString(), icon: '⏱️' },
|
||||
{ label: 'DAY STREAK', value: data.currentStreak.toString(), icon: '🔥' },
|
||||
{ label: 'TOTAL HOURS', value: (data.totalMinutes / 60).toFixed(1), icon: '⌚' },
|
||||
];
|
||||
|
||||
stats.forEach(stat => {
|
||||
const statCard = statsGrid.createEl('div', { cls: 'immerse-stat-card' });
|
||||
statCard.createEl('div', { text: stat.label, cls: 'immerse-stat-label' });
|
||||
const valueRow = statCard.createEl('div', { cls: 'immerse-stat-value-row' });
|
||||
valueRow.createEl('span', { text: stat.icon, cls: 'immerse-stat-icon' });
|
||||
valueRow.createEl('span', { text: stat.value, cls: 'immerse-stat-value' });
|
||||
});
|
||||
}
|
||||
|
||||
renderTimeByList(container: Element, data: import('./types').ReportData) {
|
||||
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||
section.createEl('h3', { text: 'Time by List', cls: 'immerse-report-section-title' });
|
||||
|
||||
const listContainer = section.createEl('div', { cls: 'immerse-time-by-list' });
|
||||
|
||||
data.timeByList.forEach(item => {
|
||||
const listItem = listContainer.createEl('div', { cls: 'immerse-list-stat-item' });
|
||||
|
||||
// List info
|
||||
const listInfo = listItem.createEl('div', { cls: 'immerse-list-info' });
|
||||
listInfo.createEl('span', { text: item.listIcon, cls: 'immerse-list-icon' });
|
||||
listInfo.createEl('span', { text: item.listName, cls: 'immerse-list-name' });
|
||||
|
||||
// Progress bar
|
||||
const progressBar = listItem.createEl('div', { cls: 'immerse-list-progress' });
|
||||
const progress = progressBar.createEl('div', { cls: 'immerse-list-progress-fill' });
|
||||
progress.style.width = `${item.percentage}%`;
|
||||
progress.style.backgroundColor = item.listColor;
|
||||
|
||||
// Stats
|
||||
const stats = listItem.createEl('div', { cls: 'immerse-list-stats' });
|
||||
stats.createEl('span', {
|
||||
text: `${item.taskCount} tasks`,
|
||||
cls: 'immerse-list-stat-text'
|
||||
});
|
||||
stats.createEl('span', {
|
||||
text: `${this.plugin.formatTimeHuman(item.minutes)}`,
|
||||
cls: 'immerse-list-stat-text'
|
||||
});
|
||||
stats.createEl('span', {
|
||||
text: `${item.percentage.toFixed(1)}%`,
|
||||
cls: 'immerse-list-stat-percentage'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderInsights(container: Element, data: import('./types').ReportData) {
|
||||
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||
section.createEl('h3', { text: 'Productivity Insights', cls: 'immerse-report-section-title' });
|
||||
|
||||
const insightsGrid = section.createEl('div', { cls: 'immerse-insights-grid' });
|
||||
|
||||
if (data.mostProductiveHour !== undefined) {
|
||||
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||
card.createEl('div', { text: 'MOST PRODUCTIVE HOUR', cls: 'immerse-insight-label' });
|
||||
card.createEl('div', {
|
||||
text: `${data.mostProductiveHour}:00 - ${data.mostProductiveHour + 1}:00`,
|
||||
cls: 'immerse-insight-value'
|
||||
});
|
||||
}
|
||||
|
||||
if (data.mostProductiveDay) {
|
||||
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||
card.createEl('div', { text: 'MOST PRODUCTIVE DAY', cls: 'immerse-insight-label' });
|
||||
card.createEl('div', { text: data.mostProductiveDay, cls: 'immerse-insight-value' });
|
||||
}
|
||||
|
||||
if (data.mostProductiveMonth) {
|
||||
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||
card.createEl('div', { text: 'MOST PRODUCTIVE MONTH', cls: 'immerse-insight-label' });
|
||||
card.createEl('div', { text: data.mostProductiveMonth, cls: 'immerse-insight-value' });
|
||||
}
|
||||
}
|
||||
|
||||
renderDailyBreakdown(container: Element, data: import('./types').ReportData) {
|
||||
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||
section.createEl('h3', { text: 'Daily Breakdown', cls: 'immerse-report-section-title' });
|
||||
|
||||
const table = section.createEl('table', { cls: 'immerse-daily-table' });
|
||||
const thead = table.createEl('thead');
|
||||
const headerRow = thead.createEl('tr');
|
||||
headerRow.createEl('th', { text: 'Date' });
|
||||
headerRow.createEl('th', { text: 'Tasks' });
|
||||
headerRow.createEl('th', { text: 'Hours' });
|
||||
headerRow.createEl('th', { text: 'Pomodoros' });
|
||||
|
||||
const tbody = table.createEl('tbody');
|
||||
// Show last 14 days max
|
||||
const recentData = data.dailyBreakdown.slice(-14);
|
||||
recentData.forEach(day => {
|
||||
const row = tbody.createEl('tr');
|
||||
row.createEl('td', { text: day.date });
|
||||
row.createEl('td', { text: day.tasks.toString() });
|
||||
row.createEl('td', { text: day.hours.toFixed(1) });
|
||||
row.createEl('td', { text: day.pomodoros.toString() });
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
// ============ Emoji Picker Modal ============
|
||||
|
||||
export class EmojiPickerModal extends Modal {
|
||||
onSelect: (emoji: string) => void;
|
||||
currentEmoji: string;
|
||||
|
||||
constructor(app: App, currentEmoji: string, onSelect: (emoji: string) => void) {
|
||||
super(app);
|
||||
this.currentEmoji = currentEmoji;
|
||||
this.onSelect = onSelect;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
contentEl.addClass('immerse-emoji-picker');
|
||||
|
||||
contentEl.createEl('h2', { text: 'Select Emoji' });
|
||||
|
||||
// Current selection
|
||||
if (this.currentEmoji) {
|
||||
const currentDiv = contentEl.createDiv({ cls: 'immerse-emoji-current' });
|
||||
currentDiv.createEl('span', { text: 'Current: ' });
|
||||
currentDiv.createEl('span', { text: this.currentEmoji, cls: 'immerse-emoji-current-icon' });
|
||||
}
|
||||
|
||||
// Search box
|
||||
const searchContainer = contentEl.createDiv({ cls: 'immerse-emoji-search' });
|
||||
const searchInput = searchContainer.createEl('input', {
|
||||
type: 'text',
|
||||
placeholder: 'Search emojis...',
|
||||
cls: 'immerse-emoji-search-input'
|
||||
});
|
||||
|
||||
// Emoji grid container
|
||||
const gridContainer = contentEl.createDiv({ cls: 'immerse-emoji-categories' });
|
||||
|
||||
// Render all categories
|
||||
const renderCategories = (filter: string = '') => {
|
||||
gridContainer.empty();
|
||||
|
||||
Object.entries(EMOJI_CATEGORIES).forEach(([category, emojis]) => {
|
||||
const filteredEmojis = filter
|
||||
? emojis.filter(emoji => {
|
||||
const keywords = EMOJI_KEYWORDS[emoji] || '';
|
||||
return keywords.toLowerCase().includes(filter) || emoji.includes(filter);
|
||||
})
|
||||
: emojis;
|
||||
|
||||
if (filteredEmojis.length === 0) return;
|
||||
|
||||
const categoryDiv = gridContainer.createDiv({ cls: 'immerse-emoji-category' });
|
||||
categoryDiv.createEl('h3', { text: category, cls: 'immerse-emoji-category-title' });
|
||||
|
||||
const grid = categoryDiv.createDiv({ cls: 'immerse-emoji-grid' });
|
||||
|
||||
filteredEmojis.forEach(emoji => {
|
||||
const button = grid.createEl('button', {
|
||||
text: emoji,
|
||||
cls: 'immerse-emoji-button'
|
||||
});
|
||||
|
||||
if (emoji === this.currentEmoji) {
|
||||
button.addClass('immerse-emoji-selected');
|
||||
}
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
this.onSelect(emoji);
|
||||
this.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
// Initial render
|
||||
renderCategories();
|
||||
|
||||
// Search functionality
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const filter = (e.target as HTMLInputElement).value.toLowerCase();
|
||||
renderCategories(filter);
|
||||
});
|
||||
|
||||
// Custom emoji input
|
||||
const customDiv = contentEl.createDiv({ cls: 'immerse-emoji-custom' });
|
||||
customDiv.createEl('span', { text: 'Or enter custom emoji: ' });
|
||||
const customInput = customDiv.createEl('input', {
|
||||
type: 'text',
|
||||
placeholder: 'Paste emoji',
|
||||
cls: 'immerse-emoji-custom-input',
|
||||
});
|
||||
|
||||
const customBtn = customDiv.createEl('button', {
|
||||
text: 'Use Custom',
|
||||
cls: 'immerse-btn immerse-btn-primary'
|
||||
});
|
||||
|
||||
customBtn.addEventListener('click', () => {
|
||||
const customEmoji = customInput.value.trim();
|
||||
if (customEmoji) {
|
||||
this.onSelect(customEmoji);
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
||||
426
src/reportView.ts
Normal file
426
src/reportView.ts
Normal file
@@ -0,0 +1,426 @@
|
||||
import {
|
||||
ItemView,
|
||||
WorkspaceLeaf,
|
||||
Setting,
|
||||
} from 'obsidian';
|
||||
|
||||
import { ReportFilters, ReportData } from './types';
|
||||
import ImmersePlugin from './main';
|
||||
|
||||
export const VIEW_TYPE_REPORT = 'immerse-report-view';
|
||||
|
||||
export class ReportView extends ItemView {
|
||||
plugin: ImmersePlugin;
|
||||
startDate: string;
|
||||
endDate: string;
|
||||
selectedListIds: string[] = [];
|
||||
activeQuickFilter: string | null = 'Last 7 days'; // Track active filter
|
||||
startDateInput: HTMLInputElement | null = null;
|
||||
endDateInput: HTMLInputElement | null = null;
|
||||
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ImmersePlugin) {
|
||||
super(leaf);
|
||||
this.plugin = plugin;
|
||||
|
||||
// Default to last 7 days
|
||||
const today = new Date();
|
||||
this.endDate = today.toISOString().split('T')[0];
|
||||
const weekAgo = new Date(today);
|
||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||
this.startDate = weekAgo.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
getViewType(): string {
|
||||
return VIEW_TYPE_REPORT;
|
||||
}
|
||||
|
||||
getDisplayText(): string {
|
||||
return '📊 Reports';
|
||||
}
|
||||
|
||||
getIcon(): string {
|
||||
return 'bar-chart-2';
|
||||
}
|
||||
|
||||
async onOpen() {
|
||||
const container = this.containerEl.children[1];
|
||||
container.empty();
|
||||
container.addClass('immerse-report-view');
|
||||
|
||||
this.renderContent();
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
// Nothing to clean up
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const container = this.containerEl.children[1];
|
||||
container.empty();
|
||||
|
||||
// Header
|
||||
const header = container.createEl('div', { cls: 'immerse-report-header' });
|
||||
header.createEl('h2', { text: '📊 Reports', cls: 'immerse-report-title' });
|
||||
|
||||
// Filters section
|
||||
this.renderFilters(container);
|
||||
|
||||
// Generate button
|
||||
const generateBtn = container.createEl('button', {
|
||||
text: '🔄 Generate Report',
|
||||
cls: 'immerse-btn immerse-btn-primary immerse-report-generate-btn'
|
||||
});
|
||||
generateBtn.addEventListener('click', () => this.renderReport(container));
|
||||
|
||||
// Initial report render
|
||||
this.renderReport(container);
|
||||
}
|
||||
|
||||
renderFilters(container: Element) {
|
||||
const filtersSection = container.createEl('div', { cls: 'immerse-report-filters' });
|
||||
|
||||
// Date range
|
||||
const dateRow = filtersSection.createEl('div', { cls: 'immerse-report-filter-row' });
|
||||
|
||||
new Setting(dateRow)
|
||||
.setName('Start Date')
|
||||
.addText(text => {
|
||||
text.setValue(this.startDate)
|
||||
.onChange(value => {
|
||||
this.startDate = value;
|
||||
this.activeQuickFilter = null; // Clear active filter when manually changing dates
|
||||
});
|
||||
text.inputEl.type = 'date';
|
||||
this.startDateInput = text.inputEl; // Store reference
|
||||
});
|
||||
|
||||
new Setting(dateRow)
|
||||
.setName('End Date')
|
||||
.addText(text => {
|
||||
text.setValue(this.endDate)
|
||||
.onChange(value => {
|
||||
this.endDate = value;
|
||||
this.activeQuickFilter = null; // Clear active filter when manually changing dates
|
||||
});
|
||||
text.inputEl.type = 'date';
|
||||
this.endDateInput = text.inputEl; // Store reference
|
||||
});
|
||||
|
||||
// Quick filters
|
||||
const quickFilters = filtersSection.createEl('div', { cls: 'immerse-report-quick-filters' });
|
||||
quickFilters.createEl('span', { text: 'Quick select: ', cls: 'immerse-filter-label' });
|
||||
|
||||
const filters = [
|
||||
{ label: 'Today', days: 0 },
|
||||
{ label: 'Last 7 days', days: 7 },
|
||||
{ label: 'Last 30 days', days: 30 },
|
||||
{ label: 'Last 90 days', days: 90 },
|
||||
];
|
||||
|
||||
filters.forEach(filter => {
|
||||
const btn = quickFilters.createEl('button', {
|
||||
text: filter.label,
|
||||
cls: 'immerse-quick-filter-btn'
|
||||
});
|
||||
|
||||
// Set active class if this is the default active filter
|
||||
if (filter.label === this.activeQuickFilter) {
|
||||
btn.addClass('active');
|
||||
}
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
// Update dates
|
||||
const today = new Date();
|
||||
this.endDate = today.toISOString().split('T')[0];
|
||||
const startDate = new Date(today);
|
||||
startDate.setDate(startDate.getDate() - filter.days);
|
||||
this.startDate = startDate.toISOString().split('T')[0];
|
||||
|
||||
// Update date inputs
|
||||
if (this.startDateInput) this.startDateInput.value = this.startDate;
|
||||
if (this.endDateInput) this.endDateInput.value = this.endDate;
|
||||
|
||||
// Update active filter
|
||||
this.activeQuickFilter = filter.label;
|
||||
|
||||
// Update button states
|
||||
quickFilters.querySelectorAll('.immerse-quick-filter-btn').forEach(b => {
|
||||
b.removeClass('active');
|
||||
});
|
||||
btn.addClass('active');
|
||||
|
||||
this.renderReport(container);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderReport(container: Element) {
|
||||
// Remove old report if exists
|
||||
const oldReport = container.querySelector('.immerse-report-content');
|
||||
if (oldReport) oldReport.remove();
|
||||
|
||||
// Generate report data
|
||||
const filters: ReportFilters = {
|
||||
startDate: this.startDate,
|
||||
endDate: this.endDate,
|
||||
listIds: this.selectedListIds.length > 0 ? this.selectedListIds : undefined,
|
||||
};
|
||||
|
||||
const reportData = this.plugin.generateReport(filters);
|
||||
|
||||
// Create report content
|
||||
const reportContent = container.createEl('div', { cls: 'immerse-report-content' });
|
||||
|
||||
// Check if we have data
|
||||
if (reportData.totalTasks === 0) {
|
||||
reportContent.createEl('div', {
|
||||
text: 'No data available for the selected period. Complete some tasks to see your stats!',
|
||||
cls: 'immerse-no-data-message'
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Summary stats
|
||||
this.renderSummaryStats(reportContent, reportData);
|
||||
|
||||
// Time by list
|
||||
if (reportData.timeByList.length > 0) {
|
||||
this.renderTimeByList(reportContent, reportData);
|
||||
}
|
||||
|
||||
// Productivity insights
|
||||
this.renderInsights(reportContent, reportData);
|
||||
|
||||
// Daily breakdown
|
||||
if (reportData.dailyBreakdown.length > 0) {
|
||||
this.renderDailyBreakdown(reportContent, reportData);
|
||||
}
|
||||
}
|
||||
|
||||
renderSummaryStats(container: Element, data: ReportData) {
|
||||
const statsGrid = container.createEl('div', { cls: 'immerse-stats-grid' });
|
||||
|
||||
const stats = [
|
||||
{ label: 'TASKS DONE', value: data.totalTasks.toString(), icon: '✓' },
|
||||
{ label: 'TASKS PER DAY', value: data.tasksPerDay.toFixed(1), icon: '📅' },
|
||||
{ label: 'HOURS PER DAY', value: data.hoursPerDay.toFixed(1), icon: '⏰' },
|
||||
{ label: 'MINS PER TASK', value: data.minsPerTask.toString(), icon: '⏱️' },
|
||||
{ label: 'DAY STREAK', value: data.currentStreak.toString(), icon: '🔥' },
|
||||
{ label: 'TOTAL HOURS', value: (data.totalMinutes / 60).toFixed(1), icon: '⌚' },
|
||||
];
|
||||
|
||||
stats.forEach(stat => {
|
||||
const statCard = statsGrid.createEl('div', { cls: 'immerse-stat-card' });
|
||||
statCard.createEl('div', { text: stat.label, cls: 'immerse-stat-label' });
|
||||
const valueRow = statCard.createEl('div', { cls: 'immerse-stat-value-row' });
|
||||
valueRow.createEl('span', { text: stat.icon, cls: 'immerse-stat-icon' });
|
||||
valueRow.createEl('span', { text: stat.value, cls: 'immerse-stat-value' });
|
||||
});
|
||||
}
|
||||
|
||||
renderTimeByList(container: Element, data: ReportData) {
|
||||
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||
section.createEl('h3', { text: 'Time by List', cls: 'immerse-report-section-title' });
|
||||
|
||||
const listContainer = section.createEl('div', { cls: 'immerse-time-by-list' });
|
||||
|
||||
data.timeByList.forEach(item => {
|
||||
const listItem = listContainer.createEl('div', { cls: 'immerse-list-stat-item' });
|
||||
|
||||
// List info
|
||||
const listInfo = listItem.createEl('div', { cls: 'immerse-list-info' });
|
||||
listInfo.createEl('span', { text: item.listIcon, cls: 'immerse-list-icon' });
|
||||
listInfo.createEl('span', { text: item.listName, cls: 'immerse-list-name' });
|
||||
|
||||
// Progress bar
|
||||
const progressBar = listItem.createEl('div', { cls: 'immerse-list-progress' });
|
||||
const progress = progressBar.createEl('div', { cls: 'immerse-list-progress-fill' });
|
||||
progress.style.width = `${item.percentage}%`;
|
||||
progress.style.background = item.listColor;
|
||||
|
||||
// Stats
|
||||
const stats = listItem.createEl('div', { cls: 'immerse-list-stats' });
|
||||
stats.createEl('span', {
|
||||
text: `${item.taskCount} tasks`,
|
||||
cls: 'immerse-list-stat-text'
|
||||
});
|
||||
stats.createEl('span', {
|
||||
text: `${this.plugin.formatTimeHuman(item.minutes)}`,
|
||||
cls: 'immerse-list-stat-text'
|
||||
});
|
||||
stats.createEl('span', {
|
||||
text: `${item.percentage.toFixed(1)}%`,
|
||||
cls: 'immerse-list-stat-percentage'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
renderInsights(container: Element, data: ReportData) {
|
||||
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||
section.createEl('h3', { text: 'Productivity Insights', cls: 'immerse-report-section-title' });
|
||||
|
||||
const insightsGrid = section.createEl('div', { cls: 'immerse-insights-grid' });
|
||||
|
||||
if (data.mostProductiveHour !== undefined) {
|
||||
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||
card.createEl('span', { text: '🕐', cls: 'immerse-insight-icon' });
|
||||
card.createEl('span', { text: 'MOST PRODUCTIVE HOUR', cls: 'immerse-insight-label' });
|
||||
card.createEl('span', {
|
||||
text: `${data.mostProductiveHour}:00 - ${data.mostProductiveHour + 1}:00`,
|
||||
cls: 'immerse-insight-value'
|
||||
});
|
||||
}
|
||||
|
||||
if (data.mostProductiveDay) {
|
||||
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||
card.createEl('span', { text: '📅', cls: 'immerse-insight-icon' });
|
||||
card.createEl('span', { text: 'MOST PRODUCTIVE DAY', cls: 'immerse-insight-label' });
|
||||
card.createEl('span', { text: data.mostProductiveDay, cls: 'immerse-insight-value' });
|
||||
}
|
||||
|
||||
if (data.mostProductiveMonth) {
|
||||
const card = insightsGrid.createEl('div', { cls: 'immerse-insight-card' });
|
||||
card.createEl('span', { text: '🗓️', cls: 'immerse-insight-icon' });
|
||||
card.createEl('span', { text: 'MOST PRODUCTIVE MONTH', cls: 'immerse-insight-label' });
|
||||
card.createEl('span', { text: data.mostProductiveMonth, cls: 'immerse-insight-value' });
|
||||
}
|
||||
}
|
||||
|
||||
renderPieChart(container: Element, data: ReportData) {
|
||||
const pieContainer = container.createEl('div', { cls: 'immerse-daily-pie-container' });
|
||||
|
||||
// Calculate totals - normalize to minutes for fair comparison
|
||||
const totalTasks = data.totalTasks;
|
||||
const totalMinutes = data.totalMinutes;
|
||||
const totalPomodoros = data.totalPomodoros;
|
||||
|
||||
// For pie chart, let's use minutes as the base unit
|
||||
// Assume average task takes data.minsPerTask minutes
|
||||
const taskMinutes = totalTasks * data.minsPerTask;
|
||||
const pomodoroMinutes = totalPomodoros * 25; // Standard pomodoro is 25 minutes
|
||||
|
||||
const totalTime = taskMinutes + totalMinutes + pomodoroMinutes;
|
||||
|
||||
if (totalTime === 0) return;
|
||||
|
||||
// Calculate percentages based on time
|
||||
const tasksPercent = (taskMinutes / totalTime) * 100;
|
||||
const hoursPercent = (totalMinutes / totalTime) * 100;
|
||||
const pomodorosPercent = (pomodoroMinutes / totalTime) * 100;
|
||||
|
||||
// Calculate degrees for conic gradient
|
||||
const tasksDeg = (tasksPercent / 100) * 360;
|
||||
const hoursDeg = tasksDeg + (hoursPercent / 100) * 360;
|
||||
|
||||
// Create pie chart with segments
|
||||
const pieChart = pieContainer.createEl('div', { cls: 'immerse-daily-pie-chart' });
|
||||
|
||||
// Build conic-gradient string with explicit color stops
|
||||
const gradient = `conic-gradient(from 0deg, #6366f1 0deg ${tasksDeg}deg, #22c55e ${tasksDeg}deg ${hoursDeg}deg, #f59e0b ${hoursDeg}deg 360deg)`;
|
||||
pieChart.style.background = gradient;
|
||||
|
||||
// Center circle with total
|
||||
const center = pieChart.createEl('div', { cls: 'immerse-daily-pie-center' });
|
||||
center.createEl('div', { text: data.totalTasks.toString(), cls: 'immerse-daily-pie-center-value' });
|
||||
center.createEl('div', { text: 'TOTAL TASKS', cls: 'immerse-daily-pie-center-label' });
|
||||
|
||||
// Legend
|
||||
const legend = pieContainer.createEl('div', { cls: 'immerse-daily-pie-legend' });
|
||||
|
||||
// Tasks legend item
|
||||
const tasksItem = legend.createEl('div', { cls: 'immerse-daily-pie-legend-item' });
|
||||
const tasksColor = tasksItem.createEl('div', { cls: 'immerse-daily-pie-legend-color' });
|
||||
tasksColor.style.background = '#6366f1';
|
||||
const tasksInfo = tasksItem.createEl('div', { cls: 'immerse-daily-pie-legend-info' });
|
||||
const tasksLabel = tasksInfo.createEl('div', { cls: 'immerse-daily-pie-legend-label' });
|
||||
tasksLabel.createEl('span', { text: '✓' });
|
||||
tasksLabel.appendText('Tasks Completed');
|
||||
tasksInfo.createEl('div', { text: totalTasks.toString(), cls: 'immerse-daily-pie-legend-value' });
|
||||
tasksInfo.createEl('div', { text: `${tasksPercent.toFixed(1)}%`, cls: 'immerse-daily-pie-legend-percentage' });
|
||||
|
||||
// Hours legend item
|
||||
const hoursItem = legend.createEl('div', { cls: 'immerse-daily-pie-legend-item' });
|
||||
const hoursColor = hoursItem.createEl('div', { cls: 'immerse-daily-pie-legend-color' });
|
||||
hoursColor.style.background = '#22c55e';
|
||||
const hoursInfo = hoursItem.createEl('div', { cls: 'immerse-daily-pie-legend-info' });
|
||||
const hoursLabel = hoursInfo.createEl('div', { cls: 'immerse-daily-pie-legend-label' });
|
||||
hoursLabel.createEl('span', { text: '⏱️' });
|
||||
hoursLabel.appendText('Total Hours');
|
||||
hoursInfo.createEl('div', { text: (totalMinutes / 60).toFixed(1), cls: 'immerse-daily-pie-legend-value' });
|
||||
hoursInfo.createEl('div', { text: `${hoursPercent.toFixed(1)}%`, cls: 'immerse-daily-pie-legend-percentage' });
|
||||
|
||||
// Pomodoros legend item
|
||||
const pomodorosItem = legend.createEl('div', { cls: 'immerse-daily-pie-legend-item' });
|
||||
const pomodorosColor = pomodorosItem.createEl('div', { cls: 'immerse-daily-pie-legend-color' });
|
||||
pomodorosColor.style.background = '#f59e0b';
|
||||
const pomodorosInfo = pomodorosItem.createEl('div', { cls: 'immerse-daily-pie-legend-info' });
|
||||
const pomodorosLabel = pomodorosInfo.createEl('div', { cls: 'immerse-daily-pie-legend-label' });
|
||||
pomodorosLabel.createEl('span', { text: '🍅' });
|
||||
pomodorosLabel.appendText('Pomodoros');
|
||||
pomodorosInfo.createEl('div', { text: totalPomodoros.toString(), cls: 'immerse-daily-pie-legend-value' });
|
||||
pomodorosInfo.createEl('div', { text: `${pomodorosPercent.toFixed(1)}%`, cls: 'immerse-daily-pie-legend-percentage' });
|
||||
}
|
||||
|
||||
renderDailyBreakdown(container: Element, data: ReportData) {
|
||||
const section = container.createEl('div', { cls: 'immerse-report-section' });
|
||||
section.createEl('h3', { text: 'Daily Breakdown', cls: 'immerse-report-section-title' });
|
||||
|
||||
// Add pie chart for overall summary
|
||||
this.renderPieChart(section, data);
|
||||
|
||||
const breakdownContainer = section.createEl('div', { cls: 'immerse-daily-breakdown-container' });
|
||||
|
||||
// Show last 10 days max
|
||||
const recentData = data.dailyBreakdown.slice(-10);
|
||||
|
||||
// Find max values for scaling bars
|
||||
const maxTasks = Math.max(...recentData.map(d => d.tasks), 1);
|
||||
const maxHours = Math.max(...recentData.map(d => d.hours), 1);
|
||||
const maxPomodoros = Math.max(...recentData.map(d => d.pomodoros), 1);
|
||||
|
||||
recentData.forEach(day => {
|
||||
const row = breakdownContainer.createEl('div', { cls: 'immerse-daily-row' });
|
||||
|
||||
// Date column with formatted date
|
||||
const dateEl = row.createEl('div', { cls: 'immerse-daily-date' });
|
||||
const date = new Date(day.date + 'T00:00:00');
|
||||
const dayName = date.toLocaleDateString('en-US', { weekday: 'short' }).toUpperCase();
|
||||
const monthDay = date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
||||
dateEl.createEl('div', { text: dayName, cls: 'immerse-daily-date-day' });
|
||||
dateEl.createEl('div', { text: monthDay, cls: 'immerse-daily-date-num' });
|
||||
|
||||
// Bars column
|
||||
const barsContainer = row.createEl('div', { cls: 'immerse-daily-bars' });
|
||||
|
||||
// Tasks bar
|
||||
const tasksRow = barsContainer.createEl('div', { cls: 'immerse-daily-bar-row' });
|
||||
const tasksLabel = tasksRow.createEl('span', { cls: 'immerse-daily-bar-label' });
|
||||
tasksLabel.createEl('span', { text: '✓', cls: 'immerse-daily-bar-icon' });
|
||||
tasksLabel.appendText('Tasks');
|
||||
const tasksTrack = tasksRow.createEl('div', { cls: 'immerse-daily-bar-track' });
|
||||
const tasksFill = tasksTrack.createEl('div', { cls: 'immerse-daily-bar-fill tasks' });
|
||||
tasksFill.style.width = `${(day.tasks / maxTasks) * 100}%`;
|
||||
tasksRow.createEl('span', { text: day.tasks.toString(), cls: 'immerse-daily-bar-value' });
|
||||
|
||||
// Hours bar
|
||||
const hoursRow = barsContainer.createEl('div', { cls: 'immerse-daily-bar-row' });
|
||||
const hoursLabel = hoursRow.createEl('span', { cls: 'immerse-daily-bar-label' });
|
||||
hoursLabel.createEl('span', { text: '⏱️', cls: 'immerse-daily-bar-icon' });
|
||||
hoursLabel.appendText('Hours');
|
||||
const hoursTrack = hoursRow.createEl('div', { cls: 'immerse-daily-bar-track' });
|
||||
const hoursFill = hoursTrack.createEl('div', { cls: 'immerse-daily-bar-fill hours' });
|
||||
hoursFill.style.width = `${(day.hours / maxHours) * 100}%`;
|
||||
hoursRow.createEl('span', { text: day.hours.toFixed(1), cls: 'immerse-daily-bar-value' });
|
||||
|
||||
// Pomodoros bar
|
||||
const pomodorosRow = barsContainer.createEl('div', { cls: 'immerse-daily-bar-row' });
|
||||
const pomodorosLabel = pomodorosRow.createEl('span', { cls: 'immerse-daily-bar-label' });
|
||||
pomodorosLabel.createEl('span', { text: '🍅', cls: 'immerse-daily-bar-icon' });
|
||||
pomodorosLabel.appendText('Pomodoros');
|
||||
const pomodorosTrack = pomodorosRow.createEl('div', { cls: 'immerse-daily-bar-track' });
|
||||
const pomodorosFill = pomodorosTrack.createEl('div', { cls: 'immerse-daily-bar-fill pomodoros' });
|
||||
pomodorosFill.style.width = `${(day.pomodoros / maxPomodoros) * 100}%`;
|
||||
pomodorosRow.createEl('span', { text: day.pomodoros.toString(), cls: 'immerse-daily-bar-value' });
|
||||
});
|
||||
}
|
||||
}
|
||||
71
src/types.ts
71
src/types.ts
@@ -41,6 +41,29 @@ export interface ImmerseSettings {
|
||||
defaultReminderMinutes: number; // Default minutes before task to remind
|
||||
}
|
||||
|
||||
// Daily statistics snapshot
|
||||
export interface DailyStats {
|
||||
date: string; // YYYY-MM-DD format
|
||||
tasksCompleted: number;
|
||||
totalMinutes: number;
|
||||
pomodorosCompleted: number;
|
||||
tasksByList: Record<string, number>; // listId -> count
|
||||
minutesByList: Record<string, number>; // listId -> minutes
|
||||
}
|
||||
|
||||
// Archived completed task (for historical reporting)
|
||||
export interface CompletedTaskRecord {
|
||||
id: string;
|
||||
text: string;
|
||||
list: string;
|
||||
estimatedMinutes: number;
|
||||
actualMinutes: number;
|
||||
createdAt: number;
|
||||
completedAt: number;
|
||||
scheduledDate?: string;
|
||||
wasOverdue: boolean; // was it completed after scheduled time?
|
||||
}
|
||||
|
||||
export interface ImmerseData {
|
||||
tasks: ImmerseTask[];
|
||||
completedToday: number;
|
||||
@@ -48,6 +71,9 @@ export interface ImmerseData {
|
||||
streak: number;
|
||||
lastActiveDate: string;
|
||||
pomodorosCompleted: number;
|
||||
// Historical data for reporting
|
||||
dailyStats: DailyStats[]; // Array of daily statistics
|
||||
completedTasksArchive: CompletedTaskRecord[]; // All completed tasks history
|
||||
}
|
||||
|
||||
export const DEFAULT_SETTINGS: ImmerseSettings = {
|
||||
@@ -79,10 +105,55 @@ export const DEFAULT_DATA: ImmerseData = {
|
||||
streak: 0,
|
||||
lastActiveDate: '',
|
||||
pomodorosCompleted: 0,
|
||||
dailyStats: [],
|
||||
completedTasksArchive: [],
|
||||
};
|
||||
|
||||
export const VIEW_TYPE_IMMERSE = 'immerse-view';
|
||||
|
||||
// ============ Reporting Types ============
|
||||
|
||||
export interface ReportFilters {
|
||||
startDate: string; // YYYY-MM-DD
|
||||
endDate: string; // YYYY-MM-DD
|
||||
listIds?: string[]; // Filter by specific lists (undefined = all)
|
||||
}
|
||||
|
||||
export interface ReportData {
|
||||
// Summary stats
|
||||
totalTasks: number;
|
||||
totalMinutes: number;
|
||||
totalPomodoros: number;
|
||||
tasksPerDay: number;
|
||||
hoursPerDay: number;
|
||||
minsPerTask: number;
|
||||
currentStreak: number;
|
||||
|
||||
// Time breakdown by list
|
||||
timeByList: Array<{
|
||||
listId: string;
|
||||
listName: string;
|
||||
listIcon: string;
|
||||
listColor: string;
|
||||
minutes: number;
|
||||
taskCount: number;
|
||||
percentage: number;
|
||||
}>;
|
||||
|
||||
// Daily breakdown for charts
|
||||
dailyBreakdown: Array<{
|
||||
date: string; // YYYY-MM-DD
|
||||
tasks: number;
|
||||
hours: number;
|
||||
pomodoros: number;
|
||||
}>;
|
||||
|
||||
// Productivity insights
|
||||
mostProductiveHour?: number; // 0-23
|
||||
mostProductiveDay?: string; // day name
|
||||
mostProductiveMonth?: string; // month name
|
||||
}
|
||||
|
||||
// ============ Celebration Messages ============
|
||||
|
||||
export const CELEBRATION_MESSAGES = [
|
||||
|
||||
54
src/view.ts
54
src/view.ts
@@ -107,6 +107,12 @@ export class ImmerseView extends ItemView {
|
||||
|
||||
const actions = header.createEl('div', { cls: 'immerse-header-actions' });
|
||||
|
||||
const reportsBtn = actions.createEl('button', { cls: 'immerse-btn' });
|
||||
reportsBtn.innerHTML = '📊 Reports';
|
||||
reportsBtn.addEventListener('click', () => {
|
||||
this.plugin.activateReportView();
|
||||
});
|
||||
|
||||
const addBtn = actions.createEl('button', { cls: 'immerse-btn immerse-btn-primary' });
|
||||
addBtn.innerHTML = '+ Add Task';
|
||||
addBtn.addEventListener('click', () => {
|
||||
@@ -121,7 +127,7 @@ export class ImmerseView extends ItemView {
|
||||
const statItems = [
|
||||
{ label: 'Pending', value: stats.pendingCount.toString(), icon: '📋' },
|
||||
{ label: 'Done Today', value: stats.completedToday.toString(), icon: '✅' },
|
||||
{ label: 'Focus Time', value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), icon: '⏱️' },
|
||||
{ label: 'Today\'s Focus', value: this.plugin.formatTimeHuman(stats.totalFocusMinutesToday), icon: '⏱️' },
|
||||
{ label: 'Streak', value: `${stats.streak} days`, icon: '🔥' },
|
||||
];
|
||||
|
||||
@@ -148,7 +154,13 @@ export class ImmerseView extends ItemView {
|
||||
const breakLabel = this.plugin.currentTimerSeconds > 0 ? '☕ BREAK TIME' : '✨ BREAK COMPLETE';
|
||||
activeCard.createEl('div', { cls: 'immerse-active-label', text: breakLabel });
|
||||
} else {
|
||||
const workLabel = this.plugin.currentTimerSeconds > 0 ? '🎯 FOCUSING ON' : '🍅 POMODORO COMPLETE';
|
||||
// Determine label based on whether timer is active and mode (stopwatch vs pomodoro)
|
||||
let workLabel: string;
|
||||
if (this.plugin.currentTimerSeconds > 0 || this.plugin.isStopwatchMode) {
|
||||
workLabel = '🎯 FOCUSING ON';
|
||||
} else {
|
||||
workLabel = '🍅 POMODORO COMPLETE';
|
||||
}
|
||||
activeCard.createEl('div', { cls: 'immerse-active-label', text: workLabel });
|
||||
}
|
||||
|
||||
@@ -161,24 +173,26 @@ export class ImmerseView extends ItemView {
|
||||
text: this.plugin.formatTime(this.plugin.currentTimerSeconds)
|
||||
});
|
||||
|
||||
// Progress bar - store reference for updates
|
||||
const progressWrap = activeCard.createEl('div', { cls: 'immerse-progress-wrap' });
|
||||
this.progressBarEl = progressWrap.createEl('div', { cls: 'immerse-progress-bar' });
|
||||
// Progress bar - only show in pomodoro/break mode, not stopwatch
|
||||
if (!this.plugin.isStopwatchMode) {
|
||||
const progressWrap = activeCard.createEl('div', { cls: 'immerse-progress-wrap' });
|
||||
this.progressBarEl = progressWrap.createEl('div', { cls: 'immerse-progress-bar' });
|
||||
|
||||
let progressPercent = 0;
|
||||
if (this.plugin.isBreakMode) {
|
||||
const breakDuration = this.plugin.pomodoroCount % this.plugin.settings.longBreakInterval === 0
|
||||
? this.plugin.settings.longBreakMinutes * 60
|
||||
: this.plugin.settings.pomodoroBreakMinutes * 60;
|
||||
progressPercent = ((breakDuration - this.plugin.currentTimerSeconds) / breakDuration) * 100;
|
||||
} else {
|
||||
const workDuration = this.plugin.settings.pomodoroWorkMinutes * 60;
|
||||
progressPercent = ((workDuration - this.plugin.currentTimerSeconds) / workDuration) * 100;
|
||||
let progressPercent = 0;
|
||||
if (this.plugin.isBreakMode) {
|
||||
const breakDuration = this.plugin.pomodoroCount % this.plugin.settings.longBreakInterval === 0
|
||||
? this.plugin.settings.longBreakMinutes * 60
|
||||
: this.plugin.settings.pomodoroBreakMinutes * 60;
|
||||
progressPercent = ((breakDuration - this.plugin.currentTimerSeconds) / breakDuration) * 100;
|
||||
} else {
|
||||
const workDuration = this.plugin.settings.pomodoroWorkMinutes * 60;
|
||||
progressPercent = ((workDuration - this.plugin.currentTimerSeconds) / workDuration) * 100;
|
||||
}
|
||||
|
||||
this.progressBarEl.style.width = `${Math.min(Math.max(progressPercent, 0), 100)}%`;
|
||||
if (progressPercent >= 100) this.progressBarEl.addClass('immerse-overtime');
|
||||
}
|
||||
|
||||
this.progressBarEl.style.width = `${Math.min(Math.max(progressPercent, 0), 100)}%`;
|
||||
if (progressPercent >= 100) this.progressBarEl.addClass('immerse-overtime');
|
||||
|
||||
// Time info - store reference for actual time updates
|
||||
if (!this.plugin.isBreakMode) {
|
||||
const timeInfo = activeCard.createEl('div', { cls: 'immerse-time-info' });
|
||||
@@ -228,8 +242,8 @@ export class ImmerseView extends ItemView {
|
||||
}
|
||||
} else {
|
||||
// Work mode controls
|
||||
if (this.plugin.currentTimerSeconds > 0) {
|
||||
// Work session still running
|
||||
if (this.plugin.currentTimerSeconds > 0 || this.plugin.isStopwatchMode) {
|
||||
// Work session still running (or stopwatch mode active)
|
||||
this.pauseBtnEl = controls.createEl('button', { cls: 'immerse-btn immerse-btn-secondary' });
|
||||
this.pauseBtnEl.innerHTML = this.plugin.isTimerRunning ? '⏸ Pause' : '▶ Resume';
|
||||
this.pauseBtnEl.addEventListener('click', () => this.plugin.toggleTimer());
|
||||
@@ -242,7 +256,7 @@ export class ImmerseView extends ItemView {
|
||||
stopBtn.innerHTML = '✕ Stop';
|
||||
stopBtn.addEventListener('click', () => this.plugin.stopTimer());
|
||||
} else {
|
||||
// Work session finished - show break and completion options
|
||||
// Pomodoro session finished - show break and completion options
|
||||
const startBreakBtn = controls.createEl('button', { cls: 'immerse-btn immerse-btn-secondary' });
|
||||
startBreakBtn.innerHTML = '☕ Start Break';
|
||||
startBreakBtn.addEventListener('click', () => this.plugin.startBreak());
|
||||
|
||||
951
styles.css
951
styles.css
@@ -288,11 +288,16 @@
|
||||
}
|
||||
|
||||
.immerse-active-controls .immerse-btn {
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
flex: 1 1 auto;
|
||||
min-width: 100px;
|
||||
max-width: 100%;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: white;
|
||||
border: none;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
line-height: 1.3;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.immerse-active-controls .immerse-btn:hover {
|
||||
@@ -649,3 +654,945 @@
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============ Report View ============ */
|
||||
.immerse-report-view {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.immerse-report-header {
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid var(--ft-border);
|
||||
}
|
||||
|
||||
.immerse-report-title {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
margin: 0 0 10px 0;
|
||||
color: var(--ft-text);
|
||||
}
|
||||
|
||||
.immerse-report-filters {
|
||||
background: var(--ft-bg-secondary);
|
||||
padding: 12px;
|
||||
border-radius: var(--ft-radius);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.immerse-report-filter-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.immerse-report-filter-row .setting-item {
|
||||
flex: 1;
|
||||
min-width: 200px;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
border: none !important;
|
||||
display: flex !important;
|
||||
flex-direction: row !important;
|
||||
align-items: center !important;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.immerse-report-filter-row .setting-item > * {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.immerse-report-filter-row .setting-item-info {
|
||||
flex-shrink: 0 !important;
|
||||
width: auto !important;
|
||||
min-width: 80px !important;
|
||||
max-width: 100px !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.immerse-report-filter-row .setting-item-control {
|
||||
flex-grow: 1 !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.immerse-report-filter-row .setting-item-name {
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
line-height: 1.4 !important;
|
||||
}
|
||||
|
||||
.immerse-report-filter-row .setting-item-description {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.immerse-report-filter-row input[type="date"] {
|
||||
width: 100% !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.immerse-report-quick-filters {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.immerse-filter-label {
|
||||
font-weight: 500;
|
||||
color: var(--ft-text);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.immerse-quick-filter-btn {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
background: var(--ft-bg);
|
||||
border: 1px solid var(--ft-border);
|
||||
color: var(--ft-text);
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.immerse-quick-filter-btn:hover {
|
||||
background: var(--ft-primary);
|
||||
color: white;
|
||||
border-color: var(--ft-primary);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.immerse-quick-filter-btn.active {
|
||||
background: var(--ft-primary);
|
||||
color: white;
|
||||
border-color: var(--ft-primary);
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2),
|
||||
0 0 20px rgba(99, 102, 241, 0.4);
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.2),
|
||||
0 0 20px rgba(99, 102, 241, 0.4);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.3),
|
||||
0 0 25px rgba(99, 102, 241, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
.immerse-report-generate-btn {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.immerse-report-content {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.immerse-no-data-message {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: var(--ft-text-muted);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Stats Grid */
|
||||
.immerse-stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.immerse-stat-card {
|
||||
background: var(--ft-bg-secondary);
|
||||
padding: 16px;
|
||||
border-radius: var(--ft-radius);
|
||||
text-align: center;
|
||||
border: 1px solid var(--ft-border);
|
||||
}
|
||||
|
||||
.immerse-stat-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--ft-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.immerse-stat-value-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.immerse-stat-icon {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.immerse-stat-value {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--ft-text);
|
||||
}
|
||||
|
||||
/* Report Sections */
|
||||
.immerse-report-section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.immerse-report-section-title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
margin-bottom: 20px;
|
||||
color: var(--ft-text);
|
||||
padding-bottom: 10px;
|
||||
border-bottom: 2px solid var(--ft-border);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.immerse-report-section-title::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -2px;
|
||||
left: 0;
|
||||
width: 60px;
|
||||
height: 2px;
|
||||
background: linear-gradient(90deg, var(--ft-primary), var(--ft-success));
|
||||
}
|
||||
|
||||
/* Time by List */
|
||||
.immerse-time-by-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.immerse-list-stat-item {
|
||||
background: var(--ft-bg-secondary);
|
||||
padding: 16px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--ft-border);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.immerse-list-stat-item:hover {
|
||||
transform: translateX(4px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.immerse-list-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.immerse-list-icon {
|
||||
font-size: 22px;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.immerse-list-name {
|
||||
font-weight: 600;
|
||||
color: var(--ft-text);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.immerse-list-progress {
|
||||
width: 100%;
|
||||
height: 10px;
|
||||
background: var(--ft-bg);
|
||||
border-radius: 5px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 10px;
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.immerse-list-progress-fill {
|
||||
height: 100%;
|
||||
transition: width 0.6s ease;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.immerse-list-stats {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.immerse-list-stat-text {
|
||||
color: var(--ft-text-muted);
|
||||
}
|
||||
|
||||
.immerse-list-stat-percentage {
|
||||
font-weight: 600;
|
||||
color: var(--ft-text);
|
||||
}
|
||||
|
||||
/* Insights */
|
||||
.immerse-insights-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.immerse-insight-card {
|
||||
background: var(--ft-bg-secondary);
|
||||
padding: 28px;
|
||||
border-radius: 16px;
|
||||
text-align: center;
|
||||
border: 2px solid var(--ft-border);
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.immerse-insight-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 4px;
|
||||
background: var(--ft-primary);
|
||||
border-radius: 16px 16px 0 0;
|
||||
}
|
||||
|
||||
.immerse-insight-card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
|
||||
border-color: var(--ft-primary);
|
||||
}
|
||||
|
||||
.immerse-insight-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 12px;
|
||||
display: block;
|
||||
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
|
||||
}
|
||||
|
||||
.immerse-insight-label {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--ft-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1.5px;
|
||||
margin-bottom: 16px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.immerse-insight-value {
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
color: var(--ft-primary);
|
||||
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.08);
|
||||
line-height: 1.2;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Daily Breakdown - Enhanced Visual Chart Style */
|
||||
.immerse-daily-breakdown-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.immerse-daily-row {
|
||||
display: grid;
|
||||
grid-template-columns: 110px 1fr;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, var(--ft-bg-secondary) 0%, var(--ft-bg) 100%);
|
||||
border-radius: 12px;
|
||||
border: 2px solid var(--ft-border);
|
||||
transition: all 0.3s ease;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.immerse-daily-row::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: 4px;
|
||||
background: linear-gradient(180deg, var(--ft-primary), var(--ft-success));
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.immerse-daily-row:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.immerse-daily-row:hover {
|
||||
transform: translateX(6px);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
|
||||
border-color: var(--ft-primary);
|
||||
}
|
||||
|
||||
.immerse-daily-date {
|
||||
font-weight: 800;
|
||||
color: var(--ft-text);
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.immerse-daily-date-day {
|
||||
font-size: 11px;
|
||||
color: var(--ft-text-muted);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.immerse-daily-date-num {
|
||||
font-size: 16px;
|
||||
color: var(--ft-primary);
|
||||
}
|
||||
|
||||
.immerse-daily-bars {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.immerse-daily-bar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.immerse-daily-bar-label {
|
||||
min-width: 90px;
|
||||
font-size: 12px;
|
||||
color: var(--ft-text-muted);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.immerse-daily-bar-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.immerse-daily-bar-track {
|
||||
flex: 1;
|
||||
height: 12px;
|
||||
background: var(--ft-bg);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.immerse-daily-bar-fill {
|
||||
height: 100%;
|
||||
border-radius: 6px;
|
||||
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Pie chart for daily breakdown */
|
||||
.immerse-daily-pie-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 40px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-chart {
|
||||
position: relative;
|
||||
width: 280px;
|
||||
height: 280px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* Background set via inline style for proper gradient rendering */
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-chart:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.immerse-daily-pie-center {
|
||||
position: absolute;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
background: var(--ft-bg);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.immerse-daily-pie-center-value {
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
color: var(--ft-text-primary);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-center-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--ft-text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-legend {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-legend-color {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-legend-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-legend-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--ft-text-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-legend-value {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: var(--ft-text-primary);
|
||||
}
|
||||
|
||||
.immerse-daily-pie-legend-percentage {
|
||||
font-size: 12px;
|
||||
color: var(--ft-text-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.immerse-daily-bar-fill.tasks {
|
||||
background: linear-gradient(90deg, var(--ft-primary), #7c3aed);
|
||||
}
|
||||
|
||||
.immerse-daily-bar-fill.hours {
|
||||
background: linear-gradient(90deg, var(--ft-success), #059669);
|
||||
}
|
||||
|
||||
.immerse-daily-bar-fill.pomodoros {
|
||||
background: linear-gradient(90deg, var(--ft-warning), #dc2626);
|
||||
}
|
||||
|
||||
.immerse-daily-bar-value {
|
||||
min-width: 50px;
|
||||
text-align: right;
|
||||
font-weight: 800;
|
||||
font-size: 14px;
|
||||
color: var(--ft-text);
|
||||
font-feature-settings: 'tnum';
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Responsive adjustments for smaller screens */
|
||||
@media (max-width: 768px) {
|
||||
.immerse-report-view {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.immerse-stats-grid {
|
||||
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.immerse-insights-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.immerse-report-filter-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.immerse-report-filter-row .setting-item {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
/* Pie chart mobile optimization */
|
||||
.immerse-daily-pie-container {
|
||||
flex-direction: column;
|
||||
gap: 30px;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-chart {
|
||||
width: 220px;
|
||||
height: 220px;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-center {
|
||||
width: 130px;
|
||||
height: 130px;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-center-value {
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
/* Daily breakdown mobile optimization */
|
||||
.immerse-daily-row {
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.immerse-daily-date {
|
||||
flex-direction: row;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.immerse-daily-bars {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.immerse-daily-bar-label {
|
||||
min-width: 70px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.immerse-daily-bar-track {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.immerse-daily-bar-value {
|
||||
min-width: 40px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Task list mobile optimization */
|
||||
.immerse-task-item {
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.immerse-task-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.immerse-task-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* Active card mobile optimization */
|
||||
.immerse-active-card {
|
||||
padding: 20px 16px;
|
||||
}
|
||||
|
||||
.immerse-timer-time {
|
||||
font-size: 40px;
|
||||
}
|
||||
|
||||
/* Stats bar mobile optimization */
|
||||
.immerse-stats-bar {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.immerse-stat-item {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.immerse-stat-value {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.immerse-stat-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
/* Modal optimization for mobile */
|
||||
.immerse-modal {
|
||||
padding: 16px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.immerse-modal-buttons {
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.immerse-modal-buttons .immerse-btn {
|
||||
width: 100%;
|
||||
padding: 12px 16px;
|
||||
font-size: 14px;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
/* Increase touch target sizes */
|
||||
.immerse-checkbox {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
min-width: 28px;
|
||||
}
|
||||
|
||||
.immerse-filter-btn {
|
||||
padding: 8px 14px;
|
||||
font-size: 14px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
/* Better spacing for buttons */
|
||||
.immerse-header-actions {
|
||||
width: 100%;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.immerse-header-actions .immerse-btn {
|
||||
flex: 1;
|
||||
min-height: 44px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Extra small screens (phones in portrait) */
|
||||
@media (max-width: 480px) {
|
||||
.immerse-container {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.immerse-report-view {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
/* Make pie chart smaller on very small screens */
|
||||
.immerse-daily-pie-chart {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-center {
|
||||
width: 110px;
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-center-value {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-center-label {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.immerse-daily-pie-legend-value {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Stack stats grid in single column on very small screens */
|
||||
.immerse-stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
/* Smaller buttons and text */
|
||||
.immerse-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.immerse-timer-time {
|
||||
font-size: 36px;
|
||||
}
|
||||
|
||||
.immerse-active-task-name {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
/* Quick filters wrap better */
|
||||
.immerse-quick-filter-btn {
|
||||
padding: 5px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* Report title smaller */
|
||||
.immerse-report-title {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.immerse-report-section-title {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ============ Emoji Picker ============ */
|
||||
.immerse-emoji-picker {
|
||||
width: 85vw;
|
||||
max-width: 400px;
|
||||
max-height: 80vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.immerse-emoji-picker .modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.immerse-emoji-current {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 12px;
|
||||
padding: 10px;
|
||||
background: var(--ft-bg-secondary);
|
||||
border-radius: var(--ft-radius-sm);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.immerse-emoji-current-icon {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.immerse-emoji-search {
|
||||
margin-bottom: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.immerse-emoji-search-input {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--ft-radius-sm);
|
||||
border: 1px solid var(--ft-border);
|
||||
background: var(--ft-bg);
|
||||
color: var(--ft-text);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.immerse-emoji-categories {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 12px;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.immerse-emoji-category {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.immerse-emoji-category-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--ft-text-muted);
|
||||
margin-bottom: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.immerse-emoji-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(32px, 1fr));
|
||||
gap: 4px;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.immerse-emoji-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid var(--ft-border);
|
||||
background: var(--ft-bg);
|
||||
border-radius: 6px;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.immerse-emoji-button:hover {
|
||||
background: var(--ft-bg-tertiary);
|
||||
transform: scale(1.1);
|
||||
border-color: var(--ft-primary);
|
||||
}
|
||||
|
||||
.immerse-emoji-button.immerse-emoji-selected {
|
||||
background: var(--ft-primary);
|
||||
border-color: var(--ft-primary);
|
||||
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.3);
|
||||
}
|
||||
|
||||
.immerse-emoji-custom {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--ft-border);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.immerse-emoji-custom-input {
|
||||
flex: 1;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--ft-radius-sm);
|
||||
border: 1px solid var(--ft-border);
|
||||
background: var(--ft-bg);
|
||||
color: var(--ft-text);
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"1.0.4": "0.15.0",
|
||||
"1.0.5": "0.15.0",
|
||||
"1.0.6": "0.15.0",
|
||||
"1.0.7": "0.15.0",
|
||||
"1.0.8": "0.15.0",
|
||||
"1.0.9": "0.15.0"
|
||||
"1.0.4": "0.15.0",
|
||||
"1.0.5": "0.15.0",
|
||||
"1.0.6": "0.15.0",
|
||||
"1.0.7": "0.15.0",
|
||||
"1.0.8": "0.15.0",
|
||||
"1.0.9": "0.15.0",
|
||||
"1.1.3": "0.15.0",
|
||||
"1.1.4": "0.15.0"
|
||||
}
|
||||
Reference in New Issue
Block a user