camox 0.48.0 → 0.51.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 (57) hide show
  1. package/dist/core/layoutContext.d.ts +1 -0
  2. package/dist/features/canvas/sidebar/CanvasInspector.js +1 -1
  3. package/dist/features/content/ContentAssets.js +84 -88
  4. package/dist/features/preview/CamoxPreview.js +1 -1
  5. package/dist/features/preview/EditablePageExperience.js +17 -9
  6. package/dist/features/preview/components/BlockActionsPopover.js +11 -35
  7. package/dist/features/preview/components/FieldToolbar.js +1 -1
  8. package/dist/features/preview/components/MobilePreviewDrawer.js +6 -9
  9. package/dist/features/preview/components/PreviewPanel.js +10 -64
  10. package/dist/features/preview/components/PreviewToolbar.js +1 -1
  11. package/dist/features/preview/previewConstants.js +2 -8
  12. package/dist/features/preview/usePreviewViewportActions.js +1 -1
  13. package/dist/features/provider/AuthenticatedCamoxProvider.js +1 -1
  14. package/dist/features/provider/components/CommandPalette.js +2 -5
  15. package/dist/features/provider/signInShortcut.js +1 -2
  16. package/dist/features/provider/useAdminShortcuts.js +47 -53
  17. package/dist/features/routes/pageRuntime.d.ts +2 -1
  18. package/dist/features/routes/pageRuntime.js +27 -20
  19. package/dist/features/runtime/publicCache.js +83 -0
  20. package/dist/features/runtime/runtime.d.ts +13 -3
  21. package/dist/features/runtime/runtime.js +173 -79
  22. package/dist/features/runtime/worker.d.ts +5 -1
  23. package/dist/features/runtime/worker.js +20 -3
  24. package/dist/features/settings/AnalyticsSettings.js +622 -0
  25. package/dist/features/settings/CamoxSettings.js +95 -0
  26. package/dist/features/settings/FaviconSettings.js +254 -0
  27. package/dist/features/settings/ProjectNameSettings.js +145 -0
  28. package/dist/features/settings/SettingsPart.js +73 -0
  29. package/dist/features/settings/SocialProfilesSettings.js +245 -0
  30. package/dist/features/settings/useProjectMutation.js +61 -0
  31. package/dist/features/studio/components/DraftsControl.js +136 -131
  32. package/dist/features/studio/components/LocalePicker.js +178 -174
  33. package/dist/features/studio/components/Navbar.js +107 -66
  34. package/dist/features/studio/components/ProjectMenu.js +118 -170
  35. package/dist/features/studio/components/ReviewDraftSheet.js +1046 -990
  36. package/dist/features/studio/components/UserButton.js +1 -1
  37. package/dist/features/studio/routes.js +3 -1
  38. package/dist/features/studio/showInCanvas.js +129 -0
  39. package/dist/features/vite/definitionsSync.js +4 -5
  40. package/dist/features/vite/runtimeDev.js +32 -17
  41. package/dist/features/vite/staticAssetHeaders.js +52 -0
  42. package/dist/features/vite/vite.d.ts +1 -1
  43. package/dist/features/vite/vite.js +16 -4
  44. package/dist/lib/api-client-server.d.ts +1 -1
  45. package/dist/lib/api-client.d.ts +2 -1
  46. package/dist/lib/draft-scope.d.ts +1 -0
  47. package/dist/lib/locale-path.d.ts +3 -11
  48. package/dist/lib/locale-path.js +3 -27
  49. package/dist/lib/queries.js +7 -1
  50. package/dist/lib/shortcuts.js +29 -0
  51. package/dist/lib/studio-reads.d.ts +1 -0
  52. package/dist/lib/utils.js +1 -104
  53. package/dist/package.js +1 -1
  54. package/dist/studio.css +1 -1
  55. package/package.json +5 -4
  56. package/dist/features/studio/components/FaviconDialog.js +0 -318
  57. package/dist/features/studio/components/SocialProfilesDialog.js +0 -342
@@ -1,7 +1,7 @@
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
5
  import { STUDIO_BASE_PATH } from "../routes.js";
6
6
  import { studioStore } from "../studioStore.js";
7
7
  import { previewStore, selectIsCanvasShown } from "../../preview/previewStore.js";
@@ -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
  };
@@ -183,7 +183,10 @@ export default stylesheetUrl;
183
183
  `;
184
184
  }
185
185
  function generateVirtualCamoxServer(runtimeBasePath) {
186
- return `import { handleCamoxRequest as handleRuntimeRequest } from "camox/_internal/runtime";
186
+ return `import {
187
+ handleCamoxRequest as handleRuntimeRequest,
188
+ isPublicRequest as isPublicRuntimeRequest,
189
+ } from "camox/_internal/runtime";
187
190
  import { blockModulePaths, camoxApp } from "virtual:camox/app";
188
191
  import blockChunks from "virtual:camox/block-chunks";
189
192
  import { camoxDocument } from "virtual:camox/document";
@@ -198,23 +201,35 @@ import devHtml from "virtual:camox/dev-html";
198
201
  export { devHtml };
199
202
  export const runtimeBasePath = ${JSON.stringify(normalizeRuntimeBasePath(runtimeBasePath))};
200
203
 
204
+ const options = {
205
+ apiUrl: __CAMOX_API_URL__,
206
+ authenticationUrl: __CAMOX_AUTHENTICATION_URL__,
207
+ blockPreloads: { modulePaths: blockModulePaths, chunks: blockChunks },
208
+ pageClientEntryUrl,
209
+ studioClientEntryUrl,
210
+ developmentDraft: __CAMOX_DEVELOPMENT_DRAFT__,
211
+ getCamoxApp: async () => camoxApp,
212
+ getDocument: async () => camoxDocument,
213
+ projectUuid: __CAMOX_PROJECT_ID__,
214
+ renderPage,
215
+ renderStudio,
216
+ runtimeBasePath: ${JSON.stringify(runtimeBasePath)},
217
+ stylesheetUrl: appStylesheetUrl,
218
+ studioStylesheetUrl,
219
+ };
220
+
221
+ export const publicCacheSource = {
222
+ apiUrl: __CAMOX_API_URL__,
223
+ projectUuid: __CAMOX_PROJECT_ID__,
224
+ build: __CAMOX_BUILD_ID__,
225
+ };
226
+
201
227
  export async function handleCamoxRequest(request) {
202
- return handleRuntimeRequest(request, {
203
- apiUrl: __CAMOX_API_URL__,
204
- authenticationUrl: __CAMOX_AUTHENTICATION_URL__,
205
- blockPreloads: { modulePaths: blockModulePaths, chunks: blockChunks },
206
- pageClientEntryUrl,
207
- studioClientEntryUrl,
208
- developmentDraft: __CAMOX_DEVELOPMENT_DRAFT__,
209
- getCamoxApp: async () => camoxApp,
210
- getDocument: async () => camoxDocument,
211
- projectUuid: __CAMOX_PROJECT_ID__,
212
- renderPage,
213
- renderStudio,
214
- runtimeBasePath: ${JSON.stringify(runtimeBasePath)},
215
- stylesheetUrl: appStylesheetUrl,
216
- studioStylesheetUrl,
217
- });
228
+ return handleRuntimeRequest(request, options);
229
+ }
230
+
231
+ export function isPublicRequest(request) {
232
+ return isPublicRuntimeRequest(request, options);
218
233
  }
219
234
  `;
220
235
  }
@@ -0,0 +1,52 @@
1
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { join, relative } from "node:path";
3
+
4
+ //#region src/features/vite/staticAssetHeaders.ts
5
+ /** Where the client build puts the files it names by their content's hash. */
6
+ const HASHED_DIRS = ["assets", "_chunks"];
7
+ const IMMUTABLE = "public, max-age=31536000, immutable";
8
+ /** Workers Static Assets reads at most this many rules. */
9
+ const MAX_RULES = 100;
10
+ /**
11
+ * The `_headers` file Workers Static Assets serves the client build with. Files under
12
+ * `/assets/` and `/_chunks/` are named by their content's hash, so browsers keep them for good;
13
+ * the files there named otherwise, the entry chunks pages load by a fixed name and whatever the
14
+ * app's public directory puts there, keep the default, which revalidates. The app's own
15
+ * `_headers` comes first, as it wrote it.
16
+ */
17
+ function staticAssetHeaders({ ownHeaders, unhashedFiles }) {
18
+ const rules = [...HASHED_DIRS.map((dir) => `/${dir}/*\n Cache-Control: ${IMMUTABLE}`), ...[...unhashedFiles].sort().map((file) => `/${file}\n ! Cache-Control`)];
19
+ const own = ownHeaders?.trimEnd();
20
+ const ownRules = own ? own.split("\n").filter((line) => /^\S/.test(line) && !line.startsWith("#")) : [];
21
+ if (ownRules.length + rules.length > MAX_RULES) throw new Error(`Camox: _headers would hold ${ownRules.length + rules.length} rules, past Workers Static Assets' ${MAX_RULES}. Move the public directory's files out of /assets/ and /_chunks/, or drop rules from public/_headers.`);
22
+ return [
23
+ ...own ? [own] : [],
24
+ "# Camox: files named by their content's hash never change.",
25
+ ...rules,
26
+ ""
27
+ ].join("\n");
28
+ }
29
+ /** The entry chunks of a bundle under the hashed directories, which pages load by name. */
30
+ function unhashedEntries(bundle) {
31
+ return Object.values(bundle).flatMap((output) => output.type === "chunk" && output.isEntry ? [output.fileName] : []).filter(inHashedDir);
32
+ }
33
+ /** The files a public directory puts under the hashed directories, relative to it. */
34
+ function publicFilesInHashedDirs(publicDir) {
35
+ if (!publicDir) return [];
36
+ return HASHED_DIRS.flatMap((dir) => filesUnder(join(publicDir, dir))).map((file) => relative(publicDir, file).replaceAll("\\", "/"));
37
+ }
38
+ /** The app's own `_headers`, if its public directory has one. */
39
+ function readOwnHeaders(publicDir) {
40
+ const path = publicDir && join(publicDir, "_headers");
41
+ return path && existsSync(path) ? readFileSync(path, "utf-8") : void 0;
42
+ }
43
+ function inHashedDir(file) {
44
+ return HASHED_DIRS.some((dir) => file.startsWith(`${dir}/`));
45
+ }
46
+ function filesUnder(dir) {
47
+ if (!existsSync(dir)) return [];
48
+ return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => entry.isDirectory() ? filesUnder(join(dir, entry.name)) : [join(dir, entry.name)]);
49
+ }
50
+
51
+ //#endregion
52
+ export { publicFilesInHashedDirs, readOwnHeaders, staticAssetHeaders, unhashedEntries };
@@ -23,7 +23,7 @@ export interface CamoxPluginOptions {
23
23
  _internal?: {
24
24
  /** URL of the Camox API backend, used for data fetching */
25
25
  apiUrl?: string;
26
- /** URL of the Camox authentication backend (default: https://app.camox.dev) */
26
+ /** URL of the Camox authentication backend (default: https://app.camox.ai) */
27
27
  authenticationUrl?: string;
28
28
  /** Show Tanstack query devtools (default: false) */
29
29
  enableTanstackDevtools?: boolean;
@@ -8,6 +8,7 @@ import { installDevAuthenticationMiddleware } from "./devAuthentication.js";
8
8
  import { installDevSignInLink } from "./devSignIn.js";
9
9
  import { generateIconTypes } from "./iconGeneration.js";
10
10
  import { generateLayoutFiles, watchLayoutFiles } from "./layoutGeneration.js";
11
+ import { publicFilesInHashedDirs, readOwnHeaders, staticAssetHeaders, unhashedEntries } from "./staticAssetHeaders.js";
11
12
  import { buildClientThenWorker } from "./workerBuild.js";
12
13
  import { VIRTUAL_CAMOX_APP, syncDefinitions, syncDefinitionsToApi } from "./definitionsSync.js";
13
14
  import { publishSeededContent } from "./publishSeededContent.js";
@@ -15,7 +16,7 @@ import { cleanupGeneratedRouteFiles } from "./routeGeneration.js";
15
16
  import { RESOLVED_VIRTUAL_DEV_HTML, createRuntimeRegistryPlugin, loadRuntimeDevModule, resolveRuntimeDevId } from "./runtimeDev.js";
16
17
  import { projectIdSchema } from "@camox/api-contract/ids";
17
18
  import { DEVELOPMENT_DRAFT } from "@camox/api-contract/drafts";
18
- import { createHash } from "node:crypto";
19
+ import { createHash, randomUUID } from "node:crypto";
19
20
  import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
20
21
  import { dirname, join, resolve } from "node:path";
21
22
  import { fileURLToPath } from "node:url";
@@ -34,9 +35,9 @@ const RESOLVED_VIRTUAL_PAGE_CLIENT_URL = "\0virtual:camox/page-client-url";
34
35
  const VIRTUAL_STUDIO_CLIENT = "virtual:camox/studio-client";
35
36
  const RESOLVED_VIRTUAL_STUDIO_CLIENT_URL = "\0virtual:camox/studio-client-url";
36
37
  const RESOLVED_VIRTUAL_BLOCK_CHUNKS = "\0virtual:camox/block-chunks";
37
- const PRODUCTION_API_URL = "https://api.camox.dev";
38
+ const PRODUCTION_API_URL = "https://api.camox.ai";
38
39
  /** Authentication URL to use for Camox authentication (production Camox Dashboard) */
39
- const DEFAULT_AUTHENTICATION_URL = "https://app.camox.dev";
40
+ const DEFAULT_AUTHENTICATION_URL = "https://app.camox.ai";
40
41
  /** What Vite injects into HTML in dev, which the Worker can't ask Vite for itself. */
41
42
  async function getDevHtml(server) {
42
43
  const html = await server.transformIndexHtml("/", "<html><head></head><body></body></html>");
@@ -226,6 +227,16 @@ function camox(options) {
226
227
  generateBundle(_outputOptions, bundle) {
227
228
  if (this.environment.name !== "client") return;
228
229
  blockChunks = collectBlockChunks(bundle, resolvedConfig.root);
230
+ if (!isBuild) return;
231
+ const { publicDir } = resolvedConfig;
232
+ this.emitFile({
233
+ type: "asset",
234
+ fileName: "_headers",
235
+ source: staticAssetHeaders({
236
+ ownHeaders: readOwnHeaders(publicDir),
237
+ unhashedFiles: [...unhashedEntries(bundle), ...publicFilesInHashedDirs(publicDir)]
238
+ })
239
+ });
229
240
  },
230
241
  async config(_config, env) {
231
242
  const iconIds = await generateIconTypes(resolve(_config.root ?? process.cwd()), options.icons);
@@ -248,7 +259,8 @@ function camox(options) {
248
259
  __CAMOX_DEVELOPMENT_DRAFT__: JSON.stringify(!isRelease),
249
260
  __CAMOX_API_URL__: JSON.stringify(apiUrl),
250
261
  __CAMOX_AUTHENTICATION_URL__: JSON.stringify(authenticationUrl),
251
- __CAMOX_PROJECT_ID__: JSON.stringify(projectUuid)
262
+ __CAMOX_PROJECT_ID__: JSON.stringify(projectUuid),
263
+ __CAMOX_BUILD_ID__: JSON.stringify(randomUUID())
252
264
  },
253
265
  environments: { client: { build: { rollupOptions: {
254
266
  onLog: withoutExpectedCamoxLogs(findClientOnLog(_config)),
@@ -1,4 +1,4 @@
1
- import { Router } from "@camox/api-contract";
1
+ import { Router } from "@camox/api-contract/router";
2
2
  import { RouterClient } from "@orpc/server";
3
3
  //#region src/lib/api-client-server.d.ts
4
4
  export type ServerApiClient = RouterClient<Router>;
@@ -1,5 +1,6 @@
1
+ import "./locale-path.js";
1
2
  import "./draft-scope.js";
2
- import { Router } from "@camox/api-contract";
3
+ import { Router } from "@camox/api-contract/router";
3
4
  import { RouterClient } from "@orpc/server";
4
5
  //#region src/lib/api-client.d.ts
5
6
  export type ApiClient = RouterClient<Router>;
@@ -1,3 +1,4 @@
1
+ import "./locale-path.js";
1
2
  import "../features/routes/pageRuntime.js";
2
3
  import "react";
3
4
  import { EndedDraft } from "@camox/api-contract/drafts";
@@ -1,14 +1,5 @@
1
+ import { LocaleView } from "@camox/api-contract/locales";
1
2
  //#region src/lib/locale-path.d.ts
2
- /**
3
- * The URL names the locale: a non-default locale's paths sit under its prefix (`/de/preise`), the
4
- * default locale's have none. The studio's routes follow the same rule (`/de/camox/content`).
5
- * Pure, so the runtime and the browser share it.
6
- */
7
- /** A locale of a project, as `locales.list` returns it. */
8
- export type LocaleView = {
9
- tag: string;
10
- isDefault: boolean;
11
- };
12
3
  /**
13
4
  * Where a page sits in one locale it exists in: the locale's tag and the page's path there, under
14
5
  * the locale's prefix. The runtime finds them for `hreflang` links and the sitemap; the
@@ -19,4 +10,5 @@ export interface AlternatePath {
19
10
  /** The page's runtime path, with its locale's prefix and without the runtime base path. */
20
11
  path: string;
21
12
  }
22
- //#endregion
13
+ //#endregion
14
+ export type { LocaleView };
@@ -1,10 +1,11 @@
1
1
  import { LOCALE_HEADER } from "@camox/api-contract/drafts";
2
+ import { localePrefix, parseLocalePath } from "@camox/api-contract/locales";
2
3
 
3
4
  //#region src/lib/locale-path.ts
4
5
  /**
5
6
  * The URL names the locale: a non-default locale's paths sit under its prefix (`/de/preise`), the
6
7
  * default locale's have none. The studio's routes follow the same rule (`/de/camox/content`).
7
- * Pure, so the runtime and the browser share it.
8
+ * Pure, so the runtime and the browser share it, and the API reads paths by the same rule.
8
9
  */
9
10
  /**
10
11
  * The default locale where nothing says its tag yet: before a render shows a locale, outside the
@@ -21,31 +22,6 @@ const UNNAMED_DEFAULT_LOCALE = {
21
22
  function localeHeaders(locale) {
22
23
  return locale && !locale.isDefault ? { [LOCALE_HEADER]: locale.tag } : {};
23
24
  }
24
- /** A locale's URL prefix: its tag in lowercase. The default locale has none. */
25
- function localePrefix(locale) {
26
- return locale.isDefault ? "" : `/${locale.tag.toLowerCase()}`;
27
- }
28
- /**
29
- * The locale a runtime path names, and the path after its prefix, without search or hash. A first
30
- * segment that isn't a non-default locale's prefix leaves the path the default locale's. A
31
- * locale's home is `/de`; `/de/` names it too.
32
- */
33
- function parseLocalePath(pathname, locales) {
34
- const path = pathname.replace(/[?#].*$/s, "") || "/";
35
- const end = path.indexOf("/", 1);
36
- const segment = end === -1 ? path.slice(1) : path.slice(1, end);
37
- const named = locales.find((locale) => !locale.isDefault && localePrefix(locale) === `/${segment}`);
38
- if (named) return {
39
- locale: named,
40
- path: end === -1 ? "/" : path.slice(end)
41
- };
42
- const fallback = locales.find((locale) => locale.isDefault);
43
- if (!fallback) throw new Error("A project's locales include its default locale.");
44
- return {
45
- locale: fallback,
46
- path
47
- };
48
- }
49
25
  /** A locale-relative path under its locale's prefix, keeping its search and hash: `/` is `/de`. */
50
26
  function localizedPath(path, locale) {
51
27
  const prefix = localePrefix(locale);
@@ -54,4 +30,4 @@ function localizedPath(path, locale) {
54
30
  }
55
31
 
56
32
  //#endregion
57
- export { UNNAMED_DEFAULT_LOCALE, localeHeaders, localePrefix, localizedPath, parseLocalePath };
33
+ export { UNNAMED_DEFAULT_LOCALE, localeHeaders, localizedPath, parseLocalePath };
@@ -25,6 +25,8 @@ async function sendFaviconChange(path, init) {
25
25
  return res;
26
26
  }
27
27
  const projectMutations = {
28
+ update: () => getOrpc().projects.update.mutationOptions(),
29
+ updateAnalytics: () => getOrpc().projects.updateAnalytics.mutationOptions(),
28
30
  updateSocialProfiles: () => getOrpc().projects.updateSocialProfiles.mutationOptions(),
29
31
  uploadFavicon: () => ({ mutationFn: async ({ projectId, file }) => {
30
32
  const formData = new FormData();
@@ -41,7 +43,11 @@ const projectMutations = {
41
43
  };
42
44
  const draftMutations = {
43
45
  publish: () => getOrpc().drafts.publish.mutationOptions(),
44
- discard: () => getOrpc().drafts.discard.mutationOptions()
46
+ discard: () => getOrpc().drafts.discard.mutationOptions(),
47
+ /** Requests the draft's overview again right away, once it failed. */
48
+ retryOverview: () => getOrpc().drafts.retryOverview.mutationOptions(),
49
+ /** Drops the draft's copy of an entity it conflicts on, to use Live's version instead. */
50
+ revertToLive: () => getOrpc().drafts.revertToLive.mutationOptions()
45
51
  };
46
52
  /** Adding and removing locales: content writes, so on Live they start a draft (ADR 0015). */
47
53
  const localeMutations = {
@@ -0,0 +1,29 @@
1
+ import { PlatformLabel } from "../components/PlatformLabel.js";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import { Kbd } from "@camox/ui/kbd";
4
+ import { formatForDisplay } from "@tanstack/react-hotkeys";
5
+
6
+ //#region src/lib/shortcuts.tsx
7
+ /**
8
+ * Formats a shortcut as a keyboard shortcut display component, labelled for the user's platform.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * formatShortcut("Mod+E") // ⌘ E on Mac, Ctrl E elsewhere
13
+ * formatShortcut("L") // L
14
+ * ```
15
+ */
16
+ function formatShortcut(shortcut) {
17
+ if (!shortcut) return null;
18
+ const label = (platform) => formatForDisplay(shortcut, {
19
+ platform,
20
+ separatorToken: " "
21
+ });
22
+ return /* @__PURE__ */ jsx(Kbd, { children: /* @__PURE__ */ jsx(PlatformLabel, {
23
+ mac: label("mac"),
24
+ other: label("windows")
25
+ }) });
26
+ }
27
+
28
+ //#endregion
29
+ export { formatShortcut };
@@ -1,3 +1,4 @@
1
+ import "./locale-path.js";
1
2
  import "../core/createLayout.js";
2
3
  import "../features/runtime/runtime.js";
3
4
  import "./draft-scope.js";
package/dist/lib/utils.js CHANGED
@@ -1,112 +1,9 @@
1
- import { PlatformLabel } from "../components/PlatformLabel.js";
2
- import { c } from "react/compiler-runtime";
3
- import { jsx } from "react/jsx-runtime";
4
- import { Kbd } from "@camox/ui/kbd";
5
1
  import { INPUT_BASE_STYLES, INPUT_FOCUS_STYLES, cn } from "@camox/ui/utils";
6
2
 
7
3
  //#region src/lib/utils.tsx
8
4
  function checkIfInputFocused(document = window.document) {
9
5
  return document.activeElement?.tagName === "INPUT" || document.activeElement?.tagName === "TEXTAREA" || (document.activeElement?.getAttribute("contenteditable") ?? "false") === "true";
10
6
  }
11
- /**
12
- * Formats an action shortcut into a keyboard shortcut display component.
13
- * Automatically detects the platform and uses the appropriate modifier keys.
14
- *
15
- * @param shortcut - The shortcut configuration from an Action
16
- * @returns React element displaying the keyboard shortcut
17
- *
18
- * @example
19
- * ```tsx
20
- * formatShortcut({ key: "e", withMeta: true }) // Returns <KbdGroup><Kbd>⌘</Kbd><Kbd>E</Kbd></KbdGroup>
21
- * formatShortcut({ key: "l" }) // Returns <Kbd>L</Kbd>
22
- * ```
23
- */
24
- function formatShortcut(shortcut) {
25
- if (!shortcut) return null;
26
- return /* @__PURE__ */ jsx(Shortcut, { shortcut });
27
- }
28
- function Shortcut(t0) {
29
- const $ = c(14);
30
- const { shortcut } = t0;
31
- let t1;
32
- bb0: {
33
- if (shortcut.key === "Enter") {
34
- t1 = "↵";
35
- break bb0;
36
- }
37
- if (shortcut.key === "Escape") {
38
- t1 = "Esc";
39
- break bb0;
40
- }
41
- if (shortcut.key === "Backspace") {
42
- t1 = "⌫";
43
- break bb0;
44
- }
45
- if (shortcut.key === "ArrowUp") {
46
- t1 = "↑";
47
- break bb0;
48
- }
49
- if (shortcut.key === "ArrowDown") {
50
- t1 = "↓";
51
- break bb0;
52
- }
53
- if (shortcut.key === "ArrowLeft") {
54
- t1 = "←";
55
- break bb0;
56
- }
57
- if (shortcut.key === "ArrowRight") {
58
- t1 = "→";
59
- break bb0;
60
- }
61
- let t2;
62
- if ($[0] !== shortcut.key) {
63
- t2 = shortcut.key.toUpperCase();
64
- $[0] = shortcut.key;
65
- $[1] = t2;
66
- } else t2 = $[1];
67
- t1 = t2;
68
- }
69
- const formattedKey = t1;
70
- let t2;
71
- if ($[2] !== formattedKey || $[3] !== shortcut.withAlt || $[4] !== shortcut.withMeta || $[5] !== shortcut.withShift) {
72
- t2 = (mac) => {
73
- const modifiers = [];
74
- if (shortcut.withMeta) modifiers.push(mac ? "⌘" : "Ctrl");
75
- if (shortcut.withAlt) modifiers.push(mac ? "⌥" : "Alt");
76
- if (shortcut.withShift) modifiers.push(mac ? "⇧" : "Shift");
77
- return `${modifiers.join()} ${formattedKey}`.trim();
78
- };
79
- $[2] = formattedKey;
80
- $[3] = shortcut.withAlt;
81
- $[4] = shortcut.withMeta;
82
- $[5] = shortcut.withShift;
83
- $[6] = t2;
84
- } else t2 = $[6];
85
- const label = t2;
86
- let t3;
87
- if ($[7] !== label) {
88
- t3 = label(true);
89
- $[7] = label;
90
- $[8] = t3;
91
- } else t3 = $[8];
92
- let t4;
93
- if ($[9] !== label) {
94
- t4 = label(false);
95
- $[9] = label;
96
- $[10] = t4;
97
- } else t4 = $[10];
98
- let t5;
99
- if ($[11] !== t3 || $[12] !== t4) {
100
- t5 = /* @__PURE__ */ jsx(Kbd, { children: /* @__PURE__ */ jsx(PlatformLabel, {
101
- mac: t3,
102
- other: t4
103
- }) });
104
- $[11] = t3;
105
- $[12] = t4;
106
- $[13] = t5;
107
- } else t5 = $[13];
108
- return t5;
109
- }
110
7
 
111
8
  //#endregion
112
- export { INPUT_BASE_STYLES, INPUT_FOCUS_STYLES, checkIfInputFocused, cn, formatShortcut };
9
+ export { INPUT_BASE_STYLES, INPUT_FOCUS_STYLES, checkIfInputFocused, cn };
package/dist/package.js CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.48.0";
2
+ var version = "0.51.0";
3
3
 
4
4
  //#endregion
5
5
  export { version };