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,12 +1,6 @@
1
1
  //#region src/features/preview/previewConstants.ts
2
- const TOGGLE_CANVAS_SHORTCUT = {
3
- key: "Enter",
4
- withMeta: true
5
- };
6
- const SHOW_WHOLE_CANVAS_SHORTCUT = {
7
- key: "Escape",
8
- withMeta: true
9
- };
2
+ const TOGGLE_CANVAS_SHORTCUT = "Mod+Enter";
3
+ const SHOW_WHOLE_CANVAS_SHORTCUT = "Mod+Escape";
10
4
 
11
5
  //#endregion
12
6
  export { SHOW_WHOLE_CANVAS_SHORTCUT, TOGGLE_CANVAS_SHORTCUT };
@@ -27,7 +27,7 @@ function _temp10() {
27
27
  groupLabel: "Website",
28
28
  checkIfAvailable: _temp,
29
29
  execute: _temp2,
30
- shortcut: { key: "m" }
30
+ shortcut: "M"
31
31
  },
32
32
  {
33
33
  id: "set-viewport-full",
@@ -1,13 +1,13 @@
1
1
  import { AuthContext, useAuthActions } from "../../lib/auth.js";
2
2
  import { useDraftScope } from "../../lib/draft-scope.js";
3
3
  import { reads, useReadQuery, useStudioCache } from "../../lib/studio-reads.js";
4
+ import { useAdminShortcuts } from "./useAdminShortcuts.js";
4
5
  import { useApplyTheme, useThemeActions } from "../studio/useTheme.js";
5
6
  import { usePreviewPagesActions, useShowCanvasAction } from "../preview/CamoxPreview.js";
6
7
  import { useProjectRoom } from "../../lib/use-project-room.js";
7
8
  import { useNavbarActions } from "../studio/components/Navbar.js";
8
9
  import { draftNoticeMessage } from "../studio/draftNotice.js";
9
10
  import { CommandPalette, useCommandPaletteActions } from "./components/CommandPalette.js";
10
- import { useAdminShortcuts } from "./useAdminShortcuts.js";
11
11
  import { c } from "react/compiler-runtime";
12
12
  import * as React from "react";
13
13
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -1,5 +1,5 @@
1
1
  import { actionsStore } from "../actionsStore.js";
2
- import { formatShortcut } from "../../../lib/utils.js";
2
+ import { formatShortcut } from "../../../lib/shortcuts.js";
3
3
  import { studioStore } from "../../studio/studioStore.js";
4
4
  import { c } from "react/compiler-runtime";
5
5
  import * as React from "react";
@@ -249,10 +249,7 @@ function _temp7() {
249
249
  groupLabel: "Invisible",
250
250
  checkIfAvailable: _temp4,
251
251
  execute: _temp5,
252
- shortcut: {
253
- key: "k",
254
- withMeta: true
255
- }
252
+ shortcut: "Mod+K"
256
253
  }
257
254
  });
258
255
  return _temp6;
@@ -1,5 +1,4 @@
1
1
  import { useSignInRedirect } from "../../lib/auth.js";
2
- import { TOGGLE_CANVAS_SHORTCUT } from "../preview/previewConstants.js";
3
2
  import { c } from "react/compiler-runtime";
4
3
  import * as React from "react";
5
4
 
@@ -28,7 +27,7 @@ function useSignInShortcut(enabled) {
28
27
  t0 = () => {
29
28
  if (!enabled) return;
30
29
  const handleKeyDown = (event) => {
31
- if (event.key !== TOGGLE_CANVAS_SHORTCUT.key || !(event.metaKey || event.ctrlKey)) return;
30
+ if (event.key !== "Enter" || !(event.metaKey || event.ctrlKey)) return;
32
31
  if (event.altKey || event.shiftKey || event.defaultPrevented || isTyping()) return;
33
32
  event.preventDefault();
34
33
  signIn();
@@ -1,75 +1,69 @@
1
1
  import { actionsStore } from "./actionsStore.js";
2
2
  import { checkIfInputFocused } from "../../lib/utils.js";
3
3
  import { c } from "react/compiler-runtime";
4
- import * as React from "react";
5
4
  import { useSelector } from "@xstate/store-react";
5
+ import { parseHotkey, useHotkeys } from "@tanstack/react-hotkeys";
6
6
 
7
7
  //#region src/features/provider/useAdminShortcuts.tsx
8
+ /** Listens for the shortcuts of the actions in the actionsStore, on the Studio's window. */
9
+ function useAdminShortcuts() {
10
+ useActionShortcuts(typeof window === "undefined" ? null : window);
11
+ }
8
12
  /**
9
- * Hook that listens for global keyboard shortcuts defined in the actionsStore
13
+ * Runs the first available action whose shortcut is pressed in `win`, which may be a preview
14
+ * iframe's window. Shortcuts no available action claims leave the key to the page.
10
15
  */
11
- function useAdminShortcuts() {
12
- const $ = c(3);
16
+ function useActionShortcuts(win) {
17
+ const $ = c(4);
13
18
  const actions = useSelector(actionsStore, _temp);
14
19
  let t0;
15
20
  let t1;
16
- if ($[0] !== actions) {
17
- t0 = () => {
18
- const handleKeyDown = (event) => {
19
- const matchingAction = actions.find((action) => {
20
- if (!action.shortcut) return false;
21
- if (!action.checkIfAvailable()) return false;
22
- const { key, withMeta, withAlt, withShift } = action.shortcut;
23
- return (withAlt && key.length === 1 && /[a-z]/i.test(key) ? event.code === `Key${key.toUpperCase()}` : key.toLowerCase() === event.key.toLowerCase()) && !!withMeta === (event.metaKey || event.ctrlKey) && !!withAlt === event.altKey && !!withShift === event.shiftKey;
24
- });
25
- if (!matchingAction) return;
26
- const shortcut = matchingAction.shortcut;
27
- if (checkIfInputFocused()) {
28
- if (shortcut.key !== "Escape" && !shortcut.withMeta && !shortcut.withAlt) return;
29
- if (shortcut.key === "Backspace") return;
30
- }
31
- event.preventDefault();
32
- event.stopPropagation();
33
- if (shortcut.key === "Escape" && checkIfInputFocused()) document.activeElement.blur();
34
- matchingAction.execute();
35
- };
36
- const handleCapturedKeyDown = (event_0) => {
37
- if (event_0.key === "Escape") return;
38
- handleKeyDown(event_0);
39
- };
40
- const handleEscape = (event_1) => {
41
- if (event_1.key !== "Escape" || event_1.defaultPrevented) return;
42
- handleKeyDown(event_1);
43
- };
44
- const handleMessage = (event_2) => {
45
- if (event_2.data?.type === "executeAction") {
46
- const { actionId } = event_2.data;
47
- const action_0 = actions.find((a) => a.id === actionId);
48
- if (action_0) action_0.execute();
21
+ if ($[0] !== actions || $[1] !== win) {
22
+ const shortcuts = [...new Set(actions.flatMap(_temp2))];
23
+ t0 = useHotkeys;
24
+ t1 = win ? shortcuts.map((shortcut) => {
25
+ const { key, ctrl, meta, alt } = parseHotkey(shortcut);
26
+ const escape = key === "Escape";
27
+ return {
28
+ hotkey: shortcut,
29
+ callback: (event) => {
30
+ if (escape && event.defaultPrevented) return;
31
+ const action_0 = findAvailableAction(shortcut);
32
+ if (!action_0) return;
33
+ event.preventDefault();
34
+ event.stopPropagation();
35
+ if (escape && checkIfInputFocused(win.document)) win.document.activeElement.blur();
36
+ action_0.execute();
37
+ },
38
+ options: {
39
+ target: escape ? win : win.document,
40
+ capture: !escape,
41
+ ignoreInputs: key === "Backspace" || !ctrl && !meta && !alt && !escape,
42
+ preventDefault: false,
43
+ stopPropagation: false,
44
+ conflictBehavior: "allow"
49
45
  }
50
46
  };
51
- document.addEventListener("keydown", handleCapturedKeyDown, true);
52
- window.addEventListener("keydown", handleEscape);
53
- window.addEventListener("message", handleMessage);
54
- return () => {
55
- document.removeEventListener("keydown", handleCapturedKeyDown, true);
56
- window.removeEventListener("keydown", handleEscape);
57
- window.removeEventListener("message", handleMessage);
58
- };
59
- };
60
- t1 = [actions];
47
+ }) : [];
61
48
  $[0] = actions;
62
- $[1] = t0;
63
- $[2] = t1;
49
+ $[1] = win;
50
+ $[2] = t0;
51
+ $[3] = t1;
64
52
  } else {
65
- t0 = $[1];
66
- t1 = $[2];
53
+ t0 = $[2];
54
+ t1 = $[3];
67
55
  }
68
- React.useEffect(t0, t1);
56
+ t0(t1);
57
+ }
58
+ function _temp2(action) {
59
+ return action.shortcut ?? [];
69
60
  }
70
61
  function _temp(state) {
71
62
  return state.context.actions;
72
63
  }
64
+ function findAvailableAction(shortcut) {
65
+ return actionsStore.getSnapshot().context.actions.find((action) => action.shortcut === shortcut && action.checkIfAvailable());
66
+ }
73
67
 
74
68
  //#endregion
75
- export { useAdminShortcuts };
69
+ export { useActionShortcuts, useAdminShortcuts };
@@ -1,11 +1,12 @@
1
1
  import { LocaleView } from "../../lib/locale-path.js";
2
+ import "../../core/layoutContext.js";
2
3
  import "../../core/structuredData.js";
3
4
  import "../../core/createLayout.js";
4
5
  import "../../core/createApp.js";
5
6
  import "../../lib/studio-reads.js";
6
- import { Router } from "@camox/api-contract";
7
7
  import { EndedDraft } from "@camox/api-contract/drafts";
8
8
  import { QueryClient } from "@tanstack/react-query";
9
+ import { Router } from "@camox/api-contract/router";
9
10
  import { RouterClient } from "@orpc/server";
10
11
  //#region src/features/routes/pageRuntime.d.ts
11
12
  type PageWithBlocks = Awaited<ReturnType<RouterClient<Router>["pages"]["getByPath"]>>;
@@ -218,25 +218,32 @@ async function loadCamoxPageForRequest({ apiUrl, authCookieHeader, draftId, loca
218
218
  shouldClearAuthCookie: member.clearAuthCookie
219
219
  };
220
220
  }
221
- async function createMarkdownResponse({ apiUrl, camoxApp, locale, pathname, projectUuid, request, url, onNotFound }) {
222
- const api = createServerApiClient(apiUrl, { locale });
223
- const requestPathname = pathname ?? new URL(request.url).pathname;
224
- try {
225
- const page = await api.pages.getByPath({
226
- path: requestPathname,
227
- projectUuid
228
- });
229
- const { markdown } = await api.blocks.getPageMarkdown({ pageId: page.page.id });
230
- if (!markdown) return null;
231
- const meta = buildPageMeta(curatedPageMetaInput(camoxApp, page, locale));
232
- return new Response(withFrontMatter(markdown, {
233
- ...meta,
234
- url
235
- }), { headers: { "Content-Type": "text/markdown; charset=utf-8" } });
236
- } catch (error) {
237
- if (onNotFound && isNotFoundError(error)) return onNotFound();
238
- return null;
239
- }
221
+ /**
222
+ * Reads the Markdown Representation of the page at a runtime path, with its locale's prefix, in
223
+ * one API read: the API finds the locale the prefix names, the page there, its Markdown and what
224
+ * its front matter is built from. Markdown is a public output, so it reads Live whoever asks.
225
+ */
226
+ function readPageMarkdown({ apiUrl, path, projectUuid }) {
227
+ return createServerApiClient(apiUrl).pages.getMarkdown({
228
+ path,
229
+ projectUuid
230
+ });
231
+ }
232
+ /**
233
+ * A curated page's Markdown Representation, opened by front matter stating its title,
234
+ * description and absolute `url`. Null when the path has no curated page, or the page has no
235
+ * Markdown.
236
+ */
237
+ function curatedMarkdownResponse(camoxApp, read, url) {
238
+ if (!read.page || !read.markdown) return null;
239
+ const meta = buildPageMeta(curatedPageMetaInput(camoxApp, {
240
+ ...read,
241
+ page: read.page
242
+ }, read.locale));
243
+ return new Response(withFrontMatter(read.markdown, {
244
+ ...meta,
245
+ url
246
+ }), { headers: { "Content-Type": "text/markdown; charset=utf-8" } });
240
247
  }
241
248
  /** A page's title and description, built by its layout's `toTitle` and `toDescription`. */
242
249
  function buildPageMeta(input) {
@@ -337,4 +344,4 @@ function buildCamoxPageHead(camoxApp, loaderData, locale) {
337
344
  }
338
345
 
339
346
  //#endregion
340
- export { buildCamoxPageHead, buildCamoxPageLoaderData, buildDefaultFaviconUrl, buildFaviconUrl, buildPageHead, buildPageMeta, createMarkdownResponse, createServerApiClient, createServerStudioCache, curatedPageMetaInput, declarePage, getMarkdownPath, isAuthSessionError, isNotFoundError, loadCamoxPage, loadCamoxPageForRequest, parseMarkdownPath, parseQuality, prefersMarkdown, readDraftOrLive, readScopeLocales, renderedHref, resolveMemberProject, withFrontMatter };
347
+ export { buildCamoxPageHead, buildCamoxPageLoaderData, buildDefaultFaviconUrl, buildFaviconUrl, buildPageHead, buildPageMeta, createServerApiClient, createServerStudioCache, curatedMarkdownResponse, curatedPageMetaInput, declarePage, getMarkdownPath, isAuthSessionError, isNotFoundError, loadCamoxPage, loadCamoxPageForRequest, parseMarkdownPath, parseQuality, prefersMarkdown, readDraftOrLive, readPageMarkdown, readScopeLocales, renderedHref, resolveMemberProject, withFrontMatter };
@@ -0,0 +1,83 @@
1
+ import { prefersMarkdown } from "../routes/pageRuntime.js";
2
+
3
+ //#region src/features/runtime/publicCache.ts
4
+ /** How long the edge keeps a render. Live changing replaces its key long before. */
5
+ const EDGE_MAX_AGE = 86400;
6
+ /** Browsers can't be purged, so they ask again each time; the edge answers them. */
7
+ const VISITOR_CACHE_CONTROL = "public, max-age=0, must-revalidate";
8
+ /** The search parameter the cache key adds to a request's URL. */
9
+ const CACHE_KEY_PARAM = "__camox_cache";
10
+ /** The project's Live cache key, as the API serves it from its edge cache. */
11
+ async function readLiveCacheKey(apiUrl, projectUuid) {
12
+ try {
13
+ const response = await fetch(`${apiUrl}/live-cache-keys/${encodeURIComponent(projectUuid)}`);
14
+ return response.ok ? await response.text() || null : null;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ /**
20
+ * Answers a public request from the cache, or renders it and keeps the render when it may be
21
+ * shared. Without a Live cache key, it renders without the cache: nothing tells when Live changed.
22
+ */
23
+ async function respondThroughPublicCache(request, publicCache, render) {
24
+ const liveCacheKey = await publicCache.liveCacheKey();
25
+ if (!liveCacheKey) return render();
26
+ const key = cacheKeyOf(request, `${liveCacheKey}.${publicCache.build}`);
27
+ const cached = await publicCache.cache.match(withValidators(key, request));
28
+ if (cached) return forVisitors(cached, request);
29
+ const response = await render();
30
+ if (!response || !mayBeShared(response)) return response;
31
+ publicCache.waitUntil(publicCache.cache.put(key, forEdge(response.clone())));
32
+ return forVisitors(response, request);
33
+ }
34
+ /**
35
+ * What a request's render is kept under: its URL, the representation it asks for, and the
36
+ * version of Live and of the website it renders.
37
+ */
38
+ function cacheKeyOf(request, version) {
39
+ const url = new URL(request.url);
40
+ const representation = prefersMarkdown(request.headers.get("Accept") ?? "") ? "md" : "any";
41
+ url.searchParams.set(CACHE_KEY_PARAM, `${version}.${representation}`);
42
+ return new Request(url.toString(), { method: "GET" });
43
+ }
44
+ /** The cache key, asking for the part of the render the request does. */
45
+ function withValidators(key, request) {
46
+ const headers = new Headers();
47
+ for (const name of [
48
+ "Range",
49
+ "If-None-Match",
50
+ "If-Modified-Since"
51
+ ]) {
52
+ const value = request.headers.get(name);
53
+ if (value !== null) headers.set(name, value);
54
+ }
55
+ return new Request(key, { headers });
56
+ }
57
+ /** Whether a response is the same for every visitor: what a private render never is. */
58
+ function mayBeShared(response) {
59
+ if (response.status !== 200 || response.headers.has("Set-Cookie")) return false;
60
+ const cacheControl = response.headers.get("Cache-Control") ?? "";
61
+ return !/\b(private|no-store)\b/i.test(cacheControl);
62
+ }
63
+ /** How long browsers may keep a render: what it asked for, if it's versioned, else not at all. */
64
+ function visitorCacheControl(response) {
65
+ const cacheControl = response.headers.get("Cache-Control") ?? "";
66
+ return /\bmax-age=[1-9]/.test(cacheControl) ? cacheControl : VISITOR_CACHE_CONTROL;
67
+ }
68
+ /** The render as the edge keeps it: as visitors get it, for as long as the edge keeps renders. */
69
+ function forEdge(response) {
70
+ const kept = new Response(response.body, response);
71
+ kept.headers.set("Cache-Control", `${visitorCacheControl(response)}, s-maxage=${EDGE_MAX_AGE}`);
72
+ return kept;
73
+ }
74
+ /** The render as visitors get it, without the edge's lifetime, which no cache downstream gets. */
75
+ function forVisitors(response, request) {
76
+ const cacheControl = visitorCacheControl(response).replace(/,\s*s-maxage=\d+/, "");
77
+ const answered = new Response(request.method === "HEAD" ? null : response.body, response);
78
+ answered.headers.set("Cache-Control", cacheControl);
79
+ return answered;
80
+ }
81
+
82
+ //#endregion
83
+ export { readLiveCacheKey, respondThroughPublicCache };
@@ -8,7 +8,10 @@ import { StudioRenderInput } from "./studioApp.js";
8
8
  import { DraftRead, MemberProject, PageHead, createServerApiClient } from "../routes/pageRuntime.js";
9
9
  import { UseHeadInput } from "unhead/types";
10
10
  //#region src/features/runtime/runtime.d.ts
11
- export type RuntimeRouteKind = RuntimeOwnedRouteKind | "canvas" | "page" | "studio" | "studio-content" | "studio-nested";
11
+ /** The studio's own routes, rather than a page or the Canvas. */
12
+ declare const STUDIO_ROUTE_KINDS: readonly ["studio", "studio-content", "studio-settings", "studio-nested"];
13
+ export type StudioRouteKind = (typeof STUDIO_ROUTE_KINDS)[number];
14
+ export type RuntimeRouteKind = RuntimeOwnedRouteKind | "canvas" | "page" | StudioRouteKind;
12
15
  export interface RuntimeRouteMatch {
13
16
  kind: RuntimeRouteKind;
14
17
  pathname: string;
@@ -32,7 +35,7 @@ export interface PageRenderInput extends DraftRead {
32
35
  /** Authenticate before rendering, to show the Studio around the page. */
33
36
  previewHandoff?: boolean;
34
37
  previewDocument?: string;
35
- routeKind?: "studio" | "studio-content" | "studio-nested";
38
+ routeKind?: StudioRouteKind;
36
39
  /** Whether the location is the Canvas with nothing selected, which only the Canvas shows. */
37
40
  canvasOnly?: boolean;
38
41
  project?: MemberProject;
@@ -96,7 +99,14 @@ export interface RuntimeOptions {
96
99
  studioStylesheetUrl?: string;
97
100
  }
98
101
  export declare function getRuntimePathname(url: string, runtimeBasePath?: string): string | null;
102
+ /**
103
+ * Whether a request reads what every visitor sees, so a shared cache may answer it: a GET or
104
+ * HEAD of a page or a public output, with no session, no draft named and no preview handoff. A
105
+ * response that comes out private all the same, such as the Studio's, still says so.
106
+ */
107
+ export declare function isPublicRequest(request: Request, options?: RuntimeOptions): boolean;
99
108
  export declare function matchRuntimeRoute(pathname: string): RuntimeRouteMatch;
100
109
  export declare function createPageHeadInput(head: PageHead): UseHeadInput;
101
110
  export declare function handleCamoxRequest(request: Request, options?: RuntimeOptions): Promise<Response | null>;
102
- //#endregion
111
+ //#endregion
112
+ export {};