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.
Files changed (136) hide show
  1. package/CHANGELOG.md +202 -43
  2. package/dist/api/assets.d.ts +27 -7
  3. package/dist/api/assets.js +118 -47
  4. package/dist/api/client.d.ts +5 -3
  5. package/dist/api/client.js +7 -3
  6. package/dist/api/index.d.ts +1 -1
  7. package/dist/assets/asset-prefixes.d.ts +4 -1
  8. package/dist/assets/asset-prefixes.js +3 -1
  9. package/dist/assets/editor-thumbnail-widths.d.ts +11 -0
  10. package/dist/assets/editor-thumbnail-widths.js +7 -0
  11. package/dist/assets/finalize.d.ts +7 -6
  12. package/dist/assets/finalize.js +29 -10
  13. package/dist/assets/keys.d.ts +4 -1
  14. package/dist/assets/keys.js +4 -1
  15. package/dist/assets/materialize.d.ts +17 -2
  16. package/dist/assets/materialize.js +57 -4
  17. package/dist/assets/store-local.d.ts +18 -9
  18. package/dist/assets/store-local.js +58 -26
  19. package/dist/assets/store-s3.d.ts +25 -6
  20. package/dist/assets/store-s3.js +81 -19
  21. package/dist/assets/types.d.ts +33 -4
  22. package/dist/cli/cli.js +151 -56
  23. package/dist/config/types.d.ts +3 -2
  24. package/dist/editor/BranchManager.js +2 -1
  25. package/dist/editor/CommentsPanel.js +67 -29
  26. package/dist/editor/Editor.d.ts +3 -0
  27. package/dist/editor/Editor.js +93 -39
  28. package/dist/editor/EntryNavigator.d.ts +17 -5
  29. package/dist/editor/EntryNavigator.js +66 -31
  30. package/dist/editor/FormHeader.d.ts +2 -1
  31. package/dist/editor/FormHeader.js +9 -5
  32. package/dist/editor/FormRenderer.d.ts +7 -2
  33. package/dist/editor/FormRenderer.js +51 -16
  34. package/dist/editor/PreviewFrame.d.ts +13 -2
  35. package/dist/editor/PreviewFrame.js +62 -5
  36. package/dist/editor/PreviewPane.d.ts +17 -1
  37. package/dist/editor/PreviewPane.js +55 -8
  38. package/dist/editor/PreviewToolbarTools.d.ts +8 -0
  39. package/dist/editor/PreviewToolbarTools.js +9 -1
  40. package/dist/editor/branch-status.d.ts +6 -0
  41. package/dist/editor/branch-status.js +10 -0
  42. package/dist/editor/comments/BranchComments.d.ts +2 -4
  43. package/dist/editor/comments/BranchComments.js +7 -11
  44. package/dist/editor/comments/ThreadCarousel.d.ts +2 -8
  45. package/dist/editor/comments/ThreadCarousel.js +4 -49
  46. package/dist/editor/comments/ThreadReplyControls.js +5 -2
  47. package/dist/editor/components/BranchLockBanner.d.ts +8 -7
  48. package/dist/editor/components/BranchLockBanner.js +30 -3
  49. package/dist/editor/components/BranchPill.d.ts +5 -4
  50. package/dist/editor/components/BranchPill.js +6 -8
  51. package/dist/editor/components/BranchRowLock.d.ts +12 -0
  52. package/dist/editor/components/BranchRowLock.js +19 -0
  53. package/dist/editor/components/BranchesDrawer.js +3 -3
  54. package/dist/editor/components/EditorHeader.d.ts +2 -6
  55. package/dist/editor/components/EditorHeader.js +14 -28
  56. package/dist/editor/components/EditorRail.d.ts +3 -4
  57. package/dist/editor/components/EditorRail.js +3 -4
  58. package/dist/editor/components/EntryBreadcrumb.d.ts +9 -10
  59. package/dist/editor/components/EntryBreadcrumb.js +8 -11
  60. package/dist/editor/components/PreviewLinkToast.d.ts +5 -0
  61. package/dist/editor/components/PreviewLinkToast.js +33 -0
  62. package/dist/editor/components/QuickOpenPalette.d.ts +7 -1
  63. package/dist/editor/components/QuickOpenPalette.js +6 -1
  64. package/dist/editor/components/RenameEntryModal.d.ts +6 -1
  65. package/dist/editor/components/RenameEntryModal.js +20 -5
  66. package/dist/editor/components/SaveSplitButton.d.ts +3 -2
  67. package/dist/editor/components/SaveSplitButton.js +25 -12
  68. package/dist/editor/components/StagedChangesDrawer.d.ts +0 -1
  69. package/dist/editor/components/StagedChangesDrawer.js +5 -4
  70. package/dist/editor/components/UndoToastMessage.d.ts +6 -5
  71. package/dist/editor/components/UndoToastMessage.js +6 -4
  72. package/dist/editor/components/UnsavedMark.d.ts +9 -0
  73. package/dist/editor/components/UnsavedMark.js +20 -0
  74. package/dist/editor/components/UserBadge.js +13 -11
  75. package/dist/editor/copy.d.ts +4 -0
  76. package/dist/editor/copy.js +4 -0
  77. package/dist/editor/editor-utils.d.ts +49 -9
  78. package/dist/editor/editor-utils.js +117 -16
  79. package/dist/editor/entry-search.d.ts +4 -2
  80. package/dist/editor/entry-search.js +2 -3
  81. package/dist/editor/fields/FieldLabel.d.ts +1 -1
  82. package/dist/editor/fields/FieldLabel.js +1 -1
  83. package/dist/editor/fields/ImageField.d.ts +1 -1
  84. package/dist/editor/fields/ImageField.js +36 -27
  85. package/dist/editor/fields/ObjectField.d.ts +13 -2
  86. package/dist/editor/fields/ObjectField.js +38 -9
  87. package/dist/editor/fields/entry-link/EntryLinkContext.d.ts +2 -0
  88. package/dist/editor/fields/entry-link/InsertEntryLink.js +3 -1
  89. package/dist/editor/group-manager/index.js +1 -1
  90. package/dist/editor/hooks/useBranchManager.d.ts +2 -0
  91. package/dist/editor/hooks/useBranchManager.js +1 -0
  92. package/dist/editor/hooks/useEditorLayout.d.ts +4 -0
  93. package/dist/editor/hooks/useEditorLayout.js +5 -0
  94. package/dist/editor/hooks/useEntryManager.d.ts +2 -0
  95. package/dist/editor/hooks/useEntryManager.js +10 -8
  96. package/dist/editor/media/AssetCard.d.ts +6 -3
  97. package/dist/editor/media/AssetCard.js +24 -9
  98. package/dist/editor/media/AssetListRow.d.ts +9 -4
  99. package/dist/editor/media/AssetListRow.js +18 -9
  100. package/dist/editor/media/CropStep.d.ts +8 -10
  101. package/dist/editor/media/CropStep.js +39 -27
  102. package/dist/editor/media/MediaLibrary.js +5 -4
  103. package/dist/editor/media/MediaLibraryBody.d.ts +1 -1
  104. package/dist/editor/media/MediaLibraryBody.js +65 -37
  105. package/dist/editor/media/crop-availability.d.ts +6 -0
  106. package/dist/editor/media/crop-availability.js +19 -0
  107. package/dist/editor/media/crop-math.d.ts +19 -13
  108. package/dist/editor/media/crop-math.js +50 -11
  109. package/dist/editor/media/useAssetLibrary.d.ts +6 -3
  110. package/dist/editor/media/useAssetLibrary.js +17 -10
  111. package/dist/editor/pane-header.d.ts +6 -0
  112. package/dist/editor/pane-header.js +6 -0
  113. package/dist/editor/permission-manager/index.js +1 -1
  114. package/dist/editor/preview-bridge.d.ts +23 -0
  115. package/dist/editor/preview-bridge.js +105 -4
  116. package/dist/editor/preview-path.d.ts +16 -4
  117. package/dist/editor/preview-path.js +80 -19
  118. package/dist/editor/shell/ActivityRail.d.ts +1 -3
  119. package/dist/editor/shell/ActivityRail.js +3 -3
  120. package/dist/editor/shell/EditorShell.d.ts +15 -0
  121. package/dist/editor/shell/EditorShell.js +17 -1
  122. package/dist/editor/shell/OverlayHeader.d.ts +19 -0
  123. package/dist/editor/shell/OverlayHeader.js +15 -0
  124. package/dist/editor/shell/PaneLayout.d.ts +6 -1
  125. package/dist/editor/shell/PaneLayout.js +68 -13
  126. package/dist/editor/shell/SidePanelHost.d.ts +8 -6
  127. package/dist/editor/shell/SidePanelHost.js +70 -33
  128. package/dist/editor/shell/TopBar.js +2 -1
  129. package/dist/editor/theme.d.ts +1 -0
  130. package/dist/editor/theme.js +19 -1
  131. package/dist/editor/undo-removal.d.ts +19 -10
  132. package/dist/editor/undo-removal.js +44 -16
  133. package/dist/server.d.ts +2 -0
  134. package/dist/version.d.ts +1 -1
  135. package/dist/version.js +1 -1
  136. 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/delete core for both MediaLibrary
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
- // Mirrors the server's rule in api/assets.ts's deleteAssetHandler: an admin
55
- // may delete anything, anyone else only what they uploaded. Kept per-asset
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 canDeleteAsset = (asset) => {
65
+ const canChangeVisibility = (asset) => {
59
66
  if (mode !== 'manage')
60
67
  return false;
61
- if (isAdmin(userContext?.groups))
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 removed card itself is gone.
76
- // `id` re-keys the region's text, so a second remove with the same words is announced too.
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 removed card takes its focused button with it, so focus moves to the search field once the
80
- // removal has rendered (Mantine returns focus to the trash button when the confirm closes).
81
- const focusAfterRemove = useRef(false);
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 (!focusAfterRemove.current)
94
+ if (!focusAfterAction.current)
84
95
  return;
85
- focusAfterRemove.current = false;
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
- const handleDelete = async (asset) => {
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 failure = (detail) => `Couldn't remove ${asset.filename} from the library. ${detail}`;
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 client.assets.delete({ key: asset.hash32 });
169
+ const result = await request();
152
170
  if (!result.ok) {
153
- // An editor sees this button only for their own upload, and the endpoint answers a
154
- // missing file with the same 403 as someone else's, so their 403 means it is gone.
155
- const alreadyGone = result.status === 403 && !isNonApiResponse(result) && !isAdmin(userContext?.groups);
156
- setActionError(alreadyGone ? failure('It may have been removed already.') : generic(result.error));
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
- library.remove(asset.hash32);
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
- setActionError(generic(getErrorMessage(err)));
182
+ fail(getErrorMessage(err));
168
183
  }
169
184
  };
170
- const handleDeleteClick = (asset) => {
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: ["Remove \u201C", asset.filename, "\u201D from the media library? Content that already uses this file keeps working, and the file itself is not deleted."] })),
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 handleDelete(asset);
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, onDelete: canDeleteAsset(asset) ? () => handleDeleteClick(asset) : undefined }, asset.hash32))) }));
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, onDelete: canDeleteAsset(asset) ? () => handleDeleteClick(asset) : undefined }, asset.hash32))) }));
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-easy-crop's
3
- * percentage-based `Area` shape and CanopyCMS's normalized 0..1 `CropRect`
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-easy-crop import) so it's trivially unit-testable and reusable
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
- /** react-easy-crop's `Area` shape, expressed in percentages (0..100). */
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 react-easy-crop's `onCropComplete(croppedArea, croppedAreaPixels)`
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
- * Inverse of `cropAreaPercentToRect` - seeds react-easy-crop's
29
- * `initialCroppedAreaPercentages` when re-opening the crop step for an
30
- * already-cropped image, so the user sees their previous selection instead
31
- * of starting over.
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 cropRectToAreaPercent(rect: CropRect): CropAreaPercent;
34
- /** "W:H" -> the numeric aspect ratio react-easy-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). */
35
- export declare function parseAspectRatio(aspect: string | undefined): number | undefined;
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-easy-crop's
3
- * percentage-based `Area` shape and CanopyCMS's normalized 0..1 `CropRect`
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-easy-crop import) so it's trivially unit-testable and reusable
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 react-easy-crop's `onCropComplete(croppedArea, croppedAreaPixels)`
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 react-easy-crop's
35
- * `initialCroppedAreaPercentages` when re-opening the crop step for an
36
- * already-cropped image, so the user sees their previous selection instead
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
- export function cropRectToAreaPercent(rect) {
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-easy-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). */
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 deleted asset, including from a page still on its way. */
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(cursor === undefined
48
- ? { limit: String(PAGE_SIZE) }
49
- : { cursor, limit: String(PAGE_SIZE) });
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 asset is deleted), so pages may be missing: never report
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 identity changes.
106
- }, [opened]);
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
- return { assets, firstPageLoaded, complete, error, retry, prepend, remove };
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", description: "Manage content access by path (read, edit, review)", 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 })) })] }) }));
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;