@orthacms/media-admin 0.10.3 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/components/AssetDetailPanel/MetaRow/index.d.ts +11 -0
- package/dist/lib/components/AssetDetailPanel/MetaRow/index.d.ts.map +1 -0
- package/dist/lib/components/AssetDetailPanel/MetaRow/index.js +9 -0
- package/dist/lib/components/AssetDetailPanel/index.d.ts +35 -0
- package/dist/lib/components/AssetDetailPanel/index.d.ts.map +1 -0
- package/dist/lib/components/AssetDetailPanel/index.js +158 -0
- package/dist/lib/components/FadeInImage/index.d.ts +24 -0
- package/dist/lib/components/FadeInImage/index.d.ts.map +1 -0
- package/dist/lib/components/FadeInImage/index.js +34 -0
- package/dist/lib/components/FolderActionsMenu/index.d.ts +3 -1
- package/dist/lib/components/FolderActionsMenu/index.d.ts.map +1 -1
- package/dist/lib/components/FolderActionsMenu/index.js +3 -3
- package/dist/lib/components/MediaAssetTable/index.d.ts +25 -0
- package/dist/lib/components/MediaAssetTable/index.d.ts.map +1 -0
- package/dist/lib/components/MediaAssetTable/index.js +62 -0
- package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.d.ts.map +1 -1
- package/dist/lib/components/MediaFieldControl/MediaFieldItem/index.js +2 -1
- package/dist/lib/components/MediaFieldControl/index.d.ts.map +1 -1
- package/dist/lib/components/MediaFieldControl/index.js +19 -14
- package/dist/lib/components/MediaFieldInput/index.d.ts +19 -0
- package/dist/lib/components/MediaFieldInput/index.d.ts.map +1 -0
- package/dist/lib/components/MediaFieldInput/index.js +23 -0
- package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts +17 -3
- package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts.map +1 -1
- package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.js +16 -6
- package/dist/lib/components/MediaFoldersNav/index.d.ts +10 -5
- package/dist/lib/components/MediaFoldersNav/index.d.ts.map +1 -1
- package/dist/lib/components/MediaFoldersNav/index.js +17 -9
- package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts +15 -4
- package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts.map +1 -1
- package/dist/lib/components/MediaGrid/MediaAssetCard/index.js +27 -11
- package/dist/lib/components/MediaGrid/index.d.ts +10 -10
- package/dist/lib/components/MediaGrid/index.d.ts.map +1 -1
- package/dist/lib/components/MediaGrid/index.js +13 -15
- package/dist/lib/components/MediaSelectionBar/index.d.ts +9 -5
- package/dist/lib/components/MediaSelectionBar/index.d.ts.map +1 -1
- package/dist/lib/components/MediaSelectionBar/index.js +16 -10
- package/dist/lib/components/MediaThumbnail/index.d.ts +11 -7
- package/dist/lib/components/MediaThumbnail/index.d.ts.map +1 -1
- package/dist/lib/components/MediaThumbnail/index.js +10 -7
- package/dist/lib/components/MediaToolbar/index.d.ts +10 -8
- package/dist/lib/components/MediaToolbar/index.d.ts.map +1 -1
- package/dist/lib/components/MediaToolbar/index.js +37 -13
- package/dist/lib/components/MediaTopBar/index.d.ts +7 -1
- package/dist/lib/components/MediaTopBar/index.d.ts.map +1 -1
- package/dist/lib/components/MediaTopBar/index.js +6 -3
- package/dist/lib/components/MediaUploadBanner/index.d.ts +7 -5
- package/dist/lib/components/MediaUploadBanner/index.d.ts.map +1 -1
- package/dist/lib/components/MediaUploadBanner/index.js +15 -8
- package/dist/lib/components/UploadDialog/StagedFileRow/index.d.ts.map +1 -1
- package/dist/lib/components/UploadDialog/StagedFileRow/index.js +2 -1
- package/dist/lib/constants/index.d.ts +26 -0
- package/dist/lib/constants/index.d.ts.map +1 -1
- package/dist/lib/constants/index.js +24 -0
- package/dist/lib/hooks/useUploadQueue/index.d.ts +2 -0
- package/dist/lib/hooks/useUploadQueue/index.d.ts.map +1 -1
- package/dist/lib/hooks/useUploadQueue/index.js +15 -1
- package/dist/lib/pages/MediaLibraryPage/index.d.ts +6 -4
- package/dist/lib/pages/MediaLibraryPage/index.d.ts.map +1 -1
- package/dist/lib/pages/MediaLibraryPage/index.js +86 -38
- package/dist/lib/utils/fileExtension/index.d.ts +9 -0
- package/dist/lib/utils/fileExtension/index.d.ts.map +1 -0
- package/dist/lib/utils/fileExtension/index.js +14 -0
- package/dist/lib/utils/mediaPlugin/index.d.ts.map +1 -1
- package/dist/lib/utils/mediaPlugin/index.js +19 -24
- package/package.json +10 -10
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* One label/value line in the details panel’s metadata list — a muted term on
|
|
4
|
+
* the left and its value on the right, aligned as a `dt`/`dd` pair. Purely
|
|
5
|
+
* presentational; the panel supplies localized labels and formatted values.
|
|
6
|
+
*/
|
|
7
|
+
export declare function MetaRow({ label, children }: {
|
|
8
|
+
label: ReactNode;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
}): import("react").JSX.Element;
|
|
11
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/AssetDetailPanel/MetaRow/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,EACpB,KAAK,EACL,QAAQ,EACX,EAAE;IACC,KAAK,EAAE,SAAS,CAAC;IACjB,QAAQ,EAAE,SAAS,CAAC;CACvB,+BASA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* One label/value line in the details panel’s metadata list — a muted term on
|
|
4
|
+
* the left and its value on the right, aligned as a `dt`/`dd` pair. Purely
|
|
5
|
+
* presentational; the panel supplies localized labels and formatted values.
|
|
6
|
+
*/
|
|
7
|
+
export function MetaRow({ label, children }) {
|
|
8
|
+
return (_jsxs("div", { className: "flex items-start justify-between gap-4 py-2", children: [_jsx("dt", { className: "shrink-0 text-sm text-muted-foreground", children: label }), _jsx("dd", { className: "min-w-0 truncate text-right text-sm font-medium", children: children })] }));
|
|
9
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { MediaAsset } from '../../types/mediaAsset';
|
|
2
|
+
import { type AssetActionKind } from '../AssetActionsMenu';
|
|
3
|
+
/**
|
|
4
|
+
* The asset details panel — a **non-modal** column beside the grid, not a
|
|
5
|
+
* drawer over it: the files stay visible and clickable while it is open, and
|
|
6
|
+
* clicking another one simply shows that file here. Under `lg` the page lays
|
|
7
|
+
* it over the browser instead (see the page), still without a focus trap.
|
|
8
|
+
*
|
|
9
|
+
* Its header names the file and carries Previous / Next (also ← / →, when focus
|
|
10
|
+
* isn't in a field or a menu) and Close (also Esc). Below: a large preview, the
|
|
11
|
+
* primary Download / Copy link actions and the shared ⋯ menu, the alt text —
|
|
12
|
+
* **editable here** with `media:update`, the only route to an image's
|
|
13
|
+
* description after upload — then the metadata list and tags.
|
|
14
|
+
*
|
|
15
|
+
* Opening moves focus to the panel (it is a labelled region); closing hands it
|
|
16
|
+
* back to whatever opened it, when that control is still on the page.
|
|
17
|
+
*/
|
|
18
|
+
export declare function AssetDetailPanel({ asset, locationLabel, onClose, onPrevious, onNext, onAction, onSaveAlt, canCreate, canUpdate, canDelete, className }: {
|
|
19
|
+
asset: MediaAsset;
|
|
20
|
+
/** Human name of the folder the asset lives in. */
|
|
21
|
+
locationLabel: string;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
/** Shows the previous file; omitted on the first one. */
|
|
24
|
+
onPrevious?: () => void;
|
|
25
|
+
/** Shows the next file; omitted on the last one. */
|
|
26
|
+
onNext?: () => void;
|
|
27
|
+
onAction: (kind: AssetActionKind, asset: MediaAsset) => void;
|
|
28
|
+
/** Persists the alt text; resolves whether the server accepted it. */
|
|
29
|
+
onSaveAlt?: (id: string, alt: string) => Promise<boolean>;
|
|
30
|
+
canCreate: boolean;
|
|
31
|
+
canUpdate: boolean;
|
|
32
|
+
canDelete: boolean;
|
|
33
|
+
className?: string;
|
|
34
|
+
}): import("react").JSX.Element;
|
|
35
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/AssetDetailPanel/index.tsx"],"names":[],"mappings":"AAkBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAIzD,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAkE7E;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,gBAAgB,CAAC,EAC7B,KAAK,EACL,aAAa,EACb,OAAO,EACP,UAAU,EACV,MAAM,EACN,QAAQ,EACR,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,KAAK,EAAE,UAAU,CAAC;IAClB,mDAAmD;IACnD,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,yDAAyD;IACzD,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,oDAAoD;IACpD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,KAAK,IAAI,CAAC;IAC7D,sEAAsE;IACtE,SAAS,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1D,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,+BA+RA"}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
3
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
4
|
+
import { Badge, Button, InputField, Separator, cn } from '@orthacms/design-system';
|
|
5
|
+
import { ChevronLeft, ChevronRight, Download, Link2, MoreVertical, X } from 'lucide-react';
|
|
6
|
+
import { MEDIA_KIND } from '../../constants';
|
|
7
|
+
import { formatBytes } from '../../utils/formatBytes';
|
|
8
|
+
import { formatDuration } from '../../utils/formatDuration';
|
|
9
|
+
import { buildAssetHandlers } from '../../utils/assetHandlers';
|
|
10
|
+
import { AssetActionsMenu } from '../AssetActionsMenu';
|
|
11
|
+
import { MediaKindBadge } from '../MediaKindBadge';
|
|
12
|
+
import { MediaThumbnail } from '../MediaThumbnail';
|
|
13
|
+
import { MetaRow } from './MetaRow';
|
|
14
|
+
/** Intl descriptors for {@link AssetDetailPanel}, co-located. */
|
|
15
|
+
const messages = defineMessages({
|
|
16
|
+
close: { id: 'media.detail.close', defaultMessage: 'Close' },
|
|
17
|
+
previous: { id: 'media.detail.previous', defaultMessage: 'Previous file' },
|
|
18
|
+
next: { id: 'media.detail.next', defaultMessage: 'Next file' },
|
|
19
|
+
download: { id: 'media.detail.download', defaultMessage: 'Download' },
|
|
20
|
+
copyLink: { id: 'media.detail.copyLink', defaultMessage: 'Copy link' },
|
|
21
|
+
more: { id: 'media.detail.more', defaultMessage: 'More actions' },
|
|
22
|
+
details: { id: 'media.detail.details', defaultMessage: 'Details' },
|
|
23
|
+
type: { id: 'media.detail.type', defaultMessage: 'Type' },
|
|
24
|
+
size: { id: 'media.detail.size', defaultMessage: 'Size' },
|
|
25
|
+
dimensions: { id: 'media.detail.dimensions', defaultMessage: 'Dimensions' },
|
|
26
|
+
duration: { id: 'media.detail.duration', defaultMessage: 'Duration' },
|
|
27
|
+
format: { id: 'media.detail.format', defaultMessage: 'Format' },
|
|
28
|
+
location: { id: 'media.detail.location', defaultMessage: 'Folder' },
|
|
29
|
+
uploadedBy: {
|
|
30
|
+
id: 'media.detail.uploadedBy',
|
|
31
|
+
defaultMessage: 'Uploaded by'
|
|
32
|
+
},
|
|
33
|
+
created: { id: 'media.detail.created', defaultMessage: 'Created' },
|
|
34
|
+
modified: { id: 'media.detail.modified', defaultMessage: 'Modified' },
|
|
35
|
+
altText: { id: 'media.detail.altText', defaultMessage: 'Alt text' },
|
|
36
|
+
altMissingBadge: {
|
|
37
|
+
id: 'media.detail.altMissingBadge',
|
|
38
|
+
defaultMessage: 'Missing'
|
|
39
|
+
},
|
|
40
|
+
altPlaceholder: {
|
|
41
|
+
id: 'media.detail.altPlaceholder',
|
|
42
|
+
defaultMessage: 'Describe this image'
|
|
43
|
+
},
|
|
44
|
+
altHint: {
|
|
45
|
+
id: 'media.detail.altHint',
|
|
46
|
+
defaultMessage: 'Read aloud in place of the image. Leave blank only if it is purely decorative.'
|
|
47
|
+
},
|
|
48
|
+
altSave: { id: 'media.detail.altSave', defaultMessage: 'Save alt text' },
|
|
49
|
+
altMissing: {
|
|
50
|
+
id: 'media.detail.altMissing',
|
|
51
|
+
defaultMessage: 'No alt text yet'
|
|
52
|
+
},
|
|
53
|
+
tags: { id: 'media.detail.tags', defaultMessage: 'Tags' },
|
|
54
|
+
keys: {
|
|
55
|
+
id: 'media.detail.keys',
|
|
56
|
+
defaultMessage: '← → move between files · Esc closes the panel'
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* Whether a key press belongs to whatever has focus rather than to the panel:
|
|
61
|
+
* typing in a field, a select or combobox, or anything inside an open menu or
|
|
62
|
+
* dialog (whose own Escape must close *it*, not the panel underneath).
|
|
63
|
+
*/
|
|
64
|
+
function ownedByFocus(target) {
|
|
65
|
+
if (!(target instanceof HTMLElement))
|
|
66
|
+
return false;
|
|
67
|
+
if (target.isContentEditable)
|
|
68
|
+
return true;
|
|
69
|
+
if (/^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName))
|
|
70
|
+
return true;
|
|
71
|
+
return !!target.closest('[role="menu"],[role="dialog"],[role="alertdialog"],[role="listbox"],[role="combobox"]');
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The asset details panel — a **non-modal** column beside the grid, not a
|
|
75
|
+
* drawer over it: the files stay visible and clickable while it is open, and
|
|
76
|
+
* clicking another one simply shows that file here. Under `lg` the page lays
|
|
77
|
+
* it over the browser instead (see the page), still without a focus trap.
|
|
78
|
+
*
|
|
79
|
+
* Its header names the file and carries Previous / Next (also ← / →, when focus
|
|
80
|
+
* isn't in a field or a menu) and Close (also Esc). Below: a large preview, the
|
|
81
|
+
* primary Download / Copy link actions and the shared ⋯ menu, the alt text —
|
|
82
|
+
* **editable here** with `media:update`, the only route to an image's
|
|
83
|
+
* description after upload — then the metadata list and tags.
|
|
84
|
+
*
|
|
85
|
+
* Opening moves focus to the panel (it is a labelled region); closing hands it
|
|
86
|
+
* back to whatever opened it, when that control is still on the page.
|
|
87
|
+
*/
|
|
88
|
+
export function AssetDetailPanel({ asset, locationLabel, onClose, onPrevious, onNext, onAction, onSaveAlt, canCreate, canUpdate, canDelete, className }) {
|
|
89
|
+
const intl = useIntl();
|
|
90
|
+
const titleId = useId();
|
|
91
|
+
const altId = useId();
|
|
92
|
+
const panelRef = useRef(null);
|
|
93
|
+
const [alt, setAlt] = useState('');
|
|
94
|
+
const [saving, setSaving] = useState(false);
|
|
95
|
+
// Re-seed whenever a different asset opens, so the field never shows the
|
|
96
|
+
// previous asset's description over this one's.
|
|
97
|
+
const assetAlt = asset.alt ?? '';
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
setAlt(assetAlt);
|
|
100
|
+
}, [asset.id, assetAlt]);
|
|
101
|
+
// Focus in on open, and back out on close. Mount-only on purpose: moving
|
|
102
|
+
// between files keeps the panel mounted and focus where the reader put it.
|
|
103
|
+
useEffect(() => {
|
|
104
|
+
const opener = document.activeElement instanceof HTMLElement
|
|
105
|
+
? document.activeElement
|
|
106
|
+
: null;
|
|
107
|
+
panelRef.current?.focus();
|
|
108
|
+
return () => {
|
|
109
|
+
if (opener?.isConnected)
|
|
110
|
+
opener.focus();
|
|
111
|
+
};
|
|
112
|
+
}, []);
|
|
113
|
+
// ← / → / Esc from anywhere on the page, unless focus owns the key.
|
|
114
|
+
const keys = useRef({ onPrevious, onNext, onClose });
|
|
115
|
+
keys.current = { onPrevious, onNext, onClose };
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const onKeyDown = (event) => {
|
|
118
|
+
if (event.defaultPrevented || event.altKey || event.metaKey)
|
|
119
|
+
return;
|
|
120
|
+
if (event.ctrlKey || ownedByFocus(event.target))
|
|
121
|
+
return;
|
|
122
|
+
const { onPrevious: prev, onNext: next, onClose: close } = keys.current;
|
|
123
|
+
if (event.key === 'ArrowLeft' && prev)
|
|
124
|
+
prev();
|
|
125
|
+
else if (event.key === 'ArrowRight' && next)
|
|
126
|
+
next();
|
|
127
|
+
else if (event.key === 'Escape')
|
|
128
|
+
close();
|
|
129
|
+
else
|
|
130
|
+
return;
|
|
131
|
+
event.preventDefault();
|
|
132
|
+
};
|
|
133
|
+
window.addEventListener('keydown', onKeyDown);
|
|
134
|
+
return () => window.removeEventListener('keydown', onKeyDown);
|
|
135
|
+
}, []);
|
|
136
|
+
const isImage = asset.kind === MEDIA_KIND.Image;
|
|
137
|
+
const canEditAlt = canUpdate && !!onSaveAlt && isImage;
|
|
138
|
+
const altDirty = alt !== (asset.alt ?? '');
|
|
139
|
+
const missingAlt = isImage && !asset.alt?.trim();
|
|
140
|
+
const saveAlt = () => {
|
|
141
|
+
if (!onSaveAlt)
|
|
142
|
+
return;
|
|
143
|
+
setSaving(true);
|
|
144
|
+
void onSaveAlt(asset.id, alt).finally(() => setSaving(false));
|
|
145
|
+
};
|
|
146
|
+
const dateOpts = {
|
|
147
|
+
year: 'numeric',
|
|
148
|
+
month: 'short',
|
|
149
|
+
day: 'numeric',
|
|
150
|
+
hour: '2-digit',
|
|
151
|
+
minute: '2-digit'
|
|
152
|
+
};
|
|
153
|
+
const iconButton = 'size-7 shrink-0 shadow-none';
|
|
154
|
+
return (_jsxs("section", { ref: panelRef, tabIndex: -1, "aria-labelledby": titleId, className: cn('flex min-h-0 flex-col bg-background focus-visible:outline-none', className), children: [_jsxs("div", { className: "flex h-12 shrink-0 items-center gap-1 border-b px-4", children: [_jsx("h2", { id: titleId, className: "min-w-0 flex-1 truncate text-sm font-semibold", title: asset.name, children: asset.name }), _jsx(Button, { variant: "ghost", size: "icon", className: iconButton, disabled: !onPrevious, onClick: onPrevious, "aria-label": intl.formatMessage(messages.previous), children: _jsx(ChevronLeft, { "aria-hidden": true }) }), _jsx(Button, { variant: "ghost", size: "icon", className: iconButton, disabled: !onNext, onClick: onNext, "aria-label": intl.formatMessage(messages.next), children: _jsx(ChevronRight, { "aria-hidden": true }) }), _jsx(Button, { variant: "ghost", size: "icon", className: iconButton, onClick: onClose, "aria-label": intl.formatMessage(messages.close), children: _jsx(X, { "aria-hidden": true }) })] }), _jsxs("div", { className: "min-h-0 flex-1 space-y-5 overflow-auto p-4", children: [_jsx(MediaThumbnail, { asset: asset, size: "preview", className: "aspect-[4/3] w-full rounded-lg border", iconClassName: "size-12" }), _jsxs("div", { className: "flex items-center gap-2", children: [_jsxs(Button, { className: "flex-1", onClick: () => onAction('download', asset), children: [_jsx(Download, { "aria-hidden": true }), intl.formatMessage(messages.download)] }), _jsxs(Button, { variant: "outline", className: "flex-1 shadow-none", onClick: () => onAction('copyLink', asset), children: [_jsx(Link2, { "aria-hidden": true }), intl.formatMessage(messages.copyLink)] }), _jsx(AssetActionsMenu, { handlers: buildAssetHandlers(asset, onAction), canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete, trigger: _jsx(Button, { variant: "outline", size: "icon", className: "shadow-none", "aria-label": intl.formatMessage(messages.more), children: _jsx(MoreVertical, { "aria-hidden": true }) }) })] }), isImage ? (canEditAlt ? (_jsxs("div", { className: "space-y-2", children: [_jsx(InputField, { id: `asset-alt-${altId}`, label: intl.formatMessage(messages.altText), labelAction: missingAlt ? (_jsx(Badge, { variant: "warning", size: "xs", children: intl.formatMessage(messages.altMissingBadge) })) : undefined, description: intl.formatMessage(messages.altHint), placeholder: intl.formatMessage(messages.altPlaceholder), value: alt, maxLength: 1000, onChange: (event) => setAlt(event.target.value) }), _jsx(Button, { size: "sm", variant: "outline", className: "shadow-none", disabled: !altDirty || saving, onClick: saveAlt, children: intl.formatMessage(messages.altSave) })] })) : (_jsxs("div", { children: [_jsx("h3", { className: "mb-1 text-sm font-medium", children: intl.formatMessage(messages.altText) }), _jsx("p", { className: asset.alt
|
|
155
|
+
? 'text-sm'
|
|
156
|
+
: 'text-sm text-muted-foreground', children: asset.alt ??
|
|
157
|
+
intl.formatMessage(messages.altMissing) })] }))) : null, _jsxs("div", { children: [_jsx("h3", { className: "mb-1 text-sm font-medium", children: intl.formatMessage(messages.details) }), _jsxs("dl", { className: "divide-y", children: [_jsx(MetaRow, { label: intl.formatMessage(messages.type), children: _jsx(MediaKindBadge, { kind: asset.kind }) }), _jsx(MetaRow, { label: intl.formatMessage(messages.size), children: formatBytes(asset.size) }), asset.dimensions ? (_jsxs(MetaRow, { label: intl.formatMessage(messages.dimensions), children: [asset.dimensions.width, " \u00D7", ' ', asset.dimensions.height] })) : null, asset.duration ? (_jsx(MetaRow, { label: intl.formatMessage(messages.duration), children: formatDuration(asset.duration) })) : null, _jsx(MetaRow, { label: intl.formatMessage(messages.format), children: asset.mimeType }), _jsx(MetaRow, { label: intl.formatMessage(messages.location), children: locationLabel }), _jsx(MetaRow, { label: intl.formatMessage(messages.uploadedBy), children: asset.uploadedBy }), _jsx(MetaRow, { label: intl.formatMessage(messages.created), children: intl.formatDate(asset.createdAt, dateOpts) }), _jsx(MetaRow, { label: intl.formatMessage(messages.modified), children: intl.formatDate(asset.updatedAt, dateOpts) })] })] }), asset.tags.length > 0 ? (_jsxs("div", { children: [_jsx(Separator, { className: "mb-3" }), _jsx("h3", { className: "mb-2 text-sm font-medium", children: intl.formatMessage(messages.tags) }), _jsx("div", { className: "flex flex-wrap gap-1.5", children: asset.tags.map((tag) => (_jsx(Badge, { variant: "outline", className: "font-normal", children: tag }, tag))) })] })) : null, _jsx("p", { className: "hidden text-center text-xs text-muted-foreground lg:block", children: intl.formatMessage(messages.keys) })] })] }));
|
|
158
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ImgHTMLAttributes } from 'react';
|
|
2
|
+
/** Props for {@link FadeInImage} — a plain `<img>`'s, `src` required. */
|
|
3
|
+
export type FadeInImageProps = ImgHTMLAttributes<HTMLImageElement> & {
|
|
4
|
+
src: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* An `<img>` that fades in once its bytes have arrived, instead of painting
|
|
8
|
+
* top-down over the tile. Whatever sits behind it (the tile's gradient or
|
|
9
|
+
* muted fill) is the placeholder, so nothing shifts when it lands.
|
|
10
|
+
*
|
|
11
|
+
* Two cases would otherwise leave it invisible for good:
|
|
12
|
+
*
|
|
13
|
+
* - **A cached image** can finish before React attaches `onLoad`, so the event
|
|
14
|
+
* never reaches us. It is already `complete` on mount, which the layout
|
|
15
|
+
* effect reads before the first paint — no fade, no flash.
|
|
16
|
+
* - **A broken image** never fires `load`. `error` reveals it too: the alt text
|
|
17
|
+
* (and the browser's broken-image glyph) is information, an empty tile is not.
|
|
18
|
+
*
|
|
19
|
+
* Visibility is keyed on the `src` that settled, so a tile reused for a
|
|
20
|
+
* different asset starts hidden again rather than showing the new image
|
|
21
|
+
* half-drawn. Reduced motion skips the fade, not the image.
|
|
22
|
+
*/
|
|
23
|
+
export declare function FadeInImage({ src, className, onLoad, onError, alt, ...props }: FadeInImageProps): import("react").JSX.Element;
|
|
24
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/FadeInImage/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAkB,MAAM,OAAO,CAAC;AAG/D,yEAAyE;AACzE,MAAM,MAAM,gBAAgB,GAAG,iBAAiB,CAAC,gBAAgB,CAAC,GAAG;IACjE,GAAG,EAAE,MAAM,CAAC;CACf,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,WAAW,CAAC,EACxB,GAAG,EACH,SAAS,EACT,MAAM,EACN,OAAO,EACP,GAAG,EACH,GAAG,KAAK,EACX,EAAE,gBAAgB,+BAiClB"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useLayoutEffect, useRef, useState } from 'react';
|
|
3
|
+
import { cn } from '@orthacms/design-system';
|
|
4
|
+
/**
|
|
5
|
+
* An `<img>` that fades in once its bytes have arrived, instead of painting
|
|
6
|
+
* top-down over the tile. Whatever sits behind it (the tile's gradient or
|
|
7
|
+
* muted fill) is the placeholder, so nothing shifts when it lands.
|
|
8
|
+
*
|
|
9
|
+
* Two cases would otherwise leave it invisible for good:
|
|
10
|
+
*
|
|
11
|
+
* - **A cached image** can finish before React attaches `onLoad`, so the event
|
|
12
|
+
* never reaches us. It is already `complete` on mount, which the layout
|
|
13
|
+
* effect reads before the first paint — no fade, no flash.
|
|
14
|
+
* - **A broken image** never fires `load`. `error` reveals it too: the alt text
|
|
15
|
+
* (and the browser's broken-image glyph) is information, an empty tile is not.
|
|
16
|
+
*
|
|
17
|
+
* Visibility is keyed on the `src` that settled, so a tile reused for a
|
|
18
|
+
* different asset starts hidden again rather than showing the new image
|
|
19
|
+
* half-drawn. Reduced motion skips the fade, not the image.
|
|
20
|
+
*/
|
|
21
|
+
export function FadeInImage({ src, className, onLoad, onError, alt, ...props }) {
|
|
22
|
+
const ref = useRef(null);
|
|
23
|
+
const [settledSrc, setSettledSrc] = useState(null);
|
|
24
|
+
useLayoutEffect(() => {
|
|
25
|
+
if (ref.current?.complete)
|
|
26
|
+
setSettledSrc(src);
|
|
27
|
+
}, [src]);
|
|
28
|
+
const settle = (handler) => (event) => {
|
|
29
|
+
setSettledSrc(src);
|
|
30
|
+
handler?.(event);
|
|
31
|
+
};
|
|
32
|
+
const visible = settledSrc === src;
|
|
33
|
+
return (_jsx("img", { ...props, ref: ref, src: src, alt: alt, onLoad: settle(onLoad), onError: settle(onError), "data-loaded": visible ? 'true' : 'false', className: cn('transition-opacity duration-(--ds-duration-base) ease-(--ds-ease-out) motion-reduce:transition-none', visible ? 'opacity-100' : 'opacity-0', className) }));
|
|
34
|
+
}
|
|
@@ -4,7 +4,7 @@ import type { ReactNode } from 'react';
|
|
|
4
4
|
* removes the folder and everything inside). Rename/Delete are hidden unless the
|
|
5
5
|
* caller passes the matching permission. Pass `trigger` to override the default ⋯.
|
|
6
6
|
*/
|
|
7
|
-
export declare function FolderActionsMenu({ name, onOpen, onRename, onDelete, canUpdate, canDelete, trigger }: {
|
|
7
|
+
export declare function FolderActionsMenu({ name, onOpen, onRename, onDelete, canUpdate, canDelete, trigger, triggerClassName }: {
|
|
8
8
|
/**
|
|
9
9
|
* The folder this menu acts on — its name goes in the trigger's accessible
|
|
10
10
|
* name, so a grid of folders doesn't present a row of buttons all called
|
|
@@ -17,5 +17,7 @@ export declare function FolderActionsMenu({ name, onOpen, onRename, onDelete, ca
|
|
|
17
17
|
canUpdate: boolean;
|
|
18
18
|
canDelete: boolean;
|
|
19
19
|
trigger?: ReactNode;
|
|
20
|
+
/** Extra classes for the default ⋯ trigger (the tree's smaller one). */
|
|
21
|
+
triggerClassName?: string;
|
|
20
22
|
}): import("react").JSX.Element;
|
|
21
23
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/FolderActionsMenu/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/FolderActionsMenu/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAwBvC;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAC9B,IAAI,EACJ,MAAM,EACN,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,SAAS,EACT,OAAO,EACP,gBAAgB,EACnB,EAAE;IACC;;;;OAIG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,IAAI,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;IACnB,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,wEAAwE;IACxE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC7B,+BA6CA"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
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, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@orthacms/design-system';
|
|
3
|
+
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, cn } from '@orthacms/design-system';
|
|
4
4
|
import { FolderOpen, MoreVertical, Pencil, Trash2 } from 'lucide-react';
|
|
5
5
|
/** Intl descriptors for {@link FolderActionsMenu}, co-located. */
|
|
6
6
|
const messages = defineMessages({
|
|
@@ -17,9 +17,9 @@ const messages = defineMessages({
|
|
|
17
17
|
* removes the folder and everything inside). Rename/Delete are hidden unless the
|
|
18
18
|
* caller passes the matching permission. Pass `trigger` to override the default ⋯.
|
|
19
19
|
*/
|
|
20
|
-
export function FolderActionsMenu({ name, onOpen, onRename, onDelete, canUpdate, canDelete, trigger }) {
|
|
20
|
+
export function FolderActionsMenu({ name, onOpen, onRename, onDelete, canUpdate, canDelete, trigger, triggerClassName }) {
|
|
21
21
|
const intl = useIntl();
|
|
22
|
-
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: trigger ?? (_jsx(Button, { variant: "ghost", size: "icon", className:
|
|
22
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: trigger ?? (_jsx(Button, { variant: "ghost", size: "icon", className: cn('size-8 shadow-none', triggerClassName), "aria-label": intl.formatMessage(messages.trigger, {
|
|
23
23
|
name
|
|
24
24
|
}), children: _jsx(MoreVertical, { "aria-hidden": true }) })) }), _jsxs(DropdownMenuContent, { align: "end", className: "w-44", children: [_jsxs(DropdownMenuItem, { onSelect: onOpen, children: [_jsx(FolderOpen, { "aria-hidden": true }), intl.formatMessage(messages.open)] }), canUpdate ? (_jsxs(DropdownMenuItem, { onSelect: onRename, children: [_jsx(Pencil, { "aria-hidden": true }), intl.formatMessage(messages.rename)] })) : null, canDelete ? (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { className: "text-destructive focus:text-destructive", onSelect: onDelete, children: [_jsx(Trash2, { "aria-hidden": true }), intl.formatMessage(messages.delete)] })] })) : null] })] }));
|
|
25
25
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MediaAsset } from '../../types/mediaAsset';
|
|
2
|
+
import { type AssetActionKind } from '../AssetActionsMenu';
|
|
3
|
+
/**
|
|
4
|
+
* The list view of the open folder's files — the design-system `Table`, used
|
|
5
|
+
* exactly as every other admin table uses it (no restyled cells), one row per
|
|
6
|
+
* asset: a select checkbox, a small thumbnail beside the name (a button that
|
|
7
|
+
* opens the details panel, named "Open {name}" as the grid tile is), the
|
|
8
|
+
* type, size, dimensions or duration, uploader and date, and the shared ⋯
|
|
9
|
+
* menu. The header checkbox selects or clears every file on the page.
|
|
10
|
+
* Presentational: selection and every action dispatch up to the page.
|
|
11
|
+
*/
|
|
12
|
+
export declare function MediaAssetTable({ assets, selectedIds, activeId, onToggleSelect, onToggleAll, onAction, canCreate, canUpdate, canDelete }: {
|
|
13
|
+
assets: MediaAsset[];
|
|
14
|
+
selectedIds: Set<string>;
|
|
15
|
+
/** The asset open in the details panel, marked as the current row. */
|
|
16
|
+
activeId?: string | null;
|
|
17
|
+
onToggleSelect: (id: string) => void;
|
|
18
|
+
/** Select every row on the page, or clear them all when all are selected. */
|
|
19
|
+
onToggleAll: () => void;
|
|
20
|
+
onAction: (kind: AssetActionKind, asset: MediaAsset) => void;
|
|
21
|
+
canCreate: boolean;
|
|
22
|
+
canUpdate: boolean;
|
|
23
|
+
canDelete: boolean;
|
|
24
|
+
}): import("react").JSX.Element;
|
|
25
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaAssetTable/index.tsx"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAKzD,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAyB7E;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,EAC5B,MAAM,EACN,WAAW,EACX,QAAQ,EACR,cAAc,EACd,WAAW,EACX,QAAQ,EACR,SAAS,EACT,SAAS,EACT,SAAS,EACZ,EAAE;IACC,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,cAAc,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,6EAA6E;IAC7E,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,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,+BA0IA"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
+
import { Badge, Checkbox, Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@orthacms/design-system';
|
|
4
|
+
import { MEDIA_KIND } from '../../constants';
|
|
5
|
+
import { buildAssetHandlers } from '../../utils/assetHandlers';
|
|
6
|
+
import { fileExtension } from '../../utils/fileExtension';
|
|
7
|
+
import { formatBytes } from '../../utils/formatBytes';
|
|
8
|
+
import { formatDuration } from '../../utils/formatDuration';
|
|
9
|
+
import { AssetActionsMenu } from '../AssetActionsMenu';
|
|
10
|
+
import { MediaThumbnail } from '../MediaThumbnail';
|
|
11
|
+
/** Intl descriptors for {@link MediaAssetTable}, co-located. */
|
|
12
|
+
const messages = defineMessages({
|
|
13
|
+
label: { id: 'media.table.label', defaultMessage: 'Files' },
|
|
14
|
+
selectAll: {
|
|
15
|
+
id: 'media.table.selectAll',
|
|
16
|
+
defaultMessage: 'Select all files on this page'
|
|
17
|
+
},
|
|
18
|
+
select: { id: 'media.table.select', defaultMessage: 'Select {name}' },
|
|
19
|
+
open: { id: 'media.table.open', defaultMessage: 'Open {name}' },
|
|
20
|
+
name: { id: 'media.table.name', defaultMessage: 'Name' },
|
|
21
|
+
type: { id: 'media.table.type', defaultMessage: 'Type' },
|
|
22
|
+
size: { id: 'media.table.size', defaultMessage: 'Size' },
|
|
23
|
+
dimensions: { id: 'media.table.dimensions', defaultMessage: 'Dimensions' },
|
|
24
|
+
uploadedBy: {
|
|
25
|
+
id: 'media.table.uploadedBy',
|
|
26
|
+
defaultMessage: 'Uploaded by'
|
|
27
|
+
},
|
|
28
|
+
date: { id: 'media.table.date', defaultMessage: 'Date' },
|
|
29
|
+
actions: { id: 'media.table.actions', defaultMessage: 'Actions' },
|
|
30
|
+
noAlt: { id: 'media.table.noAlt', defaultMessage: 'No alt text' }
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* The list view of the open folder's files — the design-system `Table`, used
|
|
34
|
+
* exactly as every other admin table uses it (no restyled cells), one row per
|
|
35
|
+
* asset: a select checkbox, a small thumbnail beside the name (a button that
|
|
36
|
+
* opens the details panel, named "Open {name}" as the grid tile is), the
|
|
37
|
+
* type, size, dimensions or duration, uploader and date, and the shared ⋯
|
|
38
|
+
* menu. The header checkbox selects or clears every file on the page.
|
|
39
|
+
* Presentational: selection and every action dispatch up to the page.
|
|
40
|
+
*/
|
|
41
|
+
export function MediaAssetTable({ assets, selectedIds, activeId, onToggleSelect, onToggleAll, onAction, canCreate, canUpdate, canDelete }) {
|
|
42
|
+
const intl = useIntl();
|
|
43
|
+
const selectedHere = assets.filter((a) => selectedIds.has(a.id)).length;
|
|
44
|
+
const allChecked = selectedHere === 0
|
|
45
|
+
? false
|
|
46
|
+
: selectedHere === assets.length
|
|
47
|
+
? true
|
|
48
|
+
: 'indeterminate';
|
|
49
|
+
return (_jsxs(Table, { "aria-label": intl.formatMessage(messages.label), children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [_jsx(TableHead, { className: "w-9", children: _jsx(Checkbox, { checked: allChecked, onCheckedChange: onToggleAll, "aria-label": intl.formatMessage(messages.selectAll) }) }), _jsx(TableHead, { children: intl.formatMessage(messages.name) }), _jsx(TableHead, { children: intl.formatMessage(messages.type) }), _jsx(TableHead, { className: "text-right", children: intl.formatMessage(messages.size) }), _jsx(TableHead, { className: "hidden md:table-cell", children: intl.formatMessage(messages.dimensions) }), _jsx(TableHead, { className: "hidden lg:table-cell", children: intl.formatMessage(messages.uploadedBy) }), _jsx(TableHead, { className: "hidden sm:table-cell", children: intl.formatMessage(messages.date) }), _jsx(TableHead, { className: "w-10", children: _jsx("span", { className: "sr-only", children: intl.formatMessage(messages.actions) }) })] }) }), _jsx(TableBody, { children: assets.map((asset) => {
|
|
50
|
+
const selected = selectedIds.has(asset.id);
|
|
51
|
+
const missingAlt = asset.kind === MEDIA_KIND.Image && !asset.alt?.trim();
|
|
52
|
+
return (_jsxs(TableRow, { "data-state": selected ? 'selected' : undefined, "aria-current": asset.id === activeId ? 'true' : undefined, children: [_jsx(TableCell, { children: _jsx(Checkbox, { checked: selected, onCheckedChange: () => onToggleSelect(asset.id), "aria-label": intl.formatMessage(messages.select, { name: asset.name }) }) }), _jsx(TableCell, { className: "max-w-0 w-full min-w-48", children: _jsxs("span", { className: "flex min-w-0 items-center gap-2.5", children: [_jsx(MediaThumbnail, { asset: asset, className: "h-6 w-8 shrink-0 rounded", iconClassName: "size-3.5" }), _jsx("button", { type: "button", onClick: () => onAction('open', asset), "aria-label": intl.formatMessage(messages.open, { name: asset.name }), title: asset.name, className: "min-w-0 truncate text-left font-medium hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: asset.name }), missingAlt ? (_jsx(Badge, { variant: "warning", size: "xs", className: "shrink-0", children: intl.formatMessage(messages.noAlt) })) : null] }) }), _jsx(TableCell, { className: "whitespace-nowrap text-muted-foreground", children: fileExtension(asset.name) ?? asset.mimeType }), _jsx(TableCell, { className: "whitespace-nowrap text-right tabular-nums", children: formatBytes(asset.size) }), _jsx(TableCell, { className: "hidden whitespace-nowrap text-muted-foreground md:table-cell", children: asset.dimensions
|
|
53
|
+
? `${asset.dimensions.width}×${asset.dimensions.height}`
|
|
54
|
+
: asset.duration
|
|
55
|
+
? formatDuration(asset.duration)
|
|
56
|
+
: '—' }), _jsx(TableCell, { className: "hidden whitespace-nowrap lg:table-cell", children: asset.uploadedBy }), _jsx(TableCell, { className: "hidden whitespace-nowrap text-muted-foreground sm:table-cell", children: intl.formatDate(asset.createdAt, {
|
|
57
|
+
month: 'short',
|
|
58
|
+
day: 'numeric',
|
|
59
|
+
year: 'numeric'
|
|
60
|
+
}) }), _jsx(TableCell, { className: "text-right", children: _jsx(AssetActionsMenu, { handlers: buildAssetHandlers(asset, onAction), assetName: asset.name, canCreate: canCreate, canUpdate: canUpdate, canDelete: canDelete }) })] }, asset.id));
|
|
61
|
+
}) })] }));
|
|
62
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaFieldControl/MediaFieldItem/index.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/lib/components/MediaFieldControl/MediaFieldItem/index.tsx"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kCAAkC,CAAC;AAqE1E;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,EAC3B,IAAI,EACJ,KAAK,EACL,KAAK,EACL,QAAQ,EACR,QAAgB,EAChB,QAAQ,EACR,MAAM,EACN,GAAG,EACH,UAAkB,EAClB,WAAW,EACX,kBAAkB,EACrB,EAAE;IACC,IAAI,EAAE,iBAAiB,CAAC;IACxB,wEAAwE;IACxE,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,2EAA2E;IAC3E,QAAQ,EAAE,OAAO,CAAC;IAClB,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,6DAA6D;IAC7D,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC,kBAAkB,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;CAChD,+BAsOA"}
|
|
@@ -7,6 +7,7 @@ import { MEDIA_ALT_MAX_LENGTH } from '@orthacms/content-domain';
|
|
|
7
7
|
import { MEDIA_KIND } from '../../../constants';
|
|
8
8
|
import { assetGradient } from '../../../utils/assetGradient';
|
|
9
9
|
import { formatBytes } from '../../../utils/formatBytes';
|
|
10
|
+
import { FadeInImage } from '../../FadeInImage';
|
|
10
11
|
import { MediaKindIcon } from '../../MediaKindIcon';
|
|
11
12
|
/** Intl descriptors for {@link MediaFieldItem}, co-located. */
|
|
12
13
|
const messages = defineMessages({
|
|
@@ -98,7 +99,7 @@ export function MediaFieldItem({ item, index, total, multiple, readOnly = false,
|
|
|
98
99
|
.join(' · ');
|
|
99
100
|
return (_jsxs("li", { className: cn('group/item relative overflow-hidden rounded-lg border bg-card transition-shadow hover:shadow-md', item.missing && 'border-destructive/50', (item.pending || item.resolving) && 'border-dashed'), children: [_jsxs("div", { className: "relative aspect-[4/3] w-full overflow-hidden bg-muted", style: !item.missing && !item.resolving && !asImage
|
|
100
101
|
? { backgroundImage: assetGradient(item.id) }
|
|
101
|
-
: undefined, children: [item.resolving ? (_jsx("span", { className: "flex size-full animate-pulse items-center justify-center bg-muted text-[11px] text-muted-foreground", children: intl.formatMessage(messages.resolving) })) : item.missing ? (_jsxs("span", { className: "flex size-full flex-col items-center justify-center gap-1 bg-destructive/5 px-2 text-center text-destructive", children: [_jsx(FileWarning, { className: "size-6", "aria-hidden": true }), _jsx("span", { className: "text-[11px] font-medium leading-tight", children: intl.formatMessage(messages.unavailableHint) })] })) : asImage ? (_jsx(
|
|
102
|
+
: undefined, children: [item.resolving ? (_jsx("span", { className: "flex size-full animate-pulse items-center justify-center bg-muted text-[11px] text-muted-foreground", children: intl.formatMessage(messages.resolving) })) : item.missing ? (_jsxs("span", { className: "flex size-full flex-col items-center justify-center gap-1 bg-destructive/5 px-2 text-center text-destructive", children: [_jsx(FileWarning, { className: "size-6", "aria-hidden": true }), _jsx("span", { className: "text-[11px] font-medium leading-tight", children: intl.formatMessage(messages.unavailableHint) })] })) : asImage ? (_jsx(FadeInImage, { src: item.url, alt: item.name, loading: "lazy", className: "absolute inset-0 size-full object-cover" })) : (_jsx("span", { className: "flex size-full items-center justify-center", children: _jsx(MediaKindIcon, { kind: kindOf(item.kind), className: "size-8 text-white/90 drop-shadow-sm" }) })), multiple ? (_jsx("span", { className: "absolute left-1.5 top-1.5 grid size-5 place-items-center rounded bg-black/60 text-[11px] font-medium tabular-nums text-white", title: intl.formatMessage(messages.position, {
|
|
102
103
|
index: index + 1,
|
|
103
104
|
total
|
|
104
105
|
}), children: index + 1 })) : null, item.pending ? (_jsx("span", { className: "absolute bottom-1.5 left-1.5 rounded bg-warning-soft px-1.5 py-0.5 text-[10px] font-medium leading-none text-warning-soft-foreground shadow-sm", children: intl.formatMessage(messages.pending) })) : null, _jsxs("div", { className: "absolute right-1.5 top-1.5 flex items-center gap-0.5 rounded-md bg-background/85 p-0.5 shadow-sm backdrop-blur transition-opacity focus-within:opacity-100 group-hover/item:opacity-100 sm:opacity-0", children: [item.missing || item.pending || item.resolving ? null : (_jsx(Button, { asChild: true, variant: "ghost", size: "icon", className: "size-6", children: _jsx("a", { href: item.originalUrl ?? item.url, target: "_blank", rel: "noreferrer", "aria-label": intl.formatMessage(messages.preview, { name: item.name }), children: _jsx(ExternalLink, { className: "size-3.5", "aria-hidden": true }) }) })), readOnly ? null : (_jsx(Button, { type: "button", variant: "ghost", size: "icon", className: "size-6 text-muted-foreground hover:bg-destructive/10 hover:text-destructive", "aria-label": intl.formatMessage(messages.remove, {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaFieldControl/index.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaFieldControl/index.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAKxD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAErE,OAAO,EAGH,KAAK,WAAW,EACnB,MAAM,yBAAyB,CAAC;AAqHjC;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,iBAAiB,CAAC,EAC9B,EAAE,EACF,QAAQ,EACR,MAAM,EACN,KAAK,EACL,WAAW,EACX,WAAW,EACX,OAAO,EACP,WAAW,EACX,QAAQ,EACR,OAAO,EACP,QAAgB,EAChB,QAAQ,EACR,MAAM,EACT,EAAE;IACC,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,KAAK,EAAE,OAAO,CAAC;IACf,gFAAgF;IAChF,WAAW,CAAC,EAAE,QAAQ,EAAE,CAAC;IACzB;;;;;OAKG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B;;;OAGG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACvB,+BAmeA"}
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
2
2
|
import { useMemo, useRef, useState } from 'react';
|
|
3
3
|
import { defineMessages, useIntl } from 'react-intl';
|
|
4
4
|
import { ImagePlus, UploadCloud } from 'lucide-react';
|
|
5
|
-
import { Button, cn,
|
|
5
|
+
import { Button, cn, toast } from '@orthacms/design-system';
|
|
6
6
|
import { useHasPermission } from '@orthacms/identity-admin';
|
|
7
7
|
import { toMediaValueRef } from '@orthacms/content-domain';
|
|
8
8
|
import { MEDIA_CREATE, MEDIA_KIND, MEDIA_READ } from '../../constants';
|
|
@@ -373,19 +373,24 @@ export function MediaFieldControl({ id, multiple, accept, value, initialRefs, re
|
|
|
373
373
|
// Straight into the same dialog the Upload button opens, so
|
|
374
374
|
// a drop is reviewed before it's staged.
|
|
375
375
|
openUploadWith(Array.from(event.dataTransfer.files));
|
|
376
|
-
}, className: cn('rounded-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
376
|
+
}, className: cn('rounded-lg border bg-muted/20 transition-colors', isEmpty ? 'border-dashed px-2 py-1' : 'p-3', invalid && 'border-destructive/60', dragging && 'border-primary bg-primary/5'), children: isEmpty ? (
|
|
377
|
+
// One line, not a centred empty-state card: the field sits
|
|
378
|
+
// in the record's form among single-line inputs now, and a
|
|
379
|
+
// 200px block for "nothing attached" outweighed the fields
|
|
380
|
+
// around it.
|
|
381
|
+
_jsxs("div", { "data-testid": "media-field-empty", className: "flex min-h-9 flex-wrap items-center gap-x-2 gap-y-0.5 px-1 text-sm", children: [dragging ? (_jsx(UploadCloud, { "aria-hidden": true, className: "size-4 shrink-0 text-primary" })) : (_jsx(ImagePlus, { "aria-hidden": true, className: "size-4 shrink-0 text-muted-foreground" })), _jsx("span", { className: "font-medium", children: readOnly
|
|
382
|
+
? intl.formatMessage(messages.emptyReadOnly)
|
|
383
|
+
: dragging
|
|
384
|
+
? intl.formatMessage(messages.dropHere)
|
|
385
|
+
: intl.formatMessage(multiple
|
|
386
|
+
? messages.emptyMultiple
|
|
387
|
+
: messages.emptySingle) }), _jsx("span", { className: "text-muted-foreground", children: intl.formatMessage(readOnly
|
|
388
|
+
? messages.emptyBodyReadOnly
|
|
389
|
+
: dragging
|
|
390
|
+
? messages.dropBody
|
|
391
|
+
: canStage
|
|
392
|
+
? messages.emptyBody
|
|
393
|
+
: messages.emptyBodyPickOnly) })] })) : (_jsx("ul", { className: cn('grid gap-3', multiple
|
|
389
394
|
? 'grid-cols-2 sm:grid-cols-3 xl:grid-cols-4'
|
|
390
395
|
: 'grid-cols-1 sm:max-w-[15rem]'), children: displays.map((item, index) => (_jsx(MediaFieldItem, { item: item, index: index, total: displays.length, multiple: multiple, readOnly: readOnly, onRemove: () => removeAt(index), onMove: (delta) => move(index, delta), ...(refs[index]?.alt !== undefined
|
|
391
396
|
? { alt: refs[index].alt }
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { EntryFieldControlContext } from '@orthacms/content-admin';
|
|
2
|
+
/**
|
|
3
|
+
* A media field **in the entry form**, contributed through content-admin's
|
|
4
|
+
* `ENTRY_FIELD_CONTROL_SLOT`. It used to be a titled card on a Media tab of
|
|
5
|
+
* its own; a media field is a field with its own control, so it now sits in
|
|
6
|
+
* the schema's order beside the rest (ADR-0022), and content draws its label
|
|
7
|
+
* row (label, required mark, locale mark, "Changed"), description and error
|
|
8
|
+
* like any other field's.
|
|
9
|
+
*
|
|
10
|
+
* The item is registered `composite`, so the field is named as a group by
|
|
11
|
+
* that label — the control is several buttons, and a `<label for>` would
|
|
12
|
+
* rename the one it pointed at.
|
|
13
|
+
*
|
|
14
|
+
* The staged uploads come from the plugin's own presave handle, held above the
|
|
15
|
+
* form so they outlive any remount; the saved ids' display refs come from the
|
|
16
|
+
* editor's one media read.
|
|
17
|
+
*/
|
|
18
|
+
export declare function MediaFieldInput({ field, id, value, error, describedBy, readOnly, onChange, onBlur, mediaRefs, mediaRefsPending, presave }: EntryFieldControlContext): import("react").JSX.Element;
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaFieldInput/index.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,yBAAyB,CAAC;AAMxE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAAC,EAC5B,KAAK,EACL,EAAE,EACF,KAAK,EACL,KAAK,EACL,WAAW,EACX,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,SAAS,EACT,gBAAgB,EAChB,OAAO,EACV,EAAE,wBAAwB,+BAqB1B"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { MEDIA_PRESAVE_ID } from '../../hooks/usePendingMediaUploads';
|
|
3
|
+
import { MediaFieldControl } from '../MediaFieldControl';
|
|
4
|
+
/**
|
|
5
|
+
* A media field **in the entry form**, contributed through content-admin's
|
|
6
|
+
* `ENTRY_FIELD_CONTROL_SLOT`. It used to be a titled card on a Media tab of
|
|
7
|
+
* its own; a media field is a field with its own control, so it now sits in
|
|
8
|
+
* the schema's order beside the rest (ADR-0022), and content draws its label
|
|
9
|
+
* row (label, required mark, locale mark, "Changed"), description and error
|
|
10
|
+
* like any other field's.
|
|
11
|
+
*
|
|
12
|
+
* The item is registered `composite`, so the field is named as a group by
|
|
13
|
+
* that label — the control is several buttons, and a `<label for>` would
|
|
14
|
+
* rename the one it pointed at.
|
|
15
|
+
*
|
|
16
|
+
* The staged uploads come from the plugin's own presave handle, held above the
|
|
17
|
+
* form so they outlive any remount; the saved ids' display refs come from the
|
|
18
|
+
* editor's one media read.
|
|
19
|
+
*/
|
|
20
|
+
export function MediaFieldInput({ field, id, value, error, describedBy, readOnly, onChange, onBlur, mediaRefs, mediaRefsPending, presave }) {
|
|
21
|
+
const uploads = presave?.[MEDIA_PRESAVE_ID];
|
|
22
|
+
return (_jsx(MediaFieldControl, { id: id, multiple: !!field.multiple, accept: field.accept, required: field.required, value: value, initialRefs: mediaRefs, refsPending: mediaRefsPending, uploads: uploads, readOnly: readOnly, invalid: !!error, describedBy: describedBy, onChange: onChange, onBlur: onBlur ?? (() => undefined) }));
|
|
23
|
+
}
|