@orion-studios/payload-studio 0.5.0-beta.7 → 0.5.0-beta.70

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 (67) hide show
  1. package/README.md +20 -0
  2. package/dist/admin/client.d.mts +2 -0
  3. package/dist/admin/client.d.ts +2 -0
  4. package/dist/admin/client.js +779 -137
  5. package/dist/admin/client.mjs +769 -129
  6. package/dist/admin/index.d.mts +1 -1
  7. package/dist/admin/index.d.ts +1 -1
  8. package/dist/admin/index.js +100 -8
  9. package/dist/admin/index.mjs +3 -1
  10. package/dist/admin-app/client.d.mts +7 -0
  11. package/dist/admin-app/client.d.ts +7 -0
  12. package/dist/admin-app/client.js +1262 -3
  13. package/dist/admin-app/client.mjs +1164 -2
  14. package/dist/admin-app/index.d.mts +1 -1
  15. package/dist/admin-app/index.d.ts +1 -1
  16. package/dist/admin-app/index.js +167 -0
  17. package/dist/admin-app/index.mjs +13 -1
  18. package/dist/admin-app/styles.css +229 -0
  19. package/dist/blocks/index.js +633 -8
  20. package/dist/blocks/index.mjs +2 -2
  21. package/dist/{chunk-ZLLNO5FM.mjs → chunk-CKX5Y2HU.mjs} +44 -13
  22. package/dist/{chunk-J7W5EE3B.mjs → chunk-HCEPGEAI.mjs} +100 -8
  23. package/dist/chunk-HXGAG6I7.mjs +325 -0
  24. package/dist/chunk-ROTPP5CU.mjs +99 -0
  25. package/dist/{chunk-ETRRXURT.mjs → chunk-SIL2J5MF.mjs} +14 -0
  26. package/dist/chunk-VDGSMD6H.mjs +1072 -0
  27. package/dist/{chunk-PC5622T7.mjs → chunk-XK3K5GRP.mjs} +620 -9
  28. package/dist/chunk-XVH5SCBD.mjs +234 -0
  29. package/dist/{index-CmR6NInu.d.ts → index-BIwu3qIH.d.mts} +29 -3
  30. package/dist/{index-CmR6NInu.d.mts → index-BIwu3qIH.d.ts} +29 -3
  31. package/dist/index-CdnUNrvX.d.mts +134 -0
  32. package/dist/{index-DbH0Ljwp.d.ts → index-CpG3UHcS.d.mts} +1 -0
  33. package/dist/{index-DbH0Ljwp.d.mts → index-CpG3UHcS.d.ts} +1 -0
  34. package/dist/index-DyHbWliW.d.ts +134 -0
  35. package/dist/index-ZbOx4OCF.d.mts +128 -0
  36. package/dist/index-ZbOx4OCF.d.ts +128 -0
  37. package/dist/{index-DJFhANvJ.d.mts → index-cDYkEj29.d.mts} +20 -2
  38. package/dist/{index-DJFhANvJ.d.ts → index-cDYkEj29.d.ts} +20 -2
  39. package/dist/index.d.mts +5 -5
  40. package/dist/index.d.ts +5 -5
  41. package/dist/index.js +2145 -305
  42. package/dist/index.mjs +9 -9
  43. package/dist/nextjs/index.d.mts +1 -1
  44. package/dist/nextjs/index.d.ts +1 -1
  45. package/dist/nextjs/index.js +996 -13
  46. package/dist/nextjs/index.mjs +4 -1
  47. package/dist/studio/index.d.mts +2 -1
  48. package/dist/studio/index.d.ts +2 -1
  49. package/dist/studio/index.js +171 -2
  50. package/dist/studio/index.mjs +7 -3
  51. package/dist/studio-pages/builder.css +358 -8
  52. package/dist/studio-pages/client.d.mts +17 -0
  53. package/dist/studio-pages/client.d.ts +17 -0
  54. package/dist/studio-pages/client.js +5657 -1478
  55. package/dist/studio-pages/client.mjs +5549 -1461
  56. package/dist/studio-pages/index.d.mts +3 -2
  57. package/dist/studio-pages/index.d.ts +3 -2
  58. package/dist/studio-pages/index.js +799 -29
  59. package/dist/studio-pages/index.mjs +6 -2
  60. package/package.json +26 -12
  61. package/dist/chunk-AAOHJDNS.mjs +0 -67
  62. package/dist/chunk-N67KVM2S.mjs +0 -156
  63. package/dist/chunk-UJFU323N.mjs +0 -301
  64. package/dist/index-B9N5MyjF.d.mts +0 -39
  65. package/dist/index-BallJs-K.d.mts +0 -43
  66. package/dist/index-BallJs-K.d.ts +0 -43
  67. package/dist/index-g8tBHLKD.d.ts +0 -39
@@ -17,8 +17,8 @@ import {
17
17
  defaultPageLayoutBlocks,
18
18
  sectionPresets,
19
19
  templateStarterPresets
20
- } from "../chunk-PC5622T7.mjs";
21
- import "../chunk-ETRRXURT.mjs";
20
+ } from "../chunk-XK3K5GRP.mjs";
21
+ import "../chunk-SIL2J5MF.mjs";
22
22
  import "../chunk-6BWS3CLP.mjs";
23
23
  export {
24
24
  BeforeAfterBlock,
@@ -1,3 +1,9 @@
1
+ import {
2
+ assertStudioDocumentV1
3
+ } from "./chunk-HXGAG6I7.mjs";
4
+ import {
5
+ studioDocumentToLayout
6
+ } from "./chunk-VDGSMD6H.mjs";
1
7
  import {
2
8
  __export
3
9
  } from "./chunk-6BWS3CLP.mjs";
@@ -27,6 +33,24 @@ function createPayloadClient(config) {
27
33
 
28
34
  // src/nextjs/queries/pages.ts
29
35
  import { unstable_cache } from "next/cache";
36
+ var PAGE_QUERY_CACHE_VERSION = "v4-published-only-public";
37
+ function withStudioDocumentLayout(page) {
38
+ if (!page) {
39
+ return null;
40
+ }
41
+ try {
42
+ const studioDocument = assertStudioDocumentV1(page.studioDocument);
43
+ const compiledLayout = studioDocumentToLayout(studioDocument);
44
+ if (Array.isArray(compiledLayout) && compiledLayout.length > 0) {
45
+ return {
46
+ ...page,
47
+ layout: compiledLayout
48
+ };
49
+ }
50
+ } catch {
51
+ }
52
+ return page;
53
+ }
30
54
  function normalizePath(segments) {
31
55
  if (!segments || segments.length === 0) {
32
56
  return "/";
@@ -35,35 +59,42 @@ function normalizePath(segments) {
35
59
  return cleaned.length > 0 ? `/${cleaned}` : "/";
36
60
  }
37
61
  async function queryPageByPath(payload, path, draft) {
62
+ const pathWhere = {
63
+ path: {
64
+ equals: path
65
+ }
66
+ };
67
+ const publishedWhere = {
68
+ _status: {
69
+ equals: "published"
70
+ }
71
+ };
38
72
  const result = await payload.find({
39
73
  collection: "pages",
40
74
  depth: 2,
41
75
  draft,
42
76
  limit: 1,
43
77
  overrideAccess: false,
44
- where: {
45
- path: {
46
- equals: path
47
- }
48
- }
78
+ where: draft ? pathWhere : { and: [pathWhere, publishedWhere] }
49
79
  });
50
80
  if (result.docs.length > 0) {
51
- return result.docs[0] || null;
81
+ return withStudioDocumentLayout(result.docs[0] || null);
52
82
  }
53
83
  if (path === "/") {
84
+ const homeWhere = {
85
+ slug: {
86
+ equals: "home"
87
+ }
88
+ };
54
89
  const homeResult = await payload.find({
55
90
  collection: "pages",
56
91
  depth: 2,
57
92
  draft,
58
93
  limit: 1,
59
94
  overrideAccess: false,
60
- where: {
61
- slug: {
62
- equals: "home"
63
- }
64
- }
95
+ where: draft ? homeWhere : { and: [homeWhere, publishedWhere] }
65
96
  });
66
- return homeResult.docs[0] || null;
97
+ return withStudioDocumentLayout(homeResult.docs[0] || null);
67
98
  }
68
99
  return null;
69
100
  }
@@ -73,7 +104,7 @@ function createPageQueries(getPayloadClient, contentTag = "website-content") {
73
104
  const payload = await getPayloadClient();
74
105
  return queryPageByPath(payload, path, false);
75
106
  },
76
- ["page-by-path"],
107
+ ["page-by-path", PAGE_QUERY_CACHE_VERSION],
77
108
  { tags: [contentTag] }
78
109
  );
79
110
  async function getPageBySegments(segments, draft = false) {
@@ -7,6 +7,7 @@ import {
7
7
  var admin_exports = {};
8
8
  __export(admin_exports, {
9
9
  configureAdmin: () => configureAdmin,
10
+ createHeaderNavItemsField: () => createHeaderNavItemsField,
10
11
  createThemePreferenceField: () => createThemePreferenceField,
11
12
  themePreferenceField: () => themePreferenceField,
12
13
  withTooltips: () => withTooltips
@@ -61,14 +62,25 @@ function configureAdmin(config) {
61
62
  defaultTheme = "brand-light",
62
63
  logoUrl
63
64
  } = config;
64
- const studioEnabled = Boolean(config.studio?.enabled);
65
+ const studioEnabled = config.studio?.enabled ?? true;
65
66
  const pagesCollectionSlug = config.studio?.pages?.collectionSlug || "pages";
66
67
  const mediaCollectionSlug = config.studio?.media?.collectionSlug || "media";
67
- const globals = config.studio?.globals || [
68
+ const contactFormStudioPath = "/studio-contact-form";
69
+ const configuredGlobals = config.studio?.globals || [
68
70
  { slug: "site-settings", label: "Website Settings" },
69
71
  { slug: "header", label: "Header & Navigation" },
70
- { slug: "footer", label: "Footer" }
72
+ { slug: "footer", label: "Footer" },
73
+ { slug: "contact-form", label: "Contact Form" }
71
74
  ];
75
+ const globals = configuredGlobals.map((global) => {
76
+ if (global.slug !== "contact-form" || global.href) {
77
+ return global;
78
+ }
79
+ return {
80
+ ...global,
81
+ href: contactFormStudioPath
82
+ };
83
+ });
72
84
  let cssPath;
73
85
  const pkgDist = getPkgDistDir();
74
86
  const sourceCssPath = path.resolve(pkgDist, "admin.css");
@@ -98,7 +110,8 @@ function configureAdmin(config) {
98
110
  clientProps: {
99
111
  brandName,
100
112
  logoUrl,
101
- globalsBasePath: "/admin/studio-globals",
113
+ globalsBasePath: "/studio-globals",
114
+ globalsExtraMatchPrefixes: [contactFormStudioPath],
102
115
  mediaCollectionSlug,
103
116
  pagesCollectionSlug
104
117
  }
@@ -130,7 +143,8 @@ function configureAdmin(config) {
130
143
  clientProps: {
131
144
  brandName,
132
145
  logoUrl,
133
- globalsBasePath: "/admin/studio-globals",
146
+ globalsBasePath: "/studio-globals",
147
+ globalsExtraMatchPrefixes: [contactFormStudioPath],
134
148
  mediaCollectionSlug,
135
149
  pagesCollectionSlug
136
150
  }
@@ -144,7 +158,18 @@ function configureAdmin(config) {
144
158
  path: clientPath,
145
159
  clientProps: {
146
160
  globals,
147
- globalsBasePath: "/admin/studio-globals"
161
+ globalsBasePath: "/studio-globals"
162
+ }
163
+ }
164
+ },
165
+ studioContactForm: {
166
+ path: "/studio-contact-form",
167
+ Component: {
168
+ exportName: "AdminStudioContactFormView",
169
+ path: clientPath,
170
+ clientProps: {
171
+ globalSlug: "contact-form",
172
+ globalsBasePath: "/studio-globals"
148
173
  }
149
174
  }
150
175
  }
@@ -191,16 +216,47 @@ function configureAdmin(config) {
191
216
  const labelMap = {
192
217
  header: { group: "Site Design", label: "Header & Navigation" },
193
218
  footer: { group: "Site Design", label: "Footer" },
194
- "site-settings": { group: "Site Design", label: "Website Settings" }
219
+ "site-settings": { group: "Site Design", label: "Website Settings" },
220
+ "contact-form": { group: "Lead Forms", label: "Contact Form" }
195
221
  };
196
222
  return globals2.map((global) => {
197
223
  const mapping = labelMap[global.slug];
198
224
  if (!mapping) return global;
225
+ const shouldAttachContactFormRedirect = studioEnabled && global.slug === "contact-form";
226
+ const existingViews = global.admin?.components?.views;
227
+ const existingEditViews = existingViews?.edit;
228
+ const hasCustomContactFormEditView = Boolean(
229
+ existingEditViews?.root || existingEditViews?.default && typeof existingEditViews.default === "object" && existingEditViews.default.Component
230
+ );
231
+ const contactFormEditViews = shouldAttachContactFormRedirect && !hasCustomContactFormEditView ? {
232
+ ...existingEditViews,
233
+ default: {
234
+ ...typeof existingEditViews?.default === "object" ? existingEditViews.default : {},
235
+ Component: {
236
+ exportName: "StudioContactFormRedirect",
237
+ path: clientPath,
238
+ clientProps: {
239
+ studioContactFormPath: contactFormStudioPath
240
+ }
241
+ }
242
+ }
243
+ } : existingEditViews;
199
244
  return {
200
245
  ...global,
201
246
  admin: {
202
247
  ...global.admin,
203
- group: mapping.group
248
+ group: mapping.group,
249
+ components: {
250
+ ...global.admin?.components,
251
+ ...shouldAttachContactFormRedirect ? {
252
+ views: {
253
+ ...existingViews,
254
+ ...contactFormEditViews ? {
255
+ edit: contactFormEditViews
256
+ } : {}
257
+ }
258
+ } : {}
259
+ }
204
260
  },
205
261
  label: mapping.label
206
262
  };
@@ -269,10 +325,46 @@ function addTooltipToField(field, tooltips) {
269
325
  return field;
270
326
  }
271
327
 
328
+ // src/admin/fields/headerNav.ts
329
+ var createHeaderNavItemsField = () => ({
330
+ name: "navItems",
331
+ type: "array",
332
+ labels: { singular: "Navigation Link", plural: "Navigation Links" },
333
+ admin: {
334
+ description: "The links displayed in your website's main navigation menu."
335
+ },
336
+ fields: [
337
+ {
338
+ name: "label",
339
+ type: "text",
340
+ required: true,
341
+ admin: {
342
+ description: "The text shown for this navigation link."
343
+ }
344
+ },
345
+ {
346
+ name: "href",
347
+ type: "text",
348
+ required: true,
349
+ admin: {
350
+ description: 'The URL this link points to (e.g., "/about" or "https://example.com").'
351
+ }
352
+ },
353
+ {
354
+ name: "parentHref",
355
+ type: "text",
356
+ admin: {
357
+ description: "Optional parent link URL. If set to another nav item href, this item appears in that dropdown."
358
+ }
359
+ }
360
+ ]
361
+ });
362
+
272
363
  export {
273
364
  createThemePreferenceField,
274
365
  themePreferenceField,
275
366
  configureAdmin,
276
367
  withTooltips,
368
+ createHeaderNavItemsField,
277
369
  admin_exports
278
370
  };
@@ -0,0 +1,325 @@
1
+ import {
2
+ __export
3
+ } from "./chunk-6BWS3CLP.mjs";
4
+
5
+ // src/studio/index.ts
6
+ var studio_exports = {};
7
+ __export(studio_exports, {
8
+ assertStudioDocumentV1: () => assertStudioDocumentV1,
9
+ compileStudioDocument: () => compileStudioDocument,
10
+ createEmptyStudioDocument: () => createEmptyStudioDocument,
11
+ createImageUploadOptimizationHook: () => createImageUploadOptimizationHook,
12
+ createStudioRegistry: () => createStudioRegistry,
13
+ migrateStudioDocument: () => migrateStudioDocument,
14
+ validateStudioDocument: () => validateStudioDocument,
15
+ withImageUploadOptimization: () => withImageUploadOptimization
16
+ });
17
+
18
+ // src/studio/imageUploadOptimization.ts
19
+ import { readFile, unlink } from "fs/promises";
20
+ var DEFAULT_SUPPORTED_MIME_TYPES = [
21
+ "image/jpeg",
22
+ "image/jpg",
23
+ "image/png",
24
+ "image/webp",
25
+ "image/avif",
26
+ "image/tiff"
27
+ ];
28
+ var DEFAULT_OPTIONS = {
29
+ avifQuality: 50,
30
+ enforceSmallerForLossy: true,
31
+ jpegQuality: 78,
32
+ minBytes: 48 * 1024,
33
+ minQualityFloor: 42,
34
+ onlyIfSmaller: true,
35
+ pngCompressionLevel: 9,
36
+ skipAnimated: true,
37
+ tiffQuality: 75,
38
+ webpQuality: 78
39
+ };
40
+ var clamp = (value, min, max) => Math.max(min, Math.min(max, Math.round(value)));
41
+ var isUploadMutationOperation = (operation) => operation === "create" || operation === "update" || operation === "updateByID";
42
+ var isLossyMimeType = (mimetype) => mimetype === "image/jpeg" || mimetype === "image/jpg" || mimetype === "image/webp" || mimetype === "image/avif" || mimetype === "image/tiff";
43
+ var readIncomingBuffer = async (file) => {
44
+ if (file.tempFilePath) {
45
+ return readFile(file.tempFilePath);
46
+ }
47
+ return file.data;
48
+ };
49
+ var writeOptimizedBuffer = async (file, buffer) => {
50
+ if (file.tempFilePath) {
51
+ await unlink(file.tempFilePath).catch(() => void 0);
52
+ }
53
+ return {
54
+ ...file,
55
+ data: buffer,
56
+ size: buffer.length,
57
+ tempFilePath: void 0
58
+ };
59
+ };
60
+ var createImageUploadOptimizationHook = (options = {}) => {
61
+ const supportedMimeTypes = new Set(options.supportedMimeTypes || DEFAULT_SUPPORTED_MIME_TYPES);
62
+ const settings = { ...DEFAULT_OPTIONS, ...options };
63
+ return async ({ operation, req }) => {
64
+ if (!isUploadMutationOperation(operation)) {
65
+ return;
66
+ }
67
+ const file = req.file;
68
+ if (!file) {
69
+ return;
70
+ }
71
+ if (!supportedMimeTypes.has(file.mimetype)) {
72
+ return;
73
+ }
74
+ if (typeof file.size === "number" && file.size < settings.minBytes) {
75
+ return;
76
+ }
77
+ const sharpFactory = req.payload?.config?.sharp;
78
+ if (typeof sharpFactory !== "function") {
79
+ return;
80
+ }
81
+ const originalBuffer = await readIncomingBuffer(file);
82
+ if (!Buffer.isBuffer(originalBuffer) || originalBuffer.length === 0) {
83
+ return;
84
+ }
85
+ const animatedInput = file.mimetype === "image/avif" || file.mimetype === "image/webp";
86
+ const metadataProbe = await sharpFactory(
87
+ originalBuffer,
88
+ animatedInput ? {
89
+ animated: true
90
+ } : void 0
91
+ ).metadata();
92
+ if (settings.skipAnimated && typeof metadataProbe.pages === "number" && metadataProbe.pages > 1) {
93
+ return;
94
+ }
95
+ const buildPipeline = (qualityOverride) => {
96
+ let pipeline = sharpFactory(
97
+ originalBuffer,
98
+ animatedInput ? {
99
+ animated: true
100
+ } : void 0
101
+ );
102
+ switch (file.mimetype) {
103
+ case "image/jpeg":
104
+ case "image/jpg":
105
+ pipeline = pipeline.jpeg({
106
+ mozjpeg: true,
107
+ progressive: true,
108
+ quality: clamp(qualityOverride ?? settings.jpegQuality, 20, 100)
109
+ });
110
+ break;
111
+ case "image/png":
112
+ pipeline = pipeline.png({
113
+ compressionLevel: clamp(settings.pngCompressionLevel, 0, 9),
114
+ palette: false
115
+ });
116
+ break;
117
+ case "image/webp":
118
+ pipeline = pipeline.webp({
119
+ quality: clamp(qualityOverride ?? settings.webpQuality, 20, 100)
120
+ });
121
+ break;
122
+ case "image/avif":
123
+ pipeline = pipeline.avif({
124
+ quality: clamp(qualityOverride ?? settings.avifQuality, 20, 100)
125
+ });
126
+ break;
127
+ case "image/tiff":
128
+ pipeline = pipeline.tiff({
129
+ quality: clamp(qualityOverride ?? settings.tiffQuality, 20, 100)
130
+ });
131
+ break;
132
+ default:
133
+ return null;
134
+ }
135
+ return pipeline;
136
+ };
137
+ const initialPipeline = buildPipeline();
138
+ if (!initialPipeline) {
139
+ return;
140
+ }
141
+ let optimizedBuffer = await initialPipeline.toBuffer();
142
+ const shouldSweepQuality = settings.enforceSmallerForLossy && isLossyMimeType(file.mimetype);
143
+ if (shouldSweepQuality) {
144
+ const initialQuality = file.mimetype === "image/jpeg" || file.mimetype === "image/jpg" ? settings.jpegQuality : file.mimetype === "image/webp" ? settings.webpQuality : file.mimetype === "image/avif" ? settings.avifQuality : settings.tiffQuality;
145
+ let bestBuffer = optimizedBuffer;
146
+ let quality = clamp(initialQuality - 5, settings.minQualityFloor, 100);
147
+ while (quality >= settings.minQualityFloor) {
148
+ const retryPipeline = buildPipeline(quality);
149
+ if (!retryPipeline) {
150
+ break;
151
+ }
152
+ const retryBuffer = await retryPipeline.toBuffer();
153
+ if (retryBuffer.length < bestBuffer.length) {
154
+ bestBuffer = retryBuffer;
155
+ }
156
+ quality -= 5;
157
+ }
158
+ optimizedBuffer = bestBuffer;
159
+ }
160
+ if (settings.onlyIfSmaller && optimizedBuffer.length >= originalBuffer.length) {
161
+ return;
162
+ }
163
+ req.file = await writeOptimizedBuffer(file, optimizedBuffer);
164
+ };
165
+ };
166
+ var withImageUploadOptimization = (collection, options = {}) => {
167
+ if (!collection.upload) {
168
+ return collection;
169
+ }
170
+ const existingHooks = collection.hooks || {};
171
+ const beforeOperation = existingHooks.beforeOperation || [];
172
+ return {
173
+ ...collection,
174
+ hooks: {
175
+ ...existingHooks,
176
+ beforeOperation: [createImageUploadOptimizationHook(options), ...beforeOperation]
177
+ }
178
+ };
179
+ };
180
+
181
+ // src/studio/index.ts
182
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
183
+ var makeIssue = (message, path, code = "studio.invalid") => ({
184
+ code,
185
+ message,
186
+ path,
187
+ severity: "error"
188
+ });
189
+ var createEmptyStudioDocument = (title) => ({
190
+ metadata: {},
191
+ schemaVersion: 1,
192
+ title,
193
+ nodes: [],
194
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
195
+ });
196
+ function assertStudioDocumentV1(input) {
197
+ if (!isRecord(input)) {
198
+ throw new Error("Studio document must be an object");
199
+ }
200
+ if (input.schemaVersion !== 1) {
201
+ throw new Error("Unsupported studio schemaVersion");
202
+ }
203
+ if (!Array.isArray(input.nodes)) {
204
+ throw new Error("Studio document nodes must be an array");
205
+ }
206
+ const nodes = input.nodes.map((node, index) => {
207
+ if (!isRecord(node)) {
208
+ throw new Error(`Node at index ${index} must be an object`);
209
+ }
210
+ if (typeof node.id !== "string" || node.id.length === 0) {
211
+ throw new Error(`Node at index ${index} has invalid id`);
212
+ }
213
+ if (typeof node.type !== "string" || node.type.length === 0) {
214
+ throw new Error(`Node at index ${index} has invalid type`);
215
+ }
216
+ if (!isRecord(node.data)) {
217
+ throw new Error(`Node at index ${index} has invalid data`);
218
+ }
219
+ return {
220
+ id: node.id,
221
+ type: node.type,
222
+ data: node.data
223
+ };
224
+ });
225
+ return {
226
+ metadata: isRecord(input.metadata) ? input.metadata : void 0,
227
+ schemaVersion: 1,
228
+ title: typeof input.title === "string" ? input.title : void 0,
229
+ nodes,
230
+ updatedAt: typeof input.updatedAt === "string" ? input.updatedAt : void 0
231
+ };
232
+ }
233
+ function createStudioRegistry(modules) {
234
+ const moduleByID = new Map(modules.map((mod) => [mod.id, mod]));
235
+ const nodeTypes = modules.flatMap((mod) => mod.nodeTypes);
236
+ const nodeTypeByName = new Map(nodeTypes.map((definition) => [definition.type, definition]));
237
+ return {
238
+ getModuleByID: (id) => moduleByID.get(id),
239
+ getNodeTypeByName: (type) => nodeTypeByName.get(type),
240
+ listInspectorPanels: () => modules.flatMap((mod) => mod.inspectorPanels),
241
+ listModules: () => [...modules],
242
+ listPaletteGroups: () => modules.flatMap((mod) => mod.paletteGroups),
243
+ listNodeTypes: () => [...nodeTypes]
244
+ };
245
+ }
246
+ function validateStudioDocument(document, modules) {
247
+ const issues = [];
248
+ if (document.schemaVersion !== 1) {
249
+ issues.push(makeIssue("Unsupported schema version", "schemaVersion", "studio.schemaVersion"));
250
+ }
251
+ if (!Array.isArray(document.nodes)) {
252
+ issues.push(makeIssue("Nodes must be an array", "nodes", "studio.nodes"));
253
+ return issues;
254
+ }
255
+ const registry = createStudioRegistry(modules);
256
+ const nodeIDs = /* @__PURE__ */ new Set();
257
+ document.nodes.forEach((node, index) => {
258
+ if (!node.id) {
259
+ issues.push(makeIssue("Node id is required", `nodes.${index}.id`, "studio.node.id"));
260
+ }
261
+ if (nodeIDs.has(node.id)) {
262
+ issues.push(makeIssue("Node id must be unique", `nodes.${index}.id`, "studio.node.id.duplicate"));
263
+ }
264
+ nodeIDs.add(node.id);
265
+ if (!registry.getNodeTypeByName(node.type)) {
266
+ issues.push(makeIssue("Unsupported node type", `nodes.${index}.type`, "studio.node.type"));
267
+ }
268
+ });
269
+ for (const module of modules) {
270
+ for (const validate of module.validators) {
271
+ issues.push(...validate(document));
272
+ }
273
+ }
274
+ return issues;
275
+ }
276
+ function compileStudioDocument(document, modules) {
277
+ const issues = validateStudioDocument(document, modules);
278
+ const compilerEntries = modules.filter((mod) => typeof mod.compiler?.compileNode === "function").map((mod) => mod.compiler?.compileNode);
279
+ const layout = document.nodes.map((node) => {
280
+ for (const compileNode of compilerEntries) {
281
+ if (!compileNode) {
282
+ continue;
283
+ }
284
+ const compiled = compileNode(node);
285
+ if (compiled) {
286
+ return compiled;
287
+ }
288
+ }
289
+ return {
290
+ id: node.id,
291
+ blockType: node.type,
292
+ ...node.data
293
+ };
294
+ });
295
+ return {
296
+ issues,
297
+ layout
298
+ };
299
+ }
300
+ function migrateStudioDocument(value, migrations) {
301
+ const sorted = [...migrations].sort((a, b) => a.fromVersion - b.fromVersion);
302
+ if (isRecord(value) && value.schemaVersion === 1) {
303
+ return assertStudioDocumentV1(value);
304
+ }
305
+ let current = value;
306
+ for (const migration of sorted) {
307
+ if (!isRecord(current) || current.schemaVersion !== migration.fromVersion) {
308
+ continue;
309
+ }
310
+ current = migration.migrate(current);
311
+ }
312
+ return assertStudioDocumentV1(current);
313
+ }
314
+
315
+ export {
316
+ createImageUploadOptimizationHook,
317
+ withImageUploadOptimization,
318
+ createEmptyStudioDocument,
319
+ assertStudioDocumentV1,
320
+ createStudioRegistry,
321
+ validateStudioDocument,
322
+ compileStudioDocument,
323
+ migrateStudioDocument,
324
+ studio_exports
325
+ };
@@ -0,0 +1,99 @@
1
+ 'use client';
2
+
3
+ // src/shared/clientImageUploadOptimization.ts
4
+ var MAX_DIRECT_UPLOAD_BYTES = 4e6;
5
+ var extensionForMimeType = (mimeType) => {
6
+ switch (mimeType) {
7
+ case "image/webp":
8
+ return ".webp";
9
+ case "image/png":
10
+ return ".png";
11
+ default:
12
+ return ".jpg";
13
+ }
14
+ };
15
+ var detectCanvasTransparency = (context, width, height) => {
16
+ try {
17
+ const { data } = context.getImageData(0, 0, width, height);
18
+ for (let index = 3; index < data.length; index += 4) {
19
+ if (data[index] < 255) {
20
+ return true;
21
+ }
22
+ }
23
+ } catch {
24
+ }
25
+ return false;
26
+ };
27
+ var resolveOutputMimeTypes = (sourceMime, hasTransparency) => {
28
+ const candidates = [];
29
+ if (hasTransparency) {
30
+ candidates.push("image/webp", "image/png");
31
+ } else if (sourceMime === "image/webp") {
32
+ candidates.push("image/webp", "image/jpeg");
33
+ } else if (sourceMime === "image/png") {
34
+ candidates.push("image/webp", "image/jpeg", "image/png");
35
+ } else {
36
+ candidates.push("image/jpeg", "image/webp");
37
+ }
38
+ return [...new Set(candidates)];
39
+ };
40
+ async function optimizeImageForUpload(file) {
41
+ if (!file.type.startsWith("image/")) {
42
+ return file;
43
+ }
44
+ const objectURL = URL.createObjectURL(file);
45
+ try {
46
+ const image = await new Promise((resolve, reject) => {
47
+ const nextImage = new Image();
48
+ nextImage.onload = () => resolve(nextImage);
49
+ nextImage.onerror = () => reject(new Error("Could not read image for upload optimization."));
50
+ nextImage.src = objectURL;
51
+ });
52
+ const canvas = document.createElement("canvas");
53
+ canvas.width = Math.max(1, image.width);
54
+ canvas.height = Math.max(1, image.height);
55
+ const context = canvas.getContext("2d");
56
+ if (!context) {
57
+ return file;
58
+ }
59
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
60
+ const sourceMime = file.type.toLowerCase();
61
+ const hasTransparency = detectCanvasTransparency(context, canvas.width, canvas.height);
62
+ const outputMimes = resolveOutputMimeTypes(sourceMime, hasTransparency);
63
+ const qualityPasses = [0.82, 0.74, 0.66, 0.58, 0.5, 0.42, 0.36, 0.3, 0.26];
64
+ let bestFile = null;
65
+ for (const outputMime of outputMimes) {
66
+ const passes = outputMime === "image/png" ? [void 0] : qualityPasses;
67
+ for (const quality of passes) {
68
+ const blob = await new Promise((resolve) => {
69
+ canvas.toBlob((value) => resolve(value), outputMime, quality);
70
+ });
71
+ if (!blob) {
72
+ continue;
73
+ }
74
+ const optimizedName = file.name.replace(/\.[^/.]+$/, extensionForMimeType(outputMime));
75
+ const optimized = new File([blob], optimizedName, {
76
+ lastModified: Date.now(),
77
+ type: outputMime
78
+ });
79
+ if (!bestFile || optimized.size < bestFile.size) {
80
+ bestFile = optimized;
81
+ }
82
+ if (optimized.size <= MAX_DIRECT_UPLOAD_BYTES) {
83
+ break;
84
+ }
85
+ }
86
+ }
87
+ if (!bestFile) {
88
+ return file;
89
+ }
90
+ return bestFile.size < file.size ? bestFile : file;
91
+ } finally {
92
+ URL.revokeObjectURL(objectURL);
93
+ }
94
+ }
95
+
96
+ export {
97
+ MAX_DIRECT_UPLOAD_BYTES,
98
+ optimizeImageForUpload
99
+ };