@ttt-productions/upload-ui 0.7.12 → 0.7.14

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.
@@ -1,4 +1,4 @@
1
- export { LocalUploadGuardProvider, useLocalUploadGuard, useLocalUploadGuardContext, useOptionalLocalUploadGuard, type ConfirmNavigationOptions, type LocalUploadGuardProviderProps, } from '../local-upload-guard-provider.js';
1
+ export { LocalUploadGuardProvider, useLocalUploadGuard, useLocalUploadGuardContext, useOptionalLocalUploadGuard, useUnsavedWorkGuard, type ConfirmNavigationOptions, type LocalUploadGuardProviderProps, } from '../local-upload-guard-provider.js';
2
2
  export { useGuardedNavigation } from '../use-guarded-navigation.js';
3
3
  export { GuardedLink, type GuardedLinkProps, type GuardedLinkRenderArgs, } from '../guarded-link.js';
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/guard/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,0BAA0B,EAC1B,2BAA2B,EAC3B,KAAK,wBAAwB,EAC7B,KAAK,6BAA6B,GACnC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EACL,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/guard/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,0BAA0B,EAC1B,2BAA2B,EAC3B,mBAAmB,EACnB,KAAK,wBAAwB,EAC7B,KAAK,6BAA6B,GACnC,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EACL,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,oBAAoB,CAAC"}
@@ -1,4 +1,4 @@
1
- export { LocalUploadGuardProvider, useLocalUploadGuard, useLocalUploadGuardContext, useOptionalLocalUploadGuard, } from '../local-upload-guard-provider.js';
1
+ export { LocalUploadGuardProvider, useLocalUploadGuard, useLocalUploadGuardContext, useOptionalLocalUploadGuard, useUnsavedWorkGuard, } from '../local-upload-guard-provider.js';
2
2
  export { useGuardedNavigation } from '../use-guarded-navigation.js';
3
3
  export { GuardedLink, } from '../guarded-link.js';
4
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/guard/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,0BAA0B,EAC1B,2BAA2B,GAG5B,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EACL,WAAW,GAGZ,MAAM,oBAAoB,CAAC","sourcesContent":["export {\n LocalUploadGuardProvider,\n useLocalUploadGuard,\n useLocalUploadGuardContext,\n useOptionalLocalUploadGuard,\n type ConfirmNavigationOptions,\n type LocalUploadGuardProviderProps,\n} from '../local-upload-guard-provider.js';\nexport { useGuardedNavigation } from '../use-guarded-navigation.js';\nexport {\n GuardedLink,\n type GuardedLinkProps,\n type GuardedLinkRenderArgs,\n} from '../guarded-link.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/guard/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,0BAA0B,EAC1B,2BAA2B,EAC3B,mBAAmB,GAGpB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAAE,oBAAoB,EAAE,MAAM,8BAA8B,CAAC;AACpE,OAAO,EACL,WAAW,GAGZ,MAAM,oBAAoB,CAAC","sourcesContent":["export {\n LocalUploadGuardProvider,\n useLocalUploadGuard,\n useLocalUploadGuardContext,\n useOptionalLocalUploadGuard,\n useUnsavedWorkGuard,\n type ConfirmNavigationOptions,\n type LocalUploadGuardProviderProps,\n} from '../local-upload-guard-provider.js';\nexport { useGuardedNavigation } from '../use-guarded-navigation.js';\nexport {\n GuardedLink,\n type GuardedLinkProps,\n type GuardedLinkRenderArgs,\n} from '../guarded-link.js';\n"]}
@@ -1,6 +1,6 @@
1
1
  /**
2
- * Render-prop link wrapper that guards navigation when an upload is in
3
- * progress. The consumer owns the actual navigator component (Next.js Link,
2
+ * Render-prop link wrapper that guards navigation while an upload or unsaved
3
+ * work is registered. The consumer owns the actual navigator component (Next.js Link,
4
4
  * React Router Link, plain anchor, etc.) — this wrapper only injects the
5
5
  * onClick interceptor.
6
6
  *
@@ -1 +1 @@
1
- {"version":3,"file":"guarded-link.js","sourceRoot":"","sources":["../../src/react/guarded-link.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AA6Bb,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAcvE,MAAM,UAAU,WAAW,CAAC,KAAuB;IACjD,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,KAAK,CAAC;IACvC,MAAM,EAAE,iBAAiB,EAAE,GAAG,mBAAmB,EAAE,CAAC;IAEpD,MAAM,OAAO,GAAG,CAAC,KAA8B,EAAE,EAAE;QACjD,IAAI,CAAC,iBAAiB,EAAE,EAAE,CAAC;YACzB,KAAK,CAAC,cAAc,EAAE,CAAC;QACzB,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,4BAAG,UAAU,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAI,CAAC;AAClD,CAAC","sourcesContent":["'use client';\n\n/**\n * Render-prop link wrapper that guards navigation when an upload is in\n * progress. The consumer owns the actual navigator component (Next.js Link,\n * React Router Link, plain anchor, etc.) — this wrapper only injects the\n * onClick interceptor.\n *\n * Why render-prop and not `as={Link}`? Polymorphic `as` props create\n * notoriously bad TypeScript inference and force the wrapper to forward all\n * link-specific props. Render-prop keeps the consumer in full control of the\n * link API, costs one extra wrapper closure, and keeps upload-ui agnostic of\n * Next.js / React Router / etc.\n *\n * Design note: the design spec lists `<GuardedNavigationButton>` as a second\n * component. We deliberately don't ship it — buttons that navigate use\n * `useGuardedNavigation()` directly in one line. A second component would\n * split the same surface for no payoff.\n *\n * Usage:\n *\n * <GuardedLink renderLink={({ onClick, children }) => (\n * <Link href=\"/your/destination\" onClick={onClick}>{children}</Link>\n * )}>\n * View all uploads →\n * </GuardedLink>\n */\n\nimport type { MouseEvent, ReactNode } from 'react';\nimport { useLocalUploadGuard } from './local-upload-guard-provider.js';\n\nexport interface GuardedLinkRenderArgs {\n /** Click handler the consumer MUST attach to its underlying link element. */\n onClick: (event: MouseEvent<HTMLElement>) => void;\n /** The children passed to `<GuardedLink>` — forward them to the link. */\n children: ReactNode;\n}\n\nexport interface GuardedLinkProps {\n children: ReactNode;\n renderLink: (args: GuardedLinkRenderArgs) => ReactNode;\n}\n\nexport function GuardedLink(props: GuardedLinkProps) {\n const { children, renderLink } = props;\n const { confirmNavigation } = useLocalUploadGuard();\n\n const onClick = (event: MouseEvent<HTMLElement>) => {\n if (!confirmNavigation()) {\n event.preventDefault();\n }\n };\n\n return <>{renderLink({ onClick, children })}</>;\n}\n"]}
1
+ {"version":3,"file":"guarded-link.js","sourceRoot":"","sources":["../../src/react/guarded-link.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AA6Bb,OAAO,EAAE,mBAAmB,EAAE,MAAM,kCAAkC,CAAC;AAcvE,MAAM,UAAU,WAAW,CAAC,KAAuB;IACjD,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,GAAG,KAAK,CAAC;IACvC,MAAM,EAAE,iBAAiB,EAAE,GAAG,mBAAmB,EAAE,CAAC;IAEpD,MAAM,OAAO,GAAG,CAAC,KAA8B,EAAE,EAAE;QACjD,IAAI,CAAC,iBAAiB,EAAE,EAAE,CAAC;YACzB,KAAK,CAAC,cAAc,EAAE,CAAC;QACzB,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,4BAAG,UAAU,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,GAAI,CAAC;AAClD,CAAC","sourcesContent":["'use client';\n\n/**\n * Render-prop link wrapper that guards navigation while an upload or unsaved\n * work is registered. The consumer owns the actual navigator component (Next.js Link,\n * React Router Link, plain anchor, etc.) — this wrapper only injects the\n * onClick interceptor.\n *\n * Why render-prop and not `as={Link}`? Polymorphic `as` props create\n * notoriously bad TypeScript inference and force the wrapper to forward all\n * link-specific props. Render-prop keeps the consumer in full control of the\n * link API, costs one extra wrapper closure, and keeps upload-ui agnostic of\n * Next.js / React Router / etc.\n *\n * Design note: the design spec lists `<GuardedNavigationButton>` as a second\n * component. We deliberately don't ship it — buttons that navigate use\n * `useGuardedNavigation()` directly in one line. A second component would\n * split the same surface for no payoff.\n *\n * Usage:\n *\n * <GuardedLink renderLink={({ onClick, children }) => (\n * <Link href=\"/your/destination\" onClick={onClick}>{children}</Link>\n * )}>\n * View all uploads →\n * </GuardedLink>\n */\n\nimport type { MouseEvent, ReactNode } from 'react';\nimport { useLocalUploadGuard } from './local-upload-guard-provider.js';\n\nexport interface GuardedLinkRenderArgs {\n /** Click handler the consumer MUST attach to its underlying link element. */\n onClick: (event: MouseEvent<HTMLElement>) => void;\n /** The children passed to `<GuardedLink>` — forward them to the link. */\n children: ReactNode;\n}\n\nexport interface GuardedLinkProps {\n children: ReactNode;\n renderLink: (args: GuardedLinkRenderArgs) => ReactNode;\n}\n\nexport function GuardedLink(props: GuardedLinkProps) {\n const { children, renderLink } = props;\n const { confirmNavigation } = useLocalUploadGuard();\n\n const onClick = (event: MouseEvent<HTMLElement>) => {\n if (!confirmNavigation()) {\n event.preventDefault();\n }\n };\n\n return <>{renderLink({ onClick, children })}</>;\n}\n"]}
@@ -215,6 +215,17 @@ export type FirestoreLikeDocChange = {
215
215
  data: () => unknown;
216
216
  };
217
217
  };
218
+ /** Restarting the uploads listener after it failed, and whether that restart is running. */
219
+ export interface UploadsSourceRetry {
220
+ /**
221
+ * Tear the listener down and subscribe again on a fresh retry ladder. While the restart runs the
222
+ * source state stays as it was (an `error` stays `error`), so a failure shown on it stays on
223
+ * screen with its control pending.
224
+ */
225
+ retry: () => void;
226
+ /** A `retry` is running: true until the restarted listener answers or its ladder fails again. */
227
+ retrying: boolean;
228
+ }
218
229
  export interface InFlightUploadsProviderProps<TFileOrigin extends string = string> {
219
230
  adapter: InFlightUploadsAdapter<TFileOrigin>;
220
231
  /** Firestore subscription function. Defaults to the firebase/firestore implementation. */
@@ -231,6 +242,11 @@ export declare function useInFlightUpload<TFileOrigin extends string = string>(i
231
242
  * `offline`/`error`/`connecting`.
232
243
  */
233
244
  export declare function useUploadsSourceState(): UploadsSourceState;
245
+ /**
246
+ * The uploads listener's restart — the Try again of a failure shown on `useUploadsSourceState` —
247
+ * and whether it is running.
248
+ */
249
+ export declare function useUploadsSourceRetry(): UploadsSourceRetry;
234
250
  /**
235
251
  * `true` for `completed` / `failed` / `rejected`. Used by tray UI to decide
236
252
  * whether a Clear action is available for an item.
@@ -1 +1 @@
1
- {"version":3,"file":"in-flight-uploads-provider.d.ts","sourceRoot":"","sources":["../../src/react/in-flight-uploads-provider.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"in-flight-uploads-provider.d.ts","sourceRoot":"","sources":["../../src/react/in-flight-uploads-provider.tsx"],"names":[],"mappings":"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"}
@@ -50,7 +50,7 @@ import { collection, onSnapshot, query, where } from 'firebase/firestore';
50
50
  * non-terminal → terminal transition tracking. It does NOT own anything about
51
51
  * what a file-origin is or what message text appears in toasts.
52
52
  */
53
- import { createContext, useContext, useEffect, useMemo, useRef, useState, } from 'react';
53
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
54
54
  import { isPendingMediaNonTerminalStatus, isPendingMediaTerminalStatus, PendingMediaStatusSchema, } from '@ttt-productions/media-schemas';
55
55
  // Status values derive from media-schemas' one declaration.
56
56
  const STATUS = PendingMediaStatusSchema.enum;
@@ -114,6 +114,10 @@ export function InFlightUploadsProvider(props) {
114
114
  const { adapter, subscribe = defaultFirestoreSubscribe, children } = props;
115
115
  const [uploads, setUploads] = useState(new Map());
116
116
  const [sourceState, setSourceState] = useState('connecting');
117
+ const [restarts, setRestarts] = useState(0);
118
+ const [retrying, setRetrying] = useState(false);
119
+ /** Set by `retry` for the subscription the restart opens; that run keeps the source state. */
120
+ const restartRequestedRef = useRef(false);
117
121
  const seenTerminalIdsRef = useRef(new Set());
118
122
  const statusByIdRef = useRef(new Map());
119
123
  const hasProcessedInitialSnapshotRef = useRef(false);
@@ -138,8 +142,15 @@ export function InFlightUploadsProvider(props) {
138
142
  // the gap. A real identity change — including UID-A → UID-B with no
139
143
  // intermediate null user — starts clean; never rely on an intermediate
140
144
  // sign-out to clear refs between two authenticated UIDs.
145
+ // A restart the person asked for keeps the source state it had (a failure stays a failure
146
+ // until the restarted listener answers); every other run starts over as `connecting`.
147
+ let restarting = restartRequestedRef.current;
148
+ restartRequestedRef.current = false;
141
149
  setUploads(new Map());
142
- setSourceState('connecting');
150
+ if (!restarting) {
151
+ setSourceState('connecting');
152
+ setRetrying(false);
153
+ }
143
154
  if (userId !== null && userId !== trackedUserIdRef.current) {
144
155
  seenTerminalIdsRef.current = new Set();
145
156
  statusByIdRef.current = new Map();
@@ -210,7 +221,10 @@ export function InFlightUploadsProvider(props) {
210
221
  // Metadata-derived source state: stay `connecting` until a server-confirmed
211
222
  // snapshot, `offline` if only cached data arrives after being live.
212
223
  const fromCache = snapshot.metadata?.fromCache ?? false;
213
- setSourceState((prev) => (fromCache ? (prev === 'connecting' ? 'connecting' : 'offline') : 'live'));
224
+ // A snapshot after a failed listener comes from a new subscription: not yet live, never offline.
225
+ setSourceState((prev) => fromCache ? (prev === 'connecting' || prev === 'error' ? 'connecting' : 'offline') : 'live');
226
+ restarting = false;
227
+ setRetrying(false);
214
228
  if (updates.length > 0) {
215
229
  setUploads((prev) => {
216
230
  const next = new Map(prev);
@@ -278,9 +292,11 @@ export function InFlightUploadsProvider(props) {
278
292
  unsubscribe = undefined;
279
293
  if (disposed || attempt >= RESUBSCRIBE_DELAYS_MS.length) {
280
294
  setSourceState('error');
295
+ setRetrying(false);
281
296
  return;
282
297
  }
283
- setSourceState('connecting');
298
+ if (!restarting)
299
+ setSourceState('connecting');
284
300
  retryTimer = setTimeout(() => {
285
301
  if (disposed)
286
302
  return;
@@ -299,8 +315,13 @@ export function InFlightUploadsProvider(props) {
299
315
  };
300
316
  // adapter is intentionally NOT in the deps array — adapter is read via
301
317
  // adapterRef on every snapshot, so identity changes don't re-subscribe.
302
- }, [userId, subscribe]);
303
- const value = useMemo(() => ({ uploads, sourceState }), [uploads, sourceState]);
318
+ }, [userId, subscribe, restarts]);
319
+ const retry = useCallback(() => {
320
+ restartRequestedRef.current = true;
321
+ setRetrying(true);
322
+ setRestarts((count) => count + 1);
323
+ }, []);
324
+ const value = useMemo(() => ({ uploads, sourceState, retry, retrying }), [uploads, sourceState, retry, retrying]);
304
325
  return (_jsx(InFlightUploadsContext.Provider, { value: value, children: children }));
305
326
  }
306
327
  // =============================================================================
@@ -335,6 +356,17 @@ export function useUploadsSourceState() {
335
356
  }
336
357
  return ctx.sourceState;
337
358
  }
359
+ /**
360
+ * The uploads listener's restart — the Try again of a failure shown on `useUploadsSourceState` —
361
+ * and whether it is running.
362
+ */
363
+ export function useUploadsSourceRetry() {
364
+ const ctx = useContext(InFlightUploadsContext);
365
+ if (ctx === undefined) {
366
+ throw new Error('useUploadsSourceRetry must be used within InFlightUploadsProvider');
367
+ }
368
+ return { retry: ctx.retry, retrying: ctx.retrying };
369
+ }
338
370
  // =============================================================================
339
371
  // Tray helpers
340
372
  // =============================================================================
@@ -1 +1 @@
1
- {"version":3,"file":"in-flight-uploads-provider.js","sourceRoot":"","sources":["../../src/react/in-flight-uploads-provider.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// SANCTIONED EXCEPTION to the \"packages read/write Firestore only through\n// @ttt-productions/query-core\" rule (see docs/packages/package-architecture.md →\n// \"Firestore access goes through query-core\").\n//\n// WHY this provider reads Firestore directly instead of via a query-core hook:\n// 1. It needs snapshot *transition events* — onSnapshot's `docChanges()` with\n// added / modified / removed — to fire one-shot terminal callbacks and to\n// suppress the initial snapshot. query-core's hooks return \"the current\n// list\", not per-doc transitions, so they cannot express this state machine.\n// 2. The subscription is already abstracted behind an injectable\n// `FirestoreSubscribeFn` (the `subscribe` prop); the direct firebase code\n// path lives ONLY in `defaultFirestoreSubscribe` below and is fully\n// swappable (tests / alternate backends inject their own).\n// 3. The static ESM `firebase/firestore` import is load-bearing for Next.js +\n// Turbopack module identity (see defaultFirestoreSubscribe's comment).\n//\n// Do NOT \"migrate\" this onto query-core without preserving docChanges() semantics.\n// ─────────────────────────────────────────────────────────────────────────────\nimport { collection, onSnapshot, query, where } from 'firebase/firestore';\n\n/**\n * Generic in-flight uploads provider.\n *\n * Provides a global Map of in-flight upload state by `pendingMediaId`, derived\n * from a Firestore listener over the consumer-supplied collection. On terminal\n * status transitions observed THIS SESSION, fires consumer-supplied success /\n * failure / rejection callbacks. Records monitoring breadcrumbs and parse\n * errors through the injected monitoring callback.\n *\n * EVERY app-specific value is supplied by the consumer:\n * - userId resolution\n * - Firestore `db` reference\n * - collection path\n * - listener time window (ms)\n * - parser/schema for raw Firestore doc → typed PendingMedia\n * - success copy per origin\n * - notification callback (cache invalidation, etc.)\n * - toast callback\n * - rejection callback (for the media-rejection dialog or equivalent)\n * - monitoring callback (breadcrumbs + captured errors)\n *\n * Read APIs:\n * - `useInFlightUploadsState()` — full Map.\n * - `useInFlightUpload(id)` — single doc, narrowable on status.\n * - `useUploadActivityState()` — tray-visible items, sorted newest-first.\n *\n * The package owns the deduplication, initial-snapshot suppression, and\n * non-terminal → terminal transition tracking. It does NOT own anything about\n * what a file-origin is or what message text appears in toasts.\n */\n\nimport {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport {\n isPendingMediaNonTerminalStatus,\n isPendingMediaTerminalStatus,\n PendingMediaStatusSchema,\n type PendingMediaNonTerminalStatus,\n type PendingMediaStatus,\n} from '@ttt-productions/media-schemas';\n\n// Status values derive from media-schemas' one declaration.\nconst STATUS = PendingMediaStatusSchema.enum;\n\n// =============================================================================\n// Generic shape of the data the provider tracks\n// =============================================================================\n\n/** Generic pending-media status — media-schemas' one declaration. */\nexport type InFlightUploadStatus = PendingMediaStatus;\n\n/**\n * Generic in-flight upload record. The provider keeps a Map<id, InFlightUpload>\n * built from each parsed PendingMedia. Consumers parameterize the `fileOrigin`\n * type via the parser's return type (the provider treats it as opaque).\n *\n * `TFileOrigin` defaults to `string` so unannotated consumers get a permissive\n * shape; consuming apps bind it to their own `FileOrigin` union at the call site.\n */\nexport type InFlightUploadBase<TFileOrigin extends string = string> = {\n id: string;\n fileOrigin: TFileOrigin;\n surface: string;\n createdAt: number;\n targetIds?: string[];\n originalContentType?: string;\n originalSize?: number;\n};\n\nexport type InFlightUploadActive<TFileOrigin extends string = string> =\n InFlightUploadBase<TFileOrigin> & {\n status: PendingMediaNonTerminalStatus;\n };\n\nexport type InFlightUploadCompleted<TFileOrigin extends string = string> =\n InFlightUploadBase<TFileOrigin> & {\n status: typeof PendingMediaStatusSchema.enum.completed;\n completedAt: number;\n uploadTrayClearedAt?: number;\n uploadTrayClearedBy?: string;\n uploadTraySeenAt?: number;\n uploadTraySeenBy?: string;\n };\n\nexport type InFlightUploadFailed<TFileOrigin extends string = string> =\n InFlightUploadBase<TFileOrigin> & {\n status: typeof PendingMediaStatusSchema.enum.failed;\n failedAt: number;\n errorCategory: string;\n errorMessage: string;\n uploadTrayClearedAt?: number;\n uploadTrayClearedBy?: string;\n uploadTraySeenAt?: number;\n uploadTraySeenBy?: string;\n };\n\nexport type InFlightUploadRejected<TFileOrigin extends string = string> =\n InFlightUploadBase<TFileOrigin> & {\n status: typeof PendingMediaStatusSchema.enum.rejected;\n rejectedAt: number;\n rejectionType: 'text' | 'media';\n errorMessage: string;\n violationId?: string;\n uploadTrayClearedAt?: number;\n uploadTrayClearedBy?: string;\n uploadTraySeenAt?: number;\n uploadTraySeenBy?: string;\n };\n\nexport type InFlightUpload<TFileOrigin extends string = string> =\n | InFlightUploadActive<TFileOrigin>\n | InFlightUploadCompleted<TFileOrigin>\n | InFlightUploadFailed<TFileOrigin>\n | InFlightUploadRejected<TFileOrigin>;\n\n// =============================================================================\n// Adapter contract — every app-specific behavior is injected here\n// =============================================================================\n\n/**\n * Result of the consumer-supplied parser. The provider keeps the raw parsed\n * record for callbacks (so consumers can read domain events, target IDs, etc.)\n * AND derives the generic InFlightUpload from it for the public Map.\n *\n * The parser MUST throw if validation fails; the provider catches and routes\n * the error to the monitoring callback.\n */\nexport type ParsedPendingMedia<TFileOrigin extends string = string> = {\n id: string;\n userId: string;\n fileOrigin: TFileOrigin;\n status: InFlightUploadStatus;\n createdAt: number;\n surface: string;\n targetIds?: string[];\n originalContentType?: string;\n originalSize?: number;\n // Terminal-status fields (only meaningful for the matching status)\n completedAt?: number;\n failedAt?: number;\n rejectedAt?: number;\n errorCategory?: string;\n errorMessage?: string;\n rejectionType?: 'text' | 'media';\n violationId?: string;\n uploadTrayClearedAt?: number;\n uploadTrayClearedBy?: string;\n uploadTraySeenAt?: number;\n uploadTraySeenBy?: string;\n /**\n * Opaque consumer-controlled domain-event payload, surfaced to the\n * `onUploadCompleted` and `onUploadRejected` callbacks for cache\n * invalidation. The provider does not interpret it.\n */\n domainEvents?: unknown;\n};\n\nexport type InFlightUploadsAdapter<TFileOrigin extends string = string> = {\n /** Returns the current user's uid, or null if not signed in. */\n getCurrentUserId: () => string | null;\n\n /** Firestore database adapter (loose typing — `unknown` keeps zero firebase coupling at the type level). */\n db: unknown;\n\n /** Collection path holding pending-media docs (e.g. \"pendingMedia\"). */\n collectionPath: string;\n\n /** Listener window in milliseconds — only docs with `createdAt >= now - window` are tracked. */\n listenerWindowMs: number;\n\n /**\n * Parse a raw Firestore doc snapshot's `.data()` into the generic shape the\n * provider tracks. Throws on validation failure; the provider routes thrown\n * errors to `onMonitoringEvent`.\n */\n parsePendingMedia: (raw: unknown) => ParsedPendingMedia<TFileOrigin>;\n\n /** Called once per observed non-terminal → terminal completed transition. */\n onUploadCompleted: (parsed: ParsedPendingMedia<TFileOrigin>) => void;\n\n /** Called once per observed non-terminal → terminal failed transition. */\n onUploadFailed: (parsed: ParsedPendingMedia<TFileOrigin>) => void;\n\n /** Called once per observed non-terminal → terminal rejected transition. */\n onUploadRejected: (parsed: ParsedPendingMedia<TFileOrigin>) => void;\n\n /**\n * Monitoring callback. Receives:\n * - `breadcrumb`: informational, fired on every observed terminal transition.\n * - `parse-error`: when `parsePendingMedia` throws.\n * - `listener-error`: when the Firestore listener's error callback fires.\n *\n * Consumers wire this to Sentry, OpenTelemetry, or any other observability\n * provider. The provider does NOT import any monitoring SDK directly.\n */\n onMonitoringEvent: (event: InFlightUploadsMonitoringEvent<TFileOrigin>) => void;\n};\n\nexport type InFlightUploadsMonitoringEvent<TFileOrigin extends string = string> =\n | {\n type: 'breadcrumb';\n category: 'upload.terminal';\n level: 'info';\n message: string;\n data: {\n id: string;\n fileOrigin: TFileOrigin;\n status: InFlightUploadStatus;\n surface: string;\n };\n }\n | {\n type: 'parse-error';\n docId: string;\n error: unknown;\n data: unknown;\n }\n | {\n type: 'listener-error';\n error: unknown;\n };\n\n// =============================================================================\n// Firestore subscription contract — abstracted to avoid firebase coupling\n// =============================================================================\n\n/**\n * Minimal subscription contract used by the provider. The default\n * implementation lives in `firestore-subscribe.ts` and uses the standard\n * `firebase/firestore` `onSnapshot` + `query` + `where` builders.\n *\n * Tests inject a fake to drive the listener without spinning up Firestore.\n */\nexport type FirestoreSubscribeFn = (args: {\n db: unknown;\n collectionPath: string;\n userId: string;\n windowStartMs: number;\n onSnapshot: (snapshot: FirestoreLikeSnapshot) => void;\n onError: (error: unknown) => void;\n}) => () => void;\n\nexport type FirestoreLikeSnapshot = {\n docChanges: () => FirestoreLikeDocChange[];\n /**\n * Snapshot metadata. Present when the subscription is opened with\n * `{ includeMetadataChanges: true }` (the default firebase implementation does).\n * `fromCache === false` means a server-confirmed snapshot. Optional so injected\n * test fakes need not supply it (absent ⇒ treated as server-confirmed).\n */\n metadata?: { fromCache: boolean };\n};\n\n/**\n * Metadata-derived liveness of the uploads listener, mirroring query-core's\n * `FirestoreSourceState`. Firestore serves cached snapshots offline WITHOUT\n * firing the error callback, so the Files tray source needs this to show a\n * degraded indicator instead of a false \"all caught up\":\n * - `connecting` — no server-confirmed snapshot yet.\n * - `live` — at least one server-confirmed snapshot (`fromCache === false`).\n * - `offline` — only cached data after having been live.\n * - `error` — the listener error callback fired.\n */\nexport type UploadsSourceState = 'connecting' | 'live' | 'offline' | 'error';\n\nexport type FirestoreLikeDocChange = {\n type: 'added' | 'modified' | 'removed';\n doc: {\n id: string;\n data: () => unknown;\n };\n};\n\n// =============================================================================\n// Provider\n// =============================================================================\n\ninterface InFlightUploadsContextValue<TFileOrigin extends string = string> {\n uploads: Map<string, InFlightUpload<TFileOrigin>>;\n sourceState: UploadsSourceState;\n}\n\nconst InFlightUploadsContext = createContext<InFlightUploadsContextValue | undefined>(undefined);\n\nexport interface InFlightUploadsProviderProps<TFileOrigin extends string = string> {\n adapter: InFlightUploadsAdapter<TFileOrigin>;\n /** Firestore subscription function. Defaults to the firebase/firestore implementation. */\n subscribe?: FirestoreSubscribeFn;\n children: ReactNode;\n}\n\nfunction fromParsed<TFileOrigin extends string>(\n parsed: ParsedPendingMedia<TFileOrigin>,\n): InFlightUpload<TFileOrigin> {\n const base: InFlightUploadBase<TFileOrigin> = {\n id: parsed.id,\n fileOrigin: parsed.fileOrigin,\n surface: parsed.surface,\n createdAt: parsed.createdAt,\n targetIds: parsed.targetIds,\n originalContentType: parsed.originalContentType,\n originalSize: parsed.originalSize,\n };\n if (isPendingMediaNonTerminalStatus(parsed.status)) {\n return { ...base, status: parsed.status };\n }\n if (parsed.status === STATUS.completed) {\n return {\n ...base,\n status: STATUS.completed,\n completedAt: parsed.completedAt ?? parsed.createdAt,\n uploadTrayClearedAt: parsed.uploadTrayClearedAt,\n uploadTrayClearedBy: parsed.uploadTrayClearedBy,\n uploadTraySeenAt: parsed.uploadTraySeenAt,\n uploadTraySeenBy: parsed.uploadTraySeenBy,\n };\n }\n if (parsed.status === STATUS.failed) {\n return {\n ...base,\n status: STATUS.failed,\n failedAt: parsed.failedAt ?? parsed.createdAt,\n errorCategory: parsed.errorCategory ?? 'system',\n errorMessage: parsed.errorMessage ?? '',\n uploadTrayClearedAt: parsed.uploadTrayClearedAt,\n uploadTrayClearedBy: parsed.uploadTrayClearedBy,\n uploadTraySeenAt: parsed.uploadTraySeenAt,\n uploadTraySeenBy: parsed.uploadTraySeenBy,\n };\n }\n // rejected\n return {\n ...base,\n status: STATUS.rejected,\n rejectedAt: parsed.rejectedAt ?? parsed.createdAt,\n rejectionType: parsed.rejectionType ?? 'media',\n errorMessage: parsed.errorMessage ?? '',\n violationId: parsed.violationId,\n uploadTrayClearedAt: parsed.uploadTrayClearedAt,\n uploadTrayClearedBy: parsed.uploadTrayClearedBy,\n // Carry the seen fields through (round-9 finding 9): dropping them on the\n // `rejected` branch — while `completed`/`failed` keep them — made a seen\n // rejected upload relight the Files dot forever, because the tray's\n // unseen-terminal check never saw `uploadTraySeenAt`.\n uploadTraySeenAt: parsed.uploadTraySeenAt,\n uploadTraySeenBy: parsed.uploadTraySeenBy,\n };\n}\n\nexport function InFlightUploadsProvider<TFileOrigin extends string = string>(\n props: InFlightUploadsProviderProps<TFileOrigin>,\n) {\n const { adapter, subscribe = defaultFirestoreSubscribe, children } = props;\n\n const [uploads, setUploads] = useState<Map<string, InFlightUpload<TFileOrigin>>>(new Map());\n const [sourceState, setSourceState] = useState<UploadsSourceState>('connecting');\n const seenTerminalIdsRef = useRef<Set<string>>(new Set());\n const statusByIdRef = useRef<Map<string, InFlightUploadStatus>>(new Map());\n const hasProcessedInitialSnapshotRef = useRef(false);\n /** The identity the dedup/tracking refs belong to — they outlive resubscribes\n * for the SAME user (auth-settling flaps) and reset only on a real identity\n * change. See the reset block in the subscription effect. */\n const trackedUserIdRef = useRef<string | null>(null);\n\n // Stable refs for adapter callbacks — read from latest adapter inside effect.\n const adapterRef = useRef(adapter);\n useEffect(() => {\n adapterRef.current = adapter;\n });\n\n const userId = adapter.getCurrentUserId();\n\n useEffect(() => {\n // The uploads map and source state rebuild from every subscription's initial\n // snapshot, so they reset on every effect run. The terminal-dedup and\n // status-tracking refs are IDENTITY-scoped, not subscription-scoped: an\n // auth-settling flap (uid → null → same uid re-subscribes) must neither\n // re-fire terminals the previous subscription already fired (seen set\n // persists) nor lose the observed non-terminal statuses that let the new\n // subscription's initial listing detect a transition that completed during\n // the gap. A real identity change — including UID-A → UID-B with no\n // intermediate null user — starts clean; never rely on an intermediate\n // sign-out to clear refs between two authenticated UIDs.\n setUploads(new Map());\n setSourceState('connecting');\n if (userId !== null && userId !== trackedUserIdRef.current) {\n seenTerminalIdsRef.current = new Set();\n statusByIdRef.current = new Map();\n trackedUserIdRef.current = userId;\n }\n hasProcessedInitialSnapshotRef.current = false;\n\n if (!userId) {\n return;\n }\n\n const a = adapterRef.current;\n const windowStartMs = Date.now() - a.listenerWindowMs;\n\n const handleSnapshot = (snapshot: FirestoreLikeSnapshot) => {\n const updates: Array<{ id: string; upload: InFlightUpload<TFileOrigin> | null }> = [];\n const newlyTerminal: Array<ParsedPendingMedia<TFileOrigin>> = [];\n const isInitialSnapshot = !hasProcessedInitialSnapshotRef.current;\n\n for (const change of snapshot.docChanges()) {\n if (change.type === 'removed') {\n updates.push({ id: change.doc.id, upload: null });\n statusByIdRef.current.delete(change.doc.id);\n seenTerminalIdsRef.current.delete(change.doc.id);\n continue;\n }\n\n let parsed: ParsedPendingMedia<TFileOrigin>;\n try {\n parsed = adapterRef.current.parsePendingMedia(change.doc.data());\n } catch (error) {\n adapterRef.current.onMonitoringEvent({\n type: 'parse-error',\n docId: change.doc.id,\n error,\n data: change.doc.data(),\n });\n continue;\n }\n\n const upload = fromParsed(parsed);\n updates.push({ id: parsed.id, upload });\n\n const previousStatus = statusByIdRef.current.get(parsed.id);\n const currentStatus = parsed.status;\n const currentIsTerminal = isPendingMediaTerminalStatus(currentStatus);\n const previousWasNonTerminal = isPendingMediaNonTerminalStatus(previousStatus);\n\n if (isInitialSnapshot) {\n // A subscription's first snapshot is a (re-)listing of current state,\n // not a live event: already-terminal docs stay suppressed so a page\n // refresh never re-toasts. ONE exception — this provider instance\n // tracked the doc as non-terminal before (same-identity resubscribe\n // gap; the tracking refs survive those), so the terminal listing IS\n // the transition, observed across the gap, and must fire.\n if (currentIsTerminal) {\n if (previousWasNonTerminal && !seenTerminalIdsRef.current.has(parsed.id)) {\n newlyTerminal.push(parsed);\n }\n seenTerminalIdsRef.current.add(parsed.id);\n }\n } else if (currentIsTerminal && !seenTerminalIdsRef.current.has(parsed.id)) {\n // Any unseen terminal in a LIVE snapshot fires — including a doc whose\n // FIRST client-visible state is already terminal. A fast backend\n // outcome (moderation rejection in ~2s) can coalesce created→terminal\n // into one delivery, and the uploader is actively waiting on exactly\n // this callback; swallowing it silently loses a rejection dialog.\n seenTerminalIdsRef.current.add(parsed.id);\n newlyTerminal.push(parsed);\n }\n\n statusByIdRef.current.set(parsed.id, currentStatus);\n }\n\n hasProcessedInitialSnapshotRef.current = true;\n\n // Metadata-derived source state: stay `connecting` until a server-confirmed\n // snapshot, `offline` if only cached data arrives after being live.\n const fromCache = snapshot.metadata?.fromCache ?? false;\n setSourceState((prev) => (fromCache ? (prev === 'connecting' ? 'connecting' : 'offline') : 'live'));\n\n if (updates.length > 0) {\n setUploads((prev) => {\n const next = new Map(prev);\n for (const { id, upload } of updates) {\n if (upload === null) next.delete(id);\n else next.set(id, upload);\n }\n return next;\n });\n }\n\n for (const parsed of newlyTerminal) {\n handleTerminal(parsed);\n }\n };\n\n function handleTerminal(parsed: ParsedPendingMedia<TFileOrigin>) {\n adapterRef.current.onMonitoringEvent({\n type: 'breadcrumb',\n category: 'upload.terminal',\n level: 'info',\n message: `${parsed.fileOrigin}:${parsed.status}`,\n data: {\n id: parsed.id,\n fileOrigin: parsed.fileOrigin,\n status: parsed.status,\n surface: parsed.surface,\n },\n });\n\n if (parsed.status === STATUS.completed) {\n adapterRef.current.onUploadCompleted(parsed);\n } else if (parsed.status === STATUS.failed) {\n adapterRef.current.onUploadFailed(parsed);\n } else if (parsed.status === STATUS.rejected) {\n adapterRef.current.onUploadRejected(parsed);\n }\n }\n\n // Bounded RESUBSCRIBE on listener error: a cold-session race (e.g. Firestore\n // App Check enforcement rejecting the first attach before the token is\n // minted) must not kill the listener for the whole session — observed live:\n // one permission-denied at first login → sourceState 'error' forever, every\n // tray file status dead until reload. Retry on a short ladder; a snapshot\n // resets the ladder (healthy again); exhausting it goes terminal 'error'\n // (a real rules denial stays denied — retries are cheap and bounded).\n const RESUBSCRIBE_DELAYS_MS = [1_000, 3_000, 10_000, 30_000];\n let attempt = 0;\n let disposed = false;\n let retryTimer: ReturnType<typeof setTimeout> | undefined;\n let unsubscribe: (() => void) | undefined;\n\n const start = () => {\n unsubscribe = subscribe({\n db: a.db,\n collectionPath: a.collectionPath,\n userId,\n windowStartMs,\n onSnapshot: (snapshot) => {\n attempt = 0; // healthy — future errors get a fresh ladder\n handleSnapshot(snapshot);\n },\n onError: (error) => {\n adapterRef.current.onMonitoringEvent({ type: 'listener-error', error });\n unsubscribe?.();\n unsubscribe = undefined;\n if (disposed || attempt >= RESUBSCRIBE_DELAYS_MS.length) {\n setSourceState('error');\n return;\n }\n setSourceState('connecting');\n retryTimer = setTimeout(() => {\n if (disposed) return;\n attempt += 1;\n start();\n }, RESUBSCRIBE_DELAYS_MS[attempt]);\n },\n });\n };\n start();\n\n return () => {\n disposed = true;\n if (retryTimer) clearTimeout(retryTimer);\n unsubscribe?.();\n };\n // adapter is intentionally NOT in the deps array — adapter is read via\n // adapterRef on every snapshot, so identity changes don't re-subscribe.\n\n }, [userId, subscribe]);\n\n const value = useMemo<InFlightUploadsContextValue<TFileOrigin>>(\n () => ({ uploads, sourceState }),\n [uploads, sourceState],\n );\n\n return (\n <InFlightUploadsContext.Provider value={value as InFlightUploadsContextValue}>\n {children}\n </InFlightUploadsContext.Provider>\n );\n}\n\n// =============================================================================\n// Read APIs\n// =============================================================================\n\nexport function useInFlightUploadsState<TFileOrigin extends string = string>(): Map<\n string,\n InFlightUpload<TFileOrigin>\n> {\n const ctx = useContext(InFlightUploadsContext);\n if (ctx === undefined) {\n throw new Error('useInFlightUploadsState must be used within InFlightUploadsProvider');\n }\n return ctx.uploads as Map<string, InFlightUpload<TFileOrigin>>;\n}\n\nexport function useInFlightUpload<TFileOrigin extends string = string>(\n id: string | null | undefined,\n): InFlightUpload<TFileOrigin> | undefined {\n const ctx = useContext(InFlightUploadsContext);\n if (ctx === undefined) {\n throw new Error('useInFlightUpload must be used within InFlightUploadsProvider');\n }\n if (!id) return undefined;\n return ctx.uploads.get(id) as InFlightUpload<TFileOrigin> | undefined;\n}\n\n/**\n * Metadata-derived source state of the uploads listener\n * (`connecting | live | offline | error`). The Files tray uses this to show a\n * degraded indicator and never display \"all caught up\" while the source is\n * `offline`/`error`/`connecting`.\n */\nexport function useUploadsSourceState(): UploadsSourceState {\n const ctx = useContext(InFlightUploadsContext);\n if (ctx === undefined) {\n throw new Error('useUploadsSourceState must be used within InFlightUploadsProvider');\n }\n return ctx.sourceState;\n}\n\n// =============================================================================\n// Tray helpers\n// =============================================================================\n\n/**\n * `true` for `completed` / `failed` / `rejected`. Used by tray UI to decide\n * whether a Clear action is available for an item.\n */\nexport function isTerminalUpload<TFileOrigin extends string = string>(\n item: InFlightUpload<TFileOrigin>,\n): item is Exclude<InFlightUpload<TFileOrigin>, { status: PendingMediaNonTerminalStatus }> {\n return isPendingMediaTerminalStatus(item.status);\n}\n\n/**\n * Tray-visible items. Includes every pending/processing upload, plus terminal\n * uploads that have NOT been cleared from the tray. Sorted newest-first by\n * createdAt.\n *\n * The public selector name in upload-ui is `useUploadActivityState`. The\n * mutation hook is `useClearUploadActivity` (in a separate file).\n */\nexport function useUploadActivityState<TFileOrigin extends string = string>(): InFlightUpload<TFileOrigin>[] {\n const uploads = useInFlightUploadsState<TFileOrigin>();\n return useMemo(() => {\n return Array.from(uploads.values())\n .filter((item) => {\n if (!isTerminalUpload(item)) return true;\n return item.uploadTrayClearedAt === undefined;\n })\n .sort((a, b) => b.createdAt - a.createdAt);\n }, [uploads]);\n}\n\n// =============================================================================\n// Default Firestore subscription implementation\n// =============================================================================\n\n/**\n * Default Firestore subscription using the consumer's `firebase/firestore`\n * module instance (via static ESM import at the top of this file).\n *\n * Using static ESM import — NOT `require()` — is load-bearing. CommonJS\n * `require('firebase/firestore')` and ESM `import 'firebase/firestore'` resolve\n * to different module instances under Next.js + Turbopack. If the consumer\n * constructs `db` via ESM and we receive it here, but we then call\n * `collection()` from a CJS-loaded copy of firebase, the firebase runtime\n * does not recognize the db as a Firestore instance and throws\n * \"Expected first argument to collection() to be a CollectionReference,\n * a DocumentReference or FirebaseFirestore\".\n *\n * Consumers who want to skip the firebase code path entirely (tests, alt\n * backends) supply their own `subscribe` prop to `InFlightUploadsProvider`.\n */\nconst defaultFirestoreSubscribe: FirestoreSubscribeFn = (args) => {\n const q = query(\n\n collection(args.db as any, args.collectionPath),\n where('userId', '==', args.userId),\n where('createdAt', '>=', args.windowStartMs),\n );\n // includeMetadataChanges so the provider can derive a `connecting|live|offline|error`\n // source state from `snapshot.metadata.fromCache` (the Files tray degraded indicator).\n return onSnapshot(q, { includeMetadataChanges: true }, args.onSnapshot as never, args.onError as never);\n};\n"]}
1
+ {"version":3,"file":"in-flight-uploads-provider.js","sourceRoot":"","sources":["../../src/react/in-flight-uploads-provider.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\n// ─────────────────────────────────────────────────────────────────────────────\n// SANCTIONED EXCEPTION to the \"packages read/write Firestore only through\n// @ttt-productions/query-core\" rule (see docs/packages/package-architecture.md →\n// \"Firestore access goes through query-core\").\n//\n// WHY this provider reads Firestore directly instead of via a query-core hook:\n// 1. It needs snapshot *transition events* — onSnapshot's `docChanges()` with\n// added / modified / removed — to fire one-shot terminal callbacks and to\n// suppress the initial snapshot. query-core's hooks return \"the current\n// list\", not per-doc transitions, so they cannot express this state machine.\n// 2. The subscription is already abstracted behind an injectable\n// `FirestoreSubscribeFn` (the `subscribe` prop); the direct firebase code\n// path lives ONLY in `defaultFirestoreSubscribe` below and is fully\n// swappable (tests / alternate backends inject their own).\n// 3. The static ESM `firebase/firestore` import is load-bearing for Next.js +\n// Turbopack module identity (see defaultFirestoreSubscribe's comment).\n//\n// Do NOT \"migrate\" this onto query-core without preserving docChanges() semantics.\n// ─────────────────────────────────────────────────────────────────────────────\nimport { collection, onSnapshot, query, where } from 'firebase/firestore';\n\n/**\n * Generic in-flight uploads provider.\n *\n * Provides a global Map of in-flight upload state by `pendingMediaId`, derived\n * from a Firestore listener over the consumer-supplied collection. On terminal\n * status transitions observed THIS SESSION, fires consumer-supplied success /\n * failure / rejection callbacks. Records monitoring breadcrumbs and parse\n * errors through the injected monitoring callback.\n *\n * EVERY app-specific value is supplied by the consumer:\n * - userId resolution\n * - Firestore `db` reference\n * - collection path\n * - listener time window (ms)\n * - parser/schema for raw Firestore doc → typed PendingMedia\n * - success copy per origin\n * - notification callback (cache invalidation, etc.)\n * - toast callback\n * - rejection callback (for the media-rejection dialog or equivalent)\n * - monitoring callback (breadcrumbs + captured errors)\n *\n * Read APIs:\n * - `useInFlightUploadsState()` — full Map.\n * - `useInFlightUpload(id)` — single doc, narrowable on status.\n * - `useUploadActivityState()` — tray-visible items, sorted newest-first.\n *\n * The package owns the deduplication, initial-snapshot suppression, and\n * non-terminal → terminal transition tracking. It does NOT own anything about\n * what a file-origin is or what message text appears in toasts.\n */\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\nimport {\n isPendingMediaNonTerminalStatus,\n isPendingMediaTerminalStatus,\n PendingMediaStatusSchema,\n type PendingMediaNonTerminalStatus,\n type PendingMediaStatus,\n} from '@ttt-productions/media-schemas';\n\n// Status values derive from media-schemas' one declaration.\nconst STATUS = PendingMediaStatusSchema.enum;\n\n// =============================================================================\n// Generic shape of the data the provider tracks\n// =============================================================================\n\n/** Generic pending-media status — media-schemas' one declaration. */\nexport type InFlightUploadStatus = PendingMediaStatus;\n\n/**\n * Generic in-flight upload record. The provider keeps a Map<id, InFlightUpload>\n * built from each parsed PendingMedia. Consumers parameterize the `fileOrigin`\n * type via the parser's return type (the provider treats it as opaque).\n *\n * `TFileOrigin` defaults to `string` so unannotated consumers get a permissive\n * shape; consuming apps bind it to their own `FileOrigin` union at the call site.\n */\nexport type InFlightUploadBase<TFileOrigin extends string = string> = {\n id: string;\n fileOrigin: TFileOrigin;\n surface: string;\n createdAt: number;\n targetIds?: string[];\n originalContentType?: string;\n originalSize?: number;\n};\n\nexport type InFlightUploadActive<TFileOrigin extends string = string> =\n InFlightUploadBase<TFileOrigin> & {\n status: PendingMediaNonTerminalStatus;\n };\n\nexport type InFlightUploadCompleted<TFileOrigin extends string = string> =\n InFlightUploadBase<TFileOrigin> & {\n status: typeof PendingMediaStatusSchema.enum.completed;\n completedAt: number;\n uploadTrayClearedAt?: number;\n uploadTrayClearedBy?: string;\n uploadTraySeenAt?: number;\n uploadTraySeenBy?: string;\n };\n\nexport type InFlightUploadFailed<TFileOrigin extends string = string> =\n InFlightUploadBase<TFileOrigin> & {\n status: typeof PendingMediaStatusSchema.enum.failed;\n failedAt: number;\n errorCategory: string;\n errorMessage: string;\n uploadTrayClearedAt?: number;\n uploadTrayClearedBy?: string;\n uploadTraySeenAt?: number;\n uploadTraySeenBy?: string;\n };\n\nexport type InFlightUploadRejected<TFileOrigin extends string = string> =\n InFlightUploadBase<TFileOrigin> & {\n status: typeof PendingMediaStatusSchema.enum.rejected;\n rejectedAt: number;\n rejectionType: 'text' | 'media';\n errorMessage: string;\n violationId?: string;\n uploadTrayClearedAt?: number;\n uploadTrayClearedBy?: string;\n uploadTraySeenAt?: number;\n uploadTraySeenBy?: string;\n };\n\nexport type InFlightUpload<TFileOrigin extends string = string> =\n | InFlightUploadActive<TFileOrigin>\n | InFlightUploadCompleted<TFileOrigin>\n | InFlightUploadFailed<TFileOrigin>\n | InFlightUploadRejected<TFileOrigin>;\n\n// =============================================================================\n// Adapter contract — every app-specific behavior is injected here\n// =============================================================================\n\n/**\n * Result of the consumer-supplied parser. The provider keeps the raw parsed\n * record for callbacks (so consumers can read domain events, target IDs, etc.)\n * AND derives the generic InFlightUpload from it for the public Map.\n *\n * The parser MUST throw if validation fails; the provider catches and routes\n * the error to the monitoring callback.\n */\nexport type ParsedPendingMedia<TFileOrigin extends string = string> = {\n id: string;\n userId: string;\n fileOrigin: TFileOrigin;\n status: InFlightUploadStatus;\n createdAt: number;\n surface: string;\n targetIds?: string[];\n originalContentType?: string;\n originalSize?: number;\n // Terminal-status fields (only meaningful for the matching status)\n completedAt?: number;\n failedAt?: number;\n rejectedAt?: number;\n errorCategory?: string;\n errorMessage?: string;\n rejectionType?: 'text' | 'media';\n violationId?: string;\n uploadTrayClearedAt?: number;\n uploadTrayClearedBy?: string;\n uploadTraySeenAt?: number;\n uploadTraySeenBy?: string;\n /**\n * Opaque consumer-controlled domain-event payload, surfaced to the\n * `onUploadCompleted` and `onUploadRejected` callbacks for cache\n * invalidation. The provider does not interpret it.\n */\n domainEvents?: unknown;\n};\n\nexport type InFlightUploadsAdapter<TFileOrigin extends string = string> = {\n /** Returns the current user's uid, or null if not signed in. */\n getCurrentUserId: () => string | null;\n\n /** Firestore database adapter (loose typing — `unknown` keeps zero firebase coupling at the type level). */\n db: unknown;\n\n /** Collection path holding pending-media docs (e.g. \"pendingMedia\"). */\n collectionPath: string;\n\n /** Listener window in milliseconds — only docs with `createdAt >= now - window` are tracked. */\n listenerWindowMs: number;\n\n /**\n * Parse a raw Firestore doc snapshot's `.data()` into the generic shape the\n * provider tracks. Throws on validation failure; the provider routes thrown\n * errors to `onMonitoringEvent`.\n */\n parsePendingMedia: (raw: unknown) => ParsedPendingMedia<TFileOrigin>;\n\n /** Called once per observed non-terminal → terminal completed transition. */\n onUploadCompleted: (parsed: ParsedPendingMedia<TFileOrigin>) => void;\n\n /** Called once per observed non-terminal → terminal failed transition. */\n onUploadFailed: (parsed: ParsedPendingMedia<TFileOrigin>) => void;\n\n /** Called once per observed non-terminal → terminal rejected transition. */\n onUploadRejected: (parsed: ParsedPendingMedia<TFileOrigin>) => void;\n\n /**\n * Monitoring callback. Receives:\n * - `breadcrumb`: informational, fired on every observed terminal transition.\n * - `parse-error`: when `parsePendingMedia` throws.\n * - `listener-error`: when the Firestore listener's error callback fires.\n *\n * Consumers wire this to Sentry, OpenTelemetry, or any other observability\n * provider. The provider does NOT import any monitoring SDK directly.\n */\n onMonitoringEvent: (event: InFlightUploadsMonitoringEvent<TFileOrigin>) => void;\n};\n\nexport type InFlightUploadsMonitoringEvent<TFileOrigin extends string = string> =\n | {\n type: 'breadcrumb';\n category: 'upload.terminal';\n level: 'info';\n message: string;\n data: {\n id: string;\n fileOrigin: TFileOrigin;\n status: InFlightUploadStatus;\n surface: string;\n };\n }\n | {\n type: 'parse-error';\n docId: string;\n error: unknown;\n data: unknown;\n }\n | {\n type: 'listener-error';\n error: unknown;\n };\n\n// =============================================================================\n// Firestore subscription contract — abstracted to avoid firebase coupling\n// =============================================================================\n\n/**\n * Minimal subscription contract used by the provider. The default\n * implementation lives in `firestore-subscribe.ts` and uses the standard\n * `firebase/firestore` `onSnapshot` + `query` + `where` builders.\n *\n * Tests inject a fake to drive the listener without spinning up Firestore.\n */\nexport type FirestoreSubscribeFn = (args: {\n db: unknown;\n collectionPath: string;\n userId: string;\n windowStartMs: number;\n onSnapshot: (snapshot: FirestoreLikeSnapshot) => void;\n onError: (error: unknown) => void;\n}) => () => void;\n\nexport type FirestoreLikeSnapshot = {\n docChanges: () => FirestoreLikeDocChange[];\n /**\n * Snapshot metadata. Present when the subscription is opened with\n * `{ includeMetadataChanges: true }` (the default firebase implementation does).\n * `fromCache === false` means a server-confirmed snapshot. Optional so injected\n * test fakes need not supply it (absent ⇒ treated as server-confirmed).\n */\n metadata?: { fromCache: boolean };\n};\n\n/**\n * Metadata-derived liveness of the uploads listener, mirroring query-core's\n * `FirestoreSourceState`. Firestore serves cached snapshots offline WITHOUT\n * firing the error callback, so the Files tray source needs this to show a\n * degraded indicator instead of a false \"all caught up\":\n * - `connecting` — no server-confirmed snapshot yet.\n * - `live` — at least one server-confirmed snapshot (`fromCache === false`).\n * - `offline` — only cached data after having been live.\n * - `error` — the listener error callback fired.\n */\nexport type UploadsSourceState = 'connecting' | 'live' | 'offline' | 'error';\n\nexport type FirestoreLikeDocChange = {\n type: 'added' | 'modified' | 'removed';\n doc: {\n id: string;\n data: () => unknown;\n };\n};\n\n// =============================================================================\n// Provider\n// =============================================================================\n\ninterface InFlightUploadsContextValue<TFileOrigin extends string = string> {\n uploads: Map<string, InFlightUpload<TFileOrigin>>;\n sourceState: UploadsSourceState;\n retry: () => void;\n retrying: boolean;\n}\n\n/** Restarting the uploads listener after it failed, and whether that restart is running. */\nexport interface UploadsSourceRetry {\n /**\n * Tear the listener down and subscribe again on a fresh retry ladder. While the restart runs the\n * source state stays as it was (an `error` stays `error`), so a failure shown on it stays on\n * screen with its control pending.\n */\n retry: () => void;\n /** A `retry` is running: true until the restarted listener answers or its ladder fails again. */\n retrying: boolean;\n}\n\nconst InFlightUploadsContext = createContext<InFlightUploadsContextValue | undefined>(undefined);\n\nexport interface InFlightUploadsProviderProps<TFileOrigin extends string = string> {\n adapter: InFlightUploadsAdapter<TFileOrigin>;\n /** Firestore subscription function. Defaults to the firebase/firestore implementation. */\n subscribe?: FirestoreSubscribeFn;\n children: ReactNode;\n}\n\nfunction fromParsed<TFileOrigin extends string>(\n parsed: ParsedPendingMedia<TFileOrigin>,\n): InFlightUpload<TFileOrigin> {\n const base: InFlightUploadBase<TFileOrigin> = {\n id: parsed.id,\n fileOrigin: parsed.fileOrigin,\n surface: parsed.surface,\n createdAt: parsed.createdAt,\n targetIds: parsed.targetIds,\n originalContentType: parsed.originalContentType,\n originalSize: parsed.originalSize,\n };\n if (isPendingMediaNonTerminalStatus(parsed.status)) {\n return { ...base, status: parsed.status };\n }\n if (parsed.status === STATUS.completed) {\n return {\n ...base,\n status: STATUS.completed,\n completedAt: parsed.completedAt ?? parsed.createdAt,\n uploadTrayClearedAt: parsed.uploadTrayClearedAt,\n uploadTrayClearedBy: parsed.uploadTrayClearedBy,\n uploadTraySeenAt: parsed.uploadTraySeenAt,\n uploadTraySeenBy: parsed.uploadTraySeenBy,\n };\n }\n if (parsed.status === STATUS.failed) {\n return {\n ...base,\n status: STATUS.failed,\n failedAt: parsed.failedAt ?? parsed.createdAt,\n errorCategory: parsed.errorCategory ?? 'system',\n errorMessage: parsed.errorMessage ?? '',\n uploadTrayClearedAt: parsed.uploadTrayClearedAt,\n uploadTrayClearedBy: parsed.uploadTrayClearedBy,\n uploadTraySeenAt: parsed.uploadTraySeenAt,\n uploadTraySeenBy: parsed.uploadTraySeenBy,\n };\n }\n // rejected\n return {\n ...base,\n status: STATUS.rejected,\n rejectedAt: parsed.rejectedAt ?? parsed.createdAt,\n rejectionType: parsed.rejectionType ?? 'media',\n errorMessage: parsed.errorMessage ?? '',\n violationId: parsed.violationId,\n uploadTrayClearedAt: parsed.uploadTrayClearedAt,\n uploadTrayClearedBy: parsed.uploadTrayClearedBy,\n // Carry the seen fields through (round-9 finding 9): dropping them on the\n // `rejected` branch — while `completed`/`failed` keep them — made a seen\n // rejected upload relight the Files dot forever, because the tray's\n // unseen-terminal check never saw `uploadTraySeenAt`.\n uploadTraySeenAt: parsed.uploadTraySeenAt,\n uploadTraySeenBy: parsed.uploadTraySeenBy,\n };\n}\n\nexport function InFlightUploadsProvider<TFileOrigin extends string = string>(\n props: InFlightUploadsProviderProps<TFileOrigin>,\n) {\n const { adapter, subscribe = defaultFirestoreSubscribe, children } = props;\n\n const [uploads, setUploads] = useState<Map<string, InFlightUpload<TFileOrigin>>>(new Map());\n const [sourceState, setSourceState] = useState<UploadsSourceState>('connecting');\n const [restarts, setRestarts] = useState(0);\n const [retrying, setRetrying] = useState(false);\n /** Set by `retry` for the subscription the restart opens; that run keeps the source state. */\n const restartRequestedRef = useRef(false);\n const seenTerminalIdsRef = useRef<Set<string>>(new Set());\n const statusByIdRef = useRef<Map<string, InFlightUploadStatus>>(new Map());\n const hasProcessedInitialSnapshotRef = useRef(false);\n /** The identity the dedup/tracking refs belong to — they outlive resubscribes\n * for the SAME user (auth-settling flaps) and reset only on a real identity\n * change. See the reset block in the subscription effect. */\n const trackedUserIdRef = useRef<string | null>(null);\n\n // Stable refs for adapter callbacks — read from latest adapter inside effect.\n const adapterRef = useRef(adapter);\n useEffect(() => {\n adapterRef.current = adapter;\n });\n\n const userId = adapter.getCurrentUserId();\n\n useEffect(() => {\n // The uploads map and source state rebuild from every subscription's initial\n // snapshot, so they reset on every effect run. The terminal-dedup and\n // status-tracking refs are IDENTITY-scoped, not subscription-scoped: an\n // auth-settling flap (uid → null → same uid re-subscribes) must neither\n // re-fire terminals the previous subscription already fired (seen set\n // persists) nor lose the observed non-terminal statuses that let the new\n // subscription's initial listing detect a transition that completed during\n // the gap. A real identity change — including UID-A → UID-B with no\n // intermediate null user — starts clean; never rely on an intermediate\n // sign-out to clear refs between two authenticated UIDs.\n // A restart the person asked for keeps the source state it had (a failure stays a failure\n // until the restarted listener answers); every other run starts over as `connecting`.\n let restarting = restartRequestedRef.current;\n restartRequestedRef.current = false;\n setUploads(new Map());\n if (!restarting) {\n setSourceState('connecting');\n setRetrying(false);\n }\n if (userId !== null && userId !== trackedUserIdRef.current) {\n seenTerminalIdsRef.current = new Set();\n statusByIdRef.current = new Map();\n trackedUserIdRef.current = userId;\n }\n hasProcessedInitialSnapshotRef.current = false;\n\n if (!userId) {\n return;\n }\n\n const a = adapterRef.current;\n const windowStartMs = Date.now() - a.listenerWindowMs;\n\n const handleSnapshot = (snapshot: FirestoreLikeSnapshot) => {\n const updates: Array<{ id: string; upload: InFlightUpload<TFileOrigin> | null }> = [];\n const newlyTerminal: Array<ParsedPendingMedia<TFileOrigin>> = [];\n const isInitialSnapshot = !hasProcessedInitialSnapshotRef.current;\n\n for (const change of snapshot.docChanges()) {\n if (change.type === 'removed') {\n updates.push({ id: change.doc.id, upload: null });\n statusByIdRef.current.delete(change.doc.id);\n seenTerminalIdsRef.current.delete(change.doc.id);\n continue;\n }\n\n let parsed: ParsedPendingMedia<TFileOrigin>;\n try {\n parsed = adapterRef.current.parsePendingMedia(change.doc.data());\n } catch (error) {\n adapterRef.current.onMonitoringEvent({\n type: 'parse-error',\n docId: change.doc.id,\n error,\n data: change.doc.data(),\n });\n continue;\n }\n\n const upload = fromParsed(parsed);\n updates.push({ id: parsed.id, upload });\n\n const previousStatus = statusByIdRef.current.get(parsed.id);\n const currentStatus = parsed.status;\n const currentIsTerminal = isPendingMediaTerminalStatus(currentStatus);\n const previousWasNonTerminal = isPendingMediaNonTerminalStatus(previousStatus);\n\n if (isInitialSnapshot) {\n // A subscription's first snapshot is a (re-)listing of current state,\n // not a live event: already-terminal docs stay suppressed so a page\n // refresh never re-toasts. ONE exception — this provider instance\n // tracked the doc as non-terminal before (same-identity resubscribe\n // gap; the tracking refs survive those), so the terminal listing IS\n // the transition, observed across the gap, and must fire.\n if (currentIsTerminal) {\n if (previousWasNonTerminal && !seenTerminalIdsRef.current.has(parsed.id)) {\n newlyTerminal.push(parsed);\n }\n seenTerminalIdsRef.current.add(parsed.id);\n }\n } else if (currentIsTerminal && !seenTerminalIdsRef.current.has(parsed.id)) {\n // Any unseen terminal in a LIVE snapshot fires — including a doc whose\n // FIRST client-visible state is already terminal. A fast backend\n // outcome (moderation rejection in ~2s) can coalesce created→terminal\n // into one delivery, and the uploader is actively waiting on exactly\n // this callback; swallowing it silently loses a rejection dialog.\n seenTerminalIdsRef.current.add(parsed.id);\n newlyTerminal.push(parsed);\n }\n\n statusByIdRef.current.set(parsed.id, currentStatus);\n }\n\n hasProcessedInitialSnapshotRef.current = true;\n\n // Metadata-derived source state: stay `connecting` until a server-confirmed\n // snapshot, `offline` if only cached data arrives after being live.\n const fromCache = snapshot.metadata?.fromCache ?? false;\n // A snapshot after a failed listener comes from a new subscription: not yet live, never offline.\n setSourceState((prev) =>\n fromCache ? (prev === 'connecting' || prev === 'error' ? 'connecting' : 'offline') : 'live',\n );\n restarting = false;\n setRetrying(false);\n\n if (updates.length > 0) {\n setUploads((prev) => {\n const next = new Map(prev);\n for (const { id, upload } of updates) {\n if (upload === null) next.delete(id);\n else next.set(id, upload);\n }\n return next;\n });\n }\n\n for (const parsed of newlyTerminal) {\n handleTerminal(parsed);\n }\n };\n\n function handleTerminal(parsed: ParsedPendingMedia<TFileOrigin>) {\n adapterRef.current.onMonitoringEvent({\n type: 'breadcrumb',\n category: 'upload.terminal',\n level: 'info',\n message: `${parsed.fileOrigin}:${parsed.status}`,\n data: {\n id: parsed.id,\n fileOrigin: parsed.fileOrigin,\n status: parsed.status,\n surface: parsed.surface,\n },\n });\n\n if (parsed.status === STATUS.completed) {\n adapterRef.current.onUploadCompleted(parsed);\n } else if (parsed.status === STATUS.failed) {\n adapterRef.current.onUploadFailed(parsed);\n } else if (parsed.status === STATUS.rejected) {\n adapterRef.current.onUploadRejected(parsed);\n }\n }\n\n // Bounded RESUBSCRIBE on listener error: a cold-session race (e.g. Firestore\n // App Check enforcement rejecting the first attach before the token is\n // minted) must not kill the listener for the whole session — observed live:\n // one permission-denied at first login → sourceState 'error' forever, every\n // tray file status dead until reload. Retry on a short ladder; a snapshot\n // resets the ladder (healthy again); exhausting it goes terminal 'error'\n // (a real rules denial stays denied — retries are cheap and bounded).\n const RESUBSCRIBE_DELAYS_MS = [1_000, 3_000, 10_000, 30_000];\n let attempt = 0;\n let disposed = false;\n let retryTimer: ReturnType<typeof setTimeout> | undefined;\n let unsubscribe: (() => void) | undefined;\n\n const start = () => {\n unsubscribe = subscribe({\n db: a.db,\n collectionPath: a.collectionPath,\n userId,\n windowStartMs,\n onSnapshot: (snapshot) => {\n attempt = 0; // healthy — future errors get a fresh ladder\n handleSnapshot(snapshot);\n },\n onError: (error) => {\n adapterRef.current.onMonitoringEvent({ type: 'listener-error', error });\n unsubscribe?.();\n unsubscribe = undefined;\n if (disposed || attempt >= RESUBSCRIBE_DELAYS_MS.length) {\n setSourceState('error');\n setRetrying(false);\n return;\n }\n if (!restarting) setSourceState('connecting');\n retryTimer = setTimeout(() => {\n if (disposed) return;\n attempt += 1;\n start();\n }, RESUBSCRIBE_DELAYS_MS[attempt]);\n },\n });\n };\n start();\n\n return () => {\n disposed = true;\n if (retryTimer) clearTimeout(retryTimer);\n unsubscribe?.();\n };\n // adapter is intentionally NOT in the deps array — adapter is read via\n // adapterRef on every snapshot, so identity changes don't re-subscribe.\n\n }, [userId, subscribe, restarts]);\n\n const retry = useCallback(() => {\n restartRequestedRef.current = true;\n setRetrying(true);\n setRestarts((count) => count + 1);\n }, []);\n\n const value = useMemo<InFlightUploadsContextValue<TFileOrigin>>(\n () => ({ uploads, sourceState, retry, retrying }),\n [uploads, sourceState, retry, retrying],\n );\n\n return (\n <InFlightUploadsContext.Provider value={value as InFlightUploadsContextValue}>\n {children}\n </InFlightUploadsContext.Provider>\n );\n}\n\n// =============================================================================\n// Read APIs\n// =============================================================================\n\nexport function useInFlightUploadsState<TFileOrigin extends string = string>(): Map<\n string,\n InFlightUpload<TFileOrigin>\n> {\n const ctx = useContext(InFlightUploadsContext);\n if (ctx === undefined) {\n throw new Error('useInFlightUploadsState must be used within InFlightUploadsProvider');\n }\n return ctx.uploads as Map<string, InFlightUpload<TFileOrigin>>;\n}\n\nexport function useInFlightUpload<TFileOrigin extends string = string>(\n id: string | null | undefined,\n): InFlightUpload<TFileOrigin> | undefined {\n const ctx = useContext(InFlightUploadsContext);\n if (ctx === undefined) {\n throw new Error('useInFlightUpload must be used within InFlightUploadsProvider');\n }\n if (!id) return undefined;\n return ctx.uploads.get(id) as InFlightUpload<TFileOrigin> | undefined;\n}\n\n/**\n * Metadata-derived source state of the uploads listener\n * (`connecting | live | offline | error`). The Files tray uses this to show a\n * degraded indicator and never display \"all caught up\" while the source is\n * `offline`/`error`/`connecting`.\n */\nexport function useUploadsSourceState(): UploadsSourceState {\n const ctx = useContext(InFlightUploadsContext);\n if (ctx === undefined) {\n throw new Error('useUploadsSourceState must be used within InFlightUploadsProvider');\n }\n return ctx.sourceState;\n}\n\n/**\n * The uploads listener's restart — the Try again of a failure shown on `useUploadsSourceState` —\n * and whether it is running.\n */\nexport function useUploadsSourceRetry(): UploadsSourceRetry {\n const ctx = useContext(InFlightUploadsContext);\n if (ctx === undefined) {\n throw new Error('useUploadsSourceRetry must be used within InFlightUploadsProvider');\n }\n return { retry: ctx.retry, retrying: ctx.retrying };\n}\n\n// =============================================================================\n// Tray helpers\n// =============================================================================\n\n/**\n * `true` for `completed` / `failed` / `rejected`. Used by tray UI to decide\n * whether a Clear action is available for an item.\n */\nexport function isTerminalUpload<TFileOrigin extends string = string>(\n item: InFlightUpload<TFileOrigin>,\n): item is Exclude<InFlightUpload<TFileOrigin>, { status: PendingMediaNonTerminalStatus }> {\n return isPendingMediaTerminalStatus(item.status);\n}\n\n/**\n * Tray-visible items. Includes every pending/processing upload, plus terminal\n * uploads that have NOT been cleared from the tray. Sorted newest-first by\n * createdAt.\n *\n * The public selector name in upload-ui is `useUploadActivityState`. The\n * mutation hook is `useClearUploadActivity` (in a separate file).\n */\nexport function useUploadActivityState<TFileOrigin extends string = string>(): InFlightUpload<TFileOrigin>[] {\n const uploads = useInFlightUploadsState<TFileOrigin>();\n return useMemo(() => {\n return Array.from(uploads.values())\n .filter((item) => {\n if (!isTerminalUpload(item)) return true;\n return item.uploadTrayClearedAt === undefined;\n })\n .sort((a, b) => b.createdAt - a.createdAt);\n }, [uploads]);\n}\n\n// =============================================================================\n// Default Firestore subscription implementation\n// =============================================================================\n\n/**\n * Default Firestore subscription using the consumer's `firebase/firestore`\n * module instance (via static ESM import at the top of this file).\n *\n * Using static ESM import — NOT `require()` — is load-bearing. CommonJS\n * `require('firebase/firestore')` and ESM `import 'firebase/firestore'` resolve\n * to different module instances under Next.js + Turbopack. If the consumer\n * constructs `db` via ESM and we receive it here, but we then call\n * `collection()` from a CJS-loaded copy of firebase, the firebase runtime\n * does not recognize the db as a Firestore instance and throws\n * \"Expected first argument to collection() to be a CollectionReference,\n * a DocumentReference or FirebaseFirestore\".\n *\n * Consumers who want to skip the firebase code path entirely (tests, alt\n * backends) supply their own `subscribe` prop to `InFlightUploadsProvider`.\n */\nconst defaultFirestoreSubscribe: FirestoreSubscribeFn = (args) => {\n const q = query(\n\n collection(args.db as any, args.collectionPath),\n where('userId', '==', args.userId),\n where('createdAt', '>=', args.windowStartMs),\n );\n // includeMetadataChanges so the provider can derive a `connecting|live|offline|error`\n // source state from `snapshot.metadata.fromCache` (the Files tray degraded indicator).\n return onSnapshot(q, { includeMetadataChanges: true }, args.onSnapshot as never, args.onError as never);\n};\n"]}
@@ -1,25 +1,19 @@
1
1
  /**
2
- * Local upload navigation guard provider.
2
+ * The navigation and leave guard: warns before the user leaves while work that leaving would lose
3
+ * is registered — a LOCAL byte upload in flight, or an editor's unsaved input.
3
4
  *
4
- * Tracks the count of in-progress LOCAL byte uploads (file → Firebase
5
- * Storage). While the count is > 0, installs a `beforeunload` listener
6
- * to warn the user before they close the tab or reload mid-upload.
5
+ * Uploads: phases `preparing` and `uploading` only. `finalizing` and backend `pendingMedia`
6
+ * processing are safe to leave (the bytes are already in Storage), so they are not registered.
7
+ * Unsaved work: an editor registers while it holds input its user has not saved, and unregisters
8
+ * when the input is saved, discarded, or the editor unmounts.
7
9
  *
8
- * Scope:
9
- * - Covers phases `preparing` and `uploading` only.
10
- * - Does NOT cover `finalizing` — at that point bytes are already in
11
- * Storage and the only remaining work is the `startUpload` callable.
12
- * - Does NOT cover backend `pendingMedia` status (`pending` /
13
- * `processing`). Those run server-side and are safe to leave.
10
+ * While anything is registered the provider installs ONE `beforeunload` listener (tab close,
11
+ * reload, full-page navigation), and `confirmNavigation()` asks before an in-app navigation. It is
12
+ * the one leave guard: an editor never installs its own `beforeunload` handler (FRONTEND-207).
14
13
  *
15
- * Consumers register with a stable id (typically the local upload
16
- * session id or `pendingMedia` doc id once known) on entering
17
- * `preparing`/`uploading`, and unregister on transition to
18
- * `finalizing`/success/error/cancel/unmount.
19
- *
20
- * Public hook: `useLocalUploadGuard()` returns
21
- * `{ activeUploadCount, registerUpload, unregisterUpload }`. Throws if
22
- * used outside a `LocalUploadGuardProvider`.
14
+ * Consumers register with a stable id per piece of work; registering a registered id, or
15
+ * unregistering an absent one, is a no-op. Uploads and unsaved work are counted separately, so the
16
+ * same id may be both.
23
17
  */
24
18
  import { type ReactNode } from 'react';
25
19
  export interface ConfirmNavigationOptions {
@@ -33,43 +27,50 @@ export interface ConfirmNavigationOptions {
33
27
  fullPageNavigation?: boolean;
34
28
  }
35
29
  interface LocalUploadGuardContextValue {
30
+ /** Registered local uploads. */
36
31
  activeUploadCount: number;
32
+ /** Registered pieces of unsaved work. */
33
+ unsavedWorkCount: number;
34
+ /** Everything the guard warns about: uploads plus unsaved work. Zero means leaving asks nothing. */
35
+ guardedWorkCount: number;
37
36
  registerUpload: (id: string) => void;
38
37
  unregisterUpload: (id: string) => void;
39
- /** Returns true iff at least one upload is currently registered. Pure read. */
38
+ registerUnsavedWork: (id: string) => void;
39
+ unregisterUnsavedWork: (id: string) => void;
40
+ /** Returns true iff an upload or unsaved work is currently registered. Pure read. */
40
41
  shouldConfirmNavigation: () => boolean;
41
42
  /**
42
- * Shows a confirm() dialog with the configured message and returns the user's
43
- * choice. Safe to call when no upload is active — returns true without prompting.
43
+ * Shows a confirm() dialog with the message for what is registered and returns the user's
44
+ * choice. Safe to call when nothing is registered — returns true without prompting.
44
45
  *
45
- * With `{ fullPageNavigation: true }` and a result of true (confirmed, or no
46
- * upload active), arms a ONE-SHOT bypass: the next `beforeunload` is not
47
- * prevented, so a confirmed full-page navigation does not prompt twice. A
48
- * declined confirm never arms it; the bypass is cleared when it is consumed, when
49
- * a new upload registers, and on `pageshow` (a back/forward-cache return).
46
+ * With `{ fullPageNavigation: true }` and a result of true (confirmed, or nothing registered),
47
+ * arms a ONE-SHOT bypass: the next `beforeunload` is not prevented, so a confirmed full-page
48
+ * navigation does not prompt twice. A declined confirm never arms it; the bypass is cleared when
49
+ * it is consumed, when new work (an upload or unsaved work) registers, and on `pageshow` (a
50
+ * back/forward-cache return).
50
51
  */
51
52
  confirmNavigation: (options?: ConfirmNavigationOptions) => boolean;
52
53
  }
53
54
  export interface LocalUploadGuardProviderProps {
54
55
  /**
55
- * Message shown by the browser when the user attempts to close the tab or
56
- * reload during an active upload. Modern browsers may ignore the custom
57
- * string and show a generic dialog.
56
+ * Message the browser is given when the user closes the tab, reloads, or leaves the page while
57
+ * work is registered. Modern browsers show their own generic dialog and ignore it; when omitted,
58
+ * the default for what is registered is used.
58
59
  */
59
60
  beforeUnloadMessage?: string;
60
- /**
61
- * Message shown by `confirmNavigation()` when an upload is in progress and
62
- * the user attempts to navigate away within the app. Defaults to a
63
- * reasonable English string.
64
- */
61
+ /** `confirmNavigation()`'s message while an upload is in progress (and no unsaved work). */
65
62
  navigationConfirmMessage?: string;
63
+ /** `confirmNavigation()`'s message while unsaved work is registered (and no upload). */
64
+ unsavedWorkConfirmMessage?: string;
65
+ /** `confirmNavigation()`'s message while both an upload and unsaved work are registered. */
66
+ uploadAndUnsavedWorkConfirmMessage?: string;
66
67
  children: ReactNode;
67
68
  }
68
69
  export declare function LocalUploadGuardProvider(props: LocalUploadGuardProviderProps): import("react").JSX.Element;
69
70
  /**
70
- * Low-level accessor for the local-upload guard context. Throws if used
71
- * outside a `LocalUploadGuardProvider`. Most consumers should use
72
- * `useLocalUploadGuard` (the public re-export below) instead.
71
+ * Low-level accessor for the guard context. Throws if used outside a
72
+ * `LocalUploadGuardProvider`. Most consumers should use `useLocalUploadGuard`
73
+ * (the public re-export below) instead.
73
74
  */
74
75
  export declare function useLocalUploadGuardContext(): LocalUploadGuardContextValue;
75
76
  /**
@@ -80,9 +81,7 @@ export declare function useLocalUploadGuardContext(): LocalUploadGuardContextVal
80
81
  */
81
82
  export declare function useOptionalLocalUploadGuard(): LocalUploadGuardContextValue | null;
82
83
  /**
83
- * Public hook for the local upload navigation guard.
84
- *
85
- * Returns `{ activeUploadCount, registerUpload, unregisterUpload }`.
84
+ * Public hook for the navigation and leave guard.
86
85
  *
87
86
  * Typical usage in an upload flow:
88
87
  *
@@ -93,7 +92,21 @@ export declare function useOptionalLocalUploadGuard(): LocalUploadGuardContextVa
93
92
  * return () => unregisterUpload(uploadId);
94
93
  * }
95
94
  * }, [phase, uploadId, registerUpload, unregisterUpload]);
95
+ *
96
+ * An editor holding input uses `useUnsavedWorkGuard(hasUnsavedWork)` instead.
96
97
  */
97
98
  export declare function useLocalUploadGuard(): LocalUploadGuardContextValue;
99
+ /**
100
+ * Registers the calling editor's unsaved input with the leave guard while `hasUnsavedWork` is
101
+ * true, and unregisters it when that turns false or the editor unmounts. While registered, closing
102
+ * the tab, reloading, a full-page navigation, and every guarded in-app navigation ask first.
103
+ * Throws outside a `LocalUploadGuardProvider`.
104
+ *
105
+ * Returns `release`, which unregisters at once: an editor that saves and then navigates in the
106
+ * same handler calls it after the save, because its dirty flag clears only on the next render.
107
+ */
108
+ export declare function useUnsavedWorkGuard(hasUnsavedWork: boolean): {
109
+ release: () => void;
110
+ };
98
111
  export {};
99
112
  //# sourceMappingURL=local-upload-guard-provider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"local-upload-guard-provider.d.ts","sourceRoot":"","sources":["../../src/react/local-upload-guard-provider.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAQL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,MAAM,WAAW,wBAAwB;IACvC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,UAAU,4BAA4B;IACpC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,gBAAgB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,+EAA+E;IAC/E,uBAAuB,EAAE,MAAM,OAAO,CAAC;IACvC;;;;;;;;;OASG;IACH,iBAAiB,EAAE,CAAC,OAAO,CAAC,EAAE,wBAAwB,KAAK,OAAO,CAAC;CACpE;AAID,MAAM,WAAW,6BAA6B;IAC5C;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,QAAQ,EAAE,SAAS,CAAC;CACrB;AAOD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,6BAA6B,+BA4F5E;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,4BAA4B,CAQzE;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,IAAI,4BAA4B,GAAG,IAAI,CAEjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,IAAI,4BAA4B,CAElE"}
1
+ {"version":3,"file":"local-upload-guard-provider.d.ts","sourceRoot":"","sources":["../../src/react/local-upload-guard-provider.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EASL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,MAAM,WAAW,wBAAwB;IACvC;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,UAAU,4BAA4B;IACpC,gCAAgC;IAChC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,yCAAyC;IACzC,gBAAgB,EAAE,MAAM,CAAC;IACzB,oGAAoG;IACpG,gBAAgB,EAAE,MAAM,CAAC;IACzB,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,gBAAgB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,mBAAmB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1C,qBAAqB,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5C,qFAAqF;IACrF,uBAAuB,EAAE,MAAM,OAAO,CAAC;IACvC;;;;;;;;;OASG;IACH,iBAAiB,EAAE,CAAC,OAAO,CAAC,EAAE,wBAAwB,KAAK,OAAO,CAAC;CACpE;AAID,MAAM,WAAW,6BAA6B;IAC5C;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,4FAA4F;IAC5F,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,wFAAwF;IACxF,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,4FAA4F;IAC5F,kCAAkC,CAAC,EAAE,MAAM,CAAC;IAC5C,QAAQ,EAAE,SAAS,CAAC;CACrB;AAoCD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,6BAA6B,+BAoH5E;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,4BAA4B,CAQzE;AAED;;;;;GAKG;AACH,wBAAgB,2BAA2B,IAAI,4BAA4B,GAAG,IAAI,CAEjF;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,IAAI,4BAA4B,CAElE;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CAAC,cAAc,EAAE,OAAO,GAAG;IAAE,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAUpF"}
@@ -1,54 +1,77 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
3
  /**
4
- * Local upload navigation guard provider.
4
+ * The navigation and leave guard: warns before the user leaves while work that leaving would lose
5
+ * is registered — a LOCAL byte upload in flight, or an editor's unsaved input.
5
6
  *
6
- * Tracks the count of in-progress LOCAL byte uploads (file → Firebase
7
- * Storage). While the count is > 0, installs a `beforeunload` listener
8
- * to warn the user before they close the tab or reload mid-upload.
7
+ * Uploads: phases `preparing` and `uploading` only. `finalizing` and backend `pendingMedia`
8
+ * processing are safe to leave (the bytes are already in Storage), so they are not registered.
9
+ * Unsaved work: an editor registers while it holds input its user has not saved, and unregisters
10
+ * when the input is saved, discarded, or the editor unmounts.
9
11
  *
10
- * Scope:
11
- * - Covers phases `preparing` and `uploading` only.
12
- * - Does NOT cover `finalizing` — at that point bytes are already in
13
- * Storage and the only remaining work is the `startUpload` callable.
14
- * - Does NOT cover backend `pendingMedia` status (`pending` /
15
- * `processing`). Those run server-side and are safe to leave.
12
+ * While anything is registered the provider installs ONE `beforeunload` listener (tab close,
13
+ * reload, full-page navigation), and `confirmNavigation()` asks before an in-app navigation. It is
14
+ * the one leave guard: an editor never installs its own `beforeunload` handler (FRONTEND-207).
16
15
  *
17
- * Consumers register with a stable id (typically the local upload
18
- * session id or `pendingMedia` doc id once known) on entering
19
- * `preparing`/`uploading`, and unregister on transition to
20
- * `finalizing`/success/error/cancel/unmount.
21
- *
22
- * Public hook: `useLocalUploadGuard()` returns
23
- * `{ activeUploadCount, registerUpload, unregisterUpload }`. Throws if
24
- * used outside a `LocalUploadGuardProvider`.
16
+ * Consumers register with a stable id per piece of work; registering a registered id, or
17
+ * unregistering an absent one, is a no-op. Uploads and unsaved work are counted separately, so the
18
+ * same id may be both.
25
19
  */
26
- import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
20
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState, } from 'react';
27
21
  const LocalUploadGuardContext = createContext(undefined);
28
- const DEFAULT_BEFORE_UNLOAD_MESSAGE = 'File upload in progress. Are you sure you want to leave and end the file upload?';
29
22
  const DEFAULT_NAVIGATION_CONFIRM_MESSAGE = 'An upload is currently in progress. If you leave this page, the upload will be canceled. Continue?';
23
+ const DEFAULT_UNSAVED_WORK_CONFIRM_MESSAGE = 'You have unsaved changes. If you leave this page, they will be lost. Continue?';
24
+ const DEFAULT_UPLOAD_AND_UNSAVED_WORK_CONFIRM_MESSAGE = 'An upload is in progress and you have unsaved changes. If you leave this page, the upload will be canceled and your changes will be lost. Continue?';
25
+ const DEFAULT_UPLOAD_BEFORE_UNLOAD_MESSAGE = 'File upload in progress. Are you sure you want to leave and end the file upload?';
26
+ const DEFAULT_UNSAVED_WORK_BEFORE_UNLOAD_MESSAGE = 'You have unsaved changes. Are you sure you want to leave and lose them?';
27
+ function useRegistry(onRegister) {
28
+ const idsRef = useRef(new Set());
29
+ const [count, setCount] = useState(0);
30
+ const register = useCallback((id) => {
31
+ if (idsRef.current.has(id))
32
+ return;
33
+ onRegister();
34
+ idsRef.current.add(id);
35
+ setCount(idsRef.current.size);
36
+ }, [onRegister]);
37
+ const unregister = useCallback((id) => {
38
+ if (!idsRef.current.has(id))
39
+ return;
40
+ idsRef.current.delete(id);
41
+ setCount(idsRef.current.size);
42
+ }, []);
43
+ return { idsRef, count, register, unregister };
44
+ }
30
45
  export function LocalUploadGuardProvider(props) {
31
- const { children, beforeUnloadMessage = DEFAULT_BEFORE_UNLOAD_MESSAGE, navigationConfirmMessage = DEFAULT_NAVIGATION_CONFIRM_MESSAGE, } = props;
32
- const activeIdsRef = useRef(new Set());
33
- const [activeUploadCount, setActiveUploadCount] = useState(0);
46
+ const { children, beforeUnloadMessage, navigationConfirmMessage = DEFAULT_NAVIGATION_CONFIRM_MESSAGE, unsavedWorkConfirmMessage = DEFAULT_UNSAVED_WORK_CONFIRM_MESSAGE, uploadAndUnsavedWorkConfirmMessage = DEFAULT_UPLOAD_AND_UNSAVED_WORK_CONFIRM_MESSAGE, } = props;
34
47
  // One-shot: the user already confirmed the full-page navigation now unloading.
35
48
  const bypassNextUnloadRef = useRef(false);
36
- const registerUpload = useCallback((id) => {
37
- if (activeIdsRef.current.has(id))
38
- return;
39
- // Work that starts after a confirmation was not covered by it.
49
+ // Also run when work registers: work that starts after a confirmation was not covered by it.
50
+ const clearBypass = useCallback(() => {
40
51
  bypassNextUnloadRef.current = false;
41
- activeIdsRef.current.add(id);
42
- setActiveUploadCount(activeIdsRef.current.size);
43
- }, []);
44
- const unregisterUpload = useCallback((id) => {
45
- if (!activeIdsRef.current.has(id))
46
- return;
47
- activeIdsRef.current.delete(id);
48
- setActiveUploadCount(activeIdsRef.current.size);
49
52
  }, []);
53
+ const uploads = useRegistry(clearBypass);
54
+ const unsavedWork = useRegistry(clearBypass);
55
+ const uploadIdsRef = uploads.idsRef;
56
+ const unsavedWorkIdsRef = unsavedWork.idsRef;
57
+ const guardedWorkCount = uploads.count + unsavedWork.count;
58
+ const hasGuardedWork = guardedWorkCount > 0;
59
+ // Read at the moment of asking, so a message always matches what is registered right then.
60
+ const confirmMessage = useCallback(() => {
61
+ const uploading = uploadIdsRef.current.size > 0;
62
+ const unsaved = unsavedWorkIdsRef.current.size > 0;
63
+ if (uploading && unsaved)
64
+ return uploadAndUnsavedWorkConfirmMessage;
65
+ return uploading ? navigationConfirmMessage : unsavedWorkConfirmMessage;
66
+ }, [
67
+ uploadIdsRef,
68
+ unsavedWorkIdsRef,
69
+ navigationConfirmMessage,
70
+ unsavedWorkConfirmMessage,
71
+ uploadAndUnsavedWorkConfirmMessage,
72
+ ]);
50
73
  useEffect(() => {
51
- if (activeUploadCount <= 0)
74
+ if (!hasGuardedWork)
52
75
  return;
53
76
  const handleBeforeUnload = (event) => {
54
77
  if (bypassNextUnloadRef.current) {
@@ -56,51 +79,65 @@ export function LocalUploadGuardProvider(props) {
56
79
  return;
57
80
  }
58
81
  event.preventDefault();
59
- // Modern browsers may ignore the custom string and show their own
60
- // generic confirmation. The key behavior is that navigation is
61
- // blocked/warned, not the exact text.
62
- event.returnValue = beforeUnloadMessage;
82
+ // Modern browsers may ignore the custom string and show their own generic confirmation;
83
+ // what matters is that leaving is warned, not the exact text.
84
+ event.returnValue =
85
+ beforeUnloadMessage ??
86
+ (uploadIdsRef.current.size > 0
87
+ ? DEFAULT_UPLOAD_BEFORE_UNLOAD_MESSAGE
88
+ : DEFAULT_UNSAVED_WORK_BEFORE_UNLOAD_MESSAGE);
63
89
  return event.returnValue;
64
90
  };
65
91
  window.addEventListener('beforeunload', handleBeforeUnload);
66
92
  return () => {
67
93
  window.removeEventListener('beforeunload', handleBeforeUnload);
68
94
  };
69
- }, [activeUploadCount, beforeUnloadMessage]);
95
+ }, [hasGuardedWork, beforeUnloadMessage, uploadIdsRef]);
70
96
  // A back/forward-cache return re-shows this page without reloading it: an
71
97
  // armed bypass from the navigation that left must never outlive that navigation.
72
98
  useEffect(() => {
73
- const clearBypass = () => {
74
- bypassNextUnloadRef.current = false;
75
- };
76
99
  window.addEventListener('pageshow', clearBypass);
77
100
  return () => {
78
101
  window.removeEventListener('pageshow', clearBypass);
79
102
  };
80
- }, []);
81
- const shouldConfirmNavigation = useCallback(() => activeIdsRef.current.size > 0, []);
103
+ }, [clearBypass]);
104
+ const shouldConfirmNavigation = useCallback(() => uploadIdsRef.current.size > 0 || unsavedWorkIdsRef.current.size > 0, [uploadIdsRef, unsavedWorkIdsRef]);
82
105
  const confirmNavigation = useCallback((options) => {
83
106
  // SSR / non-browser safety: if `window` isn't available, default to allow.
84
107
  if (typeof window === 'undefined')
85
108
  return true;
86
- const proceed = activeIdsRef.current.size === 0 || window.confirm(navigationConfirmMessage);
109
+ const proceed = !shouldConfirmNavigation() || window.confirm(confirmMessage());
87
110
  if (proceed && options?.fullPageNavigation)
88
111
  bypassNextUnloadRef.current = true;
89
112
  return proceed;
90
- }, [navigationConfirmMessage]);
113
+ }, [shouldConfirmNavigation, confirmMessage]);
91
114
  const value = useMemo(() => ({
92
- activeUploadCount,
93
- registerUpload,
94
- unregisterUpload,
115
+ activeUploadCount: uploads.count,
116
+ unsavedWorkCount: unsavedWork.count,
117
+ guardedWorkCount,
118
+ registerUpload: uploads.register,
119
+ unregisterUpload: uploads.unregister,
120
+ registerUnsavedWork: unsavedWork.register,
121
+ unregisterUnsavedWork: unsavedWork.unregister,
95
122
  shouldConfirmNavigation,
96
123
  confirmNavigation,
97
- }), [activeUploadCount, registerUpload, unregisterUpload, shouldConfirmNavigation, confirmNavigation]);
124
+ }), [
125
+ uploads.count,
126
+ uploads.register,
127
+ uploads.unregister,
128
+ unsavedWork.count,
129
+ unsavedWork.register,
130
+ unsavedWork.unregister,
131
+ guardedWorkCount,
132
+ shouldConfirmNavigation,
133
+ confirmNavigation,
134
+ ]);
98
135
  return (_jsx(LocalUploadGuardContext.Provider, { value: value, children: children }));
99
136
  }
100
137
  /**
101
- * Low-level accessor for the local-upload guard context. Throws if used
102
- * outside a `LocalUploadGuardProvider`. Most consumers should use
103
- * `useLocalUploadGuard` (the public re-export below) instead.
138
+ * Low-level accessor for the guard context. Throws if used outside a
139
+ * `LocalUploadGuardProvider`. Most consumers should use `useLocalUploadGuard`
140
+ * (the public re-export below) instead.
104
141
  */
105
142
  export function useLocalUploadGuardContext() {
106
143
  const ctx = useContext(LocalUploadGuardContext);
@@ -119,9 +156,7 @@ export function useOptionalLocalUploadGuard() {
119
156
  return useContext(LocalUploadGuardContext) ?? null;
120
157
  }
121
158
  /**
122
- * Public hook for the local upload navigation guard.
123
- *
124
- * Returns `{ activeUploadCount, registerUpload, unregisterUpload }`.
159
+ * Public hook for the navigation and leave guard.
125
160
  *
126
161
  * Typical usage in an upload flow:
127
162
  *
@@ -132,8 +167,31 @@ export function useOptionalLocalUploadGuard() {
132
167
  * return () => unregisterUpload(uploadId);
133
168
  * }
134
169
  * }, [phase, uploadId, registerUpload, unregisterUpload]);
170
+ *
171
+ * An editor holding input uses `useUnsavedWorkGuard(hasUnsavedWork)` instead.
135
172
  */
136
173
  export function useLocalUploadGuard() {
137
174
  return useLocalUploadGuardContext();
138
175
  }
176
+ /**
177
+ * Registers the calling editor's unsaved input with the leave guard while `hasUnsavedWork` is
178
+ * true, and unregisters it when that turns false or the editor unmounts. While registered, closing
179
+ * the tab, reloading, a full-page navigation, and every guarded in-app navigation ask first.
180
+ * Throws outside a `LocalUploadGuardProvider`.
181
+ *
182
+ * Returns `release`, which unregisters at once: an editor that saves and then navigates in the
183
+ * same handler calls it after the save, because its dirty flag clears only on the next render.
184
+ */
185
+ export function useUnsavedWorkGuard(hasUnsavedWork) {
186
+ const { registerUnsavedWork, unregisterUnsavedWork } = useLocalUploadGuardContext();
187
+ const id = useId();
188
+ useEffect(() => {
189
+ if (!hasUnsavedWork)
190
+ return;
191
+ registerUnsavedWork(id);
192
+ return () => unregisterUnsavedWork(id);
193
+ }, [hasUnsavedWork, id, registerUnsavedWork, unregisterUnsavedWork]);
194
+ const release = useCallback(() => unregisterUnsavedWork(id), [id, unregisterUnsavedWork]);
195
+ return useMemo(() => ({ release }), [release]);
196
+ }
139
197
  //# sourceMappingURL=local-upload-guard-provider.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"local-upload-guard-provider.js","sourceRoot":"","sources":["../../src/react/local-upload-guard-provider.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\n/**\n * Local upload navigation guard provider.\n *\n * Tracks the count of in-progress LOCAL byte uploads (file → Firebase\n * Storage). While the count is > 0, installs a `beforeunload` listener\n * to warn the user before they close the tab or reload mid-upload.\n *\n * Scope:\n * - Covers phases `preparing` and `uploading` only.\n * - Does NOT cover `finalizing` — at that point bytes are already in\n * Storage and the only remaining work is the `startUpload` callable.\n * - Does NOT cover backend `pendingMedia` status (`pending` /\n * `processing`). Those run server-side and are safe to leave.\n *\n * Consumers register with a stable id (typically the local upload\n * session id or `pendingMedia` doc id once known) on entering\n * `preparing`/`uploading`, and unregister on transition to\n * `finalizing`/success/error/cancel/unmount.\n *\n * Public hook: `useLocalUploadGuard()` returns\n * `{ activeUploadCount, registerUpload, unregisterUpload }`. Throws if\n * used outside a `LocalUploadGuardProvider`.\n */\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\n\nexport interface ConfirmNavigationOptions {\n /**\n * The caller leaves through a FULL-PAGE navigation (`location.assign` /\n * `location.replace`) once this returns true. The provider then lets that\n * navigation's next `beforeunload` pass without the browser's own\n * \"Leave site?\" prompt, because the user has just answered this one. Soft\n * (in-app router) navigations never pass it: they fire no `beforeunload`.\n */\n fullPageNavigation?: boolean;\n}\n\ninterface LocalUploadGuardContextValue {\n activeUploadCount: number;\n registerUpload: (id: string) => void;\n unregisterUpload: (id: string) => void;\n /** Returns true iff at least one upload is currently registered. Pure read. */\n shouldConfirmNavigation: () => boolean;\n /**\n * Shows a confirm() dialog with the configured message and returns the user's\n * choice. Safe to call when no upload is active — returns true without prompting.\n *\n * With `{ fullPageNavigation: true }` and a result of true (confirmed, or no\n * upload active), arms a ONE-SHOT bypass: the next `beforeunload` is not\n * prevented, so a confirmed full-page navigation does not prompt twice. A\n * declined confirm never arms it; the bypass is cleared when it is consumed, when\n * a new upload registers, and on `pageshow` (a back/forward-cache return).\n */\n confirmNavigation: (options?: ConfirmNavigationOptions) => boolean;\n}\n\nconst LocalUploadGuardContext = createContext<LocalUploadGuardContextValue | undefined>(undefined);\n\nexport interface LocalUploadGuardProviderProps {\n /**\n * Message shown by the browser when the user attempts to close the tab or\n * reload during an active upload. Modern browsers may ignore the custom\n * string and show a generic dialog.\n */\n beforeUnloadMessage?: string;\n /**\n * Message shown by `confirmNavigation()` when an upload is in progress and\n * the user attempts to navigate away within the app. Defaults to a\n * reasonable English string.\n */\n navigationConfirmMessage?: string;\n children: ReactNode;\n}\n\nconst DEFAULT_BEFORE_UNLOAD_MESSAGE =\n 'File upload in progress. Are you sure you want to leave and end the file upload?';\nconst DEFAULT_NAVIGATION_CONFIRM_MESSAGE =\n 'An upload is currently in progress. If you leave this page, the upload will be canceled. Continue?';\n\nexport function LocalUploadGuardProvider(props: LocalUploadGuardProviderProps) {\n const {\n children,\n beforeUnloadMessage = DEFAULT_BEFORE_UNLOAD_MESSAGE,\n navigationConfirmMessage = DEFAULT_NAVIGATION_CONFIRM_MESSAGE,\n } = props;\n const activeIdsRef = useRef<Set<string>>(new Set());\n const [activeUploadCount, setActiveUploadCount] = useState(0);\n // One-shot: the user already confirmed the full-page navigation now unloading.\n const bypassNextUnloadRef = useRef(false);\n\n const registerUpload = useCallback((id: string) => {\n if (activeIdsRef.current.has(id)) return;\n // Work that starts after a confirmation was not covered by it.\n bypassNextUnloadRef.current = false;\n activeIdsRef.current.add(id);\n setActiveUploadCount(activeIdsRef.current.size);\n }, []);\n\n const unregisterUpload = useCallback((id: string) => {\n if (!activeIdsRef.current.has(id)) return;\n activeIdsRef.current.delete(id);\n setActiveUploadCount(activeIdsRef.current.size);\n }, []);\n\n useEffect(() => {\n if (activeUploadCount <= 0) return;\n\n const handleBeforeUnload = (event: BeforeUnloadEvent) => {\n if (bypassNextUnloadRef.current) {\n bypassNextUnloadRef.current = false;\n return;\n }\n event.preventDefault();\n // Modern browsers may ignore the custom string and show their own\n // generic confirmation. The key behavior is that navigation is\n // blocked/warned, not the exact text.\n event.returnValue = beforeUnloadMessage;\n return event.returnValue;\n };\n\n window.addEventListener('beforeunload', handleBeforeUnload);\n return () => {\n window.removeEventListener('beforeunload', handleBeforeUnload);\n };\n }, [activeUploadCount, beforeUnloadMessage]);\n\n // A back/forward-cache return re-shows this page without reloading it: an\n // armed bypass from the navigation that left must never outlive that navigation.\n useEffect(() => {\n const clearBypass = () => {\n bypassNextUnloadRef.current = false;\n };\n window.addEventListener('pageshow', clearBypass);\n return () => {\n window.removeEventListener('pageshow', clearBypass);\n };\n }, []);\n\n const shouldConfirmNavigation = useCallback(\n () => activeIdsRef.current.size > 0,\n [],\n );\n\n const confirmNavigation = useCallback(\n (options?: ConfirmNavigationOptions): boolean => {\n // SSR / non-browser safety: if `window` isn't available, default to allow.\n if (typeof window === 'undefined') return true;\n const proceed =\n activeIdsRef.current.size === 0 || window.confirm(navigationConfirmMessage);\n if (proceed && options?.fullPageNavigation) bypassNextUnloadRef.current = true;\n return proceed;\n },\n [navigationConfirmMessage],\n );\n\n const value = useMemo<LocalUploadGuardContextValue>(\n () => ({\n activeUploadCount,\n registerUpload,\n unregisterUpload,\n shouldConfirmNavigation,\n confirmNavigation,\n }),\n [activeUploadCount, registerUpload, unregisterUpload, shouldConfirmNavigation, confirmNavigation],\n );\n\n return (\n <LocalUploadGuardContext.Provider value={value}>\n {children}\n </LocalUploadGuardContext.Provider>\n );\n}\n\n/**\n * Low-level accessor for the local-upload guard context. Throws if used\n * outside a `LocalUploadGuardProvider`. Most consumers should use\n * `useLocalUploadGuard` (the public re-export below) instead.\n */\nexport function useLocalUploadGuardContext(): LocalUploadGuardContextValue {\n const ctx = useContext(LocalUploadGuardContext);\n if (ctx === undefined) {\n throw new Error(\n 'useLocalUploadGuard must be used within LocalUploadGuardProvider',\n );\n }\n return ctx;\n}\n\n/**\n * Optional accessor: returns the guard context, or `null` when rendered\n * outside a `LocalUploadGuardProvider`. For package components (e.g. the\n * chat composer registering an in-flight send) that must degrade gracefully\n * in consumers that don't mount the provider, instead of throwing.\n */\nexport function useOptionalLocalUploadGuard(): LocalUploadGuardContextValue | null {\n return useContext(LocalUploadGuardContext) ?? null;\n}\n\n/**\n * Public hook for the local upload navigation guard.\n *\n * Returns `{ activeUploadCount, registerUpload, unregisterUpload }`.\n *\n * Typical usage in an upload flow:\n *\n * const { registerUpload, unregisterUpload } = useLocalUploadGuard();\n * useEffect(() => {\n * if (phase === 'preparing' || phase === 'uploading') {\n * registerUpload(uploadId);\n * return () => unregisterUpload(uploadId);\n * }\n * }, [phase, uploadId, registerUpload, unregisterUpload]);\n */\nexport function useLocalUploadGuard(): LocalUploadGuardContextValue {\n return useLocalUploadGuardContext();\n}\n"]}
1
+ {"version":3,"file":"local-upload-guard-provider.js","sourceRoot":"","sources":["../../src/react/local-upload-guard-provider.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,EACL,aAAa,EACb,WAAW,EACX,UAAU,EACV,SAAS,EACT,KAAK,EACL,OAAO,EACP,MAAM,EACN,QAAQ,GAET,MAAM,OAAO,CAAC;AAuCf,MAAM,uBAAuB,GAAG,aAAa,CAA2C,SAAS,CAAC,CAAC;AAkBnG,MAAM,kCAAkC,GACtC,oGAAoG,CAAC;AACvG,MAAM,oCAAoC,GACxC,gFAAgF,CAAC;AACnF,MAAM,+CAA+C,GACnD,qJAAqJ,CAAC;AACxJ,MAAM,oCAAoC,GACxC,kFAAkF,CAAC;AACrF,MAAM,0CAA0C,GAC9C,yEAAyE,CAAC;AAE5E,SAAS,WAAW,CAAC,UAAsB;IACzC,MAAM,MAAM,GAAG,MAAM,CAAc,IAAI,GAAG,EAAE,CAAC,CAAC;IAC9C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAEtC,MAAM,QAAQ,GAAG,WAAW,CAC1B,CAAC,EAAU,EAAE,EAAE;QACb,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO;QACnC,UAAU,EAAE,CAAC;QACb,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QACvB,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC,EACD,CAAC,UAAU,CAAC,CACb,CAAC;IAEF,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,EAAU,EAAE,EAAE;QAC5C,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO;QACpC,MAAM,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC1B,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAChC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,QAAQ,EAAE,UAAU,EAAE,CAAC;AACjD,CAAC;AAED,MAAM,UAAU,wBAAwB,CAAC,KAAoC;IAC3E,MAAM,EACJ,QAAQ,EACR,mBAAmB,EACnB,wBAAwB,GAAG,kCAAkC,EAC7D,yBAAyB,GAAG,oCAAoC,EAChE,kCAAkC,GAAG,+CAA+C,GACrF,GAAG,KAAK,CAAC;IACV,+EAA+E;IAC/E,MAAM,mBAAmB,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC1C,6FAA6F;IAC7F,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE;QACnC,mBAAmB,CAAC,OAAO,GAAG,KAAK,CAAC;IACtC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,OAAO,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;IACzC,MAAM,WAAW,GAAG,WAAW,CAAC,WAAW,CAAC,CAAC;IAC7C,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IACpC,MAAM,iBAAiB,GAAG,WAAW,CAAC,MAAM,CAAC;IAC7C,MAAM,gBAAgB,GAAG,OAAO,CAAC,KAAK,GAAG,WAAW,CAAC,KAAK,CAAC;IAC3D,MAAM,cAAc,GAAG,gBAAgB,GAAG,CAAC,CAAC;IAE5C,2FAA2F;IAC3F,MAAM,cAAc,GAAG,WAAW,CAAC,GAAW,EAAE;QAC9C,MAAM,SAAS,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC;QAChD,MAAM,OAAO,GAAG,iBAAiB,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC;QACnD,IAAI,SAAS,IAAI,OAAO;YAAE,OAAO,kCAAkC,CAAC;QACpE,OAAO,SAAS,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,yBAAyB,CAAC;IAC1E,CAAC,EAAE;QACD,YAAY;QACZ,iBAAiB;QACjB,wBAAwB;QACxB,yBAAyB;QACzB,kCAAkC;KACnC,CAAC,CAAC;IAEH,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,cAAc;YAAE,OAAO;QAE5B,MAAM,kBAAkB,GAAG,CAAC,KAAwB,EAAE,EAAE;YACtD,IAAI,mBAAmB,CAAC,OAAO,EAAE,CAAC;gBAChC,mBAAmB,CAAC,OAAO,GAAG,KAAK,CAAC;gBACpC,OAAO;YACT,CAAC;YACD,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,wFAAwF;YACxF,8DAA8D;YAC9D,KAAK,CAAC,WAAW;gBACf,mBAAmB;oBACnB,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC;wBAC5B,CAAC,CAAC,oCAAoC;wBACtC,CAAC,CAAC,0CAA0C,CAAC,CAAC;YAClD,OAAO,KAAK,CAAC,WAAW,CAAC;QAC3B,CAAC,CAAC;QAEF,MAAM,CAAC,gBAAgB,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;QAC5D,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,mBAAmB,CAAC,cAAc,EAAE,kBAAkB,CAAC,CAAC;QACjE,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,cAAc,EAAE,mBAAmB,EAAE,YAAY,CAAC,CAAC,CAAC;IAExD,0EAA0E;IAC1E,iFAAiF;IACjF,SAAS,CAAC,GAAG,EAAE;QACb,MAAM,CAAC,gBAAgB,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QACjD,OAAO,GAAG,EAAE;YACV,MAAM,CAAC,mBAAmB,CAAC,UAAU,EAAE,WAAW,CAAC,CAAC;QACtD,CAAC,CAAC;IACJ,CAAC,EAAE,CAAC,WAAW,CAAC,CAAC,CAAC;IAElB,MAAM,uBAAuB,GAAG,WAAW,CACzC,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,IAAI,iBAAiB,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,EACzE,CAAC,YAAY,EAAE,iBAAiB,CAAC,CAClC,CAAC;IAEF,MAAM,iBAAiB,GAAG,WAAW,CACnC,CAAC,OAAkC,EAAW,EAAE;QAC9C,2EAA2E;QAC3E,IAAI,OAAO,MAAM,KAAK,WAAW;YAAE,OAAO,IAAI,CAAC;QAC/C,MAAM,OAAO,GAAG,CAAC,uBAAuB,EAAE,IAAI,MAAM,CAAC,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC;QAC/E,IAAI,OAAO,IAAI,OAAO,EAAE,kBAAkB;YAAE,mBAAmB,CAAC,OAAO,GAAG,IAAI,CAAC;QAC/E,OAAO,OAAO,CAAC;IACjB,CAAC,EACD,CAAC,uBAAuB,EAAE,cAAc,CAAC,CAC1C,CAAC;IAEF,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC;QACL,iBAAiB,EAAE,OAAO,CAAC,KAAK;QAChC,gBAAgB,EAAE,WAAW,CAAC,KAAK;QACnC,gBAAgB;QAChB,cAAc,EAAE,OAAO,CAAC,QAAQ;QAChC,gBAAgB,EAAE,OAAO,CAAC,UAAU;QACpC,mBAAmB,EAAE,WAAW,CAAC,QAAQ;QACzC,qBAAqB,EAAE,WAAW,CAAC,UAAU;QAC7C,uBAAuB;QACvB,iBAAiB;KAClB,CAAC,EACF;QACE,OAAO,CAAC,KAAK;QACb,OAAO,CAAC,QAAQ;QAChB,OAAO,CAAC,UAAU;QAClB,WAAW,CAAC,KAAK;QACjB,WAAW,CAAC,QAAQ;QACpB,WAAW,CAAC,UAAU;QACtB,gBAAgB;QAChB,uBAAuB;QACvB,iBAAiB;KAClB,CACF,CAAC;IAEF,OAAO,CACL,KAAC,uBAAuB,CAAC,QAAQ,IAAC,KAAK,EAAE,KAAK,YAC3C,QAAQ,GACwB,CACpC,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,0BAA0B;IACxC,MAAM,GAAG,GAAG,UAAU,CAAC,uBAAuB,CAAC,CAAC;IAChD,IAAI,GAAG,KAAK,SAAS,EAAE,CAAC;QACtB,MAAM,IAAI,KAAK,CACb,kEAAkE,CACnE,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,2BAA2B;IACzC,OAAO,UAAU,CAAC,uBAAuB,CAAC,IAAI,IAAI,CAAC;AACrD,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,0BAA0B,EAAE,CAAC;AACtC,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,mBAAmB,CAAC,cAAuB;IACzD,MAAM,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,GAAG,0BAA0B,EAAE,CAAC;IACpF,MAAM,EAAE,GAAG,KAAK,EAAE,CAAC;IACnB,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,CAAC,cAAc;YAAE,OAAO;QAC5B,mBAAmB,CAAC,EAAE,CAAC,CAAC;QACxB,OAAO,GAAG,EAAE,CAAC,qBAAqB,CAAC,EAAE,CAAC,CAAC;IACzC,CAAC,EAAE,CAAC,cAAc,EAAE,EAAE,EAAE,mBAAmB,EAAE,qBAAqB,CAAC,CAAC,CAAC;IACrE,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,qBAAqB,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,qBAAqB,CAAC,CAAC,CAAC;IAC1F,OAAO,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;AACjD,CAAC","sourcesContent":["'use client';\n\n/**\n * The navigation and leave guard: warns before the user leaves while work that leaving would lose\n * is registered — a LOCAL byte upload in flight, or an editor's unsaved input.\n *\n * Uploads: phases `preparing` and `uploading` only. `finalizing` and backend `pendingMedia`\n * processing are safe to leave (the bytes are already in Storage), so they are not registered.\n * Unsaved work: an editor registers while it holds input its user has not saved, and unregisters\n * when the input is saved, discarded, or the editor unmounts.\n *\n * While anything is registered the provider installs ONE `beforeunload` listener (tab close,\n * reload, full-page navigation), and `confirmNavigation()` asks before an in-app navigation. It is\n * the one leave guard: an editor never installs its own `beforeunload` handler (FRONTEND-207).\n *\n * Consumers register with a stable id per piece of work; registering a registered id, or\n * unregistering an absent one, is a no-op. Uploads and unsaved work are counted separately, so the\n * same id may be both.\n */\n\nimport {\n createContext,\n useCallback,\n useContext,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from 'react';\n\nexport interface ConfirmNavigationOptions {\n /**\n * The caller leaves through a FULL-PAGE navigation (`location.assign` /\n * `location.replace`) once this returns true. The provider then lets that\n * navigation's next `beforeunload` pass without the browser's own\n * \"Leave site?\" prompt, because the user has just answered this one. Soft\n * (in-app router) navigations never pass it: they fire no `beforeunload`.\n */\n fullPageNavigation?: boolean;\n}\n\ninterface LocalUploadGuardContextValue {\n /** Registered local uploads. */\n activeUploadCount: number;\n /** Registered pieces of unsaved work. */\n unsavedWorkCount: number;\n /** Everything the guard warns about: uploads plus unsaved work. Zero means leaving asks nothing. */\n guardedWorkCount: number;\n registerUpload: (id: string) => void;\n unregisterUpload: (id: string) => void;\n registerUnsavedWork: (id: string) => void;\n unregisterUnsavedWork: (id: string) => void;\n /** Returns true iff an upload or unsaved work is currently registered. Pure read. */\n shouldConfirmNavigation: () => boolean;\n /**\n * Shows a confirm() dialog with the message for what is registered and returns the user's\n * choice. Safe to call when nothing is registered — returns true without prompting.\n *\n * With `{ fullPageNavigation: true }` and a result of true (confirmed, or nothing registered),\n * arms a ONE-SHOT bypass: the next `beforeunload` is not prevented, so a confirmed full-page\n * navigation does not prompt twice. A declined confirm never arms it; the bypass is cleared when\n * it is consumed, when new work (an upload or unsaved work) registers, and on `pageshow` (a\n * back/forward-cache return).\n */\n confirmNavigation: (options?: ConfirmNavigationOptions) => boolean;\n}\n\nconst LocalUploadGuardContext = createContext<LocalUploadGuardContextValue | undefined>(undefined);\n\nexport interface LocalUploadGuardProviderProps {\n /**\n * Message the browser is given when the user closes the tab, reloads, or leaves the page while\n * work is registered. Modern browsers show their own generic dialog and ignore it; when omitted,\n * the default for what is registered is used.\n */\n beforeUnloadMessage?: string;\n /** `confirmNavigation()`'s message while an upload is in progress (and no unsaved work). */\n navigationConfirmMessage?: string;\n /** `confirmNavigation()`'s message while unsaved work is registered (and no upload). */\n unsavedWorkConfirmMessage?: string;\n /** `confirmNavigation()`'s message while both an upload and unsaved work are registered. */\n uploadAndUnsavedWorkConfirmMessage?: string;\n children: ReactNode;\n}\n\nconst DEFAULT_NAVIGATION_CONFIRM_MESSAGE =\n 'An upload is currently in progress. If you leave this page, the upload will be canceled. Continue?';\nconst DEFAULT_UNSAVED_WORK_CONFIRM_MESSAGE =\n 'You have unsaved changes. If you leave this page, they will be lost. Continue?';\nconst DEFAULT_UPLOAD_AND_UNSAVED_WORK_CONFIRM_MESSAGE =\n 'An upload is in progress and you have unsaved changes. If you leave this page, the upload will be canceled and your changes will be lost. Continue?';\nconst DEFAULT_UPLOAD_BEFORE_UNLOAD_MESSAGE =\n 'File upload in progress. Are you sure you want to leave and end the file upload?';\nconst DEFAULT_UNSAVED_WORK_BEFORE_UNLOAD_MESSAGE =\n 'You have unsaved changes. Are you sure you want to leave and lose them?';\n\nfunction useRegistry(onRegister: () => void) {\n const idsRef = useRef<Set<string>>(new Set());\n const [count, setCount] = useState(0);\n\n const register = useCallback(\n (id: string) => {\n if (idsRef.current.has(id)) return;\n onRegister();\n idsRef.current.add(id);\n setCount(idsRef.current.size);\n },\n [onRegister],\n );\n\n const unregister = useCallback((id: string) => {\n if (!idsRef.current.has(id)) return;\n idsRef.current.delete(id);\n setCount(idsRef.current.size);\n }, []);\n\n return { idsRef, count, register, unregister };\n}\n\nexport function LocalUploadGuardProvider(props: LocalUploadGuardProviderProps) {\n const {\n children,\n beforeUnloadMessage,\n navigationConfirmMessage = DEFAULT_NAVIGATION_CONFIRM_MESSAGE,\n unsavedWorkConfirmMessage = DEFAULT_UNSAVED_WORK_CONFIRM_MESSAGE,\n uploadAndUnsavedWorkConfirmMessage = DEFAULT_UPLOAD_AND_UNSAVED_WORK_CONFIRM_MESSAGE,\n } = props;\n // One-shot: the user already confirmed the full-page navigation now unloading.\n const bypassNextUnloadRef = useRef(false);\n // Also run when work registers: work that starts after a confirmation was not covered by it.\n const clearBypass = useCallback(() => {\n bypassNextUnloadRef.current = false;\n }, []);\n\n const uploads = useRegistry(clearBypass);\n const unsavedWork = useRegistry(clearBypass);\n const uploadIdsRef = uploads.idsRef;\n const unsavedWorkIdsRef = unsavedWork.idsRef;\n const guardedWorkCount = uploads.count + unsavedWork.count;\n const hasGuardedWork = guardedWorkCount > 0;\n\n // Read at the moment of asking, so a message always matches what is registered right then.\n const confirmMessage = useCallback((): string => {\n const uploading = uploadIdsRef.current.size > 0;\n const unsaved = unsavedWorkIdsRef.current.size > 0;\n if (uploading && unsaved) return uploadAndUnsavedWorkConfirmMessage;\n return uploading ? navigationConfirmMessage : unsavedWorkConfirmMessage;\n }, [\n uploadIdsRef,\n unsavedWorkIdsRef,\n navigationConfirmMessage,\n unsavedWorkConfirmMessage,\n uploadAndUnsavedWorkConfirmMessage,\n ]);\n\n useEffect(() => {\n if (!hasGuardedWork) return;\n\n const handleBeforeUnload = (event: BeforeUnloadEvent) => {\n if (bypassNextUnloadRef.current) {\n bypassNextUnloadRef.current = false;\n return;\n }\n event.preventDefault();\n // Modern browsers may ignore the custom string and show their own generic confirmation;\n // what matters is that leaving is warned, not the exact text.\n event.returnValue =\n beforeUnloadMessage ??\n (uploadIdsRef.current.size > 0\n ? DEFAULT_UPLOAD_BEFORE_UNLOAD_MESSAGE\n : DEFAULT_UNSAVED_WORK_BEFORE_UNLOAD_MESSAGE);\n return event.returnValue;\n };\n\n window.addEventListener('beforeunload', handleBeforeUnload);\n return () => {\n window.removeEventListener('beforeunload', handleBeforeUnload);\n };\n }, [hasGuardedWork, beforeUnloadMessage, uploadIdsRef]);\n\n // A back/forward-cache return re-shows this page without reloading it: an\n // armed bypass from the navigation that left must never outlive that navigation.\n useEffect(() => {\n window.addEventListener('pageshow', clearBypass);\n return () => {\n window.removeEventListener('pageshow', clearBypass);\n };\n }, [clearBypass]);\n\n const shouldConfirmNavigation = useCallback(\n () => uploadIdsRef.current.size > 0 || unsavedWorkIdsRef.current.size > 0,\n [uploadIdsRef, unsavedWorkIdsRef],\n );\n\n const confirmNavigation = useCallback(\n (options?: ConfirmNavigationOptions): boolean => {\n // SSR / non-browser safety: if `window` isn't available, default to allow.\n if (typeof window === 'undefined') return true;\n const proceed = !shouldConfirmNavigation() || window.confirm(confirmMessage());\n if (proceed && options?.fullPageNavigation) bypassNextUnloadRef.current = true;\n return proceed;\n },\n [shouldConfirmNavigation, confirmMessage],\n );\n\n const value = useMemo<LocalUploadGuardContextValue>(\n () => ({\n activeUploadCount: uploads.count,\n unsavedWorkCount: unsavedWork.count,\n guardedWorkCount,\n registerUpload: uploads.register,\n unregisterUpload: uploads.unregister,\n registerUnsavedWork: unsavedWork.register,\n unregisterUnsavedWork: unsavedWork.unregister,\n shouldConfirmNavigation,\n confirmNavigation,\n }),\n [\n uploads.count,\n uploads.register,\n uploads.unregister,\n unsavedWork.count,\n unsavedWork.register,\n unsavedWork.unregister,\n guardedWorkCount,\n shouldConfirmNavigation,\n confirmNavigation,\n ],\n );\n\n return (\n <LocalUploadGuardContext.Provider value={value}>\n {children}\n </LocalUploadGuardContext.Provider>\n );\n}\n\n/**\n * Low-level accessor for the guard context. Throws if used outside a\n * `LocalUploadGuardProvider`. Most consumers should use `useLocalUploadGuard`\n * (the public re-export below) instead.\n */\nexport function useLocalUploadGuardContext(): LocalUploadGuardContextValue {\n const ctx = useContext(LocalUploadGuardContext);\n if (ctx === undefined) {\n throw new Error(\n 'useLocalUploadGuard must be used within LocalUploadGuardProvider',\n );\n }\n return ctx;\n}\n\n/**\n * Optional accessor: returns the guard context, or `null` when rendered\n * outside a `LocalUploadGuardProvider`. For package components (e.g. the\n * chat composer registering an in-flight send) that must degrade gracefully\n * in consumers that don't mount the provider, instead of throwing.\n */\nexport function useOptionalLocalUploadGuard(): LocalUploadGuardContextValue | null {\n return useContext(LocalUploadGuardContext) ?? null;\n}\n\n/**\n * Public hook for the navigation and leave guard.\n *\n * Typical usage in an upload flow:\n *\n * const { registerUpload, unregisterUpload } = useLocalUploadGuard();\n * useEffect(() => {\n * if (phase === 'preparing' || phase === 'uploading') {\n * registerUpload(uploadId);\n * return () => unregisterUpload(uploadId);\n * }\n * }, [phase, uploadId, registerUpload, unregisterUpload]);\n *\n * An editor holding input uses `useUnsavedWorkGuard(hasUnsavedWork)` instead.\n */\nexport function useLocalUploadGuard(): LocalUploadGuardContextValue {\n return useLocalUploadGuardContext();\n}\n\n/**\n * Registers the calling editor's unsaved input with the leave guard while `hasUnsavedWork` is\n * true, and unregisters it when that turns false or the editor unmounts. While registered, closing\n * the tab, reloading, a full-page navigation, and every guarded in-app navigation ask first.\n * Throws outside a `LocalUploadGuardProvider`.\n *\n * Returns `release`, which unregisters at once: an editor that saves and then navigates in the\n * same handler calls it after the save, because its dirty flag clears only on the next render.\n */\nexport function useUnsavedWorkGuard(hasUnsavedWork: boolean): { release: () => void } {\n const { registerUnsavedWork, unregisterUnsavedWork } = useLocalUploadGuardContext();\n const id = useId();\n useEffect(() => {\n if (!hasUnsavedWork) return;\n registerUnsavedWork(id);\n return () => unregisterUnsavedWork(id);\n }, [hasUnsavedWork, id, registerUnsavedWork, unregisterUnsavedWork]);\n const release = useCallback(() => unregisterUnsavedWork(id), [id, unregisterUnsavedWork]);\n return useMemo(() => ({ release }), [release]);\n}\n"]}
@@ -1,4 +1,4 @@
1
- export { InFlightUploadsProvider, useInFlightUploadsState, useInFlightUpload, useUploadActivityState, useUploadsSourceState, isTerminalUpload, type UploadsSourceState, type InFlightUpload, type InFlightUploadActive, type InFlightUploadCompleted, type InFlightUploadFailed, type InFlightUploadRejected, type InFlightUploadBase, type InFlightUploadStatus, type InFlightUploadsAdapter, type InFlightUploadsMonitoringEvent, type InFlightUploadsProviderProps, type ParsedPendingMedia, type FirestoreSubscribeFn, type FirestoreLikeSnapshot, type FirestoreLikeDocChange, } from '../in-flight-uploads-provider.js';
1
+ export { InFlightUploadsProvider, useInFlightUploadsState, useInFlightUpload, useUploadActivityState, useUploadsSourceState, useUploadsSourceRetry, isTerminalUpload, type UploadsSourceState, type UploadsSourceRetry, type InFlightUpload, type InFlightUploadActive, type InFlightUploadCompleted, type InFlightUploadFailed, type InFlightUploadRejected, type InFlightUploadBase, type InFlightUploadStatus, type InFlightUploadsAdapter, type InFlightUploadsMonitoringEvent, type InFlightUploadsProviderProps, type ParsedPendingMedia, type FirestoreSubscribeFn, type FirestoreLikeSnapshot, type FirestoreLikeDocChange, } from '../in-flight-uploads-provider.js';
2
2
  export { useClearUploadActivity, type ClearUploadActivityOptions, } from '../use-clear-upload-activity.js';
3
3
  export { useMarkUploadActivitySeen, type MarkUploadActivitySeenOptions, } from '../use-mark-upload-activity-seen.js';
4
4
  export { useUploadProcessing, type UploadProcessingState, type UseUploadProcessingOptions, } from '../use-upload-processing.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/tray/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,8BAA8B,EACnC,KAAK,4BAA4B,EACjC,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,sBAAsB,EACtB,KAAK,0BAA0B,GAChC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,yBAAyB,EACzB,KAAK,6BAA6B,GACnC,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,mBAAmB,EACnB,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,GAChC,MAAM,6BAA6B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/tray/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,EAChB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,cAAc,EACnB,KAAK,oBAAoB,EACzB,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,sBAAsB,EAC3B,KAAK,8BAA8B,EACnC,KAAK,4BAA4B,EACjC,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,EAC1B,KAAK,sBAAsB,GAC5B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,sBAAsB,EACtB,KAAK,0BAA0B,GAChC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,yBAAyB,EACzB,KAAK,6BAA6B,GACnC,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,mBAAmB,EACnB,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,GAChC,MAAM,6BAA6B,CAAC"}
@@ -1,4 +1,4 @@
1
- export { InFlightUploadsProvider, useInFlightUploadsState, useInFlightUpload, useUploadActivityState, useUploadsSourceState, isTerminalUpload, } from '../in-flight-uploads-provider.js';
1
+ export { InFlightUploadsProvider, useInFlightUploadsState, useInFlightUpload, useUploadActivityState, useUploadsSourceState, useUploadsSourceRetry, isTerminalUpload, } from '../in-flight-uploads-provider.js';
2
2
  export { useClearUploadActivity, } from '../use-clear-upload-activity.js';
3
3
  export { useMarkUploadActivitySeen, } from '../use-mark-upload-activity-seen.js';
4
4
  export { useUploadProcessing, } from '../use-upload-processing.js';
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/tray/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,gBAAgB,GAgBjB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,sBAAsB,GAEvB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,yBAAyB,GAE1B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,mBAAmB,GAGpB,MAAM,6BAA6B,CAAC","sourcesContent":["export {\n InFlightUploadsProvider,\n useInFlightUploadsState,\n useInFlightUpload,\n useUploadActivityState,\n useUploadsSourceState,\n isTerminalUpload,\n type UploadsSourceState,\n type InFlightUpload,\n type InFlightUploadActive,\n type InFlightUploadCompleted,\n type InFlightUploadFailed,\n type InFlightUploadRejected,\n type InFlightUploadBase,\n type InFlightUploadStatus,\n type InFlightUploadsAdapter,\n type InFlightUploadsMonitoringEvent,\n type InFlightUploadsProviderProps,\n type ParsedPendingMedia,\n type FirestoreSubscribeFn,\n type FirestoreLikeSnapshot,\n type FirestoreLikeDocChange,\n} from '../in-flight-uploads-provider.js';\nexport {\n useClearUploadActivity,\n type ClearUploadActivityOptions,\n} from '../use-clear-upload-activity.js';\nexport {\n useMarkUploadActivitySeen,\n type MarkUploadActivitySeenOptions,\n} from '../use-mark-upload-activity-seen.js';\nexport {\n useUploadProcessing,\n type UploadProcessingState,\n type UseUploadProcessingOptions,\n} from '../use-upload-processing.js';\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/tray/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,iBAAiB,EACjB,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACrB,gBAAgB,GAiBjB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,sBAAsB,GAEvB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,yBAAyB,GAE1B,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EACL,mBAAmB,GAGpB,MAAM,6BAA6B,CAAC","sourcesContent":["export {\n InFlightUploadsProvider,\n useInFlightUploadsState,\n useInFlightUpload,\n useUploadActivityState,\n useUploadsSourceState,\n useUploadsSourceRetry,\n isTerminalUpload,\n type UploadsSourceState,\n type UploadsSourceRetry,\n type InFlightUpload,\n type InFlightUploadActive,\n type InFlightUploadCompleted,\n type InFlightUploadFailed,\n type InFlightUploadRejected,\n type InFlightUploadBase,\n type InFlightUploadStatus,\n type InFlightUploadsAdapter,\n type InFlightUploadsMonitoringEvent,\n type InFlightUploadsProviderProps,\n type ParsedPendingMedia,\n type FirestoreSubscribeFn,\n type FirestoreLikeSnapshot,\n type FirestoreLikeDocChange,\n} from '../in-flight-uploads-provider.js';\nexport {\n useClearUploadActivity,\n type ClearUploadActivityOptions,\n} from '../use-clear-upload-activity.js';\nexport {\n useMarkUploadActivitySeen,\n type MarkUploadActivitySeenOptions,\n} from '../use-mark-upload-activity-seen.js';\nexport {\n useUploadProcessing,\n type UploadProcessingState,\n type UseUploadProcessingOptions,\n} from '../use-upload-processing.js';\n"]}
@@ -1,8 +1,8 @@
1
1
  import { type ConfirmNavigationOptions } from './local-upload-guard-provider.js';
2
2
  /**
3
3
  * Returns a callable that wraps a navigation action. Calling the returned
4
- * function with `performNavigation` either invokes it immediately (when no
5
- * active uploads) or shows a confirmation prompt; if the user confirms,
4
+ * function with `performNavigation` either invokes it immediately (when
5
+ * nothing is registered) or shows a confirmation prompt; if the user confirms,
6
6
  * `performNavigation` is invoked. `options` passes through to
7
7
  * `confirmNavigation()`.
8
8
  */
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
  /**
3
- * Wraps a navigation action so it prompts the user when an upload is in
4
- * progress. Returns a function that takes the consumer's actual navigation
5
- * call and either runs it immediately (no active uploads) or shows a confirm()
3
+ * Wraps a navigation action so it prompts the user while an upload or unsaved
4
+ * work is registered. Returns a function that takes the consumer's actual navigation
5
+ * call and either runs it immediately (nothing registered) or shows a confirm()
6
6
  * dialog first.
7
7
  *
8
8
  * Designed to be framework-agnostic — the consumer supplies the navigator
@@ -24,8 +24,8 @@ import { useCallback } from 'react';
24
24
  import { useLocalUploadGuard } from './local-upload-guard-provider.js';
25
25
  /**
26
26
  * Returns a callable that wraps a navigation action. Calling the returned
27
- * function with `performNavigation` either invokes it immediately (when no
28
- * active uploads) or shows a confirmation prompt; if the user confirms,
27
+ * function with `performNavigation` either invokes it immediately (when
28
+ * nothing is registered) or shows a confirmation prompt; if the user confirms,
29
29
  * `performNavigation` is invoked. `options` passes through to
30
30
  * `confirmNavigation()`.
31
31
  */
@@ -1 +1 @@
1
- {"version":3,"file":"use-guarded-navigation.js","sourceRoot":"","sources":["../../src/react/use-guarded-navigation.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAiC,MAAM,kCAAkC,CAAC;AAEtG;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB;IAIlC,MAAM,EAAE,iBAAiB,EAAE,GAAG,mBAAmB,EAAE,CAAC;IACpD,OAAO,WAAW,CAChB,CAAC,iBAA6B,EAAE,OAAkC,EAAE,EAAE;QACpE,IAAI,iBAAiB,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/B,iBAAiB,EAAE,CAAC;QACtB,CAAC;IACH,CAAC,EACD,CAAC,iBAAiB,CAAC,CACpB,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\n/**\n * Wraps a navigation action so it prompts the user when an upload is in\n * progress. Returns a function that takes the consumer's actual navigation\n * call and either runs it immediately (no active uploads) or shows a confirm()\n * dialog first.\n *\n * Designed to be framework-agnostic — the consumer supplies the navigator\n * (Next.js router.push, React Router navigate, plain location.assign, etc.).\n * A FULL-PAGE navigator (`location.assign` / `location.replace`) passes\n * `{ fullPageNavigation: true }`, so the unload that follows a confirmed\n * navigation does not raise the browser's own \"Leave site?\" as a second prompt\n * (see `ConfirmNavigationOptions`). Router navigations pass nothing.\n *\n * Typical use inside a button:\n *\n * const guardedNav = useGuardedNavigation();\n * <Button onClick={() => guardedNav(() => router.push('/somewhere'))}>Go</Button>\n * <Button onClick={() => guardedNav(() => location.assign(url), { fullPageNavigation: true })}>Leave</Button>\n *\n * For links, use the `<GuardedLink>` render-prop wrapper.\n */\n\nimport { useCallback } from 'react';\nimport { useLocalUploadGuard, type ConfirmNavigationOptions } from './local-upload-guard-provider.js';\n\n/**\n * Returns a callable that wraps a navigation action. Calling the returned\n * function with `performNavigation` either invokes it immediately (when no\n * active uploads) or shows a confirmation prompt; if the user confirms,\n * `performNavigation` is invoked. `options` passes through to\n * `confirmNavigation()`.\n */\nexport function useGuardedNavigation(): (\n performNavigation: () => void,\n options?: ConfirmNavigationOptions,\n) => void {\n const { confirmNavigation } = useLocalUploadGuard();\n return useCallback(\n (performNavigation: () => void, options?: ConfirmNavigationOptions) => {\n if (confirmNavigation(options)) {\n performNavigation();\n }\n },\n [confirmNavigation],\n );\n}\n"]}
1
+ {"version":3,"file":"use-guarded-navigation.js","sourceRoot":"","sources":["../../src/react/use-guarded-navigation.ts"],"names":[],"mappings":"AAAA,YAAY,CAAC;AAEb;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAiC,MAAM,kCAAkC,CAAC;AAEtG;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB;IAIlC,MAAM,EAAE,iBAAiB,EAAE,GAAG,mBAAmB,EAAE,CAAC;IACpD,OAAO,WAAW,CAChB,CAAC,iBAA6B,EAAE,OAAkC,EAAE,EAAE;QACpE,IAAI,iBAAiB,CAAC,OAAO,CAAC,EAAE,CAAC;YAC/B,iBAAiB,EAAE,CAAC;QACtB,CAAC;IACH,CAAC,EACD,CAAC,iBAAiB,CAAC,CACpB,CAAC;AACJ,CAAC","sourcesContent":["'use client';\n\n/**\n * Wraps a navigation action so it prompts the user while an upload or unsaved\n * work is registered. Returns a function that takes the consumer's actual navigation\n * call and either runs it immediately (nothing registered) or shows a confirm()\n * dialog first.\n *\n * Designed to be framework-agnostic — the consumer supplies the navigator\n * (Next.js router.push, React Router navigate, plain location.assign, etc.).\n * A FULL-PAGE navigator (`location.assign` / `location.replace`) passes\n * `{ fullPageNavigation: true }`, so the unload that follows a confirmed\n * navigation does not raise the browser's own \"Leave site?\" as a second prompt\n * (see `ConfirmNavigationOptions`). Router navigations pass nothing.\n *\n * Typical use inside a button:\n *\n * const guardedNav = useGuardedNavigation();\n * <Button onClick={() => guardedNav(() => router.push('/somewhere'))}>Go</Button>\n * <Button onClick={() => guardedNav(() => location.assign(url), { fullPageNavigation: true })}>Leave</Button>\n *\n * For links, use the `<GuardedLink>` render-prop wrapper.\n */\n\nimport { useCallback } from 'react';\nimport { useLocalUploadGuard, type ConfirmNavigationOptions } from './local-upload-guard-provider.js';\n\n/**\n * Returns a callable that wraps a navigation action. Calling the returned\n * function with `performNavigation` either invokes it immediately (when\n * nothing is registered) or shows a confirmation prompt; if the user confirms,\n * `performNavigation` is invoked. `options` passes through to\n * `confirmNavigation()`.\n */\nexport function useGuardedNavigation(): (\n performNavigation: () => void,\n options?: ConfirmNavigationOptions,\n) => void {\n const { confirmNavigation } = useLocalUploadGuard();\n return useCallback(\n (performNavigation: () => void, options?: ConfirmNavigationOptions) => {\n if (confirmNavigation(options)) {\n performNavigation();\n }\n },\n [confirmNavigation],\n );\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ttt-productions/upload-ui",
3
- "version": "0.7.12",
3
+ "version": "0.7.14",
4
4
  "description": "Upload UI primitives: local upload guard provider, useGuardedUpload helper, DeferredUploadFormShell for forms that defer Firebase Storage upload until form submit.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -40,10 +40,10 @@
40
40
  "prepublishOnly": "npm run clean && npm run build"
41
41
  },
42
42
  "dependencies": {
43
- "@ttt-productions/file-input": "^0.11.12",
44
- "@ttt-productions/media-schemas": "^0.14.7",
45
- "@ttt-productions/ui-core": "^0.11.19",
46
- "@ttt-productions/upload-core": "^0.7.4"
43
+ "@ttt-productions/file-input": "^0.11.14",
44
+ "@ttt-productions/media-schemas": "^0.14.9",
45
+ "@ttt-productions/ui-core": "^0.11.30",
46
+ "@ttt-productions/upload-core": "^0.7.5"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "@tanstack/react-query": ">=5.0.0",