@orthacms/media-admin 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/lib/components/AssetDetailPanel/MetaRow/index.d.ts +11 -0
  2. package/dist/lib/components/AssetDetailPanel/MetaRow/index.d.ts.map +1 -0
  3. package/dist/lib/components/AssetDetailPanel/MetaRow/index.js +9 -0
  4. package/dist/lib/components/AssetDetailPanel/index.d.ts +35 -0
  5. package/dist/lib/components/AssetDetailPanel/index.d.ts.map +1 -0
  6. package/dist/lib/components/AssetDetailPanel/index.js +158 -0
  7. package/dist/lib/components/FolderActionsMenu/index.d.ts +3 -1
  8. package/dist/lib/components/FolderActionsMenu/index.d.ts.map +1 -1
  9. package/dist/lib/components/FolderActionsMenu/index.js +3 -3
  10. package/dist/lib/components/MediaAssetTable/index.d.ts +25 -0
  11. package/dist/lib/components/MediaAssetTable/index.d.ts.map +1 -0
  12. package/dist/lib/components/MediaAssetTable/index.js +62 -0
  13. package/dist/lib/components/MediaFieldControl/index.d.ts.map +1 -1
  14. package/dist/lib/components/MediaFieldControl/index.js +19 -14
  15. package/dist/lib/components/MediaFieldInput/index.d.ts +19 -0
  16. package/dist/lib/components/MediaFieldInput/index.d.ts.map +1 -0
  17. package/dist/lib/components/MediaFieldInput/index.js +23 -0
  18. package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts +17 -3
  19. package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts.map +1 -1
  20. package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.js +16 -6
  21. package/dist/lib/components/MediaFoldersNav/index.d.ts +10 -5
  22. package/dist/lib/components/MediaFoldersNav/index.d.ts.map +1 -1
  23. package/dist/lib/components/MediaFoldersNav/index.js +17 -9
  24. package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts +9 -4
  25. package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts.map +1 -1
  26. package/dist/lib/components/MediaGrid/MediaAssetCard/index.js +27 -11
  27. package/dist/lib/components/MediaGrid/index.d.ts +10 -10
  28. package/dist/lib/components/MediaGrid/index.d.ts.map +1 -1
  29. package/dist/lib/components/MediaGrid/index.js +10 -15
  30. package/dist/lib/components/MediaSelectionBar/index.d.ts +9 -5
  31. package/dist/lib/components/MediaSelectionBar/index.d.ts.map +1 -1
  32. package/dist/lib/components/MediaSelectionBar/index.js +16 -10
  33. package/dist/lib/components/MediaThumbnail/index.d.ts +9 -6
  34. package/dist/lib/components/MediaThumbnail/index.d.ts.map +1 -1
  35. package/dist/lib/components/MediaThumbnail/index.js +7 -6
  36. package/dist/lib/components/MediaToolbar/index.d.ts +10 -8
  37. package/dist/lib/components/MediaToolbar/index.d.ts.map +1 -1
  38. package/dist/lib/components/MediaToolbar/index.js +37 -13
  39. package/dist/lib/components/MediaTopBar/index.d.ts +7 -1
  40. package/dist/lib/components/MediaTopBar/index.d.ts.map +1 -1
  41. package/dist/lib/components/MediaTopBar/index.js +6 -3
  42. package/dist/lib/components/MediaUploadBanner/index.d.ts +7 -5
  43. package/dist/lib/components/MediaUploadBanner/index.d.ts.map +1 -1
  44. package/dist/lib/components/MediaUploadBanner/index.js +15 -8
  45. package/dist/lib/constants/index.d.ts +13 -0
  46. package/dist/lib/constants/index.d.ts.map +1 -1
  47. package/dist/lib/constants/index.js +11 -0
  48. package/dist/lib/pages/MediaLibraryPage/index.d.ts +6 -4
  49. package/dist/lib/pages/MediaLibraryPage/index.d.ts.map +1 -1
  50. package/dist/lib/pages/MediaLibraryPage/index.js +82 -36
  51. package/dist/lib/utils/fileExtension/index.d.ts +9 -0
  52. package/dist/lib/utils/fileExtension/index.d.ts.map +1 -0
  53. package/dist/lib/utils/fileExtension/index.js +14 -0
  54. package/dist/lib/utils/mediaPlugin/index.d.ts.map +1 -1
  55. package/dist/lib/utils/mediaPlugin/index.js +16 -20
  56. package/package.json +10 -10
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaFoldersNav/index.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAe3D;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,EAC5B,OAAO,EACP,eAAe,EACf,MAAM,EACN,SAAS,EACT,UAAU,EACV,WAAW,EACX,SAAS,EACT,SAAS,EACZ,EAAE;IACC,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,+BAmFA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaFoldersNav/index.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAe3D;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,EAC5B,OAAO,EACP,eAAe,EACf,MAAM,EACN,SAAS,EACT,UAAU,EACV,WAAW,EACX,cAAc,EACd,cAAc,EACd,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5B,yDAAyD;IACzD,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,cAAc,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9C,cAAc,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9C,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,+BAsFA"}
@@ -1,13 +1,13 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { defineMessages, useIntl } from 'react-intl';
3
3
  import { Button, cn } from '@orthacms/design-system';
4
- import { FolderPlus, Home, Images } from 'lucide-react';
4
+ import { Home, Plus } from 'lucide-react';
5
5
  import { ROOT_FOLDER_ID } from '../../constants';
6
6
  import { MediaFolderTreeItem } from './MediaFolderTreeItem';
7
7
  /** Intl descriptors for {@link MediaFoldersNav}, co-located. */
8
8
  const messages = defineMessages({
9
9
  nav: { id: 'media.nav.nav', defaultMessage: 'Folders' },
10
- heading: { id: 'media.nav.heading', defaultMessage: 'Library' },
10
+ heading: { id: 'media.nav.heading', defaultMessage: 'Folders' },
11
11
  allMedia: { id: 'media.nav.allMedia', defaultMessage: 'All media' },
12
12
  allCount: {
13
13
  id: 'media.nav.allCount',
@@ -19,22 +19,30 @@ const messages = defineMessages({
19
19
  * The Media Library's folder sidebar — a **flat** nav region sitting flush on the
20
20
  * page background (mirroring the Content Library sidebar), not a bordered island.
21
21
  * The page supplies the divider (`lg:border-r`) via `className`, so the mobile
22
- * drawer copy renders without one. An "All media" root entry sits above the
23
- * folder tree, where any folder with sub-folders is collapsible; a New folder
24
- * action closes the list. Highlights the open folder and dispatches navigation /
25
- * create up to the page.
22
+ * drawer copy renders without one. A "Folders" header carries a + (New folder);
23
+ * an "All media" root entry sits above the folder tree, where any folder with
24
+ * sub-folders is collapsible and every folder has its ⋯ menu (Rename / Delete)
25
+ * — the tree is the one place folders are drawn. Highlights the open folder and
26
+ * dispatches navigation / create / rename / delete up to the page.
26
27
  */
27
- export function MediaFoldersNav({ folders, currentFolderId, counts, rootCount, onNavigate, onNewFolder, canCreate, className }) {
28
+ export function MediaFoldersNav({ folders, currentFolderId, counts, rootCount, onNavigate, onNewFolder, onRenameFolder, onDeleteFolder, canCreate, canUpdate, canDelete, className }) {
28
29
  const intl = useIntl();
29
30
  const topLevel = folders
30
31
  .filter((f) => f.parentId === ROOT_FOLDER_ID)
31
32
  .sort((a, b) => a.name.localeCompare(b.name));
32
33
  const rootActive = currentFolderId === ROOT_FOLDER_ID;
33
- return (_jsxs("nav", { "aria-label": intl.formatMessage(messages.nav), className: cn('flex h-full w-60 shrink-0 flex-col overflow-hidden', className), children: [_jsxs("div", { className: "flex items-center gap-2 p-2", children: [_jsx(Images, { className: "size-4 text-muted-foreground", "aria-hidden": true }), _jsx("h2", { className: "text-sm font-semibold tracking-[-0.01em]", children: intl.formatMessage(messages.heading) })] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-2", children: _jsxs("ul", { className: "space-y-0.5", children: [_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onNavigate(ROOT_FOLDER_ID), "aria-current": rootActive ? 'true' : undefined, className: cn('flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', rootActive
34
+ const actions = {
35
+ onNavigate,
36
+ onRename: onRenameFolder,
37
+ onDelete: onDeleteFolder,
38
+ canUpdate,
39
+ canDelete
40
+ };
41
+ return (_jsxs("nav", { "aria-label": intl.formatMessage(messages.nav), className: cn('flex h-full w-60 shrink-0 flex-col overflow-hidden', className), children: [_jsxs("div", { className: "flex h-10 items-center gap-2 pl-3 pr-2", children: [_jsx("h2", { className: "flex-1 text-xs font-medium text-muted-foreground", children: intl.formatMessage(messages.heading) }), canCreate ? (_jsx(Button, { variant: "ghost", size: "icon", className: "size-6 shadow-none", onClick: onNewFolder, "aria-label": intl.formatMessage(messages.newFolder), children: _jsx(Plus, { "aria-hidden": true }) })) : null] }), _jsx("div", { className: "min-h-0 flex-1 overflow-y-auto px-2", children: _jsxs("ul", { className: "space-y-0.5", children: [_jsx("li", { children: _jsxs("button", { type: "button", onClick: () => onNavigate(ROOT_FOLDER_ID), "aria-current": rootActive ? 'true' : undefined, className: cn('flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring', rootActive
34
42
  ? 'bg-primary/10 font-medium text-primary'
35
43
  : 'text-foreground hover:bg-accent'), children: [_jsx(Home, { className: cn('size-4 shrink-0', rootActive
36
44
  ? 'text-primary'
37
45
  : 'text-muted-foreground'), "aria-hidden": true }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: intl.formatMessage(messages.allMedia) }), _jsx("span", { className: "shrink-0 text-xs tabular-nums text-muted-foreground", children: intl.formatMessage(messages.allCount, {
38
46
  count: rootCount
39
- }) })] }) }), topLevel.map((folder) => (_jsx(MediaFolderTreeItem, { folder: folder, folders: folders, currentFolderId: currentFolderId, counts: counts, onNavigate: onNavigate }, folder.id)))] }) }), canCreate ? (_jsx("div", { className: "p-2", children: _jsxs(Button, { variant: "outline", size: "sm", className: "w-full justify-start shadow-none", onClick: onNewFolder, children: [_jsx(FolderPlus, { "aria-hidden": true }), intl.formatMessage(messages.newFolder)] }) })) : null] }));
47
+ }) })] }) }), topLevel.map((folder) => (_jsx(MediaFolderTreeItem, { folder: folder, folders: folders, currentFolderId: currentFolderId, counts: counts, actions: actions }, folder.id)))] }) })] }));
40
48
  }
@@ -1,14 +1,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 gradient thumbnail with a hover ⋯ menu and a
4
+ * An asset tile in the grid — a 4:3 thumbnail with a hover ⋯ menu and a
5
5
  * selection checkbox (always visible once selected), over the name and a
6
- * size/dimensions line. Clicking the thumbnail or name opens the detail drawer;
7
- * the checkbox toggles selection. A selected tile gets a primary ring.
6
+ * dimensions · size line. A file with no picture shows its
7
+ * extension ("PDF", "MP4") under the kind glyph, and an image with no alt text
8
+ * says so on the tile, since that is the gap a library is usually swept for.
9
+ * Clicking the thumbnail or name opens the details panel; the checkbox toggles
10
+ * selection. A selected tile, and the one open in the panel, get a ring.
8
11
  */
9
- export declare function MediaAssetCard({ asset, selected, onToggleSelect, onAction, canCreate, canUpdate, canDelete, 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":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAG5D,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAShF;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,cAAc,EACd,QAAQ,EACR,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAoB,EACvB,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB;;;;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,+BAuEA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaGrid/MediaAssetCard/index.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAI5D,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAUhF;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,QAAQ,EACR,MAAc,EACd,cAAc,EACd,QAAQ,EACR,SAAS,EACT,SAAS,EACT,SAAS,EACT,YAAoB,EACvB,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,QAAQ,EAAE,OAAO,CAAC;IAClB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC7D,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BA4FA"}
@@ -1,29 +1,45 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { defineMessages, useIntl } from 'react-intl';
3
- import { Checkbox, cn } from '@orthacms/design-system';
3
+ import { Badge, Checkbox, cn } from '@orthacms/design-system';
4
+ import { MEDIA_KIND } from '../../../constants';
4
5
  import { formatBytes } from '../../../utils/formatBytes';
5
6
  import { buildAssetHandlers } from '../../../utils/assetHandlers';
7
+ import { fileExtension } from '../../../utils/fileExtension';
6
8
  import { AssetActionsMenu } from '../../AssetActionsMenu';
7
9
  import { MediaThumbnail } from '../../MediaThumbnail';
8
10
  /** Intl descriptors for {@link MediaAssetCard}, co-located. */
9
11
  const messages = defineMessages({
10
12
  open: { id: 'media.assetCard.open', defaultMessage: 'Open {name}' },
11
- select: { id: 'media.assetCard.select', defaultMessage: 'Select {name}' }
13
+ select: { id: 'media.assetCard.select', defaultMessage: 'Select {name}' },
14
+ noAlt: { id: 'media.assetCard.noAlt', defaultMessage: 'No alt text' }
12
15
  });
13
16
  /**
14
- * An asset tile in the grid — a gradient thumbnail with a hover ⋯ menu and a
17
+ * An asset tile in the grid — a 4:3 thumbnail with a hover ⋯ menu and a
15
18
  * selection checkbox (always visible once selected), over the name and a
16
- * size/dimensions line. Clicking the thumbnail or name opens the detail drawer;
17
- * the checkbox toggles selection. A selected tile gets a primary ring.
19
+ * dimensions · size line. A file with no picture shows its
20
+ * extension ("PDF", "MP4") under the kind glyph, and an image with no alt text
21
+ * says so on the tile, since that is the gap a library is usually swept for.
22
+ * Clicking the thumbnail or name opens the details panel; the checkbox toggles
23
+ * selection. A selected tile, and the one open in the panel, get a ring.
18
24
  */
19
- export function MediaAssetCard({ asset, selected, onToggleSelect, onAction, canCreate, canUpdate, canDelete, 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
- return (_jsxs("div", { className: cn('group relative overflow-hidden rounded-xl border bg-card transition-shadow hover:shadow-md', selected && 'ring-2 ring-primary ring-offset-1', justUploaded && 'ds-item-enter'), children: [_jsxs("div", { className: "relative", children: [_jsx("button", { type: "button", onClick: () => onAction('open', asset), "aria-label": intl.formatMessage(messages.open, {
28
+ const isImage = asset.kind === MEDIA_KIND.Image;
29
+ const extension = isImage ? null : fileExtension(asset.name);
30
+ const missingAlt = isImage && !asset.alt?.trim();
31
+ const meta = [
32
+ // A clip's duration is already on its thumbnail.
33
+ asset.dimensions
34
+ ? `${asset.dimensions.width}×${asset.dimensions.height}`
35
+ : null,
36
+ formatBytes(asset.size)
37
+ ]
38
+ .filter(Boolean)
39
+ .join(' · ');
40
+ return (_jsxs("div", { "aria-current": active ? 'true' : undefined, className: cn('group relative overflow-hidden rounded-xl border bg-card transition-shadow hover:shadow-md', (selected || active) && 'ring-2 ring-primary ring-offset-1', justUploaded && 'ds-item-enter'), children: [_jsxs("div", { className: "relative", children: [_jsx("button", { type: "button", onClick: () => onAction('open', asset), "aria-label": intl.formatMessage(messages.open, {
23
41
  name: asset.name
24
- }), className: "block w-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(MediaThumbnail, { asset: asset, className: "aspect-[4/3] w-full" }) }), _jsx("div", { className: cn('absolute left-2 top-2 rounded-md bg-background/80 p-0.5 shadow-sm backdrop-blur transition-opacity focus-within:opacity-100 group-hover:opacity-100', selected ? 'opacity-100' : 'opacity-0'), children: _jsx(Checkbox, { checked: selected, onCheckedChange: () => onToggleSelect(asset.id), "aria-label": intl.formatMessage(messages.select, {
42
+ }), className: "block w-full focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: _jsx(MediaThumbnail, { asset: asset, className: "aspect-[4/3] w-full", label: extension }) }), _jsx("div", { className: cn('absolute left-2 top-2 rounded-md bg-background/80 p-0.5 shadow-sm backdrop-blur transition-opacity focus-within:opacity-100 group-hover:opacity-100', selected ? 'opacity-100' : 'opacity-0'), children: _jsx(Checkbox, { checked: selected, onCheckedChange: () => onToggleSelect(asset.id), "aria-label": intl.formatMessage(messages.select, {
25
43
  name: asset.name
26
- }) }) }), _jsx("div", { className: "absolute right-2 top-2 rounded-md bg-background/80 shadow-sm backdrop-blur transition-opacity focus-within:opacity-100 group-hover:opacity-100 sm:opacity-0", children: _jsx(AssetActionsMenu, { handlers: handlers, assetName: asset.name, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete }) })] }), _jsxs("div", { className: "p-3", children: [_jsx("button", { type: "button", onClick: () => onAction('open', asset), className: "block w-full truncate text-left text-sm font-medium hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", title: asset.name, children: asset.name }), _jsxs("p", { className: "mt-0.5 truncate text-xs text-muted-foreground", children: [formatBytes(asset.size), asset.dimensions
27
- ? ` · ${asset.dimensions.width}×${asset.dimensions.height}`
28
- : null] })] })] }));
44
+ }) }) }), _jsx("div", { className: "absolute right-2 top-2 rounded-md bg-background/80 shadow-sm backdrop-blur transition-opacity focus-within:opacity-100 group-hover:opacity-100 sm:opacity-0", children: _jsx(AssetActionsMenu, { handlers: handlers, assetName: asset.name, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete }) }), missingAlt ? (_jsx(Badge, { variant: "warning", size: "xs", className: "pointer-events-none absolute bottom-2 left-2 shadow-sm", children: intl.formatMessage(messages.noAlt) })) : null] }), _jsxs("div", { className: "px-3 py-2.5", children: [_jsx("button", { type: "button", onClick: () => onAction('open', asset), className: "block w-full truncate text-left text-sm font-medium hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", title: asset.name, children: asset.name }), _jsx("p", { className: "mt-0.5 truncate text-xs text-muted-foreground", children: meta })] })] }));
29
45
  }
@@ -1,20 +1,20 @@
1
1
  import type { MediaAsset } from '../../types/mediaAsset';
2
- import type { MediaFolder } from '../../types/mediaFolder';
3
2
  import type { AssetActionKind } from '../AssetActionsMenu';
4
3
  /**
5
- * The grid view of the open folder — a responsive grid of folder tiles above a
6
- * responsive grid of asset tiles, each under a small section label (shown only
7
- * when both kinds are present). Purely presentational: selection state and every
4
+ * The grid view of the open folder's files — a responsive grid of asset
5
+ * tiles. Folders are not tiles here: they live in the folder tree beside the
6
+ * grid (with their own ⋯ menu), so the grid is the folder's files alone.
7
+ *
8
+ * Columns follow the **pane's** width, not the viewport's (a container query),
9
+ * so opening the details panel beside the grid drops a column instead of
10
+ * squeezing every tile. Purely presentational: selection state and every
8
11
  * action are dispatched up to the page.
9
12
  */
10
- export declare function MediaGrid({ folders, assets, folderCounts, selectedIds, onOpenFolder, onRenameFolder, onDeleteFolder, onToggleSelect, onAssetAction, canCreate, canUpdate, canDelete }: {
11
- folders: MediaFolder[];
13
+ export declare function MediaGrid({ assets, selectedIds, activeId, onToggleSelect, onAssetAction, canCreate, canUpdate, canDelete }: {
12
14
  assets: MediaAsset[];
13
- folderCounts: Map<string, number>;
14
15
  selectedIds: Set<string>;
15
- onOpenFolder: (folderId: string) => void;
16
- onRenameFolder: (folder: MediaFolder) => void;
17
- onDeleteFolder: (folder: MediaFolder) => void;
16
+ /** The asset open in the details panel. */
17
+ activeId?: string | null;
18
18
  onToggleSelect: (id: string) => void;
19
19
  onAssetAction: (kind: AssetActionKind, asset: MediaAsset) => void;
20
20
  canCreate: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaGrid/index.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAC3D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAU3D;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,EACtB,OAAO,EACP,MAAM,EACN,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,cAAc,EACd,cAAc,EACd,cAAc,EACd,aAAa,EACb,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,OAAO,EAAE,WAAW,EAAE,CAAC;IACvB,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAClC,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,YAAY,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACzC,cAAc,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9C,cAAc,EAAE,CAAC,MAAM,EAAE,WAAW,KAAK,IAAI,CAAC;IAC9C,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,aAAa,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAClE,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BAyDA"}
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, jsxs as _jsxs } from "react/jsx-runtime";
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 folder tiles above a
14
- * responsive grid of asset tiles, each under a small section label (shown only
15
- * when both kinds are present). Purely presentational: selection state and every
6
+ * The grid view of the open folder's files — a responsive grid of asset
7
+ * tiles. Folders are not tiles here: they live in the folder tree beside the
8
+ * grid (with their own ⋯ menu), so the grid is the folder's files alone.
9
+ *
10
+ * Columns follow the **pane's** width, not the viewport's (a container query),
11
+ * so opening the details panel beside the grid drops a column instead of
12
+ * squeezing every tile. Purely presentational: selection state and every
16
13
  * action are dispatched up to the page.
17
14
  */
18
- export function MediaGrid({ folders, assets, folderCounts, selectedIds, onOpenFolder, onRenameFolder, onDeleteFolder, onToggleSelect, onAssetAction, canCreate, canUpdate, canDelete }) {
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
- const showLabels = folders.length > 0 && assets.length > 0;
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 — a "{n}
3
- * selected" count and, permission-gated, Download / Duplicate / Move / Delete,
4
- * with a Clear control. The count is visual only (the bar mounts/unmounts with
5
- * the selection); the page owns any spoken announcement.
2
+ * The bulk-action bar shown once one or more assets are selected — the
3
+ * design-system `SelectionBar` the records table uses too, floating at the
4
+ * bottom centre of the work area: "{n} selected of {total}", then
5
+ * (permission-gated) Move to… / Download / Duplicate, Delete after a divider,
6
+ * and a ✕ that clears the selection. The count is visual only (the bar
7
+ * mounts/unmounts with the selection); the page owns any spoken announcement.
6
8
  */
7
- export declare function MediaSelectionBar({ count, onDownload, onDuplicate, onMove, onDelete, onClear, canCreate, canUpdate, canDelete }: {
9
+ export declare function MediaSelectionBar({ count, total, onDownload, onDuplicate, onMove, onDelete, onClear, canCreate, canUpdate, canDelete }: {
8
10
  count: number;
11
+ /** Every file matching the controls, across pages. */
12
+ total?: number;
9
13
  onDownload: () => void;
10
14
  onDuplicate: () => void;
11
15
  onMove: () => void;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaSelectionBar/index.tsx"],"names":[],"mappings":"AAiBA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAC9B,KAAK,EACL,UAAU,EACV,WAAW,EACX,MAAM,EACN,QAAQ,EACR,OAAO,EACP,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BAgEA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaSelectionBar/index.tsx"],"names":[],"mappings":"AAuBA;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,EAC9B,KAAK,EACL,KAAK,EACL,UAAU,EACV,WAAW,EACX,MAAM,EACN,QAAQ,EACR,OAAO,EACP,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACtB,+BAuDA"}
@@ -1,26 +1,32 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { defineMessages, useIntl } from 'react-intl';
3
- import { Button, Separator } from '@orthacms/design-system';
4
- import { Copy, Download, FolderInput, Trash2, X } from 'lucide-react';
3
+ import { Button, SelectionBar, SelectionBarDivider } from '@orthacms/design-system';
4
+ import { Copy, Download, FolderInput, Trash2 } from 'lucide-react';
5
5
  /** Intl descriptors for {@link MediaSelectionBar}, co-located. */
6
6
  const messages = defineMessages({
7
+ label: { id: 'media.selection.label', defaultMessage: 'Selection' },
7
8
  count: {
8
9
  id: 'media.selection.count',
9
10
  defaultMessage: '{count, plural, one {# selected} other {# selected}}'
10
11
  },
12
+ of: { id: 'media.selection.of', defaultMessage: 'of {total}' },
11
13
  download: { id: 'media.selection.download', defaultMessage: 'Download' },
12
14
  duplicate: { id: 'media.selection.duplicate', defaultMessage: 'Duplicate' },
13
- move: { id: 'media.selection.move', defaultMessage: 'Move' },
15
+ move: { id: 'media.selection.move', defaultMessage: 'Move to…' },
14
16
  delete: { id: 'media.selection.delete', defaultMessage: 'Delete' },
15
17
  clear: { id: 'media.selection.clear', defaultMessage: 'Clear selection' }
16
18
  });
17
19
  /**
18
- * The bulk-action bar shown once one or more assets are selected — a "{n}
19
- * selected" count and, permission-gated, Download / Duplicate / Move / Delete,
20
- * with a Clear control. The count is visual only (the bar mounts/unmounts with
21
- * the selection); the page owns any spoken announcement.
20
+ * The bulk-action bar shown once one or more assets are selected — the
21
+ * design-system `SelectionBar` the records table uses too, floating at the
22
+ * bottom centre of the work area: "{n} selected of {total}", then
23
+ * (permission-gated) Move to… / Download / Duplicate, Delete after a divider,
24
+ * and a ✕ that clears the selection. The count is visual only (the bar
25
+ * mounts/unmounts with the selection); the page owns any spoken announcement.
22
26
  */
23
- export function MediaSelectionBar({ count, onDownload, onDuplicate, onMove, onDelete, onClear, canCreate, canUpdate, canDelete }) {
27
+ export function MediaSelectionBar({ count, total, onDownload, onDuplicate, onMove, onDelete, onClear, canCreate, canUpdate, canDelete }) {
24
28
  const intl = useIntl();
25
- return (_jsxs("div", { className: "mb-4 flex flex-wrap items-center justify-between gap-3 rounded-xl border bg-muted/40 px-4 py-2", children: [_jsx("span", { className: "text-sm font-medium", children: intl.formatMessage(messages.count, { count }) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [_jsxs(Button, { variant: "outline", size: "sm", className: "shadow-none", onClick: onDownload, children: [_jsx(Download, { "aria-hidden": true }), intl.formatMessage(messages.download)] }), canCreate ? (_jsxs(Button, { variant: "outline", size: "sm", className: "shadow-none", onClick: onDuplicate, children: [_jsx(Copy, { "aria-hidden": true }), intl.formatMessage(messages.duplicate)] })) : null, canUpdate ? (_jsxs(Button, { variant: "outline", size: "sm", className: "shadow-none", onClick: onMove, children: [_jsx(FolderInput, { "aria-hidden": true }), intl.formatMessage(messages.move)] })) : null, canDelete ? (_jsxs(Button, { variant: "outline", size: "sm", className: "text-destructive shadow-none hover:text-destructive", onClick: onDelete, children: [_jsx(Trash2, { "aria-hidden": true }), intl.formatMessage(messages.delete)] })) : null, _jsx(Separator, { orientation: "vertical", className: "h-6" }), _jsxs(Button, { variant: "ghost", size: "sm", className: "shadow-none", onClick: onClear, children: [_jsx(X, { "aria-hidden": true }), intl.formatMessage(messages.clear)] })] })] }));
29
+ return (_jsxs(SelectionBar, { label: intl.formatMessage(messages.label), clearLabel: intl.formatMessage(messages.clear), onClear: onClear, count: _jsxs(_Fragment, { children: [_jsx("span", { className: "font-medium", children: intl.formatMessage(messages.count, { count }) }), total !== undefined ? (_jsx("span", { className: "text-xs text-muted-foreground", children: intl.formatMessage(messages.of, {
30
+ total: intl.formatNumber(total)
31
+ }) })) : null] }), children: [canUpdate ? (_jsxs(Button, { variant: "ghost", size: "sm", onClick: onMove, children: [_jsx(FolderInput, { "aria-hidden": true }), intl.formatMessage(messages.move)] })) : null, _jsxs(Button, { variant: "ghost", size: "sm", onClick: onDownload, children: [_jsx(Download, { "aria-hidden": true }), intl.formatMessage(messages.download)] }), canCreate ? (_jsxs(Button, { variant: "ghost", size: "sm", onClick: onDuplicate, children: [_jsx(Copy, { "aria-hidden": true }), intl.formatMessage(messages.duplicate)] })) : null, canDelete ? (_jsxs(_Fragment, { children: [_jsx(SelectionBarDivider, {}), _jsxs(Button, { variant: "ghost", size: "sm", className: "text-destructive hover:text-destructive", onClick: onDelete, children: [_jsx(Trash2, { "aria-hidden": true }), intl.formatMessage(messages.delete)] })] })) : null] }));
26
32
  }
@@ -2,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 detail drawer) — falling back to the
5
+ * `preview` when `size="preview"` (the details panel) — falling back to the
6
6
  * original when no derivative exists (e.g. SVG). A deterministic gradient tile
7
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. Video/audio get a play affordance and a duration pill.
10
- * Sized entirely by the parent via `className` (aspect + radius), so it serves
11
- * tiles, list rows, and the drawer.
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
- /** Which derivative to prefer — the grid uses `thumb`, the drawer `preview`. */
18
+ /** Text under a non-image's glyph, e.g. `PDF`. Ignored for images. */
19
+ label?: string | null;
20
+ /** Which derivative to prefer — the grid uses `thumb`, the panel `preview`. */
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;;;;;;;;;;GAUG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,SAAS,EACT,aAAa,EACb,IAAc,EACjB,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gFAAgF;IAChF,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC9B,+BA4CA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaThumbnail/index.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAMzD;;;;;;;;;;;GAWG;AACH,wBAAgB,cAAc,CAAC,EAC3B,KAAK,EACL,SAAS,EACT,aAAa,EACb,KAAK,EACL,IAAc,EACjB,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,+EAA+E;IAC/E,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;CAC9B,+BAsDA"}
@@ -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 detail drawer) — falling back to 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. Video/audio get a play affordance and a duration pill.
17
- * Sized entirely by the parent via `className` (aspect + radius), so it serves
18
- * tiles, list rows, and the drawer.
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('relative size-8 text-white/90 drop-shadow-sm', iconClassName) })), isPlayable ? (_jsxs("span", { className: "absolute bottom-1.5 right-1.5 inline-flex items-center gap-1 rounded-md bg-black/55 px-1.5 py-0.5 text-[11px] font-medium text-white", children: [_jsx(Play, { className: "size-3 fill-current", "aria-hidden": true }), asset.duration ? formatDuration(asset.duration) : null] })) : null] }));
28
+ return (_jsxs("div", { className: cn('relative flex items-center justify-center overflow-hidden bg-muted', className), style: { backgroundImage: assetGradient(asset.id) }, children: [_jsx("div", { className: "absolute inset-0 bg-black/5", "aria-hidden": true }), isImage ? (_jsx(FadeInImage, { src: imageSrc, alt: asset.alt ?? '', loading: "lazy", className: "absolute inset-0 size-full object-cover" })) : (_jsxs("span", { className: "relative flex flex-col items-center gap-1.5", children: [_jsx(MediaKindIcon, { kind: asset.kind, className: cn('size-8 text-white/90 drop-shadow-sm', iconClassName) }), label ? (_jsx("span", { className: "text-xs font-semibold tracking-wide text-white drop-shadow-sm", "aria-hidden": true, children: label })) : null] })), isPlayable ? (_jsxs("span", { className: "absolute bottom-1.5 right-1.5 inline-flex items-center gap-1 rounded-md bg-black/55 px-1.5 py-0.5 text-[11px] font-medium text-white", children: [_jsx(Play, { className: "size-3 fill-current", "aria-hidden": true }), asset.duration ? formatDuration(asset.duration) : null] })) : null] }));
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 — a search box, a type filter, a sort select,
5
- * and (permission-gated) New folder + Upload actions. Fully controlled: every
6
- * value lives in the page's store and changes dispatch straight back up.
4
+ * The Media Library's control bar — the **type chips** (All · Images · Video ·
5
+ * Audio · Documents · Archives, one pressed at a time; the same pill the
6
+ * records table's status chips are) on the left, then a search box, a sort
7
+ * select and the Grid / List switch on the right. Fully controlled: every value
8
+ * lives in the page and changes dispatch straight back up. New folder and
9
+ * Upload are the page's top-bar actions, not this bar's.
7
10
  */
8
- export declare function MediaToolbar({ search, onSearchChange, kindFilter, onKindFilterChange, sort, onSortChange, onNewFolder, onUpload, canCreate }: {
11
+ export declare function MediaToolbar({ search, onSearchChange, kindFilter, onKindFilterChange, sort, onSortChange, view, onViewChange }: {
9
12
  search: string;
10
13
  onSearchChange: (value: string) => void;
11
14
  kindFilter: KindFilter;
12
15
  onKindFilterChange: (value: KindFilter) => void;
13
16
  sort: MediaSort;
14
17
  onSortChange: (value: MediaSort) => void;
15
- onNewFolder: () => void;
16
- onUpload: () => void;
17
- canCreate: boolean;
18
+ view: MediaView;
19
+ onViewChange: (value: MediaView) => void;
18
20
  }): import("react").JSX.Element;
19
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaToolbar/index.tsx"],"names":[],"mappings":"AAcA,OAAO,EAIH,KAAK,SAAS,EACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAmD9D;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,EACzB,MAAM,EACN,cAAc,EACd,UAAU,EACV,kBAAkB,EAClB,IAAI,EACJ,YAAY,EACZ,WAAW,EACX,QAAQ,EACR,SAAS,EACZ,EAAE;IACC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,UAAU,EAAE,UAAU,CAAC;IACvB,kBAAkB,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAChD,IAAI,EAAE,SAAS,CAAC;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACzC,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;CACtB,+BA8GA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaToolbar/index.tsx"],"names":[],"mappings":"AAgBA,OAAO,EAKH,KAAK,SAAS,EACd,KAAK,SAAS,EACjB,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AA8D9D;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,EACzB,MAAM,EACN,cAAc,EACd,UAAU,EACV,kBAAkB,EAClB,IAAI,EACJ,YAAY,EACZ,IAAI,EACJ,YAAY,EACf,EAAE;IACC,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,UAAU,EAAE,UAAU,CAAC;IACvB,kBAAkB,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAChD,IAAI,EAAE,SAAS,CAAC;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;IACzC,IAAI,EAAE,SAAS,CAAC;IAChB,YAAY,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,CAAC;CAC5C,+BAiHA"}
@@ -1,8 +1,8 @@
1
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { defineMessages, useIntl } from 'react-intl';
3
- import { Button, DensityProvider, InputGroup, InputGroupAddon, InputGroupInput, Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@orthacms/design-system';
4
- import { FolderPlus, Search, UploadCloud } from 'lucide-react';
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 types' },
17
+ kindAll: { id: 'media.toolbar.kindAll', defaultMessage: 'All' },
18
18
  kindImage: { id: 'media.toolbar.kindImage', defaultMessage: 'Images' },
19
- kindVideo: { id: 'media.toolbar.kindVideo', defaultMessage: 'Videos' },
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
- newFolder: { id: 'media.toolbar.newFolder', defaultMessage: 'New folder' },
52
- upload: { id: 'media.toolbar.upload', defaultMessage: 'Upload' }
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 — a search box, a type filter, a sort select,
56
- * and (permission-gated) New folder + Upload actions. Fully controlled: every
57
- * value lives in the page's store and changes dispatch straight back up.
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, onNewFolder, onUpload, canCreate }) {
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: [_jsxs(InputGroup, { className: "w-full shadow-none sm:w-auto sm:max-w-[280px] sm:flex-1", 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: kindFilter, onValueChange: (value) => onKindFilterChange(value), children: [_jsx(SelectTrigger, { className: "w-[150px] shadow-none", "aria-label": intl.formatMessage(messages.kindLabel), children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: KIND_FILTER_ALL, children: intl.formatMessage(messages.kindAll) }), _jsx(SelectItem, { value: MEDIA_KIND.Image, children: intl.formatMessage(messages.kindImage) }), _jsx(SelectItem, { value: MEDIA_KIND.Video, children: intl.formatMessage(messages.kindVideo) }), _jsx(SelectItem, { value: MEDIA_KIND.Audio, children: intl.formatMessage(messages.kindAudio) }), _jsx(SelectItem, { value: MEDIA_KIND.Document, children: intl.formatMessage(messages.kindDocument) }), _jsx(SelectItem, { value: MEDIA_KIND.Archive, children: intl.formatMessage(messages.kindArchive) })] })] }), _jsxs(Select, { value: sort, onValueChange: (value) => onSortChange(value), children: [_jsx(SelectTrigger, { className: "w-[150px] 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) })] })] }), _jsx("div", { className: "ml-auto flex items-center gap-2 sm:gap-3", children: canCreate ? (_jsxs(_Fragment, { children: [_jsxs(Button, { variant: "outline", className: "shadow-none", onClick: onNewFolder, children: [_jsx(FolderPlus, { "aria-hidden": true }), _jsx("span", { className: "hidden sm:inline", children: intl.formatMessage(messages.newFolder) })] }), _jsxs(Button, { onClick: onUpload, children: [_jsx(UploadCloud, { "aria-hidden": true }), intl.formatMessage(messages.upload)] })] })) : null })] }) }));
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":"AAaA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAQ3D;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,EACxB,WAAW,EACX,UAAU,EACb,EAAE;IACC,gFAAgF;IAChF,WAAW,EAAE,WAAW,EAAE,CAAC;IAC3B,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CAC1C,+BA4CA"}
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
  }