@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.
Files changed (107) hide show
  1. package/dist/{chunk-UMSFICAC.js → chunk-DKOUFIP6.js} +0 -1
  2. package/dist/{chunk-FSVPD7TW.js → chunk-HXXZBTPF.js} +12 -5
  3. package/dist/{chunk-IP6ODLXX.js → chunk-JHSYLVKI.js} +19 -84
  4. package/dist/{chunk-P24YUT3O.js → chunk-MNK7XA6S.js} +1 -1
  5. package/dist/{chunk-EAEX6DS7.js → chunk-V43WVSVS.js} +3 -2
  6. package/dist/components/editor/SectionOrderingModal.d.ts +10 -0
  7. package/dist/components/editor/SectionOrderingModal.d.ts.map +1 -0
  8. package/dist/components/editor/SectionWrapper.d.ts.map +1 -1
  9. package/dist/components/primitives/EditableRichText.d.ts.map +1 -1
  10. package/dist/components/sections/Button/index.d.ts.map +1 -1
  11. package/dist/components/sections/Colors/index.d.ts.map +1 -1
  12. package/dist/components/sections/DoDontMediaGrid/index.d.ts.map +1 -1
  13. package/dist/components/sections/IconList/index.d.ts.map +1 -1
  14. package/dist/components/sections/LinkHeading/index.d.ts.map +1 -1
  15. package/dist/components/sections/MediaGrid/index.d.ts.map +1 -1
  16. package/dist/components/sections/Prose/index.d.ts.map +1 -1
  17. package/dist/components/sections/SplitContent/index.d.ts.map +1 -1
  18. package/dist/components/sections/SubHeading/index.d.ts.map +1 -1
  19. package/dist/components/sections/SubSubHeading/index.d.ts.map +1 -1
  20. package/dist/components/sections/ViewRenderer.d.ts +0 -1
  21. package/dist/components/sections/ViewRenderer.d.ts.map +1 -1
  22. package/dist/components/sections/register-schemas.d.ts.map +1 -1
  23. package/dist/components/sections/register.d.ts.map +1 -1
  24. package/dist/components/shell/EditorShell.d.ts +1 -1
  25. package/dist/components/shell/EditorShell.d.ts.map +1 -1
  26. package/dist/deploy/index.d.ts +2 -0
  27. package/dist/deploy/index.d.ts.map +1 -0
  28. package/dist/deploy/types.d.ts +12 -0
  29. package/dist/deploy/types.d.ts.map +1 -0
  30. package/dist/hooks/useEditorPublish.d.ts.map +1 -1
  31. package/dist/hooks/useMediaPipeline.d.ts.map +1 -1
  32. package/dist/index.d.ts +2 -0
  33. package/dist/index.d.ts.map +1 -1
  34. package/dist/index.js +10 -8
  35. package/dist/lib/dexie.d.ts +4 -1
  36. package/dist/lib/dexie.d.ts.map +1 -1
  37. package/dist/lib/dexie.js +319 -0
  38. package/dist/lib/index.js +3 -3
  39. package/dist/lib/nav.d.ts +2 -6
  40. package/dist/lib/nav.d.ts.map +1 -1
  41. package/dist/lib/registry.d.ts +14 -0
  42. package/dist/lib/registry.d.ts.map +1 -1
  43. package/dist/lib/text.d.ts +3 -0
  44. package/dist/lib/text.d.ts.map +1 -0
  45. package/dist/media/index.d.ts +4 -2
  46. package/dist/media/index.d.ts.map +1 -1
  47. package/dist/media/index.js +8 -6
  48. package/dist/media/provider.d.ts +7 -0
  49. package/dist/media/provider.d.ts.map +1 -0
  50. package/dist/media/resolve.d.ts +3 -2
  51. package/dist/media/resolve.d.ts.map +1 -1
  52. package/dist/media/types.d.ts +0 -9
  53. package/dist/media/types.d.ts.map +1 -1
  54. package/dist/schemas/index.js +3 -3
  55. package/dist/schemas/media.d.ts +0 -3
  56. package/dist/schemas/media.d.ts.map +1 -1
  57. package/dist/schemas/site-config.d.ts +1 -3
  58. package/dist/schemas/site-config.d.ts.map +1 -1
  59. package/dist/storage/index.d.ts +2 -0
  60. package/dist/storage/index.d.ts.map +1 -0
  61. package/dist/storage/types.d.ts +21 -0
  62. package/dist/storage/types.d.ts.map +1 -0
  63. package/package.json +5 -1
  64. package/src/components/editor/DragHandle.tsx +1 -1
  65. package/src/components/editor/SectionOrderingModal.tsx +215 -0
  66. package/src/components/editor/SectionWrapper.tsx +3 -1
  67. package/src/components/primitives/EditableRichText.tsx +4 -2
  68. package/src/components/sections/Button/index.tsx +1 -0
  69. package/src/components/sections/Colors/index.tsx +8 -0
  70. package/src/components/sections/DoDontMediaGrid/index.tsx +8 -0
  71. package/src/components/sections/IconList/index.tsx +4 -0
  72. package/src/components/sections/LinkHeading/index.tsx +2 -0
  73. package/src/components/sections/MediaGrid/index.tsx +8 -0
  74. package/src/components/sections/Prose/index.tsx +2 -0
  75. package/src/components/sections/SplitContent/index.tsx +16 -2
  76. package/src/components/sections/SubHeading/index.tsx +2 -0
  77. package/src/components/sections/SubSubHeading/index.tsx +2 -0
  78. package/src/components/sections/ViewRenderer.tsx +3 -1
  79. package/src/components/sections/register-schemas.ts +0 -2
  80. package/src/components/sections/register.ts +0 -2
  81. package/src/components/shell/EditorShell.tsx +41 -9
  82. package/src/deploy/index.ts +1 -0
  83. package/src/deploy/types.ts +12 -0
  84. package/src/hooks/useEditorPublish.ts +18 -43
  85. package/src/hooks/useMediaPipeline.ts +41 -11
  86. package/src/hooks/useResolvedMedia.ts +3 -3
  87. package/src/index.ts +2 -0
  88. package/src/lib/dexie.ts +28 -1
  89. package/src/lib/nav.ts +16 -9
  90. package/src/lib/registry.ts +10 -0
  91. package/src/lib/text.ts +8 -0
  92. package/src/media/index.ts +13 -4
  93. package/src/media/provider.ts +7 -0
  94. package/src/media/resolve.ts +9 -6
  95. package/src/media/types.ts +0 -9
  96. package/src/schemas/media.ts +0 -1
  97. package/src/schemas/site-config.ts +1 -0
  98. package/src/storage/index.ts +1 -0
  99. package/src/storage/types.ts +23 -0
  100. package/dist/components/sections/SplitContent/SplitContentSettings.d.ts +0 -9
  101. package/dist/components/sections/SplitContent/SplitContentSettings.d.ts.map +0 -1
  102. package/dist/media/github.d.ts +0 -3
  103. package/dist/media/github.d.ts.map +0 -1
  104. package/src/components/sections/SplitContent/SplitContentSettings.d.ts +0 -9
  105. package/src/components/sections/SplitContent/SplitContentSettings.d.ts.map +0 -1
  106. package/src/components/sections/SplitContent/SplitContentSettings.tsx +0 -42
  107. 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.26",
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</>} className="-ml-[34px]">
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
- <DragHandle ref={handleRef} />
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 = placeholder && isContentEmpty(value);
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 { SplitContentSettings } from "./SplitContentSettings";
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
- settingsForm: SplitContentSettings,
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
- getPendingMediaLocalUrls,
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 urls = await getPendingMediaLocalUrls(pi.id);
275
- if (urls) {
276
- const smallestKey = Object.keys(urls).find((k) => k !== "primary") ?? "primary";
277
- if (urls[smallestKey]) urlMap[pi.id] = urls[smallestKey];
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
+ }