@drawnagency/primitives 0.1.78 → 0.1.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-BAQYZSFF.js → chunk-4TVODHD2.js} +5 -5
- package/dist/{chunk-NS3UTJ3E.js → chunk-7XWER6YR.js} +18 -11
- package/dist/{chunk-UA6OCJJK.js → chunk-KVQFMVXE.js} +79 -26
- package/dist/{chunk-MK4POMLZ.js → chunk-QB5Q23G2.js} +1 -1
- package/dist/{chunk-4SIWBWKZ.js → chunk-RSITF24B.js} +8 -0
- package/dist/{chunk-Z5IYWUW3.js → chunk-UNUJPLZV.js} +1 -1
- package/dist/{chunk-K4JD6FD3.js → chunk-VI26O4E3.js} +1 -1
- package/dist/components/brandguide/Colors.d.ts +2 -1
- package/dist/components/brandguide/Colors.d.ts.map +1 -1
- package/dist/components/editor/index.js +1 -1
- package/dist/components/primitives/EditableGrid.d.ts +11 -1
- package/dist/components/primitives/EditableGrid.d.ts.map +1 -1
- package/dist/components/primitives/gridDrop.d.ts +18 -0
- package/dist/components/primitives/gridDrop.d.ts.map +1 -0
- package/dist/components/primitives/index.js +3 -3
- package/dist/components/primitives/useEditableCollection.d.ts +1 -0
- package/dist/components/primitives/useEditableCollection.d.ts.map +1 -1
- package/dist/components/sections/Colors/index.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.js +3 -3
- package/dist/components/sections/register-schemas.js +5 -5
- package/dist/components/sections/register.js +5 -5
- package/dist/components/shell/EditorContext.d.ts +2 -0
- package/dist/components/shell/EditorContext.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts +13 -0
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/components/shell/HistoryToolbar.d.ts +8 -1
- package/dist/components/shell/HistoryToolbar.d.ts.map +1 -1
- package/dist/components/shell/SiteSettingsDisplay.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/lib/corner-radius.d.ts +6 -0
- package/dist/lib/corner-radius.d.ts.map +1 -0
- package/dist/lib/index.js +2 -2
- package/dist/lib/registry.d.ts +3 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/media/index.js +1 -1
- package/dist/schemas/index.js +2 -2
- package/dist/schemas/site-config.d.ts +6 -0
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/brandguide/Colors.tsx +15 -5
- package/src/components/chat/ChatPanel.tsx +8 -8
- package/src/components/primitives/EditableGrid.tsx +86 -24
- package/src/components/primitives/ResolvedMedia.tsx +1 -1
- package/src/components/primitives/gridDrop.ts +26 -0
- package/src/components/primitives/useEditableCollection.ts +16 -1
- package/src/components/sections/Button/CTAButton.tsx +1 -1
- package/src/components/sections/Colors/index.tsx +2 -1
- package/src/components/sections/Media/MediaBlock.tsx +1 -1
- package/src/components/shared/Navigation.tsx +5 -5
- package/src/components/shell/EditorContext.tsx +6 -1
- package/src/components/shell/EditorModal.tsx +7 -7
- package/src/components/shell/EditorShell.tsx +46 -5
- package/src/components/shell/HistoryToolbar.tsx +24 -2
- package/src/components/shell/SiteSettingsDisplay.tsx +19 -0
- package/src/lib/corner-radius.ts +22 -0
- package/src/lib/registry.ts +3 -0
- package/src/schemas/site-config.ts +4 -0
|
@@ -16,6 +16,7 @@ interface UseEditableCollectionReturn<T> {
|
|
|
16
16
|
onReorder: (fromIndex: number, toIndex: number) => void;
|
|
17
17
|
onAdd: () => void;
|
|
18
18
|
onInsert: (index: number) => void;
|
|
19
|
+
onInsertItem: (index: number, data: T) => void;
|
|
19
20
|
onRemove: (id: string) => void;
|
|
20
21
|
}
|
|
21
22
|
|
|
@@ -89,6 +90,20 @@ export function useEditableCollection<T>({
|
|
|
89
90
|
[createItem, onChange],
|
|
90
91
|
);
|
|
91
92
|
|
|
93
|
+
// Insert an externally-provided item (e.g. one dragged in from another grid)
|
|
94
|
+
// at a specific index. Distinct from onInsert, which creates a blank item.
|
|
95
|
+
const onInsertItem = useCallback(
|
|
96
|
+
(index: number, data: T) => {
|
|
97
|
+
setWrappedItems((prev) => {
|
|
98
|
+
const next = [...prev];
|
|
99
|
+
next.splice(index, 0, { id: uid(), data });
|
|
100
|
+
onChange(next.map((w) => w.data));
|
|
101
|
+
return next;
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
[onChange]
|
|
105
|
+
);
|
|
106
|
+
|
|
92
107
|
const onRemove = useCallback(
|
|
93
108
|
(id: string) => {
|
|
94
109
|
setWrappedItems((prev) => {
|
|
@@ -100,5 +115,5 @@ export function useEditableCollection<T>({
|
|
|
100
115
|
[onChange]
|
|
101
116
|
);
|
|
102
117
|
|
|
103
|
-
return { wrappedItems, onReorder, onAdd, onInsert, onRemove };
|
|
118
|
+
return { wrappedItems, onReorder, onAdd, onInsert, onInsertItem, onRemove };
|
|
104
119
|
}
|
|
@@ -12,7 +12,7 @@ interface Props {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export default function Button({ text, link, download, onChange }: Props) {
|
|
15
|
-
const base = "inline-block rounded-
|
|
15
|
+
const base = "inline-block rounded-outer px-6 py-3 font-bold transition-colors";
|
|
16
16
|
const variant = "border-2 border-primary text-primary hover:bg-primary hover:text-primary-contrast";
|
|
17
17
|
|
|
18
18
|
if (onChange) {
|
|
@@ -30,7 +30,7 @@ export default defineSection({
|
|
|
30
30
|
schema,
|
|
31
31
|
authoringNote:
|
|
32
32
|
"A brand color palette. Each color may carry hex/rgb/cmyk/pantone values. Use to document brand colors with swatches.",
|
|
33
|
-
component: ({ content, options, onChange, openModal }) => (
|
|
33
|
+
component: ({ content, options, onChange, openModal, onDelete }) => (
|
|
34
34
|
<Colors
|
|
35
35
|
colors={content.content.colors}
|
|
36
36
|
columns={options?.columns as number}
|
|
@@ -39,6 +39,7 @@ export default defineSection({
|
|
|
39
39
|
showLabel={options?.showLabel as boolean}
|
|
40
40
|
onChange={onChange ? (c) => onChange(c as typeof content) : undefined}
|
|
41
41
|
openModal={openModal}
|
|
42
|
+
onDelete={onDelete}
|
|
42
43
|
/>
|
|
43
44
|
),
|
|
44
45
|
defaults: () => ({
|
|
@@ -77,7 +77,7 @@ export function MediaBlock({ refValue, options, link, isEditMode, onRefChange }:
|
|
|
77
77
|
<div
|
|
78
78
|
data-testid="media-open-surface"
|
|
79
79
|
className={cn(
|
|
80
|
-
"overflow-hidden rounded-
|
|
80
|
+
"overflow-hidden rounded-outer",
|
|
81
81
|
showBorder && "border border-base-200",
|
|
82
82
|
options.square && "aspect-square",
|
|
83
83
|
onImageClick && "cursor-pointer",
|
|
@@ -89,7 +89,7 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
89
89
|
<li key={pid}>
|
|
90
90
|
<button
|
|
91
91
|
onClick={() => handleHeadingNav(parent.href)}
|
|
92
|
-
className={cn("cursor-pointer w-full rounded px-2 py-1 text-left text-sm transition-colors",
|
|
92
|
+
className={cn("cursor-pointer w-full rounded-inner px-2 py-1 text-left text-sm transition-colors",
|
|
93
93
|
activeParentId === pid ? "font-bold text-primary" : "text-base-contrast-light hover:text-primary",
|
|
94
94
|
!isEditMode && parent.status && parent.status !== "live" && "opacity-50")}
|
|
95
95
|
>
|
|
@@ -102,7 +102,7 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
102
102
|
return (
|
|
103
103
|
<li key={cid}>
|
|
104
104
|
<button onClick={() => handleHeadingNav(child.href)}
|
|
105
|
-
className={cn("cursor-pointer w-full rounded px-2 py-1 text-left text-sm transition-colors",
|
|
105
|
+
className={cn("cursor-pointer w-full rounded-inner px-2 py-1 text-left text-sm transition-colors",
|
|
106
106
|
activeChildId === cid ? "font-bold text-primary" : "text-base-contrast-light hover:text-primary",
|
|
107
107
|
!isEditMode && child.status && child.status !== "live" && "opacity-50")}>
|
|
108
108
|
{child.label}
|
|
@@ -150,7 +150,7 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
150
150
|
{asButton ? (
|
|
151
151
|
<button
|
|
152
152
|
onClick={() => handlePageClick(page.id)}
|
|
153
|
-
className={cn("cursor-pointer w-full rounded px-3 py-2 text-left text-sm font-bold transition-colors",
|
|
153
|
+
className={cn("cursor-pointer w-full rounded-inner px-3 py-2 text-left text-sm font-bold transition-colors",
|
|
154
154
|
page.isActive ? "text-primary" : "text-base-contrast hover:text-primary")}
|
|
155
155
|
>
|
|
156
156
|
{page.title}
|
|
@@ -158,7 +158,7 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
158
158
|
) : (
|
|
159
159
|
<a
|
|
160
160
|
href={page.href}
|
|
161
|
-
className={cn("block w-full rounded px-3 py-2 text-left text-sm font-bold transition-colors",
|
|
161
|
+
className={cn("block w-full rounded-inner px-3 py-2 text-left text-sm font-bold transition-colors",
|
|
162
162
|
page.isActive ? "text-primary" : "text-base-contrast hover:text-primary")}
|
|
163
163
|
>
|
|
164
164
|
{page.title}
|
|
@@ -179,7 +179,7 @@ export default function Navigation({ siteNav: initialNav, siteName, darkMode, la
|
|
|
179
179
|
type="button"
|
|
180
180
|
aria-expanded={isOpen}
|
|
181
181
|
onClick={() => setOpenGroups((g) => ({ ...g, [label]: !isOpen }))}
|
|
182
|
-
className="flex w-full cursor-pointer items-center justify-between rounded px-3 py-1 text-xs font-semibold uppercase tracking-wide text-base-contrast/70 transition-colors hover:text-primary"
|
|
182
|
+
className="flex w-full cursor-pointer items-center justify-between rounded-inner px-3 py-1 text-xs font-semibold uppercase tracking-wide text-base-contrast/70 transition-colors hover:text-primary"
|
|
183
183
|
>
|
|
184
184
|
<span>{`${label} (${pages.length})`}</span>
|
|
185
185
|
<ChevronRight size={14} aria-hidden="true" className={cn("transition-transform", isOpen && "rotate-90")} />
|
|
@@ -16,10 +16,12 @@ interface EditorContextValue {
|
|
|
16
16
|
showAllChrome: boolean;
|
|
17
17
|
viewBranch: "saved" | "live";
|
|
18
18
|
historyState: HistoryState;
|
|
19
|
+
historyPageId: string | null;
|
|
19
20
|
toggleEditMode: () => void;
|
|
20
21
|
toggleShowAllChrome: () => void;
|
|
21
22
|
setViewBranch: (branch: "saved" | "live") => void;
|
|
22
23
|
setHistoryState: (state: HistoryState) => void;
|
|
24
|
+
setHistoryPageId: (pageId: string | null) => void;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const EditorContext = createContext<EditorContextValue | null>(null);
|
|
@@ -33,7 +35,10 @@ export function EditorProvider({ children }: EditorProviderProps) {
|
|
|
33
35
|
const [showAllChrome, setShowAllChrome] = useState(false);
|
|
34
36
|
const [viewBranch, setViewBranchState] = useState<"saved" | "live">("saved");
|
|
35
37
|
const [historyState, setHistoryStateRaw] = useState<HistoryState>(null);
|
|
38
|
+
const [historyPageId, setHistoryPageId] = useState<string | null>(null);
|
|
36
39
|
const setHistoryState = useCallback((state: HistoryState) => {
|
|
40
|
+
// null = "auto": resolved to the current page (or home) at render time
|
|
41
|
+
setHistoryPageId(null);
|
|
37
42
|
setHistoryStateRaw(state);
|
|
38
43
|
}, []);
|
|
39
44
|
|
|
@@ -49,7 +54,7 @@ export function EditorProvider({ children }: EditorProviderProps) {
|
|
|
49
54
|
const setViewBranch = useCallback((b: "saved" | "live") => setViewBranchState(b), []);
|
|
50
55
|
|
|
51
56
|
return (
|
|
52
|
-
<EditorContext.Provider value={{ isEditMode, showAllChrome, viewBranch, historyState, toggleEditMode, toggleShowAllChrome, setViewBranch, setHistoryState }}>
|
|
57
|
+
<EditorContext.Provider value={{ isEditMode, showAllChrome, viewBranch, historyState, historyPageId, toggleEditMode, toggleShowAllChrome, setViewBranch, setHistoryState, setHistoryPageId }}>
|
|
53
58
|
{children}
|
|
54
59
|
</EditorContext.Provider>
|
|
55
60
|
);
|
|
@@ -59,15 +59,15 @@ export function EditorModal({ isOpen, onClose, title, blocking = false, size = "
|
|
|
59
59
|
aria-modal="true"
|
|
60
60
|
aria-labelledby={titleId}
|
|
61
61
|
className={cn(
|
|
62
|
-
"mx-4 w-full rounded-lg bg-base shadow-lg",
|
|
62
|
+
"mx-4 flex w-full flex-col rounded-lg bg-base shadow-lg",
|
|
63
63
|
size === "large"
|
|
64
|
-
? "max-w-[1400px] max-h-[900px] h-[90vh]
|
|
64
|
+
? "max-w-[1400px] max-h-[900px] h-[90vh]"
|
|
65
65
|
: size === "settings"
|
|
66
|
-
? "max-w-lg
|
|
67
|
-
: "max-w-md",
|
|
66
|
+
? "max-w-lg min-h-[min(32rem,calc(100vh-4rem))] max-h-[calc(100vh-4rem)]"
|
|
67
|
+
: "max-w-md max-h-[calc(100vh-4rem)]",
|
|
68
68
|
)}
|
|
69
69
|
>
|
|
70
|
-
<div className="flex items-center justify-between border-b border-base-200 px-6 py-4">
|
|
70
|
+
<div className="flex shrink-0 items-center justify-between border-b border-base-200 px-6 py-4">
|
|
71
71
|
<h3 id={titleId} className="text-lg font-semibold text-base-contrast">{title}</h3>
|
|
72
72
|
{!blocking && (
|
|
73
73
|
<IconButton
|
|
@@ -81,8 +81,8 @@ export function EditorModal({ isOpen, onClose, title, blocking = false, size = "
|
|
|
81
81
|
</div>
|
|
82
82
|
<div className={cn(
|
|
83
83
|
!noPadding && "px-6 py-4",
|
|
84
|
-
|
|
85
|
-
size === "settings" && "flex
|
|
84
|
+
"flex-1 overflow-y-auto",
|
|
85
|
+
size === "settings" && "flex flex-col",
|
|
86
86
|
)}>
|
|
87
87
|
{children}
|
|
88
88
|
</div>
|
|
@@ -12,7 +12,7 @@ import type { MediaManifest } from "../../media/types";
|
|
|
12
12
|
import type { QueueItem } from "../../media/queue";
|
|
13
13
|
import { SiteConfigSchema, IndexSchema } from "../../schemas/site-config";
|
|
14
14
|
import { ensureSanitizer } from "../../lib/sanitize";
|
|
15
|
-
import { EditorProvider, useEditorContext } from "./EditorContext";
|
|
15
|
+
import { EditorProvider, useEditorContext, type HistoryState } from "./EditorContext";
|
|
16
16
|
import { makeBlockMoveDispatch } from "./blockMoveDispatch";
|
|
17
17
|
import { ROOT_CONTAINER_ID, resolveDropIndex } from "../../lib/block-dnd";
|
|
18
18
|
import type { BlockMove } from "../../lib/block-move";
|
|
@@ -28,6 +28,7 @@ import { SectionSkeleton } from "./SectionSkeleton";
|
|
|
28
28
|
import { SectionTypePicker } from "./SectionTypePicker";
|
|
29
29
|
import { ensureSectionsRegistered } from "../sections/register";
|
|
30
30
|
import { getSection, getAllSections, getSchema } from "../../lib/registry";
|
|
31
|
+
import { cornerRadiusValue } from "../../lib/corner-radius";
|
|
31
32
|
import { childInsertableTypes, insertChildAt } from "../../lib/container-ops";
|
|
32
33
|
import { buildInsertMenuOptions, resolveInsertedContent } from "../../lib/insert-menu";
|
|
33
34
|
|
|
@@ -825,6 +826,7 @@ export default function EditorShell({
|
|
|
825
826
|
root.style.setProperty("--heading-text-transform", config.uppercaseHeadings ? "uppercase" : "none");
|
|
826
827
|
root.style.setProperty("--subheading-text-transform", config.uppercaseSubheadings ? "uppercase" : "none");
|
|
827
828
|
root.style.setProperty("--nav-text-transform", config.uppercaseNavHeadings ? "uppercase" : "none");
|
|
829
|
+
root.style.setProperty("--radius-outer", cornerRadiusValue(config.cornerRadius));
|
|
828
830
|
|
|
829
831
|
if (config.googleFontsUrl) {
|
|
830
832
|
if (fontLinkRef.current?.href !== config.googleFontsUrl) {
|
|
@@ -853,6 +855,7 @@ export default function EditorShell({
|
|
|
853
855
|
localStorage.setItem("portal-dark-mode", config.darkMode);
|
|
854
856
|
localStorage.setItem("portal-primary-color", config.primaryColor);
|
|
855
857
|
localStorage.setItem("portal-primary-contrast", config.primaryContrast);
|
|
858
|
+
localStorage.setItem("portal-corner-radius", cornerRadiusValue(config.cornerRadius));
|
|
856
859
|
darkModeEvent.dispatch(config.darkMode);
|
|
857
860
|
// The nav title lives in a separate island that mounts with a build-time
|
|
858
861
|
// siteName prop. Push the live (possibly draft) name so editing it in Site
|
|
@@ -1451,6 +1454,7 @@ export default function EditorShell({
|
|
|
1451
1454
|
onSectionChange={onSectionChange}
|
|
1452
1455
|
onAddSection={onAddSection}
|
|
1453
1456
|
onDeleteSection={setPendingDeleteSectionId}
|
|
1457
|
+
onDeleteSectionImmediate={performDeleteSection}
|
|
1454
1458
|
onReorderSections={onReorderSections}
|
|
1455
1459
|
onBlockMove={onBlockMove}
|
|
1456
1460
|
onMoveSection={siteIndex.pages.length > 1 ? setMovingSectionId : undefined}
|
|
@@ -1690,6 +1694,18 @@ function HistoryWatcher() {
|
|
|
1690
1694
|
return null;
|
|
1691
1695
|
}
|
|
1692
1696
|
|
|
1697
|
+
/** Resolve which historical page to show: explicit selection → the page the
|
|
1698
|
+
* editor is currently on (if it existed at that version) → home → first. */
|
|
1699
|
+
export function resolveHistoryPage(
|
|
1700
|
+
historyState: NonNullable<HistoryState>,
|
|
1701
|
+
historyPageId: string | null,
|
|
1702
|
+
preferredPageId: string | null,
|
|
1703
|
+
) {
|
|
1704
|
+
const pages = historyState.index.pages;
|
|
1705
|
+
const byId = (id: string | null) => (id ? pages.find((p) => p.id === id) : undefined);
|
|
1706
|
+
return byId(historyPageId) ?? byId(preferredPageId) ?? pages.find((p) => p.isHome) ?? pages[0];
|
|
1707
|
+
}
|
|
1708
|
+
|
|
1693
1709
|
function HistoryOrEditorContent({
|
|
1694
1710
|
children,
|
|
1695
1711
|
index,
|
|
@@ -1701,9 +1717,11 @@ function HistoryOrEditorContent({
|
|
|
1701
1717
|
activeSections: LoadedSection[];
|
|
1702
1718
|
activePageId: string | null;
|
|
1703
1719
|
}) {
|
|
1704
|
-
const { historyState } = useEditorContext();
|
|
1720
|
+
const { historyState, historyPageId, setHistoryPageId } = useEditorContext();
|
|
1705
1721
|
const wasInHistory = useRef(false);
|
|
1706
1722
|
|
|
1723
|
+
const historyPage = historyState ? resolveHistoryPage(historyState, historyPageId, activePageId) : null;
|
|
1724
|
+
|
|
1707
1725
|
useEffect(() => {
|
|
1708
1726
|
if (historyState) {
|
|
1709
1727
|
wasInHistory.current = true;
|
|
@@ -1717,8 +1735,22 @@ function HistoryOrEditorContent({
|
|
|
1717
1735
|
}
|
|
1718
1736
|
}, [historyState, index, activeSections, activePageId]);
|
|
1719
1737
|
|
|
1720
|
-
|
|
1721
|
-
|
|
1738
|
+
// Pin the auto-resolved page into context so the toolbar selector agrees,
|
|
1739
|
+
// and keep the nav highlighting in sync with the page being viewed.
|
|
1740
|
+
useEffect(() => {
|
|
1741
|
+
if (!historyState || !historyPage) return;
|
|
1742
|
+
if (!historyPageId) setHistoryPageId(historyPage.id);
|
|
1743
|
+
siteNavChangeEvent.dispatch(
|
|
1744
|
+
generateSiteNav({ index: historyState.index, activeSections: [], activePageId: historyPage.id, mode: "editor", audience: null }),
|
|
1745
|
+
);
|
|
1746
|
+
}, [historyState, historyPage?.id]);
|
|
1747
|
+
|
|
1748
|
+
if (historyState && historyPage) {
|
|
1749
|
+
const position = new Map(historyPage.order.map((id, i) => [id, i]));
|
|
1750
|
+
const pageSections = historyState.sections
|
|
1751
|
+
.filter((s) => position.has(s.section.id))
|
|
1752
|
+
.sort((a, b) => (position.get(a.section.id) ?? 0) - (position.get(b.section.id) ?? 0));
|
|
1753
|
+
return <ViewRenderer sections={pageSections} />;
|
|
1722
1754
|
}
|
|
1723
1755
|
return <>{children}</>;
|
|
1724
1756
|
}
|
|
@@ -1761,6 +1793,7 @@ function EditorContent({
|
|
|
1761
1793
|
onSectionChange,
|
|
1762
1794
|
onAddSection,
|
|
1763
1795
|
onDeleteSection,
|
|
1796
|
+
onDeleteSectionImmediate,
|
|
1764
1797
|
onReorderSections,
|
|
1765
1798
|
onBlockMove,
|
|
1766
1799
|
onMoveSection,
|
|
@@ -1781,6 +1814,7 @@ function EditorContent({
|
|
|
1781
1814
|
onSectionChange: (sectionId: string, content: SectionContent) => void;
|
|
1782
1815
|
onAddSection: (insertIndex: number, selection: string) => void;
|
|
1783
1816
|
onDeleteSection: (sectionId: string) => void;
|
|
1817
|
+
onDeleteSectionImmediate: (sectionId: string) => void;
|
|
1784
1818
|
onReorderSections: (fromIndex: number, toIndex: number) => void;
|
|
1785
1819
|
onBlockMove: (move: BlockMove) => void;
|
|
1786
1820
|
onMoveSection?: (sectionId: string) => void;
|
|
@@ -1975,6 +2009,9 @@ function EditorContent({
|
|
|
1975
2009
|
onChange={editingEnabled ? (newContent) => onSectionChange(section.id, newContent as SectionContent) : undefined}
|
|
1976
2010
|
isEditMode={isEditMode}
|
|
1977
2011
|
openModal={editingEnabled ? openModal : undefined}
|
|
2012
|
+
// Immediate (no confirm modal): used when a colors section's
|
|
2013
|
+
// last swatch is dragged out, auto-removing the empty section.
|
|
2014
|
+
onDelete={editingEnabled ? () => onDeleteSectionImmediate(section.id) : undefined}
|
|
1978
2015
|
/>
|
|
1979
2016
|
</SectionWrapper>
|
|
1980
2017
|
</SectionLayout>
|
|
@@ -2147,12 +2184,16 @@ function EditorToolbar({
|
|
|
2147
2184
|
collabSelfId: string | null;
|
|
2148
2185
|
collabStatus: CollabStatus;
|
|
2149
2186
|
}) {
|
|
2150
|
-
const { isEditMode, viewBranch, setViewBranch, toggleEditMode, historyState, setHistoryState } = useEditorContext();
|
|
2187
|
+
const { isEditMode, viewBranch, setViewBranch, toggleEditMode, historyState, setHistoryState, historyPageId, setHistoryPageId } = useEditorContext();
|
|
2151
2188
|
|
|
2152
2189
|
if (historyState) {
|
|
2190
|
+
const page = resolveHistoryPage(historyState, historyPageId, null);
|
|
2153
2191
|
return (
|
|
2154
2192
|
<HistoryToolbar
|
|
2155
2193
|
date={historyState.date}
|
|
2194
|
+
pages={historyState.index.pages.map((p) => ({ id: p.id, title: p.title }))}
|
|
2195
|
+
activePageId={page?.id ?? ""}
|
|
2196
|
+
onPageChange={setHistoryPageId}
|
|
2156
2197
|
onBackToCurrent={() => setHistoryState(null)}
|
|
2157
2198
|
onRestore={onRestoreClick}
|
|
2158
2199
|
/>
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
import { formatDateTime } from "../../lib/timestamp";
|
|
2
2
|
import { Button } from "../shared/Button";
|
|
3
3
|
|
|
4
|
+
interface HistoryPageOption {
|
|
5
|
+
id: string;
|
|
6
|
+
title: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
4
9
|
interface HistoryToolbarProps {
|
|
5
10
|
date: string;
|
|
11
|
+
pages: HistoryPageOption[];
|
|
12
|
+
activePageId: string;
|
|
13
|
+
onPageChange: (pageId: string) => void;
|
|
6
14
|
onBackToCurrent: () => void;
|
|
7
15
|
onRestore: () => void;
|
|
8
16
|
}
|
|
9
17
|
|
|
10
|
-
export function HistoryToolbar({ date, onBackToCurrent, onRestore }: HistoryToolbarProps) {
|
|
18
|
+
export function HistoryToolbar({ date, pages, activePageId, onPageChange, onBackToCurrent, onRestore }: HistoryToolbarProps) {
|
|
11
19
|
return (
|
|
12
20
|
<div className="fixed top-0 right-0 left-0 z-50 border-b border-base-200 bg-base">
|
|
13
21
|
<div className="mx-auto max-w-screen-xl grid grid-cols-3 items-center px-4 py-2">
|
|
@@ -16,10 +24,24 @@ export function HistoryToolbar({ date, onBackToCurrent, onRestore }: HistoryTool
|
|
|
16
24
|
Back to current
|
|
17
25
|
</Button>
|
|
18
26
|
</div>
|
|
19
|
-
<div className="flex items-center justify-center">
|
|
27
|
+
<div className="flex items-center justify-center gap-2">
|
|
20
28
|
<span className="text-xs font-medium text-base-contrast-light">
|
|
21
29
|
Viewing {formatDateTime(date)}
|
|
22
30
|
</span>
|
|
31
|
+
{pages.length > 1 && (
|
|
32
|
+
<select
|
|
33
|
+
value={activePageId}
|
|
34
|
+
onChange={(e) => onPageChange(e.target.value)}
|
|
35
|
+
className="rounded border border-base-200 bg-base px-1.5 py-0.5 text-xs font-medium text-base-contrast"
|
|
36
|
+
aria-label="Page at this version"
|
|
37
|
+
>
|
|
38
|
+
{pages.map((p) => (
|
|
39
|
+
<option key={p.id} value={p.id}>
|
|
40
|
+
{p.title}
|
|
41
|
+
</option>
|
|
42
|
+
))}
|
|
43
|
+
</select>
|
|
44
|
+
)}
|
|
23
45
|
</div>
|
|
24
46
|
<div className="flex items-center justify-end">
|
|
25
47
|
<Button variant="primary" size="sm" onClick={onRestore}>
|
|
@@ -21,6 +21,13 @@ const darkModeOptions = [
|
|
|
21
21
|
{ label: "Optional (viewer toggle)", value: "optional" },
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
+
const cornerRadiusOptions = [
|
|
25
|
+
{ label: "None (square)", value: "none" },
|
|
26
|
+
{ label: "Small", value: "small" },
|
|
27
|
+
{ label: "Medium", value: "medium" },
|
|
28
|
+
{ label: "Large", value: "large" },
|
|
29
|
+
];
|
|
30
|
+
|
|
24
31
|
const FAVICON_TYPES = ["image/svg+xml", "image/png"];
|
|
25
32
|
const FAVICON_MAX_BYTES = 100 * 1024;
|
|
26
33
|
|
|
@@ -154,6 +161,18 @@ export function SiteSettingsDisplay({ siteConfig, onChange }: Props) {
|
|
|
154
161
|
options={darkModeOptions}
|
|
155
162
|
/>
|
|
156
163
|
|
|
164
|
+
<div>
|
|
165
|
+
<Select
|
|
166
|
+
label="Corner radius"
|
|
167
|
+
value={siteConfig.cornerRadius}
|
|
168
|
+
onChange={(value) => update({ cornerRadius: value as SiteConfig["cornerRadius"] })}
|
|
169
|
+
options={cornerRadiusOptions}
|
|
170
|
+
/>
|
|
171
|
+
<p className="mt-1 text-xs text-base-contrast/70">
|
|
172
|
+
Rounding of cards, media frames, buttons, and panels across the site.
|
|
173
|
+
</p>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
157
176
|
<FontPicker
|
|
158
177
|
label="Heading font"
|
|
159
178
|
value={siteConfig.headingFont}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Corner-radius design token. The site-level `cornerRadius` setting maps to a
|
|
2
|
+
// CSS length for `--radius-outer`; `--radius-inner` derives from it in
|
|
3
|
+
// core/styles/base.css (concentric rule: outer minus the standard 0.25rem
|
|
4
|
+
// gap, floored at 0). Browser-safe: no node imports, no env access.
|
|
5
|
+
|
|
6
|
+
export const CORNER_RADIUS_OPTIONS = ["none", "small", "medium", "large"] as const;
|
|
7
|
+
|
|
8
|
+
export type CornerRadius = (typeof CORNER_RADIUS_OPTIONS)[number];
|
|
9
|
+
|
|
10
|
+
// "medium" matches the pre-token look (Tailwind's rounded-md), so existing
|
|
11
|
+
// sites render unchanged when they take this update.
|
|
12
|
+
export const CORNER_RADIUS_VALUES: Record<CornerRadius, string> = {
|
|
13
|
+
none: "0px",
|
|
14
|
+
small: "0.25rem",
|
|
15
|
+
medium: "0.375rem",
|
|
16
|
+
large: "0.75rem",
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/** CSS length for `--radius-outer`; unknown/missing values fall back to medium. */
|
|
20
|
+
export function cornerRadiusValue(radius?: string | null): string {
|
|
21
|
+
return CORNER_RADIUS_VALUES[radius as CornerRadius] ?? CORNER_RADIUS_VALUES.medium;
|
|
22
|
+
}
|
package/src/lib/registry.ts
CHANGED
|
@@ -83,6 +83,9 @@ export interface SectionProps<T = unknown> {
|
|
|
83
83
|
onChange?: (content: T) => void;
|
|
84
84
|
isEditMode: boolean;
|
|
85
85
|
openModal?: (title: string, content: ReactNode) => void;
|
|
86
|
+
/** Delete this whole section. Used e.g. when a colors section's last swatch
|
|
87
|
+
* is dragged into another section, leaving it empty. */
|
|
88
|
+
onDelete?: () => void;
|
|
86
89
|
}
|
|
87
90
|
|
|
88
91
|
export interface WrapperProps {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { MediaConfigSchema } from "./media";
|
|
3
3
|
import { HexColorSchema, FontNameSchema } from "./shared";
|
|
4
|
+
import { CORNER_RADIUS_OPTIONS } from "../lib/corner-radius";
|
|
4
5
|
|
|
5
6
|
const StatusSchema = z.enum(["draft", "live", "archived", "published"]).transform(
|
|
6
7
|
(val) => val === "published" ? "live" as const : val,
|
|
@@ -177,6 +178,9 @@ export const SiteConfigSchema = z.object({
|
|
|
177
178
|
primaryColor: HexColorSchema.default("#009ca6"),
|
|
178
179
|
primaryContrast: HexColorSchema.default("#f0f0f0"),
|
|
179
180
|
darkMode: z.enum(["light", "dark", "optional"]).default("light"),
|
|
181
|
+
// Maps to the --radius-outer design token (see lib/corner-radius.ts);
|
|
182
|
+
// defaults so pre-existing site-config.json files keep parsing.
|
|
183
|
+
cornerRadius: z.enum(CORNER_RADIUS_OPTIONS).default("medium"),
|
|
180
184
|
headingFont: FontNameSchema.default("system-ui"),
|
|
181
185
|
bodyFont: FontNameSchema.default("system-ui"),
|
|
182
186
|
uppercaseHeadings: z.boolean().default(true),
|