diff --git a/CLAUDE.md b/CLAUDE.md
index 01b44928d..71159db41 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -54,6 +54,7 @@ For comprehensive guidelines on code patterns and conventions, see the `.claude/
| **citycorners** | City guide for Konstanz | Web, Landing |
| **inventar** | Inventory management | Web |
| **traces** | City exploration | Backend, Mobile |
+| **taktik** | Time tracking | Web |
| **playground** | LLM playground | Web |
### Archived Projects (`apps-archived/`)
@@ -541,7 +542,7 @@ Logged in: App → IndexedDB → UI → SyncEngine → mana-sync (Go) → Postg
← WebSocket push ←
```
-### Migrated Apps (19/22)
+### Migrated Apps (20/23)
| App | Collections | Status |
|-----|------------|--------|
@@ -564,6 +565,7 @@ Logged in: App → IndexedDB → UI → SyncEngine → mana-sync (Go) → Postg
| Photos | albums, albumItems, favorites, tags, photoTags | Done |
| SkilltTree | skills, activities, achievements | Done |
| CityCorners | locations, favorites | Done |
+| Taktik | clients, projects, timeEntries, tags, templates, settings | Done |
**Not migrated (no CRUD data model):** ManaCore (hub), Matrix (protocol client), Playground (stateless)
diff --git a/apps/taktik/CLAUDE.md b/apps/taktik/CLAUDE.md
new file mode 100644
index 000000000..25172062a
--- /dev/null
+++ b/apps/taktik/CLAUDE.md
@@ -0,0 +1,73 @@
+# Taktik
+
+Zeiterfassung & Timetracking - Dein Arbeitsrhythmus, messbar gemacht.
+
+**Web App Port:** 5197
+
+## Project Overview
+
+Taktik is a professional time tracking app with timer, manual entry, projects, clients, reports, and guild (team) integration. Built local-first for offline capability and instant UI.
+
+### Tech Stack
+
+| Layer | Technology |
+|-------|------------|
+| Frontend | SvelteKit 2, Svelte 5 (runes), Tailwind CSS 4 |
+| Data | @manacore/local-store (Dexie.js + mana-sync) |
+| Icons | @manacore/shared-icons (Phosphor) |
+| PWA | @vite-pwa/sveltekit + Workbox |
+| i18n | svelte-i18n (de, en) |
+
+## Key Concepts
+
+- **Timer**: Start/stop time tracking with live counter, persists in IndexedDB
+- **Time Entries**: Core entity with duration, project, client, tags, billable flag
+- **Projects**: Client projects or internal, with budgets and billing rates
+- **Clients**: Customer management with billing rates and short codes
+- **Templates**: Saved entry patterns for quick start
+- **Reports**: Charts and statistics (ActivityHeatmap, DonutChart, TrendLineChart)
+- **Gilden**: Team time tracking with shared projects and visibility controls
+
+## Development
+
+```bash
+# From monorepo root
+pnpm dev:taktik:web # Start web app on port 5197
+pnpm dev:taktik:full # Start with auth + sync server
+```
+
+## Data Collections
+
+| Collection | Purpose |
+|------------|---------|
+| clients | Customer/client management |
+| projects | Project tracking with budgets |
+| timeEntries | Core time entry records |
+| tags | Entry categorization |
+| templates | Quick-start entry templates |
+| settings | App configuration |
+
+## Project Structure
+
+```
+apps/taktik/
+├── apps/
+│ └── web/ # SvelteKit web client
+│ ├── src/
+│ │ ├── routes/
+│ │ │ ├── (auth)/ # Login flow
+│ │ │ └── (app)/ # Authenticated app
+│ │ │ ├── entries/
+│ │ │ ├── projects/
+│ │ │ ├── clients/
+│ │ │ ├── reports/
+│ │ │ └── settings/
+│ │ └── lib/
+│ │ ├── stores/ # Svelte 5 rune stores
+│ │ ├── components/ # UI components
+│ │ ├── i18n/ # Translations (de, en)
+│ │ └── data/ # Local-store, queries, guest seed
+│ └── static/
+└── packages/
+ └── shared/ # Shared types & constants
+```
diff --git a/apps/taktik/apps/web/package.json b/apps/taktik/apps/web/package.json
new file mode 100644
index 000000000..976c4072b
--- /dev/null
+++ b/apps/taktik/apps/web/package.json
@@ -0,0 +1,52 @@
+{
+ "name": "@taktik/web",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "dev": "vite dev",
+ "build": "vite build",
+ "preview": "vite preview",
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
+ "type-check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
+ "test": "vitest run",
+ "test:unit": "vitest"
+ },
+ "devDependencies": {
+ "@sveltejs/adapter-node": "^5.2.12",
+ "@sveltejs/kit": "^2.21.0",
+ "@sveltejs/vite-plugin-svelte": "^5.1.0",
+ "@tailwindcss/vite": "^4.1.7",
+ "@types/node": "^22.15.29",
+ "svelte": "^5.41.0",
+ "svelte-check": "^4.2.1",
+ "tailwindcss": "^4.1.7",
+ "typescript": "^5.9.3",
+ "vite": "^6.3.5",
+ "vitest": "^3.2.1",
+ "@vite-pwa/sveltekit": "^1.1.0",
+ "@manacore/shared-vite-config": "workspace:*",
+ "@manacore/shared-tailwind": "workspace:*"
+ },
+ "dependencies": {
+ "@manacore/shared-auth": "workspace:*",
+ "@manacore/shared-auth-stores": "workspace:*",
+ "@manacore/shared-branding": "workspace:*",
+ "@manacore/shared-error-tracking": "workspace:*",
+ "@manacore/shared-icons": "workspace:*",
+ "@manacore/local-store": "workspace:*",
+ "@manacore/shared-auth-ui": "workspace:*",
+ "@manacore/shared-landing-ui": "workspace:*",
+ "@manacore/shared-profile-ui": "workspace:*",
+ "@manacore/shared-stores": "workspace:*",
+ "@manacore/subscriptions": "workspace:*",
+ "@manacore/shared-theme": "workspace:*",
+ "@manacore/shared-types": "workspace:*",
+ "@manacore/shared-ui": "workspace:*",
+ "@manacore/shared-utils": "workspace:*",
+ "@taktik/shared": "workspace:*",
+ "date-fns": "^4.1.0",
+ "svelte-i18n": "^4.0.1"
+ },
+ "type": "module"
+}
diff --git a/apps/taktik/apps/web/src/app.css b/apps/taktik/apps/web/src/app.css
new file mode 100644
index 000000000..398fb3cd3
--- /dev/null
+++ b/apps/taktik/apps/web/src/app.css
@@ -0,0 +1,43 @@
+@import 'tailwindcss';
+@import '@manacore/shared-tailwind/themes.css';
+
+/* Scan shared packages for Tailwind classes */
+@source "../../../packages/shared/src";
+@source "../../../../../packages/shared-ui/src";
+@source "../../../../../packages/shared-theme-ui/src";
+@source "../../../../../packages/shared-theme-ui/src/components";
+@source "../../../../../packages/shared-theme-ui/src/pages";
+
+:root {
+ --primary: 38 92% 50%;
+ --primary-foreground: 0 0% 100%;
+ --accent: 38 92% 50%;
+ --accent-foreground: 0 0% 100%;
+}
+
+/* Timer card glow */
+.timer-active {
+ box-shadow: 0 0 20px rgba(245, 158, 11, 0.15);
+}
+
+/* Entry item transitions */
+.entry-item {
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
+}
+.entry-item:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+/* Duration display monospace */
+.duration-display {
+ font-variant-numeric: tabular-nums;
+}
+
+/* Project color dot */
+.project-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
diff --git a/apps/taktik/apps/web/src/app.html b/apps/taktik/apps/web/src/app.html
new file mode 100644
index 000000000..a5df47bb5
--- /dev/null
+++ b/apps/taktik/apps/web/src/app.html
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+ Taktik
+ %sveltekit.head%
+
+
+ %sveltekit.body%
+
+
diff --git a/apps/taktik/apps/web/src/lib/components/EntryForm.svelte b/apps/taktik/apps/web/src/lib/components/EntryForm.svelte
new file mode 100644
index 000000000..548042100
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/components/EntryForm.svelte
@@ -0,0 +1,240 @@
+
+
+{#if visible}
+
+
+
+
+
+ {$_('entry.manual')}
+
+
+
+
+
+
+
+{/if}
diff --git a/apps/taktik/apps/web/src/lib/components/EntryItem.svelte b/apps/taktik/apps/web/src/lib/components/EntryItem.svelte
new file mode 100644
index 000000000..4f0a52d32
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/components/EntryItem.svelte
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+ {#if isExpanded}
+
+
+
handleDescriptionChange((e.target as HTMLInputElement).value)}
+ placeholder={$_('entry.description')}
+ class="w-full rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--input))] px-3 py-2 text-sm text-[hsl(var(--foreground))] focus:border-[hsl(var(--primary))] focus:outline-none"
+ />
+
+
+
+
+
+
+
+ handleDurationChange(parseInt((e.target as HTMLInputElement).value) || 0)}
+ min="0"
+ class="w-20 rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--input))] px-3 py-2 text-center text-sm text-[hsl(var(--foreground))]"
+ />
+ min
+
+
+
+
+
+
+
+
+
+
+ {/if}
+
diff --git a/apps/taktik/apps/web/src/lib/components/EntryList.svelte b/apps/taktik/apps/web/src/lib/components/EntryList.svelte
new file mode 100644
index 000000000..6aed4789a
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/components/EntryList.svelte
@@ -0,0 +1,79 @@
+
+
+{#if entries.length === 0}
+
+
+
{$_('entry.noEntries')}
+
+{:else}
+
+ {#each groupedEntries() as [date, dayEntries]}
+
+
+
+
+ {formatDateHeader(date)}
+
+
+ {formatDurationCompact(getTotalDuration(dayEntries))}
+
+
+
+
+
+ {#each dayEntries as entry (entry.id)}
+ (expandedEntryId = entry.id)}
+ onCollapse={() => (expandedEntryId = null)}
+ />
+ {/each}
+
+
+ {/each}
+
+{/if}
diff --git a/apps/taktik/apps/web/src/lib/components/QuickStart.svelte b/apps/taktik/apps/web/src/lib/components/QuickStart.svelte
new file mode 100644
index 000000000..0b4e35fa8
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/components/QuickStart.svelte
@@ -0,0 +1,59 @@
+
+
+{#if recentEntries().length > 0}
+
+
Quick Start
+
+ {#each recentEntries() as entry}
+ {@const project = entry.projectId
+ ? allProjects.value.find((p) => p.id === entry.projectId)
+ : undefined}
+
+ {/each}
+
+
+{/if}
diff --git a/apps/taktik/apps/web/src/lib/components/TimerCard.svelte b/apps/taktik/apps/web/src/lib/components/TimerCard.svelte
new file mode 100644
index 000000000..603647aa1
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/components/TimerCard.svelte
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+ handleDescriptionChange((e.target as HTMLInputElement).value)}
+ placeholder={$_('timer.whatAreYouWorkingOn')}
+ class="w-full rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--input))] px-4 py-2.5 text-sm text-[hsl(var(--foreground))] placeholder:text-[hsl(var(--muted-foreground))] focus:border-[hsl(var(--primary))] focus:outline-none focus:ring-1 focus:ring-[hsl(var(--primary))]"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {#if timerStore.isRunning && selectedProject}
+
+
+
{selectedProject.name}
+ {#if selectedClient}
+
· {selectedClient.name}
+ {/if}
+
+ {/if}
+
diff --git a/apps/taktik/apps/web/src/lib/data/guest-seed.ts b/apps/taktik/apps/web/src/lib/data/guest-seed.ts
new file mode 100644
index 000000000..ccad98d30
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/data/guest-seed.ts
@@ -0,0 +1,183 @@
+/**
+ * Guest seed data for the Taktik app.
+ *
+ * Provides demo clients, projects, and time entries for the guest experience.
+ */
+
+import type {
+ LocalClient,
+ LocalProject,
+ LocalTimeEntry,
+ LocalTag,
+ LocalSettings,
+} from './local-store';
+
+const DEMO_CLIENT_ID = 'demo-client-acme';
+const DEMO_PROJECT_ID = 'demo-project-redesign';
+const DEMO_INTERNAL_PROJECT_ID = 'demo-project-internal';
+
+function todayStr(): string {
+ return new Date().toISOString().split('T')[0];
+}
+
+function yesterdayStr(): string {
+ const d = new Date();
+ d.setDate(d.getDate() - 1);
+ return d.toISOString().split('T')[0];
+}
+
+export const guestClients: LocalClient[] = [
+ {
+ id: DEMO_CLIENT_ID,
+ name: 'Acme Corp',
+ shortCode: 'ACME',
+ email: 'kontakt@acme.de',
+ color: '#3b82f6',
+ isArchived: false,
+ billingRate: { amount: 95, currency: 'EUR', per: 'hour' },
+ order: 0,
+ },
+ {
+ id: 'demo-client-startup',
+ name: 'TechStartup GmbH',
+ shortCode: 'TS',
+ color: '#8b5cf6',
+ isArchived: false,
+ billingRate: { amount: 85, currency: 'EUR', per: 'hour' },
+ order: 1,
+ },
+];
+
+export const guestProjects: LocalProject[] = [
+ {
+ id: DEMO_PROJECT_ID,
+ clientId: DEMO_CLIENT_ID,
+ name: 'Website Redesign',
+ description: 'Kompletter Relaunch der Unternehmenswebsite',
+ color: '#3b82f6',
+ isArchived: false,
+ isBillable: true,
+ billingRate: { amount: 95, currency: 'EUR', per: 'hour' },
+ budget: { type: 'hours', amount: 120 },
+ visibility: 'private',
+ order: 0,
+ },
+ {
+ id: DEMO_INTERNAL_PROJECT_ID,
+ name: 'Intern / Meetings',
+ description: 'Interne Meetings, Orga, Admin',
+ color: '#6b7280',
+ isArchived: false,
+ isBillable: false,
+ visibility: 'private',
+ order: 1,
+ },
+ {
+ id: 'demo-project-app',
+ clientId: 'demo-client-startup',
+ name: 'Mobile App',
+ description: 'React Native App Entwicklung',
+ color: '#8b5cf6',
+ isArchived: false,
+ isBillable: true,
+ budget: { type: 'hours', amount: 200 },
+ visibility: 'private',
+ order: 2,
+ },
+];
+
+export const guestTimeEntries: LocalTimeEntry[] = [
+ {
+ id: 'entry-1',
+ projectId: DEMO_PROJECT_ID,
+ clientId: DEMO_CLIENT_ID,
+ description: 'Homepage Layout erstellen',
+ date: todayStr(),
+ startTime: new Date(new Date().setHours(9, 0, 0, 0)).toISOString(),
+ endTime: new Date(new Date().setHours(11, 30, 0, 0)).toISOString(),
+ duration: 9000,
+ isBillable: true,
+ isRunning: false,
+ tags: ['design'],
+ visibility: 'private',
+ source: { app: 'manual' },
+ },
+ {
+ id: 'entry-2',
+ projectId: DEMO_INTERNAL_PROJECT_ID,
+ description: 'Sprint Planning',
+ date: todayStr(),
+ startTime: new Date(new Date().setHours(11, 30, 0, 0)).toISOString(),
+ endTime: new Date(new Date().setHours(12, 15, 0, 0)).toISOString(),
+ duration: 2700,
+ isBillable: false,
+ isRunning: false,
+ tags: ['meeting'],
+ visibility: 'private',
+ source: { app: 'manual' },
+ },
+ {
+ id: 'entry-3',
+ projectId: DEMO_PROJECT_ID,
+ clientId: DEMO_CLIENT_ID,
+ description: 'API Integration',
+ date: todayStr(),
+ startTime: new Date(new Date().setHours(13, 0, 0, 0)).toISOString(),
+ endTime: new Date(new Date().setHours(15, 0, 0, 0)).toISOString(),
+ duration: 7200,
+ isBillable: true,
+ isRunning: false,
+ tags: ['development'],
+ visibility: 'private',
+ source: { app: 'timer' },
+ },
+ {
+ id: 'entry-4',
+ projectId: 'demo-project-app',
+ clientId: 'demo-client-startup',
+ description: 'Login Screen implementieren',
+ date: yesterdayStr(),
+ startTime: new Date(new Date().setHours(9, 0, 0, 0)).toISOString(),
+ endTime: new Date(new Date().setHours(12, 0, 0, 0)).toISOString(),
+ duration: 10800,
+ isBillable: true,
+ isRunning: false,
+ tags: ['development'],
+ visibility: 'private',
+ source: { app: 'timer' },
+ },
+ {
+ id: 'entry-5',
+ projectId: DEMO_PROJECT_ID,
+ clientId: DEMO_CLIENT_ID,
+ description: 'Code Review & Testing',
+ date: yesterdayStr(),
+ duration: 5400,
+ isBillable: true,
+ isRunning: false,
+ tags: ['review'],
+ visibility: 'private',
+ source: { app: 'manual' },
+ },
+];
+
+export const guestTags: LocalTag[] = [
+ { id: 'tag-design', name: 'design', color: '#f59e0b', order: 0 },
+ { id: 'tag-dev', name: 'development', color: '#3b82f6', order: 1 },
+ { id: 'tag-meeting', name: 'meeting', color: '#6b7280', order: 2 },
+ { id: 'tag-review', name: 'review', color: '#22c55e', order: 3 },
+];
+
+export const guestSettings: LocalSettings[] = [
+ {
+ id: 'default-settings',
+ workingHoursPerDay: 8,
+ workingDaysPerWeek: 5,
+ roundingIncrement: 0,
+ roundingMethod: 'none',
+ defaultVisibility: 'private',
+ weekStartsOn: 1,
+ timerReminderMinutes: 0,
+ autoStopTimerHours: 0,
+ },
+];
diff --git a/apps/taktik/apps/web/src/lib/data/local-store.ts b/apps/taktik/apps/web/src/lib/data/local-store.ts
new file mode 100644
index 000000000..22e5c759e
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/data/local-store.ts
@@ -0,0 +1,153 @@
+/**
+ * Taktik — Local-First Data Layer
+ *
+ * IndexedDB (Dexie.js) with sync support for time tracking.
+ * Clients, projects, time entries, tags, templates, and settings.
+ */
+
+import { createLocalStore, type BaseRecord } from '@manacore/local-store';
+import {
+ guestClients,
+ guestProjects,
+ guestTimeEntries,
+ guestTags,
+ guestSettings,
+} from './guest-seed';
+import type { BillingRate, ProjectVisibility, EntrySourceRef } from '@taktik/shared';
+
+// ─── Types ──────────────────────────────────────────────────
+
+export interface LocalClient extends BaseRecord {
+ name: string;
+ shortCode?: string | null;
+ contactId?: string | null;
+ email?: string | null;
+ color: string;
+ isArchived: boolean;
+ billingRate?: BillingRate | null;
+ notes?: string | null;
+ order: number;
+}
+
+export interface LocalProject extends BaseRecord {
+ clientId?: string | null;
+ name: string;
+ description?: string | null;
+ color: string;
+ isArchived: boolean;
+ isBillable: boolean;
+ billingRate?: BillingRate | null;
+ budget?: {
+ type: 'hours' | 'fixed';
+ amount: number;
+ currency?: string;
+ } | null;
+ visibility: ProjectVisibility;
+ guildId?: string | null;
+ order: number;
+}
+
+export interface LocalTimeEntry extends BaseRecord {
+ projectId?: string | null;
+ clientId?: string | null;
+ description: string;
+ date: string;
+ startTime?: string | null;
+ endTime?: string | null;
+ duration: number;
+ isBillable: boolean;
+ isRunning: boolean;
+ tags: string[];
+ billingRate?: BillingRate | null;
+ visibility: ProjectVisibility;
+ guildId?: string | null;
+ source?: EntrySourceRef | null;
+}
+
+export interface LocalTag extends BaseRecord {
+ name: string;
+ color: string;
+ order: number;
+}
+
+export interface LocalTemplate extends BaseRecord {
+ name: string;
+ projectId?: string | null;
+ clientId?: string | null;
+ description: string;
+ isBillable: boolean;
+ tags: string[];
+ usageCount: number;
+ lastUsedAt?: string | null;
+}
+
+export interface LocalSettings extends BaseRecord {
+ defaultBillingRate?: BillingRate | null;
+ workingHoursPerDay: number;
+ workingDaysPerWeek: number;
+ roundingIncrement: number;
+ roundingMethod: 'none' | 'up' | 'down' | 'nearest';
+ defaultVisibility: ProjectVisibility;
+ weekStartsOn: 0 | 1;
+ timerReminderMinutes: number;
+ autoStopTimerHours: number;
+}
+
+// ─── Store ──────────────────────────────────────────────────
+
+const SYNC_SERVER_URL = import.meta.env.PUBLIC_SYNC_SERVER_URL || 'http://localhost:3050';
+
+export const taktikStore = createLocalStore({
+ appId: 'taktik',
+ collections: [
+ {
+ name: 'clients',
+ indexes: ['order', 'isArchived', 'shortCode'],
+ guestSeed: guestClients,
+ },
+ {
+ name: 'projects',
+ indexes: ['clientId', 'isArchived', 'isBillable', 'guildId', 'visibility', 'order'],
+ guestSeed: guestProjects,
+ },
+ {
+ name: 'timeEntries',
+ indexes: [
+ 'projectId',
+ 'clientId',
+ 'date',
+ 'isRunning',
+ '[date+projectId]',
+ '[date+clientId]',
+ 'guildId',
+ 'visibility',
+ ],
+ guestSeed: guestTimeEntries,
+ },
+ {
+ name: 'tags',
+ indexes: ['name', 'order'],
+ guestSeed: guestTags,
+ },
+ {
+ name: 'templates',
+ indexes: ['usageCount', 'lastUsedAt', 'projectId'],
+ },
+ {
+ name: 'settings',
+ indexes: [],
+ guestSeed: guestSettings,
+ },
+ ],
+ sync: {
+ serverUrl: SYNC_SERVER_URL,
+ },
+});
+
+// Typed collection accessors
+export const clientCollection = taktikStore.collection('clients');
+export const projectCollection = taktikStore.collection('projects');
+export const timeEntryCollection = taktikStore.collection('timeEntries');
+export const tagCollection = taktikStore.collection('tags');
+export const templateCollection = taktikStore.collection('templates');
+export const settingsCollection = taktikStore.collection('settings');
diff --git a/apps/taktik/apps/web/src/lib/data/queries.ts b/apps/taktik/apps/web/src/lib/data/queries.ts
new file mode 100644
index 000000000..e9a4d7970
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/data/queries.ts
@@ -0,0 +1,338 @@
+/**
+ * Reactive Queries & Pure Helpers for Taktik
+ *
+ * Uses Dexie liveQuery to automatically re-render when IndexedDB changes
+ * (local writes, sync updates, other tabs).
+ */
+
+import { useLiveQueryWithDefault } from '@manacore/local-store/svelte';
+import {
+ clientCollection,
+ projectCollection,
+ timeEntryCollection,
+ tagCollection,
+ templateCollection,
+ settingsCollection,
+ type LocalClient,
+ type LocalProject,
+ type LocalTimeEntry,
+ type LocalTag,
+ type LocalTemplate,
+ type LocalSettings,
+} from './local-store';
+import type {
+ Client,
+ Project,
+ TimeEntry,
+ Tag,
+ EntryTemplate,
+ TaktikSettings,
+ FilterCriteria,
+ SortOption,
+} from '@taktik/shared';
+
+// ─── Type Converters ───────────────────────────────────────
+
+export function toClient(local: LocalClient): Client {
+ return {
+ id: local.id,
+ name: local.name,
+ shortCode: local.shortCode ?? undefined,
+ contactId: local.contactId ?? undefined,
+ email: local.email ?? undefined,
+ color: local.color,
+ isArchived: local.isArchived,
+ billingRate: local.billingRate ?? undefined,
+ notes: local.notes ?? undefined,
+ order: local.order,
+ createdAt: local.createdAt ?? new Date().toISOString(),
+ updatedAt: local.updatedAt ?? new Date().toISOString(),
+ };
+}
+
+export function toProject(local: LocalProject): Project {
+ return {
+ id: local.id,
+ clientId: local.clientId ?? undefined,
+ name: local.name,
+ description: local.description ?? undefined,
+ color: local.color,
+ isArchived: local.isArchived,
+ isBillable: local.isBillable,
+ billingRate: local.billingRate ?? undefined,
+ budget: local.budget ?? undefined,
+ visibility: local.visibility,
+ guildId: local.guildId ?? undefined,
+ order: local.order,
+ createdAt: local.createdAt ?? new Date().toISOString(),
+ updatedAt: local.updatedAt ?? new Date().toISOString(),
+ };
+}
+
+export function toTimeEntry(local: LocalTimeEntry): TimeEntry {
+ return {
+ id: local.id,
+ projectId: local.projectId ?? undefined,
+ clientId: local.clientId ?? undefined,
+ description: local.description,
+ date: local.date,
+ startTime: local.startTime ?? undefined,
+ endTime: local.endTime ?? undefined,
+ duration: local.duration,
+ isBillable: local.isBillable,
+ isRunning: local.isRunning,
+ tags: local.tags,
+ billingRate: local.billingRate ?? undefined,
+ visibility: local.visibility,
+ guildId: local.guildId ?? undefined,
+ source: local.source ?? undefined,
+ createdAt: local.createdAt ?? new Date().toISOString(),
+ updatedAt: local.updatedAt ?? new Date().toISOString(),
+ };
+}
+
+export function toTag(local: LocalTag): Tag {
+ return {
+ id: local.id,
+ name: local.name,
+ color: local.color,
+ order: local.order,
+ createdAt: local.createdAt ?? new Date().toISOString(),
+ updatedAt: local.updatedAt ?? new Date().toISOString(),
+ };
+}
+
+export function toTemplate(local: LocalTemplate): EntryTemplate {
+ return {
+ id: local.id,
+ name: local.name,
+ projectId: local.projectId ?? undefined,
+ clientId: local.clientId ?? undefined,
+ description: local.description,
+ isBillable: local.isBillable,
+ tags: local.tags,
+ usageCount: local.usageCount,
+ lastUsedAt: local.lastUsedAt ?? undefined,
+ createdAt: local.createdAt ?? new Date().toISOString(),
+ updatedAt: local.updatedAt ?? new Date().toISOString(),
+ };
+}
+
+export function toSettings(local: LocalSettings): TaktikSettings {
+ return {
+ id: local.id,
+ defaultBillingRate: local.defaultBillingRate ?? undefined,
+ workingHoursPerDay: local.workingHoursPerDay,
+ workingDaysPerWeek: local.workingDaysPerWeek,
+ roundingIncrement: local.roundingIncrement,
+ roundingMethod: local.roundingMethod,
+ defaultVisibility: local.defaultVisibility,
+ weekStartsOn: local.weekStartsOn,
+ timerReminderMinutes: local.timerReminderMinutes,
+ autoStopTimerHours: local.autoStopTimerHours,
+ createdAt: local.createdAt ?? new Date().toISOString(),
+ updatedAt: local.updatedAt ?? new Date().toISOString(),
+ };
+}
+
+// ─── Live Query Hooks ──────────────────────────────────────
+
+export function useAllClients() {
+ return useLiveQueryWithDefault(async () => {
+ const locals = await clientCollection.getAll();
+ return locals.map(toClient);
+ }, [] as Client[]);
+}
+
+export function useAllProjects() {
+ return useLiveQueryWithDefault(async () => {
+ const locals = await projectCollection.getAll();
+ return locals.map(toProject);
+ }, [] as Project[]);
+}
+
+export function useAllTimeEntries() {
+ return useLiveQueryWithDefault(async () => {
+ const locals = await timeEntryCollection.getAll();
+ return locals.map(toTimeEntry);
+ }, [] as TimeEntry[]);
+}
+
+export function useAllTags() {
+ return useLiveQueryWithDefault(async () => {
+ const locals = await tagCollection.getAll();
+ return locals.map(toTag);
+ }, [] as Tag[]);
+}
+
+export function useAllTemplates() {
+ return useLiveQueryWithDefault(async () => {
+ const locals = await templateCollection.getAll();
+ return locals.map(toTemplate);
+ }, [] as EntryTemplate[]);
+}
+
+export function useSettings() {
+ return useLiveQueryWithDefault(
+ async () => {
+ const locals = await settingsCollection.getAll();
+ return locals.length > 0 ? toSettings(locals[0]) : null;
+ },
+ null as TaktikSettings | null
+ );
+}
+
+// ─── Pure Helpers ──────────────────────────────────────────
+
+/** Format duration in seconds to HH:MM:SS */
+export function formatDuration(seconds: number): string {
+ const h = Math.floor(seconds / 3600);
+ const m = Math.floor((seconds % 3600) / 60);
+ const s = seconds % 60;
+ return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`;
+}
+
+/** Format duration in seconds to compact form (e.g., "2h 30m") */
+export function formatDurationCompact(seconds: number): string {
+ const h = Math.floor(seconds / 3600);
+ const m = Math.floor((seconds % 3600) / 60);
+ if (h === 0) return `${m}m`;
+ if (m === 0) return `${h}h`;
+ return `${h}h ${m}m`;
+}
+
+/** Format duration in seconds to decimal hours (e.g., "2.50") */
+export function formatDurationDecimal(seconds: number): string {
+ return (seconds / 3600).toFixed(2);
+}
+
+/** Get entries for a specific date */
+export function getEntriesByDate(entries: TimeEntry[], date: string): TimeEntry[] {
+ return entries
+ .filter((e) => e.date === date)
+ .sort((a, b) => {
+ if (a.startTime && b.startTime) return a.startTime.localeCompare(b.startTime);
+ return 0;
+ });
+}
+
+/** Get entries for a date range */
+export function getEntriesByDateRange(entries: TimeEntry[], from: string, to: string): TimeEntry[] {
+ return entries.filter((e) => e.date >= from && e.date <= to);
+}
+
+/** Get total duration for a list of entries */
+export function getTotalDuration(entries: TimeEntry[]): number {
+ return entries.reduce((sum, e) => sum + e.duration, 0);
+}
+
+/** Get billable duration for a list of entries */
+export function getBillableDuration(entries: TimeEntry[]): number {
+ return entries.filter((e) => e.isBillable).reduce((sum, e) => sum + e.duration, 0);
+}
+
+/** Group entries by date */
+export function groupEntriesByDate(entries: TimeEntry[]): Map {
+ const groups = new Map();
+ for (const entry of entries) {
+ const existing = groups.get(entry.date) || [];
+ existing.push(entry);
+ groups.set(entry.date, existing);
+ }
+ return groups;
+}
+
+/** Group entries by project */
+export function groupEntriesByProject(entries: TimeEntry[]): Map {
+ const groups = new Map();
+ for (const entry of entries) {
+ const key = entry.projectId || 'no-project';
+ const existing = groups.get(key) || [];
+ existing.push(entry);
+ groups.set(key, existing);
+ }
+ return groups;
+}
+
+/** Filter entries by criteria */
+export function getFilteredEntries(entries: TimeEntry[], filters: FilterCriteria): TimeEntry[] {
+ let result = entries;
+
+ if (filters.projectId) {
+ result = result.filter((e) => e.projectId === filters.projectId);
+ }
+ if (filters.clientId) {
+ result = result.filter((e) => e.clientId === filters.clientId);
+ }
+ if (filters.isBillable !== undefined) {
+ result = result.filter((e) => e.isBillable === filters.isBillable);
+ }
+ if (filters.tagIds?.length) {
+ result = result.filter((e) => filters.tagIds!.some((t) => e.tags.includes(t)));
+ }
+ if (filters.dateFrom) {
+ result = result.filter((e) => e.date >= filters.dateFrom!);
+ }
+ if (filters.dateTo) {
+ result = result.filter((e) => e.date <= filters.dateTo!);
+ }
+ if (filters.search) {
+ const q = filters.search.toLowerCase();
+ result = result.filter((e) => e.description.toLowerCase().includes(q));
+ }
+
+ return result;
+}
+
+/** Sort entries */
+export function getSortedEntries(entries: TimeEntry[], sort: SortOption): TimeEntry[] {
+ return [...entries].sort((a, b) => {
+ let cmp = 0;
+ switch (sort.field) {
+ case 'date':
+ cmp = a.date.localeCompare(b.date);
+ if (cmp === 0 && a.startTime && b.startTime) {
+ cmp = a.startTime.localeCompare(b.startTime);
+ }
+ break;
+ case 'duration':
+ cmp = a.duration - b.duration;
+ break;
+ case 'project':
+ cmp = (a.projectId || '').localeCompare(b.projectId || '');
+ break;
+ case 'client':
+ cmp = (a.clientId || '').localeCompare(b.clientId || '');
+ break;
+ case 'createdAt':
+ cmp = (a.createdAt || '').localeCompare(b.createdAt || '');
+ break;
+ }
+ return sort.direction === 'desc' ? -cmp : cmp;
+ });
+}
+
+/** Get active projects (not archived) */
+export function getActiveProjects(projects: Project[]): Project[] {
+ return projects.filter((p) => !p.isArchived).sort((a, b) => a.order - b.order);
+}
+
+/** Get active clients (not archived) */
+export function getActiveClients(clients: Client[]): Client[] {
+ return clients.filter((c) => !c.isArchived).sort((a, b) => a.order - b.order);
+}
+
+/** Get project by ID */
+export function getProjectById(projects: Project[], id: string): Project | undefined {
+ return projects.find((p) => p.id === id);
+}
+
+/** Get client by ID */
+export function getClientById(clients: Client[], id: string): Client | undefined {
+ return clients.find((c) => c.id === id);
+}
+
+/** Get projects for a client */
+export function getProjectsByClient(projects: Project[], clientId: string): Project[] {
+ return projects.filter((p) => p.clientId === clientId);
+}
diff --git a/apps/taktik/apps/web/src/lib/i18n/index.ts b/apps/taktik/apps/web/src/lib/i18n/index.ts
new file mode 100644
index 000000000..bf8800877
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/i18n/index.ts
@@ -0,0 +1,38 @@
+import { browser } from '$app/environment';
+import { init, register, locale, waitLocale } from 'svelte-i18n';
+
+export const supportedLocales = ['de', 'en'] as const;
+export type SupportedLocale = (typeof supportedLocales)[number];
+
+const defaultLocale = 'de';
+
+register('de', () => import('./locales/de.json'));
+register('en', () => import('./locales/en.json'));
+
+function getInitialLocale(): SupportedLocale {
+ if (browser) {
+ const stored = localStorage.getItem('taktik_locale');
+ if (stored && supportedLocales.includes(stored as SupportedLocale)) {
+ return stored as SupportedLocale;
+ }
+ const browserLang = navigator.language.split('-')[0];
+ if (supportedLocales.includes(browserLang as SupportedLocale)) {
+ return browserLang as SupportedLocale;
+ }
+ }
+ return defaultLocale;
+}
+
+init({
+ fallbackLocale: defaultLocale,
+ initialLocale: getInitialLocale(),
+});
+
+export function setLocale(newLocale: SupportedLocale) {
+ locale.set(newLocale);
+ if (browser) {
+ localStorage.setItem('taktik_locale', newLocale);
+ }
+}
+
+export { waitLocale };
diff --git a/apps/taktik/apps/web/src/lib/i18n/locales/de.json b/apps/taktik/apps/web/src/lib/i18n/locales/de.json
new file mode 100644
index 000000000..2cd17850a
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/i18n/locales/de.json
@@ -0,0 +1,135 @@
+{
+ "app": {
+ "name": "Taktik",
+ "loading": "Laden...",
+ "tagline": "Dein Arbeitsrhythmus, messbar gemacht."
+ },
+ "nav": {
+ "timer": "Timer",
+ "entries": "Einträge",
+ "projects": "Projekte",
+ "clients": "Kunden",
+ "reports": "Reports",
+ "settings": "Einstellungen",
+ "templates": "Vorlagen"
+ },
+ "timer": {
+ "start": "Timer starten",
+ "stop": "Timer stoppen",
+ "resume": "Fortsetzen",
+ "running": "Läuft",
+ "noDescription": "Keine Beschreibung",
+ "whatAreYouWorkingOn": "Woran arbeitest du?"
+ },
+ "entry": {
+ "create": "Eintrag erstellen",
+ "edit": "Eintrag bearbeiten",
+ "delete": "Eintrag löschen",
+ "description": "Beschreibung",
+ "date": "Datum",
+ "startTime": "Startzeit",
+ "endTime": "Endzeit",
+ "duration": "Dauer",
+ "billable": "Abrechenbar",
+ "notBillable": "Nicht abrechenbar",
+ "noEntries": "Keine Einträge",
+ "today": "Heute",
+ "thisWeek": "Diese Woche",
+ "thisMonth": "Dieser Monat",
+ "manual": "Manuell erfassen"
+ },
+ "project": {
+ "create": "Projekt erstellen",
+ "edit": "Projekt bearbeiten",
+ "delete": "Projekt löschen",
+ "name": "Name",
+ "description": "Beschreibung",
+ "client": "Kunde",
+ "billable": "Abrechenbar",
+ "budget": "Budget",
+ "noProjects": "Keine Projekte",
+ "archived": "Archiviert",
+ "internal": "Intern"
+ },
+ "client": {
+ "create": "Kunde erstellen",
+ "edit": "Kunde bearbeiten",
+ "delete": "Kunde löschen",
+ "name": "Name",
+ "shortCode": "Kürzel",
+ "email": "E-Mail",
+ "billingRate": "Stundensatz",
+ "noClients": "Keine Kunden"
+ },
+ "report": {
+ "title": "Reports",
+ "totalHours": "Gesamtstunden",
+ "billableHours": "Abrechenbare Stunden",
+ "avgPerDay": "Durchschnitt/Tag",
+ "topProject": "Top-Projekt",
+ "byProject": "Nach Projekt",
+ "byClient": "Nach Kunde",
+ "byDay": "Nach Tag",
+ "export": "Exportieren",
+ "dateRange": "Zeitraum"
+ },
+ "template": {
+ "create": "Vorlage erstellen",
+ "edit": "Vorlage bearbeiten",
+ "delete": "Vorlage löschen",
+ "noTemplates": "Keine Vorlagen",
+ "useTemplate": "Vorlage verwenden"
+ },
+ "settings": {
+ "title": "Einstellungen",
+ "workingHours": "Arbeitsstunden/Tag",
+ "workingDays": "Arbeitstage/Woche",
+ "rounding": "Rundung",
+ "roundingMethod": "Rundungsmethode",
+ "none": "Keine",
+ "up": "Aufrunden",
+ "down": "Abrunden",
+ "nearest": "Nächster Wert",
+ "currency": "Währung",
+ "billingRate": "Standard-Stundensatz",
+ "weekStart": "Woche beginnt am",
+ "monday": "Montag",
+ "sunday": "Sonntag",
+ "timerReminder": "Timer-Erinnerung (Min.)",
+ "autoStop": "Auto-Stop (Std.)"
+ },
+ "auth": {
+ "login": "Anmelden",
+ "logout": "Abmelden",
+ "register": "Registrieren",
+ "email": "E-Mail",
+ "password": "Passwort",
+ "forgotPassword": "Passwort vergessen?"
+ },
+ "common": {
+ "save": "Speichern",
+ "cancel": "Abbrechen",
+ "delete": "Löschen",
+ "edit": "Bearbeiten",
+ "add": "Hinzufügen",
+ "close": "Schließen",
+ "search": "Suchen",
+ "error": "Fehler",
+ "success": "Erfolgreich",
+ "loading": "Laden...",
+ "noResults": "Keine Ergebnisse",
+ "confirm": "Bestätigen",
+ "back": "Zurück",
+ "next": "Weiter",
+ "create": "Erstellen",
+ "archive": "Archivieren",
+ "unarchive": "Wiederherstellen",
+ "total": "Gesamt",
+ "hours": "Stunden",
+ "minutes": "Minuten"
+ },
+ "error": {
+ "notFound": "Seite nicht gefunden",
+ "backToHome": "Zurück zur Startseite"
+ }
+}
diff --git a/apps/taktik/apps/web/src/lib/i18n/locales/en.json b/apps/taktik/apps/web/src/lib/i18n/locales/en.json
new file mode 100644
index 000000000..58f7847cc
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/i18n/locales/en.json
@@ -0,0 +1,135 @@
+{
+ "app": {
+ "name": "Taktik",
+ "loading": "Loading...",
+ "tagline": "Your work rhythm, made measurable."
+ },
+ "nav": {
+ "timer": "Timer",
+ "entries": "Entries",
+ "projects": "Projects",
+ "clients": "Clients",
+ "reports": "Reports",
+ "settings": "Settings",
+ "templates": "Templates"
+ },
+ "timer": {
+ "start": "Start Timer",
+ "stop": "Stop Timer",
+ "resume": "Resume",
+ "running": "Running",
+ "noDescription": "No description",
+ "whatAreYouWorkingOn": "What are you working on?"
+ },
+ "entry": {
+ "create": "Create Entry",
+ "edit": "Edit Entry",
+ "delete": "Delete Entry",
+ "description": "Description",
+ "date": "Date",
+ "startTime": "Start Time",
+ "endTime": "End Time",
+ "duration": "Duration",
+ "billable": "Billable",
+ "notBillable": "Not Billable",
+ "noEntries": "No entries",
+ "today": "Today",
+ "thisWeek": "This Week",
+ "thisMonth": "This Month",
+ "manual": "Manual Entry"
+ },
+ "project": {
+ "create": "Create Project",
+ "edit": "Edit Project",
+ "delete": "Delete Project",
+ "name": "Name",
+ "description": "Description",
+ "client": "Client",
+ "billable": "Billable",
+ "budget": "Budget",
+ "noProjects": "No projects",
+ "archived": "Archived",
+ "internal": "Internal"
+ },
+ "client": {
+ "create": "Create Client",
+ "edit": "Edit Client",
+ "delete": "Delete Client",
+ "name": "Name",
+ "shortCode": "Short Code",
+ "email": "Email",
+ "billingRate": "Billing Rate",
+ "noClients": "No clients"
+ },
+ "report": {
+ "title": "Reports",
+ "totalHours": "Total Hours",
+ "billableHours": "Billable Hours",
+ "avgPerDay": "Avg/Day",
+ "topProject": "Top Project",
+ "byProject": "By Project",
+ "byClient": "By Client",
+ "byDay": "By Day",
+ "export": "Export",
+ "dateRange": "Date Range"
+ },
+ "template": {
+ "create": "Create Template",
+ "edit": "Edit Template",
+ "delete": "Delete Template",
+ "noTemplates": "No templates",
+ "useTemplate": "Use Template"
+ },
+ "settings": {
+ "title": "Settings",
+ "workingHours": "Working Hours/Day",
+ "workingDays": "Working Days/Week",
+ "rounding": "Rounding",
+ "roundingMethod": "Rounding Method",
+ "none": "None",
+ "up": "Round Up",
+ "down": "Round Down",
+ "nearest": "Nearest",
+ "currency": "Currency",
+ "billingRate": "Default Billing Rate",
+ "weekStart": "Week Starts On",
+ "monday": "Monday",
+ "sunday": "Sunday",
+ "timerReminder": "Timer Reminder (min)",
+ "autoStop": "Auto-Stop (hours)"
+ },
+ "auth": {
+ "login": "Login",
+ "logout": "Logout",
+ "register": "Register",
+ "email": "Email",
+ "password": "Password",
+ "forgotPassword": "Forgot password?"
+ },
+ "common": {
+ "save": "Save",
+ "cancel": "Cancel",
+ "delete": "Delete",
+ "edit": "Edit",
+ "add": "Add",
+ "close": "Close",
+ "search": "Search",
+ "error": "Error",
+ "success": "Success",
+ "loading": "Loading...",
+ "noResults": "No results",
+ "confirm": "Confirm",
+ "back": "Back",
+ "next": "Next",
+ "create": "Create",
+ "archive": "Archive",
+ "unarchive": "Restore",
+ "total": "Total",
+ "hours": "Hours",
+ "minutes": "Minutes"
+ },
+ "error": {
+ "notFound": "Page not found",
+ "backToHome": "Back to home"
+ }
+}
diff --git a/apps/taktik/apps/web/src/lib/stores/auth.svelte.ts b/apps/taktik/apps/web/src/lib/stores/auth.svelte.ts
new file mode 100644
index 000000000..5df932791
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/stores/auth.svelte.ts
@@ -0,0 +1,7 @@
+/**
+ * Auth Store — uses centralized Mana auth factory.
+ */
+
+import { createManaAuthStore } from '@manacore/shared-auth-stores';
+
+export const authStore = createManaAuthStore();
diff --git a/apps/taktik/apps/web/src/lib/stores/navigation.ts b/apps/taktik/apps/web/src/lib/stores/navigation.ts
new file mode 100644
index 000000000..a0dd7b724
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/stores/navigation.ts
@@ -0,0 +1,6 @@
+import { createSimpleNavigationStores } from '@manacore/shared-stores';
+
+export const { isNavCollapsed, isToolbarCollapsed } = createSimpleNavigationStores({
+ withToolbar: true,
+ toolbarCollapsedDefault: true,
+});
diff --git a/apps/taktik/apps/web/src/lib/stores/theme.ts b/apps/taktik/apps/web/src/lib/stores/theme.ts
new file mode 100644
index 000000000..d5ded30af
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/stores/theme.ts
@@ -0,0 +1,6 @@
+import { createThemeStore } from '@manacore/shared-theme';
+
+export const theme = createThemeStore({
+ appId: 'taktik',
+ defaultVariant: 'ocean',
+});
diff --git a/apps/taktik/apps/web/src/lib/stores/timer.svelte.ts b/apps/taktik/apps/web/src/lib/stores/timer.svelte.ts
new file mode 100644
index 000000000..140db31d8
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/stores/timer.svelte.ts
@@ -0,0 +1,164 @@
+/**
+ * Timer Store — manages the active time tracking timer.
+ *
+ * The timer state persists in IndexedDB via the timeEntries collection.
+ * When a timer is running, there's a timeEntry with isRunning=true.
+ * This store provides reactive access to the running entry and elapsed time.
+ */
+
+import { browser } from '$app/environment';
+import { timeEntryCollection, type LocalTimeEntry } from '$lib/data/local-store';
+
+let runningEntry = $state(null);
+let elapsedSeconds = $state(0);
+let tickInterval: ReturnType | null = null;
+let autoSaveInterval: ReturnType | null = null;
+
+function startTicking() {
+ stopTicking();
+ tickInterval = setInterval(() => {
+ if (runningEntry?.startTime) {
+ elapsedSeconds = Math.floor((Date.now() - new Date(runningEntry.startTime).getTime()) / 1000);
+ }
+ }, 1000);
+}
+
+function stopTicking() {
+ if (tickInterval) {
+ clearInterval(tickInterval);
+ tickInterval = null;
+ }
+ if (autoSaveInterval) {
+ clearInterval(autoSaveInterval);
+ autoSaveInterval = null;
+ }
+}
+
+function startAutoSave() {
+ if (autoSaveInterval) clearInterval(autoSaveInterval);
+ autoSaveInterval = setInterval(async () => {
+ if (runningEntry) {
+ await timeEntryCollection.update(runningEntry.id, {
+ duration: elapsedSeconds,
+ });
+ }
+ }, 10000); // Auto-save every 10 seconds
+}
+
+export const timerStore = {
+ get runningEntry() {
+ return runningEntry;
+ },
+ get elapsedSeconds() {
+ return elapsedSeconds;
+ },
+ get isRunning() {
+ return runningEntry !== null;
+ },
+
+ /** Initialize: check for any running entry in IndexedDB */
+ async initialize() {
+ if (!browser) return;
+ const entries = await timeEntryCollection.getAll();
+ const running = entries.find((e) => e.isRunning);
+ if (running) {
+ runningEntry = running;
+ if (running.startTime) {
+ elapsedSeconds = Math.floor((Date.now() - new Date(running.startTime).getTime()) / 1000);
+ }
+ startTicking();
+ startAutoSave();
+ }
+ },
+
+ /** Start a new timer */
+ async start(options?: {
+ projectId?: string;
+ clientId?: string;
+ description?: string;
+ isBillable?: boolean;
+ tags?: string[];
+ }) {
+ // Stop any existing timer first
+ if (runningEntry) {
+ await timerStore.stop();
+ }
+
+ const now = new Date();
+ const entry: LocalTimeEntry = {
+ id: crypto.randomUUID(),
+ projectId: options?.projectId ?? null,
+ clientId: options?.clientId ?? null,
+ description: options?.description ?? '',
+ date: now.toISOString().split('T')[0],
+ startTime: now.toISOString(),
+ endTime: null,
+ duration: 0,
+ isBillable: options?.isBillable ?? false,
+ isRunning: true,
+ tags: options?.tags ?? [],
+ billingRate: null,
+ visibility: 'private',
+ guildId: null,
+ source: { app: 'timer' },
+ };
+
+ await timeEntryCollection.insert(entry);
+ runningEntry = entry;
+ elapsedSeconds = 0;
+ startTicking();
+ startAutoSave();
+ },
+
+ /** Stop the running timer */
+ async stop(): Promise {
+ if (!runningEntry) return null;
+
+ const now = new Date();
+ const finalDuration = runningEntry.startTime
+ ? Math.floor((now.getTime() - new Date(runningEntry.startTime).getTime()) / 1000)
+ : elapsedSeconds;
+
+ await timeEntryCollection.update(runningEntry.id, {
+ isRunning: false,
+ endTime: now.toISOString(),
+ duration: finalDuration,
+ });
+
+ const stoppedEntry = {
+ ...runningEntry,
+ isRunning: false,
+ endTime: now.toISOString(),
+ duration: finalDuration,
+ };
+ stopTicking();
+ runningEntry = null;
+ elapsedSeconds = 0;
+ return stoppedEntry as LocalTimeEntry;
+ },
+
+ /** Discard the running timer without saving */
+ async discard() {
+ if (!runningEntry) return;
+ await timeEntryCollection.delete(runningEntry.id);
+ stopTicking();
+ runningEntry = null;
+ elapsedSeconds = 0;
+ },
+
+ /** Update the running entry's metadata (project, description, etc.) */
+ async updateRunning(
+ updates: Partial<
+ Pick
+ >
+ ) {
+ if (!runningEntry) return;
+ await timeEntryCollection.update(runningEntry.id, updates);
+ runningEntry = { ...runningEntry, ...updates };
+ },
+
+ /** Cleanup on unmount */
+ destroy() {
+ stopTicking();
+ },
+};
diff --git a/apps/taktik/apps/web/src/lib/stores/user-settings.svelte.ts b/apps/taktik/apps/web/src/lib/stores/user-settings.svelte.ts
new file mode 100644
index 000000000..73ffe777f
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/stores/user-settings.svelte.ts
@@ -0,0 +1,18 @@
+import { browser } from '$app/environment';
+import { createUserSettingsStore } from '@manacore/shared-theme';
+import { authStore } from './auth.svelte';
+
+function getAuthUrl(): string {
+ if (browser && typeof window !== 'undefined') {
+ const injectedUrl = (window as unknown as { __PUBLIC_MANA_CORE_AUTH_URL__?: string })
+ .__PUBLIC_MANA_CORE_AUTH_URL__;
+ if (injectedUrl) return injectedUrl;
+ }
+ return import.meta.env.DEV ? 'http://localhost:3001' : '';
+}
+
+export const userSettings = createUserSettingsStore({
+ appId: 'taktik',
+ authUrl: getAuthUrl,
+ getAccessToken: () => authStore.getAccessToken(),
+});
diff --git a/apps/taktik/apps/web/src/lib/stores/view.svelte.ts b/apps/taktik/apps/web/src/lib/stores/view.svelte.ts
new file mode 100644
index 000000000..d0b94a1a9
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/stores/view.svelte.ts
@@ -0,0 +1,106 @@
+import { browser } from '$app/environment';
+import type { ViewMode, SortOption, FilterCriteria, SavedFilter } from '@taktik/shared';
+
+const VIEW_KEY = 'taktik_view_mode';
+const SORT_KEY = 'taktik_sort';
+const FILTERS_KEY = 'taktik_saved_filters';
+
+function load(key: string, fallback: T): T {
+ if (!browser) return fallback;
+ try {
+ const data = localStorage.getItem(key);
+ return data ? JSON.parse(data) : fallback;
+ } catch {
+ return fallback;
+ }
+}
+
+function save(key: string, value: unknown) {
+ if (!browser) return;
+ localStorage.setItem(key, JSON.stringify(value));
+}
+
+let viewMode = $state('week');
+let sort = $state({ field: 'date', direction: 'desc' });
+let activeFilters = $state({});
+let savedFilters = $state([]);
+let initialized = $state(false);
+
+export const viewStore = {
+ get viewMode() {
+ return viewMode;
+ },
+ get sort() {
+ return sort;
+ },
+ get activeFilters() {
+ return activeFilters;
+ },
+ get savedFilters() {
+ return savedFilters;
+ },
+ get hasActiveFilters() {
+ return !!(
+ activeFilters.search ||
+ activeFilters.projectId ||
+ activeFilters.clientId ||
+ activeFilters.tagIds?.length ||
+ activeFilters.isBillable !== undefined ||
+ activeFilters.dateFrom ||
+ activeFilters.dateTo
+ );
+ },
+
+ initialize() {
+ if (initialized) return;
+ viewMode = load(VIEW_KEY, 'week');
+ sort = load(SORT_KEY, { field: 'date', direction: 'desc' });
+ savedFilters = load(FILTERS_KEY, []);
+ initialized = true;
+ },
+
+ setViewMode(mode: ViewMode) {
+ viewMode = mode;
+ save(VIEW_KEY, mode);
+ },
+
+ setSort(newSort: SortOption) {
+ sort = newSort;
+ save(SORT_KEY, newSort);
+ },
+
+ setFilters(filters: FilterCriteria) {
+ activeFilters = filters;
+ },
+
+ updateFilter(key: K, value: FilterCriteria[K]) {
+ activeFilters = { ...activeFilters, [key]: value };
+ },
+
+ clearFilters() {
+ activeFilters = {};
+ },
+
+ saveFilter(name: string) {
+ const filter: SavedFilter = {
+ id: crypto.randomUUID(),
+ name,
+ criteria: { ...activeFilters },
+ createdAt: new Date().toISOString(),
+ };
+ savedFilters = [...savedFilters, filter];
+ save(FILTERS_KEY, savedFilters);
+ },
+
+ loadFilter(id: string) {
+ const filter = savedFilters.find((f) => f.id === id);
+ if (filter) {
+ activeFilters = { ...filter.criteria };
+ }
+ },
+
+ deleteSavedFilter(id: string) {
+ savedFilters = savedFilters.filter((f) => f.id !== id);
+ save(FILTERS_KEY, savedFilters);
+ },
+};
diff --git a/apps/taktik/apps/web/src/lib/version.ts b/apps/taktik/apps/web/src/lib/version.ts
new file mode 100644
index 000000000..93d9ace5f
--- /dev/null
+++ b/apps/taktik/apps/web/src/lib/version.ts
@@ -0,0 +1,7 @@
+declare const __BUILD_TIME__: string;
+declare const __BUILD_HASH__: string;
+
+export const APP_VERSION = '1.0.0';
+export const BUILD_TIME: string =
+ typeof __BUILD_TIME__ !== 'undefined' ? __BUILD_TIME__ : new Date().toISOString();
+export const BUILD_HASH: string = typeof __BUILD_HASH__ !== 'undefined' ? __BUILD_HASH__ : 'dev';
diff --git a/apps/taktik/apps/web/src/routes/(app)/+layout.svelte b/apps/taktik/apps/web/src/routes/(app)/+layout.svelte
new file mode 100644
index 000000000..c90473a78
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/(app)/+layout.svelte
@@ -0,0 +1,203 @@
+
+
+
+
+
+ {#if showNav}
+
+ {/if}
+
+
+
+ {@render children()}
+
+
+
+
+
+
+ (showGuestWelcome = false)}
+ onLogin={() => goto('/login')}
+ onRegister={() => goto('/register')}
+ locale="de"
+ />
+
+
diff --git a/apps/taktik/apps/web/src/routes/(app)/+page.svelte b/apps/taktik/apps/web/src/routes/(app)/+page.svelte
new file mode 100644
index 000000000..5bdafdeef
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/(app)/+page.svelte
@@ -0,0 +1,84 @@
+
+
+
+ Timer | Taktik
+
+
+
+
+
+
+
+
+
+
{$_('common.total')}
+
+ {formatDurationCompact(todayTotal)}
+
+
+
+
{$_('entry.billable')}
+
+ {formatDurationCompact(todayBillable)}
+
+
+
+
+
+
+
+
+
+
+
+ {$_('entry.today')} ({formatDurationCompact(todayTotal)})
+
+
+
+
+
+
+
+
+
+ (showEntryForm = false)} />
diff --git a/apps/taktik/apps/web/src/routes/(app)/clients/+page.svelte b/apps/taktik/apps/web/src/routes/(app)/clients/+page.svelte
new file mode 100644
index 000000000..8c629edbe
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/(app)/clients/+page.svelte
@@ -0,0 +1,347 @@
+
+
+
+ {$_('nav.clients')} | Taktik
+
+
+
+
+
{$_('nav.clients')}
+
+
+
+ {#if showCreateForm}
+
+ {/if}
+
+ {#if activeClients.length === 0 && !showCreateForm}
+
+
{$_('client.noClients')}
+
+ {:else}
+
+ {#each activeClients as client (client.id)}
+ {@const projects = getClientProjects(client.id)}
+ {@const hours = getClientHours(client.id)}
+
+ {#if editingClientId === client.id}
+
+
+ {
+ editName = (e.target as HTMLInputElement).value;
+ autoSave({ name: editName });
+ }}
+ class="flex-1 rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--input))] px-3 py-2 text-sm"
+ />
+ {
+ editShortCode = (e.target as HTMLInputElement).value;
+ autoSave({ shortCode: editShortCode || null });
+ }}
+ placeholder={$_('client.shortCode')}
+ class="w-24 rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--input))] px-3 py-2 text-sm"
+ />
+
+
+
+ {#each PROJECT_COLORS as color}
+
+ {/each}
+
+
+
+
+
+
+
+ {:else}
+
+ {/if}
+
+ {/each}
+
+ {/if}
+
+ {#if archivedClients.length > 0}
+
+
+ {#if showArchived}
+
+ {#each archivedClients as client}
+
+
+
+ {client.shortCode || client.name.charAt(0)}
+
+
{client.name}
+
+
+
+ {/each}
+
+ {/if}
+
+ {/if}
+
diff --git a/apps/taktik/apps/web/src/routes/(app)/entries/+page.svelte b/apps/taktik/apps/web/src/routes/(app)/entries/+page.svelte
new file mode 100644
index 000000000..4d2fccb47
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/(app)/entries/+page.svelte
@@ -0,0 +1,113 @@
+
+
+
+ {$_('nav.entries')} | Taktik
+
+
+
+
+
+
{$_('nav.entries')}
+
+
+
+
+
+ {#each ['week', 'month', 'all'] as period}
+
+ {/each}
+
+
+
+
+ {$_('common.total')}:
+ {formatDurationCompact(totalDuration)}
+
+
+ {$_('entry.billable')}:
+ {formatDurationCompact(billableDuration)}
+
+
+
+
+
+
+
+
+
+ (showEntryForm = false)} />
diff --git a/apps/taktik/apps/web/src/routes/(app)/feedback/+page.svelte b/apps/taktik/apps/web/src/routes/(app)/feedback/+page.svelte
new file mode 100644
index 000000000..c71b318f0
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/(app)/feedback/+page.svelte
@@ -0,0 +1,12 @@
+
+
+
+ Feedback | Taktik
+
+
+
+
Feedback
+
Feedback-Formular kommt bald.
+
diff --git a/apps/taktik/apps/web/src/routes/(app)/help/+page.svelte b/apps/taktik/apps/web/src/routes/(app)/help/+page.svelte
new file mode 100644
index 000000000..14e51499b
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/(app)/help/+page.svelte
@@ -0,0 +1,12 @@
+
+
+
+ Hilfe | Taktik
+
+
+
+
Hilfe
+
Hilfe & Dokumentation.
+
diff --git a/apps/taktik/apps/web/src/routes/(app)/mana/+page.svelte b/apps/taktik/apps/web/src/routes/(app)/mana/+page.svelte
new file mode 100644
index 000000000..8e964a054
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/(app)/mana/+page.svelte
@@ -0,0 +1,12 @@
+
+
+
+ Mana | Taktik
+
+
+
+
Mana
+
Mana Credits & Abo-Verwaltung.
+
diff --git a/apps/taktik/apps/web/src/routes/(app)/profile/+page.svelte b/apps/taktik/apps/web/src/routes/(app)/profile/+page.svelte
new file mode 100644
index 000000000..c3a07ad34
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/(app)/profile/+page.svelte
@@ -0,0 +1,12 @@
+
+
+
+ Profil | Taktik
+
+
+
+
Profil
+
Profil-Einstellungen.
+
diff --git a/apps/taktik/apps/web/src/routes/(app)/projects/+page.svelte b/apps/taktik/apps/web/src/routes/(app)/projects/+page.svelte
new file mode 100644
index 000000000..0655c662e
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/(app)/projects/+page.svelte
@@ -0,0 +1,356 @@
+
+
+
+ {$_('nav.projects')} | Taktik
+
+
+
+
+
+
{$_('nav.projects')}
+
+
+
+
+ {#if showCreateForm}
+
+ {/if}
+
+
+ {#if activeProjects.length === 0 && !showCreateForm}
+
+
{$_('project.noProjects')}
+
+ {:else}
+
+ {#each activeProjects as project (project.id)}
+ {@const client = project.clientId
+ ? allClients.value.find((c) => c.id === project.clientId)
+ : undefined}
+ {@const hours = getProjectHours(project.id)}
+ {@const budgetPct = getBudgetPercent(project)}
+
+
+
+
+ {#if editingProjectId === project.id}
+
+
+
{
+ editName = (e.target as HTMLInputElement).value;
+ autoSaveProject({ name: editName });
+ }}
+ class="w-full rounded-lg border border-[hsl(var(--border))] bg-[hsl(var(--input))] px-3 py-2 text-sm text-[hsl(var(--foreground))] focus:outline-none"
+ />
+
+
+ {#each PROJECT_COLORS as color}
+
+ {/each}
+
+
+
+
+
+
+
+
+
+
+ {:else}
+
+
+ {/if}
+
+ {/each}
+
+ {/if}
+
+
+ {#if archivedProjects.length > 0}
+
+
+
+ {#if showArchived}
+
+ {#each archivedProjects as project}
+
+
+
+
+ {/each}
+
+ {/if}
+
+ {/if}
+
diff --git a/apps/taktik/apps/web/src/routes/(app)/reports/+page.svelte b/apps/taktik/apps/web/src/routes/(app)/reports/+page.svelte
new file mode 100644
index 000000000..3a875e77f
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/(app)/reports/+page.svelte
@@ -0,0 +1,206 @@
+
+
+
+ {$_('nav.reports')} | Taktik
+
+
+
+
+
+
{$_('nav.reports')}
+
+ {#each ['week', 'month'] as p}
+
+ {/each}
+
+
+
+
+
+
+
{$_('report.totalHours')}
+
+ {formatDurationDecimal(totalDuration)}h
+
+
+
+
{$_('report.billableHours')}
+
+ {formatDurationDecimal(billableDuration)}h
+
+
+
+
{$_('report.avgPerDay')}
+
+ {formatDurationCompact(Math.round(avgPerDay))}
+
+
+
+
{$_('nav.entries')}
+
{entryCount}
+
+
+
+
+ {#if totalDuration > 0}
+
+
+ {$_('entry.billable')} vs. {$_('entry.notBillable')}
+
+
+
+ {$_('entry.billable')}: {formatDurationCompact(billableDuration)}
+ {$_('entry.notBillable')}: {formatDurationCompact(nonBillableDuration)}
+
+
+ {/if}
+
+
+ {#if projectBreakdown().length > 0}
+
+
+ {$_('report.byProject')}
+
+
+ {#each projectBreakdown() as item}
+
+
+
+
+ {formatDurationCompact(item.duration)}
+
+
+
+
+ {/each}
+
+
+ {/if}
+
+
+ {#if period === 'week' && dailyBreakdown().length > 0}
+
+
{$_('report.byDay')}
+
+ {#each dailyBreakdown() as day}
+
+ {/each}
+
+
+ {/if}
+
diff --git a/apps/taktik/apps/web/src/routes/(app)/settings/+page.svelte b/apps/taktik/apps/web/src/routes/(app)/settings/+page.svelte
new file mode 100644
index 000000000..90bf2e12c
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/(app)/settings/+page.svelte
@@ -0,0 +1,12 @@
+
+
+
+ {$_('nav.settings')} | Taktik
+
+
+
+
{$_('nav.settings')}
+
Einstellungen kommen bald.
+
diff --git a/apps/taktik/apps/web/src/routes/(app)/themes/+page.svelte b/apps/taktik/apps/web/src/routes/(app)/themes/+page.svelte
new file mode 100644
index 000000000..8a8878ce4
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/(app)/themes/+page.svelte
@@ -0,0 +1,12 @@
+
+
+
+ Themes | Taktik
+
+
+
+
Themes
+
Theme-Auswahl.
+
diff --git a/apps/taktik/apps/web/src/routes/(auth)/+layout.svelte b/apps/taktik/apps/web/src/routes/(auth)/+layout.svelte
new file mode 100644
index 000000000..a54cfdcb7
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/(auth)/+layout.svelte
@@ -0,0 +1,5 @@
+
+
+{@render children()}
diff --git a/apps/taktik/apps/web/src/routes/(auth)/login/+page.svelte b/apps/taktik/apps/web/src/routes/(auth)/login/+page.svelte
new file mode 100644
index 000000000..74438d766
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/(auth)/login/+page.svelte
@@ -0,0 +1,270 @@
+
+
+
+ {showRegister ? $_('auth.register') : $_('auth.login')} | Taktik
+
+
+
+
+
+
+
+
Taktik
+
Zeiterfassung
+
+
+ {#if verificationSent}
+
+
+ {showForgotPassword ? 'Link zum Zurücksetzen gesendet!' : 'Bestätigungsmail gesendet!'}
+
+
Bitte prüfe dein Postfach.
+
+
+ {:else if show2FA}
+
+
+ Zwei-Faktor-Authentifizierung
+
+
e.key === 'Enter' && handle2FA()}
+ />
+ {#if error}
{error}
{/if}
+
+
+ {:else}
+
+
+ {showForgotPassword
+ ? 'Passwort zurücksetzen'
+ : showRegister
+ ? $_('auth.register')
+ : $_('auth.login')}
+
+
+
+
+ {#if !showForgotPassword}
+
+ e.key === 'Enter' && (showRegister ? handleRegister() : handleLogin())}
+ />
+ {/if}
+
+
+ {#if error}
{error}
{/if}
+
+
+
+ {#if !showForgotPassword && authStore.isPasskeyAvailable()}
+
+ {/if}
+
+
+ {#if showForgotPassword}
+
+ {:else}
+
+
+ {/if}
+
+
+ {/if}
+
+
+
+ {#each getPillAppItems() as app}
+ {#if app.id !== 'taktik'}
+
+ {app.name}
+
+ {/if}
+ {/each}
+
+
+
diff --git a/apps/taktik/apps/web/src/routes/+error.svelte b/apps/taktik/apps/web/src/routes/+error.svelte
new file mode 100644
index 000000000..6ef4029e4
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/+error.svelte
@@ -0,0 +1,19 @@
+
+
+
diff --git a/apps/taktik/apps/web/src/routes/+layout.svelte b/apps/taktik/apps/web/src/routes/+layout.svelte
new file mode 100644
index 000000000..69d701b16
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/+layout.svelte
@@ -0,0 +1,35 @@
+
+
+{#if ready}
+
+ {@render children()}
+
+{:else}
+
+{/if}
diff --git a/apps/taktik/apps/web/src/routes/+layout.ts b/apps/taktik/apps/web/src/routes/+layout.ts
new file mode 100644
index 000000000..ad6cddb06
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/+layout.ts
@@ -0,0 +1,2 @@
+// Disable SSR — all data is local-first (IndexedDB + mana-sync)
+export const ssr = false;
diff --git a/apps/taktik/apps/web/src/routes/health/+server.ts b/apps/taktik/apps/web/src/routes/health/+server.ts
new file mode 100644
index 000000000..b558386f2
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/health/+server.ts
@@ -0,0 +1,10 @@
+import { json } from '@sveltejs/kit';
+import type { RequestHandler } from './$types';
+
+export const GET: RequestHandler = async () => {
+ return json({
+ status: 'ok',
+ timestamp: new Date().toISOString(),
+ service: 'taktik-web',
+ });
+};
diff --git a/apps/taktik/apps/web/src/routes/offline/+page.svelte b/apps/taktik/apps/web/src/routes/offline/+page.svelte
new file mode 100644
index 000000000..4c1ddb1d9
--- /dev/null
+++ b/apps/taktik/apps/web/src/routes/offline/+page.svelte
@@ -0,0 +1,39 @@
+
+
+
+ Offline | Taktik
+
+
+
+
+
+
Offline
+
+ Du bist gerade nicht mit dem Internet verbunden.
+
+
+
+
diff --git a/apps/taktik/apps/web/svelte.config.js b/apps/taktik/apps/web/svelte.config.js
new file mode 100644
index 000000000..a7a917e4c
--- /dev/null
+++ b/apps/taktik/apps/web/svelte.config.js
@@ -0,0 +1,14 @@
+import adapter from '@sveltejs/adapter-node';
+import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
+
+/** @type {import('@sveltejs/kit').Config} */
+const config = {
+ preprocess: vitePreprocess(),
+ kit: {
+ adapter: adapter({
+ out: 'build',
+ }),
+ },
+};
+
+export default config;
diff --git a/apps/taktik/apps/web/tsconfig.json b/apps/taktik/apps/web/tsconfig.json
new file mode 100644
index 000000000..a8f10c8e3
--- /dev/null
+++ b/apps/taktik/apps/web/tsconfig.json
@@ -0,0 +1,14 @@
+{
+ "extends": "./.svelte-kit/tsconfig.json",
+ "compilerOptions": {
+ "allowJs": true,
+ "checkJs": true,
+ "esModuleInterop": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true,
+ "skipLibCheck": true,
+ "sourceMap": true,
+ "strict": true,
+ "moduleResolution": "bundler"
+ }
+}
diff --git a/apps/taktik/apps/web/vite.config.ts b/apps/taktik/apps/web/vite.config.ts
new file mode 100644
index 000000000..a0d688e48
--- /dev/null
+++ b/apps/taktik/apps/web/vite.config.ts
@@ -0,0 +1,64 @@
+import tailwindcss from '@tailwindcss/vite';
+import { sveltekit } from '@sveltejs/kit/vite';
+import { SvelteKitPWA } from '@vite-pwa/sveltekit';
+import { defineConfig } from 'vitest/config';
+import { getBuildDefines } from '@manacore/shared-vite-config';
+
+export default defineConfig({
+ plugins: [
+ tailwindcss(),
+ sveltekit(),
+ SvelteKitPWA({
+ registerType: 'autoUpdate',
+ manifest: {
+ name: 'Taktik',
+ short_name: 'Taktik',
+ description: 'Zeiterfassung & Timetracking',
+ theme_color: '#f59e0b',
+ background_color: '#0f172a',
+ display: 'standalone',
+ icons: [
+ {
+ src: 'pwa-192x192.png',
+ sizes: '192x192',
+ type: 'image/png',
+ },
+ {
+ src: 'pwa-512x512.png',
+ sizes: '512x512',
+ type: 'image/png',
+ },
+ ],
+ shortcuts: [
+ {
+ name: 'Timer starten',
+ short_name: 'Timer',
+ url: '/?action=start',
+ icons: [{ src: 'icons/icon.svg', sizes: '96x96' }],
+ },
+ {
+ name: 'Neuer Eintrag',
+ short_name: 'Eintrag',
+ url: '/entries?action=new',
+ icons: [{ src: 'icons/icon.svg', sizes: '96x96' }],
+ },
+ ],
+ },
+ workbox: {
+ globPatterns: ['client/**/*.{js,css,ico,png,svg,webp,woff,woff2}'],
+ },
+ }),
+ ],
+ server: {
+ port: 5197,
+ strictPort: true,
+ },
+ preview: {
+ port: 5197,
+ },
+ define: getBuildDefines(),
+ test: {
+ include: ['src/**/*.{test,spec}.{js,ts}'],
+ environment: 'jsdom',
+ },
+});
diff --git a/apps/taktik/package.json b/apps/taktik/package.json
new file mode 100644
index 000000000..279d3617b
--- /dev/null
+++ b/apps/taktik/package.json
@@ -0,0 +1,14 @@
+{
+ "name": "taktik",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Taktik - Zeiterfassung & Timetracking",
+ "scripts": {
+ "dev": "pnpm --filter @taktik/web dev",
+ "dev:web": "pnpm --filter @taktik/web dev"
+ },
+ "devDependencies": {
+ "typescript": "^5.9.3"
+ },
+ "packageManager": "pnpm@9.15.0"
+}
diff --git a/apps/taktik/packages/shared/package.json b/apps/taktik/packages/shared/package.json
new file mode 100644
index 000000000..c7cf82295
--- /dev/null
+++ b/apps/taktik/packages/shared/package.json
@@ -0,0 +1,22 @@
+{
+ "name": "@taktik/shared",
+ "version": "1.0.0",
+ "private": true,
+ "type": "module",
+ "main": "./src/index.ts",
+ "types": "./src/index.ts",
+ "exports": {
+ ".": "./src/index.ts",
+ "./types": "./src/types/index.ts",
+ "./constants": "./src/constants/index.ts"
+ },
+ "scripts": {
+ "type-check": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@manacore/shared-types": "workspace:*"
+ },
+ "devDependencies": {
+ "typescript": "^5.9.3"
+ }
+}
diff --git a/apps/taktik/packages/shared/src/constants/index.ts b/apps/taktik/packages/shared/src/constants/index.ts
new file mode 100644
index 000000000..62052c39c
--- /dev/null
+++ b/apps/taktik/packages/shared/src/constants/index.ts
@@ -0,0 +1,40 @@
+export const CURRENCIES = [
+ { code: 'EUR', symbol: '€', name: 'Euro' },
+ { code: 'CHF', symbol: 'CHF', name: 'Schweizer Franken' },
+ { code: 'USD', symbol: '$', name: 'US Dollar' },
+ { code: 'GBP', symbol: '£', name: 'Britisches Pfund' },
+] as const;
+
+export const DEFAULT_CURRENCY = 'EUR';
+
+export const ROUNDING_INCREMENTS = [0, 1, 5, 6, 10, 15] as const;
+
+export const PROJECT_COLORS: string[] = [
+ '#ef4444',
+ '#f97316',
+ '#f59e0b',
+ '#eab308',
+ '#84cc16',
+ '#22c55e',
+ '#14b8a6',
+ '#06b6d4',
+ '#0ea5e9',
+ '#3b82f6',
+ '#6366f1',
+ '#8b5cf6',
+ '#a855f7',
+ '#d946ef',
+ '#ec4899',
+ '#f43f5e',
+];
+
+export const DEFAULT_SETTINGS = {
+ workingHoursPerDay: 8,
+ workingDaysPerWeek: 5,
+ roundingIncrement: 0,
+ roundingMethod: 'none' as const,
+ defaultVisibility: 'private' as const,
+ weekStartsOn: 1 as const,
+ timerReminderMinutes: 0,
+ autoStopTimerHours: 0,
+};
diff --git a/apps/taktik/packages/shared/src/index.ts b/apps/taktik/packages/shared/src/index.ts
new file mode 100644
index 000000000..98583d97c
--- /dev/null
+++ b/apps/taktik/packages/shared/src/index.ts
@@ -0,0 +1,2 @@
+export * from './types/index.js';
+export * from './constants/index.js';
diff --git a/apps/taktik/packages/shared/src/types/index.ts b/apps/taktik/packages/shared/src/types/index.ts
new file mode 100644
index 000000000..acdd8d3ce
--- /dev/null
+++ b/apps/taktik/packages/shared/src/types/index.ts
@@ -0,0 +1,154 @@
+// ─── Billing Rate ────────────────────────────────────────
+
+export interface BillingRate {
+ amount: number;
+ currency: string;
+ per: 'hour' | 'day';
+}
+
+// ─── Client ──────────────────────────────────────────────
+
+export interface Client {
+ id: string;
+ name: string;
+ shortCode?: string;
+ contactId?: string;
+ email?: string;
+ color: string;
+ isArchived: boolean;
+ billingRate?: BillingRate;
+ notes?: string;
+ order: number;
+ createdAt: string;
+ updatedAt: string;
+}
+
+// ─── Project ─────────────────────────────────────────────
+
+export interface ProjectBudget {
+ type: 'hours' | 'fixed';
+ amount: number;
+ currency?: string;
+}
+
+export type ProjectVisibility = 'private' | 'guild';
+
+export interface Project {
+ id: string;
+ clientId?: string;
+ name: string;
+ description?: string;
+ color: string;
+ isArchived: boolean;
+ isBillable: boolean;
+ billingRate?: BillingRate;
+ budget?: ProjectBudget;
+ visibility: ProjectVisibility;
+ guildId?: string;
+ order: number;
+ createdAt: string;
+ updatedAt: string;
+}
+
+// ─── Time Entry ──────────────────────────────────────────
+
+export type EntrySource = 'todo' | 'calendar' | 'manual' | 'timer';
+
+export interface EntrySourceRef {
+ app: EntrySource;
+ refId?: string;
+}
+
+export interface TimeEntry {
+ id: string;
+ projectId?: string;
+ clientId?: string;
+ description: string;
+ date: string;
+ startTime?: string;
+ endTime?: string;
+ duration: number;
+ isBillable: boolean;
+ isRunning: boolean;
+ tags: string[];
+ billingRate?: BillingRate;
+ visibility: ProjectVisibility;
+ guildId?: string;
+ source?: EntrySourceRef;
+ createdAt: string;
+ updatedAt: string;
+}
+
+// ─── Tag ─────────────────────────────────────────────────
+
+export interface Tag {
+ id: string;
+ name: string;
+ color: string;
+ order: number;
+ createdAt: string;
+ updatedAt: string;
+}
+
+// ─── Template ────────────────────────────────────────────
+
+export interface EntryTemplate {
+ id: string;
+ name: string;
+ projectId?: string;
+ clientId?: string;
+ description: string;
+ isBillable: boolean;
+ tags: string[];
+ usageCount: number;
+ lastUsedAt?: string;
+ createdAt: string;
+ updatedAt: string;
+}
+
+// ─── Settings ────────────────────────────────────────────
+
+export type RoundingMethod = 'none' | 'up' | 'down' | 'nearest';
+
+export interface TaktikSettings {
+ id: string;
+ defaultBillingRate?: BillingRate;
+ workingHoursPerDay: number;
+ workingDaysPerWeek: number;
+ roundingIncrement: number;
+ roundingMethod: RoundingMethod;
+ defaultVisibility: ProjectVisibility;
+ weekStartsOn: 0 | 1;
+ timerReminderMinutes: number;
+ autoStopTimerHours: number;
+ createdAt: string;
+ updatedAt: string;
+}
+
+// ─── View & Filter ───────────────────────────────────────
+
+export type ViewMode = 'day' | 'week' | 'month';
+export type SortField = 'date' | 'duration' | 'project' | 'client' | 'createdAt';
+export type SortDirection = 'asc' | 'desc';
+
+export interface SortOption {
+ field: SortField;
+ direction: SortDirection;
+}
+
+export interface FilterCriteria {
+ search?: string;
+ projectId?: string;
+ clientId?: string;
+ tagIds?: string[];
+ isBillable?: boolean;
+ dateFrom?: string;
+ dateTo?: string;
+}
+
+export interface SavedFilter {
+ id: string;
+ name: string;
+ criteria: FilterCriteria;
+ createdAt: string;
+}
diff --git a/apps/taktik/packages/shared/tsconfig.json b/apps/taktik/packages/shared/tsconfig.json
new file mode 100644
index 000000000..d2e7ec71f
--- /dev/null
+++ b/apps/taktik/packages/shared/tsconfig.json
@@ -0,0 +1,16 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true
+ },
+ "include": ["src/**/*"]
+}
diff --git a/package.json b/package.json
index 9594299a6..b20e32a09 100644
--- a/package.json
+++ b/package.json
@@ -117,6 +117,9 @@
"dev:tags-test": "./scripts/setup-databases.sh todo && ./scripts/setup-databases.sh calendar && ./scripts/setup-databases.sh contacts && ./scripts/setup-databases.sh auth && concurrently -n auth,todo-be,todo-web,cal-be,cal-web,con-be,con-web -c blue,green,cyan,yellow,magenta,red,white \"pnpm dev:auth\" \"pnpm dev:todo:backend\" \"pnpm dev:todo:web\" \"pnpm dev:calendar:backend\" \"pnpm dev:calendar:web\" \"pnpm dev:contacts:backend\" \"pnpm dev:contacts:web\"",
"inventar:dev": "turbo run dev --filter=inventar...",
"dev:inventar:web": "pnpm --filter @inventar/web dev",
+ "taktik:dev": "turbo run dev --filter=taktik...",
+ "dev:taktik:web": "pnpm --filter @taktik/web dev",
+ "dev:taktik:full": "concurrently -n auth,sync,web -c blue,magenta,cyan \"pnpm dev:auth\" \"pnpm dev:sync\" \"pnpm dev:taktik:web\"",
"moodlit:dev": "turbo run dev --filter=moodlit...",
"dev:moodlit:mobile": "pnpm --filter @moodlit/mobile dev",
"dev:moodlit:web": "pnpm --filter @moodlit/web dev",
diff --git a/packages/shared-branding/src/app-icons.ts b/packages/shared-branding/src/app-icons.ts
index 8ebf90513..5ceaf69b0 100644
--- a/packages/shared-branding/src/app-icons.ts
+++ b/packages/shared-branding/src/app-icons.ts
@@ -75,6 +75,9 @@ const playgroundSvg = ``;
+// Taktik icon (clock with play button, amber gradient)
+const taktikSvg = ``;
+
// Context icon (document/knowledge with sky blue gradient)
const contextSvg = ``;
@@ -106,6 +109,7 @@ export const APP_ICONS = {
playground: svgToDataUrl(playgroundSvg),
context: svgToDataUrl(contextSvg),
citycorners: svgToDataUrl(citycornersSvg),
+ taktik: svgToDataUrl(taktikSvg),
} as const;
export type AppIconId = keyof typeof APP_ICONS;
diff --git a/packages/shared-branding/src/mana-apps.ts b/packages/shared-branding/src/mana-apps.ts
index 059a65e00..fd5b385a5 100644
--- a/packages/shared-branding/src/mana-apps.ts
+++ b/packages/shared-branding/src/mana-apps.ts
@@ -340,6 +340,22 @@ export const MANA_APPS: ManaApp[] = [
comingSoon: false,
status: 'development',
},
+ {
+ id: 'taktik',
+ name: 'Taktik',
+ description: {
+ de: 'Zeiterfassung & Timetracking',
+ en: 'Time Tracking',
+ },
+ longDescription: {
+ de: 'Professionelle Zeiterfassung mit Timer, Projekten, Kunden, Reports und Gilden-Integration.',
+ en: 'Professional time tracking with timer, projects, clients, reports, and guild integration.',
+ },
+ icon: APP_ICONS.taktik,
+ color: '#f59e0b',
+ comingSoon: false,
+ status: 'development',
+ },
{
id: 'citycorners',
name: 'CityCorners',
@@ -447,6 +463,7 @@ export const APP_URLS: Record = {
playground: { dev: 'http://localhost:5190', prod: 'https://playground.mana.how' },
context: { dev: 'http://localhost:5192', prod: 'https://context.mana.how' },
citycorners: { dev: 'http://localhost:5196', prod: 'https://citycorners.mana.how' },
+ taktik: { dev: 'http://localhost:5197', prod: 'https://taktik.mana.how' },
};
/**
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 068e5f1f9..18a29c8f8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -78,14 +78,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.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1)
+ version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(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.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.21.0)(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.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.21.0)(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.21.0)(yaml@2.8.1))
@@ -94,13 +94,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
@@ -1491,7 +1491,7 @@ importers:
version: 3.6.3(@types/node@24.10.1)(@types/react-dom@18.3.7(@types/react@18.3.27))(@types/react@18.3.27)(lightningcss@1.30.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(terser@5.44.1)
'@astrojs/tailwind':
specifier: ^5.1.0
- version: 5.1.5(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.1))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
+ version: 5.1.5(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.1))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))
'@iconify-json/heroicons':
specifier: ^1.2.2
version: 1.2.3
@@ -1509,7 +1509,7 @@ importers:
version: link:../../../../packages/shared-landing-ui
astro:
specifier: ^5.16.0
- version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1)
+ version: 5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1)
astro-icon:
specifier: ^1.1.5
version: 1.1.5
@@ -4146,6 +4146,122 @@ importers:
specifier: ^5.7.3
version: 5.9.3
+ apps/taktik:
+ devDependencies:
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
+
+ apps/taktik/apps/web:
+ dependencies:
+ '@manacore/local-store':
+ specifier: workspace:*
+ version: link:../../../../packages/local-store
+ '@manacore/shared-auth':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-auth
+ '@manacore/shared-auth-stores':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-auth-stores
+ '@manacore/shared-auth-ui':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-auth-ui
+ '@manacore/shared-branding':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-branding
+ '@manacore/shared-error-tracking':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-error-tracking
+ '@manacore/shared-icons':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-icons
+ '@manacore/shared-landing-ui':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-landing-ui
+ '@manacore/shared-profile-ui':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-profile-ui
+ '@manacore/shared-stores':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-stores
+ '@manacore/shared-theme':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-theme
+ '@manacore/shared-types':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-types
+ '@manacore/shared-ui':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-ui
+ '@manacore/shared-utils':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-utils
+ '@manacore/subscriptions':
+ specifier: workspace:*
+ version: link:../../../../packages/subscriptions
+ '@taktik/shared':
+ specifier: workspace:*
+ version: link:../../packages/shared
+ date-fns:
+ specifier: ^4.1.0
+ version: 4.1.0
+ svelte-i18n:
+ specifier: ^4.0.1
+ version: 4.0.1(svelte@5.44.0)
+ devDependencies:
+ '@manacore/shared-tailwind':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-tailwind
+ '@manacore/shared-vite-config':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-vite-config
+ '@sveltejs/adapter-node':
+ specifier: ^5.2.12
+ version: 5.4.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))
+ '@sveltejs/kit':
+ specifier: ^2.21.0
+ version: 2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
+ '@sveltejs/vite-plugin-svelte':
+ specifier: ^5.1.0
+ version: 5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
+ '@tailwindcss/vite':
+ specifier: ^4.1.7
+ version: 4.1.17(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
+ '@types/node':
+ specifier: ^22.15.29
+ version: 22.19.1
+ '@vite-pwa/sveltekit':
+ specifier: ^1.1.0
+ version: 1.1.0(@sveltejs/kit@2.49.0(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.44.0)(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(svelte@5.44.0)(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)))(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))(workbox-build@7.4.0(@types/babel__core@7.20.5))(workbox-window@7.4.0)
+ svelte:
+ specifier: ^5.41.0
+ version: 5.44.0
+ svelte-check:
+ specifier: ^4.2.1
+ version: 4.3.4(picomatch@4.0.3)(svelte@5.44.0)(typescript@5.9.3)
+ tailwindcss:
+ specifier: ^4.1.7
+ version: 4.1.17
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
+ vite:
+ specifier: ^6.3.5
+ version: 6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
+ vitest:
+ specifier: ^3.2.1
+ version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
+
+ apps/taktik/packages/shared:
+ dependencies:
+ '@manacore/shared-types':
+ specifier: workspace:*
+ version: link:../../../../packages/shared-types
+ devDependencies:
+ typescript:
+ specifier: ^5.9.3
+ version: 5.9.3
+
apps/todo:
devDependencies:
typescript:
@@ -21416,9 +21532,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@astrojs/tailwind@5.1.5(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.1))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))':
+ '@astrojs/tailwind@5.1.5(astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.21.0)(yaml@2.8.1))(ts-node@10.9.2(@types/node@24.10.1)(typescript@5.9.3))':
dependencies:
- astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1)
+ astro: 5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(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@24.10.1)(typescript@5.9.3))
@@ -21426,6 +21542,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.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.21.0)(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.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(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.21.0)(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.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1))(tailwindcss@3.4.18(tsx@4.21.0)(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.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1)
@@ -23578,6 +23704,11 @@ snapshots:
eslint: 8.57.1
eslint-visitor-keys: 3.4.3
+ '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@1.21.7))':
+ dependencies:
+ eslint: 9.39.1(jiti@1.21.7)
+ eslint-visitor-keys: 3.4.3
+
'@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))':
dependencies:
eslint: 9.39.1(jiti@2.6.1)
@@ -23942,7 +24073,7 @@ snapshots:
ws: 8.18.3
zod: 3.25.76
optionalDependencies:
- expo-router: 55.0.5(oinrqag3kg73e5vim3pjq4pqwa)
+ expo-router: 55.0.5(3s5jslrd73ksoqlrblc4nkbaxq)
react-native: 0.83.2(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0)
transitivePeerDependencies:
- '@expo/dom-webview'
@@ -24108,7 +24239,6 @@ snapshots:
- supports-color
- typescript
- utf-8-validate
- optional: true
'@expo/code-signing-certificates@0.0.5':
dependencies:
@@ -24263,7 +24393,6 @@ snapshots:
optionalDependencies:
react: 19.2.4
react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4)
- optional: true
'@expo/dom-webview@55.0.3(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)':
dependencies:
@@ -24289,7 +24418,6 @@ snapshots:
expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.2)(expo-router@55.0.5)(react-dom@19.2.4(react@19.2.4))(react-native-webview@13.12.2(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
react: 19.2.4
react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4)
- optional: true
'@expo/env@2.0.7':
dependencies:
@@ -24416,7 +24544,6 @@ snapshots:
react: 19.2.4
react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4)
stacktrace-parser: 0.1.11
- optional: true
'@expo/mcp-tunnel@0.1.0':
dependencies:
@@ -24537,7 +24664,7 @@ snapshots:
postcss: 8.4.49
resolve-from: 5.0.0
optionalDependencies:
- expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.2)(expo-router@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.12.2(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.2)(expo-router@55.0.5)(react-dom@19.2.4(react@19.2.4))(react-native-webview@13.12.2(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -24718,7 +24845,7 @@ snapshots:
'@expo/json-file': 10.0.12
'@react-native/normalize-colors': 0.83.2
debug: 4.4.3
- expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.2)(expo-router@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.12.2(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.2)(expo-router@55.0.5)(react-dom@19.2.4(react@19.2.4))(react-native-webview@13.12.2(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
resolve-from: 5.0.0
semver: 7.7.3
xml2js: 0.6.0
@@ -24785,7 +24912,6 @@ snapshots:
react-dom: 19.2.4(react@19.2.4)
transitivePeerDependencies:
- supports-color
- optional: true
'@expo/schema-utils@0.1.7': {}
@@ -24847,7 +24973,6 @@ snapshots:
expo-font: 55.0.4(expo@55.0.5)(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)
react: 19.2.4
react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4)
- optional: true
'@expo/ws-tunnel@1.0.6': {}
@@ -27252,8 +27377,7 @@ snapshots:
'@react-native/assets-registry@0.83.2': {}
- '@react-native/assets-registry@0.84.1':
- optional: true
+ '@react-native/assets-registry@0.84.1': {}
'@react-native/babel-plugin-codegen@0.81.5(@babel/core@7.28.5)':
dependencies:
@@ -27410,7 +27534,6 @@ snapshots:
nullthrows: 1.1.1
tinyglobby: 0.2.15
yargs: 17.7.2
- optional: true
'@react-native/community-cli-plugin@0.81.4':
dependencies:
@@ -27467,7 +27590,6 @@ snapshots:
- bufferutil
- supports-color
- utf-8-validate
- optional: true
'@react-native/debugger-frontend@0.81.4': {}
@@ -27475,8 +27597,7 @@ snapshots:
'@react-native/debugger-frontend@0.83.2': {}
- '@react-native/debugger-frontend@0.84.1':
- optional: true
+ '@react-native/debugger-frontend@0.84.1': {}
'@react-native/debugger-shell@0.83.2':
dependencies:
@@ -27490,7 +27611,6 @@ snapshots:
fb-dotslash: 0.5.8
transitivePeerDependencies:
- supports-color
- optional: true
'@react-native/dev-middleware@0.81.4':
dependencies:
@@ -27565,7 +27685,6 @@ snapshots:
- bufferutil
- supports-color
- utf-8-validate
- optional: true
'@react-native/gradle-plugin@0.81.4': {}
@@ -27573,8 +27692,7 @@ snapshots:
'@react-native/gradle-plugin@0.83.2': {}
- '@react-native/gradle-plugin@0.84.1':
- optional: true
+ '@react-native/gradle-plugin@0.84.1': {}
'@react-native/js-polyfills@0.81.4': {}
@@ -27582,8 +27700,7 @@ snapshots:
'@react-native/js-polyfills@0.83.2': {}
- '@react-native/js-polyfills@0.84.1':
- optional: true
+ '@react-native/js-polyfills@0.84.1': {}
'@react-native/normalize-colors@0.74.89': {}
@@ -27593,8 +27710,7 @@ snapshots:
'@react-native/normalize-colors@0.83.2': {}
- '@react-native/normalize-colors@0.84.1':
- optional: true
+ '@react-native/normalize-colors@0.84.1': {}
'@react-native/virtualized-lists@0.81.4(@types/react@19.2.14)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)':
dependencies:
@@ -27649,7 +27765,6 @@ snapshots:
react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4)
optionalDependencies:
'@types/react': 19.2.14
- optional: true
'@react-navigation/bottom-tabs@7.15.5(@react-navigation/native@7.1.33(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.7.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native-screens@4.24.0(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0)':
dependencies:
@@ -30999,11 +31114,11 @@ snapshots:
- vite
optional: true
- '@vitest/browser@3.2.4(playwright@1.57.0)(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))(vitest@3.2.4)':
+ '@vitest/browser@3.2.4(playwright@1.57.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))(vitest@3.2.4)':
dependencies:
'@testing-library/dom': 10.4.1
'@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1)
- '@vitest/mocker': 3.2.4(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
+ '@vitest/mocker': 3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))
'@vitest/utils': 3.2.4
magic-string: 0.30.21
sirv: 3.0.2
@@ -31133,6 +31248,15 @@ snapshots:
optionalDependencies:
vite: 6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
+ '@vitest/mocker@3.2.4(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))':
+ dependencies:
+ '@vitest/spy': 3.2.4
+ estree-walker: 3.0.3
+ magic-string: 0.30.21
+ optionalDependencies:
+ vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
+ optional: true
+
'@vitest/mocker@4.0.14(vite@6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))':
dependencies:
'@vitest/spy': 4.0.14
@@ -31315,7 +31439,7 @@ snapshots:
sirv: 3.0.2
tinyglobby: 0.2.15
tinyrainbow: 2.0.0
- vitest: 3.2.4(@types/debug@4.1.12)(@types/node@24.10.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
+ vitest: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.1)(@vitest/browser@3.2.4)(@vitest/ui@3.2.4)(jiti@2.6.1)(jsdom@29.0.1(@noble/hashes@2.0.1))(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
optional: true
'@vitest/ui@4.0.14(vitest@4.0.14)':
@@ -31827,6 +31951,108 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@20.19.25)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(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.9.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.21.0)(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.21.0)(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.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1):
dependencies:
'@astrojs/compiler': 2.13.0
@@ -31929,108 +32155,6 @@ snapshots:
- uploadthing
- yaml
- astro@5.16.0(@netlify/blobs@10.4.1)(@types/node@24.10.1)(ioredis@5.9.2)(jiti@1.21.7)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(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.9.2)
- vfile: 6.0.3
- vite: 6.4.1(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
- vitefu: 1.1.1(vite@6.4.1(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(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@24.10.1)(ioredis@5.9.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.53.3)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.1):
dependencies:
'@astrojs/compiler': 2.13.0
@@ -34255,6 +34379,11 @@ snapshots:
eslint: 9.39.1(jiti@2.6.1)
semver: 7.7.3
+ 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)
@@ -34323,6 +34452,10 @@ snapshots:
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)
@@ -34484,6 +34617,20 @@ snapshots:
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
+
eslint-plugin-astro@1.5.0(eslint@9.39.1(jiti@2.6.1)):
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
@@ -34915,6 +35062,47 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ eslint@9.39.1(jiti@1.21.7):
+ dependencies:
+ '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@1.21.7))
+ '@eslint-community/regexpp': 4.12.2
+ '@eslint/config-array': 0.21.1
+ '@eslint/config-helpers': 0.4.2
+ '@eslint/core': 0.17.0
+ '@eslint/eslintrc': 3.3.1
+ '@eslint/js': 9.39.1
+ '@eslint/plugin-kit': 0.4.1
+ '@humanfs/node': 0.16.7
+ '@humanwhocodes/module-importer': 1.0.1
+ '@humanwhocodes/retry': 0.4.3
+ '@types/estree': 1.0.8
+ ajv: 6.12.6
+ chalk: 4.1.2
+ cross-spawn: 7.0.6
+ debug: 4.4.3
+ escape-string-regexp: 4.0.0
+ eslint-scope: 8.4.0
+ eslint-visitor-keys: 4.2.1
+ espree: 10.4.0
+ esquery: 1.6.0
+ esutils: 2.0.3
+ fast-deep-equal: 3.1.3
+ file-entry-cache: 8.0.0
+ find-up: 5.0.0
+ glob-parent: 6.0.2
+ ignore: 5.3.2
+ imurmurhash: 0.1.4
+ is-glob: 4.0.3
+ json-stable-stringify-without-jsonify: 1.0.1
+ lodash.merge: 4.6.2
+ minimatch: 3.1.2
+ natural-compare: 1.4.0
+ optionator: 0.9.4
+ optionalDependencies:
+ jiti: 1.21.7
+ transitivePeerDependencies:
+ - supports-color
+
eslint@9.39.1(jiti@2.6.1):
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
@@ -35170,7 +35358,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- typescript
- optional: true
expo-audio@55.0.8(expo-asset@55.0.8(expo@55.0.5)(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3))(expo@55.0.5)(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0):
dependencies:
@@ -35280,7 +35467,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- typescript
- optional: true
expo-dev-client@6.0.18(expo@55.0.5):
dependencies:
@@ -35340,7 +35526,6 @@ snapshots:
dependencies:
expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.2)(expo-router@55.0.5)(react-dom@19.2.4(react@19.2.4))(react-native-webview@13.12.2(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4)
- optional: true
expo-font@14.0.10(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0):
dependencies:
@@ -35383,7 +35568,6 @@ snapshots:
fontfaceobserver: 2.3.0
react: 19.2.4
react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4)
- optional: true
expo-glass-effect@55.0.8(expo@54.0.25)(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0):
dependencies:
@@ -35417,11 +35601,11 @@ snapshots:
expo-image-loader@55.0.0(expo@55.0.5):
dependencies:
- expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.2)(expo-router@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.12.2(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.2)(expo-router@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.12.2(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
expo-image-picker@55.0.12(expo@55.0.5):
dependencies:
- expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.2)(expo-router@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.12.2(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.2)(expo-router@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.12.2(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
expo-image-loader: 55.0.0(expo@55.0.5)
expo-image@55.0.6(expo@54.0.25)(react-native-web@0.21.2(encoding@0.1.13)(react-dom@19.2.4(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.1.0))(react@19.1.0):
@@ -35478,7 +35662,6 @@ snapshots:
dependencies:
expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.2)(expo-router@55.0.5)(react-dom@19.2.4(react@19.2.4))(react-native-webview@13.12.2(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4))(react-native@0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4))(react@19.2.4)(typescript@5.9.3)
react: 19.2.4
- optional: true
expo-linear-gradient@15.0.7(expo@55.0.5)(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0):
dependencies:
@@ -35644,7 +35827,6 @@ snapshots:
invariant: 2.2.4
react: 19.2.4
react-native: 0.84.1(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.4)
- optional: true
expo-notifications@55.0.12(expo@55.0.5)(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3):
dependencies:
@@ -36114,7 +36296,7 @@ snapshots:
expo-secure-store@55.0.8(expo@55.0.5):
dependencies:
- expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.2)(expo-router@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.12.2(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.2.14)(react@19.2.0))(react@19.2.0)(typescript@5.9.3)
+ expo: 55.0.5(@babel/core@7.28.5)(@expo/dom-webview@55.0.3)(@expo/metro-runtime@6.1.2)(expo-router@55.0.5)(react-dom@19.2.0(react@19.2.0))(react-native-webview@13.12.2(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0))(react-native@0.83.2(@babel/core@7.28.5)(@types/react@19.1.17)(react@19.2.0))(react@19.2.0)(typescript@5.3.3)
expo-server@1.0.4: {}
@@ -36519,7 +36701,6 @@ snapshots:
- supports-color
- typescript
- utf-8-validate
- optional: true
exponential-backoff@3.1.3: {}
@@ -37296,8 +37477,7 @@ snapshots:
hermes-compiler@0.14.1: {}
- hermes-compiler@250829098.0.9:
- optional: true
+ hermes-compiler@250829098.0.9: {}
hermes-estree@0.29.1: {}
@@ -42293,7 +42473,6 @@ snapshots:
- bufferutil
- supports-color
- utf-8-validate
- optional: true
react-refresh@0.14.2: {}
@@ -44709,6 +44888,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.21.0)(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.21.0
+ 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.21.0)(yaml@2.8.1):
dependencies:
esbuild: 0.25.12
@@ -44743,23 +44939,6 @@ snapshots:
tsx: 4.21.0
yaml: 2.8.1
- vite@6.4.1(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(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': 24.10.1
- fsevents: 2.3.3
- jiti: 1.21.7
- lightningcss: 1.30.2
- terser: 5.44.1
- tsx: 4.21.0
- yaml: 2.8.1
-
vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1):
dependencies:
esbuild: 0.25.12
@@ -44811,6 +44990,10 @@ snapshots:
tsx: 4.21.0
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.21.0)(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.21.0)(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.21.0)(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.21.0)(yaml@2.8.1)
@@ -44819,10 +45002,6 @@ snapshots:
optionalDependencies:
vite: 6.4.1(@types/node@22.19.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
- vitefu@1.1.1(vite@6.4.1(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)):
- optionalDependencies:
- vite: 6.4.1(@types/node@24.10.1)(jiti@1.21.7)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
-
vitefu@1.1.1(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)):
optionalDependencies:
vite: 6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1)
@@ -45096,7 +45275,7 @@ snapshots:
optionalDependencies:
'@types/debug': 4.1.12
'@types/node': 24.10.1
- '@vitest/browser': 3.2.4(playwright@1.57.0)(vite@6.4.1(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))(vitest@3.2.4)
+ '@vitest/browser': 3.2.4(playwright@1.57.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.1))(vitest@3.2.4)
'@vitest/ui': 3.2.4(vitest@3.2.4)
jsdom: 29.0.1(@noble/hashes@2.0.1)
transitivePeerDependencies: