@drawnagency/primitives 0.1.26 → 0.1.28
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-UMSFICAC.js → chunk-DKOUFIP6.js} +0 -1
- package/dist/{chunk-FSVPD7TW.js → chunk-HXXZBTPF.js} +12 -5
- package/dist/{chunk-IP6ODLXX.js → chunk-JHSYLVKI.js} +19 -84
- package/dist/{chunk-P24YUT3O.js → chunk-MNK7XA6S.js} +1 -1
- package/dist/{chunk-EAEX6DS7.js → chunk-V43WVSVS.js} +3 -2
- package/dist/components/editor/SectionOrderingModal.d.ts +10 -0
- package/dist/components/editor/SectionOrderingModal.d.ts.map +1 -0
- package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
- package/dist/components/primitives/EditableRichText.d.ts.map +1 -1
- package/dist/components/sections/Button/index.d.ts.map +1 -1
- package/dist/components/sections/Colors/index.d.ts.map +1 -1
- package/dist/components/sections/DoDontMediaGrid/index.d.ts.map +1 -1
- package/dist/components/sections/IconList/index.d.ts.map +1 -1
- package/dist/components/sections/LinkHeading/index.d.ts.map +1 -1
- package/dist/components/sections/MediaGrid/index.d.ts.map +1 -1
- package/dist/components/sections/Prose/index.d.ts.map +1 -1
- package/dist/components/sections/SplitContent/index.d.ts.map +1 -1
- package/dist/components/sections/SubHeading/index.d.ts.map +1 -1
- package/dist/components/sections/SubSubHeading/index.d.ts.map +1 -1
- package/dist/components/sections/ViewRenderer.d.ts +0 -1
- package/dist/components/sections/ViewRenderer.d.ts.map +1 -1
- package/dist/components/sections/register-schemas.d.ts.map +1 -1
- package/dist/components/sections/register.d.ts.map +1 -1
- package/dist/components/shell/EditorShell.d.ts +1 -1
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/deploy/index.d.ts +2 -0
- package/dist/deploy/index.d.ts.map +1 -0
- package/dist/deploy/types.d.ts +12 -0
- package/dist/deploy/types.d.ts.map +1 -0
- package/dist/hooks/useEditorPublish.d.ts.map +1 -1
- package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -8
- package/dist/lib/dexie.d.ts +4 -1
- package/dist/lib/dexie.d.ts.map +1 -1
- package/dist/lib/dexie.js +319 -0
- package/dist/lib/index.js +3 -3
- package/dist/lib/nav.d.ts +2 -6
- package/dist/lib/nav.d.ts.map +1 -1
- package/dist/lib/registry.d.ts +14 -0
- package/dist/lib/registry.d.ts.map +1 -1
- package/dist/lib/text.d.ts +3 -0
- package/dist/lib/text.d.ts.map +1 -0
- package/dist/media/index.d.ts +4 -2
- package/dist/media/index.d.ts.map +1 -1
- package/dist/media/index.js +8 -6
- package/dist/media/provider.d.ts +7 -0
- package/dist/media/provider.d.ts.map +1 -0
- package/dist/media/resolve.d.ts +3 -2
- package/dist/media/resolve.d.ts.map +1 -1
- package/dist/media/types.d.ts +0 -9
- package/dist/media/types.d.ts.map +1 -1
- package/dist/schemas/index.js +3 -3
- package/dist/schemas/media.d.ts +0 -3
- package/dist/schemas/media.d.ts.map +1 -1
- package/dist/schemas/site-config.d.ts +1 -3
- package/dist/schemas/site-config.d.ts.map +1 -1
- package/dist/storage/index.d.ts +2 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/types.d.ts +21 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/package.json +5 -1
- package/src/components/editor/DragHandle.tsx +1 -1
- package/src/components/editor/SectionOrderingModal.tsx +215 -0
- package/src/components/editor/SectionWrapper.tsx +3 -1
- package/src/components/primitives/EditableRichText.tsx +4 -2
- package/src/components/sections/Button/index.tsx +1 -0
- package/src/components/sections/Colors/index.tsx +8 -0
- package/src/components/sections/DoDontMediaGrid/index.tsx +8 -0
- package/src/components/sections/IconList/index.tsx +4 -0
- package/src/components/sections/LinkHeading/index.tsx +2 -0
- package/src/components/sections/MediaGrid/index.tsx +8 -0
- package/src/components/sections/Prose/index.tsx +2 -0
- package/src/components/sections/SplitContent/index.tsx +16 -2
- package/src/components/sections/SubHeading/index.tsx +2 -0
- package/src/components/sections/SubSubHeading/index.tsx +2 -0
- package/src/components/sections/ViewRenderer.tsx +3 -1
- package/src/components/sections/register-schemas.ts +0 -2
- package/src/components/sections/register.ts +0 -2
- package/src/components/shell/EditorShell.tsx +41 -9
- package/src/deploy/index.ts +1 -0
- package/src/deploy/types.ts +12 -0
- package/src/hooks/useEditorPublish.ts +18 -43
- package/src/hooks/useMediaPipeline.ts +41 -11
- package/src/hooks/useResolvedMedia.ts +3 -3
- package/src/index.ts +2 -0
- package/src/lib/dexie.ts +28 -1
- package/src/lib/nav.ts +16 -9
- package/src/lib/registry.ts +10 -0
- package/src/lib/text.ts +8 -0
- package/src/media/index.ts +13 -4
- package/src/media/provider.ts +7 -0
- package/src/media/resolve.ts +9 -6
- package/src/media/types.ts +0 -9
- package/src/schemas/media.ts +0 -1
- package/src/schemas/site-config.ts +1 -0
- package/src/storage/index.ts +1 -0
- package/src/storage/types.ts +23 -0
- package/dist/components/sections/SplitContent/SplitContentSettings.d.ts +0 -9
- package/dist/components/sections/SplitContent/SplitContentSettings.d.ts.map +0 -1
- package/dist/media/github.d.ts +0 -3
- package/dist/media/github.d.ts.map +0 -1
- package/src/components/sections/SplitContent/SplitContentSettings.d.ts +0 -9
- package/src/components/sections/SplitContent/SplitContentSettings.d.ts.map +0 -1
- package/src/components/sections/SplitContent/SplitContentSettings.tsx +0 -42
- package/src/media/github.ts +0 -72
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@drawnagency/primitives",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
"./package.json": "./package.json",
|
|
@@ -16,6 +16,10 @@
|
|
|
16
16
|
"types": "./dist/lib/index.d.ts",
|
|
17
17
|
"default": "./dist/lib/index.js"
|
|
18
18
|
},
|
|
19
|
+
"./lib/dexie": {
|
|
20
|
+
"types": "./dist/lib/dexie.d.ts",
|
|
21
|
+
"default": "./dist/lib/dexie.js"
|
|
22
|
+
},
|
|
19
23
|
"./auth": {
|
|
20
24
|
"types": "./dist/auth/index.d.ts",
|
|
21
25
|
"default": "./dist/auth/index.js"
|
|
@@ -5,7 +5,7 @@ import { Tooltip, Kbd } from "../shared/Tooltip";
|
|
|
5
5
|
|
|
6
6
|
export const DragHandle = forwardRef<HTMLButtonElement>(function DragHandle(_, ref) {
|
|
7
7
|
return (
|
|
8
|
-
<Tooltip content={<><Kbd>Drag</Kbd> to move</>}
|
|
8
|
+
<Tooltip content={<><Kbd>Drag</Kbd> to move</>}>
|
|
9
9
|
<IconButton
|
|
10
10
|
ref={ref}
|
|
11
11
|
icon={<DragHandleIcon size={18} />}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { useState, useEffect, useRef } from "react";
|
|
2
|
+
import type { LoadedSection } from "../../lib/loader";
|
|
3
|
+
import type { Thumbnail } from "../../lib/registry";
|
|
4
|
+
import { getSection } from "../../lib/registry";
|
|
5
|
+
import { getMediaProvider } from "../../media";
|
|
6
|
+
import type { MediaManifest } from "../../media/types";
|
|
7
|
+
import { DragHandle } from "./DragHandle";
|
|
8
|
+
import { cn } from "../../lib/cn";
|
|
9
|
+
|
|
10
|
+
interface SectionOrderingModalProps {
|
|
11
|
+
sections: LoadedSection[];
|
|
12
|
+
mediaManifest: MediaManifest;
|
|
13
|
+
onReorder: (fromIndex: number, toIndex: number) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function resolveThumbnailSrc(thumb: Thumbnail, manifest: MediaManifest): string | null {
|
|
17
|
+
if (thumb.type === "color") return null;
|
|
18
|
+
const item = manifest.images[thumb.src];
|
|
19
|
+
if (!item) return null;
|
|
20
|
+
const provider = getMediaProvider();
|
|
21
|
+
const resolved = provider.resolve(item, [200]);
|
|
22
|
+
if (!resolved) return null;
|
|
23
|
+
return resolved.tag === "img" || resolved.tag === "video" ? resolved.src : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function SectionRow({
|
|
27
|
+
section,
|
|
28
|
+
index,
|
|
29
|
+
mediaManifest,
|
|
30
|
+
onReorder,
|
|
31
|
+
}: {
|
|
32
|
+
section: LoadedSection;
|
|
33
|
+
index: number;
|
|
34
|
+
mediaManifest: MediaManifest;
|
|
35
|
+
onReorder: (fromIndex: number, toIndex: number) => void;
|
|
36
|
+
}) {
|
|
37
|
+
const rowRef = useRef<HTMLDivElement>(null);
|
|
38
|
+
const handleRef = useRef<HTMLButtonElement>(null);
|
|
39
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
40
|
+
const [closestEdge, setClosestEdge] = useState<"top" | "bottom" | null>(null);
|
|
41
|
+
|
|
42
|
+
const definition = getSection(section.section.type);
|
|
43
|
+
const label = definition?.getLabel?.(section.section) ?? definition?.label ?? section.section.type;
|
|
44
|
+
const typeLabel = definition?.label ?? section.section.type;
|
|
45
|
+
const allThumbnails = definition?.getThumbnails?.(section.section) ?? [];
|
|
46
|
+
const thumbnails = allThumbnails.slice(0, 3);
|
|
47
|
+
const remainingCount = Math.max(0, allThumbnails.length - 3);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
const row = rowRef.current;
|
|
51
|
+
const handle = handleRef.current;
|
|
52
|
+
if (!row || !handle) return;
|
|
53
|
+
|
|
54
|
+
let cleanup: (() => void) | undefined;
|
|
55
|
+
let cancelled = false;
|
|
56
|
+
|
|
57
|
+
Promise.all([
|
|
58
|
+
import("@atlaskit/pragmatic-drag-and-drop/element/adapter"),
|
|
59
|
+
import("@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge"),
|
|
60
|
+
]).then(([{ draggable, dropTargetForElements }, { attachClosestEdge, extractClosestEdge }]) => {
|
|
61
|
+
if (cancelled) return;
|
|
62
|
+
|
|
63
|
+
const cleanupDraggable = draggable({
|
|
64
|
+
element: row,
|
|
65
|
+
dragHandle: handle,
|
|
66
|
+
getInitialData: () => ({ dragType: "ordering-row", index }),
|
|
67
|
+
onGenerateDragPreview: () => {
|
|
68
|
+
row.style.opacity = "0.4";
|
|
69
|
+
requestAnimationFrame(() => {
|
|
70
|
+
row.style.opacity = "";
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
onDragStart: () => setIsDragging(true),
|
|
74
|
+
onDrop: () => setIsDragging(false),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const cleanupDropTarget = dropTargetForElements({
|
|
78
|
+
element: row,
|
|
79
|
+
canDrop: ({ source }) => source.data.dragType === "ordering-row",
|
|
80
|
+
getData: ({ input, element }) =>
|
|
81
|
+
attachClosestEdge({ index }, { input, element, allowedEdges: ["top", "bottom"] }),
|
|
82
|
+
onDragEnter: ({ self }) => {
|
|
83
|
+
const edge = extractClosestEdge(self.data);
|
|
84
|
+
setClosestEdge(edge === "top" || edge === "bottom" ? edge : null);
|
|
85
|
+
},
|
|
86
|
+
onDrag: ({ self }) => {
|
|
87
|
+
const edge = extractClosestEdge(self.data);
|
|
88
|
+
setClosestEdge(edge === "top" || edge === "bottom" ? edge : null);
|
|
89
|
+
},
|
|
90
|
+
onDragLeave: () => setClosestEdge(null),
|
|
91
|
+
onDrop: ({ source, self }) => {
|
|
92
|
+
setClosestEdge(null);
|
|
93
|
+
const fromIndex = source.data.index as number;
|
|
94
|
+
const edge = extractClosestEdge(self.data);
|
|
95
|
+
let toIndex = index;
|
|
96
|
+
if (edge === "bottom") toIndex = index + 1;
|
|
97
|
+
if (fromIndex < toIndex) toIndex--;
|
|
98
|
+
if (fromIndex !== toIndex) {
|
|
99
|
+
onReorder(fromIndex, toIndex);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
cleanup = () => {
|
|
105
|
+
cleanupDraggable();
|
|
106
|
+
cleanupDropTarget();
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return () => {
|
|
111
|
+
cancelled = true;
|
|
112
|
+
cleanup?.();
|
|
113
|
+
};
|
|
114
|
+
}, [index, onReorder]);
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<div
|
|
118
|
+
ref={rowRef}
|
|
119
|
+
className={cn(
|
|
120
|
+
"relative flex items-center gap-1",
|
|
121
|
+
isDragging && "opacity-50",
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
{closestEdge === "top" && (
|
|
125
|
+
<div className="absolute top-0 right-0 left-0 z-10 h-0.5 -translate-y-1/2 bg-primary" />
|
|
126
|
+
)}
|
|
127
|
+
{closestEdge === "bottom" && (
|
|
128
|
+
<div className="absolute right-0 bottom-0 left-0 z-10 h-0.5 translate-y-1/2 bg-primary" />
|
|
129
|
+
)}
|
|
130
|
+
|
|
131
|
+
<div className="shrink-0 [&_[role=tooltip]]:hidden">
|
|
132
|
+
<DragHandle ref={handleRef} />
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
<div className={cn(
|
|
136
|
+
"flex min-w-0 flex-1 items-center gap-3 rounded-md border border-base-200 bg-base px-3 py-2",
|
|
137
|
+
isDragging && "outline outline-2 outline-primary/50",
|
|
138
|
+
)}>
|
|
139
|
+
<div className="min-w-0 flex-1">
|
|
140
|
+
<div className="text-xs text-base-contrast-light">{typeLabel}</div>
|
|
141
|
+
<div className="truncate text-sm font-medium text-base-contrast">
|
|
142
|
+
{label}
|
|
143
|
+
</div>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{thumbnails.length > 0 && (
|
|
147
|
+
<div className="flex items-center -space-x-2">
|
|
148
|
+
{thumbnails.map((thumb, i) => (
|
|
149
|
+
thumb.type === "color" ? (
|
|
150
|
+
<div
|
|
151
|
+
key={i}
|
|
152
|
+
className="h-8 w-8 rounded border-2 border-base"
|
|
153
|
+
style={{ backgroundColor: thumb.value }}
|
|
154
|
+
/>
|
|
155
|
+
) : (
|
|
156
|
+
<ResolvedImage
|
|
157
|
+
key={i}
|
|
158
|
+
thumb={thumb}
|
|
159
|
+
manifest={mediaManifest}
|
|
160
|
+
/>
|
|
161
|
+
)
|
|
162
|
+
))}
|
|
163
|
+
{remainingCount > 0 && (
|
|
164
|
+
<div className="ml-1 flex h-8 w-8 items-center justify-center rounded border-2 border-base bg-base-accent text-xs text-base-contrast-light">
|
|
165
|
+
+{remainingCount}
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
</div>
|
|
169
|
+
)}
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function ResolvedImage({
|
|
176
|
+
thumb,
|
|
177
|
+
manifest,
|
|
178
|
+
}: {
|
|
179
|
+
thumb: Thumbnail & { type: "image" };
|
|
180
|
+
manifest: MediaManifest;
|
|
181
|
+
}) {
|
|
182
|
+
const src = resolveThumbnailSrc(thumb, manifest);
|
|
183
|
+
if (!src) {
|
|
184
|
+
return (
|
|
185
|
+
<div className="h-8 w-8 rounded border-2 border-base bg-base-accent" />
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
return (
|
|
189
|
+
<img
|
|
190
|
+
src={src}
|
|
191
|
+
alt={thumb.alt ?? ""}
|
|
192
|
+
className="h-8 w-8 rounded border-2 border-base object-cover"
|
|
193
|
+
/>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function SectionOrderingModal({
|
|
198
|
+
sections,
|
|
199
|
+
mediaManifest,
|
|
200
|
+
onReorder,
|
|
201
|
+
}: SectionOrderingModalProps) {
|
|
202
|
+
return (
|
|
203
|
+
<div className="flex flex-col gap-2 overflow-y-auto">
|
|
204
|
+
{sections.map((loadedSection, index) => (
|
|
205
|
+
<SectionRow
|
|
206
|
+
key={loadedSection.section.id}
|
|
207
|
+
section={loadedSection}
|
|
208
|
+
index={index}
|
|
209
|
+
mediaManifest={mediaManifest}
|
|
210
|
+
onReorder={onReorder}
|
|
211
|
+
/>
|
|
212
|
+
))}
|
|
213
|
+
</div>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
@@ -273,7 +273,9 @@ export function SectionWrapper({
|
|
|
273
273
|
alwaysShow ? "opacity-100" : "opacity-0 group-hover:opacity-100",
|
|
274
274
|
)}
|
|
275
275
|
>
|
|
276
|
-
<
|
|
276
|
+
<div className="-ml-[34px]">
|
|
277
|
+
<DragHandle ref={handleRef} />
|
|
278
|
+
</div>
|
|
277
279
|
<span className="pointer-events-none select-none whitespace-nowrap text-sm capitalize text-base-contrast-light/80">
|
|
278
280
|
{definition.label}
|
|
279
281
|
</span>
|
|
@@ -19,13 +19,15 @@ function isContentEmpty(html: string): boolean {
|
|
|
19
19
|
return html.replace(/<[^>]*>/g, "").trim().length === 0;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
const DEFAULT_PLACEHOLDER = "Click to edit…";
|
|
23
|
+
|
|
22
24
|
export function EditableRichText({
|
|
23
25
|
value,
|
|
24
26
|
onChange,
|
|
25
27
|
isEditMode,
|
|
26
28
|
preset = "rich",
|
|
27
29
|
className,
|
|
28
|
-
placeholder,
|
|
30
|
+
placeholder = DEFAULT_PLACEHOLDER,
|
|
29
31
|
}: EditableRichTextProps) {
|
|
30
32
|
const { isEditorActive, editor, activate, deactivate } =
|
|
31
33
|
useEditableRichText({ value, onChange, preset, placeholder });
|
|
@@ -106,7 +108,7 @@ export function EditableRichText({
|
|
|
106
108
|
// Edit mode, not yet active: static HTML with click-to-activate.
|
|
107
109
|
// When content is empty, show the placeholder so the area is visibly clickable.
|
|
108
110
|
if (!isEditorActive || !editor) {
|
|
109
|
-
const empty =
|
|
111
|
+
const empty = isContentEmpty(value);
|
|
110
112
|
if (empty) {
|
|
111
113
|
return (
|
|
112
114
|
<div className={className} onClick={handleClick}>
|
|
@@ -26,6 +26,7 @@ export default defineSection({
|
|
|
26
26
|
/>
|
|
27
27
|
),
|
|
28
28
|
defaults: () => ({ type: "button" as const, content: { text: "Button" } }),
|
|
29
|
+
getLabel: (content) => content.content.text,
|
|
29
30
|
settings: {
|
|
30
31
|
href: { type: "text", label: "URL", default: "", target: "content", placeholder: "https://..." },
|
|
31
32
|
target: {
|
|
@@ -33,6 +33,14 @@ export default defineSection({
|
|
|
33
33
|
type: "colors" as const,
|
|
34
34
|
content: { colors: [{ spaces: [{ hex: "#000000" }] }] },
|
|
35
35
|
}),
|
|
36
|
+
getLabel: (content) => {
|
|
37
|
+
const n = content.content.colors.length;
|
|
38
|
+
return `${n} color${n === 1 ? "" : "s"}`;
|
|
39
|
+
},
|
|
40
|
+
getThumbnails: (content) =>
|
|
41
|
+
content.content.colors
|
|
42
|
+
.filter((c) => c.spaces[0]?.hex)
|
|
43
|
+
.map((c) => ({ type: "color" as const, value: c.spaces[0].hex! })),
|
|
36
44
|
settings: {
|
|
37
45
|
columns: {
|
|
38
46
|
type: "select", label: "Columns", default: "3", coerce: "number",
|
|
@@ -40,6 +40,14 @@ export default defineSection({
|
|
|
40
40
|
],
|
|
41
41
|
},
|
|
42
42
|
}),
|
|
43
|
+
getLabel: (content) => {
|
|
44
|
+
const n = content.content.media.length;
|
|
45
|
+
return `${n} item${n === 1 ? "" : "s"}`;
|
|
46
|
+
},
|
|
47
|
+
getThumbnails: (content) =>
|
|
48
|
+
content.content.media
|
|
49
|
+
.filter((m) => m.imageId)
|
|
50
|
+
.map((m) => ({ type: "image" as const, src: m.imageId })),
|
|
43
51
|
settings: {
|
|
44
52
|
columns: {
|
|
45
53
|
type: "select", label: "Columns", default: "2", target: "content", coerce: "number",
|
|
@@ -32,5 +32,9 @@ export default defineSection({
|
|
|
32
32
|
type: "icon_list" as const,
|
|
33
33
|
content: { items: [{ label: "", text: "" }] },
|
|
34
34
|
}),
|
|
35
|
+
getLabel: (content) => {
|
|
36
|
+
const n = content.content.items.length;
|
|
37
|
+
return `${n} icon${n === 1 ? "" : "s"}`;
|
|
38
|
+
},
|
|
35
39
|
settingsForm: IconListSettings,
|
|
36
40
|
});
|
|
@@ -11,6 +11,7 @@ export default defineSection({
|
|
|
11
11
|
type: "link_heading",
|
|
12
12
|
label: "Link Heading",
|
|
13
13
|
schema,
|
|
14
|
+
navRole: "h1",
|
|
14
15
|
component: ({ content, onChange }) => (
|
|
15
16
|
<HeadingSection
|
|
16
17
|
heading={content.content.heading}
|
|
@@ -21,4 +22,5 @@ export default defineSection({
|
|
|
21
22
|
/>
|
|
22
23
|
),
|
|
23
24
|
defaults: () => ({ type: "link_heading" as const, content: { heading: "New Section" } }),
|
|
25
|
+
getLabel: (content) => content.content.heading,
|
|
24
26
|
});
|
|
@@ -34,6 +34,14 @@ export default defineSection({
|
|
|
34
34
|
content: { columns: 2, media: [{ type: "image" as const, imageId: "" }] },
|
|
35
35
|
options: {},
|
|
36
36
|
}),
|
|
37
|
+
getLabel: (content) => {
|
|
38
|
+
const n = content.content.media.length;
|
|
39
|
+
return `${n} image${n === 1 ? "" : "s"}`;
|
|
40
|
+
},
|
|
41
|
+
getThumbnails: (content) =>
|
|
42
|
+
content.content.media
|
|
43
|
+
.filter((m) => m.imageId)
|
|
44
|
+
.map((m) => ({ type: "image" as const, src: m.imageId })),
|
|
37
45
|
settings: {
|
|
38
46
|
columns: {
|
|
39
47
|
type: "select", label: "Columns", default: "2", target: "content", coerce: "number",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { defineSection } from "../../../lib/registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import Prose from "./Prose";
|
|
4
|
+
import { stripHtmlToPlainText, truncate } from "../../../lib/text";
|
|
4
5
|
|
|
5
6
|
const schema = z.object({
|
|
6
7
|
type: z.literal("prose"),
|
|
@@ -15,4 +16,5 @@ export default defineSection({
|
|
|
15
16
|
<Prose body={content.content.body} onChange={onChange ? (c) => onChange(c as typeof content) : undefined} />
|
|
16
17
|
),
|
|
17
18
|
defaults: () => ({ type: "prose" as const, content: { body: "<p></p>" } }),
|
|
19
|
+
getLabel: (content) => truncate(stripHtmlToPlainText(content.content.body), 60),
|
|
18
20
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { defineSection } from "../../../lib/registry";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import SplitContent from "./SplitContent";
|
|
4
|
-
import {
|
|
4
|
+
import { stripHtmlToPlainText, truncate } from "../../../lib/text";
|
|
5
5
|
|
|
6
6
|
const schema = z.object({
|
|
7
7
|
type: z.literal("split_content"),
|
|
@@ -35,5 +35,19 @@ export default defineSection({
|
|
|
35
35
|
type: "split_content" as const,
|
|
36
36
|
content: { imageId: undefined, body: "<p></p>" },
|
|
37
37
|
}),
|
|
38
|
-
|
|
38
|
+
settings: {
|
|
39
|
+
imagePosition: {
|
|
40
|
+
type: "select",
|
|
41
|
+
label: "Image Position",
|
|
42
|
+
default: "left",
|
|
43
|
+
options: [
|
|
44
|
+
{ label: "Left", value: "left" },
|
|
45
|
+
{ label: "Right", value: "right" },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
border: { type: "checkbox", label: "Show border", default: false },
|
|
49
|
+
},
|
|
50
|
+
getLabel: (content) => truncate(stripHtmlToPlainText(content.content.body), 60),
|
|
51
|
+
getThumbnails: (content) =>
|
|
52
|
+
content.content.imageId ? [{ type: "image" as const, src: content.content.imageId }] : [],
|
|
39
53
|
});
|
|
@@ -11,6 +11,7 @@ export default defineSection({
|
|
|
11
11
|
type: "sub_heading",
|
|
12
12
|
label: "Sub Heading",
|
|
13
13
|
schema,
|
|
14
|
+
navRole: "h2",
|
|
14
15
|
component: ({ content, onChange }) => (
|
|
15
16
|
<HeadingSection
|
|
16
17
|
heading={content.content.heading}
|
|
@@ -21,6 +22,7 @@ export default defineSection({
|
|
|
21
22
|
/>
|
|
22
23
|
),
|
|
23
24
|
defaults: () => ({ type: "sub_heading" as const, content: { heading: "New Sub Heading" } }),
|
|
25
|
+
getLabel: (content) => content.content.heading,
|
|
24
26
|
settings: {
|
|
25
27
|
excludeFromNav: { type: "checkbox", label: "Exclude from navigation", default: false, target: "content" },
|
|
26
28
|
},
|
|
@@ -11,6 +11,7 @@ export default defineSection({
|
|
|
11
11
|
type: "sub_sub_heading",
|
|
12
12
|
label: "Sub Sub Heading",
|
|
13
13
|
schema,
|
|
14
|
+
navRole: "h3",
|
|
14
15
|
component: ({ content, onChange }) => (
|
|
15
16
|
<HeadingSection
|
|
16
17
|
heading={content.content.heading}
|
|
@@ -21,6 +22,7 @@ export default defineSection({
|
|
|
21
22
|
/>
|
|
22
23
|
),
|
|
23
24
|
defaults: () => ({ type: "sub_sub_heading" as const, content: { heading: "New Sub Sub Heading" } }),
|
|
25
|
+
getLabel: (content) => content.content.heading,
|
|
24
26
|
settings: {
|
|
25
27
|
excludeFromNav: { type: "checkbox", label: "Exclude from navigation", default: false, target: "content" },
|
|
26
28
|
},
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import "./register";
|
|
1
|
+
import { ensureSectionsRegistered } from "./register";
|
|
2
2
|
import { getSection } from "../../lib/registry";
|
|
3
|
+
|
|
4
|
+
ensureSectionsRegistered();
|
|
3
5
|
import { SectionLayout } from "./SectionLayout";
|
|
4
6
|
import type { LoadedSection } from "../../lib/loader";
|
|
5
7
|
|
|
@@ -15,8 +15,6 @@ const allDefs = [linkHeading, subHeading, subSubHeading, prose, mediaGrid,
|
|
|
15
15
|
splitContent, button, colors, doDontList, doDontImageGrid, iconList,
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
-
allDefs.forEach((def) => registerSchema(def.type, def.schema));
|
|
19
|
-
|
|
20
18
|
let _ensured = false;
|
|
21
19
|
export function ensureSchemasRegistered(): number {
|
|
22
20
|
if (!_ensured) {
|
|
@@ -15,8 +15,6 @@ const allDefs = [linkHeading, subHeading, subSubHeading, prose, mediaGrid,
|
|
|
15
15
|
splitContent, button, colors, doDontList, doDontImageGrid, iconList,
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
-
allDefs.forEach(registerSection);
|
|
19
|
-
|
|
20
18
|
let _ensured = false;
|
|
21
19
|
export function ensureSectionsRegistered(): number {
|
|
22
20
|
if (!_ensured) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Fragment, useState, useCallback, useEffect, useRef, type ReactNode } from "react";
|
|
2
|
+
import "@/lib/media";
|
|
2
3
|
|
|
3
4
|
import { monitorForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
4
5
|
import type { LoadedSection } from "../../lib/loader";
|
|
@@ -16,9 +17,12 @@ import { MediaLibraryModal } from "./MediaLibraryModal";
|
|
|
16
17
|
import { MediaLibraryContext } from "./MediaLibraryContext";
|
|
17
18
|
import { ProcessingIndicator } from "./ProcessingIndicator";
|
|
18
19
|
import { SectionSkeleton } from "./SectionSkeleton";
|
|
19
|
-
import "../sections/register";
|
|
20
|
+
import { ensureSectionsRegistered } from "../sections/register";
|
|
20
21
|
import { getSection, getAllSections } from "../../lib/registry";
|
|
22
|
+
|
|
23
|
+
ensureSectionsRegistered();
|
|
21
24
|
import { SectionWrapper } from "../editor/SectionWrapper";
|
|
25
|
+
import { SectionOrderingModal } from "../editor/SectionOrderingModal";
|
|
22
26
|
import { SectionLayout } from "../sections/SectionLayout";
|
|
23
27
|
import {
|
|
24
28
|
initEditorStore,
|
|
@@ -30,7 +34,7 @@ import {
|
|
|
30
34
|
persistMediaManifest,
|
|
31
35
|
getMediaManifest,
|
|
32
36
|
getPendingMediaItems,
|
|
33
|
-
|
|
37
|
+
getPendingMediaBlobs,
|
|
34
38
|
getPendingMediaDeletions,
|
|
35
39
|
} from "../../lib/dexie";
|
|
36
40
|
import { useEditorPersistence } from "../../hooks/useEditorPersistence";
|
|
@@ -47,7 +51,7 @@ import { SplitButton } from "../shared/SplitButton";
|
|
|
47
51
|
import { IconButton } from "../shared/IconButton";
|
|
48
52
|
import { SegmentedControl } from "../shared/SegmentedControl";
|
|
49
53
|
import { SettingsIcon } from "../shared/icons";
|
|
50
|
-
import { ImageIcon, X } from "lucide-react";
|
|
54
|
+
import { ImageIcon, ListOrderedIcon, X } from "lucide-react";
|
|
51
55
|
import { ErrorBoundary } from "../shared/ErrorBoundary";
|
|
52
56
|
import { HistoryToolbar } from "./HistoryToolbar";
|
|
53
57
|
import { RestoreModal } from "./RestoreModal";
|
|
@@ -110,6 +114,7 @@ export default function EditorShell({
|
|
|
110
114
|
const [viewSections, setViewSections] = useState<LoadedSection[] | null>(null);
|
|
111
115
|
const [isLoadingViewContent, setIsLoadingViewContent] = useState(false);
|
|
112
116
|
const [showRestoreModal, setShowRestoreModal] = useState(false);
|
|
117
|
+
const [showOrderingModal, setShowOrderingModal] = useState(false);
|
|
113
118
|
const [isRestoring, setIsRestoring] = useState(false);
|
|
114
119
|
|
|
115
120
|
const siteIndexRef = useRef<SiteIndex>({ siteId, order: [], sections: {} });
|
|
@@ -149,6 +154,9 @@ export default function EditorShell({
|
|
|
149
154
|
pendingMediaItems: mediaPipeline.pendingMediaItems,
|
|
150
155
|
pendingMediaDeletions: mediaPipeline.pendingDeletions,
|
|
151
156
|
onMediaPublished: (publishedItems, publishedDeletions) => {
|
|
157
|
+
for (const url of Object.values(mediaPipeline.pendingLocalUrls)) {
|
|
158
|
+
URL.revokeObjectURL(url);
|
|
159
|
+
}
|
|
152
160
|
mediaPipeline.setPendingMediaItems([]);
|
|
153
161
|
mediaPipeline.setPendingLocalUrls({});
|
|
154
162
|
mediaPipeline.setPendingDeletions([]);
|
|
@@ -265,16 +273,16 @@ export default function EditorShell({
|
|
|
265
273
|
siteIndexRef.current = loadedIndex;
|
|
266
274
|
applySiteConfigPreview(loadedConfig);
|
|
267
275
|
|
|
268
|
-
// Load pending media from Dexie
|
|
276
|
+
// Load pending media from Dexie — recreate blob URLs from stored blobs
|
|
269
277
|
const savedPendingItems = await getPendingMediaItems();
|
|
270
278
|
if (!cancelled && savedPendingItems.length > 0) {
|
|
271
279
|
mediaPipeline.setPendingMediaItems(savedPendingItems);
|
|
272
280
|
const urlMap: Record<string, string> = {};
|
|
273
281
|
for (const pi of savedPendingItems) {
|
|
274
|
-
const
|
|
275
|
-
if (
|
|
276
|
-
const
|
|
277
|
-
if (
|
|
282
|
+
const blobs = await getPendingMediaBlobs(pi.id);
|
|
283
|
+
if (blobs) {
|
|
284
|
+
const displayKey = Object.keys(blobs).find((k) => k !== "primary" && k !== "poster") ?? "primary";
|
|
285
|
+
if (blobs[displayKey]) urlMap[pi.id] = URL.createObjectURL(blobs[displayKey]);
|
|
278
286
|
}
|
|
279
287
|
}
|
|
280
288
|
if (!cancelled) mediaPipeline.setPendingLocalUrls(urlMap);
|
|
@@ -352,6 +360,9 @@ export default function EditorShell({
|
|
|
352
360
|
await discardLocalChanges();
|
|
353
361
|
setLocalChangesExist(false);
|
|
354
362
|
setDirtySectionIds(new Set());
|
|
363
|
+
for (const url of Object.values(mediaPipeline.pendingLocalUrls)) {
|
|
364
|
+
URL.revokeObjectURL(url);
|
|
365
|
+
}
|
|
355
366
|
mediaPipeline.setPendingMediaItems([]);
|
|
356
367
|
mediaPipeline.setPendingLocalUrls({});
|
|
357
368
|
mediaPipeline.setPendingDeletions([]);
|
|
@@ -534,7 +545,6 @@ export default function EditorShell({
|
|
|
534
545
|
})),
|
|
535
546
|
siteIndex: historyContent.index,
|
|
536
547
|
siteConfig: historyContent.siteConfig,
|
|
537
|
-
targetBranch: "main",
|
|
538
548
|
};
|
|
539
549
|
|
|
540
550
|
const response = await fetch("/api/save", {
|
|
@@ -626,6 +636,7 @@ export default function EditorShell({
|
|
|
626
636
|
buildElapsed={buildStatus.elapsedSeconds}
|
|
627
637
|
onBuildDismiss={buildStatus.dismiss}
|
|
628
638
|
onRestoreClick={() => setShowRestoreModal(true)}
|
|
639
|
+
onOrderingClick={() => setShowOrderingModal(true)}
|
|
629
640
|
/>
|
|
630
641
|
|
|
631
642
|
<HistoryOrEditorContent sections={sections}>
|
|
@@ -704,6 +715,18 @@ export default function EditorShell({
|
|
|
704
715
|
maxFileSize={siteConfig?.media.maxFileSize}
|
|
705
716
|
/>
|
|
706
717
|
</EditorModal>
|
|
718
|
+
<EditorModal
|
|
719
|
+
isOpen={showOrderingModal}
|
|
720
|
+
onClose={() => setShowOrderingModal(false)}
|
|
721
|
+
title="Reorder Sections"
|
|
722
|
+
size="settings"
|
|
723
|
+
>
|
|
724
|
+
<SectionOrderingModal
|
|
725
|
+
sections={sections}
|
|
726
|
+
mediaManifest={mediaManifest}
|
|
727
|
+
onReorder={onReorderSections}
|
|
728
|
+
/>
|
|
729
|
+
</EditorModal>
|
|
707
730
|
<RestoreHandler
|
|
708
731
|
showRestoreModal={showRestoreModal}
|
|
709
732
|
setShowRestoreModal={setShowRestoreModal}
|
|
@@ -1097,6 +1120,7 @@ function EditorToolbar({
|
|
|
1097
1120
|
buildElapsed,
|
|
1098
1121
|
onBuildDismiss,
|
|
1099
1122
|
onRestoreClick,
|
|
1123
|
+
onOrderingClick,
|
|
1100
1124
|
}: {
|
|
1101
1125
|
buttonState: "synced" | "publish" | "saveAndPublish";
|
|
1102
1126
|
localChangesExist: boolean;
|
|
@@ -1113,6 +1137,7 @@ function EditorToolbar({
|
|
|
1113
1137
|
buildElapsed: number;
|
|
1114
1138
|
onBuildDismiss: () => void;
|
|
1115
1139
|
onRestoreClick: () => void;
|
|
1140
|
+
onOrderingClick: () => void;
|
|
1116
1141
|
}) {
|
|
1117
1142
|
const { isEditMode, viewBranch, setViewBranch, toggleEditMode, historyState, setHistoryState } = useEditorContext();
|
|
1118
1143
|
|
|
@@ -1177,6 +1202,13 @@ function EditorToolbar({
|
|
|
1177
1202
|
</div>
|
|
1178
1203
|
<div className="flex items-center justify-end gap-2">
|
|
1179
1204
|
<ProcessingIndicator items={processingItems} />
|
|
1205
|
+
<IconButton
|
|
1206
|
+
icon={<ListOrderedIcon size={16} />}
|
|
1207
|
+
label="Reorder sections"
|
|
1208
|
+
size="md"
|
|
1209
|
+
onClick={onOrderingClick}
|
|
1210
|
+
className="border border-base-200 bg-base-accent"
|
|
1211
|
+
/>
|
|
1180
1212
|
<IconButton
|
|
1181
1213
|
icon={<ImageIcon size={16} />}
|
|
1182
1214
|
label="Media library"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { DeployStatus, DeployStatusProvider } from "./types";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface DeployStatus {
|
|
2
|
+
deployId: string;
|
|
3
|
+
state: "building" | "ready" | "error";
|
|
4
|
+
deployUrl: string;
|
|
5
|
+
commitSha: string | null;
|
|
6
|
+
updatedAt: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface DeployStatusProvider {
|
|
10
|
+
get(siteId: string): Promise<DeployStatus | null>;
|
|
11
|
+
upsert(siteId: string, data: Omit<DeployStatus, "updatedAt">): Promise<void>;
|
|
12
|
+
}
|