camox 0.47.0 → 0.48.0

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 (75) hide show
  1. package/dist/core/createApp.d.ts +19 -6
  2. package/dist/core/createApp.js +7 -2
  3. package/dist/core/createBlock.js +54 -43
  4. package/dist/core/createCollection.d.ts +25 -7
  5. package/dist/core/createCollection.js +28 -9
  6. package/dist/core/createLayout.d.ts +98 -71
  7. package/dist/core/createLayout.js +10 -16
  8. package/dist/core/derivedRoutes.js +1 -1
  9. package/dist/core/editing/BlockEditingRuntime.d.ts +3 -2
  10. package/dist/core/editing/CompleteBlockEditingRuntime.js +2 -1
  11. package/dist/core/editing/createEditableBlock.d.ts +59 -19
  12. package/dist/core/editing/createEditableBlock.js +162 -151
  13. package/dist/core/editing/useLatestRecord.js +7 -7
  14. package/dist/core/layoutContext.d.ts +54 -0
  15. package/dist/core/layoutContext.js +66 -0
  16. package/dist/core/lib/contentType.d.ts +91 -4
  17. package/dist/core/lib/contentType.js +84 -7
  18. package/dist/core/lib/recordScope.js +19 -17
  19. package/dist/core/lib/reference.d.ts +6 -2
  20. package/dist/core/recordItem.js +55 -2
  21. package/dist/core/requestContext.d.ts +18 -0
  22. package/dist/core/structuredData.d.ts +126 -0
  23. package/dist/core/structuredData.js +40 -0
  24. package/dist/features/canvas/canvasCamera.js +1 -1
  25. package/dist/features/canvas/sidebar/CanvasSidebarFrame.js +1 -1
  26. package/dist/features/content/ContentCollection.js +13 -7
  27. package/dist/features/content/collection-form.js +6 -2
  28. package/dist/features/content/components/MissingFromLocale.js +1 -1
  29. package/dist/features/og/pageImage.js +89 -0
  30. package/dist/features/page/CuratedPageContent.js +45 -32
  31. package/dist/features/page/DerivedPageContent.js +49 -22
  32. package/dist/features/page/SharedLayoutContent.js +26 -22
  33. package/dist/features/preview/CamoxPreview.js +40 -26
  34. package/dist/features/preview/components/AddBlockSheet.js +1 -0
  35. package/dist/features/preview/components/BlockActionsPopover.js +8 -6
  36. package/dist/features/preview/components/PageInfo.js +244 -264
  37. package/dist/features/preview/components/PageTree.js +63 -53
  38. package/dist/features/preview/components/RecordCard.js +50 -34
  39. package/dist/features/preview/components/RecordCombobox.js +12 -5
  40. package/dist/features/preview/components/ReferenceFieldEditor.js +4 -2
  41. package/dist/features/preview/components/ReferenceListFieldEditor.js +24 -19
  42. package/dist/features/preview/components/RepeatableItemsList.js +62 -46
  43. package/dist/features/preview/components/SelectionEditor.js +3 -2
  44. package/dist/features/preview/components/useFieldWriter.js +2 -4
  45. package/dist/features/preview/components/useRecordView.js +2 -2
  46. package/dist/features/preview/entityName.js +11 -0
  47. package/dist/features/preview/pageRecord.js +17 -18
  48. package/dist/features/routes/pageRuntime.d.ts +15 -1
  49. package/dist/features/routes/pageRuntime.js +35 -38
  50. package/dist/features/runtime/RuntimeChrome.js +1 -1
  51. package/dist/features/runtime/breadcrumbs.js +68 -0
  52. package/dist/features/runtime/livePages.js +3 -2
  53. package/dist/features/runtime/pageGraph.js +92 -0
  54. package/dist/features/runtime/projectLogo.js +110 -0
  55. package/dist/features/runtime/runtime.d.ts +11 -6
  56. package/dist/features/runtime/runtime.js +278 -55
  57. package/dist/features/runtime/runtimeRoutes.d.ts +1 -0
  58. package/dist/features/runtime/runtimeRoutes.js +1 -0
  59. package/dist/features/runtime/structuredData.js +213 -0
  60. package/dist/features/studio/components/ChatSidebar.js +5 -5
  61. package/dist/features/studio/components/ProjectMenu.js +127 -100
  62. package/dist/features/studio/components/SocialProfilesDialog.js +342 -0
  63. package/dist/features/studio/components/ThreadMessages.js +2 -2
  64. package/dist/features/vite/blockBoilerplate.js +3 -2
  65. package/dist/features/vite/layoutGeneration.js +3 -2
  66. package/dist/features/vite/vite.js +1 -0
  67. package/dist/lib/queries.d.ts +4 -0
  68. package/dist/lib/queries.js +1 -0
  69. package/dist/lib/studio-reads.js +3 -1
  70. package/dist/package.js +1 -1
  71. package/dist/studio.css +1 -1
  72. package/package.json +5 -4
  73. package/skills/camox/references/block-definitions.md +105 -25
  74. package/skills/camox/references/layout-definitions.md +53 -17
  75. package/skills/camox/references/routed-layouts.md +17 -14
@@ -0,0 +1,342 @@
1
+ import { reads, useStudioCache } from "../../../lib/studio-reads.js";
2
+ import { projectMutations } from "../../../lib/queries.js";
3
+ import { c } from "react/compiler-runtime";
4
+ import * as React from "react";
5
+ import { jsx, jsxs } from "react/jsx-runtime";
6
+ import { useMutation } from "@tanstack/react-query";
7
+ import { Button } from "@camox/ui/button";
8
+ import { Input } from "@camox/ui/input";
9
+ import { ArrowDown, ArrowUp, Plus, Trash2, TriangleAlert } from "lucide-react";
10
+ import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@camox/ui/dialog";
11
+ import { toast } from "@camox/ui/toaster";
12
+ import { Spinner } from "@camox/ui/spinner";
13
+ import { Alert, AlertDescription } from "@camox/ui/alert";
14
+ import { socialProfileUrlSchema } from "@camox/api-contract/projects";
15
+
16
+ //#region src/features/studio/components/SocialProfilesDialog.tsx
17
+ let nextRowKey = 0;
18
+ const row = (url) => ({
19
+ key: nextRowKey++,
20
+ url
21
+ });
22
+ function SocialProfilesDialog(t0) {
23
+ const $ = c(10);
24
+ const { project, open, onOpenChange } = t0;
25
+ let t1;
26
+ if ($[0] !== onOpenChange || $[1] !== open || $[2] !== project) {
27
+ t1 = open && /* @__PURE__ */ jsx(SocialProfilesForm, {
28
+ project,
29
+ onSaved: () => onOpenChange(false)
30
+ });
31
+ $[0] = onOpenChange;
32
+ $[1] = open;
33
+ $[2] = project;
34
+ $[3] = t1;
35
+ } else t1 = $[3];
36
+ let t2;
37
+ if ($[4] !== t1) {
38
+ t2 = /* @__PURE__ */ jsx(DialogContent, {
39
+ className: "gap-5 sm:max-w-lg",
40
+ children: t1
41
+ });
42
+ $[4] = t1;
43
+ $[5] = t2;
44
+ } else t2 = $[5];
45
+ let t3;
46
+ if ($[6] !== onOpenChange || $[7] !== open || $[8] !== t2) {
47
+ t3 = /* @__PURE__ */ jsx(Dialog, {
48
+ open,
49
+ onOpenChange,
50
+ children: t2
51
+ });
52
+ $[6] = onOpenChange;
53
+ $[7] = open;
54
+ $[8] = t2;
55
+ $[9] = t3;
56
+ } else t3 = $[9];
57
+ return t3;
58
+ }
59
+ function SocialProfilesForm(t0) {
60
+ const $ = c(47);
61
+ const { project, onSaved } = t0;
62
+ const studioCache = useStudioCache();
63
+ let t1;
64
+ if ($[0] !== project) {
65
+ t1 = () => project.socialProfiles.map(row);
66
+ $[0] = project;
67
+ $[1] = t1;
68
+ } else t1 = $[1];
69
+ const [rows, setRows] = React.useState(t1);
70
+ const [showErrors, setShowErrors] = React.useState(false);
71
+ let t2;
72
+ if ($[2] === Symbol.for("react.memo_cache_sentinel")) {
73
+ t2 = projectMutations.updateSocialProfiles();
74
+ $[2] = t2;
75
+ } else t2 = $[2];
76
+ let t3;
77
+ if ($[3] !== onSaved || $[4] !== project || $[5] !== studioCache) {
78
+ t3 = {
79
+ ...t2,
80
+ onSuccess: async () => {
81
+ await studioCache.invalidate(reads.project({ projectUuid: project.uuid }));
82
+ toast.success("Social profiles updated");
83
+ onSaved();
84
+ },
85
+ onError: _temp
86
+ };
87
+ $[3] = onSaved;
88
+ $[4] = project;
89
+ $[5] = studioCache;
90
+ $[6] = t3;
91
+ } else t3 = $[6];
92
+ const save = useMutation(t3);
93
+ let t4;
94
+ if ($[7] !== rows) {
95
+ t4 = rows.map(_temp2).filter(_temp3);
96
+ $[7] = rows;
97
+ $[8] = t4;
98
+ } else t4 = $[8];
99
+ const filled = t4;
100
+ const errorOf = _temp4;
101
+ let t5;
102
+ if ($[9] === Symbol.for("react.memo_cache_sentinel")) {
103
+ t5 = (key, url_0) => setRows((current) => current.map((entry_0) => entry_0.key === key ? {
104
+ ...entry_0,
105
+ url: url_0
106
+ } : entry_0));
107
+ $[9] = t5;
108
+ } else t5 = $[9];
109
+ const update = t5;
110
+ let t6;
111
+ if ($[10] === Symbol.for("react.memo_cache_sentinel")) {
112
+ t6 = (key_0) => setRows((current_0) => current_0.filter((entry_1) => entry_1.key !== key_0));
113
+ $[10] = t6;
114
+ } else t6 = $[10];
115
+ const remove = t6;
116
+ let t7;
117
+ if ($[11] === Symbol.for("react.memo_cache_sentinel")) {
118
+ t7 = (index, by) => setRows((current_1) => {
119
+ const next = [...current_1];
120
+ const [moved] = next.splice(index, 1);
121
+ next.splice(index + by, 0, moved);
122
+ return next;
123
+ });
124
+ $[11] = t7;
125
+ } else t7 = $[11];
126
+ const move = t7;
127
+ let t8;
128
+ if ($[12] !== filled || $[13] !== project || $[14] !== save) {
129
+ t8 = (event) => {
130
+ event.preventDefault();
131
+ if (filled.some((entry_2) => errorOf(entry_2.url))) {
132
+ setShowErrors(true);
133
+ return;
134
+ }
135
+ save.mutate({
136
+ id: project.id,
137
+ socialProfiles: filled.map(_temp5)
138
+ });
139
+ };
140
+ $[12] = filled;
141
+ $[13] = project;
142
+ $[14] = save;
143
+ $[15] = t8;
144
+ } else t8 = $[15];
145
+ const submit = t8;
146
+ let t9;
147
+ if ($[16] === Symbol.for("react.memo_cache_sentinel")) {
148
+ t9 = /* @__PURE__ */ jsxs(DialogHeader, { children: [/* @__PURE__ */ jsx(DialogTitle, { children: "Social profiles" }), /* @__PURE__ */ jsx(DialogDescription, { children: "The project's accounts elsewhere, such as on LinkedIn, X or GitHub. Search engines and AI answer engines use them to tie the website to your organization." })] });
149
+ $[16] = t9;
150
+ } else t9 = $[16];
151
+ let t10;
152
+ if ($[17] === Symbol.for("react.memo_cache_sentinel")) {
153
+ t10 = /* @__PURE__ */ jsxs(Alert, {
154
+ variant: "warning",
155
+ children: [/* @__PURE__ */ jsx(TriangleAlert, {}), /* @__PURE__ */ jsx(AlertDescription, { children: "Applies to the whole site, including Live and every draft." })]
156
+ });
157
+ $[17] = t10;
158
+ } else t10 = $[17];
159
+ let t11;
160
+ if ($[18] !== rows || $[19] !== save.isPending || $[20] !== showErrors) {
161
+ let t12;
162
+ if ($[22] !== rows.length || $[23] !== save.isPending || $[24] !== showErrors) {
163
+ t12 = (entry_4, index_0) => {
164
+ const error_0 = showErrors && entry_4.url.trim() ? errorOf(entry_4.url) : void 0;
165
+ return /* @__PURE__ */ jsxs("li", {
166
+ className: "flex flex-col gap-1",
167
+ children: [/* @__PURE__ */ jsxs("div", {
168
+ className: "flex items-center gap-1",
169
+ children: [
170
+ /* @__PURE__ */ jsx(Input, {
171
+ type: "url",
172
+ value: entry_4.url,
173
+ placeholder: "https://www.linkedin.com/company/…",
174
+ "aria-label": `Profile ${index_0 + 1}`,
175
+ "aria-invalid": error_0 ? true : void 0,
176
+ disabled: save.isPending,
177
+ onChange: (event_0) => update(entry_4.key, event_0.target.value)
178
+ }),
179
+ /* @__PURE__ */ jsx(Button, {
180
+ type: "button",
181
+ variant: "ghost",
182
+ size: "icon",
183
+ "aria-label": "Move up",
184
+ disabled: index_0 === 0 || save.isPending,
185
+ onClick: () => move(index_0, -1),
186
+ children: /* @__PURE__ */ jsx(ArrowUp, {})
187
+ }),
188
+ /* @__PURE__ */ jsx(Button, {
189
+ type: "button",
190
+ variant: "ghost",
191
+ size: "icon",
192
+ "aria-label": "Move down",
193
+ disabled: index_0 === rows.length - 1 || save.isPending,
194
+ onClick: () => move(index_0, 1),
195
+ children: /* @__PURE__ */ jsx(ArrowDown, {})
196
+ }),
197
+ /* @__PURE__ */ jsx(Button, {
198
+ type: "button",
199
+ variant: "ghost",
200
+ size: "icon",
201
+ "aria-label": "Remove",
202
+ disabled: save.isPending,
203
+ onClick: () => remove(entry_4.key),
204
+ children: /* @__PURE__ */ jsx(Trash2, {})
205
+ })
206
+ ]
207
+ }), error_0 && /* @__PURE__ */ jsx("p", {
208
+ className: "text-destructive text-xs",
209
+ children: error_0
210
+ })]
211
+ }, entry_4.key);
212
+ };
213
+ $[22] = rows.length;
214
+ $[23] = save.isPending;
215
+ $[24] = showErrors;
216
+ $[25] = t12;
217
+ } else t12 = $[25];
218
+ t11 = rows.map(t12);
219
+ $[18] = rows;
220
+ $[19] = save.isPending;
221
+ $[20] = showErrors;
222
+ $[21] = t11;
223
+ } else t11 = $[21];
224
+ let t12;
225
+ if ($[26] !== t11) {
226
+ t12 = /* @__PURE__ */ jsx("ol", {
227
+ className: "flex flex-col gap-2",
228
+ children: t11
229
+ });
230
+ $[26] = t11;
231
+ $[27] = t12;
232
+ } else t12 = $[27];
233
+ let t13;
234
+ let t14;
235
+ if ($[28] === Symbol.for("react.memo_cache_sentinel")) {
236
+ t13 = () => setRows(_temp6);
237
+ t14 = /* @__PURE__ */ jsx(Plus, {});
238
+ $[28] = t13;
239
+ $[29] = t14;
240
+ } else {
241
+ t13 = $[28];
242
+ t14 = $[29];
243
+ }
244
+ let t15;
245
+ if ($[30] !== save.isPending) {
246
+ t15 = /* @__PURE__ */ jsxs(Button, {
247
+ type: "button",
248
+ variant: "ghost",
249
+ className: "sm:mr-auto",
250
+ disabled: save.isPending,
251
+ onClick: t13,
252
+ children: [t14, "Add profile"]
253
+ });
254
+ $[30] = save.isPending;
255
+ $[31] = t15;
256
+ } else t15 = $[31];
257
+ let t16;
258
+ if ($[32] !== save.isPending) {
259
+ t16 = /* @__PURE__ */ jsx(DialogClose, {
260
+ render: /* @__PURE__ */ jsx(Button, {
261
+ type: "button",
262
+ variant: "outline",
263
+ disabled: save.isPending
264
+ }),
265
+ children: "Cancel"
266
+ });
267
+ $[32] = save.isPending;
268
+ $[33] = t16;
269
+ } else t16 = $[33];
270
+ let t17;
271
+ if ($[34] !== save.isPending) {
272
+ t17 = save.isPending && /* @__PURE__ */ jsx(Spinner, {});
273
+ $[34] = save.isPending;
274
+ $[35] = t17;
275
+ } else t17 = $[35];
276
+ let t18;
277
+ if ($[36] !== save.isPending || $[37] !== t17) {
278
+ t18 = /* @__PURE__ */ jsxs(Button, {
279
+ type: "submit",
280
+ disabled: save.isPending,
281
+ children: [t17, "Save"]
282
+ });
283
+ $[36] = save.isPending;
284
+ $[37] = t17;
285
+ $[38] = t18;
286
+ } else t18 = $[38];
287
+ let t19;
288
+ if ($[39] !== t15 || $[40] !== t16 || $[41] !== t18) {
289
+ t19 = /* @__PURE__ */ jsxs(DialogFooter, { children: [
290
+ t15,
291
+ t16,
292
+ t18
293
+ ] });
294
+ $[39] = t15;
295
+ $[40] = t16;
296
+ $[41] = t18;
297
+ $[42] = t19;
298
+ } else t19 = $[42];
299
+ let t20;
300
+ if ($[43] !== submit || $[44] !== t12 || $[45] !== t19) {
301
+ t20 = /* @__PURE__ */ jsxs("form", {
302
+ onSubmit: submit,
303
+ className: "contents",
304
+ children: [
305
+ t9,
306
+ t10,
307
+ t12,
308
+ t19
309
+ ]
310
+ });
311
+ $[43] = submit;
312
+ $[44] = t12;
313
+ $[45] = t19;
314
+ $[46] = t20;
315
+ } else t20 = $[46];
316
+ return t20;
317
+ }
318
+ function _temp6(current_2) {
319
+ return [...current_2, row("")];
320
+ }
321
+ function _temp5(entry_3) {
322
+ return entry_3.url;
323
+ }
324
+ function _temp4(url) {
325
+ const result = socialProfileUrlSchema.safeParse(url.trim());
326
+ return result.success ? void 0 : result.error.issues[0]?.message;
327
+ }
328
+ function _temp3(e) {
329
+ return e.url;
330
+ }
331
+ function _temp2(entry) {
332
+ return {
333
+ ...entry,
334
+ url: entry.url.trim()
335
+ };
336
+ }
337
+ function _temp(error) {
338
+ toast.error(error.message || "Could not save the social profiles");
339
+ }
340
+
341
+ //#endregion
342
+ export { SocialProfilesDialog };
@@ -86,7 +86,7 @@ function ThreadMessages(t0) {
86
86
  let t6;
87
87
  if ($[18] !== t3 || $[19] !== t4 || $[20] !== t5) {
88
88
  t6 = /* @__PURE__ */ jsx(MessageScrollerViewport, { children: /* @__PURE__ */ jsxs(MessageScrollerContent, {
89
- className: "flex flex-col gap-4 p-3",
89
+ className: "flex flex-col gap-4 px-2 py-3",
90
90
  children: [
91
91
  t3,
92
92
  t4,
@@ -211,7 +211,7 @@ function ContinuedIn(t0) {
211
211
  let t1;
212
212
  if ($[0] !== link.href || $[1] !== link.label || $[2] !== link.onOpen) {
213
213
  t1 = /* @__PURE__ */ jsxs("p", {
214
- className: "text-muted-foreground px-4 pt-3 text-center text-sm",
214
+ className: "text-muted-foreground px-2 pt-3 text-center text-sm",
215
215
  children: [
216
216
  "Continued in",
217
217
  " ",
@@ -26,8 +26,9 @@ const ${camelName} = createBlock({
26
26
  content: (field) => ({
27
27
  title: field.string({ default: "Title" }),
28
28
  }),
29
- component: ${pascalName}Component,
30
- toMarkdown: (c) => [c.title]
29
+ toReact: ${pascalName}Component,
30
+ toTitle: ({ content }) => content.title,
31
+ toMarkdown: ({ content }) => [content.title],
31
32
  });
32
33
 
33
34
  function ${pascalName}Component() {
@@ -23,8 +23,9 @@ export const Layout = createLayout(${JSON.stringify(id)})({
23
23
  title: ${JSON.stringify(id)},
24
24
  description: "",
25
25
  blocks: { before: [], after: [] },
26
- buildMetaTitle: ({ pageMetaTitle }) => pageMetaTitle,
27
- component: ({ children }) => <main><Layout.BeforeBlocks />{children}<Layout.AfterBlocks /></main>,
26
+ toTitle: ({ page }) => page.title,
27
+ toDescription: ({ page }) => page.description,
28
+ toReact: ({ children }) => <main><Layout.BeforeBlocks />{children}<Layout.AfterBlocks /></main>,
28
29
  });
29
30
  `;
30
31
  source = source.replace(/(createLayout\(\s*)["'][^"']+["'](\s*\)\s*\()/g, `$1${JSON.stringify(id)}$2`);
@@ -123,6 +123,7 @@ const CAMOX_OPTIMIZED_DEPS = [
123
123
  "camox > @tanstack/react-virtual",
124
124
  "camox > @unhead/react/client",
125
125
  "camox > @unhead/react/server",
126
+ "camox > @unhead/schema-org",
126
127
  "camox > @xstate/store",
127
128
  "camox > @xstate/store-react",
128
129
  "camox > better-auth/client/plugins",
@@ -7,5 +7,9 @@ export type PageStructure = {
7
7
  layout: PageWithBlocks["layout"];
8
8
  projectName: string;
9
9
  project: PageWithBlocks["project"];
10
+ /** Each block's Structured Data nodes, from a full read: a structure read has none. */
11
+ structuredData?: PageWithBlocks["structuredData"];
12
+ /** The nodes of the records its blocks reference, from a full read. */
13
+ recordStructuredData?: PageWithBlocks["recordStructuredData"];
10
14
  };
11
15
  //#endregion
@@ -25,6 +25,7 @@ async function sendFaviconChange(path, init) {
25
25
  return res;
26
26
  }
27
27
  const projectMutations = {
28
+ updateSocialProfiles: () => getOrpc().projects.updateSocialProfiles.mutationOptions(),
28
29
  uploadFavicon: () => ({ mutationFn: async ({ projectId, file }) => {
29
30
  const formData = new FormData();
30
31
  formData.append("file", file);
@@ -83,7 +83,9 @@ const reads = {
83
83
  page: page.page,
84
84
  layout: page.layout,
85
85
  projectName: page.projectName,
86
- project: page.project
86
+ project: page.project,
87
+ structuredData: page.structuredData,
88
+ recordStructuredData: page.recordStructuredData
87
89
  };
88
90
  }
89
91
  },
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.47.0";
2
+ var version = "0.48.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };