@prabhask5/stellar-engine 1.0.3

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.
Files changed (138) hide show
  1. package/README.md +295 -0
  2. package/dist/actions/remoteChange.d.ts +79 -0
  3. package/dist/actions/remoteChange.d.ts.map +1 -0
  4. package/dist/actions/remoteChange.js +300 -0
  5. package/dist/actions/remoteChange.js.map +1 -0
  6. package/dist/auth/admin.d.ts +12 -0
  7. package/dist/auth/admin.d.ts.map +1 -0
  8. package/dist/auth/admin.js +23 -0
  9. package/dist/auth/admin.js.map +1 -0
  10. package/dist/auth/offlineCredentials.d.ts +41 -0
  11. package/dist/auth/offlineCredentials.d.ts.map +1 -0
  12. package/dist/auth/offlineCredentials.js +121 -0
  13. package/dist/auth/offlineCredentials.js.map +1 -0
  14. package/dist/auth/offlineLogin.d.ts +34 -0
  15. package/dist/auth/offlineLogin.d.ts.map +1 -0
  16. package/dist/auth/offlineLogin.js +75 -0
  17. package/dist/auth/offlineLogin.js.map +1 -0
  18. package/dist/auth/offlineSession.d.ts +22 -0
  19. package/dist/auth/offlineSession.d.ts.map +1 -0
  20. package/dist/auth/offlineSession.js +54 -0
  21. package/dist/auth/offlineSession.js.map +1 -0
  22. package/dist/auth/resolveAuthState.d.ts +24 -0
  23. package/dist/auth/resolveAuthState.d.ts.map +1 -0
  24. package/dist/auth/resolveAuthState.js +69 -0
  25. package/dist/auth/resolveAuthState.js.map +1 -0
  26. package/dist/config.d.ts +53 -0
  27. package/dist/config.d.ts.map +1 -0
  28. package/dist/config.js +55 -0
  29. package/dist/config.js.map +1 -0
  30. package/dist/conflicts.d.ts +70 -0
  31. package/dist/conflicts.d.ts.map +1 -0
  32. package/dist/conflicts.js +321 -0
  33. package/dist/conflicts.js.map +1 -0
  34. package/dist/data.d.ts +77 -0
  35. package/dist/data.d.ts.map +1 -0
  36. package/dist/data.js +360 -0
  37. package/dist/data.js.map +1 -0
  38. package/dist/database.d.ts +31 -0
  39. package/dist/database.d.ts.map +1 -0
  40. package/dist/database.js +51 -0
  41. package/dist/database.js.map +1 -0
  42. package/dist/debug.d.ts +11 -0
  43. package/dist/debug.d.ts.map +1 -0
  44. package/dist/debug.js +48 -0
  45. package/dist/debug.js.map +1 -0
  46. package/dist/deviceId.d.ts +16 -0
  47. package/dist/deviceId.d.ts.map +1 -0
  48. package/dist/deviceId.js +48 -0
  49. package/dist/deviceId.js.map +1 -0
  50. package/dist/engine.d.ts +14 -0
  51. package/dist/engine.d.ts.map +1 -0
  52. package/dist/engine.js +1903 -0
  53. package/dist/engine.js.map +1 -0
  54. package/dist/entries/actions.d.ts +2 -0
  55. package/dist/entries/actions.d.ts.map +1 -0
  56. package/dist/entries/actions.js +3 -0
  57. package/dist/entries/actions.js.map +1 -0
  58. package/dist/entries/auth.d.ts +7 -0
  59. package/dist/entries/auth.d.ts.map +1 -0
  60. package/dist/entries/auth.js +6 -0
  61. package/dist/entries/auth.js.map +1 -0
  62. package/dist/entries/config.d.ts +3 -0
  63. package/dist/entries/config.d.ts.map +1 -0
  64. package/dist/entries/config.js +3 -0
  65. package/dist/entries/config.js.map +1 -0
  66. package/dist/entries/stores.d.ts +9 -0
  67. package/dist/entries/stores.d.ts.map +1 -0
  68. package/dist/entries/stores.js +9 -0
  69. package/dist/entries/stores.js.map +1 -0
  70. package/dist/entries/types.d.ts +11 -0
  71. package/dist/entries/types.d.ts.map +1 -0
  72. package/dist/entries/types.js +2 -0
  73. package/dist/entries/types.js.map +1 -0
  74. package/dist/entries/utils.d.ts +3 -0
  75. package/dist/entries/utils.d.ts.map +1 -0
  76. package/dist/entries/utils.js +4 -0
  77. package/dist/entries/utils.js.map +1 -0
  78. package/dist/index.d.ts +32 -0
  79. package/dist/index.d.ts.map +1 -0
  80. package/dist/index.js +39 -0
  81. package/dist/index.js.map +1 -0
  82. package/dist/operations.d.ts +73 -0
  83. package/dist/operations.d.ts.map +1 -0
  84. package/dist/operations.js +227 -0
  85. package/dist/operations.js.map +1 -0
  86. package/dist/queue.d.ts +32 -0
  87. package/dist/queue.d.ts.map +1 -0
  88. package/dist/queue.js +377 -0
  89. package/dist/queue.js.map +1 -0
  90. package/dist/realtime.d.ts +57 -0
  91. package/dist/realtime.d.ts.map +1 -0
  92. package/dist/realtime.js +491 -0
  93. package/dist/realtime.js.map +1 -0
  94. package/dist/reconnectHandler.d.ts +16 -0
  95. package/dist/reconnectHandler.d.ts.map +1 -0
  96. package/dist/reconnectHandler.js +21 -0
  97. package/dist/reconnectHandler.js.map +1 -0
  98. package/dist/runtime/runtimeConfig.d.ts +27 -0
  99. package/dist/runtime/runtimeConfig.d.ts.map +1 -0
  100. package/dist/runtime/runtimeConfig.js +133 -0
  101. package/dist/runtime/runtimeConfig.js.map +1 -0
  102. package/dist/stores/authState.d.ts +57 -0
  103. package/dist/stores/authState.d.ts.map +1 -0
  104. package/dist/stores/authState.js +154 -0
  105. package/dist/stores/authState.js.map +1 -0
  106. package/dist/stores/network.d.ts +9 -0
  107. package/dist/stores/network.d.ts.map +1 -0
  108. package/dist/stores/network.js +97 -0
  109. package/dist/stores/network.js.map +1 -0
  110. package/dist/stores/remoteChanges.d.ts +142 -0
  111. package/dist/stores/remoteChanges.d.ts.map +1 -0
  112. package/dist/stores/remoteChanges.js +353 -0
  113. package/dist/stores/remoteChanges.js.map +1 -0
  114. package/dist/stores/sync.d.ts +35 -0
  115. package/dist/stores/sync.d.ts.map +1 -0
  116. package/dist/stores/sync.js +115 -0
  117. package/dist/stores/sync.js.map +1 -0
  118. package/dist/supabase/auth.d.ts +60 -0
  119. package/dist/supabase/auth.d.ts.map +1 -0
  120. package/dist/supabase/auth.js +298 -0
  121. package/dist/supabase/auth.js.map +1 -0
  122. package/dist/supabase/client.d.ts +15 -0
  123. package/dist/supabase/client.d.ts.map +1 -0
  124. package/dist/supabase/client.js +149 -0
  125. package/dist/supabase/client.js.map +1 -0
  126. package/dist/supabase/validate.d.ts +11 -0
  127. package/dist/supabase/validate.d.ts.map +1 -0
  128. package/dist/supabase/validate.js +38 -0
  129. package/dist/supabase/validate.js.map +1 -0
  130. package/dist/types.d.ts +78 -0
  131. package/dist/types.d.ts.map +1 -0
  132. package/dist/types.js +16 -0
  133. package/dist/types.js.map +1 -0
  134. package/dist/utils.d.ts +24 -0
  135. package/dist/utils.d.ts.map +1 -0
  136. package/dist/utils.js +56 -0
  137. package/dist/utils.js.map +1 -0
  138. package/package.json +84 -0
package/README.md ADDED
@@ -0,0 +1,295 @@
1
+ # @prabhask5/stellar-engine
2
+
3
+ A local-first, offline-capable sync engine for **SvelteKit + Supabase + Dexie** applications. All reads come from IndexedDB, all writes land locally first, and a background sync loop ships changes to Supabase -- so your app stays fast and functional regardless of network state.
4
+
5
+ ## Documentation
6
+
7
+ - [API Reference](./API_REFERENCE.md) -- full signatures, parameters, and usage examples for every public export
8
+ - [Architecture](./ARCHITECTURE.md) -- internal design, data flow, and module responsibilities
9
+ - [Framework Integration](./FRAMEWORKS.md) -- SvelteKit-specific patterns and conventions
10
+
11
+ ## Features
12
+
13
+ - **Intent-based sync operations** -- operations preserve intent (`increment`, `set`, `create`, `delete`) instead of just final state, enabling smarter coalescing and conflict handling.
14
+ - **Three-tier conflict resolution** -- field-level diffing, numeric merge fields, and configurable exclusion lists let you resolve conflicts precisely rather than with blanket last-write-wins.
15
+ - **Offline authentication** -- credential caching and offline session tokens let users sign in and work without connectivity; sessions reconcile automatically on reconnect.
16
+ - **Realtime subscriptions** -- Supabase Realtime channels push remote changes into local state instantly, with duplicate-delivery guards to prevent re-processing.
17
+ - **Operation coalescing** -- batches of rapid local writes (e.g., 50 individual increments) are compressed into a single outbound operation, reducing sync traffic dramatically.
18
+ - **Tombstone management** -- soft deletes are propagated cleanly, and stale tombstones are garbage-collected after a configurable retention period.
19
+ - **Egress optimization** -- column-level select lists and ownership filters ensure only the data your client needs is fetched from Supabase.
20
+
21
+ ## Quick start
22
+
23
+ Install from GitHub Packages:
24
+
25
+ ```bash
26
+ npm install @prabhask5/stellar-engine@^1.0.0
27
+ ```
28
+
29
+ > Requires an `.npmrc` with `@stellar:registry=https://npm.pkg.github.com`.
30
+
31
+ Initialize the engine at app startup (e.g., in a SvelteKit root `+layout.ts`):
32
+
33
+ ```ts
34
+ import { initEngine, startSyncEngine, supabase } from '@prabhask5/stellar-engine';
35
+ import { initConfig } from '@prabhask5/stellar-engine/config';
36
+ import { resolveAuthState } from '@prabhask5/stellar-engine/auth';
37
+
38
+ initEngine({
39
+ prefix: 'myapp',
40
+ supabase,
41
+ tables: [
42
+ {
43
+ supabaseName: 'projects',
44
+ dexieTable: 'projects',
45
+ columns: 'id, name, created_at, updated_at, is_deleted, user_id',
46
+ },
47
+ // ...more tables
48
+ ],
49
+ database: {
50
+ name: 'MyAppDB',
51
+ versions: [
52
+ {
53
+ version: 1,
54
+ stores: {
55
+ projects: 'id, user_id, updated_at',
56
+ tasks: 'id, project_id, user_id, updated_at',
57
+ }
58
+ }
59
+ ]
60
+ },
61
+ auth: {
62
+ enableOfflineAuth: true,
63
+ },
64
+ });
65
+
66
+ await initConfig();
67
+ const auth = await resolveAuthState();
68
+ if (auth.authMode !== 'none') await startSyncEngine();
69
+ ```
70
+
71
+ ## Subpath exports
72
+
73
+ Import only what you need via subpath exports:
74
+
75
+ | Subpath | Contents |
76
+ |---|---|
77
+ | `@prabhask5/stellar-engine` | `initEngine`, `startSyncEngine`, `runFullSync`, `supabase`, `getDb`, `validateSupabaseCredentials` |
78
+ | `@prabhask5/stellar-engine/data` | All engine CRUD + query operations (`engineCreate`, `engineUpdate`, etc.) |
79
+ | `@prabhask5/stellar-engine/auth` | All auth functions (`signIn`, `signUp`, `resolveAuthState`, `isAdmin`, etc.) |
80
+ | `@prabhask5/stellar-engine/stores` | Reactive stores + event subscriptions (`syncStatusStore`, `authState`, `onSyncComplete`, etc.) |
81
+ | `@prabhask5/stellar-engine/types` | All type exports (`Session`, `SyncEngineConfig`, `BatchOperation`, etc.) |
82
+ | `@prabhask5/stellar-engine/utils` | Utility functions (`generateId`, `now`, `calculateNewOrder`, `debug`, etc.) |
83
+ | `@prabhask5/stellar-engine/actions` | Svelte `use:` actions (`remoteChangeAnimation`, `trackEditing`, `triggerLocalAnimation`) |
84
+ | `@prabhask5/stellar-engine/config` | Runtime config (`initConfig`, `getConfig`, `setConfig`) |
85
+
86
+ The root export (`@prabhask5/stellar-engine`) re-exports everything for backward compatibility.
87
+
88
+ ## Requirements
89
+
90
+ **Supabase**
91
+
92
+ Your Supabase project needs tables matching the `supabaseName` entries in your config. Each table should have at minimum:
93
+ - `id` (uuid primary key)
94
+ - `updated_at` (timestamptz) -- used as the sync cursor
95
+ - `is_deleted` (boolean, default false) -- for soft-delete / tombstone support
96
+ - An ownership column (e.g., `user_id`) if you use `ownershipFilter`
97
+
98
+ Row-Level Security policies should scope reads and writes to the authenticated user.
99
+
100
+ **Dexie (IndexedDB)**
101
+
102
+ When you provide a `database` config to `initEngine`, the engine creates and manages the Dexie instance for you. System tables (`syncQueue`, `conflictHistory`, `offlineCredentials`, `offlineSession`) are automatically merged into every schema version -- you only declare your application tables:
103
+
104
+ ```ts
105
+ database: {
106
+ name: 'MyAppDB',
107
+ versions: [
108
+ {
109
+ version: 1,
110
+ stores: {
111
+ projects: 'id, user_id, updated_at',
112
+ tasks: 'id, project_id, user_id, updated_at',
113
+ }
114
+ }
115
+ ]
116
+ }
117
+ ```
118
+
119
+ Alternatively, you can provide a pre-created Dexie instance via the `db` config option for full control.
120
+
121
+ ## Architecture
122
+
123
+ ```
124
+ +---------------------+
125
+ | Application UI |
126
+ +---------------------+
127
+ |
128
+ v
129
+ +---------------------+ +-------------------+
130
+ | Repositories | ----> | Dexie (IDB) |
131
+ | (read/write local) | | - app tables |
132
+ +---------------------+ | - syncQueue |
133
+ | | - conflictHistory |
134
+ | queueSyncOperation +-------------------+
135
+ v ^
136
+ +---------------------+ |
137
+ | Sync Engine | ---------------+
138
+ | - coalesce ops | hydrate / reconcile
139
+ | - push to remote |
140
+ | - pull from remote |
141
+ | - resolve conflicts |
142
+ +---------------------+
143
+ |
144
+ v
145
+ +---------------------+ +---------------------+
146
+ | Supabase REST | | Supabase Realtime |
147
+ | (push / pull) | | (live subscriptions)|
148
+ +---------------------+ +---------------------+
149
+ ```
150
+
151
+ 1. **Repositories** read from and write to Dexie, then enqueue a `SyncOperationItem` describing the intent of the change.
152
+ 2. **The engine** debounces outbound operations, coalesces them, and pushes to Supabase via REST. On pull, it fetches rows newer than the local sync cursor and reconciles them with any pending local operations.
153
+ 3. **Realtime** channels deliver server-side changes immediately, skipping the next poll cycle when the subscription is healthy.
154
+
155
+ ## API overview
156
+
157
+ ### Configuration
158
+
159
+ | Export | Description |
160
+ |---|---|
161
+ | `initEngine(config)` | Initialize the engine with table definitions, Supabase client, and Dexie instance. |
162
+ | `getEngineConfig()` | Retrieve the current config (throws if not initialized). |
163
+ | `SyncEngineConfig` / `TableConfig` | TypeScript interfaces for the config objects. |
164
+
165
+ ### Engine lifecycle
166
+
167
+ | Export | Description |
168
+ |---|---|
169
+ | `startSyncEngine()` | Start the sync loop, realtime subscriptions, and event listeners. |
170
+ | `stopSyncEngine()` | Tear down everything cleanly. |
171
+ | `scheduleSyncPush()` | Trigger a debounced push of pending operations. |
172
+ | `runFullSync()` | Run a complete pull-then-push cycle. |
173
+ | `clearLocalCache()` | Wipe all local application data. |
174
+ | `clearPendingSyncQueue()` | Drop all pending outbound operations. |
175
+
176
+ ### Entity tracking
177
+
178
+ | Export | Description |
179
+ |---|---|
180
+ | `markEntityModified(table, id)` | Record that an entity was recently modified locally (prevents incoming realtime from overwriting). |
181
+ | `onSyncComplete(callback)` | Register a callback invoked after each successful sync cycle. |
182
+
183
+ ### Auth
184
+
185
+ | Export | Description |
186
+ |---|---|
187
+ | `signIn` / `signUp` / `signOut` | Supabase auth wrappers that also manage offline credential caching. |
188
+ | `getSession` / `isSessionExpired` | Session inspection helpers. |
189
+ | `changePassword` / `resendConfirmationEmail` | Account management. |
190
+ | `getUserProfile` / `updateProfile` | Profile read/write via Supabase user metadata. |
191
+
192
+ ### Offline auth
193
+
194
+ | Export | Description |
195
+ |---|---|
196
+ | `cacheOfflineCredentials` / `getOfflineCredentials` / `verifyOfflineCredentials` / `clearOfflineCredentials` | Store and verify credentials locally for offline sign-in. |
197
+ | `createOfflineSession` / `getValidOfflineSession` / `clearOfflineSession` | Manage offline session tokens in IndexedDB. |
198
+
199
+ ### Queue
200
+
201
+ | Export | Description |
202
+ |---|---|
203
+ | `queueSyncOperation(item)` | Enqueue a raw `SyncOperationItem`. |
204
+ | `queueCreateOperation(table, id, payload)` | Enqueue entity creation. |
205
+ | `queueDeleteOperation(table, id)` | Enqueue a soft delete. |
206
+ | `coalescePendingOps()` | Compress the outbox in-place (called automatically before push). |
207
+ | `getPendingSync()` / `getPendingEntityIds()` | Inspect the current outbox. |
208
+
209
+ ### Conflict resolution
210
+
211
+ | Export | Description |
212
+ |---|---|
213
+ | `resolveConflicts(table, localEntity, remoteEntity, pendingOps)` | Three-tier field-level conflict resolver. Returns the merged entity. |
214
+
215
+ ### Realtime
216
+
217
+ | Export | Description |
218
+ |---|---|
219
+ | `startRealtimeSubscriptions()` / `stopRealtimeSubscriptions()` | Manage Supabase Realtime channels for all configured tables. |
220
+ | `isRealtimeHealthy()` | Realtime connection health check. |
221
+ | `wasRecentlyProcessedByRealtime(table, id)` | Guard against duplicate processing. |
222
+ | `onRealtimeDataUpdate(callback)` | Register a handler for incoming realtime changes. |
223
+
224
+ ### Stores (Svelte 5 compatible)
225
+
226
+ | Export | Description |
227
+ |---|---|
228
+ | `syncStatusStore` | Reactive store exposing current `SyncStatus`, last sync time, and errors. |
229
+ | `remoteChangesStore` | Tracks which entities were recently changed by remote peers. |
230
+ | `createRecentChangeIndicator(table, id)` | Derived indicator for UI highlighting of remote changes. |
231
+ | `createPendingDeleteIndicator(table, id)` | Derived indicator for entities awaiting delete confirmation. |
232
+ | `isOnline` | Reactive boolean reflecting network state. |
233
+ | `authState` / `isAuthenticated` / `userDisplayInfo` | Reactive auth status stores. |
234
+
235
+ ### Supabase client
236
+
237
+ | Export | Description |
238
+ |---|---|
239
+ | `supabase` | The configured `SupabaseClient` instance. |
240
+ | `getSupabaseAsync()` | Async getter that waits for initialization. |
241
+ | `resetSupabaseClient()` | Tear down and reinitialize the client. |
242
+
243
+ ### Runtime config
244
+
245
+ | Export | Description |
246
+ |---|---|
247
+ | `initConfig` / `getConfig` / `waitForConfig` / `setConfig` | Manage app-level runtime configuration (e.g., feature flags loaded from the server). |
248
+ | `isConfigured()` / `clearConfigCache()` | Status and cache management. |
249
+
250
+ ### Utilities
251
+
252
+ | Export | Description |
253
+ |---|---|
254
+ | `generateId()` | Generate a UUID. |
255
+ | `now()` | Current ISO timestamp string. |
256
+ | `calculateNewOrder(before, after)` | Fractional ordering helper for drag-and-drop reorder. |
257
+ | `getDeviceId()` | Stable per-device identifier (persisted in localStorage). |
258
+ | `debugLog` / `debugWarn` / `debugError` | Prefixed console helpers (gated by `setDebugMode`). |
259
+
260
+ ### Browser console debug utilities
261
+
262
+ When debug mode is enabled, the engine exposes utilities on the `window` object using the configured app prefix (e.g. `stellar`):
263
+
264
+ | Window property | Description |
265
+ |---|---|
266
+ | `window.__<prefix>SyncStats()` | View sync cycle statistics (total cycles, recent cycle details, trigger types). |
267
+ | `window.__<prefix>Egress()` | Monitor data transfer from Supabase (total bytes, per-table breakdown, recent cycles). |
268
+ | `window.__<prefix>Tombstones()` | Check soft-deleted record counts across all tables. |
269
+ | `window.__<prefix>Tombstones({ cleanup: true })` | Manually trigger tombstone cleanup. |
270
+ | `window.__<prefix>Tombstones({ cleanup: true, force: true })` | Force server cleanup (bypasses 24-hour interval). |
271
+ | `window.__<prefix>Sync.forceFullSync()` | Reset sync cursor, clear local data, and re-download everything from server. |
272
+ | `window.__<prefix>Sync.resetSyncCursor()` | Clear the stored cursor so the next sync pulls all data. |
273
+ | `window.__<prefix>Sync.sync()` | Trigger a manual sync cycle. |
274
+ | `window.__<prefix>Sync.getStatus()` | View current sync cursor and pending operation count. |
275
+ | `window.__<prefix>Sync.checkConnection()` | Test Supabase connectivity. |
276
+ | `window.__<prefix>Sync.realtimeStatus()` | Check realtime connection state and health. |
277
+
278
+ ### Svelte actions
279
+
280
+ | Export | Description |
281
+ |---|---|
282
+ | `remoteChangeAnimation` | Svelte `use:` action that animates an element when a remote change arrives. |
283
+ | `trackEditing` | Action that signals the engine a field is being actively edited (suppresses incoming overwrites). |
284
+ | `triggerLocalAnimation` | Programmatically trigger the local-change animation on a node. |
285
+
286
+ ## Use cases
287
+
288
+ - **Productivity and task management apps** -- offline-capable task boards, habit trackers, daily planners with cross-device sync.
289
+ - **Notion-like editors** -- block-based documents where each block is a synced entity with field-level conflict resolution.
290
+ - **Personal finance trackers** -- numeric merge fields handle concurrent balance adjustments across devices.
291
+ - **File and asset management UIs** -- fractional ordering keeps drag-and-drop sort order consistent without rewriting every row.
292
+
293
+ ## License
294
+
295
+ Private -- not yet published under an open-source license.
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Remote Change Animation Action
3
+ *
4
+ * A Svelte action that automatically adds remote change animations to elements.
5
+ * Use this on list items, cards, or any element that can be updated remotely.
6
+ *
7
+ * The action detects the ACTION TYPE from the remote change and applies
8
+ * the appropriate animation:
9
+ * - 'create' → item-created (slide in with burst)
10
+ * - 'delete' → item-deleting (slide out with fade)
11
+ * - 'toggle' → checkbox-animating + completion-ripple
12
+ * - 'increment' → counter-increment
13
+ * - 'decrement' → counter-decrement
14
+ * - 'reorder' → item-reordering
15
+ * - 'rename' → text-changed
16
+ * - 'update' → item-changed (default highlight)
17
+ *
18
+ * Usage:
19
+ * ```svelte
20
+ * <div use:remoteChangeAnimation={{ entityId: item.id, entityType: 'goals' }}>
21
+ * ...
22
+ * </div>
23
+ * ```
24
+ */
25
+ import { type RemoteActionType } from '../stores/remoteChanges';
26
+ interface RemoteChangeOptions {
27
+ entityId: string;
28
+ entityType: string;
29
+ fields?: string[];
30
+ animationClass?: string;
31
+ onAction?: (actionType: RemoteActionType, fields: string[]) => void;
32
+ }
33
+ export declare function remoteChangeAnimation(node: HTMLElement, options: RemoteChangeOptions): {
34
+ update(newOptions: RemoteChangeOptions): void;
35
+ destroy(): void;
36
+ };
37
+ /**
38
+ * Action for form elements that should track editing state.
39
+ * Use this on modal forms with Save buttons to defer remote changes.
40
+ *
41
+ * Usage:
42
+ * ```svelte
43
+ * <form use:trackEditing={{ entityId: item.id, entityType: 'goals', formType: 'manual-save' }}>
44
+ * ...
45
+ * </form>
46
+ * ```
47
+ */
48
+ interface TrackEditingOptions {
49
+ entityId: string;
50
+ entityType: string;
51
+ formType: 'auto-save' | 'manual-save';
52
+ fields?: string[];
53
+ onDeferredChanges?: (changes: unknown[]) => void;
54
+ }
55
+ export declare function trackEditing(node: HTMLElement, options: TrackEditingOptions): {
56
+ update(newOptions: TrackEditingOptions): void;
57
+ destroy(): void;
58
+ };
59
+ /**
60
+ * Trigger a local action animation on an element.
61
+ * Use this to make local actions animate the same way as remote actions.
62
+ *
63
+ * Usage in components:
64
+ * ```svelte
65
+ * <script>
66
+ * import { triggerLocalAnimation } from '@prabhask5/stellar-engine';
67
+ * let element: HTMLElement;
68
+ *
69
+ * function handleToggle() {
70
+ * triggerLocalAnimation(element, 'toggle');
71
+ * onToggle?.();
72
+ * }
73
+ * </script>
74
+ * <div bind:this={element}>...</div>
75
+ * ```
76
+ */
77
+ export declare function triggerLocalAnimation(element: HTMLElement | null, actionType: RemoteActionType): void;
78
+ export {};
79
+ //# sourceMappingURL=remoteChange.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remoteChange.d.ts","sourceRoot":"","sources":["../../src/actions/remoteChange.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAIL,KAAK,gBAAgB,EACtB,MAAM,yBAAyB,CAAC;AAEjC,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IAEnB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAElB,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,QAAQ,CAAC,EAAE,CAAC,UAAU,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACrE;AAiCD,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,mBAAmB;uBAqH9D,mBAAmB;;EAkCzC;AAED;;;;;;;;;;GAUG;AAEH,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,WAAW,GAAG,aAAa,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAElB,iBAAiB,CAAC,EAAE,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CAClD;AAED,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,OAAO,EAAE,mBAAmB;uBAqBrD,mBAAmB;;EAyBzC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,WAAW,GAAG,IAAI,EAC3B,UAAU,EAAE,gBAAgB,GAC3B,IAAI,CA8DN"}
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Remote Change Animation Action
3
+ *
4
+ * A Svelte action that automatically adds remote change animations to elements.
5
+ * Use this on list items, cards, or any element that can be updated remotely.
6
+ *
7
+ * The action detects the ACTION TYPE from the remote change and applies
8
+ * the appropriate animation:
9
+ * - 'create' → item-created (slide in with burst)
10
+ * - 'delete' → item-deleting (slide out with fade)
11
+ * - 'toggle' → checkbox-animating + completion-ripple
12
+ * - 'increment' → counter-increment
13
+ * - 'decrement' → counter-decrement
14
+ * - 'reorder' → item-reordering
15
+ * - 'rename' → text-changed
16
+ * - 'update' → item-changed (default highlight)
17
+ *
18
+ * Usage:
19
+ * ```svelte
20
+ * <div use:remoteChangeAnimation={{ entityId: item.id, entityType: 'goals' }}>
21
+ * ...
22
+ * </div>
23
+ * ```
24
+ */
25
+ import { remoteChangesStore, createRecentChangeIndicator, createPendingDeleteIndicator } from '../stores/remoteChanges';
26
+ /**
27
+ * Map action types to CSS animation classes
28
+ */
29
+ const ACTION_ANIMATION_MAP = {
30
+ create: 'item-created',
31
+ delete: 'item-deleting',
32
+ toggle: 'item-toggled',
33
+ increment: 'counter-increment',
34
+ decrement: 'counter-decrement',
35
+ reorder: 'item-reordering',
36
+ rename: 'text-changed',
37
+ update: 'item-changed'
38
+ };
39
+ /**
40
+ * Animation durations for cleanup (ms)
41
+ */
42
+ const ACTION_DURATION_MAP = {
43
+ create: 600,
44
+ delete: 500,
45
+ toggle: 600,
46
+ increment: 400,
47
+ decrement: 400,
48
+ reorder: 400,
49
+ rename: 700,
50
+ update: 1600
51
+ };
52
+ // Track currently animating elements to prevent overlapping animations
53
+ const animatingElements = new WeakSet();
54
+ export function remoteChangeAnimation(node, options) {
55
+ let { entityId, entityType, fields, animationClass, onAction } = options;
56
+ // Add base class for styling hooks
57
+ node.classList.add('syncable-item');
58
+ // Helper function to apply animation
59
+ function applyAnimation(change) {
60
+ // If fields are specified, only animate if those fields changed
61
+ if (fields && fields.length > 0) {
62
+ const fieldsList = fields; // Capture for closure
63
+ const hasRelevantChange = change.fields.some((f) => f === '*' || fieldsList.includes(f));
64
+ if (!hasRelevantChange)
65
+ return;
66
+ }
67
+ // Prevent overlapping animations on the same element
68
+ if (animatingElements.has(node))
69
+ return;
70
+ animatingElements.add(node);
71
+ // Determine animation class based on action type
72
+ const actionType = change.actionType;
73
+ const cssClass = animationClass || ACTION_ANIMATION_MAP[actionType] || 'item-changed';
74
+ const duration = ACTION_DURATION_MAP[actionType] || 1600;
75
+ // Call action callback if provided (for component-specific handling)
76
+ if (onAction) {
77
+ onAction(actionType, change.fields);
78
+ }
79
+ // Apply animation class
80
+ node.classList.add(cssClass);
81
+ // For toggle actions, also add checkbox animation to child checkbox elements
82
+ if (actionType === 'toggle') {
83
+ const checkbox = node.querySelector('.checkbox, [class*="checkbox"]');
84
+ if (checkbox) {
85
+ checkbox.classList.add('checkbox-animating');
86
+ setTimeout(() => checkbox.classList.remove('checkbox-animating'), 500);
87
+ }
88
+ // Add completion ripple effect
89
+ const ripple = document.createElement('span');
90
+ ripple.className = 'completion-ripple';
91
+ node.appendChild(ripple);
92
+ setTimeout(() => ripple.remove(), 700);
93
+ }
94
+ // For increment/decrement, animate the counter element
95
+ if (actionType === 'increment' || actionType === 'decrement') {
96
+ const counter = node.querySelector('[class*="value"], [class*="counter"], [class*="current"]');
97
+ if (counter) {
98
+ counter.classList.add(cssClass);
99
+ setTimeout(() => counter.classList.remove(cssClass), duration);
100
+ }
101
+ }
102
+ // For delete animations, don't remove the class — the element will be
103
+ // removed from DOM after the animation. Removing it early causes the item
104
+ // to briefly reappear between animation end and DOM removal.
105
+ if (actionType === 'delete')
106
+ return;
107
+ // Remove class after animation completes
108
+ const handleAnimationEnd = () => {
109
+ node.classList.remove(cssClass);
110
+ animatingElements.delete(node);
111
+ node.removeEventListener('animationend', handleAnimationEnd);
112
+ };
113
+ node.addEventListener('animationend', handleAnimationEnd);
114
+ // Fallback removal in case animationend doesn't fire
115
+ setTimeout(() => {
116
+ node.classList.remove(cssClass);
117
+ animatingElements.delete(node);
118
+ }, duration + 100);
119
+ }
120
+ // Check for recent change immediately on mount (important for CREATE animations)
121
+ // This handles the case where the element mounts after a remote INSERT
122
+ const initialChange = remoteChangesStore.getRecentChange(entityId, entityType);
123
+ if (initialChange) {
124
+ // Use requestAnimationFrame to ensure DOM is ready
125
+ requestAnimationFrame(() => {
126
+ applyAnimation(initialChange);
127
+ });
128
+ }
129
+ // Create derived stores to watch for future changes and pending deletes
130
+ let changeIndicator = createRecentChangeIndicator(entityId, entityType);
131
+ let deleteIndicator = createPendingDeleteIndicator(entityId, entityType);
132
+ // Track the current unsubscribe functions
133
+ let unsubscribeChange = changeIndicator.subscribe((change) => {
134
+ // Skip if no change or if this is the same change we already animated on mount
135
+ if (!change)
136
+ return;
137
+ if (initialChange && change.timestamp === initialChange.timestamp)
138
+ return;
139
+ applyAnimation(change);
140
+ });
141
+ // Watch for pending deletes to apply delete animation
142
+ let unsubscribeDelete = deleteIndicator.subscribe((isPendingDelete) => {
143
+ if (isPendingDelete) {
144
+ // Apply delete animation immediately
145
+ const deleteClass = ACTION_ANIMATION_MAP['delete'];
146
+ node.classList.add(deleteClass);
147
+ // Call action callback if provided
148
+ if (onAction) {
149
+ onAction('delete', ['*']);
150
+ }
151
+ }
152
+ });
153
+ return {
154
+ update(newOptions) {
155
+ // If entity changed, re-subscribe with new entity
156
+ if (newOptions.entityId !== entityId || newOptions.entityType !== entityType) {
157
+ unsubscribeChange();
158
+ unsubscribeDelete();
159
+ entityId = newOptions.entityId;
160
+ entityType = newOptions.entityType;
161
+ fields = newOptions.fields;
162
+ animationClass = newOptions.animationClass;
163
+ onAction = newOptions.onAction;
164
+ changeIndicator = createRecentChangeIndicator(entityId, entityType);
165
+ deleteIndicator = createPendingDeleteIndicator(entityId, entityType);
166
+ unsubscribeChange = changeIndicator.subscribe((change) => {
167
+ if (!change)
168
+ return;
169
+ applyAnimation(change);
170
+ });
171
+ unsubscribeDelete = deleteIndicator.subscribe((isPendingDelete) => {
172
+ if (isPendingDelete) {
173
+ const deleteClass = ACTION_ANIMATION_MAP['delete'];
174
+ node.classList.add(deleteClass);
175
+ if (onAction) {
176
+ onAction('delete', ['*']);
177
+ }
178
+ }
179
+ });
180
+ }
181
+ },
182
+ destroy() {
183
+ unsubscribeChange();
184
+ unsubscribeDelete();
185
+ node.classList.remove('syncable-item');
186
+ animatingElements.delete(node);
187
+ }
188
+ };
189
+ }
190
+ export function trackEditing(node, options) {
191
+ const { entityId, entityType, formType, fields, onDeferredChanges } = options;
192
+ // Start tracking when the element mounts
193
+ remoteChangesStore.startEditing(entityId, entityType, formType, fields);
194
+ // Check for deferred changes indicator
195
+ const updateDeferredIndicator = () => {
196
+ const hasDeferred = remoteChangesStore.hasDeferredChanges(entityId, entityType);
197
+ if (hasDeferred) {
198
+ node.classList.add('has-deferred-changes');
199
+ }
200
+ else {
201
+ node.classList.remove('has-deferred-changes');
202
+ }
203
+ };
204
+ // Check periodically for deferred changes
205
+ const interval = setInterval(updateDeferredIndicator, 1000);
206
+ updateDeferredIndicator();
207
+ return {
208
+ update(newOptions) {
209
+ // If entity changed, stop old tracking and start new
210
+ if (newOptions.entityId !== entityId || newOptions.entityType !== entityType) {
211
+ remoteChangesStore.stopEditing(entityId, entityType);
212
+ remoteChangesStore.startEditing(newOptions.entityId, newOptions.entityType, newOptions.formType, newOptions.fields);
213
+ }
214
+ },
215
+ destroy() {
216
+ clearInterval(interval);
217
+ node.classList.remove('has-deferred-changes');
218
+ // Stop tracking and get any deferred changes
219
+ const deferredChanges = remoteChangesStore.stopEditing(entityId, entityType);
220
+ // Notify callback if there are deferred changes
221
+ if (deferredChanges.length > 0 && onDeferredChanges) {
222
+ onDeferredChanges(deferredChanges);
223
+ }
224
+ }
225
+ };
226
+ }
227
+ /**
228
+ * Trigger a local action animation on an element.
229
+ * Use this to make local actions animate the same way as remote actions.
230
+ *
231
+ * Usage in components:
232
+ * ```svelte
233
+ * <script>
234
+ * import { triggerLocalAnimation } from '@prabhask5/stellar-engine';
235
+ * let element: HTMLElement;
236
+ *
237
+ * function handleToggle() {
238
+ * triggerLocalAnimation(element, 'toggle');
239
+ * onToggle?.();
240
+ * }
241
+ * </script>
242
+ * <div bind:this={element}>...</div>
243
+ * ```
244
+ */
245
+ export function triggerLocalAnimation(element, actionType) {
246
+ if (!element)
247
+ return;
248
+ const cssClass = ACTION_ANIMATION_MAP[actionType] || 'item-changed';
249
+ const duration = ACTION_DURATION_MAP[actionType] || 1600;
250
+ // For increment/decrement, restart animation on rapid taps instead of blocking
251
+ if (actionType === 'increment' || actionType === 'decrement') {
252
+ if (animatingElements.has(element)) {
253
+ // Force restart: remove class, trigger reflow, re-add
254
+ element.classList.remove(cssClass);
255
+ void element.offsetWidth;
256
+ }
257
+ }
258
+ else {
259
+ // Prevent overlapping animations for other types
260
+ if (animatingElements.has(element))
261
+ return;
262
+ }
263
+ animatingElements.add(element);
264
+ // Apply animation class
265
+ element.classList.add(cssClass);
266
+ // For toggle actions, also animate checkbox elements
267
+ if (actionType === 'toggle') {
268
+ const checkbox = element.querySelector('.checkbox, [class*="checkbox"]');
269
+ if (checkbox) {
270
+ checkbox.classList.add('checkbox-animating');
271
+ setTimeout(() => checkbox.classList.remove('checkbox-animating'), 500);
272
+ }
273
+ // Add completion ripple effect
274
+ const ripple = document.createElement('span');
275
+ ripple.className = 'completion-ripple';
276
+ element.appendChild(ripple);
277
+ setTimeout(() => ripple.remove(), 700);
278
+ }
279
+ // For increment/decrement, animate the counter element
280
+ if (actionType === 'increment' || actionType === 'decrement') {
281
+ const counter = element.querySelector('[class*="value"], [class*="counter"], [class*="current"]');
282
+ if (counter) {
283
+ counter.classList.add(cssClass);
284
+ setTimeout(() => counter.classList.remove(cssClass), duration);
285
+ }
286
+ }
287
+ // Remove class after animation completes
288
+ const handleAnimationEnd = () => {
289
+ element.classList.remove(cssClass);
290
+ animatingElements.delete(element);
291
+ element.removeEventListener('animationend', handleAnimationEnd);
292
+ };
293
+ element.addEventListener('animationend', handleAnimationEnd);
294
+ // Fallback removal
295
+ setTimeout(() => {
296
+ element.classList.remove(cssClass);
297
+ animatingElements.delete(element);
298
+ }, duration + 100);
299
+ }
300
+ //# sourceMappingURL=remoteChange.js.map