canopycms 0.2.0-int.119 → 0.2.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/CHANGELOG.md +123 -62
- package/dist/api/assets.js +20 -10
- 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 +15 -9
- package/dist/assets/materialize.d.ts +17 -2
- package/dist/assets/materialize.js +57 -4
- package/dist/assets/store-s3.d.ts +7 -3
- package/dist/assets/store-s3.js +25 -6
- package/dist/assets/types.d.ts +7 -2
- package/dist/cli/cli.js +40 -23
- package/dist/config/types.d.ts +3 -2
- package/dist/dev-content-watcher.js +8 -1
- package/dist/editor/Editor.js +60 -7
- package/dist/editor/EntryNavigator.d.ts +9 -4
- package/dist/editor/EntryNavigator.js +39 -17
- package/dist/editor/PreviewFrame.d.ts +3 -1
- package/dist/editor/PreviewFrame.js +43 -2
- package/dist/editor/PreviewPane.d.ts +2 -0
- package/dist/editor/components/BranchLockBanner.d.ts +7 -8
- package/dist/editor/components/BranchLockBanner.js +25 -2
- package/dist/editor/components/BranchRowLock.d.ts +14 -0
- package/dist/editor/components/BranchRowLock.js +21 -0
- package/dist/editor/components/EditorHeader.js +3 -18
- package/dist/editor/components/PreviewLinkToast.d.ts +5 -0
- package/dist/editor/components/PreviewLinkToast.js +33 -0
- 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/hooks/editor-history.d.ts +7 -0
- package/dist/editor/hooks/editor-history.js +80 -0
- package/dist/editor/hooks/useBranchActions.js +3 -1
- package/dist/editor/hooks/useBranchManager.d.ts +2 -0
- package/dist/editor/hooks/useBranchManager.js +6 -2
- package/dist/editor/hooks/useEntryManager.d.ts +9 -2
- package/dist/editor/hooks/useEntryManager.js +131 -17
- package/dist/editor/media/AssetCard.js +4 -2
- package/dist/editor/media/AssetListRow.js +4 -2
- package/dist/editor/media/CropStep.d.ts +8 -10
- package/dist/editor/media/CropStep.js +39 -27
- 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/preview-bridge.d.ts +7 -0
- package/dist/editor/preview-bridge.js +93 -1
- package/dist/editor/preview-path.d.ts +5 -0
- package/dist/editor/preview-path.js +18 -0
- package/dist/editor/theme.d.ts +1 -0
- package/dist/editor/theme.js +38 -1
- 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,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
|
+
}
|
|
@@ -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
|
|
@@ -84,6 +85,11 @@ export interface PreviewMarksMessage {
|
|
|
84
85
|
count: number;
|
|
85
86
|
paths?: string[];
|
|
86
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
|
+
}
|
|
87
93
|
/**
|
|
88
94
|
* Preview → editor report that the current draft fails to compile/render
|
|
89
95
|
* (e.g. malformed MDX). `message: null` clears an earlier error.
|
|
@@ -142,6 +148,7 @@ export declare const usePreviewHighlight: (opts?: {
|
|
|
142
148
|
}) => boolean;
|
|
143
149
|
/**
|
|
144
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.
|
|
145
152
|
*/
|
|
146
153
|
export declare const usePreviewFocusEmitter: (entryPath: string, opts?: {
|
|
147
154
|
editorOrigin?: string;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* hosts through `canopycms/preview`, so everything it reaches stays free of CSS and editor UI
|
|
5
5
|
* (`pnpm lint:bundle`); the editor's side is `PreviewFrame.tsx`.
|
|
6
6
|
*/
|
|
7
|
-
import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
|
7
|
+
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
8
8
|
import { createFieldProps } from './field-props.js';
|
|
9
9
|
import { setPreviewAssetBase } from './preview-asset-base.js';
|
|
10
10
|
import { isSamePreviewPath } from './preview-path.js';
|
|
@@ -15,6 +15,7 @@ export const CANOPY_PREVIEW_HIGHLIGHT = 'canopycms:preview:highlight';
|
|
|
15
15
|
export const CANOPY_PREVIEW_READY = 'canopycms:preview:ready';
|
|
16
16
|
export const CANOPY_PREVIEW_ERROR = 'canopycms:preview:error';
|
|
17
17
|
export const CANOPY_PREVIEW_MARKS = 'canopycms:preview:marks';
|
|
18
|
+
export const CANOPY_PREVIEW_NAVIGATE = 'canopycms:preview:navigate';
|
|
18
19
|
/**
|
|
19
20
|
* Resolve a (possibly relative) URL to an origin for postMessage targeting.
|
|
20
21
|
* Falls back to the page's own origin, so same-origin editor/preview setups
|
|
@@ -79,6 +80,7 @@ export const useCanopyPreview = (opts) => {
|
|
|
79
80
|
});
|
|
80
81
|
const highlightEnabled = usePreviewHighlight(bridgeOpts);
|
|
81
82
|
usePreviewFocusEmitter(resolvedPath, bridgeOpts);
|
|
83
|
+
usePreviewLinkNavigation({ editorOrigin, highlighting: highlightEnabled });
|
|
82
84
|
const fieldProps = createFieldProps();
|
|
83
85
|
/**
|
|
84
86
|
* Report that the current draft fails to compile/render (the editor surfaces it
|
|
@@ -271,8 +273,96 @@ export const usePreviewHighlight = (opts) => {
|
|
|
271
273
|
}, [enabled, editorOrigin]);
|
|
272
274
|
return enabled;
|
|
273
275
|
};
|
|
276
|
+
const isPlainClick = (event) => event.button === 0 && !event.metaKey && !event.ctrlKey && !event.shiftKey && !event.altKey;
|
|
277
|
+
const linkClick = (event) => {
|
|
278
|
+
// The composed path reaches a link inside a shadow root, which the retargeted `target` hides.
|
|
279
|
+
const anchor = event
|
|
280
|
+
.composedPath()
|
|
281
|
+
.find((node) => node instanceof Element && node.matches('a[href], area[href]'));
|
|
282
|
+
if (!anchor)
|
|
283
|
+
return undefined;
|
|
284
|
+
let url;
|
|
285
|
+
try {
|
|
286
|
+
url = new URL(anchor.getAttribute('href') ?? '', document.baseURI);
|
|
287
|
+
}
|
|
288
|
+
catch {
|
|
289
|
+
return undefined;
|
|
290
|
+
}
|
|
291
|
+
if (url.protocol !== 'http:' && url.protocol !== 'https:')
|
|
292
|
+
return undefined;
|
|
293
|
+
const here = new URL(window.location.href);
|
|
294
|
+
if (url.href.split('#')[0] === here.href.split('#')[0])
|
|
295
|
+
return undefined;
|
|
296
|
+
const target = anchor.getAttribute('target') ?? document.querySelector('base[target]')?.getAttribute('target');
|
|
297
|
+
if (!isPlainClick(event) || (target && target !== '_self') || anchor.hasAttribute('download')) {
|
|
298
|
+
return { kind: 'browser' };
|
|
299
|
+
}
|
|
300
|
+
return { kind: url.origin === here.origin ? 'navigate' : 'open', href: url.href };
|
|
301
|
+
};
|
|
302
|
+
const isInMark = (event) => event
|
|
303
|
+
.composedPath()
|
|
304
|
+
.some((node) => node instanceof Element && node.hasAttribute('data-canopy-path'));
|
|
305
|
+
/** Clicks `usePreviewLinkNavigation` followed itself, so click-to-focus leaves them be. */
|
|
306
|
+
const followedLinkClicks = new WeakSet();
|
|
307
|
+
/**
|
|
308
|
+
* Hook for preview pages to keep link clicks off public pages that may refuse framing. Captured on
|
|
309
|
+
* the window, so it runs before the page's own handlers, a client router's included.
|
|
310
|
+
*/
|
|
311
|
+
const usePreviewLinkNavigation = (opts) => {
|
|
312
|
+
const editorOrigin = opts.editorOrigin;
|
|
313
|
+
const highlighting = useRef(opts.highlighting);
|
|
314
|
+
useEffect(() => {
|
|
315
|
+
highlighting.current = opts.highlighting;
|
|
316
|
+
});
|
|
317
|
+
useEffect(() => {
|
|
318
|
+
if (window.parent === window)
|
|
319
|
+
return;
|
|
320
|
+
const targetOrigin = resolveMessageOrigin(editorOrigin);
|
|
321
|
+
if (isOpaqueOrigin(targetOrigin))
|
|
322
|
+
return;
|
|
323
|
+
// Links stay the browser's until the editor speaks: a frame that isn't a trusted editor, such
|
|
324
|
+
// as a site embedding a public page, would drop the message and leave every link dead.
|
|
325
|
+
let connected = false;
|
|
326
|
+
const handleMessage = (event) => {
|
|
327
|
+
const type = event.data?.type;
|
|
328
|
+
if (type !== CANOPY_PREVIEW_HIGHLIGHT && type !== CANOPY_PREVIEW_MESSAGE)
|
|
329
|
+
return;
|
|
330
|
+
if (isTrustedEditorMessage(event, editorOrigin))
|
|
331
|
+
connected = true;
|
|
332
|
+
};
|
|
333
|
+
const handleClick = (event) => {
|
|
334
|
+
if (!connected || event.defaultPrevented)
|
|
335
|
+
return;
|
|
336
|
+
const link = linkClick(event);
|
|
337
|
+
if (!link)
|
|
338
|
+
return;
|
|
339
|
+
// While fields are outlined, a marked link is a field to pick, which click-to-focus does.
|
|
340
|
+
if (highlighting.current && isPlainClick(event) && isInMark(event)) {
|
|
341
|
+
event.preventDefault();
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (link.kind === 'browser')
|
|
345
|
+
return;
|
|
346
|
+
event.preventDefault();
|
|
347
|
+
followedLinkClicks.add(event);
|
|
348
|
+
if (link.kind === 'open') {
|
|
349
|
+
window.open(link.href, '_blank', 'noopener,noreferrer');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const msg = { type: CANOPY_PREVIEW_NAVIGATE, href: link.href };
|
|
353
|
+
window.parent.postMessage(msg, targetOrigin);
|
|
354
|
+
};
|
|
355
|
+
window.addEventListener('message', handleMessage);
|
|
356
|
+
window.addEventListener('click', handleClick, true);
|
|
357
|
+
return () => {
|
|
358
|
+
window.removeEventListener('message', handleMessage);
|
|
359
|
+
window.removeEventListener('click', handleClick, true);
|
|
360
|
+
};
|
|
361
|
+
}, [editorOrigin]);
|
|
362
|
+
};
|
|
274
363
|
/**
|
|
275
364
|
* Hook for preview pages to emit focus messages when elements with data-canopy-path are clicked.
|
|
365
|
+
* A click that follows a link, ours or the browser's, goes to another page, not to a field.
|
|
276
366
|
*/
|
|
277
367
|
export const usePreviewFocusEmitter = (entryPath, opts) => {
|
|
278
368
|
const editorOrigin = opts?.editorOrigin;
|
|
@@ -280,6 +370,8 @@ export const usePreviewFocusEmitter = (entryPath, opts) => {
|
|
|
280
370
|
if (window.parent === window)
|
|
281
371
|
return;
|
|
282
372
|
const handleClick = (event) => {
|
|
373
|
+
if (followedLinkClicks.has(event) || (!event.defaultPrevented && linkClick(event)))
|
|
374
|
+
return;
|
|
283
375
|
const target = event.target;
|
|
284
376
|
if (!target)
|
|
285
377
|
return;
|
|
@@ -27,3 +27,8 @@ export declare const siteChromeSrc: (src: string, mount?: PreviewMount) => strin
|
|
|
27
27
|
* absolute `previewPrefix` mount is what was taken out.
|
|
28
28
|
*/
|
|
29
29
|
export declare const displayPreviewPath: (src: string, mount?: PreviewMount) => string;
|
|
30
|
+
/**
|
|
31
|
+
* The page `url` shows, as origin plus `displayPreviewPath` without a trailing slash, so a preview
|
|
32
|
+
* link and the entry `src` framing its page match, under the `previewPrefix` mount or not.
|
|
33
|
+
*/
|
|
34
|
+
export declare const previewPageKey: (url: string, mount: PreviewMount, base: string) => string | undefined;
|
|
@@ -105,3 +105,21 @@ export const displayPreviewPath = (src, mount = {}) => {
|
|
|
105
105
|
? `//${parsed.host}${path}`
|
|
106
106
|
: `${parsed.protocol}//${parsed.host}${path}`;
|
|
107
107
|
};
|
|
108
|
+
/**
|
|
109
|
+
* The page `url` shows, as origin plus `displayPreviewPath` without a trailing slash, so a preview
|
|
110
|
+
* link and the entry `src` framing its page match, under the `previewPrefix` mount or not.
|
|
111
|
+
*/
|
|
112
|
+
export const previewPageKey = (url, mount, base) => {
|
|
113
|
+
let parsed;
|
|
114
|
+
try {
|
|
115
|
+
parsed = new URL(url, base);
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
// A relative mount is a path on the editor's origin, so a URL there is matched to it as a path.
|
|
121
|
+
const spelled = splitPreviewPrefix(parsed.href, mount) || parsed.origin !== new URL(base).origin
|
|
122
|
+
? parsed.href
|
|
123
|
+
: `${parsed.pathname}${parsed.search}`;
|
|
124
|
+
return `${parsed.origin}${normalizePreviewPath(displayPreviewPath(spelled, mount))}`;
|
|
125
|
+
};
|
package/dist/editor/theme.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import '@mantine/core/styles.css';
|
|
|
4
4
|
import '@mantine/dates/styles.css';
|
|
5
5
|
import '@mantine/notifications/styles.css';
|
|
6
6
|
import '@mantine/dropzone/styles.css';
|
|
7
|
+
import 'react-image-crop/dist/ReactCrop.css';
|
|
7
8
|
import '@mdxeditor/editor/style.css';
|
|
8
9
|
export type CanopyColorInput = MantineColorsTuple | string;
|
|
9
10
|
export interface CanopyColorConfig {
|
package/dist/editor/theme.js
CHANGED
|
@@ -10,6 +10,7 @@ import '@mantine/core/styles.css';
|
|
|
10
10
|
import '@mantine/dates/styles.css';
|
|
11
11
|
import '@mantine/notifications/styles.css';
|
|
12
12
|
import '@mantine/dropzone/styles.css';
|
|
13
|
+
import 'react-image-crop/dist/ReactCrop.css';
|
|
13
14
|
import '@mdxeditor/editor/style.css';
|
|
14
15
|
const defaultCanopyColors = {
|
|
15
16
|
brand: '#3b82f6',
|
|
@@ -318,7 +319,12 @@ const canopyCssVariablesResolver = (theme) => ({
|
|
|
318
319
|
* What Mantine draws from class rules, out of the theme variables' reach: unchecked control borders,
|
|
319
320
|
* Switch thumbs, white text on the primary colour (moved to the fill), and dimmed text Mantine
|
|
320
321
|
* draws itself (a blocked menu item, a combobox's empty message, outside days). Outside days lose
|
|
321
|
-
* Mantine's half opacity, under 4.5:1.
|
|
322
|
+
* Mantine's half opacity, under 4.5:1. The crop box and its handles sit on an arbitrary image, where
|
|
323
|
+
* no single colour holds 3:1, so they take a white and black pair. A navigator row's lock is
|
|
324
|
+
* drawn only while its row is hovered, focused (Mantine's Tree focuses the `li` around the row, on click and arrow keys), has focus
|
|
325
|
+
* within, or is selected. It hides by opacity alone, so it keeps its place and stays in the tab order
|
|
326
|
+
* and the accessibility tree; while hidden it takes no pointer events, so a tap on its slot reaches
|
|
327
|
+
* the row.
|
|
322
328
|
*/
|
|
323
329
|
const CANOPY_STYLES = `
|
|
324
330
|
.mantine-Checkbox-input:not(:checked, :disabled, [data-indeterminate], [data-error]),
|
|
@@ -337,6 +343,37 @@ const CANOPY_STYLES = `
|
|
|
337
343
|
--switch-thumb-bg: var(--canopy-switch-thumb-on, var(--mantine-color-white));
|
|
338
344
|
--switch-thumb-bd: var(--canopy-switch-thumb-on, var(--mantine-color-white));
|
|
339
345
|
}
|
|
346
|
+
.ReactCrop .ReactCrop__crop-selection:focus {
|
|
347
|
+
outline: 2px solid var(--mantine-color-white);
|
|
348
|
+
outline-offset: 0;
|
|
349
|
+
box-shadow: 0 0 0 4px var(--mantine-color-black);
|
|
350
|
+
}
|
|
351
|
+
.ReactCrop .ReactCrop__drag-handle {
|
|
352
|
+
--rc-drag-handle-bg-colour: var(--mantine-color-white);
|
|
353
|
+
border-color: var(--mantine-color-black);
|
|
354
|
+
}
|
|
355
|
+
.ReactCrop .ReactCrop__drag-handle:focus {
|
|
356
|
+
background: var(--canopy-focus-ring);
|
|
357
|
+
outline: 2px solid var(--mantine-color-white);
|
|
358
|
+
box-shadow: 0 0 0 4px var(--mantine-color-black);
|
|
359
|
+
}
|
|
360
|
+
[data-nav-row] [data-row-lock] {
|
|
361
|
+
opacity: 0;
|
|
362
|
+
pointer-events: none;
|
|
363
|
+
transition: opacity 120ms ease;
|
|
364
|
+
}
|
|
365
|
+
[data-nav-row]:hover [data-row-lock],
|
|
366
|
+
[data-nav-row]:focus-within [data-row-lock],
|
|
367
|
+
[role='treeitem']:focus > [data-nav-row] [data-row-lock],
|
|
368
|
+
[data-nav-row][data-nav-selected] [data-row-lock] {
|
|
369
|
+
opacity: 1;
|
|
370
|
+
pointer-events: auto;
|
|
371
|
+
}
|
|
372
|
+
@media (prefers-reduced-motion: reduce) {
|
|
373
|
+
[data-nav-row] [data-row-lock] {
|
|
374
|
+
transition: none;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
340
377
|
[data-combobox-selected] {
|
|
341
378
|
--mantine-primary-color-filled: var(--canopy-primary-fill);
|
|
342
379
|
}
|
package/dist/server.d.ts
CHANGED
|
@@ -195,6 +195,8 @@ export { authenticatedAssetBase, readAssetBase } from './editor/raw-asset-base.j
|
|
|
195
195
|
export { setServerPreviewAssetBaseGetter } from './editor/preview-asset-base.js';
|
|
196
196
|
/** The asset store `createAssetStore` returns and `materializeAssets` takes. */
|
|
197
197
|
export type { AssetStore } from './assets/types.js';
|
|
198
|
+
/** What `AssetStore.presignPublicObjectRead` returns: the URL and how long it may be reused. */
|
|
199
|
+
export type { PresignedRead } from './assets/types.js';
|
|
198
200
|
/** What a create-only store write did: `created`, or `already-exists` (the stored object was kept). */
|
|
199
201
|
export type { CreateOnlyResult } from './assets/types.js';
|
|
200
202
|
/** Thrown by `materializeAssets` when sharp cannot load and keys are missing, so none can be produced. */
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CANOPYCMS_VERSION = "0.2.0
|
|
1
|
+
export declare const CANOPYCMS_VERSION = "0.2.0";
|
package/dist/version.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Generated by scripts/bump-version.mjs, its only writer. The package builds with plain
|
|
2
2
|
// tsc (rootDir: src), so src/ cannot import ../package.json; version.test.ts keeps the two equal.
|
|
3
|
-
export const CANOPYCMS_VERSION = '0.2.0
|
|
3
|
+
export const CANOPYCMS_VERSION = '0.2.0';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"//": "@codemirror/language, @lezer/highlight: workaround — @mdxeditor/editor uses cm6-theme-basic-light which peer-requires these but mdxeditor doesn't declare them as dependencies",
|
|
3
3
|
"name": "canopycms",
|
|
4
|
-
"version": "0.2.0
|
|
4
|
+
"version": "0.2.0",
|
|
5
5
|
"description": "CanopyCMS core package: schema-driven content, branch-aware editing, and editor UI for Next.js.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
@@ -143,7 +143,7 @@
|
|
|
143
143
|
"pathe": "^1.1.2",
|
|
144
144
|
"picocolors": "^1.1.1",
|
|
145
145
|
"proper-lockfile": "^4.1.2",
|
|
146
|
-
"react-
|
|
146
|
+
"react-image-crop": "^11.1.2",
|
|
147
147
|
"react-remove-scroll": "^2.7.2",
|
|
148
148
|
"react-resizable-panels": "4.14.1",
|
|
149
149
|
"sanitize-html": "^2.17.5",
|