@orion-studios/payload-studio 0.4.0-beta.0 → 0.4.0-beta.1

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/README.md CHANGED
@@ -13,6 +13,9 @@ npm install @orion-studios/payload-studio
13
13
  - `@orion-studios/payload-studio/admin`
14
14
  - `@orion-studios/payload-studio/admin/client`
15
15
  - `@orion-studios/payload-studio/admin.css`
16
+ - `@orion-studios/payload-studio/admin-app`
17
+ - `@orion-studios/payload-studio/admin-app/client`
18
+ - `@orion-studios/payload-studio/admin-app/styles.css`
16
19
  - `@orion-studios/payload-studio/blocks`
17
20
  - `@orion-studios/payload-studio/nextjs`
18
21
  - `@orion-studios/payload-studio/studio`
@@ -35,6 +38,12 @@ import { BuilderPageEditor } from '@orion-studios/payload-studio/studio-pages/cl
35
38
  import '@orion-studios/payload-studio/studio-pages/builder.css'
36
39
  ```
37
40
 
41
+ ```ts
42
+ import { AdminPage } from '@orion-studios/payload-studio/admin-app'
43
+ import { AdminShellClient } from '@orion-studios/payload-studio/admin-app/client'
44
+ import '@orion-studios/payload-studio/admin-app/styles.css'
45
+ ```
46
+
38
47
  ## Build
39
48
 
40
49
  ```bash
@@ -115,3 +115,8 @@
115
115
  min-width: 0;
116
116
  padding: 1.15rem 1.2rem 2.6rem;
117
117
  }
118
+
119
+ .orion-admin-page {
120
+ max-width: none;
121
+ width: 100%;
122
+ }
package/dist/index.mjs CHANGED
@@ -1,18 +1,18 @@
1
1
  import {
2
2
  admin_exports
3
3
  } from "./chunk-J7W5EE3B.mjs";
4
- import {
5
- admin_app_exports
6
- } from "./chunk-AAOHJDNS.mjs";
7
4
  import {
8
5
  studio_exports
9
6
  } from "./chunk-WLXZDMK3.mjs";
10
- import {
11
- studio_pages_exports
12
- } from "./chunk-Q76U4Z53.mjs";
13
7
  import {
14
8
  nextjs_exports
15
9
  } from "./chunk-ZLLNO5FM.mjs";
10
+ import {
11
+ admin_app_exports
12
+ } from "./chunk-AAOHJDNS.mjs";
13
+ import {
14
+ studio_pages_exports
15
+ } from "./chunk-Q76U4Z53.mjs";
16
16
  import {
17
17
  blocks_exports
18
18
  } from "./chunk-L62FYT57.mjs";
@@ -112,8 +112,13 @@ body {
112
112
  .features {
113
113
  border-radius: var(--orion-studio-radius-lg);
114
114
  padding: 1.25rem;
115
- background: #ffffff;
115
+ background: linear-gradient(135deg, #124a37 0%, #1f684f 100%);
116
116
  border: 1px solid var(--orion-studio-border);
117
+ color: #fff;
118
+ }
119
+
120
+ .features .inner {
121
+ padding: 1.8rem;
117
122
  }
118
123
 
119
124
  .feature-grid {
@@ -124,12 +129,20 @@ body {
124
129
  }
125
130
 
126
131
  .feature-item {
127
- background: #fff;
128
- border: 1px solid rgba(19, 33, 28, 0.1);
132
+ background: rgba(255, 255, 255, 0.1);
133
+ border: 1px solid rgba(255, 255, 255, 0.2);
129
134
  border-radius: 14px;
130
135
  padding: 0.9rem;
131
136
  }
132
137
 
138
+ .feature-item h3 {
139
+ color: #fff;
140
+ }
141
+
142
+ .feature-item p {
143
+ color: rgba(255, 255, 255, 0.88);
144
+ }
145
+
133
146
  .feature-icon {
134
147
  width: 44px;
135
148
  height: 44px;
@@ -518,6 +518,37 @@ function parseColor(value, fallback) {
518
518
  }
519
519
  return fallback;
520
520
  }
521
+ function getRelationID(value) {
522
+ if (typeof value === "number" || typeof value === "string") {
523
+ return value;
524
+ }
525
+ if (!value || typeof value !== "object") {
526
+ return null;
527
+ }
528
+ if ("id" in value) {
529
+ const id = value.id;
530
+ if (typeof id === "number" || typeof id === "string") {
531
+ return id;
532
+ }
533
+ }
534
+ return null;
535
+ }
536
+ function extractUploadedMedia(value) {
537
+ const candidate = value && typeof value === "object" && "doc" in value ? value.doc : value;
538
+ if (!candidate || typeof candidate !== "object") {
539
+ return null;
540
+ }
541
+ const id = getRelationID(candidate);
542
+ if (id === null) {
543
+ return null;
544
+ }
545
+ return {
546
+ alt: typeof candidate.alt === "string" ? candidate.alt : "",
547
+ filename: typeof candidate.filename === "string" ? candidate.filename : "",
548
+ id,
549
+ url: typeof candidate.url === "string" ? candidate.url : ""
550
+ };
551
+ }
521
552
  function InlineText({
522
553
  as = "p",
523
554
  className,
@@ -807,6 +838,13 @@ function BuilderPageEditor({ initialDoc, pageID }) {
807
838
  const [selectedIndex, setSelectedIndex] = (0, import_react.useState)(null);
808
839
  const [dragIndex, setDragIndex] = (0, import_react.useState)(null);
809
840
  const [sidebarOpen, setSidebarOpen] = (0, import_react.useState)(true);
841
+ const [savingStatus, setSavingStatus] = (0, import_react.useState)(null);
842
+ const [saveMessage, setSaveMessage] = (0, import_react.useState)("");
843
+ const [saveError, setSaveError] = (0, import_react.useState)("");
844
+ const [uploadingTarget, setUploadingTarget] = (0, import_react.useState)(null);
845
+ const [uploadError, setUploadError] = (0, import_react.useState)("");
846
+ const [uploadMessage, setUploadMessage] = (0, import_react.useState)("");
847
+ const [uploadAltText, setUploadAltText] = (0, import_react.useState)("");
810
848
  const selectedBlock = (0, import_react.useMemo)(
811
849
  () => selectedIndex !== null ? layout[selectedIndex] : null,
812
850
  [layout, selectedIndex]
@@ -873,6 +911,75 @@ function BuilderPageEditor({ initialDoc, pageID }) {
873
911
  const currentItems = Array.isArray(selectedBlock[fieldName]) ? selectedBlock[fieldName] : [];
874
912
  updateSelectedArray(fieldName, [...currentItems, item]);
875
913
  };
914
+ const uploadMediaForSelected = async (target, file) => {
915
+ if (selectedIndex === null) {
916
+ setUploadError("Select a section first.");
917
+ return;
918
+ }
919
+ setUploadingTarget(target);
920
+ setUploadError("");
921
+ setUploadMessage("");
922
+ try {
923
+ const formData = new FormData();
924
+ const fallbackAlt = file.name.replace(/\.[^/.]+$/, "").trim();
925
+ formData.set("alt", uploadAltText.trim() || fallbackAlt || "Uploaded image");
926
+ formData.set("file", file);
927
+ const response = await fetch("/api/media", {
928
+ body: formData,
929
+ credentials: "include",
930
+ method: "POST"
931
+ });
932
+ if (!response.ok) {
933
+ const body = await response.text();
934
+ throw new Error(body || "Upload failed");
935
+ }
936
+ const json = await response.json();
937
+ const uploaded = extractUploadedMedia(json);
938
+ if (!uploaded) {
939
+ throw new Error("Upload succeeded but returned media data was malformed.");
940
+ }
941
+ const nextLayout = cloneBlockLayout(layout);
942
+ const block = nextLayout[selectedIndex];
943
+ if (target === "hero") {
944
+ nextLayout[selectedIndex] = {
945
+ ...block,
946
+ backgroundImageURL: "",
947
+ media: uploaded
948
+ };
949
+ } else {
950
+ nextLayout[selectedIndex] = {
951
+ ...block,
952
+ image: uploaded
953
+ };
954
+ }
955
+ setLayout(nextLayout);
956
+ setUploadMessage("Image uploaded and attached to this section.");
957
+ } catch (error) {
958
+ setUploadError(error instanceof Error ? error.message : "Upload failed.");
959
+ } finally {
960
+ setUploadingTarget(null);
961
+ }
962
+ };
963
+ const toPersistedLayout = (sourceLayout) => sourceLayout.map((block) => {
964
+ if (!block || typeof block !== "object") {
965
+ return block;
966
+ }
967
+ const nextBlock = { ...block };
968
+ const blockType = normalizeText(nextBlock.blockType);
969
+ if (blockType === "hero") {
970
+ const mediaID = getRelationID(nextBlock.media);
971
+ if (mediaID !== null) {
972
+ nextBlock.media = mediaID;
973
+ }
974
+ }
975
+ if (blockType === "media") {
976
+ const imageID = getRelationID(nextBlock.image);
977
+ if (imageID !== null) {
978
+ nextBlock.image = imageID;
979
+ }
980
+ }
981
+ return nextBlock;
982
+ });
876
983
  const sidebarSectionStyle = {
877
984
  border: "1px solid rgba(13, 74, 55, 0.15)",
878
985
  borderRadius: 12,
@@ -921,12 +1028,16 @@ function BuilderPageEditor({ initialDoc, pageID }) {
921
1028
  });
922
1029
  };
923
1030
  const saveLayout = async (status) => {
1031
+ setSavingStatus(status);
1032
+ setSaveError("");
1033
+ setSaveMessage("");
924
1034
  try {
1035
+ const persistedLayout = toPersistedLayout(layout);
925
1036
  const response = await fetch(`/api/pages/${pageID}`, {
926
1037
  body: JSON.stringify({
927
1038
  _status: status,
928
- layout,
929
- studioDocument: layoutToStudioDocument(layout, title),
1039
+ layout: persistedLayout,
1040
+ studioDocument: layoutToStudioDocument(persistedLayout, title),
930
1041
  title
931
1042
  }),
932
1043
  headers: {
@@ -938,6 +1049,7 @@ function BuilderPageEditor({ initialDoc, pageID }) {
938
1049
  const body = await response.text();
939
1050
  throw new Error(body || "Failed to save page");
940
1051
  }
1052
+ setSaveMessage(status === "published" ? "Published." : "Draft saved.");
941
1053
  window.parent?.postMessage(
942
1054
  {
943
1055
  message: status === "published" ? "Published." : "Draft saved.",
@@ -948,8 +1060,10 @@ function BuilderPageEditor({ initialDoc, pageID }) {
948
1060
  },
949
1061
  "*"
950
1062
  );
1063
+ return true;
951
1064
  } catch (error) {
952
1065
  console.error(error);
1066
+ setSaveError("Could not save. Check permissions/session and retry.");
953
1067
  window.parent?.postMessage(
954
1068
  {
955
1069
  message: "Could not save. Check permissions/session and retry.",
@@ -960,6 +1074,9 @@ function BuilderPageEditor({ initialDoc, pageID }) {
960
1074
  },
961
1075
  "*"
962
1076
  );
1077
+ return false;
1078
+ } finally {
1079
+ setSavingStatus(null);
963
1080
  }
964
1081
  };
965
1082
  (0, import_react.useEffect)(() => {
@@ -1632,6 +1749,48 @@ function BuilderPageEditor({ initialDoc, pageID }) {
1632
1749
  }
1633
1750
  ),
1634
1751
  sidebarOpen ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "grid", gap: 12, maxHeight: "calc(100vh - 90px)", overflowY: "auto", padding: 12 }, children: [
1752
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { style: sidebarSectionStyle, children: [
1753
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 8 }, children: "Save & Publish" }),
1754
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", gap: 6 }, children: [
1755
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1756
+ "button",
1757
+ {
1758
+ disabled: savingStatus !== null,
1759
+ onClick: () => void saveLayout("draft"),
1760
+ style: {
1761
+ borderRadius: 999,
1762
+ cursor: savingStatus ? "not-allowed" : "pointer",
1763
+ fontSize: 12,
1764
+ fontWeight: 700,
1765
+ padding: "7px 10px"
1766
+ },
1767
+ type: "button",
1768
+ children: savingStatus === "draft" ? "Saving..." : "Save Draft"
1769
+ }
1770
+ ),
1771
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1772
+ "button",
1773
+ {
1774
+ disabled: savingStatus !== null,
1775
+ onClick: () => void saveLayout("published"),
1776
+ style: {
1777
+ background: "#0f7d52",
1778
+ border: "none",
1779
+ borderRadius: 999,
1780
+ color: "#fff",
1781
+ cursor: savingStatus ? "not-allowed" : "pointer",
1782
+ fontSize: 12,
1783
+ fontWeight: 700,
1784
+ padding: "7px 10px"
1785
+ },
1786
+ type: "button",
1787
+ children: savingStatus === "published" ? "Publishing..." : "Publish"
1788
+ }
1789
+ )
1790
+ ] }),
1791
+ saveMessage ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#0f7d52", fontSize: 11, fontWeight: 700, marginTop: 8 }, children: saveMessage }) : null,
1792
+ saveError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#8d1d1d", fontSize: 11, fontWeight: 700, marginTop: 8 }, children: saveError }) : null
1793
+ ] }),
1635
1794
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", { style: sidebarSectionStyle, children: [
1636
1795
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 8 }, children: "Add Sections" }),
1637
1796
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
@@ -1788,6 +1947,38 @@ function BuilderPageEditor({ initialDoc, pageID }) {
1788
1947
  value: parseColor(selectedBlock.backgroundColor, "#124a37")
1789
1948
  }
1790
1949
  )
1950
+ ] }),
1951
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
1952
+ "Upload Alt Text",
1953
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1954
+ "input",
1955
+ {
1956
+ onChange: (event) => setUploadAltText(event.target.value),
1957
+ placeholder: "Describe the image",
1958
+ style: sidebarInputStyle,
1959
+ type: "text",
1960
+ value: uploadAltText
1961
+ }
1962
+ )
1963
+ ] }),
1964
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
1965
+ "Upload Hero Background Image",
1966
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1967
+ "input",
1968
+ {
1969
+ accept: "image/*",
1970
+ disabled: uploadingTarget !== null,
1971
+ onChange: (event) => {
1972
+ const file = event.currentTarget.files?.[0];
1973
+ if (file) {
1974
+ void uploadMediaForSelected("hero", file);
1975
+ }
1976
+ event.currentTarget.value = "";
1977
+ },
1978
+ style: sidebarInputStyle,
1979
+ type: "file"
1980
+ }
1981
+ )
1791
1982
  ] })
1792
1983
  ] }) : null,
1793
1984
  selectedType === "featureGrid" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
@@ -1861,20 +2052,54 @@ function BuilderPageEditor({ initialDoc, pageID }) {
1861
2052
  )
1862
2053
  ] })
1863
2054
  ] }) : null,
1864
- selectedType === "media" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
1865
- "Image Size",
1866
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1867
- "select",
1868
- {
1869
- onChange: (event) => updateSelectedField("size", event.target.value),
1870
- style: sidebarInputStyle,
1871
- value: normalizeText(selectedBlock.size, "default"),
1872
- children: [
1873
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "default", children: "Default" }),
1874
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "wide", children: "Wide" })
1875
- ]
1876
- }
1877
- )
2055
+ selectedType === "media" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
2056
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
2057
+ "Image Size",
2058
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
2059
+ "select",
2060
+ {
2061
+ onChange: (event) => updateSelectedField("size", event.target.value),
2062
+ style: sidebarInputStyle,
2063
+ value: normalizeText(selectedBlock.size, "default"),
2064
+ children: [
2065
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "default", children: "Default" }),
2066
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("option", { value: "wide", children: "Wide" })
2067
+ ]
2068
+ }
2069
+ )
2070
+ ] }),
2071
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
2072
+ "Upload Alt Text",
2073
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2074
+ "input",
2075
+ {
2076
+ onChange: (event) => setUploadAltText(event.target.value),
2077
+ placeholder: "Describe the image",
2078
+ style: sidebarInputStyle,
2079
+ type: "text",
2080
+ value: uploadAltText
2081
+ }
2082
+ )
2083
+ ] }),
2084
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
2085
+ "Upload Section Image",
2086
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2087
+ "input",
2088
+ {
2089
+ accept: "image/*",
2090
+ disabled: uploadingTarget !== null,
2091
+ onChange: (event) => {
2092
+ const file = event.currentTarget.files?.[0];
2093
+ if (file) {
2094
+ void uploadMediaForSelected("media", file);
2095
+ }
2096
+ event.currentTarget.value = "";
2097
+ },
2098
+ style: sidebarInputStyle,
2099
+ type: "file"
2100
+ }
2101
+ )
2102
+ ] })
1878
2103
  ] }) : null,
1879
2104
  selectedType === "richText" ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("label", { style: sidebarLabelStyle, children: [
1880
2105
  "Content Width",
@@ -1928,7 +2153,10 @@ function BuilderPageEditor({ initialDoc, pageID }) {
1928
2153
  children: "Add Testimonial"
1929
2154
  }
1930
2155
  ) : null,
1931
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "var(--ink-700)", fontSize: 11 }, children: "Click section text directly on the page for copy edits. Use this panel for layout and style options." })
2156
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "var(--ink-700)", fontSize: 11 }, children: "Click section text directly on the page for copy edits. Use this panel for layout and style options." }),
2157
+ uploadingTarget ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "var(--ink-700)", fontSize: 11 }, children: "Uploading image..." }) : null,
2158
+ uploadMessage ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#0f7d52", fontSize: 11, fontWeight: 700 }, children: uploadMessage }) : null,
2159
+ uploadError ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { color: "#8d1d1d", fontSize: 11, fontWeight: 700 }, children: uploadError }) : null
1932
2160
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", { style: { color: "var(--ink-700)", fontSize: 12, margin: 0 }, children: "Click a section on the page preview to edit its options here." })
1933
2161
  ] })
1934
2162
  ] }) : null
@@ -490,6 +490,37 @@ function parseColor(value, fallback) {
490
490
  }
491
491
  return fallback;
492
492
  }
493
+ function getRelationID(value) {
494
+ if (typeof value === "number" || typeof value === "string") {
495
+ return value;
496
+ }
497
+ if (!value || typeof value !== "object") {
498
+ return null;
499
+ }
500
+ if ("id" in value) {
501
+ const id = value.id;
502
+ if (typeof id === "number" || typeof id === "string") {
503
+ return id;
504
+ }
505
+ }
506
+ return null;
507
+ }
508
+ function extractUploadedMedia(value) {
509
+ const candidate = value && typeof value === "object" && "doc" in value ? value.doc : value;
510
+ if (!candidate || typeof candidate !== "object") {
511
+ return null;
512
+ }
513
+ const id = getRelationID(candidate);
514
+ if (id === null) {
515
+ return null;
516
+ }
517
+ return {
518
+ alt: typeof candidate.alt === "string" ? candidate.alt : "",
519
+ filename: typeof candidate.filename === "string" ? candidate.filename : "",
520
+ id,
521
+ url: typeof candidate.url === "string" ? candidate.url : ""
522
+ };
523
+ }
493
524
  function InlineText({
494
525
  as = "p",
495
526
  className,
@@ -779,6 +810,13 @@ function BuilderPageEditor({ initialDoc, pageID }) {
779
810
  const [selectedIndex, setSelectedIndex] = useState(null);
780
811
  const [dragIndex, setDragIndex] = useState(null);
781
812
  const [sidebarOpen, setSidebarOpen] = useState(true);
813
+ const [savingStatus, setSavingStatus] = useState(null);
814
+ const [saveMessage, setSaveMessage] = useState("");
815
+ const [saveError, setSaveError] = useState("");
816
+ const [uploadingTarget, setUploadingTarget] = useState(null);
817
+ const [uploadError, setUploadError] = useState("");
818
+ const [uploadMessage, setUploadMessage] = useState("");
819
+ const [uploadAltText, setUploadAltText] = useState("");
782
820
  const selectedBlock = useMemo(
783
821
  () => selectedIndex !== null ? layout[selectedIndex] : null,
784
822
  [layout, selectedIndex]
@@ -845,6 +883,75 @@ function BuilderPageEditor({ initialDoc, pageID }) {
845
883
  const currentItems = Array.isArray(selectedBlock[fieldName]) ? selectedBlock[fieldName] : [];
846
884
  updateSelectedArray(fieldName, [...currentItems, item]);
847
885
  };
886
+ const uploadMediaForSelected = async (target, file) => {
887
+ if (selectedIndex === null) {
888
+ setUploadError("Select a section first.");
889
+ return;
890
+ }
891
+ setUploadingTarget(target);
892
+ setUploadError("");
893
+ setUploadMessage("");
894
+ try {
895
+ const formData = new FormData();
896
+ const fallbackAlt = file.name.replace(/\.[^/.]+$/, "").trim();
897
+ formData.set("alt", uploadAltText.trim() || fallbackAlt || "Uploaded image");
898
+ formData.set("file", file);
899
+ const response = await fetch("/api/media", {
900
+ body: formData,
901
+ credentials: "include",
902
+ method: "POST"
903
+ });
904
+ if (!response.ok) {
905
+ const body = await response.text();
906
+ throw new Error(body || "Upload failed");
907
+ }
908
+ const json = await response.json();
909
+ const uploaded = extractUploadedMedia(json);
910
+ if (!uploaded) {
911
+ throw new Error("Upload succeeded but returned media data was malformed.");
912
+ }
913
+ const nextLayout = cloneBlockLayout(layout);
914
+ const block = nextLayout[selectedIndex];
915
+ if (target === "hero") {
916
+ nextLayout[selectedIndex] = {
917
+ ...block,
918
+ backgroundImageURL: "",
919
+ media: uploaded
920
+ };
921
+ } else {
922
+ nextLayout[selectedIndex] = {
923
+ ...block,
924
+ image: uploaded
925
+ };
926
+ }
927
+ setLayout(nextLayout);
928
+ setUploadMessage("Image uploaded and attached to this section.");
929
+ } catch (error) {
930
+ setUploadError(error instanceof Error ? error.message : "Upload failed.");
931
+ } finally {
932
+ setUploadingTarget(null);
933
+ }
934
+ };
935
+ const toPersistedLayout = (sourceLayout) => sourceLayout.map((block) => {
936
+ if (!block || typeof block !== "object") {
937
+ return block;
938
+ }
939
+ const nextBlock = { ...block };
940
+ const blockType = normalizeText(nextBlock.blockType);
941
+ if (blockType === "hero") {
942
+ const mediaID = getRelationID(nextBlock.media);
943
+ if (mediaID !== null) {
944
+ nextBlock.media = mediaID;
945
+ }
946
+ }
947
+ if (blockType === "media") {
948
+ const imageID = getRelationID(nextBlock.image);
949
+ if (imageID !== null) {
950
+ nextBlock.image = imageID;
951
+ }
952
+ }
953
+ return nextBlock;
954
+ });
848
955
  const sidebarSectionStyle = {
849
956
  border: "1px solid rgba(13, 74, 55, 0.15)",
850
957
  borderRadius: 12,
@@ -893,12 +1000,16 @@ function BuilderPageEditor({ initialDoc, pageID }) {
893
1000
  });
894
1001
  };
895
1002
  const saveLayout = async (status) => {
1003
+ setSavingStatus(status);
1004
+ setSaveError("");
1005
+ setSaveMessage("");
896
1006
  try {
1007
+ const persistedLayout = toPersistedLayout(layout);
897
1008
  const response = await fetch(`/api/pages/${pageID}`, {
898
1009
  body: JSON.stringify({
899
1010
  _status: status,
900
- layout,
901
- studioDocument: layoutToStudioDocument(layout, title),
1011
+ layout: persistedLayout,
1012
+ studioDocument: layoutToStudioDocument(persistedLayout, title),
902
1013
  title
903
1014
  }),
904
1015
  headers: {
@@ -910,6 +1021,7 @@ function BuilderPageEditor({ initialDoc, pageID }) {
910
1021
  const body = await response.text();
911
1022
  throw new Error(body || "Failed to save page");
912
1023
  }
1024
+ setSaveMessage(status === "published" ? "Published." : "Draft saved.");
913
1025
  window.parent?.postMessage(
914
1026
  {
915
1027
  message: status === "published" ? "Published." : "Draft saved.",
@@ -920,8 +1032,10 @@ function BuilderPageEditor({ initialDoc, pageID }) {
920
1032
  },
921
1033
  "*"
922
1034
  );
1035
+ return true;
923
1036
  } catch (error) {
924
1037
  console.error(error);
1038
+ setSaveError("Could not save. Check permissions/session and retry.");
925
1039
  window.parent?.postMessage(
926
1040
  {
927
1041
  message: "Could not save. Check permissions/session and retry.",
@@ -932,6 +1046,9 @@ function BuilderPageEditor({ initialDoc, pageID }) {
932
1046
  },
933
1047
  "*"
934
1048
  );
1049
+ return false;
1050
+ } finally {
1051
+ setSavingStatus(null);
935
1052
  }
936
1053
  };
937
1054
  useEffect(() => {
@@ -1604,6 +1721,48 @@ function BuilderPageEditor({ initialDoc, pageID }) {
1604
1721
  }
1605
1722
  ),
1606
1723
  sidebarOpen ? /* @__PURE__ */ jsxs("div", { style: { display: "grid", gap: 12, maxHeight: "calc(100vh - 90px)", overflowY: "auto", padding: 12 }, children: [
1724
+ /* @__PURE__ */ jsxs("section", { style: sidebarSectionStyle, children: [
1725
+ /* @__PURE__ */ jsx("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 8 }, children: "Save & Publish" }),
1726
+ /* @__PURE__ */ jsxs("div", { style: { display: "flex", gap: 6 }, children: [
1727
+ /* @__PURE__ */ jsx(
1728
+ "button",
1729
+ {
1730
+ disabled: savingStatus !== null,
1731
+ onClick: () => void saveLayout("draft"),
1732
+ style: {
1733
+ borderRadius: 999,
1734
+ cursor: savingStatus ? "not-allowed" : "pointer",
1735
+ fontSize: 12,
1736
+ fontWeight: 700,
1737
+ padding: "7px 10px"
1738
+ },
1739
+ type: "button",
1740
+ children: savingStatus === "draft" ? "Saving..." : "Save Draft"
1741
+ }
1742
+ ),
1743
+ /* @__PURE__ */ jsx(
1744
+ "button",
1745
+ {
1746
+ disabled: savingStatus !== null,
1747
+ onClick: () => void saveLayout("published"),
1748
+ style: {
1749
+ background: "#0f7d52",
1750
+ border: "none",
1751
+ borderRadius: 999,
1752
+ color: "#fff",
1753
+ cursor: savingStatus ? "not-allowed" : "pointer",
1754
+ fontSize: 12,
1755
+ fontWeight: 700,
1756
+ padding: "7px 10px"
1757
+ },
1758
+ type: "button",
1759
+ children: savingStatus === "published" ? "Publishing..." : "Publish"
1760
+ }
1761
+ )
1762
+ ] }),
1763
+ saveMessage ? /* @__PURE__ */ jsx("div", { style: { color: "#0f7d52", fontSize: 11, fontWeight: 700, marginTop: 8 }, children: saveMessage }) : null,
1764
+ saveError ? /* @__PURE__ */ jsx("div", { style: { color: "#8d1d1d", fontSize: 11, fontWeight: 700, marginTop: 8 }, children: saveError }) : null
1765
+ ] }),
1607
1766
  /* @__PURE__ */ jsxs("section", { style: sidebarSectionStyle, children: [
1608
1767
  /* @__PURE__ */ jsx("div", { style: { fontSize: 13, fontWeight: 700, marginBottom: 8 }, children: "Add Sections" }),
1609
1768
  /* @__PURE__ */ jsxs(
@@ -1760,6 +1919,38 @@ function BuilderPageEditor({ initialDoc, pageID }) {
1760
1919
  value: parseColor(selectedBlock.backgroundColor, "#124a37")
1761
1920
  }
1762
1921
  )
1922
+ ] }),
1923
+ /* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
1924
+ "Upload Alt Text",
1925
+ /* @__PURE__ */ jsx(
1926
+ "input",
1927
+ {
1928
+ onChange: (event) => setUploadAltText(event.target.value),
1929
+ placeholder: "Describe the image",
1930
+ style: sidebarInputStyle,
1931
+ type: "text",
1932
+ value: uploadAltText
1933
+ }
1934
+ )
1935
+ ] }),
1936
+ /* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
1937
+ "Upload Hero Background Image",
1938
+ /* @__PURE__ */ jsx(
1939
+ "input",
1940
+ {
1941
+ accept: "image/*",
1942
+ disabled: uploadingTarget !== null,
1943
+ onChange: (event) => {
1944
+ const file = event.currentTarget.files?.[0];
1945
+ if (file) {
1946
+ void uploadMediaForSelected("hero", file);
1947
+ }
1948
+ event.currentTarget.value = "";
1949
+ },
1950
+ style: sidebarInputStyle,
1951
+ type: "file"
1952
+ }
1953
+ )
1763
1954
  ] })
1764
1955
  ] }) : null,
1765
1956
  selectedType === "featureGrid" ? /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -1833,20 +2024,54 @@ function BuilderPageEditor({ initialDoc, pageID }) {
1833
2024
  )
1834
2025
  ] })
1835
2026
  ] }) : null,
1836
- selectedType === "media" ? /* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
1837
- "Image Size",
1838
- /* @__PURE__ */ jsxs(
1839
- "select",
1840
- {
1841
- onChange: (event) => updateSelectedField("size", event.target.value),
1842
- style: sidebarInputStyle,
1843
- value: normalizeText(selectedBlock.size, "default"),
1844
- children: [
1845
- /* @__PURE__ */ jsx("option", { value: "default", children: "Default" }),
1846
- /* @__PURE__ */ jsx("option", { value: "wide", children: "Wide" })
1847
- ]
1848
- }
1849
- )
2027
+ selectedType === "media" ? /* @__PURE__ */ jsxs(Fragment, { children: [
2028
+ /* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
2029
+ "Image Size",
2030
+ /* @__PURE__ */ jsxs(
2031
+ "select",
2032
+ {
2033
+ onChange: (event) => updateSelectedField("size", event.target.value),
2034
+ style: sidebarInputStyle,
2035
+ value: normalizeText(selectedBlock.size, "default"),
2036
+ children: [
2037
+ /* @__PURE__ */ jsx("option", { value: "default", children: "Default" }),
2038
+ /* @__PURE__ */ jsx("option", { value: "wide", children: "Wide" })
2039
+ ]
2040
+ }
2041
+ )
2042
+ ] }),
2043
+ /* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
2044
+ "Upload Alt Text",
2045
+ /* @__PURE__ */ jsx(
2046
+ "input",
2047
+ {
2048
+ onChange: (event) => setUploadAltText(event.target.value),
2049
+ placeholder: "Describe the image",
2050
+ style: sidebarInputStyle,
2051
+ type: "text",
2052
+ value: uploadAltText
2053
+ }
2054
+ )
2055
+ ] }),
2056
+ /* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
2057
+ "Upload Section Image",
2058
+ /* @__PURE__ */ jsx(
2059
+ "input",
2060
+ {
2061
+ accept: "image/*",
2062
+ disabled: uploadingTarget !== null,
2063
+ onChange: (event) => {
2064
+ const file = event.currentTarget.files?.[0];
2065
+ if (file) {
2066
+ void uploadMediaForSelected("media", file);
2067
+ }
2068
+ event.currentTarget.value = "";
2069
+ },
2070
+ style: sidebarInputStyle,
2071
+ type: "file"
2072
+ }
2073
+ )
2074
+ ] })
1850
2075
  ] }) : null,
1851
2076
  selectedType === "richText" ? /* @__PURE__ */ jsxs("label", { style: sidebarLabelStyle, children: [
1852
2077
  "Content Width",
@@ -1900,7 +2125,10 @@ function BuilderPageEditor({ initialDoc, pageID }) {
1900
2125
  children: "Add Testimonial"
1901
2126
  }
1902
2127
  ) : null,
1903
- /* @__PURE__ */ jsx("div", { style: { color: "var(--ink-700)", fontSize: 11 }, children: "Click section text directly on the page for copy edits. Use this panel for layout and style options." })
2128
+ /* @__PURE__ */ jsx("div", { style: { color: "var(--ink-700)", fontSize: 11 }, children: "Click section text directly on the page for copy edits. Use this panel for layout and style options." }),
2129
+ uploadingTarget ? /* @__PURE__ */ jsx("div", { style: { color: "var(--ink-700)", fontSize: 11 }, children: "Uploading image..." }) : null,
2130
+ uploadMessage ? /* @__PURE__ */ jsx("div", { style: { color: "#0f7d52", fontSize: 11, fontWeight: 700 }, children: uploadMessage }) : null,
2131
+ uploadError ? /* @__PURE__ */ jsx("div", { style: { color: "#8d1d1d", fontSize: 11, fontWeight: 700 }, children: uploadError }) : null
1904
2132
  ] }) : /* @__PURE__ */ jsx("p", { style: { color: "var(--ink-700)", fontSize: 12, margin: 0 }, children: "Click a section on the page preview to edit its options here." })
1905
2133
  ] })
1906
2134
  ] }) : null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@orion-studios/payload-studio",
3
- "version": "0.4.0-beta.0",
3
+ "version": "0.4.0-beta.1",
4
4
  "description": "Unified Payload CMS toolkit for Orion Studios",
5
5
  "types": "./dist/index.d.ts",
6
6
  "exports": {