@orthacms/media-admin 0.10.3 → 0.12.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 (66) hide show
  1. package/dist/lib/components/AssetDetailPanel/MetaRow/index.d.ts +11 -0
  2. package/dist/lib/components/AssetDetailPanel/MetaRow/index.d.ts.map +1 -0
  3. package/dist/lib/components/AssetDetailPanel/MetaRow/index.js +9 -0
  4. package/dist/lib/components/AssetDetailPanel/index.d.ts +35 -0
  5. package/dist/lib/components/AssetDetailPanel/index.d.ts.map +1 -0
  6. package/dist/lib/components/AssetDetailPanel/index.js +158 -0
  7. package/dist/lib/components/FadeInImage/index.d.ts +24 -0
  8. package/dist/lib/components/FadeInImage/index.d.ts.map +1 -0
  9. package/dist/lib/components/FadeInImage/index.js +34 -0
  10. package/dist/lib/components/FolderActionsMenu/index.d.ts +3 -1
  11. package/dist/lib/components/FolderActionsMenu/index.d.ts.map +1 -1
  12. package/dist/lib/components/FolderActionsMenu/index.js +3 -3
  13. package/dist/lib/components/MediaAssetTable/index.d.ts +25 -0
  14. package/dist/lib/components/MediaAssetTable/index.d.ts.map +1 -0
  15. package/dist/lib/components/MediaAssetTable/index.js +62 -0
  16. package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.d.ts.map +1 -1
  17. package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.js +2 -1
  18. package/dist/lib/components/MediaFieldControl/index.d.ts.map +1 -1
  19. package/dist/lib/components/MediaFieldControl/index.js +19 -14
  20. package/dist/lib/components/MediaFieldInput/index.d.ts +19 -0
  21. package/dist/lib/components/MediaFieldInput/index.d.ts.map +1 -0
  22. package/dist/lib/components/MediaFieldInput/index.js +23 -0
  23. package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts +17 -3
  24. package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts.map +1 -1
  25. package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.js +16 -6
  26. package/dist/lib/components/MediaFoldersNav/index.d.ts +10 -5
  27. package/dist/lib/components/MediaFoldersNav/index.d.ts.map +1 -1
  28. package/dist/lib/components/MediaFoldersNav/index.js +17 -9
  29. package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts +15 -4
  30. package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts.map +1 -1
  31. package/dist/lib/components/MediaGrid/MediaAssetCard/index.js +27 -11
  32. package/dist/lib/components/MediaGrid/index.d.ts +10 -10
  33. package/dist/lib/components/MediaGrid/index.d.ts.map +1 -1
  34. package/dist/lib/components/MediaGrid/index.js +13 -15
  35. package/dist/lib/components/MediaSelectionBar/index.d.ts +9 -5
  36. package/dist/lib/components/MediaSelectionBar/index.d.ts.map +1 -1
  37. package/dist/lib/components/MediaSelectionBar/index.js +16 -10
  38. package/dist/lib/components/MediaThumbnail/index.d.ts +11 -7
  39. package/dist/lib/components/MediaThumbnail/index.d.ts.map +1 -1
  40. package/dist/lib/components/MediaThumbnail/index.js +10 -7
  41. package/dist/lib/components/MediaToolbar/index.d.ts +10 -8
  42. package/dist/lib/components/MediaToolbar/index.d.ts.map +1 -1
  43. package/dist/lib/components/MediaToolbar/index.js +37 -13
  44. package/dist/lib/components/MediaTopBar/index.d.ts +7 -1
  45. package/dist/lib/components/MediaTopBar/index.d.ts.map +1 -1
  46. package/dist/lib/components/MediaTopBar/index.js +6 -3
  47. package/dist/lib/components/MediaUploadBanner/index.d.ts +7 -5
  48. package/dist/lib/components/MediaUploadBanner/index.d.ts.map +1 -1
  49. package/dist/lib/components/MediaUploadBanner/index.js +15 -8
  50. package/dist/lib/components/UploadDialog/StagedFileRow/index.d.ts.map +1 -1
  51. package/dist/lib/components/UploadDialog/StagedFileRow/index.js +2 -1
  52. package/dist/lib/constants/index.d.ts +26 -0
  53. package/dist/lib/constants/index.d.ts.map +1 -1
  54. package/dist/lib/constants/index.js +24 -0
  55. package/dist/lib/hooks/useUploadQueue/index.d.ts +2 -0
  56. package/dist/lib/hooks/useUploadQueue/index.d.ts.map +1 -1
  57. package/dist/lib/hooks/useUploadQueue/index.js +15 -1
  58. package/dist/lib/pages/MediaLibraryPage/index.d.ts +6 -4
  59. package/dist/lib/pages/MediaLibraryPage/index.d.ts.map +1 -1
  60. package/dist/lib/pages/MediaLibraryPage/index.js +86 -38
  61. package/dist/lib/utils/fileExtension/index.d.ts +9 -0
  62. package/dist/lib/utils/fileExtension/index.d.ts.map +1 -0
  63. package/dist/lib/utils/fileExtension/index.js +14 -0
  64. package/dist/lib/utils/mediaPlugin/index.d.ts.map +1 -1
  65. package/dist/lib/utils/mediaPlugin/index.js +19 -24
  66. package/package.json +10 -10
@@ -1,10 +1,10 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { useRef, useState } from 'react';
3
3
  import { defineMessages, useIntl } from 'react-intl';
4
- import { Button, ConfirmDialog, Drawer, DrawerContent, DrawerTitle, cn, toast } from '@orthacms/design-system';
5
- import { PanelLeft } from 'lucide-react';
4
+ import { Button, ConfirmDialog, Drawer, DrawerContent, DrawerTitle, ResizeHandle, cn, toast, useResizablePanel } from '@orthacms/design-system';
5
+ import { FolderPlus, PanelLeft, UploadCloud } from 'lucide-react';
6
6
  import { useHasPermission } from '@orthacms/identity-admin';
7
- import { MEDIA_CREATE, MEDIA_DELETE, MEDIA_READ, MEDIA_UPDATE, ROOT_FOLDER_ID } from '../../constants';
7
+ import { MEDIA_CREATE, MEDIA_DELETE, MEDIA_FOLDERS_PANEL, MEDIA_READ, MEDIA_UPDATE, MEDIA_VIEW, MEDIA_VIEW_STORAGE_KEY, ROOT_FOLDER_ID } from '../../constants';
8
8
  import { useMediaLibrary } from '../../hooks/useMediaLibrary';
9
9
  import { MediaFoldersNav } from '../../components/MediaFoldersNav';
10
10
  import { MediaTopBar } from '../../components/MediaTopBar';
@@ -12,9 +12,10 @@ import { MediaToolbar } from '../../components/MediaToolbar';
12
12
  import { MediaSelectionBar } from '../../components/MediaSelectionBar';
13
13
  import { MediaUploadBanner } from '../../components/MediaUploadBanner';
14
14
  import { MediaGrid } from '../../components/MediaGrid';
15
+ import { MediaAssetTable } from '../../components/MediaAssetTable';
15
16
  import { MediaPagination } from '../../components/MediaPagination';
16
17
  import { MediaEmptyState } from '../../components/MediaEmptyState';
17
- import { AssetDetailDrawer } from '../../components/AssetDetailDrawer';
18
+ import { AssetDetailPanel } from '../../components/AssetDetailPanel';
18
19
  import { NewFolderDialog } from '../../components/NewFolderDialog';
19
20
  import { RenameDialog } from '../../components/RenameDialog';
20
21
  import { MoveAssetsDialog } from '../../components/MoveAssetsDialog';
@@ -24,10 +25,12 @@ import { useDocumentTitle } from '@orthacms/utils-admin';
24
25
  /** Intl descriptors for the Media Library page + its toasts, co-located. */
25
26
  const messages = defineMessages({
26
27
  title: { id: 'media.page.title', defaultMessage: 'Media Library' },
27
- subtitle: {
28
- id: 'media.page.subtitle',
29
- defaultMessage: '{count, plural, one {# item} other {# items}} in {location}'
28
+ fileCount: {
29
+ id: 'media.page.fileCount',
30
+ defaultMessage: '{count, plural, one {# file} other {# files}}'
30
31
  },
32
+ newFolder: { id: 'media.page.newFolder', defaultMessage: 'New folder' },
33
+ upload: { id: 'media.page.upload', defaultMessage: 'Upload' },
31
34
  allMedia: { id: 'media.page.allMedia', defaultMessage: 'All media' },
32
35
  noAccess: {
33
36
  id: 'media.page.noAccess',
@@ -39,6 +42,7 @@ const messages = defineMessages({
39
42
  },
40
43
  retry: { id: 'media.page.retry', defaultMessage: 'Try again' },
41
44
  openNav: { id: 'media.page.openNav', defaultMessage: 'Folders' },
45
+ resizeNav: { id: 'media.page.resizeNav', defaultMessage: 'Resize folders' },
42
46
  navTitle: {
43
47
  id: 'media.page.navTitle',
44
48
  defaultMessage: 'Folder navigation'
@@ -132,19 +136,31 @@ const messages = defineMessages({
132
136
  * nothing.
133
137
  */
134
138
  const FOCUS_RECLAIM_MS = 1_000;
139
+ /** The remembered view, or the grid — storage can be absent or refuse. */
140
+ function readView() {
141
+ try {
142
+ const stored = window.localStorage.getItem(MEDIA_VIEW_STORAGE_KEY);
143
+ return stored === MEDIA_VIEW.List ? MEDIA_VIEW.List : MEDIA_VIEW.Grid;
144
+ }
145
+ catch {
146
+ return MEDIA_VIEW.Grid;
147
+ }
148
+ }
135
149
  /**
136
150
  * The Media Library, mounted inside the workspace shell at
137
151
  * `/workspaces/:id/media`. `useMediaLibrary` is the store — folders and the open
138
152
  * folder's assets come from `@orthacms/media-server`, and every action (browse
139
153
  * folders, upload, create folder, rename, duplicate, move, delete, filter, sort,
140
- * open the detail drawer) runs against the API, firing a toast per action —
154
+ * open the details panel) runs against the API, firing a toast per action —
141
155
  * except uploads, whose status lives in the `MediaUploadBanner`. Controls gate
142
156
  * on the real `media:*` matrix via `useHasPermission`.
143
157
  *
144
158
  * A sticky `MediaTopBar` heads the page (the shared icon-tile + breadcrumb
145
- * header every admin page carries), spanning both panes. Below it sits a
146
- * two-pane layout — a folders sidebar beside the asset browser — plus the detail
147
- * drawer and the create/rename/move/upload dialogs. Both panes sit
159
+ * header every admin page carries, with New folder / Upload at its right end),
160
+ * spanning every pane. Below it: the folder tree, the file browser (title, type
161
+ * chips + search + sort + Grid/List, the selection bar, then tiles or the
162
+ * table) and — while a file is open — the non-modal details panel as a third
163
+ * column; plus the create/rename/move/upload dialogs. The panes sit
148
164
  * **flush on the canvas** (no muted board, no bordered island), mirroring the
149
165
  * Content Library's work area: the sidebar is divided by a hairline `border-r`
150
166
  * and each pane scrolls independently. Sized with `flex-1` against the workspace
@@ -170,6 +186,7 @@ export function MediaLibraryPage() {
170
186
  /** Set by a confirmed delete, read once by the dialog's close-focus hook. */
171
187
  const focusGridOnCloseRef = useRef(false);
172
188
  const [navOpen, setNavOpen] = useState(false);
189
+ const foldersPanel = useResizablePanel(MEDIA_FOLDERS_PANEL);
173
190
  const [newFolderOpen, setNewFolderOpen] = useState(false);
174
191
  const [uploadOpen, setUploadOpen] = useState(false);
175
192
  const [renameTarget, setRenameTarget] = useState(null);
@@ -177,6 +194,16 @@ export function MediaLibraryPage() {
177
194
  const [deleteTarget, setDeleteTarget] = useState(null);
178
195
  /** A confirmed delete is in flight — the dialog shows a spinner meanwhile. */
179
196
  const [deleting, setDeleting] = useState(false);
197
+ const [view, setViewState] = useState(readView);
198
+ const setView = (next) => {
199
+ setViewState(next);
200
+ try {
201
+ window.localStorage.setItem(MEDIA_VIEW_STORAGE_KEY, next);
202
+ }
203
+ catch {
204
+ // A private window or blocked storage: the choice lasts the visit.
205
+ }
206
+ };
180
207
  /**
181
208
  * Puts focus on the grid and **keeps** it there for a few frames.
182
209
  *
@@ -211,14 +238,29 @@ export function MediaLibraryPage() {
211
238
  ? store.currentFolder.name
212
239
  : intl.formatMessage(messages.allMedia);
213
240
  const hasFilters = store.search.trim() !== '' || store.kindFilter !== 'all';
214
- // Sub-folders belong to the folder, not to a page of its assets, so they are
215
- // drawn once on the first page rather than repeating above every page of
216
- // files. On any later page the grid is assets alone.
217
- const showFolders = store.page === 1;
218
- const visibleFolders = showFolders ? store.childFolders : [];
219
- // Emptiness is now a fact about the whole folder, not about what happened to
220
- // load: `total` counts every asset matching the search and filter across it.
221
- const isEmpty = visibleFolders.length === 0 && store.total === 0;
241
+ // Emptiness is a fact about the whole folder, not about what happened to
242
+ // load: `total` counts every asset matching the search and filter across
243
+ // it. Sub-folders don't count — they are drawn in the tree, not the grid.
244
+ const isEmpty = store.total === 0;
245
+ // The details panel steps through the files on screen, in their order.
246
+ const detailIndex = store.detailAsset
247
+ ? store.visibleAssets.findIndex((a) => a.id === store.detailAsset?.id)
248
+ : -1;
249
+ const previousAsset = detailIndex > 0 ? store.visibleAssets[detailIndex - 1] : undefined;
250
+ const nextAsset = detailIndex >= 0 ? store.visibleAssets[detailIndex + 1] : undefined;
251
+ // The table's header checkbox: select the page, or clear it when it's all
252
+ // selected already.
253
+ const toggleAllOnPage = () => {
254
+ const page = store.visibleAssets;
255
+ if (page.length > 0 && page.every((a) => store.selectedIds.has(a.id))) {
256
+ store.clearSelection();
257
+ return;
258
+ }
259
+ for (const asset of page) {
260
+ if (!store.selectedIds.has(asset.id))
261
+ store.toggleSelect(asset.id);
262
+ }
263
+ };
222
264
  // Triggers a browser download for one asset (its bytes stream from the
223
265
  // `media:read`-gated raw route). Shared by the row action and bulk download.
224
266
  const downloadAsset = (asset) => {
@@ -229,7 +271,7 @@ export function MediaLibraryPage() {
229
271
  link.click();
230
272
  link.remove();
231
273
  };
232
- // A single dispatcher every card / row / drawer forwards asset actions to.
274
+ // A single dispatcher every tile / row / panel forwards asset actions to.
233
275
  const handleAssetAction = (kind, asset) => {
234
276
  switch (kind) {
235
277
  case 'open':
@@ -346,14 +388,13 @@ export function MediaLibraryPage() {
346
388
  if (store.isError) {
347
389
  return (_jsxs("div", { className: "flex min-h-0 flex-1 flex-col items-center justify-center gap-3 p-6 text-center", children: [_jsx("p", { className: "max-w-sm text-sm text-muted-foreground", role: "alert", children: intl.formatMessage(messages.loadError) }), _jsx(Button, { variant: "outline", onClick: store.reload, children: intl.formatMessage(messages.retry) })] }));
348
390
  }
349
- return (_jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [_jsx(MediaTopBar, { breadcrumbs: store.breadcrumbs, onNavigate: store.navigateTo }), _jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col lg:flex-row", children: [_jsx("div", { className: "border-b p-3 lg:hidden", children: _jsxs(Button, { variant: "outline", size: "sm", className: "shadow-none", onClick: () => setNavOpen(true), children: [_jsx(PanelLeft, { "aria-hidden": true }), intl.formatMessage(messages.openNav)] }) }), _jsx(MediaFoldersNav, { className: "hidden lg:flex lg:border-r", folders: store.folders, currentFolderId: store.currentFolderId, counts: store.folderCounts, rootCount: store.folderCounts.get(ROOT_FOLDER_ID) ?? 0, onNavigate: store.navigateTo, onNewFolder: () => setNewFolderOpen(true), canCreate: canCreate }), _jsx("div", { className: "min-w-0 flex-1 overflow-auto", children: _jsxs("div", { className: "p-4 sm:p-6", children: [_jsxs("div", { className: "mb-4", children: [_jsx("h1", { className: "text-2xl font-semibold tracking-[-0.01em]", children: store.currentFolder
391
+ return (_jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col", children: [_jsx(MediaTopBar, { breadcrumbs: store.breadcrumbs, onNavigate: store.navigateTo, actions: canCreate ? (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", size: "sm", className: "shadow-none", onClick: () => setNewFolderOpen(true), children: [_jsx(FolderPlus, { "aria-hidden": true }), _jsx("span", { className: "hidden sm:inline", children: intl.formatMessage(messages.newFolder) })] }), _jsxs(Button, { size: "sm", onClick: () => setUploadOpen(true), children: [_jsx(UploadCloud, { "aria-hidden": true }), intl.formatMessage(messages.upload)] })] })) : null }), _jsxs("div", { className: "flex min-h-0 min-w-0 flex-1 flex-col lg:flex-row", children: [_jsx("div", { className: "border-b p-3 lg:hidden", children: _jsxs(Button, { variant: "outline", size: "sm", className: "shadow-none", onClick: () => setNavOpen(true), children: [_jsx(PanelLeft, { "aria-hidden": true }), intl.formatMessage(messages.openNav)] }) }), _jsxs("div", { ref: foldersPanel.ref, style: foldersPanel.style, className: "relative hidden shrink-0 lg:flex", children: [_jsx(MediaFoldersNav, { className: "w-(--media-folders-width) border-r", folders: store.folders, currentFolderId: store.currentFolderId, counts: store.folderCounts, rootCount: store.folderCounts.get(ROOT_FOLDER_ID) ?? 0, onNavigate: store.navigateTo, onNewFolder: () => setNewFolderOpen(true), onRenameFolder: handleRenameFolder, onDeleteFolder: handleDeleteFolder, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete }), _jsx(ResizeHandle, { ...foldersPanel.handleProps, edge: "right", label: intl.formatMessage(messages.resizeNav) })] }), _jsx("div", { className: "min-w-0 flex-1 overflow-auto", children: _jsxs("div", { className: cn('px-4 py-6 sm:px-8', selectedIds.length > 0 && 'pb-24'), children: [_jsxs("div", { className: "mb-4", children: [_jsx("h1", { className: "text-2xl font-semibold tracking-[-0.01em]", children: store.currentFolder
350
392
  ? store.currentFolder.name
351
- : intl.formatMessage(messages.title) }), _jsx("p", { className: "mt-0.5 text-sm text-muted-foreground", children: intl.formatMessage(messages.subtitle, {
352
- count: store.total,
353
- location: locationLabel
354
- }) })] }), _jsx(MediaToolbar, { search: store.search, onSearchChange: store.setSearch, kindFilter: store.kindFilter, onKindFilterChange: store.setKindFilter, sort: store.sort, onSortChange: store.setSort, onNewFolder: () => setNewFolderOpen(true), onUpload: () => setUploadOpen(true), canCreate: canCreate }), _jsx("p", { className: "sr-only", "aria-live": "polite", children: intl.formatMessage(messages.selection, {
393
+ : intl.formatMessage(messages.title) }), _jsx("p", { className: "mt-0.5 text-[13px] text-muted-foreground", children: intl.formatMessage(messages.fileCount, {
394
+ count: store.total
395
+ }) })] }), _jsx(MediaToolbar, { search: store.search, onSearchChange: store.setSearch, kindFilter: store.kindFilter, onKindFilterChange: store.setKindFilter, sort: store.sort, onSortChange: store.setSort, view: view, onViewChange: setView }), _jsx("p", { className: "sr-only", "aria-live": "polite", children: intl.formatMessage(messages.selection, {
355
396
  count: selectedIds.length
356
- }) }), _jsx(MediaUploadBanner, { items: store.uploadItems, summary: store.uploadSummary, onRetry: store.retryUpload, onCancel: store.cancelUpload, onDismiss: store.dismissUploads }), selectedIds.length > 0 ? (_jsx(MediaSelectionBar, { count: selectedIds.length, onDownload: () => {
397
+ }) }), _jsx(MediaUploadBanner, { items: store.uploadItems, summary: store.uploadSummary, onRetry: store.retryUpload, onCancel: store.cancelUpload, onDismiss: store.dismissUploads }), selectedIds.length > 0 ? (_jsx(MediaSelectionBar, { count: selectedIds.length, total: store.total, onDownload: () => {
357
398
  store.selectedAssets.forEach(downloadAsset);
358
399
  toast.success(intl.formatMessage(messages.tDownload, {
359
400
  name: `${selectedIds.length} files`
@@ -370,24 +411,31 @@ export function MediaLibraryPage() {
370
411
  }, onMove: () => setMoveIds(selectedIds), onDelete: () => setDeleteTarget({
371
412
  kind: 'assets',
372
413
  ids: selectedIds
373
- }), onClear: store.clearSelection, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete })) : null, _jsx("section", { ref: gridRef, tabIndex: -1, "aria-label": intl.formatMessage(messages.assetsRegion), "aria-busy": store.isRefreshing, className: cn('transition-opacity focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', store.isRefreshing && 'opacity-60'), children: isEmpty ? (_jsx(MediaEmptyState, { hasFilters: hasFilters, canCreate: canCreate, onUpload: () => setUploadOpen(true), onClearFilters: () => {
414
+ }), onClear: store.clearSelection, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete })) : null, _jsx("section", { ref: gridRef, tabIndex: -1, "aria-label": intl.formatMessage(messages.assetsRegion), "aria-busy": store.isRefreshing, className: cn('transition-opacity duration-(--ds-duration-base) motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', store.isRefreshing && 'opacity-60 delay-150'), children: isEmpty ? (_jsx(MediaEmptyState, { hasFilters: hasFilters, canCreate: canCreate, onUpload: () => setUploadOpen(true), onClearFilters: () => {
374
415
  store.setSearch('');
375
416
  store.setKindFilter('all');
376
- } })) : (_jsx(MediaGrid, { folders: visibleFolders, assets: store.visibleAssets, folderCounts: store.folderCounts, selectedIds: store.selectedIds, onOpenFolder: store.navigateTo, onRenameFolder: handleRenameFolder, onDeleteFolder: handleDeleteFolder, onToggleSelect: store.toggleSelect, onAssetAction: handleAssetAction, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete })) }), store.total > 0 ? (_jsx(MediaPagination, { page: store.page, pageCount: store.pageCount, pageSize: store.pageSize, total: store.total, onPageChange: store.setPage, onPageSizeChange: store.setPageSize })) : null] }) })] }), _jsx(Drawer, { direction: "left", open: navOpen, onOpenChange: setNavOpen, shouldScaleBackground: false, children: _jsxs(DrawerContent, { className: "data-[vaul-drawer-direction=left]:w-72", children: [_jsx(DrawerTitle, { className: "sr-only", children: intl.formatMessage(messages.navTitle) }), _jsx(MediaFoldersNav, { className: "w-full", folders: store.folders, currentFolderId: store.currentFolderId, counts: store.folderCounts, rootCount: store.folderCounts.get(ROOT_FOLDER_ID) ?? 0, onNavigate: (folderId) => {
417
+ } })) : view === MEDIA_VIEW.List ? (_jsx(MediaAssetTable, { assets: store.visibleAssets, selectedIds: store.selectedIds, activeId: store.detailAsset?.id, onToggleSelect: store.toggleSelect, onToggleAll: toggleAllOnPage, onAction: handleAssetAction, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete })) : (_jsx(MediaGrid, { assets: store.visibleAssets, selectedIds: store.selectedIds, activeId: store.detailAsset?.id, onToggleSelect: store.toggleSelect, onAssetAction: handleAssetAction, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete })) }), store.total > 0 ? (_jsx(MediaPagination, { page: store.page, pageCount: store.pageCount, pageSize: store.pageSize, total: store.total, onPageChange: store.setPage, onPageSizeChange: store.setPageSize })) : null] }) }), store.detailAsset ? (_jsx(AssetDetailPanel, { className: "fixed inset-0 z-40 lg:static lg:z-auto lg:w-[340px] lg:shrink-0 lg:border-l", asset: store.detailAsset, locationLabel: locationLabel, onClose: store.closeDetail, onPrevious: previousAsset
418
+ ? () => store.openDetail(previousAsset.id)
419
+ : undefined, onNext: nextAsset
420
+ ? () => store.openDetail(nextAsset.id)
421
+ : undefined, onAction: handleAssetAction, onSaveAlt: (id, alt) => store.setAssetAlt(id, alt).then((ok) => {
422
+ if (ok) {
423
+ toast.success(intl.formatMessage(messages.tAltSaved));
424
+ }
425
+ return ok;
426
+ }), canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete })) : null] }), _jsx(Drawer, { direction: "left", open: navOpen, onOpenChange: setNavOpen, shouldScaleBackground: false, children: _jsxs(DrawerContent, { className: "data-[vaul-drawer-direction=left]:w-72", children: [_jsx(DrawerTitle, { className: "sr-only", children: intl.formatMessage(messages.navTitle) }), _jsx(MediaFoldersNav, { className: "w-full", folders: store.folders, currentFolderId: store.currentFolderId, counts: store.folderCounts, rootCount: store.folderCounts.get(ROOT_FOLDER_ID) ?? 0, onNavigate: (folderId) => {
377
427
  store.navigateTo(folderId);
378
428
  setNavOpen(false);
379
429
  }, onNewFolder: () => {
380
430
  setNavOpen(false);
381
431
  setNewFolderOpen(true);
382
- }, canCreate: canCreate })] }) }), _jsx(AssetDetailDrawer, { asset: store.detailAsset, locationLabel: locationLabel, onOpenChange: (open) => {
383
- if (!open)
384
- store.closeDetail();
385
- }, onAction: handleAssetAction, onSaveAlt: (id, alt) => store.setAssetAlt(id, alt).then((ok) => {
386
- if (ok) {
387
- toast.success(intl.formatMessage(messages.tAltSaved));
388
- }
389
- return ok;
390
- }), canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete }), _jsx(NewFolderDialog, { open: newFolderOpen, onOpenChange: setNewFolderOpen, locationLabel: locationLabel, onCreate: (name) => {
432
+ }, onRenameFolder: (folder) => {
433
+ setNavOpen(false);
434
+ handleRenameFolder(folder);
435
+ }, onDeleteFolder: (folder) => {
436
+ setNavOpen(false);
437
+ handleDeleteFolder(folder);
438
+ }, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete })] }) }), _jsx(NewFolderDialog, { open: newFolderOpen, onOpenChange: setNewFolderOpen, locationLabel: locationLabel, onCreate: (name) => {
391
439
  void store.createFolder(name).then((ok) => {
392
440
  if (ok) {
393
441
  toast.success(intl.formatMessage(messages.tCreated, { name }));
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The short, upper-cased extension of a file name (`report-q3.pdf` → `PDF`),
3
+ * or `null` when there is none worth showing — no dot, a leading-dot name
4
+ * like `.env`, or a "extension" too long to be one. Labels the tiles and rows
5
+ * of files that have no picture to show, where a glyph alone can't tell a
6
+ * PDF from a DOCX.
7
+ */
8
+ export declare function fileExtension(name: string): string | null;
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/utils/fileExtension/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAKzD"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The short, upper-cased extension of a file name (`report-q3.pdf` → `PDF`),
3
+ * or `null` when there is none worth showing — no dot, a leading-dot name
4
+ * like `.env`, or a "extension" too long to be one. Labels the tiles and rows
5
+ * of files that have no picture to show, where a glyph alone can't tell a
6
+ * PDF from a DOCX.
7
+ */
8
+ export function fileExtension(name) {
9
+ const dot = name.lastIndexOf('.');
10
+ if (dot <= 0 || dot === name.length - 1)
11
+ return null;
12
+ const ext = name.slice(dot + 1);
13
+ return ext.length > 5 ? null : ext.toUpperCase();
14
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/utils/mediaPlugin/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AA6C7D;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC;AAE3C;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,gBAAgB,CAqJ9C"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/utils/mediaPlugin/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAyC7D;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,WAAW,CAAC;AAE3C;;;;;;GAMG;AACH,wBAAgB,WAAW,IAAI,gBAAgB,CAiJ9C"}
@@ -1,7 +1,8 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Suspense, lazy } from 'react';
2
+ import { Suspense } from 'react';
3
+ import { lazyPage } from '@orthacms/utils-admin';
3
4
  import { WORKSPACE_ROUTE_SLOT, WORKSPACE_NAV_SLOT } from '@orthacms/workspaces-admin';
4
- import { CONTENT_FIELD_TYPE, ENTRY_PRESAVE_SLOT, ENTRY_TAB, ENTRY_TAB_SLOT } from '@orthacms/content-admin';
5
+ import { CONTENT_FIELD_TYPE, ENTRY_FIELD_CONTROL_SLOT, ENTRY_PRESAVE_SLOT } from '@orthacms/content-admin';
5
6
  import { WYSIWYG_MEDIA_SLOT } from '@orthacms/wysiwyg-admin';
6
7
  import { INSIGHTS_SECTION_IDS, INSIGHTS_WIDGET_SLOT } from '@orthacms/insights-admin';
7
8
  import { Image, Upload } from 'lucide-react';
@@ -9,18 +10,16 @@ import { MediaStorageStat } from '../../components/MediaStorageStat';
9
10
  import { MediaStorageWidget } from '../../components/MediaStorageWidget';
10
11
  import { MediaUploadsWidget } from '../../components/MediaUploadsWidget';
11
12
  import { MediaAltTextWidget } from '../../components/MediaAltTextWidget';
12
- import { EntryMediaTab } from '../../components/EntryMediaTab';
13
+ import { MediaFieldInput } from '../../components/MediaFieldInput';
13
14
  import { MediaLibraryPageSkeleton } from '../../components/MediaLibrarySkeleton';
14
15
  import { WysiwygLibrarySource } from '../../components/WysiwygLibrarySource';
15
16
  import { WysiwygUploadSource } from '../../components/WysiwygUploadSource';
16
17
  import { MEDIA_PRESAVE_ID, usePendingMediaUploads } from '../../hooks/usePendingMediaUploads';
17
18
  import { MEDIA_READ } from '../../constants';
18
- const MediaLibraryPage = lazy(() => import('../../pages/MediaLibraryPage').then((module) => ({
19
- default: module.MediaLibraryPage
20
- })));
21
- /** The Media tab applies to any type that declares a media field. */
22
- function hasMediaField(schema) {
23
- return schema.fields.some((field) => field.type === CONTENT_FIELD_TYPE.Media);
19
+ const MediaLibraryPage = lazyPage(() => import('../../pages/MediaLibraryPage'), 'MediaLibraryPage');
20
+ /** The media control owns every `media` field, wherever the form draws it. */
21
+ function isMediaField(field) {
22
+ return field.type === CONTENT_FIELD_TYPE.Media;
24
23
  }
25
24
  /**
26
25
  * Creates the admin-side Media Library plugin. It lives **strictly inside a
@@ -103,23 +102,19 @@ export function MediaPlugin() {
103
102
  }
104
103
  ]
105
104
  },
106
- // The entry editor's Media tab — rendered by content-admin's
107
- // ENTRY_TAB_SLOT only when the open type has a media field, so the
108
- // tab appears exactly where media applies. Its `slug` is content's
109
- // own `media` tab route, already accepted by the tab router.
105
+ // A media field's control in the entry form. It used to be a Media
106
+ // tab of its own; the field now sits in the schema's order with the
107
+ // rest (ADR-0022), and content draws its label row and error.
108
+ // `composite`: the control is several buttons, so the field is
109
+ // named as a group rather than by a label on one of them.
110
110
  {
111
- slot: ENTRY_TAB_SLOT,
111
+ slot: ENTRY_FIELD_CONTROL_SLOT,
112
112
  items: [
113
113
  {
114
- id: 'media.entry.tab',
115
- slug: ENTRY_TAB.Media,
116
- label: {
117
- id: 'media.tab.label',
118
- defaultMessage: 'Media'
119
- },
120
- order: 10,
121
- appliesTo: hasMediaField,
122
- Component: EntryMediaTab
114
+ id: 'media.entry.field',
115
+ appliesTo: isMediaField,
116
+ composite: true,
117
+ Component: MediaFieldInput
123
118
  }
124
119
  ]
125
120
  },
@@ -162,7 +157,7 @@ export function MediaPlugin() {
162
157
  // runs inside the record's save and puts them in the library then,
163
158
  // so an abandoned edit leaves no orphan assets behind. It also owns
164
159
  // the staging state, mounted above the tab body (a route) so it
165
- // survives switching tabs.
160
+ // survives the form remounting.
166
161
  {
167
162
  slot: ENTRY_PRESAVE_SLOT,
168
163
  items: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orthacms/media-admin",
3
- "version": "0.10.3",
3
+ "version": "0.12.0",
4
4
  "description": "@orthacms/media-admin — part of Ortha CMS.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://orthacms.com",
@@ -25,15 +25,15 @@
25
25
  "dist"
26
26
  ],
27
27
  "dependencies": {
28
- "@orthacms/bootstrap-admin": "^0.10.3",
29
- "@orthacms/content-admin": "^0.10.3",
30
- "@orthacms/content-domain": "^0.10.3",
31
- "@orthacms/design-system": "^0.10.3",
32
- "@orthacms/identity-admin": "^0.10.3",
33
- "@orthacms/insights-admin": "^0.10.3",
34
- "@orthacms/utils-admin": "^0.10.3",
35
- "@orthacms/workspaces-admin": "^0.10.3",
36
- "@orthacms/wysiwyg-admin": "^0.10.3",
28
+ "@orthacms/bootstrap-admin": "^0.12.0",
29
+ "@orthacms/content-admin": "^0.12.0",
30
+ "@orthacms/content-domain": "^0.12.0",
31
+ "@orthacms/design-system": "^0.12.0",
32
+ "@orthacms/identity-admin": "^0.12.0",
33
+ "@orthacms/insights-admin": "^0.12.0",
34
+ "@orthacms/utils-admin": "^0.12.0",
35
+ "@orthacms/workspaces-admin": "^0.12.0",
36
+ "@orthacms/wysiwyg-admin": "^0.12.0",
37
37
  "@tanstack/react-query": "^5.0.0",
38
38
  "lucide-react": "^1.17.0",
39
39
  "react-intl": "^7.0.0",