From 8f9e14c832b678ffdd94489c9a421bb179135a59 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 02:51:30 +0100 Subject: [PATCH 01/26] docs: update git workflow - keep individual commits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove squash strategy, emphasize keeping individual commits for: - Better context during conflict resolution - Easier debugging and reverting - More granular code reviews đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- docs/GIT_WORKFLOW.md | 155 ++++++++++++++++++++----------------------- 1 file changed, 71 insertions(+), 84 deletions(-) diff --git a/docs/GIT_WORKFLOW.md b/docs/GIT_WORKFLOW.md index cdb6be794..33780e35b 100644 --- a/docs/GIT_WORKFLOW.md +++ b/docs/GIT_WORKFLOW.md @@ -4,11 +4,11 @@ Dokumentation des Git-Workflows fĂŒr das ManaCore Monorepo. ## Branch-Struktur -| Branch | Zweck | -|--------|-------| -| `main` | Produktion - stabile Releases | -| `dev` | Entwicklung - Integration aller Features | -| `till-dev`, `{name}-dev` | Persönliche Entwicklungs-Branches | +| Branch | Zweck | +| ------------------------------ | ---------------------------------------- | +| `main` | Produktion - stabile Releases | +| `dev` | Entwicklung - Integration aller Features | +| `till-dev`, `{name}-dev` | Persönliche Entwicklungs-Branches | ## Workflow-Übersicht @@ -19,7 +19,7 @@ main (Produktion) │ dev (Integration) ↑ - │ PR (squashed) + │ PR (einzelne Commits behalten) │ till-dev (Feature-Entwicklung) ``` @@ -32,37 +32,16 @@ till-dev (Feature-Entwicklung) # Sicherstellen, dass du auf deinem Branch bist git checkout till-dev -# Änderungen committen (viele kleine Commits sind OK) +# Änderungen committen - kleine, aussagekrĂ€ftige Commits git add . git commit -m "feat(app): add feature X" git commit -m "fix(app): fix bug Y" git commit -m "refactor(app): cleanup Z" ``` -### 2. Vor dem PR: Commits squashen +### 2. RegelmĂ€ĂŸig mit dev synchronisieren -Wenn viele Commits angesammelt sind, sollten diese vor dem PR gequasht werden, um: -- Merge-Konflikte zu minimieren -- Die Git-History sauber zu halten -- Code-Reviews zu vereinfachen - -```bash -# Anzahl der Commits seit dev zĂ€hlen -git log --oneline origin/dev..HEAD | wc -l - -# Alle Commits seit dev zu einem squashen -git reset --soft origin/dev - -# Einen neuen, zusammengefassten Commit erstellen -git commit -m "feat: descriptive summary of all changes" - -# Force-Push zum Remote (ĂŒberschreibt alte Commits) -git push --force-with-lease -``` - -### 3. Rebase mit dev (falls nötig) - -Falls `dev` sich geĂ€ndert hat, muss rebased werden: +Halte deinen Branch aktuell, um große Konflikte zu vermeiden: ```bash # Neuesten Stand von dev holen @@ -71,16 +50,17 @@ git fetch origin dev # Rebase durchfĂŒhren git rebase origin/dev -# Bei Konflikten: -# 1. Konflikte lösen +# Bei Konflikten: Jeden Commit einzeln lösen +# 1. Konflikte in den angezeigten Dateien lösen # 2. git add # 3. git rebase --continue +# 4. Wiederholen bis alle Commits durchlaufen sind # Nach erfolgreichem Rebase pushen git push --force-with-lease ``` -### 4. Pull Request erstellen +### 3. Pull Request erstellen ```bash gh pr create --base dev --head till-dev \ @@ -94,45 +74,37 @@ gh pr create --base dev --head till-dev \ - [ ] Test case 2" ``` -## Squash-Strategie +## Konflikt-Lösung beim Rebase -### Wann squashen? +### Allgemeiner Ablauf -| Situation | Aktion | -|-----------|--------| -| Vor jedem PR | Immer squashen | -| Bei vielen kleinen Commits (10+) | Squashen empfohlen | -| Bei Rebase-Konflikten | Erst squashen, dann rebasen | -| TĂ€gliche Arbeit | Kleine Commits OK | +Bei einem Rebase werden Commits einzeln auf den neuen Base-Branch angewendet. Konflikte mĂŒssen fĂŒr jeden Commit separat gelöst werden - das gibt mehr Kontext und macht die Lösung einfacher. -### Squash-Commit-Message Format +```bash +# Rebase starten +git rebase origin/dev +# Bei Konflikt: Status prĂŒfen +git status # Zeigt konfliktbehaftete Dateien + +# Konflikte lösen, dann: +git add +git rebase --continue + +# NĂ€chster Commit wird angewendet... +# Wiederholen bis fertig ``` -feat: kurze Zusammenfassung (max 50 Zeichen) - -## Neue Features -- Feature 1: Beschreibung -- Feature 2: Beschreibung - -## Bug Fixes -- Fix 1: Beschreibung - -## Breaking Changes (falls vorhanden) -- Breaking Change 1 - -đŸ€– Generated with [Claude Code](https://claude.com/claude-code) - -Co-Authored-By: Claude Opus 4.5 -``` - -## Konflikt-Lösung ### Einfache Konflikte ```bash -# Konflikt in einer Datei -git checkout --ours path/to/file # Unsere Version behalten -git checkout --theirs path/to/file # Ihre Version behalten +# Unsere Version behalten (die aus dem Feature-Branch) +git checkout --ours path/to/file + +# Ihre Version behalten (die aus dev) +git checkout --theirs path/to/file + +# Nach der Wahl: git add path/to/file git rebase --continue ``` @@ -142,7 +114,7 @@ git rebase --continue Diese Datei sollte nie manuell gemerged werden: ```bash -# Ihre Version nehmen und neu installieren +# Version aus dev nehmen und neu installieren git checkout --theirs pnpm-lock.yaml pnpm install --frozen-lockfile=false git add pnpm-lock.yaml @@ -169,15 +141,15 @@ git rebase --abort # ZurĂŒck zum Zustand vor dem Rebase Verwende [Conventional Commits](https://www.conventionalcommits.org/): -| Prefix | Verwendung | -|--------|------------| -| `feat` | Neue Features | -| `fix` | Bug Fixes | -| `docs` | Dokumentation | -| `style` | Formatting (kein Code-Change) | -| `refactor` | Code-Refactoring | -| `test` | Tests hinzufĂŒgen/Ă€ndern | -| `chore` | Build, CI, Dependencies | +| Prefix | Verwendung | +| ---------- | ----------------------------- | +| `feat` | Neue Features | +| `fix` | Bug Fixes | +| `docs` | Dokumentation | +| `style` | Formatting (kein Code-Change) | +| `refactor` | Code-Refactoring | +| `test` | Tests hinzufĂŒgen/Ă€ndern | +| `chore` | Build, CI, Dependencies | ### Scope (optional) @@ -187,6 +159,13 @@ fix(calendar): fix event drag and drop docs(readme): update installation guide ``` +### Kleine, fokussierte Commits + +- Ein Commit = eine logische Änderung +- AussagekrĂ€ftige Commit Messages +- Leichter zu reviewen und bei Problemen zu debuggen +- Einzelne Commits können bei Bedarf reverted werden + ### Branch-Hygiene ```bash @@ -202,23 +181,23 @@ git fetch --prune ```bash # 1. Feature entwickeln git checkout till-dev -# ... viele Commits ĂŒber mehrere Tage ... +git commit -m "feat(network): add D3 force simulation" +git commit -m "feat(network): add zoom and pan controls" +git commit -m "fix(network): fix node positioning on load" +git commit -m "docs(network): add keyboard shortcuts help" -# 2. Vor PR: Status prĂŒfen +# 2. Vor PR: Mit dev synchronisieren git fetch origin dev -git log --oneline origin/dev..HEAD # 54 commits +git rebase origin/dev +# Konflikte einzeln lösen falls nötig... -# 3. Squashen -git reset --soft origin/dev -git commit -m "feat: major update with network graphs, themes, and more" - -# 4. Pushen +# 3. Pushen git push --force-with-lease -# 5. PR erstellen -gh pr create --base dev --head till-dev --title "feat: major update" +# 4. PR erstellen +gh pr create --base dev --head till-dev --title "feat(network): add network graph visualization" -# 6. Nach Merge: Branch aktualisieren +# 5. Nach Merge: Branch aktualisieren git fetch origin dev git checkout till-dev git reset --hard origin/dev @@ -301,6 +280,14 @@ git reflog git reset --hard HEAD@{2} ``` +### Viele Konflikte beim Rebase + +Wenn zu viele Konflikte auftreten: + +1. `git rebase --abort` - Rebase abbrechen +2. RegelmĂ€ĂŸiger rebasen (tĂ€glich/wöchentlich) +3. Bei sehr alten Branches: Mit dem Team absprechen + --- *Zuletzt aktualisiert: 10.12.2025* From 59324cae1c3cf255a702bf4394c0fbf2f4fdc348 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 02:52:55 +0100 Subject: [PATCH 02/26] docs(pr-reviews): add code review for PR #14 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add comprehensive code review document analyzing the major update PR: - Summary of 382 changed files with +39,514/-6,251 lines - Code quality analysis highlighting strengths and improvements - Security considerations and authorization patterns - Recommendations for future PRs and test coverage đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- docs/pr-reviews/PR-014-major-update.md | 272 +++++++++++++++++++++++++ 1 file changed, 272 insertions(+) create mode 100644 docs/pr-reviews/PR-014-major-update.md diff --git a/docs/pr-reviews/PR-014-major-update.md b/docs/pr-reviews/PR-014-major-update.md new file mode 100644 index 000000000..6bb024b2d --- /dev/null +++ b/docs/pr-reviews/PR-014-major-update.md @@ -0,0 +1,272 @@ +# Code Review: PR #14 + +**Title:** feat: major update with network graphs, themes, todo extensions, and more +**Author:** Till-JS +**Date:** 2025-12-10 +**Status:** OPEN +**URL:** https://github.com/Memo-2023/manacore-monorepo/pull/14 + +--- + +## Summary + +| Metric | Value | +|--------|-------| +| Files Changed | 382 | +| Additions | +39,514 | +| Deletions | -6,251 | + +--- + +## Overview + +This is a **major feature release** introducing: + +1. **Network Graph Visualization** - D3.js force-directed graphs for Contacts, Calendar, and Todo apps +2. **Central Tags API** - Unified tagging system in mana-core-auth +3. **Custom Themes System** - Theme editor, community gallery, and sharing +4. **Todo App Extensions** - Kanban boards, statistics, settings page, PWA support +5. **Contacts App Features** - Duplicate detection, photo upload, batch operations, favorites views +6. **Help System** - Shared packages for content, UI, and types (`shared-help-content`, `shared-help-ui`, `shared-help-types`, `shared-help-mobile`) +7. **Skeleton Loaders** - Better loading states across apps +8. **CommandBar** - Global search (Cmd+K) +9. **Bug Fixes** - Network graph simulation fixes, database schema TEXT for user_id + +--- + +## Code Quality Analysis + +### Strengths + +#### 1. Excellent Architecture +- Clean separation of concerns with shared packages (`shared-ui`, `shared-theme`, `shared-tags`, `shared-help-*`) +- Proper Svelte 5 runes usage (`$state`, `$derived`, `$effect`) +- Good TypeScript typing throughout + +#### 2. NetworkGraph Component (`packages/shared-ui/src/organisms/network/`) +- Well-structured D3.js integration with `d3-zoom` and `d3-selection` +- Proper zoom/pan handling +- Keyboard shortcuts implemented: + - `+`/`-` for zoom in/out + - `0` to reset zoom + - `Esc` to deselect + - `F` to focus on selected node + - `/` to focus search +- Accessible with `role="button"`, `aria-label`, `tabindex` +- Efficient re-rendering with proper state management + +#### 3. Tags Service (`services/mana-core-auth/src/tags/`) +- Proper validation (duplicate name check before create/update) +- Good use of Drizzle's `returning()` for immediate results +- User-scoped queries with proper authorization (`userId` checks) +- Default tags created for new users + +#### 4. Custom Themes Store (`packages/shared-theme/src/custom-themes-store.svelte.ts`) +- Clean API design with factory function pattern +- Proper state management with Svelte 5 runes +- Good separation of public/authenticated API calls +- CSS variable application for runtime theming + +--- + +### Suggestions for Improvement + +#### 1. Hardcoded German Strings + +**Location:** `packages/shared-ui/src/organisms/network/NetworkGraph.svelte:440-442` + +```svelte +

Keine Verbindungen gefunden

+

Elemente werden verbunden, wenn sie gemeinsame Tags haben.

+``` + +**Recommendation:** Use i18n for user-facing strings to maintain consistency across the monorepo. + +--- + +#### 2. Default Tags in German Only + +**Location:** `services/mana-core-auth/src/tags/tags.service.ts:10-15` + +```typescript +const DEFAULT_TAGS = [ + { name: 'Arbeit', color: '#3B82F6', icon: 'Briefcase' }, + { name: 'Persönlich', color: '#10B981', icon: 'User' }, + { name: 'Familie', color: '#EC4899', icon: 'Heart' }, + { name: 'Wichtig', color: '#EF4444', icon: 'Star' }, +]; +``` + +**Recommendation:** Consider locale-aware default tags or use English defaults that users can customize. + +--- + +#### 3. Database Connection Pattern + +**Location:** `services/mana-core-auth/src/tags/tags.service.ts:21-24` + +```typescript +private getDb() { + const databaseUrl = this.configService.get('database.url'); + return getDb(databaseUrl!); +} +``` + +**Issue:** Using `!` assertion is less safe. + +**Recommendation:** Inject the database connection via NestJS dependency injection instead of calling `getDb()` on every method call. + +--- + +#### 4. Missing Error Boundary Handling + +The NetworkGraph component handles empty states but doesn't have explicit error handling for malformed node/link data. + +**Recommendation:** Add defensive checks for invalid data structures. + +--- + +### Potential Issues + +#### 1. PR Size + +- 382 files is extremely large for a single PR +- Makes code review difficult and increases risk +- Consider splitting into feature branches for easier review and rollback + +#### 2. Database Schema Consistency + +**Location:** `services/mana-core-auth/src/db/schema/tags.schema.ts:11` + +```typescript +userId: text('user_id').notNull(), +``` + +This uses `TEXT` type for user_id. Verify this aligns with how user IDs are stored in other tables (some use `UUID`). + +#### 3. Missing Test Coverage + +This major PR adds significant functionality without visible test changes. Consider adding: +- Unit tests for `TagsService` +- Component tests for `NetworkGraph` +- Integration tests for the themes API + +--- + +### Security Considerations + +#### Authorization Checks ✅ + +- Tag operations properly scope by `userId` +- Custom themes store requires authentication for write operations +- Community theme browsing allows public access (appropriate) + +#### Input Validation + +- DTOs should be reviewed for proper validation (max lengths, format checks) +- Tag color field accepts any 7-char string - consider validating hex format (`/^#[0-9A-Fa-f]{6}$/`) + +#### Environment Files ✅ + +- `.env.development` only removes 6 lines, no secrets added +- No credentials exposed in the diff + +--- + +## New Shared Packages + +| Package | Purpose | +|---------|---------| +| `@manacore/shared-tags` | Client for central tags API | +| `@manacore/shared-help-content` | Markdown content loader and search | +| `@manacore/shared-help-ui` | Svelte help page components | +| `@manacore/shared-help-types` | TypeScript types for help system | +| `@manacore/shared-help-mobile` | React Native help components | +| `@manacore/shared-theme-ui` | Theme editor and community gallery | + +--- + +## Files Changed by Category + +| Category | Count | Notable Changes | +|----------|-------|-----------------| +| Contacts App | ~40 | Duplicates, batch ops, network, favorites | +| Todo App | ~30 | Kanban, statistics, settings, PWA | +| Calendar App | ~25 | Event tags, network graph | +| Shared UI | ~30 | NetworkGraph, skeleton loaders, tags | +| Shared Theme | ~15 | Custom themes store, editor | +| Shared Help | ~35 | Content, UI, types, mobile | +| mana-core-auth | ~15 | Tags API, themes API | +| Archived Apps | ~100 | Documentation cleanup | + +--- + +## Recommendations + +### 1. Split Future PRs + +Consider creating separate PRs for major features: +- Network Graph feature +- Central Tags API +- Custom Themes System +- Help System packages + +### 2. Add i18n + +Replace hardcoded German strings in shared components. + +### 3. Add Tests + +At minimum, add unit tests for: +- `TagsService` (create, update, delete, defaults) +- `ThemesService` (publish, download, rate) +- `NetworkGraph` (props, events, accessibility) + +### 4. Database Migration Plan + +Ensure `tags` and `themes` table migrations are coordinated across environments. + +### 5. Documentation Updates + +The CLAUDE.md files are helpful. Ensure README updates for new packages. + +--- + +## Rating Summary + +| Aspect | Rating | Notes | +|--------|--------|-------| +| Code Quality | ⭐⭐⭐⭐ | Clean, well-structured code | +| Architecture | ⭐⭐⭐⭐⭐ | Excellent use of shared packages | +| Test Coverage | ⭐⭐ | Missing tests for new features | +| PR Size | ⭐⭐ | Too large for single review | +| Security | ⭐⭐⭐⭐ | Good authorization patterns | +| i18n | ⭐⭐⭐ | Some hardcoded German strings | + +--- + +## Conclusion + +This is solid, well-architected code with good separation of concerns. The main concerns are: + +1. **PR size** - Makes review difficult +2. **Missing tests** - New features lack test coverage +3. **Hardcoded strings** - Some German text in shared components + +Consider splitting future releases of this scale into smaller, focused PRs. + +--- + +## Test Plan Checklist + +From the PR description: + +- [ ] Verify network graph loads correctly in Contacts, Calendar, Todo +- [ ] Test theme editor and community themes page +- [ ] Check Todo app new features (kanban, statistics, settings) +- [ ] Verify contacts duplicate detection and batch operations +- [ ] Test skeleton loaders appear during loading states + +--- + +*Review by Claude Code - 2025-12-10* From d8f1bbbbcea1aed5cd0f5c987fd75db74b0308d8 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:11:17 +0100 Subject: [PATCH 03/26] fix(todo-backend): implement recurrence handling and fix N+1 query MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implement recurring task handling using rrule library - createNextOccurrence() creates next task instance when completing recurring task - calculateNextStartDate() maintains offset between start/due dates - Copies labels, subtasks (reset), and metadata to new occurrence - Respects recurrenceEndDate limit - Fix N+1 query problem for task labels - Replace individual loadTaskLabels() calls with batch loadTaskLabelsBatch() - Reduces database queries from O(2n) to O(2) for task lists - Uses Maps for O(1) lookups when combining tasks with labels - Add Jest test coverage (25 tests) - CRUD operations, task status changes, recurrence - Special queries (inbox, today, completed) - Batch loading efficiency verification đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/todo/apps/backend/jest.config.js | 16 + apps/todo/apps/backend/package.json | 10 +- .../src/task/__tests__/task.service.spec.ts | 480 +++++++++++++ .../apps/backend/src/task/task.service.ts | 224 +++++- pnpm-lock.yaml | 641 ++++++++++++------ 5 files changed, 1123 insertions(+), 248 deletions(-) create mode 100644 apps/todo/apps/backend/jest.config.js create mode 100644 apps/todo/apps/backend/src/task/__tests__/task.service.spec.ts diff --git a/apps/todo/apps/backend/jest.config.js b/apps/todo/apps/backend/jest.config.js new file mode 100644 index 000000000..dcb95fd63 --- /dev/null +++ b/apps/todo/apps/backend/jest.config.js @@ -0,0 +1,16 @@ +/** @type {import('jest').Config} */ +module.exports = { + moduleFileExtensions: ['js', 'json', 'ts'], + rootDir: 'src', + testRegex: '.*\\.spec\\.ts$', + transform: { + '^.+\\.(t|j)s$': 'ts-jest', + }, + collectCoverageFrom: ['**/*.(t|j)s'], + coverageDirectory: '../coverage', + testEnvironment: 'node', + moduleNameMapper: { + '^@todo/shared$': '/../../packages/shared/src', + '^@manacore/shared-nestjs-auth$': '/../../../../../packages/shared-nestjs-auth/src', + }, +}; diff --git a/apps/todo/apps/backend/package.json b/apps/todo/apps/backend/package.json index 59bdc6dcd..fdbcad75d 100644 --- a/apps/todo/apps/backend/package.json +++ b/apps/todo/apps/backend/package.json @@ -9,33 +9,41 @@ "start": "nest start", "start:prod": "node dist/main", "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", + "test": "jest", + "test:watch": "jest --watch", + "test:cov": "jest --coverage", "db:push": "drizzle-kit push", "db:studio": "drizzle-kit studio", "db:seed": "tsx src/db/seed.ts", "db:generate": "drizzle-kit generate" }, "dependencies": { - "@todo/shared": "workspace:*", "@manacore/shared-nestjs-auth": "workspace:*", "@nestjs/common": "^10.4.9", "@nestjs/config": "^3.3.0", "@nestjs/core": "^10.4.9", "@nestjs/platform-express": "^10.4.9", "@nestjs/schedule": "^4.1.2", + "@todo/shared": "workspace:*", "class-transformer": "^0.5.1", "class-validator": "^0.14.1", "dotenv": "^16.4.7", "drizzle-orm": "^0.38.3", "postgres": "^3.4.5", "reflect-metadata": "^0.2.2", + "rrule": "^2.8.1", "rxjs": "^7.8.1" }, "devDependencies": { "@nestjs/cli": "^10.4.9", "@nestjs/schematics": "^10.2.3", + "@nestjs/testing": "^11.1.9", "@types/express": "^5.0.1", + "@types/jest": "^30.0.0", "@types/node": "^22.15.21", "drizzle-kit": "^0.30.2", + "jest": "^30.2.0", + "ts-jest": "^29.2.5", "tsx": "^4.19.4", "typescript": "^5.9.3" } diff --git a/apps/todo/apps/backend/src/task/__tests__/task.service.spec.ts b/apps/todo/apps/backend/src/task/__tests__/task.service.spec.ts new file mode 100644 index 000000000..3b5ca9a5b --- /dev/null +++ b/apps/todo/apps/backend/src/task/__tests__/task.service.spec.ts @@ -0,0 +1,480 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { NotFoundException } from '@nestjs/common'; +import { TaskService } from '../task.service'; +import { ProjectService } from '../../project/project.service'; +import { DATABASE_CONNECTION } from '../../db/database.module'; + +// Mock database +const mockDb = { + query: { + tasks: { + findMany: jest.fn(), + findFirst: jest.fn(), + }, + taskLabels: { + findMany: jest.fn(), + }, + labels: { + findMany: jest.fn(), + }, + }, + insert: jest.fn().mockReturnThis(), + update: jest.fn().mockReturnThis(), + delete: jest.fn().mockReturnThis(), + values: jest.fn().mockReturnThis(), + set: jest.fn().mockReturnThis(), + where: jest.fn().mockReturnThis(), + returning: jest.fn(), +}; + +// Mock ProjectService +const mockProjectService = { + findByIdOrThrow: jest.fn(), +}; + +describe('TaskService', () => { + let service: TaskService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + TaskService, + { + provide: DATABASE_CONNECTION, + useValue: mockDb, + }, + { + provide: ProjectService, + useValue: mockProjectService, + }, + ], + }).compile(); + + service = module.get(TaskService); + + // Reset all mocks before each test + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('findAll', () => { + it('should return all tasks for a user', async () => { + const userId = 'user-123'; + const mockTasks = [ + { id: 'task-1', title: 'Task 1', userId }, + { id: 'task-2', title: 'Task 2', userId }, + ]; + + mockDb.query.tasks.findMany.mockResolvedValue(mockTasks); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + + const result = await service.findAll(userId); + + expect(result).toHaveLength(2); + expect(result[0].labels).toEqual([]); + expect(result[1].labels).toEqual([]); + }); + + it('should filter by projectId when provided', async () => { + const userId = 'user-123'; + const projectId = 'project-1'; + + mockDb.query.tasks.findMany.mockResolvedValue([]); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + + await service.findAll(userId, { projectId }); + + expect(mockDb.query.tasks.findMany).toHaveBeenCalled(); + }); + + it('should filter by priority when provided', async () => { + const userId = 'user-123'; + + mockDb.query.tasks.findMany.mockResolvedValue([]); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + + await service.findAll(userId, { priority: 'high' }); + + expect(mockDb.query.tasks.findMany).toHaveBeenCalled(); + }); + }); + + describe('findById', () => { + it('should return a task when found', async () => { + const userId = 'user-123'; + const taskId = 'task-1'; + const mockTask = { id: taskId, title: 'Test Task', userId }; + + mockDb.query.tasks.findFirst.mockResolvedValue(mockTask); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + + const result = await service.findById(taskId, userId); + + expect(result).toBeDefined(); + expect(result?.id).toBe(taskId); + expect(result?.labels).toEqual([]); + }); + + it('should return null when task not found', async () => { + mockDb.query.tasks.findFirst.mockResolvedValue(null); + + const result = await service.findById('non-existent', 'user-123'); + + expect(result).toBeNull(); + }); + }); + + describe('findByIdOrThrow', () => { + it('should return a task when found', async () => { + const userId = 'user-123'; + const taskId = 'task-1'; + const mockTask = { id: taskId, title: 'Test Task', userId }; + + mockDb.query.tasks.findFirst.mockResolvedValue(mockTask); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + + const result = await service.findByIdOrThrow(taskId, userId); + + expect(result.id).toBe(taskId); + }); + + it('should throw NotFoundException when task not found', async () => { + mockDb.query.tasks.findFirst.mockResolvedValue(null); + + await expect(service.findByIdOrThrow('non-existent', 'user-123')).rejects.toThrow( + NotFoundException + ); + }); + }); + + describe('create', () => { + it('should create a task with basic fields', async () => { + const userId = 'user-123'; + const dto = { title: 'New Task' }; + const createdTask = { id: 'task-new', title: 'New Task', userId, order: 0 }; + + mockDb.query.tasks.findMany.mockResolvedValue([]); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + mockDb.returning.mockResolvedValue([createdTask]); + + const result = await service.create(userId, dto); + + expect(result.title).toBe('New Task'); + expect(mockDb.insert).toHaveBeenCalled(); + }); + + it('should verify project belongs to user when projectId is provided', async () => { + const userId = 'user-123'; + const projectId = 'project-1'; + const dto = { title: 'New Task', projectId }; + const createdTask = { id: 'task-new', title: 'New Task', userId, projectId, order: 0 }; + + mockProjectService.findByIdOrThrow.mockResolvedValue({ id: projectId, userId }); + mockDb.query.tasks.findMany.mockResolvedValue([]); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + mockDb.returning.mockResolvedValue([createdTask]); + + await service.create(userId, dto); + + expect(mockProjectService.findByIdOrThrow).toHaveBeenCalledWith(projectId, userId); + }); + + it('should calculate order based on existing tasks', async () => { + const userId = 'user-123'; + const dto = { title: 'New Task' }; + const existingTasks = [ + { id: 'task-1', order: 0 }, + { id: 'task-2', order: 1 }, + { id: 'task-3', order: 2 }, + ]; + const createdTask = { id: 'task-new', title: 'New Task', userId, order: 3 }; + + mockDb.query.tasks.findMany.mockResolvedValue(existingTasks); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + mockDb.returning.mockResolvedValue([createdTask]); + + const result = await service.create(userId, dto); + + expect(result.order).toBe(3); + }); + }); + + describe('update', () => { + it('should update a task', async () => { + const userId = 'user-123'; + const taskId = 'task-1'; + const dto = { title: 'Updated Title' }; + const existingTask = { id: taskId, title: 'Original', userId }; + const updatedTask = { id: taskId, title: 'Updated Title', userId }; + + mockDb.query.tasks.findFirst.mockResolvedValue(existingTask); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + mockDb.returning.mockResolvedValue([updatedTask]); + + const result = await service.update(taskId, userId, dto); + + expect(result.title).toBe('Updated Title'); + }); + + it('should throw when task does not exist', async () => { + mockDb.query.tasks.findFirst.mockResolvedValue(null); + + await expect(service.update('non-existent', 'user-123', { title: 'Test' })).rejects.toThrow( + NotFoundException + ); + }); + }); + + describe('delete', () => { + it('should delete a task', async () => { + const userId = 'user-123'; + const taskId = 'task-1'; + const existingTask = { id: taskId, userId }; + + mockDb.query.tasks.findFirst.mockResolvedValue(existingTask); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + + await service.delete(taskId, userId); + + expect(mockDb.delete).toHaveBeenCalled(); + }); + + it('should throw when task does not exist', async () => { + mockDb.query.tasks.findFirst.mockResolvedValue(null); + + await expect(service.delete('non-existent', 'user-123')).rejects.toThrow(NotFoundException); + }); + }); + + describe('complete', () => { + it('should mark a task as completed', async () => { + const userId = 'user-123'; + const taskId = 'task-1'; + const existingTask = { id: taskId, title: 'Test', userId, recurrenceRule: null }; + const completedTask = { ...existingTask, isCompleted: true, status: 'completed' }; + + mockDb.query.tasks.findFirst.mockResolvedValue(existingTask); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + mockDb.returning.mockResolvedValue([completedTask]); + + const result = await service.complete(taskId, userId); + + expect(result.isCompleted).toBe(true); + expect(result.status).toBe('completed'); + }); + + it('should create next occurrence for recurring task', async () => { + const userId = 'user-123'; + const taskId = 'task-1'; + const tomorrow = new Date(); + tomorrow.setDate(tomorrow.getDate() + 1); + + const existingTask = { + id: taskId, + title: 'Daily Task', + userId, + recurrenceRule: 'FREQ=DAILY', + dueDate: new Date(), + labels: [], + }; + + const completedTask = { + ...existingTask, + isCompleted: true, + status: 'completed', + completedAt: new Date(), + lastOccurrence: new Date(), + }; + + const newTask = { + id: 'task-new', + title: 'Daily Task', + userId, + recurrenceRule: 'FREQ=DAILY', + dueDate: tomorrow, + isCompleted: false, + status: 'pending', + }; + + // First call for findByIdOrThrow + mockDb.query.tasks.findFirst.mockResolvedValue(existingTask); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + + // For completing the task + mockDb.returning + .mockResolvedValueOnce([newTask]) // For creating new occurrence + .mockResolvedValueOnce([completedTask]); // For completing original + + const result = await service.complete(taskId, userId); + + expect(result.isCompleted).toBe(true); + // Verify that a new task was created + expect(mockDb.insert).toHaveBeenCalled(); + }); + }); + + describe('uncomplete', () => { + it('should mark a task as not completed', async () => { + const userId = 'user-123'; + const taskId = 'task-1'; + const existingTask = { id: taskId, title: 'Test', userId, isCompleted: true }; + const uncompletedTask = { ...existingTask, isCompleted: false, status: 'pending' }; + + mockDb.query.tasks.findFirst.mockResolvedValue(existingTask); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + mockDb.returning.mockResolvedValue([uncompletedTask]); + + const result = await service.uncomplete(taskId, userId); + + expect(result.isCompleted).toBe(false); + expect(result.status).toBe('pending'); + }); + }); + + describe('move', () => { + it('should move a task to a different project', async () => { + const userId = 'user-123'; + const taskId = 'task-1'; + const newProjectId = 'project-2'; + const existingTask = { id: taskId, title: 'Test', userId, projectId: 'project-1' }; + const movedTask = { ...existingTask, projectId: newProjectId }; + + mockProjectService.findByIdOrThrow.mockResolvedValue({ id: newProjectId, userId }); + mockDb.query.tasks.findFirst.mockResolvedValue(existingTask); + mockDb.query.tasks.findMany.mockResolvedValue([]); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + mockDb.returning.mockResolvedValue([movedTask]); + + const result = await service.move(taskId, userId, newProjectId); + + expect(result.projectId).toBe(newProjectId); + expect(mockProjectService.findByIdOrThrow).toHaveBeenCalledWith(newProjectId, userId); + }); + + it('should move a task to inbox (null project)', async () => { + const userId = 'user-123'; + const taskId = 'task-1'; + const existingTask = { id: taskId, title: 'Test', userId, projectId: 'project-1' }; + const movedTask = { ...existingTask, projectId: null }; + + mockDb.query.tasks.findFirst.mockResolvedValue(existingTask); + mockDb.query.tasks.findMany.mockResolvedValue([]); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + mockDb.returning.mockResolvedValue([movedTask]); + + const result = await service.move(taskId, userId, null); + + expect(result.projectId).toBeNull(); + expect(mockProjectService.findByIdOrThrow).not.toHaveBeenCalled(); + }); + }); + + describe('getInboxTasks', () => { + it('should return incomplete tasks', async () => { + const userId = 'user-123'; + const mockTasks = [ + { id: 'task-1', title: 'Task 1', userId, isCompleted: false }, + { id: 'task-2', title: 'Task 2', userId, isCompleted: false }, + ]; + + mockDb.query.tasks.findMany.mockResolvedValue(mockTasks); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + + const result = await service.getInboxTasks(userId); + + expect(result).toHaveLength(2); + expect(result.every((t) => t.isCompleted === false)).toBe(true); + }); + }); + + describe('getTodayTasks', () => { + it('should return tasks due today', async () => { + const userId = 'user-123'; + const today = new Date(); + const mockTasks = [{ id: 'task-1', title: 'Today Task', userId, dueDate: today }]; + + mockDb.query.tasks.findMany.mockResolvedValue(mockTasks); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + + const result = await service.getTodayTasks(userId); + + expect(result).toHaveLength(1); + }); + }); + + describe('getCompletedTasks', () => { + it('should return completed tasks with default limit', async () => { + const userId = 'user-123'; + const mockTasks = Array(50) + .fill(null) + .map((_, i) => ({ + id: `task-${i}`, + title: `Task ${i}`, + userId, + isCompleted: true, + })); + + mockDb.query.tasks.findMany.mockResolvedValue(mockTasks); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + + const result = await service.getCompletedTasks(userId); + + expect(result).toHaveLength(50); + }); + + it('should respect custom limit', async () => { + const userId = 'user-123'; + const mockTasks = Array(10) + .fill(null) + .map((_, i) => ({ + id: `task-${i}`, + title: `Task ${i}`, + userId, + isCompleted: true, + })); + + mockDb.query.tasks.findMany.mockResolvedValue(mockTasks); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + + const result = await service.getCompletedTasks(userId, 10); + + expect(result).toHaveLength(10); + }); + }); + + describe('loadTaskLabelsBatch', () => { + it('should batch load labels for multiple tasks', async () => { + const userId = 'user-123'; + const mockTasks = [ + { id: 'task-1', title: 'Task 1', userId }, + { id: 'task-2', title: 'Task 2', userId }, + ]; + + const mockTaskLabels = [ + { taskId: 'task-1', labelId: 'label-1' }, + { taskId: 'task-1', labelId: 'label-2' }, + { taskId: 'task-2', labelId: 'label-1' }, + ]; + + const mockLabels = [ + { id: 'label-1', name: 'Important', color: '#ff0000' }, + { id: 'label-2', name: 'Work', color: '#0000ff' }, + ]; + + mockDb.query.tasks.findMany.mockResolvedValue(mockTasks); + mockDb.query.taskLabels.findMany.mockResolvedValue(mockTaskLabels); + mockDb.query.labels.findMany.mockResolvedValue(mockLabels); + + const result = await service.findAll(userId); + + expect(result[0].labels).toHaveLength(2); + expect(result[1].labels).toHaveLength(1); + // Should only make 2 queries for labels (taskLabels + labels), not N+1 + expect(mockDb.query.taskLabels.findMany).toHaveBeenCalledTimes(1); + expect(mockDb.query.labels.findMany).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/apps/todo/apps/backend/src/task/task.service.ts b/apps/todo/apps/backend/src/task/task.service.ts index 4f394bf6d..5dfc7d825 100644 --- a/apps/todo/apps/backend/src/task/task.service.ts +++ b/apps/todo/apps/backend/src/task/task.service.ts @@ -1,11 +1,15 @@ import { Injectable, Inject, NotFoundException } from '@nestjs/common'; import { eq, and, or, gte, lte, ilike, asc, desc, isNull, SQL } from 'drizzle-orm'; +import { RRule, RRuleSet, rrulestr } from 'rrule'; import { DATABASE_CONNECTION } from '../db/database.module'; import { type Database } from '../db/connection'; import { tasks, taskLabels, labels, type Task, type NewTask, type Subtask } from '../db/schema'; import { ProjectService } from '../project/project.service'; import { CreateTaskDto, UpdateTaskDto, QueryTasksDto } from './dto'; +// Extended Task type that includes labels (populated after loading from DB) +type TaskWithLabels = Task & { labels: (typeof labels.$inferSelect)[] }; + @Injectable() export class TaskService { constructor( @@ -13,7 +17,7 @@ export class TaskService { private projectService: ProjectService ) {} - async findAll(userId: string, query: QueryTasksDto = {}): Promise { + async findAll(userId: string, query: QueryTasksDto = {}): Promise { const conditions: SQL[] = [eq(tasks.userId, userId)]; if (query.projectId) { @@ -73,11 +77,11 @@ export class TaskService { offset: query.offset, }); - // Load labels for each task - return Promise.all(result.map((task) => this.loadTaskLabels(task))); + // Batch load labels for all tasks (2 queries instead of N+1) + return this.loadTaskLabelsBatch(result); } - async findById(id: string, userId: string): Promise { + async findById(id: string, userId: string): Promise { const result = await this.db.query.tasks.findFirst({ where: and(eq(tasks.id, id), eq(tasks.userId, userId)), }); @@ -86,7 +90,7 @@ export class TaskService { return this.loadTaskLabels(result); } - async findByIdOrThrow(id: string, userId: string): Promise { + async findByIdOrThrow(id: string, userId: string): Promise { const task = await this.findById(id, userId); if (!task) { throw new NotFoundException(`Task with id ${id} not found`); @@ -94,7 +98,7 @@ export class TaskService { return task; } - async create(userId: string, dto: CreateTaskDto): Promise { + async create(userId: string, dto: CreateTaskDto): Promise { // Verify project belongs to user if provided if (dto.projectId) { await this.projectService.findByIdOrThrow(dto.projectId, userId); @@ -139,7 +143,7 @@ export class TaskService { return this.loadTaskLabels(created); } - async update(id: string, userId: string, dto: UpdateTaskDto): Promise { + async update(id: string, userId: string, dto: UpdateTaskDto): Promise { await this.findByIdOrThrow(id, userId); // Verify project belongs to user if changing project @@ -185,13 +189,28 @@ export class TaskService { await this.db.delete(tasks).where(and(eq(tasks.id, id), eq(tasks.userId, userId))); } - async complete(id: string, userId: string): Promise { + async complete(id: string, userId: string): Promise { const task = await this.findByIdOrThrow(id, userId); // If task has recurrence, create next occurrence instead of completing if (task.recurrenceRule) { - // TODO: Implement recurrence handling - // For now, just mark as complete + const nextOccurrence = await this.createNextOccurrence(task, userId); + if (nextOccurrence) { + // Mark current task as completed and update lastOccurrence + const [completed] = await this.db + .update(tasks) + .set({ + isCompleted: true, + status: 'completed', + completedAt: new Date(), + lastOccurrence: new Date(), + updatedAt: new Date(), + }) + .where(and(eq(tasks.id, id), eq(tasks.userId, userId))) + .returning(); + + return this.loadTaskLabels(completed); + } } return this.update(id, userId, { @@ -200,14 +219,113 @@ export class TaskService { }); } - async uncomplete(id: string, userId: string): Promise { + /** + * Creates the next occurrence of a recurring task based on its RRULE. + * Returns the newly created task, or null if no more occurrences should be created. + */ + private async createNextOccurrence( + task: TaskWithLabels, + userId: string + ): Promise { + if (!task.recurrenceRule) return null; + + try { + // Parse the RRULE string + const rule = rrulestr(task.recurrenceRule); + const now = new Date(); + + // Get the next occurrence after now + const nextDate = rule.after(now, false); + + // Check if we've exceeded the recurrence end date + if (task.recurrenceEndDate) { + const endDate = new Date(task.recurrenceEndDate); + if (!nextDate || nextDate > endDate) { + return null; // No more occurrences + } + } + + if (!nextDate) { + return null; // No more occurrences according to RRULE + } + + // Reset subtasks (mark all as incomplete) + const resetSubtasks: Subtask[] | undefined = task.subtasks?.map((s) => ({ + ...s, + isCompleted: false, + completedAt: null, + })); + + // Create new task for the next occurrence + const newTask: NewTask = { + userId, + projectId: task.projectId, + parentTaskId: task.parentTaskId, + title: task.title, + description: task.description, + dueDate: nextDate, + dueTime: task.dueTime, + startDate: task.startDate + ? this.calculateNextStartDate(task.startDate, task.dueDate, nextDate) + : null, + priority: task.priority ?? 'medium', + status: 'pending', + isCompleted: false, + recurrenceRule: task.recurrenceRule, + recurrenceEndDate: task.recurrenceEndDate, + subtasks: resetSubtasks, + metadata: task.metadata, + order: task.order, + columnId: task.columnId, + columnOrder: task.columnOrder, + }; + + const [created] = await this.db.insert(tasks).values(newTask).returning(); + + // Copy labels from original task + if (task.labels && task.labels.length > 0) { + await this.db.insert(taskLabels).values( + task.labels.map((label) => ({ + taskId: created.id, + labelId: label.id, + })) + ); + } + + return this.loadTaskLabels(created); + } catch (error) { + // If RRULE parsing fails, log and return null + console.error('Failed to parse recurrence rule:', error); + return null; + } + } + + /** + * Calculates the new start date based on the offset between original start and due dates. + */ + private calculateNextStartDate( + originalStartDate: Date | string | null, + originalDueDate: Date | string | null, + nextDueDate: Date + ): Date | null { + if (!originalStartDate || !originalDueDate) return null; + + const start = new Date(originalStartDate); + const due = new Date(originalDueDate); + const diffMs = due.getTime() - start.getTime(); + + // New start date maintains the same offset from the new due date + return new Date(nextDueDate.getTime() - diffMs); + } + + async uncomplete(id: string, userId: string): Promise { return this.update(id, userId, { isCompleted: false, status: 'pending', }); } - async move(id: string, userId: string, projectId: string | null): Promise { + async move(id: string, userId: string, projectId: string | null): Promise { // Verify new project if provided if (projectId) { await this.projectService.findByIdOrThrow(projectId, userId); @@ -247,11 +365,11 @@ export class TaskService { } } - async getInboxTasks(userId: string): Promise { + async getInboxTasks(userId: string): Promise { return this.findAll(userId, { isCompleted: false }); } - async getTodayTasks(userId: string): Promise { + async getTodayTasks(userId: string): Promise { const today = new Date(); today.setHours(0, 0, 0, 0); const tomorrow = new Date(today); @@ -270,10 +388,10 @@ export class TaskService { orderBy: [asc(tasks.dueDate), asc(tasks.order)], }); - return Promise.all(result.map((task) => this.loadTaskLabels(task))); + return this.loadTaskLabelsBatch(result); } - async getUpcomingTasks(userId: string, days: number = 7): Promise { + async getUpcomingTasks(userId: string, days: number = 7): Promise { const today = new Date(); today.setHours(0, 0, 0, 0); const endDate = new Date(today); @@ -289,20 +407,24 @@ export class TaskService { orderBy: [asc(tasks.dueDate), asc(tasks.order)], }); - return Promise.all(result.map((task) => this.loadTaskLabels(task))); + return this.loadTaskLabelsBatch(result); } - async getCompletedTasks(userId: string, limit: number = 50): Promise { + async getCompletedTasks(userId: string, limit: number = 50): Promise { const result = await this.db.query.tasks.findMany({ where: and(eq(tasks.userId, userId), eq(tasks.isCompleted, true)), orderBy: [desc(tasks.completedAt)], limit, }); - return Promise.all(result.map((task) => this.loadTaskLabels(task))); + return this.loadTaskLabelsBatch(result); } - async reorder(userId: string, taskIds: string[], projectId?: string | null): Promise { + async reorder( + userId: string, + taskIds: string[], + projectId?: string | null + ): Promise { // Update order for each task const updates = taskIds.map((id, index) => this.db @@ -316,22 +438,66 @@ export class TaskService { return this.findAll(userId, { projectId: projectId ?? undefined }); } + /** + * Loads labels for a single task (used for single task operations). + * For multiple tasks, use loadTaskLabelsBatch instead. + */ private async loadTaskLabels( task: Task ): Promise { - const taskLabelRows = await this.db.query.taskLabels.findMany({ - where: eq(taskLabels.taskId, task.id), - }); + const [result] = await this.loadTaskLabelsBatch([task]); + return result; + } - if (taskLabelRows.length === 0) { - return { ...task, labels: [] }; + /** + * Batch loads labels for multiple tasks in just 2 queries (instead of N+1). + * This significantly improves performance when loading task lists. + */ + private async loadTaskLabelsBatch( + taskList: Task[] + ): Promise<(Task & { labels: (typeof labels.$inferSelect)[] })[]> { + if (taskList.length === 0) { + return []; } - const labelIds = taskLabelRows.map((tl) => tl.labelId); - const taskLabelsData = await this.db.query.labels.findMany({ - where: or(...labelIds.map((id) => eq(labels.id, id))), + const taskIds = taskList.map((t) => t.id); + + // Single query to get all task-label relationships + const allTaskLabels = await this.db.query.taskLabels.findMany({ + where: or(...taskIds.map((id) => eq(taskLabels.taskId, id))), }); - return { ...task, labels: taskLabelsData }; + if (allTaskLabels.length === 0) { + // No labels for any task - return tasks with empty labels array + return taskList.map((task) => ({ ...task, labels: [] })); + } + + // Get unique label IDs + const uniqueLabelIds = [...new Set(allTaskLabels.map((tl) => tl.labelId))]; + + // Single query to get all labels + const allLabels = await this.db.query.labels.findMany({ + where: or(...uniqueLabelIds.map((id) => eq(labels.id, id))), + }); + + // Create a map of labelId -> label for fast lookup + const labelMap = new Map(allLabels.map((l) => [l.id, l])); + + // Create a map of taskId -> labelIds for fast lookup + const taskLabelMap = new Map(); + for (const tl of allTaskLabels) { + const existing = taskLabelMap.get(tl.taskId) || []; + existing.push(tl.labelId); + taskLabelMap.set(tl.taskId, existing); + } + + // Combine tasks with their labels + return taskList.map((task) => { + const labelIds = taskLabelMap.get(task.id) || []; + const taskLabelsData = labelIds + .map((id) => labelMap.get(id)) + .filter((l): l is typeof labels.$inferSelect => l !== undefined); + return { ...task, labels: taskLabelsData }; + }); } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1d7dc9fca..41309c052 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1377,7 +1377,7 @@ importers: version: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-router: specifier: ~6.0.15 - version: 6.0.15(dux2nvtiztnejw7mxzfaajqvh4) + version: 6.0.15(qtdvwowq57almfvuwkjsocja4a) expo-status-bar: specifier: ~3.0.8 version: 3.0.8(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -1792,7 +1792,7 @@ importers: version: 8.0.9(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-router: specifier: ~6.0.10 - version: 6.0.15(qjp3usx4acoq47dkosl6pmu254) + version: 6.0.15(mhyeodk7fqgr5mbfi6fab2y3iy) expo-secure-store: specifier: ^15.0.7 version: 15.0.7(expo@54.0.13) @@ -2238,7 +2238,7 @@ importers: version: 18.2.0(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) expo-router: specifier: ~6.0.10 - version: 6.0.15(ucgv42olhsnvykdrhhfuls4dzq) + version: 6.0.15(dhhg3hnzbvo62ldg5nj2klgrdy) expo-secure-store: specifier: ~15.0.7 version: 15.0.7(expo@54.0.12) @@ -2567,25 +2567,40 @@ importers: reflect-metadata: specifier: ^0.2.2 version: 0.2.2 + rrule: + specifier: ^2.8.1 + version: 2.8.1 rxjs: specifier: ^7.8.1 version: 7.8.2 devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9(esbuild@0.27.0) + version: 10.4.9(esbuild@0.19.12) '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) + '@nestjs/testing': + specifier: ^11.1.9 + version: 11.1.9(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/platform-express@10.4.20) '@types/express': specifier: ^5.0.1 version: 5.0.5 + '@types/jest': + specifier: ^30.0.0 + version: 30.0.0 '@types/node': specifier: ^22.15.21 version: 22.19.1 drizzle-kit: specifier: ^0.30.2 version: 0.30.6 + jest: + specifier: ^30.2.0 + version: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + ts-jest: + specifier: ^29.2.5 + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) tsx: specifier: ^4.19.4 version: 4.20.6 @@ -2888,7 +2903,7 @@ importers: version: 17.0.7(expo@54.0.25)(react@19.1.0) expo-router: specifier: ~6.0.8 - version: 6.0.15(5e7ih2rh6mb55wruwvjljgzihq) + version: 6.0.15(skjv6vjhiyv7rnm556zyu7suye) expo-sharing: specifier: ~14.0.7 version: 14.0.7(expo@54.0.25) @@ -2958,7 +2973,7 @@ importers: version: 5.4.3(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) '@testing-library/react-native': specifier: ^13.3.3 - version: 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + version: 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) '@types/jest': specifier: ^30.0.0 version: 30.0.0 @@ -2985,10 +3000,10 @@ importers: version: 4.0.3 jest: specifier: ^30.2.0 - version: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)) + version: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) jest-expo: specifier: ^54.0.12 - version: 54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) + version: 54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) prettier: specifier: ^3.2.5 version: 3.6.2 @@ -17508,6 +17523,9 @@ packages: resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} engines: {node: '>= 18'} + rrule@2.8.1: + resolution: {integrity: sha512-hM3dHSBMeaJ0Ktp7W38BJZ7O1zOgaFEsn41PDk+yHoEtfLV+PoJt9E9xAlZiWgf/iqEqionN0ebHFZIDAp+iGw==} + rtl-detect@1.1.2: resolution: {integrity: sha512-PGMBq03+TTG/p/cRB7HCLKJ1MgDIi07+QU1faSjiYRfmY5UsAttV9Hs08jDAHVwcOwmVLcSJkpwyfXszVjWfIQ==} @@ -22253,7 +22271,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(ucgv42olhsnvykdrhhfuls4dzq) + expo-router: 6.0.15(dhhg3hnzbvo62ldg5nj2klgrdy) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -22330,7 +22348,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(qjp3usx4acoq47dkosl6pmu254) + expo-router: 6.0.15(mhyeodk7fqgr5mbfi6fab2y3iy) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -22407,7 +22425,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(jiucxy5ca3jdtbnulaxuc46jdq) + expo-router: 6.0.15(yfeeu53gqw3nsmwmcr4www7tle) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -22484,7 +22502,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(hwqworfppxvioilmgvd7t3oifm) + expo-router: 6.0.15(7opviov3h7cq44cxi7hciczi2i) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -23927,9 +23945,8 @@ snapshots: - esbuild-register - supports-color - ts-node - optional: true - '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))': + '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': dependencies: '@jest/console': 30.2.0 '@jest/pattern': 30.0.1 @@ -23944,7 +23961,44 @@ snapshots: exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)) + jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + jest-haste-map: 30.2.0 + jest-message-util: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-resolve-dependencies: 30.2.0 + jest-runner: 30.2.0 + jest-runtime: 30.2.0 + jest-snapshot: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + jest-watcher: 30.2.0 + micromatch: 4.0.8 + pretty-format: 30.2.0 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + optional: true + + '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': + dependencies: + '@jest/console': 30.2.0 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 22.19.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.3.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.2.0 + jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) jest-haste-map: 30.2.0 jest-message-util: 30.2.0 jest-regex-util: 30.0.1 @@ -23965,7 +24019,7 @@ snapshots: - supports-color - ts-node - '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': + '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3))': dependencies: '@jest/console': 30.2.0 '@jest/pattern': 30.0.1 @@ -23980,7 +24034,44 @@ snapshots: exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) + jest-haste-map: 30.2.0 + jest-message-util: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-resolve-dependencies: 30.2.0 + jest-runner: 30.2.0 + jest-runtime: 30.2.0 + jest-snapshot: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + jest-watcher: 30.2.0 + micromatch: 4.0.8 + pretty-format: 30.2.0 + slash: 3.0.0 + transitivePeerDependencies: + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + optional: true + + '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))': + dependencies: + '@jest/console': 30.2.0 + '@jest/pattern': 30.0.1 + '@jest/reporters': 30.2.0 + '@jest/test-result': 30.2.0 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + '@types/node': 22.19.1 + ansi-escapes: 4.3.2 + chalk: 4.1.2 + ci-info: 4.3.1 + exit-x: 0.2.2 + graceful-fs: 4.2.11 + jest-changed-files: 30.2.0 + jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) jest-haste-map: 30.2.0 jest-message-util: 30.2.0 jest-regex-util: 30.0.1 @@ -24639,6 +24730,14 @@ snapshots: optionalDependencies: '@nestjs/platform-express': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + '@nestjs/testing@11.1.9(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20)(@nestjs/platform-express@10.4.20)': + dependencies: + '@nestjs/common': 10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) + '@nestjs/core': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/platform-express@10.4.20)(@nestjs/websockets@10.4.20)(reflect-metadata@0.2.2)(rxjs@7.8.2) + tslib: 2.8.1 + optionalDependencies: + '@nestjs/platform-express': 10.4.20(@nestjs/common@10.4.20(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@10.4.20) + '@nestjs/testing@11.1.9(@nestjs/common@11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.9)(@nestjs/platform-express@11.1.9)': dependencies: '@nestjs/common': 11.1.9(class-transformer@0.5.1)(class-validator@0.14.3)(reflect-metadata@0.2.2)(rxjs@7.8.2) @@ -27274,7 +27373,7 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 - '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 @@ -27284,7 +27383,7 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 optionalDependencies: - jest: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) + jest: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) optional: true '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': @@ -27300,7 +27399,7 @@ snapshots: jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) optional: true - '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 @@ -27310,22 +27409,10 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 optionalDependencies: - jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)) - - '@testing-library/react-native@13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - jest-matcher-utils: 30.2.0 - picocolors: 1.1.1 - pretty-format: 30.2.0 - react: 19.1.0 - react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) - react-test-renderer: 19.1.0(react@19.1.0) - redent: 3.0.0 - optionalDependencies: - jest: 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) optional: true - '@testing-library/react-native@13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react-test-renderer@19.1.0(react@18.3.1))(react@18.3.1)': + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react-test-renderer@19.1.0(react@18.3.1))(react@18.3.1)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 @@ -27335,10 +27422,22 @@ snapshots: react-test-renderer: 19.1.0(react@18.3.1) redent: 3.0.0 optionalDependencies: - jest: 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) optional: true - '@testing-library/react-native@13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + jest-matcher-utils: 30.2.0 + picocolors: 1.1.1 + pretty-format: 30.2.0 + react: 19.1.0 + react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-test-renderer: 19.1.0(react@19.1.0) + redent: 3.0.0 + optionalDependencies: + jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 @@ -27348,7 +27447,7 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 optionalDependencies: - jest: 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) optional: true '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': @@ -33122,99 +33221,7 @@ snapshots: - react-native - supports-color - expo-router@6.0.15(5e7ih2rh6mb55wruwvjljgzihq): - dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@expo/schema-utils': 0.1.7 - '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) - '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native': 7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - client-only: 0.0.1 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-server: 1.0.4 - fast-deep-equal: 3.1.3 - invariant: 2.2.4 - nanoid: 3.3.11 - query-string: 7.1.3 - react: 19.1.0 - react-fast-compare: 3.2.2 - react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) - react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-safe-area-context: 5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-screens: 4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - semver: 7.6.3 - server-only: 0.0.1 - sf-symbols-typescript: 2.1.0 - shallowequal: 1.1.0 - use-latest-callback: 0.2.6(react@19.1.0) - vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - optionalDependencies: - '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.0(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) - react-dom: 19.1.0(react@19.1.0) - react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-reanimated: 4.1.0(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) - transitivePeerDependencies: - - '@react-native-masked-view/masked-view' - - '@types/react' - - '@types/react-dom' - - supports-color - - expo-router@6.0.15(dux2nvtiztnejw7mxzfaajqvh4): - dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@expo/schema-utils': 0.1.7 - '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) - '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - client-only: 0.0.1 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-server: 1.0.4 - fast-deep-equal: 3.1.3 - invariant: 2.2.4 - nanoid: 3.3.11 - query-string: 7.1.3 - react: 19.1.0 - react-fast-compare: 3.2.2 - react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) - react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - semver: 7.6.3 - server-only: 0.0.1 - sf-symbols-typescript: 2.1.0 - shallowequal: 1.1.0 - use-latest-callback: 0.2.6(react@19.1.0) - vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - optionalDependencies: - '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) - react-dom: 19.1.0(react@19.1.0) - react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) - transitivePeerDependencies: - - '@react-native-masked-view/masked-view' - - '@types/react' - - '@types/react-dom' - - supports-color - - expo-router@6.0.15(hwqworfppxvioilmgvd7t3oifm): + expo-router@6.0.15(7opviov3h7cq44cxi7hciczi2i): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) '@expo/schema-utils': 0.1.7 @@ -33248,7 +33255,7 @@ snapshots: vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.1.0(react@18.3.1))(react@18.3.1) optionalDependencies: '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) - '@testing-library/react-native': 13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react-test-renderer@19.1.0(react@18.3.1))(react@18.3.1) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react-test-renderer@19.1.0(react@18.3.1))(react@18.3.1) react-dom: 19.1.0(react@18.3.1) react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) @@ -33261,9 +33268,9 @@ snapshots: - supports-color optional: true - expo-router@6.0.15(jiucxy5ca3jdtbnulaxuc46jdq): + expo-router@6.0.15(dhhg3hnzbvo62ldg5nj2klgrdy): dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/metro-runtime': 6.1.2(expo@54.0.12)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -33273,9 +33280,9 @@ snapshots: client-only: 0.0.1 debug: 4.4.3 escape-string-regexp: 4.0.0 - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo: 54.0.12(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-server: 1.0.4 fast-deep-equal: 3.1.3 invariant: 2.2.4 @@ -33295,7 +33302,7 @@ snapshots: vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -33306,7 +33313,52 @@ snapshots: - '@types/react' - '@types/react-dom' - supports-color - optional: true + + expo-router@6.0.15(mhyeodk7fqgr5mbfi6fab2y3iy): + dependencies: + '@expo/metro-runtime': 6.1.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/schema-utils': 0.1.7 + '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + client-only: 0.0.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 54.0.13(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-server: 1.0.4 + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.11 + query-string: 7.1.3 + react: 19.1.0 + react-fast-compare: 3.2.2 + react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + semver: 7.6.3 + server-only: 0.0.1 + sf-symbols-typescript: 2.1.0 + shallowequal: 1.1.0 + use-latest-callback: 0.2.6(react@19.1.0) + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + optionalDependencies: + '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + react-dom: 19.1.0(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - '@types/react' + - '@types/react-dom' + - supports-color expo-router@6.0.15(nttrd3tw67nnyhowcwgdzipb5e): dependencies: @@ -33355,9 +33407,55 @@ snapshots: - supports-color optional: true - expo-router@6.0.15(qjp3usx4acoq47dkosl6pmu254): + expo-router@6.0.15(qtdvwowq57almfvuwkjsocja4a): dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/schema-utils': 0.1.7 + '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + client-only: 0.0.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-server: 1.0.4 + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.11 + query-string: 7.1.3 + react: 19.1.0 + react-fast-compare: 3.2.2 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + semver: 7.6.3 + server-only: 0.0.1 + sf-symbols-typescript: 2.1.0 + shallowequal: 1.1.0 + use-latest-callback: 0.2.6(react@19.1.0) + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + optionalDependencies: + '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + react-dom: 19.1.0(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - '@types/react' + - '@types/react-dom' + - supports-color + + expo-router@6.0.15(skjv6vjhiyv7rnm556zyu7suye): + dependencies: + '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -33367,9 +33465,9 @@ snapshots: client-only: 0.0.1 debug: 4.4.3 escape-string-regexp: 4.0.0 - expo: 54.0.13(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-constants: 18.0.10(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-linking: 8.0.9(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-server: 1.0.4 fast-deep-equal: 3.1.3 invariant: 2.2.4 @@ -33388,11 +33486,11 @@ snapshots: use-latest-callback: 0.2.6(react@19.1.0) vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: - '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.0(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.0(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) transitivePeerDependencies: @@ -33401,9 +33499,9 @@ snapshots: - '@types/react-dom' - supports-color - expo-router@6.0.15(ucgv42olhsnvykdrhhfuls4dzq): + expo-router@6.0.15(yfeeu53gqw3nsmwmcr4www7tle): dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.12)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -33413,9 +33511,9 @@ snapshots: client-only: 0.0.1 debug: 4.4.3 escape-string-regexp: 4.0.0 - expo: 54.0.12(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-constants: 18.0.10(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-linking: 8.0.9(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-server: 1.0.4 fast-deep-equal: 3.1.3 invariant: 2.2.4 @@ -33435,7 +33533,7 @@ snapshots: vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -33446,6 +33544,7 @@ snapshots: - '@types/react' - '@types/react-dom' - supports-color + optional: true expo-secure-store@15.0.7(expo@54.0.12): dependencies: @@ -35497,15 +35596,15 @@ snapshots: - supports-color - ts-node - jest-cli@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): + jest-cli@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) + jest-config: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -35535,7 +35634,6 @@ snapshots: - esbuild-register - supports-color - ts-node - optional: true jest-cli@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: @@ -35556,34 +35654,15 @@ snapshots: - supports-color - ts-node - jest-cli@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)): + jest-cli@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)) - jest-util: 30.2.0 - jest-validate: 30.2.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - - jest-cli@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)): - dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) - '@jest/test-result': 30.2.0 - '@jest/types': 30.2.0 - chalk: 4.1.2 - exit-x: 0.2.2 - import-local: 3.2.0 - jest-config: 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + jest-config: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -35595,6 +35674,25 @@ snapshots: - ts-node optional: true + jest-cli@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + '@jest/test-result': 30.2.0 + '@jest/types': 30.2.0 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest-util: 30.2.0 + jest-validate: 30.2.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 @@ -35688,7 +35786,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): + jest-config@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -35717,6 +35815,7 @@ snapshots: optionalDependencies: '@types/node': 20.19.25 esbuild-register: 3.6.0(esbuild@0.27.0) + ts-node: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color @@ -35755,9 +35854,8 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros - supports-color - optional: true - jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)): + jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -35786,9 +35884,11 @@ snapshots: optionalDependencies: '@types/node': 22.19.1 esbuild-register: 3.6.0(esbuild@0.27.0) + ts-node: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color + optional: true jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: @@ -35824,7 +35924,76 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)): + jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)): + dependencies: + '@babel/core': 7.28.5 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 4.3.1 + deepmerge: 4.3.1 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-circus: 30.2.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-runner: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.2.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.1 + esbuild-register: 3.6.0(esbuild@0.27.0) + ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.8.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + optional: true + + jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + dependencies: + '@babel/core': 7.28.5 + '@jest/get-type': 30.1.0 + '@jest/pattern': 30.0.1 + '@jest/test-sequencer': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + chalk: 4.1.2 + ci-info: 4.3.1 + deepmerge: 4.3.1 + glob: 10.5.0 + graceful-fs: 4.2.11 + jest-circus: 30.2.0 + jest-docblock: 30.2.0 + jest-environment-node: 30.2.0 + jest-regex-util: 30.0.1 + jest-resolve: 30.2.0 + jest-runner: 30.2.0 + jest-util: 30.2.0 + jest-validate: 30.2.0 + micromatch: 4.0.8 + parse-json: 5.2.0 + pretty-format: 30.2.0 + slash: 3.0.0 + strip-json-comments: 3.1.1 + optionalDependencies: + '@types/node': 22.19.1 + esbuild-register: 3.6.0(esbuild@0.27.0) + ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) + transitivePeerDependencies: + - babel-plugin-macros + - supports-color + + jest-config@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -35853,11 +36022,13 @@ snapshots: optionalDependencies: '@types/node': 24.10.1 esbuild-register: 3.6.0(esbuild@0.27.0) + ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.8.3) transitivePeerDependencies: - babel-plugin-macros - supports-color + optional: true - jest-config@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)): + jest-config@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -35884,11 +36055,12 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: + '@types/node': 24.10.1 esbuild-register: 3.6.0(esbuild@0.27.0) + ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - optional: true jest-diff@29.7.0: dependencies: @@ -35962,7 +36134,7 @@ snapshots: jest-util: 30.2.0 jest-validate: 30.2.0 - jest-expo@54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)): + jest-expo@54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)): dependencies: '@expo/config': 12.0.10 '@expo/json-file': 10.0.7 @@ -35973,7 +36145,7 @@ snapshots: jest-environment-jsdom: 29.7.0 jest-snapshot: 29.7.0 jest-watch-select-projects: 2.0.0 - jest-watch-typeahead: 2.2.1(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))) + jest-watch-typeahead: 2.2.1(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))) json5: 2.2.3 lodash: 4.17.21 react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) @@ -36334,11 +36506,11 @@ snapshots: chalk: 3.0.0 prompts: 2.4.2 - jest-watch-typeahead@2.2.1(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))): + jest-watch-typeahead@2.2.1(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))): dependencies: ansi-escapes: 6.2.1 chalk: 4.1.2 - jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)) + jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) jest-regex-util: 29.6.3 jest-watcher: 29.7.0 slash: 5.1.0 @@ -36412,12 +36584,12 @@ snapshots: - supports-color - ts-node - jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)): + jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0)) + jest-cli: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -36438,7 +36610,6 @@ snapshots: - esbuild-register - supports-color - ts-node - optional: true jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: @@ -36453,25 +36624,12 @@ snapshots: - supports-color - ts-node - jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)): + jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - - jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)): - dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) - '@jest/types': 30.2.0 - import-local: 3.2.0 - jest-cli: 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + jest-cli: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -36480,6 +36638,19 @@ snapshots: - ts-node optional: true + jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + dependencies: + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + '@jest/types': 30.2.0 + import-local: 3.2.0 + jest-cli: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + jimp-compact@0.16.1: {} jiti@1.21.7: {} @@ -40841,6 +41012,10 @@ snapshots: transitivePeerDependencies: - supports-color + rrule@2.8.1: + dependencies: + tslib: 2.8.1 + rtl-detect@1.1.2: {} run-async@2.4.1: {} @@ -41903,6 +42078,27 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + handlebars: 4.7.8 + jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.7.3 + type-fest: 4.41.0 + typescript: 5.9.3 + yargs-parser: 21.1.1 + optionalDependencies: + '@babel/core': 7.28.5 + '@jest/transform': 30.2.0 + '@jest/types': 30.2.0 + babel-jest: 30.2.0(@babel/core@7.28.5) + esbuild: 0.19.12 + jest-util: 30.2.0 + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 @@ -41964,16 +42160,6 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2 - ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)): - dependencies: - chalk: 4.1.2 - enhanced-resolve: 5.18.3 - micromatch: 4.0.8 - semver: 7.7.3 - source-map: 0.7.6 - typescript: 5.9.3 - webpack: 5.97.1(esbuild@0.19.12) - ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -42011,6 +42197,25 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 + ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.12 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 24.10.1 + acorn: 8.15.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.8.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 From 89a2b3da9eb93e46cfab94104f2a2c725b64477f Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:15:12 +0100 Subject: [PATCH 04/26] feat(todo): add quick task creation via CommandBar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add natural language parser for task input (date, time, priority, project, labels) - Extend CommandBar with onCreate/onParseCreate callbacks - Show create preview with parsed attributes as first option - Support Cmd/Ctrl+Enter to create directly - Fix service worker to not intercept Vite dev server requests - Update deprecated apple-mobile-web-app-capable meta tag Example: "Meeting morgen 14 Uhr !hoch @Arbeit #wichtig" → Creates task with due date, time, priority, project and label đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/todo/apps/web/src/app.html | 4 +- .../apps/web/src/lib/utils/task-parser.ts | 260 ++++++++++++++++++ .../apps/web/src/routes/(app)/+layout.svelte | 38 ++- apps/todo/apps/web/static/sw.js | 17 +- .../src/command-bar/CommandBar.svelte | 162 ++++++++++- packages/shared-ui/src/command-bar/index.ts | 2 +- packages/shared-ui/src/index.ts | 2 +- 7 files changed, 460 insertions(+), 25 deletions(-) create mode 100644 apps/todo/apps/web/src/lib/utils/task-parser.ts diff --git a/apps/todo/apps/web/src/app.html b/apps/todo/apps/web/src/app.html index 076d6148e..95592e23e 100644 --- a/apps/todo/apps/web/src/app.html +++ b/apps/todo/apps/web/src/app.html @@ -15,8 +15,8 @@ - - + + diff --git a/apps/todo/apps/web/src/lib/utils/task-parser.ts b/apps/todo/apps/web/src/lib/utils/task-parser.ts new file mode 100644 index 000000000..399e08504 --- /dev/null +++ b/apps/todo/apps/web/src/lib/utils/task-parser.ts @@ -0,0 +1,260 @@ +import { + addDays, + nextMonday, + nextTuesday, + nextWednesday, + nextThursday, + nextFriday, + nextSaturday, + nextSunday, + setHours, + setMinutes, + parse, +} from 'date-fns'; +import { de } from 'date-fns/locale'; +import type { TaskPriority } from '@todo/shared'; + +export interface ParsedTask { + title: string; + dueDate?: Date; + priority?: TaskPriority; + projectName?: string; + labelNames: string[]; +} + +interface Project { + id: string; + name: string; +} + +interface Label { + id: string; + name: string; +} + +export interface ParsedTaskWithIds { + title: string; + dueDate?: string; + priority?: TaskPriority; + projectId?: string; + labelIds: string[]; +} + +// Priority patterns +const PRIORITY_PATTERNS: { pattern: RegExp; priority: TaskPriority }[] = [ + { pattern: /!{3,}|!dringend|!urgent/i, priority: 'urgent' }, + { pattern: /!{2}|!hoch|!high/i, priority: 'high' }, + { pattern: /!mittel|!medium/i, priority: 'medium' }, + { pattern: /!niedrig|!low/i, priority: 'low' }, +]; + +// Date patterns (German) +const DATE_PATTERNS: { pattern: RegExp; getDate: () => Date }[] = [ + { pattern: /\bheute\b/i, getDate: () => new Date() }, + { pattern: /\bmorgen\b/i, getDate: () => addDays(new Date(), 1) }, + { pattern: /\bĂŒbermorgen\b/i, getDate: () => addDays(new Date(), 2) }, + { pattern: /\bin\s*(\d+)\s*tage?n?\b/i, getDate: () => new Date() }, // Handled specially + { pattern: /\bnĂ€chste[nr]?\s*woche\b/i, getDate: () => addDays(new Date(), 7) }, + { pattern: /\bnĂ€chste[nr]?\s*montag\b/i, getDate: () => nextMonday(new Date()) }, + { pattern: /\bnĂ€chste[nr]?\s*dienstag\b/i, getDate: () => nextTuesday(new Date()) }, + { pattern: /\bnĂ€chste[nr]?\s*mittwoch\b/i, getDate: () => nextWednesday(new Date()) }, + { pattern: /\bnĂ€chste[nr]?\s*donnerstag\b/i, getDate: () => nextThursday(new Date()) }, + { pattern: /\bnĂ€chste[nr]?\s*freitag\b/i, getDate: () => nextFriday(new Date()) }, + { pattern: /\bnĂ€chste[nr]?\s*samstag\b/i, getDate: () => nextSaturday(new Date()) }, + { pattern: /\bnĂ€chste[nr]?\s*sonntag\b/i, getDate: () => nextSunday(new Date()) }, + { pattern: /\bmontag\b/i, getDate: () => nextMonday(new Date()) }, + { pattern: /\bdienstag\b/i, getDate: () => nextTuesday(new Date()) }, + { pattern: /\bmittwoch\b/i, getDate: () => nextWednesday(new Date()) }, + { pattern: /\bdonnerstag\b/i, getDate: () => nextThursday(new Date()) }, + { pattern: /\bfreitag\b/i, getDate: () => nextFriday(new Date()) }, + { pattern: /\bsamstag\b/i, getDate: () => nextSaturday(new Date()) }, + { pattern: /\bsonntag\b/i, getDate: () => nextSunday(new Date()) }, +]; + +// Time pattern +const TIME_PATTERN = /\b(?:um\s*)?(\d{1,2})(?::(\d{2}))?\s*(?:uhr)?\b/i; + +// Specific date pattern (DD.MM. or DD.MM.YYYY) +const SPECIFIC_DATE_PATTERN = /\b(\d{1,2})\.(\d{1,2})\.?(\d{2,4})?\b/; + +/** + * Parse natural language task input + */ +export function parseTaskInput(input: string): ParsedTask { + let text = input.trim(); + let dueDate: Date | undefined; + let priority: TaskPriority | undefined; + let projectName: string | undefined; + const labelNames: string[] = []; + + // Extract priority (!hoch, !!, etc.) + for (const { pattern, priority: p } of PRIORITY_PATTERNS) { + if (pattern.test(text)) { + priority = p; + text = text.replace(pattern, '').trim(); + break; + } + } + + // Extract project (@ProjectName) + const projectMatch = text.match(/@(\S+)/); + if (projectMatch) { + projectName = projectMatch[1]; + text = text.replace(/@\S+/, '').trim(); + } + + // Extract labels (#label1 #label2) + const labelRegex = /#(\S+)/g; + let labelMatch; + while ((labelMatch = labelRegex.exec(text)) !== null) { + labelNames.push(labelMatch[1]); + } + text = text.replace(/#\S+/g, '').trim(); + + // Extract specific date (DD.MM. or DD.MM.YYYY) + const specificDateMatch = text.match(SPECIFIC_DATE_PATTERN); + if (specificDateMatch) { + const day = parseInt(specificDateMatch[1], 10); + const month = parseInt(specificDateMatch[2], 10) - 1; + const year = specificDateMatch[3] + ? parseInt(specificDateMatch[3], 10) < 100 + ? 2000 + parseInt(specificDateMatch[3], 10) + : parseInt(specificDateMatch[3], 10) + : new Date().getFullYear(); + + dueDate = new Date(year, month, day); + text = text.replace(SPECIFIC_DATE_PATTERN, '').trim(); + } + + // Extract relative date (heute, morgen, nĂ€chsten Montag, etc.) + if (!dueDate) { + // Special handling for "in X Tagen" + const inDaysMatch = text.match(/\bin\s*(\d+)\s*tage?n?\b/i); + if (inDaysMatch) { + const days = parseInt(inDaysMatch[1], 10); + dueDate = addDays(new Date(), days); + text = text.replace(/\bin\s*\d+\s*tage?n?\b/i, '').trim(); + } else { + for (const { pattern, getDate } of DATE_PATTERNS) { + if (pattern.test(text)) { + dueDate = getDate(); + text = text.replace(pattern, '').trim(); + break; + } + } + } + } + + // Extract time (um 14 Uhr, 14:00, etc.) + const timeMatch = text.match(TIME_PATTERN); + if (timeMatch && dueDate) { + const hours = parseInt(timeMatch[1], 10); + const minutes = timeMatch[2] ? parseInt(timeMatch[2], 10) : 0; + dueDate = setHours(setMinutes(dueDate, minutes), hours); + text = text.replace(TIME_PATTERN, '').trim(); + } else if (timeMatch && !dueDate) { + // Time without date = today + dueDate = new Date(); + const hours = parseInt(timeMatch[1], 10); + const minutes = timeMatch[2] ? parseInt(timeMatch[2], 10) : 0; + dueDate = setHours(setMinutes(dueDate, minutes), hours); + text = text.replace(TIME_PATTERN, '').trim(); + } + + // Clean up multiple spaces + const title = text.replace(/\s+/g, ' ').trim(); + + return { + title, + dueDate, + priority, + projectName, + labelNames, + }; +} + +/** + * Resolve project and label names to IDs + */ +export function resolveTaskIds( + parsed: ParsedTask, + projects: Project[], + labels: Label[] +): ParsedTaskWithIds { + let projectId: string | undefined; + const labelIds: string[] = []; + + // Find project by name (case-insensitive) + if (parsed.projectName) { + const project = projects.find( + (p) => p.name.toLowerCase() === parsed.projectName!.toLowerCase() + ); + if (project) { + projectId = project.id; + } + } + + // Find labels by name (case-insensitive) + for (const labelName of parsed.labelNames) { + const label = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase()); + if (label) { + labelIds.push(label.id); + } + } + + return { + title: parsed.title, + dueDate: parsed.dueDate?.toISOString(), + priority: parsed.priority, + projectId, + labelIds, + }; +} + +/** + * Format parsed task for preview display + */ +export function formatParsedTaskPreview(parsed: ParsedTask): string { + const parts: string[] = []; + + if (parsed.dueDate) { + const now = new Date(); + const tomorrow = addDays(now, 1); + + if (parsed.dueDate.toDateString() === now.toDateString()) { + parts.push('📅 Heute'); + } else if (parsed.dueDate.toDateString() === tomorrow.toDateString()) { + parts.push('📅 Morgen'); + } else { + parts.push( + `📅 ${parsed.dueDate.toLocaleDateString('de-DE', { weekday: 'short', day: 'numeric', month: 'short' })}` + ); + } + + // Add time if not midnight + if (parsed.dueDate.getHours() !== 0 || parsed.dueDate.getMinutes() !== 0) { + parts[parts.length - 1] += + ` ${parsed.dueDate.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}`; + } + } + + if (parsed.priority) { + const priorityLabels: Record = { + low: '🟱 Niedrig', + medium: '🟡 Mittel', + high: '🟠 Hoch', + urgent: '🔮 Dringend', + }; + parts.push(priorityLabels[parsed.priority]); + } + + if (parsed.projectName) { + parts.push(`📁 ${parsed.projectName}`); + } + + if (parsed.labelNames.length > 0) { + parts.push(`đŸ·ïž ${parsed.labelNames.join(', ')}`); + } + + return parts.join(' · '); +} diff --git a/apps/todo/apps/web/src/routes/(app)/+layout.svelte b/apps/todo/apps/web/src/routes/(app)/+layout.svelte index 847cacb4f..5f51106a5 100644 --- a/apps/todo/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/todo/apps/web/src/routes/(app)/+layout.svelte @@ -9,11 +9,13 @@ PillDropdownItem, CommandBarItem, QuickAction, + CreatePreview, } from '@manacore/shared-ui'; import { authStore } from '$lib/stores/auth.svelte'; import { userSettings } from '$lib/stores/user-settings.svelte'; import { projectsStore } from '$lib/stores/projects.svelte'; import { labelsStore } from '$lib/stores/labels.svelte'; + import { tasksStore } from '$lib/stores/tasks.svelte'; import { theme } from '$lib/stores/theme'; import { isSidebarMode as sidebarModeStore, @@ -28,6 +30,7 @@ import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n'; import { getPillAppItems } from '@manacore/shared-branding'; import { getTasks } from '$lib/api/tasks'; + import { parseTaskInput, resolveTaskIds, formatParsedTaskPreview } from '$lib/utils/task-parser'; // App switcher items const appItems = getPillAppItems('todo'); @@ -69,6 +72,35 @@ goto(`/task/${item.id}`); } + // CommandBar create - parse input and show preview + function handleCommandBarParseCreate(query: string): CreatePreview | null { + if (!query.trim()) return null; + + const parsed = parseTaskInput(query); + const preview = formatParsedTaskPreview(parsed); + + return { + title: `"${parsed.title}" als Aufgabe erstellen`, + subtitle: preview || 'Neue Aufgabe', + }; + } + + // CommandBar create - actually create the task + async function handleCommandBarCreate(query: string): Promise { + if (!query.trim()) return; + + const parsed = parseTaskInput(query); + const resolved = resolveTaskIds(parsed, projectsStore.projects, labelsStore.labels); + + await tasksStore.createTask({ + title: resolved.title, + dueDate: resolved.dueDate, + priority: resolved.priority, + projectId: resolved.projectId, + labelIds: resolved.labelIds, + }); + } + let isSidebarMode = $state(false); let isCollapsed = $state(false); @@ -326,9 +358,13 @@ onSearch={handleCommandBarSearch} onSelect={handleCommandBarSelect} quickActions={commandBarQuickActions} - placeholder="Aufgabe suchen..." + placeholder="Aufgabe suchen oder erstellen..." emptyText="Keine Aufgaben gefunden" searchingText="Suche..." + onCreate={handleCommandBarCreate} + onParseCreate={handleCommandBarParseCreate} + createText="Als Aufgabe erstellen" + createShortcut="⌘↔" /> diff --git a/apps/todo/apps/web/static/sw.js b/apps/todo/apps/web/static/sw.js index 7992ed8ec..f2af67d36 100644 --- a/apps/todo/apps/web/static/sw.js +++ b/apps/todo/apps/web/static/sw.js @@ -1,4 +1,4 @@ -const CACHE_NAME = 'todo-v1'; +const CACHE_NAME = 'todo-v2'; const OFFLINE_URL = '/offline.html'; // Assets, die immer gecacht werden sollen @@ -8,23 +8,16 @@ const STATIC_CACHE_URLS = ['/', '/offline.html', '/icons/icon.svg', '/manifest.j const CACHE_STRATEGIES = { // Netzwerk zuerst, dann Cache (fĂŒr HTML/Navigation) networkFirst: [/\/$/, /\.html$/, /^\/kanban/, /^\/settings/, /^\/mana/, /^\/feedback/], - // Cache zuerst, dann Netzwerk (fĂŒr Assets) + // Cache zuerst, dann Netzwerk (fĂŒr Assets) - nur fĂŒr gebaute Assets, nicht /src/ cacheFirst: [ - /\.css$/, - /\.js$/, + /\/_app\//, // SvelteKit gebaute Assets /\.woff2?$/, /\.ttf$/, /\.otf$/, - /\.svg$/, - /\.png$/, - /\.jpg$/, - /\.jpeg$/, - /\.webp$/, /\.ico$/, - /\/_app\//, ], - // Nur Netzwerk (fĂŒr API-Calls) - networkOnly: [/\/api\//, /localhost:3018/], + // Nur Netzwerk (fĂŒr API-Calls und Dev-Server) + networkOnly: [/\/api\//, /localhost:3018/, /^\/src\//, /^\/@/, /^\/node_modules\//], }; // Service Worker Installation diff --git a/packages/shared-ui/src/command-bar/CommandBar.svelte b/packages/shared-ui/src/command-bar/CommandBar.svelte index 71822e8a7..bef7acb19 100644 --- a/packages/shared-ui/src/command-bar/CommandBar.svelte +++ b/packages/shared-ui/src/command-bar/CommandBar.svelte @@ -19,6 +19,11 @@ onclick?: () => void; } + export interface CreatePreview { + title: string; + subtitle: string; + } + interface Props { open: boolean; onClose: () => void; @@ -28,6 +33,11 @@ placeholder?: string; emptyText?: string; searchingText?: string; + // New: Task creation support + onCreate?: (query: string) => Promise; + onParseCreate?: (query: string) => CreatePreview | null; + createText?: string; + createShortcut?: string; } let { @@ -39,21 +49,35 @@ placeholder = 'Suchen...', emptyText = 'Keine Ergebnisse gefunden', searchingText = 'Suche...', + onCreate, + onParseCreate, + createText = 'Als Eintrag erstellen', + createShortcut = '⌘↔', }: Props = $props(); let searchQuery = $state(''); let results = $state([]); let loading = $state(false); + let creating = $state(false); let selectedIndex = $state(0); let searchTimeout: ReturnType; let inputElement: HTMLInputElement; + // Computed create preview + let createPreview = $derived( + searchQuery.trim() && onParseCreate ? onParseCreate(searchQuery) : null + ); + + // Check if create option is selected (it's always first when available) + let isCreateSelected = $derived(selectedIndex === 0 && createPreview !== null); + // Reset state when modal opens $effect(() => { if (open) { searchQuery = ''; results = []; selectedIndex = 0; + creating = false; setTimeout(() => inputElement?.focus(), 50); } }); @@ -82,6 +106,20 @@ }, 150); } + async function handleCreate() { + if (!onCreate || !searchQuery.trim() || creating) return; + + creating = true; + try { + await onCreate(searchQuery); + onClose(); + } catch (error) { + console.error('Create error:', error); + } finally { + creating = false; + } + } + function handleKeydown(event: KeyboardEvent) { if (event.key === 'Escape') { event.preventDefault(); @@ -89,10 +127,23 @@ return; } + // Cmd/Ctrl+Enter to create directly + if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { + event.preventDefault(); + if (onCreate && searchQuery.trim()) { + handleCreate(); + } + return; + } + if (event.key === 'ArrowDown') { event.preventDefault(); - const maxIndex = searchQuery.trim() ? results.length - 1 : quickActions.length - 1; - selectedIndex = Math.min(selectedIndex + 1, maxIndex); + // Calculate max index including create option + const hasCreate = createPreview !== null; + const maxIndex = searchQuery.trim() + ? (hasCreate ? 1 : 0) + results.length - 1 + : quickActions.length - 1; + selectedIndex = Math.min(selectedIndex + 1, Math.max(0, maxIndex)); return; } @@ -104,8 +155,17 @@ if (event.key === 'Enter') { event.preventDefault(); - if (searchQuery.trim() && results.length > 0) { - selectItem(results[selectedIndex]); + if (searchQuery.trim()) { + // If create option is selected + if (isCreateSelected && onCreate) { + handleCreate(); + } else if (results.length > 0) { + // Adjust index for results (subtract 1 if create option exists) + const resultIndex = createPreview !== null ? selectedIndex - 1 : selectedIndex; + if (resultIndex >= 0 && resultIndex < results.length) { + selectItem(results[resultIndex]); + } + } } else if (!searchQuery.trim() && quickActions.length > 0) { const action = quickActions[selectedIndex]; if (action.href) { @@ -184,23 +244,63 @@ {#if searchQuery.trim()}
+ + {#if createPreview && onCreate} + + {/if} + {#if loading}
{searchingText}
- {:else if results.length === 0} + {:else if results.length === 0 && !createPreview}
{emptyText}
- {:else} + {:else if results.length > 0} +
+ Suchergebnisse +
{#each results as item, index (item.id)} + {@const adjustedIndex = createPreview ? index + 1 : index} - {/each} -
+ (priority = p)} /> @@ -346,68 +242,10 @@
-
- - - {#if showLabelDropdown} -
- {#each labelsStore.labels as label} - - {/each} - {#if labelsStore.labels.length === 0} -
Keine Labels vorhanden
- {/if} -
- {/if} -
+ (selectedLabelIds = ids)} + />
@@ -441,140 +279,22 @@
-
- {#each storyPointOptions as sp} - - {/each} - {#if storyPoints !== null} - - {/if} -
+ (storyPoints = v)} />
-
- {#each durationOptions as opt} - - {/each} - - {#if effectiveDurationValue !== null} - - {/if} -
- {#if showCustomDuration} -
- - -
- {/if} + (effectiveDuration = v)} />
-
- {#each Array(10) as _, i} - {@const rating = i + 1} - - {/each} - {#if funRating !== null} - - {/if} -
-
- 1 - 5 - 10 -
+ (funRating = v)} />
@@ -788,184 +508,6 @@ min-height: 80px; } - /* Priority buttons */ - .priority-buttons { - display: flex; - gap: 0.5rem; - flex-wrap: wrap; - } - - .priority-btn { - display: flex; - align-items: center; - gap: 0.375rem; - padding: 0.5rem 0.875rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 9999px; - background: rgba(255, 255, 255, 0.8); - font-size: 0.8125rem; - color: #374151; - cursor: pointer; - transition: all 0.15s; - } - - :global(.dark) .priority-btn { - background: rgba(255, 255, 255, 0.1); - border-color: rgba(255, 255, 255, 0.15); - color: #e5e7eb; - } - - .priority-btn:hover { - border-color: var(--priority-color); - } - - .priority-btn.selected { - background: color-mix(in srgb, var(--priority-color) 15%, transparent); - border-color: var(--priority-color); - color: var(--priority-color); - } - - .priority-dot { - width: 0.5rem; - height: 0.5rem; - border-radius: 9999px; - } - - /* Label selector */ - .label-selector { - position: relative; - } - - .label-trigger { - width: 100%; - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.625rem 0.875rem; - border: 1px solid rgba(0, 0, 0, 0.15); - border-radius: 0.75rem; - background: rgba(255, 255, 255, 0.8); - cursor: pointer; - transition: all 0.15s; - } - - :global(.dark) .label-trigger { - background: rgba(255, 255, 255, 0.1); - border-color: rgba(255, 255, 255, 0.15); - } - - .label-trigger:hover { - border-color: rgba(0, 0, 0, 0.25); - } - - .text-muted { - color: #9ca3af; - font-size: 0.875rem; - } - - .selected-labels { - display: flex; - flex-wrap: wrap; - gap: 0.375rem; - } - - .label-tag { - font-size: 0.75rem; - padding: 0.125rem 0.5rem; - border-radius: 9999px; - background: color-mix(in srgb, var(--label-color) 15%, transparent); - color: var(--label-color); - font-weight: 500; - } - - .label-more { - font-size: 0.75rem; - color: #6b7280; - } - - .dropdown-arrow { - width: 1rem; - height: 1rem; - color: #9ca3af; - } - - .label-dropdown { - position: absolute; - top: calc(100% + 0.5rem); - left: 0; - right: 0; - max-height: 200px; - overflow-y: auto; - padding: 0.375rem; - border-radius: 0.75rem; - background: rgba(255, 255, 255, 0.95); - backdrop-filter: blur(12px); - border: 1px solid rgba(0, 0, 0, 0.1); - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1); - z-index: 10; - } - - :global(.dark) .label-dropdown { - background: rgba(40, 40, 40, 0.95); - border-color: rgba(255, 255, 255, 0.15); - } - - .label-option { - display: flex; - align-items: center; - gap: 0.5rem; - width: 100%; - padding: 0.5rem 0.75rem; - border: none; - background: transparent; - border-radius: 0.5rem; - cursor: pointer; - transition: background 0.15s; - text-align: left; - } - - .label-option:hover { - background: rgba(0, 0, 0, 0.05); - } - - :global(.dark) .label-option:hover { - background: rgba(255, 255, 255, 0.1); - } - - .label-option.selected { - background: rgba(139, 92, 246, 0.1); - } - - .label-dot { - width: 0.625rem; - height: 0.625rem; - border-radius: 9999px; - flex-shrink: 0; - } - - .label-name { - flex: 1; - font-size: 0.875rem; - color: #374151; - } - - :global(.dark) .label-name { - color: #e5e7eb; - } - - .check-icon { - width: 1rem; - height: 1rem; - color: #8b5cf6; - } - - .no-labels { - padding: 0.75rem; - text-align: center; - font-size: 0.875rem; - color: #9ca3af; - } - /* Footer */ .modal-footer { display: flex; @@ -1056,164 +598,6 @@ } } - /* Storypoints */ - .storypoint-buttons { - display: flex; - gap: 0.375rem; - flex-wrap: wrap; - align-items: center; - } - - .storypoint-btn { - min-width: 2.25rem; - height: 2.25rem; - display: flex; - align-items: center; - justify-content: center; - padding: 0 0.5rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 9999px; - background: rgba(255, 255, 255, 0.8); - font-size: 0.8125rem; - font-weight: 500; - color: #374151; - cursor: pointer; - transition: all 0.15s; - } - - :global(.dark) .storypoint-btn { - background: rgba(255, 255, 255, 0.1); - border-color: rgba(255, 255, 255, 0.15); - color: #e5e7eb; - } - - .storypoint-btn:hover { - border-color: #8b5cf6; - } - - .storypoint-btn.selected { - background: rgba(139, 92, 246, 0.15); - border-color: #8b5cf6; - color: #8b5cf6; - } - - .storypoint-clear, - .duration-clear, - .fun-rating-clear { - width: 2rem; - height: 2rem; - display: flex; - align-items: center; - justify-content: center; - padding: 0; - border: none; - border-radius: 9999px; - background: rgba(239, 68, 68, 0.1); - color: #ef4444; - cursor: pointer; - transition: all 0.15s; - } - - .storypoint-clear:hover, - .duration-clear:hover, - .fun-rating-clear:hover { - background: rgba(239, 68, 68, 0.2); - } - - /* Duration */ - .duration-buttons { - display: flex; - gap: 0.375rem; - flex-wrap: wrap; - align-items: center; - } - - .duration-btn { - padding: 0.5rem 0.75rem; - border: 1px solid rgba(0, 0, 0, 0.1); - border-radius: 9999px; - background: rgba(255, 255, 255, 0.8); - font-size: 0.8125rem; - color: #374151; - cursor: pointer; - transition: all 0.15s; - } - - :global(.dark) .duration-btn { - background: rgba(255, 255, 255, 0.1); - border-color: rgba(255, 255, 255, 0.15); - color: #e5e7eb; - } - - .duration-btn:hover { - border-color: #8b5cf6; - } - - .duration-btn.selected { - background: rgba(139, 92, 246, 0.15); - border-color: #8b5cf6; - color: #8b5cf6; - } - - .duration-custom { - display: flex; - gap: 0.5rem; - margin-top: 0.75rem; - } - - .duration-input { - width: 80px; - } - - .duration-unit { - width: 120px; - } - - /* Fun Rating */ - .fun-rating { - display: flex; - gap: 0.25rem; - align-items: center; - } - - .fun-rating-dot { - padding: 0.25rem; - border: none; - background: transparent; - cursor: pointer; - transition: transform 0.15s; - } - - .fun-rating-dot:hover { - transform: scale(1.2); - } - - .fun-rating-dot .dot { - display: block; - width: 1.25rem; - height: 1.25rem; - border-radius: 9999px; - background: rgba(0, 0, 0, 0.1); - transition: all 0.15s; - } - - :global(.dark) .fun-rating-dot .dot { - background: rgba(255, 255, 255, 0.15); - } - - .fun-rating-dot.filled .dot { - background: var(--dot-color); - } - - .fun-rating-labels { - display: flex; - justify-content: space-between; - padding: 0 0.25rem; - margin-top: 0.25rem; - font-size: 0.6875rem; - color: #9ca3af; - } - .fun-rating-value { font-weight: 600; } diff --git a/apps/todo/apps/web/src/lib/components/form/DurationPicker.svelte b/apps/todo/apps/web/src/lib/components/form/DurationPicker.svelte new file mode 100644 index 000000000..c0be0f92a --- /dev/null +++ b/apps/todo/apps/web/src/lib/components/form/DurationPicker.svelte @@ -0,0 +1,238 @@ + + +
+
+ {#each quickOptions as opt} + + {/each} + + {#if value !== null} + + {/if} +
+ + {#if showCustom} +
+ + +
+ {/if} +
+ + diff --git a/apps/todo/apps/web/src/lib/components/form/FunRatingPicker.svelte b/apps/todo/apps/web/src/lib/components/form/FunRatingPicker.svelte new file mode 100644 index 000000000..2d2fbbbd6 --- /dev/null +++ b/apps/todo/apps/web/src/lib/components/form/FunRatingPicker.svelte @@ -0,0 +1,127 @@ + + +
+
+ {#each Array(10) as _, i} + {@const rating = i + 1} + + {/each} + {#if value !== null} + + {/if} +
+
+ 1 + 5 + 10 +
+
+ + diff --git a/apps/todo/apps/web/src/lib/components/form/LabelSelector.svelte b/apps/todo/apps/web/src/lib/components/form/LabelSelector.svelte new file mode 100644 index 000000000..86d8d644c --- /dev/null +++ b/apps/todo/apps/web/src/lib/components/form/LabelSelector.svelte @@ -0,0 +1,223 @@ + + + + +
+ + + {#if showDropdown} +
e.stopPropagation()} role="listbox"> + {#each labelsStore.labels as label} + + {/each} + {#if labelsStore.labels.length === 0} +
Keine Labels vorhanden
+ {/if} +
+ {/if} +
+ + diff --git a/apps/todo/apps/web/src/lib/components/form/PrioritySelector.svelte b/apps/todo/apps/web/src/lib/components/form/PrioritySelector.svelte new file mode 100644 index 000000000..429081926 --- /dev/null +++ b/apps/todo/apps/web/src/lib/components/form/PrioritySelector.svelte @@ -0,0 +1,76 @@ + + +
+ {#each priorities as p} + + {/each} +
+ + diff --git a/apps/todo/apps/web/src/lib/components/form/StorypointsSelector.svelte b/apps/todo/apps/web/src/lib/components/form/StorypointsSelector.svelte new file mode 100644 index 000000000..553bc7305 --- /dev/null +++ b/apps/todo/apps/web/src/lib/components/form/StorypointsSelector.svelte @@ -0,0 +1,105 @@ + + +
+ {#each options as sp} + + {/each} + {#if value !== null} + + {/if} +
+ + diff --git a/apps/todo/apps/web/src/lib/components/form/index.ts b/apps/todo/apps/web/src/lib/components/form/index.ts new file mode 100644 index 000000000..cd7e13ded --- /dev/null +++ b/apps/todo/apps/web/src/lib/components/form/index.ts @@ -0,0 +1,5 @@ +export { default as PrioritySelector } from './PrioritySelector.svelte'; +export { default as StorypointsSelector } from './StorypointsSelector.svelte'; +export { default as DurationPicker } from './DurationPicker.svelte'; +export { default as FunRatingPicker } from './FunRatingPicker.svelte'; +export { default as LabelSelector } from './LabelSelector.svelte'; From c6b48d8f95cb9e6452029507e0faf13b8b9916a5 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:25:24 +0100 Subject: [PATCH 06/26] fix(todo): add input validation, N+1 fix, and RRULE bounds checking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Security & Validation: - Add @IsNotEmpty and @MinLength(1) validators to prevent empty inputs - CreateTaskDto.title - CreateLabelDto.name - CreateProjectDto.name - Add German error messages for validation failures Performance: - Fix N+1 query in network.service.ts getGraph() - Batch load all task-label relationships in single query - Reduces queries from O(n) to O(1) for label fetching Security: - Add validateRRule() to prevent DoS via malicious recurrence rules - Reject rules > 500 chars - Reject rules with > 5000 occurrences in 10 years - Prevents hourly/minutely abuse while allowing daily tasks Cleanup: - Remove debug console.log from tasks.svelte.ts đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../backend/src/label/dto/create-label.dto.ts | 6 ++- .../backend/src/network/network.service.ts | 29 ++++++++---- .../src/project/dto/create-project.dto.ts | 14 +++++- .../backend/src/task/dto/create-task.dto.ts | 6 ++- .../apps/backend/src/task/task.service.ts | 42 +++++++++++++++++ .../apps/web/src/lib/stores/tasks.svelte.ts | 47 ++++++++++++++++++- 6 files changed, 129 insertions(+), 15 deletions(-) diff --git a/apps/todo/apps/backend/src/label/dto/create-label.dto.ts b/apps/todo/apps/backend/src/label/dto/create-label.dto.ts index 5efeb87f3..920537e9a 100644 --- a/apps/todo/apps/backend/src/label/dto/create-label.dto.ts +++ b/apps/todo/apps/backend/src/label/dto/create-label.dto.ts @@ -1,8 +1,10 @@ -import { IsString, IsOptional, MaxLength } from 'class-validator'; +import { IsString, IsOptional, MaxLength, MinLength, IsNotEmpty } from 'class-validator'; export class CreateLabelDto { @IsString() - @MaxLength(100) + @IsNotEmpty({ message: 'Name darf nicht leer sein' }) + @MinLength(1, { message: 'Name muss mindestens 1 Zeichen haben' }) + @MaxLength(100, { message: 'Name darf maximal 100 Zeichen haben' }) name: string; @IsOptional() diff --git a/apps/todo/apps/backend/src/network/network.service.ts b/apps/todo/apps/backend/src/network/network.service.ts index 8b5d26524..1c67c19f0 100644 --- a/apps/todo/apps/backend/src/network/network.service.ts +++ b/apps/todo/apps/backend/src/network/network.service.ts @@ -1,5 +1,5 @@ import { Injectable, Inject } from '@nestjs/common'; -import { eq } from 'drizzle-orm'; +import { eq, inArray } from 'drizzle-orm'; import { DATABASE_CONNECTION } from '../db/database.module'; import { Database } from '../db/connection'; import { tasks, labels, taskLabels, projects } from '../db/schema'; @@ -54,21 +54,32 @@ export class NetworkService { const projectMap = new Map(userProjects.map((p) => [p.id, p.name])); - // 3. Get labels for each task + // 3. Get all labels for all tasks in a single batch query (fix N+1) + const taskIds = userTasks.map(({ task }) => task.id); const taskLabelsMap = new Map(); - for (const { task } of userTasks) { - const taskLabelRows = await this.db + if (taskIds.length > 0) { + const allTaskLabels = await this.db .select({ - id: labels.id, - name: labels.name, - color: labels.color, + taskId: taskLabels.taskId, + labelId: labels.id, + labelName: labels.name, + labelColor: labels.color, }) .from(taskLabels) .innerJoin(labels, eq(taskLabels.labelId, labels.id)) - .where(eq(taskLabels.taskId, task.id)); + .where(inArray(taskLabels.taskId, taskIds)); - taskLabelsMap.set(task.id, taskLabelRows); + // Group labels by taskId + for (const row of allTaskLabels) { + const existing = taskLabelsMap.get(row.taskId) || []; + existing.push({ + id: row.labelId, + name: row.labelName, + color: row.labelColor, + }); + taskLabelsMap.set(row.taskId, existing); + } } // 4. Filter tasks that have at least one label diff --git a/apps/todo/apps/backend/src/project/dto/create-project.dto.ts b/apps/todo/apps/backend/src/project/dto/create-project.dto.ts index cf13948b0..266bd7f00 100644 --- a/apps/todo/apps/backend/src/project/dto/create-project.dto.ts +++ b/apps/todo/apps/backend/src/project/dto/create-project.dto.ts @@ -1,9 +1,19 @@ -import { IsString, IsOptional, IsBoolean, MaxLength, IsObject } from 'class-validator'; +import { + IsString, + IsOptional, + IsBoolean, + MaxLength, + MinLength, + IsObject, + IsNotEmpty, +} from 'class-validator'; import type { ProjectSettings } from '../../db/schema/projects.schema'; export class CreateProjectDto { @IsString() - @MaxLength(255) + @IsNotEmpty({ message: 'Name darf nicht leer sein' }) + @MinLength(1, { message: 'Name muss mindestens 1 Zeichen haben' }) + @MaxLength(255, { message: 'Name darf maximal 255 Zeichen haben' }) name: string; @IsOptional() diff --git a/apps/todo/apps/backend/src/task/dto/create-task.dto.ts b/apps/todo/apps/backend/src/task/dto/create-task.dto.ts index 32e7fdd22..f266923d4 100644 --- a/apps/todo/apps/backend/src/task/dto/create-task.dto.ts +++ b/apps/todo/apps/backend/src/task/dto/create-task.dto.ts @@ -6,13 +6,17 @@ import { IsArray, IsObject, MaxLength, + MinLength, IsDateString, + IsNotEmpty, } from 'class-validator'; import type { TaskPriority, Subtask, TaskMetadata } from '../../db/schema/tasks.schema'; export class CreateTaskDto { @IsString() - @MaxLength(500) + @IsNotEmpty({ message: 'Titel darf nicht leer sein' }) + @MinLength(1, { message: 'Titel muss mindestens 1 Zeichen haben' }) + @MaxLength(500, { message: 'Titel darf maximal 500 Zeichen haben' }) title: string; @IsOptional() diff --git a/apps/todo/apps/backend/src/task/task.service.ts b/apps/todo/apps/backend/src/task/task.service.ts index 5dfc7d825..cbb420d1e 100644 --- a/apps/todo/apps/backend/src/task/task.service.ts +++ b/apps/todo/apps/backend/src/task/task.service.ts @@ -219,6 +219,42 @@ export class TaskService { }); } + /** + * Validates an RRULE string to prevent abuse (DoS, excessive occurrences). + * Returns true if valid, false if invalid or too complex. + */ + private validateRRule(rruleString: string): boolean { + // Basic length check + if (!rruleString || rruleString.length > 500) { + return false; + } + + try { + const rule = rrulestr(rruleString); + + // Get occurrences for the next 10 years with a limit + // Daily tasks = ~3650/10yrs, hourly would be ~87600 (reject) + const maxOccurrences = 5000; + const tenYearsFromNow = new Date(); + tenYearsFromNow.setFullYear(tenYearsFromNow.getFullYear() + 10); + + const occurrences = rule.between(new Date(), tenYearsFromNow, true, (_, count) => { + // Stop iteration early if we exceed limit + return count < maxOccurrences; + }); + + // Reject if too many occurrences (prevents hourly/minutely abuse) + if (occurrences.length >= maxOccurrences) { + console.warn(`RRULE rejected: too many occurrences (${occurrences.length})`); + return false; + } + + return true; + } catch { + return false; + } + } + /** * Creates the next occurrence of a recurring task based on its RRULE. * Returns the newly created task, or null if no more occurrences should be created. @@ -229,6 +265,12 @@ export class TaskService { ): Promise { if (!task.recurrenceRule) return null; + // Validate RRULE complexity before parsing + if (!this.validateRRule(task.recurrenceRule)) { + console.warn(`Invalid or too complex RRULE for task ${task.id}`); + return null; + } + try { // Parse the RRULE string const rule = rrulestr(task.recurrenceRule); diff --git a/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts b/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts index 483b3a1c1..435057133 100644 --- a/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts +++ b/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts @@ -121,7 +121,6 @@ export const tasksStore = { try { // Fetch all tasks without filter - let frontend handle filtering const allTasks = await tasksApi.getTasks({}); - console.log('API response - all tasks:', allTasks.length); tasks = allTasks; } catch (e) { error = e instanceof Error ? e.message : 'Failed to fetch all tasks'; @@ -239,6 +238,52 @@ export const tasksStore = { } }, + /** + * Update task optimistically (for drag and drop) + * Updates local state immediately, then syncs with server + */ + updateTaskOptimistic( + id: string, + data: { + dueDate?: string | null; + isCompleted?: boolean; + } + ) { + // Optimistic update - immediately update local state + const originalTask = tasks.find((t) => t.id === id); + if (!originalTask) return; + + tasks = tasks.map((t) => (t.id === id ? { ...t, ...data } : t)); + + // Sync with server in background + if (data.isCompleted !== undefined) { + const apiCall = data.isCompleted ? tasksApi.completeTask(id) : tasksApi.uncompleteTask(id); + + apiCall + .then((updatedTask) => { + tasks = tasks.map((t) => (t.id === id ? updatedTask : t)); + }) + .catch((e) => { + // Rollback on error + console.error('Failed to update task:', e); + tasks = tasks.map((t) => (t.id === id ? originalTask : t)); + }); + } + + if (data.dueDate !== undefined) { + tasksApi + .updateTask(id, { dueDate: data.dueDate }) + .then((updatedTask) => { + tasks = tasks.map((t) => (t.id === id ? updatedTask : t)); + }) + .catch((e) => { + // Rollback on error + console.error('Failed to update task:', e); + tasks = tasks.map((t) => (t.id === id ? originalTask : t)); + }); + } + }, + /** * Delete a task */ From b1877c4a087b4d1640d8f3418fc4c10b5c8aa8ca Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:37:01 +0100 Subject: [PATCH 07/26] feat: add unified CommandBar Quick-Create for Calendar and Contacts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements the same CommandBar quick-create functionality from Todo in Calendar and Contacts apps with a shared base parser architecture. - Add base-parser in shared-utils with common patterns (date, time, tags) - Refactor task-parser to use base-parser - Create event-parser for Calendar with duration, location, @calendar - Create contact-parser for Contacts with email, phone, @company detection - Integrate Quick-Create into Calendar and Contacts layouts Natural language syntax: - Common: heute, morgen, Montag, 15.12., um 14 Uhr, #tags - Calendar: fĂŒr 2h, 30 min, in Berlin, @Kalender, ganztĂ€gig - Contacts: @Firma, bei Company, auto email/phone detection đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/calendar/apps/web/package.json | 1 + .../apps/web/src/lib/utils/event-parser.ts | 261 ++++ .../apps/web/src/routes/(app)/+layout.svelte | 66 +- .../apps/web/src/lib/utils/contact-parser.ts | 227 ++++ .../apps/web/src/routes/(app)/+layout.svelte | 69 +- apps/todo/apps/web/package.json | 1 + .../apps/web/src/lib/utils/task-parser.ts | 190 +-- packages/shared-utils/src/index.ts | 3 + .../shared-utils/src/parsers/base-parser.ts | 320 +++++ packages/shared-utils/src/parsers/index.ts | 26 + pnpm-lock.yaml | 1055 ++++++++--------- 11 files changed, 1487 insertions(+), 732 deletions(-) create mode 100644 apps/calendar/apps/web/src/lib/utils/event-parser.ts create mode 100644 apps/contacts/apps/web/src/lib/utils/contact-parser.ts create mode 100644 packages/shared-utils/src/parsers/base-parser.ts create mode 100644 packages/shared-utils/src/parsers/index.ts diff --git a/apps/calendar/apps/web/package.json b/apps/calendar/apps/web/package.json index 3c8e29479..ff784f240 100644 --- a/apps/calendar/apps/web/package.json +++ b/apps/calendar/apps/web/package.json @@ -31,6 +31,7 @@ "dependencies": { "@calendar/shared": "workspace:*", "@manacore/shared-auth": "workspace:*", + "@manacore/shared-utils": "workspace:*", "@manacore/shared-tags": "workspace:*", "@manacore/shared-auth-ui": "workspace:*", "@manacore/shared-branding": "workspace:*", diff --git a/apps/calendar/apps/web/src/lib/utils/event-parser.ts b/apps/calendar/apps/web/src/lib/utils/event-parser.ts new file mode 100644 index 000000000..5a7beab9b --- /dev/null +++ b/apps/calendar/apps/web/src/lib/utils/event-parser.ts @@ -0,0 +1,261 @@ +/** + * Event Parser for Calendar App + * + * Extends the base parser with event-specific patterns: + * - Calendar: @CalendarName + * - Duration: fĂŒr 2 Stunden, 30 min + * - Location: in Berlin, bei Firma XY + */ + +import { + parseBaseInput, + extractAtReference, + combineDateAndTime, + formatDatePreview, + formatTimePreview, +} from '@manacore/shared-utils'; + +export interface ParsedEvent { + title: string; + startTime?: Date; + endTime?: Date; + calendarName?: string; + location?: string; + tagNames: string[]; + isAllDay: boolean; +} + +interface Calendar { + id: string; + name: string; +} + +interface Tag { + id: string; + name: string; +} + +export interface ParsedEventWithIds { + title: string; + startTime?: string; + endTime?: string; + calendarId?: string; + tagIds: string[]; + location?: string; + isAllDay: boolean; +} + +// Duration patterns (event-specific) +const DURATION_PATTERNS: { pattern: RegExp; getMinutes: (match: RegExpMatchArray) => number }[] = [ + // "fĂŒr X Stunden" or "X Stunden" + { + pattern: /(?:fĂŒr\s+)?(\d+(?:[.,]\d+)?)\s*(?:stunde?n?|h)\b/i, + getMinutes: (match) => Math.round(parseFloat(match[1].replace(',', '.')) * 60), + }, + // "fĂŒr X Minuten" or "X min" + { + pattern: /(?:fĂŒr\s+)?(\d+)\s*(?:minuten?|min)\b/i, + getMinutes: (match) => parseInt(match[1], 10), + }, + // "1,5h" or "1.5h" + { + pattern: /(\d+[.,]\d+)\s*h\b/i, + getMinutes: (match) => Math.round(parseFloat(match[1].replace(',', '.')) * 60), + }, +]; + +// Location patterns (event-specific) +const LOCATION_PATTERNS: RegExp[] = [ + /\bin\s+([^@#!]+?)(?=\s+(?:@|#|!|\d{1,2}[:.]\d{2}|um\s+\d|\d{1,2}\s*uhr)|$)/i, + /\bbei\s+([^@#!]+?)(?=\s+(?:@|#|!|\d{1,2}[:.]\d{2}|um\s+\d|\d{1,2}\s*uhr)|$)/i, +]; + +/** + * Extract duration from text + */ +function extractDuration(text: string): { minutes?: number; remaining: string } { + for (const { pattern, getMinutes } of DURATION_PATTERNS) { + const match = text.match(pattern); + if (match) { + return { + minutes: getMinutes(match), + remaining: text.replace(pattern, '').trim(), + }; + } + } + return { minutes: undefined, remaining: text }; +} + +/** + * Extract location from text + */ +function extractLocation(text: string): { location?: string; remaining: string } { + for (const pattern of LOCATION_PATTERNS) { + const match = text.match(pattern); + if (match) { + return { + location: match[1].trim(), + remaining: text.replace(pattern, '').trim(), + }; + } + } + return { location: undefined, remaining: text }; +} + +/** + * Parse natural language event input + * + * Examples: + * - "Meeting morgen 14 Uhr fĂŒr 1 Stunde @Arbeit in BĂŒro #wichtig" + * - "Arzttermin Montag 10:30 30 min bei Dr. MĂŒller" + * - "Geburtstag 15.12. ganztĂ€gig #privat" + */ +export function parseEventInput(input: string): ParsedEvent { + let text = input.trim(); + + // Check for all-day indicator first + const allDayPattern = /\bganztĂ€gig\b|\ball[- ]?day\b/i; + const isAllDay = allDayPattern.test(text); + text = text.replace(allDayPattern, '').trim(); + + // Extract calendar (@CalendarName) - event-specific + const calendarResult = extractAtReference(text); + text = calendarResult.remaining; + const calendarName = calendarResult.value; + + // Extract duration first (before base parser) + const durationResult = extractDuration(text); + text = durationResult.remaining; + const durationMinutes = durationResult.minutes; + + // Extract location (before base parser to avoid conflicts) + const locationResult = extractLocation(text); + text = locationResult.remaining; + const location = locationResult.location; + + // Use base parser for common patterns (date, time, tags) + const base = parseBaseInput(text); + + // Combine date and time for start + const startTime = combineDateAndTime(base.date, base.time); + + // Calculate end time based on duration (default 1 hour) + let endTime: Date | undefined; + if (startTime && !isAllDay) { + const duration = durationMinutes || 60; // Default 1 hour + endTime = new Date(startTime.getTime() + duration * 60 * 1000); + } else if (startTime && isAllDay) { + // All-day events: end time is end of day + endTime = new Date(startTime); + endTime.setHours(23, 59, 59, 999); + } + + return { + title: base.title, + startTime, + endTime, + calendarName, + location, + tagNames: base.tagNames, + isAllDay, + }; +} + +/** + * Resolve calendar and tag names to IDs + */ +export function resolveEventIds( + parsed: ParsedEvent, + calendars: Calendar[], + tags: Tag[] +): ParsedEventWithIds { + let calendarId: string | undefined; + const tagIds: string[] = []; + + // Find calendar by name (case-insensitive) + if (parsed.calendarName) { + const calendar = calendars.find( + (c) => c.name.toLowerCase() === parsed.calendarName!.toLowerCase() + ); + if (calendar) { + calendarId = calendar.id; + } + } + + // Use default calendar if none specified + if (!calendarId && calendars.length > 0) { + const defaultCalendar = calendars.find((c: any) => c.isDefault) || calendars[0]; + calendarId = defaultCalendar.id; + } + + // Find tags by name (case-insensitive) + for (const tagName of parsed.tagNames) { + const tag = tags.find((t) => t.name.toLowerCase() === tagName.toLowerCase()); + if (tag) { + tagIds.push(tag.id); + } + } + + return { + title: parsed.title, + startTime: parsed.startTime?.toISOString(), + endTime: parsed.endTime?.toISOString(), + calendarId, + tagIds, + location: parsed.location, + isAllDay: parsed.isAllDay, + }; +} + +/** + * Format parsed event for preview display + */ +export function formatParsedEventPreview(parsed: ParsedEvent): string { + const parts: string[] = []; + + if (parsed.startTime) { + let dateStr = `📅 ${formatDatePreview(parsed.startTime)}`; + + if (!parsed.isAllDay && parsed.startTime.getHours() !== 0) { + dateStr += ` ${formatTimePreview({ + hours: parsed.startTime.getHours(), + minutes: parsed.startTime.getMinutes(), + })}`; + + // Add duration if end time differs + if (parsed.endTime) { + const durationMs = parsed.endTime.getTime() - parsed.startTime.getTime(); + const durationMins = Math.round(durationMs / 60000); + if (durationMins > 0 && durationMins !== 60) { + if (durationMins >= 60) { + const hours = Math.floor(durationMins / 60); + const mins = durationMins % 60; + dateStr += mins > 0 ? ` (${hours}h ${mins}min)` : ` (${hours}h)`; + } else { + dateStr += ` (${durationMins}min)`; + } + } + } + } + + if (parsed.isAllDay) { + dateStr += ' (GanztĂ€gig)'; + } + + parts.push(dateStr); + } + + if (parsed.location) { + parts.push(`📍 ${parsed.location}`); + } + + if (parsed.calendarName) { + parts.push(`📆 ${parsed.calendarName}`); + } + + if (parsed.tagNames.length > 0) { + parts.push(`đŸ·ïž ${parsed.tagNames.join(', ')}`); + } + + return parts.join(' · '); +} diff --git a/apps/calendar/apps/web/src/routes/(app)/+layout.svelte b/apps/calendar/apps/web/src/routes/(app)/+layout.svelte index 4b63872f0..50ab7b55a 100644 --- a/apps/calendar/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/calendar/apps/web/src/routes/(app)/+layout.svelte @@ -9,12 +9,15 @@ PillDropdownItem, CommandBarItem, QuickAction, + CreatePreview, } from '@manacore/shared-ui'; import { theme } from '$lib/stores/theme'; import { authStore } from '$lib/stores/auth.svelte'; import { userSettings } from '$lib/stores/user-settings.svelte'; import { viewStore } from '$lib/stores/view.svelte'; import { calendarsStore } from '$lib/stores/calendars.svelte'; + import { eventsStore } from '$lib/stores/events.svelte'; + import { eventTagsStore } from '$lib/stores/event-tags.svelte'; import { settingsStore } from '$lib/stores/settings.svelte'; import { THEME_DEFINITIONS, @@ -32,6 +35,11 @@ import { searchEvents } from '$lib/api/events'; import { format } from 'date-fns'; import { de } from 'date-fns/locale'; + import { + parseEventInput, + resolveEventIds, + formatParsedEventPreview, + } from '$lib/utils/event-parser'; // App switcher items const appItems = getPillAppItems('calendar'); @@ -72,6 +80,54 @@ goto(`/event/${item.id}`); } + // CommandBar Quick-Create handlers + function handleCommandBarParseCreate(query: string): CreatePreview | null { + if (!query.trim()) return null; + + const parsed = parseEventInput(query); + if (!parsed.title) return null; + + return { + title: parsed.title, + subtitle: formatParsedEventPreview(parsed), + }; + } + + async function handleCommandBarCreate(query: string): Promise { + const parsed = parseEventInput(query); + if (!parsed.title) return; + + // Resolve calendar and tag names to IDs + const calendars = calendarsStore.calendars.map((c) => ({ id: c.id, name: c.name })); + const tags = eventTagsStore.tags.map((t) => ({ id: t.id, name: t.name })); + const resolved = resolveEventIds(parsed, calendars, tags); + + // Ensure we have a calendar + if (!resolved.calendarId) { + console.error('No calendar available'); + return; + } + + // Ensure we have start and end times + if (!resolved.startTime) { + // Default to now + 1 hour + const now = new Date(); + resolved.startTime = now.toISOString(); + const end = new Date(now.getTime() + 60 * 60 * 1000); + resolved.endTime = end.toISOString(); + } + + await eventsStore.createEvent({ + calendarId: resolved.calendarId, + title: resolved.title, + startTime: resolved.startTime, + endTime: resolved.endTime || resolved.startTime, + isAllDay: resolved.isAllDay, + location: resolved.location, + tagIds: resolved.tagIds, + }); + } + let isSidebarMode = $state(false); let isCollapsed = $state(false); @@ -127,6 +183,7 @@ { href: '/', label: 'Kalender', icon: 'calendar' }, { href: '/agenda', label: 'Agenda', icon: 'list' }, { href: '/tags', label: 'Tags', icon: 'tag' }, + { href: '/statistics', label: 'Statistiken', icon: 'bar-chart-3' }, { href: '/network', label: 'Netzwerk', icon: 'share-2' }, { href: '/settings', label: 'Einstellungen', icon: 'settings' }, { href: '/feedback', label: 'Feedback', icon: 'chat' }, @@ -200,8 +257,9 @@ // Initialize view state viewStore.initialize(); - // Load calendars and user settings + // Load calendars, tags, and user settings await calendarsStore.fetchCalendars(); + await eventTagsStore.fetchTags(); await userSettings.load(); // Redirect to start page if on root and a custom start page is set @@ -283,9 +341,13 @@ onSearch={handleCommandBarSearch} onSelect={handleCommandBarSelect} quickActions={commandBarQuickActions} - placeholder="Termin suchen..." + placeholder="Termin suchen oder erstellen..." emptyText="Keine Termine gefunden" searchingText="Suche..." + onCreate={handleCommandBarCreate} + onParseCreate={handleCommandBarParseCreate} + createText="Als Termin erstellen" + createShortcut="⌘↔" /> diff --git a/apps/contacts/apps/web/src/lib/utils/contact-parser.ts b/apps/contacts/apps/web/src/lib/utils/contact-parser.ts new file mode 100644 index 000000000..58cd8b1b9 --- /dev/null +++ b/apps/contacts/apps/web/src/lib/utils/contact-parser.ts @@ -0,0 +1,227 @@ +/** + * Contact Parser for Contacts App + * + * Extends the base parser with contact-specific patterns: + * - Company: @CompanyName or bei CompanyName + * - Email: Recognizes email addresses + * - Phone: Recognizes phone numbers + * - Name: First and last name extraction + */ + +import { extractTags, extractAtReference } from '@manacore/shared-utils'; + +export interface ParsedContact { + displayName: string; + firstName?: string; + lastName?: string; + company?: string; + email?: string; + phone?: string; + tagNames: string[]; +} + +interface Tag { + id: string; + name: string; +} + +export interface ParsedContactWithIds { + displayName: string; + firstName?: string; + lastName?: string; + company?: string; + email?: string; + phone?: string; + tagIds: string[]; +} + +// Email pattern +const EMAIL_PATTERN = /\b([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\b/; + +// Phone patterns (various formats) +const PHONE_PATTERNS: RegExp[] = [ + // International format: +49 123 456789, +49-123-456789 + /\+\d{1,3}[-\s]?\d{2,4}[-\s]?\d{3,}[-\s]?\d*/, + // German format: 0123 456789, 0123/456789 + /\b0\d{2,4}[-\s/]?\d{3,}[-\s]?\d*/, + // Simple format: 123456789 (at least 6 digits) + /\b\d{6,}\b/, +]; + +// Company patterns (alternative to @company) +const COMPANY_PATTERNS: RegExp[] = [ + /\bbei\s+([^@#]+?)(?=\s+(?:@|#|\+|[a-zA-Z0-9._%+-]+@)|$)/i, + /\bvon\s+([^@#]+?)(?=\s+(?:@|#|\+|[a-zA-Z0-9._%+-]+@)|$)/i, +]; + +/** + * Extract email from text + */ +function extractEmail(text: string): { email?: string; remaining: string } { + const match = text.match(EMAIL_PATTERN); + if (match) { + return { + email: match[1], + remaining: text.replace(EMAIL_PATTERN, '').trim(), + }; + } + return { email: undefined, remaining: text }; +} + +/** + * Extract phone number from text + */ +function extractPhone(text: string): { phone?: string; remaining: string } { + for (const pattern of PHONE_PATTERNS) { + const match = text.match(pattern); + if (match) { + return { + phone: match[0].trim(), + remaining: text.replace(pattern, '').trim(), + }; + } + } + return { phone: undefined, remaining: text }; +} + +/** + * Extract company from text (bei/von patterns) + */ +function extractCompanyPattern(text: string): { company?: string; remaining: string } { + for (const pattern of COMPANY_PATTERNS) { + const match = text.match(pattern); + if (match) { + return { + company: match[1].trim(), + remaining: text.replace(pattern, '').trim(), + }; + } + } + return { company: undefined, remaining: text }; +} + +/** + * Extract first and last name from display name + */ +function parseNames(displayName: string): { firstName?: string; lastName?: string } { + const parts = displayName.trim().split(/\s+/); + + if (parts.length === 0) { + return {}; + } + + if (parts.length === 1) { + return { firstName: parts[0] }; + } + + // First part is first name, rest is last name + return { + firstName: parts[0], + lastName: parts.slice(1).join(' '), + }; +} + +/** + * Parse natural language contact input + * + * Examples: + * - "Max Mustermann @ACME Corp max@example.com #kunde #wichtig" + * - "Anna Schmidt bei Google +49 123 456789" + * - "Peter MĂŒller peter@mail.de #privat" + */ +export function parseContactInput(input: string): ParsedContact { + let text = input.trim(); + + // Extract tags first (#tag1 #tag2) + const tagsResult = extractTags(text); + text = tagsResult.remaining; + const tagNames = tagsResult.value || []; + + // Extract company via @CompanyName + const atRefResult = extractAtReference(text); + text = atRefResult.remaining; + let company = atRefResult.value; + + // If no @company, try bei/von patterns + if (!company) { + const companyPatternResult = extractCompanyPattern(text); + text = companyPatternResult.remaining; + company = companyPatternResult.company; + } + + // Extract email + const emailResult = extractEmail(text); + text = emailResult.remaining; + const email = emailResult.email; + + // Extract phone + const phoneResult = extractPhone(text); + text = phoneResult.remaining; + const phone = phoneResult.phone; + + // Clean up multiple spaces and get display name + const displayName = text.replace(/\s+/g, ' ').trim(); + + // Parse first and last name + const { firstName, lastName } = parseNames(displayName); + + return { + displayName, + firstName, + lastName, + company, + email, + phone, + tagNames, + }; +} + +/** + * Resolve tag names to IDs + */ +export function resolveContactIds(parsed: ParsedContact, tags: Tag[]): ParsedContactWithIds { + const tagIds: string[] = []; + + // Find tags by name (case-insensitive) + for (const tagName of parsed.tagNames) { + const tag = tags.find((t) => t.name.toLowerCase() === tagName.toLowerCase()); + if (tag) { + tagIds.push(tag.id); + } + } + + return { + displayName: parsed.displayName, + firstName: parsed.firstName, + lastName: parsed.lastName, + company: parsed.company, + email: parsed.email, + phone: parsed.phone, + tagIds, + }; +} + +/** + * Format parsed contact for preview display + */ +export function formatParsedContactPreview(parsed: ParsedContact): string { + const parts: string[] = []; + + if (parsed.company) { + parts.push(`🏱 ${parsed.company}`); + } + + if (parsed.email) { + parts.push(`📧 ${parsed.email}`); + } + + if (parsed.phone) { + parts.push(`📞 ${parsed.phone}`); + } + + if (parsed.tagNames.length > 0) { + parts.push(`đŸ·ïž ${parsed.tagNames.join(', ')}`); + } + + return parts.join(' · '); +} diff --git a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte index fe7339282..5ac9ef360 100644 --- a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte @@ -9,6 +9,7 @@ PillDropdownItem, CommandBarItem, QuickAction, + CreatePreview, } from '@manacore/shared-ui'; import { theme } from '$lib/stores/theme'; import { authStore } from '$lib/stores/auth.svelte'; @@ -28,13 +29,21 @@ import { setLocale, supportedLocales } from '$lib/i18n'; import ContactDetailModal from '$lib/components/ContactDetailModal.svelte'; import { contactsStore } from '$lib/stores/contacts.svelte'; - import { contactsApi } from '$lib/api/contacts'; + import { contactsApi, tagsApi } from '$lib/api/contacts'; import { viewModeStore } from '$lib/stores/view-mode.svelte'; import { contactsSettings } from '$lib/stores/settings.svelte'; + import { + parseContactInput, + resolveContactIds, + formatParsedContactPreview, + } from '$lib/utils/contact-parser'; // Search modal state let searchModalOpen = $state(false); + // Tags state for Quick-Create + let availableTags = $state<{ id: string; name: string }[]>([]); + // Check if we're on a contact detail route const contactDetailMatch = $derived($page.url.pathname.match(/^\/contacts\/([0-9a-f-]{36})$/i)); const showContactModal = $derived(!!contactDetailMatch); @@ -102,6 +111,7 @@ { href: '/', label: 'Kontakte', icon: 'users' }, { href: '/tags', label: 'Tags', icon: 'tag' }, { href: '/favorites', label: 'Favoriten', icon: 'heart' }, + { href: '/statistics', label: 'Statistiken', icon: 'bar-chart-3' }, { href: '/network', label: 'Netzwerk', icon: 'share-2' }, { href: '/settings', label: 'Einstellungen', icon: 'settings' }, { href: '/feedback', label: 'Feedback', icon: 'chat' }, @@ -193,6 +203,47 @@ goto(`/contacts/${item.id}`); } + // CommandBar Quick-Create handlers + function handleCommandBarParseCreate(query: string): CreatePreview | null { + if (!query.trim()) return null; + + const parsed = parseContactInput(query); + if (!parsed.displayName) return null; + + return { + title: parsed.displayName, + subtitle: formatParsedContactPreview(parsed), + }; + } + + async function handleCommandBarCreate(query: string): Promise { + const parsed = parseContactInput(query); + if (!parsed.displayName) return; + + // Resolve tag names to IDs + const resolved = resolveContactIds(parsed, availableTags); + + try { + const contact = await contactsStore.createContact({ + displayName: resolved.displayName, + firstName: resolved.firstName, + lastName: resolved.lastName, + company: resolved.company, + email: resolved.email, + phone: resolved.phone, + }); + + // Add tags to the created contact + if (resolved.tagIds.length > 0 && contact) { + for (const tagId of resolved.tagIds) { + await tagsApi.addToContact(tagId, contact.id); + } + } + } catch (e) { + console.error('Failed to create contact:', e); + } + } + // CommandBar quick actions const commandBarQuickActions: QuickAction[] = [ { @@ -214,9 +265,17 @@ return; } - // Load user settings + // Load user settings and tags await userSettings.load(); + // Load tags for Quick-Create + try { + const tagsResult = await tagsApi.list(); + availableTags = (tagsResult.tags || []).map((t) => ({ id: t.id, name: t.name })); + } catch (e) { + console.error('Failed to load tags:', e); + } + // Initialize contacts settings and view mode contactsSettings.initialize(); viewModeStore.initialize(); @@ -302,9 +361,13 @@ onSearch={handleCommandBarSearch} onSelect={handleCommandBarSelect} quickActions={commandBarQuickActions} - placeholder="Kontakt suchen..." + placeholder="Kontakt suchen oder erstellen..." emptyText="Keine Kontakte gefunden" searchingText="Suche..." + onCreate={handleCommandBarCreate} + onParseCreate={handleCommandBarParseCreate} + createText="Als Kontakt erstellen" + createShortcut="⌘↔" /> diff --git a/apps/todo/apps/web/package.json b/apps/todo/apps/web/package.json index 4d6119404..5a4f6211b 100644 --- a/apps/todo/apps/web/package.json +++ b/apps/todo/apps/web/package.json @@ -30,6 +30,7 @@ }, "dependencies": { "@manacore/shared-auth": "workspace:*", + "@manacore/shared-utils": "workspace:*", "@manacore/shared-tags": "workspace:*", "@manacore/shared-auth-ui": "workspace:*", "@manacore/shared-branding": "workspace:*", diff --git a/apps/todo/apps/web/src/lib/utils/task-parser.ts b/apps/todo/apps/web/src/lib/utils/task-parser.ts index 399e08504..609b457de 100644 --- a/apps/todo/apps/web/src/lib/utils/task-parser.ts +++ b/apps/todo/apps/web/src/lib/utils/task-parser.ts @@ -1,17 +1,18 @@ +/** + * Task Parser for Todo App + * + * Extends the base parser with task-specific patterns: + * - Priority: !hoch, !!, !!!, !dringend + * - Project: @ProjectName + */ + import { - addDays, - nextMonday, - nextTuesday, - nextWednesday, - nextThursday, - nextFriday, - nextSaturday, - nextSunday, - setHours, - setMinutes, - parse, -} from 'date-fns'; -import { de } from 'date-fns/locale'; + parseBaseInput, + extractAtReference, + combineDateAndTime, + formatDatePreview, + formatTimePreview, +} from '@manacore/shared-utils'; import type { TaskPriority } from '@todo/shared'; export interface ParsedTask { @@ -40,7 +41,7 @@ export interface ParsedTaskWithIds { labelIds: string[]; } -// Priority patterns +// Priority patterns (task-specific) const PRIORITY_PATTERNS: { pattern: RegExp; priority: TaskPriority }[] = [ { pattern: /!{3,}|!dringend|!urgent/i, priority: 'urgent' }, { pattern: /!{2}|!hoch|!high/i, priority: 'high' }, @@ -48,128 +49,54 @@ const PRIORITY_PATTERNS: { pattern: RegExp; priority: TaskPriority }[] = [ { pattern: /!niedrig|!low/i, priority: 'low' }, ]; -// Date patterns (German) -const DATE_PATTERNS: { pattern: RegExp; getDate: () => Date }[] = [ - { pattern: /\bheute\b/i, getDate: () => new Date() }, - { pattern: /\bmorgen\b/i, getDate: () => addDays(new Date(), 1) }, - { pattern: /\bĂŒbermorgen\b/i, getDate: () => addDays(new Date(), 2) }, - { pattern: /\bin\s*(\d+)\s*tage?n?\b/i, getDate: () => new Date() }, // Handled specially - { pattern: /\bnĂ€chste[nr]?\s*woche\b/i, getDate: () => addDays(new Date(), 7) }, - { pattern: /\bnĂ€chste[nr]?\s*montag\b/i, getDate: () => nextMonday(new Date()) }, - { pattern: /\bnĂ€chste[nr]?\s*dienstag\b/i, getDate: () => nextTuesday(new Date()) }, - { pattern: /\bnĂ€chste[nr]?\s*mittwoch\b/i, getDate: () => nextWednesday(new Date()) }, - { pattern: /\bnĂ€chste[nr]?\s*donnerstag\b/i, getDate: () => nextThursday(new Date()) }, - { pattern: /\bnĂ€chste[nr]?\s*freitag\b/i, getDate: () => nextFriday(new Date()) }, - { pattern: /\bnĂ€chste[nr]?\s*samstag\b/i, getDate: () => nextSaturday(new Date()) }, - { pattern: /\bnĂ€chste[nr]?\s*sonntag\b/i, getDate: () => nextSunday(new Date()) }, - { pattern: /\bmontag\b/i, getDate: () => nextMonday(new Date()) }, - { pattern: /\bdienstag\b/i, getDate: () => nextTuesday(new Date()) }, - { pattern: /\bmittwoch\b/i, getDate: () => nextWednesday(new Date()) }, - { pattern: /\bdonnerstag\b/i, getDate: () => nextThursday(new Date()) }, - { pattern: /\bfreitag\b/i, getDate: () => nextFriday(new Date()) }, - { pattern: /\bsamstag\b/i, getDate: () => nextSaturday(new Date()) }, - { pattern: /\bsonntag\b/i, getDate: () => nextSunday(new Date()) }, -]; - -// Time pattern -const TIME_PATTERN = /\b(?:um\s*)?(\d{1,2})(?::(\d{2}))?\s*(?:uhr)?\b/i; - -// Specific date pattern (DD.MM. or DD.MM.YYYY) -const SPECIFIC_DATE_PATTERN = /\b(\d{1,2})\.(\d{1,2})\.?(\d{2,4})?\b/; +/** + * Extract priority from text + */ +function extractPriority(text: string): { priority?: TaskPriority; remaining: string } { + for (const { pattern, priority } of PRIORITY_PATTERNS) { + if (pattern.test(text)) { + return { + priority, + remaining: text.replace(pattern, '').trim(), + }; + } + } + return { priority: undefined, remaining: text }; +} /** * Parse natural language task input + * + * Examples: + * - "Meeting morgen 14 Uhr !hoch @Arbeit #wichtig" + * - "Einkaufen heute #privat" + * - "Report in 3 Tagen !!" */ export function parseTaskInput(input: string): ParsedTask { let text = input.trim(); - let dueDate: Date | undefined; - let priority: TaskPriority | undefined; - let projectName: string | undefined; - const labelNames: string[] = []; - // Extract priority (!hoch, !!, etc.) - for (const { pattern, priority: p } of PRIORITY_PATTERNS) { - if (pattern.test(text)) { - priority = p; - text = text.replace(pattern, '').trim(); - break; - } - } + // Extract priority first (task-specific) + const priorityResult = extractPriority(text); + text = priorityResult.remaining; + const priority = priorityResult.priority; - // Extract project (@ProjectName) - const projectMatch = text.match(/@(\S+)/); - if (projectMatch) { - projectName = projectMatch[1]; - text = text.replace(/@\S+/, '').trim(); - } + // Extract project (@ProjectName) - task-specific + const projectResult = extractAtReference(text); + text = projectResult.remaining; + const projectName = projectResult.value; - // Extract labels (#label1 #label2) - const labelRegex = /#(\S+)/g; - let labelMatch; - while ((labelMatch = labelRegex.exec(text)) !== null) { - labelNames.push(labelMatch[1]); - } - text = text.replace(/#\S+/g, '').trim(); + // Use base parser for common patterns (date, time, tags) + const base = parseBaseInput(text); - // Extract specific date (DD.MM. or DD.MM.YYYY) - const specificDateMatch = text.match(SPECIFIC_DATE_PATTERN); - if (specificDateMatch) { - const day = parseInt(specificDateMatch[1], 10); - const month = parseInt(specificDateMatch[2], 10) - 1; - const year = specificDateMatch[3] - ? parseInt(specificDateMatch[3], 10) < 100 - ? 2000 + parseInt(specificDateMatch[3], 10) - : parseInt(specificDateMatch[3], 10) - : new Date().getFullYear(); - - dueDate = new Date(year, month, day); - text = text.replace(SPECIFIC_DATE_PATTERN, '').trim(); - } - - // Extract relative date (heute, morgen, nĂ€chsten Montag, etc.) - if (!dueDate) { - // Special handling for "in X Tagen" - const inDaysMatch = text.match(/\bin\s*(\d+)\s*tage?n?\b/i); - if (inDaysMatch) { - const days = parseInt(inDaysMatch[1], 10); - dueDate = addDays(new Date(), days); - text = text.replace(/\bin\s*\d+\s*tage?n?\b/i, '').trim(); - } else { - for (const { pattern, getDate } of DATE_PATTERNS) { - if (pattern.test(text)) { - dueDate = getDate(); - text = text.replace(pattern, '').trim(); - break; - } - } - } - } - - // Extract time (um 14 Uhr, 14:00, etc.) - const timeMatch = text.match(TIME_PATTERN); - if (timeMatch && dueDate) { - const hours = parseInt(timeMatch[1], 10); - const minutes = timeMatch[2] ? parseInt(timeMatch[2], 10) : 0; - dueDate = setHours(setMinutes(dueDate, minutes), hours); - text = text.replace(TIME_PATTERN, '').trim(); - } else if (timeMatch && !dueDate) { - // Time without date = today - dueDate = new Date(); - const hours = parseInt(timeMatch[1], 10); - const minutes = timeMatch[2] ? parseInt(timeMatch[2], 10) : 0; - dueDate = setHours(setMinutes(dueDate, minutes), hours); - text = text.replace(TIME_PATTERN, '').trim(); - } - - // Clean up multiple spaces - const title = text.replace(/\s+/g, ' ').trim(); + // Combine date and time + const dueDate = combineDateAndTime(base.date, base.time); return { - title, + title: base.title, dueDate, priority, projectName, - labelNames, + labelNames: base.tagNames, }; } @@ -218,24 +145,17 @@ export function formatParsedTaskPreview(parsed: ParsedTask): string { const parts: string[] = []; if (parsed.dueDate) { - const now = new Date(); - const tomorrow = addDays(now, 1); - - if (parsed.dueDate.toDateString() === now.toDateString()) { - parts.push('📅 Heute'); - } else if (parsed.dueDate.toDateString() === tomorrow.toDateString()) { - parts.push('📅 Morgen'); - } else { - parts.push( - `📅 ${parsed.dueDate.toLocaleDateString('de-DE', { weekday: 'short', day: 'numeric', month: 'short' })}` - ); - } + let dateStr = `📅 ${formatDatePreview(parsed.dueDate)}`; // Add time if not midnight if (parsed.dueDate.getHours() !== 0 || parsed.dueDate.getMinutes() !== 0) { - parts[parts.length - 1] += - ` ${parsed.dueDate.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}`; + dateStr += ` ${formatTimePreview({ + hours: parsed.dueDate.getHours(), + minutes: parsed.dueDate.getMinutes(), + })}`; } + + parts.push(dateStr); } if (parsed.priority) { diff --git a/packages/shared-utils/src/index.ts b/packages/shared-utils/src/index.ts index f77937d38..611e1384d 100644 --- a/packages/shared-utils/src/index.ts +++ b/packages/shared-utils/src/index.ts @@ -22,3 +22,6 @@ export * from './keyboard'; // IndexedDB Cache export * from './cache'; + +// Natural Language Parsers +export * from './parsers'; diff --git a/packages/shared-utils/src/parsers/base-parser.ts b/packages/shared-utils/src/parsers/base-parser.ts new file mode 100644 index 000000000..bb5c02fcc --- /dev/null +++ b/packages/shared-utils/src/parsers/base-parser.ts @@ -0,0 +1,320 @@ +/** + * Base Natural Language Parser + * + * Shared parsing utilities for date, time, and tags across all apps. + * App-specific parsers (task-parser, event-parser, contact-parser) extend this. + */ + +import { + addDays, + nextMonday, + nextTuesday, + nextWednesday, + nextThursday, + nextFriday, + nextSaturday, + nextSunday, + setHours, + setMinutes, +} from 'date-fns'; + +export interface BaseParsedInput { + title: string; + date?: Date; + time?: { hours: number; minutes: number }; + tagNames: string[]; + rawInput: string; +} + +export interface ExtractResult { + value: T | undefined; + remaining: string; +} + +// ============================================================================ +// Date Extraction +// ============================================================================ + +interface DatePattern { + pattern: RegExp; + getDate: (match?: RegExpMatchArray) => Date; +} + +const DATE_PATTERNS: DatePattern[] = [ + { pattern: /\bheute\b/i, getDate: () => new Date() }, + { pattern: /\bmorgen\b/i, getDate: () => addDays(new Date(), 1) }, + { pattern: /\bĂŒbermorgen\b/i, getDate: () => addDays(new Date(), 2) }, + { pattern: /\bnĂ€chste[nr]?\s*woche\b/i, getDate: () => addDays(new Date(), 7) }, + { pattern: /\bnĂ€chste[nr]?\s*montag\b/i, getDate: () => nextMonday(new Date()) }, + { pattern: /\bnĂ€chste[nr]?\s*dienstag\b/i, getDate: () => nextTuesday(new Date()) }, + { pattern: /\bnĂ€chste[nr]?\s*mittwoch\b/i, getDate: () => nextWednesday(new Date()) }, + { pattern: /\bnĂ€chste[nr]?\s*donnerstag\b/i, getDate: () => nextThursday(new Date()) }, + { pattern: /\bnĂ€chste[nr]?\s*freitag\b/i, getDate: () => nextFriday(new Date()) }, + { pattern: /\bnĂ€chste[nr]?\s*samstag\b/i, getDate: () => nextSaturday(new Date()) }, + { pattern: /\bnĂ€chste[nr]?\s*sonntag\b/i, getDate: () => nextSunday(new Date()) }, + { pattern: /\bmontag\b/i, getDate: () => nextMonday(new Date()) }, + { pattern: /\bdienstag\b/i, getDate: () => nextTuesday(new Date()) }, + { pattern: /\bmittwoch\b/i, getDate: () => nextWednesday(new Date()) }, + { pattern: /\bdonnerstag\b/i, getDate: () => nextThursday(new Date()) }, + { pattern: /\bfreitag\b/i, getDate: () => nextFriday(new Date()) }, + { pattern: /\bsamstag\b/i, getDate: () => nextSaturday(new Date()) }, + { pattern: /\bsonntag\b/i, getDate: () => nextSunday(new Date()) }, +]; + +// Pattern for "in X Tagen" +const IN_DAYS_PATTERN = /\bin\s*(\d+)\s*tage?n?\b/i; + +// Pattern for specific date (DD.MM. or DD.MM.YYYY) +const SPECIFIC_DATE_PATTERN = /\b(\d{1,2})\.(\d{1,2})\.?(\d{2,4})?\b/; + +/** + * Extract date from text + */ +export function extractDate(text: string): ExtractResult { + let remaining = text; + let date: Date | undefined; + + // Try "in X Tagen" pattern first + const inDaysMatch = remaining.match(IN_DAYS_PATTERN); + if (inDaysMatch) { + const days = parseInt(inDaysMatch[1], 10); + date = addDays(new Date(), days); + remaining = remaining.replace(IN_DAYS_PATTERN, '').trim(); + return { value: date, remaining }; + } + + // Try specific date (DD.MM. or DD.MM.YYYY) + const specificDateMatch = remaining.match(SPECIFIC_DATE_PATTERN); + if (specificDateMatch) { + const day = parseInt(specificDateMatch[1], 10); + const month = parseInt(specificDateMatch[2], 10) - 1; + const year = specificDateMatch[3] + ? parseInt(specificDateMatch[3], 10) < 100 + ? 2000 + parseInt(specificDateMatch[3], 10) + : parseInt(specificDateMatch[3], 10) + : new Date().getFullYear(); + + date = new Date(year, month, day); + remaining = remaining.replace(SPECIFIC_DATE_PATTERN, '').trim(); + return { value: date, remaining }; + } + + // Try relative date patterns + for (const { pattern, getDate } of DATE_PATTERNS) { + if (pattern.test(remaining)) { + date = getDate(); + remaining = remaining.replace(pattern, '').trim(); + return { value: date, remaining }; + } + } + + return { value: undefined, remaining }; +} + +// ============================================================================ +// Time Extraction +// ============================================================================ + +// Pattern for time (um 14 Uhr, 14:00, etc.) +const TIME_PATTERN = /\b(?:um\s*)?(\d{1,2})(?::(\d{2}))?\s*(?:uhr)?\b/i; + +/** + * Extract time from text + */ +export function extractTime(text: string): ExtractResult<{ hours: number; minutes: number }> { + const match = text.match(TIME_PATTERN); + + if (match) { + const hours = parseInt(match[1], 10); + const minutes = match[2] ? parseInt(match[2], 10) : 0; + + // Validate time + if (hours >= 0 && hours <= 23 && minutes >= 0 && minutes <= 59) { + const remaining = text.replace(TIME_PATTERN, '').trim(); + return { value: { hours, minutes }, remaining }; + } + } + + return { value: undefined, remaining: text }; +} + +// ============================================================================ +// Tag Extraction +// ============================================================================ + +/** + * Extract tags (#tag1 #tag2) from text + */ +export function extractTags(text: string): ExtractResult { + const tags: string[] = []; + const tagRegex = /#(\S+)/g; + let match; + + while ((match = tagRegex.exec(text)) !== null) { + tags.push(match[1]); + } + + const remaining = text.replace(/#\S+/g, '').trim(); + return { value: tags, remaining }; +} + +// ============================================================================ +// @ Reference Extraction (Projects, Calendars, Companies) +// ============================================================================ + +/** + * Extract @reference from text + */ +export function extractAtReference(text: string): ExtractResult { + const match = text.match(/@(\S+)/); + + if (match) { + const remaining = text.replace(/@\S+/, '').trim(); + return { value: match[1], remaining }; + } + + return { value: undefined, remaining: text }; +} + +// ============================================================================ +// Combined Date + Time +// ============================================================================ + +/** + * Combine date and time into a single Date object + */ +export function combineDateAndTime( + date?: Date, + time?: { hours: number; minutes: number } +): Date | undefined { + if (!date) return undefined; + + if (time) { + return setHours(setMinutes(date, time.minutes), time.hours); + } + + return date; +} + +// ============================================================================ +// Preview Formatting +// ============================================================================ + +/** + * Format date for preview display + */ +export function formatDatePreview(date: Date): string { + const now = new Date(); + const tomorrow = addDays(now, 1); + + if (date.toDateString() === now.toDateString()) { + return 'Heute'; + } + if (date.toDateString() === tomorrow.toDateString()) { + return 'Morgen'; + } + + return date.toLocaleDateString('de-DE', { + weekday: 'short', + day: 'numeric', + month: 'short', + }); +} + +/** + * Format time for preview display + */ +export function formatTimePreview(time: { hours: number; minutes: number }): string { + return `${time.hours.toString().padStart(2, '0')}:${time.minutes.toString().padStart(2, '0')}`; +} + +/** + * Format date and time for preview + */ +export function formatDateTimePreview( + date?: Date, + time?: { hours: number; minutes: number } +): string { + if (!date) return ''; + + let result = formatDatePreview(date); + + if (time) { + result += ` ${formatTimePreview(time)}`; + } + + return result; +} + +// ============================================================================ +// Main Parser Function +// ============================================================================ + +/** + * Parse base input - extracts common patterns (date, time, tags, @reference) + * + * App-specific parsers should call this first, then extract their own patterns. + */ +export function parseBaseInput(input: string): BaseParsedInput { + let text = input.trim(); + const rawInput = text; + + // Extract tags first (they're clearly delimited) + const tagsResult = extractTags(text); + text = tagsResult.remaining; + const tagNames = tagsResult.value || []; + + // Extract date + const dateResult = extractDate(text); + text = dateResult.remaining; + const date = dateResult.value; + + // Extract time + const timeResult = extractTime(text); + text = timeResult.remaining; + const time = timeResult.value; + + // If we got time but no date, assume today + const finalDate = time && !date ? new Date() : date; + + // Clean up multiple spaces + const title = text.replace(/\s+/g, ' ').trim(); + + return { + title, + date: finalDate, + time, + tagNames, + rawInput, + }; +} + +// ============================================================================ +// Utility: Clean title from all patterns +// ============================================================================ + +/** + * Remove all recognized patterns from text to get clean title + */ +export function cleanTitle(text: string): string { + let result = text; + + // Remove tags + result = result.replace(/#\S+/g, ''); + + // Remove @references + result = result.replace(/@\S+/g, ''); + + // Remove dates + result = result.replace(IN_DAYS_PATTERN, ''); + result = result.replace(SPECIFIC_DATE_PATTERN, ''); + for (const { pattern } of DATE_PATTERNS) { + result = result.replace(pattern, ''); + } + + // Remove time + result = result.replace(TIME_PATTERN, ''); + + // Clean up + return result.replace(/\s+/g, ' ').trim(); +} diff --git a/packages/shared-utils/src/parsers/index.ts b/packages/shared-utils/src/parsers/index.ts new file mode 100644 index 000000000..b209804ef --- /dev/null +++ b/packages/shared-utils/src/parsers/index.ts @@ -0,0 +1,26 @@ +/** + * Natural Language Parsers + * + * Base parser with common patterns, extended by app-specific parsers. + */ + +export { + // Types + type BaseParsedInput, + type ExtractResult, + // Extraction functions + extractDate, + extractTime, + extractTags, + extractAtReference, + // Combination + combineDateAndTime, + // Preview formatting + formatDatePreview, + formatTimePreview, + formatDateTimePreview, + // Main parser + parseBaseInput, + // Utilities + cleanTitle, +} from './base-parser'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 41309c052..3bac16249 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -116,7 +116,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9(esbuild@0.27.0) + version: 10.4.9(esbuild@0.19.12) '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) @@ -149,7 +149,7 @@ importers: version: 0.5.21 ts-loader: specifier: ^9.5.1 - version: 9.5.4(typescript@5.9.3)(webpack@5.100.2(esbuild@0.27.0)) + version: 9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)) ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.19.1)(typescript@5.9.3) @@ -173,14 +173,14 @@ importers: version: link:../../../../packages/shared-landing-ui astro: specifier: ^5.16.0 - version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) typescript: specifier: ^5.9.2 version: 5.9.3 devDependencies: '@astrojs/tailwind': specifier: ^6.0.2 - version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + version: 6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) '@tailwindcss/typography': specifier: ^0.5.18 version: 0.5.19(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1)) @@ -189,13 +189,13 @@ importers: version: 20.19.25 eslint: specifier: ^9.0.0 - version: 9.39.1(jiti@2.6.1) + version: 9.39.1(jiti@1.21.7) eslint-config-prettier: specifier: ^9.1.0 - version: 9.1.2(eslint@9.39.1(jiti@2.6.1)) + version: 9.1.2(eslint@9.39.1(jiti@1.21.7)) eslint-plugin-astro: specifier: ^1.0.0 - version: 1.5.0(eslint@9.39.1(jiti@2.6.1)) + version: 1.5.0(eslint@9.39.1(jiti@1.21.7)) prettier: specifier: ^3.6.2 version: 3.6.2 @@ -256,6 +256,9 @@ importers: '@manacore/shared-ui': specifier: workspace:* version: link:../../../../packages/shared-ui + '@manacore/shared-utils': + specifier: workspace:* + version: link:../../../../packages/shared-utils '@neodrag/svelte': specifier: ^2.3.3 version: 2.3.3(svelte@5.44.0) @@ -537,19 +540,19 @@ importers: version: 18.3.27 '@typescript-eslint/eslint-plugin': specifier: ^7.7.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/parser': specifier: ^7.7.0 - version: 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + version: 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) dotenv: specifier: ^16.4.7 version: 16.6.1 eslint: specifier: ^9.39.1 - version: 9.39.1(jiti@1.21.7) + version: 9.39.1(jiti@2.6.1) eslint-config-universe: specifier: ^12.0.1 - version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2)(typescript@5.3.3) + version: 12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2)(typescript@5.3.3) prettier: specifier: ^3.2.5 version: 3.6.2 @@ -1377,7 +1380,7 @@ importers: version: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-router: specifier: ~6.0.15 - version: 6.0.15(qtdvwowq57almfvuwkjsocja4a) + version: 6.0.15(dux2nvtiztnejw7mxzfaajqvh4) expo-status-bar: specifier: ~3.0.8 version: 3.0.8(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -1792,7 +1795,7 @@ importers: version: 8.0.9(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-router: specifier: ~6.0.10 - version: 6.0.15(mhyeodk7fqgr5mbfi6fab2y3iy) + version: 6.0.15(qjp3usx4acoq47dkosl6pmu254) expo-secure-store: specifier: ^15.0.7 version: 15.0.7(expo@54.0.13) @@ -2238,7 +2241,7 @@ importers: version: 18.2.0(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) expo-router: specifier: ~6.0.10 - version: 6.0.15(dhhg3hnzbvo62ldg5nj2klgrdy) + version: 6.0.15(ucgv42olhsnvykdrhhfuls4dzq) expo-secure-store: specifier: ~15.0.7 version: 15.0.7(expo@54.0.12) @@ -2576,7 +2579,7 @@ importers: devDependencies: '@nestjs/cli': specifier: ^10.4.9 - version: 10.4.9(esbuild@0.19.12) + version: 10.4.9(esbuild@0.27.0) '@nestjs/schematics': specifier: ^10.2.3 version: 10.2.3(chokidar@3.6.0)(typescript@5.9.3) @@ -2597,10 +2600,10 @@ importers: version: 0.30.6 jest: specifier: ^30.2.0 - version: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + version: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)) ts-jest: specifier: ^29.2.5 - version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3) + version: 29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(typescript@5.9.3) tsx: specifier: ^4.19.4 version: 4.20.6 @@ -2652,6 +2655,9 @@ importers: '@manacore/shared-ui': specifier: workspace:* version: link:../../../../packages/shared-ui + '@manacore/shared-utils': + specifier: workspace:* + version: link:../../../../packages/shared-utils '@todo/shared': specifier: workspace:* version: link:../../packages/shared @@ -2903,7 +2909,7 @@ importers: version: 17.0.7(expo@54.0.25)(react@19.1.0) expo-router: specifier: ~6.0.8 - version: 6.0.15(skjv6vjhiyv7rnm556zyu7suye) + version: 6.0.15(5e7ih2rh6mb55wruwvjljgzihq) expo-sharing: specifier: ~14.0.7 version: 14.0.7(expo@54.0.25) @@ -2973,7 +2979,7 @@ importers: version: 5.4.3(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) '@testing-library/react-native': specifier: ^13.3.3 - version: 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + version: 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) '@types/jest': specifier: ^30.0.0 version: 30.0.0 @@ -3000,10 +3006,10 @@ importers: version: 4.0.3 jest: specifier: ^30.2.0 - version: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + version: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)) jest-expo: specifier: ^54.0.12 - version: 54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) + version: 54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) prettier: specifier: ^3.2.5 version: 3.6.2 @@ -6733,7 +6739,7 @@ packages: '@expo/bunyan@4.0.1': resolution: {integrity: sha512-+Lla7nYSiHZirgK+U/uYzsLv/X+HaJienbD5AKX1UQZHYfWaP+9uuQluRB4GrEVWF0GZ7vEVp/jzaOT9k/SQlg==} - engines: {node: '>=0.10.0'} + engines: {'0': node >=0.10.0} '@expo/cli@0.22.26': resolution: {integrity: sha512-I689wc8Fn/AX7aUGiwrh3HnssiORMJtR2fpksX+JIe8Cj/EDleblYMSwRPd0025wrwOV9UN1KM/RuEt/QjCS3Q==} @@ -19912,6 +19918,16 @@ snapshots: transitivePeerDependencies: - ts-node + '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': + dependencies: + astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) + autoprefixer: 10.4.22(postcss@8.5.6) + postcss: 8.5.6 + postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + tailwindcss: 3.4.18(tsx@4.20.6)(yaml@2.8.1) + transitivePeerDependencies: + - ts-node + '@astrojs/tailwind@6.0.2(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.20.6)(yaml@2.8.1))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': dependencies: astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1) @@ -22271,7 +22287,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(dhhg3hnzbvo62ldg5nj2klgrdy) + expo-router: 6.0.15(ucgv42olhsnvykdrhhfuls4dzq) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -22348,7 +22364,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(mhyeodk7fqgr5mbfi6fab2y3iy) + expo-router: 6.0.15(qjp3usx4acoq47dkosl6pmu254) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -22425,7 +22441,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(yfeeu53gqw3nsmwmcr4www7tle) + expo-router: 6.0.15(5e7ih2rh6mb55wruwvjljgzihq) react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -22502,7 +22518,7 @@ snapshots: wrap-ansi: 7.0.0 ws: 8.18.3 optionalDependencies: - expo-router: 6.0.15(7opviov3h7cq44cxi7hciczi2i) + expo-router: 6.0.15(hwqworfppxvioilmgvd7t3oifm) react-native: 0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1) transitivePeerDependencies: - '@modelcontextprotocol/sdk' @@ -23945,8 +23961,9 @@ snapshots: - esbuild-register - supports-color - ts-node + optional: true - '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3))': + '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))': dependencies: '@jest/console': 30.2.0 '@jest/pattern': 30.0.1 @@ -23961,7 +23978,7 @@ snapshots: exit-x: 0.2.2 graceful-fs: 4.2.11 jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)) jest-haste-map: 30.2.0 jest-message-util: 30.2.0 jest-regex-util: 30.0.1 @@ -23981,7 +23998,6 @@ snapshots: - esbuild-register - supports-color - ts-node - optional: true '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3))': dependencies: @@ -24019,79 +24035,6 @@ snapshots: - supports-color - ts-node - '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3))': - dependencies: - '@jest/console': 30.2.0 - '@jest/pattern': 30.0.1 - '@jest/reporters': 30.2.0 - '@jest/test-result': 30.2.0 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 - '@types/node': 22.19.1 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 4.3.1 - exit-x: 0.2.2 - graceful-fs: 4.2.11 - jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) - jest-haste-map: 30.2.0 - jest-message-util: 30.2.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.2.0 - jest-resolve-dependencies: 30.2.0 - jest-runner: 30.2.0 - jest-runtime: 30.2.0 - jest-snapshot: 30.2.0 - jest-util: 30.2.0 - jest-validate: 30.2.0 - jest-watcher: 30.2.0 - micromatch: 4.0.8 - pretty-format: 30.2.0 - slash: 3.0.0 - transitivePeerDependencies: - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - optional: true - - '@jest/core@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))': - dependencies: - '@jest/console': 30.2.0 - '@jest/pattern': 30.0.1 - '@jest/reporters': 30.2.0 - '@jest/test-result': 30.2.0 - '@jest/transform': 30.2.0 - '@jest/types': 30.2.0 - '@types/node': 22.19.1 - ansi-escapes: 4.3.2 - chalk: 4.1.2 - ci-info: 4.3.1 - exit-x: 0.2.2 - graceful-fs: 4.2.11 - jest-changed-files: 30.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) - jest-haste-map: 30.2.0 - jest-message-util: 30.2.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.2.0 - jest-resolve-dependencies: 30.2.0 - jest-runner: 30.2.0 - jest-runtime: 30.2.0 - jest-snapshot: 30.2.0 - jest-util: 30.2.0 - jest-validate: 30.2.0 - jest-watcher: 30.2.0 - micromatch: 4.0.8 - pretty-format: 30.2.0 - slash: 3.0.0 - transitivePeerDependencies: - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - '@jest/create-cache-key-function@29.7.0': dependencies: '@jest/types': 29.6.3 @@ -27373,19 +27316,6 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 - '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - jest-matcher-utils: 30.2.0 - picocolors: 1.1.1 - pretty-format: 30.2.0 - react: 19.1.0 - react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) - react-test-renderer: 19.1.0(react@19.1.0) - redent: 3.0.0 - optionalDependencies: - jest: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) - optional: true - '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 @@ -27399,7 +27329,7 @@ snapshots: jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) optional: true - '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 @@ -27409,10 +27339,22 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 optionalDependencies: - jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) + jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)) + + '@testing-library/react-native@13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + dependencies: + jest-matcher-utils: 30.2.0 + picocolors: 1.1.1 + pretty-format: 30.2.0 + react: 19.1.0 + react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-test-renderer: 19.1.0(react@19.1.0) + redent: 3.0.0 + optionalDependencies: + jest: 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) optional: true - '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react-test-renderer@19.1.0(react@18.3.1))(react@18.3.1)': + '@testing-library/react-native@13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react-test-renderer@19.1.0(react@18.3.1))(react@18.3.1)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 @@ -27422,22 +27364,10 @@ snapshots: react-test-renderer: 19.1.0(react@18.3.1) redent: 3.0.0 optionalDependencies: - jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) + jest: 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) optional: true - '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': - dependencies: - jest-matcher-utils: 30.2.0 - picocolors: 1.1.1 - pretty-format: 30.2.0 - react: 19.1.0 - react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) - react-test-renderer: 19.1.0(react@19.1.0) - redent: 3.0.0 - optionalDependencies: - jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) - - '@testing-library/react-native@13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': + '@testing-library/react-native@13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0)': dependencies: jest-matcher-utils: 30.2.0 picocolors: 1.1.1 @@ -27447,7 +27377,7 @@ snapshots: react-test-renderer: 19.1.0(react@19.1.0) redent: 3.0.0 optionalDependencies: - jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest: 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) optional: true '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': @@ -27928,16 +27858,16 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/scope-manager': 6.21.0 - '@typescript-eslint/type-utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/type-utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -27986,15 +27916,15 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/parser': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/type-utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) graphemer: 1.4.0 ignore: 5.3.2 natural-compare: 1.4.0 @@ -28086,14 +28016,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 6.21.0 debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: @@ -28125,14 +28055,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/parser@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) '@typescript-eslint/visitor-keys': 7.18.0 debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) optionalDependencies: typescript: 5.3.3 transitivePeerDependencies: @@ -28258,12 +28188,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/type-utils@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) - '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/utils': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 1.4.3(typescript@5.3.3) optionalDependencies: typescript: 5.3.3 @@ -28294,12 +28224,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/type-utils@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/type-utils@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) - '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/utils': 7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) debug: 4.4.3 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 1.4.3(typescript@5.3.3) optionalDependencies: typescript: 5.3.3 @@ -28481,15 +28411,15 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/utils@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) '@types/json-schema': 7.0.15 '@types/semver': 7.7.1 '@typescript-eslint/scope-manager': 6.21.0 '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) semver: 7.7.3 transitivePeerDependencies: - supports-color @@ -28520,13 +28450,13 @@ snapshots: - supports-color - typescript - '@typescript-eslint/utils@7.18.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3)': + '@typescript-eslint/utils@7.18.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) '@typescript-eslint/scope-manager': 7.18.0 '@typescript-eslint/types': 7.18.0 '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) transitivePeerDependencies: - supports-color - typescript @@ -29327,6 +29257,108 @@ snapshots: transitivePeerDependencies: - supports-color + astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): + dependencies: + '@astrojs/compiler': 2.13.0 + '@astrojs/internal-helpers': 0.7.5 + '@astrojs/markdown-remark': 6.3.9 + '@astrojs/telemetry': 3.3.0 + '@capsizecss/unpack': 3.0.1 + '@oslojs/encoding': 1.1.0 + '@rollup/pluginutils': 5.3.0(rollup@4.53.3) + acorn: 8.15.0 + aria-query: 5.3.2 + axobject-query: 4.1.0 + boxen: 8.0.1 + ci-info: 4.3.1 + clsx: 2.1.1 + common-ancestor-path: 1.0.1 + cookie: 1.1.0 + cssesc: 3.0.0 + debug: 4.4.3 + deterministic-object-hash: 2.0.2 + devalue: 5.5.0 + diff: 5.2.0 + dlv: 1.1.3 + dset: 3.1.4 + es-module-lexer: 1.7.0 + esbuild: 0.25.12 + estree-walker: 3.0.3 + flattie: 1.1.1 + fontace: 0.3.1 + github-slugger: 2.0.0 + html-escaper: 3.0.3 + http-cache-semantics: 4.2.0 + import-meta-resolve: 4.2.0 + js-yaml: 4.1.1 + magic-string: 0.30.21 + magicast: 0.5.1 + mrmime: 2.0.1 + neotraverse: 0.6.18 + p-limit: 6.2.0 + p-queue: 8.1.1 + package-manager-detector: 1.5.0 + piccolore: 0.1.3 + picomatch: 4.0.3 + prompts: 2.4.2 + rehype: 13.0.2 + semver: 7.7.3 + shiki: 3.15.0 + smol-toml: 1.5.2 + svgo: 4.0.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tsconfck: 3.1.6(typescript@5.9.3) + ultrahtml: 1.6.0 + unifont: 0.6.0 + unist-util-visit: 5.0.0 + unstorage: 1.17.3(@netlify/blobs@10.4.1)(ioredis@5.8.2) + vfile: 6.0.3 + vite: 6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + vitefu: 1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)) + xxhash-wasm: 1.1.0 + yargs-parser: 21.1.1 + yocto-spinner: 0.2.3 + zod: 3.25.76 + zod-to-json-schema: 3.25.0(zod@3.25.76) + zod-to-ts: 1.2.0(typescript@5.9.3)(zod@3.25.76) + optionalDependencies: + sharp: 0.34.5 + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@netlify/blobs' + - '@planetscale/database' + - '@types/node' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - db0 + - idb-keyval + - ioredis + - jiti + - less + - lightningcss + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - typescript + - uploadthing + - yaml + astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1): dependencies: '@astrojs/compiler': 2.13.0 @@ -31612,6 +31644,11 @@ snapshots: optionalDependencies: source-map: 0.6.1 + eslint-compat-utils@0.6.5(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + semver: 7.7.3 + eslint-compat-utils@0.6.5(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -31622,9 +31659,9 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 1.0.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) globals: 16.5.0 @@ -31639,9 +31676,9 @@ snapshots: '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-expo: 0.1.4(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) eslint-plugin-react-hooks: 5.2.0(eslint@9.39.1(jiti@2.6.1)) globals: 16.5.0 @@ -31659,14 +31696,14 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) + eslint-config-prettier@9.1.2(eslint@9.39.1(jiti@1.21.7)): + dependencies: + eslint: 9.39.1(jiti@1.21.7) + eslint-config-prettier@9.1.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -31691,17 +31728,17 @@ snapshots: - supports-color - typescript - eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2)(typescript@5.3.3): + eslint-config-universe@12.1.0(@types/eslint@9.6.1)(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2)(typescript@5.3.3): dependencies: - '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) - eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-node: 11.1.0(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2) - eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-react-hooks: 4.6.2(eslint@9.39.1(jiti@1.21.7)) + '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + eslint: 9.39.1(jiti@2.6.1) + eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-node: 11.1.0(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-prettier: 5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2) + eslint-plugin-react: 7.37.5(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-react-hooks: 4.6.2(eslint@9.39.1(jiti@2.6.1)) optionalDependencies: prettier: 3.6.2 transitivePeerDependencies: @@ -31739,7 +31776,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)): + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@nolyfill/is-core-module': 1.0.39 debug: 4.4.3 @@ -31750,7 +31787,22 @@ snapshots: tinyglobby: 0.2.15 unrs-resolver: 1.11.1 optionalDependencies: - eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)) + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): + dependencies: + '@nolyfill/is-core-module': 1.0.39 + debug: 4.4.3 + eslint: 9.39.1(jiti@2.6.1) + get-tsconfig: 4.13.0 + is-bun-module: 2.0.0 + stable-hash: 0.0.5 + tinyglobby: 0.2.15 + unrs-resolver: 1.11.1 + optionalDependencies: + eslint-plugin-import: 2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color @@ -31764,12 +31816,12 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@1.21.7)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) - eslint: 9.39.1(jiti@1.21.7) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) + eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 transitivePeerDependencies: - supports-color @@ -31784,25 +31836,39 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.1(jiti@2.6.1)) + eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) + transitivePeerDependencies: + - supports-color + + eslint-plugin-astro@1.5.0(eslint@9.39.1(jiti@1.21.7)): + dependencies: + '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7)) + '@jridgewell/sourcemap-codec': 1.5.5 + '@typescript-eslint/types': 8.48.0 + astro-eslint-parser: 1.2.2 + eslint: 9.39.1(jiti@1.21.7) + eslint-compat-utils: 0.6.5(eslint@9.39.1(jiti@1.21.7)) + globals: 16.5.0 + postcss: 8.5.6 + postcss-selector-parser: 7.1.0 transitivePeerDependencies: - supports-color @@ -31826,12 +31892,6 @@ snapshots: eslint-utils: 2.1.0 regexpp: 3.2.0 - eslint-plugin-es@3.0.1(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-utils: 2.1.0 - regexpp: 3.2.0 - eslint-plugin-es@3.0.1(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -31885,7 +31945,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint@9.39.1(jiti@1.21.7)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -31894,9 +31954,9 @@ snapshots: array.prototype.flatmap: 1.3.3 debug: 3.2.7 doctrine: 2.1.0 - eslint: 9.39.1(jiti@1.21.7) + eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@1.21.7)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3))(eslint-import-resolver-node@0.3.9)(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -31908,7 +31968,7 @@ snapshots: string.prototype.trimend: 1.0.9 tsconfig-paths: 3.15.0 optionalDependencies: - '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@1.21.7))(typescript@5.3.3) + '@typescript-eslint/parser': 6.21.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.3.3) transitivePeerDependencies: - eslint-import-resolver-typescript - eslint-import-resolver-webpack @@ -31943,7 +32003,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -31954,7 +32014,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.8.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -31972,7 +32032,7 @@ snapshots: - eslint-import-resolver-webpack - supports-color - eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)): + eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.9 @@ -31983,7 +32043,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.39.1(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.1(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -32011,16 +32071,6 @@ snapshots: resolve: 1.22.11 semver: 6.3.1 - eslint-plugin-node@11.1.0(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-plugin-es: 3.0.1(eslint@9.39.1(jiti@1.21.7)) - eslint-utils: 2.1.0 - ignore: 5.3.2 - minimatch: 3.1.2 - resolve: 1.22.11 - semver: 6.3.1 - eslint-plugin-node@11.1.0(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -32051,16 +32101,6 @@ snapshots: '@types/eslint': 9.6.1 eslint-config-prettier: 8.10.2(eslint@8.57.1) - eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@1.21.7)))(eslint@9.39.1(jiti@1.21.7))(prettier@3.6.2): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - prettier: 3.6.2 - prettier-linter-helpers: 1.0.0 - synckit: 0.11.11 - optionalDependencies: - '@types/eslint': 9.6.1 - eslint-config-prettier: 8.10.2(eslint@9.39.1(jiti@1.21.7)) - eslint-plugin-prettier@5.5.4(@types/eslint@9.6.1)(eslint-config-prettier@8.10.2(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -32085,10 +32125,6 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-plugin-react-hooks@4.6.2(eslint@9.39.1(jiti@1.21.7)): - dependencies: - eslint: 9.39.1(jiti@1.21.7) - eslint-plugin-react-hooks@4.6.2(eslint@9.39.1(jiti@2.6.1)): dependencies: eslint: 9.39.1(jiti@2.6.1) @@ -32119,28 +32155,6 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@1.21.7)): - dependencies: - array-includes: 3.1.9 - array.prototype.findlast: 1.2.5 - array.prototype.flatmap: 1.3.3 - array.prototype.tosorted: 1.1.4 - doctrine: 2.1.0 - es-iterator-helpers: 1.2.1 - eslint: 9.39.1(jiti@1.21.7) - estraverse: 5.3.0 - hasown: 2.0.2 - jsx-ast-utils: 3.3.5 - minimatch: 3.1.2 - object.entries: 1.1.9 - object.fromentries: 2.0.8 - object.values: 1.2.1 - prop-types: 15.8.1 - resolve: 2.0.0-next.5 - semver: 6.3.1 - string.prototype.matchall: 4.0.12 - string.prototype.repeat: 1.0.0 - eslint-plugin-react@7.37.5(eslint@9.39.1(jiti@2.6.1)): dependencies: array-includes: 3.1.9 @@ -33221,7 +33235,99 @@ snapshots: - react-native - supports-color - expo-router@6.0.15(7opviov3h7cq44cxi7hciczi2i): + expo-router@6.0.15(5e7ih2rh6mb55wruwvjljgzihq): + dependencies: + '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/schema-utils': 0.1.7 + '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + client-only: 0.0.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-server: 1.0.4 + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.11 + query-string: 7.1.3 + react: 19.1.0 + react-fast-compare: 3.2.2 + react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + semver: 7.6.3 + server-only: 0.0.1 + sf-symbols-typescript: 2.1.0 + shallowequal: 1.1.0 + use-latest-callback: 0.2.6(react@19.1.0) + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + optionalDependencies: + '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.0(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + react-dom: 19.1.0(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.0(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - '@types/react' + - '@types/react-dom' + - supports-color + + expo-router@6.0.15(dux2nvtiztnejw7mxzfaajqvh4): + dependencies: + '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/schema-utils': 0.1.7 + '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + client-only: 0.0.1 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-server: 1.0.4 + fast-deep-equal: 3.1.3 + invariant: 2.2.4 + nanoid: 3.3.11 + query-string: 7.1.3 + react: 19.1.0 + react-fast-compare: 3.2.2 + react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) + react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + semver: 7.6.3 + server-only: 0.0.1 + sf-symbols-typescript: 2.1.0 + shallowequal: 1.1.0 + use-latest-callback: 0.2.6(react@19.1.0) + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + optionalDependencies: + '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + react-dom: 19.1.0(react@19.1.0) + react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + - '@types/react' + - '@types/react-dom' + - supports-color + + expo-router@6.0.15(hwqworfppxvioilmgvd7t3oifm): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) '@expo/schema-utils': 0.1.7 @@ -33255,7 +33361,7 @@ snapshots: vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@18.3.27))(@types/react@18.3.27)(react-dom@19.1.0(react@18.3.1))(react@18.3.1) optionalDependencies: '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) - '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react-test-renderer@19.1.0(react@18.3.1))(react@18.3.1) + '@testing-library/react-native': 13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react-test-renderer@19.1.0(react@18.3.1))(react@18.3.1) react-dom: 19.1.0(react@18.3.1) react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@18.3.27)(react@18.3.1))(react@18.3.1) @@ -33268,98 +33374,6 @@ snapshots: - supports-color optional: true - expo-router@6.0.15(dhhg3hnzbvo62ldg5nj2klgrdy): - dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.12)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@expo/schema-utils': 0.1.7 - '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) - '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native': 7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - client-only: 0.0.1 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - expo: 54.0.12(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-constants: 18.0.10(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-linking: 8.0.9(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-server: 1.0.4 - fast-deep-equal: 3.1.3 - invariant: 2.2.4 - nanoid: 3.3.11 - query-string: 7.1.3 - react: 19.1.0 - react-fast-compare: 3.2.2 - react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) - react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-safe-area-context: 5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-screens: 4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - semver: 7.6.3 - server-only: 0.0.1 - sf-symbols-typescript: 2.1.0 - shallowequal: 1.1.0 - use-latest-callback: 0.2.6(react@19.1.0) - vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - optionalDependencies: - '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) - react-dom: 19.1.0(react@19.1.0) - react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) - transitivePeerDependencies: - - '@react-native-masked-view/masked-view' - - '@types/react' - - '@types/react-dom' - - supports-color - - expo-router@6.0.15(mhyeodk7fqgr5mbfi6fab2y3iy): - dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@expo/schema-utils': 0.1.7 - '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) - '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native': 7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - client-only: 0.0.1 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - expo: 54.0.13(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-constants: 18.0.10(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-linking: 8.0.9(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-server: 1.0.4 - fast-deep-equal: 3.1.3 - invariant: 2.2.4 - nanoid: 3.3.11 - query-string: 7.1.3 - react: 19.1.0 - react-fast-compare: 3.2.2 - react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) - react-native-is-edge-to-edge: 1.2.1(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-safe-area-context: 5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-screens: 4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - semver: 7.6.3 - server-only: 0.0.1 - sf-symbols-typescript: 2.1.0 - shallowequal: 1.1.0 - use-latest-callback: 0.2.6(react@19.1.0) - vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - optionalDependencies: - '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) - react-dom: 19.1.0(react@19.1.0) - react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) - transitivePeerDependencies: - - '@react-native-masked-view/masked-view' - - '@types/react' - - '@types/react-dom' - - supports-color - expo-router@6.0.15(nttrd3tw67nnyhowcwgdzipb5e): dependencies: '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -33407,55 +33421,9 @@ snapshots: - supports-color optional: true - expo-router@6.0.15(qtdvwowq57almfvuwkjsocja4a): + expo-router@6.0.15(qjp3usx4acoq47dkosl6pmu254): dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@expo/schema-utils': 0.1.7 - '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) - '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - '@react-navigation/bottom-tabs': 7.8.6(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native': 7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@react-navigation/native-stack': 7.8.0(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - client-only: 0.0.1 - debug: 4.4.3 - escape-string-regexp: 4.0.0 - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-server: 1.0.4 - fast-deep-equal: 3.1.3 - invariant: 2.2.4 - nanoid: 3.3.11 - query-string: 7.1.3 - react: 19.1.0 - react-fast-compare: 3.2.2 - react-native: 0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) - react-native-is-edge-to-edge: 1.2.1(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-safe-area-context: 5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-screens: 4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - semver: 7.6.3 - server-only: 0.0.1 - sf-symbols-typescript: 2.1.0 - shallowequal: 1.1.0 - use-latest-callback: 0.2.6(react@19.1.0) - vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - optionalDependencies: - '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) - react-dom: 19.1.0(react@19.1.0) - react-native-gesture-handler: 2.28.0(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.5(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) - react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) - transitivePeerDependencies: - - '@react-native-masked-view/masked-view' - - '@types/react' - - '@types/react-dom' - - supports-color - - expo-router@6.0.15(skjv6vjhiyv7rnm556zyu7suye): - dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/metro-runtime': 6.1.2(expo@54.0.13)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -33465,9 +33433,9 @@ snapshots: client-only: 0.0.1 debug: 4.4.3 escape-string-regexp: 4.0.0 - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo: 54.0.13(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.13)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-server: 1.0.4 fast-deep-equal: 3.1.3 invariant: 2.2.4 @@ -33486,11 +33454,11 @@ snapshots: use-latest-callback: 0.2.6(react@19.1.0) vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: - '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.0(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - react-native-reanimated: 4.1.0(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.5.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-web: 0.21.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0) react-server-dom-webpack: 19.0.0(react-dom@19.1.0(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)) transitivePeerDependencies: @@ -33499,9 +33467,9 @@ snapshots: - '@types/react-dom' - supports-color - expo-router@6.0.15(yfeeu53gqw3nsmwmcr4www7tle): + expo-router@6.0.15(ucgv42olhsnvykdrhhfuls4dzq): dependencies: - '@expo/metro-runtime': 6.1.2(expo@54.0.25)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + '@expo/metro-runtime': 6.1.2(expo@54.0.12)(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) '@expo/schema-utils': 0.1.7 '@radix-ui/react-slot': 1.2.0(@types/react@19.2.7)(react@19.1.0) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) @@ -33511,9 +33479,9 @@ snapshots: client-only: 0.0.1 debug: 4.4.3 escape-string-regexp: 4.0.0 - expo: 54.0.25(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - expo-constants: 18.0.10(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) - expo-linking: 8.0.9(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo: 54.0.12(@babel/core@7.28.5)(@expo/metro-runtime@6.1.2)(expo-router@6.0.15)(react-native-webview@13.12.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) + expo-constants: 18.0.10(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0)) + expo-linking: 8.0.9(expo@54.0.12)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) expo-server: 1.0.4 fast-deep-equal: 3.1.3 invariant: 2.2.4 @@ -33533,7 +33501,7 @@ snapshots: vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) optionalDependencies: '@react-navigation/drawer': 7.7.4(@react-navigation/native@7.1.21(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-gesture-handler@2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-reanimated@4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.6.2(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native-screens@4.16.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) - '@testing-library/react-native': 13.3.3(jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) + '@testing-library/react-native': 13.3.3(jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react-test-renderer@19.1.0(react@19.1.0))(react@19.1.0) react-dom: 19.1.0(react@19.1.0) react-native-gesture-handler: 2.28.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) react-native-reanimated: 4.1.5(@babel/core@7.28.5)(react-native-worklets@0.6.1(@babel/core@7.28.5)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0) @@ -33544,7 +33512,6 @@ snapshots: - '@types/react' - '@types/react-dom' - supports-color - optional: true expo-secure-store@15.0.7(expo@54.0.12): dependencies: @@ -35596,15 +35563,15 @@ snapshots: - supports-color - ts-node - jest-cli@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): + jest-cli@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -35616,15 +35583,15 @@ snapshots: - ts-node optional: true - jest-cli@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + jest-cli@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-config: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -35654,15 +35621,34 @@ snapshots: - supports-color - ts-node - jest-cli@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)): + jest-cli@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) '@jest/test-result': 30.2.0 '@jest/types': 30.2.0 chalk: 4.1.2 exit-x: 0.2.2 import-local: 3.2.0 - jest-config: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) + jest-config: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)) + jest-util: 30.2.0 + jest-validate: 30.2.0 + yargs: 17.7.2 + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jest-cli@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)): + dependencies: + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + '@jest/test-result': 30.2.0 + '@jest/types': 30.2.0 + chalk: 4.1.2 + exit-x: 0.2.2 + import-local: 3.2.0 + jest-config: 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) jest-util: 30.2.0 jest-validate: 30.2.0 yargs: 17.7.2 @@ -35674,25 +35660,6 @@ snapshots: - ts-node optional: true - jest-cli@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): - dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) - '@jest/test-result': 30.2.0 - '@jest/types': 30.2.0 - chalk: 4.1.2 - exit-x: 0.2.2 - import-local: 3.2.0 - jest-config: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) - jest-util: 30.2.0 - jest-validate: 30.2.0 - yargs: 17.7.2 - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - jest-config@29.7.0(@types/node@22.19.1)(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 @@ -35786,41 +35753,6 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): - dependencies: - '@babel/core': 7.28.5 - '@jest/get-type': 30.1.0 - '@jest/pattern': 30.0.1 - '@jest/test-sequencer': 30.2.0 - '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.28.5) - chalk: 4.1.2 - ci-info: 4.3.1 - deepmerge: 4.3.1 - glob: 10.5.0 - graceful-fs: 4.2.11 - jest-circus: 30.2.0 - jest-docblock: 30.2.0 - jest-environment-node: 30.2.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.2.0 - jest-runner: 30.2.0 - jest-util: 30.2.0 - jest-validate: 30.2.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 30.2.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 20.19.25 - esbuild-register: 3.6.0(esbuild@0.27.0) - ts-node: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - optional: true - jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: '@babel/core': 7.28.5 @@ -35854,8 +35786,9 @@ snapshots: transitivePeerDependencies: - babel-plugin-macros - supports-color + optional: true - jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): + jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -35884,11 +35817,9 @@ snapshots: optionalDependencies: '@types/node': 22.19.1 esbuild-register: 3.6.0(esbuild@0.27.0) - ts-node: 10.9.2(@types/node@20.19.25)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - optional: true jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: @@ -35924,76 +35855,7 @@ snapshots: - babel-plugin-macros - supports-color - jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)): - dependencies: - '@babel/core': 7.28.5 - '@jest/get-type': 30.1.0 - '@jest/pattern': 30.0.1 - '@jest/test-sequencer': 30.2.0 - '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.28.5) - chalk: 4.1.2 - ci-info: 4.3.1 - deepmerge: 4.3.1 - glob: 10.5.0 - graceful-fs: 4.2.11 - jest-circus: 30.2.0 - jest-docblock: 30.2.0 - jest-environment-node: 30.2.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.2.0 - jest-runner: 30.2.0 - jest-util: 30.2.0 - jest-validate: 30.2.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 30.2.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.19.1 - esbuild-register: 3.6.0(esbuild@0.27.0) - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.8.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - optional: true - - jest-config@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): - dependencies: - '@babel/core': 7.28.5 - '@jest/get-type': 30.1.0 - '@jest/pattern': 30.0.1 - '@jest/test-sequencer': 30.2.0 - '@jest/types': 30.2.0 - babel-jest: 30.2.0(@babel/core@7.28.5) - chalk: 4.1.2 - ci-info: 4.3.1 - deepmerge: 4.3.1 - glob: 10.5.0 - graceful-fs: 4.2.11 - jest-circus: 30.2.0 - jest-docblock: 30.2.0 - jest-environment-node: 30.2.0 - jest-regex-util: 30.0.1 - jest-resolve: 30.2.0 - jest-runner: 30.2.0 - jest-util: 30.2.0 - jest-validate: 30.2.0 - micromatch: 4.0.8 - parse-json: 5.2.0 - pretty-format: 30.2.0 - slash: 3.0.0 - strip-json-comments: 3.1.1 - optionalDependencies: - '@types/node': 22.19.1 - esbuild-register: 3.6.0(esbuild@0.27.0) - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) - transitivePeerDependencies: - - babel-plugin-macros - - supports-color - - jest-config@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)): + jest-config@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -36022,13 +35884,11 @@ snapshots: optionalDependencies: '@types/node': 24.10.1 esbuild-register: 3.6.0(esbuild@0.27.0) - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.8.3) transitivePeerDependencies: - babel-plugin-macros - supports-color - optional: true - jest-config@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): + jest-config@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: '@babel/core': 7.28.5 '@jest/get-type': 30.1.0 @@ -36055,12 +35915,11 @@ snapshots: slash: 3.0.0 strip-json-comments: 3.1.1 optionalDependencies: - '@types/node': 24.10.1 esbuild-register: 3.6.0(esbuild@0.27.0) - ts-node: 10.9.2(@types/node@24.10.1)(typescript@5.9.3) transitivePeerDependencies: - babel-plugin-macros - supports-color + optional: true jest-diff@29.7.0: dependencies: @@ -36134,7 +35993,7 @@ snapshots: jest-util: 30.2.0 jest-validate: 30.2.0 - jest-expo@54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)): + jest-expo@54.0.13(@babel/core@7.28.5)(expo@54.0.25)(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(react-dom@19.1.0(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0))(react@19.1.0)(webpack@5.100.2(esbuild@0.27.0)): dependencies: '@expo/config': 12.0.10 '@expo/json-file': 10.0.7 @@ -36145,7 +36004,7 @@ snapshots: jest-environment-jsdom: 29.7.0 jest-snapshot: 29.7.0 jest-watch-select-projects: 2.0.0 - jest-watch-typeahead: 2.2.1(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))) + jest-watch-typeahead: 2.2.1(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))) json5: 2.2.3 lodash: 4.17.21 react-native: 0.81.4(@babel/core@7.28.5)(@types/react@19.2.7)(react@19.1.0) @@ -36506,11 +36365,11 @@ snapshots: chalk: 3.0.0 prompts: 2.4.2 - jest-watch-typeahead@2.2.1(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))): + jest-watch-typeahead@2.2.1(jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))): dependencies: ansi-escapes: 6.2.1 chalk: 4.1.2 - jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) + jest: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)) jest-regex-util: 29.6.3 jest-watcher: 29.7.0 slash: 5.1.0 @@ -36584,12 +36443,12 @@ snapshots: - supports-color - ts-node - jest@30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)): + jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@20.19.25)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3)) + jest-cli: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -36598,12 +36457,12 @@ snapshots: - ts-node optional: true - jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)): + jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest-cli: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -36624,12 +36483,25 @@ snapshots: - supports-color - ts-node - jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)): + jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)): dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) '@jest/types': 30.2.0 import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3)) + jest-cli: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0)) + transitivePeerDependencies: + - '@types/node' + - babel-plugin-macros + - esbuild-register + - supports-color + - ts-node + + jest@30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)): + dependencies: + '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) + '@jest/types': 30.2.0 + import-local: 3.2.0 + jest-cli: 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0)) transitivePeerDependencies: - '@types/node' - babel-plugin-macros @@ -36638,19 +36510,6 @@ snapshots: - ts-node optional: true - jest@30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)): - dependencies: - '@jest/core': 30.2.0(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) - '@jest/types': 30.2.0 - import-local: 3.2.0 - jest-cli: 30.2.0(@types/node@24.10.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3)) - transitivePeerDependencies: - - '@types/node' - - babel-plugin-macros - - esbuild-register - - supports-color - - ts-node - jimp-compact@0.16.1: {} jiti@1.21.7: {} @@ -42078,12 +41937,12 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.19.12)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.19.12))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -42096,15 +41955,15 @@ snapshots: '@jest/transform': 30.2.0 '@jest/types': 30.2.0 babel-jest: 30.2.0(@babel/core@7.28.5) - esbuild: 0.19.12 + esbuild: 0.27.0 jest-util: 30.2.0 - ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)))(typescript@5.9.3): + ts-jest@29.4.5(@babel/core@7.28.5)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.5))(esbuild@0.27.0)(jest-util@30.2.0)(jest@30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)))(typescript@5.9.3): dependencies: bs-logger: 0.2.6 fast-json-stable-stringify: 2.1.0 handlebars: 4.7.8 - jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0))(ts-node@10.9.2(@types/node@22.19.1)(typescript@5.9.3)) + jest: 30.2.0(@types/node@22.19.1)(esbuild-register@3.6.0(esbuild@0.27.0)) json5: 2.2.3 lodash.memoize: 4.1.2 make-error: 1.3.6 @@ -42160,6 +42019,16 @@ snapshots: typescript: 5.9.3 webpack: 5.100.2 + ts-loader@9.5.4(typescript@5.9.3)(webpack@5.97.1(esbuild@0.19.12)): + dependencies: + chalk: 4.1.2 + enhanced-resolve: 5.18.3 + micromatch: 4.0.8 + semver: 7.7.3 + source-map: 0.7.6 + typescript: 5.9.3 + webpack: 5.97.1(esbuild@0.19.12) + ts-node@10.9.2(@types/node@20.19.25)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -42197,25 +42066,6 @@ snapshots: v8-compile-cache-lib: 3.0.1 yn: 3.1.1 - ts-node@10.9.2(@types/node@24.10.1)(typescript@5.8.3): - dependencies: - '@cspotcode/source-map-support': 0.8.1 - '@tsconfig/node10': 1.0.12 - '@tsconfig/node12': 1.0.11 - '@tsconfig/node14': 1.0.3 - '@tsconfig/node16': 1.0.4 - '@types/node': 24.10.1 - acorn: 8.15.0 - acorn-walk: 8.3.4 - arg: 4.1.3 - create-require: 1.1.1 - diff: 4.0.2 - make-error: 1.3.6 - typescript: 5.8.3 - v8-compile-cache-lib: 3.0.1 - yn: 3.1.1 - optional: true - ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -42785,6 +42635,23 @@ snapshots: lightningcss: 1.30.2 terser: 5.44.1 + vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): + dependencies: + esbuild: 0.25.12 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.53.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 20.19.25 + fsevents: 2.3.3 + jiti: 1.21.7 + lightningcss: 1.30.2 + terser: 5.44.1 + tsx: 4.20.6 + yaml: 2.8.1 + vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1): dependencies: esbuild: 0.25.12 @@ -42887,6 +42754,10 @@ snapshots: tsx: 4.20.6 yaml: 2.8.1 + vitefu@1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): + optionalDependencies: + vite: 6.4.1(@types/node@20.19.25)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) + vitefu@1.1.1(vite@6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1)): optionalDependencies: vite: 6.4.1(@types/node@20.19.25)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.20.6)(yaml@2.8.1) From 74604b09d39d43ea2580831ac07d9fbc505dd9cc Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:38:11 +0100 Subject: [PATCH 08/26] feat(todo): add deep validation for DTOs and completed task pagination MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Validation: - Create SubtaskDto with title MinLength/MaxLength, order validation - Create TaskMetadataDto with typed EffectiveDuration, storyPoints enum validation (Fibonacci only), funRating bounds (1-10) - Use @ValidateNested with @Type() for deep validation in CreateTaskDto Pagination: - Add offset parameter and total count to getCompletedTasks() - Return { tasks, total, hasMore } structure for frontend pagination - Enforce max limit of 100 to prevent abuse - Add 3 pagination tests (basic, offset, max enforcement) Error handling: - Add try-catch to localStorage operations in +layout.svelte - Handles private browsing mode and quota exceeded errors gracefully đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/task/__tests__/task.service.spec.ts | 45 ++++++++++++-- .../backend/src/task/dto/create-task.dto.ts | 16 +++-- .../apps/backend/src/task/dto/metadata.dto.ts | 58 +++++++++++++++++++ .../apps/backend/src/task/dto/subtask.dto.ts | 48 +++++++++++++++ .../apps/backend/src/task/task.controller.ts | 10 +++- .../apps/backend/src/task/task.service.ts | 37 +++++++++--- .../apps/web/src/routes/(app)/+layout.svelte | 38 +++++++----- 7 files changed, 218 insertions(+), 34 deletions(-) create mode 100644 apps/todo/apps/backend/src/task/dto/metadata.dto.ts create mode 100644 apps/todo/apps/backend/src/task/dto/subtask.dto.ts diff --git a/apps/todo/apps/backend/src/task/__tests__/task.service.spec.ts b/apps/todo/apps/backend/src/task/__tests__/task.service.spec.ts index 3b5ca9a5b..b5d9756fd 100644 --- a/apps/todo/apps/backend/src/task/__tests__/task.service.spec.ts +++ b/apps/todo/apps/backend/src/task/__tests__/task.service.spec.ts @@ -5,6 +5,9 @@ import { ProjectService } from '../../project/project.service'; import { DATABASE_CONNECTION } from '../../db/database.module'; // Mock database +const mockSelectFrom = jest.fn().mockReturnThis(); +const mockSelectWhere = jest.fn(); + const mockDb = { query: { tasks: { @@ -18,6 +21,10 @@ const mockDb = { findMany: jest.fn(), }, }, + select: jest.fn().mockReturnValue({ + from: mockSelectFrom, + where: mockSelectWhere, + }), insert: jest.fn().mockReturnThis(), update: jest.fn().mockReturnThis(), delete: jest.fn().mockReturnThis(), @@ -406,7 +413,7 @@ describe('TaskService', () => { }); describe('getCompletedTasks', () => { - it('should return completed tasks with default limit', async () => { + it('should return completed tasks with pagination info', async () => { const userId = 'user-123'; const mockTasks = Array(50) .fill(null) @@ -419,13 +426,16 @@ describe('TaskService', () => { mockDb.query.tasks.findMany.mockResolvedValue(mockTasks); mockDb.query.taskLabels.findMany.mockResolvedValue([]); + mockSelectWhere.mockResolvedValue([{ count: 75 }]); const result = await service.getCompletedTasks(userId); - expect(result).toHaveLength(50); + expect(result.tasks).toHaveLength(50); + expect(result.total).toBe(75); + expect(result.hasMore).toBe(true); }); - it('should respect custom limit', async () => { + it('should respect custom limit and offset', async () => { const userId = 'user-123'; const mockTasks = Array(10) .fill(null) @@ -438,10 +448,35 @@ describe('TaskService', () => { mockDb.query.tasks.findMany.mockResolvedValue(mockTasks); mockDb.query.taskLabels.findMany.mockResolvedValue([]); + mockSelectWhere.mockResolvedValue([{ count: 25 }]); - const result = await service.getCompletedTasks(userId, 10); + const result = await service.getCompletedTasks(userId, 10, 10); - expect(result).toHaveLength(10); + expect(result.tasks).toHaveLength(10); + expect(result.total).toBe(25); + expect(result.hasMore).toBe(true); // offset 10 + limit 10 = 20 < 25 + }); + + it('should enforce max limit of 100', async () => { + const userId = 'user-123'; + const mockTasks = Array(100) + .fill(null) + .map((_, i) => ({ + id: `task-${i}`, + title: `Task ${i}`, + userId, + isCompleted: true, + })); + + mockDb.query.tasks.findMany.mockResolvedValue(mockTasks); + mockDb.query.taskLabels.findMany.mockResolvedValue([]); + mockSelectWhere.mockResolvedValue([{ count: 200 }]); + + // Request 500 tasks, should be capped at 100 + const result = await service.getCompletedTasks(userId, 500, 0); + + expect(result.tasks).toHaveLength(100); + expect(result.hasMore).toBe(true); }); }); diff --git a/apps/todo/apps/backend/src/task/dto/create-task.dto.ts b/apps/todo/apps/backend/src/task/dto/create-task.dto.ts index f266923d4..2cebd0bdd 100644 --- a/apps/todo/apps/backend/src/task/dto/create-task.dto.ts +++ b/apps/todo/apps/backend/src/task/dto/create-task.dto.ts @@ -4,13 +4,16 @@ import { IsUUID, IsEnum, IsArray, - IsObject, MaxLength, MinLength, IsDateString, IsNotEmpty, + ValidateNested, } from 'class-validator'; -import type { TaskPriority, Subtask, TaskMetadata } from '../../db/schema/tasks.schema'; +import { Type } from 'class-transformer'; +import type { TaskPriority } from '../../db/schema/tasks.schema'; +import { CreateSubtaskDto } from './subtask.dto'; +import { TaskMetadataDto } from './metadata.dto'; export class CreateTaskDto { @IsString() @@ -58,7 +61,9 @@ export class CreateTaskDto { @IsOptional() @IsArray() - subtasks?: Omit[]; + @ValidateNested({ each: true }) + @Type(() => CreateSubtaskDto) + subtasks?: CreateSubtaskDto[]; @IsOptional() @IsArray() @@ -66,6 +71,7 @@ export class CreateTaskDto { labelIds?: string[]; @IsOptional() - @IsObject() - metadata?: TaskMetadata; + @ValidateNested() + @Type(() => TaskMetadataDto) + metadata?: TaskMetadataDto; } diff --git a/apps/todo/apps/backend/src/task/dto/metadata.dto.ts b/apps/todo/apps/backend/src/task/dto/metadata.dto.ts new file mode 100644 index 000000000..f6ba27ba8 --- /dev/null +++ b/apps/todo/apps/backend/src/task/dto/metadata.dto.ts @@ -0,0 +1,58 @@ +import { + IsString, + IsOptional, + IsNumber, + IsArray, + IsUUID, + IsEnum, + Min, + Max, + MaxLength, + ValidateNested, +} from 'class-validator'; +import { Type } from 'class-transformer'; + +export class EffectiveDurationDto { + @IsNumber() + @Min(1, { message: 'Dauer muss mindestens 1 sein' }) + @Max(9999, { message: 'Dauer darf maximal 9999 sein' }) + value: number; + + @IsEnum(['minutes', 'hours', 'days'], { message: 'UngĂŒltige Zeiteinheit' }) + unit: 'minutes' | 'hours' | 'days'; +} + +export class TaskMetadataDto { + @IsOptional() + @IsString() + @MaxLength(10000, { message: 'Notizen dĂŒrfen maximal 10000 Zeichen haben' }) + notes?: string; + + @IsOptional() + @IsArray() + @IsString({ each: true }) + @MaxLength(500, { each: true }) + attachments?: string[]; + + @IsOptional() + @IsUUID() + linkedCalendarEventId?: string | null; + + @IsOptional() + @IsNumber() + @IsEnum([1, 2, 3, 5, 8, 13, 21], { + message: 'Storypoints mĂŒssen Fibonacci-Zahlen sein (1,2,3,5,8,13,21)', + }) + storyPoints?: number | null; + + @IsOptional() + @ValidateNested() + @Type(() => EffectiveDurationDto) + effectiveDuration?: EffectiveDurationDto | null; + + @IsOptional() + @IsNumber() + @Min(1, { message: 'Spaß-Faktor muss mindestens 1 sein' }) + @Max(10, { message: 'Spaß-Faktor darf maximal 10 sein' }) + funRating?: number | null; +} diff --git a/apps/todo/apps/backend/src/task/dto/subtask.dto.ts b/apps/todo/apps/backend/src/task/dto/subtask.dto.ts new file mode 100644 index 000000000..1cdce1a78 --- /dev/null +++ b/apps/todo/apps/backend/src/task/dto/subtask.dto.ts @@ -0,0 +1,48 @@ +import { + IsString, + IsBoolean, + IsOptional, + IsNumber, + MinLength, + MaxLength, + Min, + IsDateString, +} from 'class-validator'; + +export class SubtaskDto { + @IsOptional() + @IsString() + id?: string; + + @IsString() + @MinLength(1, { message: 'Subtask-Titel darf nicht leer sein' }) + @MaxLength(500, { message: 'Subtask-Titel darf maximal 500 Zeichen haben' }) + title: string; + + @IsBoolean() + isCompleted: boolean; + + @IsOptional() + @IsDateString() + completedAt?: string | null; + + @IsNumber() + @Min(0) + order: number; +} + +export class CreateSubtaskDto { + @IsString() + @MinLength(1, { message: 'Subtask-Titel darf nicht leer sein' }) + @MaxLength(500, { message: 'Subtask-Titel darf maximal 500 Zeichen haben' }) + title: string; + + @IsOptional() + @IsBoolean() + isCompleted?: boolean; + + @IsOptional() + @IsNumber() + @Min(0) + order?: number; +} diff --git a/apps/todo/apps/backend/src/task/task.controller.ts b/apps/todo/apps/backend/src/task/task.controller.ts index 579923aa8..01060596a 100644 --- a/apps/todo/apps/backend/src/task/task.controller.ts +++ b/apps/todo/apps/backend/src/task/task.controller.ts @@ -33,9 +33,13 @@ export class TaskController { } @Get('completed') - async getCompleted(@CurrentUser() user: CurrentUserData, @Query('limit') limit?: number) { - const tasks = await this.taskService.getCompletedTasks(user.userId, limit ?? 50); - return { tasks }; + async getCompleted( + @CurrentUser() user: CurrentUserData, + @Query('limit') limit?: number, + @Query('offset') offset?: number + ) { + const result = await this.taskService.getCompletedTasks(user.userId, limit ?? 50, offset ?? 0); + return result; } @Get(':id') diff --git a/apps/todo/apps/backend/src/task/task.service.ts b/apps/todo/apps/backend/src/task/task.service.ts index cbb420d1e..747990a56 100644 --- a/apps/todo/apps/backend/src/task/task.service.ts +++ b/apps/todo/apps/backend/src/task/task.service.ts @@ -1,5 +1,5 @@ import { Injectable, Inject, NotFoundException } from '@nestjs/common'; -import { eq, and, or, gte, lte, ilike, asc, desc, isNull, SQL } from 'drizzle-orm'; +import { eq, and, or, gte, lte, ilike, asc, desc, isNull, SQL, sql } from 'drizzle-orm'; import { RRule, RRuleSet, rrulestr } from 'rrule'; import { DATABASE_CONNECTION } from '../db/database.module'; import { type Database } from '../db/connection'; @@ -452,14 +452,35 @@ export class TaskService { return this.loadTaskLabelsBatch(result); } - async getCompletedTasks(userId: string, limit: number = 50): Promise { - const result = await this.db.query.tasks.findMany({ - where: and(eq(tasks.userId, userId), eq(tasks.isCompleted, true)), - orderBy: [desc(tasks.completedAt)], - limit, - }); + async getCompletedTasks( + userId: string, + limit: number = 50, + offset: number = 0 + ): Promise<{ tasks: TaskWithLabels[]; total: number; hasMore: boolean }> { + // Enforce max limit to prevent abuse + const safeLimit = Math.min(limit, 100); - return this.loadTaskLabelsBatch(result); + const [result, countResult] = await Promise.all([ + this.db.query.tasks.findMany({ + where: and(eq(tasks.userId, userId), eq(tasks.isCompleted, true)), + orderBy: [desc(tasks.completedAt)], + limit: safeLimit, + offset, + }), + this.db + .select({ count: sql`count(*)::int` }) + .from(tasks) + .where(and(eq(tasks.userId, userId), eq(tasks.isCompleted, true))), + ]); + + const total = countResult[0]?.count ?? 0; + const tasksWithLabels = await this.loadTaskLabelsBatch(result); + + return { + tasks: tasksWithLabels, + total, + hasMore: offset + safeLimit < total, + }; } async reorder( diff --git a/apps/todo/apps/web/src/routes/(app)/+layout.svelte b/apps/todo/apps/web/src/routes/(app)/+layout.svelte index 5f51106a5..1f15afb2d 100644 --- a/apps/todo/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/todo/apps/web/src/routes/(app)/+layout.svelte @@ -215,16 +215,20 @@ function handleModeChange(isSidebar: boolean) { isSidebarMode = isSidebar; sidebarModeStore.set(isSidebar); - if (typeof localStorage !== 'undefined') { - localStorage.setItem('todo-nav-sidebar', String(isSidebar)); + try { + localStorage?.setItem('todo-nav-sidebar', String(isSidebar)); + } catch { + // localStorage not available or quota exceeded } } function handleCollapsedChange(collapsed: boolean) { isCollapsed = collapsed; collapsedStore.set(collapsed); - if (typeof localStorage !== 'undefined') { - localStorage.setItem('todo-nav-collapsed', String(collapsed)); + try { + localStorage?.setItem('todo-nav-collapsed', String(collapsed)); + } catch { + // localStorage not available or quota exceeded } } @@ -263,18 +267,26 @@ goto(userSettings.startPage, { replaceState: true }); } - // Initialize sidebar mode from localStorage - const savedSidebar = localStorage.getItem('todo-nav-sidebar'); - if (savedSidebar === 'true') { - isSidebarMode = true; - sidebarModeStore.set(true); + // Initialize sidebar mode from localStorage (with error handling for private browsing) + try { + const savedSidebar = localStorage?.getItem('todo-nav-sidebar'); + if (savedSidebar === 'true') { + isSidebarMode = true; + sidebarModeStore.set(true); + } + } catch { + // localStorage not available (private browsing, quota exceeded, etc.) } // Initialize collapsed state from localStorage - const savedCollapsed = localStorage.getItem('todo-nav-collapsed'); - if (savedCollapsed === 'true') { - isCollapsed = true; - collapsedStore.set(true); + try { + const savedCollapsed = localStorage?.getItem('todo-nav-collapsed'); + if (savedCollapsed === 'true') { + isCollapsed = true; + collapsedStore.set(true); + } + } catch { + // localStorage not available } // Register Service Worker for PWA From 6aa8554e2191867b98e7b929eeecf0e9bc2f83e6 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:40:20 +0100 Subject: [PATCH 09/26] style(shared-ui): refactor CommandBar to use theme CSS variables MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace hardcoded dark-mode colors with theme-aware CSS variables: - #1a1a1a → hsl(var(--color-surface-elevated)) - #333 → hsl(var(--color-border)) - #e5e5e5 → hsl(var(--color-foreground)) - #888 → hsl(var(--color-muted-foreground)) - #2a2a2a → hsl(var(--color-surface)) - #3b82f6 → hsl(var(--color-primary)) - #10b981 → hsl(var(--color-success)) - #ef4444 → hsl(var(--color-error)) CommandBar now adapts correctly to light/dark mode and all theme variants. đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/command-bar/CommandBar.svelte | 90 +++++++++---------- 1 file changed, 45 insertions(+), 45 deletions(-) diff --git a/packages/shared-ui/src/command-bar/CommandBar.svelte b/packages/shared-ui/src/command-bar/CommandBar.svelte index bef7acb19..64db1e251 100644 --- a/packages/shared-ui/src/command-bar/CommandBar.svelte +++ b/packages/shared-ui/src/command-bar/CommandBar.svelte @@ -448,9 +448,9 @@ align-items: flex-start; justify-content: center; padding-top: 15vh; - background: rgba(0, 0, 0, 0.6); - backdrop-filter: blur(4px); - -webkit-backdrop-filter: blur(4px); + background: hsl(var(--color-background) / 0.8); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); animation: fadeIn 0.15s ease; } @@ -467,15 +467,15 @@ width: 100%; max-width: 560px; margin: 0 1rem; - background: #1a1a1a; - border: 1px solid #333; + background: hsl(var(--color-surface-elevated)); + border: 1px solid hsl(var(--color-border)); border-radius: 12px; box-shadow: - 0 25px 50px -12px rgba(0, 0, 0, 0.5), - 0 0 0 1px rgba(255, 255, 255, 0.1); + 0 25px 50px -12px hsl(var(--color-background) / 0.5), + 0 0 0 1px hsl(var(--color-border) / 0.5); overflow: hidden; animation: slideIn 0.2s ease; - color: #e5e5e5; + color: hsl(var(--color-foreground)); } @keyframes slideIn { @@ -494,13 +494,13 @@ align-items: center; gap: 0.75rem; padding: 1rem 1.25rem; - border-bottom: 1px solid #333; + border-bottom: 1px solid hsl(var(--color-border)); } .command-icon { width: 1.25rem; height: 1.25rem; - color: #888; + color: hsl(var(--color-muted-foreground)); flex-shrink: 0; } @@ -509,22 +509,22 @@ border: none; background: transparent; font-size: 1rem; - color: #fff; + color: hsl(var(--color-foreground)); outline: none; } .command-input::placeholder { - color: #666; + color: hsl(var(--color-muted-foreground)); } .command-shortcut { padding: 0.25rem 0.5rem; font-size: 0.75rem; font-family: inherit; - background: #2a2a2a; - border: 1px solid #444; + background: hsl(var(--color-surface)); + border: 1px solid hsl(var(--color-border)); border-radius: 4px; - color: #888; + color: hsl(var(--color-muted-foreground)); } .command-results { @@ -539,15 +539,15 @@ justify-content: center; gap: 0.75rem; padding: 2rem; - color: #888; + color: hsl(var(--color-muted-foreground)); font-size: 0.875rem; } .loading-spinner { width: 1.25rem; height: 1.25rem; - border: 2px solid #333; - border-top-color: #3b82f6; + border: 2px solid hsl(var(--color-border)); + border-top-color: hsl(var(--color-primary)); border-radius: 50%; animation: spin 0.8s linear infinite; } @@ -555,8 +555,8 @@ .loading-spinner-small { width: 1rem; height: 1rem; - border: 2px solid #444; - border-top-color: #10b981; + border: 2px solid hsl(var(--color-border)); + border-top-color: hsl(var(--color-success)); border-radius: 50%; animation: spin 0.8s linear infinite; } @@ -569,26 +569,26 @@ /* Create option styles */ .create-option { - border-bottom: 1px solid #333; + border-bottom: 1px solid hsl(var(--color-border)); } .create-option.selected, .create-option:hover { - background: rgba(16, 185, 129, 0.1); + background: hsl(var(--color-success) / 0.1); } .create-avatar { - background: #10b981; + background: hsl(var(--color-success)); } .create-shortcut { padding: 0.25rem 0.5rem; font-size: 0.6875rem; font-family: inherit; - background: #2a2a2a; - border: 1px solid #444; + background: hsl(var(--color-surface)); + border: 1px solid hsl(var(--color-border)); border-radius: 4px; - color: #888; + color: hsl(var(--color-muted-foreground)); flex-shrink: 0; } @@ -598,7 +598,7 @@ font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; - color: #666; + color: hsl(var(--color-muted-foreground)); } .command-result { @@ -612,12 +612,12 @@ cursor: pointer; text-align: left; transition: background 0.1s ease; - color: #e5e5e5; + color: hsl(var(--color-foreground)); } .command-result:hover, .command-result.selected { - background: #2a2a2a; + background: hsl(var(--color-surface-hover)); } .result-avatar { @@ -625,8 +625,8 @@ height: 40px; min-width: 40px; border-radius: 9999px; - background: #3b82f6; - color: #fff; + background: hsl(var(--color-primary)); + color: hsl(var(--color-primary-foreground)); display: flex; align-items: center; justify-content: center; @@ -641,7 +641,7 @@ .result-name { font-weight: 500; - color: #fff; + color: hsl(var(--color-foreground)); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -651,7 +651,7 @@ display: flex; gap: 0.5rem; font-size: 0.8125rem; - color: #888; + color: hsl(var(--color-muted-foreground)); } .result-details span { @@ -663,7 +663,7 @@ .result-favorite { width: 1rem; height: 1rem; - color: #ef4444; + color: hsl(var(--color-error)); flex-shrink: 0; } @@ -678,7 +678,7 @@ width: 100%; padding: 0.75rem 1rem; border-radius: 8px; - color: #e5e5e5; + color: hsl(var(--color-foreground)); background: transparent; border: none; cursor: pointer; @@ -688,13 +688,13 @@ .quick-action:hover, .quick-action.selected { - background: #2a2a2a; + background: hsl(var(--color-surface-hover)); } .quick-action-icon { width: 1.25rem; height: 1.25rem; - color: #888; + color: hsl(var(--color-muted-foreground)); } .quick-action span { @@ -706,30 +706,30 @@ padding: 0.125rem 0.375rem; font-size: 0.6875rem; font-family: inherit; - background: #2a2a2a; - border: 1px solid #444; + background: hsl(var(--color-surface)); + border: 1px solid hsl(var(--color-border)); border-radius: 4px; - color: #888; + color: hsl(var(--color-muted-foreground)); } .command-footer { padding: 0.75rem 1.25rem; - border-top: 1px solid #333; - background: #141414; + border-top: 1px solid hsl(var(--color-border)); + background: hsl(var(--color-surface)); } .footer-hints { display: flex; gap: 1rem; font-size: 0.75rem; - color: #666; + color: hsl(var(--color-muted-foreground)); } .footer-hints kbd { padding: 0.125rem 0.25rem; font-family: inherit; - background: #2a2a2a; - border: 1px solid #444; + background: hsl(var(--color-surface-elevated)); + border: 1px solid hsl(var(--color-border)); border-radius: 3px; margin-right: 0.25rem; } From 330b9907b062c3488b91fc9b0c4f506e9735cfcb Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:52:11 +0100 Subject: [PATCH 10/26] refactor(todo): rename priority labels for better natural language input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - low → "SpĂ€ter" (was "Niedrig") - medium → "Normal" (was "Mittel") - high → "Wichtig" (was "Hoch") - urgent → "Dringend" (unchanged) CommandBar syntax now supports: !spĂ€ter, !normal, !wichtig, !dringend Shortcut syntax still works: !, !!, !!! đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/lib/components/QuickAddTask.svelte | 18 +- .../src/lib/components/TaskEditModal.svelte | 40 ++--- .../components/form/PrioritySelector.svelte | 10 +- .../lib/components/kanban/KanbanBoard.svelte | 31 +++- .../lib/components/kanban/KanbanColumn.svelte | 8 +- .../components/kanban/KanbanFilters.svelte | 6 +- .../components/kanban/KanbanTaskCard.svelte | 23 ++- .../statistics/PriorityDonutChart.svelte | 120 ++++++------- .../apps/web/src/lib/utils/task-parser.ts | 15 +- .../apps/web/src/routes/(app)/+page.svelte | 163 ++++++++++++------ .../web/src/routes/(app)/labels/+page.svelte | 33 +++- .../src/routes/(app)/settings/+page.svelte | 10 +- .../packages/shared/src/constants/index.ts | 3 + .../packages/shared/src/constants/task.ts | 55 ++++++ apps/todo/packages/shared/src/types/task.ts | 1 + 15 files changed, 346 insertions(+), 190 deletions(-) create mode 100644 apps/todo/packages/shared/src/constants/task.ts diff --git a/apps/todo/apps/web/src/lib/components/QuickAddTask.svelte b/apps/todo/apps/web/src/lib/components/QuickAddTask.svelte index 548c61e0d..bb645679f 100644 --- a/apps/todo/apps/web/src/lib/components/QuickAddTask.svelte +++ b/apps/todo/apps/web/src/lib/components/QuickAddTask.svelte @@ -4,6 +4,7 @@ import { viewStore } from '$lib/stores/view.svelte'; import { projectsStore } from '$lib/stores/projects.svelte'; import type { TaskPriority } from '@todo/shared'; + import { PRIORITY_OPTIONS } from '@todo/shared'; import { format, addDays } from 'date-fns'; import { de } from 'date-fns/locale'; @@ -21,14 +22,6 @@ let showPriorityPicker = $state(false); let showProjectPicker = $state(false); - // Priority options - const priorities: { value: TaskPriority; label: string; color: string }[] = [ - { value: 'low', label: 'Niedrig', color: '#22c55e' }, - { value: 'medium', label: 'Mittel', color: '#eab308' }, - { value: 'high', label: 'Hoch', color: '#f97316' }, - { value: 'urgent', label: 'Dringend', color: '#ef4444' }, - ]; - // Quick date options const dateOptions = [ { label: 'Heute', date: new Date() }, @@ -38,7 +31,7 @@ ]; // Derived values - let currentPriority = $derived(priorities.find((p) => p.value === selectedPriority)!); + let currentPriority = $derived(PRIORITY_OPTIONS.find((p) => p.value === selectedPriority)!); let selectedProject = $derived( selectedProjectId ? projectsStore.getById(selectedProjectId) : undefined ); @@ -81,11 +74,14 @@ if (viewStore.currentView !== 'project') { selectedProjectId = undefined; } - inputRef?.focus(); } catch (error) { console.error('Failed to create task:', error); } finally { isLoading = false; + // Focus after isLoading is reset (input is no longer disabled) + requestAnimationFrame(() => { + inputRef?.focus(); + }); } } @@ -232,7 +228,7 @@ {#if showPriorityPicker} + + { + showDeleteConfirm = false; + columnToDelete = null; + }} + onConfirm={confirmDeleteColumn} + variant="danger" + title="Spalte löschen?" + message="Alle Aufgaben dieser Spalte werden in die erste Spalte verschoben." + confirmLabel="Löschen" + cancelLabel="Abbrechen" +/> + diff --git a/apps/contacts/apps/web/src/lib/stores/statistics.svelte.ts b/apps/contacts/apps/web/src/lib/stores/statistics.svelte.ts new file mode 100644 index 000000000..4c2a112f6 --- /dev/null +++ b/apps/contacts/apps/web/src/lib/stores/statistics.svelte.ts @@ -0,0 +1,275 @@ +/** + * Contacts Statistics Store - Calculates contact statistics using Svelte 5 runes + */ + +import type { Contact } from '$lib/api/contacts'; +import { subDays, format, parseISO, isWithinInterval, getMonth, eachDayOfInterval } from 'date-fns'; +import { de } from 'date-fns/locale'; +import type { + HeatmapDataPoint, + TrendDataPoint, + DonutSegment, + ProgressItem, +} from '@manacore/shared-ui'; + +// Types +export interface ContactTag { + id: string; + name: string; + color: string; +} + +// State +let contacts = $state([]); +let tags = $state([]); + +export const contactsStatisticsStore = { + // Setters + setContacts(newContacts: Contact[]) { + contacts = newContacts; + }, + + setTags(newTags: ContactTag[]) { + tags = newTags; + }, + + // Quick Stats + get totalContacts() { + return contacts.length; + }, + + get favoriteContacts() { + return contacts.filter((c) => c.isFavorite).length; + }, + + get archivedContacts() { + return contacts.filter((c) => c.isArchived).length; + }, + + get activeContacts() { + return contacts.filter((c) => !c.isArchived).length; + }, + + get recentlyAdded() { + const weekAgo = subDays(new Date(), 7); + return contacts.filter((c) => { + const createdAt = + typeof c.createdAt === 'string' ? parseISO(c.createdAt) : new Date(c.createdAt); + return createdAt >= weekAgo; + }).length; + }, + + get birthdaysThisMonth() { + const currentMonth = getMonth(new Date()); + return contacts.filter((c) => { + if (!c.birthday) return false; + const birthday = typeof c.birthday === 'string' ? parseISO(c.birthday) : new Date(c.birthday); + return getMonth(birthday) === currentMonth; + }).length; + }, + + get contactsWithEmail() { + return contacts.filter((c) => c.email).length; + }, + + get contactsWithPhone() { + return contacts.filter((c) => c.phone || c.mobile).length; + }, + + // Completeness rate (contacts with email AND phone) + get completenessRate() { + if (contacts.length === 0) return 0; + const complete = contacts.filter((c) => c.email && (c.phone || c.mobile)).length; + return Math.round((complete / contacts.length) * 100); + }, + + // Activity Heatmap (last 6 months) - based on contact creation + get activityHeatmap(): HeatmapDataPoint[] { + const endDate = new Date(); + const startDate = subDays(endDate, 180); + + // Count contacts created per day + const creationMap = new Map(); + + contacts.forEach((c) => { + const createdAt = + typeof c.createdAt === 'string' ? parseISO(c.createdAt) : new Date(c.createdAt); + if (createdAt >= startDate && createdAt <= endDate) { + const dateKey = format(createdAt, 'yyyy-MM-dd'); + creationMap.set(dateKey, (creationMap.get(dateKey) || 0) + 1); + } + }); + + // Generate all days + const days = eachDayOfInterval({ start: startDate, end: endDate }); + + return days.map((day) => { + const dateKey = format(day, 'yyyy-MM-dd'); + return { + date: dateKey, + count: creationMap.get(dateKey) || 0, + dayOfWeek: day.getDay(), + }; + }); + }, + + // Weekly Trend (last 4 weeks) + get weeklyTrend(): TrendDataPoint[] { + const endDate = new Date(); + const startDate = subDays(endDate, 27); + + const creationMap = new Map(); + + contacts.forEach((c) => { + const createdAt = + typeof c.createdAt === 'string' ? parseISO(c.createdAt) : new Date(c.createdAt); + if (createdAt >= startDate && createdAt <= endDate) { + const dateKey = format(createdAt, 'yyyy-MM-dd'); + creationMap.set(dateKey, (creationMap.get(dateKey) || 0) + 1); + } + }); + + const days = eachDayOfInterval({ start: startDate, end: endDate }); + + return days.map((day) => { + const dateKey = format(day, 'yyyy-MM-dd'); + return { + date: dateKey, + count: creationMap.get(dateKey) || 0, + label: format(day, 'EEE', { locale: de }), + }; + }); + }, + + // Contact Status Breakdown (Donut Chart) - Favorites / Active / Archived + get statusBreakdown(): DonutSegment[] { + const total = contacts.length; + if (total === 0) return []; + + const favorites = contacts.filter((c) => c.isFavorite && !c.isArchived).length; + const archived = contacts.filter((c) => c.isArchived).length; + const regular = contacts.filter((c) => !c.isFavorite && !c.isArchived).length; + + return [ + { + id: 'favorites', + label: 'Favoriten', + count: favorites, + percentage: Math.round((favorites / total) * 100), + color: '#F59E0B', // amber + }, + { + id: 'regular', + label: 'Aktiv', + count: regular, + percentage: Math.round((regular / total) * 100), + color: '#10B981', // green + }, + { + id: 'archived', + label: 'Archiviert', + count: archived, + percentage: Math.round((archived / total) * 100), + color: '#6B7280', // gray + }, + ]; + }, + + // Tags Progress (Progress Bars) + get tagProgress(): ProgressItem[] { + // Count contacts per tag + const tagCountMap = new Map(); + + // This requires contacts to have a tags array - we'll estimate from the tag data + // For now, we'll show tags with placeholder counts + // In a real implementation, we'd need contactTags relation data + + const result: ProgressItem[] = tags.map((tag) => ({ + id: tag.id, + name: tag.name, + color: tag.color || '#6B7280', + total: contacts.length, // Total contacts as reference + completed: 0, // Would need contact-tag relation to calculate + percentage: 0, + })); + + return result.sort((a, b) => b.completed - a.completed); + }, + + // Info completeness breakdown + get infoBreakdown(): DonutSegment[] { + const total = contacts.length; + if (total === 0) return []; + + const withEmail = contacts.filter((c) => c.email).length; + const withPhone = contacts.filter((c) => c.phone || c.mobile).length; + const withCompany = contacts.filter((c) => c.company).length; + const withBirthday = contacts.filter((c) => c.birthday).length; + + return [ + { + id: 'email', + label: 'Mit E-Mail', + count: withEmail, + percentage: Math.round((withEmail / total) * 100), + color: '#3B82F6', // blue + }, + { + id: 'phone', + label: 'Mit Telefon', + count: withPhone, + percentage: Math.round((withPhone / total) * 100), + color: '#10B981', // green + }, + { + id: 'company', + label: 'Mit Firma', + count: withCompany, + percentage: Math.round((withCompany / total) * 100), + color: '#8B5CF6', // violet + }, + { + id: 'birthday', + label: 'Mit Geburtstag', + count: withBirthday, + percentage: Math.round((withBirthday / total) * 100), + color: '#EC4899', // pink + }, + ]; + }, + + // Country breakdown + get countryBreakdown(): ProgressItem[] { + const countryMap = new Map(); + + contacts.forEach((c) => { + const country = c.country || 'Unbekannt'; + countryMap.set(country, (countryMap.get(country) || 0) + 1); + }); + + const result: ProgressItem[] = []; + const colors = ['#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6', '#EC4899', '#6B7280']; + let colorIndex = 0; + + countryMap.forEach((count, country) => { + if (country !== 'Unbekannt' || count > 0) { + result.push({ + id: country, + name: country, + color: colors[colorIndex % colors.length], + total: contacts.length, + completed: count, + percentage: Math.round((count / contacts.length) * 100), + }); + colorIndex++; + } + }); + + return result.sort((a, b) => b.completed - a.completed).slice(0, 8); + }, + + // Total tags count + get totalTags() { + return tags.length; + }, +}; diff --git a/apps/contacts/apps/web/src/routes/(app)/statistics/+page.svelte b/apps/contacts/apps/web/src/routes/(app)/statistics/+page.svelte new file mode 100644 index 000000000..f288dbd02 --- /dev/null +++ b/apps/contacts/apps/web/src/routes/(app)/statistics/+page.svelte @@ -0,0 +1,280 @@ + + + + Statistiken - Kontakte + + +
+ + + {#if loading} + + {:else} + +
+ +
+ + +
+ +
+ +
+ + +
+
+ +
+ +
+ +
+
+ + +
+
+ +
+ +
+ +
+
+
+ + +
+
+ Aktive Kontakte + {contactsStatisticsStore.activeContacts} +
+ +
+ Archivierte Kontakte + {contactsStatisticsStore.archivedContacts} +
+ +
+ Tags + {contactsStatisticsStore.totalTags} +
+
+ {/if} +
+ + diff --git a/packages/shared-ui/package.json b/packages/shared-ui/package.json index d43549996..f366d440f 100644 --- a/packages/shared-ui/package.json +++ b/packages/shared-ui/package.json @@ -42,6 +42,7 @@ "d3-selection": "^3.0.0", "d3-transition": "^3.0.0", "d3-zoom": "^3.0.0", + "date-fns": "^4.1.0", "lucide-svelte": "^0.468.0" }, "devDependencies": { diff --git a/packages/shared-ui/src/charts/ActivityHeatmap.svelte b/packages/shared-ui/src/charts/ActivityHeatmap.svelte new file mode 100644 index 000000000..852539046 --- /dev/null +++ b/packages/shared-ui/src/charts/ActivityHeatmap.svelte @@ -0,0 +1,294 @@ + + +
+

{title}

+ +
+ + + {#each monthLabels as label} + + {label.month} + + {/each} + + + {#each DAY_LABELS as label, i} + {#if label} + + {label} + + {/if} + {/each} + + + {#each weeks as week, weekIndex} + {#each week as day, dayIndex} + {#if day.date} + + {formatTooltip(day)} + + {:else} + + {/if} + {/each} + {/each} + +
+ + +
+ Weniger +
+
+
+
+
+
+
+ Mehr +
+
+ + diff --git a/packages/shared-ui/src/charts/DonutChart.svelte b/packages/shared-ui/src/charts/DonutChart.svelte new file mode 100644 index 000000000..643db2d61 --- /dev/null +++ b/packages/shared-ui/src/charts/DonutChart.svelte @@ -0,0 +1,260 @@ + + +
+

{title}

+ +
+
+ + {#each arcs as arc} + (hoveredSegment = arc.id)} + onmouseleave={() => (hoveredSegment = null)} + role="graphics-symbol" + aria-label="{arc.label}: {arc.count}" + > + {arc.label}: {arc.count} ({arc.percentage}%) + + {/each} + + + + {total} + + + {centerLabel} + + +
+ + + {#if showLegend} +
+ {#each data as item} +
(hoveredSegment = item.id)} + onmouseleave={() => (hoveredSegment = null)} + role="button" + tabindex="0" + > + + {item.label} + {item.count} +
+ {/each} +
+ {/if} +
+
+ + diff --git a/packages/shared-ui/src/charts/ProgressBars.svelte b/packages/shared-ui/src/charts/ProgressBars.svelte new file mode 100644 index 000000000..21692ec04 --- /dev/null +++ b/packages/shared-ui/src/charts/ProgressBars.svelte @@ -0,0 +1,192 @@ + + +
+

{title}

+ + {#if sortedData.length === 0} +

{emptyMessage}

+ {:else} +
+ {#each sortedData as item (item.id)} +
+
+
+ + {item.name} +
+ + {item.completed}/{item.total} + +
+ +
+
+ + {#if item.completed > 0} +
+ {/if} + + + {#if item.inProgress && item.inProgress > 0} +
+ {/if} +
+ + {item.percentage}% +
+
+ {/each} +
+ {/if} +
+ + diff --git a/packages/shared-ui/src/charts/StatisticsSkeleton.svelte b/packages/shared-ui/src/charts/StatisticsSkeleton.svelte new file mode 100644 index 000000000..e50ea20dd --- /dev/null +++ b/packages/shared-ui/src/charts/StatisticsSkeleton.svelte @@ -0,0 +1,272 @@ + + +
+ +
+ {#each Array(statCards) as _, i} +
+ +
+ + +
+
+ {/each} +
+ + +
+ +
+
+ +
+
+ {#each Array(7) as _} +
+ {#each Array(12) as _} + + {/each} +
+ {/each} +
+
+ + +
+ +
+
+ +
+
+ {#each Array(7) as _, i} +
+ + +
+ {/each} +
+
+ + +
+
+ +
+
+ +
+
+ {#each Array(legendItems) as _} +
+ + +
+ {/each} +
+
+
+ + +
+
+ +
+
+ {#each Array(progressItems) as _, i} +
+
+ + +
+ +
+ {/each} +
+
+
+ + + {#if showAdditionalStats} +
+ {#each Array(3) as _} +
+ + +
+ {/each} +
+ {/if} +
+ + diff --git a/packages/shared-ui/src/charts/StatsGrid.svelte b/packages/shared-ui/src/charts/StatsGrid.svelte new file mode 100644 index 000000000..49d1ffea9 --- /dev/null +++ b/packages/shared-ui/src/charts/StatsGrid.svelte @@ -0,0 +1,136 @@ + + +
+ {#each visibleItems as item (item.id)} +
+
+ +
+
+ {item.value} + {item.label} +
+
+ {/each} +
+ + diff --git a/packages/shared-ui/src/charts/TrendLineChart.svelte b/packages/shared-ui/src/charts/TrendLineChart.svelte new file mode 100644 index 000000000..0615c490b --- /dev/null +++ b/packages/shared-ui/src/charts/TrendLineChart.svelte @@ -0,0 +1,240 @@ + + +
+

{title}

+ + + + {#each yTicks as tick} + + {/each} + + + + + + + + + + + + + + + + {#each data as point, i} + + {formatTooltip(point)} + + {/each} + + + {#each yTicks as tick} + + {tick} + + {/each} + + + {#each xLabels as label} + + {label.label} + + {/each} + +
+ + diff --git a/packages/shared-ui/src/charts/index.ts b/packages/shared-ui/src/charts/index.ts new file mode 100644 index 000000000..6246fd8a8 --- /dev/null +++ b/packages/shared-ui/src/charts/index.ts @@ -0,0 +1,20 @@ +// Charts - Statistics Visualization Components +export { default as StatsGrid } from './StatsGrid.svelte'; +export { default as ActivityHeatmap } from './ActivityHeatmap.svelte'; +export { default as TrendLineChart } from './TrendLineChart.svelte'; +export { default as DonutChart } from './DonutChart.svelte'; +export { default as ProgressBars } from './ProgressBars.svelte'; +export { default as StatisticsSkeleton } from './StatisticsSkeleton.svelte'; + +// Types +export type { + StatVariant, + StatItem, + HeatmapDataPoint, + TrendDataPoint, + DonutSegment, + ProgressItem, +} from './types'; + +// Constants +export { STAT_VARIANT_COLORS } from './types'; diff --git a/packages/shared-ui/src/charts/types.ts b/packages/shared-ui/src/charts/types.ts new file mode 100644 index 000000000..774b0d993 --- /dev/null +++ b/packages/shared-ui/src/charts/types.ts @@ -0,0 +1,62 @@ +/** + * Shared Types for Chart Components + */ + +import type { Component } from 'svelte'; + +// Stat card variant colors +export type StatVariant = 'success' | 'primary' | 'neutral' | 'danger' | 'info' | 'accent'; + +export const STAT_VARIANT_COLORS: Record = { + success: { bg: 'rgba(16, 185, 129, 0.15)', color: '#10B981' }, + primary: { bg: 'rgba(139, 92, 246, 0.15)', color: '#8B5CF6' }, + neutral: { bg: 'rgba(107, 114, 128, 0.15)', color: '#6B7280' }, + danger: { bg: 'rgba(239, 68, 68, 0.15)', color: '#EF4444' }, + info: { bg: 'rgba(59, 130, 246, 0.15)', color: '#3B82F6' }, + accent: { bg: 'rgba(236, 72, 153, 0.15)', color: '#EC4899' }, +}; + +// StatsGrid types +export interface StatItem { + id: string; + label: string; + value: number | string; + icon: Component; + variant: StatVariant; + /** Optional: only show this stat if condition is true */ + showCondition?: boolean; +} + +// ActivityHeatmap types +export interface HeatmapDataPoint { + date: string; // YYYY-MM-DD format + count: number; + dayOfWeek: number; // 0-6 (Sunday-Saturday) +} + +// TrendLineChart types +export interface TrendDataPoint { + date: string; // YYYY-MM-DD format + count: number; + label?: string; +} + +// DonutChart types +export interface DonutSegment { + id: string; + label: string; + count: number; + percentage: number; + color: string; +} + +// ProgressBars types +export interface ProgressItem { + id: string; + name: string; + color: string; + total: number; + completed: number; + inProgress?: number; + percentage: number; +} diff --git a/packages/shared-ui/src/index.ts b/packages/shared-ui/src/index.ts index 55bd31ed3..9e3262b05 100644 --- a/packages/shared-ui/src/index.ts +++ b/packages/shared-ui/src/index.ts @@ -110,3 +110,22 @@ export type { CommandBarItem, QuickAction, CreatePreview } from './command-bar'; // Pages export { default as AppsPage } from './pages/AppsPage.svelte'; + +// Charts - Statistics Visualization +export { + StatsGrid, + ActivityHeatmap, + TrendLineChart, + DonutChart, + ProgressBars, + StatisticsSkeleton, + STAT_VARIANT_COLORS, +} from './charts'; +export type { + StatVariant, + StatItem, + HeatmapDataPoint, + TrendDataPoint, + DonutSegment, + ProgressItem, +} from './charts'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3bac16249..1bbeae1aa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4298,6 +4298,9 @@ importers: d3-zoom: specifier: ^3.0.0 version: 3.0.0 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 lucide-svelte: specifier: ^0.468.0 version: 0.468.0(svelte@5.44.0) From 993b8c129ac426db49aff6c84919bccf2cc702d1 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 14:57:37 +0100 Subject: [PATCH 12/26] fix(todo): allow priority keywords without ! prefix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now supports both: - "heimfahren wichtig" → priority high - "heimfahren !wichtig" → priority high Keywords: spĂ€ter, normal, wichtig, dringend (case-insensitive) đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/todo/apps/web/src/lib/utils/task-parser.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/todo/apps/web/src/lib/utils/task-parser.ts b/apps/todo/apps/web/src/lib/utils/task-parser.ts index b0a61bc2c..d6cc251a1 100644 --- a/apps/todo/apps/web/src/lib/utils/task-parser.ts +++ b/apps/todo/apps/web/src/lib/utils/task-parser.ts @@ -42,12 +42,12 @@ export interface ParsedTaskWithIds { } // Priority patterns (task-specific) -// Supports: !spĂ€ter, !normal, !wichtig, !dringend and shortcuts !, !!, !!! +// Supports: spĂ€ter, normal, wichtig, dringend (with or without !) and shortcuts !, !!, !!! const PRIORITY_PATTERNS: { pattern: RegExp; priority: TaskPriority }[] = [ - { pattern: /!{3,}|!dringend/i, priority: 'urgent' }, - { pattern: /!{2}|!wichtig/i, priority: 'high' }, - { pattern: /!normal/i, priority: 'medium' }, - { pattern: /!spĂ€ter|!sp[aĂ€]ter/i, priority: 'low' }, + { pattern: /!{3,}|!?dringend\b/i, priority: 'urgent' }, + { pattern: /!{2}|!?wichtig\b/i, priority: 'high' }, + { pattern: /!?normal\b/i, priority: 'medium' }, + { pattern: /!?sp[aĂ€]ter\b/i, priority: 'low' }, ]; /** From d98efc455b1a086b3938a9538cc3d631903853e9 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:01:57 +0100 Subject: [PATCH 13/26] feat(shared-ui): add syntax highlighting to CommandBar input MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Keywords are now visually highlighted as you type: - Priority (wichtig, dringend, spĂ€ter, normal): orange/warning - Tags (#tag): primary color - References (@project, @calendar): green/success - Dates (heute, morgen, montag, etc.): purple - Times (14:00, um 14 Uhr): purple Uses overlay technique with transparent input over highlighted backdrop. đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/command-bar/CommandBar.svelte | 124 ++++++++++++++++-- 1 file changed, 113 insertions(+), 11 deletions(-) diff --git a/packages/shared-ui/src/command-bar/CommandBar.svelte b/packages/shared-ui/src/command-bar/CommandBar.svelte index 64db1e251..b28d2c4e7 100644 --- a/packages/shared-ui/src/command-bar/CommandBar.svelte +++ b/packages/shared-ui/src/command-bar/CommandBar.svelte @@ -1,6 +1,47 @@
+ +
+ + + +
+
- - {#if dueDateText() || subtaskProgress() || (task.labels && task.labels.length > 0)} + + {#if subtaskProgress() || (task.labels && task.labels.length > 0)}
- {#if dueDateText()} - - - - - {dueDateText()} - - {/if} - {#if subtaskProgress()} @@ -129,6 +118,17 @@ {/if} + + {#if dueDateText()} + + {dueDateText()} + + {/if} + {#if projectColor()}
@@ -151,18 +151,16 @@ .task-item { display: flex; align-items: center; - gap: 0.75rem; - padding: 0.625rem 1rem; - border-radius: 9999px; + gap: 0.625rem; + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; background: rgba(255, 255, 255, 0.85); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); - border: 1px solid rgba(0, 0, 0, 0.1); - box-shadow: - 0 4px 6px -1px rgba(0, 0, 0, 0.1), - 0 2px 4px -1px rgba(0, 0, 0, 0.06); + border: 1px solid rgba(0, 0, 0, 0.08); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); transition: all 0.2s; - margin-bottom: 0.5rem; + margin-bottom: 0.375rem; } :global(.dark) .task-item { @@ -172,11 +170,8 @@ .task-item:hover { background: rgba(255, 255, 255, 0.95); - border-color: rgba(0, 0, 0, 0.15); - transform: translateY(-1px); - box-shadow: - 0 10px 15px -3px rgba(0, 0, 0, 0.1), - 0 4px 6px -2px rgba(0, 0, 0, 0.05); + border-color: rgba(0, 0, 0, 0.12); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); } :global(.dark) .task-item:hover { @@ -188,6 +183,43 @@ opacity: 0.6; } + /* Drag handle */ + .drag-handle { + cursor: grab; + opacity: 0; + transition: opacity 0.15s; + flex-shrink: 0; + display: flex; + align-items: center; + padding: 0.125rem; + margin-left: -0.25rem; + } + + .task-item:hover .drag-handle { + opacity: 0.4; + } + + .drag-handle:hover { + opacity: 0.7 !important; + } + + .drag-handle:active { + cursor: grabbing; + } + + .drag-icon { + width: 1rem; + height: 1rem; + color: currentColor; + } + + /* During drag, disable pointer events on interactive elements */ + :global([aria-grabbed='true']) .task-checkbox, + :global([aria-grabbed='true']) .task-content, + :global([aria-grabbed='true']) .delete-btn { + pointer-events: none; + } + /* Priority dot */ .priority-dot { width: 0.5rem; @@ -284,14 +316,6 @@ color: #9ca3af; } - .meta-item.date.overdue { - color: #ef4444; - } - - .meta-item.date.today { - color: #f97316; - } - .meta-icon { width: 0.75rem; height: 0.75rem; @@ -306,6 +330,26 @@ font-weight: 500; } + /* Due date */ + .due-date { + font-size: 0.75rem; + color: #6b7280; + flex-shrink: 0; + white-space: nowrap; + } + + :global(.dark) .due-date { + color: #9ca3af; + } + + .due-date.overdue { + color: #ef4444; + } + + .due-date.today { + color: #f97316; + } + /* Project dot */ .project-dot { width: 0.5rem; diff --git a/apps/todo/apps/web/src/lib/components/TaskList.svelte b/apps/todo/apps/web/src/lib/components/TaskList.svelte index 0e7c66c65..1ce2fba42 100644 --- a/apps/todo/apps/web/src/lib/components/TaskList.svelte +++ b/apps/todo/apps/web/src/lib/components/TaskList.svelte @@ -1,4 +1,5 @@ -
- {#each tasks as task (task.id)} - handleToggleComplete(task)} - onDelete={() => handleDelete(task.id)} - onEdit={onEditTask ? () => onEditTask(task) : undefined} - /> - {/each} -
+{#if enableDragDrop} +
+ {#each items.filter((t) => t.id !== SHADOW_PLACEHOLDER_ITEM_ID) as task (task.id)} + handleToggleComplete(task)} + onDelete={() => handleDelete(task.id)} + onEdit={onEditTask ? () => onEditTask(task) : undefined} + /> + {/each} + {#if items.length === 0} +
+ Aufgabe hierher ziehen +
+ {/if} +
+{:else} +
+ {#each tasks as task (task.id)} + handleToggleComplete(task)} + onDelete={() => handleDelete(task.id)} + onEdit={onEditTask ? () => onEditTask(task) : undefined} + /> + {/each} +
+{/if} + + diff --git a/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts b/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts index 435057133..429a2ac93 100644 --- a/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts +++ b/apps/todo/apps/web/src/lib/stores/tasks.svelte.ts @@ -242,7 +242,7 @@ export const tasksStore = { * Update task optimistically (for drag and drop) * Updates local state immediately, then syncs with server */ - updateTaskOptimistic( + async updateTaskOptimistic( id: string, data: { dueDate?: string | null; @@ -255,32 +255,25 @@ export const tasksStore = { tasks = tasks.map((t) => (t.id === id ? { ...t, ...data } : t)); - // Sync with server in background - if (data.isCompleted !== undefined) { - const apiCall = data.isCompleted ? tasksApi.completeTask(id) : tasksApi.uncompleteTask(id); + try { + // Handle completion state change first + if (data.isCompleted !== undefined && data.isCompleted !== originalTask.isCompleted) { + if (data.isCompleted) { + await tasksApi.completeTask(id); + } else { + await tasksApi.uncompleteTask(id); + } + } - apiCall - .then((updatedTask) => { - tasks = tasks.map((t) => (t.id === id ? updatedTask : t)); - }) - .catch((e) => { - // Rollback on error - console.error('Failed to update task:', e); - tasks = tasks.map((t) => (t.id === id ? originalTask : t)); - }); - } - - if (data.dueDate !== undefined) { - tasksApi - .updateTask(id, { dueDate: data.dueDate }) - .then((updatedTask) => { - tasks = tasks.map((t) => (t.id === id ? updatedTask : t)); - }) - .catch((e) => { - // Rollback on error - console.error('Failed to update task:', e); - tasks = tasks.map((t) => (t.id === id ? originalTask : t)); - }); + // Handle due date change + if (data.dueDate !== undefined) { + const updatedTask = await tasksApi.updateTask(id, { dueDate: data.dueDate }); + tasks = tasks.map((t) => (t.id === id ? updatedTask : t)); + } + } catch (e) { + // Rollback on error + console.error('Failed to update task:', e); + tasks = tasks.map((t) => (t.id === id ? originalTask : t)); } }, diff --git a/packages/shared-theme/src/app-routes.ts b/packages/shared-theme/src/app-routes.ts index dcfa573c1..4b9063691 100644 --- a/packages/shared-theme/src/app-routes.ts +++ b/packages/shared-theme/src/app-routes.ts @@ -15,6 +15,8 @@ export interface AppRoute { labelKey: string; /** Optional icon name */ icon?: string; + /** If true, this route cannot be hidden (e.g., Settings, Home) */ + alwaysVisible?: boolean; } /** @@ -199,3 +201,46 @@ export function getAvailableRoutes(appId: string): AppRoute[] { export function getDefaultRoute(appId: string): string { return APP_ROUTES[appId]?.defaultRoute ?? '/'; } + +/** + * Filter hidden navigation items from a list of nav items + * @param appId The app identifier + * @param items Array of nav items with href property + * @param hiddenNavItems Hidden items config (appId -> hidden paths) + * @returns Filtered array with hidden items removed + */ +export function filterHiddenNavItems( + appId: string, + items: T[], + hiddenNavItems: Record = {} +): T[] { + const hidden = hiddenNavItems[appId] || []; + return items.filter((item) => !hidden.includes(item.href)); +} + +/** + * Get routes that can be hidden for a specific app + * (excludes routes marked as alwaysVisible) + * @param appId The app identifier + * @returns Array of routes that can be hidden + */ +export function getHideableRoutes(appId: string): AppRoute[] { + const config = APP_ROUTES[appId]; + return config?.availableRoutes.filter((r) => !r.alwaysVisible) || []; +} + +/** + * Check if a route is hidden for a specific app + * @param appId The app identifier + * @param path The route path + * @param hiddenNavItems Hidden items config + * @returns True if the route is hidden + */ +export function isRouteHidden( + appId: string, + path: string, + hiddenNavItems: Record = {} +): boolean { + const hidden = hiddenNavItems[appId] || []; + return hidden.includes(path); +} diff --git a/packages/shared-theme/src/index.ts b/packages/shared-theme/src/index.ts index 090d9659b..9a5e51864 100644 --- a/packages/shared-theme/src/index.ts +++ b/packages/shared-theme/src/index.ts @@ -117,4 +117,12 @@ export { // App Routes export type { AppRoute, AppRouteConfig } from './app-routes'; -export { APP_ROUTES, getStartPage, getAvailableRoutes, getDefaultRoute } from './app-routes'; +export { + APP_ROUTES, + getStartPage, + getAvailableRoutes, + getDefaultRoute, + filterHiddenNavItems, + getHideableRoutes, + isRouteHidden, +} from './app-routes'; diff --git a/packages/shared-theme/src/types.ts b/packages/shared-theme/src/types.ts index d8eed66a7..964771bec 100644 --- a/packages/shared-theme/src/types.ts +++ b/packages/shared-theme/src/types.ts @@ -240,6 +240,8 @@ export interface NavSettings { desktopPosition: NavPosition; /** Whether sidebar is collapsed */ sidebarCollapsed: boolean; + /** Hidden navigation items per app (appId -> list of hidden paths) */ + hiddenNavItems?: Record; } /** @@ -323,7 +325,7 @@ export const DEFAULT_GENERAL_SETTINGS: GeneralSettings = { * Default global settings */ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = { - nav: { desktopPosition: 'top', sidebarCollapsed: false }, + nav: { desktopPosition: 'top', sidebarCollapsed: false, hiddenNavItems: {} }, theme: { mode: 'system', colorScheme: 'ocean', pinnedThemes: [] }, locale: 'de', general: DEFAULT_GENERAL_SETTINGS, @@ -364,6 +366,12 @@ export interface UserSettingsStore { setStartPage: (appId: string, path: string) => Promise; /** Update general settings */ updateGeneral: (settings: Partial) => Promise; + /** Get hidden nav items for a specific app */ + getHiddenNavItemsForApp: (appId: string) => string[]; + /** Toggle visibility of a navigation item */ + toggleNavItemVisibility: (appId: string, href: string) => Promise; + /** Set hidden nav items for an app */ + setHiddenNavItems: (appId: string, hiddenHrefs: string[]) => Promise; } /** diff --git a/packages/shared-theme/src/user-settings-store.svelte.ts b/packages/shared-theme/src/user-settings-store.svelte.ts index 7a9ee94f6..c2ecabd16 100644 --- a/packages/shared-theme/src/user-settings-store.svelte.ts +++ b/packages/shared-theme/src/user-settings-store.svelte.ts @@ -314,6 +314,46 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe } } + /** + * Get hidden nav items for a specific app + */ + function getHiddenNavItemsForApp(targetAppId: string): string[] { + return globalSettings.nav.hiddenNavItems?.[targetAppId] || []; + } + + /** + * Toggle visibility of a navigation item for an app + */ + async function toggleNavItemVisibility(targetAppId: string, href: string): Promise { + const currentHidden = getHiddenNavItemsForApp(targetAppId); + const isHidden = currentHidden.includes(href); + + const newHidden = isHidden ? currentHidden.filter((h) => h !== href) : [...currentHidden, href]; + + await setHiddenNavItems(targetAppId, newHidden); + } + + /** + * Set hidden nav items for an app + */ + async function setHiddenNavItems(targetAppId: string, hiddenHrefs: string[]): Promise { + const newHiddenNavItems = { + ...globalSettings.nav.hiddenNavItems, + [targetAppId]: hiddenHrefs, + }; + + // Remove empty arrays + if (hiddenHrefs.length === 0) { + delete newHiddenNavItems[targetAppId]; + } + + await updateGlobal({ + nav: { + hiddenNavItems: newHiddenNavItems, + }, + } as Partial); + } + return { get nav() { return nav; @@ -349,5 +389,8 @@ export function createUserSettingsStore(config: UserSettingsStoreConfig): UserSe removeAppOverride, setStartPage, updateGeneral, + getHiddenNavItemsForApp, + toggleNavItemVisibility, + setHiddenNavItems, }; } diff --git a/packages/shared-ui/src/command-bar/CommandBar.svelte b/packages/shared-ui/src/command-bar/CommandBar.svelte index b28d2c4e7..623e1f3c0 100644 --- a/packages/shared-ui/src/command-bar/CommandBar.svelte +++ b/packages/shared-ui/src/command-bar/CommandBar.svelte @@ -8,11 +8,11 @@ } const HIGHLIGHT_PATTERNS: HighlightPattern[] = [ - // Priority keywords (Todo) - { - pattern: /(!{1,3}|!?dringend|!?wichtig|!?normal|!?spÀter|!?spÀer)\b/gi, - className: 'hl-priority', - }, + // Priority keywords (Todo) - with specific colors per level + { pattern: /(!{3,}|!?dringend)\b/gi, className: 'hl-priority-urgent' }, + { pattern: /(!{2}|!?wichtig)\b/gi, className: 'hl-priority-high' }, + { pattern: /!?normal\b/gi, className: 'hl-priority-medium' }, + { pattern: /!?sp[aÀ]ter\b/gi, className: 'hl-priority-low' }, // Tags { pattern: /#\w+/g, className: 'hl-tag' }, // Projects/Calendars/Companies (@reference) @@ -593,9 +593,24 @@ color: hsl(var(--color-muted-foreground)); } - /* Syntax highlighting colors */ - .input-highlight-backdrop :global(.hl-priority) { - color: hsl(var(--color-warning, 38 92% 50%)); + /* Syntax highlighting colors - Priority levels with matching UI colors */ + .input-highlight-backdrop :global(.hl-priority-urgent) { + color: #ef4444; /* red - Dringend */ + font-weight: 600; + } + + .input-highlight-backdrop :global(.hl-priority-high) { + color: #f97316; /* orange - Wichtig */ + font-weight: 600; + } + + .input-highlight-backdrop :global(.hl-priority-medium) { + color: #eab308; /* yellow - Normal */ + font-weight: 600; + } + + .input-highlight-backdrop :global(.hl-priority-low) { + color: #22c55e; /* green - SpÀter */ font-weight: 600; } diff --git a/packages/shared-ui/src/settings/GlobalSettingsSection.svelte b/packages/shared-ui/src/settings/GlobalSettingsSection.svelte index 21abe64f9..20aefb85a 100644 --- a/packages/shared-ui/src/settings/GlobalSettingsSection.svelte +++ b/packages/shared-ui/src/settings/GlobalSettingsSection.svelte @@ -8,6 +8,7 @@ import { getAvailableRoutes, getDefaultRoute } from '@manacore/shared-theme'; import SettingsSection from './SettingsSection.svelte'; import SettingsCard from './SettingsCard.svelte'; + import NavVisibilitySettings from './NavVisibilitySettings.svelte'; interface Props { /** User settings store instance */ @@ -16,6 +17,8 @@ appId?: string; /** Whether to show navigation settings */ showNavigation?: boolean; + /** Whether to show nav visibility settings */ + showNavVisibility?: boolean; /** Whether to show theme settings */ showTheme?: boolean; /** Whether to show language settings */ @@ -34,6 +37,7 @@ userSettings, appId, showNavigation = true, + showNavVisibility = true, showTheme = true, showLanguage = true, showGeneral = true, @@ -205,10 +209,21 @@
{/if} + {#if showNavVisibility && appId} + +
+ +
+ {/if} + {#if showTheme}

@@ -303,7 +318,10 @@ {#if showGeneral}
diff --git a/packages/shared-ui/src/settings/NavVisibilitySettings.svelte b/packages/shared-ui/src/settings/NavVisibilitySettings.svelte new file mode 100644 index 000000000..a6930f4d8 --- /dev/null +++ b/packages/shared-ui/src/settings/NavVisibilitySettings.svelte @@ -0,0 +1,161 @@ + + +
+
+

+ Navigation anpassen +

+

+ Versteckte Seiten bleiben ĂŒber die URL erreichbar +

+
+ +
+ {#each sortedApps as app (app.id)} +
+ + + + + {#if expandedApps[app.id]} +
+ {#each app.routes as route (route.path)} + {@const hidden = isRouteHidden(app.id, route.path)} + + {/each} +
+ {/if} +
+ {/each} +
+
diff --git a/packages/shared-ui/src/settings/index.ts b/packages/shared-ui/src/settings/index.ts index f7daa394c..06db8bce2 100644 --- a/packages/shared-ui/src/settings/index.ts +++ b/packages/shared-ui/src/settings/index.ts @@ -10,3 +10,4 @@ export { default as SettingsTimeInput } from './SettingsTimeInput.svelte'; export { default as SettingsDangerZone } from './SettingsDangerZone.svelte'; export { default as SettingsDangerButton } from './SettingsDangerButton.svelte'; export { default as GlobalSettingsSection } from './GlobalSettingsSection.svelte'; +export { default as NavVisibilitySettings } from './NavVisibilitySettings.svelte'; diff --git a/services/mana-core-auth/src/settings/dto/index.ts b/services/mana-core-auth/src/settings/dto/index.ts index bc8ee0e3d..2f315c1fe 100644 --- a/services/mana-core-auth/src/settings/dto/index.ts +++ b/services/mana-core-auth/src/settings/dto/index.ts @@ -18,6 +18,10 @@ export class NavSettingsDto { @IsOptional() @IsBoolean() sidebarCollapsed?: boolean; + + @IsOptional() + @IsObject() + hiddenNavItems?: Record; } // Theme settings @@ -70,6 +74,7 @@ export class UpdateAppOverrideDto { export interface NavSettings { desktopPosition: 'top' | 'bottom'; sidebarCollapsed: boolean; + hiddenNavItems?: Record; } export interface ThemeSettings { From 75bf7ecf4793635df9ffdf5bf335e1d0ff67c2fc Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:09:10 +0100 Subject: [PATCH 15/26] fix(todo): improve drag-and-drop reliability with ID-based sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use sorted task IDs instead of array reference to detect real changes from parent. Update lastTaskIds after finalize to prevent $effect from reverting local DnD state changes. đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../web/src/lib/components/TaskList.svelte | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/apps/todo/apps/web/src/lib/components/TaskList.svelte b/apps/todo/apps/web/src/lib/components/TaskList.svelte index 1ce2fba42..b23b5b6a2 100644 --- a/apps/todo/apps/web/src/lib/components/TaskList.svelte +++ b/apps/todo/apps/web/src/lib/components/TaskList.svelte @@ -25,14 +25,18 @@ // Local mutable state for dnd-zone let items = $state([]); - // Track last known tasks reference to detect parent updates - let lastTasksRef: Task[] | null = null; + // Create a stable key from task IDs to detect real changes + let lastTaskIds = ''; - // Sync items with tasks only when tasks array reference changes - $effect.pre(() => { - if (tasks !== lastTasksRef) { + // Sync items with tasks only when the set of task IDs changes + $effect(() => { + const currentIds = tasks + .map((t) => t.id) + .sort() + .join(','); + if (currentIds !== lastTaskIds) { items = [...tasks]; - lastTasksRef = tasks; + lastTaskIds = currentIds; } }); @@ -54,7 +58,12 @@ onTaskDrop(movedTaskId, dropTargetDate); } + // Update local state and sync lastTaskIds to prevent $effect from reverting items = newItems; + lastTaskIds = newItems + .map((t) => t.id) + .sort() + .join(','); } async function handleToggleComplete(task: Task) { From a898160423b46524189b8078c58f573301e1e083 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:21:33 +0100 Subject: [PATCH 16/26] refactor(todo): rename Labels to Tags for consistency across apps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename route /labels to /tags and /label/[id] to /tag/[id] - Rename LabelSelector component to TagSelector - Update all UI texts from "Labels" to "Tags" - Update navigation items and references - Align terminology with Calendar and Contacts apps đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .env.development | 1 + apps/calendar/apps/web/src/lib/api/todos.ts | 370 ++++++++++++++++ .../lib/components/todo/PriorityBadge.svelte | 124 ++++++ .../lib/components/todo/QuickAddTodo.svelte | 226 ++++++++++ .../lib/components/todo/TodoCheckbox.svelte | 130 ++++++ .../src/lib/components/todo/TodoItem.svelte | 287 +++++++++++++ .../apps/web/src/lib/stores/todos.svelte.ts | 405 ++++++++++++++++++ .../apps/web/src/routes/(app)/+layout.svelte | 14 +- .../web/src/routes/(protected)/+layout.svelte | 14 +- .../apps/web/src/routes/(app)/+layout.svelte | 14 +- .../apps/web/src/routes/(app)/+layout.svelte | 14 +- .../apps/web/src/routes/(app)/+layout.svelte | 10 +- .../apps/web/src/routes/app/+layout.svelte | 10 +- .../lib/components/CollapsibleSection.svelte | 34 +- .../src/lib/components/TaskEditModal.svelte | 8 +- .../web/src/lib/components/TaskItem.svelte | 104 ++++- .../web/src/lib/components/TaskList.svelte | 21 +- ...abelSelector.svelte => TagSelector.svelte} | 86 ++-- .../apps/web/src/lib/components/form/index.ts | 2 +- .../components/kanban/KanbanFilters.svelte | 7 +- .../apps/web/src/lib/stores/tasks.svelte.ts | 6 +- .../apps/web/src/routes/(app)/+layout.svelte | 27 +- .../(app)/{label => tag}/[id]/+page.svelte | 58 +-- .../(app)/{labels => tags}/+page.svelte | 40 +- .../apps/web/src/routes/(app)/+layout.svelte | 14 +- packages/shared-theme/src/app-routes.ts | 124 +++--- .../src/settings/GlobalSettingsSection.svelte | 4 +- .../src/settings/NavVisibilitySettings.svelte | 191 +++------ scripts/generate-env.mjs | 2 + 29 files changed, 1964 insertions(+), 383 deletions(-) create mode 100644 apps/calendar/apps/web/src/lib/api/todos.ts create mode 100644 apps/calendar/apps/web/src/lib/components/todo/PriorityBadge.svelte create mode 100644 apps/calendar/apps/web/src/lib/components/todo/QuickAddTodo.svelte create mode 100644 apps/calendar/apps/web/src/lib/components/todo/TodoCheckbox.svelte create mode 100644 apps/calendar/apps/web/src/lib/components/todo/TodoItem.svelte create mode 100644 apps/calendar/apps/web/src/lib/stores/todos.svelte.ts rename apps/todo/apps/web/src/lib/components/form/{LabelSelector.svelte => TagSelector.svelte} (65%) rename apps/todo/apps/web/src/routes/(app)/{label => tag}/[id]/+page.svelte (81%) rename apps/todo/apps/web/src/routes/(app)/{labels => tags}/+page.svelte (89%) diff --git a/.env.development b/.env.development index 484e7d955..f2c0b8483 100644 --- a/.env.development +++ b/.env.development @@ -228,6 +228,7 @@ CLOCK_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/clock # ============================================ TODO_BACKEND_PORT=3018 +TODO_BACKEND_URL=http://localhost:3018 TODO_DATABASE_URL=postgresql://manacore:devpassword@localhost:5432/todo # ============================================ diff --git a/apps/calendar/apps/web/src/lib/api/todos.ts b/apps/calendar/apps/web/src/lib/api/todos.ts new file mode 100644 index 000000000..fa649d829 --- /dev/null +++ b/apps/calendar/apps/web/src/lib/api/todos.ts @@ -0,0 +1,370 @@ +/** + * Cross-App API Client for Todo Backend + * Allows Calendar app to fetch/manage todos from the Todo service + */ + +import { browser } from '$app/environment'; +import { env } from '$env/dynamic/public'; + +const TODO_API_BASE = env.PUBLIC_TODO_BACKEND_URL || 'http://localhost:3018'; + +// ============================================ +// Types (mirrored from @todo/shared for cross-app use) +// ============================================ + +export type TaskPriority = 'low' | 'medium' | 'high' | 'urgent'; +export type TaskStatus = 'pending' | 'in_progress' | 'completed' | 'cancelled'; + +export interface Subtask { + id: string; + title: string; + isCompleted: boolean; + completedAt?: string | null; + order: number; +} + +export interface Label { + id: string; + userId: string; + name: string; + color: string; + createdAt: string; + updatedAt: string; +} + +export interface Project { + id: string; + userId: string; + name: string; + description?: string | null; + color: string; + icon?: string | null; + order: number; + isArchived: boolean; + isDefault: boolean; + createdAt: string; + updatedAt: string; +} + +export interface TaskMetadata { + notes?: string; + attachments?: string[]; + linkedCalendarEventId?: string | null; + storyPoints?: number | null; + effectiveDuration?: { + value: number; + unit: 'minutes' | 'hours' | 'days'; + } | null; + funRating?: number | null; +} + +export interface Task { + id: string; + projectId?: string | null; + userId: string; + parentTaskId?: string | null; + title: string; + description?: string | null; + dueDate?: string | null; + dueTime?: string | null; + startDate?: string | null; + priority: TaskPriority; + status: TaskStatus; + isCompleted: boolean; + completedAt?: string | null; + order: number; + columnId?: string | null; + columnOrder?: number; + recurrenceRule?: string | null; + recurrenceEndDate?: string | null; + lastOccurrence?: string | null; + subtasks?: Subtask[] | null; + metadata?: TaskMetadata | null; + labels?: Label[]; + project?: Project | null; + createdAt: string; + updatedAt: string; +} + +export interface CreateTaskInput { + title: string; + description?: string; + projectId?: string | null; + dueDate?: string | null; + dueTime?: string | null; + priority?: TaskPriority; + labelIds?: string[]; + subtasks?: Omit[]; + recurrenceRule?: string | null; + metadata?: TaskMetadata; +} + +export interface UpdateTaskInput { + title?: string; + description?: string | null; + projectId?: string | null; + dueDate?: string | null; + dueTime?: string | null; + priority?: TaskPriority; + status?: TaskStatus; + isCompleted?: boolean; + subtasks?: Subtask[] | null; + recurrenceRule?: string | null; + metadata?: TaskMetadata | null; + labelIds?: string[]; +} + +export interface TaskQuery { + projectId?: string; + labelId?: string; + priority?: TaskPriority; + status?: TaskStatus; + isCompleted?: boolean; + dueDateFrom?: string; + dueDateTo?: string; + search?: string; + sortBy?: 'dueDate' | 'priority' | 'createdAt' | 'order'; + sortOrder?: 'asc' | 'desc'; + limit?: number; + offset?: number; +} + +// ============================================ +// API Response Types +// ============================================ + +interface TasksResponse { + tasks: Task[]; +} + +interface TaskResponse { + task: Task; +} + +interface ProjectsResponse { + projects: Project[]; +} + +interface LabelsResponse { + labels: Label[]; +} + +// ============================================ +// API Client +// ============================================ + +type FetchOptions = { + method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; + body?: unknown; + token?: string; +}; + +async function fetchTodoApi( + endpoint: string, + options: FetchOptions = {} +): Promise<{ data: T | null; error: Error | null }> { + const { method = 'GET', body, token } = options; + + let authToken = token; + if (!authToken && browser) { + authToken = localStorage.getItem('@auth/appToken') || undefined; + } + + try { + const headers: Record = { + 'Content-Type': 'application/json', + }; + + if (authToken) { + headers['Authorization'] = `Bearer ${authToken}`; + } + + const response = await fetch(`${TODO_API_BASE}/api/v1${endpoint}`, { + method, + headers, + body: body ? JSON.stringify(body) : undefined, + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})); + return { + data: null, + error: new Error(errorData.message || `Todo API error: ${response.status}`), + }; + } + + // Handle empty responses (204 No Content) + if (response.status === 204) { + return { data: null, error: null }; + } + + const data = await response.json(); + return { data, error: null }; + } catch (error) { + return { + data: null, + error: error instanceof Error ? error : new Error('Failed to connect to Todo service'), + }; + } +} + +// ============================================ +// Helper Functions +// ============================================ + +function buildQueryString(query: TaskQuery): string { + const params = new URLSearchParams(); + Object.entries(query).forEach(([key, value]) => { + if (value !== undefined && value !== null) { + params.append(key, String(value)); + } + }); + const queryString = params.toString(); + return queryString ? `?${queryString}` : ''; +} + +// ============================================ +// Task API Functions +// ============================================ + +export async function getTasks( + query: TaskQuery = {} +): Promise<{ data: Task[] | null; error: Error | null }> { + const queryString = buildQueryString(query); + const result = await fetchTodoApi(`/tasks${queryString}`); + return { + data: result.data?.tasks || null, + error: result.error, + }; +} + +export async function getTask(id: string): Promise<{ data: Task | null; error: Error | null }> { + const result = await fetchTodoApi(`/tasks/${id}`); + return { + data: result.data?.task || null, + error: result.error, + }; +} + +export async function createTask( + data: CreateTaskInput +): Promise<{ data: Task | null; error: Error | null }> { + const result = await fetchTodoApi('/tasks', { + method: 'POST', + body: data, + }); + return { + data: result.data?.task || null, + error: result.error, + }; +} + +export async function updateTask( + id: string, + data: UpdateTaskInput +): Promise<{ data: Task | null; error: Error | null }> { + const result = await fetchTodoApi(`/tasks/${id}`, { + method: 'PUT', + body: data, + }); + return { + data: result.data?.task || null, + error: result.error, + }; +} + +export async function deleteTask(id: string): Promise<{ error: Error | null }> { + const result = await fetchTodoApi(`/tasks/${id}`, { + method: 'DELETE', + }); + return { error: result.error }; +} + +export async function completeTask( + id: string +): Promise<{ data: Task | null; error: Error | null }> { + const result = await fetchTodoApi(`/tasks/${id}/complete`, { + method: 'POST', + }); + return { + data: result.data?.task || null, + error: result.error, + }; +} + +export async function uncompleteTask( + id: string +): Promise<{ data: Task | null; error: Error | null }> { + const result = await fetchTodoApi(`/tasks/${id}/uncomplete`, { + method: 'POST', + }); + return { + data: result.data?.task || null, + error: result.error, + }; +} + +export async function getTodayTasks(): Promise<{ data: Task[] | null; error: Error | null }> { + const result = await fetchTodoApi('/tasks/today'); + return { + data: result.data?.tasks || null, + error: result.error, + }; +} + +export async function getUpcomingTasks(): Promise<{ data: Task[] | null; error: Error | null }> { + const result = await fetchTodoApi('/tasks/upcoming'); + return { + data: result.data?.tasks || null, + error: result.error, + }; +} + +// ============================================ +// Project API Functions +// ============================================ + +export async function getProjects(): Promise<{ data: Project[] | null; error: Error | null }> { + const result = await fetchTodoApi('/projects'); + return { + data: result.data?.projects || null, + error: result.error, + }; +} + +// ============================================ +// Label API Functions +// ============================================ + +export async function getLabels(): Promise<{ data: Label[] | null; error: Error | null }> { + const result = await fetchTodoApi('/labels'); + return { + data: result.data?.labels || null, + error: result.error, + }; +} + +// ============================================ +// Priority Colors Helper +// ============================================ + +export const PRIORITY_COLORS: Record = { + urgent: 'hsl(var(--color-danger))', + high: 'hsl(var(--color-warning))', + medium: 'hsl(var(--color-accent))', + low: 'hsl(var(--color-success))', +}; + +export const PRIORITY_LABELS: Record = { + urgent: 'Dringend', + high: 'Wichtig', + medium: 'Normal', + low: 'SpĂ€ter', +}; + +export const PRIORITY_ORDER: Record = { + urgent: 0, + high: 1, + medium: 2, + low: 3, +}; diff --git a/apps/calendar/apps/web/src/lib/components/todo/PriorityBadge.svelte b/apps/calendar/apps/web/src/lib/components/todo/PriorityBadge.svelte new file mode 100644 index 000000000..2ad0693ec --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/todo/PriorityBadge.svelte @@ -0,0 +1,124 @@ + + +{#if variant === 'dot'} + +{:else if variant === 'badge'} + + {#if showLabel} + {label} + {:else} + {priority.charAt(0).toUpperCase()} + {/if} + +{:else if variant === 'pill'} + + + {#if showLabel} + {label} + {/if} + +{/if} + + diff --git a/apps/calendar/apps/web/src/lib/components/todo/QuickAddTodo.svelte b/apps/calendar/apps/web/src/lib/components/todo/QuickAddTodo.svelte new file mode 100644 index 000000000..573e3794a --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/todo/QuickAddTodo.svelte @@ -0,0 +1,226 @@ + + +{#if showButton && !isExpanded} + +{:else} +
+ + + {#if showButton} + + {/if} + + +
+{/if} + + diff --git a/apps/calendar/apps/web/src/lib/components/todo/TodoCheckbox.svelte b/apps/calendar/apps/web/src/lib/components/todo/TodoCheckbox.svelte new file mode 100644 index 000000000..640043528 --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/todo/TodoCheckbox.svelte @@ -0,0 +1,130 @@ + + + + + diff --git a/apps/calendar/apps/web/src/lib/components/todo/TodoItem.svelte b/apps/calendar/apps/web/src/lib/components/todo/TodoItem.svelte new file mode 100644 index 000000000..0a24b386d --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/todo/TodoItem.svelte @@ -0,0 +1,287 @@ + + +
+ + +
+
+ {#if showPriority && variant !== 'minimal'} + + {/if} + + {task.title} + + {#if subtaskProgress && variant === 'default'} + + {subtaskProgress.completed}/{subtaskProgress.total} + + {/if} +
+ + {#if variant !== 'minimal'} +
+ {#if showDueDate && dueDateLabel} + + {dueDateLabel} + + {/if} + + {#if showProject && task.project} + + {task.project.name} + + {/if} + + {#if task.labels && task.labels.length > 0 && variant === 'default'} +
+ {#each task.labels.slice(0, 2) as label} + + {label.name} + + {/each} + {#if task.labels.length > 2} + +{task.labels.length - 2} + {/if} +
+ {/if} +
+ {/if} +
+
+ + diff --git a/apps/calendar/apps/web/src/lib/stores/todos.svelte.ts b/apps/calendar/apps/web/src/lib/stores/todos.svelte.ts new file mode 100644 index 000000000..1311d0971 --- /dev/null +++ b/apps/calendar/apps/web/src/lib/stores/todos.svelte.ts @@ -0,0 +1,405 @@ +/** + * Todos Store - Manages todos from Todo-App using Svelte 5 runes + * Cross-app integration with Todo Backend + */ + +import * as api from '$lib/api/todos'; +import type { + Task, + TaskPriority, + CreateTaskInput, + UpdateTaskInput, + TaskQuery, + Project, + Label, +} from '$lib/api/todos'; +import { PRIORITY_ORDER } from '$lib/api/todos'; +import { + format, + parseISO, + isSameDay, + isToday, + isBefore, + startOfDay, + addDays, + isWithinInterval, +} from 'date-fns'; + +// Re-export types for convenience +export type { Task, TaskPriority, CreateTaskInput, UpdateTaskInput, Project, Label }; + +// State +let todos = $state([]); +let projects = $state([]); +let labels = $state([]); +let loading = $state(false); +let error = $state(null); +let loadedRange = $state<{ start: Date; end: Date } | null>(null); +let serviceAvailable = $state(true); + +export const todosStore = { + // ========== Getters ========== + get todos() { + return todos ?? []; + }, + get projects() { + return projects ?? []; + }, + get labels() { + return labels ?? []; + }, + get loading() { + return loading; + }, + get error() { + return error; + }, + get serviceAvailable() { + return serviceAvailable; + }, + + // ========== Derived Getters ========== + + /** + * Get todos for a specific day + */ + getTodosForDay(date: Date): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + return currentTodos.filter((task) => { + if (!task.dueDate || task.isCompleted) return false; + const dueDate = typeof task.dueDate === 'string' ? parseISO(task.dueDate) : task.dueDate; + return isSameDay(dueDate, date); + }); + }, + + /** + * Get todos within a date range + */ + getTodosInRange(start: Date, end: Date): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + return currentTodos.filter((task) => { + if (!task.dueDate) return false; + const dueDate = typeof task.dueDate === 'string' ? parseISO(task.dueDate) : task.dueDate; + return isWithinInterval(dueDate, { start, end }); + }); + }, + + /** + * Get today's uncompleted todos + */ + get todaysTodos(): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + return currentTodos + .filter((task) => { + if (task.isCompleted) return false; + if (!task.dueDate) return false; + const dueDate = typeof task.dueDate === 'string' ? parseISO(task.dueDate) : task.dueDate; + return isToday(dueDate); + }) + .sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]); + }, + + /** + * Get overdue todos (due before today, not completed) + */ + get overdueTodos(): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + const today = startOfDay(new Date()); + + return currentTodos + .filter((task) => { + if (task.isCompleted) return false; + if (!task.dueDate) return false; + const dueDate = typeof task.dueDate === 'string' ? parseISO(task.dueDate) : task.dueDate; + return isBefore(startOfDay(dueDate), today); + }) + .sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]); + }, + + /** + * Get upcoming todos (next 7 days, not including today) + */ + get upcomingTodos(): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + const tomorrow = startOfDay(addDays(new Date(), 1)); + const weekFromNow = startOfDay(addDays(new Date(), 7)); + + return currentTodos + .filter((task) => { + if (task.isCompleted) return false; + if (!task.dueDate) return false; + const dueDate = typeof task.dueDate === 'string' ? parseISO(task.dueDate) : task.dueDate; + return isWithinInterval(startOfDay(dueDate), { start: tomorrow, end: weekFromNow }); + }) + .sort((a, b) => { + // First sort by date + const dateA = a.dueDate ? parseISO(a.dueDate as string) : new Date(); + const dateB = b.dueDate ? parseISO(b.dueDate as string) : new Date(); + const dateDiff = dateA.getTime() - dateB.getTime(); + if (dateDiff !== 0) return dateDiff; + // Then by priority + return PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]; + }); + }, + + /** + * Get todos without due date + */ + get unscheduledTodos(): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + return currentTodos + .filter((task) => !task.isCompleted && !task.dueDate) + .sort((a, b) => PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority]); + }, + + /** + * Get completed todos + */ + get completedTodos(): Task[] { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return []; + + return currentTodos.filter((task) => task.isCompleted); + }, + + /** + * Get combined sidebar todos (overdue + today, sorted by priority) + * Limited to show in sidebar + */ + getSidebarTodos(limit = 5): Task[] { + const overdue = this.overdueTodos; + const today = this.todaysTodos; + + // Combine and sort: overdue first, then today, both by priority + const combined = [...overdue, ...today]; + + return combined.slice(0, limit); + }, + + /** + * Get total count of active todos (not completed) + */ + get activeTodosCount(): number { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return 0; + + return currentTodos.filter((task) => !task.isCompleted).length; + }, + + // ========== API Methods ========== + + /** + * Fetch todos for a date range + */ + async fetchTodos(startDate?: Date, endDate?: Date) { + loading = true; + error = null; + + const query: TaskQuery = { + isCompleted: false, + }; + + if (startDate) { + query.dueDateFrom = format(startDate, 'yyyy-MM-dd'); + } + if (endDate) { + query.dueDateTo = format(endDate, 'yyyy-MM-dd'); + } + + const result = await api.getTasks(query); + + if (result.error) { + error = result.error.message; + serviceAvailable = false; + } else { + todos = result.data || []; + serviceAvailable = true; + if (startDate && endDate) { + loadedRange = { start: startDate, end: endDate }; + } + } + + loading = false; + return result; + }, + + /** + * Fetch today's todos (shortcut) + */ + async fetchTodayTodos() { + loading = true; + error = null; + + const result = await api.getTodayTasks(); + + if (result.error) { + error = result.error.message; + serviceAvailable = false; + } else { + // Merge with existing todos (avoid duplicates) + const newTodos = result.data || []; + const existingIds = new Set(todos.map((t) => t.id)); + const uniqueNew = newTodos.filter((t) => !existingIds.has(t.id)); + todos = [...todos, ...uniqueNew]; + serviceAvailable = true; + } + + loading = false; + return result; + }, + + /** + * Fetch upcoming todos (shortcut) + */ + async fetchUpcomingTodos() { + loading = true; + error = null; + + const result = await api.getUpcomingTasks(); + + if (result.error) { + error = result.error.message; + serviceAvailable = false; + } else { + // Merge with existing todos (avoid duplicates) + const newTodos = result.data || []; + const existingIds = new Set(todos.map((t) => t.id)); + const uniqueNew = newTodos.filter((t) => !existingIds.has(t.id)); + todos = [...todos, ...uniqueNew]; + serviceAvailable = true; + } + + loading = false; + return result; + }, + + /** + * Fetch projects + */ + async fetchProjects() { + const result = await api.getProjects(); + + if (!result.error && result.data) { + projects = result.data; + } + + return result; + }, + + /** + * Fetch labels + */ + async fetchLabels() { + const result = await api.getLabels(); + + if (!result.error && result.data) { + labels = result.data; + } + + return result; + }, + + /** + * Create a new todo + */ + async createTodo(data: CreateTaskInput) { + const result = await api.createTask(data); + + if (result.data) { + todos = [...todos, result.data]; + } + + return result; + }, + + /** + * Update a todo + */ + async updateTodo(id: string, data: UpdateTaskInput) { + const result = await api.updateTask(id, data); + + if (result.data) { + todos = todos.map((t) => (t.id === id ? result.data! : t)); + } + + return result; + }, + + /** + * Delete a todo + */ + async deleteTodo(id: string) { + const result = await api.deleteTask(id); + + if (!result.error) { + todos = todos.filter((t) => t.id !== id); + } + + return result; + }, + + /** + * Toggle todo completion + */ + async toggleComplete(id: string) { + const todo = todos.find((t) => t.id === id); + if (!todo) return { data: null, error: new Error('Todo not found') }; + + const result = todo.isCompleted ? await api.uncompleteTask(id) : await api.completeTask(id); + + if (result.data) { + todos = todos.map((t) => (t.id === id ? result.data! : t)); + } + + return result; + }, + + /** + * Get todo by ID + */ + getById(id: string): Task | undefined { + const currentTodos = todos ?? []; + if (!Array.isArray(currentTodos)) return undefined; + + return currentTodos.find((t) => t.id === id); + }, + + /** + * Get project by ID + */ + getProjectById(id: string): Project | undefined { + const currentProjects = projects ?? []; + if (!Array.isArray(currentProjects)) return undefined; + + return currentProjects.find((p) => p.id === id); + }, + + /** + * Clear todos cache + */ + clear() { + todos = []; + loadedRange = null; + }, + + /** + * Check if Todo service is available + */ + async checkServiceHealth(): Promise { + const result = await api.getTasks({ limit: 1 }); + serviceAvailable = !result.error; + return serviceAvailable; + }, +}; diff --git a/apps/calendar/apps/web/src/routes/(app)/+layout.svelte b/apps/calendar/apps/web/src/routes/(app)/+layout.svelte index 50ab7b55a..62e8f1aab 100644 --- a/apps/calendar/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/calendar/apps/web/src/routes/(app)/+layout.svelte @@ -25,6 +25,7 @@ EXTENDED_THEME_VARIANTS, } from '@manacore/shared-theme'; import type { ThemeVariant } from '@manacore/shared-theme'; + import { filterHiddenNavItems } from '@manacore/shared-theme'; import { isSidebarMode as sidebarModeStore, isNavCollapsed as collapsedStore, @@ -178,8 +179,8 @@ // User email for user dropdown let userEmail = $derived(authStore.user?.email || 'MenĂŒ'); - // Navigation items for Calendar - const navItems: PillNavItem[] = [ + // Base navigation items for Calendar + const baseNavItems: PillNavItem[] = [ { href: '/', label: 'Kalender', icon: 'calendar' }, { href: '/agenda', label: 'Agenda', icon: 'list' }, { href: '/tags', label: 'Tags', icon: 'tag' }, @@ -189,8 +190,13 @@ { href: '/feedback', label: 'Feedback', icon: 'chat' }, ]; - // Navigation shortcuts (Ctrl+1-4) - const navRoutes = navItems.map((item) => item.href); + // Navigation items filtered by visibility settings + const navItems = $derived( + filterHiddenNavItems('calendar', baseNavItems, userSettings.nav.hiddenNavItems) + ); + + // Navigation shortcuts (Ctrl+1-4) - use base items for consistent shortcuts + const navRoutes = baseNavItems.map((item) => item.href); function handleKeydown(event: KeyboardEvent) { const target = event.target as HTMLElement; diff --git a/apps/chat/apps/web/src/routes/(protected)/+layout.svelte b/apps/chat/apps/web/src/routes/(protected)/+layout.svelte index 71202a1b3..275071d82 100644 --- a/apps/chat/apps/web/src/routes/(protected)/+layout.svelte +++ b/apps/chat/apps/web/src/routes/(protected)/+layout.svelte @@ -12,6 +12,7 @@ EXTENDED_THEME_VARIANTS, } from '@manacore/shared-theme'; import type { ThemeVariant } from '@manacore/shared-theme'; + import { filterHiddenNavItems } from '@manacore/shared-theme'; import { isSidebarMode as sidebarModeStore, isNavCollapsed as collapsedStore, @@ -78,8 +79,8 @@ ); let currentLanguageLabel = $derived(getCurrentLanguageLabel(currentLocale)); - // Navigation items for Chat (settings moved to user dropdown) - const navItems: PillNavItem[] = [ + // Base navigation items for Chat (settings moved to user dropdown) + const baseNavItems: PillNavItem[] = [ { href: '/chat', label: 'Chat', icon: 'home' }, { href: '/templates', label: 'Templates', icon: 'document' }, { href: '/spaces', label: 'Spaces', icon: 'building' }, @@ -88,14 +89,19 @@ { href: '/feedback', label: 'Feedback', icon: 'chat' }, ]; + // Navigation items filtered by visibility settings + const navItems = $derived( + filterHiddenNavItems('chat', baseNavItems, userSettings.nav.hiddenNavItems) + ); + // User email for user dropdown let userEmail = $derived(authStore.user?.email); // Check if current page is a chat page (needs full-width layout) let isChatPage = $derived($page.url.pathname.startsWith('/chat')); - // Navigation shortcuts (Ctrl+1-5) - const navRoutes = navItems.map((item) => item.href); + // Navigation shortcuts (Ctrl+1-5) - use base items for consistent shortcuts + const navRoutes = baseNavItems.map((item) => item.href); function handleKeydown(event: KeyboardEvent) { const target = event.target as HTMLElement; diff --git a/apps/clock/apps/web/src/routes/(app)/+layout.svelte b/apps/clock/apps/web/src/routes/(app)/+layout.svelte index 46e5273ba..d58c91855 100644 --- a/apps/clock/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/clock/apps/web/src/routes/(app)/+layout.svelte @@ -19,6 +19,7 @@ EXTENDED_THEME_VARIANTS, } from '@manacore/shared-theme'; import type { ThemeVariant } from '@manacore/shared-theme'; + import { filterHiddenNavItems } from '@manacore/shared-theme'; import { isSidebarMode as sidebarModeStore, isNavCollapsed as collapsedStore, @@ -161,8 +162,8 @@ // User email for user dropdown let userEmail = $derived(authStore.user?.email || 'MenĂŒ'); - // Navigation items for Clock - const navItems: PillNavItem[] = [ + // Base navigation items for Clock + const baseNavItems: PillNavItem[] = [ { href: '/', label: 'Übersicht', icon: 'home' }, { href: '/alarms', label: 'Wecker', icon: 'bell' }, { href: '/timers', label: 'Timer', icon: 'timer' }, @@ -174,8 +175,13 @@ { href: '/feedback', label: 'Feedback', icon: 'chat' }, ]; - // Navigation shortcuts (Ctrl+1-9) - const navRoutes = navItems.map((item) => item.href); + // Navigation items filtered by visibility settings + const navItems = $derived( + filterHiddenNavItems('clock', baseNavItems, userSettings.nav.hiddenNavItems) + ); + + // Navigation shortcuts (Ctrl+1-9) - use base items for consistent shortcuts + const navRoutes = baseNavItems.map((item) => item.href); function handleKeydown(event: KeyboardEvent) { const target = event.target as HTMLElement; diff --git a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte index 5ac9ef360..0a8871350 100644 --- a/apps/contacts/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/contacts/apps/web/src/routes/(app)/+layout.svelte @@ -20,6 +20,7 @@ EXTENDED_THEME_VARIANTS, } from '@manacore/shared-theme'; import type { ThemeVariant } from '@manacore/shared-theme'; + import { filterHiddenNavItems } from '@manacore/shared-theme'; import { isSidebarMode as sidebarModeStore, isNavCollapsed as collapsedStore, @@ -106,8 +107,8 @@ // User email for user dropdown (fallback to 'MenĂŒ' when not logged in) let userEmail = $derived(authStore.user?.email || 'MenĂŒ'); - // Navigation items for Contacts - const navItems: PillNavItem[] = [ + // Base navigation items for Contacts + const baseNavItems: PillNavItem[] = [ { href: '/', label: 'Kontakte', icon: 'users' }, { href: '/tags', label: 'Tags', icon: 'tag' }, { href: '/favorites', label: 'Favoriten', icon: 'heart' }, @@ -118,8 +119,13 @@ { href: '/help', label: 'Hilfe', icon: 'help-circle' }, ]; - // Navigation shortcuts (Ctrl+1-5) - const navRoutes = navItems.map((item) => item.href); + // Navigation items filtered by visibility settings + const navItems = $derived( + filterHiddenNavItems('contacts', baseNavItems, userSettings.nav.hiddenNavItems) + ); + + // Navigation shortcuts (Ctrl+1-5) - use base items for consistent shortcuts + const navRoutes = baseNavItems.map((item) => item.href); function handleKeydown(event: KeyboardEvent) { const target = event.target as HTMLElement; diff --git a/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte b/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte index c4f82f189..a199c8d44 100644 --- a/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/manadeck/apps/web/src/routes/(app)/+layout.svelte @@ -18,6 +18,7 @@ EXTENDED_THEME_VARIANTS, } from '@manacore/shared-theme'; import type { ThemeVariant } from '@manacore/shared-theme'; + import { filterHiddenNavItems } from '@manacore/shared-theme'; import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n'; import { getPillAppItems } from '@manacore/shared-branding'; import { setLocale, supportedLocales } from '$lib/i18n'; @@ -33,13 +34,18 @@ // Get theme state let isDark = $derived(theme.isDark); - // Navigation items for ManaDeck (Mana and Profile are in user dropdown) - const navItems: PillNavItem[] = [ + // Base navigation items for ManaDeck (Mana and Profile are in user dropdown) + const baseNavItems: PillNavItem[] = [ { href: '/decks', label: 'Decks', icon: 'archive' }, { href: '/explore', label: 'Explore', icon: 'search' }, { href: '/progress', label: 'Progress', icon: 'chart' }, ]; + // Navigation items filtered by visibility settings + const navItems = $derived( + filterHiddenNavItems('manadeck', baseNavItems, userSettings.nav.hiddenNavItems) + ); + // Get pinned themes from user settings (extended themes only) let pinnedThemes = $derived( (userSettings.theme?.pinnedThemes || []).filter((t): t is ThemeVariant => diff --git a/apps/picture/apps/web/src/routes/app/+layout.svelte b/apps/picture/apps/web/src/routes/app/+layout.svelte index 99db8a558..447101cca 100644 --- a/apps/picture/apps/web/src/routes/app/+layout.svelte +++ b/apps/picture/apps/web/src/routes/app/+layout.svelte @@ -12,6 +12,7 @@ EXTENDED_THEME_VARIANTS, } from '@manacore/shared-theme'; import type { ThemeVariant } from '@manacore/shared-theme'; + import { filterHiddenNavItems } from '@manacore/shared-theme'; import { getLanguageDropdownItems, getCurrentLanguageLabel } from '@manacore/shared-i18n'; import { getPillAppItems } from '@manacore/shared-branding'; import { setLocale, supportedLocales } from '$lib/i18n'; @@ -93,8 +94,8 @@ } }); - // Navigation items (Mana is in user dropdown via manaHref) - const navItems: PillNavItem[] = [ + // Base navigation items (Mana is in user dropdown via manaHref) + const baseNavItems: PillNavItem[] = [ { href: '/app/gallery', label: 'Galerie', icon: 'home' }, { href: '/app/board', label: 'Moodboards', icon: 'grid' }, { href: '/app/explore', label: 'Entdecken', icon: 'search' }, @@ -104,6 +105,11 @@ { href: '/app/archive', label: 'Archiv', icon: 'archive' }, ]; + // Navigation items filtered by visibility settings + const navItems = $derived( + filterHiddenNavItems('picture', baseNavItems, userSettings.nav.hiddenNavItems) + ); + // View mode options for tab group const viewModeOptions = [ { id: 'single', icon: 'list', title: 'Liste (1)' }, diff --git a/apps/todo/apps/web/src/lib/components/CollapsibleSection.svelte b/apps/todo/apps/web/src/lib/components/CollapsibleSection.svelte index e7ac1bb11..ad8dc1014 100644 --- a/apps/todo/apps/web/src/lib/components/CollapsibleSection.svelte +++ b/apps/todo/apps/web/src/lib/components/CollapsibleSection.svelte @@ -39,7 +39,7 @@
diff --git a/apps/calendar/apps/web/src/lib/components/calendar/TodoRow.svelte b/apps/calendar/apps/web/src/lib/components/calendar/TodoRow.svelte new file mode 100644 index 000000000..e18440e09 --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/calendar/TodoRow.svelte @@ -0,0 +1,169 @@ + + +{#if todosForDay.length > 0} +
+ Aufgaben: +
+ {#each visibleTodos as task (task.id)} + + + {/each} + + {#if overflowCount > 0} + + {/if} +
+
+{/if} + + +{#if selectedTask} + +{/if} + + diff --git a/apps/calendar/apps/web/src/lib/components/calendar/TodoSidebarSection.svelte b/apps/calendar/apps/web/src/lib/components/calendar/TodoSidebarSection.svelte new file mode 100644 index 000000000..2d2f28899 --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/calendar/TodoSidebarSection.svelte @@ -0,0 +1,292 @@ + + +
+ + + + + + {#if isExpanded} +
+ {#if !todosStore.serviceAvailable} +
+ + Todo-Service nicht erreichbar +
+ {:else if todosStore.loading} +
+
+ Laden... +
+ {:else if displayTodos.length === 0} +
+ + Keine offenen Aufgaben +
+ {:else} +
+ {#each displayTodos as task (task.id)} + handleTaskClick(task)} + /> + {/each} +
+ + {#if totalActiveCount > maxItems} + + {/if} + {/if} + + + {#if showQuickAdd} +
+ +
+ {/if} +
+ {/if} +
+ + +{#if selectedTask} + +{/if} + + diff --git a/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte b/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte index ed05c57a1..4b21db563 100644 --- a/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte +++ b/apps/calendar/apps/web/src/lib/components/calendar/WeekView.svelte @@ -3,6 +3,8 @@ import { eventsStore } from '$lib/stores/events.svelte'; import { calendarsStore } from '$lib/stores/calendars.svelte'; import { settingsStore } from '$lib/stores/settings.svelte'; + import { todosStore } from '$lib/stores/todos.svelte'; + import TodoRow from './TodoRow.svelte'; import { goto } from '$app/navigation'; import { format, @@ -499,6 +501,18 @@
{/if} + + {#if todosStore.serviceAvailable} +
+
+ {#each days as day} +
+ +
+ {/each} +
+ {/if} +
@@ -651,6 +665,18 @@ cursor: pointer; } + /* Todos row */ + .todos-row { + display: flex; + border-bottom: 1px solid hsl(var(--color-border) / 0.5); + } + + .todos-cell { + flex: 1; + border-left: 1px solid hsl(var(--color-border)); + min-height: 0; + } + /* Block-style all-day events (displayed as full-day blocks in the grid) */ .all-day-block-event { position: absolute; diff --git a/apps/calendar/apps/web/src/routes/(app)/+page.svelte b/apps/calendar/apps/web/src/routes/(app)/+page.svelte index 3918aab21..6707d9259 100644 --- a/apps/calendar/apps/web/src/routes/(app)/+page.svelte +++ b/apps/calendar/apps/web/src/routes/(app)/+page.svelte @@ -16,6 +16,7 @@ import YearView from '$lib/components/calendar/YearView.svelte'; import MiniCalendar from '$lib/components/calendar/MiniCalendar.svelte'; import CalendarSidebar from '$lib/components/calendar/CalendarSidebar.svelte'; + import TodoSidebarSection from '$lib/components/calendar/TodoSidebarSection.svelte'; import QuickEventOverlay from '$lib/components/event/QuickEventOverlay.svelte'; import EventDetailModal from '$lib/components/event/EventDetailModal.svelte'; import { CalendarViewSkeleton } from '$lib/components/skeletons'; @@ -130,6 +131,8 @@ + + From cd569eff4e80f70befdea43e57a590a5cb146b7f Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:55:18 +0100 Subject: [PATCH 18/26] feat(calendar): add tasks page for todo management MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add dedicated tasks route with navigation entry and todo detail modal. đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../components/todo/TodoDetailModal.svelte | 625 ++++++++++++++++++ .../apps/web/src/routes/(app)/+layout.svelte | 2 + .../web/src/routes/(app)/tasks/+page.svelte | 486 ++++++++++++++ 3 files changed, 1113 insertions(+) create mode 100644 apps/calendar/apps/web/src/lib/components/todo/TodoDetailModal.svelte create mode 100644 apps/calendar/apps/web/src/routes/(app)/tasks/+page.svelte diff --git a/apps/calendar/apps/web/src/lib/components/todo/TodoDetailModal.svelte b/apps/calendar/apps/web/src/lib/components/todo/TodoDetailModal.svelte new file mode 100644 index 000000000..59b0e6ddb --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/todo/TodoDetailModal.svelte @@ -0,0 +1,625 @@ + + + + + + + + diff --git a/apps/calendar/apps/web/src/routes/(app)/+layout.svelte b/apps/calendar/apps/web/src/routes/(app)/+layout.svelte index 62e8f1aab..d97272905 100644 --- a/apps/calendar/apps/web/src/routes/(app)/+layout.svelte +++ b/apps/calendar/apps/web/src/routes/(app)/+layout.svelte @@ -60,6 +60,7 @@ onclick: () => viewStore.goToToday(), }, { id: 'agenda', label: 'Agenda anzeigen', icon: 'list', href: '/agenda' }, + { id: 'tasks', label: 'Aufgaben anzeigen', icon: 'check-square', href: '/tasks' }, { id: 'settings', label: 'Einstellungen', icon: 'settings', href: '/settings' }, ]; @@ -183,6 +184,7 @@ const baseNavItems: PillNavItem[] = [ { href: '/', label: 'Kalender', icon: 'calendar' }, { href: '/agenda', label: 'Agenda', icon: 'list' }, + { href: '/tasks', label: 'Aufgaben', icon: 'check-square' }, { href: '/tags', label: 'Tags', icon: 'tag' }, { href: '/statistics', label: 'Statistiken', icon: 'bar-chart-3' }, { href: '/network', label: 'Netzwerk', icon: 'share-2' }, diff --git a/apps/calendar/apps/web/src/routes/(app)/tasks/+page.svelte b/apps/calendar/apps/web/src/routes/(app)/tasks/+page.svelte new file mode 100644 index 000000000..403dbd875 --- /dev/null +++ b/apps/calendar/apps/web/src/routes/(app)/tasks/+page.svelte @@ -0,0 +1,486 @@ + + + + Aufgaben | Kalender + + +
+ + + + + + +
+ {#if showQuickAdd} + (showQuickAdd = false)} + oncancel={() => (showQuickAdd = false)} + /> + {:else} + + {/if} +
+ + + {#if loading} + + {:else if !todosStore.serviceAvailable} +
+ +

Todo-Service ist nicht erreichbar

+

Bitte versuchen Sie es spÀter erneut

+
+ {:else if groupedItems.length === 0} +
+ +

Keine EintrÀge gefunden

+

+ {#if !showEvents && !showTodos} + Aktivieren Sie mindestens einen Filter + {:else} + Erstellen Sie eine neue Aufgabe oder Àndern Sie den Zeitraum + {/if} +

+
+ {:else} +
+ {#each groupedItems as group} +
+

+ {formatDateHeader(group.date)} + ({group.items.length}) +

+ +
+ {#each group.items as item} + {#if item.type === 'event' && item.event} + handleEventClick(item.event!.id)} + /> + {:else if item.type === 'todo' && item.todo} + handleTodoClick(item.todo!)} + /> + {/if} + {/each} +
+
+ {/each} +
+ {/if} +
+ + +{#if selectedTask} + +{/if} + + From 931ee6bf9efc2565d9ec69d3ae0339084f676830 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:55:48 +0100 Subject: [PATCH 19/26] feat(calendar): add agenda view components MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add AgendaFilters and AgendaItem components for enhanced agenda view. đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../components/agenda/AgendaFilters.svelte | 151 ++++++++++++ .../lib/components/agenda/AgendaItem.svelte | 217 ++++++++++++++++++ 2 files changed, 368 insertions(+) create mode 100644 apps/calendar/apps/web/src/lib/components/agenda/AgendaFilters.svelte create mode 100644 apps/calendar/apps/web/src/lib/components/agenda/AgendaItem.svelte diff --git a/apps/calendar/apps/web/src/lib/components/agenda/AgendaFilters.svelte b/apps/calendar/apps/web/src/lib/components/agenda/AgendaFilters.svelte new file mode 100644 index 000000000..15500781d --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/agenda/AgendaFilters.svelte @@ -0,0 +1,151 @@ + + +
+
+ + +
+ +
+
+ + +
+
+
+ + diff --git a/apps/calendar/apps/web/src/lib/components/agenda/AgendaItem.svelte b/apps/calendar/apps/web/src/lib/components/agenda/AgendaItem.svelte new file mode 100644 index 000000000..ce0620e0f --- /dev/null +++ b/apps/calendar/apps/web/src/lib/components/agenda/AgendaItem.svelte @@ -0,0 +1,217 @@ + + +{#if type === 'event' && event} + +{:else if type === 'todo' && todo} +
+
+ +
+ +
+{/if} + + From 064ab3dbf6ec8069c676809f52b4446384a52941 Mon Sep 17 00:00:00 2001 From: Till-JS <101404291+Till-JS@users.noreply.github.com> Date: Wed, 10 Dec 2025 15:59:08 +0100 Subject: [PATCH 20/26] feat(shared-ui): improve nav visibility settings with icons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass navItems from app layout for app-specific configuration with icons. đŸ€– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../src/settings/GlobalSettingsSection.svelte | 16 ++- .../src/settings/NavVisibilitySettings.svelte | 131 ++++++++++++++++-- 2 files changed, 130 insertions(+), 17 deletions(-) diff --git a/packages/shared-ui/src/settings/GlobalSettingsSection.svelte b/packages/shared-ui/src/settings/GlobalSettingsSection.svelte index 1286df300..dd77908bd 100644 --- a/packages/shared-ui/src/settings/GlobalSettingsSection.svelte +++ b/packages/shared-ui/src/settings/GlobalSettingsSection.svelte @@ -10,11 +10,21 @@ import SettingsCard from './SettingsCard.svelte'; import NavVisibilitySettings from './NavVisibilitySettings.svelte'; + interface NavItem { + href: string; + label: string; + icon?: string; + } + interface Props { /** User settings store instance */ userSettings: UserSettingsStore; /** App ID for start page selection */ appId?: string; + /** Navigation items for visibility settings */ + navItems?: NavItem[]; + /** Items that should always be visible (e.g., home route) */ + alwaysVisibleHrefs?: string[]; /** Whether to show navigation settings */ showNavigation?: boolean; /** Whether to show nav visibility settings */ @@ -36,6 +46,8 @@ let { userSettings, appId, + navItems = [], + alwaysVisibleHrefs = [], showNavigation = true, showNavVisibility = true, showTheme = true, @@ -209,12 +221,12 @@
{/if} - {#if showNavVisibility && appId} + {#if showNavVisibility && appId && navItems.length > 0}
- +
{/if} diff --git a/packages/shared-ui/src/settings/NavVisibilitySettings.svelte b/packages/shared-ui/src/settings/NavVisibilitySettings.svelte index 03fbab15e..2223bfa51 100644 --- a/packages/shared-ui/src/settings/NavVisibilitySettings.svelte +++ b/packages/shared-ui/src/settings/NavVisibilitySettings.svelte @@ -1,29 +1,116 @@ @@ -41,24 +128,38 @@

- {#each hideableRoutes as route (route.path)} - {@const hidden = isRouteHidden(route.path)} + {#each hideableItems as item (item.href)} + {@const hidden = isRouteHidden(item.href)} + {@const iconPath = item.icon ? getIconPath(item.icon) : ''}