@ttt-productions/upload-ui 0.7.16 → 0.7.18
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 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -3
- package/dist/index.js.map +1 -1
- package/dist/react/guard/guarded-link.d.ts +11 -0
- package/dist/react/guard/guarded-link.d.ts.map +1 -0
- package/dist/react/guard/guarded-link.js.map +1 -0
- package/dist/react/guard/index.d.ts +3 -3
- package/dist/react/guard/index.d.ts.map +1 -1
- package/dist/react/guard/index.js +3 -3
- package/dist/react/guard/index.js.map +1 -1
- package/dist/react/guard/local-upload-guard-provider.d.ts +31 -0
- package/dist/react/guard/local-upload-guard-provider.d.ts.map +1 -0
- package/dist/react/{local-upload-guard-provider.js → guard/local-upload-guard-provider.js} +3 -60
- package/dist/react/guard/local-upload-guard-provider.js.map +1 -0
- package/dist/react/guard/use-guarded-navigation.d.ts +3 -0
- package/dist/react/guard/use-guarded-navigation.d.ts.map +1 -0
- package/dist/react/guard/use-guarded-navigation.js +12 -0
- package/dist/react/guard/use-guarded-navigation.js.map +1 -0
- package/dist/react/tray/in-flight-uploads-provider.d.ts +141 -0
- package/dist/react/tray/in-flight-uploads-provider.d.ts.map +1 -0
- package/dist/react/{in-flight-uploads-provider.js → tray/in-flight-uploads-provider.js} +16 -136
- package/dist/react/tray/in-flight-uploads-provider.js.map +1 -0
- package/dist/react/tray/index.d.ts +4 -4
- package/dist/react/tray/index.d.ts.map +1 -1
- package/dist/react/tray/index.js +4 -4
- package/dist/react/tray/index.js.map +1 -1
- package/dist/react/tray/use-clear-upload-activity.d.ts +8 -0
- package/dist/react/tray/use-clear-upload-activity.d.ts.map +1 -0
- package/dist/react/tray/use-clear-upload-activity.js +13 -0
- package/dist/react/tray/use-clear-upload-activity.js.map +1 -0
- package/dist/react/{use-mark-upload-activity-seen.d.ts → tray/use-mark-upload-activity-seen.d.ts} +0 -4
- package/dist/react/tray/use-mark-upload-activity-seen.d.ts.map +1 -0
- package/dist/react/{use-mark-upload-activity-seen.js → tray/use-mark-upload-activity-seen.js} +0 -21
- package/dist/react/tray/use-mark-upload-activity-seen.js.map +1 -0
- package/dist/react/{use-upload-processing.d.ts → tray/use-upload-processing.d.ts} +0 -10
- package/dist/react/tray/use-upload-processing.d.ts.map +1 -0
- package/dist/react/{use-upload-processing.js → tray/use-upload-processing.js} +10 -20
- package/dist/react/tray/use-upload-processing.js.map +1 -0
- package/dist/react/upload/deferred-upload-form-shell.d.ts +28 -0
- package/dist/react/upload/deferred-upload-form-shell.d.ts.map +1 -0
- package/dist/react/{deferred-upload-form-shell.js → upload/deferred-upload-form-shell.js} +5 -2
- package/dist/react/upload/deferred-upload-form-shell.js.map +1 -0
- package/dist/react/upload/index.d.ts +2 -2
- package/dist/react/upload/index.d.ts.map +1 -1
- package/dist/react/upload/index.js +2 -2
- package/dist/react/upload/index.js.map +1 -1
- package/dist/react/upload/use-guarded-upload.d.ts +16 -0
- package/dist/react/upload/use-guarded-upload.d.ts.map +1 -0
- package/dist/react/{use-guarded-upload.js → upload/use-guarded-upload.js} +2 -15
- package/dist/react/upload/use-guarded-upload.js.map +1 -0
- package/dist/upload-display.d.ts +0 -14
- package/dist/upload-display.d.ts.map +1 -1
- package/dist/upload-display.js +11 -12
- package/dist/upload-display.js.map +1 -1
- package/package.json +5 -5
- package/dist/react/deferred-upload-form-shell.d.ts +0 -46
- package/dist/react/deferred-upload-form-shell.d.ts.map +0 -1
- package/dist/react/deferred-upload-form-shell.js.map +0 -1
- package/dist/react/guarded-link.d.ts +0 -38
- package/dist/react/guarded-link.d.ts.map +0 -1
- package/dist/react/guarded-link.js.map +0 -1
- package/dist/react/in-flight-uploads-provider.d.ts +0 -266
- package/dist/react/in-flight-uploads-provider.d.ts.map +0 -1
- package/dist/react/in-flight-uploads-provider.js.map +0 -1
- package/dist/react/local-upload-guard-provider.d.ts +0 -112
- package/dist/react/local-upload-guard-provider.d.ts.map +0 -1
- package/dist/react/local-upload-guard-provider.js.map +0 -1
- package/dist/react/use-clear-upload-activity.d.ts +0 -33
- package/dist/react/use-clear-upload-activity.d.ts.map +0 -1
- package/dist/react/use-clear-upload-activity.js +0 -31
- package/dist/react/use-clear-upload-activity.js.map +0 -1
- package/dist/react/use-guarded-navigation.d.ts +0 -10
- package/dist/react/use-guarded-navigation.d.ts.map +0 -1
- package/dist/react/use-guarded-navigation.js +0 -40
- package/dist/react/use-guarded-navigation.js.map +0 -1
- package/dist/react/use-guarded-upload.d.ts +0 -45
- package/dist/react/use-guarded-upload.d.ts.map +0 -1
- package/dist/react/use-guarded-upload.js.map +0 -1
- package/dist/react/use-mark-upload-activity-seen.d.ts.map +0 -1
- package/dist/react/use-mark-upload-activity-seen.js.map +0 -1
- package/dist/react/use-upload-processing.d.ts.map +0 -1
- package/dist/react/use-upload-processing.js.map +0 -1
- /package/dist/react/{guarded-link.js → guard/guarded-link.js} +0 -0
|
@@ -41,7 +41,6 @@ function DeferredUploadFormShellInner(props, ref) {
|
|
|
41
41
|
}
|
|
42
42
|
catch (err) {
|
|
43
43
|
setUploadState(null);
|
|
44
|
-
// User-initiated cancel: swallow the AbortError, reset selection, do not fire onError.
|
|
45
44
|
if (err instanceof Error && err.name === 'AbortError') {
|
|
46
45
|
setSelectedFile(null);
|
|
47
46
|
setSelectedClaim(undefined);
|
|
@@ -54,7 +53,11 @@ function DeferredUploadFormShellInner(props, ref) {
|
|
|
54
53
|
abortControllerRef.current = null;
|
|
55
54
|
}
|
|
56
55
|
}, [mutation, selectedFile, selectedClaim, buildVariables, onSuccess, onError, onFileChange]);
|
|
57
|
-
useImperativeHandle(ref, () => ({
|
|
56
|
+
useImperativeHandle(ref, () => ({
|
|
57
|
+
submit: () => {
|
|
58
|
+
void handleSubmit();
|
|
59
|
+
},
|
|
60
|
+
}), [handleSubmit]);
|
|
58
61
|
return (_jsxs(_Fragment, { children: [children, _jsx("div", { ref: fileAreaRef, tabIndex: -1, "aria-live": "polite", className: "outline-none", children: _jsx(MediaInput, { spec: spec, selectedFile: selectedFile, uploadState: uploadState, isLoading: mutation.isPending, disabled: mutation.isPending, onChange: handleMediaChange, onClear: handleClear, onCancel: handleCancel, onBeforeSelect: onBeforeSelect }) })] }));
|
|
59
62
|
}
|
|
60
63
|
export const DeferredUploadFormShell = forwardRef(DeferredUploadFormShellInner);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deferred-upload-form-shell.js","sourceRoot":"","sources":["../../../src/react/upload/deferred-upload-form-shell.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAC;;AAEb,OAAc,EACZ,QAAQ,EACR,MAAM,EACN,WAAW,EACX,mBAAmB,EACnB,UAAU,GAGX,MAAM,OAAO,CAAC;AACf,OAAO,EAAE,UAAU,EAAE,MAAM,mCAAmC,CAAC;AAqC/D,SAAS,4BAA4B,CAOnC,KAAwD,EACxD,GAAuC;IAEvC,MAAM,EACJ,IAAI,EACJ,QAAQ,EACR,cAAc,EACd,SAAS,EACT,OAAO,EACP,YAAY,EACZ,cAAc,EACd,QAAQ,GACT,GAAG,KAAK,CAAC;IAEV,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAc,IAAI,CAAC,CAAC;IACpE,MAAM,CAAC,aAAa,EAAE,gBAAgB,CAAC,GAAG,QAAQ,CAA+B,SAAS,CAAC,CAAC;IAC5F,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAqB,IAAI,CAAC,CAAC;IACzE,MAAM,WAAW,GAAG,MAAM,CAAiB,IAAI,CAAC,CAAC;IACjD,MAAM,kBAAkB,GAAG,MAAM,CAAyB,IAAI,CAAC,CAAC;IAEhE,MAAM,iBAAiB,GAAG,WAAW,CACnC,CAAC,OAAgC,EAAE,EAAE;QACnC,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;QAC3D,eAAe,CAAC,IAAI,CAAC,CAAC;QACtB,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;QACnD,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC,EACD,CAAC,YAAY,CAAC,CACf,CAAC;IAEF,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE;QACnC,eAAe,CAAC,IAAI,CAAC,CAAC;QACtB,gBAAgB,CAAC,SAAS,CAAC,CAAC;QAC5B,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC;IACvB,CAAC,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC;IAEnB,MAAM,YAAY,GAAG,WAAW,CAAC,GAAG,EAAE;QACpC,kBAAkB,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;IACtC,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,MAAM,YAAY,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QAC1C,IAAI,QAAQ,CAAC,SAAS;YAAE,OAAO;QAC/B,IAAI,YAAY,EAAE,CAAC;YACjB,WAAW,CAAC,OAAO,EAAE,KAAK,EAAE,CAAC;QAC/B,CAAC;QACD,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;QACzC,kBAAkB,CAAC,OAAO,GAAG,UAAU,CAAC;QACxC,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,QAAQ,CAAC,WAAW,CACvC,cAAc,CAAC,YAAY,EAAE,cAAc,EAAE,UAAU,CAAC,MAAM,EAAE,aAAa,CAAC,CAC/E,CAAC;YACF,cAAc,CAAC,IAAI,CAAC,CAAC;YACrB,eAAe,CAAC,IAAI,CAAC,CAAC;YACtB,gBAAgB,CAAC,SAAS,CAAC,CAAC;YAC5B,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC;YACrB,SAAS,EAAE,CAAC,MAAM,CAAC,CAAC;QACtB,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,cAAc,CAAC,IAAI,CAAC,CAAC;YACrB,IAAI,GAAG,YAAY,KAAK,IAAI,GAAG,CAAC,IAAI,KAAK,YAAY,EAAE,CAAC;gBACtD,eAAe,CAAC,IAAI,CAAC,CAAC;gBACtB,gBAAgB,CAAC,SAAS,CAAC,CAAC;gBAC5B,YAAY,EAAE,CAAC,IAAI,CAAC,CAAC;gBACrB,OAAO;YACT,CAAC;YACD,OAAO,EAAE,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC;gBAAS,CAAC;YACT,kBAAkB,CAAC,OAAO,GAAG,IAAI,CAAC;QACpC,CAAC;IACH,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,SAAS,EAAE,OAAO,EAAE,YAAY,CAAC,CAAC,CAAC;IAE9F,mBAAmB,CACjB,GAAG,EACH,GAAG,EAAE,CAAC,CAAC;QACL,MAAM,EAAE,GAAG,EAAE;YACX,KAAK,YAAY,EAAE,CAAC;QACtB,CAAC;KACF,CAAC,EACF,CAAC,YAAY,CAAC,CACf,CAAC;IAEF,OAAO,CACL,8BACG,QAAQ,EAET,cAAK,GAAG,EAAE,WAAW,EAAE,QAAQ,EAAE,CAAC,CAAC,eAAY,QAAQ,EAAC,SAAS,EAAC,cAAc,YAC9E,KAAC,UAAU,IACT,IAAI,EAAE,IAAI,EACV,YAAY,EAAE,YAAY,EAC1B,WAAW,EAAE,WAAW,EACxB,SAAS,EAAE,QAAQ,CAAC,SAAS,EAC7B,QAAQ,EAAE,QAAQ,CAAC,SAAS,EAC5B,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,YAAY,EACtB,cAAc,EAAE,cAAc,GAC9B,GACE,IACL,CACJ,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,uBAAuB,GAAG,UAAU,CAAC,4BAA4B,CAUvD,CAAC","sourcesContent":["'use client';\n\nimport React, {\n useState,\n useRef,\n useCallback,\n useImperativeHandle,\n forwardRef,\n type ReactNode,\n type Ref,\n} from 'react';\nimport { MediaInput } from '@ttt-productions/file-input/react';\nimport type { MediaInputChangePayload } from '@ttt-productions/file-input';\nimport type {\n ClientMediaClaim,\n MediaOriginSpec,\n UploadState,\n} from '@ttt-productions/media-schemas';\n\nexport interface DeferredUploadFormShellHandle {\n submit: () => void;\n}\n\nexport interface DeferredUploadFormShellProps<\n TVariables extends {\n onProgress?: (s: UploadState | null) => void;\n signal?: AbortSignal;\n },\n TResult = unknown,\n> {\n spec: MediaOriginSpec;\n mutation: {\n mutateAsync: (vars: TVariables) => Promise<TResult>;\n isPending: boolean;\n };\n buildVariables: (\n file: File | null,\n onProgress: (s: UploadState | null) => void,\n signal: AbortSignal,\n claim?: ClientMediaClaim,\n ) => TVariables;\n onSuccess?: (result: TResult) => void;\n onError?: (err: unknown) => void;\n onFileChange?: (file: File | null) => void;\n onBeforeSelect?: () => boolean | Promise<boolean>;\n children?: ReactNode;\n}\n\nfunction DeferredUploadFormShellInner<\n TVariables extends {\n onProgress?: (s: UploadState | null) => void;\n signal?: AbortSignal;\n },\n TResult = unknown,\n>(\n props: DeferredUploadFormShellProps<TVariables, TResult>,\n ref: Ref<DeferredUploadFormShellHandle>,\n) {\n const {\n spec,\n mutation,\n buildVariables,\n onSuccess,\n onError,\n onFileChange,\n onBeforeSelect,\n children,\n } = props;\n\n const [selectedFile, setSelectedFile] = useState<File | null>(null);\n const [selectedClaim, setSelectedClaim] = useState<ClientMediaClaim | undefined>(undefined);\n const [uploadState, setUploadState] = useState<UploadState | null>(null);\n const fileAreaRef = useRef<HTMLDivElement>(null);\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const handleMediaChange = useCallback(\n (payload: MediaInputChangePayload) => {\n const next = payload.error ? null : (payload.file ?? null);\n setSelectedFile(next);\n setSelectedClaim(next ? payload.claim : undefined);\n onFileChange?.(next);\n },\n [onFileChange],\n );\n\n const handleClear = useCallback(() => {\n setSelectedFile(null);\n setSelectedClaim(undefined);\n onFileChange?.(null);\n }, [onFileChange]);\n\n const handleCancel = useCallback(() => {\n abortControllerRef.current?.abort();\n }, []);\n\n const handleSubmit = useCallback(async () => {\n if (mutation.isPending) return;\n if (selectedFile) {\n fileAreaRef.current?.focus();\n }\n const controller = new AbortController();\n abortControllerRef.current = controller;\n try {\n const result = await mutation.mutateAsync(\n buildVariables(selectedFile, setUploadState, controller.signal, selectedClaim),\n );\n setUploadState(null);\n setSelectedFile(null);\n setSelectedClaim(undefined);\n onFileChange?.(null);\n onSuccess?.(result);\n } catch (err) {\n setUploadState(null);\n if (err instanceof Error && err.name === 'AbortError') {\n setSelectedFile(null);\n setSelectedClaim(undefined);\n onFileChange?.(null);\n return;\n }\n onError?.(err);\n } finally {\n abortControllerRef.current = null;\n }\n }, [mutation, selectedFile, selectedClaim, buildVariables, onSuccess, onError, onFileChange]);\n\n useImperativeHandle(\n ref,\n () => ({\n submit: () => {\n void handleSubmit();\n },\n }),\n [handleSubmit],\n );\n\n return (\n <>\n {children}\n\n <div ref={fileAreaRef} tabIndex={-1} aria-live=\"polite\" className=\"outline-none\">\n <MediaInput\n spec={spec}\n selectedFile={selectedFile}\n uploadState={uploadState}\n isLoading={mutation.isPending}\n disabled={mutation.isPending}\n onChange={handleMediaChange}\n onClear={handleClear}\n onCancel={handleCancel}\n onBeforeSelect={onBeforeSelect}\n />\n </div>\n </>\n );\n}\n\nexport const DeferredUploadFormShell = forwardRef(DeferredUploadFormShellInner) as <\n TVariables extends {\n onProgress?: (s: UploadState | null) => void;\n signal?: AbortSignal;\n },\n TResult = unknown,\n>(\n props: DeferredUploadFormShellProps<TVariables, TResult> & {\n ref?: Ref<DeferredUploadFormShellHandle>;\n },\n) => React.ReactElement;\n"]}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { useGuardedUpload, type GuardedUploadArgs
|
|
2
|
-
export { DeferredUploadFormShell, type DeferredUploadFormShellHandle, type DeferredUploadFormShellProps, } from '
|
|
1
|
+
export { useGuardedUpload, type GuardedUploadArgs } from './use-guarded-upload.js';
|
|
2
|
+
export { DeferredUploadFormShell, type DeferredUploadFormShellHandle, type DeferredUploadFormShellProps, } from './deferred-upload-form-shell.js';
|
|
3
3
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/upload/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/react/upload/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AACnF,OAAO,EACL,uBAAuB,EACvB,KAAK,6BAA6B,EAClC,KAAK,4BAA4B,GAClC,MAAM,iCAAiC,CAAC"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { useGuardedUpload
|
|
2
|
-
export { DeferredUploadFormShell, } from '
|
|
1
|
+
export { useGuardedUpload } from './use-guarded-upload.js';
|
|
2
|
+
export { DeferredUploadFormShell, } from './deferred-upload-form-shell.js';
|
|
3
3
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/upload/index.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/react/upload/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAA0B,MAAM,yBAAyB,CAAC;AACnF,OAAO,EACL,uBAAuB,GAGxB,MAAM,iCAAiC,CAAC","sourcesContent":["export { useGuardedUpload, type GuardedUploadArgs } from './use-guarded-upload.js';\nexport {\n DeferredUploadFormShell,\n type DeferredUploadFormShellHandle,\n type DeferredUploadFormShellProps,\n} from './deferred-upload-form-shell.js';\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FirebaseStorage } from 'firebase/storage';
|
|
2
|
+
import type { UploadState } from '@ttt-productions/media-schemas';
|
|
3
|
+
export interface GuardedUploadArgs {
|
|
4
|
+
storage: FirebaseStorage;
|
|
5
|
+
path: string;
|
|
6
|
+
file: File;
|
|
7
|
+
metadata: {
|
|
8
|
+
contentType: string;
|
|
9
|
+
};
|
|
10
|
+
uploadId: string;
|
|
11
|
+
onProgress?: (state: UploadState | null) => void;
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
allowNeutralContentType?: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare function useGuardedUpload(): (args: GuardedUploadArgs) => Promise<void>;
|
|
16
|
+
//# sourceMappingURL=use-guarded-upload.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-guarded-upload.d.ts","sourceRoot":"","sources":["../../../src/react/upload/use-guarded-upload.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAGlE,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,eAAe,CAAC;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,IAAI,CAAC;IACX,QAAQ,EAAE;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IACjD,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,uBAAuB,CAAC,EAAE,OAAO,CAAC;CACnC;AAED,wBAAgB,gBAAgB,WAIf,iBAAiB,KAAG,OAAO,CAAC,IAAI,CAAC,CA+BjD"}
|
|
@@ -1,23 +1,10 @@
|
|
|
1
1
|
import { useCallback } from 'react';
|
|
2
2
|
import { uploadFileResumable } from '@ttt-productions/upload-core/browser';
|
|
3
|
-
import { useLocalUploadGuard } from '
|
|
4
|
-
/**
|
|
5
|
-
* Wraps `uploadFileResumable` with the canonical local-upload guard + phase reporting.
|
|
6
|
-
*
|
|
7
|
-
* Every upload-capable mutation hook MUST use this helper instead of calling
|
|
8
|
-
* `uploadFileResumable` directly. The helper owns:
|
|
9
|
-
* - phase reporting: preparing → uploading (with percent) → finalizing
|
|
10
|
-
* - beforeunload guard registration during the upload window only
|
|
11
|
-
* - error safety: the guard is unregistered in a `finally` block
|
|
12
|
-
*
|
|
13
|
-
* The helper does NOT call `onProgress?.(null)` — callers do that from the
|
|
14
|
-
* mutation's `onSuccess` and `onError` callbacks (because the upload window
|
|
15
|
-
* ends when the upload completes, but the mutation may have more work after).
|
|
16
|
-
*/
|
|
3
|
+
import { useLocalUploadGuard } from '../guard/local-upload-guard-provider.js';
|
|
17
4
|
export function useGuardedUpload() {
|
|
18
5
|
const { registerUpload, unregisterUpload } = useLocalUploadGuard();
|
|
19
6
|
return useCallback(async (args) => {
|
|
20
|
-
const { uploadId, onProgress, storage, path, file, metadata, signal, allowNeutralContentType } = args;
|
|
7
|
+
const { uploadId, onProgress, storage, path, file, metadata, signal, allowNeutralContentType, } = args;
|
|
21
8
|
onProgress?.({ phase: 'preparing', percent: null });
|
|
22
9
|
registerUpload(uploadId);
|
|
23
10
|
try {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-guarded-upload.js","sourceRoot":"","sources":["../../../src/react/upload/use-guarded-upload.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,OAAO,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAG3E,OAAO,EAAE,mBAAmB,EAAE,MAAM,yCAAyC,CAAC;AAa9E,MAAM,UAAU,gBAAgB;IAC9B,MAAM,EAAE,cAAc,EAAE,gBAAgB,EAAE,GAAG,mBAAmB,EAAE,CAAC;IAEnE,OAAO,WAAW,CAChB,KAAK,EAAE,IAAuB,EAAiB,EAAE;QAC/C,MAAM,EACJ,QAAQ,EACR,UAAU,EACV,OAAO,EACP,IAAI,EACJ,IAAI,EACJ,QAAQ,EACR,MAAM,EACN,uBAAuB,GACxB,GAAG,IAAI,CAAC;QAET,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,cAAc,CAAC,QAAQ,CAAC,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,mBAAmB,CAAC;gBACxB,OAAO;gBACP,IAAI;gBACJ,IAAI;gBACJ,QAAQ;gBACR,MAAM;gBACN,GAAG,CAAC,uBAAuB,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,uBAAuB,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC9E,UAAU,EAAE,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,WAAW,EAAE,OAAO,EAAE,CAAC;aAC3E,CAAC,CAAC;QACL,CAAC;gBAAS,CAAC;YACT,gBAAgB,CAAC,QAAQ,CAAC,CAAC;QAC7B,CAAC;QACD,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;IACvD,CAAC,EACD,CAAC,cAAc,EAAE,gBAAgB,CAAC,CACnC,CAAC;AACJ,CAAC","sourcesContent":["import { useCallback } from 'react';\nimport { uploadFileResumable } from '@ttt-productions/upload-core/browser';\nimport type { FirebaseStorage } from 'firebase/storage';\nimport type { UploadState } from '@ttt-productions/media-schemas';\nimport { useLocalUploadGuard } from '../guard/local-upload-guard-provider.js';\n\nexport interface GuardedUploadArgs {\n storage: FirebaseStorage;\n path: string;\n file: File;\n metadata: { contentType: string };\n uploadId: string;\n onProgress?: (state: UploadState | null) => void;\n signal?: AbortSignal;\n allowNeutralContentType?: boolean;\n}\n\nexport function useGuardedUpload() {\n const { registerUpload, unregisterUpload } = useLocalUploadGuard();\n\n return useCallback(\n async (args: GuardedUploadArgs): Promise<void> => {\n const {\n uploadId,\n onProgress,\n storage,\n path,\n file,\n metadata,\n signal,\n allowNeutralContentType,\n } = args;\n\n onProgress?.({ phase: 'preparing', percent: null });\n registerUpload(uploadId);\n try {\n await uploadFileResumable({\n storage,\n path,\n file,\n metadata,\n signal,\n ...(allowNeutralContentType === true ? { allowNeutralContentType: true } : {}),\n onProgress: ({ percent }) => onProgress?.({ phase: 'uploading', percent }),\n });\n } finally {\n unregisterUpload(uploadId);\n }\n onProgress?.({ phase: 'finalizing', percent: null });\n },\n [registerUpload, unregisterUpload],\n );\n}\n"]}
|
package/dist/upload-display.d.ts
CHANGED
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
/** A byte count as a short size: `850 B`, `24 KB`, `1.2 MB`, `3 GB`. `null` for no usable count. */
|
|
2
1
|
export declare function formatFileSize(bytes: number | undefined): string | null;
|
|
3
|
-
/** The app's words for an upload row's time and file kind. */
|
|
4
2
|
export interface UploadDisplayCopy {
|
|
5
|
-
/** Under a minute old. */
|
|
6
3
|
justNow: string;
|
|
7
4
|
minutesAgo: (minutes: number) => string;
|
|
8
5
|
hoursAgo: (hours: number) => string;
|
|
9
|
-
/** Accepted on the previous calendar day, given the local clock time. */
|
|
10
6
|
yesterdayAt: (time: string) => string;
|
|
11
|
-
/** Accepted before yesterday, given the short local date and clock time. */
|
|
12
7
|
dateAt: (date: string, time: string) => string;
|
|
13
|
-
/** The file's kind, by its simplified media type; `other` for anything else. */
|
|
14
8
|
fileType: {
|
|
15
9
|
image: string;
|
|
16
10
|
video: string;
|
|
@@ -20,19 +14,11 @@ export interface UploadDisplayCopy {
|
|
|
20
14
|
}
|
|
21
15
|
export interface UploadDisplayConfig {
|
|
22
16
|
copy: UploadDisplayCopy;
|
|
23
|
-
/** How long a row's time reads as relative before it reads as a clock time. */
|
|
24
17
|
relativeTimeCutoffMs: number;
|
|
25
|
-
/** Locale for the clock time and date; the runtime's own when omitted. */
|
|
26
18
|
locale?: Intl.LocalesArgument;
|
|
27
19
|
}
|
|
28
20
|
export interface UploadDisplayFormatters {
|
|
29
|
-
/**
|
|
30
|
-
* An upload's accept time. Within the cutoff of now: just now, minutes ago, or hours ago. Older:
|
|
31
|
-
* the clock time for today, yesterday and the clock time, or the short date and the clock time —
|
|
32
|
-
* by local calendar day.
|
|
33
|
-
*/
|
|
34
21
|
formatUploadTimestamp: (createdAtMs: number, nowMs: number) => string;
|
|
35
|
-
/** The file's kind from its stored content type; `null` when it has none. */
|
|
36
22
|
getFileTypeLabel: (contentType: string | undefined) => string | null;
|
|
37
23
|
}
|
|
38
24
|
export declare function createUploadDisplayFormatters(config: UploadDisplayConfig): UploadDisplayFormatters;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload-display.d.ts","sourceRoot":"","sources":["../src/upload-display.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"upload-display.d.ts","sourceRoot":"","sources":["../src/upload-display.ts"],"names":[],"mappings":"AAEA,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAUvE;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACxC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACtC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/C,QAAQ,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1E;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,iBAAiB,CAAC;IACxB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,MAAM,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC;CAC/B;AAED,MAAM,WAAW,uBAAuB;IACtC,qBAAqB,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACtE,gBAAgB,EAAE,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,GAAG,IAAI,CAAC;CACtE;AAUD,wBAAgB,6BAA6B,CAC3C,MAAM,EAAE,mBAAmB,GAC1B,uBAAuB,CA2CzB"}
|
package/dist/upload-display.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
// The mechanics are shared; every word, the relative-time cutoff and the locale are the app's.
|
|
3
|
-
import { getSimplifiedMediaType } from "@ttt-productions/media-schemas";
|
|
4
|
-
/** A byte count as a short size: `850 B`, `24 KB`, `1.2 MB`, `3 GB`. `null` for no usable count. */
|
|
1
|
+
import { getSimplifiedMediaType } from '@ttt-productions/media-schemas';
|
|
5
2
|
export function formatFileSize(bytes) {
|
|
6
3
|
if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0)
|
|
7
4
|
return null;
|
|
@@ -17,7 +14,9 @@ export function formatFileSize(bytes) {
|
|
|
17
14
|
return `${gb < 10 ? gb.toFixed(1) : Math.round(gb)} GB`;
|
|
18
15
|
}
|
|
19
16
|
function sameCalendarDay(a, b) {
|
|
20
|
-
return a.getFullYear() === b.getFullYear() &&
|
|
17
|
+
return (a.getFullYear() === b.getFullYear() &&
|
|
18
|
+
a.getMonth() === b.getMonth() &&
|
|
19
|
+
a.getDate() === b.getDate());
|
|
21
20
|
}
|
|
22
21
|
export function createUploadDisplayFormatters(config) {
|
|
23
22
|
const { copy, relativeTimeCutoffMs, locale } = config;
|
|
@@ -33,25 +32,25 @@ export function createUploadDisplayFormatters(config) {
|
|
|
33
32
|
}
|
|
34
33
|
const created = new Date(createdAtMs);
|
|
35
34
|
const now = new Date(nowMs);
|
|
36
|
-
const time = created.toLocaleTimeString(locale, { hour:
|
|
35
|
+
const time = created.toLocaleTimeString(locale, { hour: 'numeric', minute: '2-digit' });
|
|
37
36
|
if (sameCalendarDay(created, now))
|
|
38
37
|
return time;
|
|
39
|
-
//
|
|
40
|
-
//
|
|
38
|
+
// Stepping the date, not subtracting 24 hours: a day that crosses a daylight-saving change is 23
|
|
39
|
+
// or 25 hours long.
|
|
41
40
|
const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);
|
|
42
41
|
if (sameCalendarDay(created, yesterday))
|
|
43
42
|
return copy.yesterdayAt(time);
|
|
44
|
-
return copy.dateAt(created.toLocaleDateString(locale, { month:
|
|
43
|
+
return copy.dateAt(created.toLocaleDateString(locale, { month: 'short', day: 'numeric' }), time);
|
|
45
44
|
};
|
|
46
45
|
const getFileTypeLabel = (contentType) => {
|
|
47
46
|
if (!contentType)
|
|
48
47
|
return null;
|
|
49
48
|
switch (getSimplifiedMediaType(contentType)) {
|
|
50
|
-
case
|
|
49
|
+
case 'image':
|
|
51
50
|
return copy.fileType.image;
|
|
52
|
-
case
|
|
51
|
+
case 'video':
|
|
53
52
|
return copy.fileType.video;
|
|
54
|
-
case
|
|
53
|
+
case 'audio':
|
|
55
54
|
return copy.fileType.audio;
|
|
56
55
|
default:
|
|
57
56
|
return copy.fileType.other;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload-display.js","sourceRoot":"","sources":["../src/upload-display.ts"],"names":[],"mappings":"AAAA,
|
|
1
|
+
{"version":3,"file":"upload-display.js","sourceRoot":"","sources":["../src/upload-display.ts"],"names":[],"mappings":"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","sourcesContent":["import { getSimplifiedMediaType } from '@ttt-productions/media-schemas';\n\nexport function formatFileSize(bytes: number | undefined): string | null {\n if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0) return null;\n if (bytes < 1024) return `${bytes} B`;\n if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`;\n if (bytes < 1024 * 1024 * 1024) {\n const mb = bytes / (1024 * 1024);\n return `${mb < 10 ? mb.toFixed(1) : Math.round(mb)} MB`;\n }\n const gb = bytes / (1024 * 1024 * 1024);\n return `${gb < 10 ? gb.toFixed(1) : Math.round(gb)} GB`;\n}\n\nexport interface UploadDisplayCopy {\n justNow: string;\n minutesAgo: (minutes: number) => string;\n hoursAgo: (hours: number) => string;\n yesterdayAt: (time: string) => string;\n dateAt: (date: string, time: string) => string;\n fileType: { image: string; video: string; audio: string; other: string };\n}\n\nexport interface UploadDisplayConfig {\n copy: UploadDisplayCopy;\n relativeTimeCutoffMs: number;\n locale?: Intl.LocalesArgument;\n}\n\nexport interface UploadDisplayFormatters {\n formatUploadTimestamp: (createdAtMs: number, nowMs: number) => string;\n getFileTypeLabel: (contentType: string | undefined) => string | null;\n}\n\nfunction sameCalendarDay(a: Date, b: Date): boolean {\n return (\n a.getFullYear() === b.getFullYear() &&\n a.getMonth() === b.getMonth() &&\n a.getDate() === b.getDate()\n );\n}\n\nexport function createUploadDisplayFormatters(\n config: UploadDisplayConfig,\n): UploadDisplayFormatters {\n const { copy, relativeTimeCutoffMs, locale } = config;\n\n const formatUploadTimestamp = (createdAtMs: number, nowMs: number): string => {\n const diffMs = Math.max(0, nowMs - createdAtMs);\n if (diffMs < relativeTimeCutoffMs) {\n if (diffMs < 60_000) return copy.justNow;\n const minutes = Math.floor(diffMs / 60_000);\n if (minutes < 60) return copy.minutesAgo(minutes);\n return copy.hoursAgo(Math.floor(diffMs / 3_600_000));\n }\n\n const created = new Date(createdAtMs);\n const now = new Date(nowMs);\n const time = created.toLocaleTimeString(locale, { hour: 'numeric', minute: '2-digit' });\n if (sameCalendarDay(created, now)) return time;\n\n // Stepping the date, not subtracting 24 hours: a day that crosses a daylight-saving change is 23\n // or 25 hours long.\n const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1);\n if (sameCalendarDay(created, yesterday)) return copy.yesterdayAt(time);\n\n return copy.dateAt(\n created.toLocaleDateString(locale, { month: 'short', day: 'numeric' }),\n time,\n );\n };\n\n const getFileTypeLabel = (contentType: string | undefined): string | null => {\n if (!contentType) return null;\n switch (getSimplifiedMediaType(contentType)) {\n case 'image':\n return copy.fileType.image;\n case 'video':\n return copy.fileType.video;\n case 'audio':\n return copy.fileType.audio;\n default:\n return copy.fileType.other;\n }\n };\n\n return { formatUploadTimestamp, getFileTypeLabel };\n}\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.18",
|
|
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.
|
|
44
|
-
"@ttt-productions/media-schemas": "^0.14.
|
|
45
|
-
"@ttt-productions/ui-core": "^0.11.
|
|
46
|
-
"@ttt-productions/upload-core": "^0.7.
|
|
43
|
+
"@ttt-productions/file-input": "^0.11.17",
|
|
44
|
+
"@ttt-productions/media-schemas": "^0.14.12",
|
|
45
|
+
"@ttt-productions/ui-core": "^0.11.35",
|
|
46
|
+
"@ttt-productions/upload-core": "^0.7.8"
|
|
47
47
|
},
|
|
48
48
|
"peerDependencies": {
|
|
49
49
|
"@tanstack/react-query": ">=5.0.0",
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import React, { type ReactNode, type Ref } from 'react';
|
|
2
|
-
import type { ClientMediaClaim, MediaOriginSpec, UploadState } from '@ttt-productions/media-schemas';
|
|
3
|
-
export interface DeferredUploadFormShellHandle {
|
|
4
|
-
submit: () => void;
|
|
5
|
-
}
|
|
6
|
-
export interface DeferredUploadFormShellProps<TVariables extends {
|
|
7
|
-
onProgress?: (s: UploadState | null) => void;
|
|
8
|
-
signal?: AbortSignal;
|
|
9
|
-
}, TResult = unknown> {
|
|
10
|
-
/** The media-origin spec describing accept patterns, size caps, and label.
|
|
11
|
-
* Consumers pass this directly — the shell does not look it up from a registry. */
|
|
12
|
-
spec: MediaOriginSpec;
|
|
13
|
-
mutation: {
|
|
14
|
-
mutateAsync: (vars: TVariables) => Promise<TResult>;
|
|
15
|
-
isPending: boolean;
|
|
16
|
-
};
|
|
17
|
-
buildVariables: (file: File | null, onProgress: (s: UploadState | null) => void, signal: AbortSignal,
|
|
18
|
-
/** The untrusted claim MediaInput emitted with the selected file (advisory;
|
|
19
|
-
* the server byte inspection is the only classification authority). Rides
|
|
20
|
-
* and clears with the file — additive 4th arg, existing callers unchanged. */
|
|
21
|
-
claim?: ClientMediaClaim) => TVariables;
|
|
22
|
-
onSuccess?: (result: TResult) => void;
|
|
23
|
-
onError?: (err: unknown) => void;
|
|
24
|
-
/**
|
|
25
|
-
* Optional callback fired whenever the shell's internal selectedFile state
|
|
26
|
-
* changes — on user select, on user clear, and on the post-success internal
|
|
27
|
-
* reset. Forms typically use this to mirror selectedFile externally so they
|
|
28
|
-
* can compute their own submit-button `disabled` flag (e.g. require-a-file
|
|
29
|
-
* submit gating).
|
|
30
|
-
*/
|
|
31
|
-
onFileChange?: (file: File | null) => void;
|
|
32
|
-
/**
|
|
33
|
-
* Optional guard forwarded to MediaInput, fired before the picker/capture
|
|
34
|
-
* opens for any action. Return false (or a Promise resolving false) to abort
|
|
35
|
-
* — e.g. an email-verification gate. See MediaInputProps.onBeforeSelect.
|
|
36
|
-
*/
|
|
37
|
-
onBeforeSelect?: () => boolean | Promise<boolean>;
|
|
38
|
-
children?: ReactNode;
|
|
39
|
-
}
|
|
40
|
-
export declare const DeferredUploadFormShell: <TVariables extends {
|
|
41
|
-
onProgress?: (s: UploadState | null) => void;
|
|
42
|
-
signal?: AbortSignal;
|
|
43
|
-
}, TResult = unknown>(props: DeferredUploadFormShellProps<TVariables, TResult> & {
|
|
44
|
-
ref?: Ref<DeferredUploadFormShellHandle>;
|
|
45
|
-
}) => React.ReactElement;
|
|
46
|
-
//# sourceMappingURL=deferred-upload-form-shell.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"deferred-upload-form-shell.d.ts","sourceRoot":"","sources":["../../src/react/deferred-upload-form-shell.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,EAMZ,KAAK,SAAS,EACd,KAAK,GAAG,EACT,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,gCAAgC,CAAC;AAErG,MAAM,WAAW,6BAA6B;IAC5C,MAAM,EAAE,MAAM,IAAI,CAAC;CACpB;AAED,MAAM,WAAW,4BAA4B,CAC3C,UAAU,SAAS;IACjB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,EACD,OAAO,GAAG,OAAO;IAEjB;wFACoF;IACpF,IAAI,EAAE,eAAe,CAAC;IACtB,QAAQ,EAAE;QACR,WAAW,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;QACpD,SAAS,EAAE,OAAO,CAAC;KACpB,CAAC;IACF,cAAc,EAAE,CACd,IAAI,EAAE,IAAI,GAAG,IAAI,EACjB,UAAU,EAAE,CAAC,CAAC,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,EAC3C,MAAM,EAAE,WAAW;IACnB;;mFAE+E;IAC/E,KAAK,CAAC,EAAE,gBAAgB,KACrB,UAAU,CAAC;IAChB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC;IACjC;;;;;;OAMG;IACH,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,GAAG,IAAI,KAAK,IAAI,CAAC;IAC3C;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClD,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAyGD,eAAO,MAAM,uBAAuB,EAA+C,CACjF,UAAU,SAAS;IACjB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7C,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,EACD,OAAO,GAAG,OAAO,EAEjB,KAAK,EAAE,4BAA4B,CAAC,UAAU,EAAE,OAAO,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,GAAG,CAAC,6BAA6B,CAAC,CAAA;CAAE,KACpG,KAAK,CAAC,YAAY,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"deferred-upload-form-shell.js","sourceRoot":"","sources":["../../src/react/deferred-upload-form-shell.tsx"],"names":[],"mappings":"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","sourcesContent":["'use client';\n\nimport React, {\n useState,\n useRef,\n useCallback,\n useImperativeHandle,\n forwardRef,\n type ReactNode,\n type Ref,\n} from 'react';\nimport { MediaInput } from '@ttt-productions/file-input/react';\nimport type { MediaInputChangePayload } from '@ttt-productions/file-input';\nimport type { ClientMediaClaim, MediaOriginSpec, UploadState } from '@ttt-productions/media-schemas';\n\nexport interface DeferredUploadFormShellHandle {\n submit: () => void;\n}\n\nexport interface DeferredUploadFormShellProps<\n TVariables extends {\n onProgress?: (s: UploadState | null) => void;\n signal?: AbortSignal;\n },\n TResult = unknown,\n> {\n /** The media-origin spec describing accept patterns, size caps, and label.\n * Consumers pass this directly — the shell does not look it up from a registry. */\n spec: MediaOriginSpec;\n mutation: {\n mutateAsync: (vars: TVariables) => Promise<TResult>;\n isPending: boolean;\n };\n buildVariables: (\n file: File | null,\n onProgress: (s: UploadState | null) => void,\n signal: AbortSignal,\n /** The untrusted claim MediaInput emitted with the selected file (advisory;\n * the server byte inspection is the only classification authority). Rides\n * and clears with the file — additive 4th arg, existing callers unchanged. */\n claim?: ClientMediaClaim,\n ) => TVariables;\n onSuccess?: (result: TResult) => void;\n onError?: (err: unknown) => void;\n /**\n * Optional callback fired whenever the shell's internal selectedFile state\n * changes — on user select, on user clear, and on the post-success internal\n * reset. Forms typically use this to mirror selectedFile externally so they\n * can compute their own submit-button `disabled` flag (e.g. require-a-file\n * submit gating).\n */\n onFileChange?: (file: File | null) => void;\n /**\n * Optional guard forwarded to MediaInput, fired before the picker/capture\n * opens for any action. Return false (or a Promise resolving false) to abort\n * — e.g. an email-verification gate. See MediaInputProps.onBeforeSelect.\n */\n onBeforeSelect?: () => boolean | Promise<boolean>;\n children?: ReactNode;\n}\n\nfunction DeferredUploadFormShellInner<\n TVariables extends {\n onProgress?: (s: UploadState | null) => void;\n signal?: AbortSignal;\n },\n TResult = unknown,\n>(\n props: DeferredUploadFormShellProps<TVariables, TResult>,\n ref: Ref<DeferredUploadFormShellHandle>,\n) {\n const {\n spec,\n mutation,\n buildVariables,\n onSuccess,\n onError,\n onFileChange,\n onBeforeSelect,\n children,\n } = props;\n\n const [selectedFile, setSelectedFile] = useState<File | null>(null);\n const [selectedClaim, setSelectedClaim] = useState<ClientMediaClaim | undefined>(undefined);\n const [uploadState, setUploadState] = useState<UploadState | null>(null);\n const fileAreaRef = useRef<HTMLDivElement>(null);\n const abortControllerRef = useRef<AbortController | null>(null);\n\n const handleMediaChange = useCallback((payload: MediaInputChangePayload) => {\n const next = payload.error ? null : (payload.file ?? null);\n setSelectedFile(next);\n setSelectedClaim(next ? payload.claim : undefined);\n onFileChange?.(next);\n }, [onFileChange]);\n\n const handleClear = useCallback(() => {\n setSelectedFile(null);\n setSelectedClaim(undefined);\n onFileChange?.(null);\n }, [onFileChange]);\n\n const handleCancel = useCallback(() => {\n abortControllerRef.current?.abort();\n }, []);\n\n const handleSubmit = useCallback(async () => {\n if (mutation.isPending) return;\n if (selectedFile) {\n fileAreaRef.current?.focus();\n }\n const controller = new AbortController();\n abortControllerRef.current = controller;\n try {\n const result = await mutation.mutateAsync(\n buildVariables(selectedFile, setUploadState, controller.signal, selectedClaim),\n );\n setUploadState(null);\n setSelectedFile(null);\n setSelectedClaim(undefined);\n onFileChange?.(null);\n onSuccess?.(result);\n } catch (err) {\n setUploadState(null);\n // User-initiated cancel: swallow the AbortError, reset selection, do not fire onError.\n if (err instanceof Error && err.name === 'AbortError') {\n setSelectedFile(null);\n setSelectedClaim(undefined);\n onFileChange?.(null);\n return;\n }\n onError?.(err);\n } finally {\n abortControllerRef.current = null;\n }\n }, [mutation, selectedFile, selectedClaim, buildVariables, onSuccess, onError, onFileChange]);\n\n useImperativeHandle(ref, () => ({ submit: () => { void handleSubmit(); } }), [handleSubmit]);\n\n return (\n <>\n {children}\n\n <div\n ref={fileAreaRef}\n tabIndex={-1}\n aria-live=\"polite\"\n className=\"outline-none\"\n >\n <MediaInput\n spec={spec}\n selectedFile={selectedFile}\n uploadState={uploadState}\n isLoading={mutation.isPending}\n disabled={mutation.isPending}\n onChange={handleMediaChange}\n onClear={handleClear}\n onCancel={handleCancel}\n onBeforeSelect={onBeforeSelect}\n />\n </div>\n </>\n );\n}\n\nexport const DeferredUploadFormShell = forwardRef(DeferredUploadFormShellInner) as <\n TVariables extends {\n onProgress?: (s: UploadState | null) => void;\n signal?: AbortSignal;\n },\n TResult = unknown,\n>(\n props: DeferredUploadFormShellProps<TVariables, TResult> & { ref?: Ref<DeferredUploadFormShellHandle> },\n) => React.ReactElement;\n"]}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
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
|
-
* React Router Link, plain anchor, etc.) — this wrapper only injects the
|
|
5
|
-
* onClick interceptor.
|
|
6
|
-
*
|
|
7
|
-
* Why render-prop and not `as={Link}`? Polymorphic `as` props create
|
|
8
|
-
* notoriously bad TypeScript inference and force the wrapper to forward all
|
|
9
|
-
* link-specific props. Render-prop keeps the consumer in full control of the
|
|
10
|
-
* link API, costs one extra wrapper closure, and keeps upload-ui agnostic of
|
|
11
|
-
* Next.js / React Router / etc.
|
|
12
|
-
*
|
|
13
|
-
* Design note: the design spec lists `<GuardedNavigationButton>` as a second
|
|
14
|
-
* component. We deliberately don't ship it — buttons that navigate use
|
|
15
|
-
* `useGuardedNavigation()` directly in one line. A second component would
|
|
16
|
-
* split the same surface for no payoff.
|
|
17
|
-
*
|
|
18
|
-
* Usage:
|
|
19
|
-
*
|
|
20
|
-
* <GuardedLink renderLink={({ onClick, children }) => (
|
|
21
|
-
* <Link href="/your/destination" onClick={onClick}>{children}</Link>
|
|
22
|
-
* )}>
|
|
23
|
-
* View all uploads →
|
|
24
|
-
* </GuardedLink>
|
|
25
|
-
*/
|
|
26
|
-
import type { MouseEvent, ReactNode } from 'react';
|
|
27
|
-
export interface GuardedLinkRenderArgs {
|
|
28
|
-
/** Click handler the consumer MUST attach to its underlying link element. */
|
|
29
|
-
onClick: (event: MouseEvent<HTMLElement>) => void;
|
|
30
|
-
/** The children passed to `<GuardedLink>` — forward them to the link. */
|
|
31
|
-
children: ReactNode;
|
|
32
|
-
}
|
|
33
|
-
export interface GuardedLinkProps {
|
|
34
|
-
children: ReactNode;
|
|
35
|
-
renderLink: (args: GuardedLinkRenderArgs) => ReactNode;
|
|
36
|
-
}
|
|
37
|
-
export declare function GuardedLink(props: GuardedLinkProps): import("react").JSX.Element;
|
|
38
|
-
//# sourceMappingURL=guarded-link.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"guarded-link.d.ts","sourceRoot":"","sources":["../../src/react/guarded-link.tsx"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGnD,MAAM,WAAW,qBAAqB;IACpC,6EAA6E;IAC7E,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,CAAC,WAAW,CAAC,KAAK,IAAI,CAAC;IAClD,yEAAyE;IACzE,QAAQ,EAAE,SAAS,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,SAAS,CAAC;IACpB,UAAU,EAAE,CAAC,IAAI,EAAE,qBAAqB,KAAK,SAAS,CAAC;CACxD;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,gBAAgB,+BAWlD"}
|
|
@@ -1 +0,0 @@
|
|
|
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"]}
|