@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,25 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { defineMessages, useIntl } from 'react-intl';
3
+ import { Folder } from 'lucide-react';
4
+ import { FolderActionsMenu } from '../../FolderActionsMenu';
5
+ /** Intl descriptors for {@link MediaFolderCard}, co-located. */
6
+ const messages = defineMessages({
7
+ open: { id: 'media.folderCard.open', defaultMessage: 'Open folder {name}' },
8
+ count: {
9
+ id: 'media.folderCard.count',
10
+ defaultMessage: '{count, plural, one {# item} other {# items}}'
11
+ }
12
+ });
13
+ /**
14
+ * A folder tile in the grid — a folder glyph, name, and item count, with a hover
15
+ * ⋯ menu (Open / Rename / Delete). The tile body is a single button that opens
16
+ * the folder; the menu sits outside it so the two controls don't nest.
17
+ */
18
+ export function MediaFolderCard({ folder, assetCount, onOpen, onRename, onDelete, canUpdate, canDelete }) {
19
+ const intl = useIntl();
20
+ return (_jsxs("div", { className: "group relative rounded-xl border bg-card transition-colors hover:border-primary/40 hover:bg-accent/40", children: [_jsxs("button", { type: "button", onClick: () => onOpen(folder.id), onDoubleClick: () => onOpen(folder.id), "aria-label": intl.formatMessage(messages.open, {
21
+ name: folder.name
22
+ }), className: "flex w-full items-center gap-3 rounded-xl px-3 py-3 text-left focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [_jsx("span", { className: "flex size-10 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary", children: _jsx(Folder, { className: "size-5", "aria-hidden": true }) }), _jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "block truncate text-sm font-medium", children: folder.name }), _jsx("span", { className: "block text-xs text-muted-foreground", children: intl.formatMessage(messages.count, {
23
+ count: assetCount
24
+ }) })] })] }), _jsx("div", { className: "absolute right-1.5 top-1/2 -translate-y-1/2 opacity-0 transition-opacity focus-within:opacity-100 group-hover:opacity-100", children: _jsx(FolderActionsMenu, { name: folder.name, onOpen: () => onOpen(folder.id), onRename: () => onRename(folder), onDelete: () => onDelete(folder), canUpdate: canUpdate, canDelete: canDelete }) })] }));
25
+ }
@@ -0,0 +1,24 @@
1
+ import type { MediaAsset } from '../../types/mediaAsset';
2
+ import type { MediaFolder } from '../../types/mediaFolder';
3
+ import type { AssetActionKind } from '../AssetActionsMenu';
4
+ /**
5
+ * The grid view of the open folder — a responsive grid of folder tiles above a
6
+ * responsive grid of asset tiles, each under a small section label (shown only
7
+ * when both kinds are present). Purely presentational: selection state and every
8
+ * action are dispatched up to the page.
9
+ */
10
+ export declare function MediaGrid({ folders, assets, folderCounts, selectedIds, onOpenFolder, onRenameFolder, onDeleteFolder, onToggleSelect, onAssetAction, canCreate, canUpdate, canDelete }: {
11
+ folders: MediaFolder[];
12
+ assets: MediaAsset[];
13
+ folderCounts: Map<string, number>;
14
+ selectedIds: Set<string>;
15
+ onOpenFolder: (folderId: string) => void;
16
+ onRenameFolder: (folder: MediaFolder) => void;
17
+ onDeleteFolder: (folder: MediaFolder) => void;
18
+ onToggleSelect: (id: string) => void;
19
+ onAssetAction: (kind: AssetActionKind, asset: MediaAsset) => void;
20
+ canCreate: boolean;
21
+ canUpdate: boolean;
22
+ canDelete: boolean;
23
+ }): import("react").JSX.Element;
24
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaGrid/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAU3D;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EACtB,OAAO,EACP,MAAM,EACN,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,cAAc,EACd,cAAc,EACd,cAAc,EACd,aAAa,EACb,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,cAAc,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9C,cAAc,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9C,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,aAAa,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAClE,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BAuDA"}
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { defineMessages, useIntl } from 'react-intl';
3
+ import { MediaFolderCard } from './MediaFolderCard';
4
+ import { MediaAssetCard } from './MediaAssetCard';
5
+ /** Intl descriptors for {@link MediaGrid} section labels, co-located. */
6
+ const messages = defineMessages({
7
+ folders: { id: 'media.grid.folders', defaultMessage: 'Folders' },
8
+ files: { id: 'media.grid.files', defaultMessage: 'Files' }
9
+ });
10
+ /**
11
+ * The grid view of the open folder — a responsive grid of folder tiles above a
12
+ * responsive grid of asset tiles, each under a small section label (shown only
13
+ * when both kinds are present). Purely presentational: selection state and every
14
+ * action are dispatched up to the page.
15
+ */
16
+ export function MediaGrid({ folders, assets, folderCounts, selectedIds, onOpenFolder, onRenameFolder, onDeleteFolder, onToggleSelect, onAssetAction, canCreate, canUpdate, canDelete }) {
17
+ const intl = useIntl();
18
+ const showLabels = folders.length > 0 && assets.length > 0;
19
+ return (_jsxs("div", { className: "space-y-6", children: [folders.length > 0 ? (_jsxs("section", { children: [showLabels ? (_jsx("h2", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: intl.formatMessage(messages.folders) })) : null, _jsx("div", { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4", children: folders.map((folder) => (_jsx(MediaFolderCard, { folder: folder, assetCount: folderCounts.get(folder.id) ?? 0, onOpen: onOpenFolder, onRename: onRenameFolder, onDelete: onDeleteFolder, canUpdate: canUpdate, canDelete: canDelete }, folder.id))) })] })) : null, assets.length > 0 ? (_jsxs("section", { children: [showLabels ? (_jsx("h2", { className: "mb-2 text-xs font-semibold uppercase tracking-wide text-muted-foreground", children: intl.formatMessage(messages.files) })) : null, _jsx("div", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5", children: assets.map((asset) => (_jsx(MediaAssetCard, { asset: asset, selected: selectedIds.has(asset.id), onToggleSelect: onToggleSelect, onAction: onAssetAction, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete }, asset.id))) })] })) : null] }));
20
+ }
@@ -0,0 +1,10 @@
1
+ import { type MediaKind } from '../../constants';
2
+ /**
3
+ * A small labelled badge for an asset {@link MediaKind} — a kind glyph beside its
4
+ * localized name. Shared by the list view and the detail drawer so the kind reads
5
+ * the same everywhere.
6
+ */
7
+ export declare function MediaKindBadge({ kind }: {
8
+ kind: MediaKind;
9
+ }): import("react").JSX.Element;
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaKindBadge/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAqB7D;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,IAAI,EAAE,EAAE;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,+BAQ3D"}
@@ -0,0 +1,30 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { defineMessages, useIntl } from 'react-intl';
3
+ import { Badge } from '@orthacms/design-system';
4
+ import { MEDIA_KIND } from '../../constants';
5
+ import { MediaKindIcon } from '../MediaKindIcon';
6
+ /** Intl descriptors for the asset-kind labels, co-located. */
7
+ const messages = defineMessages({
8
+ image: { id: 'media.kind.image', defaultMessage: 'Image' },
9
+ video: { id: 'media.kind.video', defaultMessage: 'Video' },
10
+ audio: { id: 'media.kind.audio', defaultMessage: 'Audio' },
11
+ document: { id: 'media.kind.document', defaultMessage: 'Document' },
12
+ archive: { id: 'media.kind.archive', defaultMessage: 'Archive' }
13
+ });
14
+ /** Maps each kind to its label descriptor. */
15
+ const KIND_LABEL = {
16
+ [MEDIA_KIND.Image]: messages.image,
17
+ [MEDIA_KIND.Video]: messages.video,
18
+ [MEDIA_KIND.Audio]: messages.audio,
19
+ [MEDIA_KIND.Document]: messages.document,
20
+ [MEDIA_KIND.Archive]: messages.archive
21
+ };
22
+ /**
23
+ * A small labelled badge for an asset {@link MediaKind} — a kind glyph beside its
24
+ * localized name. Shared by the list view and the detail drawer so the kind reads
25
+ * the same everywhere.
26
+ */
27
+ export function MediaKindBadge({ kind }) {
28
+ const intl = useIntl();
29
+ return (_jsxs(Badge, { variant: "secondary", className: "gap-1 font-medium", children: [_jsx(MediaKindIcon, { kind: kind, className: "size-3" }), intl.formatMessage(KIND_LABEL[kind])] }));
30
+ }
@@ -0,0 +1,11 @@
1
+ import { type MediaKind } from '../../constants';
2
+ /**
3
+ * Renders the icon that stands for an asset {@link MediaKind}. A tiny shared
4
+ * mapping so the kind glyph stays identical on tiles, list rows, and the detail
5
+ * drawer. Decorative by default — pass `aria-hidden` context from the parent.
6
+ */
7
+ export declare function MediaKindIcon({ kind, className }: {
8
+ kind: MediaKind;
9
+ className?: string;
10
+ }): import("react").JSX.Element;
11
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaKindIcon/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAW7D;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,EAC1B,IAAI,EACJ,SAAS,EACZ,EAAE;IACC,IAAI,EAAE,SAAS,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,+BAGA"}
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Archive, FileText, Image, Music, Video } from 'lucide-react';
3
+ import { MEDIA_KIND } from '../../constants';
4
+ /** The Lucide glyph representing each asset kind. */
5
+ const KIND_ICON = {
6
+ [MEDIA_KIND.Image]: Image,
7
+ [MEDIA_KIND.Video]: Video,
8
+ [MEDIA_KIND.Audio]: Music,
9
+ [MEDIA_KIND.Document]: FileText,
10
+ [MEDIA_KIND.Archive]: Archive
11
+ };
12
+ /**
13
+ * Renders the icon that stands for an asset {@link MediaKind}. A tiny shared
14
+ * mapping so the kind glyph stays identical on tiles, list rows, and the detail
15
+ * drawer. Decorative by default — pass `aria-hidden` context from the parent.
16
+ */
17
+ export function MediaKindIcon({ kind, className }) {
18
+ const Icon = KIND_ICON[kind];
19
+ return _jsx(Icon, { className: className, "aria-hidden": true });
20
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Full-page placeholder for the Media Library's lazy-route `Suspense` fallback,
3
+ * mounted in the workspace shell at `/workspaces/:id/media/*`.
4
+ *
5
+ * The bar is **real**, not a skeleton: it is the page's identity, and it is
6
+ * where the sidebar-reveal trigger lives when the app sidebar is collapsed, so
7
+ * it must paint before the chunk lands. It composes the `TopBar` primitives
8
+ * here rather than reusing {@link MediaTopBar} — that module reaches into the
9
+ * library's own internals and pulls ~19 kB into the eager graph, which is a
10
+ * poor trade for a placeholder. What it renders is the trail the real bar shows
11
+ * at the root folder, so nothing moves when the page takes over; the folder
12
+ * crumbs arrive with the data that names them.
13
+ *
14
+ * The body mirrors the page's two-column shell — the folder rail and the asset
15
+ * grid — so the columns don't jump into place on swap.
16
+ */
17
+ export declare function MediaLibraryPageSkeleton(): 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/MediaLibrarySkeleton/index.tsx"],"names":[],"mappings":"AAuCA;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,wBAAwB,gCAyEvC"}
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { defineMessages, useIntl } from 'react-intl';
3
+ import { Image } from 'lucide-react';
4
+ import { Breadcrumb, BreadcrumbItem, BreadcrumbList, BreadcrumbPage, Skeleton, TopBar, TopBarIcon } from '@orthacms/design-system';
5
+ /** Intl descriptors for the media-library skeleton, co-located here. */
6
+ const messages = defineMessages({
7
+ nav: {
8
+ id: 'media.topbar.nav',
9
+ defaultMessage: 'Breadcrumb'
10
+ },
11
+ root: {
12
+ id: 'media.topbar.root',
13
+ defaultMessage: 'Media Library'
14
+ },
15
+ loading: {
16
+ id: 'media.skeleton.loading',
17
+ defaultMessage: 'Loading media library…'
18
+ }
19
+ });
20
+ /** A placeholder folder row in the nav rail. */
21
+ function FolderRowSkeleton() {
22
+ return (_jsxs("div", { className: "flex items-center gap-2 px-2 py-1.5", children: [_jsx(Skeleton, { className: "size-4 shrink-0 rounded" }), _jsx(Skeleton, { className: "h-4 flex-1" }), _jsx(Skeleton, { className: "h-3 w-6 shrink-0" })] }));
23
+ }
24
+ /**
25
+ * Full-page placeholder for the Media Library's lazy-route `Suspense` fallback,
26
+ * mounted in the workspace shell at `/workspaces/:id/media/*`.
27
+ *
28
+ * The bar is **real**, not a skeleton: it is the page's identity, and it is
29
+ * where the sidebar-reveal trigger lives when the app sidebar is collapsed, so
30
+ * it must paint before the chunk lands. It composes the `TopBar` primitives
31
+ * here rather than reusing {@link MediaTopBar} — that module reaches into the
32
+ * library's own internals and pulls ~19 kB into the eager graph, which is a
33
+ * poor trade for a placeholder. What it renders is the trail the real bar shows
34
+ * at the root folder, so nothing moves when the page takes over; the folder
35
+ * crumbs arrive with the data that names them.
36
+ *
37
+ * The body mirrors the page's two-column shell — the folder rail and the asset
38
+ * grid — so the columns don't jump into place on swap.
39
+ */
40
+ export function MediaLibraryPageSkeleton() {
41
+ const intl = useIntl();
42
+ return (_jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [_jsxs(TopBar, { children: [_jsx(TopBarIcon, { className: "bg-teal-soft text-teal-soft-foreground", children: _jsx(Image, {}) }), _jsx(Breadcrumb, { "aria-label": intl.formatMessage(messages.nav), children: _jsx(BreadcrumbList, { className: "flex-nowrap font-medium", children: _jsx(BreadcrumbItem, { className: "min-w-0 whitespace-nowrap", children: _jsx(BreadcrumbPage, { className: "flex min-w-0 items-center font-medium", children: intl.formatMessage(messages.root) }) }) }) })] }), _jsxs("div", { role: "status", className: "flex min-h-0 min-w-0 flex-1 flex-col lg:flex-row", children: [_jsx("span", { className: "sr-only", children: intl.formatMessage(messages.loading) }), _jsxs("div", { "aria-hidden": true, className: "hidden h-full w-60 shrink-0 flex-col overflow-hidden lg:flex lg:border-r", children: [_jsxs("div", { className: "flex items-center gap-2 p-2", children: [_jsx(Skeleton, { className: "size-4 shrink-0 rounded" }), _jsx(Skeleton, { className: "h-4 w-24" })] }), _jsx("div", { className: "flex flex-col gap-0.5 px-2", children: Array.from({ length: 6 }).map((_, index) => (_jsx(FolderRowSkeleton, {}, index))) })] }), _jsx("div", { "aria-hidden": true, className: "min-w-0 flex-1 overflow-hidden", children: _jsxs("div", { className: "p-4 sm:p-6", children: [_jsxs("div", { className: "mb-4 flex flex-col gap-2", children: [_jsx(Skeleton, { className: "h-8 w-48" }), _jsx(Skeleton, { className: "h-4 w-72 max-w-full" })] }), _jsxs("div", { className: "mb-4 flex flex-wrap items-center gap-3", children: [_jsx(Skeleton, { className: "h-9 w-full sm:max-w-[320px]" }), _jsx(Skeleton, { className: "h-9 w-32" }), _jsx(Skeleton, { className: "ml-auto h-9 w-28" })] }), _jsx("div", { className: "grid grid-cols-2 gap-4 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5", children: Array.from({ length: 10 }).map((_, index) => (_jsxs("div", { className: "flex flex-col gap-2 rounded-xl border p-2", children: [_jsx(Skeleton, { className: "aspect-square w-full rounded-lg" }), _jsx(Skeleton, { className: "h-4 w-3/4" }), _jsx(Skeleton, { className: "h-3 w-1/2" })] }, index))) })] }) })] })] }));
43
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * The grid's footer bar: an assets-per-page selector and an "{from}–{to} of
3
+ * {total}" readout, with previous/next controls once there is more than one
4
+ * page. Mirrors the Members and Activity pagers so the three list surfaces
5
+ * behave the same.
6
+ *
7
+ * Real buttons rather than link-styled anchors, because the page is component
8
+ * state and not yet a URL — they disable cleanly at either end. (Moving this
9
+ * state into the query string belongs with the folder-deep-linking work, which
10
+ * has to put the open folder there too.)
11
+ *
12
+ * Rendered even at one page, so the per-page control and the count stay put
13
+ * instead of appearing and vanishing as a filter narrows the folder.
14
+ */
15
+ export declare function MediaPagination({ page, pageCount, pageSize, total, onPageChange, onPageSizeChange }: {
16
+ /** 1-based current page. */
17
+ page: number;
18
+ /** How many pages the current search and filter produce; at least 1. */
19
+ pageCount: number;
20
+ pageSize: number;
21
+ /** Assets matching across the whole folder — not the number on screen. */
22
+ total: number;
23
+ onPageChange: (page: number) => void;
24
+ onPageSizeChange: (pageSize: number) => void;
25
+ }): import("react").JSX.Element;
26
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaPagination/index.tsx"],"names":[],"mappings":"AAwCA;;;;;;;;;;;;;GAaG;AACH,wBAAgB,eAAe,CAAC,EAC5B,IAAI,EACJ,SAAS,EACT,QAAQ,EACR,KAAK,EACL,YAAY,EACZ,gBAAgB,EACnB,EAAE;IACC,4BAA4B;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,wEAAwE;IACxE,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,gBAAgB,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAChD,+BAmFA"}
@@ -0,0 +1,52 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { defineMessages, useIntl } from 'react-intl';
3
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
4
+ import { Button, Pagination, PaginationContent, PaginationItem, Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@orthacms/design-system';
5
+ import { ASSETS_PAGE_SIZE_OPTIONS } from '../../constants';
6
+ /** Intl descriptors for {@link MediaPagination}, co-located here. */
7
+ const messages = defineMessages({
8
+ perPage: {
9
+ id: 'media.pagination.perPage',
10
+ defaultMessage: 'Assets per page'
11
+ },
12
+ range: {
13
+ id: 'media.pagination.range',
14
+ defaultMessage: '{from}–{to} of {total}'
15
+ },
16
+ previous: {
17
+ id: 'media.pagination.previous',
18
+ defaultMessage: 'Previous page'
19
+ },
20
+ next: {
21
+ id: 'media.pagination.next',
22
+ defaultMessage: 'Next page'
23
+ },
24
+ page: {
25
+ id: 'media.pagination.page',
26
+ defaultMessage: 'Page {page} of {pageCount}'
27
+ }
28
+ });
29
+ /**
30
+ * The grid's footer bar: an assets-per-page selector and an "{from}–{to} of
31
+ * {total}" readout, with previous/next controls once there is more than one
32
+ * page. Mirrors the Members and Activity pagers so the three list surfaces
33
+ * behave the same.
34
+ *
35
+ * Real buttons rather than link-styled anchors, because the page is component
36
+ * state and not yet a URL — they disable cleanly at either end. (Moving this
37
+ * state into the query string belongs with the folder-deep-linking work, which
38
+ * has to put the open folder there too.)
39
+ *
40
+ * Rendered even at one page, so the per-page control and the count stay put
41
+ * instead of appearing and vanishing as a filter narrows the folder.
42
+ */
43
+ export function MediaPagination({ page, pageCount, pageSize, total, onPageChange, onPageSizeChange }) {
44
+ const intl = useIntl();
45
+ const perPageLabel = intl.formatMessage(messages.perPage);
46
+ const from = total === 0 ? 0 : (page - 1) * pageSize + 1;
47
+ const to = Math.min(page * pageSize, total);
48
+ return (_jsxs("div", { className: "mt-4 flex flex-wrap items-center justify-between gap-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: perPageLabel }), _jsxs(Select, { value: String(pageSize), onValueChange: (value) => onPageSizeChange(Number(value)), children: [_jsx(SelectTrigger, { className: "h-8 w-[72px] rounded-lg shadow-none", "aria-label": perPageLabel, children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { className: "rounded-lg shadow-none", children: _jsx(SelectGroup, { children: ASSETS_PAGE_SIZE_OPTIONS.map((size) => (_jsx(SelectItem, { value: String(size), children: size }, size))) }) })] })] }), _jsxs("div", { className: "flex items-center gap-3", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: intl.formatMessage(messages.range, { from, to, total }) }), pageCount > 1 ? (_jsx(Pagination, { className: "mx-0 w-auto", children: _jsxs(PaginationContent, { className: "gap-3", children: [_jsx(PaginationItem, { children: _jsx(Button, { variant: "outline", size: "icon", className: "size-8 shadow-none", disabled: page <= 1, onClick: () => onPageChange(page - 1), "aria-label": intl.formatMessage(messages.previous), children: _jsx(ChevronLeft, {}) }) }), _jsx(PaginationItem, { children: _jsx("span", { className: "text-sm text-muted-foreground", children: intl.formatMessage(messages.page, {
49
+ page,
50
+ pageCount
51
+ }) }) }), _jsx(PaginationItem, { children: _jsx(Button, { variant: "outline", size: "icon", className: "size-8 shadow-none", disabled: page >= pageCount, onClick: () => onPageChange(page + 1), "aria-label": intl.formatMessage(messages.next), children: _jsx(ChevronRight, {}) }) })] }) })) : null] })] }));
52
+ }
@@ -0,0 +1,17 @@
1
+ import type { MediaAsset } from '../../../types/mediaAsset';
2
+ /**
3
+ * One candidate asset in the {@link MediaPickerDialog} — a thumbnail over the
4
+ * file name and its size / dimensions, as a single toggle button (`aria-pressed`
5
+ * carries the selection to assistive tech). A selected tile takes the primary
6
+ * ring and a check; one already attached to the field is marked so the user
7
+ * doesn't pick a duplicate — it stays clickable, since in single mode re-picking
8
+ * the current asset is a legitimate no-op.
9
+ */
10
+ export declare function MediaPickerTile({ asset, picked, attached, onToggle }: {
11
+ asset: MediaAsset;
12
+ picked: boolean;
13
+ /** Already on the field — rendered as a badge, still selectable. */
14
+ attached: boolean;
15
+ onToggle: () => 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/MediaPickerDialog/MediaPickerTile/index.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAgB5D;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,EAC5B,KAAK,EACL,MAAM,EACN,QAAQ,EACR,QAAQ,EACX,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,oEAAoE;IACpE,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,IAAI,CAAC;CACxB,+BAsDA"}
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { defineMessages, useIntl } from 'react-intl';
3
+ import { Check } from 'lucide-react';
4
+ import { cn } from '@orthacms/design-system';
5
+ import { formatBytes } from '../../../utils/formatBytes';
6
+ import { MediaThumbnail } from '../../MediaThumbnail';
7
+ /** Intl descriptors for {@link MediaPickerTile}, co-located. */
8
+ const messages = defineMessages({
9
+ attached: {
10
+ id: 'media.pickerTile.attached',
11
+ defaultMessage: 'Attached'
12
+ },
13
+ attachedHint: {
14
+ id: 'media.pickerTile.attachedHint',
15
+ defaultMessage: '{name} — already attached to this field'
16
+ }
17
+ });
18
+ /**
19
+ * One candidate asset in the {@link MediaPickerDialog} — a thumbnail over the
20
+ * file name and its size / dimensions, as a single toggle button (`aria-pressed`
21
+ * carries the selection to assistive tech). A selected tile takes the primary
22
+ * ring and a check; one already attached to the field is marked so the user
23
+ * doesn't pick a duplicate — it stays clickable, since in single mode re-picking
24
+ * the current asset is a legitimate no-op.
25
+ */
26
+ export function MediaPickerTile({ asset, picked, attached, onToggle }) {
27
+ const intl = useIntl();
28
+ const meta = [
29
+ formatBytes(asset.size),
30
+ asset.dimensions
31
+ ? `${asset.dimensions.width}×${asset.dimensions.height}`
32
+ : null
33
+ ]
34
+ .filter(Boolean)
35
+ .join(' · ');
36
+ return (_jsxs("button", { type: "button", "aria-pressed": picked, onClick: onToggle, title: attached
37
+ ? intl.formatMessage(messages.attachedHint, {
38
+ name: asset.name
39
+ })
40
+ : asset.name, className: cn('group/tile relative block w-full overflow-hidden rounded-lg border bg-card text-left transition hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1', picked
41
+ ? 'border-primary ring-2 ring-primary'
42
+ : 'hover:border-primary/40'), children: [_jsx(MediaThumbnail, { asset: asset, className: "aspect-[4/3] w-full" }), picked ? (_jsx("span", { className: "absolute right-1.5 top-1.5 grid size-5 place-items-center rounded-full bg-primary text-primary-foreground shadow-sm", children: _jsx(Check, { className: "size-3", "aria-hidden": true }) })) : null, attached ? (_jsx("span", { className: "absolute left-1.5 top-1.5 rounded bg-background/85 px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground shadow-sm backdrop-blur", children: intl.formatMessage(messages.attached) })) : null, _jsxs("span", { className: "block min-w-0 px-2.5 py-2", children: [_jsx("span", { className: "block truncate text-xs font-medium", children: asset.name }), _jsx("span", { className: "block truncate text-[11px] text-muted-foreground", children: meta })] })] }));
43
+ }
@@ -0,0 +1,21 @@
1
+ import type { MediaAsset } from '../../types/mediaAsset';
2
+ import { type MediaAccept } from '../../utils/mediaAccept';
3
+ /**
4
+ * A modal picker over the Media Library for a content record's media field.
5
+ * Reuses {@link useMediaLibrary} for browsing — a breadcrumb + folder chips to
6
+ * walk the tree in **both** directions, plus search, a type filter, and a sort —
7
+ * and narrows the candidates to the field's `accept` restriction. Single mode
8
+ * selects exactly one (confirming on pick is avoided so the user can preview);
9
+ * multiple accumulates a set, marking what the field already holds. Fully
10
+ * controlled — the caller owns the field value.
11
+ */
12
+ export declare function MediaPickerDialog({ open, onOpenChange, multiple, accept, attachedIds, onConfirm }: {
13
+ open: boolean;
14
+ onOpenChange: (open: boolean) => void;
15
+ multiple: boolean;
16
+ accept?: MediaAccept;
17
+ /** Ids already on the field — marked in the grid so a pick isn't a duplicate. */
18
+ attachedIds?: string[];
19
+ onConfirm: (assets: MediaAsset[]) => void;
20
+ }): import("react").JSX.Element;
21
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaPickerDialog/index.tsx"],"names":[],"mappings":"AA2CA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAgB,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAwHzE;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,CAAC,EAC9B,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,MAAM,EACN,WAAW,EACX,SAAS,EACZ,EAAE;IACC,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACtC,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,iFAAiF;IACjF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,SAAS,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;CAC7C,+BAuVA"}
@@ -0,0 +1,208 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useState } from 'react';
3
+ import { defineMessages, useIntl } from 'react-intl';
4
+ import { FileSearch, Folder, Search, TriangleAlert } from 'lucide-react';
5
+ import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, Button, Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, Empty, EmptyContent, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle, InputGroup, InputGroupAddon, InputGroupInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Skeleton } from '@orthacms/design-system';
6
+ import { KIND_FILTER_ALL, MEDIA_KIND, MEDIA_SORT, PICKER_ASSETS_PAGE_SIZE, ROOT_FOLDER_ID } from '../../constants';
7
+ import { useMediaLibrary } from '../../hooks/useMediaLibrary';
8
+ import { acceptsAsset } from '../../utils/mediaAccept';
9
+ import { MediaPickerTile } from './MediaPickerTile';
10
+ const messages = defineMessages({
11
+ titleSingle: {
12
+ id: 'media.picker.titleSingle',
13
+ defaultMessage: 'Select an asset'
14
+ },
15
+ titleMultiple: {
16
+ id: 'media.picker.titleMultiple',
17
+ defaultMessage: 'Select assets'
18
+ },
19
+ descriptionSingle: {
20
+ id: 'media.picker.descriptionSingle',
21
+ defaultMessage: 'Browse the Media Library and pick the asset for this field.'
22
+ },
23
+ descriptionMultiple: {
24
+ id: 'media.picker.descriptionMultiple',
25
+ defaultMessage: 'Browse the Media Library and pick every asset to attach — they are added in the order you select them.'
26
+ },
27
+ search: { id: 'media.picker.search', defaultMessage: 'Search assets' },
28
+ searchPlaceholder: {
29
+ id: 'media.picker.searchPlaceholder',
30
+ defaultMessage: 'Search name or tag…'
31
+ },
32
+ root: { id: 'media.picker.root', defaultMessage: 'All media' },
33
+ breadcrumb: {
34
+ id: 'media.picker.breadcrumb',
35
+ defaultMessage: 'Folder path'
36
+ },
37
+ folders: { id: 'media.picker.folders', defaultMessage: 'Folders' },
38
+ kindLabel: {
39
+ id: 'media.picker.kindLabel',
40
+ defaultMessage: 'Filter by type'
41
+ },
42
+ kindAll: { id: 'media.picker.kindAll', defaultMessage: 'All types' },
43
+ kindImage: { id: 'media.picker.kindImage', defaultMessage: 'Images' },
44
+ kindVideo: { id: 'media.picker.kindVideo', defaultMessage: 'Videos' },
45
+ kindAudio: { id: 'media.picker.kindAudio', defaultMessage: 'Audio' },
46
+ kindDocument: {
47
+ id: 'media.picker.kindDocument',
48
+ defaultMessage: 'Documents'
49
+ },
50
+ kindArchive: { id: 'media.picker.kindArchive', defaultMessage: 'Archives' },
51
+ sortLabel: { id: 'media.picker.sortLabel', defaultMessage: 'Sort' },
52
+ sortNewest: {
53
+ id: 'media.picker.sortNewest',
54
+ defaultMessage: 'Newest first'
55
+ },
56
+ sortOldest: {
57
+ id: 'media.picker.sortOldest',
58
+ defaultMessage: 'Oldest first'
59
+ },
60
+ sortNameAsc: { id: 'media.picker.sortNameAsc', defaultMessage: 'Name A–Z' },
61
+ sortNameDesc: {
62
+ id: 'media.picker.sortNameDesc',
63
+ defaultMessage: 'Name Z–A'
64
+ },
65
+ emptyTitle: {
66
+ id: 'media.picker.emptyTitle',
67
+ defaultMessage: 'Nothing to pick here'
68
+ },
69
+ emptyBody: {
70
+ id: 'media.picker.emptyBody',
71
+ defaultMessage: 'This folder has no asset this field accepts. Try another folder, or upload a file from the field.'
72
+ },
73
+ filteredTitle: {
74
+ id: 'media.picker.filteredTitle',
75
+ defaultMessage: 'No matching assets'
76
+ },
77
+ filteredBody: {
78
+ id: 'media.picker.filteredBody',
79
+ defaultMessage: 'Try a different search term or clear the filters.'
80
+ },
81
+ clear: { id: 'media.picker.clear', defaultMessage: 'Clear filters' },
82
+ errorTitle: {
83
+ id: 'media.picker.errorTitle',
84
+ defaultMessage: 'Couldn’t load the library'
85
+ },
86
+ errorBody: {
87
+ id: 'media.picker.errorBody',
88
+ defaultMessage: 'The Media Library didn’t load — this isn’t the same as having no assets. Try again, or check that you have access to it.'
89
+ },
90
+ retry: { id: 'media.picker.retry', defaultMessage: 'Try again' },
91
+ cancel: { id: 'media.picker.cancel', defaultMessage: 'Cancel' },
92
+ confirmSingle: {
93
+ id: 'media.picker.confirmSingle',
94
+ defaultMessage: 'Select asset'
95
+ },
96
+ confirmMultiple: {
97
+ id: 'media.picker.confirmMultiple',
98
+ defaultMessage: 'Add selected'
99
+ },
100
+ selected: {
101
+ id: 'media.picker.selected',
102
+ defaultMessage: '{count, plural, one {# asset selected} other {# assets selected}}'
103
+ },
104
+ nothingSelected: {
105
+ id: 'media.picker.nothingSelected',
106
+ defaultMessage: 'Nothing selected yet'
107
+ }
108
+ });
109
+ /** The filter label for each asset kind, so the select can list only allowed ones. */
110
+ const KIND_LABEL = {
111
+ [MEDIA_KIND.Image]: messages.kindImage,
112
+ [MEDIA_KIND.Video]: messages.kindVideo,
113
+ [MEDIA_KIND.Audio]: messages.kindAudio,
114
+ [MEDIA_KIND.Document]: messages.kindDocument,
115
+ [MEDIA_KIND.Archive]: messages.kindArchive
116
+ };
117
+ /** How many placeholder tiles stand in for the first, unknown-size page. */
118
+ const SKELETON_TILES = 8;
119
+ /**
120
+ * A modal picker over the Media Library for a content record's media field.
121
+ * Reuses {@link useMediaLibrary} for browsing — a breadcrumb + folder chips to
122
+ * walk the tree in **both** directions, plus search, a type filter, and a sort —
123
+ * and narrows the candidates to the field's `accept` restriction. Single mode
124
+ * selects exactly one (confirming on pick is avoided so the user can preview);
125
+ * multiple accumulates a set, marking what the field already holds. Fully
126
+ * controlled — the caller owns the field value.
127
+ */
128
+ export function MediaPickerDialog({ open, onOpenChange, multiple, accept, attachedIds, onConfirm }) {
129
+ const intl = useIntl();
130
+ // No pager in this dialog: it is a picker, and the way you narrow it is by
131
+ // typing. One large page keeps that true — and the search reaching it is now
132
+ // the server's, so it finds a file anywhere in the folder rather than only
133
+ // among the ones that happened to load.
134
+ const library = useMediaLibrary(open, {
135
+ pageSize: PICKER_ASSETS_PAGE_SIZE
136
+ });
137
+ const [picked, setPicked] = useState(new Map());
138
+ const attached = useMemo(() => new Set(attachedIds ?? []), [attachedIds]);
139
+ // Candidates: the folder's search/kind-filtered assets, further restricted
140
+ // to what the field accepts (the server enforces this too, on save).
141
+ const candidates = useMemo(() => library.visibleAssets.filter((a) => acceptsAsset(accept, a)), [library.visibleAssets, accept]);
142
+ // Only the kinds this field can hold are worth filtering by; a field pinned
143
+ // to one kind gets no select at all (it would have a single option).
144
+ const kindOptions = useMemo(() => {
145
+ const all = Object.values(MEDIA_KIND);
146
+ if (accept?.mimeTypes?.length || !accept?.kinds?.length)
147
+ return all;
148
+ const allowed = accept.kinds;
149
+ return all.filter((kind) => allowed.includes(kind));
150
+ }, [accept]);
151
+ const hasFilters = !!library.search.trim() || library.kindFilter !== KIND_FILTER_ALL;
152
+ const clearFilters = () => {
153
+ library.setSearch('');
154
+ library.setKindFilter(KIND_FILTER_ALL);
155
+ };
156
+ const toggle = (asset) => {
157
+ setPicked((current) => {
158
+ const next = new Map(multiple ? current : []);
159
+ if (next.has(asset.id))
160
+ next.delete(asset.id);
161
+ else
162
+ next.set(asset.id, asset);
163
+ return next;
164
+ });
165
+ };
166
+ const close = () => {
167
+ setPicked(new Map());
168
+ // A search left over from the last visit would silently hide the library
169
+ // the next time the dialog opens.
170
+ clearFilters();
171
+ onOpenChange(false);
172
+ };
173
+ const confirm = () => {
174
+ onConfirm([...picked.values()]);
175
+ close();
176
+ };
177
+ const crumbs = [
178
+ { id: ROOT_FOLDER_ID, name: intl.formatMessage(messages.root) },
179
+ ...library.breadcrumbs.map((folder) => ({
180
+ id: folder.id,
181
+ name: folder.name
182
+ }))
183
+ ];
184
+ const lastCrumb = crumbs.length - 1;
185
+ return (_jsx(Dialog, { open: open, onOpenChange: (next) => (next ? null : close()), children: _jsxs(DialogContent, { className: "flex max-h-[85vh] w-[min(56rem,92vw)] max-w-none flex-col gap-4 overflow-hidden", children: [_jsxs(DialogHeader, { children: [_jsx(DialogTitle, { children: intl.formatMessage(multiple
186
+ ? messages.titleMultiple
187
+ : messages.titleSingle) }), _jsx(DialogDescription, { children: intl.formatMessage(multiple
188
+ ? messages.descriptionMultiple
189
+ : messages.descriptionSingle) })] }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs(InputGroup, { className: "w-full shadow-none sm:w-auto sm:min-w-[16rem] sm:flex-1", children: [_jsx(InputGroupAddon, { children: _jsx(Search, {}) }), _jsx(InputGroupInput, { type: "search", value: library.search, onChange: (event) => library.setSearch(event.target.value), "aria-label": intl.formatMessage(messages.search), placeholder: intl.formatMessage(messages.searchPlaceholder) })] }), kindOptions.length > 1 ? (_jsxs(Select, { value: library.kindFilter, onValueChange: (value) => library.setKindFilter(value), children: [_jsx(SelectTrigger, { className: "w-[9.5rem] shadow-none", "aria-label": intl.formatMessage(messages.kindLabel), children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: KIND_FILTER_ALL, children: intl.formatMessage(messages.kindAll) }), kindOptions.map((kind) => (_jsx(SelectItem, { value: kind, children: intl.formatMessage(KIND_LABEL[kind]) }, kind)))] })] })) : null, _jsxs(Select, { value: library.sort, onValueChange: (value) => library.setSort(value), children: [_jsx(SelectTrigger, { className: "w-[9.5rem] shadow-none", "aria-label": intl.formatMessage(messages.sortLabel), children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: MEDIA_SORT.Newest, children: intl.formatMessage(messages.sortNewest) }), _jsx(SelectItem, { value: MEDIA_SORT.Oldest, children: intl.formatMessage(messages.sortOldest) }), _jsx(SelectItem, { value: MEDIA_SORT.NameAsc, children: intl.formatMessage(messages.sortNameAsc) }), _jsx(SelectItem, { value: MEDIA_SORT.NameDesc, children: intl.formatMessage(messages.sortNameDesc) })] })] })] }), _jsx(Breadcrumb, { "aria-label": intl.formatMessage(messages.breadcrumb), className: "min-h-8", children: _jsx(BreadcrumbList, { className: "text-xs", children: crumbs.map((crumb, index) => [
190
+ index > 0 ? (_jsx(BreadcrumbSeparator, {}, `${crumb.id}-sep`)) : null,
191
+ _jsx(BreadcrumbItem, { className: "min-w-0 whitespace-nowrap", children: index === lastCrumb ? (_jsx(BreadcrumbPage, { className: "font-medium", children: crumb.name })) : (_jsx(BreadcrumbLink, { asChild: true, children: _jsx("button", { type: "button", onClick: () => library.navigateTo(crumb.id), children: crumb.name }) })) }, crumb.id)
192
+ ]) }) }), _jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto rounded-lg border bg-muted/20 p-3", children: [library.childFolders.length > 0 ? (_jsx("ul", { "aria-label": intl.formatMessage(messages.folders), className: "mb-3 flex flex-wrap gap-1.5", children: library.childFolders.map((folder) => (_jsx("li", { children: _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "bg-card shadow-none", onClick: () => library.navigateTo(folder.id), children: [_jsx(Folder, { className: "size-3.5", "aria-hidden": true }), folder.name] }) }, folder.id))) })) : null, library.isError ? (
193
+ /* An error is NOT the empty state: telling someone the
194
+ folder has nothing they can pick, when the read
195
+ actually failed (or they lack `media:read`), sends
196
+ them off to hunt for assets that are right there. */
197
+ _jsxs(Empty, { className: "py-10", children: [_jsxs(EmptyHeader, { children: [_jsx(EmptyMedia, { variant: "icon", children: _jsx(TriangleAlert, { "aria-hidden": true }) }), _jsx(EmptyTitle, { className: "text-base", children: intl.formatMessage(messages.errorTitle) }), _jsx(EmptyDescription, { role: "alert", children: intl.formatMessage(messages.errorBody) })] }), _jsx(EmptyContent, { children: _jsx(Button, { type: "button", variant: "outline", className: "shadow-none", onClick: library.reload, children: intl.formatMessage(messages.retry) }) })] })) : library.isLoading ? (_jsx("ul", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4", children: Array.from({ length: SKELETON_TILES }, (_, index) => (_jsx("li", { children: _jsx(Skeleton, { className: "aspect-[4/3] w-full rounded-lg" }) }, index))) })) : candidates.length === 0 ? (_jsxs(Empty, { className: "py-10", children: [_jsxs(EmptyHeader, { children: [_jsx(EmptyMedia, { variant: "icon", children: _jsx(FileSearch, { "aria-hidden": true }) }), _jsx(EmptyTitle, { className: "text-base", children: intl.formatMessage(hasFilters
198
+ ? messages.filteredTitle
199
+ : messages.emptyTitle) }), _jsx(EmptyDescription, { children: intl.formatMessage(hasFilters
200
+ ? messages.filteredBody
201
+ : messages.emptyBody) })] }), hasFilters ? (_jsx(EmptyContent, { children: _jsx(Button, { type: "button", variant: "outline", className: "shadow-none", onClick: clearFilters, children: intl.formatMessage(messages.clear) }) })) : null] })) : (_jsx("ul", { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 md:grid-cols-4", children: candidates.map((asset) => (_jsx("li", { children: _jsx(MediaPickerTile, { asset: asset, picked: picked.has(asset.id), attached: attached.has(asset.id), onToggle: () => toggle(asset) }) }, asset.id))) }))] }), _jsxs(DialogFooter, { className: "items-center sm:justify-between", children: [_jsx("p", { className: "text-sm text-muted-foreground", children: picked.size > 0
202
+ ? intl.formatMessage(messages.selected, {
203
+ count: picked.size
204
+ })
205
+ : intl.formatMessage(messages.nothingSelected) }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Button, { type: "button", variant: "ghost", onClick: close, children: intl.formatMessage(messages.cancel) }), _jsx(Button, { type: "button", onClick: confirm, disabled: picked.size === 0, children: intl.formatMessage(multiple
206
+ ? messages.confirmMultiple
207
+ : messages.confirmSingle) })] })] })] }) }));
208
+ }