stellar-drive 1.2.28 → 1.2.29

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.
@@ -0,0 +1,168 @@
1
+ <!--
2
+ @fileoverview OfflineToast — app-neutral chunk-load error recovery toast.
3
+
4
+ Mounts a single `unhandledrejection` listener and surfaces a friendly
5
+ "page not available offline" message whenever a dynamic import fails because
6
+ its JS chunk is not in the service-worker cache.
7
+
8
+ - Mount once in your root `+layout.svelte`. No props required.
9
+ - Auto-dismisses after 5 seconds. Dismiss button available immediately.
10
+ - Positioned top-center, above all navigation chrome (z-index 1500).
11
+ - Styled with the stellar-drive design tokens — works in any theme.
12
+
13
+ Example mount:
14
+ ```svelte
15
+ import OfflineToast from 'stellar-drive/components/OfflineToast';
16
+ <OfflineToast />
17
+ ```
18
+ -->
19
+ <script lang="ts">
20
+ // ==========================================================================
21
+ // COMPONENT STATE
22
+ // ==========================================================================
23
+
24
+ /** Whether the toast is currently visible. */
25
+ let visible = $state(false);
26
+
27
+ /** Text to display in the toast. */
28
+ let message = $state('');
29
+
30
+ /** Auto-dismiss timer reference. */
31
+ let timer: ReturnType<typeof setTimeout> | null = null;
32
+
33
+ // ==========================================================================
34
+ // HELPERS
35
+ // ==========================================================================
36
+
37
+ function show(msg: string, durationMs = 5000) {
38
+ if (timer) clearTimeout(timer);
39
+ message = msg;
40
+ visible = true;
41
+ timer = setTimeout(dismiss, durationMs);
42
+ }
43
+
44
+ function dismiss() {
45
+ visible = false;
46
+ if (timer) {
47
+ clearTimeout(timer);
48
+ timer = null;
49
+ }
50
+ }
51
+
52
+ // ==========================================================================
53
+ // CHUNK ERROR LISTENER
54
+ // ==========================================================================
55
+
56
+ $effect(() => {
57
+ function handleRejection(event: PromiseRejectionEvent) {
58
+ const error = event.reason;
59
+ const isChunkError =
60
+ error?.message?.includes('Failed to fetch dynamically imported module') ||
61
+ error?.message?.includes('error loading dynamically imported module') ||
62
+ error?.message?.includes('Importing a module script failed') ||
63
+ error?.name === 'ChunkLoadError' ||
64
+ (error?.message?.includes('Loading chunk') && error?.message?.includes('failed'));
65
+
66
+ if (isChunkError) {
67
+ event.preventDefault();
68
+ show("This page isn't available offline. Please reconnect or go back.");
69
+ }
70
+ }
71
+
72
+ window.addEventListener('unhandledrejection', handleRejection);
73
+ return () => window.removeEventListener('unhandledrejection', handleRejection);
74
+ });
75
+ </script>
76
+
77
+ {#if visible}
78
+ <div class="offline-toast" role="alert" aria-live="polite">
79
+ <div class="offline-toast-icon" aria-hidden="true">
80
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
81
+ <circle cx="12" cy="12" r="10"/>
82
+ <line x1="12" y1="16" x2="12" y2="12"/>
83
+ <line x1="12" y1="8" x2="12.01" y2="8"/>
84
+ </svg>
85
+ </div>
86
+ <span class="offline-toast-message">{message}</span>
87
+ <button class="offline-toast-dismiss" onclick={dismiss} aria-label="Dismiss">
88
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round">
89
+ <line x1="18" y1="6" x2="6" y2="18"/>
90
+ <line x1="6" y1="6" x2="18" y2="18"/>
91
+ </svg>
92
+ </button>
93
+ </div>
94
+ {/if}
95
+
96
+ <style>
97
+ .offline-toast {
98
+ position: fixed;
99
+ top: calc(env(safe-area-inset-top, 0px) + 1rem);
100
+ left: 50%;
101
+ transform: translateX(-50%);
102
+ z-index: 1500;
103
+ display: flex;
104
+ align-items: center;
105
+ gap: 0.625rem;
106
+ padding: 0.5rem 0.75rem 0.5rem 0.875rem;
107
+ background: rgba(0, 0, 0, 0.6);
108
+ border: 1px solid rgba(255, 255, 255, 0.15);
109
+ border-radius: 9999px;
110
+ backdrop-filter: blur(12px);
111
+ -webkit-backdrop-filter: blur(12px);
112
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
113
+ color: #fff;
114
+ font-size: 0.8125rem;
115
+ font-weight: 500;
116
+ white-space: nowrap;
117
+ max-width: calc(100vw - 2rem);
118
+ animation: offline-toast-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
119
+ }
120
+
121
+ @keyframes offline-toast-in {
122
+ from {
123
+ opacity: 0;
124
+ transform: translateX(-50%) translateY(-0.75rem);
125
+ }
126
+ to {
127
+ opacity: 1;
128
+ transform: translateX(-50%) translateY(0);
129
+ }
130
+ }
131
+
132
+ .offline-toast-icon {
133
+ flex-shrink: 0;
134
+ color: rgba(255, 255, 255, 0.7);
135
+ display: flex;
136
+ align-items: center;
137
+ }
138
+
139
+ .offline-toast-message {
140
+ flex: 1;
141
+ opacity: 0.95;
142
+ }
143
+
144
+ .offline-toast-dismiss {
145
+ flex-shrink: 0;
146
+ display: flex;
147
+ align-items: center;
148
+ justify-content: center;
149
+ width: 1.25rem;
150
+ height: 1.25rem;
151
+ padding: 0;
152
+ margin-left: 0.125rem;
153
+ background: rgba(255, 255, 255, 0.15);
154
+ border: none;
155
+ border-radius: 50%;
156
+ color: rgba(255, 255, 255, 0.8);
157
+ cursor: pointer;
158
+ transition: background 0.15s;
159
+ }
160
+
161
+ .offline-toast-dismiss:hover {
162
+ background: rgba(255, 255, 255, 0.28);
163
+ }
164
+
165
+ @media (prefers-reduced-motion: reduce) {
166
+ .offline-toast { animation: none; }
167
+ }
168
+ </style>