@mrck-labs/vanaheim-shared 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # @mrck-labs/vanaheim-shared
2
2
 
3
- Shared types, constants, and utilities for Vanaheim apps.
3
+ > Shared types, constants, utilities, and state management for the Vanaheim ecosystem
4
+
5
+ This package provides common code used by both **Vanaheim Desktop** (Electron) and **MobileHorizon** (React Native/Expo).
4
6
 
5
7
  ## Installation
6
8
 
@@ -8,22 +10,172 @@ Shared types, constants, and utilities for Vanaheim apps.
8
10
  npm install @mrck-labs/vanaheim-shared
9
11
  ```
10
12
 
11
- ## Usage
13
+ **Peer Dependencies:**
14
+
15
+ - `jotai` (optional) - Required only if using the `/atoms` module
16
+
17
+ ## Modules
18
+
19
+ ### Types (`/types`)
20
+
21
+ Database entity types matching the Supabase schema:
22
+
23
+ ```typescript
24
+ import type {
25
+ // Focus
26
+ FocusSession,
27
+ NewFocusSession,
28
+ FocusCategory,
29
+ FocusSessionFilters,
30
+
31
+ // Expenses & Income
32
+ Expense,
33
+ NewExpense,
34
+ ExpenseCategory,
35
+ Income,
36
+ NewIncome,
37
+ IncomeCategory,
38
+
39
+ // Health
40
+ HealthHabit,
41
+ HealthCategory,
42
+ HealthCompletion,
43
+
44
+ // And more...
45
+ } from "@mrck-labs/vanaheim-shared/types";
46
+ ```
47
+
48
+ ### Constants (`/constants`)
49
+
50
+ Shared constants and enums:
51
+
52
+ ```typescript
53
+ import {
54
+ CURRENCIES, // ['CHF', 'USD', 'EUR', 'PLN']
55
+ CURRENCY_SYMBOLS, // { CHF: 'CHF', USD: '$', ... }
56
+ FREQUENCIES, // ['monthly', 'yearly', '6-monthly', ...]
57
+ DEFAULT_FOCUS_DURATIONS, // [15, 25, 30, 45, 60, 90]
58
+ FOCUS_STATUS, // ['active', 'completed', 'abandoned']
59
+ SETTING_KEYS,
60
+ API_URLS,
61
+ } from "@mrck-labs/vanaheim-shared/constants";
62
+ ```
63
+
64
+ ### Utilities (`/utils`)
65
+
66
+ Common utility functions:
67
+
68
+ ```typescript
69
+ import {
70
+ formatTime, // (seconds) => "02:05"
71
+ formatTotalTime, // (seconds) => "1h 30m"
72
+ formatCurrency, // (amount, currency) => "CHF 1'234.56"
73
+ formatRelativeTime, // (dateStr) => "2h ago"
74
+ generateId, // () => UUID v4
75
+ truncate, // (str, maxLength) => "Hello..."
76
+ } from "@mrck-labs/vanaheim-shared/utils";
77
+ ```
78
+
79
+ ### Query Keys (`/query`)
12
80
 
13
- ### Main Export
81
+ Centralized TanStack Query cache key factory:
14
82
 
15
83
  ```typescript
16
- import /* your exports */ "@mrck-labs/vanaheim-shared";
84
+ import { queryKeys } from "@mrck-labs/vanaheim-shared/query";
85
+
86
+ // In query hooks
87
+ useQuery({ queryKey: queryKeys.focusSessions.today() });
88
+ useQuery({ queryKey: queryKeys.expenses.list({ isActive: true }) });
89
+
90
+ // Invalidating
91
+ queryClient.invalidateQueries({ queryKey: queryKeys.focusSessions.all });
92
+ ```
93
+
94
+ **Covered modules:** `expenses`, `expenseCategories`, `expensePayments`, `incomes`, `incomeCategories`, `incomePayments`, `exchangeRates`, `focusSessions`, `focusCategories`, `settings`, `lieuDays`, `efLinks`, `bankConnections`, `bankTransactions`, `healthCategories`, `healthHabits`, `healthCompletions`, `healthStats`, `promptCategories`, `promptLabels`, `prompts`, `trainingActivities`, `trainingSessions`, `plannedSessions`, `races`, `trainingStats`, `strava`, `journalEntries`, `journalData`, `chatConversations`, `chatMessages`, `googleCalendar`, `linear`
95
+
96
+ ### Focus Atoms (`/atoms`)
97
+
98
+ Jotai atoms for focus timer state management:
99
+
100
+ ```typescript
101
+ import {
102
+ // Types
103
+ type TimerStatus, // 'idle' | 'running' | 'paused' | 'completed'
104
+ type ActiveSession,
105
+
106
+ // Core atoms
107
+ timerStatusAtom,
108
+ targetSecondsAtom,
109
+ elapsedSecondsAtom,
110
+ activeSessionAtom,
111
+
112
+ // Derived atoms
113
+ remainingSecondsAtom,
114
+ progressAtom, // 0 to 1
115
+ progressPercentAtom, // 0 to 100
116
+ formattedRemainingAtom, // "24:59"
117
+ formattedElapsedAtom, // "00:01"
118
+ } from "@mrck-labs/vanaheim-shared/atoms";
119
+ ```
120
+
121
+ ### Date Utilities (`/date`)
122
+
123
+ Comprehensive date manipulation:
124
+
125
+ ```typescript
126
+ import {
127
+ // Core
128
+ getTodayString, // "2024-12-16"
129
+ parseLocalDate, // Avoids timezone issues
130
+ formatDateString, // Date => "2024-12-16"
131
+
132
+ // Checks
133
+ isToday,
134
+ isOverdue,
135
+ isDueSoon, // Within N days
136
+
137
+ // Arithmetic
138
+ addDays,
139
+ subtractDays,
140
+ getYesterdayString,
141
+ getTomorrowString,
142
+
143
+ // ISO helpers (for DB queries)
144
+ getStartOfDayISO,
145
+ getEndOfDayISO,
146
+
147
+ // Week utilities
148
+ getWeekStartString,
149
+ getWeekEndString,
150
+ formatWeekRange,
151
+
152
+ // Display formatting
153
+ formatFullDate, // "Monday, December 15, 2024"
154
+ formatDateHeader, // "Today" | "Tomorrow" | "Overdue"
155
+ formatRelativeDueDate, // "Due in 3 days"
156
+ } from "@mrck-labs/vanaheim-shared/date";
157
+ ```
158
+
159
+ ## Usage in Projects
160
+
161
+ ### Vanaheim Desktop
162
+
163
+ ```typescript
164
+ // src/lib/queryClient.ts
165
+ export { queryKeys } from '@mrck-labs/vanaheim-shared/query'
166
+
167
+ // src/atoms/focus.ts
168
+ export { timerStatusAtom, ... } from '@mrck-labs/vanaheim-shared/atoms'
17
169
  ```
18
170
 
19
- ### Subpath Exports
171
+ ### MobileHorizon
20
172
 
21
173
  ```typescript
22
- // Types only
23
- import /* types */ "@mrck-labs/vanaheim-shared/types";
174
+ // src/hooks/queries/useFocusQueries.ts
175
+ import { queryKeys } from '@mrck-labs/vanaheim-shared/query'
24
176
 
25
- // Utils only
26
- import /* utils */ "@mrck-labs/vanaheim-shared/utils";
177
+ // src/atoms/focus.ts
178
+ export { timerStatusAtom, ... } from '@mrck-labs/vanaheim-shared/atoms'
27
179
  ```
28
180
 
29
181
  ## Development
@@ -47,7 +199,7 @@ npm run clean
47
199
 
48
200
  ## Release
49
201
 
50
- This package uses [changesets](https://github.com/changesets/changesets) for version management and releases.
202
+ This package uses [changesets](https://github.com/changesets/changesets) for version management.
51
203
 
52
204
  See [RELEASE_GUIDE.md](./RELEASE_GUIDE.md) for detailed release instructions.
53
205
 
@@ -0,0 +1,92 @@
1
+ import * as jotai from 'jotai';
2
+
3
+ /**
4
+ * Focus Timer Atoms
5
+ *
6
+ * Shared Jotai atoms for focus timer state management.
7
+ * Used by both desktop and mobile apps.
8
+ *
9
+ * These atoms manage in-memory timer state that syncs with
10
+ * the database via TanStack Query.
11
+ *
12
+ * @example
13
+ * import { timerStatusAtom, targetSecondsAtom } from '@mrck-labs/vanaheim-shared/atoms'
14
+ * import { useAtom, useAtomValue } from 'jotai'
15
+ *
16
+ * const [status, setStatus] = useAtom(timerStatusAtom)
17
+ * const remaining = useAtomValue(remainingSecondsAtom)
18
+ */
19
+ /**
20
+ * Timer status values
21
+ */
22
+ type TimerStatus = 'idle' | 'running' | 'paused' | 'completed';
23
+ /**
24
+ * Active focus session info (in-memory during timer run)
25
+ * This is separate from the database FocusSession type
26
+ */
27
+ interface ActiveSession {
28
+ id: string;
29
+ title: string;
30
+ categoryId: string | null;
31
+ targetMinutes: number;
32
+ startedAt?: string;
33
+ }
34
+ /**
35
+ * Current timer status
36
+ * - idle: No timer running
37
+ * - running: Timer actively counting
38
+ * - paused: Timer paused
39
+ * - completed: Timer finished (target reached)
40
+ */
41
+ declare const timerStatusAtom: jotai.PrimitiveAtom<TimerStatus> & {
42
+ init: TimerStatus;
43
+ };
44
+ /**
45
+ * Target duration in seconds
46
+ * Default: 25 minutes (Pomodoro)
47
+ */
48
+ declare const targetSecondsAtom: jotai.PrimitiveAtom<number> & {
49
+ init: number;
50
+ };
51
+ /**
52
+ * Elapsed seconds since timer started
53
+ */
54
+ declare const elapsedSecondsAtom: jotai.PrimitiveAtom<number> & {
55
+ init: number;
56
+ };
57
+ /**
58
+ * Currently active session info (in-memory during timer run)
59
+ */
60
+ declare const activeSessionAtom: jotai.PrimitiveAtom<ActiveSession | null> & {
61
+ init: ActiveSession | null;
62
+ };
63
+ /**
64
+ * Remaining seconds (target - elapsed)
65
+ * Always returns 0 or positive
66
+ */
67
+ declare const remainingSecondsAtom: jotai.Atom<number>;
68
+ /**
69
+ * Progress as a ratio (0 to 1)
70
+ * 0 = just started, 1 = completed
71
+ *
72
+ * Note: Desktop previously used 0-100 (percentage).
73
+ * If you need percentage, multiply by 100.
74
+ */
75
+ declare const progressAtom: jotai.Atom<number>;
76
+ /**
77
+ * Formatted remaining time (MM:SS)
78
+ * @example "24:59", "00:30"
79
+ */
80
+ declare const formattedRemainingAtom: jotai.Atom<string>;
81
+ /**
82
+ * Formatted elapsed time (MM:SS)
83
+ * @example "00:01", "25:00"
84
+ */
85
+ declare const formattedElapsedAtom: jotai.Atom<string>;
86
+ /**
87
+ * Progress as percentage (0 to 100)
88
+ * Convenience atom for desktop compatibility
89
+ */
90
+ declare const progressPercentAtom: jotai.Atom<number>;
91
+
92
+ export { type ActiveSession, type TimerStatus, activeSessionAtom, elapsedSecondsAtom, formattedElapsedAtom, formattedRemainingAtom, progressAtom, progressPercentAtom, remainingSecondsAtom, targetSecondsAtom, timerStatusAtom };
@@ -0,0 +1,92 @@
1
+ import * as jotai from 'jotai';
2
+
3
+ /**
4
+ * Focus Timer Atoms
5
+ *
6
+ * Shared Jotai atoms for focus timer state management.
7
+ * Used by both desktop and mobile apps.
8
+ *
9
+ * These atoms manage in-memory timer state that syncs with
10
+ * the database via TanStack Query.
11
+ *
12
+ * @example
13
+ * import { timerStatusAtom, targetSecondsAtom } from '@mrck-labs/vanaheim-shared/atoms'
14
+ * import { useAtom, useAtomValue } from 'jotai'
15
+ *
16
+ * const [status, setStatus] = useAtom(timerStatusAtom)
17
+ * const remaining = useAtomValue(remainingSecondsAtom)
18
+ */
19
+ /**
20
+ * Timer status values
21
+ */
22
+ type TimerStatus = 'idle' | 'running' | 'paused' | 'completed';
23
+ /**
24
+ * Active focus session info (in-memory during timer run)
25
+ * This is separate from the database FocusSession type
26
+ */
27
+ interface ActiveSession {
28
+ id: string;
29
+ title: string;
30
+ categoryId: string | null;
31
+ targetMinutes: number;
32
+ startedAt?: string;
33
+ }
34
+ /**
35
+ * Current timer status
36
+ * - idle: No timer running
37
+ * - running: Timer actively counting
38
+ * - paused: Timer paused
39
+ * - completed: Timer finished (target reached)
40
+ */
41
+ declare const timerStatusAtom: jotai.PrimitiveAtom<TimerStatus> & {
42
+ init: TimerStatus;
43
+ };
44
+ /**
45
+ * Target duration in seconds
46
+ * Default: 25 minutes (Pomodoro)
47
+ */
48
+ declare const targetSecondsAtom: jotai.PrimitiveAtom<number> & {
49
+ init: number;
50
+ };
51
+ /**
52
+ * Elapsed seconds since timer started
53
+ */
54
+ declare const elapsedSecondsAtom: jotai.PrimitiveAtom<number> & {
55
+ init: number;
56
+ };
57
+ /**
58
+ * Currently active session info (in-memory during timer run)
59
+ */
60
+ declare const activeSessionAtom: jotai.PrimitiveAtom<ActiveSession | null> & {
61
+ init: ActiveSession | null;
62
+ };
63
+ /**
64
+ * Remaining seconds (target - elapsed)
65
+ * Always returns 0 or positive
66
+ */
67
+ declare const remainingSecondsAtom: jotai.Atom<number>;
68
+ /**
69
+ * Progress as a ratio (0 to 1)
70
+ * 0 = just started, 1 = completed
71
+ *
72
+ * Note: Desktop previously used 0-100 (percentage).
73
+ * If you need percentage, multiply by 100.
74
+ */
75
+ declare const progressAtom: jotai.Atom<number>;
76
+ /**
77
+ * Formatted remaining time (MM:SS)
78
+ * @example "24:59", "00:30"
79
+ */
80
+ declare const formattedRemainingAtom: jotai.Atom<string>;
81
+ /**
82
+ * Formatted elapsed time (MM:SS)
83
+ * @example "00:01", "25:00"
84
+ */
85
+ declare const formattedElapsedAtom: jotai.Atom<string>;
86
+ /**
87
+ * Progress as percentage (0 to 100)
88
+ * Convenience atom for desktop compatibility
89
+ */
90
+ declare const progressPercentAtom: jotai.Atom<number>;
91
+
92
+ export { type ActiveSession, type TimerStatus, activeSessionAtom, elapsedSecondsAtom, formattedElapsedAtom, formattedRemainingAtom, progressAtom, progressPercentAtom, remainingSecondsAtom, targetSecondsAtom, timerStatusAtom };
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/atoms/index.ts
21
+ var atoms_exports = {};
22
+ __export(atoms_exports, {
23
+ activeSessionAtom: () => activeSessionAtom,
24
+ elapsedSecondsAtom: () => elapsedSecondsAtom,
25
+ formattedElapsedAtom: () => formattedElapsedAtom,
26
+ formattedRemainingAtom: () => formattedRemainingAtom,
27
+ progressAtom: () => progressAtom,
28
+ progressPercentAtom: () => progressPercentAtom,
29
+ remainingSecondsAtom: () => remainingSecondsAtom,
30
+ targetSecondsAtom: () => targetSecondsAtom,
31
+ timerStatusAtom: () => timerStatusAtom
32
+ });
33
+ module.exports = __toCommonJS(atoms_exports);
34
+
35
+ // src/atoms/focus.ts
36
+ var import_jotai = require("jotai");
37
+
38
+ // src/utils/formatters.ts
39
+ function formatTime(seconds) {
40
+ const mins = Math.floor(seconds / 60);
41
+ const secs = seconds % 60;
42
+ return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
43
+ }
44
+
45
+ // src/atoms/focus.ts
46
+ var timerStatusAtom = (0, import_jotai.atom)("idle");
47
+ var targetSecondsAtom = (0, import_jotai.atom)(25 * 60);
48
+ var elapsedSecondsAtom = (0, import_jotai.atom)(0);
49
+ var activeSessionAtom = (0, import_jotai.atom)(null);
50
+ var remainingSecondsAtom = (0, import_jotai.atom)((get) => {
51
+ const target = get(targetSecondsAtom);
52
+ const elapsed = get(elapsedSecondsAtom);
53
+ return Math.max(0, target - elapsed);
54
+ });
55
+ var progressAtom = (0, import_jotai.atom)((get) => {
56
+ const target = get(targetSecondsAtom);
57
+ const elapsed = get(elapsedSecondsAtom);
58
+ if (target === 0) return 0;
59
+ return Math.min(1, elapsed / target);
60
+ });
61
+ var formattedRemainingAtom = (0, import_jotai.atom)((get) => {
62
+ return formatTime(get(remainingSecondsAtom));
63
+ });
64
+ var formattedElapsedAtom = (0, import_jotai.atom)((get) => {
65
+ return formatTime(get(elapsedSecondsAtom));
66
+ });
67
+ var progressPercentAtom = (0, import_jotai.atom)((get) => {
68
+ return get(progressAtom) * 100;
69
+ });
70
+ // Annotate the CommonJS export names for ESM import in node:
71
+ 0 && (module.exports = {
72
+ activeSessionAtom,
73
+ elapsedSecondsAtom,
74
+ formattedElapsedAtom,
75
+ formattedRemainingAtom,
76
+ progressAtom,
77
+ progressPercentAtom,
78
+ remainingSecondsAtom,
79
+ targetSecondsAtom,
80
+ timerStatusAtom
81
+ });
82
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/atoms/index.ts","../../src/atoms/focus.ts","../../src/utils/formatters.ts"],"sourcesContent":["/**\n * Atoms Index\n *\n * Re-exports all shared Jotai atoms.\n */\n\nexport * from './focus'\n\n","/**\n * Focus Timer Atoms\n *\n * Shared Jotai atoms for focus timer state management.\n * Used by both desktop and mobile apps.\n *\n * These atoms manage in-memory timer state that syncs with\n * the database via TanStack Query.\n *\n * @example\n * import { timerStatusAtom, targetSecondsAtom } from '@mrck-labs/vanaheim-shared/atoms'\n * import { useAtom, useAtomValue } from 'jotai'\n *\n * const [status, setStatus] = useAtom(timerStatusAtom)\n * const remaining = useAtomValue(remainingSecondsAtom)\n */\n\nimport { atom } from 'jotai'\nimport { formatTime } from '../utils/formatters'\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Timer status values\n */\nexport type TimerStatus = 'idle' | 'running' | 'paused' | 'completed'\n\n/**\n * Active focus session info (in-memory during timer run)\n * This is separate from the database FocusSession type\n */\nexport interface ActiveSession {\n id: string\n title: string\n categoryId: string | null\n targetMinutes: number\n startedAt?: string // Optional - used by mobile for elapsed calculation\n}\n\n// ============================================================================\n// Core Timer Atoms\n// ============================================================================\n\n/**\n * Current timer status\n * - idle: No timer running\n * - running: Timer actively counting\n * - paused: Timer paused\n * - completed: Timer finished (target reached)\n */\nexport const timerStatusAtom = atom<TimerStatus>('idle')\n\n/**\n * Target duration in seconds\n * Default: 25 minutes (Pomodoro)\n */\nexport const targetSecondsAtom = atom<number>(25 * 60)\n\n/**\n * Elapsed seconds since timer started\n */\nexport const elapsedSecondsAtom = atom<number>(0)\n\n/**\n * Currently active session info (in-memory during timer run)\n */\nexport const activeSessionAtom = atom<ActiveSession | null>(null)\n\n// ============================================================================\n// Derived Timer Atoms (computed, read-only)\n// ============================================================================\n\n/**\n * Remaining seconds (target - elapsed)\n * Always returns 0 or positive\n */\nexport const remainingSecondsAtom = atom((get) => {\n const target = get(targetSecondsAtom)\n const elapsed = get(elapsedSecondsAtom)\n return Math.max(0, target - elapsed)\n})\n\n/**\n * Progress as a ratio (0 to 1)\n * 0 = just started, 1 = completed\n *\n * Note: Desktop previously used 0-100 (percentage).\n * If you need percentage, multiply by 100.\n */\nexport const progressAtom = atom((get) => {\n const target = get(targetSecondsAtom)\n const elapsed = get(elapsedSecondsAtom)\n if (target === 0) return 0\n return Math.min(1, elapsed / target)\n})\n\n/**\n * Formatted remaining time (MM:SS)\n * @example \"24:59\", \"00:30\"\n */\nexport const formattedRemainingAtom = atom((get) => {\n return formatTime(get(remainingSecondsAtom))\n})\n\n/**\n * Formatted elapsed time (MM:SS)\n * @example \"00:01\", \"25:00\"\n */\nexport const formattedElapsedAtom = atom((get) => {\n return formatTime(get(elapsedSecondsAtom))\n})\n\n/**\n * Progress as percentage (0 to 100)\n * Convenience atom for desktop compatibility\n */\nexport const progressPercentAtom = atom((get) => {\n return get(progressAtom) * 100\n})\n\n","/**\n * Formatting Utilities\n *\n * Pure functions for formatting data.\n */\n\n/**\n * Format seconds into MM:SS format\n * @example formatTime(125) => \"02:05\"\n */\nexport function formatTime(seconds: number): string {\n const mins = Math.floor(seconds / 60)\n const secs = seconds % 60\n return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`\n}\n\n/**\n * Format seconds into human-readable total time\n * @example formatTotalTime(3700) => \"1h 1m\"\n * @example formatTotalTime(1800) => \"30m\"\n */\nexport function formatTotalTime(seconds: number): string {\n const hours = Math.floor(seconds / 3600)\n const mins = Math.floor((seconds % 3600) / 60)\n if (hours > 0) {\n return `${hours}h ${mins}m`\n }\n return `${mins}m`\n}\n\n/**\n * Format a number as currency\n * Uses Swiss German locale for authentic Swiss formatting (apostrophe as thousands separator)\n * @example formatCurrency(1234.56, 'CHF') => \"CHF 1'234.56\"\n */\nexport function formatCurrency(\n amount: number,\n currency: string,\n locale: string = 'de-CH'\n): string {\n return new Intl.NumberFormat(locale, {\n style: 'currency',\n currency,\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n }).format(amount)\n}\n\n/**\n * Format a date as relative time (e.g., \"2h ago\", \"3d ago\")\n */\nexport function formatRelativeTime(dateStr: string): string {\n const date = new Date(dateStr)\n const now = new Date()\n const diffMs = now.getTime() - date.getTime()\n const diffMins = Math.floor(diffMs / 60000)\n const diffHours = Math.floor(diffMins / 60)\n const diffDays = Math.floor(diffHours / 24)\n\n if (diffMins < 1) return 'Just now'\n if (diffMins < 60) return `${diffMins}m ago`\n if (diffHours < 24) return `${diffHours}h ago`\n if (diffDays < 7) return `${diffDays}d ago`\n return date.toLocaleDateString()\n}\n\n/**\n * Format a date string to a readable format\n * @example formatDate('2024-01-15') => \"Jan 15, 2024\"\n */\nexport function formatDate(\n dateStr: string,\n options: Intl.DateTimeFormatOptions = {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n }\n): string {\n return new Date(dateStr).toLocaleDateString('en-US', options)\n}\n\n/**\n * Format a due date with context (Today, Tomorrow, Overdue, etc.)\n */\nexport function formatDueDate(dueDate: string | null): {\n text: string\n isOverdue: boolean\n} {\n if (!dueDate) return { text: '', isOverdue: false }\n\n const due = new Date(dueDate)\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n\n const tomorrow = new Date(today)\n tomorrow.setDate(tomorrow.getDate() + 1)\n\n const dueDay = new Date(due)\n dueDay.setHours(0, 0, 0, 0)\n\n const isOverdue = dueDay < today\n\n if (dueDay.getTime() === today.getTime()) {\n return { text: 'Today', isOverdue: false }\n } else if (dueDay.getTime() === tomorrow.getTime()) {\n return { text: 'Tomorrow', isOverdue: false }\n } else if (isOverdue) {\n const daysAgo = Math.ceil(\n (today.getTime() - dueDay.getTime()) / (1000 * 60 * 60 * 24)\n )\n return { text: `${daysAgo}d overdue`, isOverdue: true }\n } else {\n return {\n text: due.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),\n isOverdue: false,\n }\n }\n}\n\n/**\n * Truncate a string with ellipsis\n * @example truncate(\"Hello World\", 5) => \"Hello...\"\n */\nexport function truncate(str: string, maxLength: number): string {\n if (str.length <= maxLength) return str\n return str.slice(0, maxLength) + '...'\n}\n\n/**\n * Extract repo name from GitHub URL\n * @example getRepoName(\"https://github.com/owner/repo\") => \"owner/repo\"\n */\nexport function getRepoName(url: string): string {\n const match = url.match(/github\\.com\\/(.+)$/)\n return match ? match[1] : url\n}\n\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACiBA,mBAAqB;;;ACPd,SAAS,WAAW,SAAyB;AAClD,QAAM,OAAO,KAAK,MAAM,UAAU,EAAE;AACpC,QAAM,OAAO,UAAU;AACvB,SAAO,GAAG,KAAK,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC;AAChF;;;ADsCO,IAAM,sBAAkB,mBAAkB,MAAM;AAMhD,IAAM,wBAAoB,mBAAa,KAAK,EAAE;AAK9C,IAAM,yBAAqB,mBAAa,CAAC;AAKzC,IAAM,wBAAoB,mBAA2B,IAAI;AAUzD,IAAM,2BAAuB,mBAAK,CAAC,QAAQ;AAChD,QAAM,SAAS,IAAI,iBAAiB;AACpC,QAAM,UAAU,IAAI,kBAAkB;AACtC,SAAO,KAAK,IAAI,GAAG,SAAS,OAAO;AACrC,CAAC;AASM,IAAM,mBAAe,mBAAK,CAAC,QAAQ;AACxC,QAAM,SAAS,IAAI,iBAAiB;AACpC,QAAM,UAAU,IAAI,kBAAkB;AACtC,MAAI,WAAW,EAAG,QAAO;AACzB,SAAO,KAAK,IAAI,GAAG,UAAU,MAAM;AACrC,CAAC;AAMM,IAAM,6BAAyB,mBAAK,CAAC,QAAQ;AAClD,SAAO,WAAW,IAAI,oBAAoB,CAAC;AAC7C,CAAC;AAMM,IAAM,2BAAuB,mBAAK,CAAC,QAAQ;AAChD,SAAO,WAAW,IAAI,kBAAkB,CAAC;AAC3C,CAAC;AAMM,IAAM,0BAAsB,mBAAK,CAAC,QAAQ;AAC/C,SAAO,IAAI,YAAY,IAAI;AAC7B,CAAC;","names":[]}
@@ -0,0 +1,47 @@
1
+ // src/atoms/focus.ts
2
+ import { atom } from "jotai";
3
+
4
+ // src/utils/formatters.ts
5
+ function formatTime(seconds) {
6
+ const mins = Math.floor(seconds / 60);
7
+ const secs = seconds % 60;
8
+ return `${mins.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
9
+ }
10
+
11
+ // src/atoms/focus.ts
12
+ var timerStatusAtom = atom("idle");
13
+ var targetSecondsAtom = atom(25 * 60);
14
+ var elapsedSecondsAtom = atom(0);
15
+ var activeSessionAtom = atom(null);
16
+ var remainingSecondsAtom = atom((get) => {
17
+ const target = get(targetSecondsAtom);
18
+ const elapsed = get(elapsedSecondsAtom);
19
+ return Math.max(0, target - elapsed);
20
+ });
21
+ var progressAtom = atom((get) => {
22
+ const target = get(targetSecondsAtom);
23
+ const elapsed = get(elapsedSecondsAtom);
24
+ if (target === 0) return 0;
25
+ return Math.min(1, elapsed / target);
26
+ });
27
+ var formattedRemainingAtom = atom((get) => {
28
+ return formatTime(get(remainingSecondsAtom));
29
+ });
30
+ var formattedElapsedAtom = atom((get) => {
31
+ return formatTime(get(elapsedSecondsAtom));
32
+ });
33
+ var progressPercentAtom = atom((get) => {
34
+ return get(progressAtom) * 100;
35
+ });
36
+ export {
37
+ activeSessionAtom,
38
+ elapsedSecondsAtom,
39
+ formattedElapsedAtom,
40
+ formattedRemainingAtom,
41
+ progressAtom,
42
+ progressPercentAtom,
43
+ remainingSecondsAtom,
44
+ targetSecondsAtom,
45
+ timerStatusAtom
46
+ };
47
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/atoms/focus.ts","../../src/utils/formatters.ts"],"sourcesContent":["/**\n * Focus Timer Atoms\n *\n * Shared Jotai atoms for focus timer state management.\n * Used by both desktop and mobile apps.\n *\n * These atoms manage in-memory timer state that syncs with\n * the database via TanStack Query.\n *\n * @example\n * import { timerStatusAtom, targetSecondsAtom } from '@mrck-labs/vanaheim-shared/atoms'\n * import { useAtom, useAtomValue } from 'jotai'\n *\n * const [status, setStatus] = useAtom(timerStatusAtom)\n * const remaining = useAtomValue(remainingSecondsAtom)\n */\n\nimport { atom } from 'jotai'\nimport { formatTime } from '../utils/formatters'\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Timer status values\n */\nexport type TimerStatus = 'idle' | 'running' | 'paused' | 'completed'\n\n/**\n * Active focus session info (in-memory during timer run)\n * This is separate from the database FocusSession type\n */\nexport interface ActiveSession {\n id: string\n title: string\n categoryId: string | null\n targetMinutes: number\n startedAt?: string // Optional - used by mobile for elapsed calculation\n}\n\n// ============================================================================\n// Core Timer Atoms\n// ============================================================================\n\n/**\n * Current timer status\n * - idle: No timer running\n * - running: Timer actively counting\n * - paused: Timer paused\n * - completed: Timer finished (target reached)\n */\nexport const timerStatusAtom = atom<TimerStatus>('idle')\n\n/**\n * Target duration in seconds\n * Default: 25 minutes (Pomodoro)\n */\nexport const targetSecondsAtom = atom<number>(25 * 60)\n\n/**\n * Elapsed seconds since timer started\n */\nexport const elapsedSecondsAtom = atom<number>(0)\n\n/**\n * Currently active session info (in-memory during timer run)\n */\nexport const activeSessionAtom = atom<ActiveSession | null>(null)\n\n// ============================================================================\n// Derived Timer Atoms (computed, read-only)\n// ============================================================================\n\n/**\n * Remaining seconds (target - elapsed)\n * Always returns 0 or positive\n */\nexport const remainingSecondsAtom = atom((get) => {\n const target = get(targetSecondsAtom)\n const elapsed = get(elapsedSecondsAtom)\n return Math.max(0, target - elapsed)\n})\n\n/**\n * Progress as a ratio (0 to 1)\n * 0 = just started, 1 = completed\n *\n * Note: Desktop previously used 0-100 (percentage).\n * If you need percentage, multiply by 100.\n */\nexport const progressAtom = atom((get) => {\n const target = get(targetSecondsAtom)\n const elapsed = get(elapsedSecondsAtom)\n if (target === 0) return 0\n return Math.min(1, elapsed / target)\n})\n\n/**\n * Formatted remaining time (MM:SS)\n * @example \"24:59\", \"00:30\"\n */\nexport const formattedRemainingAtom = atom((get) => {\n return formatTime(get(remainingSecondsAtom))\n})\n\n/**\n * Formatted elapsed time (MM:SS)\n * @example \"00:01\", \"25:00\"\n */\nexport const formattedElapsedAtom = atom((get) => {\n return formatTime(get(elapsedSecondsAtom))\n})\n\n/**\n * Progress as percentage (0 to 100)\n * Convenience atom for desktop compatibility\n */\nexport const progressPercentAtom = atom((get) => {\n return get(progressAtom) * 100\n})\n\n","/**\n * Formatting Utilities\n *\n * Pure functions for formatting data.\n */\n\n/**\n * Format seconds into MM:SS format\n * @example formatTime(125) => \"02:05\"\n */\nexport function formatTime(seconds: number): string {\n const mins = Math.floor(seconds / 60)\n const secs = seconds % 60\n return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`\n}\n\n/**\n * Format seconds into human-readable total time\n * @example formatTotalTime(3700) => \"1h 1m\"\n * @example formatTotalTime(1800) => \"30m\"\n */\nexport function formatTotalTime(seconds: number): string {\n const hours = Math.floor(seconds / 3600)\n const mins = Math.floor((seconds % 3600) / 60)\n if (hours > 0) {\n return `${hours}h ${mins}m`\n }\n return `${mins}m`\n}\n\n/**\n * Format a number as currency\n * Uses Swiss German locale for authentic Swiss formatting (apostrophe as thousands separator)\n * @example formatCurrency(1234.56, 'CHF') => \"CHF 1'234.56\"\n */\nexport function formatCurrency(\n amount: number,\n currency: string,\n locale: string = 'de-CH'\n): string {\n return new Intl.NumberFormat(locale, {\n style: 'currency',\n currency,\n minimumFractionDigits: 2,\n maximumFractionDigits: 2,\n }).format(amount)\n}\n\n/**\n * Format a date as relative time (e.g., \"2h ago\", \"3d ago\")\n */\nexport function formatRelativeTime(dateStr: string): string {\n const date = new Date(dateStr)\n const now = new Date()\n const diffMs = now.getTime() - date.getTime()\n const diffMins = Math.floor(diffMs / 60000)\n const diffHours = Math.floor(diffMins / 60)\n const diffDays = Math.floor(diffHours / 24)\n\n if (diffMins < 1) return 'Just now'\n if (diffMins < 60) return `${diffMins}m ago`\n if (diffHours < 24) return `${diffHours}h ago`\n if (diffDays < 7) return `${diffDays}d ago`\n return date.toLocaleDateString()\n}\n\n/**\n * Format a date string to a readable format\n * @example formatDate('2024-01-15') => \"Jan 15, 2024\"\n */\nexport function formatDate(\n dateStr: string,\n options: Intl.DateTimeFormatOptions = {\n month: 'short',\n day: 'numeric',\n year: 'numeric',\n }\n): string {\n return new Date(dateStr).toLocaleDateString('en-US', options)\n}\n\n/**\n * Format a due date with context (Today, Tomorrow, Overdue, etc.)\n */\nexport function formatDueDate(dueDate: string | null): {\n text: string\n isOverdue: boolean\n} {\n if (!dueDate) return { text: '', isOverdue: false }\n\n const due = new Date(dueDate)\n const today = new Date()\n today.setHours(0, 0, 0, 0)\n\n const tomorrow = new Date(today)\n tomorrow.setDate(tomorrow.getDate() + 1)\n\n const dueDay = new Date(due)\n dueDay.setHours(0, 0, 0, 0)\n\n const isOverdue = dueDay < today\n\n if (dueDay.getTime() === today.getTime()) {\n return { text: 'Today', isOverdue: false }\n } else if (dueDay.getTime() === tomorrow.getTime()) {\n return { text: 'Tomorrow', isOverdue: false }\n } else if (isOverdue) {\n const daysAgo = Math.ceil(\n (today.getTime() - dueDay.getTime()) / (1000 * 60 * 60 * 24)\n )\n return { text: `${daysAgo}d overdue`, isOverdue: true }\n } else {\n return {\n text: due.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),\n isOverdue: false,\n }\n }\n}\n\n/**\n * Truncate a string with ellipsis\n * @example truncate(\"Hello World\", 5) => \"Hello...\"\n */\nexport function truncate(str: string, maxLength: number): string {\n if (str.length <= maxLength) return str\n return str.slice(0, maxLength) + '...'\n}\n\n/**\n * Extract repo name from GitHub URL\n * @example getRepoName(\"https://github.com/owner/repo\") => \"owner/repo\"\n */\nexport function getRepoName(url: string): string {\n const match = url.match(/github\\.com\\/(.+)$/)\n return match ? match[1] : url\n}\n\n"],"mappings":";AAiBA,SAAS,YAAY;;;ACPd,SAAS,WAAW,SAAyB;AAClD,QAAM,OAAO,KAAK,MAAM,UAAU,EAAE;AACpC,QAAM,OAAO,UAAU;AACvB,SAAO,GAAG,KAAK,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC,IAAI,KAAK,SAAS,EAAE,SAAS,GAAG,GAAG,CAAC;AAChF;;;ADsCO,IAAM,kBAAkB,KAAkB,MAAM;AAMhD,IAAM,oBAAoB,KAAa,KAAK,EAAE;AAK9C,IAAM,qBAAqB,KAAa,CAAC;AAKzC,IAAM,oBAAoB,KAA2B,IAAI;AAUzD,IAAM,uBAAuB,KAAK,CAAC,QAAQ;AAChD,QAAM,SAAS,IAAI,iBAAiB;AACpC,QAAM,UAAU,IAAI,kBAAkB;AACtC,SAAO,KAAK,IAAI,GAAG,SAAS,OAAO;AACrC,CAAC;AASM,IAAM,eAAe,KAAK,CAAC,QAAQ;AACxC,QAAM,SAAS,IAAI,iBAAiB;AACpC,QAAM,UAAU,IAAI,kBAAkB;AACtC,MAAI,WAAW,EAAG,QAAO;AACzB,SAAO,KAAK,IAAI,GAAG,UAAU,MAAM;AACrC,CAAC;AAMM,IAAM,yBAAyB,KAAK,CAAC,QAAQ;AAClD,SAAO,WAAW,IAAI,oBAAoB,CAAC;AAC7C,CAAC;AAMM,IAAM,uBAAuB,KAAK,CAAC,QAAQ;AAChD,SAAO,WAAW,IAAI,kBAAkB,CAAC;AAC3C,CAAC;AAMM,IAAM,sBAAsB,KAAK,CAAC,QAAQ;AAC/C,SAAO,IAAI,YAAY,IAAI;AAC7B,CAAC;","names":[]}