@ttt-productions/upload-ui 0.7.8 → 0.7.9
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/react/tray/index.d.ts +0 -1
- package/dist/react/tray/index.d.ts.map +1 -1
- package/dist/react/tray/index.js +0 -1
- package/dist/react/tray/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/react/upload-activity-tray.d.ts +0 -76
- package/dist/react/upload-activity-tray.d.ts.map +0 -1
- package/dist/react/upload-activity-tray.js +0 -101
- package/dist/react/upload-activity-tray.js.map +0 -1
|
@@ -1,5 +1,4 @@
|
|
|
1
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';
|
|
2
|
-
export { UploadActivityTray, type UploadActivityTrayProps, type UploadTrayLabelers, type ViewAllLinkRenderArgs, } from '../upload-activity-tray.js';
|
|
3
2
|
export { useClearUploadActivity, type ClearUploadActivityOptions, } from '../use-clear-upload-activity.js';
|
|
4
3
|
export { useMarkUploadActivitySeen, type MarkUploadActivitySeenOptions, } from '../use-mark-upload-activity-seen.js';
|
|
5
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,
|
|
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"}
|
package/dist/react/tray/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export { InFlightUploadsProvider, useInFlightUploadsState, useInFlightUpload, useUploadActivityState, useUploadsSourceState, isTerminalUpload, } from '../in-flight-uploads-provider.js';
|
|
2
|
-
export { UploadActivityTray, } from '../upload-activity-tray.js';
|
|
3
2
|
export { useClearUploadActivity, } from '../use-clear-upload-activity.js';
|
|
4
3
|
export { useMarkUploadActivitySeen, } from '../use-mark-upload-activity-seen.js';
|
|
5
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,
|
|
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"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ttt-productions/upload-ui",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
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,9 +40,9 @@
|
|
|
40
40
|
"prepublishOnly": "npm run clean && npm run build"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@ttt-productions/file-input": "^0.11.
|
|
43
|
+
"@ttt-productions/file-input": "^0.11.10",
|
|
44
44
|
"@ttt-productions/media-schemas": "^0.14.5",
|
|
45
|
-
"@ttt-productions/ui-core": "^0.11.
|
|
45
|
+
"@ttt-productions/ui-core": "^0.11.15",
|
|
46
46
|
"@ttt-productions/upload-core": "^0.7.3"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
@@ -1,76 +0,0 @@
|
|
|
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
|
-
/**
|
|
18
|
-
* Consumer-supplied formatters used by `UploadActivityTray` to render row
|
|
19
|
-
* labels. The tray is intentionally domain-agnostic — consumers map their
|
|
20
|
-
* own `FileOrigin` union to user-facing strings and choose their preferred
|
|
21
|
-
* timestamp / size / content-type formatting.
|
|
22
|
-
*/
|
|
23
|
-
export interface UploadTrayLabelers<TFileOrigin extends string = string> {
|
|
24
|
-
/** User-facing label for a file origin (e.g. 'support-ticket' -> 'Support reply'). */
|
|
25
|
-
originLabel: (fileOrigin: TFileOrigin) => string;
|
|
26
|
-
/** Human file size string, e.g. 1572864 -> '1.5 MB'. Return null when unknown. */
|
|
27
|
-
fileSizeLabel: (bytes: number | undefined) => string | null;
|
|
28
|
-
/** Friendly content-type label, e.g. 'image/png' -> 'Image'. Return null when unknown. */
|
|
29
|
-
fileTypeLabel: (contentType: string | undefined) => string | null;
|
|
30
|
-
/** Timestamp label, e.g. '3m ago' / 'Yesterday 3:42 PM'. */
|
|
31
|
-
timestampLabel: (createdAtMs: number, nowMs: number) => string;
|
|
32
|
-
}
|
|
33
|
-
export interface ViewAllLinkRenderArgs {
|
|
34
|
-
/** The href the consumer's link should navigate to (passed through verbatim). */
|
|
35
|
-
href: string;
|
|
36
|
-
/** Children for the link element (i.e. the label text + arrow). */
|
|
37
|
-
children: ReactNode;
|
|
38
|
-
}
|
|
39
|
-
export interface UploadActivityTrayProps<TFileOrigin extends string = string> {
|
|
40
|
-
/**
|
|
41
|
-
* Mutation hook result from `useClearUploadActivity({ clearFn })`. The tray
|
|
42
|
-
* uses `.mutate(item.id)`, `.isPending`, and `.variables`. The consumer
|
|
43
|
-
* owns the underlying network call.
|
|
44
|
-
*/
|
|
45
|
-
clearMutation: {
|
|
46
|
-
mutate: (pendingMediaId: string) => void;
|
|
47
|
-
isPending: boolean;
|
|
48
|
-
variables: string | undefined;
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* Consumer-supplied formatters for row labels. Required — the tray has
|
|
52
|
-
* no built-in opinion about how to render origin / file type / size /
|
|
53
|
-
* timestamp strings.
|
|
54
|
-
*/
|
|
55
|
-
labelers: UploadTrayLabelers<TFileOrigin>;
|
|
56
|
-
/**
|
|
57
|
-
* Optional view-all destination href. When omitted, no footer is rendered
|
|
58
|
-
* unless `renderFooter` is also supplied.
|
|
59
|
-
*/
|
|
60
|
-
viewAllHref?: string;
|
|
61
|
-
/**
|
|
62
|
-
* Render-prop for the view-all link. Receives the href and label children
|
|
63
|
-
* the consumer should attach. Omit to drop the link.
|
|
64
|
-
*/
|
|
65
|
-
renderViewAllLink?: (args: ViewAllLinkRenderArgs) => ReactNode;
|
|
66
|
-
/**
|
|
67
|
-
* Fully custom footer slot. Replaces the default footer entirely. If both
|
|
68
|
-
* `renderFooter` and `viewAllHref + renderViewAllLink` are supplied,
|
|
69
|
-
* `renderFooter` wins.
|
|
70
|
-
*/
|
|
71
|
-
renderFooter?: () => ReactNode;
|
|
72
|
-
/** Visible label for the view-all link. Default: "View all uploads →". */
|
|
73
|
-
viewAllLabel?: ReactNode;
|
|
74
|
-
}
|
|
75
|
-
export declare function UploadActivityTray<TFileOrigin extends string = string>(props: UploadActivityTrayProps<TFileOrigin>): import("react").JSX.Element;
|
|
76
|
-
//# sourceMappingURL=upload-activity-tray.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
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;AAiBhD;;;;;GAKG;AACH,MAAM,WAAW,kBAAkB,CAAC,WAAW,SAAS,MAAM,GAAG,MAAM;IACrE,sFAAsF;IACtF,WAAW,EAAE,CAAC,UAAU,EAAE,WAAW,KAAK,MAAM,CAAC;IACjD,kFAAkF;IAClF,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,GAAG,IAAI,CAAC;IAC5D,0FAA0F;IAC1F,aAAa,EAAE,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,GAAG,IAAI,CAAC;IAClE,4DAA4D;IAC5D,cAAc,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CAChE;AA+GD,MAAM,WAAW,qBAAqB;IACpC,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,mEAAmE;IACnE,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,MAAM,WAAW,uBAAuB,CAAC,WAAW,SAAS,MAAM,GAAG,MAAM;IAC1E;;;;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;;;;OAIG;IACH,QAAQ,EAAE,kBAAkB,CAAC,WAAW,CAAC,CAAC;IAC1C;;;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,4EAA4E;IAC5E,YAAY,CAAC,EAAE,SAAS,CAAC;CAC1B;AAED,wBAAgB,kBAAkB,CAAC,WAAW,SAAS,MAAM,GAAG,MAAM,EACpE,KAAK,EAAE,uBAAuB,CAAC,WAAW,CAAC,+BA8G5C"}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
/**
|
|
4
|
-
* Global upload activity tray.
|
|
5
|
-
*
|
|
6
|
-
* Renders a fixed bottom-left floating action button when there is any
|
|
7
|
-
* active or non-cleared terminal upload. Opens a popover listing each
|
|
8
|
-
* item with backend status. Terminal items (completed / failed / rejected)
|
|
9
|
-
* get a Clear (×) button that calls the consumer-supplied clear mutation.
|
|
10
|
-
*
|
|
11
|
-
* Framework-agnostic via render props:
|
|
12
|
-
* - `renderViewAllLink` lets the consumer plug in their app's Link
|
|
13
|
-
* component (Next.js Link, React Router Link, anchor, etc.) routed at
|
|
14
|
-
* their canonical "view all uploads" destination.
|
|
15
|
-
* - `renderFooter` lets the consumer fully replace the footer slot.
|
|
16
|
-
* - Both omitted — footer is not rendered.
|
|
17
|
-
*/
|
|
18
|
-
import { useMemo } from 'react';
|
|
19
|
-
import { Cloud, Loader2, CheckCircle2, AlertCircle, XCircle, X } from 'lucide-react';
|
|
20
|
-
import { Button, Popover, PopoverTrigger, PopoverContent, ScrollArea, Separator, } from '@ttt-productions/ui-core/react';
|
|
21
|
-
import { cn } from '@ttt-productions/ui-core';
|
|
22
|
-
import { isTerminalUpload, useUploadActivityState, } from './in-flight-uploads-provider.js';
|
|
23
|
-
function getRowMeta(item, nowMs, labelers) {
|
|
24
|
-
let statusLabel;
|
|
25
|
-
let icon;
|
|
26
|
-
let iconClassName;
|
|
27
|
-
let errorDetail;
|
|
28
|
-
switch (item.status) {
|
|
29
|
-
case 'pending':
|
|
30
|
-
statusLabel = 'Waiting to process…';
|
|
31
|
-
icon = _jsx(Loader2, { className: "icon-sm animate-spin" });
|
|
32
|
-
iconClassName = 'text-muted-foreground';
|
|
33
|
-
break;
|
|
34
|
-
case 'processing':
|
|
35
|
-
statusLabel = 'Processing…';
|
|
36
|
-
icon = _jsx(Loader2, { className: "icon-sm animate-spin" });
|
|
37
|
-
iconClassName = 'text-primary';
|
|
38
|
-
break;
|
|
39
|
-
case 'completed':
|
|
40
|
-
statusLabel = 'Completed';
|
|
41
|
-
icon = _jsx(CheckCircle2, { className: "icon-sm" });
|
|
42
|
-
iconClassName = 'text-green-600 dark:text-green-500';
|
|
43
|
-
break;
|
|
44
|
-
case 'failed':
|
|
45
|
-
statusLabel = 'Failed';
|
|
46
|
-
errorDetail = item.errorMessage;
|
|
47
|
-
icon = _jsx(AlertCircle, { className: "icon-sm" });
|
|
48
|
-
iconClassName = 'text-destructive';
|
|
49
|
-
break;
|
|
50
|
-
case 'rejected':
|
|
51
|
-
statusLabel = 'Rejected';
|
|
52
|
-
errorDetail = item.errorMessage;
|
|
53
|
-
icon = _jsx(XCircle, { className: "icon-sm" });
|
|
54
|
-
iconClassName = 'text-destructive';
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
57
|
-
return {
|
|
58
|
-
statusLabel,
|
|
59
|
-
errorDetail,
|
|
60
|
-
originLabel: labelers.originLabel(item.fileOrigin),
|
|
61
|
-
fileTypeLabel: labelers.fileTypeLabel(item.originalContentType),
|
|
62
|
-
fileSizeLabel: labelers.fileSizeLabel(item.originalSize),
|
|
63
|
-
timestampLabel: labelers.timestampLabel(item.createdAt, nowMs),
|
|
64
|
-
icon,
|
|
65
|
-
iconClassName,
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
function UploadActivityTrayRow({ item, nowMs, onClear, isClearPending, labelers, }) {
|
|
69
|
-
const meta = getRowMeta(item, nowMs, labelers);
|
|
70
|
-
const subline = [meta.originLabel, meta.fileTypeLabel, meta.fileSizeLabel]
|
|
71
|
-
.filter(Boolean)
|
|
72
|
-
.join(' · ');
|
|
73
|
-
return (_jsxs("div", { className: "flex items-start gap-3 p-3", children: [_jsx("div", { className: cn('mt-0.5 shrink-0', meta.iconClassName), children: meta.icon }), _jsxs("div", { className: "flex-1 min-w-0", children: [_jsx("p", { className: "text-sm font-medium truncate", children: meta.statusLabel }), _jsx("p", { className: "text-xs text-muted-foreground truncate", children: subline }), meta.errorDetail && (_jsx("p", { className: "text-xs text-muted-foreground mt-1 line-clamp-2", children: meta.errorDetail })), _jsx("p", { className: "text-xs text-muted-foreground mt-1", children: meta.timestampLabel })] }), onClear && (_jsx(Button, { size: "icon", variant: "ghost", className: "h-7 w-7 shrink-0", onClick: onClear, disabled: isClearPending, "aria-label": `Clear ${meta.statusLabel.toLowerCase()} upload`, children: _jsx(X, { className: "icon-sm" }) }))] }));
|
|
74
|
-
}
|
|
75
|
-
export function UploadActivityTray(props) {
|
|
76
|
-
const { clearMutation, labelers, viewAllHref, renderViewAllLink, renderFooter, viewAllLabel = 'View all uploads →', } = props;
|
|
77
|
-
const items = useUploadActivityState();
|
|
78
|
-
const nowMs = Date.now();
|
|
79
|
-
const activeCount = useMemo(() => items.filter((item) => item.status === 'pending' || item.status === 'processing').length, [items]);
|
|
80
|
-
const footerContent = (() => {
|
|
81
|
-
if (renderFooter)
|
|
82
|
-
return renderFooter();
|
|
83
|
-
if (viewAllHref && renderViewAllLink) {
|
|
84
|
-
return (_jsx("div", { className: "p-3", children: renderViewAllLink({
|
|
85
|
-
href: viewAllHref,
|
|
86
|
-
children: (_jsx("span", { className: "text-sm text-primary hover:underline", children: viewAllLabel })),
|
|
87
|
-
}) }));
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
})();
|
|
91
|
-
return (_jsx("div", { className: "fixed bottom-6 left-6 z-50", children: _jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { size: "icon", variant: items.length === 0 ? 'ghost' : 'inverted', className: "relative h-14 w-14 rounded-full shadow-lg", "aria-label": items.length === 0
|
|
92
|
-
? 'Upload activity (no recent uploads)'
|
|
93
|
-
: activeCount > 0
|
|
94
|
-
? `Upload activity: ${activeCount} in progress`
|
|
95
|
-
: `Upload activity: ${items.length} item${items.length === 1 ? '' : 's'}`, children: [activeCount > 0 ? (_jsx(Loader2, { className: "icon-md animate-spin" })) : (_jsx(Cloud, { className: "icon-md" })), items.length > 0 && (_jsx("span", { className: "absolute -top-1 -right-1 min-w-[1.25rem] h-5 px-1 rounded-full bg-destructive text-destructive-foreground text-xs font-medium flex items-center justify-center", "aria-hidden": "true", children: items.length }))] }) }), _jsxs(PopoverContent, { align: "start", side: "top", className: "app-popover p-0 w-80", children: [_jsxs("div", { className: "flex items-center justify-between p-3", children: [_jsx("h2", { className: "text-sm font-semibold", children: "Upload Activity" }), _jsx("span", { className: "text-xs text-muted-foreground", children: items.length === 0
|
|
96
|
-
? 'No recent uploads'
|
|
97
|
-
: activeCount > 0
|
|
98
|
-
? `${activeCount} in progress`
|
|
99
|
-
: `${items.length} item${items.length === 1 ? '' : 's'}` })] }), _jsx(Separator, {}), items.length === 0 ? (_jsx("div", { className: "p-4 text-center text-sm text-muted-foreground", children: "No recent uploads." })) : (_jsx(ScrollArea, { className: "max-h-80", children: _jsx("ul", { className: "divide-y", children: items.map((item) => (_jsx("li", { children: _jsx(UploadActivityTrayRow, { item: item, nowMs: nowMs, onClear: isTerminalUpload(item) ? () => clearMutation.mutate(item.id) : undefined, isClearPending: clearMutation.isPending && clearMutation.variables === item.id, labelers: labelers }) }, item.id))) }) })), footerContent !== null && (_jsxs(_Fragment, { children: [_jsx(Separator, {}), footerContent] }))] })] }) }));
|
|
100
|
-
}
|
|
101
|
-
//# sourceMappingURL=upload-activity-tray.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"upload-activity-tray.js","sourceRoot":"","sources":["../../src/react/upload-activity-tray.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\n/**\n * Global upload activity tray.\n *\n * Renders a fixed bottom-left floating action button when there is any\n * active or non-cleared terminal upload. Opens a popover listing each\n * item with backend status. Terminal items (completed / failed / rejected)\n * get a Clear (×) button that calls the consumer-supplied clear mutation.\n *\n * Framework-agnostic via render props:\n * - `renderViewAllLink` lets the consumer plug in their app's Link\n * component (Next.js Link, React Router Link, anchor, etc.) routed at\n * their canonical \"view all uploads\" destination.\n * - `renderFooter` lets the consumer fully replace the footer slot.\n * - Both omitted — footer is not rendered.\n */\n\nimport { useMemo, type ReactNode } from 'react';\nimport { Cloud, Loader2, CheckCircle2, AlertCircle, XCircle, X } from 'lucide-react';\nimport {\n Button,\n Popover,\n PopoverTrigger,\n PopoverContent,\n ScrollArea,\n Separator,\n} from '@ttt-productions/ui-core/react';\nimport { cn } from '@ttt-productions/ui-core';\nimport {\n isTerminalUpload,\n useUploadActivityState,\n type InFlightUpload,\n} from './in-flight-uploads-provider.js';\n\n/**\n * Consumer-supplied formatters used by `UploadActivityTray` to render row\n * labels. The tray is intentionally domain-agnostic — consumers map their\n * own `FileOrigin` union to user-facing strings and choose their preferred\n * timestamp / size / content-type formatting.\n */\nexport interface UploadTrayLabelers<TFileOrigin extends string = string> {\n /** User-facing label for a file origin (e.g. 'support-ticket' -> 'Support reply'). */\n originLabel: (fileOrigin: TFileOrigin) => string;\n /** Human file size string, e.g. 1572864 -> '1.5 MB'. Return null when unknown. */\n fileSizeLabel: (bytes: number | undefined) => string | null;\n /** Friendly content-type label, e.g. 'image/png' -> 'Image'. Return null when unknown. */\n fileTypeLabel: (contentType: string | undefined) => string | null;\n /** Timestamp label, e.g. '3m ago' / 'Yesterday 3:42 PM'. */\n timestampLabel: (createdAtMs: number, nowMs: number) => string;\n}\n\ninterface RowMeta {\n statusLabel: string;\n errorDetail?: string;\n originLabel: string;\n fileTypeLabel: string | null;\n fileSizeLabel: string | null;\n timestampLabel: string;\n icon: ReactNode;\n iconClassName: string;\n}\n\nfunction getRowMeta<TFileOrigin extends string>(\n item: InFlightUpload<TFileOrigin>,\n nowMs: number,\n labelers: UploadTrayLabelers<TFileOrigin>,\n): RowMeta {\n let statusLabel: string;\n let icon: ReactNode;\n let iconClassName: string;\n let errorDetail: string | undefined;\n\n switch (item.status) {\n case 'pending':\n statusLabel = 'Waiting to process…';\n icon = <Loader2 className=\"icon-sm animate-spin\" />;\n iconClassName = 'text-muted-foreground';\n break;\n case 'processing':\n statusLabel = 'Processing…';\n icon = <Loader2 className=\"icon-sm animate-spin\" />;\n iconClassName = 'text-primary';\n break;\n case 'completed':\n statusLabel = 'Completed';\n icon = <CheckCircle2 className=\"icon-sm\" />;\n iconClassName = 'text-green-600 dark:text-green-500';\n break;\n case 'failed':\n statusLabel = 'Failed';\n errorDetail = item.errorMessage;\n icon = <AlertCircle className=\"icon-sm\" />;\n iconClassName = 'text-destructive';\n break;\n case 'rejected':\n statusLabel = 'Rejected';\n errorDetail = item.errorMessage;\n icon = <XCircle className=\"icon-sm\" />;\n iconClassName = 'text-destructive';\n break;\n }\n\n return {\n statusLabel,\n errorDetail,\n originLabel: labelers.originLabel(item.fileOrigin),\n fileTypeLabel: labelers.fileTypeLabel(item.originalContentType),\n fileSizeLabel: labelers.fileSizeLabel(item.originalSize),\n timestampLabel: labelers.timestampLabel(item.createdAt, nowMs),\n icon,\n iconClassName,\n };\n}\n\ninterface UploadActivityTrayRowProps<TFileOrigin extends string> {\n item: InFlightUpload<TFileOrigin>;\n nowMs: number;\n onClear: (() => void) | undefined;\n isClearPending: boolean;\n labelers: UploadTrayLabelers<TFileOrigin>;\n}\n\nfunction UploadActivityTrayRow<TFileOrigin extends string>({\n item,\n nowMs,\n onClear,\n isClearPending,\n labelers,\n}: UploadActivityTrayRowProps<TFileOrigin>) {\n const meta = getRowMeta(item, nowMs, labelers);\n const subline = [meta.originLabel, meta.fileTypeLabel, meta.fileSizeLabel]\n .filter(Boolean)\n .join(' · ');\n return (\n <div className=\"flex items-start gap-3 p-3\">\n <div className={cn('mt-0.5 shrink-0', meta.iconClassName)}>{meta.icon}</div>\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium truncate\">{meta.statusLabel}</p>\n <p className=\"text-xs text-muted-foreground truncate\">{subline}</p>\n {meta.errorDetail && (\n <p className=\"text-xs text-muted-foreground mt-1 line-clamp-2\">{meta.errorDetail}</p>\n )}\n <p className=\"text-xs text-muted-foreground mt-1\">{meta.timestampLabel}</p>\n </div>\n {onClear && (\n <Button\n size=\"icon\"\n variant=\"ghost\"\n className=\"h-7 w-7 shrink-0\"\n onClick={onClear}\n disabled={isClearPending}\n aria-label={`Clear ${meta.statusLabel.toLowerCase()} upload`}\n >\n <X className=\"icon-sm\" />\n </Button>\n )}\n </div>\n );\n}\n\nexport interface ViewAllLinkRenderArgs {\n /** The href the consumer's link should navigate to (passed through verbatim). */\n href: string;\n /** Children for the link element (i.e. the label text + arrow). */\n children: ReactNode;\n}\n\nexport interface UploadActivityTrayProps<TFileOrigin extends string = string> {\n /**\n * Mutation hook result from `useClearUploadActivity({ clearFn })`. The tray\n * uses `.mutate(item.id)`, `.isPending`, and `.variables`. The consumer\n * owns the underlying network call.\n */\n clearMutation: {\n mutate: (pendingMediaId: string) => void;\n isPending: boolean;\n variables: string | undefined;\n };\n /**\n * Consumer-supplied formatters for row labels. Required — the tray has\n * no built-in opinion about how to render origin / file type / size /\n * timestamp strings.\n */\n labelers: UploadTrayLabelers<TFileOrigin>;\n /**\n * Optional view-all destination href. When omitted, no footer is rendered\n * unless `renderFooter` is also supplied.\n */\n viewAllHref?: string;\n /**\n * Render-prop for the view-all link. Receives the href and label children\n * the consumer should attach. Omit to drop the link.\n */\n renderViewAllLink?: (args: ViewAllLinkRenderArgs) => ReactNode;\n /**\n * Fully custom footer slot. Replaces the default footer entirely. If both\n * `renderFooter` and `viewAllHref + renderViewAllLink` are supplied,\n * `renderFooter` wins.\n */\n renderFooter?: () => ReactNode;\n /** Visible label for the view-all link. Default: \"View all uploads →\". */\n viewAllLabel?: ReactNode;\n}\n\nexport function UploadActivityTray<TFileOrigin extends string = string>(\n props: UploadActivityTrayProps<TFileOrigin>,\n) {\n const {\n clearMutation,\n labelers,\n viewAllHref,\n renderViewAllLink,\n renderFooter,\n viewAllLabel = 'View all uploads →',\n } = props;\n const items = useUploadActivityState<TFileOrigin>();\n const nowMs = Date.now();\n\n const activeCount = useMemo(\n () =>\n items.filter((item) => item.status === 'pending' || item.status === 'processing').length,\n [items],\n );\n\n const footerContent: ReactNode = (() => {\n if (renderFooter) return renderFooter();\n if (viewAllHref && renderViewAllLink) {\n return (\n <div className=\"p-3\">\n {renderViewAllLink({\n href: viewAllHref,\n children: (\n <span className=\"text-sm text-primary hover:underline\">{viewAllLabel}</span>\n ),\n })}\n </div>\n );\n }\n return null;\n })();\n\n return (\n <div className=\"fixed bottom-6 left-6 z-50\">\n <Popover>\n <PopoverTrigger asChild>\n <Button\n size=\"icon\"\n variant={items.length === 0 ? 'ghost' : 'inverted'}\n className=\"relative h-14 w-14 rounded-full shadow-lg\"\n aria-label={\n items.length === 0\n ? 'Upload activity (no recent uploads)'\n : activeCount > 0\n ? `Upload activity: ${activeCount} in progress`\n : `Upload activity: ${items.length} item${items.length === 1 ? '' : 's'}`\n }\n >\n {activeCount > 0 ? (\n <Loader2 className=\"icon-md animate-spin\" />\n ) : (\n <Cloud className=\"icon-md\" />\n )}\n {items.length > 0 && (\n <span\n className=\"absolute -top-1 -right-1 min-w-[1.25rem] h-5 px-1 rounded-full bg-destructive text-destructive-foreground text-xs font-medium flex items-center justify-center\"\n aria-hidden=\"true\"\n >\n {items.length}\n </span>\n )}\n </Button>\n </PopoverTrigger>\n <PopoverContent align=\"start\" side=\"top\" className=\"app-popover p-0 w-80\">\n <div className=\"flex items-center justify-between p-3\">\n <h2 className=\"text-sm font-semibold\">Upload Activity</h2>\n <span className=\"text-xs text-muted-foreground\">\n {items.length === 0\n ? 'No recent uploads'\n : activeCount > 0\n ? `${activeCount} in progress`\n : `${items.length} item${items.length === 1 ? '' : 's'}`}\n </span>\n </div>\n <Separator />\n {items.length === 0 ? (\n <div className=\"p-4 text-center text-sm text-muted-foreground\">\n No recent uploads.\n </div>\n ) : (\n <ScrollArea className=\"max-h-80\">\n <ul className=\"divide-y\">\n {items.map((item) => (\n <li key={item.id}>\n <UploadActivityTrayRow\n item={item}\n nowMs={nowMs}\n onClear={isTerminalUpload(item) ? () => clearMutation.mutate(item.id) : undefined}\n isClearPending={clearMutation.isPending && clearMutation.variables === item.id}\n labelers={labelers}\n />\n </li>\n ))}\n </ul>\n </ScrollArea>\n )}\n {footerContent !== null && (\n <>\n <Separator />\n {footerContent}\n </>\n )}\n </PopoverContent>\n </Popover>\n </div>\n );\n}\n"]}
|