@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 +162 -10
- package/dist/atoms/index.d.mts +92 -0
- package/dist/atoms/index.d.ts +92 -0
- package/dist/atoms/index.js +82 -0
- package/dist/atoms/index.js.map +1 -0
- package/dist/atoms/index.mjs +47 -0
- package/dist/atoms/index.mjs.map +1 -0
- package/dist/date/index.d.mts +176 -0
- package/dist/date/index.d.ts +176 -0
- package/dist/date/index.js +347 -0
- package/dist/date/index.js.map +1 -0
- package/dist/date/index.mjs +290 -0
- package/dist/date/index.mjs.map +1 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +600 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +557 -3
- package/dist/index.mjs.map +1 -1
- package/dist/query/index.d.mts +321 -0
- package/dist/query/index.d.ts +321 -0
- package/dist/query/index.js +257 -0
- package/dist/query/index.js.map +1 -0
- package/dist/query/index.mjs +232 -0
- package/dist/query/index.mjs.map +1 -0
- package/dist/utils/index.d.mts +2 -1
- package/dist/utils/index.d.ts +2 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/index.mjs +1 -1
- package/dist/utils/index.mjs.map +1 -1
- package/package.json +25 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @mrck-labs/vanaheim-shared
|
|
2
2
|
|
|
3
|
-
Shared types, constants, and
|
|
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
|
-
|
|
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
|
-
|
|
81
|
+
Centralized TanStack Query cache key factory:
|
|
14
82
|
|
|
15
83
|
```typescript
|
|
16
|
-
import
|
|
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
|
-
###
|
|
171
|
+
### MobileHorizon
|
|
20
172
|
|
|
21
173
|
```typescript
|
|
22
|
-
//
|
|
23
|
-
import
|
|
174
|
+
// src/hooks/queries/useFocusQueries.ts
|
|
175
|
+
import { queryKeys } from '@mrck-labs/vanaheim-shared/query'
|
|
24
176
|
|
|
25
|
-
//
|
|
26
|
-
|
|
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
|
|
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":[]}
|