@ttt-productions/upload-ui 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/react/deferred-upload-form-shell.d.ts +39 -0
- package/dist/react/deferred-upload-form-shell.d.ts.map +1 -0
- package/dist/react/deferred-upload-form-shell.js +56 -0
- package/dist/react/deferred-upload-form-shell.js.map +1 -0
- package/dist/react/guarded-link.d.ts +38 -0
- package/dist/react/guarded-link.d.ts.map +1 -0
- package/dist/react/guarded-link.js +14 -0
- package/dist/react/guarded-link.js.map +1 -0
- package/dist/react/in-flight-uploads-provider.d.ts +214 -0
- package/dist/react/in-flight-uploads-provider.d.ts.map +1 -0
- package/dist/react/in-flight-uploads-provider.js +286 -0
- package/dist/react/in-flight-uploads-provider.js.map +1 -0
- package/dist/react/index.d.ts +11 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +10 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/local-upload-guard-provider.d.ts +76 -0
- package/dist/react/local-upload-guard-provider.d.ts.map +1 -0
- package/dist/react/local-upload-guard-provider.js +110 -0
- package/dist/react/local-upload-guard-provider.js.map +1 -0
- package/dist/react/upload-activity-tray.d.ts +54 -0
- package/dist/react/upload-activity-tray.d.ts.map +1 -0
- package/dist/react/upload-activity-tray.js +87 -0
- package/dist/react/upload-activity-tray.js.map +1 -0
- package/dist/react/use-clear-upload-activity.d.ts +8 -0
- package/dist/react/use-clear-upload-activity.d.ts.map +1 -0
- package/dist/react/use-clear-upload-activity.js +24 -0
- package/dist/react/use-clear-upload-activity.js.map +1 -0
- package/dist/react/use-guarded-navigation.d.ts +8 -0
- package/dist/react/use-guarded-navigation.d.ts.map +1 -0
- package/dist/react/use-guarded-navigation.js +34 -0
- package/dist/react/use-guarded-navigation.js.map +1 -0
- package/dist/react/use-guarded-upload.d.ts +37 -0
- package/dist/react/use-guarded-upload.d.ts.map +1 -0
- package/dist/react/use-guarded-upload.js +39 -0
- package/dist/react/use-guarded-upload.js.map +1 -0
- package/dist/react/use-upload-processing.d.ts +27 -0
- package/dist/react/use-upload-processing.d.ts.map +1 -0
- package/dist/react/use-upload-processing.js +112 -0
- package/dist/react/use-upload-processing.js.map +1 -0
- package/package.json +53 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Generic in-flight uploads provider.
|
|
5
|
+
*
|
|
6
|
+
* Provides a global Map of in-flight upload state by `pendingMediaId`, derived
|
|
7
|
+
* from a Firestore listener over the consumer-supplied collection. On terminal
|
|
8
|
+
* status transitions observed THIS SESSION, fires consumer-supplied success /
|
|
9
|
+
* failure / rejection callbacks. Records monitoring breadcrumbs and parse
|
|
10
|
+
* errors through the injected monitoring callback.
|
|
11
|
+
*
|
|
12
|
+
* EVERY TTT-specific value is supplied by the consumer:
|
|
13
|
+
* - userId resolution
|
|
14
|
+
* - Firestore `db` reference
|
|
15
|
+
* - collection path
|
|
16
|
+
* - listener time window (ms)
|
|
17
|
+
* - parser/schema for raw Firestore doc → typed PendingMedia
|
|
18
|
+
* - success copy per origin
|
|
19
|
+
* - notification callback (cache invalidation, etc.)
|
|
20
|
+
* - toast callback
|
|
21
|
+
* - rejection callback (for the media-rejection dialog or equivalent)
|
|
22
|
+
* - monitoring callback (breadcrumbs + captured errors)
|
|
23
|
+
*
|
|
24
|
+
* Read APIs:
|
|
25
|
+
* - `useInFlightUploadsState()` — full Map.
|
|
26
|
+
* - `useInFlightUpload(id)` — single doc, narrowable on status.
|
|
27
|
+
* - `useUploadActivityState()` — tray-visible items, sorted newest-first.
|
|
28
|
+
*
|
|
29
|
+
* The package owns the deduplication, initial-snapshot suppression, and
|
|
30
|
+
* non-terminal → terminal transition tracking. It does NOT own anything about
|
|
31
|
+
* what a file-origin is or what message text appears in toasts.
|
|
32
|
+
*/
|
|
33
|
+
import { createContext, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
34
|
+
const InFlightUploadsContext = createContext(undefined);
|
|
35
|
+
function fromParsed(parsed) {
|
|
36
|
+
const base = {
|
|
37
|
+
id: parsed.id,
|
|
38
|
+
fileOrigin: parsed.fileOrigin,
|
|
39
|
+
surface: parsed.surface,
|
|
40
|
+
createdAt: parsed.createdAt,
|
|
41
|
+
targetIds: parsed.targetIds,
|
|
42
|
+
};
|
|
43
|
+
if (parsed.status === 'pending' || parsed.status === 'processing') {
|
|
44
|
+
return { ...base, status: parsed.status };
|
|
45
|
+
}
|
|
46
|
+
if (parsed.status === 'completed') {
|
|
47
|
+
return {
|
|
48
|
+
...base,
|
|
49
|
+
status: 'completed',
|
|
50
|
+
completedAt: parsed.completedAt ?? parsed.createdAt,
|
|
51
|
+
uploadTrayClearedAt: parsed.uploadTrayClearedAt,
|
|
52
|
+
uploadTrayClearedBy: parsed.uploadTrayClearedBy,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
if (parsed.status === 'failed') {
|
|
56
|
+
return {
|
|
57
|
+
...base,
|
|
58
|
+
status: 'failed',
|
|
59
|
+
failedAt: parsed.failedAt ?? parsed.createdAt,
|
|
60
|
+
errorCategory: parsed.errorCategory ?? 'system',
|
|
61
|
+
errorMessage: parsed.errorMessage ?? '',
|
|
62
|
+
uploadTrayClearedAt: parsed.uploadTrayClearedAt,
|
|
63
|
+
uploadTrayClearedBy: parsed.uploadTrayClearedBy,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
// rejected
|
|
67
|
+
return {
|
|
68
|
+
...base,
|
|
69
|
+
status: 'rejected',
|
|
70
|
+
rejectedAt: parsed.rejectedAt ?? parsed.createdAt,
|
|
71
|
+
rejectionType: parsed.rejectionType ?? 'media',
|
|
72
|
+
errorMessage: parsed.errorMessage ?? '',
|
|
73
|
+
violationId: parsed.violationId,
|
|
74
|
+
uploadTrayClearedAt: parsed.uploadTrayClearedAt,
|
|
75
|
+
uploadTrayClearedBy: parsed.uploadTrayClearedBy,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function InFlightUploadsProvider(props) {
|
|
79
|
+
const { adapter, subscribe = defaultFirestoreSubscribe, children } = props;
|
|
80
|
+
const [uploads, setUploads] = useState(new Map());
|
|
81
|
+
const seenTerminalIdsRef = useRef(new Set());
|
|
82
|
+
const statusByIdRef = useRef(new Map());
|
|
83
|
+
const hasProcessedInitialSnapshotRef = useRef(false);
|
|
84
|
+
// Stable refs for adapter callbacks — read from latest adapter inside effect.
|
|
85
|
+
const adapterRef = useRef(adapter);
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
adapterRef.current = adapter;
|
|
88
|
+
});
|
|
89
|
+
const userId = adapter.getCurrentUserId();
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (!userId) {
|
|
92
|
+
// Sign-out: clear all state.
|
|
93
|
+
setUploads(new Map());
|
|
94
|
+
seenTerminalIdsRef.current = new Set();
|
|
95
|
+
statusByIdRef.current = new Map();
|
|
96
|
+
hasProcessedInitialSnapshotRef.current = false;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const a = adapterRef.current;
|
|
100
|
+
const windowStartMs = Date.now() - a.listenerWindowMs;
|
|
101
|
+
const handleSnapshot = (snapshot) => {
|
|
102
|
+
const updates = [];
|
|
103
|
+
const newlyTerminal = [];
|
|
104
|
+
const isInitialSnapshot = !hasProcessedInitialSnapshotRef.current;
|
|
105
|
+
for (const change of snapshot.docChanges()) {
|
|
106
|
+
if (change.type === 'removed') {
|
|
107
|
+
updates.push({ id: change.doc.id, upload: null });
|
|
108
|
+
statusByIdRef.current.delete(change.doc.id);
|
|
109
|
+
seenTerminalIdsRef.current.delete(change.doc.id);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
let parsed;
|
|
113
|
+
try {
|
|
114
|
+
parsed = adapterRef.current.parsePendingMedia(change.doc.data());
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
adapterRef.current.onMonitoringEvent({
|
|
118
|
+
type: 'parse-error',
|
|
119
|
+
docId: change.doc.id,
|
|
120
|
+
error,
|
|
121
|
+
data: change.doc.data(),
|
|
122
|
+
});
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const upload = fromParsed(parsed);
|
|
126
|
+
updates.push({ id: parsed.id, upload });
|
|
127
|
+
const previousStatus = statusByIdRef.current.get(parsed.id);
|
|
128
|
+
const currentStatus = parsed.status;
|
|
129
|
+
const currentIsTerminal = currentStatus === 'completed' ||
|
|
130
|
+
currentStatus === 'failed' ||
|
|
131
|
+
currentStatus === 'rejected';
|
|
132
|
+
const previousWasNonTerminal = previousStatus === 'pending' || previousStatus === 'processing';
|
|
133
|
+
if (isInitialSnapshot) {
|
|
134
|
+
// First snapshot after mount: mark already-terminal docs as seen,
|
|
135
|
+
// but DO NOT fire callbacks. Prevents duplicate toasts on refresh.
|
|
136
|
+
if (currentIsTerminal) {
|
|
137
|
+
seenTerminalIdsRef.current.add(parsed.id);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (currentIsTerminal &&
|
|
141
|
+
previousWasNonTerminal &&
|
|
142
|
+
!seenTerminalIdsRef.current.has(parsed.id)) {
|
|
143
|
+
seenTerminalIdsRef.current.add(parsed.id);
|
|
144
|
+
newlyTerminal.push(parsed);
|
|
145
|
+
}
|
|
146
|
+
else if (currentIsTerminal) {
|
|
147
|
+
// Terminal doc appeared without an observed non-terminal predecessor
|
|
148
|
+
// (race in a non-initial snapshot). Mark as seen, no callback fire.
|
|
149
|
+
seenTerminalIdsRef.current.add(parsed.id);
|
|
150
|
+
}
|
|
151
|
+
statusByIdRef.current.set(parsed.id, currentStatus);
|
|
152
|
+
}
|
|
153
|
+
hasProcessedInitialSnapshotRef.current = true;
|
|
154
|
+
if (updates.length > 0) {
|
|
155
|
+
setUploads((prev) => {
|
|
156
|
+
const next = new Map(prev);
|
|
157
|
+
for (const { id, upload } of updates) {
|
|
158
|
+
if (upload === null)
|
|
159
|
+
next.delete(id);
|
|
160
|
+
else
|
|
161
|
+
next.set(id, upload);
|
|
162
|
+
}
|
|
163
|
+
return next;
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
for (const parsed of newlyTerminal) {
|
|
167
|
+
handleTerminal(parsed);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
function handleTerminal(parsed) {
|
|
171
|
+
adapterRef.current.onMonitoringEvent({
|
|
172
|
+
type: 'breadcrumb',
|
|
173
|
+
category: 'upload.terminal',
|
|
174
|
+
level: 'info',
|
|
175
|
+
message: `${parsed.fileOrigin}:${parsed.status}`,
|
|
176
|
+
data: {
|
|
177
|
+
id: parsed.id,
|
|
178
|
+
fileOrigin: parsed.fileOrigin,
|
|
179
|
+
status: parsed.status,
|
|
180
|
+
surface: parsed.surface,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
if (parsed.status === 'completed') {
|
|
184
|
+
adapterRef.current.onUploadCompleted(parsed);
|
|
185
|
+
}
|
|
186
|
+
else if (parsed.status === 'failed') {
|
|
187
|
+
adapterRef.current.onUploadFailed(parsed);
|
|
188
|
+
}
|
|
189
|
+
else if (parsed.status === 'rejected') {
|
|
190
|
+
adapterRef.current.onUploadRejected(parsed);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const unsubscribe = subscribe({
|
|
194
|
+
db: a.db,
|
|
195
|
+
collectionPath: a.collectionPath,
|
|
196
|
+
userId,
|
|
197
|
+
windowStartMs,
|
|
198
|
+
onSnapshot: handleSnapshot,
|
|
199
|
+
onError: (error) => {
|
|
200
|
+
adapterRef.current.onMonitoringEvent({ type: 'listener-error', error });
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
return () => {
|
|
204
|
+
unsubscribe();
|
|
205
|
+
};
|
|
206
|
+
// adapter is intentionally NOT in the deps array — adapter is read via
|
|
207
|
+
// adapterRef on every snapshot, so identity changes don't re-subscribe.
|
|
208
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
209
|
+
}, [userId, subscribe]);
|
|
210
|
+
const value = useMemo(() => ({ uploads }), [uploads]);
|
|
211
|
+
return (_jsx(InFlightUploadsContext.Provider, { value: value, children: children }));
|
|
212
|
+
}
|
|
213
|
+
// =============================================================================
|
|
214
|
+
// Read APIs
|
|
215
|
+
// =============================================================================
|
|
216
|
+
export function useInFlightUploadsState() {
|
|
217
|
+
const ctx = useContext(InFlightUploadsContext);
|
|
218
|
+
if (ctx === undefined) {
|
|
219
|
+
throw new Error('useInFlightUploadsState must be used within InFlightUploadsProvider');
|
|
220
|
+
}
|
|
221
|
+
return ctx.uploads;
|
|
222
|
+
}
|
|
223
|
+
export function useInFlightUpload(id) {
|
|
224
|
+
const ctx = useContext(InFlightUploadsContext);
|
|
225
|
+
if (ctx === undefined) {
|
|
226
|
+
throw new Error('useInFlightUpload must be used within InFlightUploadsProvider');
|
|
227
|
+
}
|
|
228
|
+
if (!id)
|
|
229
|
+
return undefined;
|
|
230
|
+
return ctx.uploads.get(id);
|
|
231
|
+
}
|
|
232
|
+
// =============================================================================
|
|
233
|
+
// Tray helpers
|
|
234
|
+
// =============================================================================
|
|
235
|
+
/**
|
|
236
|
+
* `true` for `completed` / `failed` / `rejected`. Used by tray UI to decide
|
|
237
|
+
* whether a Clear action is available for an item.
|
|
238
|
+
*/
|
|
239
|
+
export function isTerminalUpload(item) {
|
|
240
|
+
return (item.status === 'completed' ||
|
|
241
|
+
item.status === 'failed' ||
|
|
242
|
+
item.status === 'rejected');
|
|
243
|
+
}
|
|
244
|
+
/**
|
|
245
|
+
* Tray-visible items. Includes every pending/processing upload, plus terminal
|
|
246
|
+
* uploads that have NOT been cleared from the tray. Sorted newest-first by
|
|
247
|
+
* createdAt.
|
|
248
|
+
*
|
|
249
|
+
* Renamed per F22: was `useUploadActivityTrayItems` in ttt-prod; the public
|
|
250
|
+
* selector name in upload-ui is `useUploadActivityState`. The mutation hook
|
|
251
|
+
* is `useClearUploadActivity` (in a separate file).
|
|
252
|
+
*/
|
|
253
|
+
export function useUploadActivityState() {
|
|
254
|
+
const uploads = useInFlightUploadsState();
|
|
255
|
+
return useMemo(() => {
|
|
256
|
+
return Array.from(uploads.values())
|
|
257
|
+
.filter((item) => {
|
|
258
|
+
if (!isTerminalUpload(item))
|
|
259
|
+
return true;
|
|
260
|
+
return item.uploadTrayClearedAt === undefined;
|
|
261
|
+
})
|
|
262
|
+
.sort((a, b) => b.createdAt - a.createdAt);
|
|
263
|
+
}, [uploads]);
|
|
264
|
+
}
|
|
265
|
+
// =============================================================================
|
|
266
|
+
// Default Firestore subscription implementation
|
|
267
|
+
// =============================================================================
|
|
268
|
+
/**
|
|
269
|
+
* Default Firestore subscription. Imported lazily so tests can inject a fake
|
|
270
|
+
* `subscribe` and skip the firebase module load entirely.
|
|
271
|
+
*
|
|
272
|
+
* The dynamic import keeps the bundle clean: consumers who pass a custom
|
|
273
|
+
* `subscribe` (e.g. for tests or alternative backends) never pay for the
|
|
274
|
+
* firebase code path.
|
|
275
|
+
*/
|
|
276
|
+
const defaultFirestoreSubscribe = (args) => {
|
|
277
|
+
// Lazy require to keep firebase out of the synchronous module graph.
|
|
278
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports, @typescript-eslint/no-var-requires
|
|
279
|
+
const firestore = require('firebase/firestore');
|
|
280
|
+
const { collection, onSnapshot, query, where } = firestore;
|
|
281
|
+
const q = query(
|
|
282
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
283
|
+
collection(args.db, args.collectionPath), where('userId', '==', args.userId), where('createdAt', '>=', args.windowStartMs));
|
|
284
|
+
return onSnapshot(q, args.onSnapshot, args.onError);
|
|
285
|
+
};
|
|
286
|
+
//# sourceMappingURL=in-flight-uploads-provider.js.map
|
|
@@ -0,0 +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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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { LocalUploadGuardProvider, useLocalUploadGuard, useLocalUploadGuardContext, } from './local-upload-guard-provider.js';
|
|
2
|
+
export { useGuardedUpload, type GuardedUploadArgs } from './use-guarded-upload.js';
|
|
3
|
+
export { DeferredUploadFormShell, type DeferredUploadFormShellHandle, type DeferredUploadFormShellProps, } from './deferred-upload-form-shell.js';
|
|
4
|
+
export { InFlightUploadsProvider, useInFlightUploadsState, useInFlightUpload, useUploadActivityState, isTerminalUpload, 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';
|
|
5
|
+
export { useUploadProcessing, type UploadProcessingState, type UseUploadProcessingOptions, } from './use-upload-processing.js';
|
|
6
|
+
export { useClearUploadActivity, type ClearUploadActivityOptions, } from './use-clear-upload-activity.js';
|
|
7
|
+
export { useGuardedNavigation, } from './use-guarded-navigation.js';
|
|
8
|
+
export { GuardedLink, type GuardedLinkProps, type GuardedLinkRenderArgs, } from './guarded-link.js';
|
|
9
|
+
export { type LocalUploadGuardProviderProps, } from './local-upload-guard-provider.js';
|
|
10
|
+
export { UploadActivityTray, type UploadActivityTrayProps, type ViewAllLinkRenderArgs, } from './upload-activity-tray.js';
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACnF,OAAO,EACL,uBAAuB,EACvB,KAAK,6BAA6B,EAClC,KAAK,4BAA4B,GAClC,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,iBAAiB,EACjB,sBAAsB,EACtB,gBAAgB,EAChB,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,iCAAiC,CAAC;AACzC,OAAO,EACL,mBAAmB,EACnB,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,GAChC,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,sBAAsB,EACtB,KAAK,0BAA0B,GAChC,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,WAAW,EACX,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,GAC3B,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EACL,KAAK,6BAA6B,GACnC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EACL,kBAAkB,EAClB,KAAK,uBAAuB,EAC5B,KAAK,qBAAqB,GAC3B,MAAM,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { LocalUploadGuardProvider, useLocalUploadGuard, useLocalUploadGuardContext, } from './local-upload-guard-provider.js';
|
|
2
|
+
export { useGuardedUpload } from './use-guarded-upload.js';
|
|
3
|
+
export { DeferredUploadFormShell, } from './deferred-upload-form-shell.js';
|
|
4
|
+
export { InFlightUploadsProvider, useInFlightUploadsState, useInFlightUpload, useUploadActivityState, isTerminalUpload, } from './in-flight-uploads-provider.js';
|
|
5
|
+
export { useUploadProcessing, } from './use-upload-processing.js';
|
|
6
|
+
export { useClearUploadActivity, } from './use-clear-upload-activity.js';
|
|
7
|
+
export { useGuardedNavigation, } from './use-guarded-navigation.js';
|
|
8
|
+
export { GuardedLink, } from './guarded-link.js';
|
|
9
|
+
export { UploadActivityTray, } from './upload-activity-tray.js';
|
|
10
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/react/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,0BAA0B,GAC3B,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,gBAAgB,EAA0B,MAAM,yBAAyB,CAAC;AACnF,OAAO,EACL,uBAAuB,GAGxB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,uBAAuB,EACvB,uBAAuB,EACvB,iBAAiB,EACjB,sBAAsB,EACtB,gBAAgB,GAejB,MAAM,iCAAiC,CAAC;AACzC,OAAO,EACL,mBAAmB,GAGpB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EACL,sBAAsB,GAEvB,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,oBAAoB,GACrB,MAAM,6BAA6B,CAAC;AACrC,OAAO,EACL,WAAW,GAGZ,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EACL,kBAAkB,GAGnB,MAAM,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local upload navigation guard provider.
|
|
3
|
+
*
|
|
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.
|
|
7
|
+
*
|
|
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.
|
|
14
|
+
*
|
|
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`.
|
|
23
|
+
*/
|
|
24
|
+
import { type ReactNode } from 'react';
|
|
25
|
+
interface LocalUploadGuardContextValue {
|
|
26
|
+
activeUploadCount: number;
|
|
27
|
+
registerUpload: (id: string) => void;
|
|
28
|
+
unregisterUpload: (id: string) => void;
|
|
29
|
+
/** Returns true iff at least one upload is currently registered. Pure read. */
|
|
30
|
+
shouldConfirmNavigation: () => boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Shows a confirm() dialog with the configured message and returns the user's
|
|
33
|
+
* choice. Safe to call when no upload is active — returns true without prompting.
|
|
34
|
+
*/
|
|
35
|
+
confirmNavigation: () => boolean;
|
|
36
|
+
}
|
|
37
|
+
export interface LocalUploadGuardProviderProps {
|
|
38
|
+
/**
|
|
39
|
+
* Message shown by the browser when the user attempts to close the tab or
|
|
40
|
+
* reload during an active upload. Modern browsers may ignore the custom
|
|
41
|
+
* string and show a generic dialog.
|
|
42
|
+
*/
|
|
43
|
+
beforeUnloadMessage?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Message shown by `confirmNavigation()` when an upload is in progress and
|
|
46
|
+
* the user attempts to navigate away within the app. Defaults to a
|
|
47
|
+
* reasonable English string.
|
|
48
|
+
*/
|
|
49
|
+
navigationConfirmMessage?: string;
|
|
50
|
+
children: ReactNode;
|
|
51
|
+
}
|
|
52
|
+
export declare function LocalUploadGuardProvider(props: LocalUploadGuardProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
53
|
+
/**
|
|
54
|
+
* Low-level accessor for the local-upload guard context. Throws if used
|
|
55
|
+
* outside a `LocalUploadGuardProvider`. Most consumers should use
|
|
56
|
+
* `useLocalUploadGuard` (the public re-export below) instead.
|
|
57
|
+
*/
|
|
58
|
+
export declare function useLocalUploadGuardContext(): LocalUploadGuardContextValue;
|
|
59
|
+
/**
|
|
60
|
+
* Public hook for the local upload navigation guard.
|
|
61
|
+
*
|
|
62
|
+
* Returns `{ activeUploadCount, registerUpload, unregisterUpload }`.
|
|
63
|
+
*
|
|
64
|
+
* Typical usage in an upload flow:
|
|
65
|
+
*
|
|
66
|
+
* const { registerUpload, unregisterUpload } = useLocalUploadGuard();
|
|
67
|
+
* useEffect(() => {
|
|
68
|
+
* if (phase === 'preparing' || phase === 'uploading') {
|
|
69
|
+
* registerUpload(uploadId);
|
|
70
|
+
* return () => unregisterUpload(uploadId);
|
|
71
|
+
* }
|
|
72
|
+
* }, [phase, uploadId, registerUpload, unregisterUpload]);
|
|
73
|
+
*/
|
|
74
|
+
export declare function useLocalUploadGuard(): LocalUploadGuardContextValue;
|
|
75
|
+
export {};
|
|
76
|
+
//# sourceMappingURL=local-upload-guard-provider.d.ts.map
|
|
@@ -0,0 +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,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;;;OAGG;IACH,iBAAiB,EAAE,MAAM,OAAO,CAAC;CAClC;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,2CAmE5E;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,IAAI,4BAA4B,CAQzE;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mBAAmB,IAAI,4BAA4B,CAElE"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
/**
|
|
4
|
+
* Local upload navigation guard provider.
|
|
5
|
+
*
|
|
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.
|
|
9
|
+
*
|
|
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.
|
|
16
|
+
*
|
|
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`.
|
|
25
|
+
*/
|
|
26
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
27
|
+
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
|
+
const DEFAULT_NAVIGATION_CONFIRM_MESSAGE = 'An upload is currently in progress. If you leave this page, the upload will be canceled. Continue?';
|
|
30
|
+
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);
|
|
34
|
+
const registerUpload = useCallback((id) => {
|
|
35
|
+
if (activeIdsRef.current.has(id))
|
|
36
|
+
return;
|
|
37
|
+
activeIdsRef.current.add(id);
|
|
38
|
+
setActiveUploadCount(activeIdsRef.current.size);
|
|
39
|
+
}, []);
|
|
40
|
+
const unregisterUpload = useCallback((id) => {
|
|
41
|
+
if (!activeIdsRef.current.has(id))
|
|
42
|
+
return;
|
|
43
|
+
activeIdsRef.current.delete(id);
|
|
44
|
+
setActiveUploadCount(activeIdsRef.current.size);
|
|
45
|
+
}, []);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
if (activeUploadCount <= 0)
|
|
48
|
+
return;
|
|
49
|
+
const handleBeforeUnload = (event) => {
|
|
50
|
+
event.preventDefault();
|
|
51
|
+
// Modern browsers may ignore the custom string and show their own
|
|
52
|
+
// generic confirmation. The key behavior is that navigation is
|
|
53
|
+
// blocked/warned, not the exact text.
|
|
54
|
+
event.returnValue = beforeUnloadMessage;
|
|
55
|
+
return event.returnValue;
|
|
56
|
+
};
|
|
57
|
+
window.addEventListener('beforeunload', handleBeforeUnload);
|
|
58
|
+
return () => {
|
|
59
|
+
window.removeEventListener('beforeunload', handleBeforeUnload);
|
|
60
|
+
};
|
|
61
|
+
}, [activeUploadCount, beforeUnloadMessage]);
|
|
62
|
+
const shouldConfirmNavigation = useCallback(() => activeIdsRef.current.size > 0, []);
|
|
63
|
+
const confirmNavigation = useCallback(() => {
|
|
64
|
+
if (activeIdsRef.current.size === 0)
|
|
65
|
+
return true;
|
|
66
|
+
// SSR / non-browser safety: if `window` isn't available, default to allow.
|
|
67
|
+
if (typeof window === 'undefined')
|
|
68
|
+
return true;
|
|
69
|
+
return window.confirm(navigationConfirmMessage);
|
|
70
|
+
}, [navigationConfirmMessage]);
|
|
71
|
+
const value = useMemo(() => ({
|
|
72
|
+
activeUploadCount,
|
|
73
|
+
registerUpload,
|
|
74
|
+
unregisterUpload,
|
|
75
|
+
shouldConfirmNavigation,
|
|
76
|
+
confirmNavigation,
|
|
77
|
+
}), [activeUploadCount, registerUpload, unregisterUpload, shouldConfirmNavigation, confirmNavigation]);
|
|
78
|
+
return (_jsx(LocalUploadGuardContext.Provider, { value: value, children: children }));
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Low-level accessor for the local-upload guard context. Throws if used
|
|
82
|
+
* outside a `LocalUploadGuardProvider`. Most consumers should use
|
|
83
|
+
* `useLocalUploadGuard` (the public re-export below) instead.
|
|
84
|
+
*/
|
|
85
|
+
export function useLocalUploadGuardContext() {
|
|
86
|
+
const ctx = useContext(LocalUploadGuardContext);
|
|
87
|
+
if (ctx === undefined) {
|
|
88
|
+
throw new Error('useLocalUploadGuard must be used within LocalUploadGuardProvider');
|
|
89
|
+
}
|
|
90
|
+
return ctx;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Public hook for the local upload navigation guard.
|
|
94
|
+
*
|
|
95
|
+
* Returns `{ activeUploadCount, registerUpload, unregisterUpload }`.
|
|
96
|
+
*
|
|
97
|
+
* Typical usage in an upload flow:
|
|
98
|
+
*
|
|
99
|
+
* const { registerUpload, unregisterUpload } = useLocalUploadGuard();
|
|
100
|
+
* useEffect(() => {
|
|
101
|
+
* if (phase === 'preparing' || phase === 'uploading') {
|
|
102
|
+
* registerUpload(uploadId);
|
|
103
|
+
* return () => unregisterUpload(uploadId);
|
|
104
|
+
* }
|
|
105
|
+
* }, [phase, uploadId, registerUpload, unregisterUpload]);
|
|
106
|
+
*/
|
|
107
|
+
export function useLocalUploadGuard() {
|
|
108
|
+
return useLocalUploadGuardContext();
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=local-upload-guard-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"local-upload-guard-provider.js","sourceRoot":"","sources":["../../src/react/local-upload-guard-provider.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,aAAa,EACb,WAAW,EACX,UAAU,EACV,SAAS,EACT,OAAO,EACP,MAAM,EACN,QAAQ,GAET,MAAM,OAAO,CAAC;AAef,MAAM,uBAAuB,GAAG,aAAa,CAA2C,SAAS,CAAC,CAAC;AAkBnG,MAAM,6BAA6B,GACjC,kFAAkF,CAAC;AACrF,MAAM,kCAAkC,GACtC,oGAAoG,CAAC;AAEvG,MAAM,UAAU,wBAAwB,CAAC,KAAoC;IAC3E,MAAM,EACJ,QAAQ,EACR,mBAAmB,GAAG,6BAA6B,EACnD,wBAAwB,GAAG,kCAAkC,GAC9D,GAAG,KAAK,CAAC;IACV,MAAM,YAAY,GAAG,MAAM,CAAc,IAAI,GAAG,EAAE,CAAC,CAAC;IACpD,MAAM,CAAC,iBAAiB,EAAE,oBAAoB,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAE9D,MAAM,cAAc,GAAG,WAAW,CAAC,CAAC,EAAU,EAAE,EAAE;QAChD,IAAI,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO;QACzC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7B,oBAAoB,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,gBAAgB,GAAG,WAAW,CAAC,CAAC,EAAU,EAAE,EAAE;QAClD,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;YAAE,OAAO;QAC1C,YAAY,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAChC,oBAAoB,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,iBAAiB,IAAI,CAAC;YAAE,OAAO;QAEnC,MAAM,kBAAkB,GAAG,CAAC,KAAwB,EAAE,EAAE;YACtD,KAAK,CAAC,cAAc,EAAE,CAAC;YACvB,kEAAkE;YAClE,+DAA+D;YAC/D,sCAAsC;YACtC,KAAK,CAAC,WAAW,GAAG,mBAAmB,CAAC;YACxC,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,iBAAiB,EAAE,mBAAmB,CAAC,CAAC,CAAC;IAE7C,MAAM,uBAAuB,GAAG,WAAW,CACzC,GAAG,EAAE,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,EACnC,EAAE,CACH,CAAC;IAEF,MAAM,iBAAiB,GAAG,WAAW,CAAC,GAAY,EAAE;QAClD,IAAI,YAAY,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QACjD,2EAA2E;QAC3E,IAAI,OAAO,MAAM,KAAK,WAAW;YAAE,OAAO,IAAI,CAAC;QAC/C,OAAO,MAAM,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAC;IAClD,CAAC,EAAE,CAAC,wBAAwB,CAAC,CAAC,CAAC;IAE/B,MAAM,KAAK,GAAG,OAAO,CACnB,GAAG,EAAE,CAAC,CAAC;QACL,iBAAiB;QACjB,cAAc;QACd,gBAAgB;QAChB,uBAAuB;QACvB,iBAAiB;KAClB,CAAC,EACF,CAAC,iBAAiB,EAAE,cAAc,EAAE,gBAAgB,EAAE,uBAAuB,EAAE,iBAAiB,CAAC,CAClG,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;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,mBAAmB;IACjC,OAAO,0BAA0B,EAAE,CAAC;AACtC,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global upload activity tray.
|
|
3
|
+
*
|
|
4
|
+
* Renders a fixed bottom-left floating action button when there is any
|
|
5
|
+
* active or non-cleared terminal upload. Opens a popover listing each
|
|
6
|
+
* item with backend status. Terminal items (completed / failed / rejected)
|
|
7
|
+
* get a Clear (×) button that calls the consumer-supplied clear mutation.
|
|
8
|
+
*
|
|
9
|
+
* Framework-agnostic via render props:
|
|
10
|
+
* - `renderViewAllLink` lets the consumer plug in their app's Link
|
|
11
|
+
* component (Next.js Link, React Router Link, anchor, etc.) routed at
|
|
12
|
+
* their canonical "view all uploads" destination.
|
|
13
|
+
* - `renderFooter` lets the consumer fully replace the footer slot.
|
|
14
|
+
* - Both omitted — footer is not rendered.
|
|
15
|
+
*/
|
|
16
|
+
import { type ReactNode } from 'react';
|
|
17
|
+
export interface ViewAllLinkRenderArgs {
|
|
18
|
+
/** The href the consumer's link should navigate to (passed through verbatim). */
|
|
19
|
+
href: string;
|
|
20
|
+
/** Children for the link element (i.e. the label text + arrow). */
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
export interface UploadActivityTrayProps {
|
|
24
|
+
/**
|
|
25
|
+
* Mutation hook result from `useClearUploadActivity({ clearFn })`. The tray
|
|
26
|
+
* uses `.mutate(item.id)`, `.isPending`, and `.variables`. The consumer
|
|
27
|
+
* owns the underlying network call.
|
|
28
|
+
*/
|
|
29
|
+
clearMutation: {
|
|
30
|
+
mutate: (pendingMediaId: string) => void;
|
|
31
|
+
isPending: boolean;
|
|
32
|
+
variables: string | undefined;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Optional view-all destination href. When omitted, no footer is rendered
|
|
36
|
+
* unless `renderFooter` is also supplied.
|
|
37
|
+
*/
|
|
38
|
+
viewAllHref?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Render-prop for the view-all link. Receives the href and label children
|
|
41
|
+
* the consumer should attach. Omit to drop the link.
|
|
42
|
+
*/
|
|
43
|
+
renderViewAllLink?: (args: ViewAllLinkRenderArgs) => ReactNode;
|
|
44
|
+
/**
|
|
45
|
+
* Fully custom footer slot. Replaces the default footer entirely. If both
|
|
46
|
+
* `renderFooter` and `viewAllHref + renderViewAllLink` are supplied,
|
|
47
|
+
* `renderFooter` wins.
|
|
48
|
+
*/
|
|
49
|
+
renderFooter?: () => ReactNode;
|
|
50
|
+
/** Visible label for the view-all link. Default: "View all uploads →". */
|
|
51
|
+
viewAllLabel?: ReactNode;
|
|
52
|
+
}
|
|
53
|
+
export declare function UploadActivityTray<TFileOrigin extends string = string>(props: UploadActivityTrayProps): import("react/jsx-runtime").JSX.Element | null;
|
|
54
|
+
//# sourceMappingURL=upload-activity-tray.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"upload-activity-tray.d.ts","sourceRoot":"","sources":["../../src/react/upload-activity-tray.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAW,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAmGhD,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,aAAa,EAAE;QACb,MAAM,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,IAAI,CAAC;QACzC,SAAS,EAAE,OAAO,CAAC;QACnB,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;KAC/B,CAAC;IACF;;;OAGG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,CAAC,IAAI,EAAE,qBAAqB,KAAK,SAAS,CAAC;IAC/D;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,SAAS,CAAC;IAC/B,0EAA0E;IAC1E,YAAY,CAAC,EAAE,SAAS,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAAC,WAAW,SAAS,MAAM,GAAG,MAAM,EACpE,KAAK,EAAE,uBAAuB,kDAoG/B"}
|