@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.
- package/README.md +295 -0
- package/dist/actions/remoteChange.d.ts +79 -0
- package/dist/actions/remoteChange.d.ts.map +1 -0
- package/dist/actions/remoteChange.js +300 -0
- package/dist/actions/remoteChange.js.map +1 -0
- package/dist/auth/admin.d.ts +12 -0
- package/dist/auth/admin.d.ts.map +1 -0
- package/dist/auth/admin.js +23 -0
- package/dist/auth/admin.js.map +1 -0
- package/dist/auth/offlineCredentials.d.ts +41 -0
- package/dist/auth/offlineCredentials.d.ts.map +1 -0
- package/dist/auth/offlineCredentials.js +121 -0
- package/dist/auth/offlineCredentials.js.map +1 -0
- package/dist/auth/offlineLogin.d.ts +34 -0
- package/dist/auth/offlineLogin.d.ts.map +1 -0
- package/dist/auth/offlineLogin.js +75 -0
- package/dist/auth/offlineLogin.js.map +1 -0
- package/dist/auth/offlineSession.d.ts +22 -0
- package/dist/auth/offlineSession.d.ts.map +1 -0
- package/dist/auth/offlineSession.js +54 -0
- package/dist/auth/offlineSession.js.map +1 -0
- package/dist/auth/resolveAuthState.d.ts +24 -0
- package/dist/auth/resolveAuthState.d.ts.map +1 -0
- package/dist/auth/resolveAuthState.js +69 -0
- package/dist/auth/resolveAuthState.js.map +1 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +55 -0
- package/dist/config.js.map +1 -0
- package/dist/conflicts.d.ts +70 -0
- package/dist/conflicts.d.ts.map +1 -0
- package/dist/conflicts.js +321 -0
- package/dist/conflicts.js.map +1 -0
- package/dist/data.d.ts +77 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +360 -0
- package/dist/data.js.map +1 -0
- package/dist/database.d.ts +31 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +51 -0
- package/dist/database.js.map +1 -0
- package/dist/debug.d.ts +11 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +48 -0
- package/dist/debug.js.map +1 -0
- package/dist/deviceId.d.ts +16 -0
- package/dist/deviceId.d.ts.map +1 -0
- package/dist/deviceId.js +48 -0
- package/dist/deviceId.js.map +1 -0
- package/dist/engine.d.ts +14 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1903 -0
- package/dist/engine.js.map +1 -0
- package/dist/entries/actions.d.ts +2 -0
- package/dist/entries/actions.d.ts.map +1 -0
- package/dist/entries/actions.js +3 -0
- package/dist/entries/actions.js.map +1 -0
- package/dist/entries/auth.d.ts +7 -0
- package/dist/entries/auth.d.ts.map +1 -0
- package/dist/entries/auth.js +6 -0
- package/dist/entries/auth.js.map +1 -0
- package/dist/entries/config.d.ts +3 -0
- package/dist/entries/config.d.ts.map +1 -0
- package/dist/entries/config.js +3 -0
- package/dist/entries/config.js.map +1 -0
- package/dist/entries/stores.d.ts +9 -0
- package/dist/entries/stores.d.ts.map +1 -0
- package/dist/entries/stores.js +9 -0
- package/dist/entries/stores.js.map +1 -0
- package/dist/entries/types.d.ts +11 -0
- package/dist/entries/types.d.ts.map +1 -0
- package/dist/entries/types.js +2 -0
- package/dist/entries/types.js.map +1 -0
- package/dist/entries/utils.d.ts +3 -0
- package/dist/entries/utils.d.ts.map +1 -0
- package/dist/entries/utils.js +4 -0
- package/dist/entries/utils.js.map +1 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +39 -0
- package/dist/index.js.map +1 -0
- package/dist/operations.d.ts +73 -0
- package/dist/operations.d.ts.map +1 -0
- package/dist/operations.js +227 -0
- package/dist/operations.js.map +1 -0
- package/dist/queue.d.ts +32 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +377 -0
- package/dist/queue.js.map +1 -0
- package/dist/realtime.d.ts +57 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +491 -0
- package/dist/realtime.js.map +1 -0
- package/dist/reconnectHandler.d.ts +16 -0
- package/dist/reconnectHandler.d.ts.map +1 -0
- package/dist/reconnectHandler.js +21 -0
- package/dist/reconnectHandler.js.map +1 -0
- package/dist/runtime/runtimeConfig.d.ts +27 -0
- package/dist/runtime/runtimeConfig.d.ts.map +1 -0
- package/dist/runtime/runtimeConfig.js +133 -0
- package/dist/runtime/runtimeConfig.js.map +1 -0
- package/dist/stores/authState.d.ts +57 -0
- package/dist/stores/authState.d.ts.map +1 -0
- package/dist/stores/authState.js +154 -0
- package/dist/stores/authState.js.map +1 -0
- package/dist/stores/network.d.ts +9 -0
- package/dist/stores/network.d.ts.map +1 -0
- package/dist/stores/network.js +97 -0
- package/dist/stores/network.js.map +1 -0
- package/dist/stores/remoteChanges.d.ts +142 -0
- package/dist/stores/remoteChanges.d.ts.map +1 -0
- package/dist/stores/remoteChanges.js +353 -0
- package/dist/stores/remoteChanges.js.map +1 -0
- package/dist/stores/sync.d.ts +35 -0
- package/dist/stores/sync.d.ts.map +1 -0
- package/dist/stores/sync.js +115 -0
- package/dist/stores/sync.js.map +1 -0
- package/dist/supabase/auth.d.ts +60 -0
- package/dist/supabase/auth.d.ts.map +1 -0
- package/dist/supabase/auth.js +298 -0
- package/dist/supabase/auth.js.map +1 -0
- package/dist/supabase/client.d.ts +15 -0
- package/dist/supabase/client.d.ts.map +1 -0
- package/dist/supabase/client.js +149 -0
- package/dist/supabase/client.js.map +1 -0
- package/dist/supabase/validate.d.ts +11 -0
- package/dist/supabase/validate.d.ts.map +1 -0
- package/dist/supabase/validate.js +38 -0
- package/dist/supabase/validate.js.map +1 -0
- package/dist/types.d.ts +78 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +16 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +24 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +56 -0
- package/dist/utils.js.map +1 -0
- 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
|