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.
- package/dist/core/createApp.d.ts +1 -0
- package/dist/core/createApp.js +1 -0
- package/dist/core/editing/contentEdits.js +78 -0
- package/dist/core/editing/createEditableBlock.js +277 -311
- package/dist/core/layoutContext.d.ts +1 -0
- package/dist/core/lib/contentType.js +2 -2
- package/dist/core/recordItem.js +3 -3
- package/dist/features/canvas/CamoxCanvas.js +15 -15
- package/dist/features/canvas/CanvasLoadingScreen.js +15 -11
- package/dist/features/canvas/CanvasPageFrame.js +81 -87
- package/dist/features/canvas/CanvasPreviewPageSlot.js +1 -1
- package/dist/features/canvas/DerivedPagePathInput.js +12 -12
- package/dist/features/canvas/canvasOverlayGeometry.js +1 -1
- package/dist/features/canvas/canvasStore.js +2 -2
- package/dist/features/canvas/sidebar/CanvasInspector.js +1 -1
- package/dist/features/canvas/sidebar/CuratedPageInspector.js +1 -1
- package/dist/features/canvas/sidebar/LayoutPageBlocks.js +23 -31
- package/dist/features/canvas/useCanvasCamera.js +1 -1
- package/dist/features/canvas/useDiscoveredPreviewPaths.js +16 -9
- package/dist/features/content/ContentAssets.js +84 -88
- package/dist/features/page/CuratedPageContent.js +1 -1
- package/dist/features/preview/CamoxPreview.d.ts +8 -4
- package/dist/features/preview/CamoxPreview.js +258 -443
- package/dist/features/preview/EditablePageContent.js +244 -2
- package/dist/features/preview/EditablePageExperience.js +19 -13
- package/dist/features/preview/components/AddBlockSheet.js +55 -15
- package/dist/features/preview/components/BlockActionsPopover.js +23 -51
- package/dist/features/preview/components/BlockPeek.js +66 -0
- package/dist/features/preview/components/BlockThumbnail.js +134 -119
- package/dist/features/preview/components/CanvasPreview.js +1 -1
- package/dist/features/preview/components/CreatePageModal.js +150 -156
- package/dist/features/preview/components/FieldToolbar.js +1 -1
- package/dist/features/preview/components/Frame.js +27 -4
- package/dist/features/preview/components/MobilePreviewDrawer.js +6 -9
- package/dist/features/preview/components/PageInfo.js +161 -126
- package/dist/features/preview/components/PagePicker.js +2 -2
- package/dist/features/preview/components/PageTree.js +174 -190
- package/dist/features/preview/components/PreviewPanel.js +13 -67
- package/dist/features/preview/components/PreviewToolbar.js +1 -1
- package/dist/features/preview/components/ReferenceFieldEditor.js +4 -4
- package/dist/features/preview/components/ReferenceListFieldEditor.js +4 -4
- package/dist/features/preview/components/RemovedBlockPlaceholder.js +37 -70
- package/dist/features/preview/components/RepeatableItemsList.js +1 -1
- package/dist/features/preview/components/SelectionEditor.js +248 -256
- package/dist/features/preview/components/StudioEditingFallback.js +22 -26
- package/dist/features/preview/components/useFieldWriter.js +19 -19
- package/dist/features/preview/components/useRecordView.js +105 -147
- package/dist/features/preview/components/useRepeatableItemActions.js +0 -2
- package/dist/features/preview/pageRecord.js +30 -24
- package/dist/features/preview/previewConstants.js +2 -8
- package/dist/features/preview/previewStore.js +2 -0
- package/dist/features/preview/studioNotice.js +20 -0
- package/dist/features/preview/usePreviewViewportActions.js +1 -1
- package/dist/features/provider/AuthenticatedCamoxProvider.js +47 -31
- 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/renderer/RendererApp.js +542 -0
- package/dist/features/renderer/RendererFrames.js +450 -0
- package/dist/features/renderer/definitionAnswers.js +26 -0
- package/dist/features/renderer/protocol.js +35 -0
- package/dist/features/renderer/rendererHost.js +249 -0
- package/dist/features/renderer/studioConnection.js +186 -0
- package/dist/features/routes/pageRuntime.d.ts +2 -1
- package/dist/features/routes/pageRuntime.js +34 -22
- package/dist/features/runtime/RuntimeChrome.js +2 -5
- package/dist/features/runtime/assetPrefix.js +14 -0
- package/dist/features/runtime/breadcrumbs.js +1 -1
- package/dist/features/runtime/editingFallback.js +4 -8
- package/dist/features/runtime/livePages.js +1 -1
- package/dist/features/runtime/pageApp.js +57 -83
- package/dist/features/runtime/pageClient.js +1 -1
- package/dist/features/runtime/pageNavigation.js +3 -2
- package/dist/features/runtime/publicCache.js +95 -0
- package/dist/features/runtime/publishingDraft.js +23 -0
- package/dist/features/runtime/rendererClient.d.ts +8 -0
- package/dist/features/runtime/rendererClient.js +77 -0
- package/dist/features/runtime/runtime.d.ts +22 -3
- package/dist/features/runtime/runtime.js +209 -96
- package/dist/features/runtime/runtimeRoutes.d.ts +2 -0
- package/dist/features/runtime/runtimeRoutes.js +2 -0
- package/dist/features/runtime/worker.d.ts +14 -1
- package/dist/features/runtime/worker.js +34 -4
- 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/FollowAgentProvider.js +10 -10
- 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 +119 -171
- package/dist/features/studio/components/ReviewDraftSheet.js +1151 -991
- package/dist/features/studio/components/ThreadMessages.js +100 -65
- package/dist/features/studio/components/UserButton.js +2 -2
- package/dist/features/studio/routes.js +3 -1
- package/dist/features/studio/showInCanvas.js +129 -0
- package/dist/features/vite/definitionsSync.js +5 -6
- package/dist/features/vite/hostedChecks.js +127 -0
- package/dist/features/vite/runtimeDev.js +58 -17
- package/dist/features/vite/staticAssetHeaders.js +52 -0
- package/dist/features/vite/vite.d.ts +1 -1
- package/dist/features/vite/vite.js +49 -9
- package/dist/hooks/use-page-destinations.js +10 -10
- 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/revisions.js +20 -0
- package/dist/lib/shortcuts.js +29 -0
- package/dist/lib/studio-definitions.js +160 -0
- package/dist/lib/studio-reads.d.ts +1 -0
- package/dist/lib/studio-reads.js +65 -21
- package/dist/lib/use-project-room.js +16 -10
- package/dist/lib/utils.js +1 -104
- package/dist/package.js +1 -1
- package/dist/studio.css +1 -1
- package/package.json +9 -4
- package/skills/camox/SKILL.md +4 -0
- package/skills/camox/references/artifacts.md +1 -1
- package/dist/features/studio/components/FaviconDialog.js +0 -318
- package/dist/features/studio/components/SocialProfilesDialog.js +0 -342
|
@@ -22,6 +22,10 @@ const VIRTUAL_STUDIO_CLIENT = "virtual:camox/studio-client";
|
|
|
22
22
|
const RESOLVED_VIRTUAL_STUDIO_CLIENT = "\0" + VIRTUAL_STUDIO_CLIENT;
|
|
23
23
|
const VIRTUAL_STUDIO_CLIENT_URL = "virtual:camox/studio-client-url";
|
|
24
24
|
const RESOLVED_VIRTUAL_STUDIO_CLIENT_URL = "\0" + VIRTUAL_STUDIO_CLIENT_URL;
|
|
25
|
+
const VIRTUAL_RENDERER_CLIENT = "virtual:camox/renderer-client";
|
|
26
|
+
const RESOLVED_VIRTUAL_RENDERER_CLIENT = "\0" + VIRTUAL_RENDERER_CLIENT;
|
|
27
|
+
const VIRTUAL_RENDERER_CLIENT_URL = "virtual:camox/renderer-client-url";
|
|
28
|
+
const RESOLVED_VIRTUAL_RENDERER_CLIENT_URL = "\0" + VIRTUAL_RENDERER_CLIENT_URL;
|
|
25
29
|
const VIRTUAL_BLOCK_CHUNKS = "virtual:camox/block-chunks";
|
|
26
30
|
const RESOLVED_VIRTUAL_BLOCK_CHUNKS = "\0" + VIRTUAL_BLOCK_CHUNKS;
|
|
27
31
|
const VIRTUAL_APP_STYLESHEET_URL = "virtual:camox/app-stylesheet-url";
|
|
@@ -64,6 +68,7 @@ function resolveCamoxSourceImport(id) {
|
|
|
64
68
|
"camox/_internal/pageServer": resolve(sdkRoot, "src/features/runtime/pageServer.tsx"),
|
|
65
69
|
"camox/_internal/pageClient": resolve(sdkRoot, "src/features/runtime/pageClient.tsx"),
|
|
66
70
|
"camox/_internal/studioClient": resolve(sdkRoot, "src/features/runtime/studioClient.tsx"),
|
|
71
|
+
"camox/_internal/rendererClient": resolve(sdkRoot, "src/features/runtime/rendererClient.tsx"),
|
|
67
72
|
"camox/_internal/studioServer": resolve(sdkRoot, "src/features/runtime/studioServer.tsx"),
|
|
68
73
|
"camox/_internal/runtime": resolve(sdkRoot, "src/features/runtime/runtime.ts"),
|
|
69
74
|
"camox/worker": resolve(sdkRoot, "src/features/runtime/worker.ts")
|
|
@@ -167,6 +172,13 @@ import { camoxApp } from "virtual:camox/app";
|
|
|
167
172
|
hydrateStudioWithApp(camoxApp);
|
|
168
173
|
`;
|
|
169
174
|
}
|
|
175
|
+
function generateVirtualRendererClient() {
|
|
176
|
+
return `import { startRendererWithApp } from "camox/_internal/rendererClient";
|
|
177
|
+
import { camoxApp } from "virtual:camox/app";
|
|
178
|
+
|
|
179
|
+
startRendererWithApp(camoxApp);
|
|
180
|
+
`;
|
|
181
|
+
}
|
|
170
182
|
function wrapViteDevId(id) {
|
|
171
183
|
return `/@id/${id.replace("\0", "__x00__")}`;
|
|
172
184
|
}
|
|
@@ -176,6 +188,9 @@ function generateVirtualPageClientUrl() {
|
|
|
176
188
|
function generateVirtualStudioClientUrl() {
|
|
177
189
|
return `export default ${JSON.stringify(wrapViteDevId(RESOLVED_VIRTUAL_STUDIO_CLIENT))};`;
|
|
178
190
|
}
|
|
191
|
+
function generateVirtualRendererClientUrl() {
|
|
192
|
+
return `export default ${JSON.stringify(wrapViteDevId(RESOLVED_VIRTUAL_RENDERER_CLIENT))};`;
|
|
193
|
+
}
|
|
179
194
|
function generateVirtualAppStylesheetUrl() {
|
|
180
195
|
return `import stylesheetUrl from "/src/styles.css?url";
|
|
181
196
|
|
|
@@ -183,7 +198,10 @@ export default stylesheetUrl;
|
|
|
183
198
|
`;
|
|
184
199
|
}
|
|
185
200
|
function generateVirtualCamoxServer(runtimeBasePath) {
|
|
186
|
-
return `import {
|
|
201
|
+
return `import {
|
|
202
|
+
handleCamoxRequest as handleRuntimeRequest,
|
|
203
|
+
isPublicRequest as isPublicRuntimeRequest,
|
|
204
|
+
} from "camox/_internal/runtime";
|
|
187
205
|
import { blockModulePaths, camoxApp } from "virtual:camox/app";
|
|
188
206
|
import blockChunks from "virtual:camox/block-chunks";
|
|
189
207
|
import { camoxDocument } from "virtual:camox/document";
|
|
@@ -191,6 +209,7 @@ import appStylesheetUrl from "virtual:camox/app-stylesheet-url";
|
|
|
191
209
|
import studioStylesheetUrl from "virtual:camox-studio-css";
|
|
192
210
|
import pageClientEntryUrl from "virtual:camox/page-client-url";
|
|
193
211
|
import studioClientEntryUrl from "virtual:camox/studio-client-url";
|
|
212
|
+
import rendererClientEntryUrl from "virtual:camox/renderer-client-url";
|
|
194
213
|
import { renderPage } from "virtual:camox/page-server";
|
|
195
214
|
import { renderStudio } from "virtual:camox/studio-server";
|
|
196
215
|
import devHtml from "virtual:camox/dev-html";
|
|
@@ -198,23 +217,41 @@ import devHtml from "virtual:camox/dev-html";
|
|
|
198
217
|
export { devHtml };
|
|
199
218
|
export const runtimeBasePath = ${JSON.stringify(normalizeRuntimeBasePath(runtimeBasePath))};
|
|
200
219
|
|
|
220
|
+
const options = {
|
|
221
|
+
apiUrl: __CAMOX_API_URL__,
|
|
222
|
+
authenticationUrl: __CAMOX_AUTHENTICATION_URL__,
|
|
223
|
+
blockPreloads: { modulePaths: blockModulePaths, chunks: blockChunks },
|
|
224
|
+
pageClientEntryUrl,
|
|
225
|
+
studioClientEntryUrl,
|
|
226
|
+
rendererClientEntryUrl,
|
|
227
|
+
developmentDraft: __CAMOX_DEVELOPMENT_DRAFT__,
|
|
228
|
+
getCamoxApp: async () => camoxApp,
|
|
229
|
+
getDocument: async () => camoxDocument,
|
|
230
|
+
projectUuid: __CAMOX_PROJECT_ID__,
|
|
231
|
+
renderPage,
|
|
232
|
+
renderStudio,
|
|
233
|
+
runtimeBasePath: ${JSON.stringify(runtimeBasePath)},
|
|
234
|
+
stylesheetUrl: appStylesheetUrl,
|
|
235
|
+
studioStylesheetUrl,
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
export const publicCacheSource = {
|
|
239
|
+
apiUrl: __CAMOX_API_URL__,
|
|
240
|
+
projectUuid: __CAMOX_PROJECT_ID__,
|
|
241
|
+
build: __CAMOX_BUILD_ID__,
|
|
242
|
+
};
|
|
243
|
+
|
|
201
244
|
export async function handleCamoxRequest(request) {
|
|
202
|
-
return handleRuntimeRequest(request,
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
renderPage,
|
|
213
|
-
renderStudio,
|
|
214
|
-
runtimeBasePath: ${JSON.stringify(runtimeBasePath)},
|
|
215
|
-
stylesheetUrl: appStylesheetUrl,
|
|
216
|
-
studioStylesheetUrl,
|
|
217
|
-
});
|
|
245
|
+
return handleRuntimeRequest(request, options);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function isPublicRequest(request) {
|
|
249
|
+
return isPublicRuntimeRequest(request, options);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// JSON, as \`definitions.sync\` receives it: what isn't JSON never reaches the API.
|
|
253
|
+
export async function readDefinitionSet() {
|
|
254
|
+
return JSON.parse(JSON.stringify(await camoxApp.getDefinitionSet()));
|
|
218
255
|
}
|
|
219
256
|
`;
|
|
220
257
|
}
|
|
@@ -232,6 +269,8 @@ function resolveRuntimeDevId(id, importer) {
|
|
|
232
269
|
if (id === VIRTUAL_PAGE_CLIENT_URL) return RESOLVED_VIRTUAL_PAGE_CLIENT_URL;
|
|
233
270
|
if (id === VIRTUAL_STUDIO_CLIENT) return RESOLVED_VIRTUAL_STUDIO_CLIENT;
|
|
234
271
|
if (id === VIRTUAL_STUDIO_CLIENT_URL) return RESOLVED_VIRTUAL_STUDIO_CLIENT_URL;
|
|
272
|
+
if (id === VIRTUAL_RENDERER_CLIENT) return RESOLVED_VIRTUAL_RENDERER_CLIENT;
|
|
273
|
+
if (id === VIRTUAL_RENDERER_CLIENT_URL) return RESOLVED_VIRTUAL_RENDERER_CLIENT_URL;
|
|
235
274
|
if (id === VIRTUAL_APP_STYLESHEET_URL) return RESOLVED_VIRTUAL_APP_STYLESHEET_URL;
|
|
236
275
|
if (id === VIRTUAL_BLOCK_CHUNKS) return RESOLVED_VIRTUAL_BLOCK_CHUNKS;
|
|
237
276
|
if (id === VIRTUAL_STUDIO_CSS) return RESOLVED_VIRTUAL_STUDIO_CSS;
|
|
@@ -249,6 +288,8 @@ function loadRuntimeDevModule(id, options = {}) {
|
|
|
249
288
|
if (id === RESOLVED_VIRTUAL_PAGE_CLIENT_URL) return generateVirtualPageClientUrl();
|
|
250
289
|
if (id === RESOLVED_VIRTUAL_STUDIO_CLIENT) return generateVirtualStudioClient();
|
|
251
290
|
if (id === RESOLVED_VIRTUAL_STUDIO_CLIENT_URL) return generateVirtualStudioClientUrl();
|
|
291
|
+
if (id === RESOLVED_VIRTUAL_RENDERER_CLIENT) return generateVirtualRendererClient();
|
|
292
|
+
if (id === RESOLVED_VIRTUAL_RENDERER_CLIENT_URL) return generateVirtualRendererClientUrl();
|
|
252
293
|
if (id === RESOLVED_VIRTUAL_APP_STYLESHEET_URL) return generateVirtualAppStylesheetUrl();
|
|
253
294
|
if (id === RESOLVED_VIRTUAL_STUDIO_CSS) {
|
|
254
295
|
const cssUrl = `/@fs/${resolve(sdkRoot, "dist/studio.css").replaceAll("\\", "/").replace(/^\/+/, "")}`;
|
|
@@ -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;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createServerApiClient } from "../../lib/api-client-server.js";
|
|
2
|
+
import { ASSET_PREFIX_GLOBAL } from "../runtime/assetPrefix.js";
|
|
2
3
|
import { transformArtifactRuntime } from "./artifactRuntimeMinification.js";
|
|
3
4
|
import { readAuthTokenForUrl } from "./auth.js";
|
|
4
5
|
import { watchNewBlockFiles } from "./blockBoilerplate.js";
|
|
@@ -6,8 +7,10 @@ import { collectBlockChunks } from "./blockChunks.js";
|
|
|
6
7
|
import { withoutExpectedCamoxLogs } from "./bundlerLogs.js";
|
|
7
8
|
import { installDevAuthenticationMiddleware } from "./devAuthentication.js";
|
|
8
9
|
import { installDevSignInLink } from "./devSignIn.js";
|
|
10
|
+
import { HOSTED_BUILD_ENV, forbiddenImport, misnamedBlock, readHostedProject } from "./hostedChecks.js";
|
|
9
11
|
import { generateIconTypes } from "./iconGeneration.js";
|
|
10
12
|
import { generateLayoutFiles, watchLayoutFiles } from "./layoutGeneration.js";
|
|
13
|
+
import { publicFilesInHashedDirs, readOwnHeaders, staticAssetHeaders, unhashedEntries } from "./staticAssetHeaders.js";
|
|
11
14
|
import { buildClientThenWorker } from "./workerBuild.js";
|
|
12
15
|
import { VIRTUAL_CAMOX_APP, syncDefinitions, syncDefinitionsToApi } from "./definitionsSync.js";
|
|
13
16
|
import { publishSeededContent } from "./publishSeededContent.js";
|
|
@@ -15,9 +18,9 @@ import { cleanupGeneratedRouteFiles } from "./routeGeneration.js";
|
|
|
15
18
|
import { RESOLVED_VIRTUAL_DEV_HTML, createRuntimeRegistryPlugin, loadRuntimeDevModule, resolveRuntimeDevId } from "./runtimeDev.js";
|
|
16
19
|
import { projectIdSchema } from "@camox/api-contract/ids";
|
|
17
20
|
import { DEVELOPMENT_DRAFT } from "@camox/api-contract/drafts";
|
|
18
|
-
import { createHash } from "node:crypto";
|
|
21
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
19
22
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
-
import { dirname, join, resolve } from "node:path";
|
|
23
|
+
import { dirname, join, posix, resolve } from "node:path";
|
|
21
24
|
import { fileURLToPath } from "node:url";
|
|
22
25
|
import { createServer } from "vite-plus";
|
|
23
26
|
|
|
@@ -33,10 +36,14 @@ const VIRTUAL_PAGE_CLIENT = "virtual:camox/page-client";
|
|
|
33
36
|
const RESOLVED_VIRTUAL_PAGE_CLIENT_URL = "\0virtual:camox/page-client-url";
|
|
34
37
|
const VIRTUAL_STUDIO_CLIENT = "virtual:camox/studio-client";
|
|
35
38
|
const RESOLVED_VIRTUAL_STUDIO_CLIENT_URL = "\0virtual:camox/studio-client-url";
|
|
39
|
+
const VIRTUAL_RENDERER_CLIENT = "virtual:camox/renderer-client";
|
|
40
|
+
const RESOLVED_VIRTUAL_RENDERER_CLIENT_URL = "\0virtual:camox/renderer-client-url";
|
|
36
41
|
const RESOLVED_VIRTUAL_BLOCK_CHUNKS = "\0virtual:camox/block-chunks";
|
|
37
|
-
|
|
42
|
+
/** A built file's URL in the server bundle: under the asset prefix, read when the bundle loads. */
|
|
43
|
+
const serverAssetUrl = (file) => `(globalThis.${ASSET_PREFIX_GLOBAL} ?? "/") + ${JSON.stringify(file)}`;
|
|
44
|
+
const PRODUCTION_API_URL = "https://api.camox.ai";
|
|
38
45
|
/** Authentication URL to use for Camox authentication (production Camox Dashboard) */
|
|
39
|
-
const DEFAULT_AUTHENTICATION_URL = "https://app.camox.
|
|
46
|
+
const DEFAULT_AUTHENTICATION_URL = "https://app.camox.ai";
|
|
40
47
|
/** What Vite injects into HTML in dev, which the Worker can't ask Vite for itself. */
|
|
41
48
|
async function getDevHtml(server) {
|
|
42
49
|
const html = await server.transformIndexHtml("/", "<html><head></head><body></body></html>");
|
|
@@ -173,6 +180,8 @@ function camox(options) {
|
|
|
173
180
|
let isRelease = false;
|
|
174
181
|
/** A release's build, without syncing to Live: what the upgrade job checks a project with. */
|
|
175
182
|
let isDryRun = false;
|
|
183
|
+
/** Set when the build checks a hosted project's code. */
|
|
184
|
+
let hostedProject;
|
|
176
185
|
let resolvedConfig;
|
|
177
186
|
let localAuth = null;
|
|
178
187
|
let blockChunks = {};
|
|
@@ -181,6 +190,10 @@ function camox(options) {
|
|
|
181
190
|
name: "camox",
|
|
182
191
|
enforce: "pre",
|
|
183
192
|
resolveId(id, importer) {
|
|
193
|
+
if (hostedProject) {
|
|
194
|
+
const forbidden = forbiddenImport(hostedProject, id, importer);
|
|
195
|
+
if (forbidden) this.error(forbidden);
|
|
196
|
+
}
|
|
184
197
|
const runtimeId = resolveRuntimeDevId(id, importer);
|
|
185
198
|
if (runtimeId) return runtimeId;
|
|
186
199
|
if (id === VIRTUAL_STUDIO_CSS) return RESOLVED_VIRTUAL_STUDIO_CSS;
|
|
@@ -198,7 +211,8 @@ function camox(options) {
|
|
|
198
211
|
fileName,
|
|
199
212
|
source: css
|
|
200
213
|
});
|
|
201
|
-
return `export default ${JSON.stringify(
|
|
214
|
+
if (this.environment.config.consumer === "client") return `export default new URL(/* @vite-ignore */ ${JSON.stringify(`../${fileName}`)}, import.meta.url).href;`;
|
|
215
|
+
return `export default ${serverAssetUrl(fileName)};`;
|
|
202
216
|
}
|
|
203
217
|
const cssUrl = `/@fs/${cssPath.replaceAll("\\", "/").replace(/^\/+/, "")}`;
|
|
204
218
|
return `export default ${JSON.stringify(cssUrl)};`;
|
|
@@ -208,8 +222,9 @@ function camox(options) {
|
|
|
208
222
|
const css = readFileSync(cssPath, "utf-8");
|
|
209
223
|
return `export default ${JSON.stringify(css)};`;
|
|
210
224
|
}
|
|
211
|
-
if (id === RESOLVED_VIRTUAL_PAGE_CLIENT_URL && isBuild) return
|
|
212
|
-
if (id === RESOLVED_VIRTUAL_STUDIO_CLIENT_URL && isBuild) return
|
|
225
|
+
if (id === RESOLVED_VIRTUAL_PAGE_CLIENT_URL && isBuild) return `export default ${serverAssetUrl("_chunks/camox-page-client.mjs")};`;
|
|
226
|
+
if (id === RESOLVED_VIRTUAL_STUDIO_CLIENT_URL && isBuild) return `export default ${serverAssetUrl("_chunks/camox-studio-client.mjs")};`;
|
|
227
|
+
if (id === RESOLVED_VIRTUAL_RENDERER_CLIENT_URL && isBuild) return `export default ${serverAssetUrl("_chunks/camox-renderer-client.mjs")};`;
|
|
213
228
|
if (id === RESOLVED_VIRTUAL_BLOCK_CHUNKS && isBuild) return `export default ${JSON.stringify(blockChunks)};`;
|
|
214
229
|
if (id === "\0virtual:camox/dev-html") {
|
|
215
230
|
if (!devServer) return "export default null;";
|
|
@@ -221,11 +236,25 @@ function camox(options) {
|
|
|
221
236
|
});
|
|
222
237
|
},
|
|
223
238
|
transform(code, id) {
|
|
239
|
+
if (hostedProject) {
|
|
240
|
+
const misnamed = misnamedBlock(hostedProject, id, code);
|
|
241
|
+
if (misnamed) this.error(misnamed.message, misnamed.at);
|
|
242
|
+
}
|
|
224
243
|
return transformArtifactRuntime(code, id);
|
|
225
244
|
},
|
|
226
245
|
generateBundle(_outputOptions, bundle) {
|
|
227
246
|
if (this.environment.name !== "client") return;
|
|
228
247
|
blockChunks = collectBlockChunks(bundle, resolvedConfig.root);
|
|
248
|
+
if (!isBuild) return;
|
|
249
|
+
const { publicDir } = resolvedConfig;
|
|
250
|
+
this.emitFile({
|
|
251
|
+
type: "asset",
|
|
252
|
+
fileName: "_headers",
|
|
253
|
+
source: staticAssetHeaders({
|
|
254
|
+
ownHeaders: readOwnHeaders(publicDir),
|
|
255
|
+
unhashedFiles: [...unhashedEntries(bundle), ...publicFilesInHashedDirs(publicDir)]
|
|
256
|
+
})
|
|
257
|
+
});
|
|
229
258
|
},
|
|
230
259
|
async config(_config, env) {
|
|
231
260
|
const iconIds = await generateIconTypes(resolve(_config.root ?? process.cwd()), options.icons);
|
|
@@ -238,6 +267,14 @@ function camox(options) {
|
|
|
238
267
|
if (isBuild && !localAuth) throw new Error("Camox: not authenticated. Run `npx camox login` before building the site.");
|
|
239
268
|
}
|
|
240
269
|
return {
|
|
270
|
+
...isBuild && {
|
|
271
|
+
base: "./",
|
|
272
|
+
experimental: { renderBuiltUrl(filename, { ssr, hostType }) {
|
|
273
|
+
if (hostType === "css") return posix.relative("assets", filename);
|
|
274
|
+
if (!ssr) return { relative: true };
|
|
275
|
+
return { runtime: serverAssetUrl(filename) };
|
|
276
|
+
} }
|
|
277
|
+
},
|
|
241
278
|
build: { emitAssets: true },
|
|
242
279
|
..._config.builder?.buildApp ? {} : { builder: { buildApp: buildClientThenWorker } },
|
|
243
280
|
define: {
|
|
@@ -248,13 +285,15 @@ function camox(options) {
|
|
|
248
285
|
__CAMOX_DEVELOPMENT_DRAFT__: JSON.stringify(!isRelease),
|
|
249
286
|
__CAMOX_API_URL__: JSON.stringify(apiUrl),
|
|
250
287
|
__CAMOX_AUTHENTICATION_URL__: JSON.stringify(authenticationUrl),
|
|
251
|
-
__CAMOX_PROJECT_ID__: JSON.stringify(projectUuid)
|
|
288
|
+
__CAMOX_PROJECT_ID__: JSON.stringify(projectUuid),
|
|
289
|
+
__CAMOX_BUILD_ID__: JSON.stringify(randomUUID())
|
|
252
290
|
},
|
|
253
291
|
environments: { client: { build: { rollupOptions: {
|
|
254
292
|
onLog: withoutExpectedCamoxLogs(findClientOnLog(_config)),
|
|
255
293
|
input: {
|
|
256
294
|
"camox-page-client": VIRTUAL_PAGE_CLIENT,
|
|
257
|
-
"camox-studio-client": VIRTUAL_STUDIO_CLIENT
|
|
295
|
+
"camox-studio-client": VIRTUAL_STUDIO_CLIENT,
|
|
296
|
+
"camox-renderer-client": VIRTUAL_RENDERER_CLIENT
|
|
258
297
|
},
|
|
259
298
|
output: {
|
|
260
299
|
assetFileNames: "assets/[name]-[hash][extname]",
|
|
@@ -286,6 +325,7 @@ function camox(options) {
|
|
|
286
325
|
},
|
|
287
326
|
configResolved(config) {
|
|
288
327
|
resolvedConfig = config;
|
|
328
|
+
if (process.env["CAMOX_INTERNAL_HOSTED"] === "1") hostedProject = readHostedProject(config.root);
|
|
289
329
|
if (!config.plugins.some((plugin) => plugin.name.startsWith("vite-plugin-cloudflare"))) throw new Error("Camox: Camox apps run on Cloudflare Workers. Add `cloudflare()` from `@cloudflare/vite-plugin` to your Vite plugins, and point `main` in `wrangler.jsonc` at `camox/worker`.");
|
|
290
330
|
const routesDir = resolve(config.root, "src/routes");
|
|
291
331
|
writeRuntimeSidecar(config.root, {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useProjectUuid } from "../lib/auth.js";
|
|
2
|
-
import { useCamoxApp } from "../features/provider/components/CamoxAppContext.js";
|
|
3
2
|
import { reads, useReadQuery } from "../lib/studio-reads.js";
|
|
4
3
|
import { linkablePages, useLinkablePages } from "../lib/link-pages.js";
|
|
4
|
+
import { useRoutableLayouts } from "../lib/studio-definitions.js";
|
|
5
5
|
import { getPageDestinations } from "../core/pageDestinations.js";
|
|
6
6
|
import { c } from "react/compiler-runtime";
|
|
7
7
|
|
|
@@ -9,7 +9,7 @@ import { c } from "react/compiler-runtime";
|
|
|
9
9
|
/** The pages of the locale viewed, which the studio navigates to. */
|
|
10
10
|
function usePageDestinations() {
|
|
11
11
|
const $ = c(11);
|
|
12
|
-
const
|
|
12
|
+
const layouts = useRoutableLayouts();
|
|
13
13
|
const projectUuid = useProjectUuid();
|
|
14
14
|
let t0;
|
|
15
15
|
if ($[0] !== projectUuid) {
|
|
@@ -39,9 +39,9 @@ function usePageDestinations() {
|
|
|
39
39
|
$[7] = t4;
|
|
40
40
|
} else t4 = $[7];
|
|
41
41
|
let t5;
|
|
42
|
-
if ($[8] !==
|
|
43
|
-
t5 = getPageDestinations(t4,
|
|
44
|
-
$[8] =
|
|
42
|
+
if ($[8] !== layouts || $[9] !== t4) {
|
|
43
|
+
t5 = getPageDestinations(t4, layouts);
|
|
44
|
+
$[8] = layouts;
|
|
45
45
|
$[9] = t4;
|
|
46
46
|
$[10] = t5;
|
|
47
47
|
} else t5 = $[10];
|
|
@@ -53,7 +53,7 @@ function usePageDestinations() {
|
|
|
53
53
|
*/
|
|
54
54
|
function useLinkDestinations() {
|
|
55
55
|
const $ = c(7);
|
|
56
|
-
const
|
|
56
|
+
const layouts = useRoutableLayouts();
|
|
57
57
|
let t0;
|
|
58
58
|
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
|
|
59
59
|
t0 = { fetchProject: true };
|
|
@@ -61,16 +61,16 @@ function useLinkDestinations() {
|
|
|
61
61
|
} else t0 = $[0];
|
|
62
62
|
const { pages, fallbackPages } = useLinkablePages(useProjectUuid(), t0);
|
|
63
63
|
let t1;
|
|
64
|
-
if ($[1] !==
|
|
64
|
+
if ($[1] !== fallbackPages || $[2] !== layouts || $[3] !== pages) {
|
|
65
65
|
let t2;
|
|
66
66
|
if ($[5] !== pages) {
|
|
67
67
|
t2 = pages ?? [];
|
|
68
68
|
$[5] = pages;
|
|
69
69
|
$[6] = t2;
|
|
70
70
|
} else t2 = $[6];
|
|
71
|
-
t1 = getPageDestinations(linkablePages(t2, fallbackPages).map(_temp),
|
|
72
|
-
$[1] =
|
|
73
|
-
$[2] =
|
|
71
|
+
t1 = getPageDestinations(linkablePages(t2, fallbackPages).map(_temp), layouts);
|
|
72
|
+
$[1] = fallbackPages;
|
|
73
|
+
$[2] = layouts;
|
|
74
74
|
$[3] = pages;
|
|
75
75
|
$[4] = t1;
|
|
76
76
|
} else t1 = $[4];
|
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,20 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
|
|
3
|
+
//#region src/lib/revisions.ts
|
|
4
|
+
function createRevisions() {
|
|
5
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
6
|
+
return {
|
|
7
|
+
announce(revision) {
|
|
8
|
+
for (const listener of listeners) listener(revision);
|
|
9
|
+
},
|
|
10
|
+
subscribe(listener) {
|
|
11
|
+
listeners.add(listener);
|
|
12
|
+
return () => listeners.delete(listener);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** The studio's announced revisions. The signed-in studio provides it. */
|
|
17
|
+
const RevisionsContext = React.createContext(null);
|
|
18
|
+
|
|
19
|
+
//#endregion
|
|
20
|
+
export { RevisionsContext, createRevisions };
|
|
@@ -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 };
|