@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.
Files changed (84) hide show
  1. package/dist/index.d.ts +2 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +1 -3
  4. package/dist/index.js.map +1 -1
  5. package/dist/react/guard/guarded-link.d.ts +11 -0
  6. package/dist/react/guard/guarded-link.d.ts.map +1 -0
  7. package/dist/react/guard/guarded-link.js.map +1 -0
  8. package/dist/react/guard/index.d.ts +3 -3
  9. package/dist/react/guard/index.d.ts.map +1 -1
  10. package/dist/react/guard/index.js +3 -3
  11. package/dist/react/guard/index.js.map +1 -1
  12. package/dist/react/guard/local-upload-guard-provider.d.ts +31 -0
  13. package/dist/react/guard/local-upload-guard-provider.d.ts.map +1 -0
  14. package/dist/react/{local-upload-guard-provider.js → guard/local-upload-guard-provider.js} +3 -60
  15. package/dist/react/guard/local-upload-guard-provider.js.map +1 -0
  16. package/dist/react/guard/use-guarded-navigation.d.ts +3 -0
  17. package/dist/react/guard/use-guarded-navigation.d.ts.map +1 -0
  18. package/dist/react/guard/use-guarded-navigation.js +12 -0
  19. package/dist/react/guard/use-guarded-navigation.js.map +1 -0
  20. package/dist/react/tray/in-flight-uploads-provider.d.ts +141 -0
  21. package/dist/react/tray/in-flight-uploads-provider.d.ts.map +1 -0
  22. package/dist/react/{in-flight-uploads-provider.js → tray/in-flight-uploads-provider.js} +16 -136
  23. package/dist/react/tray/in-flight-uploads-provider.js.map +1 -0
  24. package/dist/react/tray/index.d.ts +4 -4
  25. package/dist/react/tray/index.d.ts.map +1 -1
  26. package/dist/react/tray/index.js +4 -4
  27. package/dist/react/tray/index.js.map +1 -1
  28. package/dist/react/tray/use-clear-upload-activity.d.ts +8 -0
  29. package/dist/react/tray/use-clear-upload-activity.d.ts.map +1 -0
  30. package/dist/react/tray/use-clear-upload-activity.js +13 -0
  31. package/dist/react/tray/use-clear-upload-activity.js.map +1 -0
  32. package/dist/react/{use-mark-upload-activity-seen.d.ts → tray/use-mark-upload-activity-seen.d.ts} +0 -4
  33. package/dist/react/tray/use-mark-upload-activity-seen.d.ts.map +1 -0
  34. package/dist/react/{use-mark-upload-activity-seen.js → tray/use-mark-upload-activity-seen.js} +0 -21
  35. package/dist/react/tray/use-mark-upload-activity-seen.js.map +1 -0
  36. package/dist/react/{use-upload-processing.d.ts → tray/use-upload-processing.d.ts} +0 -10
  37. package/dist/react/tray/use-upload-processing.d.ts.map +1 -0
  38. package/dist/react/{use-upload-processing.js → tray/use-upload-processing.js} +10 -20
  39. package/dist/react/tray/use-upload-processing.js.map +1 -0
  40. package/dist/react/upload/deferred-upload-form-shell.d.ts +28 -0
  41. package/dist/react/upload/deferred-upload-form-shell.d.ts.map +1 -0
  42. package/dist/react/{deferred-upload-form-shell.js → upload/deferred-upload-form-shell.js} +5 -2
  43. package/dist/react/upload/deferred-upload-form-shell.js.map +1 -0
  44. package/dist/react/upload/index.d.ts +2 -2
  45. package/dist/react/upload/index.d.ts.map +1 -1
  46. package/dist/react/upload/index.js +2 -2
  47. package/dist/react/upload/index.js.map +1 -1
  48. package/dist/react/upload/use-guarded-upload.d.ts +16 -0
  49. package/dist/react/upload/use-guarded-upload.d.ts.map +1 -0
  50. package/dist/react/{use-guarded-upload.js → upload/use-guarded-upload.js} +2 -15
  51. package/dist/react/upload/use-guarded-upload.js.map +1 -0
  52. package/dist/upload-display.d.ts +0 -14
  53. package/dist/upload-display.d.ts.map +1 -1
  54. package/dist/upload-display.js +11 -12
  55. package/dist/upload-display.js.map +1 -1
  56. package/package.json +5 -5
  57. package/dist/react/deferred-upload-form-shell.d.ts +0 -46
  58. package/dist/react/deferred-upload-form-shell.d.ts.map +0 -1
  59. package/dist/react/deferred-upload-form-shell.js.map +0 -1
  60. package/dist/react/guarded-link.d.ts +0 -38
  61. package/dist/react/guarded-link.d.ts.map +0 -1
  62. package/dist/react/guarded-link.js.map +0 -1
  63. package/dist/react/in-flight-uploads-provider.d.ts +0 -266
  64. package/dist/react/in-flight-uploads-provider.d.ts.map +0 -1
  65. package/dist/react/in-flight-uploads-provider.js.map +0 -1
  66. package/dist/react/local-upload-guard-provider.d.ts +0 -112
  67. package/dist/react/local-upload-guard-provider.d.ts.map +0 -1
  68. package/dist/react/local-upload-guard-provider.js.map +0 -1
  69. package/dist/react/use-clear-upload-activity.d.ts +0 -33
  70. package/dist/react/use-clear-upload-activity.d.ts.map +0 -1
  71. package/dist/react/use-clear-upload-activity.js +0 -31
  72. package/dist/react/use-clear-upload-activity.js.map +0 -1
  73. package/dist/react/use-guarded-navigation.d.ts +0 -10
  74. package/dist/react/use-guarded-navigation.d.ts.map +0 -1
  75. package/dist/react/use-guarded-navigation.js +0 -40
  76. package/dist/react/use-guarded-navigation.js.map +0 -1
  77. package/dist/react/use-guarded-upload.d.ts +0 -45
  78. package/dist/react/use-guarded-upload.d.ts.map +0 -1
  79. package/dist/react/use-guarded-upload.js.map +0 -1
  80. package/dist/react/use-mark-upload-activity-seen.d.ts.map +0 -1
  81. package/dist/react/use-mark-upload-activity-seen.js.map +0 -1
  82. package/dist/react/use-upload-processing.d.ts.map +0 -1
  83. package/dist/react/use-upload-processing.js.map +0 -1
  84. /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, () => ({ submit: () => { void handleSubmit(); } }), [handleSubmit]);
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":"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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 {\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, } from '../use-guarded-upload.js';
2
- export { DeferredUploadFormShell, type DeferredUploadFormShellHandle, type DeferredUploadFormShellProps, } from '../deferred-upload-form-shell.js';
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,EACL,gBAAgB,EAChB,KAAK,iBAAiB,GACvB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,uBAAuB,EACvB,KAAK,6BAA6B,EAClC,KAAK,4BAA4B,GAClC,MAAM,kCAAkC,CAAC"}
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, } from '../use-guarded-upload.js';
2
- export { DeferredUploadFormShell, } from '../deferred-upload-form-shell.js';
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,EACL,gBAAgB,GAEjB,MAAM,0BAA0B,CAAC;AAClC,OAAO,EACL,uBAAuB,GAGxB,MAAM,kCAAkC,CAAC","sourcesContent":["export {\n useGuardedUpload,\n type GuardedUploadArgs,\n} from '../use-guarded-upload.js';\nexport {\n DeferredUploadFormShell,\n type DeferredUploadFormShellHandle,\n type DeferredUploadFormShellProps,\n} from '../deferred-upload-form-shell.js';\n"]}
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 './local-upload-guard-provider.js';
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"]}
@@ -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":"AAKA,oGAAoG;AACpG,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAUvE;AAED,8DAA8D;AAC9D,MAAM,WAAW,iBAAiB;IAChC,0BAA0B;IAC1B,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,yEAAyE;IACzE,WAAW,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IACtC,4EAA4E;IAC5E,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/C,gFAAgF;IAChF,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,+EAA+E;IAC/E,oBAAoB,EAAE,MAAM,CAAC;IAC7B,0EAA0E;IAC1E,MAAM,CAAC,EAAE,IAAI,CAAC,eAAe,CAAC;CAC/B;AAED,MAAM,WAAW,uBAAuB;IACtC;;;;OAIG;IACH,qBAAqB,EAAE,CAAC,WAAW,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACtE,6EAA6E;IAC7E,gBAAgB,EAAE,CAAC,WAAW,EAAE,MAAM,GAAG,SAAS,KAAK,MAAM,GAAG,IAAI,CAAC;CACtE;AAMD,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,mBAAmB,GAAG,uBAAuB,CAwClG"}
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"}
@@ -1,7 +1,4 @@
1
- // What an upload row shows besides its title: the file's size, its kind, and when it was accepted.
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() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
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: "numeric", minute: "2-digit" });
35
+ const time = created.toLocaleTimeString(locale, { hour: 'numeric', minute: '2-digit' });
37
36
  if (sameCalendarDay(created, now))
38
37
  return time;
39
- // The previous calendar day, found by stepping the date rather than subtracting 24 hours: a
40
- // day that crosses a daylight-saving change is 23 or 25 hours long.
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: "short", day: "numeric" }), time);
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 "image":
49
+ case 'image':
51
50
  return copy.fileType.image;
52
- case "video":
51
+ case 'video':
53
52
  return copy.fileType.video;
54
- case "audio":
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,mGAAmG;AACnG,+FAA+F;AAE/F,OAAO,EAAE,sBAAsB,EAAE,MAAM,gCAAgC,CAAC;AAExE,oGAAoG;AACpG,MAAM,UAAU,cAAc,CAAC,KAAyB;IACtD,IAAI,KAAK,KAAK,SAAS,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,KAAK,GAAG,CAAC;QAAE,OAAO,IAAI,CAAC;IAC7E,IAAI,KAAK,GAAG,IAAI;QAAE,OAAO,GAAG,KAAK,IAAI,CAAC;IACtC,IAAI,KAAK,GAAG,IAAI,GAAG,IAAI;QAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC;IACjE,IAAI,KAAK,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,EAAE,CAAC;QAC/B,MAAM,EAAE,GAAG,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;QACjC,OAAO,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC;IAC1D,CAAC;IACD,MAAM,EAAE,GAAG,KAAK,GAAG,CAAC,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC;IACxC,OAAO,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC;AAC1D,CAAC;AAmCD,SAAS,eAAe,CAAC,CAAO,EAAE,CAAO;IACvC,OAAO,CAAC,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC,WAAW,EAAE,IAAI,CAAC,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,OAAO,EAAE,CAAC;AAC7G,CAAC;AAED,MAAM,UAAU,6BAA6B,CAAC,MAA2B;IACvE,MAAM,EAAE,IAAI,EAAE,oBAAoB,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IAEtD,MAAM,qBAAqB,GAAG,CAAC,WAAmB,EAAE,KAAa,EAAU,EAAE;QAC3E,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,WAAW,CAAC,CAAC;QAChD,IAAI,MAAM,GAAG,oBAAoB,EAAE,CAAC;YAClC,IAAI,MAAM,GAAG,MAAM;gBAAE,OAAO,IAAI,CAAC,OAAO,CAAC;YACzC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,CAAC;YAC5C,IAAI,OAAO,GAAG,EAAE;gBAAE,OAAO,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC;YAClD,OAAO,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC;QACvD,CAAC;QAED,MAAM,OAAO,GAAG,IAAI,IAAI,CAAC,WAAW,CAAC,CAAC;QACtC,MAAM,GAAG,GAAG,IAAI,IAAI,CAAC,KAAK,CAAC,CAAC;QAC5B,MAAM,IAAI,GAAG,OAAO,CAAC,kBAAkB,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;QACxF,IAAI,eAAe,CAAC,OAAO,EAAE,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QAE/C,4FAA4F;QAC5F,oEAAoE;QACpE,MAAM,SAAS,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;QACjF,IAAI,eAAe,CAAC,OAAO,EAAE,SAAS,CAAC;YAAE,OAAO,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;QAEvE,OAAO,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,kBAAkB,CAAC,MAAM,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,CAAC,EAAE,IAAI,CAAC,CAAC;IACnG,CAAC,CAAC;IAEF,MAAM,gBAAgB,GAAG,CAAC,WAA+B,EAAiB,EAAE;QAC1E,IAAI,CAAC,WAAW;YAAE,OAAO,IAAI,CAAC;QAC9B,QAAQ,sBAAsB,CAAC,WAAW,CAAC,EAAE,CAAC;YAC5C,KAAK,OAAO;gBACV,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAC7B,KAAK,OAAO;gBACV,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAC7B,KAAK,OAAO;gBACV,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;YAC7B;gBACE,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC;QAC/B,CAAC;IACH,CAAC,CAAC;IAEF,OAAO,EAAE,qBAAqB,EAAE,gBAAgB,EAAE,CAAC;AACrD,CAAC","sourcesContent":["// What an upload row shows besides its title: the file's size, its kind, and when it was accepted.\n// The mechanics are shared; every word, the relative-time cutoff and the locale are the app's.\n\nimport { getSimplifiedMediaType } from \"@ttt-productions/media-schemas\";\n\n/** A byte count as a short size: `850 B`, `24 KB`, `1.2 MB`, `3 GB`. `null` for no usable count. */\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\n/** The app's words for an upload row's time and file kind. */\nexport interface UploadDisplayCopy {\n /** Under a minute old. */\n justNow: string;\n minutesAgo: (minutes: number) => string;\n hoursAgo: (hours: number) => string;\n /** Accepted on the previous calendar day, given the local clock time. */\n yesterdayAt: (time: string) => string;\n /** Accepted before yesterday, given the short local date and clock time. */\n dateAt: (date: string, time: string) => string;\n /** The file's kind, by its simplified media type; `other` for anything else. */\n fileType: { image: string; video: string; audio: string; other: string };\n}\n\nexport interface UploadDisplayConfig {\n copy: UploadDisplayCopy;\n /** How long a row's time reads as relative before it reads as a clock time. */\n relativeTimeCutoffMs: number;\n /** Locale for the clock time and date; the runtime's own when omitted. */\n locale?: Intl.LocalesArgument;\n}\n\nexport interface UploadDisplayFormatters {\n /**\n * An upload's accept time. Within the cutoff of now: just now, minutes ago, or hours ago. Older:\n * the clock time for today, yesterday and the clock time, or the short date and the clock time —\n * by local calendar day.\n */\n formatUploadTimestamp: (createdAtMs: number, nowMs: number) => string;\n /** The file's kind from its stored content type; `null` when it has none. */\n getFileTypeLabel: (contentType: string | undefined) => string | null;\n}\n\nfunction sameCalendarDay(a: Date, b: Date): boolean {\n return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();\n}\n\nexport function createUploadDisplayFormatters(config: UploadDisplayConfig): 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 // The previous calendar day, found by stepping the date rather than subtracting 24 hours: a\n // day that crosses a daylight-saving change is 23 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(created.toLocaleDateString(locale, { month: \"short\", day: \"numeric\" }), time);\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"]}
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.16",
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.15",
44
- "@ttt-productions/media-schemas": "^0.14.10",
45
- "@ttt-productions/ui-core": "^0.11.33",
46
- "@ttt-productions/upload-core": "^0.7.5"
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"]}