@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.
- package/dist/lib/components/AssetDetailPanel/MetaRow/index.d.ts +11 -0
- package/dist/lib/components/AssetDetailPanel/MetaRow/index.d.ts.map +1 -0
- package/dist/lib/components/AssetDetailPanel/MetaRow/index.js +9 -0
- package/dist/lib/components/AssetDetailPanel/index.d.ts +35 -0
- package/dist/lib/components/AssetDetailPanel/index.d.ts.map +1 -0
- package/dist/lib/components/AssetDetailPanel/index.js +158 -0
- package/dist/lib/components/FadeInImage/index.d.ts +24 -0
- package/dist/lib/components/FadeInImage/index.d.ts.map +1 -0
- package/dist/lib/components/FadeInImage/index.js +34 -0
- package/dist/lib/components/FolderActionsMenu/index.d.ts +3 -1
- package/dist/lib/components/FolderActionsMenu/index.d.ts.map +1 -1
- package/dist/lib/components/FolderActionsMenu/index.js +3 -3
- package/dist/lib/components/MediaAssetTable/index.d.ts +25 -0
- package/dist/lib/components/MediaAssetTable/index.d.ts.map +1 -0
- package/dist/lib/components/MediaAssetTable/index.js +62 -0
- package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.d.ts.map +1 -1
- package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.js +2 -1
- package/dist/lib/components/MediaFieldControl/index.d.ts.map +1 -1
- package/dist/lib/components/MediaFieldControl/index.js +19 -14
- package/dist/lib/components/MediaFieldInput/index.d.ts +19 -0
- package/dist/lib/components/MediaFieldInput/index.d.ts.map +1 -0
- package/dist/lib/components/MediaFieldInput/index.js +23 -0
- package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts +17 -3
- package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts.map +1 -1
- package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.js +16 -6
- package/dist/lib/components/MediaFoldersNav/index.d.ts +10 -5
- package/dist/lib/components/MediaFoldersNav/index.d.ts.map +1 -1
- package/dist/lib/components/MediaFoldersNav/index.js +17 -9
- package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts +15 -4
- package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts.map +1 -1
- package/dist/lib/components/MediaGrid/MediaAssetCard/index.js +27 -11
- package/dist/lib/components/MediaGrid/index.d.ts +10 -10
- package/dist/lib/components/MediaGrid/index.d.ts.map +1 -1
- package/dist/lib/components/MediaGrid/index.js +13 -15
- package/dist/lib/components/MediaSelectionBar/index.d.ts +9 -5
- package/dist/lib/components/MediaSelectionBar/index.d.ts.map +1 -1
- package/dist/lib/components/MediaSelectionBar/index.js +16 -10
- package/dist/lib/components/MediaThumbnail/index.d.ts +11 -7
- package/dist/lib/components/MediaThumbnail/index.d.ts.map +1 -1
- package/dist/lib/components/MediaThumbnail/index.js +10 -7
- package/dist/lib/components/MediaToolbar/index.d.ts +10 -8
- package/dist/lib/components/MediaToolbar/index.d.ts.map +1 -1
- package/dist/lib/components/MediaToolbar/index.js +37 -13
- package/dist/lib/components/MediaTopBar/index.d.ts +7 -1
- package/dist/lib/components/MediaTopBar/index.d.ts.map +1 -1
- package/dist/lib/components/MediaTopBar/index.js +6 -3
- package/dist/lib/components/MediaUploadBanner/index.d.ts +7 -5
- package/dist/lib/components/MediaUploadBanner/index.d.ts.map +1 -1
- package/dist/lib/components/MediaUploadBanner/index.js +15 -8
- package/dist/lib/components/UploadDialog/StagedFileRow/index.d.ts.map +1 -1
- package/dist/lib/components/UploadDialog/StagedFileRow/index.js +2 -1
- package/dist/lib/constants/index.d.ts +26 -0
- package/dist/lib/constants/index.d.ts.map +1 -1
- package/dist/lib/constants/index.js +24 -0
- package/dist/lib/hooks/useUploadQueue/index.d.ts +2 -0
- package/dist/lib/hooks/useUploadQueue/index.d.ts.map +1 -1
- package/dist/lib/hooks/useUploadQueue/index.js +15 -1
- package/dist/lib/pages/MediaLibraryPage/index.d.ts +6 -4
- package/dist/lib/pages/MediaLibraryPage/index.d.ts.map +1 -1
- package/dist/lib/pages/MediaLibraryPage/index.js +86 -38
- package/dist/lib/utils/fileExtension/index.d.ts +9 -0
- package/dist/lib/utils/fileExtension/index.d.ts.map +1 -0
- package/dist/lib/utils/fileExtension/index.js +14 -0
- package/dist/lib/utils/mediaPlugin/index.d.ts.map +1 -1
- package/dist/lib/utils/mediaPlugin/index.js +19 -24
- 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.
|
|
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,
|
|
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
|
-
|
|
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;
|
|
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.
|
|
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,
|
|
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
|
|
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:
|
|
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:
|
|
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,
|
|
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.
|
|
7
|
-
* folder tree, where any folder with
|
|
8
|
-
*
|
|
9
|
-
*
|
|
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
|
|
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 {
|
|
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: '
|
|
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.
|
|
23
|
-
* folder tree, where any folder with
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
-
*
|
|
7
|
-
* the
|
|
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":"
|
|
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
|
|
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
|
-
*
|
|
17
|
-
* the
|
|
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
|
-
|
|
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: "
|
|
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
|
|
6
|
-
*
|
|
7
|
-
*
|
|
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({
|
|
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
|
-
|
|
16
|
-
|
|
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":"
|
|
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
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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({
|
|
17
|
-
const
|
|
18
|
-
|
|
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 —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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":"
|
|
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,
|
|
4
|
-
import { Copy, Download, FolderInput, Trash2
|
|
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 —
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
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(
|
|
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
|
|
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
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
-
/**
|
|
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;
|
|
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
|
|
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
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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(
|
|
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 —
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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,
|
|
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
|
-
|
|
16
|
-
|
|
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":"
|
|
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"}
|