@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,17 +1,31 @@
1
1
  import type { MediaFolder } from '../../../types/mediaFolder';
2
+ /** Callbacks a tree row dispatches up — navigation and the ⋯ menu's actions. */
3
+ export type FolderTreeActions = {
4
+ onNavigate: (folderId: string) => void;
5
+ onRename: (folder: MediaFolder) => void;
6
+ onDelete: (folder: MediaFolder) => void;
7
+ canUpdate: boolean;
8
+ canDelete: boolean;
9
+ };
2
10
  /**
3
11
  * One folder in the sidebar tree. A folder with sub-folders is a
4
12
  * design-system `Collapsible` (a chevron toggles its children under a left tree
5
13
  * line; Radix manages `aria-expanded`), starting expanded; a leaf folder gets a
6
14
  * matching spacer so names align. The name is a separate button that navigates
7
15
  * into the folder — clicking it never toggles the group — and the open folder is
8
- * highlighted. Navigation dispatches up.
16
+ * highlighted.
17
+ *
18
+ * The tree is the **only** place folders are drawn (the grid shows files), so
19
+ * each row carries the folder's ⋯ menu — Open / Rename / Delete — revealed on
20
+ * hover or focus and kept while it is open. With neither `media:update` nor
21
+ * `media:delete` the menu would hold only Open, which the row already is, so
22
+ * it is left out.
9
23
  */
10
- export declare function MediaFolderTreeItem({ folder, folders, currentFolderId, counts, onNavigate }: {
24
+ export declare function MediaFolderTreeItem({ folder, folders, currentFolderId, counts, actions }: {
11
25
  folder: MediaFolder;
12
26
  folders: MediaFolder[];
13
27
  currentFolderId: string;
14
28
  counts: Map<string, number>;
15
- onNavigate: (folderId: string) => void;
29
+ actions: FolderTreeActions;
16
30
  }): import("react").JSX.Element;
17
31
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAW9D;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,EAChC,MAAM,EACN,OAAO,EACP,eAAe,EACf,MAAM,EACN,UAAU,EACb,EAAE;IACC,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C,+BAyFA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.tsx"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAY9D,gFAAgF;AAChF,MAAM,MAAM,iBAAiB,GAAG;IAC5B,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC,QAAQ,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IACxC,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,CAAC;AAEF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,EAChC,MAAM,EACN,OAAO,EACP,eAAe,EACf,MAAM,EACN,OAAO,EACV,EAAE;IACC,MAAM,EAAE,WAAW,CAAC;IACpB,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,OAAO,EAAE,iBAAiB,CAAC;CAC9B,+BAoGA"}
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { defineMessages, useIntl } from 'react-intl';
3
3
  import { Collapsible, CollapsibleContent, CollapsibleTrigger, cn } from '@orthacms/design-system';
4
4
  import { ChevronRight, Folder } from 'lucide-react';
5
+ import { FolderActionsMenu } from '../../FolderActionsMenu';
5
6
  /** Intl descriptors for {@link MediaFolderTreeItem}, co-located. */
6
7
  const messages = defineMessages({
7
8
  toggle: { id: 'media.tree.toggle', defaultMessage: 'Toggle {name}' },
@@ -16,22 +17,31 @@ const messages = defineMessages({
16
17
  * line; Radix manages `aria-expanded`), starting expanded; a leaf folder gets a
17
18
  * matching spacer so names align. The name is a separate button that navigates
18
19
  * into the folder — clicking it never toggles the group — and the open folder is
19
- * highlighted. Navigation dispatches up.
20
+ * highlighted.
21
+ *
22
+ * The tree is the **only** place folders are drawn (the grid shows files), so
23
+ * each row carries the folder's ⋯ menu — Open / Rename / Delete — revealed on
24
+ * hover or focus and kept while it is open. With neither `media:update` nor
25
+ * `media:delete` the menu would hold only Open, which the row already is, so
26
+ * it is left out.
20
27
  */
21
- export function MediaFolderTreeItem({ folder, folders, currentFolderId, counts, onNavigate }) {
28
+ export function MediaFolderTreeItem({ folder, folders, currentFolderId, counts, actions }) {
22
29
  const intl = useIntl();
23
30
  const children = folders
24
31
  .filter((f) => f.parentId === folder.id)
25
32
  .sort((a, b) => a.name.localeCompare(b.name));
26
33
  const hasChildren = children.length > 0;
27
34
  const active = currentFolderId === folder.id;
28
- const nameButton = (_jsxs("button", { type: "button", onClick: () => onNavigate(folder.id), "aria-current": active ? 'true' : undefined, className: cn('flex min-w-0 flex-1 items-center gap-2 rounded-md py-1.5 pr-2 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', active ? 'font-medium text-primary' : 'text-foreground'), children: [_jsx(Folder, { className: cn('size-4 shrink-0', active ? 'text-primary' : 'text-muted-foreground'), "aria-hidden": true }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: folder.name }), _jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: intl.formatMessage(messages.count, {
35
+ const { onNavigate, canUpdate, canDelete } = actions;
36
+ const nameButton = (_jsxs("button", { type: "button", onClick: () => onNavigate(folder.id), "aria-current": active ? 'true' : undefined, className: cn('flex min-w-0 flex-1 items-center gap-2 rounded-md py-1.5 pr-1 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', active ? 'font-medium text-primary' : 'text-foreground'), children: [_jsx(Folder, { className: cn('size-4 shrink-0', active ? 'text-primary' : 'text-muted-foreground'), "aria-hidden": true }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: folder.name }), _jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: intl.formatMessage(messages.count, {
29
37
  count: counts.get(folder.id) ?? 0
30
38
  }) })] }));
39
+ const menu = canUpdate || canDelete ? (_jsx(FolderActionsMenu, { name: folder.name, onOpen: () => onNavigate(folder.id), onRename: () => actions.onRename(folder), onDelete: () => actions.onDelete(folder), canUpdate: canUpdate, canDelete: canDelete, triggerClassName: "mr-0.5 size-6 opacity-0 transition-opacity group-hover/row:opacity-100 focus-visible:opacity-100 data-[state=open]:opacity-100 [@media(hover:none)]:opacity-100" })) : null;
40
+ const rowClass = cn('group/row flex items-center gap-0.5 rounded-md pl-1 transition-colors', active ? 'bg-primary/10' : 'hover:bg-accent');
31
41
  if (!hasChildren) {
32
- return (_jsx("li", { children: _jsxs("div", { className: cn('flex items-center gap-0.5 rounded-md pl-1 transition-colors', active ? 'bg-primary/10' : 'hover:bg-accent'), children: [_jsx("span", { className: "size-6 shrink-0", "aria-hidden": true }), nameButton] }) }));
42
+ return (_jsx("li", { children: _jsxs("div", { className: rowClass, children: [_jsx("span", { className: "size-6 shrink-0", "aria-hidden": true }), nameButton, menu] }) }));
33
43
  }
34
- return (_jsx("li", { children: _jsxs(Collapsible, { defaultOpen: true, className: "group/folder", children: [_jsxs("div", { className: cn('flex items-center gap-0.5 rounded-md pl-1 transition-colors', active ? 'bg-primary/10' : 'hover:bg-accent'), children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": intl.formatMessage(messages.toggle, {
44
+ return (_jsx("li", { children: _jsxs(Collapsible, { defaultOpen: true, className: "group/folder", children: [_jsxs("div", { className: rowClass, children: [_jsx(CollapsibleTrigger, { asChild: true, children: _jsx("button", { type: "button", "aria-label": intl.formatMessage(messages.toggle, {
35
45
  name: folder.name
36
- }), className: "flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(ChevronRight, { className: "size-3.5 transition-transform duration-200 group-data-[state=open]/folder:rotate-90" }) }) }), nameButton] }), _jsx(CollapsibleContent, { children: _jsx("ul", { className: "ml-[0.85rem] mt-0.5 space-y-0.5 border-l border-border pl-1.5", children: children.map((child) => (_jsx(MediaFolderTreeItem, { folder: child, folders: folders, currentFolderId: currentFolderId, counts: counts, onNavigate: onNavigate }, child.id))) }) })] }) }));
46
+ }), className: "flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(ChevronRight, { className: "size-3.5 transition-transform duration-200 group-data-[state=open]/folder:rotate-90" }) }) }), nameButton, menu] }), _jsx(CollapsibleContent, { children: _jsx("ul", { className: "ml-[0.85rem] mt-0.5 space-y-0.5 border-l border-border pl-1.5", children: children.map((child) => (_jsx(MediaFolderTreeItem, { folder: child, folders: folders, currentFolderId: currentFolderId, counts: counts, actions: actions }, child.id))) }) })] }) }));
37
47
  }
@@ -3,12 +3,13 @@ import type { MediaFolder } from '../../types/mediaFolder';
3
3
  * The Media Library's folder sidebar — a **flat** nav region sitting flush on the
4
4
  * page background (mirroring the Content Library sidebar), not a bordered island.
5
5
  * The page supplies the divider (`lg:border-r`) via `className`, so the mobile
6
- * drawer copy renders without one. An "All media" root entry sits above the
7
- * folder tree, where any folder with sub-folders is collapsible; a New folder
8
- * action closes the list. Highlights the open folder and dispatches navigation /
9
- * create up to the page.
6
+ * drawer copy renders without one. A "Folders" header carries a + (New folder);
7
+ * an "All media" root entry sits above the folder tree, where any folder with
8
+ * sub-folders is collapsible and every folder has its ⋯ menu (Rename / Delete)
9
+ * — the tree is the one place folders are drawn. Highlights the open folder and
10
+ * dispatches navigation / create / rename / delete up to the page.
10
11
  */
11
- export declare function MediaFoldersNav({ folders, currentFolderId, counts, rootCount, onNavigate, onNewFolder, canCreate, className }: {
12
+ export declare function MediaFoldersNav({ folders, currentFolderId, counts, rootCount, onNavigate, onNewFolder, onRenameFolder, onDeleteFolder, canCreate, canUpdate, canDelete, className }: {
12
13
  folders: MediaFolder[];
13
14
  currentFolderId: string;
14
15
  counts: Map<string, number>;
@@ -16,7 +17,11 @@ export declare function MediaFoldersNav({ folders, currentFolderId, counts, root
16
17
  rootCount: number;
17
18
  onNavigate: (folderId: string) => void;
18
19
  onNewFolder: () => void;
20
+ onRenameFolder: (folder: MediaFolder) => void;
21
+ onDeleteFolder: (folder: MediaFolder) => void;
19
22
  canCreate: boolean;
23
+ canUpdate: boolean;
24
+ canDelete: boolean;
20
25
  className?: string;
21
26
  }): import("react").JSX.Element;
22
27
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaFoldersNav/index.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAe3D;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,EAC5B,OAAO,EACP,eAAe,EACf,MAAM,EACN,SAAS,EACT,UAAU,EACV,WAAW,EACX,SAAS,EACT,SAAS,EACZ,EAAE;IACC,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,+BAmFA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaFoldersNav/index.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAe3D;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,EAC5B,OAAO,EACP,eAAe,EACf,MAAM,EACN,SAAS,EACT,UAAU,EACV,WAAW,EACX,cAAc,EACd,cAAc,EACd,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,cAAc,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9C,cAAc,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9C,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,+BAsFA"}
@@ -1,13 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { defineMessages, useIntl } from 'react-intl';
3
3
  import { Button, cn } from '@orthacms/design-system';
4
- import { FolderPlus, Home, Images } from 'lucide-react';
4
+ import { Home, Plus } from 'lucide-react';
5
5
  import { ROOT_FOLDER_ID } from '../../constants';
6
6
  import { MediaFolderTreeItem } from './MediaFolderTreeItem';
7
7
  /** Intl descriptors for {@link MediaFoldersNav}, co-located. */
8
8
  const messages = defineMessages({
9
9
  nav: { id: 'media.nav.nav', defaultMessage: 'Folders' },
10
- heading: { id: 'media.nav.heading', defaultMessage: 'Library' },
10
+ heading: { id: 'media.nav.heading', defaultMessage: 'Folders' },
11
11
  allMedia: { id: 'media.nav.allMedia', defaultMessage: 'All media' },
12
12
  allCount: {
13
13
  id: 'media.nav.allCount',
@@ -19,22 +19,30 @@ const messages = defineMessages({
19
19
  * The Media Library's folder sidebar — a **flat** nav region sitting flush on the
20
20
  * page background (mirroring the Content Library sidebar), not a bordered island.
21
21
  * The page supplies the divider (`lg:border-r`) via `className`, so the mobile
22
- * drawer copy renders without one. An "All media" root entry sits above the
23
- * folder tree, where any folder with sub-folders is collapsible; a New folder
24
- * action closes the list. Highlights the open folder and dispatches navigation /
25
- * create up to the page.
22
+ * drawer copy renders without one. A "Folders" header carries a + (New folder);
23
+ * an "All media" root entry sits above the folder tree, where any folder with
24
+ * sub-folders is collapsible and every folder has its ⋯ menu (Rename / Delete)
25
+ * — the tree is the one place folders are drawn. Highlights the open folder and
26
+ * dispatches navigation / create / rename / delete up to the page.
26
27
  */
27
- export function MediaFoldersNav({ folders, currentFolderId, counts, rootCount, onNavigate, onNewFolder, canCreate, className }) {
28
+ export function MediaFoldersNav({ folders, currentFolderId, counts, rootCount, onNavigate, onNewFolder, onRenameFolder, onDeleteFolder, canCreate, canUpdate, canDelete, className }) {
28
29
  const intl = useIntl();
29
30
  const topLevel = folders
30
31
  .filter((f) => f.parentId === ROOT_FOLDER_ID)
31
32
  .sort((a, b) => a.name.localeCompare(b.name));
32
33
  const rootActive = currentFolderId === ROOT_FOLDER_ID;
33
- return (_jsxs("nav", { "aria-label": intl.formatMessage(messages.nav), className: cn('flex h-full w-60 shrink-0 flex-col overflow-hidden', className), children: [_jsxs("div", { className: "flex items-center gap-2 p-2", children: [_jsx(Images, { className: "size-4 text-muted-foreground", "aria-hidden": true }), _jsx("h2", { className: "text-sm font-semibold tracking-[-0.01em]", children: intl.formatMessage(messages.heading) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-2", children: _jsxs("ul", { className: "space-y-0.5", children: [_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onNavigate(ROOT_FOLDER_ID), "aria-current": rootActive ? 'true' : undefined, className: cn('flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', rootActive
34
+ const actions = {
35
+ onNavigate,
36
+ onRename: onRenameFolder,
37
+ onDelete: onDeleteFolder,
38
+ canUpdate,
39
+ canDelete
40
+ };
41
+ return (_jsxs("nav", { "aria-label": intl.formatMessage(messages.nav), className: cn('flex h-full w-60 shrink-0 flex-col overflow-hidden', className), children: [_jsxs("div", { className: "flex h-10 items-center gap-2 pl-3 pr-2", children: [_jsx("h2", { className: "flex-1 text-xs font-medium text-muted-foreground", children: intl.formatMessage(messages.heading) }), canCreate ? (_jsx(Button, { variant: "ghost", size: "icon", className: "size-6 shadow-none", onClick: onNewFolder, "aria-label": intl.formatMessage(messages.newFolder), children: _jsx(Plus, { "aria-hidden": true }) })) : null] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-2", children: _jsxs("ul", { className: "space-y-0.5", children: [_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onNavigate(ROOT_FOLDER_ID), "aria-current": rootActive ? 'true' : undefined, className: cn('flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', rootActive
34
42
  ? 'bg-primary/10 font-medium text-primary'
35
43
  : 'text-foreground hover:bg-accent'), children: [_jsx(Home, { className: cn('size-4 shrink-0', rootActive
36
44
  ? 'text-primary'
37
45
  : 'text-muted-foreground'), "aria-hidden": true }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: intl.formatMessage(messages.allMedia) }), _jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: intl.formatMessage(messages.allCount, {
38
46
  count: rootCount
39
- }) })] }) }), topLevel.map((folder) => (_jsx(MediaFolderTreeItem, { folder: folder, folders: folders, currentFolderId: currentFolderId, counts: counts, onNavigate: onNavigate }, folder.id)))] }) }), canCreate ? (_jsx("div", { className: "p-2", children: _jsxs(Button, { variant: "outline", size: "sm", className: "w-full justify-start shadow-none", onClick: onNewFolder, children: [_jsx(FolderPlus, { "aria-hidden": true }), intl.formatMessage(messages.newFolder)] }) })) : null] }));
47
+ }) })] }) }), topLevel.map((folder) => (_jsx(MediaFolderTreeItem, { folder: folder, folders: folders, currentFolderId: currentFolderId, counts: counts, actions: actions }, folder.id)))] }) })] }));
40
48
  }
@@ -1,14 +1,25 @@
1
1
  import type { MediaAsset } from '../../../types/mediaAsset';
2
2
  import { type AssetActionKind } from '../../AssetActionsMenu';
3
3
  /**
4
- * An asset tile in the grid — a gradient thumbnail with a hover ⋯ menu and a
4
+ * An asset tile in the grid — a 4:3 thumbnail with a hover ⋯ menu and a
5
5
  * selection checkbox (always visible once selected), over the name and a
6
- * size/dimensions line. Clicking the thumbnail or name opens the detail drawer;
7
- * the checkbox toggles selection. A selected tile gets a primary ring.
6
+ * dimensions · size line. A file with no picture shows its
7
+ * extension ("PDF", "MP4") under the kind glyph, and an image with no alt text
8
+ * says so on the tile, since that is the gap a library is usually swept for.
9
+ * Clicking the thumbnail or name opens the details panel; the checkbox toggles
10
+ * selection. A selected tile, and the one open in the panel, get a ring.
8
11
  */
9
- export declare function MediaAssetCard({ asset, selected, onToggleSelect, onAction, canCreate, canUpdate, canDelete }: {
12
+ export declare function MediaAssetCard({ asset, selected, active, onToggleSelect, onAction, canCreate, canUpdate, canDelete, justUploaded }: {
10
13
  asset: MediaAsset;
11
14
  selected: boolean;
15
+ /** The tile's file is the one open in the details panel. */
16
+ active?: boolean;
17
+ /**
18
+ * The asset was just uploaded: the tile rises into its slot. Not
19
+ * `ds-flash` — its keyframes animate `background-color` to transparent,
20
+ * which would drop the tile's card background for the length of the fade.
21
+ */
22
+ justUploaded?: boolean;
12
23
  onToggleSelect: (id: string) => void;
13
24
  onAction: (kind: AssetActionKind, asset: MediaAsset) => void;
14
25
  canCreate: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaGrid/MediaAssetCard/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAG5D,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAShF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,cAAc,EACd,QAAQ,EACR,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC7D,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BAsEA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaGrid/MediaAssetCard/index.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAI5D,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAUhF;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,MAAc,EACd,cAAc,EACd,QAAQ,EACR,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAoB,EACvB,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC7D,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BA4FA"}
@@ -1,29 +1,45 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { defineMessages, useIntl } from 'react-intl';
3
- import { Checkbox, cn } from '@orthacms/design-system';
3
+ import { Badge, Checkbox, cn } from '@orthacms/design-system';
4
+ import { MEDIA_KIND } from '../../../constants';
4
5
  import { formatBytes } from '../../../utils/formatBytes';
5
6
  import { buildAssetHandlers } from '../../../utils/assetHandlers';
7
+ import { fileExtension } from '../../../utils/fileExtension';
6
8
  import { AssetActionsMenu } from '../../AssetActionsMenu';
7
9
  import { MediaThumbnail } from '../../MediaThumbnail';
8
10
  /** Intl descriptors for {@link MediaAssetCard}, co-located. */
9
11
  const messages = defineMessages({
10
12
  open: { id: 'media.assetCard.open', defaultMessage: 'Open {name}' },
11
- select: { id: 'media.assetCard.select', defaultMessage: 'Select {name}' }
13
+ select: { id: 'media.assetCard.select', defaultMessage: 'Select {name}' },
14
+ noAlt: { id: 'media.assetCard.noAlt', defaultMessage: 'No alt text' }
12
15
  });
13
16
  /**
14
- * An asset tile in the grid — a gradient thumbnail with a hover ⋯ menu and a
17
+ * An asset tile in the grid — a 4:3 thumbnail with a hover ⋯ menu and a
15
18
  * selection checkbox (always visible once selected), over the name and a
16
- * size/dimensions line. Clicking the thumbnail or name opens the detail drawer;
17
- * the checkbox toggles selection. A selected tile gets a primary ring.
19
+ * dimensions · size line. A file with no picture shows its
20
+ * extension ("PDF", "MP4") under the kind glyph, and an image with no alt text
21
+ * says so on the tile, since that is the gap a library is usually swept for.
22
+ * Clicking the thumbnail or name opens the details panel; the checkbox toggles
23
+ * selection. A selected tile, and the one open in the panel, get a ring.
18
24
  */
19
- export function MediaAssetCard({ asset, selected, onToggleSelect, onAction, canCreate, canUpdate, canDelete }) {
25
+ export function MediaAssetCard({ asset, selected, active = false, onToggleSelect, onAction, canCreate, canUpdate, canDelete, justUploaded = false }) {
20
26
  const intl = useIntl();
21
27
  const handlers = buildAssetHandlers(asset, onAction);
22
- return (_jsxs("div", { className: cn('group relative overflow-hidden rounded-xl border bg-card transition-shadow hover:shadow-md', selected && 'ring-2 ring-primary ring-offset-1'), children: [_jsxs("div", { className: "relative", children: [_jsx("button", { type: "button", onClick: () => onAction('open', asset), "aria-label": intl.formatMessage(messages.open, {
28
+ const isImage = asset.kind === MEDIA_KIND.Image;
29
+ const extension = isImage ? null : fileExtension(asset.name);
30
+ const missingAlt = isImage && !asset.alt?.trim();
31
+ const meta = [
32
+ // A clip's duration is already on its thumbnail.
33
+ asset.dimensions
34
+ ? `${asset.dimensions.width}×${asset.dimensions.height}`
35
+ : null,
36
+ formatBytes(asset.size)
37
+ ]
38
+ .filter(Boolean)
39
+ .join(' · ');
40
+ return (_jsxs("div", { "aria-current": active ? 'true' : undefined, className: cn('group relative overflow-hidden rounded-xl border bg-card transition-shadow hover:shadow-md', (selected || active) && 'ring-2 ring-primary ring-offset-1', justUploaded && 'ds-item-enter'), children: [_jsxs("div", { className: "relative", children: [_jsx("button", { type: "button", onClick: () => onAction('open', asset), "aria-label": intl.formatMessage(messages.open, {
23
41
  name: asset.name
24
- }), className: "block w-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(MediaThumbnail, { asset: asset, className: "aspect-[4/3] w-full" }) }), _jsx("div", { className: cn('absolute left-2 top-2 rounded-md bg-background/80 p-0.5 shadow-sm backdrop-blur transition-opacity focus-within:opacity-100 group-hover:opacity-100', selected ? 'opacity-100' : 'opacity-0'), children: _jsx(Checkbox, { checked: selected, onCheckedChange: () => onToggleSelect(asset.id), "aria-label": intl.formatMessage(messages.select, {
42
+ }), className: "block w-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(MediaThumbnail, { asset: asset, className: "aspect-[4/3] w-full", label: extension }) }), _jsx("div", { className: cn('absolute left-2 top-2 rounded-md bg-background/80 p-0.5 shadow-sm backdrop-blur transition-opacity focus-within:opacity-100 group-hover:opacity-100', selected ? 'opacity-100' : 'opacity-0'), children: _jsx(Checkbox, { checked: selected, onCheckedChange: () => onToggleSelect(asset.id), "aria-label": intl.formatMessage(messages.select, {
25
43
  name: asset.name
26
- }) }) }), _jsx("div", { className: "absolute right-2 top-2 rounded-md bg-background/80 shadow-sm backdrop-blur transition-opacity focus-within:opacity-100 group-hover:opacity-100 sm:opacity-0", children: _jsx(AssetActionsMenu, { handlers: handlers, assetName: asset.name, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete }) })] }), _jsxs("div", { className: "p-3", children: [_jsx("button", { type: "button", onClick: () => onAction('open', asset), className: "block w-full truncate text-left text-sm font-medium hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", title: asset.name, children: asset.name }), _jsxs("p", { className: "mt-0.5 truncate text-xs text-muted-foreground", children: [formatBytes(asset.size), asset.dimensions
27
- ? ` · ${asset.dimensions.width}×${asset.dimensions.height}`
28
- : null] })] })] }));
44
+ }) }) }), _jsx("div", { className: "absolute right-2 top-2 rounded-md bg-background/80 shadow-sm backdrop-blur transition-opacity focus-within:opacity-100 group-hover:opacity-100 sm:opacity-0", children: _jsx(AssetActionsMenu, { handlers: handlers, assetName: asset.name, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete }) }), missingAlt ? (_jsx(Badge, { variant: "warning", size: "xs", className: "pointer-events-none absolute bottom-2 left-2 shadow-sm", children: intl.formatMessage(messages.noAlt) })) : null] }), _jsxs("div", { className: "px-3 py-2.5", children: [_jsx("button", { type: "button", onClick: () => onAction('open', asset), className: "block w-full truncate text-left text-sm font-medium hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", title: asset.name, children: asset.name }), _jsx("p", { className: "mt-0.5 truncate text-xs text-muted-foreground", children: meta })] })] }));
29
45
  }
@@ -1,20 +1,20 @@
1
1
  import type { MediaAsset } from '../../types/mediaAsset';
2
- import type { MediaFolder } from '../../types/mediaFolder';
3
2
  import type { AssetActionKind } from '../AssetActionsMenu';
4
3
  /**
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
4
+ * The grid view of the open folder's files — a responsive grid of asset
5
+ * tiles. Folders are not tiles here: they live in the folder tree beside the
6
+ * grid (with their own ⋯ menu), so the grid is the folder's files alone.
7
+ *
8
+ * Columns follow the **pane's** width, not the viewport's (a container query),
9
+ * so opening the details panel beside the grid drops a column instead of
10
+ * squeezing every tile. Purely presentational: selection state and every
8
11
  * action are dispatched up to the page.
9
12
  */
10
- export declare function MediaGrid({ folders, assets, folderCounts, selectedIds, onOpenFolder, onRenameFolder, onDeleteFolder, onToggleSelect, onAssetAction, canCreate, canUpdate, canDelete }: {
11
- folders: MediaFolder[];
13
+ export declare function MediaGrid({ assets, selectedIds, activeId, onToggleSelect, onAssetAction, canCreate, canUpdate, canDelete }: {
12
14
  assets: MediaAsset[];
13
- folderCounts: Map<string, number>;
14
15
  selectedIds: Set<string>;
15
- onOpenFolder: (folderId: string) => void;
16
- onRenameFolder: (folder: MediaFolder) => void;
17
- onDeleteFolder: (folder: MediaFolder) => void;
16
+ /** The asset open in the details panel. */
17
+ activeId?: string | null;
18
18
  onToggleSelect: (id: string) => void;
19
19
  onAssetAction: (kind: AssetActionKind, asset: MediaAsset) => void;
20
20
  canCreate: boolean;
@@ -1 +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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaGrid/index.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAG3D;;;;;;;;;GASG;AACH,wBAAgB,SAAS,CAAC,EACtB,MAAM,EACN,WAAW,EACX,QAAQ,EACR,cAAc,EACd,aAAa,EACb,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,2CAA2C;IAC3C,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,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,+BAuBA"}
@@ -1,20 +1,18 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { defineMessages, useIntl } from 'react-intl';
3
- import { MediaFolderCard } from './MediaFolderCard';
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useJustCreated } from '@orthacms/utils-admin';
3
+ import { MEDIA_ASSETS_SCOPE } from '../../hooks/useUploadQueue';
4
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
5
  /**
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
6
+ * The grid view of the open folder's files — a responsive grid of asset
7
+ * tiles. Folders are not tiles here: they live in the folder tree beside the
8
+ * grid (with their own ⋯ menu), so the grid is the folder's files alone.
9
+ *
10
+ * Columns follow the **pane's** width, not the viewport's (a container query),
11
+ * so opening the details panel beside the grid drops a column instead of
12
+ * squeezing every tile. Purely presentational: selection state and every
14
13
  * action are dispatched up to the page.
15
14
  */
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] }));
15
+ export function MediaGrid({ assets, selectedIds, activeId, onToggleSelect, onAssetAction, canCreate, canUpdate, canDelete }) {
16
+ const isJustUploaded = useJustCreated(MEDIA_ASSETS_SCOPE);
17
+ return (_jsx("div", { className: "@container", children: _jsx("div", { className: "grid grid-cols-2 gap-3 @lg:grid-cols-3 @3xl:grid-cols-4 @5xl:grid-cols-5", children: assets.map((asset) => (_jsx(MediaAssetCard, { asset: asset, selected: selectedIds.has(asset.id), active: asset.id === activeId, justUploaded: isJustUploaded(asset.id), onToggleSelect: onToggleSelect, onAction: onAssetAction, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete }, asset.id))) }) }));
20
18
  }
@@ -1,11 +1,15 @@
1
1
  /**
2
- * The bulk-action bar shown once one or more assets are selected — a "{n}
3
- * selected" count and, permission-gated, Download / Duplicate / Move / Delete,
4
- * with a Clear control. The count is visual only (the bar mounts/unmounts with
5
- * the selection); the page owns any spoken announcement.
2
+ * The bulk-action bar shown once one or more assets are selected — the
3
+ * design-system `SelectionBar` the records table uses too, floating at the
4
+ * bottom centre of the work area: "{n} selected of {total}", then
5
+ * (permission-gated) Move to… / Download / Duplicate, Delete after a divider,
6
+ * and a ✕ that clears the selection. The count is visual only (the bar
7
+ * mounts/unmounts with the selection); the page owns any spoken announcement.
6
8
  */
7
- export declare function MediaSelectionBar({ count, onDownload, onDuplicate, onMove, onDelete, onClear, canCreate, canUpdate, canDelete }: {
9
+ export declare function MediaSelectionBar({ count, total, onDownload, onDuplicate, onMove, onDelete, onClear, canCreate, canUpdate, canDelete }: {
8
10
  count: number;
11
+ /** Every file matching the controls, across pages. */
12
+ total?: number;
9
13
  onDownload: () => void;
10
14
  onDuplicate: () => void;
11
15
  onMove: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaSelectionBar/index.tsx"],"names":[],"mappings":"AAiBA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAC9B,KAAK,EACL,UAAU,EACV,WAAW,EACX,MAAM,EACN,QAAQ,EACR,OAAO,EACP,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BAgEA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaSelectionBar/index.tsx"],"names":[],"mappings":"AAuBA;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAC9B,KAAK,EACL,KAAK,EACL,UAAU,EACV,WAAW,EACX,MAAM,EACN,QAAQ,EACR,OAAO,EACP,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BAuDA"}
@@ -1,26 +1,32 @@
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 { defineMessages, useIntl } from 'react-intl';
3
- import { Button, Separator } from '@orthacms/design-system';
4
- import { Copy, Download, FolderInput, Trash2, X } from 'lucide-react';
3
+ import { Button, SelectionBar, SelectionBarDivider } from '@orthacms/design-system';
4
+ import { Copy, Download, FolderInput, Trash2 } from 'lucide-react';
5
5
  /** Intl descriptors for {@link MediaSelectionBar}, co-located. */
6
6
  const messages = defineMessages({
7
+ label: { id: 'media.selection.label', defaultMessage: 'Selection' },
7
8
  count: {
8
9
  id: 'media.selection.count',
9
10
  defaultMessage: '{count, plural, one {# selected} other {# selected}}'
10
11
  },
12
+ of: { id: 'media.selection.of', defaultMessage: 'of {total}' },
11
13
  download: { id: 'media.selection.download', defaultMessage: 'Download' },
12
14
  duplicate: { id: 'media.selection.duplicate', defaultMessage: 'Duplicate' },
13
- move: { id: 'media.selection.move', defaultMessage: 'Move' },
15
+ move: { id: 'media.selection.move', defaultMessage: 'Move to…' },
14
16
  delete: { id: 'media.selection.delete', defaultMessage: 'Delete' },
15
17
  clear: { id: 'media.selection.clear', defaultMessage: 'Clear selection' }
16
18
  });
17
19
  /**
18
- * The bulk-action bar shown once one or more assets are selected — a "{n}
19
- * selected" count and, permission-gated, Download / Duplicate / Move / Delete,
20
- * with a Clear control. The count is visual only (the bar mounts/unmounts with
21
- * the selection); the page owns any spoken announcement.
20
+ * The bulk-action bar shown once one or more assets are selected — the
21
+ * design-system `SelectionBar` the records table uses too, floating at the
22
+ * bottom centre of the work area: "{n} selected of {total}", then
23
+ * (permission-gated) Move to… / Download / Duplicate, Delete after a divider,
24
+ * and a ✕ that clears the selection. The count is visual only (the bar
25
+ * mounts/unmounts with the selection); the page owns any spoken announcement.
22
26
  */
23
- export function MediaSelectionBar({ count, onDownload, onDuplicate, onMove, onDelete, onClear, canCreate, canUpdate, canDelete }) {
27
+ export function MediaSelectionBar({ count, total, onDownload, onDuplicate, onMove, onDelete, onClear, canCreate, canUpdate, canDelete }) {
24
28
  const intl = useIntl();
25
- return (_jsxs("div", { className: "mb-4 flex flex-wrap items-center justify-between gap-3 rounded-xl border bg-muted/40 px-4 py-2", children: [_jsx("span", { className: "text-sm font-medium", children: intl.formatMessage(messages.count, { count }) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", className: "shadow-none", onClick: onDownload, children: [_jsx(Download, { "aria-hidden": true }), intl.formatMessage(messages.download)] }), canCreate ? (_jsxs(Button, { variant: "outline", size: "sm", className: "shadow-none", onClick: onDuplicate, children: [_jsx(Copy, { "aria-hidden": true }), intl.formatMessage(messages.duplicate)] })) : null, canUpdate ? (_jsxs(Button, { variant: "outline", size: "sm", className: "shadow-none", onClick: onMove, children: [_jsx(FolderInput, { "aria-hidden": true }), intl.formatMessage(messages.move)] })) : null, canDelete ? (_jsxs(Button, { variant: "outline", size: "sm", className: "text-destructive shadow-none hover:text-destructive", onClick: onDelete, children: [_jsx(Trash2, { "aria-hidden": true }), intl.formatMessage(messages.delete)] })) : null, _jsx(Separator, { orientation: "vertical", className: "h-6" }), _jsxs(Button, { variant: "ghost", size: "sm", className: "shadow-none", onClick: onClear, children: [_jsx(X, { "aria-hidden": true }), intl.formatMessage(messages.clear)] })] })] }));
29
+ return (_jsxs(SelectionBar, { label: intl.formatMessage(messages.label), clearLabel: intl.formatMessage(messages.clear), onClear: onClear, count: _jsxs(_Fragment, { children: [_jsx("span", { className: "font-medium", children: intl.formatMessage(messages.count, { count }) }), total !== undefined ? (_jsx("span", { className: "text-xs text-muted-foreground", children: intl.formatMessage(messages.of, {
30
+ total: intl.formatNumber(total)
31
+ }) })) : null] }), children: [canUpdate ? (_jsxs(Button, { variant: "ghost", size: "sm", onClick: onMove, children: [_jsx(FolderInput, { "aria-hidden": true }), intl.formatMessage(messages.move)] })) : null, _jsxs(Button, { variant: "ghost", size: "sm", onClick: onDownload, children: [_jsx(Download, { "aria-hidden": true }), intl.formatMessage(messages.download)] }), canCreate ? (_jsxs(Button, { variant: "ghost", size: "sm", onClick: onDuplicate, children: [_jsx(Copy, { "aria-hidden": true }), intl.formatMessage(messages.duplicate)] })) : null, canDelete ? (_jsxs(_Fragment, { children: [_jsx(SelectionBarDivider, {}), _jsxs(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", onClick: onDelete, children: [_jsx(Trash2, { "aria-hidden": true }), intl.formatMessage(messages.delete)] })] })) : null] }));
26
32
  }
@@ -2,18 +2,22 @@ import type { MediaAsset } from '../../types/mediaAsset';
2
2
  /**
3
3
  * The visual preview for an asset. Image assets render a real thumbnail served
4
4
  * from the media API — the small `thumb` derivative by default, or the larger
5
- * `preview` when `size="preview"` (the detail drawer) — falling back to the
5
+ * `preview` when `size="preview"` (the details panel) — falling back to the
6
6
  * original when no derivative exists (e.g. SVG). A deterministic gradient tile
7
- * sits underneath as the backdrop / loading state; non-image kinds show it with
8
- * a centred kind glyph. Video/audio get a play affordance and a duration pill.
9
- * Sized entirely by the parent via `className` (aspect + radius), so it serves
10
- * tiles, list rows, and the drawer.
7
+ * sits underneath as the backdrop / loading state — the image fades in over it
8
+ * once loaded (`FadeInImage`) — and non-image kinds show it with a centred kind
9
+ * glyph, over a short `label` (the grid passes the file's extension, so a PDF
10
+ * and a DOCX don't look alike). Video/audio get a play affordance and a
11
+ * duration pill. Sized entirely by the parent via `className` (aspect +
12
+ * radius), so it serves tiles, list rows, and the panel.
11
13
  */
12
- export declare function MediaThumbnail({ asset, className, iconClassName, size }: {
14
+ export declare function MediaThumbnail({ asset, className, iconClassName, label, size }: {
13
15
  asset: MediaAsset;
14
16
  className?: string;
15
17
  iconClassName?: string;
16
- /** Which derivative to prefer — the grid uses `thumb`, the drawer `preview`. */
18
+ /** Text under a non-image's glyph, e.g. `PDF`. Ignored for images. */
19
+ label?: string | null;
20
+ /** Which derivative to prefer — the grid uses `thumb`, the panel `preview`. */
17
21
  size?: 'thumb' | 'preview';
18
22
  }): import("react").JSX.Element;
19
23
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaThumbnail/index.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAKzD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,SAAS,EACT,aAAa,EACb,IAAc,EACjB,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC9B,+BA4CA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaThumbnail/index.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAMzD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,SAAS,EACT,aAAa,EACb,KAAK,EACL,IAAc,EACjB,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC9B,+BAsDA"}
@@ -4,23 +4,26 @@ import { cn } from '@orthacms/design-system';
4
4
  import { MEDIA_KIND } from '../../constants';
5
5
  import { assetGradient } from '../../utils/assetGradient';
6
6
  import { formatDuration } from '../../utils/formatDuration';
7
+ import { FadeInImage } from '../FadeInImage';
7
8
  import { MediaKindIcon } from '../MediaKindIcon';
8
9
  /**
9
10
  * The visual preview for an asset. Image assets render a real thumbnail served
10
11
  * from the media API — the small `thumb` derivative by default, or the larger
11
- * `preview` when `size="preview"` (the detail drawer) — falling back to the
12
+ * `preview` when `size="preview"` (the details panel) — falling back to the
12
13
  * original when no derivative exists (e.g. SVG). A deterministic gradient tile
13
- * sits underneath as the backdrop / loading state; non-image kinds show it with
14
- * a centred kind glyph. Video/audio get a play affordance and a duration pill.
15
- * Sized entirely by the parent via `className` (aspect + radius), so it serves
16
- * tiles, list rows, and the drawer.
14
+ * sits underneath as the backdrop / loading state — the image fades in over it
15
+ * once loaded (`FadeInImage`) — and non-image kinds show it with a centred kind
16
+ * glyph, over a short `label` (the grid passes the file's extension, so a PDF
17
+ * and a DOCX don't look alike). Video/audio get a play affordance and a
18
+ * duration pill. Sized entirely by the parent via `className` (aspect +
19
+ * radius), so it serves tiles, list rows, and the panel.
17
20
  */
18
- export function MediaThumbnail({ asset, className, iconClassName, size = 'thumb' }) {
21
+ export function MediaThumbnail({ asset, className, iconClassName, label, size = 'thumb' }) {
19
22
  const isImage = asset.kind === MEDIA_KIND.Image;
20
23
  const isPlayable = asset.kind === MEDIA_KIND.Video || asset.kind === MEDIA_KIND.Audio;
21
24
  // Prefer the requested derivative, then the other, then the original.
22
25
  const imageSrc = size === 'preview'
23
26
  ? (asset.previewUrl ?? asset.thumbUrl ?? asset.url)
24
27
  : (asset.thumbUrl ?? asset.previewUrl ?? asset.url);
25
- return (_jsxs("div", { className: cn('relative flex items-center justify-center overflow-hidden bg-muted', className), style: { backgroundImage: assetGradient(asset.id) }, children: [_jsx("div", { className: "absolute inset-0 bg-black/5", "aria-hidden": true }), isImage ? (_jsx("img", { src: imageSrc, alt: asset.alt ?? '', loading: "lazy", className: "absolute inset-0 size-full object-cover" })) : (_jsx(MediaKindIcon, { kind: asset.kind, className: cn('relative size-8 text-white/90 drop-shadow-sm', iconClassName) })), isPlayable ? (_jsxs("span", { className: "absolute bottom-1.5 right-1.5 inline-flex items-center gap-1 rounded-md bg-black/55 px-1.5 py-0.5 text-[11px] font-medium text-white", children: [_jsx(Play, { className: "size-3 fill-current", "aria-hidden": true }), asset.duration ? formatDuration(asset.duration) : null] })) : null] }));
28
+ return (_jsxs("div", { className: cn('relative flex items-center justify-center overflow-hidden bg-muted', className), style: { backgroundImage: assetGradient(asset.id) }, children: [_jsx("div", { className: "absolute inset-0 bg-black/5", "aria-hidden": true }), isImage ? (_jsx(FadeInImage, { src: imageSrc, alt: asset.alt ?? '', loading: "lazy", className: "absolute inset-0 size-full object-cover" })) : (_jsxs("span", { className: "relative flex flex-col items-center gap-1.5", children: [_jsx(MediaKindIcon, { kind: asset.kind, className: cn('size-8 text-white/90 drop-shadow-sm', iconClassName) }), label ? (_jsx("span", { className: "text-xs font-semibold tracking-wide text-white drop-shadow-sm", "aria-hidden": true, children: label })) : null] })), isPlayable ? (_jsxs("span", { className: "absolute bottom-1.5 right-1.5 inline-flex items-center gap-1 rounded-md bg-black/55 px-1.5 py-0.5 text-[11px] font-medium text-white", children: [_jsx(Play, { className: "size-3 fill-current", "aria-hidden": true }), asset.duration ? formatDuration(asset.duration) : null] })) : null] }));
26
29
  }
@@ -1,19 +1,21 @@
1
- import { type MediaSort } from '../../constants';
1
+ import { type MediaSort, type MediaView } from '../../constants';
2
2
  import type { KindFilter } from '../../hooks/useMediaLibrary';
3
3
  /**
4
- * The Media Library's control bar — a search box, a type filter, a sort select,
5
- * and (permission-gated) New folder + Upload actions. Fully controlled: every
6
- * value lives in the page's store and changes dispatch straight back up.
4
+ * The Media Library's control bar — the **type chips** (All · Images · Video ·
5
+ * Audio · Documents · Archives, one pressed at a time; the same pill the
6
+ * records table's status chips are) on the left, then a search box, a sort
7
+ * select and the Grid / List switch on the right. Fully controlled: every value
8
+ * lives in the page and changes dispatch straight back up. New folder and
9
+ * Upload are the page's top-bar actions, not this bar's.
7
10
  */
8
- export declare function MediaToolbar({ search, onSearchChange, kindFilter, onKindFilterChange, sort, onSortChange, onNewFolder, onUpload, canCreate }: {
11
+ export declare function MediaToolbar({ search, onSearchChange, kindFilter, onKindFilterChange, sort, onSortChange, view, onViewChange }: {
9
12
  search: string;
10
13
  onSearchChange: (value: string) => void;
11
14
  kindFilter: KindFilter;
12
15
  onKindFilterChange: (value: KindFilter) => void;
13
16
  sort: MediaSort;
14
17
  onSortChange: (value: MediaSort) => void;
15
- onNewFolder: () => void;
16
- onUpload: () => void;
17
- canCreate: boolean;
18
+ view: MediaView;
19
+ onViewChange: (value: MediaView) => void;
18
20
  }): import("react").JSX.Element;
19
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaToolbar/index.tsx"],"names":[],"mappings":"AAcA,OAAO,EAIH,KAAK,SAAS,EACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAmD9D;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EACzB,MAAM,EACN,cAAc,EACd,UAAU,EACV,kBAAkB,EAClB,IAAI,EACJ,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,SAAS,EACZ,EAAE;IACC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,UAAU,EAAE,UAAU,CAAC;IACvB,kBAAkB,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAChD,IAAI,EAAE,SAAS,CAAC;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACzC,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;CACtB,+BA8GA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaToolbar/index.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EACd,KAAK,SAAS,EACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AA8D9D;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,EACzB,MAAM,EACN,cAAc,EACd,UAAU,EACV,kBAAkB,EAClB,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,YAAY,EACf,EAAE;IACC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,UAAU,EAAE,UAAU,CAAC;IACvB,kBAAkB,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAChD,IAAI,EAAE,SAAS,CAAC;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACzC,IAAI,EAAE,SAAS,CAAC;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CAC5C,+BAiHA"}