camox 0.48.0 → 0.52.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.
Files changed (127) hide show
  1. package/dist/core/createApp.d.ts +1 -0
  2. package/dist/core/createApp.js +1 -0
  3. package/dist/core/editing/contentEdits.js +78 -0
  4. package/dist/core/editing/createEditableBlock.js +277 -311
  5. package/dist/core/layoutContext.d.ts +1 -0
  6. package/dist/core/lib/contentType.js +2 -2
  7. package/dist/core/recordItem.js +3 -3
  8. package/dist/features/canvas/CamoxCanvas.js +15 -15
  9. package/dist/features/canvas/CanvasLoadingScreen.js +15 -11
  10. package/dist/features/canvas/CanvasPageFrame.js +81 -87
  11. package/dist/features/canvas/CanvasPreviewPageSlot.js +1 -1
  12. package/dist/features/canvas/DerivedPagePathInput.js +12 -12
  13. package/dist/features/canvas/canvasOverlayGeometry.js +1 -1
  14. package/dist/features/canvas/canvasStore.js +2 -2
  15. package/dist/features/canvas/sidebar/CanvasInspector.js +1 -1
  16. package/dist/features/canvas/sidebar/CuratedPageInspector.js +1 -1
  17. package/dist/features/canvas/sidebar/LayoutPageBlocks.js +23 -31
  18. package/dist/features/canvas/useCanvasCamera.js +1 -1
  19. package/dist/features/canvas/useDiscoveredPreviewPaths.js +16 -9
  20. package/dist/features/content/ContentAssets.js +84 -88
  21. package/dist/features/page/CuratedPageContent.js +1 -1
  22. package/dist/features/preview/CamoxPreview.d.ts +8 -4
  23. package/dist/features/preview/CamoxPreview.js +258 -443
  24. package/dist/features/preview/EditablePageContent.js +244 -2
  25. package/dist/features/preview/EditablePageExperience.js +19 -13
  26. package/dist/features/preview/components/AddBlockSheet.js +55 -15
  27. package/dist/features/preview/components/BlockActionsPopover.js +23 -51
  28. package/dist/features/preview/components/BlockPeek.js +66 -0
  29. package/dist/features/preview/components/BlockThumbnail.js +134 -119
  30. package/dist/features/preview/components/CanvasPreview.js +1 -1
  31. package/dist/features/preview/components/CreatePageModal.js +150 -156
  32. package/dist/features/preview/components/FieldToolbar.js +1 -1
  33. package/dist/features/preview/components/Frame.js +27 -4
  34. package/dist/features/preview/components/MobilePreviewDrawer.js +6 -9
  35. package/dist/features/preview/components/PageInfo.js +161 -126
  36. package/dist/features/preview/components/PagePicker.js +2 -2
  37. package/dist/features/preview/components/PageTree.js +174 -190
  38. package/dist/features/preview/components/PreviewPanel.js +13 -67
  39. package/dist/features/preview/components/PreviewToolbar.js +1 -1
  40. package/dist/features/preview/components/ReferenceFieldEditor.js +4 -4
  41. package/dist/features/preview/components/ReferenceListFieldEditor.js +4 -4
  42. package/dist/features/preview/components/RemovedBlockPlaceholder.js +37 -70
  43. package/dist/features/preview/components/RepeatableItemsList.js +1 -1
  44. package/dist/features/preview/components/SelectionEditor.js +248 -256
  45. package/dist/features/preview/components/StudioEditingFallback.js +22 -26
  46. package/dist/features/preview/components/useFieldWriter.js +19 -19
  47. package/dist/features/preview/components/useRecordView.js +105 -147
  48. package/dist/features/preview/components/useRepeatableItemActions.js +0 -2
  49. package/dist/features/preview/pageRecord.js +30 -24
  50. package/dist/features/preview/previewConstants.js +2 -8
  51. package/dist/features/preview/previewStore.js +2 -0
  52. package/dist/features/preview/studioNotice.js +20 -0
  53. package/dist/features/preview/usePreviewViewportActions.js +1 -1
  54. package/dist/features/provider/AuthenticatedCamoxProvider.js +47 -31
  55. package/dist/features/provider/components/CommandPalette.js +2 -5
  56. package/dist/features/provider/signInShortcut.js +1 -2
  57. package/dist/features/provider/useAdminShortcuts.js +47 -53
  58. package/dist/features/renderer/RendererApp.js +542 -0
  59. package/dist/features/renderer/RendererFrames.js +450 -0
  60. package/dist/features/renderer/definitionAnswers.js +26 -0
  61. package/dist/features/renderer/protocol.js +35 -0
  62. package/dist/features/renderer/rendererHost.js +249 -0
  63. package/dist/features/renderer/studioConnection.js +186 -0
  64. package/dist/features/routes/pageRuntime.d.ts +2 -1
  65. package/dist/features/routes/pageRuntime.js +34 -22
  66. package/dist/features/runtime/RuntimeChrome.js +2 -5
  67. package/dist/features/runtime/assetPrefix.js +14 -0
  68. package/dist/features/runtime/breadcrumbs.js +1 -1
  69. package/dist/features/runtime/editingFallback.js +4 -8
  70. package/dist/features/runtime/livePages.js +1 -1
  71. package/dist/features/runtime/pageApp.js +57 -83
  72. package/dist/features/runtime/pageClient.js +1 -1
  73. package/dist/features/runtime/pageNavigation.js +3 -2
  74. package/dist/features/runtime/publicCache.js +95 -0
  75. package/dist/features/runtime/publishingDraft.js +23 -0
  76. package/dist/features/runtime/rendererClient.d.ts +8 -0
  77. package/dist/features/runtime/rendererClient.js +77 -0
  78. package/dist/features/runtime/runtime.d.ts +22 -3
  79. package/dist/features/runtime/runtime.js +209 -96
  80. package/dist/features/runtime/runtimeRoutes.d.ts +2 -0
  81. package/dist/features/runtime/runtimeRoutes.js +2 -0
  82. package/dist/features/runtime/worker.d.ts +14 -1
  83. package/dist/features/runtime/worker.js +34 -4
  84. package/dist/features/settings/AnalyticsSettings.js +622 -0
  85. package/dist/features/settings/CamoxSettings.js +95 -0
  86. package/dist/features/settings/FaviconSettings.js +254 -0
  87. package/dist/features/settings/ProjectNameSettings.js +145 -0
  88. package/dist/features/settings/SettingsPart.js +73 -0
  89. package/dist/features/settings/SocialProfilesSettings.js +245 -0
  90. package/dist/features/settings/useProjectMutation.js +61 -0
  91. package/dist/features/studio/FollowAgentProvider.js +10 -10
  92. package/dist/features/studio/components/DraftsControl.js +136 -131
  93. package/dist/features/studio/components/LocalePicker.js +178 -174
  94. package/dist/features/studio/components/Navbar.js +107 -66
  95. package/dist/features/studio/components/ProjectMenu.js +119 -171
  96. package/dist/features/studio/components/ReviewDraftSheet.js +1151 -991
  97. package/dist/features/studio/components/ThreadMessages.js +100 -65
  98. package/dist/features/studio/components/UserButton.js +2 -2
  99. package/dist/features/studio/routes.js +3 -1
  100. package/dist/features/studio/showInCanvas.js +129 -0
  101. package/dist/features/vite/definitionsSync.js +5 -6
  102. package/dist/features/vite/hostedChecks.js +127 -0
  103. package/dist/features/vite/runtimeDev.js +58 -17
  104. package/dist/features/vite/staticAssetHeaders.js +52 -0
  105. package/dist/features/vite/vite.d.ts +1 -1
  106. package/dist/features/vite/vite.js +49 -9
  107. package/dist/hooks/use-page-destinations.js +10 -10
  108. package/dist/lib/api-client-server.d.ts +1 -1
  109. package/dist/lib/api-client.d.ts +2 -1
  110. package/dist/lib/draft-scope.d.ts +1 -0
  111. package/dist/lib/locale-path.d.ts +3 -11
  112. package/dist/lib/locale-path.js +3 -27
  113. package/dist/lib/queries.js +7 -1
  114. package/dist/lib/revisions.js +20 -0
  115. package/dist/lib/shortcuts.js +29 -0
  116. package/dist/lib/studio-definitions.js +160 -0
  117. package/dist/lib/studio-reads.d.ts +1 -0
  118. package/dist/lib/studio-reads.js +65 -21
  119. package/dist/lib/use-project-room.js +16 -10
  120. package/dist/lib/utils.js +1 -104
  121. package/dist/package.js +1 -1
  122. package/dist/studio.css +1 -1
  123. package/package.json +9 -4
  124. package/skills/camox/SKILL.md +4 -0
  125. package/skills/camox/references/artifacts.md +1 -1
  126. package/dist/features/studio/components/FaviconDialog.js +0 -318
  127. package/dist/features/studio/components/SocialProfilesDialog.js +0 -342
@@ -9,12 +9,13 @@ import { MessageScroller, MessageScrollerButton, MessageScrollerContent, Message
9
9
  //#region src/features/studio/components/ThreadMessages.tsx
10
10
  /**
11
11
  * A thread's messages, and "Working…" while a run is going or a message is still `sending`: on
12
- * its way to the thread, which hasn't accepted it yet. A draft's thread is shared, so it shows
12
+ * its way to the thread, which hasn't accepted it yet. While the end of a turn builds the
13
+ * agent's code, or the agent fixes a build error, it says so instead. A draft's thread is shared, so it shows
13
14
  * senders' names too (`showSenders`). A fresh Live thread links to the draft the editor's
14
15
  * previous one continued in (`continuedIn`).
15
16
  */
16
17
  function ThreadMessages(t0) {
17
- const $ = c(25);
18
+ const $ = c(28);
18
19
  const { view, sending: t1, showSenders: t2, continuedIn } = t0;
19
20
  const sending = t1 === void 0 ? false : t1;
20
21
  const showSenders = t2 === void 0 ? false : t2;
@@ -53,9 +54,9 @@ function ThreadMessages(t0) {
53
54
  $[6] = t3;
54
55
  } else t3 = $[6];
55
56
  let t4;
56
- if ($[7] !== showSenders || $[8] !== view.messages || $[9] !== working) {
57
+ if ($[7] !== showSenders || $[8] !== view.messages || $[9] !== view.status || $[10] !== working) {
57
58
  let t5;
58
- if ($[11] !== showSenders || $[12] !== view.messages.length || $[13] !== working) {
59
+ if ($[12] !== showSenders || $[13] !== view.messages.length || $[14] !== view.status || $[15] !== working) {
59
60
  t5 = (message, index) => /* @__PURE__ */ jsxs(MessageScrollerItem, {
60
61
  messageId: message.id,
61
62
  scrollAnchor: message.from === "editor",
@@ -63,28 +64,31 @@ function ThreadMessages(t0) {
63
64
  children: [/* @__PURE__ */ jsx(ThreadMessageRow, {
64
65
  message,
65
66
  showSender: showSenders
66
- }), working && index === view.messages.length - 1 && /* @__PURE__ */ jsx(Working, {})]
67
+ }), working && index === view.messages.length - 1 && /* @__PURE__ */ jsx(Working, { status: view.status })]
67
68
  }, message.id);
68
- $[11] = showSenders;
69
- $[12] = view.messages.length;
70
- $[13] = working;
71
- $[14] = t5;
72
- } else t5 = $[14];
69
+ $[12] = showSenders;
70
+ $[13] = view.messages.length;
71
+ $[14] = view.status;
72
+ $[15] = working;
73
+ $[16] = t5;
74
+ } else t5 = $[16];
73
75
  t4 = view.messages.map(t5);
74
76
  $[7] = showSenders;
75
77
  $[8] = view.messages;
76
- $[9] = working;
77
- $[10] = t4;
78
- } else t4 = $[10];
78
+ $[9] = view.status;
79
+ $[10] = working;
80
+ $[11] = t4;
81
+ } else t4 = $[11];
79
82
  let t5;
80
- if ($[15] !== view.messages.length || $[16] !== working) {
81
- t5 = working && view.messages.length === 0 && /* @__PURE__ */ jsx(Working, {});
82
- $[15] = view.messages.length;
83
- $[16] = working;
84
- $[17] = t5;
85
- } else t5 = $[17];
83
+ if ($[17] !== view.messages.length || $[18] !== view.status || $[19] !== working) {
84
+ t5 = working && view.messages.length === 0 && /* @__PURE__ */ jsx(Working, { status: view.status });
85
+ $[17] = view.messages.length;
86
+ $[18] = view.status;
87
+ $[19] = working;
88
+ $[20] = t5;
89
+ } else t5 = $[20];
86
90
  let t6;
87
- if ($[18] !== t3 || $[19] !== t4 || $[20] !== t5) {
91
+ if ($[21] !== t3 || $[22] !== t4 || $[23] !== t5) {
88
92
  t6 = /* @__PURE__ */ jsx(MessageScrollerViewport, { children: /* @__PURE__ */ jsxs(MessageScrollerContent, {
89
93
  className: "flex flex-col gap-4 px-2 py-3",
90
94
  children: [
@@ -93,46 +97,53 @@ function ThreadMessages(t0) {
93
97
  t5
94
98
  ]
95
99
  }) });
96
- $[18] = t3;
97
- $[19] = t4;
98
- $[20] = t5;
99
- $[21] = t6;
100
- } else t6 = $[21];
100
+ $[21] = t3;
101
+ $[22] = t4;
102
+ $[23] = t5;
103
+ $[24] = t6;
104
+ } else t6 = $[24];
101
105
  let t7;
102
- if ($[22] === Symbol.for("react.memo_cache_sentinel")) {
106
+ if ($[25] === Symbol.for("react.memo_cache_sentinel")) {
103
107
  t7 = /* @__PURE__ */ jsx(MessageScrollerButton, {});
104
- $[22] = t7;
105
- } else t7 = $[22];
108
+ $[25] = t7;
109
+ } else t7 = $[25];
106
110
  let t8;
107
- if ($[23] !== t6) {
111
+ if ($[26] !== t6) {
108
112
  t8 = /* @__PURE__ */ jsx(MessageScrollerProvider, { children: /* @__PURE__ */ jsxs(MessageScroller, {
109
113
  className: "min-h-0 flex-1",
110
114
  children: [t6, t7]
111
115
  }) });
112
- $[23] = t6;
113
- $[24] = t8;
114
- } else t8 = $[24];
116
+ $[26] = t6;
117
+ $[27] = t8;
118
+ } else t8 = $[27];
115
119
  return t8;
116
120
  }
117
121
  /**
118
122
  * Shown in the last message, not after it: the scroller only brings a message the editor just
119
123
  * sent into view when it's appended last, and it arrives while the agent is already working.
120
124
  */
121
- function Working() {
122
- const $ = c(1);
123
- let t0;
124
- if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
125
- t0 = /* @__PURE__ */ jsx("p", {
125
+ function Working(t0) {
126
+ const $ = c(2);
127
+ const { status } = t0;
128
+ const t1 = status ? STATUS_LINES[status] : "Working…";
129
+ let t2;
130
+ if ($[0] !== t1) {
131
+ t2 = /* @__PURE__ */ jsx("p", {
126
132
  role: "status",
127
133
  className: "shimmer text-muted-foreground text-sm",
128
- children: "Working…"
134
+ children: t1
129
135
  });
130
- $[0] = t0;
131
- } else t0 = $[0];
132
- return t0;
136
+ $[0] = t1;
137
+ $[1] = t2;
138
+ } else t2 = $[1];
139
+ return t2;
133
140
  }
141
+ const STATUS_LINES = {
142
+ building: "Building…",
143
+ fixing: "Fixing a build error…"
144
+ };
134
145
  function ThreadMessageRow(t0) {
135
- const $ = c(17);
146
+ const $ = c(20);
136
147
  const { message, showSender } = t0;
137
148
  if (message.from === "stopped" || message.from === "failed") {
138
149
  const t1 = message.from === "stopped" ? `Stopped by ${message.sender.name}` : "Camox couldn’t finish this. Send your message again to retry.";
@@ -156,24 +167,48 @@ function ThreadMessageRow(t0) {
156
167
  } else t1 = $[3];
157
168
  return t1;
158
169
  }
170
+ if (message.from === "camox") {
171
+ let t1;
172
+ if ($[4] === Symbol.for("react.memo_cache_sentinel")) {
173
+ t1 = /* @__PURE__ */ jsx(MessageHeader, {
174
+ className: "text-muted-foreground text-xs",
175
+ children: "Camox"
176
+ });
177
+ $[4] = t1;
178
+ } else t1 = $[4];
179
+ let t2;
180
+ if ($[5] !== message.text) {
181
+ t2 = /* @__PURE__ */ jsx(Message, { children: /* @__PURE__ */ jsxs(MessageContent, { children: [t1, /* @__PURE__ */ jsx(Bubble, {
182
+ variant: "muted",
183
+ className: "max-w-full",
184
+ children: /* @__PURE__ */ jsx(BubbleContent, {
185
+ className: "whitespace-pre-wrap",
186
+ children: message.text
187
+ })
188
+ })] }) });
189
+ $[5] = message.text;
190
+ $[6] = t2;
191
+ } else t2 = $[6];
192
+ return t2;
193
+ }
159
194
  let t1;
160
- if ($[4] !== message.sender) {
195
+ if ($[7] !== message.sender) {
161
196
  t1 = /* @__PURE__ */ jsx(MessageAvatar, { children: /* @__PURE__ */ jsx(SenderAvatar, { sender: message.sender }) });
162
- $[4] = message.sender;
163
- $[5] = t1;
164
- } else t1 = $[5];
197
+ $[7] = message.sender;
198
+ $[8] = t1;
199
+ } else t1 = $[8];
165
200
  let t2;
166
- if ($[6] !== message.sender || $[7] !== showSender) {
201
+ if ($[9] !== message.sender || $[10] !== showSender) {
167
202
  t2 = showSender && /* @__PURE__ */ jsx(MessageHeader, {
168
203
  className: "text-muted-foreground text-xs",
169
204
  children: message.sender.name
170
205
  });
171
- $[6] = message.sender;
172
- $[7] = showSender;
173
- $[8] = t2;
174
- } else t2 = $[8];
206
+ $[9] = message.sender;
207
+ $[10] = showSender;
208
+ $[11] = t2;
209
+ } else t2 = $[11];
175
210
  let t3;
176
- if ($[9] !== message.text) {
211
+ if ($[12] !== message.text) {
177
212
  t3 = /* @__PURE__ */ jsx(Bubble, {
178
213
  align: "end",
179
214
  variant: "muted",
@@ -183,26 +218,26 @@ function ThreadMessageRow(t0) {
183
218
  children: message.text
184
219
  })
185
220
  });
186
- $[9] = message.text;
187
- $[10] = t3;
188
- } else t3 = $[10];
221
+ $[12] = message.text;
222
+ $[13] = t3;
223
+ } else t3 = $[13];
189
224
  let t4;
190
- if ($[11] !== t2 || $[12] !== t3) {
225
+ if ($[14] !== t2 || $[15] !== t3) {
191
226
  t4 = /* @__PURE__ */ jsxs(MessageContent, { children: [t2, t3] });
192
- $[11] = t2;
193
- $[12] = t3;
194
- $[13] = t4;
195
- } else t4 = $[13];
227
+ $[14] = t2;
228
+ $[15] = t3;
229
+ $[16] = t4;
230
+ } else t4 = $[16];
196
231
  let t5;
197
- if ($[14] !== t1 || $[15] !== t4) {
232
+ if ($[17] !== t1 || $[18] !== t4) {
198
233
  t5 = /* @__PURE__ */ jsxs(Message, {
199
234
  align: "end",
200
235
  children: [t1, t4]
201
236
  });
202
- $[14] = t1;
203
- $[15] = t4;
204
- $[16] = t5;
205
- } else t5 = $[16];
237
+ $[17] = t1;
238
+ $[18] = t4;
239
+ $[19] = t5;
240
+ } else t5 = $[19];
206
241
  return t5;
207
242
  }
208
243
  function ContinuedIn(t0) {
@@ -1,10 +1,10 @@
1
1
  import { useAuthContext, useAuthState } from "../../../lib/auth.js";
2
2
  import { useNavigate } from "../../navigation/navigation.js";
3
- import { PlatformLabel } from "../../../components/PlatformLabel.js";
4
3
  import { useLocalePath, useLocalizedPath } from "../../../lib/view-locale.js";
4
+ import { PlatformLabel } from "../../../components/PlatformLabel.js";
5
+ import { previewStore, selectIsCanvasShown } from "../../preview/previewStore.js";
5
6
  import { STUDIO_BASE_PATH } from "../routes.js";
6
7
  import { studioStore } from "../studioStore.js";
7
- import { previewStore, selectIsCanvasShown } from "../../preview/previewStore.js";
8
8
  import { useApplyTheme } from "../useTheme.js";
9
9
  import { c } from "react/compiler-runtime";
10
10
  import { useSelector } from "@xstate/store-react";
@@ -3,6 +3,8 @@ const STUDIO_ROUTE_SEGMENT = "camox";
3
3
  const STUDIO_BASE_PATH = `/${STUDIO_ROUTE_SEGMENT}`;
4
4
  const STUDIO_CONTENT_PATH = `${STUDIO_BASE_PATH}/content`;
5
5
  const STUDIO_ASSETS_PATH = `${STUDIO_CONTENT_PATH}/assets`;
6
+ /** The Settings Section, where members change Project Settings. */
7
+ const STUDIO_SETTINGS_PATH = `${STUDIO_BASE_PATH}/settings`;
6
8
  const collectionContentPath = (collectionId) => `${STUDIO_CONTENT_PATH}/collections/${encodeURIComponent(collectionId)}`;
7
9
  function matchCollectionContentPath(pathname) {
8
10
  const match = /^\/camox\/content\/collections\/([^/]+)(?:(\/new)|\/([^/]+)\/edit)?$/.exec(pathname);
@@ -29,4 +31,4 @@ function matchCollectionContentPath(pathname) {
29
31
  const STUDIO_CANVAS_PATH = `${STUDIO_BASE_PATH}/canvas`;
30
32
 
31
33
  //#endregion
32
- export { STUDIO_ASSETS_PATH, STUDIO_BASE_PATH, STUDIO_CANVAS_PATH, STUDIO_CONTENT_PATH, STUDIO_ROUTE_SEGMENT, collectionContentPath, matchCollectionContentPath };
34
+ export { STUDIO_ASSETS_PATH, STUDIO_BASE_PATH, STUDIO_CANVAS_PATH, STUDIO_CONTENT_PATH, STUDIO_ROUTE_SEGMENT, STUDIO_SETTINGS_PATH, collectionContentPath, matchCollectionContentPath };
@@ -0,0 +1,129 @@
1
+ import { useProjectUuid } from "../../lib/auth.js";
2
+ import { localizedPath } from "../../lib/locale-path.js";
3
+ import { useViewLocale } from "../../lib/view-locale-context.js";
4
+ import { useLocation, useNavigate } from "../navigation/navigation.js";
5
+ import { reads, useStudioCache } from "../../lib/studio-reads.js";
6
+ import { previewStore, sameEditingOwner } from "../preview/previewStore.js";
7
+ import { fieldTypesDictionary } from "../../core/lib/fieldTypes.js";
8
+ import { c } from "react/compiler-runtime";
9
+
10
+ //#region src/features/studio/showInCanvas.ts
11
+ /**
12
+ * How long showing something in the Canvas keeps selecting it, in milliseconds, while the Canvas
13
+ * mounts and its page's editing frame claims, each of which can clear the selection. A product
14
+ * tuning value: long enough for a page to load, short enough not to fight the editor.
15
+ */
16
+ const SELECTING_FOR = 5e3;
17
+ const isFieldType = (kind) => !!kind && Object.hasOwn(fieldTypesDictionary, kind);
18
+ /** What to select on the target's page: its field, else its item or block, else nothing. */
19
+ function selectionOf({ blockId, itemId, field }) {
20
+ if (!blockId) return null;
21
+ const fieldType = field && isFieldType(field.kind) ? field.kind : null;
22
+ if (itemId) return field && fieldType ? {
23
+ type: "item-field",
24
+ blockId,
25
+ itemId,
26
+ fieldName: field.name,
27
+ fieldType
28
+ } : {
29
+ type: "item",
30
+ blockId,
31
+ itemId
32
+ };
33
+ return field && fieldType ? {
34
+ type: "block-field",
35
+ blockId,
36
+ fieldName: field.name,
37
+ fieldType
38
+ } : {
39
+ type: "block",
40
+ blockId
41
+ };
42
+ }
43
+ /** Stops selecting what was last shown in the Canvas. */
44
+ let stopSelecting = null;
45
+ /**
46
+ * Selects `selection` once `owner`'s editing frame is active, and again whenever it's cleared,
47
+ * until the editor selects something else or the time runs out.
48
+ */
49
+ function selectOnceShown(owner, selection) {
50
+ stopSelecting?.();
51
+ const select = () => {
52
+ const { editingContext } = previewStore.getSnapshot().context;
53
+ if (!sameEditingOwner(editingContext, owner)) return;
54
+ const current = editingContext?.selection ?? null;
55
+ if (current === null) {
56
+ previewStore.send({
57
+ type: "selectTarget",
58
+ ...owner,
59
+ selection
60
+ });
61
+ return;
62
+ }
63
+ if (JSON.stringify(current) !== JSON.stringify(selection)) stop();
64
+ };
65
+ const subscription = previewStore.subscribe(select);
66
+ const timer = setTimeout(() => stop(), SELECTING_FOR);
67
+ const stop = () => {
68
+ subscription.unsubscribe();
69
+ clearTimeout(timer);
70
+ if (stopSelecting === stop) stopSelecting = null;
71
+ };
72
+ stopSelecting = stop;
73
+ const { editingContext } = previewStore.getSnapshot().context;
74
+ if (sameEditingOwner(editingContext, owner)) previewStore.send({
75
+ type: "selectTarget",
76
+ ...owner,
77
+ selection
78
+ });
79
+ }
80
+ /**
81
+ * Shows a page in the Canvas, in the target's locale, and selects the block, item or field in it
82
+ * once the page's editing frame is active.
83
+ */
84
+ function useShowInCanvas() {
85
+ const $ = c(8);
86
+ const cache = useStudioCache();
87
+ const projectUuid = useProjectUuid();
88
+ const view = useViewLocale();
89
+ const navigate = useNavigate();
90
+ let t0;
91
+ if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
92
+ t0 = { select: _temp };
93
+ $[0] = t0;
94
+ } else t0 = $[0];
95
+ const search = useLocation(t0);
96
+ let t1;
97
+ if ($[1] !== cache || $[2] !== navigate || $[3] !== projectUuid || $[4] !== search || $[5] !== view.locale || $[6] !== view.locales) {
98
+ t1 = async (target) => {
99
+ const locale = view.locales.find((each) => each.tag === target.locale) ?? view.locale;
100
+ const page = (await cache.ensure(reads.pageList({ projectUuid }), { locale }).catch(_temp2)).find((each_0) => each_0.id === target.pageId);
101
+ if (!page) return;
102
+ const selection = selectionOf(target);
103
+ if (selection) selectOnceShown({
104
+ kind: "page",
105
+ pageId: page.id
106
+ }, selection);
107
+ else stopSelecting?.();
108
+ previewStore.send({ type: "showCanvas" });
109
+ await navigate({ to: `${localizedPath(page.fullPath, locale)}${search}` });
110
+ };
111
+ $[1] = cache;
112
+ $[2] = navigate;
113
+ $[3] = projectUuid;
114
+ $[4] = search;
115
+ $[5] = view.locale;
116
+ $[6] = view.locales;
117
+ $[7] = t1;
118
+ } else t1 = $[7];
119
+ return t1;
120
+ }
121
+ function _temp2() {
122
+ return [];
123
+ }
124
+ function _temp(location) {
125
+ return location.search;
126
+ }
127
+
128
+ //#endregion
129
+ export { useShowInCanvas };
@@ -56,14 +56,13 @@ async function syncDefinitionsToApi(options) {
56
56
  throw error;
57
57
  }
58
58
  if (result.draftCreated) logger.info("[camox] Created your development draft", { timestamp: true });
59
- logger.info(`[camox] Synced ${counted(result.blocks.count, "block definition")}`, { timestamp: true });
59
+ const definitionCount = result.blocks.count + result.collections.count + (result.layouts?.count ?? 0);
60
+ logger.info(`[camox] Synced ${counted(definitionCount, "Camox definition")}`, { timestamp: true });
60
61
  const { deletedDefinitionTypes } = result.blocks;
61
62
  if (deletedDefinitionTypes.length > 0) logger.info(`[camox] Removed ${counted(deletedDefinitionTypes.length, "block definition")} no longer in code: ${quoted(deletedDefinitionTypes)}. Their blocks are kept and render nothing until the definition comes back.`, { timestamp: true });
62
- logger.info(`[camox] Synced ${result.collections.count} collection definitions`, { timestamp: true });
63
63
  for (const id of result.collections.retired) logger.warn(`[camox] Removed collection "${id}"; its records are kept and hidden`, { timestamp: true });
64
64
  const layoutSyncResults = result.layouts;
65
65
  if (layoutSyncResults) {
66
- logger.info(`[camox] Synced ${counted(layoutSyncResults.count, "layout")} to Camox API`, { timestamp: true });
67
66
  for (const layout of layoutSyncResults.layouts) {
68
67
  const { layoutId, createdBlockTypes, removedBlockTypes, skippedOrphanTypes } = layout;
69
68
  if (layout.wasExisting && createdBlockTypes.length > 0) logger.info(`[camox] Added ${counted(createdBlockTypes.length, "block")} to existing layout "${layoutId}": ${quoted(createdBlockTypes)}`, { timestamp: true });
@@ -180,7 +179,7 @@ async function syncDefinitions(server, options) {
180
179
  await sync({ initial: true });
181
180
  synced = true;
182
181
  } catch (error) {
183
- logger.error(`[camox] Failed to sync block definitions: ${String(error)}`, { timestamp: true });
182
+ logger.error(`[camox] Failed to sync Camox definitions: ${String(error)}`, { timestamp: true });
184
183
  }
185
184
  let running = Promise.resolve();
186
185
  let timer;
@@ -188,7 +187,7 @@ async function syncDefinitions(server, options) {
188
187
  clearTimeout(timer);
189
188
  timer = setTimeout(() => {
190
189
  running = running.then(() => sync().catch((error) => {
191
- logger.error(`[camox] Failed to sync block definitions: ${String(error)}`, { timestamp: true });
190
+ logger.error(`[camox] Failed to sync Camox definitions: ${String(error)}`, { timestamp: true });
192
191
  }));
193
192
  }, SYNC_DEBOUNCE_DELAY_MS);
194
193
  };
@@ -208,4 +207,4 @@ async function syncDefinitions(server, options) {
208
207
  }
209
208
 
210
209
  //#endregion
211
- export { VIRTUAL_CAMOX_APP, syncDefinitions, syncDefinitionsToApi };
210
+ export { VIRTUAL_CAMOX_APP, loadDefinitionSet, syncDefinitions, syncDefinitionsToApi };
@@ -0,0 +1,127 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { Visitor, parseSync } from "vite-plus";
5
+
6
+ //#region src/features/vite/hostedChecks.ts
7
+ /**
8
+ * Set by Camox's revision and release sandboxes: a hosted project's build checks the code an
9
+ * agent may have written (ADR 0021). `apps/landing` and dev mode never set it.
10
+ */
11
+ const HOSTED_BUILD_ENV = "CAMOX_INTERNAL_HOSTED";
12
+ const sdkRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
13
+ /** `camox`'s public entry points, as `camox/<path>`: its exports, less its internal ones. */
14
+ function camoxEntryPoints() {
15
+ const { exports } = JSON.parse(readFileSync(join(sdkRoot, "package.json"), "utf8"));
16
+ return new Set(Object.keys(exports).filter((path) => !path.startsWith("./_internal/")).map((path) => `camox${path.slice(1)}`));
17
+ }
18
+ function readHostedProject(root) {
19
+ const { dependencies = {} } = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
20
+ return {
21
+ root,
22
+ src: join(root, "src"),
23
+ dependencies: new Set(Object.keys(dependencies)),
24
+ camoxEntryPoints: camoxEntryPoints()
25
+ };
26
+ }
27
+ const withoutQuery = (path) => path.replace(/[?#].*$/, "");
28
+ const isInside = (dir, path) => path.startsWith(dir + sep);
29
+ const displayPath = (project, file) => relative(project.root, file).split(sep).join("/");
30
+ /** The project's own file an importer names, when it is one of `src/`'s: the code it checks. */
31
+ function sourceFile(project, importer) {
32
+ if (!importer || importer.startsWith("\0")) return;
33
+ const file = withoutQuery(importer);
34
+ return isAbsolute(file) && isInside(project.src, file) ? file : void 0;
35
+ }
36
+ function isAllowedImport(project, specifier, importer) {
37
+ if (specifier.startsWith("\0")) return true;
38
+ const path = withoutQuery(specifier);
39
+ let file;
40
+ if (path.startsWith("./") || path.startsWith("../")) file = resolve(dirname(importer), path);
41
+ else if (path.startsWith("@/")) file = resolve(project.src, path.slice(2));
42
+ else if (path.startsWith("/")) file = isInside(project.root, path) ? resolve(path) : resolve(project.root, `.${path}`);
43
+ if (file !== void 0) return isInside(project.src, file);
44
+ if (path === "camox" || path.startsWith("camox/")) return project.camoxEntryPoints.has(path);
45
+ const [scopeOrName, name] = path.split("/");
46
+ const packageName = scopeOrName.startsWith("@") ? `${scopeOrName}/${name}` : scopeOrName;
47
+ return project.dependencies.has(packageName);
48
+ }
49
+ /**
50
+ * Why a file of `src/` can't import `specifier`, or nothing when it may: files of `src/`,
51
+ * `camox`'s public entry points and `package.json`'s `dependencies`.
52
+ */
53
+ function forbiddenImport(project, specifier, importer) {
54
+ const file = sourceFile(project, importer);
55
+ if (!file || isAllowedImport(project, specifier, file)) return;
56
+ return `${displayPath(project, file)} imports "${specifier}", which a hosted project can't import. It can import files in src/, camox's public entry points and the packages package.json lists in dependencies.`;
57
+ }
58
+ /** The modules the registry loads blocks from, as its globs name them. */
59
+ const BLOCK_MODULE = /^src\/(?:camox\/)?blocks\/([^/]+)\.tsx?$/;
60
+ /** A string an expression spells out, without running it. */
61
+ function literalString(node) {
62
+ if (node?.type === "Literal" && typeof node.value === "string") return node.value;
63
+ if (node?.type === "TemplateLiteral" && node.expressions.length === 0) return node.quasis[0]?.value.cooked ?? void 0;
64
+ }
65
+ /**
66
+ * Whether `callee` calls `createBlock`: by its name, the names a module imports it as
67
+ * (`import { createBlock as defineBlock }`), or as a namespace's member.
68
+ */
69
+ function isCreateBlock(callee, names) {
70
+ if (callee.type === "Identifier") return names.has(callee.name);
71
+ return callee.type === "MemberExpression" && callee.property.type === "Identifier" && callee.property.name === "createBlock";
72
+ }
73
+ /** The local names a module calls `createBlock` by: its own name, and its imports' aliases. */
74
+ function createBlockNames(program) {
75
+ const names = /* @__PURE__ */ new Set(["createBlock"]);
76
+ for (const statement of program.body) {
77
+ if (statement.type !== "ImportDeclaration") continue;
78
+ for (const specifier of statement.specifiers) {
79
+ if (specifier.type !== "ImportSpecifier") continue;
80
+ const imported = specifier.imported;
81
+ if ((imported.type === "Identifier" ? imported.name : imported.value) === "createBlock") names.add(specifier.local.name);
82
+ }
83
+ }
84
+ return names;
85
+ }
86
+ /** The `id` a block definition's object literal spells out. */
87
+ function definitionId(definition) {
88
+ if (definition?.type !== "ObjectExpression") return;
89
+ for (const property of definition.properties) {
90
+ if (property.type !== "Property" || property.computed) continue;
91
+ const key = property.key;
92
+ if (key.type === "Identifier" && key.name === "id" || key.type === "Literal" && key.value === "id") return property.value;
93
+ }
94
+ }
95
+ /**
96
+ * Why a block's module breaks the registry, or nothing when it doesn't. The browser finds a block
97
+ * by its file name, so a block's file must be named after its id: the build reads the id from the
98
+ * `createBlock()` call, and needs it written as a string.
99
+ */
100
+ function misnamedBlock(project, moduleId, code) {
101
+ const file = sourceFile(project, moduleId);
102
+ if (!file) return;
103
+ const path = displayPath(project, file);
104
+ const fileName = BLOCK_MODULE.exec(path)?.[1];
105
+ if (!fileName) return;
106
+ const { program, errors } = parseSync(file, code);
107
+ if (errors.length > 0) return;
108
+ let problem;
109
+ const names = createBlockNames(program);
110
+ new Visitor({ CallExpression(call) {
111
+ if (problem || !isCreateBlock(call.callee, names)) return;
112
+ const id = literalString(definitionId(call.arguments[0]));
113
+ const at = definitionId(call.arguments[0])?.start ?? call.start;
114
+ if (id === void 0) problem = {
115
+ message: `${path} calls createBlock() without its id as a string literal, which the build needs to check that the file is named after it: write \`id: "${fileName}"\`.`,
116
+ at
117
+ };
118
+ else if (id !== fileName) problem = {
119
+ message: `${path} defines the block "${id}", but its file is named "${fileName}": a block's file is named after its id. Rename the file to ${id}${file.slice(file.lastIndexOf("."))} or the id to "${fileName}".`,
120
+ at
121
+ };
122
+ } }).visit(program);
123
+ return problem;
124
+ }
125
+
126
+ //#endregion
127
+ export { HOSTED_BUILD_ENV, forbiddenImport, misnamedBlock, readHostedProject };