@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,422 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useMemo, useRef, useState } from 'react';
3
+ import { defineMessages, useIntl } from 'react-intl';
4
+ import { ImagePlus, UploadCloud } from 'lucide-react';
5
+ import { Button, cn, Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, toast } from '@orthacms/design-system';
6
+ import { useHasPermission } from '@orthacms/identity-admin';
7
+ import { toMediaValueRef } from '@orthacms/content-domain';
8
+ import { MEDIA_CREATE, MEDIA_KIND, MEDIA_READ } from '../../constants';
9
+ import { kindFromMime } from '../../utils/kindFromMime';
10
+ import { acceptsFile, describeAccept } from '../../utils/mediaAccept';
11
+ import { MediaPickerDialog } from '../MediaPickerDialog';
12
+ import { UploadDialog } from '../UploadDialog';
13
+ import { MediaFieldItem } from './MediaFieldItem';
14
+ const messages = defineMessages({
15
+ select: { id: 'media.field.select', defaultMessage: 'Select from library' },
16
+ add: { id: 'media.field.add', defaultMessage: 'Add from library' },
17
+ upload: { id: 'media.field.upload', defaultMessage: 'Upload' },
18
+ replace: { id: 'media.field.replace', defaultMessage: 'Replace' },
19
+ attach: { id: 'media.field.attach', defaultMessage: 'Attach files' },
20
+ uploadTo: {
21
+ id: 'media.field.uploadTo',
22
+ defaultMessage: 'Files are attached now and uploaded to {location} when you save the record.'
23
+ },
24
+ stagedCount: {
25
+ id: 'media.field.stagedCount',
26
+ defaultMessage: '{count, plural, one {# file uploads on save} other {# files upload on save}}'
27
+ },
28
+ // No size is quoted: the cap is server config the admin cannot read, and
29
+ // the old "Up to 250 MB each" was five times the shipped 50 MB default.
30
+ uploadHintAccept: {
31
+ id: 'media.field.uploadHintAccept',
32
+ defaultMessage: 'This field accepts {what}.'
33
+ },
34
+ /**
35
+ * Names the panel so its drop affordance isn't sighted-mouse-only. The
36
+ * Upload button remains the keyboard route; this just says the drop exists.
37
+ */
38
+ dropZone: {
39
+ id: 'media.field.dropZone',
40
+ defaultMessage: 'Attached assets. Drop files here to attach them, or use the Upload button.'
41
+ },
42
+ dropZoneReadOnly: {
43
+ id: 'media.field.dropZoneReadOnly',
44
+ defaultMessage: 'Attached assets'
45
+ },
46
+ libraryRoot: { id: 'media.field.libraryRoot', defaultMessage: 'All media' },
47
+ emptySingle: {
48
+ id: 'media.field.emptySingle',
49
+ defaultMessage: 'No asset selected'
50
+ },
51
+ emptyMultiple: {
52
+ id: 'media.field.emptyMultiple',
53
+ defaultMessage: 'No assets attached'
54
+ },
55
+ emptyBody: {
56
+ id: 'media.field.emptyBody',
57
+ defaultMessage: 'Drop a file here, or pick one from the Media Library. New files upload when you save.'
58
+ },
59
+ emptyBodyPickOnly: {
60
+ id: 'media.field.emptyBodyPickOnly',
61
+ defaultMessage: 'Pick an asset from the Media Library.'
62
+ },
63
+ dropHere: {
64
+ id: 'media.field.dropHere',
65
+ defaultMessage: 'Drop to attach'
66
+ },
67
+ dropBody: {
68
+ id: 'media.field.dropBody',
69
+ defaultMessage: 'Dropped files open in the upload dialog first, so you can check them.'
70
+ },
71
+ accepts: { id: 'media.field.accepts', defaultMessage: 'Accepts {what}' },
72
+ count: {
73
+ id: 'media.field.count',
74
+ defaultMessage: '{count, plural, one {# asset attached} other {# assets attached}}'
75
+ },
76
+ uploadRejected: {
77
+ id: 'media.field.uploadRejected',
78
+ defaultMessage: '{name} is not an accepted file type for this field.'
79
+ },
80
+ noMediaAccess: {
81
+ id: 'media.field.noMediaAccess',
82
+ defaultMessage: 'You don’t have access to the Media Library, so this field can only be read.'
83
+ },
84
+ emptyReadOnly: {
85
+ id: 'media.field.emptyReadOnly',
86
+ defaultMessage: 'Nothing attached'
87
+ },
88
+ emptyBodyReadOnly: {
89
+ id: 'media.field.emptyBodyReadOnly',
90
+ defaultMessage: 'This field has no assets.'
91
+ }
92
+ });
93
+ /**
94
+ * Normalize the field value to an ordered list of {@link MediaValueRef}s.
95
+ *
96
+ * The value is `{ id, alt?, decorative? }` (or a list of them) since `ORT-83`,
97
+ * and a bare id or id array before that — both still arrive, so both are
98
+ * normalized here and the legacy form stays confined to the kernel's
99
+ * `toMediaValueRef`. A legacy value reads as "no alternative supplied", which is
100
+ * exactly what it is.
101
+ */
102
+ function toRefs(value) {
103
+ return (Array.isArray(value) ? value : [value])
104
+ .map(toMediaValueRef)
105
+ .filter((ref) => ref !== null && !!ref.id);
106
+ }
107
+ /** The original-bytes route for an id we know nothing else about. */
108
+ function rawUrl(id) {
109
+ return `/api/media/assets/${id}/raw`;
110
+ }
111
+ /** Whether a drag carries files (as opposed to text or an in-page element). */
112
+ function dragHasFiles(transfer) {
113
+ return !!transfer && Array.from(transfer.types).includes('Files');
114
+ }
115
+ /**
116
+ * The editor control for one media field — a single asset or an ordered list.
117
+ * The attached assets render as preview tiles ({@link MediaFieldItem}, resolved
118
+ * from the server-provided refs or a just-picked/uploaded asset) inside a panel
119
+ * that doubles as a **drop zone**. Below it sit **Select from library** (the
120
+ * {@link MediaPickerDialog}) and **Upload**, plus the field's accept hint and, in
121
+ * multiple mode, the attached count.
122
+ *
123
+ * **Choosing a file uploads nothing.** The {@link UploadDialog} — the Media
124
+ * Library's own modal, reused whole — stages and previews the files (a drop on
125
+ * the panel opens it pre-staged), and confirming only *stages* them: they get a
126
+ * placeholder id, render as "Pending upload" tiles, and the bytes move when the
127
+ * record is **saved or published** (`usePendingMediaUploads`, the plugin's
128
+ * presave contribution). So abandoning an edit leaves nothing behind in the
129
+ * library, and a record plus its new assets land as one commit.
130
+ *
131
+ * Fully controlled: the parent (the Media tab, bound to the entry form) owns the
132
+ * value as an asset id or id array.
133
+ *
134
+ * **Read-only** (the reader has no `content:update`/`content:create` on the
135
+ * record) reduces it to the tiles: no drop zone, no Select/Upload row, no
136
+ * remove or reorder on a tile. Note this is a *content* permission, distinct
137
+ * from the `media:*` gates below — a user may hold every media permission there
138
+ * is and still not be allowed to change **this record**, and attaching an asset
139
+ * to a record is a write to the record.
140
+ */
141
+ export function MediaFieldControl({ id, multiple, accept, value, initialRefs, refsPending, invalid, describedBy, required, uploads, readOnly = false, onChange, onBlur }) {
142
+ const intl = useIntl();
143
+ // Mirror the server's media matrix. Without these the controls still
144
+ // *render*, and the failure lands late and loud: a deferred upload 403s
145
+ // inside the save, which aborts the whole record write — so a missing
146
+ // permission would cost the user their unrelated edits too.
147
+ const canPickMedia = useHasPermission(MEDIA_READ);
148
+ const canUploadMedia = useHasPermission(MEDIA_CREATE);
149
+ const [pickerOpen, setPickerOpen] = useState(false);
150
+ const [uploadOpen, setUploadOpen] = useState(false);
151
+ // Files dropped on the panel, staged into the upload dialog when it opens —
152
+ // a drop gets the same preview-before-you-commit step as the Upload button.
153
+ const [dropped, setDropped] = useState([]);
154
+ const [dragging, setDragging] = useState(false);
155
+ // Nested elements fire dragleave as the pointer crosses them, so the highlight
156
+ // is held by a depth counter rather than the last event to arrive.
157
+ const dragDepth = useRef(0);
158
+ // Locally-known assets (just picked or uploaded), merged over the server refs
159
+ // so a freshly-added asset shows its real name/thumbnail before any refetch.
160
+ const [known, setKnown] = useState(new Map());
161
+ const refs = toRefs(value);
162
+ const ids = refs.map((ref) => ref.id);
163
+ // Staging is only offered when the user may actually upload; the presave
164
+ // step would otherwise fail the save on their behalf. A read-only record is
165
+ // the same argument one level up: there is no save to ride.
166
+ //
167
+ // This also disarms the drop zone, whose enter/drop handlers both bail on
168
+ // `!canStage` — so a read-only field can't be dropped onto.
169
+ const canStage = !!uploads && canUploadMedia && !readOnly;
170
+ const pending = uploads?.pending;
171
+ const displays = useMemo(() => {
172
+ const refs = new Map(initialRefs?.map((ref) => [ref.id, ref]));
173
+ return ids.map((assetId) => {
174
+ // A staged (not yet uploaded) file: everything is known locally, and
175
+ // an image previews straight off its object URL.
176
+ const staged = pending?.get(assetId);
177
+ if (staged)
178
+ return {
179
+ id: assetId,
180
+ name: staged.file.name,
181
+ url: staged.previewUrl ?? '',
182
+ kind: kindFromMime(staged.file.type),
183
+ mimeType: staged.file.type,
184
+ size: staged.file.size,
185
+ pending: true
186
+ };
187
+ const local = known.get(assetId);
188
+ if (local)
189
+ return {
190
+ id: assetId,
191
+ name: local.name,
192
+ // Tiles are small: prefer the derivative the server made.
193
+ url: local.thumbUrl ?? local.previewUrl ?? local.url,
194
+ originalUrl: local.url,
195
+ kind: local.kind,
196
+ mimeType: local.mimeType,
197
+ size: local.size,
198
+ dimensions: local.dimensions
199
+ };
200
+ const ref = refs.get(assetId);
201
+ if (ref)
202
+ return {
203
+ id: assetId,
204
+ name: ref.name,
205
+ url: ref.thumbUrl ?? ref.previewUrl ?? ref.url,
206
+ originalUrl: ref.url,
207
+ kind: ref.kind,
208
+ mimeType: ref.mimeType,
209
+ missing: ref.missing
210
+ };
211
+ // Nothing known about this id. While the entry's media read is in
212
+ // flight, wait: guessing the raw route made every edit-mode open
213
+ // pull **full-size originals** to draw 180px tiles, and printed a
214
+ // uuid where the file name goes. Once the read has settled without
215
+ // a ref (it failed, or no media plugin is bound) waiting would
216
+ // never end — so fall back to the raw route and show *something*.
217
+ // The id names the controls either way, so "Remove …" stays unique.
218
+ if (refsPending)
219
+ return {
220
+ id: assetId,
221
+ name: assetId,
222
+ url: '',
223
+ kind: '',
224
+ mimeType: '',
225
+ resolving: true
226
+ };
227
+ return {
228
+ id: assetId,
229
+ name: assetId,
230
+ url: rawUrl(assetId),
231
+ kind: MEDIA_KIND.Image,
232
+ mimeType: ''
233
+ };
234
+ });
235
+ }, [ids, initialRefs, known, pending, refsPending]);
236
+ const remember = (assets) => {
237
+ setKnown((current) => {
238
+ const next = new Map(current);
239
+ for (const asset of assets)
240
+ next.set(asset.id, asset);
241
+ return next;
242
+ });
243
+ };
244
+ const setRefs = (nextRefs) => {
245
+ // Anything staged that just left the field is forgotten here — one place,
246
+ // so a remove, a replace, and a re-pick all release the preview blob (and
247
+ // stop the save uploading a file nothing references).
248
+ if (pending) {
249
+ const kept = new Set(nextRefs.map((ref) => ref.id));
250
+ for (const assetId of ids) {
251
+ if (!kept.has(assetId) && pending.has(assetId))
252
+ uploads?.drop(assetId);
253
+ }
254
+ }
255
+ onChange(multiple ? nextRefs : (nextRefs[0] ?? null));
256
+ onBlur?.();
257
+ };
258
+ /** Attaching by id, keeping the alt text of anything already attached. */
259
+ const setIds = (nextIds) => {
260
+ const byId = new Map(refs.map((ref) => [ref.id, ref]));
261
+ setRefs(nextIds.map((assetId) => byId.get(assetId) ?? { id: assetId }));
262
+ };
263
+ /**
264
+ * Patches one usage's text alternative.
265
+ *
266
+ * `alt` and `decorative` are mutually exclusive by the kernel's validation —
267
+ * an image cannot be presentational *and* carry a description — so marking a
268
+ * usage decorative drops whatever was typed rather than leaving a value the
269
+ * publish gate would refuse. Un-marking it leaves the field empty, which is
270
+ * the honest state: the author has not answered yet.
271
+ */
272
+ const patchRef = (index, patch) => {
273
+ setRefs(refs.map((ref, at) => {
274
+ if (at !== index)
275
+ return ref;
276
+ const next = { ...ref, ...patch };
277
+ if (next.decorative === true)
278
+ delete next.alt;
279
+ if (next.decorative !== true)
280
+ delete next.decorative;
281
+ if (next.alt === '')
282
+ delete next.alt;
283
+ return next;
284
+ }));
285
+ };
286
+ const onPicked = (assets) => {
287
+ if (!assets.length)
288
+ return;
289
+ remember(assets);
290
+ if (multiple) {
291
+ // Append the newly-picked, skipping any already attached.
292
+ const existing = new Set(ids);
293
+ const added = assets
294
+ .map((a) => a.id)
295
+ .filter((assetId) => !existing.has(assetId));
296
+ setIds([...ids, ...added]);
297
+ }
298
+ else {
299
+ setIds(assets[0] ? [assets[0].id] : []);
300
+ }
301
+ };
302
+ /**
303
+ * Stage the confirmed files against the field — no request yet. Each gets a
304
+ * placeholder id that rides the form until the save uploads it. The field's
305
+ * own `accept` is enforced here on the file's MIME type (the server checks
306
+ * the real asset again on save), so a rejected file never reaches the value.
307
+ */
308
+ const onStaged = (files) => {
309
+ if (!uploads || !canStage)
310
+ return;
311
+ const allowed = [];
312
+ for (const file of files) {
313
+ if (acceptsFile(accept, file))
314
+ allowed.push(file);
315
+ else
316
+ toast.error(intl.formatMessage(messages.uploadRejected, {
317
+ name: file.name
318
+ }));
319
+ }
320
+ if (!allowed.length)
321
+ return;
322
+ const staged = uploads.stage(multiple ? allowed : allowed.slice(0, 1));
323
+ setIds(multiple ? [...ids, ...staged] : staged.slice(0, 1));
324
+ };
325
+ /** Open the upload dialog, optionally pre-staged with dropped files. */
326
+ const openUploadWith = (files) => {
327
+ setDropped(multiple ? files : files.slice(0, 1));
328
+ setUploadOpen(true);
329
+ };
330
+ const removeAt = (index) => setIds(ids.filter((_, i) => i !== index));
331
+ const move = (index, delta) => {
332
+ const target = index + delta;
333
+ if (target < 0 || target >= ids.length)
334
+ return;
335
+ const next = [...ids];
336
+ [next[index], next[target]] = [next[target], next[index]];
337
+ setIds(next);
338
+ };
339
+ // Best-effort `accept` on the native file input (server still enforces).
340
+ const acceptAttr = useMemo(() => {
341
+ const mimes = accept?.mimeTypes ?? [];
342
+ const kinds = accept?.kinds ?? [];
343
+ const fromKinds = kinds
344
+ .filter((k) => k === MEDIA_KIND.Image ||
345
+ k === MEDIA_KIND.Video ||
346
+ k === MEDIA_KIND.Audio)
347
+ .map((k) => `${k}/*`);
348
+ const all = [...mimes, ...fromKinds];
349
+ return all.length ? all.join(',') : undefined;
350
+ }, [accept]);
351
+ const acceptHint = describeAccept(accept);
352
+ const isEmpty = displays.length === 0;
353
+ const stagedCount = displays.filter((item) => item.pending).length;
354
+ return (_jsxs("div", { className: "flex flex-col gap-3", children: [_jsx("div", { role: "group", "aria-label": intl.formatMessage(canStage ? messages.dropZone : messages.dropZoneReadOnly), onDragEnter: (event) => {
355
+ if (!dragHasFiles(event.dataTransfer) || !canStage)
356
+ return;
357
+ dragDepth.current += 1;
358
+ setDragging(true);
359
+ }, onDragOver: (event) => {
360
+ // Without this the browser opens the file instead of dropping.
361
+ if (dragHasFiles(event.dataTransfer))
362
+ event.preventDefault();
363
+ }, onDragLeave: () => {
364
+ dragDepth.current = Math.max(0, dragDepth.current - 1);
365
+ if (dragDepth.current === 0)
366
+ setDragging(false);
367
+ }, onDrop: (event) => {
368
+ if (!dragHasFiles(event.dataTransfer) || !canStage)
369
+ return;
370
+ event.preventDefault();
371
+ dragDepth.current = 0;
372
+ setDragging(false);
373
+ // Straight into the same dialog the Upload button opens, so
374
+ // a drop is reviewed before it's staged.
375
+ openUploadWith(Array.from(event.dataTransfer.files));
376
+ }, className: cn('rounded-xl border bg-muted/20 p-3 transition-colors', isEmpty && 'border-dashed', invalid && 'border-destructive/60', dragging && 'border-primary bg-primary/5'), children: isEmpty ? (_jsx(Empty, { className: "gap-4 p-6 md:p-8", children: _jsxs(EmptyHeader, { children: [_jsx(EmptyMedia, { variant: "icon", children: dragging ? (_jsx(UploadCloud, { "aria-hidden": true })) : (_jsx(ImagePlus, { "aria-hidden": true })) }), _jsx(EmptyTitle, { className: "text-base", children: readOnly
377
+ ? intl.formatMessage(messages.emptyReadOnly)
378
+ : dragging
379
+ ? intl.formatMessage(messages.dropHere)
380
+ : intl.formatMessage(multiple
381
+ ? messages.emptyMultiple
382
+ : messages.emptySingle) }), _jsx(EmptyDescription, { children: intl.formatMessage(readOnly
383
+ ? messages.emptyBodyReadOnly
384
+ : dragging
385
+ ? messages.dropBody
386
+ : canStage
387
+ ? messages.emptyBody
388
+ : messages.emptyBodyPickOnly) })] }) })) : (_jsx("ul", { className: cn('grid gap-3', multiple
389
+ ? 'grid-cols-2 sm:grid-cols-3 xl:grid-cols-4'
390
+ : 'grid-cols-1 sm:max-w-[15rem]'), children: displays.map((item, index) => (_jsx(MediaFieldItem, { item: item, index: index, total: displays.length, multiple: multiple, readOnly: readOnly, onRemove: () => removeAt(index), onMove: (delta) => move(index, delta), ...(refs[index]?.alt !== undefined
391
+ ? { alt: refs[index].alt }
392
+ : {}), decorative: refs[index]?.decorative === true, onAltChange: readOnly
393
+ ? undefined
394
+ : (next) => patchRef(index, { alt: next }), onDecorativeChange: readOnly
395
+ ? undefined
396
+ : (next) => patchRef(index, {
397
+ decorative: next
398
+ }) }, `${item.id}-${index}`))) })) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [readOnly ? null : (_jsxs(_Fragment, { children: [_jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "shadow-none", id: id, "aria-required": required, "aria-invalid": invalid, "aria-describedby": describedBy, disabled: !canPickMedia, onClick: () => setPickerOpen(true), children: [_jsx(ImagePlus, { className: "size-4", "aria-hidden": true }), intl.formatMessage(!multiple && displays.length > 0
399
+ ? messages.replace
400
+ : multiple
401
+ ? messages.add
402
+ : messages.select)] }), canStage ? (_jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "shadow-none", onClick: () => openUploadWith([]), children: [_jsx(UploadCloud, { className: "size-4", "aria-hidden": true }), intl.formatMessage(messages.upload)] })) : null] })), _jsxs("p", { className: cn('ml-auto flex flex-wrap items-center gap-x-2 text-xs text-muted-foreground', !stagedCount &&
403
+ (!acceptHint || readOnly) &&
404
+ (canPickMedia || readOnly) &&
405
+ !(multiple && displays.length > 0) &&
406
+ 'hidden'), children: [stagedCount > 0 ? (_jsx("span", { className: "text-foreground", children: intl.formatMessage(messages.stagedCount, {
407
+ count: stagedCount
408
+ }) })) : null, multiple && displays.length > 0 ? (_jsx("span", { children: intl.formatMessage(messages.count, {
409
+ count: displays.length
410
+ }) })) : null, acceptHint && !readOnly ? (_jsx("span", { children: intl.formatMessage(messages.accepts, {
411
+ what: acceptHint
412
+ }) })) : null, canPickMedia || readOnly ? null : (_jsx("span", { children: intl.formatMessage(messages.noMediaAccess) }))] })] }), pickerOpen ? (_jsx(MediaPickerDialog, { open: true, onOpenChange: setPickerOpen, multiple: multiple, accept: accept, attachedIds: ids, onConfirm: onPicked })) : null, readOnly ? null : (_jsx(UploadDialog, { open: uploadOpen, onOpenChange: setUploadOpen, locationLabel: intl.formatMessage(messages.libraryRoot), description: intl.formatMessage(messages.uploadTo, {
413
+ location: intl.formatMessage(messages.libraryRoot)
414
+ }), hint: acceptHint
415
+ ? intl.formatMessage(messages.uploadHintAccept, {
416
+ what: acceptHint
417
+ })
418
+ : undefined, accept: acceptAttr, multiple: multiple, confirmLabel: intl.formatMessage(messages.attach), initialFiles: dropped,
419
+ // No `collectAlt`: a field's files upload with the record, and
420
+ // this control has nowhere to carry a description that far.
421
+ onUpload: (uploads) => onStaged(uploads.map((upload) => upload.file)) }))] }));
422
+ }
@@ -0,0 +1,17 @@
1
+ import type { MediaFolder } from '../../../types/mediaFolder';
2
+ /**
3
+ * One folder in the sidebar tree. A folder with sub-folders is a
4
+ * design-system `Collapsible` (a chevron toggles its children under a left tree
5
+ * line; Radix manages `aria-expanded`), starting expanded; a leaf folder gets a
6
+ * matching spacer so names align. The name is a separate button that navigates
7
+ * into the folder — clicking it never toggles the group — and the open folder is
8
+ * highlighted. Navigation dispatches up.
9
+ */
10
+ export declare function MediaFolderTreeItem({ folder, folders, currentFolderId, counts, onNavigate }: {
11
+ folder: MediaFolder;
12
+ folders: MediaFolder[];
13
+ currentFolderId: string;
14
+ counts: Map<string, number>;
15
+ onNavigate: (folderId: string) => void;
16
+ }): import("react").JSX.Element;
17
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAW9D;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,EAChC,MAAM,EACN,OAAO,EACP,eAAe,EACf,MAAM,EACN,UAAU,EACb,EAAE;IACC,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C,+BAyFA"}
@@ -0,0 +1,37 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { defineMessages, useIntl } from 'react-intl';
3
+ import { Collapsible, CollapsibleContent, CollapsibleTrigger, cn } from '@orthacms/design-system';
4
+ import { ChevronRight, Folder } from 'lucide-react';
5
+ /** Intl descriptors for {@link MediaFolderTreeItem}, co-located. */
6
+ const messages = defineMessages({
7
+ toggle: { id: 'media.tree.toggle', defaultMessage: 'Toggle {name}' },
8
+ count: {
9
+ id: 'media.tree.count',
10
+ defaultMessage: '{count, plural, one {# item} other {# items}}'
11
+ }
12
+ });
13
+ /**
14
+ * One folder in the sidebar tree. A folder with sub-folders is a
15
+ * design-system `Collapsible` (a chevron toggles its children under a left tree
16
+ * line; Radix manages `aria-expanded`), starting expanded; a leaf folder gets a
17
+ * matching spacer so names align. The name is a separate button that navigates
18
+ * into the folder — clicking it never toggles the group — and the open folder is
19
+ * highlighted. Navigation dispatches up.
20
+ */
21
+ export function MediaFolderTreeItem({ folder, folders, currentFolderId, counts, onNavigate }) {
22
+ const intl = useIntl();
23
+ const children = folders
24
+ .filter((f) => f.parentId === folder.id)
25
+ .sort((a, b) => a.name.localeCompare(b.name));
26
+ const hasChildren = children.length > 0;
27
+ const active = currentFolderId === folder.id;
28
+ const nameButton = (_jsxs("button", { type: "button", onClick: () => onNavigate(folder.id), "aria-current": active ? 'true' : undefined, className: cn('flex min-w-0 flex-1 items-center gap-2 rounded-md py-1.5 pr-2 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', active ? 'font-medium text-primary' : 'text-foreground'), children: [_jsx(Folder, { className: cn('size-4 shrink-0', active ? 'text-primary' : 'text-muted-foreground'), "aria-hidden": true }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: folder.name }), _jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: intl.formatMessage(messages.count, {
29
+ count: counts.get(folder.id) ?? 0
30
+ }) })] }));
31
+ if (!hasChildren) {
32
+ return (_jsx("li", { children: _jsxs("div", { className: cn('flex items-center gap-0.5 rounded-md pl-1 transition-colors', active ? 'bg-primary/10' : 'hover:bg-accent'), children: [_jsx("span", { className: "size-6 shrink-0", "aria-hidden": true }), nameButton] }) }));
33
+ }
34
+ return (_jsx("li", { children: _jsxs(Collapsible, { defaultOpen: true, className: "group/folder", children: [_jsxs("div", { className: cn('flex items-center gap-0.5 rounded-md pl-1 transition-colors', active ? 'bg-primary/10' : 'hover:bg-accent'), children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": intl.formatMessage(messages.toggle, {
35
+ name: folder.name
36
+ }), className: "flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(ChevronRight, { className: "size-3.5 transition-transform duration-200 group-data-[state=open]/folder:rotate-90" }) }) }), nameButton] }), _jsx(CollapsibleContent, { children: _jsx("ul", { className: "ml-[0.85rem] mt-0.5 space-y-0.5 border-l border-border pl-1.5", children: children.map((child) => (_jsx(MediaFolderTreeItem, { folder: child, folders: folders, currentFolderId: currentFolderId, counts: counts, onNavigate: onNavigate }, child.id))) }) })] }) }));
37
+ }
@@ -0,0 +1,22 @@
1
+ import type { MediaFolder } from '../../types/mediaFolder';
2
+ /**
3
+ * The Media Library's folder sidebar — a **flat** nav region sitting flush on the
4
+ * page background (mirroring the Content Library sidebar), not a bordered island.
5
+ * The page supplies the divider (`lg:border-r`) via `className`, so the mobile
6
+ * drawer copy renders without one. An "All media" root entry sits above the
7
+ * folder tree, where any folder with sub-folders is collapsible; a New folder
8
+ * action closes the list. Highlights the open folder and dispatches navigation /
9
+ * create up to the page.
10
+ */
11
+ export declare function MediaFoldersNav({ folders, currentFolderId, counts, rootCount, onNavigate, onNewFolder, canCreate, className }: {
12
+ folders: MediaFolder[];
13
+ currentFolderId: string;
14
+ counts: Map<string, number>;
15
+ /** Item count shown against the "All media" root row. */
16
+ rootCount: number;
17
+ onNavigate: (folderId: string) => void;
18
+ onNewFolder: () => void;
19
+ canCreate: boolean;
20
+ className?: string;
21
+ }): import("react").JSX.Element;
22
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaFoldersNav/index.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAe3D;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,EAC5B,OAAO,EACP,eAAe,EACf,MAAM,EACN,SAAS,EACT,UAAU,EACV,WAAW,EACX,SAAS,EACT,SAAS,EACZ,EAAE;IACC,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,+BAmFA"}
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { defineMessages, useIntl } from 'react-intl';
3
+ import { Button, cn } from '@orthacms/design-system';
4
+ import { FolderPlus, Home, Images } from 'lucide-react';
5
+ import { ROOT_FOLDER_ID } from '../../constants';
6
+ import { MediaFolderTreeItem } from './MediaFolderTreeItem';
7
+ /** Intl descriptors for {@link MediaFoldersNav}, co-located. */
8
+ const messages = defineMessages({
9
+ nav: { id: 'media.nav.nav', defaultMessage: 'Folders' },
10
+ heading: { id: 'media.nav.heading', defaultMessage: 'Library' },
11
+ allMedia: { id: 'media.nav.allMedia', defaultMessage: 'All media' },
12
+ allCount: {
13
+ id: 'media.nav.allCount',
14
+ defaultMessage: '{count, plural, one {# item} other {# items}}'
15
+ },
16
+ newFolder: { id: 'media.nav.newFolder', defaultMessage: 'New folder' }
17
+ });
18
+ /**
19
+ * The Media Library's folder sidebar — a **flat** nav region sitting flush on the
20
+ * page background (mirroring the Content Library sidebar), not a bordered island.
21
+ * The page supplies the divider (`lg:border-r`) via `className`, so the mobile
22
+ * drawer copy renders without one. An "All media" root entry sits above the
23
+ * folder tree, where any folder with sub-folders is collapsible; a New folder
24
+ * action closes the list. Highlights the open folder and dispatches navigation /
25
+ * create up to the page.
26
+ */
27
+ export function MediaFoldersNav({ folders, currentFolderId, counts, rootCount, onNavigate, onNewFolder, canCreate, className }) {
28
+ const intl = useIntl();
29
+ const topLevel = folders
30
+ .filter((f) => f.parentId === ROOT_FOLDER_ID)
31
+ .sort((a, b) => a.name.localeCompare(b.name));
32
+ const rootActive = currentFolderId === ROOT_FOLDER_ID;
33
+ return (_jsxs("nav", { "aria-label": intl.formatMessage(messages.nav), className: cn('flex h-full w-60 shrink-0 flex-col overflow-hidden', className), children: [_jsxs("div", { className: "flex items-center gap-2 p-2", children: [_jsx(Images, { className: "size-4 text-muted-foreground", "aria-hidden": true }), _jsx("h2", { className: "text-sm font-semibold tracking-[-0.01em]", children: intl.formatMessage(messages.heading) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-2", children: _jsxs("ul", { className: "space-y-0.5", children: [_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onNavigate(ROOT_FOLDER_ID), "aria-current": rootActive ? 'true' : undefined, className: cn('flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', rootActive
34
+ ? 'bg-primary/10 font-medium text-primary'
35
+ : 'text-foreground hover:bg-accent'), children: [_jsx(Home, { className: cn('size-4 shrink-0', rootActive
36
+ ? 'text-primary'
37
+ : 'text-muted-foreground'), "aria-hidden": true }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: intl.formatMessage(messages.allMedia) }), _jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: intl.formatMessage(messages.allCount, {
38
+ count: rootCount
39
+ }) })] }) }), topLevel.map((folder) => (_jsx(MediaFolderTreeItem, { folder: folder, folders: folders, currentFolderId: currentFolderId, counts: counts, onNavigate: onNavigate }, folder.id)))] }) }), canCreate ? (_jsx("div", { className: "p-2", children: _jsxs(Button, { variant: "outline", size: "sm", className: "w-full justify-start shadow-none", onClick: onNewFolder, children: [_jsx(FolderPlus, { "aria-hidden": true }), intl.formatMessage(messages.newFolder)] }) })) : null] }));
40
+ }
@@ -0,0 +1,18 @@
1
+ import type { MediaAsset } from '../../../types/mediaAsset';
2
+ import { type AssetActionKind } from '../../AssetActionsMenu';
3
+ /**
4
+ * An asset tile in the grid — a gradient thumbnail with a hover ⋯ menu and a
5
+ * selection checkbox (always visible once selected), over the name and a
6
+ * size/dimensions line. Clicking the thumbnail or name opens the detail drawer;
7
+ * the checkbox toggles selection. A selected tile gets a primary ring.
8
+ */
9
+ export declare function MediaAssetCard({ asset, selected, onToggleSelect, onAction, canCreate, canUpdate, canDelete }: {
10
+ asset: MediaAsset;
11
+ selected: boolean;
12
+ onToggleSelect: (id: string) => void;
13
+ onAction: (kind: AssetActionKind, asset: MediaAsset) => void;
14
+ canCreate: boolean;
15
+ canUpdate: boolean;
16
+ canDelete: boolean;
17
+ }): import("react").JSX.Element;
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaGrid/MediaAssetCard/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAG5D,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAShF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,cAAc,EACd,QAAQ,EACR,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC7D,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BAsEA"}
@@ -0,0 +1,29 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { defineMessages, useIntl } from 'react-intl';
3
+ import { Checkbox, cn } from '@orthacms/design-system';
4
+ import { formatBytes } from '../../../utils/formatBytes';
5
+ import { buildAssetHandlers } from '../../../utils/assetHandlers';
6
+ import { AssetActionsMenu } from '../../AssetActionsMenu';
7
+ import { MediaThumbnail } from '../../MediaThumbnail';
8
+ /** Intl descriptors for {@link MediaAssetCard}, co-located. */
9
+ const messages = defineMessages({
10
+ open: { id: 'media.assetCard.open', defaultMessage: 'Open {name}' },
11
+ select: { id: 'media.assetCard.select', defaultMessage: 'Select {name}' }
12
+ });
13
+ /**
14
+ * An asset tile in the grid — a gradient thumbnail with a hover ⋯ menu and a
15
+ * selection checkbox (always visible once selected), over the name and a
16
+ * size/dimensions line. Clicking the thumbnail or name opens the detail drawer;
17
+ * the checkbox toggles selection. A selected tile gets a primary ring.
18
+ */
19
+ export function MediaAssetCard({ asset, selected, onToggleSelect, onAction, canCreate, canUpdate, canDelete }) {
20
+ const intl = useIntl();
21
+ const handlers = buildAssetHandlers(asset, onAction);
22
+ return (_jsxs("div", { className: cn('group relative overflow-hidden rounded-xl border bg-card transition-shadow hover:shadow-md', selected && 'ring-2 ring-primary ring-offset-1'), children: [_jsxs("div", { className: "relative", children: [_jsx("button", { type: "button", onClick: () => onAction('open', asset), "aria-label": intl.formatMessage(messages.open, {
23
+ name: asset.name
24
+ }), className: "block w-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(MediaThumbnail, { asset: asset, className: "aspect-[4/3] w-full" }) }), _jsx("div", { className: cn('absolute left-2 top-2 rounded-md bg-background/80 p-0.5 shadow-sm backdrop-blur transition-opacity focus-within:opacity-100 group-hover:opacity-100', selected ? 'opacity-100' : 'opacity-0'), children: _jsx(Checkbox, { checked: selected, onCheckedChange: () => onToggleSelect(asset.id), "aria-label": intl.formatMessage(messages.select, {
25
+ name: asset.name
26
+ }) }) }), _jsx("div", { className: "absolute right-2 top-2 rounded-md bg-background/80 shadow-sm backdrop-blur transition-opacity focus-within:opacity-100 group-hover:opacity-100 sm:opacity-0", children: _jsx(AssetActionsMenu, { handlers: handlers, assetName: asset.name, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete }) })] }), _jsxs("div", { className: "p-3", children: [_jsx("button", { type: "button", onClick: () => onAction('open', asset), className: "block w-full truncate text-left text-sm font-medium hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", title: asset.name, children: asset.name }), _jsxs("p", { className: "mt-0.5 truncate text-xs text-muted-foreground", children: [formatBytes(asset.size), asset.dimensions
27
+ ? ` · ${asset.dimensions.width}×${asset.dimensions.height}`
28
+ : null] })] })] }));
29
+ }
@@ -0,0 +1,16 @@
1
+ import type { MediaFolder } from '../../../types/mediaFolder';
2
+ /**
3
+ * A folder tile in the grid — a folder glyph, name, and item count, with a hover
4
+ * ⋯ menu (Open / Rename / Delete). The tile body is a single button that opens
5
+ * the folder; the menu sits outside it so the two controls don't nest.
6
+ */
7
+ export declare function MediaFolderCard({ folder, assetCount, onOpen, onRename, onDelete, canUpdate, canDelete }: {
8
+ folder: MediaFolder;
9
+ assetCount: number;
10
+ onOpen: (folderId: string) => void;
11
+ onRename: (folder: MediaFolder) => void;
12
+ onDelete: (folder: MediaFolder) => void;
13
+ canUpdate: boolean;
14
+ canDelete: boolean;
15
+ }): import("react").JSX.Element;
16
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaGrid/MediaFolderCard/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAY9D;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAC5B,MAAM,EACN,UAAU,EACV,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,SAAS,EACZ,EAAE;IACC,MAAM,EAAE,WAAW,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BAwCA"}