camox 0.35.0 → 0.36.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.
@@ -8,25 +8,26 @@ import { Button } from "@camox/ui/button";
8
8
  import { Monitor, Smartphone, Tablet, X } from "lucide-react";
9
9
  import { Label } from "@camox/ui/label";
10
10
  import { ButtonGroup } from "@camox/ui/button-group";
11
- import { Switch } from "@camox/ui/switch";
12
11
  import * as Tooltip$1 from "@camox/ui/tooltip";
12
+ import { Switch } from "@camox/ui/switch";
13
13
  import { FloatingToolbar } from "@camox/ui/floating-toolbar";
14
14
  import { Toggle } from "@camox/ui/toggle";
15
15
 
16
16
  //#region src/features/preview/components/PreviewToolbar.tsx
17
17
  const PreviewToolbar = (t0) => {
18
- const $ = c(68);
19
- if ($[0] !== "68e35a20462160afd6c2aeaffaf03edcd93240318bca212e17001e624c1d829c") {
20
- for (let $i = 0; $i < 68; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
21
- $[0] = "68e35a20462160afd6c2aeaffaf03edcd93240318bca212e17001e624c1d829c";
18
+ const $ = c(74);
19
+ if ($[0] !== "88648f3760cc744b4acae6ce6f9f000de00b69d7e7f65268463e98389ba90d90") {
20
+ for (let $i = 0; $i < 74; $i += 1) $[$i] = Symbol.for("react.memo_cache_sentinel");
21
+ $[0] = "88648f3760cc744b4acae6ce6f9f000de00b69d7e7f65268463e98389ba90d90";
22
22
  }
23
- const { onEditModeChange } = t0;
23
+ const { onEditModeChange, pageStatus, hasLiveVersion } = t0;
24
24
  const isEditMode = useSelector(previewStore, _temp);
25
25
  const isToolbarHidden = useSelector(previewStore, _temp2);
26
- const actions = useSelector(actionsStore, _temp3);
27
- const viewportMode = useSelector(previewStore, _temp4);
26
+ const peekedBlock = useSelector(previewStore, _temp3);
27
+ const actions = useSelector(actionsStore, _temp4);
28
+ const viewportMode = useSelector(previewStore, _temp5);
28
29
  const editModeActionId = isEditMode ? "exit-edit-mode" : "enter-edit-mode";
29
- if (isToolbarHidden) return null;
30
+ if (isToolbarHidden || peekedBlock) return null;
30
31
  let t1;
31
32
  if ($[1] !== onEditModeChange) {
32
33
  t1 = (checked) => {
@@ -70,7 +71,7 @@ const PreviewToolbar = (t0) => {
70
71
  let t5;
71
72
  if ($[11] !== t2 || $[12] !== t4) {
72
73
  t5 = /* @__PURE__ */ jsxs("div", {
73
- className: "flex items-center gap-2 px-2",
74
+ className: "flex shrink-0 items-center gap-2 px-2",
74
75
  children: [t2, t4]
75
76
  });
76
77
  $[11] = t2;
@@ -246,65 +247,88 @@ const PreviewToolbar = (t0) => {
246
247
  $[53] = t30;
247
248
  } else t30 = $[53];
248
249
  let t31;
249
- if ($[54] !== isEditMode) {
250
- t31 = /* @__PURE__ */ jsx(Button, {
250
+ if ($[54] !== hasLiveVersion || $[55] !== pageStatus) {
251
+ t31 = pageStatus && /* @__PURE__ */ jsx(Button, {
252
+ type: "button",
253
+ variant: "outline",
254
+ disabled: pageStatus === "published" || !hasLiveVersion,
255
+ onClick: _temp6,
256
+ children: "View live site"
257
+ });
258
+ $[54] = hasLiveVersion;
259
+ $[55] = pageStatus;
260
+ $[56] = t31;
261
+ } else t31 = $[56];
262
+ let t32;
263
+ if ($[57] !== isEditMode) {
264
+ t32 = /* @__PURE__ */ jsx(Button, {
251
265
  type: "button",
252
266
  variant: "ghost",
253
267
  size: "icon",
254
268
  disabled: isEditMode,
255
- onClick: _temp5,
269
+ onClick: _temp7,
256
270
  "aria-label": "Hide toolbar",
257
271
  className: "text-muted-foreground"
258
272
  });
259
- $[54] = isEditMode;
260
- $[55] = t31;
261
- } else t31 = $[55];
262
- let t32;
263
- if ($[56] === Symbol.for("react.memo_cache_sentinel")) {
264
- t32 = /* @__PURE__ */ jsx(X, {});
265
- $[56] = t32;
266
- } else t32 = $[56];
273
+ $[57] = isEditMode;
274
+ $[58] = t32;
275
+ } else t32 = $[58];
267
276
  let t33;
268
- if ($[57] !== t31) {
269
- t33 = /* @__PURE__ */ jsx(Tooltip$1.TooltipTrigger, {
270
- render: t31,
271
- children: t32
272
- });
273
- $[57] = t31;
274
- $[58] = t33;
275
- } else t33 = $[58];
276
- let t34;
277
277
  if ($[59] === Symbol.for("react.memo_cache_sentinel")) {
278
- t34 = /* @__PURE__ */ jsx(Tooltip$1.TooltipContent, { children: "Hide toolbar" });
279
- $[59] = t34;
280
- } else t34 = $[59];
278
+ t33 = /* @__PURE__ */ jsx(X, {});
279
+ $[59] = t33;
280
+ } else t33 = $[59];
281
+ let t34;
282
+ if ($[60] !== t32) {
283
+ t34 = /* @__PURE__ */ jsx(Tooltip$1.TooltipTrigger, {
284
+ render: t32,
285
+ children: t33
286
+ });
287
+ $[60] = t32;
288
+ $[61] = t34;
289
+ } else t34 = $[61];
281
290
  let t35;
282
- if ($[60] !== t33) {
283
- t35 = /* @__PURE__ */ jsxs(Tooltip$1.Tooltip, { children: [t33, t34] });
284
- $[60] = t33;
285
- $[61] = t35;
286
- } else t35 = $[61];
291
+ if ($[62] === Symbol.for("react.memo_cache_sentinel")) {
292
+ t35 = /* @__PURE__ */ jsx(Tooltip$1.TooltipContent, { children: "Hide toolbar" });
293
+ $[62] = t35;
294
+ } else t35 = $[62];
287
295
  let t36;
288
- if ($[62] !== t30 || $[63] !== t35) {
289
- t36 = /* @__PURE__ */ jsxs("div", {
290
- className: "flex items-center gap-2",
291
- children: [t30, t35]
292
- });
293
- $[62] = t30;
294
- $[63] = t35;
296
+ if ($[63] !== t34) {
297
+ t36 = /* @__PURE__ */ jsxs(Tooltip$1.Tooltip, { children: [t34, t35] });
298
+ $[63] = t34;
295
299
  $[64] = t36;
296
300
  } else t36 = $[64];
297
301
  let t37;
298
- if ($[65] !== t36 || $[66] !== t5) {
299
- t37 = /* @__PURE__ */ jsxs(FloatingToolbar, {
300
- className: "bottom-2 justify-between gap-8 transition-none",
301
- children: [t5, t36]
302
+ if ($[65] !== t31 || $[66] !== t36) {
303
+ t37 = /* @__PURE__ */ jsxs("div", {
304
+ className: "flex items-center gap-2",
305
+ children: [t31, t36]
302
306
  });
303
- $[65] = t36;
304
- $[66] = t5;
307
+ $[65] = t31;
308
+ $[66] = t36;
305
309
  $[67] = t37;
306
310
  } else t37 = $[67];
307
- return t37;
311
+ let t38;
312
+ if ($[68] !== t30 || $[69] !== t37) {
313
+ t38 = /* @__PURE__ */ jsxs("div", {
314
+ className: "flex shrink-0 items-center gap-8 self-stretch",
315
+ children: [t30, t37]
316
+ });
317
+ $[68] = t30;
318
+ $[69] = t37;
319
+ $[70] = t38;
320
+ } else t38 = $[70];
321
+ let t39;
322
+ if ($[71] !== t38 || $[72] !== t5) {
323
+ t39 = /* @__PURE__ */ jsxs(FloatingToolbar, {
324
+ className: "bottom-2 w-max justify-between gap-8 transition-none",
325
+ children: [t5, t38]
326
+ });
327
+ $[71] = t38;
328
+ $[72] = t5;
329
+ $[73] = t39;
330
+ } else t39 = $[73];
331
+ return t39;
308
332
  };
309
333
  function _temp(state) {
310
334
  return state.context.isEditMode;
@@ -313,12 +337,18 @@ function _temp2(state_0) {
313
337
  return state_0.context.isToolbarHidden;
314
338
  }
315
339
  function _temp3(state_1) {
316
- return state_1.context.actions;
340
+ return state_1.context.peekedBlock;
317
341
  }
318
342
  function _temp4(state_2) {
319
- return state_2.context.viewportMode;
343
+ return state_2.context.actions;
344
+ }
345
+ function _temp5(state_3) {
346
+ return state_3.context.viewportMode;
347
+ }
348
+ function _temp6() {
349
+ return previewStore.send({ type: "viewLivePage" });
320
350
  }
321
- function _temp5() {
351
+ function _temp7() {
322
352
  return previewStore.send({ type: "hideToolbar" });
323
353
  }
324
354
 
@@ -26,7 +26,6 @@ const previewStore = createStore({
26
26
  context: {
27
27
  isEditMode: false,
28
28
  isToolbarHidden: false,
29
- isSidebarOpen: true,
30
29
  isPageEditorSidebarOpen: false,
31
30
  isAddBlockSidebarOpen: false,
32
31
  addBlockSource: null,
@@ -64,17 +63,25 @@ const previewStore = createStore({
64
63
  return {
65
64
  ...context,
66
65
  isEditMode: true,
67
- isSidebarOpen: true
66
+ isToolbarHidden: false
68
67
  };
69
68
  },
70
69
  hideToolbar: (context) => ({
71
70
  ...context,
72
71
  isToolbarHidden: true
73
72
  }),
74
- toggleSidebar: (context) => ({
75
- ...context,
76
- isSidebarOpen: true
77
- }),
73
+ viewLivePage: (context, _, enqueue) => {
74
+ enqueue.effect(() => {
75
+ if (context.isEditMode) trackClientEvent("edit_mode_toggled", { enabled: false });
76
+ toast("Viewing live version of the site");
77
+ });
78
+ return {
79
+ ...context,
80
+ isEditMode: false,
81
+ isToolbarHidden: true,
82
+ previewSource: "live"
83
+ };
84
+ },
78
85
  setViewportMode: (context, event) => {
79
86
  if (context.viewportMode === event.mode) return context;
80
87
  return {
@@ -1,6 +1,244 @@
1
- import "../../lib/telemetry.js";
1
+ import { trackEvent } from "../../lib/telemetry.js";
2
2
  import { queryKeys } from "@camox/api-contract/query-keys";
3
3
  import { createORPCClient } from "@orpc/client";
4
4
  import { RPCLink } from "@orpc/client/fetch";
5
5
 
6
- export { };
6
+ //#region src/features/routes/pageRuntime.ts
7
+ function parseQuality(part) {
8
+ const match = part.match(/;\s*q=([0-9.]+)/);
9
+ return match ? parseFloat(match[1]) : 1;
10
+ }
11
+ function prefersMarkdown(accept) {
12
+ let markdownQ = -1;
13
+ let htmlQ = -1;
14
+ for (const part of accept.split(",")) {
15
+ const trimmed = part.trim();
16
+ if (trimmed.startsWith("text/markdown")) markdownQ = parseQuality(trimmed);
17
+ else if (trimmed.startsWith("text/html")) htmlQ = parseQuality(trimmed);
18
+ }
19
+ return markdownQ > 0 && markdownQ >= htmlQ;
20
+ }
21
+ function createServerApiClient(apiUrl, environmentName, options) {
22
+ const headers = {};
23
+ if (environmentName) headers["x-environment-name"] = environmentName;
24
+ return createORPCClient(new RPCLink({
25
+ url: `${apiUrl}/rpc`,
26
+ headers,
27
+ fetch: (request, init) => {
28
+ if (options?.authCookieHeader && request instanceof Request) request.headers.set("Better-Auth-Cookie", options.authCookieHeader);
29
+ return fetch(request, init);
30
+ }
31
+ }));
32
+ }
33
+ function getErrorDetails(error) {
34
+ if (!error || typeof error !== "object") return { message: String(error) };
35
+ const errorRecord = error;
36
+ return {
37
+ code: typeof errorRecord.code === "string" ? errorRecord.code : void 0,
38
+ status: typeof errorRecord.status === "number" ? errorRecord.status : void 0,
39
+ message: error instanceof Error ? error.message : typeof errorRecord.message === "string" ? errorRecord.message : ""
40
+ };
41
+ }
42
+ function isAuthSessionError(error) {
43
+ const { code, status, message } = getErrorDetails(error);
44
+ if (code === "UNAUTHORIZED" || code === "FORBIDDEN") return true;
45
+ if (status === 401 || status === 403) return true;
46
+ const lowerMessage = message.toLowerCase();
47
+ return lowerMessage.includes("unauthorized") || lowerMessage.includes("forbidden") || lowerMessage.includes("invalid session") || lowerMessage.includes("session expired") || lowerMessage.includes("expired session");
48
+ }
49
+ function isNotFoundError(error) {
50
+ const { code, status } = getErrorDetails(error);
51
+ return code === "NOT_FOUND" || status === 404;
52
+ }
53
+ function visitForFileIds(value, ids) {
54
+ if (value == null || typeof value !== "object") return;
55
+ if ("_fileId" in value && typeof value._fileId === "number") {
56
+ ids.add(value._fileId);
57
+ return;
58
+ }
59
+ if (Array.isArray(value)) {
60
+ for (const item of value) visitForFileIds(item, ids);
61
+ return;
62
+ }
63
+ for (const child of Object.values(value)) visitForFileIds(child, ids);
64
+ }
65
+ function collectFileIdsFromContent(content, ids) {
66
+ for (const value of Object.values(content)) visitForFileIds(value, ids);
67
+ }
68
+ function seedBlockCaches(queryClient, pageData, source = "draft") {
69
+ const filesById = new Map(pageData.files.map((f) => [f.id, f]));
70
+ for (const block of pageData.blocks) {
71
+ const blockItems = pageData.repeatableItems.filter((i) => i.blockId === block.id);
72
+ const fileIds = /* @__PURE__ */ new Set();
73
+ collectFileIdsFromContent(block.content, fileIds);
74
+ for (const item of blockItems) collectFileIdsFromContent(item.content, fileIds);
75
+ const blockFiles = [...fileIds].map((id) => filesById.get(id)).filter((f) => f != null);
76
+ queryClient.setQueryData(queryKeys.blocks.get(block.id, source), {
77
+ block,
78
+ repeatableItems: blockItems,
79
+ files: blockFiles
80
+ });
81
+ for (const file of blockFiles) queryClient.setQueryData(queryKeys.files.get(file.id), file);
82
+ }
83
+ }
84
+ async function loadCamoxPage({ apiUrl, authCookieHeader, environmentName, pathname, projectSlug, queryClient, source }) {
85
+ const serverApi = createServerApiClient(apiUrl, environmentName, { authCookieHeader });
86
+ return queryClient.ensureQueryData({
87
+ queryKey: queryKeys.pages.getByPath(pathname, source),
88
+ queryFn: async () => {
89
+ const [data, pagesList] = await Promise.all([serverApi.pages.getByPath({
90
+ path: pathname,
91
+ projectSlug,
92
+ source
93
+ }), serverApi.pages.listBySlug({ projectSlug }).catch(() => null)]);
94
+ seedBlockCaches(queryClient, data, source);
95
+ queryClient.setQueryData(queryKeys.pages.getByPath(pathname, source), {
96
+ page: data.page,
97
+ layout: data.layout,
98
+ projectName: data.projectName,
99
+ project: data.project
100
+ });
101
+ if (source === "live") {
102
+ seedBlockCaches(queryClient, data, "draft");
103
+ queryClient.setQueryData(queryKeys.pages.getByPath(pathname, "draft"), {
104
+ page: data.page,
105
+ layout: data.layout,
106
+ projectName: data.projectName,
107
+ project: data.project
108
+ });
109
+ }
110
+ if (pagesList) queryClient.setQueryData(queryKeys.pages.list, pagesList);
111
+ return {
112
+ page: data.page,
113
+ layout: data.layout,
114
+ projectName: data.projectName,
115
+ project: data.project
116
+ };
117
+ },
118
+ staleTime: Infinity
119
+ });
120
+ }
121
+ async function loadLivePage(options) {
122
+ return loadCamoxPage({
123
+ ...options,
124
+ source: "live"
125
+ });
126
+ }
127
+ function buildCamoxPageLoaderData(apiUrl, page, origin) {
128
+ return {
129
+ page,
130
+ origin,
131
+ faviconUrl: `${apiUrl}/favicons/${page.project.id}?v=${page.project.updatedAt}`
132
+ };
133
+ }
134
+ async function loadCamoxPageForRequest({ apiUrl, authCookieHeader, environmentName, origin, pathname, projectSlug, queryClient }) {
135
+ const loadOptions = {
136
+ apiUrl,
137
+ environmentName,
138
+ pathname,
139
+ projectSlug,
140
+ queryClient
141
+ };
142
+ if (!authCookieHeader) return {
143
+ data: buildCamoxPageLoaderData(apiUrl, await loadLivePage(loadOptions), origin),
144
+ shouldClearAuthCookie: false,
145
+ source: "live"
146
+ };
147
+ try {
148
+ return {
149
+ data: buildCamoxPageLoaderData(apiUrl, await loadCamoxPage({
150
+ ...loadOptions,
151
+ authCookieHeader,
152
+ source: "draft"
153
+ }), origin),
154
+ shouldClearAuthCookie: false,
155
+ source: "draft"
156
+ };
157
+ } catch (error) {
158
+ if (!isAuthSessionError(error)) throw error;
159
+ console.warn("[camox] Ignoring stale camox_auth_cookie and retrying published page load.");
160
+ return {
161
+ data: buildCamoxPageLoaderData(apiUrl, await loadLivePage(loadOptions), origin),
162
+ shouldClearAuthCookie: true,
163
+ source: "live"
164
+ };
165
+ }
166
+ }
167
+ async function createMarkdownResponse({ apiUrl, environmentName, pathname, projectSlug, request }) {
168
+ if (!prefersMarkdown(request.headers.get("Accept") ?? "")) return null;
169
+ const api = createServerApiClient(apiUrl, environmentName);
170
+ const requestPathname = pathname ?? new URL(request.url).pathname;
171
+ try {
172
+ const page = await api.pages.getByPath({
173
+ path: requestPathname,
174
+ projectSlug,
175
+ source: "live"
176
+ });
177
+ const { markdown } = await api.blocks.getPageMarkdown({ pageId: page.page.id });
178
+ if (!markdown) return null;
179
+ trackEvent("markdown_served", {
180
+ pathname: requestPathname,
181
+ projectId: page.page.projectId,
182
+ projectName: page.projectName
183
+ });
184
+ return new Response(markdown, { headers: { "Content-Type": "text/markdown; charset=utf-8" } });
185
+ } catch {
186
+ return null;
187
+ }
188
+ }
189
+ function buildCamoxPageHead(camoxApp, loaderData) {
190
+ if (!loaderData) return {};
191
+ const { page, origin, faviconUrl } = loaderData;
192
+ const pageMetaTitle = page.page.metaTitle ?? page.page.pathSegment;
193
+ const meta = [];
194
+ let title = pageMetaTitle;
195
+ if (page.layout) {
196
+ const layout = camoxApp.getLayoutById(page.layout.layoutId);
197
+ if (layout) {
198
+ title = layout._internal.buildMetaTitle({
199
+ pageMetaTitle,
200
+ projectName: page.projectName,
201
+ pageFullPath: page.page.fullPath
202
+ });
203
+ meta.push({ title });
204
+ }
205
+ }
206
+ if (page.page.metaDescription) meta.push({
207
+ name: "description",
208
+ content: page.page.metaDescription
209
+ });
210
+ const ogImageParams = new URLSearchParams({
211
+ ...page.layout && { layoutId: page.layout.layoutId },
212
+ title: pageMetaTitle,
213
+ ...page.page.metaDescription && { description: page.page.metaDescription },
214
+ ...page.projectName && { projectName: page.projectName }
215
+ });
216
+ const ogImageUrl = page.page.customOgImageUrl ?? `${origin}/og?${ogImageParams.toString()}`;
217
+ meta.push({
218
+ property: "og:title",
219
+ content: title
220
+ }, {
221
+ property: "og:image",
222
+ content: ogImageUrl
223
+ }, {
224
+ property: "og:image:width",
225
+ content: "1200"
226
+ }, {
227
+ property: "og:image:height",
228
+ content: "630"
229
+ });
230
+ if (page.page.metaDescription) meta.push({
231
+ property: "og:description",
232
+ content: page.page.metaDescription
233
+ });
234
+ return {
235
+ meta,
236
+ links: [{
237
+ rel: "icon",
238
+ href: faviconUrl
239
+ }]
240
+ };
241
+ }
242
+
243
+ //#endregion
244
+ export { buildCamoxPageHead, createMarkdownResponse, createServerApiClient, isNotFoundError, loadCamoxPageForRequest };
@@ -1,6 +1,14 @@
1
+ import { CamoxApp } from "../../core/createApp.js";
2
+ import { CamoxDocument } from "../../core/defineDocument.js";
3
+ import { StudioRenderInput } from "./studioApp.js";
1
4
  import { UseHeadInput } from "unhead/types";
2
5
 
3
6
  //#region src/features/runtime/runtime.d.ts
7
+ type RuntimeRouteKind = "data" | "health" | "og" | "page" | "registry" | "sitemap" | "studio" | "studio-content" | "studio-nested";
8
+ interface RuntimeRouteMatch {
9
+ kind: RuntimeRouteKind;
10
+ pathname: string;
11
+ }
4
12
  interface LayoutIdentity {
5
13
  blockIds: number[];
6
14
  id: number;
@@ -21,5 +29,26 @@ interface PageRenderInput {
21
29
  runtimeBasePath: string;
22
30
  source: "live" | "draft";
23
31
  }
32
+ interface RuntimeOptions {
33
+ apiUrl?: string;
34
+ authenticationUrl?: string;
35
+ environmentName?: string;
36
+ pageClientEntryUrl?: string;
37
+ studioClientEntryUrl?: string;
38
+ getCamoxApp?: () => Promise<CamoxApp>;
39
+ getDocument?: () => Promise<CamoxDocument>;
40
+ projectSlug?: string;
41
+ renderPage?: (input: PageRenderInput) => Promise<string>;
42
+ renderStudio?: (input: StudioRenderInput) => Promise<string>;
43
+ runtimeBasePath?: string;
44
+ stylesheetUrl?: string;
45
+ }
46
+ declare function getRuntimePathname(url: string, runtimeBasePath?: string): string | null;
47
+ declare function matchRuntimeRoute(pathname: string): RuntimeRouteMatch;
48
+ declare function createPageHeadInput(head: {
49
+ meta?: Array<Record<string, string>>;
50
+ links?: Array<Record<string, string>>;
51
+ }): UseHeadInput;
52
+ declare function handleCamoxRequest(request: Request, options?: RuntimeOptions): Promise<Response | null>;
24
53
  //#endregion
25
- export { PageRenderInput };
54
+ export { LayoutIdentity, PageRenderInput, RuntimeOptions, RuntimeRouteKind, RuntimeRouteMatch, createPageHeadInput, getRuntimePathname, handleCamoxRequest, matchRuntimeRoute };