svelte-firekit 0.2.4 → 0.2.6

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 CHANGED
@@ -7,6 +7,8 @@ A complete Firebase integration library for **Svelte 5** and **SvelteKit**. Buil
7
7
  - SSR-safe — all services degrade gracefully on the server
8
8
  - Works without SvelteKit (`$app/*`, `$env/*` are never imported)
9
9
 
10
+ **Documentation:** [sveltefirekit.com](https://sveltefirekit.com/)
11
+
10
12
  ---
11
13
 
12
14
  ## Installation
@@ -162,28 +164,41 @@ try {
162
164
 
163
165
  ```ts
164
166
  // Wait for auth to initialize (useful for SSR / load functions)
165
- const user = await firekitUser.waitForAuth();
167
+ const user = await firekitUser.waitForAuth(); // 10s timeout (default)
168
+ const user = await firekitUser.waitForAuth(5_000); // custom timeout in ms
166
169
  ```
167
170
 
168
171
  ### Auth components
169
172
 
173
+ All auth components wait for Firebase Auth to initialize before rendering. Use the optional `fallback` snippet to show a loading state.
174
+
170
175
  ```svelte
171
- <SignedIn> <p>Only shown when signed in</p> </SignedIn>
172
- <SignedOut> <p>Only shown when signed out</p> </SignedOut>
176
+ <SignedIn>
177
+ {#snippet children(user)}<p>Welcome, {user.displayName}</p>{/snippet}
178
+ {#snippet fallback()}<p>Loading...</p>{/snippet}
179
+ </SignedIn>
180
+
181
+ <SignedOut>
182
+ {#snippet children(signIn)}<button onclick={signIn}>Sign in</button>{/snippet}
183
+ {#snippet fallback()}<p>Loading...</p>{/snippet}
184
+ </SignedOut>
173
185
 
174
186
  <!-- Route guard with redirect callback -->
175
187
  <AuthGuard requireAuth={true} onUnauthorized={() => goto('/login')}>
176
- <p>Protected content</p>
188
+ {#snippet children(user, signOut)}<p>Protected content</p>{/snippet}
189
+ {#snippet fallback()}<p>Loading...</p>{/snippet}
177
190
  </AuthGuard>
178
191
 
179
192
  <!-- Custom guard with async checks (e.g. role verification) -->
180
193
  <CustomGuard
181
194
  verificationChecks={[
182
- async () => { const doc = await getDoc(...); return doc.data()?.role === 'admin'; }
195
+ async (user) => user.emailVerified,
196
+ async (user) => { const doc = await getDoc(...); return doc.data()?.role === 'admin'; }
183
197
  ]}
184
198
  onUnauthorized={() => goto('/403')}
185
199
  >
186
- <p>Admin only</p>
200
+ {#snippet children(user, signOut)}<p>Admin only</p>{/snippet}
201
+ {#snippet fallback()}<p>Checking permissions...</p>{/snippet}
187
202
  </CustomGuard>
188
203
  ```
189
204
 
@@ -415,16 +430,44 @@ List files:
415
430
  {#each dir.items as item}<p>{item.name}</p>{/each}
416
431
  ```
417
432
 
418
- Using the `<DownloadURL>` and `<UploadTask>` components:
433
+ ### File upload validation
434
+
435
+ Validate files before uploading — checks size, MIME type, and image dimensions.
436
+
437
+ ```ts
438
+ import { validateFile } from 'svelte-firekit';
439
+
440
+ const result = await validateFile(file, {
441
+ maxSize: 5 * 1024 * 1024, // 5 MB
442
+ accept: ['image/png', 'image/jpeg', '.webp'],
443
+ maxWidth: 2048,
444
+ maxHeight: 2048,
445
+ minWidth: 100
446
+ });
447
+
448
+ if (!result.valid) {
449
+ result.errors.forEach((e) => console.log(e.code, e.message));
450
+ }
451
+ ```
452
+
453
+ The `<UploadTask>` component supports an optional `validate` prop and `invalid` snippet:
454
+
455
+ ```svelte
456
+ <UploadTask path="uploads/{file.name}" {file} validate={{ maxSize: 5_000_000, accept: ['image/*'] }}>
457
+ {#snippet uploading(task)}<progress value={task.progress} max={100} />{/snippet}
458
+ {#snippet complete(url)}<img src={url} alt="uploaded" />{/snippet}
459
+ {#snippet invalid(result)}
460
+ {#each result.errors as err}<p class="error">{err.message}</p>{/each}
461
+ {/snippet}
462
+ </UploadTask>
463
+ ```
464
+
465
+ Using the `<DownloadURL>` component:
419
466
 
420
467
  ```svelte
421
468
  <DownloadURL path="images/avatar.jpg">
422
469
  {#snippet data(url)}<img src={url} />{/snippet}
423
470
  </DownloadURL>
424
-
425
- <UploadTask path="uploads/file.jpg" {file}>
426
- {#snippet data({ progress, downloadURL })}<progress value={progress} max={100} />{/snippet}
427
- </UploadTask>
428
471
  ```
429
472
 
430
473
  ---
@@ -566,6 +609,44 @@ firekitInAppMessaging.unsuppress();
566
609
 
567
610
  ---
568
611
 
612
+ ## Network / Offline Status
613
+
614
+ Track browser connectivity and Firestore sync state reactively.
615
+
616
+ ```svelte
617
+ <script lang="ts">
618
+ import { firekitNetwork } from 'svelte-firekit';
619
+ </script>
620
+
621
+ {#if !firekitNetwork.online}
622
+ <p>You're offline. Changes will sync when reconnected.</p>
623
+ {:else if firekitNetwork.hasPendingWrites}
624
+ <p>Saving...</p>
625
+ {:else}
626
+ <p>All changes saved</p>
627
+ {/if}
628
+ ```
629
+
630
+ Using the `<NetworkStatus>` component:
631
+
632
+ ```svelte
633
+ <NetworkStatus>
634
+ {#snippet online()}<span class="green">Connected</span>{/snippet}
635
+ {#snippet offline()}<span class="red">Offline</span>{/snippet}
636
+ {#snippet pending()}<span>Saving...</span>{/snippet}
637
+ </NetworkStatus>
638
+ ```
639
+
640
+ Manual control:
641
+
642
+ ```ts
643
+ await firekitNetwork.goOffline(); // force offline mode
644
+ await firekitNetwork.goOnline(); // reconnect
645
+ firekitNetwork.trackWrite(); // mark a pending write
646
+ ```
647
+
648
+ ---
649
+
569
650
  ## Presence
570
651
 
571
652
  ```ts
@@ -656,6 +737,7 @@ const data = await posts.waitForReady();
656
737
  | `firekitAnalytics` | Analytics event logging |
657
738
  | `firekitMessaging` | Firebase Cloud Messaging |
658
739
  | `firekitInAppMessaging` | In-App Messaging suppression control |
740
+ | `firekitNetwork` | Reactive network/offline status |
659
741
  | `firekitAppCheck` | App Check initialization |
660
742
 
661
743
  ### Reactive classes
@@ -676,6 +758,12 @@ const data = await posts.waitForReady();
676
758
  | `FirekitCallable` / `firekitCallable()` | Typed Cloud Function caller |
677
759
  | `FirekitCallableFromURL` / `firekitCallableFromURL()` | Cloud Function by URL |
678
760
 
761
+ ### Utilities
762
+
763
+ | Import | Description |
764
+ |---|---|
765
+ | `validateFile()` | Pre-upload file validation (size, type, dimensions) |
766
+
679
767
  ### Components
680
768
 
681
769
  | Component | Description |
@@ -689,7 +777,8 @@ const data = await posts.waitForReady();
689
777
  | `<Collection>` | Reactive Firestore collection |
690
778
  | `<Node>` | Reactive RTDB node |
691
779
  | `<DownloadURL>` | Storage download URL |
692
- | `<UploadTask>` | Resumable file upload |
780
+ | `<UploadTask>` | Resumable file upload with optional validation |
781
+ | `<NetworkStatus>` | Network/sync status display |
693
782
 
694
783
  ---
695
784
 
@@ -0,0 +1,37 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { firekitNetwork } from '../services/network.svelte.js';
4
+
5
+ /**
6
+ * Renders snippets based on network/sync status.
7
+ *
8
+ * @example
9
+ * ```svelte
10
+ * <NetworkStatus>
11
+ * {#snippet online()}<span class="dot green" />{/snippet}
12
+ * {#snippet offline()}<span class="dot red">Offline</span>{/snippet}
13
+ * {#snippet pending()}<span>Saving...</span>{/snippet}
14
+ * </NetworkStatus>
15
+ * ```
16
+ */
17
+ let {
18
+ online: onlineSnippet,
19
+ offline: offlineSnippet,
20
+ pending: pendingSnippet
21
+ }: {
22
+ /** Rendered when online and all writes are synced. */
23
+ online?: Snippet;
24
+ /** Rendered when the browser is offline. */
25
+ offline?: Snippet;
26
+ /** Rendered when there are pending Firestore writes. */
27
+ pending?: Snippet;
28
+ } = $props();
29
+ </script>
30
+
31
+ {#if !firekitNetwork.online && offlineSnippet}
32
+ {@render offlineSnippet()}
33
+ {:else if firekitNetwork.hasPendingWrites && pendingSnippet}
34
+ {@render pendingSnippet()}
35
+ {:else if firekitNetwork.online && onlineSnippet}
36
+ {@render onlineSnippet()}
37
+ {/if}
@@ -0,0 +1,12 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ /** Rendered when online and all writes are synced. */
4
+ online?: Snippet;
5
+ /** Rendered when the browser is offline. */
6
+ offline?: Snippet;
7
+ /** Rendered when there are pending Firestore writes. */
8
+ pending?: Snippet;
9
+ };
10
+ declare const NetworkStatus: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type NetworkStatus = ReturnType<typeof NetworkStatus>;
12
+ export default NetworkStatus;
@@ -2,16 +2,20 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { UploadMetadata } from 'firebase/storage';
4
4
  import { FirekitUploadTask, type UploadState } from '../services/storage.svelte.js';
5
+ import { validateFile } from '../utils/validation.js';
6
+ import type { FileValidationOptions, FileValidationResult } from '../types/storage.js';
5
7
 
6
8
  /**
7
9
  * Manages a Firebase Storage upload with reactive progress.
8
- *
9
- * Exposes upload controls (pause, resume, cancel) and progress state
10
- * to the `uploading` snippet.
10
+ * Optionally validates the file before uploading.
11
11
  *
12
12
  * @example
13
13
  * ```svelte
14
- * <UploadTask path="uploads/{file.name}" {file}>
14
+ * <UploadTask
15
+ * path="uploads/{file.name}"
16
+ * {file}
17
+ * validate={{ maxSize: 5 * 1024 * 1024, accept: ['image/*'] }}
18
+ * >
15
19
  * {#snippet uploading(task)}
16
20
  * <progress value={task.progress} max={100} />
17
21
  * <button onclick={task.pause}>Pause</button>
@@ -19,6 +23,11 @@
19
23
  * {#snippet complete(url)}
20
24
  * <img src={url} alt="uploaded" />
21
25
  * {/snippet}
26
+ * {#snippet invalid(result)}
27
+ * {#each result.errors as err}
28
+ * <p class="error">{err.message}</p>
29
+ * {/each}
30
+ * {/snippet}
22
31
  * </UploadTask>
23
32
  * ```
24
33
  */
@@ -26,9 +35,11 @@
26
35
  path,
27
36
  file,
28
37
  metadata,
38
+ validate,
29
39
  uploading: uploadingSnippet,
30
40
  complete: completeSnippet,
31
- error: errorSnippet
41
+ error: errorSnippet,
42
+ invalid: invalidSnippet
32
43
  }: {
33
44
  /** Storage destination path. */
34
45
  path: string;
@@ -36,6 +47,8 @@
36
47
  file: Blob | Uint8Array | ArrayBuffer;
37
48
  /** Optional upload metadata (content type, custom metadata, etc.). */
38
49
  metadata?: UploadMetadata;
50
+ /** Optional validation rules. Only applied when `file` is a File instance. */
51
+ validate?: FileValidationOptions;
39
52
  /**
40
53
  * Rendered while the upload is active.
41
54
  * Receives an object with `{ progress, state, bytesTransferred, totalBytes, pause, resume, cancel }`.
@@ -51,25 +64,55 @@
51
64
  }]>;
52
65
  /** Rendered after a successful upload. Receives the download URL. */
53
66
  complete?: Snippet<[string]>;
54
- /** Rendered on error. */
67
+ /** Rendered on upload error. */
55
68
  error?: Snippet<[Error]>;
69
+ /** Rendered when validation fails. Receives the validation result with errors. */
70
+ invalid?: Snippet<[FileValidationResult]>;
56
71
  } = $props();
57
72
 
58
- const task = new FirekitUploadTask(path, file, metadata);
73
+ let validationResult = $state<FileValidationResult | null>(null);
74
+ let task = $state<FirekitUploadTask | null>(null);
75
+ let started = false;
76
+
77
+ // Run validation (if configured) then start upload.
78
+ // Uses $effect so Svelte properly tracks the prop references.
79
+ $effect(() => {
80
+ if (started) return;
81
+ started = true;
82
+
83
+ if (validate && file instanceof File) {
84
+ validateFile(file, validate).then((result) => {
85
+ validationResult = result;
86
+ if (result.valid) {
87
+ task = new FirekitUploadTask(path, file as Blob, metadata);
88
+ }
89
+ });
90
+ } else {
91
+ task = new FirekitUploadTask(path, file, metadata);
92
+ }
93
+ });
59
94
  </script>
60
95
 
61
- {#if task.error && errorSnippet}
62
- {@render errorSnippet(task.error)}
63
- {:else if task.completed && completeSnippet && task.downloadURL}
64
- {@render completeSnippet(task.downloadURL)}
65
- {:else if task.active && uploadingSnippet}
66
- {@render uploadingSnippet({
67
- progress: task.progress,
68
- state: task.state,
69
- bytesTransferred: task.bytesTransferred,
70
- totalBytes: task.totalBytes,
71
- pause: () => task.pause(),
72
- resume: () => task.resume(),
73
- cancel: () => task.cancel()
74
- })}
96
+ {#if validationResult && !validationResult.valid}
97
+ {#if invalidSnippet}
98
+ {@render invalidSnippet(validationResult)}
99
+ {:else if errorSnippet}
100
+ {@render errorSnippet(new Error(validationResult.errors.map((e) => e.message).join('; ')))}
101
+ {/if}
102
+ {:else if task}
103
+ {#if task.error && errorSnippet}
104
+ {@render errorSnippet(task.error)}
105
+ {:else if task.completed && completeSnippet && task.downloadURL}
106
+ {@render completeSnippet(task.downloadURL)}
107
+ {:else if task.active && uploadingSnippet}
108
+ {@render uploadingSnippet({
109
+ progress: task.progress,
110
+ state: task.state,
111
+ bytesTransferred: task.bytesTransferred,
112
+ totalBytes: task.totalBytes,
113
+ pause: () => task!.pause(),
114
+ resume: () => task!.resume(),
115
+ cancel: () => task!.cancel()
116
+ })}
117
+ {/if}
75
118
  {/if}
@@ -1,6 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
2
  import type { UploadMetadata } from 'firebase/storage';
3
3
  import { type UploadState } from '../services/storage.svelte.js';
4
+ import type { FileValidationOptions, FileValidationResult } from '../types/storage.js';
4
5
  type $$ComponentProps = {
5
6
  /** Storage destination path. */
6
7
  path: string;
@@ -8,6 +9,8 @@ type $$ComponentProps = {
8
9
  file: Blob | Uint8Array | ArrayBuffer;
9
10
  /** Optional upload metadata (content type, custom metadata, etc.). */
10
11
  metadata?: UploadMetadata;
12
+ /** Optional validation rules. Only applied when `file` is a File instance. */
13
+ validate?: FileValidationOptions;
11
14
  /**
12
15
  * Rendered while the upload is active.
13
16
  * Receives an object with `{ progress, state, bytesTransferred, totalBytes, pause, resume, cancel }`.
@@ -25,8 +28,10 @@ type $$ComponentProps = {
25
28
  ]>;
26
29
  /** Rendered after a successful upload. Receives the download URL. */
27
30
  complete?: Snippet<[string]>;
28
- /** Rendered on error. */
31
+ /** Rendered on upload error. */
29
32
  error?: Snippet<[Error]>;
33
+ /** Rendered when validation fails. Receives the validation result with errors. */
34
+ invalid?: Snippet<[FileValidationResult]>;
30
35
  };
31
36
  declare const UploadTask: import("svelte").Component<$$ComponentProps, {}, "">;
32
37
  type UploadTask = ReturnType<typeof UploadTask>;
package/dist/index.d.ts CHANGED
@@ -20,6 +20,7 @@ export { FirekitRemoteConfig, firekitRemoteConfig, type FirekitRemoteConfigOptio
20
20
  export { firekitPerformance, type PerformanceTrace } from './services/performance.js';
21
21
  export { firekitMessaging, type MessagePayload } from './services/messaging.svelte.js';
22
22
  export { firekitAnalytics } from './services/analytics.js';
23
+ export { firekitNetwork } from './services/network.svelte.js';
23
24
  export { loadFirestoreBundle, getNamedQuery } from './services/bundles.js';
24
25
  export { default as FirebaseApp } from './components/FirebaseApp.svelte';
25
26
  export { default as SignedIn } from './components/SignedIn.svelte';
@@ -31,5 +32,6 @@ export { default as Collection } from './components/Collection.svelte';
31
32
  export { default as Node } from './components/Node.svelte';
32
33
  export { default as DownloadURL } from './components/DownloadURL.svelte';
33
34
  export { default as UploadTask } from './components/UploadTask.svelte';
35
+ export { default as NetworkStatus } from './components/NetworkStatus.svelte';
34
36
  export * from './types/index.js';
35
37
  export * from './utils/index.js';
package/dist/index.js CHANGED
@@ -32,6 +32,8 @@ export { firekitPerformance } from './services/performance.js';
32
32
  export { firekitMessaging } from './services/messaging.svelte.js';
33
33
  // ─── Analytics ────────────────────────────────────────────────────────────────
34
34
  export { firekitAnalytics } from './services/analytics.js';
35
+ // ─── Network / Offline ───────────────────────────────────────────────────────
36
+ export { firekitNetwork } from './services/network.svelte.js';
35
37
  // ─── Firestore bundles ────────────────────────────────────────────────────────
36
38
  export { loadFirestoreBundle, getNamedQuery } from './services/bundles.js';
37
39
  // ─── Components ──────────────────────────────────────────────────────────────
@@ -45,6 +47,7 @@ export { default as Collection } from './components/Collection.svelte';
45
47
  export { default as Node } from './components/Node.svelte';
46
48
  export { default as DownloadURL } from './components/DownloadURL.svelte';
47
49
  export { default as UploadTask } from './components/UploadTask.svelte';
50
+ export { default as NetworkStatus } from './components/NetworkStatus.svelte';
48
51
  // ─── Types ───────────────────────────────────────────────────────────────────
49
52
  export * from './types/index.js';
50
53
  // ─── Utilities ───────────────────────────────────────────────────────────────
@@ -210,7 +210,19 @@ export class FirekitChat {
210
210
  // Seed history from initialHistory
211
211
  for (const turn of initialHistory) {
212
212
  const text = turn.parts
213
- .map((p) => ('text' in p ? p.text : ''))
213
+ .map((p) => {
214
+ if ('text' in p)
215
+ return p.text;
216
+ if ('inlineData' in p)
217
+ return '[image]';
218
+ if ('fileData' in p)
219
+ return '[file]';
220
+ if ('functionCall' in p)
221
+ return `[function: ${p.functionCall.name}]`;
222
+ if ('functionResponse' in p)
223
+ return `[function response: ${p.functionResponse.name}]`;
224
+ return '[unknown part]';
225
+ })
214
226
  .filter(Boolean)
215
227
  .join('');
216
228
  this._history.push({
@@ -41,7 +41,11 @@ class FirekitAnalytics {
41
41
  if (!supported)
42
42
  return;
43
43
  this._analytics = getAnalytics(getApp());
44
- })();
44
+ })().catch((err) => {
45
+ // Reset so next call retries instead of returning a rejected promise forever
46
+ this._initPromise = null;
47
+ throw err;
48
+ });
45
49
  return this._initPromise;
46
50
  }
47
51
  async _get() {
@@ -18,8 +18,8 @@ declare class FirekitAuth {
18
18
  private recaptchaVerifiers;
19
19
  private constructor();
20
20
  static getInstance(): FirekitAuth;
21
- private bootstrap;
22
21
  private getAuth;
22
+ private getFirestore;
23
23
  private syncToFirestore;
24
24
  private profile;
25
25
  signInWithEmail(email: string, password: string): Promise<SignInResult>;
@@ -20,9 +20,8 @@ class FirekitAuth {
20
20
  firestore = null;
21
21
  recaptchaVerifiers = new Map();
22
22
  constructor() {
23
- if (typeof window !== 'undefined') {
24
- this.bootstrap();
25
- }
23
+ // Do NOT bootstrap here — Firebase config may not be set yet.
24
+ // Auth and Firestore instances are resolved lazily via getAuth() / getFirestore().
26
25
  }
27
26
  static getInstance() {
28
27
  if (!FirekitAuth.instance) {
@@ -30,20 +29,6 @@ class FirekitAuth {
30
29
  }
31
30
  return FirekitAuth.instance;
32
31
  }
33
- bootstrap() {
34
- try {
35
- this.auth = firebaseService.getAuthInstance();
36
- try {
37
- this.firestore = firebaseService.getDbInstance();
38
- }
39
- catch {
40
- this.firestore = null;
41
- }
42
- }
43
- catch {
44
- // Firebase not yet configured — services will be accessed lazily
45
- }
46
- }
47
32
  getAuth() {
48
33
  if (!this.auth) {
49
34
  this.auth = firebaseService.getAuthInstance();
@@ -53,10 +38,22 @@ class FirekitAuth {
53
38
  }
54
39
  return this.auth;
55
40
  }
41
+ getFirestore() {
42
+ if (!this.firestore) {
43
+ try {
44
+ this.firestore = firebaseService.getDbInstance();
45
+ }
46
+ catch {
47
+ this.firestore = null;
48
+ }
49
+ }
50
+ return this.firestore;
51
+ }
56
52
  async syncToFirestore(user) {
57
- if (!this.firestore)
53
+ const fs = this.getFirestore();
54
+ if (!fs)
58
55
  return;
59
- await updateUserInFirestore(this.firestore, user);
56
+ await updateUserInFirestore(fs, user);
60
57
  }
61
58
  profile(user) {
62
59
  return mapFirebaseUserToProfile(user);
@@ -327,9 +324,10 @@ class FirekitAuth {
327
324
  if (currentPassword)
328
325
  await this.reauthenticate(currentPassword);
329
326
  // Soft-delete record in Firestore before removing auth
330
- if (this.firestore) {
327
+ const fs = this.getFirestore();
328
+ if (fs) {
331
329
  try {
332
- await setDoc(doc(this.firestore, 'users', user.uid), { deleted: true, deletedAt: serverTimestamp() }, { merge: true });
330
+ await setDoc(doc(fs, 'users', user.uid), { deleted: true, deletedAt: serverTimestamp() }, { merge: true });
333
331
  }
334
332
  catch {
335
333
  // Non-blocking
@@ -560,16 +558,22 @@ class FirekitAuth {
560
558
  }
561
559
  // ─── Utility getters ─────────────────────────────────────────────────────────
562
560
  getCurrentUser() {
563
- return this.auth?.currentUser ?? null;
561
+ try {
562
+ return this.getAuth().currentUser;
563
+ }
564
+ catch {
565
+ return null;
566
+ }
564
567
  }
565
568
  isAuthenticated() {
566
- return this.auth?.currentUser !== null && !this.auth?.currentUser?.isAnonymous;
569
+ const user = this.getCurrentUser();
570
+ return user !== null && !user.isAnonymous;
567
571
  }
568
572
  isAnonymous() {
569
- return this.auth?.currentUser?.isAnonymous ?? false;
573
+ return this.getCurrentUser()?.isAnonymous ?? false;
570
574
  }
571
575
  isEmailVerified() {
572
- return this.auth?.currentUser?.emailVerified ?? false;
576
+ return this.getCurrentUser()?.emailVerified ?? false;
573
577
  }
574
578
  async cleanup() {
575
579
  this.recaptchaVerifiers.forEach((v) => v.clear());
@@ -33,6 +33,7 @@ declare class FirekitMessaging {
33
33
  private _messaging;
34
34
  private _unsubscribeMessage;
35
35
  private _requesting;
36
+ private _listening;
36
37
  private constructor();
37
38
  static getInstance(): FirekitMessaging;
38
39
  get token(): string | null;
@@ -40,6 +40,7 @@ class FirekitMessaging {
40
40
  _messaging = null;
41
41
  _unsubscribeMessage = null;
42
42
  _requesting = false;
43
+ _listening = false;
43
44
  constructor() {
44
45
  this._initPermissionState();
45
46
  }
@@ -161,7 +162,10 @@ class FirekitMessaging {
161
162
  }
162
163
  // ── Foreground messages ───────────────────────────────────────────────────
163
164
  _listenForMessages(msg) {
165
+ if (this._listening)
166
+ return;
164
167
  this._unsubscribeMessage?.();
168
+ this._listening = true;
165
169
  this._unsubscribeMessage = onMessage(msg, (payload) => {
166
170
  this._lastMessage = payload;
167
171
  this._messages = [...this._messages, payload];
@@ -185,6 +189,7 @@ class FirekitMessaging {
185
189
  dispose() {
186
190
  this._unsubscribeMessage?.();
187
191
  this._unsubscribeMessage = null;
192
+ this._listening = false;
188
193
  }
189
194
  }
190
195
  export const firekitMessaging = FirekitMessaging.getInstance();
@@ -46,7 +46,7 @@ async function withRetry(fn, retryConfig) {
46
46
  }
47
47
  }
48
48
  }
49
- throw lastError;
49
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
50
50
  }
51
51
  /**
52
52
  * Firestore document mutations — add, set, update, delete, batch, transaction.
@@ -0,0 +1,71 @@
1
+ import { NetworkError } from '../types/network.js';
2
+ /**
3
+ * Reactive network/offline status store.
4
+ *
5
+ * Tracks browser connectivity, Firestore sync state, and pending writes.
6
+ * Provides manual control to go offline/online.
7
+ *
8
+ * @example
9
+ * ```svelte
10
+ * <script>
11
+ * import { firekitNetwork } from 'svelte-firekit';
12
+ * </script>
13
+ *
14
+ * {#if !firekitNetwork.online}
15
+ * <p>You're offline. Changes will sync when reconnected.</p>
16
+ * {/if}
17
+ *
18
+ * {#if firekitNetwork.hasPendingWrites}
19
+ * <p>Saving...</p>
20
+ * {:else}
21
+ * <p>All changes saved</p>
22
+ * {/if}
23
+ * ```
24
+ */
25
+ declare class FirekitNetwork {
26
+ private static instance;
27
+ private _online;
28
+ private _synced;
29
+ private _hasPendingWrites;
30
+ private _initialized;
31
+ private _error;
32
+ private _firestoreEnabled;
33
+ private _listening;
34
+ private _db;
35
+ private _syncUnsub;
36
+ private _onlineHandler;
37
+ private _offlineHandler;
38
+ private constructor();
39
+ static getInstance(): FirekitNetwork;
40
+ private ensureListening;
41
+ /**
42
+ * Called by FirebaseApp or lazily on first getter access.
43
+ * Safe to call multiple times.
44
+ */
45
+ initialize(): void;
46
+ /** Whether the browser has network connectivity. */
47
+ get online(): boolean;
48
+ /** Whether all Firestore snapshots are in sync with the server. */
49
+ get synced(): boolean;
50
+ /** Whether there are unconfirmed Firestore writes. */
51
+ get hasPendingWrites(): boolean;
52
+ /** Whether the network store has been initialized. */
53
+ get initialized(): boolean;
54
+ /** Last error from network operations. */
55
+ get error(): NetworkError | null;
56
+ /** Whether Firestore network is currently enabled. */
57
+ get firestoreEnabled(): boolean;
58
+ /**
59
+ * Call this when you perform a Firestore write to track pending state.
60
+ * Uses `waitForPendingWrites` to automatically clear when the write is acknowledged.
61
+ */
62
+ trackWrite(): void;
63
+ /** Enables Firestore network access (reconnects to server). */
64
+ goOnline(): Promise<void>;
65
+ /** Disables Firestore network access (forces offline mode). */
66
+ goOffline(): Promise<void>;
67
+ clearError(): void;
68
+ dispose(): void;
69
+ }
70
+ export declare const firekitNetwork: FirekitNetwork;
71
+ export {};
@@ -0,0 +1,167 @@
1
+ import { onSnapshotsInSync, enableNetwork, disableNetwork, waitForPendingWrites } from 'firebase/firestore';
2
+ import { firebaseService } from '../firebase.js';
3
+ import { NetworkError, NetworkErrorCode } from '../types/network.js';
4
+ /**
5
+ * Reactive network/offline status store.
6
+ *
7
+ * Tracks browser connectivity, Firestore sync state, and pending writes.
8
+ * Provides manual control to go offline/online.
9
+ *
10
+ * @example
11
+ * ```svelte
12
+ * <script>
13
+ * import { firekitNetwork } from 'svelte-firekit';
14
+ * </script>
15
+ *
16
+ * {#if !firekitNetwork.online}
17
+ * <p>You're offline. Changes will sync when reconnected.</p>
18
+ * {/if}
19
+ *
20
+ * {#if firekitNetwork.hasPendingWrites}
21
+ * <p>Saving...</p>
22
+ * {:else}
23
+ * <p>All changes saved</p>
24
+ * {/if}
25
+ * ```
26
+ */
27
+ class FirekitNetwork {
28
+ static instance;
29
+ // ── Reactive state ──────────────────────────────────────────────────────
30
+ _online = $state(typeof navigator !== 'undefined' ? navigator.onLine : true);
31
+ _synced = $state(true);
32
+ _hasPendingWrites = $state(false);
33
+ _initialized = $state(false);
34
+ _error = $state(null);
35
+ _firestoreEnabled = $state(true);
36
+ // ── Internal ────────────────────────────────────────────────────────────
37
+ _listening = false;
38
+ _db = null;
39
+ _syncUnsub = null;
40
+ _onlineHandler = null;
41
+ _offlineHandler = null;
42
+ constructor() { }
43
+ static getInstance() {
44
+ if (!FirekitNetwork.instance) {
45
+ FirekitNetwork.instance = new FirekitNetwork();
46
+ }
47
+ return FirekitNetwork.instance;
48
+ }
49
+ ensureListening() {
50
+ if (this._listening || typeof window === 'undefined')
51
+ return;
52
+ try {
53
+ const db = firebaseService.getDbInstance();
54
+ if (!db)
55
+ return;
56
+ this._listening = true;
57
+ // Browser online/offline events
58
+ this._onlineHandler = () => { this._online = true; };
59
+ this._offlineHandler = () => { this._online = false; };
60
+ window.addEventListener('online', this._onlineHandler);
61
+ window.addEventListener('offline', this._offlineHandler);
62
+ // Firestore sync state — fires when all active listeners are caught up
63
+ this._syncUnsub = onSnapshotsInSync(db, () => {
64
+ this._synced = true;
65
+ this._hasPendingWrites = false;
66
+ });
67
+ this._db = db;
68
+ this._initialized = true;
69
+ }
70
+ catch {
71
+ // Firestore not configured yet — will retry on next getter access
72
+ }
73
+ }
74
+ /**
75
+ * Called by FirebaseApp or lazily on first getter access.
76
+ * Safe to call multiple times.
77
+ */
78
+ initialize() {
79
+ this.ensureListening();
80
+ }
81
+ // ── Public getters (reactive) ───────────────────────────────────────────
82
+ /** Whether the browser has network connectivity. */
83
+ get online() { this.ensureListening(); return this._online; }
84
+ /** Whether all Firestore snapshots are in sync with the server. */
85
+ get synced() { this.ensureListening(); return this._synced; }
86
+ /** Whether there are unconfirmed Firestore writes. */
87
+ get hasPendingWrites() { this.ensureListening(); return this._hasPendingWrites; }
88
+ /** Whether the network store has been initialized. */
89
+ get initialized() { return this._initialized; }
90
+ /** Last error from network operations. */
91
+ get error() { return this._error; }
92
+ /** Whether Firestore network is currently enabled. */
93
+ get firestoreEnabled() { return this._firestoreEnabled; }
94
+ // ── Methods ─────────────────────────────────────────────────────────────
95
+ /**
96
+ * Call this when you perform a Firestore write to track pending state.
97
+ * Uses `waitForPendingWrites` to automatically clear when the write is acknowledged.
98
+ */
99
+ trackWrite() {
100
+ this._hasPendingWrites = true;
101
+ this._synced = false;
102
+ if (this._db) {
103
+ waitForPendingWrites(this._db)
104
+ .then(() => {
105
+ this._hasPendingWrites = false;
106
+ this._synced = true;
107
+ })
108
+ .catch(() => {
109
+ // Firestore may have been disabled
110
+ });
111
+ }
112
+ }
113
+ /** Enables Firestore network access (reconnects to server). */
114
+ async goOnline() {
115
+ try {
116
+ const db = firebaseService.getDbInstance();
117
+ if (!db)
118
+ throw new NetworkError(NetworkErrorCode.FIRESTORE_UNAVAILABLE, 'Firestore is not available.');
119
+ await enableNetwork(db);
120
+ this._firestoreEnabled = true;
121
+ this._error = null;
122
+ }
123
+ catch (err) {
124
+ if (err instanceof NetworkError)
125
+ throw err;
126
+ const error = new NetworkError(NetworkErrorCode.ENABLE_NETWORK_FAILED, 'Failed to enable network.', err);
127
+ this._error = error;
128
+ throw error;
129
+ }
130
+ }
131
+ /** Disables Firestore network access (forces offline mode). */
132
+ async goOffline() {
133
+ try {
134
+ const db = firebaseService.getDbInstance();
135
+ if (!db)
136
+ throw new NetworkError(NetworkErrorCode.FIRESTORE_UNAVAILABLE, 'Firestore is not available.');
137
+ await disableNetwork(db);
138
+ this._firestoreEnabled = false;
139
+ this._error = null;
140
+ }
141
+ catch (err) {
142
+ if (err instanceof NetworkError)
143
+ throw err;
144
+ const error = new NetworkError(NetworkErrorCode.DISABLE_NETWORK_FAILED, 'Failed to disable network.', err);
145
+ this._error = error;
146
+ throw error;
147
+ }
148
+ }
149
+ clearError() {
150
+ this._error = null;
151
+ }
152
+ dispose() {
153
+ this._listening = false;
154
+ this._syncUnsub?.();
155
+ this._syncUnsub = null;
156
+ if (this._onlineHandler) {
157
+ window.removeEventListener('online', this._onlineHandler);
158
+ this._onlineHandler = null;
159
+ }
160
+ if (this._offlineHandler) {
161
+ window.removeEventListener('offline', this._offlineHandler);
162
+ this._offlineHandler = null;
163
+ }
164
+ this._initialized = false;
165
+ }
166
+ }
167
+ export const firekitNetwork = FirekitNetwork.getInstance();
@@ -23,6 +23,7 @@ declare class FirekitPresence {
23
23
  private _geo;
24
24
  private _connectedUnsub;
25
25
  private _currentUser;
26
+ private _visibilityListenerAdded;
26
27
  private constructor();
27
28
  static getInstance(): FirekitPresence;
28
29
  get initialized(): boolean;
@@ -174,9 +174,11 @@ class FirekitPresence {
174
174
  _geo = null;
175
175
  _connectedUnsub = null;
176
176
  _currentUser = null;
177
+ _visibilityListenerAdded = false;
177
178
  constructor() {
178
179
  if (typeof window !== 'undefined') {
179
180
  document.addEventListener('visibilitychange', this._onVisibilityChange);
181
+ this._visibilityListenerAdded = true;
180
182
  }
181
183
  }
182
184
  static getInstance() {
@@ -236,10 +238,15 @@ class FirekitPresence {
236
238
  if (!db)
237
239
  throw new PresenceError(PresenceErrorCode.DATABASE_ERROR, 'Realtime Database is not initialized.');
238
240
  const connectedRef = ref(db, '.info/connected');
239
- this._connectedUnsub = onValue(connectedRef, async (snap) => {
241
+ this._connectedUnsub = onValue(connectedRef, (snap) => {
240
242
  if (snap.val() === true) {
241
- await this.setPresence('online');
242
- await this._setupDisconnectHandler();
243
+ this.setPresence('online')
244
+ .then(() => this._setupDisconnectHandler())
245
+ .catch((err) => {
246
+ this._error = err instanceof PresenceError
247
+ ? err
248
+ : new PresenceError(PresenceErrorCode.DATABASE_ERROR, err.message, err);
249
+ });
243
250
  }
244
251
  else {
245
252
  this._status = 'offline';
@@ -377,8 +384,9 @@ class FirekitPresence {
377
384
  this._geo?.dispose();
378
385
  this._connectedUnsub?.();
379
386
  this._connectedUnsub = null;
380
- if (typeof document !== 'undefined') {
387
+ if (this._visibilityListenerAdded) {
381
388
  document.removeEventListener('visibilitychange', this._onVisibilityChange);
389
+ this._visibilityListenerAdded = false;
382
390
  }
383
391
  this._initialized = false;
384
392
  this._status = 'offline';
@@ -92,6 +92,9 @@ export class FirekitRemoteConfig {
92
92
  },
93
93
  error: (err) => {
94
94
  this._error = err instanceof Error ? err : new Error(String(err));
95
+ // Clean up the broken subscription so it doesn't keep firing errors
96
+ this._unsubscribe?.();
97
+ this._unsubscribe = null;
95
98
  },
96
99
  complete: () => { }
97
100
  });
@@ -197,6 +197,7 @@ export class FirekitUploadTask {
197
197
  if (this._storageRef) {
198
198
  this._downloadURL = await getDownloadURL(this._storageRef);
199
199
  }
200
+ this._error = null;
200
201
  this._state = 'success';
201
202
  this._progress = 100;
202
203
  });
@@ -5,3 +5,5 @@ export * from './collection.js';
5
5
  export * from './mutations.js';
6
6
  export * from './presence.js';
7
7
  export * from './analytics.js';
8
+ export * from './storage.js';
9
+ export * from './network.js';
@@ -5,3 +5,5 @@ export * from './collection.js';
5
5
  export * from './mutations.js';
6
6
  export * from './presence.js';
7
7
  export * from './analytics.js';
8
+ export * from './storage.js';
9
+ export * from './network.js';
@@ -0,0 +1,10 @@
1
+ export declare enum NetworkErrorCode {
2
+ FIRESTORE_UNAVAILABLE = "network/firestore-unavailable",
3
+ ENABLE_NETWORK_FAILED = "network/enable-network-failed",
4
+ DISABLE_NETWORK_FAILED = "network/disable-network-failed"
5
+ }
6
+ export declare class NetworkError extends Error {
7
+ code: NetworkErrorCode;
8
+ originalError?: unknown | undefined;
9
+ constructor(code: NetworkErrorCode, message: string, originalError?: unknown | undefined);
10
+ }
@@ -0,0 +1,16 @@
1
+ export var NetworkErrorCode;
2
+ (function (NetworkErrorCode) {
3
+ NetworkErrorCode["FIRESTORE_UNAVAILABLE"] = "network/firestore-unavailable";
4
+ NetworkErrorCode["ENABLE_NETWORK_FAILED"] = "network/enable-network-failed";
5
+ NetworkErrorCode["DISABLE_NETWORK_FAILED"] = "network/disable-network-failed";
6
+ })(NetworkErrorCode || (NetworkErrorCode = {}));
7
+ export class NetworkError extends Error {
8
+ code;
9
+ originalError;
10
+ constructor(code, message, originalError) {
11
+ super(message);
12
+ this.code = code;
13
+ this.originalError = originalError;
14
+ this.name = 'NetworkError';
15
+ }
16
+ }
@@ -0,0 +1,34 @@
1
+ export interface FileValidationOptions {
2
+ /** Maximum file size in bytes. */
3
+ maxSize?: number;
4
+ /** Minimum file size in bytes. */
5
+ minSize?: number;
6
+ /** Allowed MIME types or extensions. Supports wildcards like `'image/*'` and extensions like `'.jpg'`. */
7
+ accept?: string[];
8
+ /** Maximum image width in pixels. Only checked for image files. */
9
+ maxWidth?: number;
10
+ /** Maximum image height in pixels. Only checked for image files. */
11
+ maxHeight?: number;
12
+ /** Minimum image width in pixels. Only checked for image files. */
13
+ minWidth?: number;
14
+ /** Minimum image height in pixels. Only checked for image files. */
15
+ minHeight?: number;
16
+ }
17
+ export declare enum FileValidationErrorCode {
18
+ FILE_TOO_LARGE = "validation/file-too-large",
19
+ FILE_TOO_SMALL = "validation/file-too-small",
20
+ INVALID_MIME_TYPE = "validation/invalid-mime-type",
21
+ IMAGE_TOO_WIDE = "validation/image-too-wide",
22
+ IMAGE_TOO_TALL = "validation/image-too-tall",
23
+ IMAGE_TOO_NARROW = "validation/image-too-narrow",
24
+ IMAGE_TOO_SHORT = "validation/image-too-short",
25
+ DIMENSION_CHECK_FAILED = "validation/dimension-check-failed"
26
+ }
27
+ export interface FileValidationError {
28
+ code: FileValidationErrorCode;
29
+ message: string;
30
+ }
31
+ export interface FileValidationResult {
32
+ valid: boolean;
33
+ errors: FileValidationError[];
34
+ }
@@ -0,0 +1,11 @@
1
+ export var FileValidationErrorCode;
2
+ (function (FileValidationErrorCode) {
3
+ FileValidationErrorCode["FILE_TOO_LARGE"] = "validation/file-too-large";
4
+ FileValidationErrorCode["FILE_TOO_SMALL"] = "validation/file-too-small";
5
+ FileValidationErrorCode["INVALID_MIME_TYPE"] = "validation/invalid-mime-type";
6
+ FileValidationErrorCode["IMAGE_TOO_WIDE"] = "validation/image-too-wide";
7
+ FileValidationErrorCode["IMAGE_TOO_TALL"] = "validation/image-too-tall";
8
+ FileValidationErrorCode["IMAGE_TOO_NARROW"] = "validation/image-too-narrow";
9
+ FileValidationErrorCode["IMAGE_TOO_SHORT"] = "validation/image-too-short";
10
+ FileValidationErrorCode["DIMENSION_CHECK_FAILED"] = "validation/dimension-check-failed";
11
+ })(FileValidationErrorCode || (FileValidationErrorCode = {}));
@@ -2,3 +2,4 @@ export * from './errors.js';
2
2
  export * from './providers.js';
3
3
  export * from './user.js';
4
4
  export * from './firestore.js';
5
+ export * from './validation.js';
@@ -2,3 +2,4 @@ export * from './errors.js';
2
2
  export * from './providers.js';
3
3
  export * from './user.js';
4
4
  export * from './firestore.js';
5
+ export * from './validation.js';
@@ -0,0 +1,20 @@
1
+ import { type FileValidationOptions, type FileValidationResult } from '../types/storage.js';
2
+ /**
3
+ * Validates a File against the given options.
4
+ * Returns a result with `valid: true` if all checks pass, or `valid: false` with an array of errors.
5
+ *
6
+ * @example
7
+ * ```ts
8
+ * const result = await validateFile(file, {
9
+ * maxSize: 5 * 1024 * 1024,
10
+ * accept: ['image/png', 'image/jpeg', '.webp'],
11
+ * maxWidth: 2048,
12
+ * maxHeight: 2048
13
+ * });
14
+ *
15
+ * if (!result.valid) {
16
+ * console.log(result.errors);
17
+ * }
18
+ * ```
19
+ */
20
+ export declare function validateFile(file: File, options: FileValidationOptions): Promise<FileValidationResult>;
@@ -0,0 +1,147 @@
1
+ import { FileValidationErrorCode } from '../types/storage.js';
2
+ /** Common extension-to-MIME mappings for the `accept` option. */
3
+ const EXT_TO_MIME = {
4
+ '.jpg': 'image/jpeg',
5
+ '.jpeg': 'image/jpeg',
6
+ '.png': 'image/png',
7
+ '.gif': 'image/gif',
8
+ '.webp': 'image/webp',
9
+ '.svg': 'image/svg+xml',
10
+ '.bmp': 'image/bmp',
11
+ '.ico': 'image/x-icon',
12
+ '.avif': 'image/avif',
13
+ '.pdf': 'application/pdf',
14
+ '.mp4': 'video/mp4',
15
+ '.webm': 'video/webm',
16
+ '.mp3': 'audio/mpeg',
17
+ '.wav': 'audio/wav',
18
+ '.ogg': 'audio/ogg',
19
+ '.json': 'application/json',
20
+ '.csv': 'text/csv',
21
+ '.txt': 'text/plain',
22
+ '.zip': 'application/zip'
23
+ };
24
+ function formatBytes(bytes) {
25
+ if (bytes < 1024)
26
+ return `${bytes} B`;
27
+ if (bytes < 1024 * 1024)
28
+ return `${(bytes / 1024).toFixed(1)} KB`;
29
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
30
+ }
31
+ function matchesMime(fileType, fileName, pattern) {
32
+ // Extension pattern: '.jpg'
33
+ if (pattern.startsWith('.')) {
34
+ const expectedMime = EXT_TO_MIME[pattern.toLowerCase()];
35
+ if (expectedMime)
36
+ return fileType === expectedMime;
37
+ // Fallback: check the file name extension
38
+ return fileName.toLowerCase().endsWith(pattern.toLowerCase());
39
+ }
40
+ // Wildcard MIME: 'image/*'
41
+ if (pattern.endsWith('/*')) {
42
+ const prefix = pattern.slice(0, -1); // 'image/'
43
+ return fileType.startsWith(prefix);
44
+ }
45
+ // Exact MIME: 'image/png'
46
+ return fileType === pattern;
47
+ }
48
+ function getImageDimensions(file) {
49
+ return new Promise((resolve, reject) => {
50
+ const url = URL.createObjectURL(file);
51
+ const img = new Image();
52
+ img.onload = () => {
53
+ URL.revokeObjectURL(url);
54
+ resolve({ width: img.naturalWidth, height: img.naturalHeight });
55
+ };
56
+ img.onerror = () => {
57
+ URL.revokeObjectURL(url);
58
+ reject(new Error('Failed to load image for dimension check.'));
59
+ };
60
+ img.src = url;
61
+ });
62
+ }
63
+ /**
64
+ * Validates a File against the given options.
65
+ * Returns a result with `valid: true` if all checks pass, or `valid: false` with an array of errors.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * const result = await validateFile(file, {
70
+ * maxSize: 5 * 1024 * 1024,
71
+ * accept: ['image/png', 'image/jpeg', '.webp'],
72
+ * maxWidth: 2048,
73
+ * maxHeight: 2048
74
+ * });
75
+ *
76
+ * if (!result.valid) {
77
+ * console.log(result.errors);
78
+ * }
79
+ * ```
80
+ */
81
+ export async function validateFile(file, options) {
82
+ const errors = [];
83
+ // ── Size checks ──────────────────────────────────────────────────────────
84
+ if (options.maxSize !== undefined && file.size > options.maxSize) {
85
+ errors.push({
86
+ code: FileValidationErrorCode.FILE_TOO_LARGE,
87
+ message: `File size ${formatBytes(file.size)} exceeds maximum ${formatBytes(options.maxSize)}.`
88
+ });
89
+ }
90
+ if (options.minSize !== undefined && file.size < options.minSize) {
91
+ errors.push({
92
+ code: FileValidationErrorCode.FILE_TOO_SMALL,
93
+ message: `File size ${formatBytes(file.size)} is below minimum ${formatBytes(options.minSize)}.`
94
+ });
95
+ }
96
+ // ── MIME type check ──────────────────────────────────────────────────────
97
+ if (options.accept && options.accept.length > 0) {
98
+ const accepted = options.accept.some((pattern) => matchesMime(file.type, file.name, pattern));
99
+ if (!accepted) {
100
+ errors.push({
101
+ code: FileValidationErrorCode.INVALID_MIME_TYPE,
102
+ message: `File type "${file.type || 'unknown'}" is not accepted. Allowed: ${options.accept.join(', ')}.`
103
+ });
104
+ }
105
+ }
106
+ // ── Image dimension checks ───────────────────────────────────────────────
107
+ const needsDimensions = options.maxWidth !== undefined ||
108
+ options.maxHeight !== undefined ||
109
+ options.minWidth !== undefined ||
110
+ options.minHeight !== undefined;
111
+ if (needsDimensions && file.type.startsWith('image/')) {
112
+ try {
113
+ const { width, height } = await getImageDimensions(file);
114
+ if (options.maxWidth !== undefined && width > options.maxWidth) {
115
+ errors.push({
116
+ code: FileValidationErrorCode.IMAGE_TOO_WIDE,
117
+ message: `Image width ${width}px exceeds maximum ${options.maxWidth}px.`
118
+ });
119
+ }
120
+ if (options.maxHeight !== undefined && height > options.maxHeight) {
121
+ errors.push({
122
+ code: FileValidationErrorCode.IMAGE_TOO_TALL,
123
+ message: `Image height ${height}px exceeds maximum ${options.maxHeight}px.`
124
+ });
125
+ }
126
+ if (options.minWidth !== undefined && width < options.minWidth) {
127
+ errors.push({
128
+ code: FileValidationErrorCode.IMAGE_TOO_NARROW,
129
+ message: `Image width ${width}px is below minimum ${options.minWidth}px.`
130
+ });
131
+ }
132
+ if (options.minHeight !== undefined && height < options.minHeight) {
133
+ errors.push({
134
+ code: FileValidationErrorCode.IMAGE_TOO_SHORT,
135
+ message: `Image height ${height}px is below minimum ${options.minHeight}px.`
136
+ });
137
+ }
138
+ }
139
+ catch {
140
+ errors.push({
141
+ code: FileValidationErrorCode.DIMENSION_CHECK_FAILED,
142
+ message: 'Could not read image dimensions for validation.'
143
+ });
144
+ }
145
+ }
146
+ return { valid: errors.length === 0, errors };
147
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-firekit",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "description": "A Svelte library for Firebase integration",
5
5
  "license": "MIT",
6
6
  "author": "Giovani Rodriguez",