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,5 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
-
/** Docked
|
|
2
|
+
/** Docked, pushing its neighbours aside, or laid over the form pane only. */
|
|
3
3
|
type SidePanelMode = 'docked' | 'overlay';
|
|
4
4
|
export interface SidePanelHostProps {
|
|
5
5
|
title: string;
|
|
@@ -26,10 +26,12 @@ export interface SidePanelHostProps {
|
|
|
26
26
|
'data-testid'?: string;
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
|
-
* A titled side panel, resizable from its inline-end edge. Docked, it
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
29
|
+
* A titled side panel, resizable from its inline-end edge. Docked, it pushes its neighbours aside
|
|
30
|
+
* and opens and closes at once: a sliding width would reflow the preview's page on every frame.
|
|
31
|
+
* As an overlay it covers the form pane only, so the preview stays in view, slides in and out as
|
|
32
|
+
* the drawers do unless the user prefers reduced motion, and Escape closes it. A `focusRequest`
|
|
33
|
+
* moves focus to its `data-autofocus` element, else to the panel; closing with focus inside
|
|
34
|
+
* returns focus to `returnFocusTo`.
|
|
33
35
|
*/
|
|
34
|
-
export declare function SidePanelHost({ title, mode, opened, width, minWidth, maxWidth, onWidthChange, onClose, focusRequest, returnFocusTo, actions, children, id, 'data-testid': testId, }: SidePanelHostProps): import("react/jsx-runtime").JSX.Element
|
|
36
|
+
export declare function SidePanelHost({ title, mode, opened, width, minWidth, maxWidth, onWidthChange, onClose, focusRequest, returnFocusTo, actions, children, id, 'data-testid': testId, }: SidePanelHostProps): import("react/jsx-runtime").JSX.Element;
|
|
35
37
|
export {};
|
|
@@ -1,58 +1,89 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useEffect, useId, useRef, useState, } from 'react';
|
|
4
|
-
import { CloseButton, Group, Text, Tooltip } from '@mantine/core';
|
|
5
|
-
import { useWindowEvent } from '@mantine/hooks';
|
|
6
|
-
import { focusIfIdle } from '../utils/focus-return.js';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useEffect, useId, useLayoutEffect, useRef, useState, } from 'react';
|
|
4
|
+
import { CloseButton, Group, Text, Tooltip, Transition } from '@mantine/core';
|
|
5
|
+
import { useReducedMotion, useWindowEvent } from '@mantine/hooks';
|
|
6
|
+
import { FOCUS_RETURN_ATTR, focusIfIdle } from '../utils/focus-return.js';
|
|
7
|
+
import { DIVIDER_COLOR, DIVIDER_TARGET_PX, DIVIDER_WIDTH_PX } from './EditorShell.js';
|
|
7
8
|
const KEY_STEP_PX = 16;
|
|
8
9
|
const RESIZING_ATTR = 'data-canopy-panel-resizing';
|
|
10
|
+
/** The drawers' slide: Mantine's `Transition` default. */
|
|
11
|
+
const SLIDE_MS = 250;
|
|
12
|
+
const targetOverhang = (target) => (target - DIVIDER_WIDTH_PX) / 2;
|
|
13
|
+
// The separator is the panel's last pixels; its `::before` is the wider pointer target.
|
|
9
14
|
const PANEL_STYLES = `
|
|
10
15
|
body[${RESIZING_ATTR}] { cursor: col-resize; user-select: none; }
|
|
11
16
|
body[${RESIZING_ATTR}] iframe { pointer-events: none; }
|
|
12
17
|
.canopy-side-panel-separator {
|
|
13
18
|
position: absolute;
|
|
14
19
|
inset-block: 0;
|
|
15
|
-
inset-inline-end:
|
|
16
|
-
width:
|
|
20
|
+
inset-inline-end: 0;
|
|
21
|
+
width: ${DIVIDER_WIDTH_PX}px;
|
|
22
|
+
background: ${DIVIDER_COLOR};
|
|
17
23
|
cursor: col-resize;
|
|
18
24
|
touch-action: none;
|
|
19
25
|
z-index: 1;
|
|
20
26
|
}
|
|
27
|
+
.canopy-side-panel-separator::before {
|
|
28
|
+
content: '';
|
|
29
|
+
position: absolute;
|
|
30
|
+
inset-block: 0;
|
|
31
|
+
inset-inline: -${targetOverhang(DIVIDER_TARGET_PX.fine)}px;
|
|
32
|
+
}
|
|
33
|
+
@media (pointer: coarse) {
|
|
34
|
+
.canopy-side-panel-separator::before {
|
|
35
|
+
inset-inline: -${targetOverhang(DIVIDER_TARGET_PX.coarse)}px;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
21
38
|
.canopy-side-panel-separator:hover,
|
|
39
|
+
.canopy-side-panel-separator:focus-visible,
|
|
22
40
|
.canopy-side-panel-separator[data-dragging] {
|
|
23
41
|
background: var(--canopy-focus-ring);
|
|
24
42
|
}
|
|
25
43
|
.canopy-side-panel-separator:focus-visible {
|
|
26
44
|
outline: 2px solid var(--canopy-focus-ring);
|
|
27
|
-
outline-offset: -2px;
|
|
28
45
|
}
|
|
29
46
|
`;
|
|
30
47
|
const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
|
|
31
48
|
/**
|
|
32
|
-
* A titled side panel, resizable from its inline-end edge. Docked, it
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
49
|
+
* A titled side panel, resizable from its inline-end edge. Docked, it pushes its neighbours aside
|
|
50
|
+
* and opens and closes at once: a sliding width would reflow the preview's page on every frame.
|
|
51
|
+
* As an overlay it covers the form pane only, so the preview stays in view, slides in and out as
|
|
52
|
+
* the drawers do unless the user prefers reduced motion, and Escape closes it. A `focusRequest`
|
|
53
|
+
* moves focus to its `data-autofocus` element, else to the panel; closing with focus inside
|
|
54
|
+
* returns focus to `returnFocusTo`.
|
|
36
55
|
*/
|
|
37
56
|
export function SidePanelHost({ title, mode, opened, width, minWidth, maxWidth, onWidthChange, onClose, focusRequest = 0, returnFocusTo, actions, children, id, 'data-testid': testId, }) {
|
|
38
57
|
const fallbackId = useId();
|
|
39
58
|
const panelId = id ?? fallbackId;
|
|
40
59
|
const titleId = `${panelId}-title`;
|
|
41
|
-
|
|
60
|
+
// State, not a ref: an overlay's section mounts a frame after `opened`, as its slide starts.
|
|
61
|
+
const [panel, setPanel] = useState(null);
|
|
42
62
|
const focusInside = useRef(false);
|
|
63
|
+
const closingWithFocus = useRef(false);
|
|
43
64
|
const wasOpened = useRef(opened);
|
|
44
65
|
const servedFocusRequest = useRef(focusRequest);
|
|
66
|
+
const reduceMotion = useReducedMotion();
|
|
67
|
+
// The slide's direction, read from the page as each opening starts, as the resize keys read it.
|
|
68
|
+
const anchorRef = useRef(null);
|
|
69
|
+
const [rtl, setRtl] = useState(false);
|
|
70
|
+
useLayoutEffect(() => {
|
|
71
|
+
const anchor = anchorRef.current;
|
|
72
|
+
if (opened && anchor)
|
|
73
|
+
setRtl(getComputedStyle(anchor).direction === 'rtl');
|
|
74
|
+
}, [opened]);
|
|
45
75
|
const drag = useRef(null);
|
|
46
76
|
const [dragWidth, setDragWidth] = useState(null);
|
|
47
77
|
if (!opened && dragWidth !== null)
|
|
48
78
|
setDragWidth(null);
|
|
49
79
|
const shownWidth = clamp(dragWidth ?? width, minWidth, maxWidth);
|
|
50
80
|
useEffect(() => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (!
|
|
81
|
+
// A request made while closed lapses; one made as it opens waits for the section to mount.
|
|
82
|
+
if (!opened)
|
|
83
|
+
servedFocusRequest.current = focusRequest;
|
|
84
|
+
if (!opened || !panel || focusRequest === servedFocusRequest.current)
|
|
55
85
|
return;
|
|
86
|
+
servedFocusRequest.current = focusRequest;
|
|
56
87
|
const autofocus = () => panel.querySelector('[data-autofocus]');
|
|
57
88
|
const field = autofocus();
|
|
58
89
|
if (field) {
|
|
@@ -73,13 +104,21 @@ export function SidePanelHost({ title, mode, opened, width, minWidth, maxWidth,
|
|
|
73
104
|
});
|
|
74
105
|
observer.observe(panel, { childList: true, subtree: true });
|
|
75
106
|
return () => observer.disconnect();
|
|
76
|
-
}, [opened, focusRequest]);
|
|
107
|
+
}, [opened, focusRequest, panel]);
|
|
108
|
+
// A sliding-out overlay is inert, so it neither holds focus nor takes the pointer. Whether focus
|
|
109
|
+
// was inside is read first, as `inert` can blur it.
|
|
110
|
+
useLayoutEffect(() => {
|
|
111
|
+
if (!opened && wasOpened.current)
|
|
112
|
+
closingWithFocus.current = focusInside.current;
|
|
113
|
+
panel?.toggleAttribute('inert', !opened);
|
|
114
|
+
}, [opened, panel]);
|
|
77
115
|
useEffect(() => {
|
|
78
116
|
if (!opened && wasOpened.current) {
|
|
79
|
-
// Its separator is gone, and with it the pointer capture that would
|
|
117
|
+
// Its separator is gone or inert, and with it the pointer capture that would end a drag.
|
|
80
118
|
drag.current = null;
|
|
81
119
|
document.body.removeAttribute(RESIZING_ATTR);
|
|
82
|
-
if (
|
|
120
|
+
if (closingWithFocus.current) {
|
|
121
|
+
closingWithFocus.current = false;
|
|
83
122
|
focusInside.current = false;
|
|
84
123
|
focusIfIdle(returnFocusTo?.() ?? null);
|
|
85
124
|
}
|
|
@@ -94,15 +133,13 @@ export function SidePanelHost({ title, mode, opened, width, minWidth, maxWidth,
|
|
|
94
133
|
event.key === 'Escape' &&
|
|
95
134
|
!event.defaultPrevented &&
|
|
96
135
|
event.target instanceof Node &&
|
|
97
|
-
|
|
136
|
+
panel?.contains(event.target)) {
|
|
98
137
|
event.preventDefault();
|
|
99
138
|
onClose();
|
|
100
139
|
}
|
|
101
140
|
});
|
|
102
141
|
// A drag cut short by unmounting must not leave the page's iframes deaf to the pointer.
|
|
103
142
|
useEffect(() => () => document.body.removeAttribute(RESIZING_ATTR), []);
|
|
104
|
-
if (!opened)
|
|
105
|
-
return null;
|
|
106
143
|
const endDrag = () => {
|
|
107
144
|
const state = drag.current;
|
|
108
145
|
if (!state)
|
|
@@ -170,15 +207,15 @@ export function SidePanelHost({ title, mode, opened, width, minWidth, maxWidth,
|
|
|
170
207
|
display: 'flex',
|
|
171
208
|
flexDirection: 'column',
|
|
172
209
|
background: 'var(--mantine-color-body)',
|
|
173
|
-
|
|
210
|
+
paddingInlineEnd: DIVIDER_WIDTH_PX,
|
|
174
211
|
};
|
|
175
|
-
return (_jsxs("section", { ref:
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
212
|
+
return (_jsxs(_Fragment, { children: [_jsx("span", { ref: anchorRef, hidden: true }), _jsx(Transition, { mounted: opened, transition: rtl ? 'slide-left' : 'slide-right', duration: overlay && !reduceMotion ? SLIDE_MS : 0, children: (transitionStyles) => (_jsxs("section", { ref: setPanel, id: panelId, "aria-labelledby": titleId, tabIndex: -1, "data-testid": testId, "data-panel-mode": mode, [FOCUS_RETURN_ATTR]: opened ? undefined : '', style: { ...style, ...transitionStyles }, onFocus: () => {
|
|
213
|
+
focusInside.current = true;
|
|
214
|
+
}, onBlur: (event) => {
|
|
215
|
+
if (!event.currentTarget.contains(event.relatedTarget))
|
|
216
|
+
focusInside.current = false;
|
|
217
|
+
}, children: [_jsx("style", { children: PANEL_STYLES }), _jsxs(Group, { justify: "space-between", wrap: "nowrap", gap: "xs", px: "md", py: "xs", style: { borderBlockEnd: '1px solid var(--canopy-input-border)' }, children: [_jsx(Text, { id: titleId, component: "h2", fw: 600, size: "md", m: 0, children: title }), _jsxs(Group, { gap: "xs", wrap: "nowrap", children: [actions, _jsx(Tooltip, { label: `Close ${title.toLowerCase()} panel`, children: _jsx(CloseButton, { "aria-label": `Close ${title.toLowerCase()} panel`, onClick: onClose }) })] })] }), _jsx("div", { style: { flex: 1, minHeight: 0, overflow: 'auto' }, children: children }), _jsx("div", { role: "separator", "aria-orientation": "vertical", "aria-label": `Resize ${title.toLowerCase()} panel`, "aria-controls": panelId, "aria-valuenow": Math.round(shownWidth), "aria-valuemin": minWidth, "aria-valuemax": maxWidth,
|
|
218
|
+
// A focusable separator is a window splitter, a widget; jsx-a11y counts every one static.
|
|
219
|
+
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
|
220
|
+
tabIndex: 0, className: "canopy-side-panel-separator", "data-dragging": dragWidth !== null || undefined, ...separatorHandlers })] })) })] }));
|
|
184
221
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Box, Group } from '@mantine/core';
|
|
4
|
+
import { SHELL_REGION_BORDER } from './EditorShell.js';
|
|
4
5
|
const TOP_BAR_HEIGHT = 48;
|
|
5
6
|
/**
|
|
6
7
|
* The shell's top bar: one 48px row from `md` up. Narrower, `leading` takes a row of its own
|
|
@@ -8,7 +9,7 @@ const TOP_BAR_HEIGHT = 48;
|
|
|
8
9
|
*/
|
|
9
10
|
export function TopBar({ leading, trailing }) {
|
|
10
11
|
return (_jsxs(Group, { gap: "xs", wrap: "wrap", align: "center", px: "sm", py: 6, mih: TOP_BAR_HEIGHT, style: {
|
|
11
|
-
borderBottom:
|
|
12
|
+
borderBottom: SHELL_REGION_BORDER,
|
|
12
13
|
background: 'var(--mantine-color-body)',
|
|
13
14
|
}, children: [_jsx(Box, { flex: { base: '1 0 100%', md: '1 1 0%' }, miw: 0, children: leading }), _jsx(Group, { gap: 6, wrap: "wrap", align: "center", justify: "flex-end", flex: { base: '1 0 100%', md: '0 1 auto' }, miw: 0, children: trailing })] }));
|
|
14
15
|
}
|
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',
|
|
@@ -254,6 +255,7 @@ const primaryFill = (theme, scheme) => {
|
|
|
254
255
|
* dark, at 3:1 on gray.1 or dark.5. Mantine draws focus rings, focused borders and a checked
|
|
255
256
|
* Switch in `--mantine-primary-color-filled`, so that becomes the ring; `canopyComponents` points
|
|
256
257
|
* Checkbox, Radio and Tabs at it. A tick on it is white, or black where white is under 3:1.
|
|
258
|
+
* `--canopy-unsaved-mark` is the same colour, named for the dot that marks unsaved changes.
|
|
257
259
|
*/
|
|
258
260
|
const focusRing = (theme, scheme) => {
|
|
259
261
|
const scale = theme.colors[theme.primaryColor];
|
|
@@ -274,6 +276,7 @@ const focusRing = (theme, scheme) => {
|
|
|
274
276
|
? ring.value
|
|
275
277
|
: `var(--mantine-color-${theme.primaryColor}-${ring.shade})`,
|
|
276
278
|
'--canopy-focus-ring-contrast': contrast,
|
|
279
|
+
'--canopy-unsaved-mark': 'var(--canopy-focus-ring)',
|
|
277
280
|
'--mantine-primary-color-filled': 'var(--canopy-focus-ring)',
|
|
278
281
|
};
|
|
279
282
|
};
|
|
@@ -316,7 +319,8 @@ const canopyCssVariablesResolver = (theme) => ({
|
|
|
316
319
|
* What Mantine draws from class rules, out of the theme variables' reach: unchecked control borders,
|
|
317
320
|
* Switch thumbs, white text on the primary colour (moved to the fill), and dimmed text Mantine
|
|
318
321
|
* draws itself (a blocked menu item, a combobox's empty message, outside days). Outside days lose
|
|
319
|
-
* 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.
|
|
320
324
|
*/
|
|
321
325
|
const CANOPY_STYLES = `
|
|
322
326
|
.mantine-Checkbox-input:not(:checked, :disabled, [data-indeterminate], [data-error]),
|
|
@@ -335,6 +339,20 @@ const CANOPY_STYLES = `
|
|
|
335
339
|
--switch-thumb-bg: var(--canopy-switch-thumb-on, var(--mantine-color-white));
|
|
336
340
|
--switch-thumb-bd: var(--canopy-switch-thumb-on, var(--mantine-color-white));
|
|
337
341
|
}
|
|
342
|
+
.ReactCrop .ReactCrop__crop-selection:focus {
|
|
343
|
+
outline: 2px solid var(--mantine-color-white);
|
|
344
|
+
outline-offset: 0;
|
|
345
|
+
box-shadow: 0 0 0 4px var(--mantine-color-black);
|
|
346
|
+
}
|
|
347
|
+
.ReactCrop .ReactCrop__drag-handle {
|
|
348
|
+
--rc-drag-handle-bg-colour: var(--mantine-color-white);
|
|
349
|
+
border-color: var(--mantine-color-black);
|
|
350
|
+
}
|
|
351
|
+
.ReactCrop .ReactCrop__drag-handle:focus {
|
|
352
|
+
background: var(--canopy-focus-ring);
|
|
353
|
+
outline: 2px solid var(--mantine-color-white);
|
|
354
|
+
box-shadow: 0 0 0 4px var(--mantine-color-black);
|
|
355
|
+
}
|
|
338
356
|
[data-combobox-selected] {
|
|
339
357
|
--mantine-primary-color-filled: var(--canopy-primary-fill);
|
|
340
358
|
}
|
|
@@ -3,12 +3,18 @@ import type { CommentThread } from '../comment-store.js';
|
|
|
3
3
|
import { type CanopyPathSegment } from './canopy-path.js';
|
|
4
4
|
/**
|
|
5
5
|
* One list a removal sits inside (a block list, or an object list holding the removed item's
|
|
6
|
-
* list): the index of the item enclosing the removal, and
|
|
6
|
+
* list): the index of the item enclosing the removal, and that item's `ITEM_KEY`, which an edit
|
|
7
|
+
* or a move keeps. An item with no usable key is anchored by every other item's content instead.
|
|
7
8
|
*/
|
|
8
|
-
export
|
|
9
|
+
export type RemovalAnchor = {
|
|
9
10
|
index: number;
|
|
11
|
+
} & ({
|
|
12
|
+
key: string;
|
|
13
|
+
siblings?: never;
|
|
14
|
+
} | {
|
|
15
|
+
key?: never;
|
|
10
16
|
siblings: unknown[];
|
|
11
|
-
}
|
|
17
|
+
});
|
|
12
18
|
/**
|
|
13
19
|
* What a form's Remove took out, for Undo. Paths are canopy paths (`blocks[2].items`), which skip
|
|
14
20
|
* a block's `value` wrapper; `restoreRemoval` resolves them against the value it restores into.
|
|
@@ -30,11 +36,10 @@ export type FieldRemoval = ({
|
|
|
30
36
|
}) & {
|
|
31
37
|
/**
|
|
32
38
|
* The enclosing lists as they were before the removal (`removalAnchors`). Undo puts nothing
|
|
33
|
-
* back once
|
|
39
|
+
* back once another item holds an enclosing index, so an item never lands in whichever block
|
|
40
|
+
* now holds it; an edit to any item keeps a keyed anchor.
|
|
34
41
|
*/
|
|
35
42
|
anchors?: RemovalAnchor[];
|
|
36
|
-
/** `clear` for an optional object's Clear, which its toast calls clearing, not removing. */
|
|
37
|
-
action?: 'remove' | 'clear';
|
|
38
43
|
};
|
|
39
44
|
/**
|
|
40
45
|
* The canopy path `restoreRemoval` puts a removal back at: a list item returns to its old index,
|
|
@@ -43,13 +48,17 @@ export type FieldRemoval = ({
|
|
|
43
48
|
export declare function restoredFieldPath(fields: readonly FieldConfig[], value: Record<string, unknown>, removal: FieldRemoval): CanopyPathSegment[];
|
|
44
49
|
/** The enclosing lists of a removal in `value`, taken before the removal applies; see `anchors`. */
|
|
45
50
|
export declare function removalAnchors(fields: readonly FieldConfig[], value: Record<string, unknown>, removal: FieldRemoval): RemovalAnchor[] | undefined;
|
|
51
|
+
/** Whether `restoreRemoval` would put the removal back, without building the restored value. */
|
|
52
|
+
export declare const canRestoreRemoval: (fields: readonly FieldConfig[], value: Record<string, unknown>, removal: FieldRemoval) => boolean;
|
|
46
53
|
/**
|
|
47
|
-
* `value` with a removal put back, or undefined when it can't be: the list or field is gone,
|
|
48
|
-
*
|
|
49
|
-
* Edits made after the removal are kept, because it changes only the one list or
|
|
50
|
-
* value as it is now.
|
|
54
|
+
* `value` with a removal put back, or undefined when it can't be: the list or field is gone,
|
|
55
|
+
* another item holds an enclosing index, the item is already there, or the image field or object
|
|
56
|
+
* has been filled. Edits made after the removal are kept, because it changes only the one list or
|
|
57
|
+
* field, in the value as it is now.
|
|
51
58
|
*/
|
|
52
59
|
export declare function restoreRemoval(fields: readonly FieldConfig[], value: Record<string, unknown>, removal: FieldRemoval): Record<string, unknown> | undefined;
|
|
60
|
+
/** Where a removal's moved threads went, said with its Undo; undefined when none moved. */
|
|
61
|
+
export declare const movedThreadsNote: (count: number) => string | undefined;
|
|
53
62
|
/**
|
|
54
63
|
* How many open field threads a removal at `path` moves to the entry: those inside it keyed to an
|
|
55
64
|
* item at or below it. One keyed only above stays on its field, which a Clear leaves in place.
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import equal from 'fast-deep-equal';
|
|
2
|
+
import { ITEM_KEY, isItemKey } from '../item-keys.js';
|
|
2
3
|
import { isPlainRecord } from '../validation/field-traversal.js';
|
|
3
4
|
import { normalizeCanopyPath, parseCanopyPath } from './canopy-path.js';
|
|
5
|
+
const itemKeyAt = (list, index) => {
|
|
6
|
+
const item = list[index];
|
|
7
|
+
const key = isPlainRecord(item) ? item[ITEM_KEY] : undefined;
|
|
8
|
+
return isItemKey(key) ? key : undefined;
|
|
9
|
+
};
|
|
4
10
|
const findField = (fields, name) => {
|
|
5
11
|
for (const field of fields) {
|
|
6
12
|
if (field.type === 'group') {
|
|
@@ -22,8 +28,8 @@ const resolvePath = (fields, value, path) => {
|
|
|
22
28
|
let scope = fields;
|
|
23
29
|
let node = value;
|
|
24
30
|
const out = [];
|
|
25
|
-
const
|
|
26
|
-
const anchor = (list, index) =>
|
|
31
|
+
const lists = [];
|
|
32
|
+
const anchor = (list, index) => lists.push({ list, index });
|
|
27
33
|
let i = 0;
|
|
28
34
|
while (i < path.length) {
|
|
29
35
|
const name = path[i];
|
|
@@ -34,7 +40,7 @@ const resolvePath = (fields, value, path) => {
|
|
|
34
40
|
node = isPlainRecord(node) ? node[name] : undefined;
|
|
35
41
|
i += 1;
|
|
36
42
|
if (i === path.length)
|
|
37
|
-
return { path: out,
|
|
43
|
+
return { path: out, lists };
|
|
38
44
|
const index = path[i];
|
|
39
45
|
if (field.type === 'block') {
|
|
40
46
|
const block = typeof index === 'number' && Array.isArray(node) ? node[index] : undefined;
|
|
@@ -65,7 +71,7 @@ const resolvePath = (fields, value, path) => {
|
|
|
65
71
|
return undefined;
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
|
-
return { path: out,
|
|
74
|
+
return { path: out, lists };
|
|
69
75
|
};
|
|
70
76
|
const getAt = (root, path) => path.reduce((node, segment) => {
|
|
71
77
|
if (typeof segment === 'number')
|
|
@@ -89,9 +95,15 @@ const sameContent = (a, b) => equal(JSON.parse(JSON.stringify(a ?? null)), JSON.
|
|
|
89
95
|
const removalPath = (removal) => removal.kind === 'list-item' ? removal.listPath : removal.path;
|
|
90
96
|
const anchorsHold = (then, now) => then === undefined ||
|
|
91
97
|
(then.length === now.length &&
|
|
92
|
-
then.every((anchor, i) =>
|
|
98
|
+
then.every((anchor, i) => {
|
|
99
|
+
const { list, index } = now[i];
|
|
100
|
+
if (anchor.index !== index)
|
|
101
|
+
return false;
|
|
102
|
+
if (anchor.key !== undefined)
|
|
103
|
+
return itemKeyAt(list, index) === anchor.key;
|
|
93
104
|
// By content as stored, so the server's copy of the same items still holds.
|
|
94
|
-
sameContent(anchor.siblings,
|
|
105
|
+
return sameContent(anchor.siblings, list.filter((_, j) => j !== index));
|
|
106
|
+
}));
|
|
95
107
|
/**
|
|
96
108
|
* The canopy path `restoreRemoval` puts a removal back at: a list item returns to its old index,
|
|
97
109
|
* or to the list's end when the list is now shorter than that.
|
|
@@ -105,17 +117,17 @@ export function restoredFieldPath(fields, value, removal) {
|
|
|
105
117
|
}
|
|
106
118
|
/** The enclosing lists of a removal in `value`, taken before the removal applies; see `anchors`. */
|
|
107
119
|
export function removalAnchors(fields, value, removal) {
|
|
108
|
-
return resolvePath(fields, value, removalPath(removal))?.
|
|
120
|
+
return resolvePath(fields, value, removalPath(removal))?.lists.map(({ list, index }) => {
|
|
121
|
+
const key = itemKeyAt(list, index);
|
|
122
|
+
return key !== undefined
|
|
123
|
+
? { index, key }
|
|
124
|
+
: { index, siblings: list.filter((_, j) => j !== index) };
|
|
125
|
+
});
|
|
109
126
|
}
|
|
110
|
-
/**
|
|
111
|
-
|
|
112
|
-
* enclosing list has changed, the item is already there, or the image field has been filled.
|
|
113
|
-
* Edits made after the removal are kept, because it changes only the one list or field, in the
|
|
114
|
-
* value as it is now.
|
|
115
|
-
*/
|
|
116
|
-
export function restoreRemoval(fields, value, removal) {
|
|
127
|
+
/** Where and what `restoreRemoval` writes, or undefined when it can't; see there. */
|
|
128
|
+
const planRestore = (fields, value, removal) => {
|
|
117
129
|
const resolved = resolvePath(fields, value, removalPath(removal));
|
|
118
|
-
if (!resolved || !anchorsHold(removal.anchors, resolved.
|
|
130
|
+
if (!resolved || !anchorsHold(removal.anchors, resolved.lists))
|
|
119
131
|
return undefined;
|
|
120
132
|
const { path } = resolved;
|
|
121
133
|
// An enclosing object cleared since would come back holding only this.
|
|
@@ -139,9 +151,25 @@ export function restoreRemoval(fields, value, removal) {
|
|
|
139
151
|
return undefined;
|
|
140
152
|
next = removal.value;
|
|
141
153
|
}
|
|
142
|
-
|
|
154
|
+
return { path, next };
|
|
155
|
+
};
|
|
156
|
+
/** Whether `restoreRemoval` would put the removal back, without building the restored value. */
|
|
157
|
+
export const canRestoreRemoval = (fields, value, removal) => planRestore(fields, value, removal) !== undefined;
|
|
158
|
+
/**
|
|
159
|
+
* `value` with a removal put back, or undefined when it can't be: the list or field is gone,
|
|
160
|
+
* another item holds an enclosing index, the item is already there, or the image field or object
|
|
161
|
+
* has been filled. Edits made after the removal are kept, because it changes only the one list or
|
|
162
|
+
* field, in the value as it is now.
|
|
163
|
+
*/
|
|
164
|
+
export function restoreRemoval(fields, value, removal) {
|
|
165
|
+
const plan = planRestore(fields, value, removal);
|
|
166
|
+
const restored = plan ? setAt(value, plan.path, plan.next) : undefined;
|
|
143
167
|
return isPlainRecord(restored) ? restored : undefined;
|
|
144
168
|
}
|
|
169
|
+
/** Where a removal's moved threads went, said with its Undo; undefined when none moved. */
|
|
170
|
+
export const movedThreadsNote = (count) => count > 0
|
|
171
|
+
? `Its ${count === 1 ? 'comment thread' : `${count} comment threads`} moved to "Comment on this entry".`
|
|
172
|
+
: undefined;
|
|
145
173
|
/**
|
|
146
174
|
* How many open field threads a removal at `path` moves to the entry: those inside it keyed to an
|
|
147
175
|
* item at or below it. One keyed only above stays on its field, which a Clear leaves in place.
|
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.
|
|
1
|
+
export declare const CANOPYCMS_VERSION = "0.2.0-int.121";
|
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.
|
|
3
|
+
export const CANOPYCMS_VERSION = '0.2.0-int.121';
|
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.
|
|
4
|
+
"version": "0.2.0-int.121",
|
|
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",
|