@drawnagency/primitives 0.1.72 → 0.1.73
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-2HZB675H.js → chunk-7T2WQSWF.js} +2 -2
- package/dist/{chunk-7SIZ2LV5.js → chunk-IFHRHV7S.js} +1 -1
- package/dist/{chunk-BTGHF3SW.js → chunk-N6AE6NBB.js} +1 -1
- package/dist/{chunk-VHINNGIF.js → chunk-NKVZGPR6.js} +1 -1
- package/dist/{chunk-TOBPMMRM.js → chunk-S55OZ7F2.js} +8 -0
- package/dist/{chunk-WCALKWPO.js → chunk-T7MP7ANP.js} +11 -7
- package/dist/{chunk-UBK2IZ6G.js → chunk-XTRTK26E.js} +5 -5
- package/dist/components/editor/index.js +3 -3
- package/dist/components/primitives/ResolvedMedia.d.ts.map +1 -1
- package/dist/components/primitives/index.js +4 -4
- package/dist/components/sections/Media/MediaBlock.d.ts.map +1 -1
- package/dist/components/sections/brand-guide.js +4 -4
- package/dist/components/sections/register-schemas.js +7 -7
- package/dist/components/sections/register.js +7 -7
- package/dist/components/shell/EditorShell.d.ts.map +1 -1
- package/dist/hooks/useEditorPublish.d.ts +4 -1
- package/dist/hooks/useEditorPublish.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/lib/save-chunks.d.ts +28 -0
- package/dist/lib/save-chunks.d.ts.map +1 -0
- package/dist/media/index.js +5 -1
- package/dist/media/resolve.d.ts +14 -0
- package/dist/media/resolve.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/primitives/ResolvedMedia.tsx +10 -5
- package/src/components/sections/Media/MediaBlock.tsx +2 -1
- package/src/components/shell/EditorShell.tsx +8 -2
- package/src/hooks/useEditorPublish.ts +134 -50
- package/src/lib/save-chunks.ts +53 -0
- package/src/media/resolve.ts +18 -0
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
MediaBlock,
|
|
5
5
|
containerGridClass,
|
|
6
6
|
useEditableCollection
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-T7MP7ANP.js";
|
|
8
8
|
import {
|
|
9
9
|
Checkbox,
|
|
10
10
|
FormLabel,
|
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
DeleteIcon,
|
|
16
16
|
DragHandle,
|
|
17
17
|
IconButton
|
|
18
|
-
} from "./chunk-
|
|
18
|
+
} from "./chunk-IFHRHV7S.js";
|
|
19
19
|
import {
|
|
20
20
|
DEFAULT_LINK,
|
|
21
21
|
LinkValueSchema
|
|
@@ -220,6 +220,12 @@ function resolveManifestReferences(content, manifest, sizes, provider) {
|
|
|
220
220
|
}
|
|
221
221
|
return content;
|
|
222
222
|
}
|
|
223
|
+
var BAKED_MEDIA_KEYS = ["src", "srcset", "alt", "width", "height", "poster"];
|
|
224
|
+
function rebindImageRef(ref, imageId) {
|
|
225
|
+
const fresh = { ...ref, imageId };
|
|
226
|
+
for (const key of BAKED_MEDIA_KEYS) delete fresh[key];
|
|
227
|
+
return fresh;
|
|
228
|
+
}
|
|
223
229
|
|
|
224
230
|
// src/media/videoPoster.ts
|
|
225
231
|
function generateVideoPoster(blob, quality) {
|
|
@@ -312,6 +318,8 @@ export {
|
|
|
312
318
|
ProcessingQueue,
|
|
313
319
|
resolveMedia,
|
|
314
320
|
resolveManifestReferences,
|
|
321
|
+
BAKED_MEDIA_KEYS,
|
|
322
|
+
rebindImageRef,
|
|
315
323
|
generateVideoPoster,
|
|
316
324
|
setMediaProvider,
|
|
317
325
|
getMediaProvider
|
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
SettingsIcon,
|
|
7
7
|
useMediaLibrary,
|
|
8
8
|
useResolvedMedia
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-IFHRHV7S.js";
|
|
10
|
+
import {
|
|
11
|
+
rebindImageRef
|
|
12
|
+
} from "./chunk-S55OZ7F2.js";
|
|
10
13
|
import {
|
|
11
14
|
cn
|
|
12
15
|
} from "./chunk-7EOSDFS3.js";
|
|
@@ -140,13 +143,14 @@ function ResolvedMedia({
|
|
|
140
143
|
invertFrom
|
|
141
144
|
}) {
|
|
142
145
|
const resolved = useResolvedMedia(imageId);
|
|
143
|
-
const
|
|
144
|
-
const
|
|
146
|
+
const hasResolved = !!resolved.src;
|
|
147
|
+
const src = hasResolved ? resolved.src : propSrc;
|
|
148
|
+
const srcset = hasResolved ? resolved.srcset : propSrcset;
|
|
145
149
|
const poster = resolved.poster;
|
|
146
|
-
const alt =
|
|
150
|
+
const alt = hasResolved ? resolved.alt : propAlt ?? "";
|
|
147
151
|
const kind = resolved.kind;
|
|
148
|
-
const width =
|
|
149
|
-
const height =
|
|
152
|
+
const width = hasResolved ? resolved.width : propWidth;
|
|
153
|
+
const height = hasResolved ? resolved.height : propHeight;
|
|
150
154
|
const [loaded, setLoaded] = useState2(false);
|
|
151
155
|
const invertClass = invertFrom === "light" ? "invert dark:invert-0" : invertFrom === "dark" ? "dark:invert" : void 0;
|
|
152
156
|
const mediaClass = cn("relative h-full w-full", imgClassName, invertClass);
|
|
@@ -259,7 +263,7 @@ function MediaBlock({ refValue, options, link, isEditMode, onRefChange }) {
|
|
|
259
263
|
const fitClass = fit === "cover" ? "object-cover" : "object-contain";
|
|
260
264
|
const showBorder = refValue.border ?? options.border;
|
|
261
265
|
const captionStr = refValue.caption ? Array.isArray(refValue.caption) ? refValue.caption.join("\n") : refValue.caption : "";
|
|
262
|
-
const replace = onRefChange ? (imageId) => onRefChange(
|
|
266
|
+
const replace = onRefChange ? (imageId) => onRefChange(rebindImageRef(refValue, imageId)) : void 0;
|
|
263
267
|
const baked = refValue;
|
|
264
268
|
const bakedWidth = typeof baked.width === "number" ? baked.width : void 0;
|
|
265
269
|
const bakedHeight = typeof baked.height === "number" ? baked.height : void 0;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
EditableRichText,
|
|
3
3
|
defineHeadingSection
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-NKVZGPR6.js";
|
|
5
5
|
import {
|
|
6
6
|
embedUrl
|
|
7
7
|
} from "./chunk-RBTSUR5V.js";
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
pluralize,
|
|
11
11
|
stripHtmlToPlainText,
|
|
12
12
|
truncate
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-7T2WQSWF.js";
|
|
14
14
|
import {
|
|
15
15
|
EditablePlainText,
|
|
16
16
|
ImageDropZone,
|
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
colSpanClass,
|
|
20
20
|
containerGridClass,
|
|
21
21
|
spacerHiddenClass
|
|
22
|
-
} from "./chunk-
|
|
22
|
+
} from "./chunk-T7MP7ANP.js";
|
|
23
23
|
import {
|
|
24
24
|
DeleteButton,
|
|
25
25
|
DragHandle,
|
|
@@ -31,11 +31,11 @@ import {
|
|
|
31
31
|
buildBlockDropData,
|
|
32
32
|
canDropBlock,
|
|
33
33
|
isBlockDragData
|
|
34
|
-
} from "./chunk-
|
|
34
|
+
} from "./chunk-N6AE6NBB.js";
|
|
35
35
|
import {
|
|
36
36
|
useMediaLibrary,
|
|
37
37
|
useResolvedMedia
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-IFHRHV7S.js";
|
|
39
39
|
import {
|
|
40
40
|
DEFAULT_LINK,
|
|
41
41
|
LinkValueSchema
|
|
@@ -11,14 +11,14 @@ import {
|
|
|
11
11
|
buildBlockDropData,
|
|
12
12
|
canDropBlock,
|
|
13
13
|
isBlockDragData
|
|
14
|
-
} from "../../chunk-
|
|
14
|
+
} from "../../chunk-N6AE6NBB.js";
|
|
15
15
|
import "../../chunk-ZSPCE6MZ.js";
|
|
16
16
|
import "../../chunk-5THS4K5D.js";
|
|
17
17
|
import {
|
|
18
18
|
AddIcon,
|
|
19
19
|
IconButton
|
|
20
|
-
} from "../../chunk-
|
|
21
|
-
import "../../chunk-
|
|
20
|
+
} from "../../chunk-IFHRHV7S.js";
|
|
21
|
+
import "../../chunk-S55OZ7F2.js";
|
|
22
22
|
import "../../chunk-677NTHAZ.js";
|
|
23
23
|
import {
|
|
24
24
|
cn
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ResolvedMedia.d.ts","sourceRoot":"","sources":["../../../src/components/primitives/ResolvedMedia.tsx"],"names":[],"mappings":"AAKA,UAAU,kBAAkB;IAC1B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,UAAU,EAClB,SAAS,EACT,YAAY,EACZ,UAAU,GACX,EAAE,kBAAkB,
|
|
1
|
+
{"version":3,"file":"ResolvedMedia.d.ts","sourceRoot":"","sources":["../../../src/components/primitives/ResolvedMedia.tsx"],"names":[],"mappings":"AAKA,UAAU,kBAAkB;IAC1B,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,aAAa,CAAC,EAC5B,OAAO,EACP,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,UAAU,EAClB,GAAG,EAAE,OAAO,EACZ,KAAK,EAAE,SAAS,EAChB,MAAM,EAAE,UAAU,EAClB,SAAS,EACT,YAAY,EACZ,UAAU,GACX,EAAE,kBAAkB,2CA2EpB"}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
EditableRichText,
|
|
3
3
|
LinkPopover,
|
|
4
4
|
defineHeadingSection
|
|
5
|
-
} from "../../chunk-
|
|
5
|
+
} from "../../chunk-NKVZGPR6.js";
|
|
6
6
|
import {
|
|
7
7
|
EditableGrid,
|
|
8
8
|
EditablePlainText,
|
|
@@ -10,14 +10,14 @@ import {
|
|
|
10
10
|
MediaBlock,
|
|
11
11
|
ResolvedMedia,
|
|
12
12
|
useEditableCollection
|
|
13
|
-
} from "../../chunk-
|
|
13
|
+
} from "../../chunk-T7MP7ANP.js";
|
|
14
14
|
import "../../chunk-ZSPCE6MZ.js";
|
|
15
15
|
import {
|
|
16
16
|
DeleteIcon,
|
|
17
17
|
DragHandle,
|
|
18
18
|
IconButton
|
|
19
|
-
} from "../../chunk-
|
|
20
|
-
import "../../chunk-
|
|
19
|
+
} from "../../chunk-IFHRHV7S.js";
|
|
20
|
+
import "../../chunk-S55OZ7F2.js";
|
|
21
21
|
import "../../chunk-53DKVAB6.js";
|
|
22
22
|
import {
|
|
23
23
|
cn
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MediaBlock.d.ts","sourceRoot":"","sources":["../../../../src/components/sections/Media/MediaBlock.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"MediaBlock.d.ts","sourceRoot":"","sources":["../../../../src/components/sections/Media/MediaBlock.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAQvD,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,OAAO,EAAE;QAAE,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,WAAW,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAC;QAAC,SAAS,CAAC,EAAE,OAAO,GAAG,SAAS,CAAA;KAAE,CAAC;IACxG,4EAA4E;IAC5E,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,UAAU,EAAE,OAAO,CAAC;IACpB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,oBAAoB,KAAK,IAAI,CAAC;CACnD;AAED,wBAAgB,UAAU,CAAC,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,EAAE,eAAe,2CA2F/F"}
|
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
ColorItemSchema,
|
|
3
3
|
ColorSpaceSchema,
|
|
4
4
|
brandGuideSectionDefs
|
|
5
|
-
} from "../../chunk-
|
|
6
|
-
import "../../chunk-
|
|
5
|
+
} from "../../chunk-7T2WQSWF.js";
|
|
6
|
+
import "../../chunk-T7MP7ANP.js";
|
|
7
7
|
import "../../chunk-5THS4K5D.js";
|
|
8
|
-
import "../../chunk-
|
|
9
|
-
import "../../chunk-
|
|
8
|
+
import "../../chunk-IFHRHV7S.js";
|
|
9
|
+
import "../../chunk-S55OZ7F2.js";
|
|
10
10
|
import "../../chunk-PZJL2S3Z.js";
|
|
11
11
|
import "../../chunk-WDSVHKAH.js";
|
|
12
12
|
import "../../chunk-7EOSDFS3.js";
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
allSectionDefs
|
|
3
|
-
} from "../../chunk-
|
|
4
|
-
import "../../chunk-
|
|
3
|
+
} from "../../chunk-XTRTK26E.js";
|
|
4
|
+
import "../../chunk-NKVZGPR6.js";
|
|
5
5
|
import "../../chunk-RBTSUR5V.js";
|
|
6
|
-
import "../../chunk-
|
|
7
|
-
import "../../chunk-
|
|
8
|
-
import "../../chunk-
|
|
6
|
+
import "../../chunk-7T2WQSWF.js";
|
|
7
|
+
import "../../chunk-T7MP7ANP.js";
|
|
8
|
+
import "../../chunk-N6AE6NBB.js";
|
|
9
9
|
import "../../chunk-ZSPCE6MZ.js";
|
|
10
10
|
import "../../chunk-5THS4K5D.js";
|
|
11
|
-
import "../../chunk-
|
|
12
|
-
import "../../chunk-
|
|
11
|
+
import "../../chunk-IFHRHV7S.js";
|
|
12
|
+
import "../../chunk-S55OZ7F2.js";
|
|
13
13
|
import "../../chunk-PZJL2S3Z.js";
|
|
14
14
|
import "../../chunk-53DKVAB6.js";
|
|
15
15
|
import "../../chunk-WDSVHKAH.js";
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import {
|
|
2
2
|
allSectionDefs
|
|
3
|
-
} from "../../chunk-
|
|
4
|
-
import "../../chunk-
|
|
3
|
+
} from "../../chunk-XTRTK26E.js";
|
|
4
|
+
import "../../chunk-NKVZGPR6.js";
|
|
5
5
|
import "../../chunk-RBTSUR5V.js";
|
|
6
|
-
import "../../chunk-
|
|
7
|
-
import "../../chunk-
|
|
8
|
-
import "../../chunk-
|
|
6
|
+
import "../../chunk-7T2WQSWF.js";
|
|
7
|
+
import "../../chunk-T7MP7ANP.js";
|
|
8
|
+
import "../../chunk-N6AE6NBB.js";
|
|
9
9
|
import "../../chunk-ZSPCE6MZ.js";
|
|
10
10
|
import "../../chunk-5THS4K5D.js";
|
|
11
|
-
import "../../chunk-
|
|
12
|
-
import "../../chunk-
|
|
11
|
+
import "../../chunk-IFHRHV7S.js";
|
|
12
|
+
import "../../chunk-S55OZ7F2.js";
|
|
13
13
|
import "../../chunk-PZJL2S3Z.js";
|
|
14
14
|
import "../../chunk-53DKVAB6.js";
|
|
15
15
|
import "../../chunk-WDSVHKAH.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AA0EjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAQxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAIhB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;IAIhE,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,YAAY,EACZ,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,EACX,sBAA8B,GAC/B,EAAE,KAAK,
|
|
1
|
+
{"version":3,"file":"EditorShell.d.ts","sourceRoot":"","sources":["../../../src/components/shell/EditorShell.tsx"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AA0EjD,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AAQxD,UAAU,KAAK;IACb,OAAO,EAAE,MAAM,CAAC;IAIhB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,YAAY,EAAE;QACZ,KAAK,EAAE,OAAO,CAAC;QACf,aAAa,EAAE,OAAO,CAAC;QACvB,YAAY,EAAE,OAAO,CAAC;QACtB,cAAc,EAAE,OAAO,CAAC;QACxB,kBAAkB,EAAE,OAAO,CAAC;QAC5B,cAAc,EAAE,OAAO,CAAC;KACzB,CAAC;IACF,WAAW,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;KAAE,GAAG,IAAI,CAAC;IAIhE,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC;AAED,MAAM,CAAC,OAAO,UAAU,WAAW,CAAC,EAClC,OAAO,EACP,YAAY,EACZ,MAAM,EACN,SAAS,EAAE,gBAAgB,EAC3B,YAAY,EACZ,WAAW,EACX,sBAA8B,GAC/B,EAAE,KAAK,2CA08BP"}
|
|
@@ -20,11 +20,14 @@ interface PublishDeps {
|
|
|
20
20
|
onShasUpdated: (savedSha: string | null, mainSha: string | null) => void;
|
|
21
21
|
onPublishComplete?: () => void;
|
|
22
22
|
getBaseVersion: () => string | null;
|
|
23
|
+
/** Test hook: overrides the per-request payload budget (default 4MB). */
|
|
24
|
+
saveChunkBudget?: number;
|
|
23
25
|
}
|
|
24
26
|
export type PublishAction = "idle" | "saving" | "publishing";
|
|
25
|
-
export declare function useEditorPublish({ flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, sections, deletedSectionIds, onSuccess, mediaManifest, manifestDirty, clearManifestDirty, pendingMediaItems, pendingMediaDeletions, onMediaPublished, onShasUpdated, onPublishComplete, getBaseVersion, }: PublishDeps): {
|
|
27
|
+
export declare function useEditorPublish({ flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, sections, deletedSectionIds, onSuccess, mediaManifest, manifestDirty, clearManifestDirty, pendingMediaItems, pendingMediaDeletions, onMediaPublished, onShasUpdated, onPublishComplete, getBaseVersion, saveChunkBudget, }: PublishDeps): {
|
|
26
28
|
publishAction: PublishAction;
|
|
27
29
|
publishFeedback: string | null;
|
|
30
|
+
saveProgress: string | null;
|
|
28
31
|
handleSave: () => Promise<void>;
|
|
29
32
|
handlePublish: () => Promise<void>;
|
|
30
33
|
handleSaveAndPublish: () => Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useEditorPublish.d.ts","sourceRoot":"","sources":["../../src/hooks/useEditorPublish.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"useEditorPublish.d.ts","sourceRoot":"","sources":["../../src/hooks/useEditorPublish.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AACnD,OAAO,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AA6C/D,UAAU,WAAW;IACnB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,aAAa,EAAE,MAAM,OAAO,CAAC;IAC7B,gBAAgB,EAAE,MAAM,IAAI,CAAC;IAC7B,YAAY,EAAE,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IACzC,UAAU,EAAE,UAAU,GAAG,IAAI,CAAC;IAC9B,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,iBAAiB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC7B,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,aAAa,EAAE,aAAa,CAAC;IAC7B,aAAa,EAAE,OAAO,CAAC;IACvB,kBAAkB,EAAE,MAAM,IAAI,CAAC;IAC/B,iBAAiB,EAAE,SAAS,EAAE,CAAC;IAC/B,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,gBAAgB,EAAE,CAAC,cAAc,EAAE,SAAS,EAAE,EAAE,kBAAkB,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IACtF,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACzE,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAI/B,cAAc,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IACpC,yEAAyE;IACzE,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAQD,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,QAAQ,GAAG,YAAY,CAAC;AAE7D,wBAAgB,gBAAgB,CAAC,EAC/B,QAAQ,EACR,kBAAkB,EAClB,aAAa,EACb,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,QAAQ,EACR,iBAAiB,EACjB,SAAS,EACT,aAAa,EACb,aAAa,EACb,kBAAkB,EAClB,iBAAiB,EACjB,qBAAqB,EACrB,gBAAgB,EAChB,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,eAAe,GAChB,EAAE,WAAW;;;;;;;EAoXb"}
|
package/dist/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
verifySessionToken
|
|
14
14
|
} from "./chunk-EM5DFHDZ.js";
|
|
15
15
|
import {
|
|
16
|
+
BAKED_MEDIA_KEYS,
|
|
16
17
|
EXT_TO_MIME,
|
|
17
18
|
MIME_TO_EXT,
|
|
18
19
|
ProcessingQueue,
|
|
@@ -23,11 +24,12 @@ import {
|
|
|
23
24
|
getMediaProvider,
|
|
24
25
|
hashFileBuffer,
|
|
25
26
|
mimeToExt,
|
|
27
|
+
rebindImageRef,
|
|
26
28
|
resolveManifestReferences,
|
|
27
29
|
resolveMedia,
|
|
28
30
|
sanitizeMediaName,
|
|
29
31
|
setMediaProvider
|
|
30
|
-
} from "./chunk-
|
|
32
|
+
} from "./chunk-S55OZ7F2.js";
|
|
31
33
|
import {
|
|
32
34
|
AudienceColorSchema,
|
|
33
35
|
AudienceNameSchema,
|
|
@@ -140,6 +142,7 @@ export {
|
|
|
140
142
|
AudienceColorSchema,
|
|
141
143
|
AudienceNameSchema,
|
|
142
144
|
AudienceSchema,
|
|
145
|
+
BAKED_MEDIA_KEYS,
|
|
143
146
|
ChatbotConfigSchema,
|
|
144
147
|
ChatbotTaskSchema,
|
|
145
148
|
DEFAULT_LINK,
|
|
@@ -210,6 +213,7 @@ export {
|
|
|
210
213
|
mimeToExt,
|
|
211
214
|
navChangeEvent,
|
|
212
215
|
normalizeSiteIndex,
|
|
216
|
+
rebindImageRef,
|
|
213
217
|
registerSchema,
|
|
214
218
|
registerSection,
|
|
215
219
|
requireSessionSecret,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { MediaItem } from "../media/types";
|
|
2
|
+
export interface MediaUploadPayload {
|
|
3
|
+
item: MediaItem;
|
|
4
|
+
blobs: {
|
|
5
|
+
path: string;
|
|
6
|
+
base64: string;
|
|
7
|
+
}[];
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Serialized-payload budget per /api/save request. Netlify rejects function
|
|
11
|
+
* request bodies over ~6MB at the platform level (before the app runs); 4MB
|
|
12
|
+
* leaves headroom for the JSON envelope plus sections/index/config riding the
|
|
13
|
+
* final chunk.
|
|
14
|
+
*/
|
|
15
|
+
export declare const SAVE_CHUNK_BUDGET_BYTES: number;
|
|
16
|
+
/** Approximate serialized size of one upload: base64 dominates; the fixed
|
|
17
|
+
* allowances cover JSON keys, quoting, and the item's manifest metadata. */
|
|
18
|
+
export declare function uploadSizeBytes(upload: MediaUploadPayload): number;
|
|
19
|
+
export interface PackedUploads {
|
|
20
|
+
chunks: MediaUploadPayload[][];
|
|
21
|
+
oversized: MediaUploadPayload[];
|
|
22
|
+
}
|
|
23
|
+
/** Greedy in-order packing of uploads into budget-sized chunks. An item whose
|
|
24
|
+
* blobs alone exceed the budget can never be sent — it lands in `oversized`
|
|
25
|
+
* for the caller to reject with a per-file error. Items are never split
|
|
26
|
+
* across chunks (the manifest may only reference fully-committed items). */
|
|
27
|
+
export declare function packUploads(uploads: MediaUploadPayload[], budget?: number): PackedUploads;
|
|
28
|
+
//# sourceMappingURL=save-chunks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"save-chunks.d.ts","sourceRoot":"","sources":["../../src/lib/save-chunks.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAEhD,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;CAC3C;AAED;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAkB,CAAC;AAEvD;6EAC6E;AAC7E,wBAAgB,eAAe,CAAC,MAAM,EAAE,kBAAkB,GAAG,MAAM,CAElE;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,kBAAkB,EAAE,EAAE,CAAC;IAC/B,SAAS,EAAE,kBAAkB,EAAE,CAAC;CACjC;AAED;;;6EAG6E;AAC7E,wBAAgB,WAAW,CAAC,OAAO,EAAE,kBAAkB,EAAE,EAAE,MAAM,SAA0B,GAAG,aAAa,CAsB1G"}
|
package/dist/media/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
BAKED_MEDIA_KEYS,
|
|
2
3
|
EXT_TO_MIME,
|
|
3
4
|
MIME_TO_EXT,
|
|
4
5
|
ProcessingQueue,
|
|
@@ -9,11 +10,12 @@ import {
|
|
|
9
10
|
getMediaProvider,
|
|
10
11
|
hashFileBuffer,
|
|
11
12
|
mimeToExt,
|
|
13
|
+
rebindImageRef,
|
|
12
14
|
resolveManifestReferences,
|
|
13
15
|
resolveMedia,
|
|
14
16
|
sanitizeMediaName,
|
|
15
17
|
setMediaProvider
|
|
16
|
-
} from "../chunk-
|
|
18
|
+
} from "../chunk-S55OZ7F2.js";
|
|
17
19
|
import "../chunk-4YEOTFVU.js";
|
|
18
20
|
import {
|
|
19
21
|
ImageManifestSchema,
|
|
@@ -23,6 +25,7 @@ import {
|
|
|
23
25
|
} from "../chunk-SWIHUIFT.js";
|
|
24
26
|
import "../chunk-JSBRDJBE.js";
|
|
25
27
|
export {
|
|
28
|
+
BAKED_MEDIA_KEYS,
|
|
26
29
|
EXT_TO_MIME,
|
|
27
30
|
ImageManifestSchema,
|
|
28
31
|
MIME_TO_EXT,
|
|
@@ -37,6 +40,7 @@ export {
|
|
|
37
40
|
getMediaProvider,
|
|
38
41
|
hashFileBuffer,
|
|
39
42
|
mimeToExt,
|
|
43
|
+
rebindImageRef,
|
|
40
44
|
resolveManifestReferences,
|
|
41
45
|
resolveMedia,
|
|
42
46
|
sanitizeMediaName,
|
package/dist/media/resolve.d.ts
CHANGED
|
@@ -12,4 +12,18 @@ export declare function resolveMedia(item: {
|
|
|
12
12
|
* without needing to know about the manifest themselves.
|
|
13
13
|
*/
|
|
14
14
|
export declare function resolveManifestReferences(content: unknown, manifest: MediaManifest, sizes: number[], provider: MediaProvider): unknown;
|
|
15
|
+
/**
|
|
16
|
+
* Keys stamped onto media refs by resolveManifestReferences above — derived
|
|
17
|
+
* from the referenced media item, never authored by the editor. Must stay in
|
|
18
|
+
* sync with the `patch` object built in resolveManifestReferences.
|
|
19
|
+
*/
|
|
20
|
+
export declare const BAKED_MEDIA_KEYS: readonly ["src", "srcset", "alt", "width", "height", "poster"];
|
|
21
|
+
/**
|
|
22
|
+
* Re-point a media ref at a different library item. Drops every baked field so
|
|
23
|
+
* stale values resolved for the PREVIOUS image can't shadow the new one in the
|
|
24
|
+
* editor (ResolvedMedia prefers an explicit src prop over live resolution).
|
|
25
|
+
*/
|
|
26
|
+
export declare function rebindImageRef<T extends {
|
|
27
|
+
imageId?: string;
|
|
28
|
+
}>(ref: T, imageId: string): T;
|
|
15
29
|
//# sourceMappingURL=resolve.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/media/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,wBAAgB,YAAY,CAC1B,IAAI,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACxC,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,aAAa,GACtB,aAAa,GAAG,IAAI,CAKtB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,aAAa,GACtB,OAAO,CA0CT"}
|
|
1
|
+
{"version":3,"file":"resolve.d.ts","sourceRoot":"","sources":["../../src/media/resolve.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,wBAAgB,YAAY,CAC1B,IAAI,EAAE;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAA;CAAE,EACxC,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,aAAa,GACtB,aAAa,GAAG,IAAI,CAKtB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,aAAa,EACvB,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,aAAa,GACtB,OAAO,CA0CT;AAED;;;;GAIG;AACH,eAAO,MAAM,gBAAgB,gEAAiE,CAAC;AAE/F;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS;IAAE,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,EAAE,GAAG,EAAE,CAAC,EAAE,OAAO,EAAE,MAAM,GAAG,CAAC,CAIzF"}
|
package/package.json
CHANGED
|
@@ -27,13 +27,18 @@ export function ResolvedMedia({
|
|
|
27
27
|
invertFrom,
|
|
28
28
|
}: ResolvedMediaProps) {
|
|
29
29
|
const resolved = useResolvedMedia(imageId);
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
// A successful live resolution is always fresher than baked props (the ref
|
|
31
|
+
// may have been re-pointed since the server baked them) — prefer it, and use
|
|
32
|
+
// the baked values only where resolution is unavailable (zero-JS viewer,
|
|
33
|
+
// deleted/unknown id).
|
|
34
|
+
const hasResolved = !!resolved.src;
|
|
35
|
+
const src = hasResolved ? resolved.src : propSrc;
|
|
36
|
+
const srcset = hasResolved ? resolved.srcset : propSrcset;
|
|
32
37
|
const poster = resolved.poster;
|
|
33
|
-
const alt =
|
|
38
|
+
const alt = hasResolved ? resolved.alt : (propAlt ?? "");
|
|
34
39
|
const kind = resolved.kind;
|
|
35
|
-
const width =
|
|
36
|
-
const height =
|
|
40
|
+
const width = hasResolved ? resolved.width : propWidth;
|
|
41
|
+
const height = hasResolved ? resolved.height : propHeight;
|
|
37
42
|
|
|
38
43
|
// `loaded` fades the placeholder out via onLoad in the hydrated editor. In the
|
|
39
44
|
// zero-JS viewer this stays false (no JS), so the placeholder remains painted
|
|
@@ -5,6 +5,7 @@ import { ResolvedMedia } from "../../primitives/ResolvedMedia";
|
|
|
5
5
|
import { ImageDropZone } from "../../primitives/ImageDropZone";
|
|
6
6
|
import { EditablePlainText } from "../../primitives/EditablePlainText";
|
|
7
7
|
import { useMediaLibrary } from "../../shell/MediaLibraryContext";
|
|
8
|
+
import { rebindImageRef } from "../../../media/resolve";
|
|
8
9
|
|
|
9
10
|
export interface MediaBlockProps {
|
|
10
11
|
refValue: SingleMediaReference;
|
|
@@ -25,7 +26,7 @@ export function MediaBlock({ refValue, options, link, isEditMode, onRefChange }:
|
|
|
25
26
|
? Array.isArray(refValue.caption) ? refValue.caption.join("\n") : refValue.caption
|
|
26
27
|
: "";
|
|
27
28
|
|
|
28
|
-
const replace = onRefChange ? (imageId: string) => onRefChange(
|
|
29
|
+
const replace = onRefChange ? (imageId: string) => onRefChange(rebindImageRef(refValue, imageId)) : undefined;
|
|
29
30
|
|
|
30
31
|
// The zero-JS viewer has no MediaLibraryContext, so useResolvedMedia() can't
|
|
31
32
|
// resolve the URL. The SSR pass (resolveManifestReferences) bakes src/srcset/alt
|
|
@@ -232,7 +232,7 @@ export default function EditorShell({
|
|
|
232
232
|
|
|
233
233
|
const buildStatus = useBuildStatus();
|
|
234
234
|
|
|
235
|
-
const { publishAction, publishFeedback, handleSave, handlePublish, handleSaveAndPublish } = useEditorPublish({
|
|
235
|
+
const { publishAction, publishFeedback, saveProgress, handleSave, handlePublish, handleSaveAndPublish } = useEditorPublish({
|
|
236
236
|
flushNow: persistence.flushNow,
|
|
237
237
|
cancelPendingFlush: persistence.cancelPendingFlush,
|
|
238
238
|
isConfigDirty: persistence.isConfigDirty,
|
|
@@ -884,6 +884,7 @@ export default function EditorShell({
|
|
|
884
884
|
localChangesExist={localChangesExist}
|
|
885
885
|
publishAction={publishAction}
|
|
886
886
|
publishFeedback={publishFeedback}
|
|
887
|
+
saveProgress={saveProgress}
|
|
887
888
|
onSave={handleSave}
|
|
888
889
|
onPublish={handlePublish}
|
|
889
890
|
onSaveAndPublish={handleSaveAndPublish}
|
|
@@ -1489,12 +1490,14 @@ function DismissButton({ onClick }: { onClick: () => void }) {
|
|
|
1489
1490
|
function StatusText({
|
|
1490
1491
|
publishAction,
|
|
1491
1492
|
publishFeedback,
|
|
1493
|
+
saveProgress,
|
|
1492
1494
|
buildState,
|
|
1493
1495
|
buildElapsed,
|
|
1494
1496
|
onDismiss,
|
|
1495
1497
|
}: {
|
|
1496
1498
|
publishAction: "idle" | "saving" | "publishing";
|
|
1497
1499
|
publishFeedback: string | null;
|
|
1500
|
+
saveProgress: string | null;
|
|
1498
1501
|
buildState: "idle" | "building" | "ready" | "error" | "fading";
|
|
1499
1502
|
buildElapsed: number;
|
|
1500
1503
|
onDismiss: () => void;
|
|
@@ -1502,7 +1505,7 @@ function StatusText({
|
|
|
1502
1505
|
if (publishAction === "saving") {
|
|
1503
1506
|
return (
|
|
1504
1507
|
<span className="relative text-xs font-medium text-base-content/60">
|
|
1505
|
-
Saving changes<LoadingEllipsis
|
|
1508
|
+
{saveProgress ?? <>Saving changes<LoadingEllipsis /></>}
|
|
1506
1509
|
</span>
|
|
1507
1510
|
);
|
|
1508
1511
|
}
|
|
@@ -1550,6 +1553,7 @@ function EditorToolbar({
|
|
|
1550
1553
|
localChangesExist,
|
|
1551
1554
|
publishAction,
|
|
1552
1555
|
publishFeedback,
|
|
1556
|
+
saveProgress,
|
|
1553
1557
|
onSave,
|
|
1554
1558
|
onPublish,
|
|
1555
1559
|
onSaveAndPublish,
|
|
@@ -1569,6 +1573,7 @@ function EditorToolbar({
|
|
|
1569
1573
|
localChangesExist: boolean;
|
|
1570
1574
|
publishAction: "idle" | "saving" | "publishing";
|
|
1571
1575
|
publishFeedback: string | null;
|
|
1576
|
+
saveProgress: string | null;
|
|
1572
1577
|
onSave: () => void;
|
|
1573
1578
|
onPublish: () => void;
|
|
1574
1579
|
onSaveAndPublish: () => void;
|
|
@@ -1640,6 +1645,7 @@ function EditorToolbar({
|
|
|
1640
1645
|
<StatusText
|
|
1641
1646
|
publishAction={publishAction}
|
|
1642
1647
|
publishFeedback={publishFeedback}
|
|
1648
|
+
saveProgress={saveProgress}
|
|
1643
1649
|
buildState={buildState}
|
|
1644
1650
|
buildElapsed={buildElapsed}
|
|
1645
1651
|
onDismiss={onBuildDismiss}
|
|
@@ -3,6 +3,7 @@ import type { SiteIndex, SiteConfig } from "../schemas/site-config";
|
|
|
3
3
|
import type { LoadedSection } from "../lib/loader";
|
|
4
4
|
import type { MediaManifest, MediaItem } from "../media/types";
|
|
5
5
|
import { getDirtySectionRows, hasLocalChanges, discardSavedChanges, cacheContent, clearContentCache, getPendingMediaBlobs, clearPendingMediaByIds } from "../lib/dexie";
|
|
6
|
+
import { packUploads, type MediaUploadPayload } from "../lib/save-chunks";
|
|
6
7
|
|
|
7
8
|
function blobToBase64(blob: Blob): Promise<string> {
|
|
8
9
|
return new Promise((resolve, reject) => {
|
|
@@ -16,6 +17,35 @@ function blobToBase64(blob: Blob): Promise<string> {
|
|
|
16
17
|
});
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
/** One /api/save request with honest failure semantics: the server's message
|
|
21
|
+
* is preserved verbatim; non-JSON bodies (platform-level rejections) get a
|
|
22
|
+
* status-coded fallback — or a size hint when the request body itself was
|
|
23
|
+
* near Netlify's ~6MB limit (should be unreachable once saves are chunked). */
|
|
24
|
+
async function postSave(payload: object): Promise<{ sha: string }> {
|
|
25
|
+
const body = JSON.stringify(payload);
|
|
26
|
+
let response: Response;
|
|
27
|
+
try {
|
|
28
|
+
response = await fetch("/api/save", {
|
|
29
|
+
method: "POST",
|
|
30
|
+
headers: { "Content-Type": "application/json" },
|
|
31
|
+
body,
|
|
32
|
+
});
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error("Network error while saving — check your connection and try again.");
|
|
35
|
+
}
|
|
36
|
+
if (!response.ok) {
|
|
37
|
+
const errorBody = await response.json().catch(() => null) as { message?: string; error?: string } | null;
|
|
38
|
+
const serverMessage = errorBody?.message || errorBody?.error;
|
|
39
|
+
const fallback = !errorBody && body.length > 5_500_000
|
|
40
|
+
? "This save is too large to send in one request — remove some images from the media library and try again."
|
|
41
|
+
: `Save failed (HTTP ${response.status})`;
|
|
42
|
+
throw Object.assign(new Error(serverMessage || fallback), { status: response.status });
|
|
43
|
+
}
|
|
44
|
+
const data = await response.json().catch(() => null) as { sha?: string } | null;
|
|
45
|
+
if (!data?.sha) throw new Error("Save response missing SHA");
|
|
46
|
+
return data as { sha: string };
|
|
47
|
+
}
|
|
48
|
+
|
|
19
49
|
interface PublishDeps {
|
|
20
50
|
flushNow: () => Promise<void>;
|
|
21
51
|
cancelPendingFlush: () => void;
|
|
@@ -38,6 +68,8 @@ interface PublishDeps {
|
|
|
38
68
|
// no draft exists yet). Sent with saves so the server can 409 on a concurrent
|
|
39
69
|
// edit instead of silently overwriting it. Read at save time (latest value).
|
|
40
70
|
getBaseVersion: () => string | null;
|
|
71
|
+
/** Test hook: overrides the per-request payload budget (default 4MB). */
|
|
72
|
+
saveChunkBudget?: number;
|
|
41
73
|
}
|
|
42
74
|
|
|
43
75
|
interface GatheredMedia {
|
|
@@ -67,9 +99,11 @@ export function useEditorPublish({
|
|
|
67
99
|
onShasUpdated,
|
|
68
100
|
onPublishComplete,
|
|
69
101
|
getBaseVersion,
|
|
102
|
+
saveChunkBudget,
|
|
70
103
|
}: PublishDeps) {
|
|
71
104
|
const [publishAction, setPublishAction] = useState<PublishAction>("idle");
|
|
72
105
|
const [publishFeedback, setPublishFeedback] = useState<string | null>(null);
|
|
106
|
+
const [saveProgress, setSaveProgress] = useState<string | null>(null);
|
|
73
107
|
const feedbackTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
74
108
|
const inFlightRef = useRef(false);
|
|
75
109
|
|
|
@@ -148,6 +182,7 @@ export function useEditorPublish({
|
|
|
148
182
|
pendingMediaDeletions: string[];
|
|
149
183
|
mediaManifest: MediaManifest;
|
|
150
184
|
manifestDirty: boolean;
|
|
185
|
+
saveChunkBudget?: number;
|
|
151
186
|
}): Promise<{ sha: string; savedSections: { sectionId: string; updatedAt: string }[]; uploadedItems: MediaItem[] }> {
|
|
152
187
|
const dirty = await getDirtySectionRows();
|
|
153
188
|
const gathered = await gatherMediaPayload(
|
|
@@ -157,6 +192,14 @@ export function useEditorPublish({
|
|
|
157
192
|
args.manifestDirty,
|
|
158
193
|
);
|
|
159
194
|
|
|
195
|
+
const { chunks, oversized } = packUploads(gathered.mediaUploads, args.saveChunkBudget);
|
|
196
|
+
if (oversized.length > 0) {
|
|
197
|
+
const names = oversized.map((u) => `"${u.item.originalName}"`).join(", ");
|
|
198
|
+
throw new Error(
|
|
199
|
+
`${names} ${oversized.length === 1 ? "is" : "are"} too large to save — resize or compress the file, re-add it to the media library, and try again.`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
160
203
|
let siteIndex = args.siteIndexRef.current;
|
|
161
204
|
if (args.deletedSectionIds?.length) {
|
|
162
205
|
const deleteSet = new Set(args.deletedSectionIds);
|
|
@@ -171,48 +214,71 @@ export function useEditorPublish({
|
|
|
171
214
|
};
|
|
172
215
|
}
|
|
173
216
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
217
|
+
let baseVersion = getBaseVersion();
|
|
218
|
+
const uploadedItems: MediaItem[] = [];
|
|
219
|
+
const totalUploads = gathered.mediaUploads.length;
|
|
220
|
+
|
|
221
|
+
// Lead chunks: media-only saves. The server skips the index write for
|
|
222
|
+
// these, so the draft never references section files that don't exist
|
|
223
|
+
// yet; a failure after chunk k leaves chunks 1..k committed AND in the
|
|
224
|
+
// committed manifest. The remainder stays pending in Dexie, but the
|
|
225
|
+
// caller's savedShaRef only advances on full success — so an IMMEDIATE
|
|
226
|
+
// retry may 409 against the moved draft head; after a reload the save
|
|
227
|
+
// resumes cleanly with only the uncommitted remainder.
|
|
228
|
+
const leadChunks = chunks.slice(0, -1); // empty when chunks.length <= 1
|
|
229
|
+
const finalUploads: MediaUploadPayload[] = chunks.length > 0 ? chunks[chunks.length - 1] : [];
|
|
230
|
+
const committedImages: Record<string, MediaItem> = {};
|
|
231
|
+
let sent = 0;
|
|
232
|
+
|
|
233
|
+
for (const chunk of leadChunks) {
|
|
234
|
+
setSaveProgress(`Saving images ${sent + chunk.length}/${totalUploads}…`);
|
|
235
|
+
for (const upload of chunk) committedImages[upload.item.id] = upload.item;
|
|
236
|
+
const manifest: MediaManifest = { images: { ...args.mediaManifest.images, ...committedImages } };
|
|
237
|
+
const data = await postSave({
|
|
238
|
+
baseVersion,
|
|
239
|
+
media: { uploads: chunk, deletions: [], manifest },
|
|
240
|
+
});
|
|
241
|
+
baseVersion = data.sha;
|
|
242
|
+
sent += chunk.length;
|
|
243
|
+
const chunkItems = chunk.map((u) => u.item);
|
|
244
|
+
uploadedItems.push(...chunkItems);
|
|
245
|
+
// Promote committed items immediately so a later failure can't re-send
|
|
246
|
+
// them. A failure HERE is tolerable: re-sending identical blobs on the
|
|
247
|
+
// next save is harmless (same content, same paths).
|
|
248
|
+
try {
|
|
249
|
+
await clearPendingMediaByIds(chunkItems.map((i) => i.id), []);
|
|
250
|
+
onMediaPublished(chunkItems, []);
|
|
251
|
+
} catch (promoteError) {
|
|
252
|
+
console.error("Failed to promote committed media chunk locally:", promoteError);
|
|
253
|
+
}
|
|
203
254
|
}
|
|
204
255
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
throw new Error("Save response missing SHA");
|
|
256
|
+
if (leadChunks.length > 0) {
|
|
257
|
+
setSaveProgress(finalUploads.length > 0 ? `Saving images ${totalUploads}/${totalUploads}…` : null);
|
|
208
258
|
}
|
|
259
|
+
|
|
260
|
+
const responseData = await postSave({
|
|
261
|
+
sections: dirty.map(({ sectionId, content }) => ({ id: sectionId, content })),
|
|
262
|
+
siteIndex,
|
|
263
|
+
baseVersion,
|
|
264
|
+
...(args.deletedSectionIds?.length ? { deletedSectionIds: args.deletedSectionIds } : {}),
|
|
265
|
+
...(args.isConfigDirty() ? { siteConfig: args.siteConfig } : {}),
|
|
266
|
+
...(gathered.hasMediaChanges ? {
|
|
267
|
+
media: {
|
|
268
|
+
uploads: finalUploads,
|
|
269
|
+
deletions: args.pendingMediaDeletions.map((id) => ({
|
|
270
|
+
id,
|
|
271
|
+
folder: args.mediaManifest.images[id]?.folder,
|
|
272
|
+
})).filter((d) => d.folder),
|
|
273
|
+
manifest: gathered.updatedManifest,
|
|
274
|
+
},
|
|
275
|
+
} : {}),
|
|
276
|
+
});
|
|
277
|
+
|
|
209
278
|
return {
|
|
210
|
-
sha: responseData.sha
|
|
279
|
+
sha: responseData.sha,
|
|
211
280
|
savedSections: dirty.map(({ sectionId, updatedAt }) => ({ sectionId, updatedAt })),
|
|
212
|
-
|
|
213
|
-
// by gatherMediaPayload (blobs not yet persisted) are excluded so the caller
|
|
214
|
-
// never promotes them into the manifest as orphans.
|
|
215
|
-
uploadedItems: gathered.mediaUploads.map(({ item }) => item),
|
|
281
|
+
uploadedItems: [...uploadedItems, ...finalUploads.map((u) => u.item)],
|
|
216
282
|
};
|
|
217
283
|
}
|
|
218
284
|
|
|
@@ -242,6 +308,7 @@ export function useEditorPublish({
|
|
|
242
308
|
siteConfig: siteConfig!,
|
|
243
309
|
siteIndexRef, deletedSectionIds, isConfigDirty,
|
|
244
310
|
pendingMediaItems, pendingMediaDeletions, mediaManifest, manifestDirty,
|
|
311
|
+
saveChunkBudget,
|
|
245
312
|
});
|
|
246
313
|
}
|
|
247
314
|
|
|
@@ -269,8 +336,9 @@ export function useEditorPublish({
|
|
|
269
336
|
|
|
270
337
|
function reportSaveError(error: unknown, failLabel: string): void {
|
|
271
338
|
console.error(`${failLabel} failed:`, error);
|
|
339
|
+
const message = error instanceof Error && error.message ? error.message : `${failLabel} failed`;
|
|
272
340
|
const isConflict = (error as { status?: number })?.status === 409;
|
|
273
|
-
showFeedback(
|
|
341
|
+
showFeedback(message, isConflict ? 8000 : 6000);
|
|
274
342
|
}
|
|
275
343
|
|
|
276
344
|
const handleSave = useCallback(async () => {
|
|
@@ -286,19 +354,26 @@ export function useEditorPublish({
|
|
|
286
354
|
}
|
|
287
355
|
|
|
288
356
|
const { sha, savedSections, uploadedItems } = await runPerformSave();
|
|
289
|
-
|
|
290
|
-
await commitSavedRows(savedSections, uploadedItems);
|
|
291
|
-
finishSave(uploadedItems);
|
|
292
357
|
onShasUpdated(sha, null);
|
|
293
358
|
|
|
359
|
+
try {
|
|
360
|
+
await commitSavedRows(savedSections, uploadedItems);
|
|
361
|
+
finishSave(uploadedItems);
|
|
362
|
+
} catch (cleanupError) {
|
|
363
|
+
console.error("Save succeeded but local cleanup failed:", cleanupError);
|
|
364
|
+
showFeedback("Saved, but the local cache couldn't be updated — reload the editor.", 8000);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
294
368
|
showFeedback("Saved", 3000);
|
|
295
369
|
} catch (error) {
|
|
296
370
|
reportSaveError(error, "Save");
|
|
297
371
|
} finally {
|
|
298
372
|
inFlightRef.current = false;
|
|
299
373
|
setPublishAction("idle");
|
|
374
|
+
setSaveProgress(null);
|
|
300
375
|
}
|
|
301
|
-
}, [flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, deletedSectionIds, onSuccess, mediaManifest, manifestDirty, clearManifestDirty, pendingMediaItems, pendingMediaDeletions, onMediaPublished, onShasUpdated, showFeedback]);
|
|
376
|
+
}, [flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, deletedSectionIds, onSuccess, mediaManifest, manifestDirty, clearManifestDirty, pendingMediaItems, pendingMediaDeletions, onMediaPublished, onShasUpdated, showFeedback, saveChunkBudget]);
|
|
302
377
|
|
|
303
378
|
const handlePublish = useCallback(async () => {
|
|
304
379
|
setPublishAction("publishing");
|
|
@@ -369,12 +444,20 @@ export function useEditorPublish({
|
|
|
369
444
|
const { sha } = publishData;
|
|
370
445
|
|
|
371
446
|
if (hasLocalEdits) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
447
|
+
try {
|
|
448
|
+
await commitSavedRows(savedSections, uploadedItems);
|
|
449
|
+
// Re-cache against the PUBLISHED sha (commitSavedRows just invalidated the
|
|
450
|
+
// cache); the draft branch is gone after publish, so this is the snapshot a
|
|
451
|
+
// later cache hit restores.
|
|
452
|
+
await cacheContent(sha, sections, siteIndexRef.current, siteConfig!);
|
|
453
|
+
finishSave(uploadedItems);
|
|
454
|
+
} catch (cleanupError) {
|
|
455
|
+
console.error("Publish succeeded but local cleanup failed:", cleanupError);
|
|
456
|
+
onShasUpdated(null, sha);
|
|
457
|
+
onPublishComplete?.();
|
|
458
|
+
showFeedback("Published, but the local cache couldn't be updated — reload the editor.", 8000);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
378
461
|
}
|
|
379
462
|
|
|
380
463
|
onShasUpdated(null, sha);
|
|
@@ -384,8 +467,9 @@ export function useEditorPublish({
|
|
|
384
467
|
} finally {
|
|
385
468
|
inFlightRef.current = false;
|
|
386
469
|
setPublishAction("idle");
|
|
470
|
+
setSaveProgress(null);
|
|
387
471
|
}
|
|
388
|
-
}, [flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, sections, deletedSectionIds, onSuccess, mediaManifest, manifestDirty, clearManifestDirty, pendingMediaItems, pendingMediaDeletions, onMediaPublished, onShasUpdated, showFeedback, onPublishComplete]);
|
|
472
|
+
}, [flushNow, cancelPendingFlush, isConfigDirty, clearConfigDirty, siteIndexRef, siteConfig, sections, deletedSectionIds, onSuccess, mediaManifest, manifestDirty, clearManifestDirty, pendingMediaItems, pendingMediaDeletions, onMediaPublished, onShasUpdated, showFeedback, onPublishComplete, saveChunkBudget]);
|
|
389
473
|
|
|
390
|
-
return { publishAction, publishFeedback, handleSave, handlePublish, handleSaveAndPublish };
|
|
474
|
+
return { publishAction, publishFeedback, saveProgress, handleSave, handlePublish, handleSaveAndPublish };
|
|
391
475
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { MediaItem } from "../media/types";
|
|
2
|
+
|
|
3
|
+
export interface MediaUploadPayload {
|
|
4
|
+
item: MediaItem;
|
|
5
|
+
blobs: { path: string; base64: string }[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Serialized-payload budget per /api/save request. Netlify rejects function
|
|
10
|
+
* request bodies over ~6MB at the platform level (before the app runs); 4MB
|
|
11
|
+
* leaves headroom for the JSON envelope plus sections/index/config riding the
|
|
12
|
+
* final chunk.
|
|
13
|
+
*/
|
|
14
|
+
export const SAVE_CHUNK_BUDGET_BYTES = 4 * 1024 * 1024;
|
|
15
|
+
|
|
16
|
+
/** Approximate serialized size of one upload: base64 dominates; the fixed
|
|
17
|
+
* allowances cover JSON keys, quoting, and the item's manifest metadata. */
|
|
18
|
+
export function uploadSizeBytes(upload: MediaUploadPayload): number {
|
|
19
|
+
return upload.blobs.reduce((sum, b) => sum + b.base64.length + b.path.length + 64, 512);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface PackedUploads {
|
|
23
|
+
chunks: MediaUploadPayload[][];
|
|
24
|
+
oversized: MediaUploadPayload[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Greedy in-order packing of uploads into budget-sized chunks. An item whose
|
|
28
|
+
* blobs alone exceed the budget can never be sent — it lands in `oversized`
|
|
29
|
+
* for the caller to reject with a per-file error. Items are never split
|
|
30
|
+
* across chunks (the manifest may only reference fully-committed items). */
|
|
31
|
+
export function packUploads(uploads: MediaUploadPayload[], budget = SAVE_CHUNK_BUDGET_BYTES): PackedUploads {
|
|
32
|
+
const chunks: MediaUploadPayload[][] = [];
|
|
33
|
+
const oversized: MediaUploadPayload[] = [];
|
|
34
|
+
let current: MediaUploadPayload[] = [];
|
|
35
|
+
let currentBytes = 0;
|
|
36
|
+
|
|
37
|
+
for (const upload of uploads) {
|
|
38
|
+
const size = uploadSizeBytes(upload);
|
|
39
|
+
if (size > budget) {
|
|
40
|
+
oversized.push(upload);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
if (current.length > 0 && currentBytes + size > budget) {
|
|
44
|
+
chunks.push(current);
|
|
45
|
+
current = [];
|
|
46
|
+
currentBytes = 0;
|
|
47
|
+
}
|
|
48
|
+
current.push(upload);
|
|
49
|
+
currentBytes += size;
|
|
50
|
+
}
|
|
51
|
+
if (current.length > 0) chunks.push(current);
|
|
52
|
+
return { chunks, oversized };
|
|
53
|
+
}
|
package/src/media/resolve.ts
CHANGED
|
@@ -68,3 +68,21 @@ export function resolveManifestReferences(
|
|
|
68
68
|
|
|
69
69
|
return content;
|
|
70
70
|
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Keys stamped onto media refs by resolveManifestReferences above — derived
|
|
74
|
+
* from the referenced media item, never authored by the editor. Must stay in
|
|
75
|
+
* sync with the `patch` object built in resolveManifestReferences.
|
|
76
|
+
*/
|
|
77
|
+
export const BAKED_MEDIA_KEYS = ["src", "srcset", "alt", "width", "height", "poster"] as const;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Re-point a media ref at a different library item. Drops every baked field so
|
|
81
|
+
* stale values resolved for the PREVIOUS image can't shadow the new one in the
|
|
82
|
+
* editor (ResolvedMedia prefers an explicit src prop over live resolution).
|
|
83
|
+
*/
|
|
84
|
+
export function rebindImageRef<T extends { imageId?: string }>(ref: T, imageId: string): T {
|
|
85
|
+
const fresh = { ...ref, imageId } as Record<string, unknown>;
|
|
86
|
+
for (const key of BAKED_MEDIA_KEYS) delete fresh[key];
|
|
87
|
+
return fresh as T;
|
|
88
|
+
}
|