@orthacms/media-admin 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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/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 +9 -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 +10 -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 +9 -6
- package/dist/lib/components/MediaThumbnail/index.d.ts.map +1 -1
- package/dist/lib/components/MediaThumbnail/index.js +7 -6
- 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/constants/index.d.ts +13 -0
- package/dist/lib/constants/index.d.ts.map +1 -1
- package/dist/lib/constants/index.js +11 -0
- 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 +82 -36
- 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 +16 -20
- package/package.json +10 -10
|
@@ -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,19 @@
|
|
|
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, justUploaded }: {
|
|
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;
|
|
12
17
|
/**
|
|
13
18
|
* The asset was just uploaded: the tile rises into its slot. Not
|
|
14
19
|
* `ds-flash` — its keyframes animate `background-color` to transparent,
|
|
@@ -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, justUploaded = false }) {
|
|
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,23 +1,18 @@
|
|
|
1
|
-
import { jsx as _jsx
|
|
2
|
-
import { defineMessages, useIntl } from 'react-intl';
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
2
|
import { useJustCreated } from '@orthacms/utils-admin';
|
|
4
3
|
import { MEDIA_ASSETS_SCOPE } from '../../hooks/useUploadQueue';
|
|
5
|
-
import { MediaFolderCard } from './MediaFolderCard';
|
|
6
4
|
import { MediaAssetCard } from './MediaAssetCard';
|
|
7
|
-
/** Intl descriptors for {@link MediaGrid} section labels, co-located. */
|
|
8
|
-
const messages = defineMessages({
|
|
9
|
-
folders: { id: 'media.grid.folders', defaultMessage: 'Folders' },
|
|
10
|
-
files: { id: 'media.grid.files', defaultMessage: 'Files' }
|
|
11
|
-
});
|
|
12
5
|
/**
|
|
13
|
-
* The grid view of the open folder — a responsive grid of
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
|
16
13
|
* action are dispatched up to the page.
|
|
17
14
|
*/
|
|
18
|
-
export function MediaGrid({
|
|
19
|
-
const intl = useIntl();
|
|
15
|
+
export function MediaGrid({ assets, selectedIds, activeId, onToggleSelect, onAssetAction, canCreate, canUpdate, canDelete }) {
|
|
20
16
|
const isJustUploaded = useJustCreated(MEDIA_ASSETS_SCOPE);
|
|
21
|
-
|
|
22
|
-
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), justUploaded: isJustUploaded(asset.id), onToggleSelect: onToggleSelect, onAction: onAssetAction, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete }, asset.id))) })] })) : null] }));
|
|
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))) }) }));
|
|
23
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,19 +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
7
|
* sits underneath as the backdrop / loading state — the image fades in over it
|
|
8
8
|
* once loaded (`FadeInImage`) — and non-image kinds show it with a centred kind
|
|
9
|
-
* glyph
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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.
|
|
12
13
|
*/
|
|
13
|
-
export declare function MediaThumbnail({ asset, className, iconClassName, size }: {
|
|
14
|
+
export declare function MediaThumbnail({ asset, className, iconClassName, label, size }: {
|
|
14
15
|
asset: MediaAsset;
|
|
15
16
|
className?: string;
|
|
16
17
|
iconClassName?: string;
|
|
17
|
-
/**
|
|
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`. */
|
|
18
21
|
size?: 'thumb' | 'preview';
|
|
19
22
|
}): import("react").JSX.Element;
|
|
20
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;AAMzD
|
|
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"}
|
|
@@ -9,20 +9,21 @@ import { MediaKindIcon } from '../MediaKindIcon';
|
|
|
9
9
|
/**
|
|
10
10
|
* The visual preview for an asset. Image assets render a real thumbnail served
|
|
11
11
|
* from the media API — the small `thumb` derivative by default, or the larger
|
|
12
|
-
* `preview` when `size="preview"` (the
|
|
12
|
+
* `preview` when `size="preview"` (the details panel) — falling back to the
|
|
13
13
|
* original when no derivative exists (e.g. SVG). A deterministic gradient tile
|
|
14
14
|
* sits underneath as the backdrop / loading state — the image fades in over it
|
|
15
15
|
* once loaded (`FadeInImage`) — and non-image kinds show it with a centred kind
|
|
16
|
-
* glyph
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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.
|
|
19
20
|
*/
|
|
20
|
-
export function MediaThumbnail({ asset, className, iconClassName, size = 'thumb' }) {
|
|
21
|
+
export function MediaThumbnail({ asset, className, iconClassName, label, size = 'thumb' }) {
|
|
21
22
|
const isImage = asset.kind === MEDIA_KIND.Image;
|
|
22
23
|
const isPlayable = asset.kind === MEDIA_KIND.Video || asset.kind === MEDIA_KIND.Audio;
|
|
23
24
|
// Prefer the requested derivative, then the other, then the original.
|
|
24
25
|
const imageSrc = size === 'preview'
|
|
25
26
|
? (asset.previewUrl ?? asset.thumbUrl ?? asset.url)
|
|
26
27
|
: (asset.thumbUrl ?? asset.previewUrl ?? asset.url);
|
|
27
|
-
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" })) : (_jsx(MediaKindIcon, { kind: asset.kind, className: cn('
|
|
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] }));
|
|
28
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"}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { KIND_FILTER_ALL, MEDIA_KIND, MEDIA_SORT } from '../../constants';
|
|
3
|
+
import { DensityProvider, InputGroup, InputGroupAddon, InputGroupInput, SegmentedControl, SegmentedControlItem, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, cn } from '@orthacms/design-system';
|
|
4
|
+
import { LayoutGrid, List, Search } from 'lucide-react';
|
|
5
|
+
import { KIND_FILTER_ALL, MEDIA_KIND, MEDIA_SORT, MEDIA_VIEW } from '../../constants';
|
|
6
6
|
/** Intl descriptors for {@link MediaToolbar}, co-located. */
|
|
7
7
|
const messages = defineMessages({
|
|
8
8
|
search: { id: 'media.toolbar.search', defaultMessage: 'Search assets' },
|
|
@@ -14,9 +14,9 @@ const messages = defineMessages({
|
|
|
14
14
|
id: 'media.toolbar.kindLabel',
|
|
15
15
|
defaultMessage: 'Filter by type'
|
|
16
16
|
},
|
|
17
|
-
kindAll: { id: 'media.toolbar.kindAll', defaultMessage: 'All
|
|
17
|
+
kindAll: { id: 'media.toolbar.kindAll', defaultMessage: 'All' },
|
|
18
18
|
kindImage: { id: 'media.toolbar.kindImage', defaultMessage: 'Images' },
|
|
19
|
-
kindVideo: { id: 'media.toolbar.kindVideo', defaultMessage: '
|
|
19
|
+
kindVideo: { id: 'media.toolbar.kindVideo', defaultMessage: 'Video' },
|
|
20
20
|
kindAudio: { id: 'media.toolbar.kindAudio', defaultMessage: 'Audio' },
|
|
21
21
|
kindDocument: {
|
|
22
22
|
id: 'media.toolbar.kindDocument',
|
|
@@ -48,15 +48,39 @@ const messages = defineMessages({
|
|
|
48
48
|
id: 'media.toolbar.sortSmallest',
|
|
49
49
|
defaultMessage: 'Smallest'
|
|
50
50
|
},
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
viewLabel: { id: 'media.toolbar.viewLabel', defaultMessage: 'View' },
|
|
52
|
+
viewGrid: { id: 'media.toolbar.viewGrid', defaultMessage: 'Grid view' },
|
|
53
|
+
viewList: { id: 'media.toolbar.viewList', defaultMessage: 'List view' }
|
|
53
54
|
});
|
|
55
|
+
/** The type chips, in reading order — "All" first, the kinds after it. */
|
|
56
|
+
const KIND_CHIPS = [
|
|
57
|
+
{ value: KIND_FILTER_ALL, label: 'kindAll' },
|
|
58
|
+
{ value: MEDIA_KIND.Image, label: 'kindImage' },
|
|
59
|
+
{ value: MEDIA_KIND.Video, label: 'kindVideo' },
|
|
60
|
+
{ value: MEDIA_KIND.Audio, label: 'kindAudio' },
|
|
61
|
+
{ value: MEDIA_KIND.Document, label: 'kindDocument' },
|
|
62
|
+
{ value: MEDIA_KIND.Archive, label: 'kindArchive' }
|
|
63
|
+
];
|
|
54
64
|
/**
|
|
55
|
-
* The Media Library's control bar —
|
|
56
|
-
*
|
|
57
|
-
*
|
|
65
|
+
* The Media Library's control bar — the **type chips** (All · Images · Video ·
|
|
66
|
+
* Audio · Documents · Archives, one pressed at a time; the same pill the
|
|
67
|
+
* records table's status chips are) on the left, then a search box, a sort
|
|
68
|
+
* select and the Grid / List switch on the right. Fully controlled: every value
|
|
69
|
+
* lives in the page and changes dispatch straight back up. New folder and
|
|
70
|
+
* Upload are the page's top-bar actions, not this bar's.
|
|
58
71
|
*/
|
|
59
|
-
export function MediaToolbar({ search, onSearchChange, kindFilter, onKindFilterChange, sort, onSortChange,
|
|
72
|
+
export function MediaToolbar({ search, onSearchChange, kindFilter, onKindFilterChange, sort, onSortChange, view, onViewChange }) {
|
|
60
73
|
const intl = useIntl();
|
|
61
|
-
return (_jsx(DensityProvider, { density: "compact", children: _jsxs("div", { className: "mb-4 flex flex-wrap items-center gap-2", children: [
|
|
74
|
+
return (_jsx(DensityProvider, { density: "compact", children: _jsxs("div", { className: "mb-4 flex flex-wrap items-center gap-2", children: [_jsx("div", { role: "group", "aria-label": intl.formatMessage(messages.kindLabel), className: "flex flex-wrap items-center gap-1.5", children: KIND_CHIPS.map(({ value, label }) => {
|
|
75
|
+
const on = kindFilter === value;
|
|
76
|
+
return (_jsx("button", { type: "button", "aria-pressed": on, onClick: () => onKindFilterChange(value), className: cn('inline-flex h-7 items-center rounded-full border px-2.5 text-xs font-medium outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring', on
|
|
77
|
+
? 'border-foreground bg-foreground text-background'
|
|
78
|
+
: 'bg-background text-foreground hover:bg-muted'), children: intl.formatMessage(messages[label]) }, value));
|
|
79
|
+
}) }), _jsxs("div", { className: "flex w-full flex-wrap items-center gap-2 sm:ml-auto sm:w-auto", children: [_jsxs(InputGroup, { className: "w-full shadow-none sm:w-[200px]", children: [_jsx(InputGroupAddon, { children: _jsx(Search, {}) }), _jsx(InputGroupInput, { type: "search", value: search, onChange: (event) => onSearchChange(event.target.value), "aria-label": intl.formatMessage(messages.search), placeholder: intl.formatMessage(messages.searchPlaceholder) })] }), _jsxs(Select, { value: sort, onValueChange: (value) => onSortChange(value), children: [_jsx(SelectTrigger, { className: "w-[132px] shadow-none", "aria-label": intl.formatMessage(messages.sortLabel), children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: MEDIA_SORT.Newest, children: intl.formatMessage(messages.sortNewest) }), _jsx(SelectItem, { value: MEDIA_SORT.Oldest, children: intl.formatMessage(messages.sortOldest) }), _jsx(SelectItem, { value: MEDIA_SORT.NameAsc, children: intl.formatMessage(messages.sortNameAsc) }), _jsx(SelectItem, { value: MEDIA_SORT.NameDesc, children: intl.formatMessage(messages.sortNameDesc) }), _jsx(SelectItem, { value: MEDIA_SORT.Largest, children: intl.formatMessage(messages.sortLargest) }), _jsx(SelectItem, { value: MEDIA_SORT.Smallest, children: intl.formatMessage(messages.sortSmallest) })] })] }), _jsxs(SegmentedControl, { variant: "muted", value: view,
|
|
80
|
+
// A single toggle group reports '' when its pressed
|
|
81
|
+
// item is pressed again; a view switch can't be off.
|
|
82
|
+
onValueChange: (value) => {
|
|
83
|
+
if (value)
|
|
84
|
+
onViewChange(value);
|
|
85
|
+
}, "aria-label": intl.formatMessage(messages.viewLabel), children: [_jsx(SegmentedControlItem, { value: MEDIA_VIEW.Grid, "aria-label": intl.formatMessage(messages.viewGrid), className: "px-2", children: _jsx(LayoutGrid, { className: "size-4", "aria-hidden": true }) }), _jsx(SegmentedControlItem, { value: MEDIA_VIEW.List, "aria-label": intl.formatMessage(messages.viewList), className: "px-2", children: _jsx(List, { className: "size-4", "aria-hidden": true }) })] })] })] }) }));
|
|
62
86
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
1
2
|
import type { MediaFolder } from '../../types/mediaFolder';
|
|
2
3
|
/**
|
|
3
4
|
* The page top bar over the Media Library — the teal `Image` tile (matching the
|
|
@@ -11,10 +12,15 @@ import type { MediaFolder } from '../../types/mediaFolder';
|
|
|
11
12
|
* the store's `breadcrumbs` and each one dispatches `onNavigate` rather than
|
|
12
13
|
* linking. They are still `BreadcrumbLink`s over real `<button>`s — a control
|
|
13
14
|
* that changes view without changing location is a button, not a link.
|
|
15
|
+
*
|
|
16
|
+
* `actions` (New folder, Upload) sit at the bar's right end, where every other
|
|
17
|
+
* admin page keeps its page-level actions.
|
|
14
18
|
*/
|
|
15
|
-
export declare function MediaTopBar({ breadcrumbs, onNavigate }: {
|
|
19
|
+
export declare function MediaTopBar({ breadcrumbs, onNavigate, actions }: {
|
|
16
20
|
/** Ancestor chain of the open folder, root-first (excludes the root itself). */
|
|
17
21
|
breadcrumbs: MediaFolder[];
|
|
18
22
|
onNavigate: (folderId: string) => void;
|
|
23
|
+
/** Page-level actions, drawn at the bar's right end. */
|
|
24
|
+
actions?: ReactNode;
|
|
19
25
|
}): import("react").JSX.Element;
|
|
20
26
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaTopBar/index.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaTopBar/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAevC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAQ3D;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,WAAW,CAAC,EACxB,WAAW,EACX,UAAU,EACV,OAAO,EACV,EAAE;IACC,gFAAgF;IAChF,WAAW,EAAE,WAAW,EAAE,CAAC;IAC3B,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,wDAAwD;IACxD,OAAO,CAAC,EAAE,SAAS,CAAC;CACvB,+BA6CA"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { defineMessages, useIntl } from 'react-intl';
|
|
3
3
|
import { Image } from 'lucide-react';
|
|
4
|
-
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, TopBar, TopBarIcon } from '@orthacms/design-system';
|
|
4
|
+
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink, BreadcrumbList, BreadcrumbPage, BreadcrumbSeparator, TopBar, TopBarActions, TopBarIcon } from '@orthacms/design-system';
|
|
5
5
|
import { ROOT_FOLDER_ID } from '../../constants';
|
|
6
6
|
/** Intl descriptors for {@link MediaTopBar}, co-located. */
|
|
7
7
|
const messages = defineMessages({
|
|
@@ -20,8 +20,11 @@ const messages = defineMessages({
|
|
|
20
20
|
* the store's `breadcrumbs` and each one dispatches `onNavigate` rather than
|
|
21
21
|
* linking. They are still `BreadcrumbLink`s over real `<button>`s — a control
|
|
22
22
|
* that changes view without changing location is a button, not a link.
|
|
23
|
+
*
|
|
24
|
+
* `actions` (New folder, Upload) sit at the bar's right end, where every other
|
|
25
|
+
* admin page keeps its page-level actions.
|
|
23
26
|
*/
|
|
24
|
-
export function MediaTopBar({ breadcrumbs, onNavigate }) {
|
|
27
|
+
export function MediaTopBar({ breadcrumbs, onNavigate, actions }) {
|
|
25
28
|
const intl = useIntl();
|
|
26
29
|
const crumbs = [
|
|
27
30
|
{ id: ROOT_FOLDER_ID, name: intl.formatMessage(messages.root) },
|
|
@@ -31,5 +34,5 @@ export function MediaTopBar({ breadcrumbs, onNavigate }) {
|
|
|
31
34
|
return (_jsxs(TopBar, { children: [_jsx(TopBarIcon, { className: "bg-teal-soft text-teal-soft-foreground", children: _jsx(Image, {}) }), _jsx(Breadcrumb, { "aria-label": intl.formatMessage(messages.nav), children: _jsx(BreadcrumbList, { className: "flex-nowrap font-medium", children: crumbs.map((crumb, index) => [
|
|
32
35
|
index > 0 ? (_jsx(BreadcrumbSeparator, {}, `${crumb.id}-sep`)) : null,
|
|
33
36
|
_jsx(BreadcrumbItem, { className: "min-w-0 whitespace-nowrap", children: index === last ? (_jsx(BreadcrumbPage, { className: "flex min-w-0 items-center font-medium", children: crumb.name })) : (_jsx(BreadcrumbLink, { asChild: true, children: _jsx("button", { type: "button", onClick: () => onNavigate(crumb.id), children: crumb.name }) })) }, crumb.id)
|
|
34
|
-
]) }) })] }));
|
|
37
|
+
]) }) }), actions ? _jsx(TopBarActions, { children: actions }) : null] }));
|
|
35
38
|
}
|