stellar-drive 1.0.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 +607 -0
- package/dist/actions/remoteChange.d.ts +204 -0
- package/dist/actions/remoteChange.d.ts.map +1 -0
- package/dist/actions/remoteChange.js +424 -0
- package/dist/actions/remoteChange.js.map +1 -0
- package/dist/actions/truncateTooltip.d.ts +56 -0
- package/dist/actions/truncateTooltip.d.ts.map +1 -0
- package/dist/actions/truncateTooltip.js +312 -0
- package/dist/actions/truncateTooltip.js.map +1 -0
- package/dist/auth/crypto.d.ts +41 -0
- package/dist/auth/crypto.d.ts.map +1 -0
- package/dist/auth/crypto.js +50 -0
- package/dist/auth/crypto.js.map +1 -0
- package/dist/auth/deviceVerification.d.ts +283 -0
- package/dist/auth/deviceVerification.d.ts.map +1 -0
- package/dist/auth/deviceVerification.js +575 -0
- package/dist/auth/deviceVerification.js.map +1 -0
- package/dist/auth/displayUtils.d.ts +98 -0
- package/dist/auth/displayUtils.d.ts.map +1 -0
- package/dist/auth/displayUtils.js +145 -0
- package/dist/auth/displayUtils.js.map +1 -0
- package/dist/auth/loginGuard.d.ts +134 -0
- package/dist/auth/loginGuard.d.ts.map +1 -0
- package/dist/auth/loginGuard.js +276 -0
- package/dist/auth/loginGuard.js.map +1 -0
- package/dist/auth/offlineCredentials.d.ts +105 -0
- package/dist/auth/offlineCredentials.d.ts.map +1 -0
- package/dist/auth/offlineCredentials.js +176 -0
- package/dist/auth/offlineCredentials.js.map +1 -0
- package/dist/auth/offlineSession.d.ts +96 -0
- package/dist/auth/offlineSession.d.ts.map +1 -0
- package/dist/auth/offlineSession.js +145 -0
- package/dist/auth/offlineSession.js.map +1 -0
- package/dist/auth/resolveAuthState.d.ts +85 -0
- package/dist/auth/resolveAuthState.d.ts.map +1 -0
- package/dist/auth/resolveAuthState.js +249 -0
- package/dist/auth/resolveAuthState.js.map +1 -0
- package/dist/auth/singleUser.d.ts +498 -0
- package/dist/auth/singleUser.d.ts.map +1 -0
- package/dist/auth/singleUser.js +1282 -0
- package/dist/auth/singleUser.js.map +1 -0
- package/dist/bin/commands.d.ts +14 -0
- package/dist/bin/commands.d.ts.map +1 -0
- package/dist/bin/commands.js +68 -0
- package/dist/bin/commands.js.map +1 -0
- package/dist/bin/install-pwa.d.ts +41 -0
- package/dist/bin/install-pwa.d.ts.map +1 -0
- package/dist/bin/install-pwa.js +4594 -0
- package/dist/bin/install-pwa.js.map +1 -0
- package/dist/config.d.ts +249 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +395 -0
- package/dist/config.js.map +1 -0
- package/dist/conflicts.d.ts +306 -0
- package/dist/conflicts.d.ts.map +1 -0
- package/dist/conflicts.js +807 -0
- package/dist/conflicts.js.map +1 -0
- package/dist/crdt/awareness.d.ts +128 -0
- package/dist/crdt/awareness.d.ts.map +1 -0
- package/dist/crdt/awareness.js +284 -0
- package/dist/crdt/awareness.js.map +1 -0
- package/dist/crdt/channel.d.ts +165 -0
- package/dist/crdt/channel.d.ts.map +1 -0
- package/dist/crdt/channel.js +522 -0
- package/dist/crdt/channel.js.map +1 -0
- package/dist/crdt/config.d.ts +58 -0
- package/dist/crdt/config.d.ts.map +1 -0
- package/dist/crdt/config.js +123 -0
- package/dist/crdt/config.js.map +1 -0
- package/dist/crdt/helpers.d.ts +104 -0
- package/dist/crdt/helpers.d.ts.map +1 -0
- package/dist/crdt/helpers.js +116 -0
- package/dist/crdt/helpers.js.map +1 -0
- package/dist/crdt/offline.d.ts +58 -0
- package/dist/crdt/offline.d.ts.map +1 -0
- package/dist/crdt/offline.js +130 -0
- package/dist/crdt/offline.js.map +1 -0
- package/dist/crdt/persistence.d.ts +65 -0
- package/dist/crdt/persistence.d.ts.map +1 -0
- package/dist/crdt/persistence.js +171 -0
- package/dist/crdt/persistence.js.map +1 -0
- package/dist/crdt/provider.d.ts +109 -0
- package/dist/crdt/provider.d.ts.map +1 -0
- package/dist/crdt/provider.js +543 -0
- package/dist/crdt/provider.js.map +1 -0
- package/dist/crdt/store.d.ts +111 -0
- package/dist/crdt/store.d.ts.map +1 -0
- package/dist/crdt/store.js +158 -0
- package/dist/crdt/store.js.map +1 -0
- package/dist/crdt/types.d.ts +281 -0
- package/dist/crdt/types.d.ts.map +1 -0
- package/dist/crdt/types.js +26 -0
- package/dist/crdt/types.js.map +1 -0
- package/dist/data.d.ts +502 -0
- package/dist/data.d.ts.map +1 -0
- package/dist/data.js +862 -0
- package/dist/data.js.map +1 -0
- package/dist/database.d.ts +153 -0
- package/dist/database.d.ts.map +1 -0
- package/dist/database.js +325 -0
- package/dist/database.js.map +1 -0
- package/dist/debug.d.ts +87 -0
- package/dist/debug.d.ts.map +1 -0
- package/dist/debug.js +135 -0
- package/dist/debug.js.map +1 -0
- package/dist/demo.d.ts +131 -0
- package/dist/demo.d.ts.map +1 -0
- package/dist/demo.js +168 -0
- package/dist/demo.js.map +1 -0
- package/dist/deviceId.d.ts +47 -0
- package/dist/deviceId.d.ts.map +1 -0
- package/dist/deviceId.js +106 -0
- package/dist/deviceId.js.map +1 -0
- package/dist/diagnostics.d.ts +292 -0
- package/dist/diagnostics.d.ts.map +1 -0
- package/dist/diagnostics.js +378 -0
- package/dist/diagnostics.js.map +1 -0
- package/dist/engine.d.ts +230 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +2636 -0
- package/dist/engine.js.map +1 -0
- package/dist/entries/actions.d.ts +16 -0
- package/dist/entries/actions.d.ts.map +1 -0
- package/dist/entries/actions.js +29 -0
- package/dist/entries/actions.js.map +1 -0
- package/dist/entries/auth.d.ts +19 -0
- package/dist/entries/auth.d.ts.map +1 -0
- package/dist/entries/auth.js +50 -0
- package/dist/entries/auth.js.map +1 -0
- package/dist/entries/config.d.ts +15 -0
- package/dist/entries/config.d.ts.map +1 -0
- package/dist/entries/config.js +20 -0
- package/dist/entries/config.js.map +1 -0
- package/dist/entries/crdt.d.ts +32 -0
- package/dist/entries/crdt.d.ts.map +1 -0
- package/dist/entries/crdt.js +52 -0
- package/dist/entries/crdt.js.map +1 -0
- package/dist/entries/kit.d.ts +22 -0
- package/dist/entries/kit.d.ts.map +1 -0
- package/dist/entries/kit.js +58 -0
- package/dist/entries/kit.js.map +1 -0
- package/dist/entries/stores.d.ts +22 -0
- package/dist/entries/stores.d.ts.map +1 -0
- package/dist/entries/stores.js +57 -0
- package/dist/entries/stores.js.map +1 -0
- package/dist/entries/types.d.ts +23 -0
- package/dist/entries/types.d.ts.map +1 -0
- package/dist/entries/types.js +12 -0
- package/dist/entries/types.js.map +1 -0
- package/dist/entries/utils.d.ts +12 -0
- package/dist/entries/utils.d.ts.map +1 -0
- package/dist/entries/utils.js +42 -0
- package/dist/entries/utils.js.map +1 -0
- package/dist/entries/vite.d.ts +20 -0
- package/dist/entries/vite.d.ts.map +1 -0
- package/dist/entries/vite.js +26 -0
- package/dist/entries/vite.js.map +1 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +234 -0
- package/dist/index.js.map +1 -0
- package/dist/kit/auth.d.ts +80 -0
- package/dist/kit/auth.d.ts.map +1 -0
- package/dist/kit/auth.js +75 -0
- package/dist/kit/auth.js.map +1 -0
- package/dist/kit/confirm.d.ts +111 -0
- package/dist/kit/confirm.d.ts.map +1 -0
- package/dist/kit/confirm.js +169 -0
- package/dist/kit/confirm.js.map +1 -0
- package/dist/kit/loads.d.ts +187 -0
- package/dist/kit/loads.d.ts.map +1 -0
- package/dist/kit/loads.js +208 -0
- package/dist/kit/loads.js.map +1 -0
- package/dist/kit/server.d.ts +175 -0
- package/dist/kit/server.d.ts.map +1 -0
- package/dist/kit/server.js +297 -0
- package/dist/kit/server.js.map +1 -0
- package/dist/kit/sw.d.ts +176 -0
- package/dist/kit/sw.d.ts.map +1 -0
- package/dist/kit/sw.js +320 -0
- package/dist/kit/sw.js.map +1 -0
- package/dist/queue.d.ts +306 -0
- package/dist/queue.d.ts.map +1 -0
- package/dist/queue.js +925 -0
- package/dist/queue.js.map +1 -0
- package/dist/realtime.d.ts +280 -0
- package/dist/realtime.d.ts.map +1 -0
- package/dist/realtime.js +1031 -0
- package/dist/realtime.js.map +1 -0
- package/dist/runtime/runtimeConfig.d.ts +110 -0
- package/dist/runtime/runtimeConfig.d.ts.map +1 -0
- package/dist/runtime/runtimeConfig.js +260 -0
- package/dist/runtime/runtimeConfig.js.map +1 -0
- package/dist/schema.d.ts +150 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +891 -0
- package/dist/schema.js.map +1 -0
- package/dist/stores/authState.d.ts +204 -0
- package/dist/stores/authState.d.ts.map +1 -0
- package/dist/stores/authState.js +336 -0
- package/dist/stores/authState.js.map +1 -0
- package/dist/stores/factories.d.ts +140 -0
- package/dist/stores/factories.d.ts.map +1 -0
- package/dist/stores/factories.js +157 -0
- package/dist/stores/factories.js.map +1 -0
- package/dist/stores/network.d.ts +48 -0
- package/dist/stores/network.d.ts.map +1 -0
- package/dist/stores/network.js +261 -0
- package/dist/stores/network.js.map +1 -0
- package/dist/stores/remoteChanges.d.ts +417 -0
- package/dist/stores/remoteChanges.d.ts.map +1 -0
- package/dist/stores/remoteChanges.js +626 -0
- package/dist/stores/remoteChanges.js.map +1 -0
- package/dist/stores/sync.d.ts +165 -0
- package/dist/stores/sync.d.ts.map +1 -0
- package/dist/stores/sync.js +275 -0
- package/dist/stores/sync.js.map +1 -0
- package/dist/supabase/auth.d.ts +219 -0
- package/dist/supabase/auth.d.ts.map +1 -0
- package/dist/supabase/auth.js +459 -0
- package/dist/supabase/auth.js.map +1 -0
- package/dist/supabase/client.d.ts +88 -0
- package/dist/supabase/client.d.ts.map +1 -0
- package/dist/supabase/client.js +313 -0
- package/dist/supabase/client.js.map +1 -0
- package/dist/supabase/validate.d.ts +118 -0
- package/dist/supabase/validate.d.ts.map +1 -0
- package/dist/supabase/validate.js +208 -0
- package/dist/supabase/validate.js.map +1 -0
- package/dist/sw/build/vite-plugin.d.ts +149 -0
- package/dist/sw/build/vite-plugin.d.ts.map +1 -0
- package/dist/sw/build/vite-plugin.js +517 -0
- package/dist/sw/build/vite-plugin.js.map +1 -0
- package/dist/sw/sw.js +664 -0
- package/dist/types.d.ts +363 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +18 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +85 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +156 -0
- package/dist/utils.js.map +1 -0
- package/package.json +117 -0
- package/src/components/DeferredChangesBanner.svelte +477 -0
- package/src/components/DemoBanner.svelte +110 -0
- package/src/components/SyncStatus.svelte +1732 -0
package/package.json
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "stellar-drive",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./data": {
|
|
13
|
+
"types": "./dist/data.d.ts",
|
|
14
|
+
"import": "./dist/data.js"
|
|
15
|
+
},
|
|
16
|
+
"./auth": {
|
|
17
|
+
"types": "./dist/entries/auth.d.ts",
|
|
18
|
+
"import": "./dist/entries/auth.js"
|
|
19
|
+
},
|
|
20
|
+
"./stores": {
|
|
21
|
+
"types": "./dist/entries/stores.d.ts",
|
|
22
|
+
"import": "./dist/entries/stores.js"
|
|
23
|
+
},
|
|
24
|
+
"./types": {
|
|
25
|
+
"types": "./dist/entries/types.d.ts",
|
|
26
|
+
"import": "./dist/entries/types.js"
|
|
27
|
+
},
|
|
28
|
+
"./utils": {
|
|
29
|
+
"types": "./dist/entries/utils.d.ts",
|
|
30
|
+
"import": "./dist/entries/utils.js"
|
|
31
|
+
},
|
|
32
|
+
"./actions": {
|
|
33
|
+
"types": "./dist/entries/actions.d.ts",
|
|
34
|
+
"import": "./dist/entries/actions.js"
|
|
35
|
+
},
|
|
36
|
+
"./config": {
|
|
37
|
+
"types": "./dist/entries/config.d.ts",
|
|
38
|
+
"import": "./dist/entries/config.js"
|
|
39
|
+
},
|
|
40
|
+
"./vite": {
|
|
41
|
+
"types": "./dist/entries/vite.d.ts",
|
|
42
|
+
"import": "./dist/entries/vite.js"
|
|
43
|
+
},
|
|
44
|
+
"./kit": {
|
|
45
|
+
"types": "./dist/entries/kit.d.ts",
|
|
46
|
+
"import": "./dist/entries/kit.js"
|
|
47
|
+
},
|
|
48
|
+
"./crdt": {
|
|
49
|
+
"types": "./dist/entries/crdt.d.ts",
|
|
50
|
+
"import": "./dist/entries/crdt.js"
|
|
51
|
+
},
|
|
52
|
+
"./components/SyncStatus": {
|
|
53
|
+
"svelte": "./src/components/SyncStatus.svelte",
|
|
54
|
+
"default": "./src/components/SyncStatus.svelte"
|
|
55
|
+
},
|
|
56
|
+
"./components/DeferredChangesBanner": {
|
|
57
|
+
"svelte": "./src/components/DeferredChangesBanner.svelte",
|
|
58
|
+
"default": "./src/components/DeferredChangesBanner.svelte"
|
|
59
|
+
},
|
|
60
|
+
"./components/DemoBanner": {
|
|
61
|
+
"svelte": "./src/components/DemoBanner.svelte",
|
|
62
|
+
"default": "./src/components/DemoBanner.svelte"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"bin": {
|
|
66
|
+
"stellar-drive": "./dist/bin/commands.js"
|
|
67
|
+
},
|
|
68
|
+
"files": [
|
|
69
|
+
"dist",
|
|
70
|
+
"src/components"
|
|
71
|
+
],
|
|
72
|
+
"publishConfig": {
|
|
73
|
+
"access": "public",
|
|
74
|
+
"registry": "https://registry.npmjs.org"
|
|
75
|
+
},
|
|
76
|
+
"scripts": {
|
|
77
|
+
"build": "tsc && tsc -p tsconfig.sw.json",
|
|
78
|
+
"check": "tsc --noEmit",
|
|
79
|
+
"lint": "eslint src",
|
|
80
|
+
"lint:fix": "eslint src --fix",
|
|
81
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
82
|
+
"format:check": "prettier --check \"src/**/*.ts\"",
|
|
83
|
+
"dead-code": "knip",
|
|
84
|
+
"dead-code:fix": "knip --fix",
|
|
85
|
+
"cleanup": "npm run lint:fix && npm run format",
|
|
86
|
+
"validate": "npm run check && npm run lint && npm run dead-code",
|
|
87
|
+
"prepare": "git config core.hooksPath .githooks",
|
|
88
|
+
"prepublishOnly": "npm run build",
|
|
89
|
+
"release": "node release.js"
|
|
90
|
+
},
|
|
91
|
+
"dependencies": {
|
|
92
|
+
"@clack/prompts": "^1.0.1",
|
|
93
|
+
"@supabase/supabase-js": "^2.49.0",
|
|
94
|
+
"dexie": "^4.2.1",
|
|
95
|
+
"picocolors": "^1.1.1",
|
|
96
|
+
"yjs": "^13.6.0"
|
|
97
|
+
},
|
|
98
|
+
"peerDependencies": {
|
|
99
|
+
"svelte": "^5.0.0"
|
|
100
|
+
},
|
|
101
|
+
"peerDependenciesMeta": {
|
|
102
|
+
"svelte": {
|
|
103
|
+
"optional": true
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
"devDependencies": {
|
|
107
|
+
"@eslint/js": "^9.39.2",
|
|
108
|
+
"esbuild": "^0.27.3",
|
|
109
|
+
"eslint": "^9.39.2",
|
|
110
|
+
"globals": "^17.3.0",
|
|
111
|
+
"knip": "^5.82.1",
|
|
112
|
+
"prettier": "^3.8.1",
|
|
113
|
+
"svelte": "^5.0.0",
|
|
114
|
+
"typescript": "^5.0.0",
|
|
115
|
+
"typescript-eslint": "^8.54.0"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview DeferredChangesBanner — notification banner for cross-device data conflicts.
|
|
4
|
+
*
|
|
5
|
+
* When another device pushes a change to an entity (e.g. a goal or project) via
|
|
6
|
+
* realtime sync, but the user is currently editing that entity, this banner
|
|
7
|
+
* appears to let them choose:
|
|
8
|
+
* - **Update** — overwrite local form state with the remote values
|
|
9
|
+
* - **Dismiss** — keep local edits and discard the remote notification
|
|
10
|
+
* - **Show/Hide changes** — expand a diff preview showing field-by-field
|
|
11
|
+
* old → new values
|
|
12
|
+
*
|
|
13
|
+
* The banner polls `remoteChangesStore` every 500ms to detect deferred changes,
|
|
14
|
+
* and uses a CSS `grid-template-rows` transition for smooth expand/collapse.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
// =============================================================================
|
|
18
|
+
// Imports
|
|
19
|
+
// =============================================================================
|
|
20
|
+
|
|
21
|
+
import { remoteChangesStore } from 'stellar-drive/stores';
|
|
22
|
+
import { onMount, onDestroy } from 'svelte';
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Props Interface
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
interface Props {
|
|
29
|
+
/** Unique identifier of the entity being edited */
|
|
30
|
+
entityId: string;
|
|
31
|
+
/** Entity collection name (e.g. `'goals'`, `'projects'`) */
|
|
32
|
+
entityType: string;
|
|
33
|
+
/** Remote (latest) data snapshot — `null` when no diff exists */
|
|
34
|
+
remoteData: Record<string, unknown> | null;
|
|
35
|
+
/** Current local form data to compare against */
|
|
36
|
+
localData: Record<string, unknown>;
|
|
37
|
+
/** Map of field keys → human-readable labels (e.g. `{ name: 'Name' }`) */
|
|
38
|
+
fieldLabels: Record<string, string>;
|
|
39
|
+
/** Optional custom formatter for display values — receives `(fieldKey, rawValue)` */
|
|
40
|
+
formatValue?: (field: string, value: unknown) => string;
|
|
41
|
+
/** Callback to apply the remote data into the local form */
|
|
42
|
+
onLoadRemote: () => void;
|
|
43
|
+
/** Callback to silently discard the remote notification */
|
|
44
|
+
onDismiss: () => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// =============================================================================
|
|
48
|
+
// Component State
|
|
49
|
+
// =============================================================================
|
|
50
|
+
|
|
51
|
+
let {
|
|
52
|
+
entityId,
|
|
53
|
+
entityType,
|
|
54
|
+
remoteData,
|
|
55
|
+
localData,
|
|
56
|
+
fieldLabels,
|
|
57
|
+
formatValue,
|
|
58
|
+
onLoadRemote,
|
|
59
|
+
onDismiss
|
|
60
|
+
}: Props = $props();
|
|
61
|
+
|
|
62
|
+
/** Controls the banner's visibility (drives CSS transition) */
|
|
63
|
+
let showBanner = $state(false);
|
|
64
|
+
|
|
65
|
+
/** Whether the diff-preview section is expanded */
|
|
66
|
+
let showPreview = $state(false);
|
|
67
|
+
|
|
68
|
+
/** Interval handle for polling deferred changes */
|
|
69
|
+
let checkInterval: ReturnType<typeof setInterval> | null = null;
|
|
70
|
+
|
|
71
|
+
// =============================================================================
|
|
72
|
+
// Diff Calculation
|
|
73
|
+
// =============================================================================
|
|
74
|
+
|
|
75
|
+
/** Shape of a single field difference */
|
|
76
|
+
interface FieldDiff {
|
|
77
|
+
field: string;
|
|
78
|
+
label: string;
|
|
79
|
+
oldValue: string;
|
|
80
|
+
newValue: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Computes an array of `FieldDiff` objects by comparing `localData` against
|
|
85
|
+
* `remoteData` for every key listed in `fieldLabels`. Uses JSON.stringify
|
|
86
|
+
* for deep equality (handles arrays, nested objects).
|
|
87
|
+
*/
|
|
88
|
+
const diffs = $derived.by(() => {
|
|
89
|
+
if (!remoteData) return [] as FieldDiff[];
|
|
90
|
+
const result: FieldDiff[] = [];
|
|
91
|
+
for (const [field, label] of Object.entries(fieldLabels)) {
|
|
92
|
+
const local = localData[field];
|
|
93
|
+
const remote = remoteData[field];
|
|
94
|
+
if (remote !== undefined && JSON.stringify(local) !== JSON.stringify(remote)) {
|
|
95
|
+
const fmt = formatValue || defaultFormat;
|
|
96
|
+
result.push({
|
|
97
|
+
field,
|
|
98
|
+
label,
|
|
99
|
+
oldValue: fmt(field, local),
|
|
100
|
+
newValue: fmt(field, remote)
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return result;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// =============================================================================
|
|
108
|
+
// Utility Functions
|
|
109
|
+
// =============================================================================
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Default value formatter — converts booleans, nulls, arrays, and
|
|
113
|
+
* other types to human-readable strings.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} _field — the field key (unused in default formatter)
|
|
116
|
+
* @param {unknown} value — the raw field value
|
|
117
|
+
* @returns {string} formatted string representation
|
|
118
|
+
*/
|
|
119
|
+
function defaultFormat(_field: string, value: unknown): string {
|
|
120
|
+
if (typeof value === 'boolean') return value ? 'On' : 'Off';
|
|
121
|
+
if (value === null || value === undefined) return 'None';
|
|
122
|
+
if (Array.isArray(value)) return value.join(', ');
|
|
123
|
+
return String(value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Checks the remote changes store for deferred changes targeting this entity.
|
|
128
|
+
* Shows the banner if changes are found.
|
|
129
|
+
*/
|
|
130
|
+
function checkDeferred() {
|
|
131
|
+
const has = remoteChangesStore.hasDeferredChanges(entityId, entityType);
|
|
132
|
+
if (has && !showBanner) {
|
|
133
|
+
showBanner = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// =============================================================================
|
|
138
|
+
// Lifecycle
|
|
139
|
+
// =============================================================================
|
|
140
|
+
|
|
141
|
+
onMount(() => {
|
|
142
|
+
checkDeferred();
|
|
143
|
+
/* Poll every 500ms — lightweight check against in-memory store */
|
|
144
|
+
checkInterval = setInterval(checkDeferred, 500);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
onDestroy(() => {
|
|
148
|
+
if (checkInterval) clearInterval(checkInterval);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// =============================================================================
|
|
152
|
+
// Action Handlers
|
|
153
|
+
// =============================================================================
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Applies remote data into the form and clears the deferred-change
|
|
157
|
+
* record so polling does not re-show the banner.
|
|
158
|
+
*/
|
|
159
|
+
function handleLoadRemote() {
|
|
160
|
+
remoteChangesStore.clearDeferredChanges(entityId, entityType);
|
|
161
|
+
showBanner = false;
|
|
162
|
+
showPreview = false;
|
|
163
|
+
onLoadRemote();
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Silently dismisses the banner and clears the deferred-change record.
|
|
168
|
+
*/
|
|
169
|
+
function handleDismiss() {
|
|
170
|
+
remoteChangesStore.clearDeferredChanges(entityId, entityType);
|
|
171
|
+
showBanner = false;
|
|
172
|
+
showPreview = false;
|
|
173
|
+
onDismiss();
|
|
174
|
+
}
|
|
175
|
+
</script>
|
|
176
|
+
|
|
177
|
+
<!-- Always rendered; CSS controls visibility via grid-template-rows transition -->
|
|
178
|
+
<div class="deferred-banner-wrapper" class:show={showBanner}>
|
|
179
|
+
<div class="deferred-banner">
|
|
180
|
+
<!-- ═══ Banner Header ═══ -->
|
|
181
|
+
<div class="banner-header">
|
|
182
|
+
<span class="banner-icon">
|
|
183
|
+
<svg
|
|
184
|
+
viewBox="0 0 24 24"
|
|
185
|
+
fill="none"
|
|
186
|
+
stroke="currentColor"
|
|
187
|
+
stroke-width="2"
|
|
188
|
+
width="16"
|
|
189
|
+
height="16"
|
|
190
|
+
>
|
|
191
|
+
<circle cx="12" cy="12" r="10" />
|
|
192
|
+
<line x1="12" y1="8" x2="12" y2="12" />
|
|
193
|
+
<line x1="12" y1="16" x2="12.01" y2="16" />
|
|
194
|
+
</svg>
|
|
195
|
+
</span>
|
|
196
|
+
<span class="banner-text">Changes were made on another device</span>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
<!-- ═══ Banner Actions ═══ -->
|
|
200
|
+
<div class="banner-actions">
|
|
201
|
+
<button class="banner-btn update-btn" onclick={handleLoadRemote} type="button">
|
|
202
|
+
Update
|
|
203
|
+
</button>
|
|
204
|
+
<button class="banner-btn dismiss-btn" onclick={handleDismiss} type="button">
|
|
205
|
+
Dismiss
|
|
206
|
+
</button>
|
|
207
|
+
<!-- Toggle to show/hide field-by-field diff preview -->
|
|
208
|
+
{#if diffs.length > 0}
|
|
209
|
+
<button class="toggle-preview" onclick={() => (showPreview = !showPreview)} type="button">
|
|
210
|
+
{showPreview ? 'Hide' : 'Show'} changes
|
|
211
|
+
<svg
|
|
212
|
+
class="chevron"
|
|
213
|
+
class:expanded={showPreview}
|
|
214
|
+
viewBox="0 0 24 24"
|
|
215
|
+
fill="none"
|
|
216
|
+
stroke="currentColor"
|
|
217
|
+
stroke-width="2"
|
|
218
|
+
width="14"
|
|
219
|
+
height="14"
|
|
220
|
+
>
|
|
221
|
+
<polyline points="6 9 12 15 18 9" />
|
|
222
|
+
</svg>
|
|
223
|
+
</button>
|
|
224
|
+
{/if}
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<!-- ═══ Diff Preview (expandable) ═══ -->
|
|
228
|
+
{#if showPreview && diffs.length > 0}
|
|
229
|
+
<div class="diff-preview">
|
|
230
|
+
{#each diffs as diff (diff.label)}
|
|
231
|
+
<div class="diff-row">
|
|
232
|
+
<span class="diff-label">{diff.label}:</span>
|
|
233
|
+
<span class="diff-old">{diff.oldValue}</span>
|
|
234
|
+
<span class="diff-arrow">→</span>
|
|
235
|
+
<span class="diff-new">{diff.newValue}</span>
|
|
236
|
+
</div>
|
|
237
|
+
{/each}
|
|
238
|
+
</div>
|
|
239
|
+
{/if}
|
|
240
|
+
</div>
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<style>
|
|
244
|
+
/* ═══ Banner Wrapper (animated visibility) ═══ */
|
|
245
|
+
|
|
246
|
+
/*
|
|
247
|
+
* Uses `grid-template-rows: 0fr → 1fr` for smooth height animation.
|
|
248
|
+
* This avoids the need for explicit height values or JS measurement.
|
|
249
|
+
*/
|
|
250
|
+
.deferred-banner-wrapper {
|
|
251
|
+
display: grid;
|
|
252
|
+
grid-template-rows: 0fr;
|
|
253
|
+
opacity: 0;
|
|
254
|
+
transition:
|
|
255
|
+
grid-template-rows 0.4s var(--ease-spring),
|
|
256
|
+
opacity 0.3s var(--ease-out);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.deferred-banner-wrapper.show {
|
|
260
|
+
grid-template-rows: 1fr;
|
|
261
|
+
opacity: 1;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* Overflow hidden on the inner element enables the grid-row trick */
|
|
265
|
+
.deferred-banner-wrapper > .deferred-banner {
|
|
266
|
+
overflow: hidden;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
@media (prefers-reduced-motion: reduce) {
|
|
270
|
+
.deferred-banner-wrapper {
|
|
271
|
+
transition: none;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/* ═══ Banner Container ═══ */
|
|
276
|
+
|
|
277
|
+
.deferred-banner {
|
|
278
|
+
background: linear-gradient(135deg, rgba(255, 165, 2, 0.12) 0%, rgba(255, 165, 2, 0.06) 100%);
|
|
279
|
+
border: 1px solid rgba(255, 165, 2, 0.3);
|
|
280
|
+
border-radius: var(--radius-lg);
|
|
281
|
+
padding: 0.75rem 1rem;
|
|
282
|
+
margin-bottom: 1rem;
|
|
283
|
+
backdrop-filter: blur(8px);
|
|
284
|
+
-webkit-backdrop-filter: blur(8px);
|
|
285
|
+
animation: bannerGlow 3s ease-in-out infinite;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/* Subtle pulsing glow to draw attention */
|
|
289
|
+
@keyframes bannerGlow {
|
|
290
|
+
0%,
|
|
291
|
+
100% {
|
|
292
|
+
box-shadow: 0 0 8px rgba(255, 165, 2, 0.15);
|
|
293
|
+
}
|
|
294
|
+
50% {
|
|
295
|
+
box-shadow: 0 0 16px rgba(255, 165, 2, 0.25);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* ═══ Header ═══ */
|
|
300
|
+
|
|
301
|
+
.banner-header {
|
|
302
|
+
display: flex;
|
|
303
|
+
align-items: center;
|
|
304
|
+
gap: 0.5rem;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.banner-icon {
|
|
308
|
+
color: var(--color-orange);
|
|
309
|
+
flex-shrink: 0;
|
|
310
|
+
display: flex;
|
|
311
|
+
align-items: center;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
.banner-text {
|
|
315
|
+
font-size: 0.8125rem;
|
|
316
|
+
font-weight: 600;
|
|
317
|
+
color: var(--color-orange);
|
|
318
|
+
white-space: nowrap;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/* ═══ Actions Row ═══ */
|
|
322
|
+
|
|
323
|
+
.banner-actions {
|
|
324
|
+
display: flex;
|
|
325
|
+
align-items: center;
|
|
326
|
+
gap: 0.5rem;
|
|
327
|
+
margin-top: 0.5rem;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/* "Show changes" toggle — pushed to the right via `margin-left: auto` */
|
|
331
|
+
.toggle-preview {
|
|
332
|
+
display: inline-flex;
|
|
333
|
+
align-items: center;
|
|
334
|
+
gap: 0.25rem;
|
|
335
|
+
font-size: 0.6875rem;
|
|
336
|
+
font-weight: 500;
|
|
337
|
+
color: var(--color-text-muted);
|
|
338
|
+
background: none;
|
|
339
|
+
border: none;
|
|
340
|
+
padding: 0.125rem 0.375rem;
|
|
341
|
+
border-radius: var(--radius-sm);
|
|
342
|
+
cursor: pointer;
|
|
343
|
+
transition: color 0.2s;
|
|
344
|
+
white-space: nowrap;
|
|
345
|
+
margin-left: auto;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.toggle-preview:hover {
|
|
349
|
+
color: var(--color-text);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/* Chevron rotates 180deg when expanded */
|
|
353
|
+
.chevron {
|
|
354
|
+
transition: transform 0.2s var(--ease-smooth);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.chevron.expanded {
|
|
358
|
+
transform: rotate(180deg);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* ═══ Action Buttons ═══ */
|
|
362
|
+
|
|
363
|
+
.banner-btn {
|
|
364
|
+
padding: 0.375rem 0.75rem;
|
|
365
|
+
font-size: 0.75rem;
|
|
366
|
+
font-weight: 600;
|
|
367
|
+
border-radius: var(--radius-md);
|
|
368
|
+
cursor: pointer;
|
|
369
|
+
transition: all 0.2s var(--ease-smooth);
|
|
370
|
+
border: none;
|
|
371
|
+
white-space: nowrap;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/* Update button — orange theme to match the warning banner */
|
|
375
|
+
.update-btn {
|
|
376
|
+
background: rgba(255, 165, 2, 0.2);
|
|
377
|
+
color: var(--color-orange);
|
|
378
|
+
border: 1px solid rgba(255, 165, 2, 0.3);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.update-btn:hover {
|
|
382
|
+
background: rgba(255, 165, 2, 0.3);
|
|
383
|
+
box-shadow: 0 0 12px rgba(255, 165, 2, 0.2);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* Dismiss button — neutral / muted */
|
|
387
|
+
.dismiss-btn {
|
|
388
|
+
background: rgba(255, 255, 255, 0.05);
|
|
389
|
+
color: var(--color-text-muted);
|
|
390
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.dismiss-btn:hover {
|
|
394
|
+
background: rgba(255, 255, 255, 0.1);
|
|
395
|
+
color: var(--color-text);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/* ═══ Diff Preview ═══ */
|
|
399
|
+
|
|
400
|
+
.diff-preview {
|
|
401
|
+
margin-top: 0.625rem;
|
|
402
|
+
padding-top: 0.625rem;
|
|
403
|
+
border-top: 1px solid rgba(255, 165, 2, 0.15);
|
|
404
|
+
display: flex;
|
|
405
|
+
flex-direction: column;
|
|
406
|
+
gap: 0.375rem;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.diff-row {
|
|
410
|
+
display: flex;
|
|
411
|
+
align-items: center;
|
|
412
|
+
gap: 0.375rem;
|
|
413
|
+
font-size: 0.75rem;
|
|
414
|
+
flex-wrap: wrap;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.diff-label {
|
|
418
|
+
color: var(--color-text-secondary);
|
|
419
|
+
font-weight: 600;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/* Strikethrough on old value to visually indicate replacement */
|
|
423
|
+
.diff-old {
|
|
424
|
+
color: var(--color-text-muted);
|
|
425
|
+
text-decoration: line-through;
|
|
426
|
+
opacity: 0.7;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.diff-arrow {
|
|
430
|
+
color: var(--color-text-muted);
|
|
431
|
+
font-size: 0.625rem;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* New value highlighted in orange to match the banner theme */
|
|
435
|
+
.diff-new {
|
|
436
|
+
color: var(--color-orange);
|
|
437
|
+
font-weight: 600;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/* ═══ Mobile Responsive ═══ */
|
|
441
|
+
|
|
442
|
+
@media (max-width: 480px) {
|
|
443
|
+
.deferred-banner {
|
|
444
|
+
padding: 0.5rem 0.75rem;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
.banner-header {
|
|
448
|
+
margin-bottom: 0.125rem;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
.banner-actions {
|
|
452
|
+
flex-wrap: wrap;
|
|
453
|
+
gap: 0.375rem;
|
|
454
|
+
margin-top: 0.375rem;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
.banner-btn {
|
|
458
|
+
text-align: center;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/* Full-width toggle on mobile for easier tap target */
|
|
462
|
+
.toggle-preview {
|
|
463
|
+
flex-basis: 100%;
|
|
464
|
+
justify-content: center;
|
|
465
|
+
margin-left: 0;
|
|
466
|
+
padding: 0.25rem 0;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/* ═══ Tablet ═══ */
|
|
471
|
+
|
|
472
|
+
@media (min-width: 481px) and (max-width: 900px) {
|
|
473
|
+
.deferred-banner {
|
|
474
|
+
padding: 0.625rem 0.875rem;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
</style>
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@fileoverview DemoBanner — fixed-position notification bar for demo mode.
|
|
3
|
+
|
|
4
|
+
Renders a subtle, dismissible banner at the bottom center of the viewport
|
|
5
|
+
when the app is running in demo mode. Informs the user that changes will
|
|
6
|
+
reset on refresh.
|
|
7
|
+
|
|
8
|
+
- Only renders when `isDemoMode()` returns `true`.
|
|
9
|
+
- Dismissible via the close button (component-local state).
|
|
10
|
+
- Glass morphism styling with backdrop-filter blur.
|
|
11
|
+
- z-index 9000 — above page content, below modals.
|
|
12
|
+
-->
|
|
13
|
+
<script lang="ts">
|
|
14
|
+
import { isDemoMode } from 'stellar-drive';
|
|
15
|
+
|
|
16
|
+
let dismissed = $state(false);
|
|
17
|
+
const visible = $derived(isDemoMode() && !dismissed);
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
{#if visible}
|
|
21
|
+
<div class="demo-banner" role="status" aria-live="polite">
|
|
22
|
+
<span class="demo-banner-text">Demo Mode — Changes reset on refresh</span>
|
|
23
|
+
<a class="demo-banner-link" href="/demo">Demo Page</a>
|
|
24
|
+
<button
|
|
25
|
+
class="demo-banner-close"
|
|
26
|
+
onclick={() => (dismissed = true)}
|
|
27
|
+
aria-label="Dismiss demo mode banner"
|
|
28
|
+
>
|
|
29
|
+
✕
|
|
30
|
+
</button>
|
|
31
|
+
</div>
|
|
32
|
+
{/if}
|
|
33
|
+
|
|
34
|
+
<style>
|
|
35
|
+
.demo-banner {
|
|
36
|
+
position: fixed;
|
|
37
|
+
bottom: 1rem;
|
|
38
|
+
left: 50%;
|
|
39
|
+
transform: translateX(-50%);
|
|
40
|
+
z-index: 9000;
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
gap: 0.75rem;
|
|
44
|
+
padding: 0.5rem 1rem;
|
|
45
|
+
border-radius: 9999px;
|
|
46
|
+
background: rgba(0, 0, 0, 0.6);
|
|
47
|
+
backdrop-filter: blur(12px);
|
|
48
|
+
-webkit-backdrop-filter: blur(12px);
|
|
49
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
50
|
+
color: #fff;
|
|
51
|
+
font-size: 0.8125rem;
|
|
52
|
+
font-weight: 500;
|
|
53
|
+
letter-spacing: 0.01em;
|
|
54
|
+
white-space: nowrap;
|
|
55
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
|
|
56
|
+
pointer-events: auto;
|
|
57
|
+
animation: demo-banner-slide-up 0.3s ease-out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.demo-banner-text {
|
|
61
|
+
opacity: 0.95;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.demo-banner-link {
|
|
65
|
+
color: rgba(255, 255, 255, 0.65);
|
|
66
|
+
font-size: 0.75rem;
|
|
67
|
+
text-decoration: none;
|
|
68
|
+
padding: 0.15rem 0.5rem;
|
|
69
|
+
border-radius: 4px;
|
|
70
|
+
background: rgba(255, 255, 255, 0.08);
|
|
71
|
+
transition: background 0.15s ease, color 0.15s ease;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.demo-banner-link:hover {
|
|
75
|
+
background: rgba(255, 255, 255, 0.18);
|
|
76
|
+
color: #fff;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.demo-banner-close {
|
|
80
|
+
display: flex;
|
|
81
|
+
align-items: center;
|
|
82
|
+
justify-content: center;
|
|
83
|
+
width: 1.25rem;
|
|
84
|
+
height: 1.25rem;
|
|
85
|
+
padding: 0;
|
|
86
|
+
border: none;
|
|
87
|
+
border-radius: 50%;
|
|
88
|
+
background: rgba(255, 255, 255, 0.15);
|
|
89
|
+
color: rgba(255, 255, 255, 0.8);
|
|
90
|
+
font-size: 0.625rem;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
transition: background 0.15s ease;
|
|
93
|
+
flex-shrink: 0;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.demo-banner-close:hover {
|
|
97
|
+
background: rgba(255, 255, 255, 0.25);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@keyframes demo-banner-slide-up {
|
|
101
|
+
from {
|
|
102
|
+
opacity: 0;
|
|
103
|
+
transform: translateX(-50%) translateY(1rem);
|
|
104
|
+
}
|
|
105
|
+
to {
|
|
106
|
+
opacity: 1;
|
|
107
|
+
transform: translateX(-50%) translateY(0);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
</style>
|