@orthacms/media-admin 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/components/AssetDetailPanel/MetaRow/index.d.ts +11 -0
- package/dist/lib/components/AssetDetailPanel/MetaRow/index.d.ts.map +1 -0
- package/dist/lib/components/AssetDetailPanel/MetaRow/index.js +9 -0
- package/dist/lib/components/AssetDetailPanel/index.d.ts +35 -0
- package/dist/lib/components/AssetDetailPanel/index.d.ts.map +1 -0
- package/dist/lib/components/AssetDetailPanel/index.js +158 -0
- package/dist/lib/components/FolderActionsMenu/index.d.ts +3 -1
- package/dist/lib/components/FolderActionsMenu/index.d.ts.map +1 -1
- package/dist/lib/components/FolderActionsMenu/index.js +3 -3
- package/dist/lib/components/MediaAltDrill/index.d.ts +9 -0
- package/dist/lib/components/MediaAltDrill/index.d.ts.map +1 -0
- package/dist/lib/components/MediaAltDrill/index.js +60 -0
- package/dist/lib/components/MediaAltSignal/index.d.ts +8 -0
- package/dist/lib/components/MediaAltSignal/index.d.ts.map +1 -0
- package/dist/lib/components/MediaAltSignal/index.js +35 -0
- package/dist/lib/components/MediaAssetTable/index.d.ts +25 -0
- package/dist/lib/components/MediaAssetTable/index.d.ts.map +1 -0
- package/dist/lib/components/MediaAssetTable/index.js +62 -0
- package/dist/lib/components/MediaFieldControl/index.d.ts.map +1 -1
- package/dist/lib/components/MediaFieldControl/index.js +19 -14
- package/dist/lib/components/MediaFieldInput/index.d.ts +19 -0
- package/dist/lib/components/MediaFieldInput/index.d.ts.map +1 -0
- package/dist/lib/components/MediaFieldInput/index.js +23 -0
- package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts +17 -3
- package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.d.ts.map +1 -1
- package/dist/lib/components/MediaFoldersNav/MediaFolderTreeItem/index.js +16 -6
- package/dist/lib/components/MediaFoldersNav/index.d.ts +10 -5
- package/dist/lib/components/MediaFoldersNav/index.d.ts.map +1 -1
- package/dist/lib/components/MediaFoldersNav/index.js +17 -9
- package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts +9 -4
- package/dist/lib/components/MediaGrid/MediaAssetCard/index.d.ts.map +1 -1
- package/dist/lib/components/MediaGrid/MediaAssetCard/index.js +27 -11
- package/dist/lib/components/MediaGrid/index.d.ts +10 -10
- package/dist/lib/components/MediaGrid/index.d.ts.map +1 -1
- package/dist/lib/components/MediaGrid/index.js +10 -15
- package/dist/lib/components/MediaSelectionBar/index.d.ts +9 -5
- package/dist/lib/components/MediaSelectionBar/index.d.ts.map +1 -1
- package/dist/lib/components/MediaSelectionBar/index.js +16 -10
- package/dist/lib/components/MediaStorageWidget/index.js +2 -2
- package/dist/lib/components/MediaThumbnail/index.d.ts +9 -6
- package/dist/lib/components/MediaThumbnail/index.d.ts.map +1 -1
- package/dist/lib/components/MediaThumbnail/index.js +7 -6
- package/dist/lib/components/MediaToolbar/index.d.ts +10 -8
- package/dist/lib/components/MediaToolbar/index.d.ts.map +1 -1
- package/dist/lib/components/MediaToolbar/index.js +37 -13
- package/dist/lib/components/MediaTopBar/index.d.ts +7 -1
- package/dist/lib/components/MediaTopBar/index.d.ts.map +1 -1
- package/dist/lib/components/MediaTopBar/index.js +6 -3
- package/dist/lib/components/MediaUploadBanner/index.d.ts +7 -5
- package/dist/lib/components/MediaUploadBanner/index.d.ts.map +1 -1
- package/dist/lib/components/MediaUploadBanner/index.js +15 -8
- package/dist/lib/constants/index.d.ts +13 -0
- package/dist/lib/constants/index.d.ts.map +1 -1
- package/dist/lib/constants/index.js +11 -0
- package/dist/lib/hooks/useMediaInsights/index.d.ts +8 -0
- package/dist/lib/hooks/useMediaInsights/index.d.ts.map +1 -1
- package/dist/lib/hooks/useMediaInsights/index.js +20 -1
- package/dist/lib/infrastructure/httpMediaInsightsGateway/index.d.ts.map +1 -1
- package/dist/lib/infrastructure/httpMediaInsightsGateway/index.js +9 -0
- package/dist/lib/infrastructure/mediaInsightsGateway/index.d.ts +17 -0
- package/dist/lib/infrastructure/mediaInsightsGateway/index.d.ts.map +1 -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 +82 -36
- package/dist/lib/utils/fileExtension/index.d.ts +9 -0
- package/dist/lib/utils/fileExtension/index.d.ts.map +1 -0
- package/dist/lib/utils/fileExtension/index.js +14 -0
- package/dist/lib/utils/mediaPlugin/index.d.ts.map +1 -1
- package/dist/lib/utils/mediaPlugin/index.js +35 -32
- 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
|
+
}
|
|
@@ -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,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The images behind "Images missing alt text", the oldest upload first, with
|
|
3
|
+
* who uploaded each — and the way to the library, where alt text is written.
|
|
4
|
+
*
|
|
5
|
+
* The library has no "missing alt text" filter of its own to land on, so the
|
|
6
|
+
* button opens the library as it is rather than claiming a filtered view.
|
|
7
|
+
*/
|
|
8
|
+
export declare function MediaAltDrill(): import("react").JSX.Element;
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaAltDrill/index.tsx"],"names":[],"mappings":"AAyCA;;;;;;GAMG;AACH,wBAAgB,aAAa,gCAiD5B"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Link } from 'react-router-dom';
|
|
3
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
4
|
+
import { ArrowRight } from 'lucide-react';
|
|
5
|
+
import { Button } from '@orthacms/design-system';
|
|
6
|
+
import { DrillRecordRow, SignalDrill } from '@orthacms/insights-admin';
|
|
7
|
+
import { useMediaAltMissing } from '../../hooks/useMediaInsights';
|
|
8
|
+
/** Intl descriptors for the alt-text panel, co-located here. */
|
|
9
|
+
const messages = defineMessages({
|
|
10
|
+
title: {
|
|
11
|
+
id: 'media.insights.drill.alt.title',
|
|
12
|
+
defaultMessage: 'Images missing alt text'
|
|
13
|
+
},
|
|
14
|
+
description: {
|
|
15
|
+
id: 'media.insights.drill.alt.description',
|
|
16
|
+
defaultMessage: 'Images in the library with no alt text, the oldest upload first.'
|
|
17
|
+
},
|
|
18
|
+
heading: {
|
|
19
|
+
id: 'media.insights.drill.alt.heading',
|
|
20
|
+
defaultMessage: 'Images missing alt text · {count, number} {count, plural, one {image} other {images}}'
|
|
21
|
+
},
|
|
22
|
+
meta: {
|
|
23
|
+
id: 'media.insights.drill.alt.meta',
|
|
24
|
+
defaultMessage: 'Uploaded {date}'
|
|
25
|
+
},
|
|
26
|
+
uploadedBy: {
|
|
27
|
+
id: 'media.insights.drill.alt.uploadedBy',
|
|
28
|
+
defaultMessage: 'Uploaded by {name}'
|
|
29
|
+
},
|
|
30
|
+
open: {
|
|
31
|
+
id: 'media.insights.drill.alt.open',
|
|
32
|
+
defaultMessage: 'Open Media Library'
|
|
33
|
+
},
|
|
34
|
+
empty: {
|
|
35
|
+
id: 'media.insights.drill.alt.empty',
|
|
36
|
+
defaultMessage: 'Every image has alt text.'
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
/**
|
|
40
|
+
* The images behind "Images missing alt text", the oldest upload first, with
|
|
41
|
+
* who uploaded each — and the way to the library, where alt text is written.
|
|
42
|
+
*
|
|
43
|
+
* The library has no "missing alt text" filter of its own to land on, so the
|
|
44
|
+
* button opens the library as it is rather than claiming a filtered view.
|
|
45
|
+
*/
|
|
46
|
+
export function MediaAltDrill() {
|
|
47
|
+
const intl = useIntl();
|
|
48
|
+
const { data, isPending, isError } = useMediaAltMissing();
|
|
49
|
+
return (_jsx(SignalDrill, { title: intl.formatMessage(messages.title), count: data?.total, heading: data
|
|
50
|
+
? intl.formatMessage(messages.heading, {
|
|
51
|
+
count: data.total
|
|
52
|
+
})
|
|
53
|
+
: undefined, shown: data?.assets.length, description: intl.formatMessage(messages.description), isPending: isPending, isError: isError, isEmpty: !!data && data.total === 0, emptyMessage: intl.formatMessage(messages.empty), actions: _jsx(Button, { asChild: true, variant: "outline", size: "sm", children: _jsxs(Link, { to: "../media", children: [_jsx(ArrowRight, { "aria-hidden": "true" }), intl.formatMessage(messages.open)] }) }), children: data?.assets.map((asset) => (_jsx(DrillRecordRow, { title: asset.name, meta: intl.formatMessage(messages.meta, {
|
|
54
|
+
date: intl.formatDate(asset.createdAt, {
|
|
55
|
+
day: 'numeric',
|
|
56
|
+
month: 'short',
|
|
57
|
+
year: 'numeric'
|
|
58
|
+
})
|
|
59
|
+
}), personId: asset.uploadedBy, personLabel: (name) => intl.formatMessage(messages.uploadedBy, { name }) }, asset.id))) }));
|
|
60
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type InsightsSignalRowProps } from '@orthacms/insights-admin';
|
|
2
|
+
/**
|
|
3
|
+
* Images with no alt text — accessibility debt somebody can pick up, as a
|
|
4
|
+
* count, with the share of the library it is. The headline is the missing
|
|
5
|
+
* count rather than a coverage score: "23 images need alt text" is a job.
|
|
6
|
+
*/
|
|
7
|
+
export declare function MediaAltSignal({ onOpen, isOpen }: InsightsSignalRowProps): import("react").JSX.Element;
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/lib/components/MediaAltSignal/index.tsx"],"names":[],"mappings":"AAEA,OAAO,EAEH,KAAK,sBAAsB,EAC9B,MAAM,0BAA0B,CAAC;AAmBlC;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,sBAAsB,+BAyBxE"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { defineMessages, useIntl } from 'react-intl';
|
|
3
|
+
import { Eye } from 'lucide-react';
|
|
4
|
+
import { SignalRow } from '@orthacms/insights-admin';
|
|
5
|
+
import { useMediaAltCoverage } from '../../hooks/useMediaInsights';
|
|
6
|
+
/** Intl descriptors for the alt-text line, co-located here. */
|
|
7
|
+
const messages = defineMessages({
|
|
8
|
+
title: {
|
|
9
|
+
id: 'media.insights.signal.alt.title',
|
|
10
|
+
defaultMessage: 'Images missing alt text'
|
|
11
|
+
},
|
|
12
|
+
hint: {
|
|
13
|
+
id: 'media.insights.signal.alt.hint',
|
|
14
|
+
defaultMessage: 'In Media Library · {percent}% of {images, number} images'
|
|
15
|
+
},
|
|
16
|
+
noImages: {
|
|
17
|
+
id: 'media.insights.signal.alt.noImages',
|
|
18
|
+
defaultMessage: 'No images in Media Library yet'
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
/**
|
|
22
|
+
* Images with no alt text — accessibility debt somebody can pick up, as a
|
|
23
|
+
* count, with the share of the library it is. The headline is the missing
|
|
24
|
+
* count rather than a coverage score: "23 images need alt text" is a job.
|
|
25
|
+
*/
|
|
26
|
+
export function MediaAltSignal({ onOpen, isOpen }) {
|
|
27
|
+
const intl = useIntl();
|
|
28
|
+
const { data, isPending, isError } = useMediaAltCoverage();
|
|
29
|
+
const images = data?.images ?? 0;
|
|
30
|
+
const missing = data?.missing ?? 0;
|
|
31
|
+
const percent = images > 0 ? Math.round((missing / images) * 100) : 0;
|
|
32
|
+
return (_jsx(SignalRow, { icon: Eye, tone: "warn", count: missing, title: intl.formatMessage(messages.title), hint: images > 0
|
|
33
|
+
? intl.formatMessage(messages.hint, { percent, images })
|
|
34
|
+
: intl.formatMessage(messages.noImages), isPending: isPending, isError: isError, onOpen: onOpen, isOpen: isOpen }));
|
|
35
|
+
}
|
|
@@ -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/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
|
+
}
|