canopycms 0.1.0 → 0.2.0-int.121
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/CHANGELOG.md +202 -43
- package/dist/api/assets.d.ts +27 -7
- package/dist/api/assets.js +118 -47
- package/dist/api/client.d.ts +5 -3
- package/dist/api/client.js +7 -3
- package/dist/api/index.d.ts +1 -1
- package/dist/assets/asset-prefixes.d.ts +4 -1
- package/dist/assets/asset-prefixes.js +3 -1
- package/dist/assets/editor-thumbnail-widths.d.ts +11 -0
- package/dist/assets/editor-thumbnail-widths.js +7 -0
- package/dist/assets/finalize.d.ts +7 -6
- package/dist/assets/finalize.js +29 -10
- package/dist/assets/keys.d.ts +4 -1
- package/dist/assets/keys.js +4 -1
- package/dist/assets/materialize.d.ts +17 -2
- package/dist/assets/materialize.js +57 -4
- package/dist/assets/store-local.d.ts +18 -9
- package/dist/assets/store-local.js +58 -26
- package/dist/assets/store-s3.d.ts +25 -6
- package/dist/assets/store-s3.js +81 -19
- package/dist/assets/types.d.ts +33 -4
- package/dist/cli/cli.js +151 -56
- package/dist/config/types.d.ts +3 -2
- package/dist/editor/BranchManager.js +2 -1
- package/dist/editor/CommentsPanel.js +67 -29
- package/dist/editor/Editor.d.ts +3 -0
- package/dist/editor/Editor.js +93 -39
- package/dist/editor/EntryNavigator.d.ts +17 -5
- package/dist/editor/EntryNavigator.js +66 -31
- package/dist/editor/FormHeader.d.ts +2 -1
- package/dist/editor/FormHeader.js +9 -5
- package/dist/editor/FormRenderer.d.ts +7 -2
- package/dist/editor/FormRenderer.js +51 -16
- package/dist/editor/PreviewFrame.d.ts +13 -2
- package/dist/editor/PreviewFrame.js +62 -5
- package/dist/editor/PreviewPane.d.ts +17 -1
- package/dist/editor/PreviewPane.js +55 -8
- package/dist/editor/PreviewToolbarTools.d.ts +8 -0
- package/dist/editor/PreviewToolbarTools.js +9 -1
- package/dist/editor/branch-status.d.ts +6 -0
- package/dist/editor/branch-status.js +10 -0
- package/dist/editor/comments/BranchComments.d.ts +2 -4
- package/dist/editor/comments/BranchComments.js +7 -11
- package/dist/editor/comments/ThreadCarousel.d.ts +2 -8
- package/dist/editor/comments/ThreadCarousel.js +4 -49
- package/dist/editor/comments/ThreadReplyControls.js +5 -2
- package/dist/editor/components/BranchLockBanner.d.ts +8 -7
- package/dist/editor/components/BranchLockBanner.js +30 -3
- package/dist/editor/components/BranchPill.d.ts +5 -4
- package/dist/editor/components/BranchPill.js +6 -8
- package/dist/editor/components/BranchRowLock.d.ts +12 -0
- package/dist/editor/components/BranchRowLock.js +19 -0
- package/dist/editor/components/BranchesDrawer.js +3 -3
- package/dist/editor/components/EditorHeader.d.ts +2 -6
- package/dist/editor/components/EditorHeader.js +14 -28
- package/dist/editor/components/EditorRail.d.ts +3 -4
- package/dist/editor/components/EditorRail.js +3 -4
- package/dist/editor/components/EntryBreadcrumb.d.ts +9 -10
- package/dist/editor/components/EntryBreadcrumb.js +8 -11
- package/dist/editor/components/PreviewLinkToast.d.ts +5 -0
- package/dist/editor/components/PreviewLinkToast.js +33 -0
- package/dist/editor/components/QuickOpenPalette.d.ts +7 -1
- package/dist/editor/components/QuickOpenPalette.js +6 -1
- package/dist/editor/components/RenameEntryModal.d.ts +6 -1
- package/dist/editor/components/RenameEntryModal.js +20 -5
- package/dist/editor/components/SaveSplitButton.d.ts +3 -2
- package/dist/editor/components/SaveSplitButton.js +25 -12
- package/dist/editor/components/StagedChangesDrawer.d.ts +0 -1
- package/dist/editor/components/StagedChangesDrawer.js +5 -4
- package/dist/editor/components/UndoToastMessage.d.ts +6 -5
- package/dist/editor/components/UndoToastMessage.js +6 -4
- package/dist/editor/components/UnsavedMark.d.ts +9 -0
- package/dist/editor/components/UnsavedMark.js +20 -0
- package/dist/editor/components/UserBadge.js +13 -11
- package/dist/editor/copy.d.ts +4 -0
- package/dist/editor/copy.js +4 -0
- package/dist/editor/editor-utils.d.ts +49 -9
- package/dist/editor/editor-utils.js +117 -16
- package/dist/editor/entry-search.d.ts +4 -2
- package/dist/editor/entry-search.js +2 -3
- package/dist/editor/fields/FieldLabel.d.ts +1 -1
- package/dist/editor/fields/FieldLabel.js +1 -1
- package/dist/editor/fields/ImageField.d.ts +1 -1
- package/dist/editor/fields/ImageField.js +36 -27
- package/dist/editor/fields/ObjectField.d.ts +13 -2
- package/dist/editor/fields/ObjectField.js +38 -9
- package/dist/editor/fields/entry-link/EntryLinkContext.d.ts +2 -0
- package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -1
- package/dist/editor/group-manager/index.js +1 -1
- package/dist/editor/hooks/useBranchManager.d.ts +2 -0
- package/dist/editor/hooks/useBranchManager.js +1 -0
- package/dist/editor/hooks/useEditorLayout.d.ts +4 -0
- package/dist/editor/hooks/useEditorLayout.js +5 -0
- package/dist/editor/hooks/useEntryManager.d.ts +2 -0
- package/dist/editor/hooks/useEntryManager.js +10 -8
- package/dist/editor/media/AssetCard.d.ts +6 -3
- package/dist/editor/media/AssetCard.js +24 -9
- package/dist/editor/media/AssetListRow.d.ts +9 -4
- package/dist/editor/media/AssetListRow.js +18 -9
- package/dist/editor/media/CropStep.d.ts +8 -10
- package/dist/editor/media/CropStep.js +39 -27
- package/dist/editor/media/MediaLibrary.js +5 -4
- package/dist/editor/media/MediaLibraryBody.d.ts +1 -1
- package/dist/editor/media/MediaLibraryBody.js +65 -37
- package/dist/editor/media/crop-availability.d.ts +6 -0
- package/dist/editor/media/crop-availability.js +19 -0
- package/dist/editor/media/crop-math.d.ts +19 -13
- package/dist/editor/media/crop-math.js +50 -11
- package/dist/editor/media/useAssetLibrary.d.ts +6 -3
- package/dist/editor/media/useAssetLibrary.js +17 -10
- package/dist/editor/pane-header.d.ts +6 -0
- package/dist/editor/pane-header.js +6 -0
- package/dist/editor/permission-manager/index.js +1 -1
- package/dist/editor/preview-bridge.d.ts +23 -0
- package/dist/editor/preview-bridge.js +105 -4
- package/dist/editor/preview-path.d.ts +16 -4
- package/dist/editor/preview-path.js +80 -19
- package/dist/editor/shell/ActivityRail.d.ts +1 -3
- package/dist/editor/shell/ActivityRail.js +3 -3
- package/dist/editor/shell/EditorShell.d.ts +15 -0
- package/dist/editor/shell/EditorShell.js +17 -1
- package/dist/editor/shell/OverlayHeader.d.ts +19 -0
- package/dist/editor/shell/OverlayHeader.js +15 -0
- package/dist/editor/shell/PaneLayout.d.ts +6 -1
- package/dist/editor/shell/PaneLayout.js +68 -13
- package/dist/editor/shell/SidePanelHost.d.ts +8 -6
- package/dist/editor/shell/SidePanelHost.js +70 -33
- package/dist/editor/shell/TopBar.js +2 -1
- package/dist/editor/theme.d.ts +1 -0
- package/dist/editor/theme.js +19 -1
- package/dist/editor/undo-removal.d.ts +19 -10
- package/dist/editor/undo-removal.js +44 -16
- package/dist/server.d.ts +2 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import { ActionIcon, Alert, Button, Center, Group, Progress, ScrollArea, SegmentedControl, Select, SimpleGrid, Stack, Text, TextInput, Tooltip, VisuallyHidden, } from '@mantine/core';
|
|
4
|
+
import { ActionIcon, Alert, Button, Center, Group, Progress, ScrollArea, SegmentedControl, Select, SimpleGrid, Stack, Switch, Text, TextInput, Tooltip, VisuallyHidden, } from '@mantine/core';
|
|
5
5
|
import { Dropzone } from '@mantine/dropzone';
|
|
6
6
|
import { openConfirm } from '../utils/confirm-modal.js';
|
|
7
7
|
import { IconAlertCircle, IconGridDots, IconLayoutGrid, IconList, IconSearch, IconUpload, IconX, } from '@tabler/icons-react';
|
|
@@ -42,8 +42,14 @@ const DENSITY_OPTIONS = [
|
|
|
42
42
|
},
|
|
43
43
|
{ value: 'list', label: _jsx(DensityLabel, { icon: _jsx(IconList, { size: 16 }), text: "List" }) },
|
|
44
44
|
];
|
|
45
|
+
/** A restored file's record: the same asset, no longer marked `hidden`. */
|
|
46
|
+
function withoutHidden(asset) {
|
|
47
|
+
const restored = { ...asset };
|
|
48
|
+
delete restored.hidden;
|
|
49
|
+
return restored;
|
|
50
|
+
}
|
|
45
51
|
/**
|
|
46
|
-
* Shared grid/list/upload/search/
|
|
52
|
+
* Shared grid/list/upload/search/remove core for both MediaLibrary
|
|
47
53
|
* presentations (Drawer "manage" mode, Modal "picker" mode) - see
|
|
48
54
|
* MediaLibrary.tsx for the two wrappers.
|
|
49
55
|
*/
|
|
@@ -51,40 +57,49 @@ export const MediaLibraryBody = ({ opened, mode, onSelect }) => {
|
|
|
51
57
|
const client = useApiClient();
|
|
52
58
|
const { baseUrl } = useAssetContext();
|
|
53
59
|
const { userContext } = useUserContext();
|
|
54
|
-
|
|
55
|
-
//
|
|
60
|
+
const admin = isAdmin(userContext?.groups);
|
|
61
|
+
// Mirrors the server's rule in api/assets.ts's visibilityRefusal: an admin may
|
|
62
|
+
// remove or restore any file, anyone else only what they uploaded. Kept per-asset
|
|
56
63
|
// (not a single boolean) because ownership varies row by row. The server is
|
|
57
64
|
// still the authority - this only decides whether to render the control.
|
|
58
|
-
const
|
|
65
|
+
const canChangeVisibility = (asset) => {
|
|
59
66
|
if (mode !== 'manage')
|
|
60
67
|
return false;
|
|
61
|
-
if (
|
|
68
|
+
if (admin)
|
|
62
69
|
return true;
|
|
63
70
|
return !!asset.uploadedBy && asset.uploadedBy === userContext?.userId;
|
|
64
71
|
};
|
|
72
|
+
// Only an admin in the manage drawer can list removed files, so a picker never offers one.
|
|
73
|
+
const [showRemovedChoice, setShowRemoved] = useState(false);
|
|
74
|
+
const showRemoved = showRemovedChoice && admin && mode === 'manage';
|
|
65
75
|
const upload = useAssetUpload();
|
|
66
76
|
// `picker` mode is used exclusively by ImageField and the MDX image dialog
|
|
67
77
|
// (see MediaLibraryProps.mode's doc comment) - both image-only contexts.
|
|
68
78
|
// `manage` mode (the Editor's Media Library drawer) keeps accepting PDFs.
|
|
69
79
|
const imageOnly = mode === 'picker';
|
|
70
|
-
const library = useAssetLibrary(opened);
|
|
80
|
+
const library = useAssetLibrary(opened, showRemoved);
|
|
71
81
|
const { sort, density, setSort, setDensity } = useMediaLibraryPrefs();
|
|
72
82
|
const [kind, setKind] = useState('all');
|
|
73
83
|
const [filterText, setFilterText] = useState('');
|
|
74
84
|
const [actionError, setActionError] = useState(null);
|
|
75
|
-
// The result of the last remove, for the live region; the
|
|
76
|
-
// `id` re-keys the region's text, so a second
|
|
85
|
+
// The result of the last remove or restore, for the live region; the card may be gone.
|
|
86
|
+
// `id` re-keys the region's text, so a second action with the same words is announced too.
|
|
77
87
|
const [actionNotice, setActionNotice] = useState(null);
|
|
78
88
|
const searchRef = useRef(null);
|
|
79
|
-
// A
|
|
80
|
-
//
|
|
81
|
-
|
|
89
|
+
// A remove or restore takes the focused button with it (the card goes, or its button swaps), so
|
|
90
|
+
// focus moves to the search field once that has rendered (Mantine returns focus to the remove
|
|
91
|
+
// button when the confirm closes).
|
|
92
|
+
const focusAfterAction = useRef(false);
|
|
82
93
|
useEffect(() => {
|
|
83
|
-
if (!
|
|
94
|
+
if (!focusAfterAction.current)
|
|
84
95
|
return;
|
|
85
|
-
|
|
96
|
+
focusAfterAction.current = false;
|
|
86
97
|
searchRef.current?.focus();
|
|
87
98
|
});
|
|
99
|
+
const announce = (text) => {
|
|
100
|
+
setActionNotice((prev) => ({ id: (prev?.id ?? 0) + 1, text }));
|
|
101
|
+
focusAfterAction.current = true;
|
|
102
|
+
};
|
|
88
103
|
// Clearing hides the control that did it, so focus moves to the search field.
|
|
89
104
|
const clearSearch = () => {
|
|
90
105
|
setFilterText('');
|
|
@@ -142,39 +157,52 @@ export const MediaLibraryBody = ({ opened, mode, onSelect }) => {
|
|
|
142
157
|
const handleReject = (rejections) => {
|
|
143
158
|
setActionError(rejections[0]?.errors[0]?.message ?? 'The file was rejected.');
|
|
144
159
|
};
|
|
145
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Runs a remove or restore; `failure` words its error, and the raw server message is for admins.
|
|
162
|
+
* A non-admin's 403 from the API is the ownership refusal, so it says so (`refusal`) rather than
|
|
163
|
+
* to retry. Restore shows only to admins, so it has none.
|
|
164
|
+
*/
|
|
165
|
+
const changeVisibility = async (request, failure, refusal, onDone) => {
|
|
146
166
|
setActionError(null);
|
|
147
|
-
const
|
|
148
|
-
// The raw server message is a diagnostic, shown to admins only.
|
|
149
|
-
const generic = (serverMessage) => failure((isAdmin(userContext?.groups) && serverMessage) || 'Try again.');
|
|
167
|
+
const fail = (serverMessage) => setActionError(`${failure} ${(admin && serverMessage) || 'Try again.'}`);
|
|
150
168
|
try {
|
|
151
|
-
const result = await
|
|
169
|
+
const result = await request();
|
|
152
170
|
if (!result.ok) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
171
|
+
if (refusal && result.status === 403 && !admin && !isNonApiResponse(result)) {
|
|
172
|
+
setActionError(`${failure} ${refusal}`);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
fail(result.error);
|
|
176
|
+
}
|
|
157
177
|
return;
|
|
158
178
|
}
|
|
159
|
-
|
|
160
|
-
setActionNotice((prev) => ({
|
|
161
|
-
id: (prev?.id ?? 0) + 1,
|
|
162
|
-
text: `Removed ${asset.filename} from library`,
|
|
163
|
-
}));
|
|
164
|
-
focusAfterRemove.current = true;
|
|
179
|
+
onDone();
|
|
165
180
|
}
|
|
166
181
|
catch (err) {
|
|
167
|
-
|
|
182
|
+
fail(getErrorMessage(err));
|
|
168
183
|
}
|
|
169
184
|
};
|
|
170
|
-
const
|
|
185
|
+
const handleRemove = (asset) => changeVisibility(() => client.assets.hide({ key: asset.hash32 }), `Couldn't remove ${asset.filename} from the library.`, 'You can only remove files you uploaded. Ask an admin to remove it.', () => {
|
|
186
|
+
if (showRemoved)
|
|
187
|
+
library.replace({ ...asset, hidden: true });
|
|
188
|
+
else
|
|
189
|
+
library.remove(asset.hash32);
|
|
190
|
+
announce(`Removed ${asset.filename} from library`);
|
|
191
|
+
});
|
|
192
|
+
const handleRestore = (asset) => changeVisibility(() => client.assets.restore({ key: asset.hash32 }), `Couldn't restore ${asset.filename} to the library.`, null, () => {
|
|
193
|
+
library.replace(withoutHidden(asset));
|
|
194
|
+
announce(`Restored ${asset.filename} to library`);
|
|
195
|
+
});
|
|
196
|
+
// A confirm, not an Undo: only an admin can restore, from the "Show removed files" view.
|
|
197
|
+
const handleRemoveClick = (asset) => {
|
|
171
198
|
openConfirm({
|
|
172
199
|
title: 'Remove from library',
|
|
173
|
-
children: (_jsxs(Text, { size: "sm", children: ["
|
|
200
|
+
children: (_jsxs(Stack, { gap: "xs", children: [_jsxs(Text, { size: "sm", children: ["\u201C", asset.filename, "\u201D will no longer be listed in the media library or offered when choosing an image. Entries that already use it keep working, including published and earlier versions of the site."] }), _jsx(Text, { size: "sm", children: admin
|
|
201
|
+
? 'To restore it, turn on “Show removed files”, or upload it again.'
|
|
202
|
+
: 'To restore it, upload it again or ask an admin.' })] })),
|
|
174
203
|
labels: { confirm: 'Remove from library', cancel: 'Cancel' },
|
|
175
|
-
confirmProps: { color: 'red' },
|
|
176
204
|
onConfirm: () => {
|
|
177
|
-
void
|
|
205
|
+
void handleRemove(asset);
|
|
178
206
|
},
|
|
179
207
|
});
|
|
180
208
|
};
|
|
@@ -228,9 +256,9 @@ export const MediaLibraryBody = ({ opened, mode, onSelect }) => {
|
|
|
228
256
|
})();
|
|
229
257
|
const renderList = () => {
|
|
230
258
|
if (density === 'list') {
|
|
231
|
-
return (_jsx(Stack, { gap: 4, children: visibleAssets.map((asset) => (_jsx(AssetListRow, { asset: asset, baseUrl: baseUrl, onSelect: mode === 'picker' ? onSelect : undefined,
|
|
259
|
+
return (_jsx(Stack, { gap: 4, children: visibleAssets.map((asset) => (_jsx(AssetListRow, { asset: asset, baseUrl: baseUrl, onSelect: mode === 'picker' ? onSelect : undefined, onRemove: canChangeVisibility(asset) ? handleRemoveClick : undefined, onRestore: canChangeVisibility(asset) ? handleRestore : undefined }, asset.hash32))) }));
|
|
232
260
|
}
|
|
233
|
-
return (_jsx(SimpleGrid, { cols: GRID_COLUMNS[density], spacing: "sm", children: visibleAssets.map((asset) => (_jsx(AssetCard, { asset: asset, baseUrl: baseUrl, density: density, onSelect: mode === 'picker' ? onSelect : undefined,
|
|
261
|
+
return (_jsx(SimpleGrid, { cols: GRID_COLUMNS[density], spacing: "sm", children: visibleAssets.map((asset) => (_jsx(AssetCard, { asset: asset, baseUrl: baseUrl, density: density, onSelect: mode === 'picker' ? onSelect : undefined, onRemove: canChangeVisibility(asset) ? handleRemoveClick : undefined, onRestore: canChangeVisibility(asset) ? handleRestore : undefined }, asset.hash32))) }));
|
|
234
262
|
};
|
|
235
263
|
const listContent = (_jsxs(_Fragment, { children: [!library.firstPageLoaded && !library.error && visibleAssets.length === 0 && (_jsx(MediaLibrarySkeleton, { density: density })), visibleAssets.length > 0 && renderList(), emptyState && (_jsxs(Stack, { align: "center", gap: "xs", py: "xl", "data-testid": "media-library-empty", children: [_jsx(Text, { size: "sm", c: "dimmed", ta: "center", children: emptyState }), _jsxs(Group, { gap: "xs", justify: "center", children: [needle && (_jsx(Button, { variant: "light", size: "xs", onClick: clearSearch, children: "Clear search" })), kind !== 'all' && !libraryEmpty && (_jsx(Button, { variant: "light", size: "xs", onClick: () => setKind('all'), children: "Show all files" }))] })] })), hasMore && (_jsxs(_Fragment, { children: [_jsx("div", { ref: setSentinel, "aria-hidden": "true", style: { height: 1 } }), _jsx(Group, { justify: "center", py: "sm", children: _jsx(Button, { variant: "light", onClick: showMore, "data-testid": "media-library-show-more", children: "Show more" }) })] }))] }));
|
|
236
264
|
return (
|
|
@@ -242,7 +270,7 @@ export const MediaLibraryBody = ({ opened, mode, onSelect }) => {
|
|
|
242
270
|
(upload.progress !== null ? (_jsx(Progress, { value: upload.progress * 100, size: "sm", "data-testid": "upload-progress" })) : (_jsx(Progress, { value: 100, size: "sm", animated: true, "data-testid": "upload-progress-indeterminate" }))), upload.error && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "red", "data-testid": "upload-error", children: upload.error })), _jsx(TextInput, { placeholder: "Search by filename", "aria-label": "Search by filename", leftSection: _jsx(IconSearch, { size: 16 }), rightSection: filterText ? (_jsx(Tooltip, { label: "Clear search", children: _jsx(ActionIcon, { variant: "subtle", color: "gray", size: "sm", "aria-label": "Clear search", onClick: clearSearch, children: _jsx(IconX, { size: 14 }) }) })) : null, ref: searchRef, value: filterText, onChange: (event) => setFilterText(event.currentTarget.value), "data-mantine-stop-propagation": filterText ? 'true' : undefined, onKeyDown: (event) => {
|
|
243
271
|
if (event.key === 'Escape' && filterText)
|
|
244
272
|
setFilterText('');
|
|
245
|
-
}, "data-testid": "media-library-filter" }), _jsxs(Group, { justify: "space-between", wrap: "wrap", gap: "xs", children: [imageOnly ? (_jsx("span", {})) : (_jsx(SegmentedControl, { "aria-label": "File type", data: KIND_OPTIONS, value: kind, onChange: (value) => setKind(KIND_OPTIONS.find((o) => o.value === value)?.value ?? 'all'), "data-testid": "media-library-kind" })), _jsxs(Group, { gap: "xs", wrap: "wrap", children: [_jsx(Select, { "aria-label": "Sort by", data: SORT_OPTIONS, value: sort, onChange: (value) => {
|
|
273
|
+
}, "data-testid": "media-library-filter" }), _jsxs(Group, { justify: "space-between", wrap: "wrap", gap: "xs", children: [imageOnly ? (_jsx("span", {})) : (_jsxs(Group, { gap: "md", wrap: "wrap", children: [_jsx(SegmentedControl, { "aria-label": "File type", data: KIND_OPTIONS, value: kind, onChange: (value) => setKind(KIND_OPTIONS.find((o) => o.value === value)?.value ?? 'all'), "data-testid": "media-library-kind" }), admin && mode === 'manage' && (_jsx(Switch, { label: "Show removed files", checked: showRemoved, onChange: (event) => setShowRemoved(event.currentTarget.checked), "data-testid": "media-library-show-removed" }))] })), _jsxs(Group, { gap: "xs", wrap: "wrap", children: [_jsx(Select, { "aria-label": "Sort by", data: SORT_OPTIONS, value: sort, onChange: (value) => {
|
|
246
274
|
const next = SORT_OPTIONS.find((option) => option.value === value);
|
|
247
275
|
if (next)
|
|
248
276
|
setSort(next.value);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why `src` can't be cropped, as the full sentence its disabled Crop button shows; `undefined`
|
|
3
|
+
* when it can. A crop is a directive on a transform URL, which `assetUrl` writes only for a raster
|
|
4
|
+
* in the asset store, so a vector file or a file outside the store never renders one.
|
|
5
|
+
*/
|
|
6
|
+
export declare function cropBlockedReason(src: string): string | undefined;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { ASSET_PREFIXES } from '../../assets/asset-prefixes.js';
|
|
2
|
+
import { isAssetStoreSrc } from '../../assets/asset-url.js';
|
|
3
|
+
const TRANSFORM_SPACE_PREFIX = `/${ASSET_PREFIXES.transform}/`;
|
|
4
|
+
/**
|
|
5
|
+
* Why `src` can't be cropped, as the full sentence its disabled Crop button shows; `undefined`
|
|
6
|
+
* when it can. A crop is a directive on a transform URL, which `assetUrl` writes only for a raster
|
|
7
|
+
* in the asset store, so a vector file or a file outside the store never renders one.
|
|
8
|
+
*/
|
|
9
|
+
export function cropBlockedReason(src) {
|
|
10
|
+
const path = src.split(/[?#]/)[0] ?? src;
|
|
11
|
+
if (/\.svg$/i.test(path))
|
|
12
|
+
return "SVG images can't be cropped. Replace it with a PNG or JPEG to crop it.";
|
|
13
|
+
if (!isAssetStoreSrc(src)) {
|
|
14
|
+
return 'Crop works on media-library images. Replace this file with a library copy to crop it.';
|
|
15
|
+
}
|
|
16
|
+
if (!src.startsWith(TRANSFORM_SPACE_PREFIX))
|
|
17
|
+
return "This file type can't be cropped. Replace it with a PNG or JPEG to crop it.";
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure math for the ImageField crop step: converting between react-
|
|
3
|
-
* percentage-based
|
|
2
|
+
* Pure math for the ImageField crop step: converting between react-image-crop's
|
|
3
|
+
* percentage-based crop shape and CanopyCMS's normalized 0..1 `CropRect`
|
|
4
4
|
* (the same shape stored in `ImageFieldValue.crop` and accepted by the `c=`
|
|
5
5
|
* transform directive - see assets/transform-directives.ts). Kept dependency-free
|
|
6
|
-
* (no react-
|
|
6
|
+
* (no react-image-crop import) so it's trivially unit-testable and reusable
|
|
7
7
|
* without pulling in the cropper UI library.
|
|
8
8
|
*/
|
|
9
9
|
import { type CropRect } from '../../assets/transform-directives.js';
|
|
10
|
-
/**
|
|
10
|
+
/** A crop box in percentages (0..100) of the image, the shape react-image-crop reports for `unit: '%'`. */
|
|
11
11
|
export interface CropAreaPercent {
|
|
12
12
|
x: number;
|
|
13
13
|
y: number;
|
|
@@ -15,8 +15,7 @@ export interface CropAreaPercent {
|
|
|
15
15
|
height: number;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
* Convert
|
|
19
|
-
* percentage area into a normalized `CropRect`, clamped to [0,1] and then
|
|
18
|
+
* Convert a percentage crop box into a normalized `CropRect`, clamped to [0,1] and then
|
|
20
19
|
* rounded by `roundCropRect` — the same rounding the transform URL's canonical
|
|
21
20
|
* form applies, so the stored crop is already canonical.
|
|
22
21
|
*
|
|
@@ -24,12 +23,19 @@ export interface CropAreaPercent {
|
|
|
24
23
|
* zero-area selection).
|
|
25
24
|
*/
|
|
26
25
|
export declare function cropAreaPercentToRect(area: CropAreaPercent): CropRect | null;
|
|
26
|
+
/** "W:H" -> the numeric aspect ratio react-image-crop's `aspect` prop expects. Mirrors config/schemas/field.ts's ASPECT_RATIO_RE; returns undefined for anything malformed rather than throwing (defensive - the field config was already validated at schema-parse time). */
|
|
27
|
+
export declare function parseAspectRatio(aspect: string | undefined): number | undefined;
|
|
27
28
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
29
|
+
* The crop box the crop step opens with, once the image's displayed size is known.
|
|
30
|
+
* A stored crop is kept as it was drawn, unless the field has an `aspect` it no longer
|
|
31
|
+
* matches (the config changed since); then, like a first crop of a locked field, a box of that
|
|
32
|
+
* ratio centred on the image. A free field's first box is the whole frame, so Apply with no change trims nothing.
|
|
32
33
|
*/
|
|
33
|
-
export declare function
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
export declare function initialCropBox(opts: {
|
|
35
|
+
imageWidth: number;
|
|
36
|
+
imageHeight: number;
|
|
37
|
+
aspect?: number;
|
|
38
|
+
existing?: CropRect;
|
|
39
|
+
}): CropAreaPercent;
|
|
40
|
+
/** True for a rectangle covering the whole frame, which renders the image unchanged. */
|
|
41
|
+
export declare function isFullFrameCrop(rect: CropRect): boolean;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pure math for the ImageField crop step: converting between react-
|
|
3
|
-
* percentage-based
|
|
2
|
+
* Pure math for the ImageField crop step: converting between react-image-crop's
|
|
3
|
+
* percentage-based crop shape and CanopyCMS's normalized 0..1 `CropRect`
|
|
4
4
|
* (the same shape stored in `ImageFieldValue.crop` and accepted by the `c=`
|
|
5
5
|
* transform directive - see assets/transform-directives.ts). Kept dependency-free
|
|
6
|
-
* (no react-
|
|
6
|
+
* (no react-image-crop import) so it's trivially unit-testable and reusable
|
|
7
7
|
* without pulling in the cropper UI library.
|
|
8
8
|
*/
|
|
9
9
|
import { isValidCropRect, roundCropRect } from '../../assets/transform-directives.js';
|
|
@@ -13,8 +13,7 @@ function clamp01(value) {
|
|
|
13
13
|
return Math.min(1, Math.max(0, value));
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Convert
|
|
17
|
-
* percentage area into a normalized `CropRect`, clamped to [0,1] and then
|
|
16
|
+
* Convert a percentage crop box into a normalized `CropRect`, clamped to [0,1] and then
|
|
18
17
|
* rounded by `roundCropRect` — the same rounding the transform URL's canonical
|
|
19
18
|
* form applies, so the stored crop is already canonical.
|
|
20
19
|
*
|
|
@@ -31,15 +30,14 @@ export function cropAreaPercentToRect(area) {
|
|
|
31
30
|
return isValidCropRect(rect.x, rect.y, rect.w, rect.h) ? rect : null;
|
|
32
31
|
}
|
|
33
32
|
/**
|
|
34
|
-
* Inverse of `cropAreaPercentToRect` - seeds
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* of starting over.
|
|
33
|
+
* Inverse of `cropAreaPercentToRect` - seeds the crop box when re-opening
|
|
34
|
+
* the crop step for an already-cropped image, so the user sees their previous
|
|
35
|
+
* selection instead of starting over.
|
|
38
36
|
*/
|
|
39
|
-
|
|
37
|
+
function cropRectToAreaPercent(rect) {
|
|
40
38
|
return { x: rect.x * 100, y: rect.y * 100, width: rect.w * 100, height: rect.h * 100 };
|
|
41
39
|
}
|
|
42
|
-
/** "W:H" -> the numeric aspect ratio react-
|
|
40
|
+
/** "W:H" -> the numeric aspect ratio react-image-crop's `aspect` prop expects. Mirrors config/schemas/field.ts's ASPECT_RATIO_RE; returns undefined for anything malformed rather than throwing (defensive - the field config was already validated at schema-parse time). */
|
|
43
41
|
export function parseAspectRatio(aspect) {
|
|
44
42
|
if (!aspect)
|
|
45
43
|
return undefined;
|
|
@@ -48,3 +46,44 @@ export function parseAspectRatio(aspect) {
|
|
|
48
46
|
return undefined;
|
|
49
47
|
return Number(match[1]) / Number(match[2]);
|
|
50
48
|
}
|
|
49
|
+
const INITIAL_BOX_FRACTION = 0.9;
|
|
50
|
+
const FREE_BOX = { x: 0, y: 0, width: 100, height: 100 };
|
|
51
|
+
/** How far a stored crop's pixel ratio may sit from the field's `aspect` before it is replaced by a fresh box. */
|
|
52
|
+
const ASPECT_TOLERANCE = 0.02;
|
|
53
|
+
/**
|
|
54
|
+
* The crop box the crop step opens with, once the image's displayed size is known.
|
|
55
|
+
* A stored crop is kept as it was drawn, unless the field has an `aspect` it no longer
|
|
56
|
+
* matches (the config changed since); then, like a first crop of a locked field, a box of that
|
|
57
|
+
* ratio centred on the image. A free field's first box is the whole frame, so Apply with no change trims nothing.
|
|
58
|
+
*/
|
|
59
|
+
export function initialCropBox(opts) {
|
|
60
|
+
const { imageWidth, imageHeight, aspect, existing } = opts;
|
|
61
|
+
const sized = imageWidth > 0 && imageHeight > 0;
|
|
62
|
+
if (existing) {
|
|
63
|
+
const box = cropRectToAreaPercent(existing);
|
|
64
|
+
if (aspect === undefined || !sized)
|
|
65
|
+
return box;
|
|
66
|
+
const storedRatio = (existing.w * imageWidth) / (existing.h * imageHeight);
|
|
67
|
+
if (Math.abs(storedRatio - aspect) / aspect <= ASPECT_TOLERANCE)
|
|
68
|
+
return box;
|
|
69
|
+
}
|
|
70
|
+
if (aspect === undefined || !sized)
|
|
71
|
+
return { ...FREE_BOX };
|
|
72
|
+
const wide = aspect >= imageWidth / imageHeight;
|
|
73
|
+
const width = wide
|
|
74
|
+
? imageWidth * INITIAL_BOX_FRACTION
|
|
75
|
+
: imageHeight * INITIAL_BOX_FRACTION * aspect;
|
|
76
|
+
const height = wide ? width / aspect : imageHeight * INITIAL_BOX_FRACTION;
|
|
77
|
+
const widthPct = (width / imageWidth) * 100;
|
|
78
|
+
const heightPct = (height / imageHeight) * 100;
|
|
79
|
+
return {
|
|
80
|
+
x: (100 - widthPct) / 2,
|
|
81
|
+
y: (100 - heightPct) / 2,
|
|
82
|
+
width: widthPct,
|
|
83
|
+
height: heightPct,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/** True for a rectangle covering the whole frame, which renders the image unchanged. */
|
|
87
|
+
export function isFullFrameCrop(rect) {
|
|
88
|
+
return rect.x === 0 && rect.y === 0 && rect.w === 1 && rect.h === 1;
|
|
89
|
+
}
|
|
@@ -15,16 +15,19 @@ export interface UseAssetLibraryResult {
|
|
|
15
15
|
retry: () => void;
|
|
16
16
|
/** Puts an uploaded asset in the list; the one already loaded under its hash is replaced. */
|
|
17
17
|
prepend: (asset: AssetRecord) => void;
|
|
18
|
-
/** Drops a
|
|
18
|
+
/** Drops a removed asset, including from a page still on its way. */
|
|
19
19
|
remove: (hash32: string) => void;
|
|
20
|
+
/** Replaces the loaded asset with the same hash in place, such as one just removed or restored. */
|
|
21
|
+
replace: (asset: AssetRecord) => void;
|
|
20
22
|
}
|
|
21
23
|
/**
|
|
22
24
|
* Loads the whole library in the background while `opened`: page 1 first, then each following
|
|
23
25
|
* page in turn, so search and sort see every asset. Closing (or unmounting) stops the walk, and
|
|
24
|
-
* reopening starts again from page 1.
|
|
26
|
+
* reopening, or changing `includeHidden`, starts again from page 1. `includeHidden` (admins only)
|
|
27
|
+
* also lists the files removed from the library, each marked `hidden`.
|
|
25
28
|
*
|
|
26
29
|
* Finalize dedups content, so a re-uploaded file comes back as an existing record that may also
|
|
27
30
|
* arrive in a later page; assets are merged by `hash32`.
|
|
28
31
|
*/
|
|
29
|
-
export declare function useAssetLibrary(opened: boolean): UseAssetLibraryResult;
|
|
32
|
+
export declare function useAssetLibrary(opened: boolean, includeHidden?: boolean): UseAssetLibraryResult;
|
|
30
33
|
export {};
|
|
@@ -18,12 +18,13 @@ function mergeAssets(existing, incoming, removed) {
|
|
|
18
18
|
/**
|
|
19
19
|
* Loads the whole library in the background while `opened`: page 1 first, then each following
|
|
20
20
|
* page in turn, so search and sort see every asset. Closing (or unmounting) stops the walk, and
|
|
21
|
-
* reopening starts again from page 1.
|
|
21
|
+
* reopening, or changing `includeHidden`, starts again from page 1. `includeHidden` (admins only)
|
|
22
|
+
* also lists the files removed from the library, each marked `hidden`.
|
|
22
23
|
*
|
|
23
24
|
* Finalize dedups content, so a re-uploaded file comes back as an existing record that may also
|
|
24
25
|
* arrive in a later page; assets are merged by `hash32`.
|
|
25
26
|
*/
|
|
26
|
-
export function useAssetLibrary(opened) {
|
|
27
|
+
export function useAssetLibrary(opened, includeHidden = false) {
|
|
27
28
|
const client = useApiClient();
|
|
28
29
|
const [assets, setAssets] = useState([]);
|
|
29
30
|
const [firstPageLoaded, setFirstPageLoaded] = useState(false);
|
|
@@ -44,9 +45,11 @@ export function useAssetLibrary(opened) {
|
|
|
44
45
|
let page;
|
|
45
46
|
let message = '';
|
|
46
47
|
try {
|
|
47
|
-
const result = await client.assets.list(
|
|
48
|
-
|
|
49
|
-
|
|
48
|
+
const result = await client.assets.list({
|
|
49
|
+
limit: String(PAGE_SIZE),
|
|
50
|
+
...(cursor !== undefined && { cursor }),
|
|
51
|
+
...(includeHidden && { includeHidden: 'true' }),
|
|
52
|
+
});
|
|
50
53
|
if (result.ok && result.data)
|
|
51
54
|
page = result.data;
|
|
52
55
|
else
|
|
@@ -74,7 +77,7 @@ export function useAssetLibrary(opened) {
|
|
|
74
77
|
return;
|
|
75
78
|
}
|
|
76
79
|
// A cursor seen before means the listing moved under the walk (the local store restarts
|
|
77
|
-
// from page 1 when its anchor
|
|
80
|
+
// from page 1 when its anchor's meta file is gone), so pages may be missing: never report
|
|
78
81
|
// that as complete. Retry starts over.
|
|
79
82
|
if (requested.current.has(next)) {
|
|
80
83
|
failedCursor.current = undefined;
|
|
@@ -83,7 +86,7 @@ export function useAssetLibrary(opened) {
|
|
|
83
86
|
}
|
|
84
87
|
cursor = next;
|
|
85
88
|
}
|
|
86
|
-
}, [client]);
|
|
89
|
+
}, [client, includeHidden]);
|
|
87
90
|
const start = useCallback(() => {
|
|
88
91
|
generation.current += 1;
|
|
89
92
|
requested.current = new Set();
|
|
@@ -102,8 +105,9 @@ export function useAssetLibrary(opened) {
|
|
|
102
105
|
return () => {
|
|
103
106
|
generation.current += 1;
|
|
104
107
|
};
|
|
105
|
-
// Restart only when the drawer/modal (re)opens, not when `start`'s
|
|
106
|
-
|
|
108
|
+
// Restart only when the drawer/modal (re)opens or the listing changes, not when `start`'s
|
|
109
|
+
// identity changes.
|
|
110
|
+
}, [opened, includeHidden]);
|
|
107
111
|
const retry = useCallback(() => {
|
|
108
112
|
const cursor = failedCursor.current;
|
|
109
113
|
if (cursor === undefined) {
|
|
@@ -122,5 +126,8 @@ export function useAssetLibrary(opened) {
|
|
|
122
126
|
removed.current.add(hash32);
|
|
123
127
|
setAssets((prev) => prev.filter((existing) => existing.hash32 !== hash32));
|
|
124
128
|
}, []);
|
|
125
|
-
|
|
129
|
+
const replace = useCallback((asset) => {
|
|
130
|
+
setAssets((prev) => prev.map((existing) => (existing.hash32 === asset.hash32 ? asset : existing)));
|
|
131
|
+
}, []);
|
|
132
|
+
return { assets, firstPageLoaded, complete, error, retry, prepend, remove, replace };
|
|
126
133
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The height of the bar atop each half of the main region, the preview toolbar and the form
|
|
3
|
+
* header, so their bottom borders line up side by side. Border-box: it includes the bar's border,
|
|
4
|
+
* and the preview pane's own top border above its toolbar.
|
|
5
|
+
*/
|
|
6
|
+
export declare const PANE_HEADER_HEIGHT = 40;
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The height of the bar atop each half of the main region, the preview toolbar and the form
|
|
3
|
+
* header, so their bottom borders line up side by side. Border-box: it includes the bar's border,
|
|
4
|
+
* and the preview pane's own top border above its toolbar.
|
|
5
|
+
*/
|
|
6
|
+
export const PANE_HEADER_HEIGHT = 40;
|
|
@@ -111,7 +111,7 @@ const PermissionManagerDrawer = ({ opened, onClose, onExited, returnFocusTo, col
|
|
|
111
111
|
allowedUsers: currentTarget.allowedUsers,
|
|
112
112
|
});
|
|
113
113
|
}, [annotatedTree, localPermissions, updateNodePermission]);
|
|
114
|
-
return (_jsx(StagedChangesDrawer, { opened: opened, onClose: onClose, title: "Permissions",
|
|
114
|
+
return (_jsx(StagedChangesDrawer, { opened: opened, onClose: onClose, title: "Permissions", size: 700, isDirty: isDirty, isSaving: isSaving, canEdit: canEdit, saveLabel: "Save permissions", onSave: handleSave, onDiscard: handleDiscard, onExited: onExited, returnFocusTo: returnFocusTo, children: _jsxs(Stack, { h: "100%", style: { display: 'flex', flexDirection: 'column' }, gap: 0, children: [!canEdit && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "yellow", mb: "sm", title: "Read-only", children: "You need admin access to edit permissions" })), error && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: errorIsConflict ? 'yellow' : 'red', mb: "sm", title: errorIsConflict ? 'Not saved' : 'Error', withCloseButton: true, onClose: () => setError(null), children: error })), groupLoadError && (_jsx(Alert, { icon: _jsx(IconAlertCircle, { size: 16 }), color: "orange", mb: "sm", title: "Warning", withCloseButton: true, onClose: clearGroupLoadError, children: groupLoadError })), _jsxs(Group, { gap: "xs", pb: "sm", children: [_jsx(Button, { variant: "subtle", onClick: expandAll, children: "Expand all" }), _jsx(Button, { variant: "subtle", onClick: collapseAll, children: "Collapse all" })] }), _jsx(ScrollArea, { style: { flex: 1 }, pb: "md", children: loading ? (_jsxs(Group, { justify: "center", py: "xl", children: [_jsx(Loader, { size: "md" }), _jsx(Text, { size: "sm", c: "dimmed", children: "Loading permissions..." })] })) : (_jsx(PermissionTree, { node: annotatedTree, expandedNodes: expandedNodes, selectedNode: selectedNode, canEdit: canEdit, groups: groupSelectData, activeLevel: activeLevel, userSearchResults: userSearchResults, isSearchingUsers: isSearchingUsers, showUserSearch: showUserSearch, userSearchQuery: userSearchQuery, userSearchError: userSearchError, showGroupSearch: showGroupSearch, groupSearchQuery: groupSearchQuery, filteredGroups: filteredGroups, onToggle: toggleNode, onSelect: selectNode, onSetActiveLevel: setActiveLevel, onUpdatePermission: updateNodePermission, onSearchUsers: setUserSearchQuery, onGetUserMetadata: onGetUserMetadata, onToggleUserSearch: toggleUserSearch, onAddUser: handleAddUser, onRemoveUser: handleRemoveUser, onSearchGroups: setGroupSearchQuery, onToggleGroupSearch: setShowGroupSearch, onAddGroup: handleAddGroup, onRemoveGroup: handleRemoveGroup })) })] }) }));
|
|
115
115
|
};
|
|
116
116
|
export { usePermissionTree } from './hooks/usePermissionTree.js';
|
|
117
117
|
export { useGroupsAndUsers } from './hooks/useGroupsAndUsers.js';
|
|
@@ -5,6 +5,7 @@ export declare const CANOPY_PREVIEW_HIGHLIGHT = "canopycms:preview:highlight";
|
|
|
5
5
|
export declare const CANOPY_PREVIEW_READY = "canopycms:preview:ready";
|
|
6
6
|
export declare const CANOPY_PREVIEW_ERROR = "canopycms:preview:error";
|
|
7
7
|
export declare const CANOPY_PREVIEW_MARKS = "canopycms:preview:marks";
|
|
8
|
+
export declare const CANOPY_PREVIEW_NAVIGATE = "canopycms:preview:navigate";
|
|
8
9
|
/**
|
|
9
10
|
* The shape of the preview's `isLoading` for data of type `T`: a `boolean` at each reference
|
|
10
11
|
* position, `true` while the editor is still resolving that reference, under the same keys and
|
|
@@ -49,6 +50,16 @@ export declare const isOpaqueOrigin: (origin: string) => boolean;
|
|
|
49
50
|
* expected editor origin (same-origin unless `editorOrigin` is configured).
|
|
50
51
|
*/
|
|
51
52
|
export declare const isTrustedEditorMessage: (event: MessageEvent, editorOrigin?: string) => boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Preview → editor, once the page listens for drafts: the editor answers with the current draft.
|
|
55
|
+
* `siteChrome` says the page renders the site's header and footer when its URL carries
|
|
56
|
+
* `chrome=1`, so the editor offers the toggle that adds it.
|
|
57
|
+
*/
|
|
58
|
+
export interface PreviewReadyMessage {
|
|
59
|
+
type: typeof CANOPY_PREVIEW_READY;
|
|
60
|
+
path: string;
|
|
61
|
+
siteChrome?: boolean;
|
|
62
|
+
}
|
|
52
63
|
export interface PreviewFocusMessage {
|
|
53
64
|
type: typeof CANOPY_PREVIEW_FOCUS;
|
|
54
65
|
entryPath: string;
|
|
@@ -74,6 +85,11 @@ export interface PreviewMarksMessage {
|
|
|
74
85
|
count: number;
|
|
75
86
|
paths?: string[];
|
|
76
87
|
}
|
|
88
|
+
/** Preview → editor: a plain click on a link to another page of this origin, left unfollowed. */
|
|
89
|
+
export interface PreviewNavigateMessage {
|
|
90
|
+
type: typeof CANOPY_PREVIEW_NAVIGATE;
|
|
91
|
+
href: string;
|
|
92
|
+
}
|
|
77
93
|
/**
|
|
78
94
|
* Preview → editor report that the current draft fails to compile/render
|
|
79
95
|
* (e.g. malformed MDX). `message: null` clears an earlier error.
|
|
@@ -93,6 +109,11 @@ export declare const useCanopyPreview: <T>(opts: {
|
|
|
93
109
|
initialData: T;
|
|
94
110
|
/** Editor origin to trust for preview messages. Defaults to this page's own origin. */
|
|
95
111
|
editorOrigin?: string;
|
|
112
|
+
/**
|
|
113
|
+
* This page renders the site's header and footer when its URL carries `chrome=1`, so the editor
|
|
114
|
+
* offers a toggle for them; see `PreviewReadyMessage`.
|
|
115
|
+
*/
|
|
116
|
+
siteChrome?: boolean;
|
|
96
117
|
}) => {
|
|
97
118
|
data: T;
|
|
98
119
|
isLoading: PreviewLoadingState<T>;
|
|
@@ -106,6 +127,7 @@ export declare const useCanopyPreview: <T>(opts: {
|
|
|
106
127
|
*/
|
|
107
128
|
export declare const usePreviewData: <T>(path: string, initialData: T, opts?: {
|
|
108
129
|
editorOrigin?: string;
|
|
130
|
+
siteChrome?: boolean;
|
|
109
131
|
}) => {
|
|
110
132
|
data: T;
|
|
111
133
|
isLoading: PreviewLoadingState<T>;
|
|
@@ -126,6 +148,7 @@ export declare const usePreviewHighlight: (opts?: {
|
|
|
126
148
|
}) => boolean;
|
|
127
149
|
/**
|
|
128
150
|
* Hook for preview pages to emit focus messages when elements with data-canopy-path are clicked.
|
|
151
|
+
* A click that follows a link, ours or the browser's, goes to another page, not to a field.
|
|
129
152
|
*/
|
|
130
153
|
export declare const usePreviewFocusEmitter: (entryPath: string, opts?: {
|
|
131
154
|
editorOrigin?: string;
|