camox 0.9.0 → 0.9.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.
@@ -147,7 +147,7 @@ function usePreviewedPage() {
147
147
  $[20] = t9;
148
148
  } else t9 = $[20];
149
149
  const { data: peekedPage } = useQuery(t9);
150
- return peekedPage ?? currentPage;
150
+ return peekedPagePathname ? peekedPage ?? currentPage : currentPage;
151
151
  }
152
152
  function _temp(state) {
153
153
  return state.context.peekedPagePathname;
@@ -22,6 +22,7 @@ import { Globe, Info } from "lucide-react";
22
22
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@camox/ui/select";
23
23
  import { Spinner } from "@camox/ui/spinner";
24
24
  import { useForm } from "@tanstack/react-form";
25
+ import { Alert, AlertDescription, AlertTitle } from "@camox/ui/alert";
25
26
  import { Switch } from "@camox/ui/switch";
26
27
 
27
28
  //#region src/features/preview/components/EditPageSheet.tsx
@@ -149,7 +150,11 @@ const EditPageSheetContent = ({ pageId }) => {
149
150
  },
150
151
  className: "space-y-4",
151
152
  children: [
152
- /* @__PURE__ */ jsx(form.Field, {
153
+ isRootPage ? /* @__PURE__ */ jsxs(Alert, { children: [
154
+ /* @__PURE__ */ jsx(Info, { className: "size-4" }),
155
+ /* @__PURE__ */ jsx(AlertTitle, { children: "Homepage" }),
156
+ /* @__PURE__ */ jsx(AlertDescription, { children: "You can't change the path of the home page." })
157
+ ] }) : /* @__PURE__ */ jsx(form.Field, {
153
158
  name: "parentPageId",
154
159
  children: (parentField) => /* @__PURE__ */ jsx(form.Field, {
155
160
  name: "pathSegment",
@@ -158,7 +163,6 @@ const EditPageSheetContent = ({ pageId }) => {
158
163
  onParentPageIdChange: parentField.handleChange,
159
164
  pathSegment: pathField.state.value,
160
165
  onPathSegmentChange: pathField.handleChange,
161
- disabled: isRootPage,
162
166
  pages,
163
167
  excludePageId: page.id
164
168
  })
@@ -30,6 +30,10 @@ const PagePicker = () => {
30
30
  });
31
31
  const { pathname } = useLocation();
32
32
  const navigate = useNavigate();
33
+ const closePopover = () => {
34
+ previewStore.send({ type: "clearPeekedPage" });
35
+ setOpen(false);
36
+ };
33
37
  const handleDeletePage = async (page) => {
34
38
  const displayName = page.metaTitle ?? formatPathSegment(page.pathSegment);
35
39
  try {
@@ -54,7 +58,10 @@ const PagePicker = () => {
54
58
  return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Popover, {
55
59
  open,
56
60
  onOpenChange: (value) => {
57
- previewStore.send({ type: "clearPeekedPage" });
61
+ if (!value) {
62
+ closePopover();
63
+ return;
64
+ }
58
65
  setOpen(value);
59
66
  },
60
67
  children: [/* @__PURE__ */ jsxs(PopoverTrigger, {
@@ -97,7 +104,7 @@ const PagePicker = () => {
97
104
  hideCheck: true,
98
105
  onSelect: () => {
99
106
  navigate({ to: page_1.fullPath });
100
- setOpen(false);
107
+ closePopover();
101
108
  },
102
109
  children: [/* @__PURE__ */ jsxs("div", {
103
110
  className: "flex min-w-0 flex-1 items-start gap-2",
@@ -122,7 +129,7 @@ const PagePicker = () => {
122
129
  type: "openEditPageSheet",
123
130
  pageId: page_1.id
124
131
  });
125
- setOpen(false);
132
+ closePopover();
126
133
  },
127
134
  onKeyDown: (e_0) => {
128
135
  if (e_0.key === "Enter" || e_0.key === " ") {
@@ -132,7 +139,7 @@ const PagePicker = () => {
132
139
  type: "openEditPageSheet",
133
140
  pageId: page_1.id
134
141
  });
135
- setOpen(false);
142
+ closePopover();
136
143
  }
137
144
  },
138
145
  children: /* @__PURE__ */ jsx(Pencil, { className: "size-4" })
@@ -158,7 +165,7 @@ const PagePicker = () => {
158
165
  /* @__PURE__ */ jsx(CommandSeparator, {}),
159
166
  /* @__PURE__ */ jsx(CommandGroup, { children: /* @__PURE__ */ jsxs(CommandItem, {
160
167
  onSelect: () => {
161
- setOpen(false);
168
+ closePopover();
162
169
  previewStore.send({ type: "openCreatePageSheet" });
163
170
  },
164
171
  value: CREATE_PAGE_VALUE,
@@ -10,7 +10,7 @@ import { useProjectRoom } from "../../lib/use-project-room.js";
10
10
  import { CommandPalette, useCommandPaletteActions } from "./components/CommandPalette.js";
11
11
  import { useAdminShortcuts } from "./useAdminShortcuts.js";
12
12
  import { c } from "react/compiler-runtime";
13
- import { Toaster } from "@camox/ui/toaster";
13
+ import { Toaster, toast } from "@camox/ui/toaster";
14
14
  import { useQuery } from "@tanstack/react-query";
15
15
  import * as React from "react";
16
16
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -62,7 +62,7 @@ const AuthenticatedCamoxProvider = (t0) => {
62
62
  return t4;
63
63
  };
64
64
  const UnauthenticatedCamoxProvider = (t0) => {
65
- const $ = c(7);
65
+ const $ = c(11);
66
66
  const { children } = t0;
67
67
  const signInRedirect = useSignInRedirect();
68
68
  const { authenticationUrl } = useAuthContext();
@@ -90,15 +90,47 @@ const UnauthenticatedCamoxProvider = (t0) => {
90
90
  } else t2 = $[4];
91
91
  React.useEffect(t1, t2);
92
92
  let t3;
93
- if ($[5] !== children) {
94
- t3 = /* @__PURE__ */ jsx(Fragment, { children: /* @__PURE__ */ jsx("div", {
93
+ let t4;
94
+ if ($[5] !== signInRedirect) {
95
+ t3 = () => {
96
+ if (!import.meta.env.PROD) return;
97
+ const toastId = toast("Sign in to open Camox Studio", {
98
+ duration: Infinity,
99
+ action: {
100
+ label: "Sign in",
101
+ onClick: () => signInRedirect()
102
+ }
103
+ });
104
+ return () => void toast.dismiss(toastId);
105
+ };
106
+ t4 = [signInRedirect];
107
+ $[5] = signInRedirect;
108
+ $[6] = t3;
109
+ $[7] = t4;
110
+ } else {
111
+ t3 = $[6];
112
+ t4 = $[7];
113
+ }
114
+ React.useEffect(t3, t4);
115
+ let t5;
116
+ if ($[8] === Symbol.for("react.memo_cache_sentinel")) {
117
+ t5 = /* @__PURE__ */ jsx(Toaster, {
118
+ theme: "light",
119
+ position: "bottom-right",
120
+ offset: { bottom: "1rem" }
121
+ });
122
+ $[8] = t5;
123
+ } else t5 = $[8];
124
+ let t6;
125
+ if ($[9] !== children) {
126
+ t6 = /* @__PURE__ */ jsxs(Fragment, { children: [t5, /* @__PURE__ */ jsx("div", {
95
127
  className: "bg-background min-h-screen",
96
128
  children
97
- }) });
98
- $[5] = children;
99
- $[6] = t3;
100
- } else t3 = $[6];
101
- return t3;
129
+ })] });
130
+ $[9] = children;
131
+ $[10] = t6;
132
+ } else t6 = $[10];
133
+ return t6;
102
134
  };
103
135
  function CamoxProvider({ children, camoxApp, authenticationUrl, apiUrl, projectSlug, environmentName }) {
104
136
  const authClient = React.useMemo(() => createCamoxAuthClient(apiUrl), [apiUrl]);
@@ -4,7 +4,7 @@ import { Popover, PopoverContent, PopoverTrigger } from "@camox/ui/popover";
4
4
  import * as React from "react";
5
5
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
6
  import { Button } from "@camox/ui/button";
7
- import { ChevronsUpDown } from "lucide-react";
7
+ import { ChevronDown } from "lucide-react";
8
8
  import { Badge } from "@camox/ui/badge";
9
9
 
10
10
  //#region src/features/studio/components/EnvironmentMenu.tsx
@@ -36,7 +36,7 @@ const EnvironmentMenu = () => {
36
36
  } else t1 = $[3];
37
37
  let t2;
38
38
  if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
39
- t2 = /* @__PURE__ */ jsx(ChevronsUpDown, { className: "shrink-0 opacity-50" });
39
+ t2 = /* @__PURE__ */ jsx(ChevronDown, { className: "shrink-0 opacity-50" });
40
40
  $[4] = t2;
41
41
  } else t2 = $[4];
42
42
  let t3;
@@ -1,7 +1,6 @@
1
- import { AuthContext, useAuthState } from "../../../lib/auth.js";
1
+ import { useAuthContext, useAuthState } from "../../../lib/auth.js";
2
2
  import { useTheme } from "../useTheme.js";
3
3
  import { c } from "react/compiler-runtime";
4
- import { useContext } from "react";
5
4
  import { jsx, jsxs } from "react/jsx-runtime";
6
5
  import { Button } from "@camox/ui/button";
7
6
  import { LogOut, Monitor, Moon, Settings, Sun, User } from "lucide-react";
@@ -34,7 +33,7 @@ const UserButton = () => {
34
33
  return t0;
35
34
  };
36
35
  function AuthenticatedUserButton({ setTheme }) {
37
- const authCtx = useContext(AuthContext);
36
+ const authCtx = useAuthContext();
38
37
  const { data: session } = authCtx.authClient.useSession();
39
38
  const authenticationUrl = authCtx.authenticationUrl;
40
39
  const userName = session?.user?.name || "User";
@@ -100,7 +99,7 @@ function AuthenticatedUserButton({ setTheme }) {
100
99
  })
101
100
  ] })] }),
102
101
  /* @__PURE__ */ jsxs(DropdownMenuItem, {
103
- onClick: () => authCtx.authClient.signOut(),
102
+ onClick: () => void authCtx.authClient.signOut(),
104
103
  children: [/* @__PURE__ */ jsx(LogOut, { className: "h-4 w-4" }), /* @__PURE__ */ jsx("span", { children: "Sign out" })]
105
104
  })
106
105
  ]
@@ -27,13 +27,14 @@ const ${camelName} = createBlock({
27
27
  title: Type.String({ default: "Title" }),
28
28
  },
29
29
  component: ${pascalName}Component,
30
+ toMarkdown: ['{{title}}']
30
31
  });
31
32
 
32
33
  function ${pascalName}Component() {
33
34
  return (
34
35
  <section>
35
36
  <${camelName}.Field name="title">
36
- {(content) => <h1>{content}</h1>}
37
+ {(props) => <h1 {...props} />}
37
38
  </${camelName}.Field>
38
39
  </section>
39
40
  );
@@ -14,7 +14,11 @@ function throwIfSyncAuthError(error) {
14
14
  async function syncDefinitionsToApi(options) {
15
15
  const { camoxApp, projectSlug, apiUrl, syncSecret, environmentName, logger } = options;
16
16
  const client = createServerApiClient(apiUrl, environmentName);
17
- const definitions = camoxApp.getBlocks().map((block) => ({
17
+ const blocks = camoxApp.getBlocks();
18
+ const layoutDefinitions = camoxApp.getSerializableLayoutDefinitions();
19
+ const typesUsedInLayouts = /* @__PURE__ */ new Set();
20
+ for (const layout of layoutDefinitions) for (const blockDef of layout.blocks) typesUsedInLayouts.add(blockDef.type);
21
+ const definitions = blocks.filter((block) => !block.layoutOnly || typesUsedInLayouts.has(block.id)).map((block) => ({
18
22
  blockId: block.id,
19
23
  title: block.title,
20
24
  description: block.description,
@@ -37,10 +41,10 @@ async function syncDefinitionsToApi(options) {
37
41
  }
38
42
  if (environmentCreated && environmentName) logger.info(`[camox] Created environment "${environmentName}" (forked from production)`, { timestamp: true });
39
43
  logger.info(`[camox] Synced ${definitions.length} block definition${definitions.length === 1 ? "" : "s"}`, { timestamp: true });
40
- const layoutDefinitions = camoxApp.getSerializableLayoutDefinitions();
41
44
  if (layoutDefinitions.length > 0) {
45
+ let layoutSyncResults;
42
46
  try {
43
- await client.layouts.sync({
47
+ layoutSyncResults = await client.layouts.sync({
44
48
  projectSlug,
45
49
  syncSecret,
46
50
  layouts: layoutDefinitions
@@ -50,6 +54,26 @@ async function syncDefinitionsToApi(options) {
50
54
  throw error;
51
55
  }
52
56
  logger.info(`[camox] Synced ${layoutDefinitions.length} layout${layoutDefinitions.length === 1 ? "" : "s"} to Camox API`, { timestamp: true });
57
+ for (const result of layoutSyncResults.layouts) {
58
+ if (result.wasExisting && result.createdBlockTypes.length > 0) {
59
+ const blockList = result.createdBlockTypes.map((t) => `"${t}"`).join(", ");
60
+ logger.info(`[camox] Added ${result.createdBlockTypes.length} block${result.createdBlockTypes.length === 1 ? "" : "s"} to existing layout "${result.layout.layoutId}": ${blockList}`, { timestamp: true });
61
+ }
62
+ if (result.removedBlockTypes.length > 0) {
63
+ const blockList = result.removedBlockTypes.map((t) => `"${t}"`).join(", ");
64
+ logger.info(`[camox] Removed ${result.removedBlockTypes.length} layoutOnly block${result.removedBlockTypes.length === 1 ? "" : "s"} from layout "${result.layout.layoutId}": ${blockList}`, { timestamp: true });
65
+ }
66
+ if (result.skippedOrphanTypes.length > 0) {
67
+ const blockList = result.skippedOrphanTypes.map((t) => `"${t}"`).join(", ");
68
+ logger.info(`[camox] Layout "${result.layout.layoutId}" has ${result.skippedOrphanTypes.length} block${result.skippedOrphanTypes.length === 1 ? "" : "s"} still in DB but removed from code: ${blockList} (kept because not layoutOnly)`, { timestamp: true });
69
+ }
70
+ }
71
+ for (const layoutId of layoutSyncResults.deletedLayoutIds) logger.info(`[camox] Deleted layout "${layoutId}"`, { timestamp: true });
72
+ for (const blocked of layoutSyncResults.blockedLayoutDeletions) logger.warn(`[camox] Cannot delete layout "${blocked.layoutId}": still used by ${blocked.pageCount} page${blocked.pageCount === 1 ? "" : "s"}. Reassign or delete those pages first.`, { timestamp: true });
73
+ if (layoutSyncResults.deletedDefinitionTypes.length > 0) {
74
+ const blockList = layoutSyncResults.deletedDefinitionTypes.map((t) => `"${t}"`).join(", ");
75
+ logger.info(`[camox] Removed ${layoutSyncResults.deletedDefinitionTypes.length} layoutOnly block definition${layoutSyncResults.deletedDefinitionTypes.length === 1 ? "" : "s"} (no layout uses ${layoutSyncResults.deletedDefinitionTypes.length === 1 ? "it" : "them"} anymore): ${blockList}`, { timestamp: true });
76
+ }
53
77
  }
54
78
  const initialPage = camoxApp.getInitialPageBundles();
55
79
  if (initialPage) try {
@@ -99,6 +123,8 @@ async function syncDefinitions(server, options) {
99
123
  const blocksDir = path.resolve(server.config.root, "src/camox/blocks");
100
124
  const client = createServerApiClient(apiUrl, environmentName);
101
125
  async function performInitialSync() {
126
+ const appModule = server.moduleGraph.getModuleById(CAMOX_APP_PATH);
127
+ if (appModule) server.moduleGraph.invalidateModule(appModule);
102
128
  const camoxModule = await ssrLoadModule(server, CAMOX_APP_PATH);
103
129
  if (!camoxModule.camoxApp) {
104
130
  server.config.logger.warn(`[camox] No camoxApp export found in ${CAMOX_APP_PATH}`, { timestamp: true });
@@ -123,6 +149,7 @@ async function syncDefinitions(server, options) {
123
149
  return;
124
150
  }
125
151
  const block = blockModule.block;
152
+ if (block.layoutOnly) return;
126
153
  let result;
127
154
  try {
128
155
  result = await client.blockDefinitions.upsert({
@@ -141,7 +168,7 @@ async function syncDefinitions(server, options) {
141
168
  throwIfSyncAuthError(error);
142
169
  throw error;
143
170
  }
144
- server.config.logger.info(`[camox] ${result.action === "created" ? "Created" : "Updated"} block "${block.id}"`, { timestamp: true });
171
+ server.config.logger.info(`[camox] ${result.action === "created" ? "Created" : "Updated"} block definition "${block.id}"`, { timestamp: true });
145
172
  }
146
173
  async function deleteBlock(filePath) {
147
174
  const blockId = getBlockIdFromFilePath(filePath);
@@ -156,7 +183,7 @@ async function syncDefinitions(server, options) {
156
183
  throwIfSyncAuthError(error);
157
184
  throw error;
158
185
  }
159
- if (result.deleted) server.config.logger.info(`[camox] Deleted block "${blockId}"`, { timestamp: true });
186
+ if (result.deleted) server.config.logger.info(`[camox] Deleted block definition "${blockId}"`, { timestamp: true });
160
187
  }
161
188
  try {
162
189
  await performInitialSync();
@@ -202,6 +229,9 @@ async function syncDefinitions(server, options) {
202
229
  let layoutSyncTimer = null;
203
230
  const handleLayoutFileChange = (filePath) => {
204
231
  if (!isLayoutFile(filePath)) return;
232
+ const relativePath = "./" + path.relative(server.config.root, filePath);
233
+ const moduleNode = server.moduleGraph.getModuleById(relativePath);
234
+ if (moduleNode) server.moduleGraph.invalidateModule(moduleNode);
205
235
  if (layoutSyncTimer) clearTimeout(layoutSyncTimer);
206
236
  layoutSyncTimer = setTimeout(async () => {
207
237
  layoutSyncTimer = null;
@@ -217,6 +247,7 @@ async function syncDefinitions(server, options) {
217
247
  server.watcher.on("add", handleBlockFileUpsert);
218
248
  server.watcher.on("add", handleLayoutFileChange);
219
249
  server.watcher.on("unlink", handleBlockFileDelete);
250
+ server.watcher.on("unlink", handleLayoutFileChange);
220
251
  }
221
252
 
222
253
  //#endregion
package/dist/lib/auth.js CHANGED
@@ -46,10 +46,12 @@ function getCookie(cookie) {
46
46
  try {
47
47
  parsed = JSON.parse(cookie);
48
48
  } catch {}
49
- return Object.entries(parsed).reduce((acc, [key, value]) => {
50
- if (value.expires && new Date(value.expires) < /* @__PURE__ */ new Date()) return acc;
51
- return `${acc}; ${key}=${value.value}`;
52
- }, "");
49
+ const parts = [];
50
+ for (const [key, value] of Object.entries(parsed)) {
51
+ if (value.expires && new Date(value.expires) < /* @__PURE__ */ new Date()) continue;
52
+ parts.push(`${key}=${value.value}`);
53
+ }
54
+ return parts.join("; ");
53
55
  }
54
56
  /**
55
57
  * Read the cross-domain auth cookie from localStorage and return it as a
@@ -20,10 +20,10 @@ function useProjectRoom(apiUrl, projectId) {
20
20
  if ($[1] !== apiUrl) {
21
21
  let t2;
22
22
  if ($[3] === Symbol.for("react.memo_cache_sentinel")) {
23
- t2 = /^https?:\/\//;
23
+ t2 = /\/+$/;
24
24
  $[3] = t2;
25
25
  } else t2 = $[3];
26
- t1 = apiUrl.replace(t2, "");
26
+ t1 = apiUrl.replace(/^https?:\/\//, "").replace(t2, "");
27
27
  $[1] = apiUrl;
28
28
  $[2] = t1;
29
29
  } else t1 = $[2];
@@ -39,18 +39,30 @@ function useProjectRoom(apiUrl, projectId) {
39
39
  prefix: "parties",
40
40
  query: _temp,
41
41
  enabled: t3,
42
- onMessage(event) {
42
+ onOpen() {
43
+ if (process.env.NODE_ENV !== "production") console.debug("[useProjectRoom] WebSocket connected");
44
+ },
45
+ onClose(event) {
46
+ console.warn(`[useProjectRoom] WebSocket closed (code=${event.code}, reason=${event.reason || "none"})`);
47
+ },
48
+ onError(event_0) {
49
+ console.error("[useProjectRoom] WebSocket error:", event_0);
50
+ },
51
+ onMessage(event_1) {
52
+ let data;
43
53
  try {
44
- const data = JSON.parse(event.data);
45
- if (data.type !== "invalidate") return;
46
- pendingRef.current.push(...data.targets);
47
- clearTimeout(timerRef.current);
48
- timerRef.current = setTimeout(() => {
49
- const targets = pendingRef.current;
50
- pendingRef.current = [];
51
- for (const queryKey of targets) queryClient.invalidateQueries({ queryKey });
52
- }, DEBOUNCE_MS);
53
- } catch {}
54
+ data = JSON.parse(event_1.data);
55
+ } catch {
56
+ return;
57
+ }
58
+ if (data.type !== "invalidate") return;
59
+ pendingRef.current.push(...data.targets);
60
+ clearTimeout(timerRef.current);
61
+ timerRef.current = setTimeout(() => {
62
+ const targets = pendingRef.current;
63
+ pendingRef.current = [];
64
+ for (const queryKey of targets) queryClient.invalidateQueries({ queryKey });
65
+ }, DEBOUNCE_MS);
54
66
  }
55
67
  };
56
68
  $[4] = host;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "camox",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "bin": {
5
5
  "camox": "./bin/camox.mjs"
6
6
  },
@@ -97,9 +97,9 @@
97
97
  "react-dom": "^19.2.5",
98
98
  "react-og-preview": "^0.2.0",
99
99
  "shiki": "^4.0.2",
100
- "@camox/api-contract": "0.9.0",
101
- "@camox/ui": "0.9.0",
102
- "@camox/cli": "0.9.0"
100
+ "@camox/api-contract": "0.9.1",
101
+ "@camox/ui": "0.9.1",
102
+ "@camox/cli": "0.9.1"
103
103
  },
104
104
  "devDependencies": {
105
105
  "@babel/core": "^7.29.0",