From 8e71096a619fc56e5d495780140fa7465b231716 Mon Sep 17 00:00:00 2001 From: Till JS Date: Tue, 7 Apr 2026 14:07:12 +0200 Subject: [PATCH] feat(dreams): scaffold Traumtagebuch module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new Dreams module to the unified Mana app for capturing dream journal entries with mood, lucid status, recurring symbols, and timeline insights. Founder-tier gated for now. - Dexie schema v5 with dreams, dreamSymbols, dreamTags - Mutation store with auto symbol counting on create/update/delete - ListView with quick capture, inline editor, mood picker, lucid toggle, monthly grouping, insights ribbon, context menu - Workbench registration with note → dream drop transform - New 'dream' DragType, dreams app icon, mana-apps catalog entry Co-Authored-By: Claude Opus 4.6 (1M context) --- .../apps/web/src/lib/app-registry/apps.ts | 42 ++ apps/mana/apps/web/src/lib/data/database.ts | 70 +- .../src/lib/modules/dreams/ListView.svelte | 624 ++++++++++++++++++ .../web/src/lib/modules/dreams/collections.ts | 88 +++ .../apps/web/src/lib/modules/dreams/index.ts | 34 + .../web/src/lib/modules/dreams/queries.ts | 142 ++++ .../modules/dreams/stores/dreams.svelte.ts | 165 +++++ .../apps/web/src/lib/modules/dreams/types.ts | 102 +++ packages/shared-branding/src/app-icons.ts | 3 + packages/shared-branding/src/mana-apps.ts | 18 + packages/shared-ui/src/dnd/types.ts | 3 +- 11 files changed, 1287 insertions(+), 4 deletions(-) create mode 100644 apps/mana/apps/web/src/lib/modules/dreams/ListView.svelte create mode 100644 apps/mana/apps/web/src/lib/modules/dreams/collections.ts create mode 100644 apps/mana/apps/web/src/lib/modules/dreams/index.ts create mode 100644 apps/mana/apps/web/src/lib/modules/dreams/queries.ts create mode 100644 apps/mana/apps/web/src/lib/modules/dreams/stores/dreams.svelte.ts create mode 100644 apps/mana/apps/web/src/lib/modules/dreams/types.ts diff --git a/apps/mana/apps/web/src/lib/app-registry/apps.ts b/apps/mana/apps/web/src/lib/app-registry/apps.ts index ec9b7f453..76612a2a4 100644 --- a/apps/mana/apps/web/src/lib/app-registry/apps.ts +++ b/apps/mana/apps/web/src/lib/app-registry/apps.ts @@ -259,6 +259,48 @@ registerApp({ }, }); +registerApp({ + id: 'dreams', + name: 'Dreams', + color: '#6366F1', + views: { + list: { load: () => import('$lib/modules/dreams/ListView.svelte') }, + }, + contextMenuActions: [ + { + id: 'new-dream', + label: 'Neuer Traum', + icon: Plus, + action: () => + window.dispatchEvent( + new CustomEvent('mana:quick-action', { detail: { app: 'dreams', action: 'new' } }) + ), + }, + ], + collection: 'dreams', + paramKey: 'dreamId', + dragType: 'dream', + acceptsDropFrom: ['note'], + transformIncoming: { + note: (source) => ({ + title: source.title as string, + content: (source.content as string) ?? '', + }), + }, + getDisplayData: (item) => ({ + title: (item.title as string) || 'Traum', + subtitle: (item.dreamDate as string) ?? undefined, + }), + createItem: async (data) => { + const { dreamsStore } = await import('$lib/modules/dreams/stores/dreams.svelte'); + const dream = await dreamsStore.createDream({ + title: (data.title as string) ?? null, + content: (data.content as string) ?? '', + }); + return dream.id; + }, +}); + registerApp({ id: 'finance', name: 'Finance', diff --git a/apps/mana/apps/web/src/lib/data/database.ts b/apps/mana/apps/web/src/lib/data/database.ts index b5b5c39ce..753b6a4bb 100644 --- a/apps/mana/apps/web/src/lib/data/database.ts +++ b/apps/mana/apps/web/src/lib/data/database.ts @@ -405,6 +405,25 @@ db.version(4).stores({ 'id, startDate, kind, type, sourceModule, sourceId, parentBlockId, [sourceModule+sourceId], [type+startDate], [kind+startDate], [parentBlockId+recurrenceDate]', }); +// ─── Version 5: Dreams (Traumtagebuch) ──────────────────────── +// Adds dreams, dreamSymbols, dreamTags tables. + +db.version(5).stores({ + dreams: 'id, dreamDate, mood, isLucid, isPinned, isArchived, updatedAt', + dreamSymbols: 'id, name, count, updatedAt', + dreamTags: 'id, dreamId, tagId, [dreamId+tagId]', +}); + +// ─── Version 6: Events (Social gatherings) ──────────────────── +// Distinct from calendar's `events` table — these are gatherings with guests/RSVPs. +// Main table is `socialEvents` to avoid collision with calendar.events. + +db.version(6).stores({ + socialEvents: 'id, status, timeBlockId, hostContactId, isPublished, [status+createdAt]', + eventGuests: 'id, eventId, contactId, rsvpStatus, [eventId+rsvpStatus], [eventId+contactId]', + eventInvitations: 'id, eventId, guestId, channel, [eventId+guestId]', +}); + // ─── Sync App Map ────────────────────────────────────────── // Maps each table to its appId for sync routing. // The SyncEngine uses this to group pending changes and push to /sync/{appId}. @@ -447,6 +466,8 @@ export const SYNC_APP_MAP: Record = { guides: ['guides', 'sections', 'steps', 'guideCollections', 'runs', 'guideTags'], habits: ['habits', 'habitLogs'], notes: ['notes', 'noteTags'], + dreams: ['dreams', 'dreamSymbols', 'dreamTags'], + events: ['socialEvents', 'eventGuests', 'eventInvitations'], finance: ['transactions', 'financeCategories', 'budgets'], places: ['places', 'locationLogs', 'placeTags'], tags: ['globalTags', 'tagGroups'], @@ -518,6 +539,8 @@ export const TABLE_TO_SYNC_NAME: Record = { guideCollections: 'collections', // finance financeCategories: 'categories', + // events (social gatherings) + socialEvents: 'events', // shared: tags globalTags: 'tags', tagGroups: 'tagGroups', @@ -550,11 +573,52 @@ export function fromSyncName(appId: string, syncCollection: string): string { // Automatically records pending changes for every write to sync-relevant tables. // This means module stores (taskTable.add(), etc.) don't need manual trackChange() calls. -let _applyingServerChanges = false; +/** + * Tables that are currently having server changes applied. Hooks for tables + * in this set skip pending-change tracking (sync loop guard) — but writes to + * OTHER tables continue tracking normally, so a user typing into chat while + * todo is syncing no longer silently drops the chat write. + * + * Replaces a single global boolean that previously caused a cross-app race: + * one app's apply could swallow another app's writes for the duration. + */ +const _applyingTables = new Set(); -/** Set to true while applying server changes to prevent sync loops. */ +/** + * Marks one or more tables as "currently applying server changes". + * Returned dispose function MUST be called (use try/finally) to clear them. + */ +export function beginApplyingTables(tables: Iterable): () => void { + const added: string[] = []; + for (const t of tables) { + if (!_applyingTables.has(t)) { + _applyingTables.add(t); + added.push(t); + } + } + return () => { + for (const t of added) _applyingTables.delete(t); + }; +} + +/** True when a write to `tableName` should bypass the pending-change hook. */ +export function isApplyingTable(tableName: string): boolean { + return _applyingTables.has(tableName); +} + +/** + * @deprecated Legacy single-flag API kept temporarily for any external + * caller. Prefer `beginApplyingTables` so per-table races stay impossible. + * When `v === true` it marks every sync-tracked table; `false` clears them. + */ export function setApplyingServerChanges(v: boolean): void { - _applyingServerChanges = v; + if (v) { + for (const tables of Object.values(SYNC_APP_MAP)) { + for (const t of tables) _applyingTables.add(t); + } + } else { + _applyingTables.clear(); + } } const pendingChangesTable = db.table('_pendingChanges'); diff --git a/apps/mana/apps/web/src/lib/modules/dreams/ListView.svelte b/apps/mana/apps/web/src/lib/modules/dreams/ListView.svelte new file mode 100644 index 000000000..0755d26cf --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/dreams/ListView.svelte @@ -0,0 +1,624 @@ + + + +
+ +
e.preventDefault()} class="quick-add"> + 🌙 + +
+ + + {#if insights.total > 0} +
+ {insights.total} Träume + {#if insights.lucidCount > 0} + ✨ {insights.lucidCount} Klarträume + {/if} + {#each insights.topSymbols as sym} + {sym.name} · {sym.count} + {/each} +
+ {/if} + + + {#if dreams.length > 5} + + {/if} + + +
+ {#each grouped as group (group.label)} +
{group.label}
+ {#each group.dreams as dream (dream.id)} + {#if editingId === dream.id} + +
{ + if (e.key === 'Escape') saveEdit(); + }} + > + + + + +
+
+ {#each MOODS as mood} + + {/each} +
+ +
+ +
+ + +
+
+ {:else} + + + {/if} + {/each} + {/each} + + {#if filtered.length === 0 && dreams.length > 0} +

Keine Treffer

+ {/if} +
+ + {#if dreams.length === 0} +

Tippe oben, um deinen ersten Traum festzuhalten.

+ {/if} + + (ctxMenu = { ...ctxMenu, visible: false, dream: null })} + /> +
+ + diff --git a/apps/mana/apps/web/src/lib/modules/dreams/collections.ts b/apps/mana/apps/web/src/lib/modules/dreams/collections.ts new file mode 100644 index 000000000..f89ad71c3 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/dreams/collections.ts @@ -0,0 +1,88 @@ +/** + * Dreams module — collection accessors and guest seed data. + */ + +import { db } from '$lib/data/database'; +import type { LocalDream, LocalDreamSymbol, LocalDreamTag } from './types'; + +// ─── Collection Accessors ────────────────────────────────── + +export const dreamTable = db.table('dreams'); +export const dreamSymbolTable = db.table('dreamSymbols'); +export const dreamTagTable = db.table('dreamTags'); + +// ─── Guest Seed ──────────────────────────────────────────── + +const today = new Date().toISOString().slice(0, 10); +const yesterday = new Date(Date.now() - 86_400_000).toISOString().slice(0, 10); + +export const DREAMS_GUEST_SEED = { + dreams: [ + { + id: 'dream-welcome', + title: 'Willkommen im Traumtagebuch', + content: + 'Notiere deine Träume direkt nach dem Aufwachen. Je früher du sie festhältst, desto mehr Details bleiben dir.\n\n**Tipps:**\n- Stichworte reichen — Sätze bilden sich später.\n- Stimmung und Klarheit helfen bei Mustern über Wochen.\n- Pinne wiederkehrende Träume.', + dreamDate: today, + mood: 'angenehm', + clarity: 4, + isLucid: false, + isRecurring: false, + sleepQuality: null, + bedtime: null, + wakeTime: null, + location: null, + people: [], + emotions: ['Ruhe', 'Neugier'], + symbols: [], + audioPath: null, + transcript: null, + interpretation: null, + aiInterpretation: null, + isPrivate: false, + isPinned: true, + isArchived: false, + }, + { + id: 'dream-fliegen', + title: 'Über der Stadt geflogen', + content: + 'Ich konnte mich aus dem Bett heben und über die Stadt fliegen. Alles war weich und leuchtete in goldenem Licht.', + dreamDate: yesterday, + mood: 'angenehm', + clarity: 5, + isLucid: true, + isRecurring: false, + sleepQuality: 4, + bedtime: '23:30', + wakeTime: '07:15', + location: 'Über einer fremden Stadt', + people: [], + emotions: ['Freiheit', 'Staunen'], + symbols: ['Fliegen', 'Licht'], + audioPath: null, + transcript: null, + interpretation: 'Gefühl von Kontrolle und Leichtigkeit nach einer entspannten Woche.', + aiInterpretation: null, + isPrivate: false, + isPinned: false, + isArchived: false, + }, + ] satisfies LocalDream[], + dreamSymbols: [ + { + id: 'dream-symbol-fliegen', + name: 'Fliegen', + meaning: 'Freiheit, Loslösung, Kontrolle', + color: '#6366f1', + count: 1, + }, + { + id: 'dream-symbol-licht', + name: 'Licht', + meaning: 'Klarheit, Bewusstsein', + color: '#f59e0b', + count: 1, + }, + ] satisfies LocalDreamSymbol[], +}; diff --git a/apps/mana/apps/web/src/lib/modules/dreams/index.ts b/apps/mana/apps/web/src/lib/modules/dreams/index.ts new file mode 100644 index 000000000..e8d86b90d --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/dreams/index.ts @@ -0,0 +1,34 @@ +/** + * Dreams module — barrel exports. + */ + +// ─── Stores ────────────────────────────────────────────── +export { dreamsStore } from './stores/dreams.svelte'; + +// ─── Queries ───────────────────────────────────────────── +export { + useAllDreams, + useAllDreamSymbols, + toDream, + toDreamSymbol, + searchDreams, + groupByMonth, + formatDreamDate, + computeInsights, +} from './queries'; + +// ─── Collections ───────────────────────────────────────── +export { dreamTable, dreamSymbolTable, dreamTagTable, DREAMS_GUEST_SEED } from './collections'; + +// ─── Types ─────────────────────────────────────────────── +export { MOOD_COLORS, MOOD_LABELS } from './types'; +export type { + LocalDream, + LocalDreamSymbol, + LocalDreamTag, + Dream, + DreamSymbol, + DreamMood, + DreamClarity, + SleepQuality, +} from './types'; diff --git a/apps/mana/apps/web/src/lib/modules/dreams/queries.ts b/apps/mana/apps/web/src/lib/modules/dreams/queries.ts new file mode 100644 index 000000000..b82af774c --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/dreams/queries.ts @@ -0,0 +1,142 @@ +/** + * Reactive Queries & Pure Helpers for Dreams module. + */ + +import { useLiveQueryWithDefault } from '@mana/local-store/svelte'; +import { db } from '$lib/data/database'; +import type { Dream, DreamSymbol, LocalDream, LocalDreamSymbol } from './types'; + +// ─── Type Converters ─────────────────────────────────────── + +export function toDream(local: LocalDream): Dream { + return { + id: local.id, + title: local.title, + content: local.content, + dreamDate: local.dreamDate, + mood: local.mood, + clarity: local.clarity, + isLucid: local.isLucid, + isRecurring: local.isRecurring, + sleepQuality: local.sleepQuality, + bedtime: local.bedtime, + wakeTime: local.wakeTime, + location: local.location, + people: local.people ?? [], + emotions: local.emotions ?? [], + symbols: local.symbols ?? [], + audioPath: local.audioPath, + transcript: local.transcript, + interpretation: local.interpretation, + aiInterpretation: local.aiInterpretation, + isPrivate: local.isPrivate, + isPinned: local.isPinned, + isArchived: local.isArchived, + createdAt: local.createdAt ?? new Date().toISOString(), + updatedAt: local.updatedAt ?? new Date().toISOString(), + }; +} + +export function toDreamSymbol(local: LocalDreamSymbol): DreamSymbol { + return { + id: local.id, + name: local.name, + meaning: local.meaning, + color: local.color, + count: local.count ?? 0, + createdAt: local.createdAt ?? new Date().toISOString(), + updatedAt: local.updatedAt ?? new Date().toISOString(), + }; +} + +// ─── Live Queries ────────────────────────────────────────── + +export function useAllDreams() { + return useLiveQueryWithDefault(async () => { + const locals = await db.table('dreams').toArray(); + return locals + .filter((d) => !d.deletedAt && !d.isArchived) + .map(toDream) + .sort((a, b) => { + if (a.isPinned !== b.isPinned) return a.isPinned ? -1 : 1; + return b.dreamDate.localeCompare(a.dreamDate); + }); + }, [] as Dream[]); +} + +export function useAllDreamSymbols() { + return useLiveQueryWithDefault(async () => { + const locals = await db.table('dreamSymbols').toArray(); + return locals + .filter((s) => !s.deletedAt) + .map(toDreamSymbol) + .sort((a, b) => b.count - a.count); + }, [] as DreamSymbol[]); +} + +// ─── Pure Helpers ────────────────────────────────────────── + +/** Search dreams by title, content, location, symbols and emotions. */ +export function searchDreams(dreams: Dream[], query: string): Dream[] { + if (!query.trim()) return dreams; + const q = query.toLowerCase(); + return dreams.filter((d) => { + const haystack = [ + d.title, + d.content, + d.location, + d.interpretation, + ...(d.symbols ?? []), + ...(d.emotions ?? []), + ...(d.people ?? []), + ] + .filter(Boolean) + .join(' ') + .toLowerCase(); + return haystack.includes(q); + }); +} + +/** Group dreams by month label (e.g. "April 2026"). */ +export function groupByMonth(dreams: Dream[]): Array<{ label: string; dreams: Dream[] }> { + const groups = new Map(); + for (const d of dreams) { + const date = new Date(d.dreamDate); + const label = date.toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }); + if (!groups.has(label)) groups.set(label, []); + groups.get(label)!.push(d); + } + return Array.from(groups, ([label, dreams]) => ({ label, dreams })); +} + +/** Format the dream date relative to today. */ +export function formatDreamDate(iso: string): string { + const date = new Date(iso); + const today = new Date(); + const diffDays = Math.floor((today.getTime() - date.getTime()) / 86_400_000); + if (diffDays === 0) return 'Heute Nacht'; + if (diffDays === 1) return 'Gestern Nacht'; + if (diffDays < 7) return `vor ${diffDays} Tagen`; + return date.toLocaleDateString('de-DE', { day: 'numeric', month: 'short', year: 'numeric' }); +} + +/** Compute insights snapshot from dreams collection. */ +export function computeInsights(dreams: Dream[]) { + const total = dreams.length; + const lucidCount = dreams.filter((d) => d.isLucid).length; + const symbolCounts = new Map(); + for (const d of dreams) { + for (const sym of d.symbols ?? []) { + symbolCounts.set(sym, (symbolCounts.get(sym) ?? 0) + 1); + } + } + const topSymbols = Array.from(symbolCounts, ([name, count]) => ({ name, count })) + .sort((a, b) => b.count - a.count) + .slice(0, 5); + return { + total, + lucidCount, + lucidRate: total ? lucidCount / total : 0, + topSymbols, + }; +} diff --git a/apps/mana/apps/web/src/lib/modules/dreams/stores/dreams.svelte.ts b/apps/mana/apps/web/src/lib/modules/dreams/stores/dreams.svelte.ts new file mode 100644 index 000000000..1bd47b2ab --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/dreams/stores/dreams.svelte.ts @@ -0,0 +1,165 @@ +/** + * Dreams Store — Mutation-Only Service + */ + +import { dreamSymbolTable, dreamTable } from '../collections'; +import { toDream } from '../queries'; +import type { DreamClarity, DreamMood, LocalDream, SleepQuality } from '../types'; + +function todayIsoDate(): string { + return new Date().toISOString().slice(0, 10); +} + +export const dreamsStore = { + async createDream(data: { + title?: string | null; + content?: string; + dreamDate?: string; + mood?: DreamMood | null; + clarity?: DreamClarity | null; + isLucid?: boolean; + symbols?: string[]; + emotions?: string[]; + }) { + const newLocal: LocalDream = { + id: crypto.randomUUID(), + title: data.title ?? null, + content: data.content ?? '', + dreamDate: data.dreamDate ?? todayIsoDate(), + mood: data.mood ?? null, + clarity: data.clarity ?? null, + isLucid: data.isLucid ?? false, + isRecurring: false, + sleepQuality: null, + bedtime: null, + wakeTime: null, + location: null, + people: [], + emotions: data.emotions ?? [], + symbols: data.symbols ?? [], + audioPath: null, + transcript: null, + interpretation: null, + aiInterpretation: null, + isPrivate: false, + isPinned: false, + isArchived: false, + }; + + await dreamTable.add(newLocal); + await this.touchSymbols(newLocal.symbols, +1); + return toDream(newLocal); + }, + + async updateDream( + id: string, + data: Partial< + Pick< + LocalDream, + | 'title' + | 'content' + | 'dreamDate' + | 'mood' + | 'clarity' + | 'isLucid' + | 'isRecurring' + | 'sleepQuality' + | 'bedtime' + | 'wakeTime' + | 'location' + | 'people' + | 'emotions' + | 'symbols' + | 'interpretation' + | 'aiInterpretation' + | 'isPrivate' + | 'isPinned' + | 'isArchived' + > + > + ) { + if (data.symbols) { + const existing = await dreamTable.get(id); + if (existing) { + const oldSet = new Set(existing.symbols ?? []); + const newSet = new Set(data.symbols); + const added = [...newSet].filter((s) => !oldSet.has(s)); + const removed = [...oldSet].filter((s) => !newSet.has(s)); + if (added.length) await this.touchSymbols(added, +1); + if (removed.length) await this.touchSymbols(removed, -1); + } + } + + await dreamTable.update(id, { + ...data, + updatedAt: new Date().toISOString(), + }); + }, + + async deleteDream(id: string) { + const existing = await dreamTable.get(id); + if (existing?.symbols?.length) { + await this.touchSymbols(existing.symbols, -1); + } + await dreamTable.update(id, { + deletedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + }, + + async togglePin(id: string) { + const dream = await dreamTable.get(id); + if (!dream) return; + await dreamTable.update(id, { + isPinned: !dream.isPinned, + updatedAt: new Date().toISOString(), + }); + }, + + async toggleLucid(id: string) { + const dream = await dreamTable.get(id); + if (!dream) return; + await dreamTable.update(id, { + isLucid: !dream.isLucid, + updatedAt: new Date().toISOString(), + }); + }, + + async setMood(id: string, mood: DreamMood | null) { + await dreamTable.update(id, { + mood, + updatedAt: new Date().toISOString(), + }); + }, + + async setSleepQuality(id: string, quality: SleepQuality | null) { + await dreamTable.update(id, { + sleepQuality: quality, + updatedAt: new Date().toISOString(), + }); + }, + + /** Increment or decrement counts for the given symbol names. Creates symbols on demand. */ + async touchSymbols(names: string[], delta: number) { + for (const name of names) { + const trimmed = name.trim(); + if (!trimmed) continue; + const existing = await dreamSymbolTable.where('name').equals(trimmed).first(); + if (existing) { + const next = Math.max(0, (existing.count ?? 0) + delta); + await dreamSymbolTable.update(existing.id, { + count: next, + updatedAt: new Date().toISOString(), + }); + } else if (delta > 0) { + await dreamSymbolTable.add({ + id: crypto.randomUUID(), + name: trimmed, + meaning: null, + color: null, + count: delta, + }); + } + } + }, +}; diff --git a/apps/mana/apps/web/src/lib/modules/dreams/types.ts b/apps/mana/apps/web/src/lib/modules/dreams/types.ts new file mode 100644 index 000000000..92f3615c2 --- /dev/null +++ b/apps/mana/apps/web/src/lib/modules/dreams/types.ts @@ -0,0 +1,102 @@ +/** + * Dreams module types — Traumtagebuch. + */ + +import type { BaseRecord } from '@mana/local-store'; + +export type DreamMood = 'angenehm' | 'neutral' | 'unangenehm' | 'albtraum'; +export type DreamClarity = 1 | 2 | 3 | 4 | 5; +export type SleepQuality = 1 | 2 | 3 | 4 | 5; + +// ─── Local Record Types (Dexie) ─────────────────────────── + +export interface LocalDream extends BaseRecord { + title: string | null; + content: string; + dreamDate: string; // ISO date (YYYY-MM-DD) — die Nacht, in der geträumt wurde + mood: DreamMood | null; + clarity: DreamClarity | null; + isLucid: boolean; + isRecurring: boolean; + sleepQuality: SleepQuality | null; + bedtime: string | null; // ISO time (HH:mm) + wakeTime: string | null; // ISO time (HH:mm) + location: string | null; + people: string[]; + emotions: string[]; + symbols: string[]; + audioPath: string | null; + transcript: string | null; + interpretation: string | null; + aiInterpretation: string | null; + isPrivate: boolean; + isPinned: boolean; + isArchived: boolean; +} + +export interface LocalDreamSymbol extends BaseRecord { + name: string; + meaning: string | null; + color: string | null; + count: number; +} + +export interface LocalDreamTag extends BaseRecord { + dreamId: string; + tagId: string; +} + +// ─── Domain Types ───────────────────────────────────────── + +export interface Dream { + id: string; + title: string | null; + content: string; + dreamDate: string; + mood: DreamMood | null; + clarity: DreamClarity | null; + isLucid: boolean; + isRecurring: boolean; + sleepQuality: SleepQuality | null; + bedtime: string | null; + wakeTime: string | null; + location: string | null; + people: string[]; + emotions: string[]; + symbols: string[]; + audioPath: string | null; + transcript: string | null; + interpretation: string | null; + aiInterpretation: string | null; + isPrivate: boolean; + isPinned: boolean; + isArchived: boolean; + createdAt: string; + updatedAt: string; +} + +export interface DreamSymbol { + id: string; + name: string; + meaning: string | null; + color: string | null; + count: number; + createdAt: string; + updatedAt: string; +} + +// ─── Constants ──────────────────────────────────────────── + +export const MOOD_COLORS: Record = { + angenehm: '#22c55e', + neutral: '#9ca3af', + unangenehm: '#f59e0b', + albtraum: '#ef4444', +}; + +export const MOOD_LABELS: Record = { + angenehm: 'Angenehm', + neutral: 'Neutral', + unangenehm: 'Unangenehm', + albtraum: 'Albtraum', +}; diff --git a/packages/shared-branding/src/app-icons.ts b/packages/shared-branding/src/app-icons.ts index f019f3e76..c5369a7bb 100644 --- a/packages/shared-branding/src/app-icons.ts +++ b/packages/shared-branding/src/app-icons.ts @@ -133,6 +133,9 @@ export const APP_ICONS = { notes: svgToDataUrl( `` ), + dreams: svgToDataUrl( + `` + ), finance: svgToDataUrl( `` ), diff --git a/packages/shared-branding/src/mana-apps.ts b/packages/shared-branding/src/mana-apps.ts index 7c0af8bb6..08c0fd46b 100644 --- a/packages/shared-branding/src/mana-apps.ts +++ b/packages/shared-branding/src/mana-apps.ts @@ -615,6 +615,23 @@ export const MANA_APPS: ManaApp[] = [ status: 'development', requiredTier: 'founder', }, + { + id: 'dreams', + name: 'Dreams', + description: { + de: 'Traumtagebuch', + en: 'Dream Journal', + }, + longDescription: { + de: 'Halte deine Träume fest, bevor sie verblassen. Stimmung, Klartraum-Status, wiederkehrende Symbole und Insights über die Zeit.', + en: 'Capture your dreams before they fade. Mood, lucid status, recurring symbols, and insights over time.', + }, + icon: APP_ICONS.dreams, + color: '#6366f1', + comingSoon: false, + status: 'development', + requiredTier: 'founder', + }, { id: 'finance', name: 'Finance', @@ -777,6 +794,7 @@ export const APP_URLS: Record = { guides: { dev: 'http://localhost:5173/guides', prod: 'https://mana.how/guides' }, habits: { dev: 'http://localhost:5173/habits', prod: 'https://mana.how/habits' }, notes: { dev: 'http://localhost:5173/notes', prod: 'https://mana.how/notes' }, + dreams: { dev: 'http://localhost:5173/dreams', prod: 'https://mana.how/dreams' }, finance: { dev: 'http://localhost:5173/finance', prod: 'https://mana.how/finance' }, places: { dev: 'http://localhost:5173/places', prod: 'https://mana.how/places' }, wisekeep: { dev: 'http://localhost:5173/wisekeep', prod: 'https://mana.how/wisekeep' }, diff --git a/packages/shared-ui/src/dnd/types.ts b/packages/shared-ui/src/dnd/types.ts index 7352945a2..c51ebe218 100644 --- a/packages/shared-ui/src/dnd/types.ts +++ b/packages/shared-ui/src/dnd/types.ts @@ -23,7 +23,8 @@ export type DragType = | 'habit' | 'note' | 'transaction' - | 'place'; + | 'place' + | 'dream'; export interface DragPayload> { type: DragType;