@orthacms/media-admin 0.11.0 → 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 (56) 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/FolderActionsMenu/index.d.ts +3 -1
  8. package/dist/lib/components/FolderActionsMenu/index.d.ts.map +1 -1
  9. package/dist/lib/components/FolderActionsMenu/index.js +3 -3
  10. package/dist/lib/components/MediaAssetTable/index.d.ts +25 -0
  11. package/dist/lib/components/MediaAssetTable/index.d.ts.map +1 -0
  12. package/dist/lib/components/MediaAssetTable/index.js +62 -0
  13. package/dist/lib/components/MediaFieldControl/index.d.ts.map +1 -1
  14. package/dist/lib/components/MediaFieldControl/index.js +19 -14
  15. package/dist/lib/components/MediaFieldInput/index.d.ts +19 -0
  16. package/dist/lib/components/MediaFieldInput/index.d.ts.map +1 -0
  17. package/dist/lib/components/MediaFieldInput/index.js +23 -0
  18. package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts +17 -3
  19. package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts.map +1 -1
  20. package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.js +16 -6
  21. package/dist/lib/components/MediaFoldersNav/index.d.ts +10 -5
  22. package/dist/lib/components/MediaFoldersNav/index.d.ts.map +1 -1
  23. package/dist/lib/components/MediaFoldersNav/index.js +17 -9
  24. package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts +9 -4
  25. package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts.map +1 -1
  26. package/dist/lib/components/MediaGrid/MediaAssetCard/index.js +27 -11
  27. package/dist/lib/components/MediaGrid/index.d.ts +10 -10
  28. package/dist/lib/components/MediaGrid/index.d.ts.map +1 -1
  29. package/dist/lib/components/MediaGrid/index.js +10 -15
  30. package/dist/lib/components/MediaSelectionBar/index.d.ts +9 -5
  31. package/dist/lib/components/MediaSelectionBar/index.d.ts.map +1 -1
  32. package/dist/lib/components/MediaSelectionBar/index.js +16 -10
  33. package/dist/lib/components/MediaThumbnail/index.d.ts +9 -6
  34. package/dist/lib/components/MediaThumbnail/index.d.ts.map +1 -1
  35. package/dist/lib/components/MediaThumbnail/index.js +7 -6
  36. package/dist/lib/components/MediaToolbar/index.d.ts +10 -8
  37. package/dist/lib/components/MediaToolbar/index.d.ts.map +1 -1
  38. package/dist/lib/components/MediaToolbar/index.js +37 -13
  39. package/dist/lib/components/MediaTopBar/index.d.ts +7 -1
  40. package/dist/lib/components/MediaTopBar/index.d.ts.map +1 -1
  41. package/dist/lib/components/MediaTopBar/index.js +6 -3
  42. package/dist/lib/components/MediaUploadBanner/index.d.ts +7 -5
  43. package/dist/lib/components/MediaUploadBanner/index.d.ts.map +1 -1
  44. package/dist/lib/components/MediaUploadBanner/index.js +15 -8
  45. package/dist/lib/constants/index.d.ts +13 -0
  46. package/dist/lib/constants/index.d.ts.map +1 -1
  47. package/dist/lib/constants/index.js +11 -0
  48. package/dist/lib/pages/MediaLibraryPage/index.d.ts +6 -4
  49. package/dist/lib/pages/MediaLibraryPage/index.d.ts.map +1 -1
  50. package/dist/lib/pages/MediaLibraryPage/index.js +82 -36
  51. package/dist/lib/utils/fileExtension/index.d.ts +9 -0
  52. package/dist/lib/utils/fileExtension/index.d.ts.map +1 -0
  53. package/dist/lib/utils/fileExtension/index.js +14 -0
  54. package/dist/lib/utils/mediaPlugin/index.d.ts.map +1 -1
  55. package/dist/lib/utils/mediaPlugin/index.js +16 -20
  56. package/package.json +10 -10
@@ -1,10 +1,12 @@
1
1
  import type { UploadItem, UploadSummary } from '../../types/upload';
2
2
  /**
3
- * The upload progress banner — the batch headline ("Uploading 3 of 5 · 47%"), an
4
- * overall size-weighted bar while anything is running, and one
5
- * {@link MediaUploadRow} per file. It sits above the browser rather than inside
6
- * the upload dialog, so the dialog can close on submit and the user keeps
7
- * browsing (and can queue more) while bytes move.
3
+ * The upload tray — a small card pinned to the **bottom-right** of the window
4
+ * with the batch headline ("Uploading 3 of 5 · 47%"), an overall size-weighted
5
+ * bar while anything is running, and one {@link MediaUploadRow} per file (the
6
+ * chevron folds the list away, leaving the headline). It floats outside the
7
+ * upload dialog and over the browser rather than pushing the grid down, so the
8
+ * dialog can close on submit and the user keeps browsing, selecting and
9
+ * queueing more while bytes move.
8
10
  *
9
11
  * `role="status"` sits on the **headline alone**, not the whole banner: a polite
10
12
  * live region re-announces its entire contents on every change, so wrapping the
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaUploadBanner/index.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAqCpE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,iBAAiB,CAAC,EAC9B,KAAK,EACL,OAAO,EACP,OAAO,EACP,QAAQ,EACR,SAAS,EACZ,EAAE;IACC,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,SAAS,EAAE,MAAM,IAAI,CAAC;CACzB,sCAwGA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaUploadBanner/index.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAyCpE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,EAC9B,KAAK,EACL,OAAO,EACP,OAAO,EACP,QAAQ,EACR,SAAS,EACZ,EAAE;IACC,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,OAAO,EAAE,aAAa,CAAC;IACvB,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC9B,QAAQ,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/B,SAAS,EAAE,MAAM,IAAI,CAAC;CACzB,sCA8HA"}
@@ -1,8 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEffect, useRef, useState } from 'react';
3
3
  import { defineMessages, useIntl } from 'react-intl';
4
- import { Button, Progress } from '@orthacms/design-system';
5
- import { UploadCloud, X } from 'lucide-react';
4
+ import { Button, Progress, cn } from '@orthacms/design-system';
5
+ import { ChevronDown, UploadCloud, X } from 'lucide-react';
6
6
  import { UPLOAD_STATUS } from '../../constants';
7
7
  import { MediaUploadRow } from './MediaUploadRow';
8
8
  /** Intl descriptors for {@link MediaUploadBanner}, co-located. */
@@ -29,6 +29,10 @@ const messages = defineMessages({
29
29
  defaultMessage: 'Overall upload progress'
30
30
  },
31
31
  dismiss: { id: 'media.uploadBanner.dismiss', defaultMessage: 'Dismiss' },
32
+ collapse: {
33
+ id: 'media.uploadBanner.collapse',
34
+ defaultMessage: 'Show the files'
35
+ },
32
36
  /** Announced when one file in a batch fails — the row itself is silent. */
33
37
  failureAnnouncement: {
34
38
  id: 'media.uploadBanner.failureAnnouncement',
@@ -36,11 +40,13 @@ const messages = defineMessages({
36
40
  }
37
41
  });
38
42
  /**
39
- * The upload progress banner — the batch headline ("Uploading 3 of 5 · 47%"), an
40
- * overall size-weighted bar while anything is running, and one
41
- * {@link MediaUploadRow} per file. It sits above the browser rather than inside
42
- * the upload dialog, so the dialog can close on submit and the user keeps
43
- * browsing (and can queue more) while bytes move.
43
+ * The upload tray — a small card pinned to the **bottom-right** of the window
44
+ * with the batch headline ("Uploading 3 of 5 · 47%"), an overall size-weighted
45
+ * bar while anything is running, and one {@link MediaUploadRow} per file (the
46
+ * chevron folds the list away, leaving the headline). It floats outside the
47
+ * upload dialog and over the browser rather than pushing the grid down, so the
48
+ * dialog can close on submit and the user keeps browsing, selecting and
49
+ * queueing more while bytes move.
44
50
  *
45
51
  * `role="status"` sits on the **headline alone**, not the whole banner: a polite
46
52
  * live region re-announces its entire contents on every change, so wrapping the
@@ -52,6 +58,7 @@ const messages = defineMessages({
52
58
  */
53
59
  export function MediaUploadBanner({ items, summary, onRetry, onCancel, onDismiss }) {
54
60
  const intl = useIntl();
61
+ const [open, setOpen] = useState(true);
55
62
  // Failures are announced from their own region. The headline's counts move
56
63
  // when a file fails, but "3 of 5" never says *which* file or why — and the
57
64
  // rows carrying that sentence sit outside the live region on purpose (a
@@ -95,5 +102,5 @@ export function MediaUploadBanner({ items, summary, onRetry, onCancel, onDismiss
95
102
  : summary.failed > 0
96
103
  ? intl.formatMessage(messages.allFailed, { failed: summary.failed })
97
104
  : intl.formatMessage(messages.complete, { count: summary.done });
98
- return (_jsxs("section", { "aria-label": intl.formatMessage(messages.region), className: "mb-4 rounded-xl border bg-muted/40 px-4 py-3", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(UploadCloud, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx("p", { className: "min-w-0 flex-1 text-sm font-medium", role: "status", children: headline }), summary.active ? null : (_jsx(Button, { variant: "ghost", size: "icon", className: "size-7 shrink-0 shadow-none", "aria-label": intl.formatMessage(messages.dismiss), onClick: onDismiss, children: _jsx(X, { "aria-hidden": true }) }))] }), summary.active ? (_jsx(Progress, { value: summary.percent, className: "mt-2 h-1.5", "aria-label": intl.formatMessage(messages.overall) })) : null, _jsx("p", { className: "sr-only", role: "status", children: failureNotice }), _jsx("ul", { className: "mt-1 max-h-48 divide-y overflow-auto", children: items.map((item) => (_jsx(MediaUploadRow, { item: item, onRetry: onRetry, onCancel: onCancel }, item.id))) })] }));
105
+ return (_jsxs("section", { "aria-label": intl.formatMessage(messages.region), className: "fixed bottom-4 right-4 z-30 w-80 max-w-[calc(100vw-2rem)] rounded-xl border bg-popover px-4 py-3 text-popover-foreground shadow-lg", children: [_jsxs("div", { className: "flex items-center gap-2", children: [_jsx(UploadCloud, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": true }), _jsx("p", { className: "min-w-0 flex-1 text-sm font-medium", role: "status", children: headline }), _jsx(Button, { variant: "ghost", size: "icon", className: "size-7 shrink-0 shadow-none", "aria-label": intl.formatMessage(messages.collapse), "aria-expanded": open, onClick: () => setOpen((value) => !value), children: _jsx(ChevronDown, { "aria-hidden": true, className: cn('transition-transform duration-(--ds-duration-base) motion-reduce:transition-none', !open && 'rotate-180') }) }), summary.active ? null : (_jsx(Button, { variant: "ghost", size: "icon", className: "size-7 shrink-0 shadow-none", "aria-label": intl.formatMessage(messages.dismiss), onClick: onDismiss, children: _jsx(X, { "aria-hidden": true }) }))] }), summary.active ? (_jsx(Progress, { value: summary.percent, className: "mt-2 h-1.5", "aria-label": intl.formatMessage(messages.overall) })) : null, _jsx("p", { className: "sr-only", role: "status", children: failureNotice }), _jsx("ul", { className: cn('mt-1 max-h-48 divide-y overflow-auto', !open && 'hidden'), children: items.map((item) => (_jsx(MediaUploadRow, { item: item, onRetry: onRetry, onCancel: onCancel }, item.id))) })] }));
99
106
  }
@@ -92,4 +92,17 @@ export declare const MEDIA_FOLDERS_PANEL: {
92
92
  readonly maxViewportRatio: 0.2;
93
93
  readonly cssVar: "--media-folders-width";
94
94
  };
95
+ /**
96
+ * How the library draws the open folder's files: tiles, or the design-system
97
+ * table. Remembered per browser under {@link MEDIA_VIEW_STORAGE_KEY} — a
98
+ * reading preference, not shared state.
99
+ */
100
+ export declare const MEDIA_VIEW: {
101
+ readonly Grid: "grid";
102
+ readonly List: "list";
103
+ };
104
+ /** One way of drawing the files. */
105
+ export type MediaView = (typeof MEDIA_VIEW)[keyof typeof MEDIA_VIEW];
106
+ /** Where the chosen {@link MediaView} is kept (`localStorage`). */
107
+ export declare const MEDIA_VIEW_STORAGE_KEY = "orthacms.media.view";
95
108
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/constants/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,+EAA+E;AAC/E,eAAO,MAAM,UAAU,eAAe,CAAC;AAEvC,qDAAqD;AACrD,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C,6DAA6D;AAC7D,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C,wDAAwD;AACxD,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,aAAa,UAAU,CAAC;AAErC,4EAA4E;AAC5E,eAAO,MAAM,cAAc,SAAS,CAAC;AAErC,0CAA0C;AAC1C,eAAO,MAAM,aAAa;IACtB,8CAA8C;;IAE9C,6BAA6B;;;;IAI7B,8CAA8C;;CAExC,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,OAAO,aAAa,CAAC,CAAC;AAE9E;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,4EAA4E;AAC5E,eAAO,MAAM,UAAU;;;;;;CAMb,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAErE,oEAAoE;AACpE,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,UAAe,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAE3C,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,0CAA0C;AAC1C,eAAO,MAAM,UAAU;;;;;;;CAOb,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAErE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;;;;;;;CAOtB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lib/constants/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,+EAA+E;AAC/E,eAAO,MAAM,UAAU,eAAe,CAAC;AAEvC,qDAAqD;AACrD,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C,6DAA6D;AAC7D,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C,wDAAwD;AACxD,eAAO,MAAM,YAAY,iBAAiB,CAAC;AAE3C;;;;GAIG;AACH,eAAO,MAAM,aAAa,UAAU,CAAC;AAErC,4EAA4E;AAC5E,eAAO,MAAM,cAAc,SAAS,CAAC;AAErC,0CAA0C;AAC1C,eAAO,MAAM,aAAa;IACtB,8CAA8C;;IAE9C,6BAA6B;;;;IAI7B,8CAA8C;;CAExC,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,OAAO,aAAa,CAAC,CAAC;AAE9E;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,4EAA4E;AAC5E,eAAO,MAAM,UAAU;;;;;;CAMb,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAErE,oEAAoE;AACpE,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC;;;;;;GAMG;AACH,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAE3C,iEAAiE;AACjE,eAAO,MAAM,wBAAwB,UAAe,CAAC;AAErD;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAE3C,yEAAyE;AACzE,eAAO,MAAM,kBAAkB,MAAM,CAAC;AAEtC,0CAA0C;AAC1C,eAAO,MAAM,UAAU;;;;;;;CAOb,CAAC;AAEX,iCAAiC;AACjC,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAErE;;;;GAIG;AACH,eAAO,MAAM,mBAAmB;;;;;;;CAOtB,CAAC;AAEX;;;;GAIG;AACH,eAAO,MAAM,UAAU;;;CAGb,CAAC;AAEX,oCAAoC;AACpC,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAC;AAErE,mEAAmE;AACnE,eAAO,MAAM,sBAAsB,wBAAwB,CAAC"}
@@ -86,3 +86,14 @@ export const MEDIA_FOLDERS_PANEL = {
86
86
  maxViewportRatio: 0.2,
87
87
  cssVar: '--media-folders-width'
88
88
  };
89
+ /**
90
+ * How the library draws the open folder's files: tiles, or the design-system
91
+ * table. Remembered per browser under {@link MEDIA_VIEW_STORAGE_KEY} — a
92
+ * reading preference, not shared state.
93
+ */
94
+ export const MEDIA_VIEW = {
95
+ Grid: 'grid',
96
+ List: 'list'
97
+ };
98
+ /** Where the chosen {@link MediaView} is kept (`localStorage`). */
99
+ export const MEDIA_VIEW_STORAGE_KEY = 'orthacms.media.view';
@@ -3,14 +3,16 @@
3
3
  * `/workspaces/:id/media`. `useMediaLibrary` is the store — folders and the open
4
4
  * folder's assets come from `@orthacms/media-server`, and every action (browse
5
5
  * folders, upload, create folder, rename, duplicate, move, delete, filter, sort,
6
- * open the detail drawer) runs against the API, firing a toast per action —
6
+ * open the details panel) runs against the API, firing a toast per action —
7
7
  * except uploads, whose status lives in the `MediaUploadBanner`. Controls gate
8
8
  * on the real `media:*` matrix via `useHasPermission`.
9
9
  *
10
10
  * A sticky `MediaTopBar` heads the page (the shared icon-tile + breadcrumb
11
- * header every admin page carries), spanning both panes. Below it sits a
12
- * two-pane layout — a folders sidebar beside the asset browser — plus the detail
13
- * drawer and the create/rename/move/upload dialogs. Both panes sit
11
+ * header every admin page carries, with New folder / Upload at its right end),
12
+ * spanning every pane. Below it: the folder tree, the file browser (title, type
13
+ * chips + search + sort + Grid/List, the selection bar, then tiles or the
14
+ * table) and — while a file is open — the non-modal details panel as a third
15
+ * column; plus the create/rename/move/upload dialogs. The panes sit
14
16
  * **flush on the canvas** (no muted board, no bordered island), mirroring the
15
17
  * Content Library's work area: the sidebar is divided by a hairline `border-r`
16
18
  * and each pane scrolls independently. Sized with `flex-1` against the workspace
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/pages/MediaLibraryPage/index.tsx"],"names":[],"mappings":"AA+KA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,gBAAgB,gCAwlB/B"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/pages/MediaLibraryPage/index.tsx"],"names":[],"mappings":"AA8LA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,gBAAgB,gCAorB/B"}
@@ -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
4
  import { Button, ConfirmDialog, Drawer, DrawerContent, DrawerTitle, ResizeHandle, cn, toast, useResizablePanel } from '@orthacms/design-system';
5
- import { PanelLeft } from 'lucide-react';
5
+ import { FolderPlus, PanelLeft, UploadCloud } from 'lucide-react';
6
6
  import { useHasPermission } from '@orthacms/identity-admin';
7
- import { MEDIA_CREATE, MEDIA_DELETE, MEDIA_FOLDERS_PANEL, 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',
@@ -133,19 +136,31 @@ const messages = defineMessages({
133
136
  * nothing.
134
137
  */
135
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
+ }
136
149
  /**
137
150
  * The Media Library, mounted inside the workspace shell at
138
151
  * `/workspaces/:id/media`. `useMediaLibrary` is the store — folders and the open
139
152
  * folder's assets come from `@orthacms/media-server`, and every action (browse
140
153
  * folders, upload, create folder, rename, duplicate, move, delete, filter, sort,
141
- * 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 —
142
155
  * except uploads, whose status lives in the `MediaUploadBanner`. Controls gate
143
156
  * on the real `media:*` matrix via `useHasPermission`.
144
157
  *
145
158
  * A sticky `MediaTopBar` heads the page (the shared icon-tile + breadcrumb
146
- * header every admin page carries), spanning both panes. Below it sits a
147
- * two-pane layout — a folders sidebar beside the asset browser — plus the detail
148
- * 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
149
164
  * **flush on the canvas** (no muted board, no bordered island), mirroring the
150
165
  * Content Library's work area: the sidebar is divided by a hairline `border-r`
151
166
  * and each pane scrolls independently. Sized with `flex-1` against the workspace
@@ -179,6 +194,16 @@ export function MediaLibraryPage() {
179
194
  const [deleteTarget, setDeleteTarget] = useState(null);
180
195
  /** A confirmed delete is in flight — the dialog shows a spinner meanwhile. */
181
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
+ };
182
207
  /**
183
208
  * Puts focus on the grid and **keeps** it there for a few frames.
184
209
  *
@@ -213,14 +238,29 @@ export function MediaLibraryPage() {
213
238
  ? store.currentFolder.name
214
239
  : intl.formatMessage(messages.allMedia);
215
240
  const hasFilters = store.search.trim() !== '' || store.kindFilter !== 'all';
216
- // Sub-folders belong to the folder, not to a page of its assets, so they are
217
- // drawn once on the first page rather than repeating above every page of
218
- // files. On any later page the grid is assets alone.
219
- const showFolders = store.page === 1;
220
- const visibleFolders = showFolders ? store.childFolders : [];
221
- // Emptiness is now a fact about the whole folder, not about what happened to
222
- // load: `total` counts every asset matching the search and filter across it.
223
- 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
+ };
224
264
  // Triggers a browser download for one asset (its bytes stream from the
225
265
  // `media:read`-gated raw route). Shared by the row action and bulk download.
226
266
  const downloadAsset = (asset) => {
@@ -231,7 +271,7 @@ export function MediaLibraryPage() {
231
271
  link.click();
232
272
  link.remove();
233
273
  };
234
- // A single dispatcher every card / row / drawer forwards asset actions to.
274
+ // A single dispatcher every tile / row / panel forwards asset actions to.
235
275
  const handleAssetAction = (kind, asset) => {
236
276
  switch (kind) {
237
277
  case 'open':
@@ -348,14 +388,13 @@ export function MediaLibraryPage() {
348
388
  if (store.isError) {
349
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) })] }));
350
390
  }
351
- 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)] }) }), _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), canCreate: canCreate }), _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: "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
352
392
  ? store.currentFolder.name
353
- : intl.formatMessage(messages.title) }), _jsx("p", { className: "mt-0.5 text-sm text-muted-foreground", children: intl.formatMessage(messages.subtitle, {
354
- count: store.total,
355
- location: locationLabel
356
- }) })] }), _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, {
357
396
  count: selectedIds.length
358
- }) }), _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: () => {
359
398
  store.selectedAssets.forEach(downloadAsset);
360
399
  toast.success(intl.formatMessage(messages.tDownload, {
361
400
  name: `${selectedIds.length} files`
@@ -375,21 +414,28 @@ export function MediaLibraryPage() {
375
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: () => {
376
415
  store.setSearch('');
377
416
  store.setKindFilter('all');
378
- } })) : (_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) => {
379
427
  store.navigateTo(folderId);
380
428
  setNavOpen(false);
381
429
  }, onNewFolder: () => {
382
430
  setNavOpen(false);
383
431
  setNewFolderOpen(true);
384
- }, canCreate: canCreate })] }) }), _jsx(AssetDetailDrawer, { asset: store.detailAsset, locationLabel: locationLabel, onOpenChange: (open) => {
385
- if (!open)
386
- store.closeDetail();
387
- }, onAction: handleAssetAction, onSaveAlt: (id, alt) => store.setAssetAlt(id, alt).then((ok) => {
388
- if (ok) {
389
- toast.success(intl.formatMessage(messages.tAltSaved));
390
- }
391
- return ok;
392
- }), 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) => {
393
439
  void store.createFolder(name).then((ok) => {
394
440
  if (ok) {
395
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":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AA4C7D;;;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"}
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Suspense } from 'react';
3
3
  import { lazyPage } from '@orthacms/utils-admin';
4
4
  import { WORKSPACE_ROUTE_SLOT, WORKSPACE_NAV_SLOT } from '@orthacms/workspaces-admin';
5
- 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';
6
6
  import { WYSIWYG_MEDIA_SLOT } from '@orthacms/wysiwyg-admin';
7
7
  import { INSIGHTS_SECTION_IDS, INSIGHTS_WIDGET_SLOT } from '@orthacms/insights-admin';
8
8
  import { Image, Upload } from 'lucide-react';
@@ -10,16 +10,16 @@ import { MediaStorageStat } from '../../components/MediaStorageStat';
10
10
  import { MediaStorageWidget } from '../../components/MediaStorageWidget';
11
11
  import { MediaUploadsWidget } from '../../components/MediaUploadsWidget';
12
12
  import { MediaAltTextWidget } from '../../components/MediaAltTextWidget';
13
- import { EntryMediaTab } from '../../components/EntryMediaTab';
13
+ import { MediaFieldInput } from '../../components/MediaFieldInput';
14
14
  import { MediaLibraryPageSkeleton } from '../../components/MediaLibrarySkeleton';
15
15
  import { WysiwygLibrarySource } from '../../components/WysiwygLibrarySource';
16
16
  import { WysiwygUploadSource } from '../../components/WysiwygUploadSource';
17
17
  import { MEDIA_PRESAVE_ID, usePendingMediaUploads } from '../../hooks/usePendingMediaUploads';
18
18
  import { MEDIA_READ } from '../../constants';
19
19
  const MediaLibraryPage = lazyPage(() => import('../../pages/MediaLibraryPage'), 'MediaLibraryPage');
20
- /** The Media tab applies to any type that declares a media field. */
21
- function hasMediaField(schema) {
22
- return schema.fields.some((field) => field.type === CONTENT_FIELD_TYPE.Media);
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;
23
23
  }
24
24
  /**
25
25
  * Creates the admin-side Media Library plugin. It lives **strictly inside a
@@ -102,23 +102,19 @@ export function MediaPlugin() {
102
102
  }
103
103
  ]
104
104
  },
105
- // The entry editor's Media tab — rendered by content-admin's
106
- // ENTRY_TAB_SLOT only when the open type has a media field, so the
107
- // tab appears exactly where media applies. Its `slug` is content's
108
- // 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.
109
110
  {
110
- slot: ENTRY_TAB_SLOT,
111
+ slot: ENTRY_FIELD_CONTROL_SLOT,
111
112
  items: [
112
113
  {
113
- id: 'media.entry.tab',
114
- slug: ENTRY_TAB.Media,
115
- label: {
116
- id: 'media.tab.label',
117
- defaultMessage: 'Media'
118
- },
119
- order: 10,
120
- appliesTo: hasMediaField,
121
- Component: EntryMediaTab
114
+ id: 'media.entry.field',
115
+ appliesTo: isMediaField,
116
+ composite: true,
117
+ Component: MediaFieldInput
122
118
  }
123
119
  ]
124
120
  },
@@ -161,7 +157,7 @@ export function MediaPlugin() {
161
157
  // runs inside the record's save and puts them in the library then,
162
158
  // so an abandoned edit leaves no orphan assets behind. It also owns
163
159
  // the staging state, mounted above the tab body (a route) so it
164
- // survives switching tabs.
160
+ // survives the form remounting.
165
161
  {
166
162
  slot: ENTRY_PRESAVE_SLOT,
167
163
  items: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orthacms/media-admin",
3
- "version": "0.11.0",
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.11.0",
29
- "@orthacms/content-admin": "^0.11.0",
30
- "@orthacms/content-domain": "^0.11.0",
31
- "@orthacms/design-system": "^0.11.0",
32
- "@orthacms/identity-admin": "^0.11.0",
33
- "@orthacms/insights-admin": "^0.11.0",
34
- "@orthacms/utils-admin": "^0.11.0",
35
- "@orthacms/workspaces-admin": "^0.11.0",
36
- "@orthacms/wysiwyg-admin": "^0.11.0",
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",