diff --git a/apps/manacore/apps/web/src/lib/data/database.ts b/apps/manacore/apps/web/src/lib/data/database.ts new file mode 100644 index 000000000..942daf04f --- /dev/null +++ b/apps/manacore/apps/web/src/lib/data/database.ts @@ -0,0 +1,231 @@ +/** + * Unified Dexie Database — Single IndexedDB for all ManaCore apps. + * + * All collections from all app modules are registered in one database. + * Table names that collide across apps are prefixed (e.g., pictureTags, storageTags). + * + * The SYNC_APP_MAP maps each table back to its appId for sync routing. + */ + +import Dexie, { type EntityTable } from 'dexie'; + +// ─── Database ────────────────────────────────────────────── + +export const db = new Dexie('manacore'); + +db.version(1).stores({ + // ─── Sync Infrastructure ─── + _pendingChanges: '++id, appId, collection, recordId, createdAt', + _syncMeta: '[appId+collection]', + + // ─── Core / ManaCore (appId: 'manacore') ─── + userSettings: 'id, key', + dashboardConfigs: 'id', + + // ─── Todo (appId: 'todo') ─── + tasks: + 'id, dueDate, isCompleted, priority, order, projectId, [isCompleted+order], [projectId+order]', + todoProjects: 'id, order, isArchived, isDefault', + labels: 'id', + taskLabels: 'id, taskId, labelId', + reminders: 'id, taskId', + boardViews: 'id, order, groupBy', + + // ─── Calendar (appId: 'calendar') ─── + calendars: 'id, isDefault, isVisible', + events: 'id, calendarId, startDate, endDate, allDay, [calendarId+startDate]', + + // ─── Contacts (appId: 'contacts') ─── + contacts: 'id, firstName, lastName, email, company, isFavorite, isArchived', + + // ─── Chat (appId: 'chat') ─── + conversations: 'id, isArchived, isPinned, spaceId, templateId', + messages: 'id, conversationId, sender, [conversationId+sender]', + chatTemplates: 'id, isDefault', + + // ─── Picture (appId: 'picture') ─── + images: 'id, isFavorite, isPublic, archivedAt, prompt', + boards: 'id, isPublic', + boardItems: 'id, boardId, itemType, zIndex, [boardId+zIndex]', + pictureTags: 'id, name', + imageTags: 'id, imageId, tagId, [imageId+tagId]', + + // ─── Cards (appId: 'cards') ─── + cardDecks: 'id, isPublic', + cards: 'id, deckId, difficulty, nextReview, order, [deckId+order]', + + // ─── Zitare (appId: 'zitare') ─── + zitareFavorites: 'id, quoteId', + zitareLists: 'id', + + // ─── Clock (appId: 'clock') ─── + alarms: 'id, enabled, time', + timers: 'id, status', + worldClocks: 'id, sortOrder, timezone', + + // ─── Mukke (appId: 'mukke') ─── + songs: 'id, artist, album, genre, favorite, title', + mukkePlaylists: 'id, name', + playlistSongs: 'id, playlistId, songId, sortOrder, [playlistId+sortOrder]', + mukkeProjects: 'id, title, songId', + markers: 'id, beatId, type, sortOrder', + + // ─── Storage (appId: 'storage') ─── + files: 'id, parentFolderId, mimeType, isFavorite, isDeleted, name', + storageFolders: 'id, parentFolderId, path, depth, isFavorite, isDeleted', + storageTags: 'id, name', + fileTags: 'id, fileId, tagId, [fileId+tagId]', + + // ─── Presi (appId: 'presi') ─── + presiDecks: 'id, isPublic', + slides: 'id, deckId, order, [deckId+order]', + + // ─── Inventar (appId: 'inventar') ─── + invCollections: 'id, order, templateId', + invItems: 'id, collectionId, locationId, categoryId, status, name, [collectionId+order]', + invLocations: 'id, parentId, path, depth, order', + invCategories: 'id, parentId, order', + + // ─── Photos (appId: 'photos') ─── + albums: 'id, isAutoGenerated, name', + albumItems: 'id, albumId, mediaId, sortOrder, [albumId+sortOrder]', + photoFavorites: 'id, mediaId', + photoTags: 'id, name', + photoMediaTags: 'id, mediaId, tagId, [mediaId+tagId]', + + // ─── SkillTree (appId: 'skilltree') ─── + skills: 'id, branch, parentId, level', + activities: 'id, skillId, timestamp', + achievements: 'id, key, unlockedAt', + + // ─── CityCorners (appId: 'citycorners') ─── + cities: 'id, slug, country, name', + ccLocations: 'id, cityId, category, name', + ccFavorites: 'id, locationId', + + // ─── Times (appId: 'times') ─── + timeClients: 'id, order, isArchived, shortCode', + timeProjects: 'id, clientId, isArchived, isBillable, guildId, visibility, order', + timeEntries: + 'id, projectId, clientId, date, isRunning, [date+projectId], [date+clientId], guildId, visibility', + timeTags: 'id, name, order', + timeTemplates: 'id, usageCount, lastUsedAt, projectId', + timeSettings: 'id', + + // ─── Context (appId: 'context') ─── + contextSpaces: 'id, pinned, prefix', + documents: 'id, spaceId, type, pinned, title, [spaceId+type]', + + // ─── Questions (appId: 'questions') ─── + qCollections: 'id, sortOrder, isDefault', + questions: 'id, collectionId, status, priority, [collectionId+status]', + answers: 'id, questionId, isAccepted', + + // ─── NutriPhi (appId: 'nutriphi') ─── + meals: 'id, date, mealType, [date+mealType]', + goals: 'id', + nutriFavorites: 'id, mealType, usageCount', + + // ─── Planta (appId: 'planta') ─── + plants: 'id, isActive, healthStatus', + plantPhotos: 'id, plantId, isPrimary, [plantId+isPrimary]', + wateringSchedules: 'id, plantId, nextWateringAt', + wateringLogs: 'id, plantId, wateredAt', + + // ─── uLoad (appId: 'uload') ─── + links: 'id, shortCode, isActive, folderId, order, clickCount, [folderId+order], [isActive+order]', + uloadTags: 'id, slug, name', + uloadFolders: 'id, order', + linkTags: 'id, linkId, tagId, [linkId+tagId]', + + // ─── Calc (appId: 'calc') ─── + calculations: 'id, mode', + savedFormulas: 'id, mode, name', + + // ─── Moodlit (appId: 'moodlit') ─── + moods: 'id, name, animation, isDefault', + sequences: 'id, name', + + // ─── Memoro (appId: 'memoro') ─── + memos: 'id, processingStatus, isArchived, isPinned, language, [isArchived+createdAt]', + memories: 'id, memoId', + memoroTags: 'id, name, sortOrder', + memoTags: 'id, memoId, tagId', + memoroSpaces: 'id, ownerId', + spaceMembers: 'id, spaceId, userId', + memoSpaces: 'id, memoId, spaceId', + + // ─── Guides (appId: 'guides') ─── + guides: 'id, category, difficulty, collectionId, tags', + sections: 'id, guideId, order', + steps: 'id, guideId, sectionId, order, [guideId+order]', + guideCollections: 'id', + runs: 'id, guideId, startedAt, completedAt', + + // ─── Playground (appId: 'playground') ─── + // No persistent data — stateless LLM playground + + // ─── Shared: Global Tags (appId: 'tags') ─── + globalTags: 'id, name, groupId', + tagGroups: 'id', + + // ─── Shared: Links (appId: 'links') ─── + manaLinks: 'id, sourceAppId, sourceRecordId, targetAppId, targetRecordId', +}); + +// ─── 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}. + +export const SYNC_APP_MAP: Record = { + manacore: ['userSettings', 'dashboardConfigs'], + todo: ['tasks', 'todoProjects', 'labels', 'taskLabels', 'reminders', 'boardViews'], + calendar: ['calendars', 'events'], + contacts: ['contacts'], + chat: ['conversations', 'messages', 'chatTemplates'], + picture: ['images', 'boards', 'boardItems', 'pictureTags', 'imageTags'], + cards: ['cardDecks', 'cards'], + zitare: ['zitareFavorites', 'zitareLists'], + clock: ['alarms', 'timers', 'worldClocks'], + mukke: ['songs', 'mukkePlaylists', 'playlistSongs', 'mukkeProjects', 'markers'], + storage: ['files', 'storageFolders', 'storageTags', 'fileTags'], + presi: ['presiDecks', 'slides'], + inventar: ['invCollections', 'invItems', 'invLocations', 'invCategories'], + photos: ['albums', 'albumItems', 'photoFavorites', 'photoTags', 'photoMediaTags'], + skilltree: ['skills', 'activities', 'achievements'], + citycorners: ['cities', 'ccLocations', 'ccFavorites'], + times: [ + 'timeClients', + 'timeProjects', + 'timeEntries', + 'timeTags', + 'timeTemplates', + 'timeSettings', + ], + context: ['contextSpaces', 'documents'], + questions: ['qCollections', 'questions', 'answers'], + nutriphi: ['meals', 'goals', 'nutriFavorites'], + planta: ['plants', 'plantPhotos', 'wateringSchedules', 'wateringLogs'], + uload: ['links', 'uloadTags', 'uloadFolders', 'linkTags'], + calc: ['calculations', 'savedFormulas'], + moodlit: ['moods', 'sequences'], + memoro: [ + 'memos', + 'memories', + 'memoroTags', + 'memoTags', + 'memoroSpaces', + 'spaceMembers', + 'memoSpaces', + ], + guides: ['guides', 'sections', 'steps', 'guideCollections', 'runs'], + tags: ['globalTags', 'tagGroups'], + links: ['manaLinks'], +}; + +// ─── Reverse Map: Table → AppId ──────────────────────────── +// Used by _pendingChanges to determine which appId to tag a change with. + +export const TABLE_TO_APP: Record = Object.fromEntries( + Object.entries(SYNC_APP_MAP).flatMap(([appId, tables]) => tables.map((table) => [table, appId])) +); diff --git a/apps/manacore/apps/web/src/lib/modules/calc/collections.ts b/apps/manacore/apps/web/src/lib/modules/calc/collections.ts new file mode 100644 index 000000000..616730aca --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calc/collections.ts @@ -0,0 +1,36 @@ +/** + * Calc module — collection accessors and guest seed data. + */ + +import { db } from '$lib/data/database'; +import type { LocalCalculation, LocalSavedFormula } from './types'; + +// ─── Collection Accessors ────────────────────────────────── + +export const calculationTable = db.table('calculations'); +export const savedFormulaTable = db.table('savedFormulas'); + +// ─── Guest Seed ──────────────────────────────────────────── + +export const CALC_GUEST_SEED = { + calculations: [ + { + id: 'calc-demo-1', + mode: 'standard' as const, + expression: '42 * 23', + result: '966', + }, + { + id: 'calc-demo-2', + mode: 'scientific' as const, + expression: 'sin(π/4)', + result: '0.7071067812', + }, + { + id: 'calc-demo-3', + mode: 'standard' as const, + expression: '1024 / 8', + result: '128', + }, + ], +}; diff --git a/apps/manacore/apps/web/src/lib/modules/calc/components/CasioSkin.svelte b/apps/manacore/apps/web/src/lib/modules/calc/components/CasioSkin.svelte new file mode 100644 index 000000000..fd6209bcc --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calc/components/CasioSkin.svelte @@ -0,0 +1,284 @@ + + +
+
+ +
+ CASIO + fx-82 +
+ + +
+ {#each Array(8) as _} +
+ {/each} +
+ + +
+
{expression || ' '}
+
+
+ {error || display} +
+ {#if display !== '0' && !error} + + {/if} +
+
+ + +
+ {#each buttons as row} + {#each row as btn} + + {/each} + {/each} +
+ + + + + +
+
+ + diff --git a/apps/manacore/apps/web/src/lib/modules/calc/components/HP35Skin.svelte b/apps/manacore/apps/web/src/lib/modules/calc/components/HP35Skin.svelte new file mode 100644 index 000000000..7ab74a669 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calc/components/HP35Skin.svelte @@ -0,0 +1,263 @@ + + +
+ +
+ + + + +
+
{expression || ' '}
+
+
+ {error || display} +
+ {#if display !== '0' && !error} + + {/if} +
+
+ + +
+ {#each buttons as row} + {#each row as btn} + + {/each} + {/each} +
+ + + + + + +
+
+ + diff --git a/apps/manacore/apps/web/src/lib/modules/calc/components/MinimalSkin.svelte b/apps/manacore/apps/web/src/lib/modules/calc/components/MinimalSkin.svelte new file mode 100644 index 000000000..2a597fb6e --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calc/components/MinimalSkin.svelte @@ -0,0 +1,178 @@ + + +
+ +
+
{expression || ' '}
+
+
+ {error || display} +
+ {#if display !== '0' && !error} + + {/if} +
+
+ + +
+ {#each buttons as row} + {#each row as btn} + + {/each} + {/each} +
+ + +
+ + diff --git a/apps/manacore/apps/web/src/lib/modules/calc/components/ModernSkin.svelte b/apps/manacore/apps/web/src/lib/modules/calc/components/ModernSkin.svelte new file mode 100644 index 000000000..d1fb096d6 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calc/components/ModernSkin.svelte @@ -0,0 +1,84 @@ + + +
+
+
+ {expression || ' '} +
+
+
+ {error || display} +
+ {#if display !== '0' && !error} + + {/if} +
+
+ +
+ {#each buttons as row} + {#each row as btn} + + {/each} + {/each} +
+ + +
diff --git a/apps/manacore/apps/web/src/lib/modules/calc/components/TI84Skin.svelte b/apps/manacore/apps/web/src/lib/modules/calc/components/TI84Skin.svelte new file mode 100644 index 000000000..8ec007fce --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calc/components/TI84Skin.svelte @@ -0,0 +1,281 @@ + + +
+
+ +
+ TEXAS INSTRUMENTS + TI-84 Plus +
+ + +
+
+
{expression || ' '}
+
+
+ {error || display} +
+ {#if display !== '0' && !error} + + {/if} +
+
+
+ + +
+ + +
+ + +
+ {#each buttons as row} + {#each row as btn} + + {/each} + {/each} +
+ + +
+
+ + diff --git a/apps/manacore/apps/web/src/lib/modules/calc/components/index.ts b/apps/manacore/apps/web/src/lib/modules/calc/components/index.ts new file mode 100644 index 000000000..35d7614d2 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calc/components/index.ts @@ -0,0 +1,6 @@ +export { default as ModernSkin } from './ModernSkin.svelte'; +export { default as HP35Skin } from './HP35Skin.svelte'; +export { default as CasioSkin } from './CasioSkin.svelte'; +export { default as TI84Skin } from './TI84Skin.svelte'; +export { default as MinimalSkin } from './MinimalSkin.svelte'; +export type { CalcSkinProps } from './types'; diff --git a/apps/manacore/apps/web/src/lib/modules/calc/components/types.ts b/apps/manacore/apps/web/src/lib/modules/calc/components/types.ts new file mode 100644 index 000000000..770f0fa9f --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calc/components/types.ts @@ -0,0 +1,14 @@ +/** + * Shared interface for all calculator skin components. + */ +export interface CalcSkinProps { + expression: string; + display: string; + error: string; + copied: boolean; + onButton: (btn: string) => void; + onClear: () => void; + onBackspace: () => void; + onEquals: () => void; + onCopy: () => void; +} diff --git a/apps/manacore/apps/web/src/lib/modules/calc/engine/evaluate.ts b/apps/manacore/apps/web/src/lib/modules/calc/engine/evaluate.ts new file mode 100644 index 000000000..761e1b6e9 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calc/engine/evaluate.ts @@ -0,0 +1,261 @@ +/** + * Safe math expression evaluator. + * + * Supports: +, -, *, /, %, ^, parentheses, and scientific functions. + * Does NOT use eval() — parses manually for safety. + */ + +const FUNCTIONS: Record number> = { + sin: Math.sin, + cos: Math.cos, + tan: Math.tan, + asin: Math.asin, + acos: Math.acos, + atan: Math.atan, + sinh: Math.sinh, + cosh: Math.cosh, + tanh: Math.tanh, + log: Math.log10, + ln: Math.log, + sqrt: Math.sqrt, + cbrt: Math.cbrt, + abs: Math.abs, + ceil: Math.ceil, + floor: Math.floor, + round: Math.round, + exp: Math.exp, +}; + +const CONSTANTS: Record = { + pi: Math.PI, + PI: Math.PI, + π: Math.PI, + e: Math.E, + E: Math.E, + φ: 1.6180339887, + phi: 1.6180339887, +}; + +type Token = + | { type: 'number'; value: number } + | { type: 'op'; value: string } + | { type: 'func'; value: string } + | { type: 'paren'; value: '(' | ')' }; + +function tokenize(expr: string): Token[] { + const tokens: Token[] = []; + let i = 0; + const s = expr.replace(/\s+/g, ''); + + while (i < s.length) { + // Numbers (including decimals) + if (/[0-9.]/.test(s[i])) { + let num = ''; + while (i < s.length && /[0-9.eE]/.test(s[i])) { + num += s[i++]; + // Handle scientific notation sign + if ((s[i] === '+' || s[i] === '-') && /[eE]/.test(s[i - 1])) { + num += s[i++]; + } + } + tokens.push({ type: 'number', value: parseFloat(num) }); + continue; + } + + // Parentheses + if (s[i] === '(' || s[i] === ')') { + tokens.push({ type: 'paren', value: s[i] as '(' | ')' }); + i++; + continue; + } + + // Operators + if ('+-*/%^'.includes(s[i])) { + // Handle unary minus + if ( + s[i] === '-' && + (tokens.length === 0 || + tokens[tokens.length - 1].type === 'op' || + (tokens[tokens.length - 1].type === 'paren' && tokens[tokens.length - 1].value === '(')) + ) { + let num = '-'; + i++; + while (i < s.length && /[0-9.eE]/.test(s[i])) { + num += s[i++]; + } + if (num.length > 1) { + tokens.push({ type: 'number', value: parseFloat(num) }); + continue; + } + // It's just a minus, push as operator + tokens.push({ type: 'op', value: '-' }); + continue; + } + tokens.push({ type: 'op', value: s[i] }); + i++; + continue; + } + + // Special characters (π, etc.) + if (s[i] === 'π' || s[i] === 'φ') { + tokens.push({ type: 'number', value: CONSTANTS[s[i]] }); + i++; + continue; + } + + // Functions and constants (letters) + if (/[a-zA-Z_]/.test(s[i])) { + let name = ''; + while (i < s.length && /[a-zA-Z_0-9]/.test(s[i])) { + name += s[i++]; + } + if (CONSTANTS[name] !== undefined) { + tokens.push({ type: 'number', value: CONSTANTS[name] }); + } else if (FUNCTIONS[name]) { + tokens.push({ type: 'func', value: name }); + } else { + throw new Error(`Unknown: ${name}`); + } + continue; + } + + // Factorial + if (s[i] === '!') { + tokens.push({ type: 'op', value: '!' }); + i++; + continue; + } + + throw new Error(`Unexpected character: ${s[i]}`); + } + + return tokens; +} + +function precedence(op: string): number { + if (op === '+' || op === '-') return 1; + if (op === '*' || op === '/' || op === '%') return 2; + if (op === '^') return 3; + return 0; +} + +function factorial(n: number): number { + if (n < 0 || !Number.isInteger(n)) throw new Error('Factorial of non-integer'); + if (n > 170) return Infinity; + let result = 1; + for (let i = 2; i <= n; i++) result *= i; + return result; +} + +function applyOp(op: string, a: number, b: number): number { + switch (op) { + case '+': + return a + b; + case '-': + return a - b; + case '*': + return a * b; + case '/': + if (b === 0) throw new Error('Division by zero'); + return a / b; + case '%': + return a % b; + case '^': + return Math.pow(a, b); + default: + throw new Error(`Unknown op: ${op}`); + } +} + +/** + * Evaluate a mathematical expression string. + * Returns the numeric result or throws on error. + */ +export function evaluate(expression: string): number { + const tokens = tokenize(expression); + const output: number[] = []; + const ops: Token[] = []; + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i]; + + if (token.type === 'number') { + output.push(token.value); + } else if (token.type === 'func') { + ops.push(token); + } else if (token.type === 'op') { + if (token.value === '!') { + const val = output.pop(); + if (val === undefined) throw new Error('Missing operand'); + output.push(factorial(val)); + } else { + while ( + ops.length > 0 && + ops[ops.length - 1].type === 'op' && + precedence(ops[ops.length - 1].value as string) >= precedence(token.value) + ) { + const op = ops.pop()!; + const b = output.pop()!; + const a = output.pop()!; + output.push(applyOp(op.value as string, a, b)); + } + ops.push(token); + } + } else if (token.type === 'paren' && token.value === '(') { + ops.push(token); + } else if (token.type === 'paren' && token.value === ')') { + while ( + ops.length > 0 && + !(ops[ops.length - 1].type === 'paren' && ops[ops.length - 1].value === '(') + ) { + const op = ops.pop()!; + const b = output.pop()!; + const a = output.pop()!; + output.push(applyOp(op.value as string, a, b)); + } + ops.pop(); // remove '(' + // If there's a function on the stack, apply it + if (ops.length > 0 && ops[ops.length - 1].type === 'func') { + const func = ops.pop()!; + const val = output.pop()!; + output.push(FUNCTIONS[func.value as string](val)); + } + } + } + + while (ops.length > 0) { + const op = ops.pop()!; + const b = output.pop()!; + const a = output.pop()!; + output.push(applyOp(op.value as string, a, b)); + } + + if (output.length !== 1) throw new Error('Invalid expression'); + return output[0]; +} + +/** + * Format a number for display — removes trailing zeros, handles very large/small numbers. + */ +export function formatResult(value: number, precision: number = 10): string { + if (!isFinite(value)) return value > 0 ? '∞' : '-∞'; + if (isNaN(value)) return 'NaN'; + + // Use scientific notation for very large/small numbers + if (Math.abs(value) > 1e15 || (Math.abs(value) < 1e-10 && value !== 0)) { + return value.toExponential(precision - 1); + } + + // Round to precision and strip trailing zeros + const result = parseFloat(value.toPrecision(precision)); + return String(result); +} + +/** + * Convert between number bases. + */ +export function convertBase(value: string, fromBase: number, toBase: number): string { + const decimal = parseInt(value, fromBase); + if (isNaN(decimal)) throw new Error('Invalid number'); + return decimal.toString(toBase).toUpperCase(); +} diff --git a/apps/manacore/apps/web/src/lib/modules/calc/index.ts b/apps/manacore/apps/web/src/lib/modules/calc/index.ts new file mode 100644 index 000000000..c72c9424b --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calc/index.ts @@ -0,0 +1,9 @@ +/** + * Calc module — barrel exports. + */ + +export { calculationsStore } from './stores/calculations.svelte'; +export { savedFormulasStore } from './stores/saved-formulas.svelte'; +export { useAllCalculations, useAllSavedFormulas, toCalculation, toSavedFormula } from './queries'; +export { calculationTable, savedFormulaTable, CALC_GUEST_SEED } from './collections'; +export type { LocalCalculation, LocalSavedFormula } from './types'; diff --git a/apps/manacore/apps/web/src/lib/modules/calc/queries.ts b/apps/manacore/apps/web/src/lib/modules/calc/queries.ts new file mode 100644 index 000000000..c7c71d40b --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calc/queries.ts @@ -0,0 +1,56 @@ +/** + * Reactive queries for Calc — uses Dexie liveQuery on the unified DB. + */ + +import { liveQuery } from 'dexie'; +import { db } from '$lib/data/database'; +import type { LocalCalculation, LocalSavedFormula } from './types'; +import type { Calculation, SavedFormula } from '@calc/shared'; + +// ─── Type Converters ─────────────────────────────────────── + +export function toCalculation(local: LocalCalculation): Calculation { + return { + id: local.id, + userId: 'local', + mode: local.mode, + expression: local.expression, + result: local.result, + skin: local.skin, + createdAt: local.createdAt ?? new Date().toISOString(), + }; +} + +export function toSavedFormula(local: LocalSavedFormula): SavedFormula { + return { + id: local.id, + userId: 'local', + name: local.name, + expression: local.expression, + description: local.description ?? undefined, + mode: local.mode, + createdAt: local.createdAt ?? new Date().toISOString(), + updatedAt: local.updatedAt ?? new Date().toISOString(), + }; +} + +// ─── Live Queries ────────────────────────────────────────── + +/** All calculations (history), newest first. */ +export function useAllCalculations() { + return liveQuery(async () => { + const locals = await db.table('calculations').toArray(); + return locals + .filter((c) => !c.deletedAt) + .map(toCalculation) + .reverse(); + }); +} + +/** All saved formulas. */ +export function useAllSavedFormulas() { + return liveQuery(async () => { + const locals = await db.table('savedFormulas').toArray(); + return locals.filter((f) => !f.deletedAt).map(toSavedFormula); + }); +} diff --git a/apps/manacore/apps/web/src/lib/modules/calc/stores/calculations.svelte.ts b/apps/manacore/apps/web/src/lib/modules/calc/stores/calculations.svelte.ts new file mode 100644 index 000000000..3033368dd --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calc/stores/calculations.svelte.ts @@ -0,0 +1,37 @@ +/** + * Calculation mutation store — write operations for the unified DB. + */ + +import { db } from '$lib/data/database'; +import type { LocalCalculation } from '../types'; +import type { CreateCalculationInput } from '@calc/shared'; + +export const calculationsStore = { + async addCalculation(input: CreateCalculationInput) { + await db.table('calculations').add({ + id: crypto.randomUUID(), + mode: input.mode, + expression: input.expression, + result: input.result, + skin: input.skin, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + }, + + async deleteCalculation(id: string) { + await db.table('calculations').update(id, { + deletedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + }, + + async clearHistory() { + const now = new Date().toISOString(); + const all = await db.table('calculations').toArray(); + const active = all.filter((c) => !c.deletedAt); + await Promise.all( + active.map((c) => db.table('calculations').update(c.id, { deletedAt: now, updatedAt: now })) + ); + }, +}; diff --git a/apps/manacore/apps/web/src/lib/modules/calc/stores/saved-formulas.svelte.ts b/apps/manacore/apps/web/src/lib/modules/calc/stores/saved-formulas.svelte.ts new file mode 100644 index 000000000..58fcc2db0 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calc/stores/saved-formulas.svelte.ts @@ -0,0 +1,35 @@ +/** + * Saved formula mutation store — write operations for the unified DB. + */ + +import { db } from '$lib/data/database'; +import type { LocalSavedFormula } from '../types'; +import type { CreateFormulaInput, UpdateFormulaInput } from '@calc/shared'; + +export const savedFormulasStore = { + async saveFormula(input: CreateFormulaInput) { + await db.table('savedFormulas').add({ + id: crypto.randomUUID(), + name: input.name, + expression: input.expression, + description: input.description ?? null, + mode: input.mode, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + }, + + async updateFormula(id: string, input: UpdateFormulaInput) { + await db.table('savedFormulas').update(id, { + ...input, + updatedAt: new Date().toISOString(), + }); + }, + + async deleteFormula(id: string) { + await db.table('savedFormulas').update(id, { + deletedAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); + }, +}; diff --git a/apps/manacore/apps/web/src/lib/modules/calc/types.ts b/apps/manacore/apps/web/src/lib/modules/calc/types.ts new file mode 100644 index 000000000..b0d16f730 --- /dev/null +++ b/apps/manacore/apps/web/src/lib/modules/calc/types.ts @@ -0,0 +1,20 @@ +/** + * Calc module types for the unified app. + */ + +import type { BaseRecord } from '@manacore/local-store'; +import type { CalculatorMode, CalculatorSkin } from '@calc/shared'; + +export interface LocalCalculation extends BaseRecord { + mode: CalculatorMode; + expression: string; + result: string; + skin?: CalculatorSkin; +} + +export interface LocalSavedFormula extends BaseRecord { + name: string; + expression: string; + description: string | null; + mode: CalculatorMode; +} diff --git a/apps/manacore/apps/web/src/routes/(app)/calc/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/calc/+page.svelte new file mode 100644 index 000000000..feb4827e0 --- /dev/null +++ b/apps/manacore/apps/web/src/routes/(app)/calc/+page.svelte @@ -0,0 +1,117 @@ + + + + Calc - ManaCore + + +
+
+

Calc

+

Dein Taschenrechner-Hub

+
+ + +
+
+
+ +
+
+
0
+
Wähle einen Rechner-Modus
+
+
+
+ + +
+ {#each quickLinks as link} + +
+
+ +
+
+
{link.label}
+
{link.description}
+
+
+
+ {/each} +
+
diff --git a/apps/manacore/apps/web/src/routes/(app)/calc/standard/+page.svelte b/apps/manacore/apps/web/src/routes/(app)/calc/standard/+page.svelte new file mode 100644 index 000000000..6a7509f2f --- /dev/null +++ b/apps/manacore/apps/web/src/routes/(app)/calc/standard/+page.svelte @@ -0,0 +1,267 @@ + + + + Calc - Standard | ManaCore + + + + +
+
+ +
+ +
+ + + {#if showSkinPicker} +
+
+ {#each CALCULATOR_SKINS as skin} + + {/each} +
+
+ {/if} + + + {#if activeSkin === 'modern'} + + {:else if activeSkin === 'hp35'} + + {:else if activeSkin === 'casio-fx'} + + {:else if activeSkin === 'ti84'} + + {:else if activeSkin === 'minimal'} + + {/if} +
+ + +
+

Verlauf

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

Noch keine Berechnungen

+ {:else} +
+ {#each recentHistory as calc} + + {/each} +
+ + {/if} +
+
+ +