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.
- package/dist/core/layoutContext.d.ts +1 -0
- package/dist/features/canvas/sidebar/CanvasInspector.js +1 -1
- package/dist/features/content/ContentAssets.js +84 -88
- package/dist/features/preview/CamoxPreview.js +1 -1
- package/dist/features/preview/EditablePageExperience.js +17 -9
- package/dist/features/preview/components/BlockActionsPopover.js +11 -35
- package/dist/features/preview/components/FieldToolbar.js +1 -1
- package/dist/features/preview/components/MobilePreviewDrawer.js +6 -9
- package/dist/features/preview/components/PreviewPanel.js +10 -64
- package/dist/features/preview/components/PreviewToolbar.js +1 -1
- package/dist/features/preview/previewConstants.js +2 -8
- package/dist/features/preview/usePreviewViewportActions.js +1 -1
- package/dist/features/provider/AuthenticatedCamoxProvider.js +1 -1
- package/dist/features/provider/components/CommandPalette.js +2 -5
- package/dist/features/provider/signInShortcut.js +1 -2
- package/dist/features/provider/useAdminShortcuts.js +47 -53
- package/dist/features/routes/pageRuntime.d.ts +2 -1
- package/dist/features/routes/pageRuntime.js +27 -20
- package/dist/features/runtime/publicCache.js +83 -0
- package/dist/features/runtime/runtime.d.ts +13 -3
- package/dist/features/runtime/runtime.js +173 -79
- package/dist/features/runtime/worker.d.ts +5 -1
- package/dist/features/runtime/worker.js +20 -3
- package/dist/features/settings/AnalyticsSettings.js +622 -0
- package/dist/features/settings/CamoxSettings.js +95 -0
- package/dist/features/settings/FaviconSettings.js +254 -0
- package/dist/features/settings/ProjectNameSettings.js +145 -0
- package/dist/features/settings/SettingsPart.js +73 -0
- package/dist/features/settings/SocialProfilesSettings.js +245 -0
- package/dist/features/settings/useProjectMutation.js +61 -0
- package/dist/features/studio/components/DraftsControl.js +136 -131
- package/dist/features/studio/components/LocalePicker.js +178 -174
- package/dist/features/studio/components/Navbar.js +107 -66
- package/dist/features/studio/components/ProjectMenu.js +118 -170
- package/dist/features/studio/components/ReviewDraftSheet.js +1046 -990
- package/dist/features/studio/components/UserButton.js +1 -1
- package/dist/features/studio/routes.js +3 -1
- package/dist/features/studio/showInCanvas.js +129 -0
- package/dist/features/vite/definitionsSync.js +4 -5
- package/dist/features/vite/runtimeDev.js +32 -17
- package/dist/features/vite/staticAssetHeaders.js +52 -0
- package/dist/features/vite/vite.d.ts +1 -1
- package/dist/features/vite/vite.js +16 -4
- package/dist/lib/api-client-server.d.ts +1 -1
- package/dist/lib/api-client.d.ts +2 -1
- package/dist/lib/draft-scope.d.ts +1 -0
- package/dist/lib/locale-path.d.ts +3 -11
- package/dist/lib/locale-path.js +3 -27
- package/dist/lib/queries.js +7 -1
- package/dist/lib/shortcuts.js +29 -0
- package/dist/lib/studio-reads.d.ts +1 -0
- package/dist/lib/utils.js +1 -104
- package/dist/package.js +1 -1
- package/dist/studio.css +1 -1
- package/package.json +5 -4
- package/dist/features/studio/components/FaviconDialog.js +0 -318
- 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
|
-
|
|
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
|
|
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
|
|
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 {
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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)),
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -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,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 };
|
package/dist/lib/locale-path.js
CHANGED
|
@@ -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,
|
|
33
|
+
export { UNNAMED_DEFAULT_LOCALE, localeHeaders, localizedPath, parseLocalePath };
|
package/dist/lib/queries.js
CHANGED
|
@@ -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 };
|
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
|
|
9
|
+
export { INPUT_BASE_STYLES, INPUT_FOCUS_STYLES, checkIfInputFocused, cn };
|
package/dist/package.js
CHANGED