@orthacms/media-admin 0.0.0-reserve.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +7 -0
  3. package/dist/index.d.ts +3 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +1 -0
  6. package/dist/lib/components/AssetActionsMenu/index.d.ts +37 -0
  7. package/dist/lib/components/AssetActionsMenu/index.d.ts.map +1 -0
  8. package/dist/lib/components/AssetActionsMenu/index.js +57 -0
  9. package/dist/lib/components/AssetDetailDrawer/MetaRow/index.d.ts +11 -0
  10. package/dist/lib/components/AssetDetailDrawer/MetaRow/index.d.ts.map +1 -0
  11. package/dist/lib/components/AssetDetailDrawer/MetaRow/index.js +9 -0
  12. package/dist/lib/components/AssetDetailDrawer/index.d.ts +28 -0
  13. package/dist/lib/components/AssetDetailDrawer/index.d.ts.map +1 -0
  14. package/dist/lib/components/AssetDetailDrawer/index.js +93 -0
  15. package/dist/lib/components/EntryMediaTab/MediaFieldSection/index.d.ts +28 -0
  16. package/dist/lib/components/EntryMediaTab/MediaFieldSection/index.d.ts.map +1 -0
  17. package/dist/lib/components/EntryMediaTab/MediaFieldSection/index.js +52 -0
  18. package/dist/lib/components/EntryMediaTab/index.d.ts +11 -0
  19. package/dist/lib/components/EntryMediaTab/index.d.ts.map +1 -0
  20. package/dist/lib/components/EntryMediaTab/index.js +31 -0
  21. package/dist/lib/components/FolderActionsMenu/index.d.ts +21 -0
  22. package/dist/lib/components/FolderActionsMenu/index.d.ts.map +1 -0
  23. package/dist/lib/components/FolderActionsMenu/index.js +25 -0
  24. package/dist/lib/components/MediaAltTextWidget/index.d.ts +9 -0
  25. package/dist/lib/components/MediaAltTextWidget/index.d.ts.map +1 -0
  26. package/dist/lib/components/MediaAltTextWidget/index.js +66 -0
  27. package/dist/lib/components/MediaBreadcrumbs/index.d.ts +11 -0
  28. package/dist/lib/components/MediaBreadcrumbs/index.d.ts.map +1 -0
  29. package/dist/lib/components/MediaEmptyState/index.d.ts +13 -0
  30. package/dist/lib/components/MediaEmptyState/index.d.ts.map +1 -0
  31. package/dist/lib/components/MediaEmptyState/index.js +38 -0
  32. package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.d.ts +34 -0
  33. package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.d.ts.map +1 -0
  34. package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.js +117 -0
  35. package/dist/lib/components/MediaFieldControl/index.d.ts +61 -0
  36. package/dist/lib/components/MediaFieldControl/index.d.ts.map +1 -0
  37. package/dist/lib/components/MediaFieldControl/index.js +422 -0
  38. package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts +17 -0
  39. package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts.map +1 -0
  40. package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.js +37 -0
  41. package/dist/lib/components/MediaFoldersNav/index.d.ts +22 -0
  42. package/dist/lib/components/MediaFoldersNav/index.d.ts.map +1 -0
  43. package/dist/lib/components/MediaFoldersNav/index.js +40 -0
  44. package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts +18 -0
  45. package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts.map +1 -0
  46. package/dist/lib/components/MediaGrid/MediaAssetCard/index.js +29 -0
  47. package/dist/lib/components/MediaGrid/MediaFolderCard/index.d.ts +16 -0
  48. package/dist/lib/components/MediaGrid/MediaFolderCard/index.d.ts.map +1 -0
  49. package/dist/lib/components/MediaGrid/MediaFolderCard/index.js +25 -0
  50. package/dist/lib/components/MediaGrid/index.d.ts +24 -0
  51. package/dist/lib/components/MediaGrid/index.d.ts.map +1 -0
  52. package/dist/lib/components/MediaGrid/index.js +20 -0
  53. package/dist/lib/components/MediaKindBadge/index.d.ts +10 -0
  54. package/dist/lib/components/MediaKindBadge/index.d.ts.map +1 -0
  55. package/dist/lib/components/MediaKindBadge/index.js +30 -0
  56. package/dist/lib/components/MediaKindIcon/index.d.ts +11 -0
  57. package/dist/lib/components/MediaKindIcon/index.d.ts.map +1 -0
  58. package/dist/lib/components/MediaKindIcon/index.js +20 -0
  59. package/dist/lib/components/MediaLibrarySkeleton/index.d.ts +18 -0
  60. package/dist/lib/components/MediaLibrarySkeleton/index.d.ts.map +1 -0
  61. package/dist/lib/components/MediaLibrarySkeleton/index.js +43 -0
  62. package/dist/lib/components/MediaPagination/index.d.ts +26 -0
  63. package/dist/lib/components/MediaPagination/index.d.ts.map +1 -0
  64. package/dist/lib/components/MediaPagination/index.js +52 -0
  65. package/dist/lib/components/MediaPickerDialog/MediaPickerTile/index.d.ts +17 -0
  66. package/dist/lib/components/MediaPickerDialog/MediaPickerTile/index.d.ts.map +1 -0
  67. package/dist/lib/components/MediaPickerDialog/MediaPickerTile/index.js +43 -0
  68. package/dist/lib/components/MediaPickerDialog/index.d.ts +21 -0
  69. package/dist/lib/components/MediaPickerDialog/index.d.ts.map +1 -0
  70. package/dist/lib/components/MediaPickerDialog/index.js +208 -0
  71. package/dist/lib/components/MediaSelectionBar/index.d.ts +18 -0
  72. package/dist/lib/components/MediaSelectionBar/index.d.ts.map +1 -0
  73. package/dist/lib/components/MediaSelectionBar/index.js +26 -0
  74. package/dist/lib/components/MediaStorageStat/index.d.ts +9 -0
  75. package/dist/lib/components/MediaStorageStat/index.d.ts.map +1 -0
  76. package/dist/lib/components/MediaStorageStat/index.js +31 -0
  77. package/dist/lib/components/MediaStorageWidget/index.d.ts +10 -0
  78. package/dist/lib/components/MediaStorageWidget/index.d.ts.map +1 -0
  79. package/dist/lib/components/MediaStorageWidget/index.js +93 -0
  80. package/dist/lib/components/MediaTable/MediaAssetRow/index.d.ts +17 -0
  81. package/dist/lib/components/MediaTable/MediaAssetRow/index.d.ts.map +1 -0
  82. package/dist/lib/components/MediaTable/MediaFolderRow/index.d.ts +16 -0
  83. package/dist/lib/components/MediaTable/MediaFolderRow/index.d.ts.map +1 -0
  84. package/dist/lib/components/MediaTable/index.d.ts +26 -0
  85. package/dist/lib/components/MediaTable/index.d.ts.map +1 -0
  86. package/dist/lib/components/MediaThumbnail/index.d.ts +19 -0
  87. package/dist/lib/components/MediaThumbnail/index.d.ts.map +1 -0
  88. package/dist/lib/components/MediaThumbnail/index.js +26 -0
  89. package/dist/lib/components/MediaToolbar/index.d.ts +19 -0
  90. package/dist/lib/components/MediaToolbar/index.d.ts.map +1 -0
  91. package/dist/lib/components/MediaToolbar/index.js +62 -0
  92. package/dist/lib/components/MediaTopBar/index.d.ts +20 -0
  93. package/dist/lib/components/MediaTopBar/index.d.ts.map +1 -0
  94. package/dist/lib/components/MediaTopBar/index.js +35 -0
  95. package/dist/lib/components/MediaUploadBanner/MediaUploadRow/index.d.ts +14 -0
  96. package/dist/lib/components/MediaUploadBanner/MediaUploadRow/index.d.ts.map +1 -0
  97. package/dist/lib/components/MediaUploadBanner/MediaUploadRow/index.js +51 -0
  98. package/dist/lib/components/MediaUploadBanner/index.d.ts +24 -0
  99. package/dist/lib/components/MediaUploadBanner/index.d.ts.map +1 -0
  100. package/dist/lib/components/MediaUploadBanner/index.js +99 -0
  101. package/dist/lib/components/MediaUploadsWidget/index.d.ts +7 -0
  102. package/dist/lib/components/MediaUploadsWidget/index.d.ts.map +1 -0
  103. package/dist/lib/components/MediaUploadsWidget/index.js +70 -0
  104. package/dist/lib/components/MoveAssetsDialog/DestinationOption/index.d.ts +19 -0
  105. package/dist/lib/components/MoveAssetsDialog/DestinationOption/index.d.ts.map +1 -0
  106. package/dist/lib/components/MoveAssetsDialog/DestinationOption/index.js +10 -0
  107. package/dist/lib/components/MoveAssetsDialog/index.d.ts +18 -0
  108. package/dist/lib/components/MoveAssetsDialog/index.d.ts.map +1 -0
  109. package/dist/lib/components/MoveAssetsDialog/index.js +41 -0
  110. package/dist/lib/components/NewFolderDialog/index.d.ts +13 -0
  111. package/dist/lib/components/NewFolderDialog/index.d.ts.map +1 -0
  112. package/dist/lib/components/NewFolderDialog/index.js +45 -0
  113. package/dist/lib/components/RenameDialog/index.d.ts +13 -0
  114. package/dist/lib/components/RenameDialog/index.d.ts.map +1 -0
  115. package/dist/lib/components/RenameDialog/index.js +37 -0
  116. package/dist/lib/components/UploadDialog/StagedFileRow/index.d.ts +25 -0
  117. package/dist/lib/components/UploadDialog/StagedFileRow/index.d.ts.map +1 -0
  118. package/dist/lib/components/UploadDialog/StagedFileRow/index.js +82 -0
  119. package/dist/lib/components/UploadDialog/index.d.ts +50 -0
  120. package/dist/lib/components/UploadDialog/index.d.ts.map +1 -0
  121. package/dist/lib/components/UploadDialog/index.js +120 -0
  122. package/dist/lib/components/WysiwygLibrarySource/index.d.ts +14 -0
  123. package/dist/lib/components/WysiwygLibrarySource/index.d.ts.map +1 -0
  124. package/dist/lib/components/WysiwygLibrarySource/index.js +28 -0
  125. package/dist/lib/components/WysiwygUploadSource/index.d.ts +25 -0
  126. package/dist/lib/components/WysiwygUploadSource/index.d.ts.map +1 -0
  127. package/dist/lib/components/WysiwygUploadSource/index.js +102 -0
  128. package/dist/lib/constants/index.d.ts +82 -0
  129. package/dist/lib/constants/index.d.ts.map +1 -0
  130. package/dist/lib/constants/index.js +75 -0
  131. package/dist/lib/hooks/useMediaInsights/index.d.ts +32 -0
  132. package/dist/lib/hooks/useMediaInsights/index.d.ts.map +1 -0
  133. package/dist/lib/hooks/useMediaInsights/index.js +85 -0
  134. package/dist/lib/hooks/useMediaLibrary/index.d.ts +83 -0
  135. package/dist/lib/hooks/useMediaLibrary/index.d.ts.map +1 -0
  136. package/dist/lib/hooks/useMediaLibrary/index.js +341 -0
  137. package/dist/lib/hooks/usePendingMediaUploads/index.d.ts +26 -0
  138. package/dist/lib/hooks/usePendingMediaUploads/index.d.ts.map +1 -0
  139. package/dist/lib/hooks/usePendingMediaUploads/index.js +217 -0
  140. package/dist/lib/hooks/useUploadQueue/index.d.ts +33 -0
  141. package/dist/lib/hooks/useUploadQueue/index.d.ts.map +1 -0
  142. package/dist/lib/hooks/useUploadQueue/index.js +218 -0
  143. package/dist/lib/hooks/useWysiwygAssetPicker/index.d.ts +23 -0
  144. package/dist/lib/hooks/useWysiwygAssetPicker/index.d.ts.map +1 -0
  145. package/dist/lib/infrastructure/apiMessage/index.d.ts +17 -0
  146. package/dist/lib/infrastructure/apiMessage/index.d.ts.map +1 -0
  147. package/dist/lib/infrastructure/apiMessage/index.js +30 -0
  148. package/dist/lib/infrastructure/httpMediaGateway/index.d.ts +9 -0
  149. package/dist/lib/infrastructure/httpMediaGateway/index.d.ts.map +1 -0
  150. package/dist/lib/infrastructure/httpMediaGateway/index.js +157 -0
  151. package/dist/lib/infrastructure/httpMediaInsightsGateway/index.d.ts +8 -0
  152. package/dist/lib/infrastructure/httpMediaInsightsGateway/index.d.ts.map +1 -0
  153. package/dist/lib/infrastructure/httpMediaInsightsGateway/index.js +35 -0
  154. package/dist/lib/infrastructure/mediaGateway/index.d.ts +120 -0
  155. package/dist/lib/infrastructure/mediaGateway/index.d.ts.map +1 -0
  156. package/dist/lib/infrastructure/mediaGateway/index.js +1 -0
  157. package/dist/lib/infrastructure/mediaInsightsGateway/index.d.ts +46 -0
  158. package/dist/lib/infrastructure/mediaInsightsGateway/index.d.ts.map +1 -0
  159. package/dist/lib/infrastructure/mediaInsightsGateway/index.js +6 -0
  160. package/dist/lib/infrastructure/mediaKeys/index.d.ts +23 -0
  161. package/dist/lib/infrastructure/mediaKeys/index.d.ts.map +1 -0
  162. package/dist/lib/infrastructure/mediaKeys/index.js +11 -0
  163. package/dist/lib/infrastructure/mediaMapper/index.d.ts +50 -0
  164. package/dist/lib/infrastructure/mediaMapper/index.d.ts.map +1 -0
  165. package/dist/lib/infrastructure/mediaMapper/index.js +39 -0
  166. package/dist/lib/pages/MediaLibraryPage/index.d.ts +23 -0
  167. package/dist/lib/pages/MediaLibraryPage/index.d.ts.map +1 -0
  168. package/dist/lib/pages/MediaLibraryPage/index.js +425 -0
  169. package/dist/lib/types/mediaAsset/index.d.ts +44 -0
  170. package/dist/lib/types/mediaAsset/index.d.ts.map +1 -0
  171. package/dist/lib/types/mediaAsset/index.js +1 -0
  172. package/dist/lib/types/mediaFieldDisplay/index.d.ts +48 -0
  173. package/dist/lib/types/mediaFieldDisplay/index.d.ts.map +1 -0
  174. package/dist/lib/types/mediaFieldDisplay/index.js +1 -0
  175. package/dist/lib/types/mediaFolder/index.d.ts +16 -0
  176. package/dist/lib/types/mediaFolder/index.d.ts.map +1 -0
  177. package/dist/lib/types/mediaFolder/index.js +1 -0
  178. package/dist/lib/types/pendingUpload/index.d.ts +38 -0
  179. package/dist/lib/types/pendingUpload/index.d.ts.map +1 -0
  180. package/dist/lib/types/pendingUpload/index.js +1 -0
  181. package/dist/lib/types/upload/index.d.ts +37 -0
  182. package/dist/lib/types/upload/index.d.ts.map +1 -0
  183. package/dist/lib/types/upload/index.js +1 -0
  184. package/dist/lib/utils/assetGradient/index.d.ts +8 -0
  185. package/dist/lib/utils/assetGradient/index.d.ts.map +1 -0
  186. package/dist/lib/utils/assetGradient/index.js +16 -0
  187. package/dist/lib/utils/assetHandlers/index.d.ts +11 -0
  188. package/dist/lib/utils/assetHandlers/index.d.ts.map +1 -0
  189. package/dist/lib/utils/assetHandlers/index.js +17 -0
  190. package/dist/lib/utils/folderContents/index.d.ts +17 -0
  191. package/dist/lib/utils/folderContents/index.d.ts.map +1 -0
  192. package/dist/lib/utils/folderContents/index.js +35 -0
  193. package/dist/lib/utils/folderTree/index.d.ts +13 -0
  194. package/dist/lib/utils/folderTree/index.d.ts.map +1 -0
  195. package/dist/lib/utils/folderTree/index.js +15 -0
  196. package/dist/lib/utils/formatBytes/index.d.ts +6 -0
  197. package/dist/lib/utils/formatBytes/index.d.ts.map +1 -0
  198. package/dist/lib/utils/formatBytes/index.js +17 -0
  199. package/dist/lib/utils/formatDuration/index.d.ts +6 -0
  200. package/dist/lib/utils/formatDuration/index.d.ts.map +1 -0
  201. package/dist/lib/utils/formatDuration/index.js +13 -0
  202. package/dist/lib/utils/kindFromMime/index.d.ts +10 -0
  203. package/dist/lib/utils/kindFromMime/index.d.ts.map +1 -0
  204. package/dist/lib/utils/kindFromMime/index.js +27 -0
  205. package/dist/lib/utils/mediaAccept/index.d.ts +30 -0
  206. package/dist/lib/utils/mediaAccept/index.d.ts.map +1 -0
  207. package/dist/lib/utils/mediaAccept/index.js +63 -0
  208. package/dist/lib/utils/mediaPlugin/index.d.ts +15 -0
  209. package/dist/lib/utils/mediaPlugin/index.d.ts.map +1 -0
  210. package/dist/lib/utils/mediaPlugin/index.js +177 -0
  211. package/dist/lib/utils/toWysiwygEmbed/index.d.ts +23 -0
  212. package/dist/lib/utils/toWysiwygEmbed/index.d.ts.map +1 -0
  213. package/dist/lib/utils/toWysiwygEmbed/index.js +44 -0
  214. package/package.json +50 -0
@@ -0,0 +1,120 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useRef, useState } from 'react';
3
+ import { defineMessages, useIntl } from 'react-intl';
4
+ import { Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, cn } from '@orthacms/design-system';
5
+ import { UploadCloud } from 'lucide-react';
6
+ import { StagedFileRow } from './StagedFileRow';
7
+ /** Stable default for `initialFiles`, so the seeding effect isn't re-run. */
8
+ const NO_FILES = [];
9
+ /** Intl descriptors for {@link UploadDialog}, co-located. */
10
+ const messages = defineMessages({
11
+ title: { id: 'media.upload.title', defaultMessage: 'Upload assets' },
12
+ description: {
13
+ id: 'media.upload.description',
14
+ defaultMessage: 'Files upload to {location}.'
15
+ },
16
+ dropHere: {
17
+ id: 'media.upload.dropHere',
18
+ defaultMessage: 'Drag & drop files here'
19
+ },
20
+ or: { id: 'media.upload.or', defaultMessage: 'or' },
21
+ browse: { id: 'media.upload.browse', defaultMessage: 'Browse files' },
22
+ /**
23
+ * The `sr-only` input is still a tab stop, so it needs a name of its own —
24
+ * axe flagged it as an unlabelled form element the moment the dialog's
25
+ * staged state was scanned. Distinct from the visible "Browse files" button
26
+ * that clicks it, so the two don't read as duplicates.
27
+ */
28
+ fileInput: {
29
+ id: 'media.upload.fileInput',
30
+ defaultMessage: 'Choose files to upload'
31
+ },
32
+ /**
33
+ * No size is quoted. The cap is `config.plugins.media.maxUploadBytes`
34
+ * (50 MB by default) and the admin has no route that reports it, so the old
35
+ * "up to 250 MB each" was a number nothing checked — a 52 MB file staged
36
+ * happily and then 413'd after transferring in full.
37
+ */
38
+ hint: {
39
+ id: 'media.upload.hint',
40
+ defaultMessage: 'Images, video, audio, and documents.'
41
+ },
42
+ staged: {
43
+ id: 'media.upload.staged',
44
+ defaultMessage: '{count, plural, one {# file ready} other {# files ready}}'
45
+ },
46
+ cancel: { id: 'media.upload.cancel', defaultMessage: 'Cancel' },
47
+ confirm: {
48
+ id: 'media.upload.confirm',
49
+ defaultMessage: 'Upload {count, plural, one {# file} other {# files}}'
50
+ }
51
+ });
52
+ /**
53
+ * The upload modal — a drag-and-drop zone plus a multi-file picker that stages
54
+ * the chosen files into a removable, **previewed** list before committing:
55
+ * images render a real thumbnail off a local object URL (see
56
+ * {@link StagedFileRow}), other kinds a glyph, so a wrong file is caught before
57
+ * a byte moves. Files accumulate across several drops/picks.
58
+ *
59
+ * On confirm the parent's `onUpload` receives the real `File` objects and the
60
+ * dialog closes immediately — progress then lives in the caller's upload banner,
61
+ * not here, so the user can keep browsing (and queue more) while bytes move.
62
+ * Staged files reset whenever the dialog reopens, seeded from `initialFiles`.
63
+ *
64
+ * **Shared by both upload entry points** — the Media Library's toolbar and a
65
+ * content record's media field — so staging, previewing, and the copy are
66
+ * identical in the two places. The field narrows it with `multiple` / `accept` /
67
+ * its own `description` + `hint`; everything else is the same dialog.
68
+ */
69
+ export function UploadDialog({ open, onOpenChange, locationLabel, description, hint, confirmLabel, accept, multiple = true, initialFiles = NO_FILES, collectAlt = false, onUpload }) {
70
+ const intl = useIntl();
71
+ const inputRef = useRef(null);
72
+ const [staged, setStaged] = useState([]);
73
+ const [dragging, setDragging] = useState(false);
74
+ useEffect(() => {
75
+ if (open) {
76
+ setStaged(initialFiles.map((file) => ({ file })));
77
+ setDragging(false);
78
+ }
79
+ }, [open, initialFiles]);
80
+ const addFiles = (files) => {
81
+ if (!files)
82
+ return;
83
+ const picked = Array.from(files).map((file) => ({ file }));
84
+ // A single-asset field holds one file: the newest pick replaces the last.
85
+ setStaged((prev) => multiple ? [...prev, ...picked] : picked.slice(0, 1));
86
+ };
87
+ const removeAt = (index) => {
88
+ setStaged((prev) => prev.filter((_, i) => i !== index));
89
+ };
90
+ const setAltAt = (index, alt) => {
91
+ setStaged((prev) => prev.map((item, i) => (i === index ? { ...item, alt } : item)));
92
+ };
93
+ const submit = () => {
94
+ if (staged.length === 0)
95
+ return;
96
+ onUpload(staged);
97
+ onOpenChange(false);
98
+ };
99
+ return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, children: _jsxs(DialogContent, { className: "sm:max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: intl.formatMessage(messages.title) }), _jsx(DialogDescription, { children: description ??
100
+ intl.formatMessage(messages.description, {
101
+ location: locationLabel
102
+ }) })] }), _jsxs("div", { onDragOver: (event) => {
103
+ event.preventDefault();
104
+ setDragging(true);
105
+ }, onDragLeave: () => setDragging(false), onDrop: (event) => {
106
+ event.preventDefault();
107
+ setDragging(false);
108
+ addFiles(event.dataTransfer.files);
109
+ }, className: cn('flex flex-col items-center justify-center gap-2 rounded-xl border-2 border-dashed px-6 py-10 text-center transition-colors', dragging
110
+ ? 'border-primary bg-primary/5'
111
+ : 'border-border bg-muted/30'), children: [_jsx(UploadCloud, { className: "size-8 text-muted-foreground", "aria-hidden": true }), _jsx("p", { className: "text-sm font-medium", children: intl.formatMessage(messages.dropHere) }), _jsx("p", { className: "text-xs text-muted-foreground", children: intl.formatMessage(messages.or) }), _jsx(Button, { variant: "outline", size: "sm", className: "shadow-none", onClick: () => inputRef.current?.click(), children: intl.formatMessage(messages.browse) }), _jsx("p", { className: "mt-1 text-xs text-muted-foreground", children: hint ?? intl.formatMessage(messages.hint) }), _jsx("input", { ref: inputRef, type: "file", accept: accept, multiple: multiple, "aria-label": intl.formatMessage(messages.fileInput), className: "sr-only", onChange: (event) => {
112
+ addFiles(event.target.files);
113
+ event.target.value = '';
114
+ } })] }), staged.length > 0 ? (_jsxs("div", { className: "space-y-2", children: [_jsx("p", { className: "text-xs font-medium text-muted-foreground", children: intl.formatMessage(messages.staged, {
115
+ count: staged.length
116
+ }) }), _jsx("ul", { className: "flex max-h-56 flex-col gap-1 overflow-auto", children: staged.map((item, index) => (_jsx(StagedFileRow, { file: item.file, alt: item.alt ?? '', collectAlt: collectAlt, onAltChange: (value) => setAltAt(index, value), onRemove: () => removeAt(index) }, `${item.file.name}-${item.file.lastModified}-${index}`))) })] })) : null, _jsxs(DialogFooter, { children: [_jsx(Button, { variant: "outline", onClick: () => onOpenChange(false), children: intl.formatMessage(messages.cancel) }), _jsx(Button, { onClick: submit, disabled: staged.length === 0, children: confirmLabel ??
117
+ intl.formatMessage(messages.confirm, {
118
+ count: staged.length
119
+ }) })] })] }) }));
120
+ }
@@ -0,0 +1,14 @@
1
+ import type { WysiwygMediaSourceContext } from '@orthacms/wysiwyg-admin';
2
+ /**
3
+ * The Media Library as a **rich-text media source** — the plugin's contribution
4
+ * to `WYSIWYG_MEDIA_SLOT`.
5
+ *
6
+ * It is the same {@link MediaPickerDialog} a media *field* opens, which is the
7
+ * point: browsing, searching, and folder navigation are one implementation, and
8
+ * an author moving between a media field and a body meets the same picker.
9
+ *
10
+ * `multiple` is on, because placing three images in a row is a normal thing to
11
+ * do in a body and re-opening the picker twice for it is not.
12
+ */
13
+ export declare function WysiwygLibrarySource({ open, onOpenChange, accept, onInsert }: WysiwygMediaSourceContext): import("react").JSX.Element;
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/WysiwygLibrarySource/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAKzE;;;;;;;;;;GAUG;AACH,wBAAgB,oBAAoB,CAAC,EACjC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,QAAQ,EACX,EAAE,yBAAyB,+BAmB3B"}
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { MediaPickerDialog } from '../MediaPickerDialog';
3
+ import { toWysiwygEmbed } from '../../utils/toWysiwygEmbed';
4
+ /**
5
+ * The Media Library as a **rich-text media source** — the plugin's contribution
6
+ * to `WYSIWYG_MEDIA_SLOT`.
7
+ *
8
+ * It is the same {@link MediaPickerDialog} a media *field* opens, which is the
9
+ * point: browsing, searching, and folder navigation are one implementation, and
10
+ * an author moving between a media field and a body meets the same picker.
11
+ *
12
+ * `multiple` is on, because placing three images in a row is a normal thing to
13
+ * do in a body and re-opening the picker twice for it is not.
14
+ */
15
+ export function WysiwygLibrarySource({ open, onOpenChange, accept, onInsert }) {
16
+ return (_jsx(MediaPickerDialog, { open: open, onOpenChange: onOpenChange, multiple: true,
17
+ // The editor's kinds, handed to the picker as its own restriction —
18
+ // so an audio file or a PDF is filtered out of the grid rather than
19
+ // being pickable and then silently dropped on insert.
20
+ accept: { kinds: [...accept] }, onConfirm: (assets) => {
21
+ const embeds = assets
22
+ .map(toWysiwygEmbed)
23
+ .filter((embed) => embed !== null);
24
+ if (embeds.length > 0)
25
+ onInsert(embeds);
26
+ onOpenChange(false);
27
+ } }));
28
+ }
@@ -0,0 +1,25 @@
1
+ import type { WysiwygMediaSourceContext } from '@orthacms/wysiwyg-admin';
2
+ /**
3
+ * Upload as a **rich-text media source** — the plugin's second contribution to
4
+ * `WYSIWYG_MEDIA_SLOT`.
5
+ *
6
+ * Files go **into the Media Library** and are then placed in the text, rather
7
+ * than being embedded as data URIs or attached to the record alone. That is the
8
+ * behaviour a CMS wants: an image used in a body is an asset like any other —
9
+ * findable, reusable, replaceable — and a body full of base64 is unshippable.
10
+ *
11
+ * ### Why this uploads immediately, unlike a media *field*
12
+ *
13
+ * A media field defers its uploads to the record's save
14
+ * (`usePendingMediaUploads`), because a field holds an id and an abandoned edit
15
+ * would otherwise litter the library. A body holds a **URL**, and there is no
16
+ * URL until the bytes exist — so the upload has to happen now. The trade is
17
+ * real and one-directional: abandoning the edit leaves the asset in the
18
+ * library, where it is visible and deletable, rather than leaving the body
19
+ * pointing at nothing.
20
+ *
21
+ * Each file is its own request, so one failure costs one file: the rest still
22
+ * insert, and the toast names the one that didn't.
23
+ */
24
+ export declare function WysiwygUploadSource({ open, onOpenChange, accept, onInsert }: WysiwygMediaSourceContext): import("react").JSX.Element;
25
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/WysiwygUploadSource/index.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,yBAAyB,EAAE,MAAM,yBAAyB,CAAC;AAoCzE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,mBAAmB,CAAC,EAChC,IAAI,EACJ,YAAY,EACZ,MAAM,EACN,QAAQ,EACX,EAAE,yBAAyB,+BAgE3B"}
@@ -0,0 +1,102 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useState } from 'react';
3
+ import { defineMessages, useIntl } from 'react-intl';
4
+ import { useQueryClient } from '@tanstack/react-query';
5
+ import { toast } from '@orthacms/design-system';
6
+ import { useCurrentWorkspace } from '@orthacms/workspaces-admin';
7
+ import { ROOT_FOLDER_ID } from '../../constants';
8
+ import { httpMediaGateway } from '../../infrastructure/httpMediaGateway';
9
+ import { mediaKeys } from '../../infrastructure/mediaKeys';
10
+ import { acceptsFile } from '../../utils/mediaAccept';
11
+ import { toWysiwygEmbed } from '../../utils/toWysiwygEmbed';
12
+ import { UploadDialog } from '../UploadDialog';
13
+ const messages = defineMessages({
14
+ location: {
15
+ id: 'media.wysiwygUpload.location',
16
+ defaultMessage: 'All media'
17
+ },
18
+ description: {
19
+ id: 'media.wysiwygUpload.description',
20
+ defaultMessage: 'Files are added to the Media Library, then placed in the text.'
21
+ },
22
+ confirm: {
23
+ id: 'media.wysiwygUpload.confirm',
24
+ defaultMessage: 'Upload and insert'
25
+ },
26
+ rejected: {
27
+ id: 'media.wysiwygUpload.rejected',
28
+ defaultMessage: '{name} isn’t an image or a video, so it can’t go in the text.'
29
+ },
30
+ failed: {
31
+ id: 'media.wysiwygUpload.failed',
32
+ defaultMessage: 'Couldn’t upload {name}. Nothing was inserted for it.'
33
+ }
34
+ });
35
+ /** The file-picker `accept` matching the kinds a body can hold. */
36
+ const FILE_ACCEPT = 'image/*,video/*';
37
+ /**
38
+ * Upload as a **rich-text media source** — the plugin's second contribution to
39
+ * `WYSIWYG_MEDIA_SLOT`.
40
+ *
41
+ * Files go **into the Media Library** and are then placed in the text, rather
42
+ * than being embedded as data URIs or attached to the record alone. That is the
43
+ * behaviour a CMS wants: an image used in a body is an asset like any other —
44
+ * findable, reusable, replaceable — and a body full of base64 is unshippable.
45
+ *
46
+ * ### Why this uploads immediately, unlike a media *field*
47
+ *
48
+ * A media field defers its uploads to the record's save
49
+ * (`usePendingMediaUploads`), because a field holds an id and an abandoned edit
50
+ * would otherwise litter the library. A body holds a **URL**, and there is no
51
+ * URL until the bytes exist — so the upload has to happen now. The trade is
52
+ * real and one-directional: abandoning the edit leaves the asset in the
53
+ * library, where it is visible and deletable, rather than leaving the body
54
+ * pointing at nothing.
55
+ *
56
+ * Each file is its own request, so one failure costs one file: the rest still
57
+ * insert, and the toast names the one that didn't.
58
+ */
59
+ export function WysiwygUploadSource({ open, onOpenChange, accept, onInsert }) {
60
+ const intl = useIntl();
61
+ const workspace = useCurrentWorkspace();
62
+ const queryClient = useQueryClient();
63
+ const [busy, setBusy] = useState(false);
64
+ const upload = async (files) => {
65
+ onOpenChange(false);
66
+ if (busy)
67
+ return;
68
+ setBusy(true);
69
+ // Checked before anything is sent: a PDF would upload fine and then be
70
+ // silently unusable, which reads as the editor losing the file.
71
+ const usable = files.filter((file) => {
72
+ if (acceptsFile({ kinds: [...accept] }, file))
73
+ return true;
74
+ toast.error(intl.formatMessage(messages.rejected, { name: file.name }));
75
+ return false;
76
+ });
77
+ let uploaded = false;
78
+ for (const file of usable) {
79
+ try {
80
+ const asset = await httpMediaGateway.uploadFile(ROOT_FOLDER_ID, file);
81
+ uploaded = true;
82
+ const embed = toWysiwygEmbed(asset);
83
+ // Inserted one at a time, in order, so a long batch appears as
84
+ // it lands rather than all at the end.
85
+ if (embed)
86
+ onInsert([embed]);
87
+ }
88
+ catch {
89
+ toast.error(intl.formatMessage(messages.failed, { name: file.name }));
90
+ }
91
+ }
92
+ // The library is stale the moment anything landed — even if a later
93
+ // file failed.
94
+ if (uploaded) {
95
+ void queryClient.invalidateQueries({
96
+ queryKey: mediaKeys.all(workspace.id)
97
+ });
98
+ }
99
+ setBusy(false);
100
+ };
101
+ return (_jsx(UploadDialog, { open: open, onOpenChange: onOpenChange, locationLabel: intl.formatMessage(messages.location), description: intl.formatMessage(messages.description), confirmLabel: intl.formatMessage(messages.confirm), accept: FILE_ACCEPT, multiple: true, onUpload: (uploads) => void upload(uploads.map((staged) => staged.file)) }));
102
+ }
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Shared string constants for the Media Library, kept here so route segments,
3
+ * layout keys, and enum-like option values live in one place instead of being
4
+ * repeated as magic literals across the page, sidebar, toolbar, and dialogs.
5
+ */
6
+ /** Permission required to browse media. Mirrors the (future) server matrix. */
7
+ export declare const MEDIA_READ = "media:read";
8
+ /** Permission required to upload / create assets. */
9
+ export declare const MEDIA_CREATE = "media:create";
10
+ /** Permission required to rename / move / replace assets. */
11
+ export declare const MEDIA_UPDATE = "media:update";
12
+ /** Permission required to delete assets and folders. */
13
+ export declare const MEDIA_DELETE = "media:delete";
14
+ /**
15
+ * Path segment under the workspace shell where the Media Library mounts
16
+ * (`/workspaces/:id/media`). Shared by the plugin's slot contribution and the
17
+ * page so they can never drift apart.
18
+ */
19
+ export declare const MEDIA_SEGMENT = "media";
20
+ /** The synthetic root folder id — the library's top level ("All media"). */
21
+ export declare const ROOT_FOLDER_ID = "root";
22
+ /** Where one queued upload has got to. */
23
+ export declare const UPLOAD_STATUS: {
24
+ /** Queued, waiting for a concurrency slot. */
25
+ readonly Pending: "pending";
26
+ /** Bytes are on the wire. */
27
+ readonly Uploading: "uploading";
28
+ readonly Done: "done";
29
+ readonly Failed: "failed";
30
+ /** Aborted by the user before it finished. */
31
+ readonly Cancelled: "cancelled";
32
+ };
33
+ /** One queued upload's state. */
34
+ export type UploadStatus = (typeof UPLOAD_STATUS)[keyof typeof UPLOAD_STATUS];
35
+ /**
36
+ * How many files upload at once. Above ~3 the browser queues the rest anyway
37
+ * (per-host connection limits) while each one's progress bar crawls, which
38
+ * reads as a stall; a small pool keeps every active bar visibly moving.
39
+ */
40
+ export declare const UPLOAD_CONCURRENCY = 3;
41
+ /** The kinds of asset the library recognises, driving icons and filters. */
42
+ export declare const MEDIA_KIND: {
43
+ readonly Image: "image";
44
+ readonly Video: "video";
45
+ readonly Audio: "audio";
46
+ readonly Document: "document";
47
+ readonly Archive: "archive";
48
+ };
49
+ /** One recognised asset kind. */
50
+ export type MediaKind = (typeof MEDIA_KIND)[keyof typeof MEDIA_KIND];
51
+ /** The special "no filter" sentinel for the kind filter control. */
52
+ export declare const KIND_FILTER_ALL = "all";
53
+ /**
54
+ * Assets per page in the library grid, and the sizes the pager offers.
55
+ *
56
+ * 24 matches the server's own default and divides evenly by the grid's column
57
+ * counts (2/3/4/5), so a full page never leaves a ragged last row. The server
58
+ * caps a page at 100, so nothing above it is offered.
59
+ */
60
+ export declare const DEFAULT_ASSETS_PAGE_SIZE = 24;
61
+ /** The page sizes the library's pager offers, smallest first. */
62
+ export declare const ASSETS_PAGE_SIZE_OPTIONS: number[];
63
+ /**
64
+ * Assets fetched by a surface that browses without a pager — the picker
65
+ * dialog, which narrows by searching rather than by paging. The server's
66
+ * ceiling, so it is one request whatever the folder holds.
67
+ */
68
+ export declare const PICKER_ASSETS_PAGE_SIZE = 100;
69
+ /** How long the search box waits after a keystroke before it queries. */
70
+ export declare const SEARCH_DEBOUNCE_MS = 300;
71
+ /** How the visible assets are ordered. */
72
+ export declare const MEDIA_SORT: {
73
+ readonly NameAsc: "name-asc";
74
+ readonly NameDesc: "name-desc";
75
+ readonly Newest: "newest";
76
+ readonly Oldest: "oldest";
77
+ readonly Largest: "largest";
78
+ readonly Smallest: "smallest";
79
+ };
80
+ /** One asset-ordering option. */
81
+ export type MediaSort = (typeof MEDIA_SORT)[keyof typeof MEDIA_SORT];
82
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/constants/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,+EAA+E;AAC/E,eAAO,MAAM,UAAU,eAAe,CAAC;AAEvC,qDAAqD;AACrD,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C,6DAA6D;AAC7D,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C,wDAAwD;AACxD,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,aAAa,UAAU,CAAC;AAErC,4EAA4E;AAC5E,eAAO,MAAM,cAAc,SAAS,CAAC;AAErC,0CAA0C;AAC1C,eAAO,MAAM,aAAa;IACtB,8CAA8C;;IAE9C,6BAA6B;;;;IAI7B,8CAA8C;;CAExC,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,OAAO,aAAa,CAAC,CAAC;AAE9E;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,4EAA4E;AAC5E,eAAO,MAAM,UAAU;;;;;;CAMb,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAErE,oEAAoE;AACpE,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,UAAe,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAE3C,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,0CAA0C;AAC1C,eAAO,MAAM,UAAU;;;;;;;CAOb,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC"}
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Shared string constants for the Media Library, kept here so route segments,
3
+ * layout keys, and enum-like option values live in one place instead of being
4
+ * repeated as magic literals across the page, sidebar, toolbar, and dialogs.
5
+ */
6
+ /** Permission required to browse media. Mirrors the (future) server matrix. */
7
+ export const MEDIA_READ = 'media:read';
8
+ /** Permission required to upload / create assets. */
9
+ export const MEDIA_CREATE = 'media:create';
10
+ /** Permission required to rename / move / replace assets. */
11
+ export const MEDIA_UPDATE = 'media:update';
12
+ /** Permission required to delete assets and folders. */
13
+ export const MEDIA_DELETE = 'media:delete';
14
+ /**
15
+ * Path segment under the workspace shell where the Media Library mounts
16
+ * (`/workspaces/:id/media`). Shared by the plugin's slot contribution and the
17
+ * page so they can never drift apart.
18
+ */
19
+ export const MEDIA_SEGMENT = 'media';
20
+ /** The synthetic root folder id — the library's top level ("All media"). */
21
+ export const ROOT_FOLDER_ID = 'root';
22
+ /** Where one queued upload has got to. */
23
+ export const UPLOAD_STATUS = {
24
+ /** Queued, waiting for a concurrency slot. */
25
+ Pending: 'pending',
26
+ /** Bytes are on the wire. */
27
+ Uploading: 'uploading',
28
+ Done: 'done',
29
+ Failed: 'failed',
30
+ /** Aborted by the user before it finished. */
31
+ Cancelled: 'cancelled'
32
+ };
33
+ /**
34
+ * How many files upload at once. Above ~3 the browser queues the rest anyway
35
+ * (per-host connection limits) while each one's progress bar crawls, which
36
+ * reads as a stall; a small pool keeps every active bar visibly moving.
37
+ */
38
+ export const UPLOAD_CONCURRENCY = 3;
39
+ /** The kinds of asset the library recognises, driving icons and filters. */
40
+ export const MEDIA_KIND = {
41
+ Image: 'image',
42
+ Video: 'video',
43
+ Audio: 'audio',
44
+ Document: 'document',
45
+ Archive: 'archive'
46
+ };
47
+ /** The special "no filter" sentinel for the kind filter control. */
48
+ export const KIND_FILTER_ALL = 'all';
49
+ /**
50
+ * Assets per page in the library grid, and the sizes the pager offers.
51
+ *
52
+ * 24 matches the server's own default and divides evenly by the grid's column
53
+ * counts (2/3/4/5), so a full page never leaves a ragged last row. The server
54
+ * caps a page at 100, so nothing above it is offered.
55
+ */
56
+ export const DEFAULT_ASSETS_PAGE_SIZE = 24;
57
+ /** The page sizes the library's pager offers, smallest first. */
58
+ export const ASSETS_PAGE_SIZE_OPTIONS = [24, 48, 96];
59
+ /**
60
+ * Assets fetched by a surface that browses without a pager — the picker
61
+ * dialog, which narrows by searching rather than by paging. The server's
62
+ * ceiling, so it is one request whatever the folder holds.
63
+ */
64
+ export const PICKER_ASSETS_PAGE_SIZE = 100;
65
+ /** How long the search box waits after a keystroke before it queries. */
66
+ export const SEARCH_DEBOUNCE_MS = 300;
67
+ /** How the visible assets are ordered. */
68
+ export const MEDIA_SORT = {
69
+ NameAsc: 'name-asc',
70
+ NameDesc: 'name-desc',
71
+ Newest: 'newest',
72
+ Oldest: 'oldest',
73
+ Largest: 'largest',
74
+ Smallest: 'smallest'
75
+ };
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Query keys for the media Insights caches.
3
+ *
4
+ * Workspace-scoped for the same reason as every other media key: the workspace
5
+ * only reaches the server as an ambient header, which is never sent on a cache
6
+ * hit — so without the id in the key, switching workspaces would serve the
7
+ * previous one's numbers and never refetch.
8
+ */
9
+ export declare const mediaInsightsKeys: {
10
+ all: (workspaceId: string) => readonly ["media-insights", string];
11
+ storage: (workspaceId: string) => readonly ["media-insights", string, "storage"];
12
+ uploads: (workspaceId: string, days: number) => readonly ["media-insights", string, "uploads", number];
13
+ alt: (workspaceId: string) => readonly ["media-insights", string, "alt"];
14
+ };
15
+ /**
16
+ * Loads assets and bytes per media kind.
17
+ *
18
+ * Both the Overview storage tile and the full storage widget call this; one
19
+ * query key means one request serves both while each keeps its own state.
20
+ */
21
+ export declare function useMediaStorage(): import("@tanstack/react-query").UseQueryResult<NoInfer<import("../../infrastructure/mediaInsightsGateway").MediaStorage>, Error>;
22
+ /** Loads assets uploaded per time bucket across the selected range. */
23
+ export declare function useMediaUploads(): import("@tanstack/react-query").UseQueryResult<NoInfer<import("../../infrastructure/mediaInsightsGateway").MediaUploads>, Error>;
24
+ /**
25
+ * Loads alt-text coverage across the workspace's images.
26
+ *
27
+ * Takes no range: accessibility debt is a standing total, not something that
28
+ * happened in the last 30 days, and windowing it would make the number shrink
29
+ * whenever someone narrowed the range.
30
+ */
31
+ export declare function useMediaAltCoverage(): import("@tanstack/react-query").UseQueryResult<NoInfer<import("../../infrastructure/mediaInsightsGateway").MediaAltCoverage>, Error>;
32
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/hooks/useMediaInsights/index.ts"],"names":[],"mappings":"AAUA;;;;;;;GAOG;AACH,eAAO,MAAM,iBAAiB;uBACP,MAAM;2BACF,MAAM;2BAEN,MAAM,QAAQ,MAAM;uBAExB,MAAM;CAE5B,CAAC;AAEF;;;;;GAKG;AACH,wBAAgB,eAAe,qIAgB9B;AAED,uEAAuE;AACvE,wBAAgB,eAAe,qIAiB9B;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,yIAgBlC"}
@@ -0,0 +1,85 @@
1
+ import { useQuery } from '@tanstack/react-query';
2
+ import { STALE_TIME } from '@orthacms/utils-admin';
3
+ import { useCurrentWorkspace } from '@orthacms/workspaces-admin';
4
+ import { useHasPermission } from '@orthacms/identity-admin';
5
+ import { useInsightsRange } from '@orthacms/insights-admin';
6
+ import { httpMediaInsightsGateway } from '../../infrastructure/httpMediaInsightsGateway';
7
+ /** Permission every media Insights read is gated on. */
8
+ const MEDIA_READ = 'media:read';
9
+ /**
10
+ * Query keys for the media Insights caches.
11
+ *
12
+ * Workspace-scoped for the same reason as every other media key: the workspace
13
+ * only reaches the server as an ambient header, which is never sent on a cache
14
+ * hit — so without the id in the key, switching workspaces would serve the
15
+ * previous one's numbers and never refetch.
16
+ */
17
+ export const mediaInsightsKeys = {
18
+ all: (workspaceId) => ['media-insights', workspaceId],
19
+ storage: (workspaceId) => ['media-insights', workspaceId, 'storage'],
20
+ uploads: (workspaceId, days) => ['media-insights', workspaceId, 'uploads', days],
21
+ alt: (workspaceId) => ['media-insights', workspaceId, 'alt']
22
+ };
23
+ /**
24
+ * Loads assets and bytes per media kind.
25
+ *
26
+ * Both the Overview storage tile and the full storage widget call this; one
27
+ * query key means one request serves both while each keeps its own state.
28
+ */
29
+ export function useMediaStorage() {
30
+ const workspace = useCurrentWorkspace();
31
+ const canRead = useHasPermission(MEDIA_READ);
32
+ return useQuery({
33
+ queryKey: mediaInsightsKeys.storage(workspace.id),
34
+ queryFn: () => httpMediaInsightsGateway.storage(),
35
+ staleTime: STALE_TIME.Standard,
36
+ // One retry, not TanStack's default three. A widget that cannot load
37
+ // must SAY so, and three attempts with exponential backoff leave it
38
+ // sitting on a skeleton for about seven seconds first — on a page whose
39
+ // whole premise is that one failing card degrades alone, that reads as
40
+ // a hang rather than a failure. One retry still covers a transient blip.
41
+ retry: 1,
42
+ enabled: canRead
43
+ });
44
+ }
45
+ /** Loads assets uploaded per time bucket across the selected range. */
46
+ export function useMediaUploads() {
47
+ const workspace = useCurrentWorkspace();
48
+ const { days } = useInsightsRange();
49
+ const canRead = useHasPermission(MEDIA_READ);
50
+ return useQuery({
51
+ queryKey: mediaInsightsKeys.uploads(workspace.id, days),
52
+ queryFn: () => httpMediaInsightsGateway.uploads(days),
53
+ staleTime: STALE_TIME.Standard,
54
+ // One retry, not TanStack's default three. A widget that cannot load
55
+ // must SAY so, and three attempts with exponential backoff leave it
56
+ // sitting on a skeleton for about seven seconds first — on a page whose
57
+ // whole premise is that one failing card degrades alone, that reads as
58
+ // a hang rather than a failure. One retry still covers a transient blip.
59
+ retry: 1,
60
+ enabled: canRead
61
+ });
62
+ }
63
+ /**
64
+ * Loads alt-text coverage across the workspace's images.
65
+ *
66
+ * Takes no range: accessibility debt is a standing total, not something that
67
+ * happened in the last 30 days, and windowing it would make the number shrink
68
+ * whenever someone narrowed the range.
69
+ */
70
+ export function useMediaAltCoverage() {
71
+ const workspace = useCurrentWorkspace();
72
+ const canRead = useHasPermission(MEDIA_READ);
73
+ return useQuery({
74
+ queryKey: mediaInsightsKeys.alt(workspace.id),
75
+ queryFn: () => httpMediaInsightsGateway.altCoverage(),
76
+ staleTime: STALE_TIME.Standard,
77
+ // One retry, not TanStack's default three. A widget that cannot load
78
+ // must SAY so, and three attempts with exponential backoff leave it
79
+ // sitting on a skeleton for about seven seconds first — on a page whose
80
+ // whole premise is that one failing card degrades alone, that reads as
81
+ // a hang rather than a failure. One retry still covers a transient blip.
82
+ retry: 1,
83
+ enabled: canRead
84
+ });
85
+ }