camox 0.75.0 → 0.76.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/dist/core/editing/createEditableBlock.js +1077 -1681
  2. package/dist/core/editing/fieldSlot.js +251 -0
  3. package/dist/core/editing/useFieldSlot.js +159 -0
  4. package/dist/core/layoutContext.d.ts +2 -0
  5. package/dist/core/layoutContext.js +27 -1
  6. package/dist/features/canvas/CanvasPageFrame.js +2 -2
  7. package/dist/features/canvas/CanvasPreviewPageSlot.js +1 -1
  8. package/dist/features/canvas/canvasCameraController.js +595 -0
  9. package/dist/features/canvas/useCanvasCamera.js +141 -587
  10. package/dist/features/canvas/useCanvasFrame.js +1 -1
  11. package/dist/features/page/DerivedPageContent.js +4 -8
  12. package/dist/features/preview/components/AssetLightbox.js +6 -65
  13. package/dist/features/preview/components/CanvasPreview.js +2 -2
  14. package/dist/features/preview/components/Frame.js +3 -3
  15. package/dist/features/preview/components/ItemFieldsEditor.js +376 -460
  16. package/dist/features/preview/components/Overlays.js +4 -9
  17. package/dist/features/preview/components/PreviewPanel.js +3 -3
  18. package/dist/features/preview/components/RemovedBlockPlaceholder.js +2 -2
  19. package/dist/features/preview/components/SelectionEditor.js +285 -339
  20. package/dist/features/preview/components/ZoomableImage.js +105 -0
  21. package/dist/features/preview/components/contentFieldSchema.js +3 -1
  22. package/dist/features/preview/components/useFieldWriter.js +30 -51
  23. package/dist/features/preview/components/useRecordView.js +3 -3
  24. package/dist/features/preview/overlayMessages.js +20 -58
  25. package/dist/features/preview/previewStore.js +6 -123
  26. package/dist/features/preview/repeatableItemPath.js +47 -0
  27. package/dist/features/provider/AuthenticatedCamoxProvider.js +75 -63
  28. package/dist/features/renderer/RendererApp.js +6 -6
  29. package/dist/features/renderer/RendererFrames.js +2 -2
  30. package/dist/features/renderer/rendererHost.js +1 -1
  31. package/dist/features/routes/pageRuntime.d.ts +8 -12
  32. package/dist/features/routes/pageRuntime.js +26 -106
  33. package/dist/features/runtime/pageApp.js +1 -1
  34. package/dist/features/runtime/pageRead.js +105 -0
  35. package/dist/features/runtime/reader.d.ts +25 -0
  36. package/dist/features/runtime/reader.js +142 -0
  37. package/dist/features/runtime/runtime.d.ts +9 -3
  38. package/dist/features/runtime/runtime.js +608 -776
  39. package/dist/features/sdkBundle/buildSdkBundle.d.ts +5 -0
  40. package/dist/features/sdkBundle/buildSdkBundle.js +9 -3
  41. package/dist/features/studio/components/ChatWidget.js +137 -121
  42. package/dist/features/studio/components/ClearThread.js +121 -0
  43. package/dist/features/studio/components/DeleteDraftDialog.js +67 -65
  44. package/dist/features/studio/components/DraftBar.js +60 -49
  45. package/dist/features/studio/components/DraftNotices.js +149 -0
  46. package/dist/features/studio/components/DraftsControl.js +217 -180
  47. package/dist/features/studio/components/DraftsSheet.js +446 -0
  48. package/dist/features/studio/components/PublishedTab.js +511 -0
  49. package/dist/features/studio/components/ReviewDraftDialog.js +104 -89
  50. package/dist/features/studio/components/SheetTable.js +46 -0
  51. package/dist/features/studio/components/Specialists.js +1 -1
  52. package/dist/features/studio/components/ThreadMessages.js +227 -176
  53. package/dist/features/studio/draftLabel.js +15 -1
  54. package/dist/features/studio/draftNotice.js +1 -0
  55. package/dist/features/studio/showInCanvas.js +9 -19
  56. package/dist/features/studio/studioStore.js +5 -5
  57. package/dist/features/studio/useArchiveDraft.js +60 -0
  58. package/dist/features/studio/useEditorSelection.js +8 -19
  59. package/dist/features/studio/useRestoreDraft.js +70 -0
  60. package/dist/features/studio/viewSwitch.js +19 -11
  61. package/dist/lib/draft-scope.d.ts +1 -1
  62. package/dist/lib/draft-scope.js +6 -3
  63. package/dist/lib/queries.js +12 -1
  64. package/dist/lib/studio-reads.js +22 -0
  65. package/dist/lib/use-project-room.js +19 -13
  66. package/dist/lib/use-thread.js +1 -1
  67. package/dist/package.js +1 -1
  68. package/dist/studio.css +1 -1
  69. package/package.json +4 -4
  70. package/dist/core/hooks/useFieldSelection.js +0 -67
  71. package/dist/core/hooks/useIsEditable.js +0 -21
  72. package/dist/core/hooks/useOverlayMessage.js +0 -70
@@ -7,18 +7,18 @@ import { useApplyTheme, useThemeActions } from "../studio/useTheme.js";
7
7
  import { usePreviewPagesActions, useShowCanvasAction } from "../preview/CamoxPreview.js";
8
8
  import { useProjectRoom } from "../../lib/use-project-room.js";
9
9
  import { useLeaveDraft } from "../studio/viewSwitch.js";
10
+ import { DraftNotices } from "../studio/components/DraftNotices.js";
10
11
  import { useNavbarActions } from "../studio/components/Navbar.js";
11
- import { draftNoticeMessage } from "../studio/draftNotice.js";
12
12
  import { CommandPalette, useCommandPaletteActions } from "./components/CommandPalette.js";
13
13
  import { c } from "react/compiler-runtime";
14
14
  import * as React from "react";
15
15
  import { jsx, jsxs } from "react/jsx-runtime";
16
- import { Toaster, toast } from "@camox/ui/toaster";
16
+ import { Toaster } from "@camox/ui/toaster";
17
17
  import studioCssUrl from "virtual:camox-studio-css";
18
18
 
19
19
  //#region src/features/provider/AuthenticatedCamoxProvider.tsx
20
20
  function AuthenticatedCamoxProvider(t0) {
21
- const $ = c(20);
21
+ const $ = c(24);
22
22
  const { children } = t0;
23
23
  useAdminShortcuts();
24
24
  useCommandPaletteActions();
@@ -37,85 +37,97 @@ function AuthenticatedCamoxProvider(t0) {
37
37
  const { data: project } = useReadQuery(t1);
38
38
  const draftScope = useDraftScope();
39
39
  const studioCache = useStudioCache();
40
+ const [revisions] = React.useState(createRevisions);
41
+ const leaveDraft = useLeaveDraft();
40
42
  let t2;
43
+ if ($[2] !== authClient || $[3] !== draftScope || $[4] !== leaveDraft) {
44
+ t2 = (left, by) => {
45
+ if (left.draftId !== draftScope.current()) return;
46
+ if (by === signedInUserId(authClient)) return leaveDraft(left);
47
+ draftScope.leave(left, { notice: true });
48
+ };
49
+ $[2] = authClient;
50
+ $[3] = draftScope;
51
+ $[4] = leaveDraft;
52
+ $[5] = t2;
53
+ } else t2 = $[5];
54
+ const onDraftLeft = t2;
41
55
  let t3;
42
- if ($[2] !== draftScope) {
43
- t2 = () => draftScope.onNotice(_temp);
44
- t3 = [draftScope];
45
- $[2] = draftScope;
46
- $[3] = t2;
47
- $[4] = t3;
56
+ let t4;
57
+ if ($[6] !== onDraftLeft) {
58
+ t3 = (ended) => onDraftLeft(ended, ended.endedBy.id);
59
+ t4 = (archived) => onDraftLeft(archived, archived.archivedBy.id);
60
+ $[6] = onDraftLeft;
61
+ $[7] = t3;
62
+ $[8] = t4;
48
63
  } else {
49
- t2 = $[3];
50
- t3 = $[4];
64
+ t3 = $[7];
65
+ t4 = $[8];
51
66
  }
52
- React.useEffect(t2, t3);
53
- const [revisions] = React.useState(createRevisions);
54
- const leaveDraft = useLeaveDraft();
55
- let t4;
56
- if ($[5] !== authClient || $[6] !== draftScope || $[7] !== leaveDraft || $[8] !== revisions) {
57
- let t5;
58
- if ($[10] !== revisions) {
59
- t5 = (revision) => revisions.announce(revision);
60
- $[10] = revisions;
61
- $[11] = t5;
62
- } else t5 = $[11];
63
- t4 = {
64
- onDraftEnded(ended) {
65
- if (ended.draftId !== draftScope.current()) return;
66
- if (ended.endedBy.id === signedInUserId(authClient)) return leaveDraft(ended);
67
- draftScope.leave(ended, { notice: true });
68
- },
67
+ let t5;
68
+ if ($[9] !== revisions) {
69
+ t5 = (revision) => revisions.announce(revision);
70
+ $[9] = revisions;
71
+ $[10] = t5;
72
+ } else t5 = $[10];
73
+ let t6;
74
+ if ($[11] !== t3 || $[12] !== t4 || $[13] !== t5) {
75
+ t6 = {
76
+ onDraftEnded: t3,
77
+ onDraftArchived: t4,
69
78
  onRevision: t5
70
79
  };
71
- $[5] = authClient;
72
- $[6] = draftScope;
73
- $[7] = leaveDraft;
74
- $[8] = revisions;
75
- $[9] = t4;
76
- } else t4 = $[9];
77
- useProjectRoom(apiUrl, project?.id, studioCache, t4);
80
+ $[11] = t3;
81
+ $[12] = t4;
82
+ $[13] = t5;
83
+ $[14] = t6;
84
+ } else t6 = $[14];
85
+ useProjectRoom(apiUrl, project?.id, studioCache, t6);
78
86
  const { resolvedTheme } = useApplyTheme();
79
- let t5;
80
- if ($[12] === Symbol.for("react.memo_cache_sentinel")) {
81
- t5 = /* @__PURE__ */ jsx("link", {
87
+ let t7;
88
+ if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
89
+ t7 = /* @__PURE__ */ jsx("link", {
82
90
  rel: "stylesheet",
83
91
  href: studioCssUrl,
84
92
  "data-camox-studio": true
85
93
  });
86
- $[12] = t5;
87
- } else t5 = $[12];
88
- let t6;
89
- if ($[13] !== resolvedTheme) {
90
- t6 = /* @__PURE__ */ jsx(Toaster, { theme: resolvedTheme });
91
- $[13] = resolvedTheme;
92
- $[14] = t6;
93
- } else t6 = $[14];
94
- let t7;
95
- if ($[15] === Symbol.for("react.memo_cache_sentinel")) {
96
- t7 = /* @__PURE__ */ jsx(CommandPalette, {});
97
94
  $[15] = t7;
98
95
  } else t7 = $[15];
99
96
  let t8;
100
- if ($[16] !== children || $[17] !== revisions || $[18] !== t6) {
101
- t8 = /* @__PURE__ */ jsxs(RevisionsContext, {
97
+ if ($[16] !== resolvedTheme) {
98
+ t8 = /* @__PURE__ */ jsx(Toaster, { theme: resolvedTheme });
99
+ $[16] = resolvedTheme;
100
+ $[17] = t8;
101
+ } else t8 = $[17];
102
+ let t10;
103
+ let t9;
104
+ if ($[18] === Symbol.for("react.memo_cache_sentinel")) {
105
+ t9 = /* @__PURE__ */ jsx(CommandPalette, {});
106
+ t10 = /* @__PURE__ */ jsx(DraftNotices, {});
107
+ $[18] = t10;
108
+ $[19] = t9;
109
+ } else {
110
+ t10 = $[18];
111
+ t9 = $[19];
112
+ }
113
+ let t11;
114
+ if ($[20] !== children || $[21] !== revisions || $[22] !== t8) {
115
+ t11 = /* @__PURE__ */ jsxs(RevisionsContext, {
102
116
  value: revisions,
103
117
  children: [
104
- t5,
118
+ t7,
105
119
  children,
106
- t6,
107
- t7
120
+ t8,
121
+ t9,
122
+ t10
108
123
  ]
109
124
  });
110
- $[16] = children;
111
- $[17] = revisions;
112
- $[18] = t6;
113
- $[19] = t8;
114
- } else t8 = $[19];
115
- return t8;
116
- }
117
- function _temp(notice) {
118
- toast(draftNoticeMessage(notice));
125
+ $[20] = children;
126
+ $[21] = revisions;
127
+ $[22] = t8;
128
+ $[23] = t11;
129
+ } else t11 = $[23];
130
+ return t11;
119
131
  }
120
132
 
121
133
  //#endregion
@@ -3,14 +3,14 @@ import { ViewLocaleContext } from "../../lib/view-locale-context.js";
3
3
  import { NavigationProvider } from "../navigation/navigation.js";
4
4
  import { reads, useRead } from "../../lib/studio-reads.js";
5
5
  import { ArtifactStudioContext, ArtifactsRunContext } from "../../core/hooks/useArtifactHost.js";
6
- import { EditableSurfaceContext } from "../../core/hooks/useIsEditable.js";
7
- import { CanvasWorkspaceContext } from "../canvas/canvasZoom.js";
8
- import { PreviewEditingOwnerContext } from "../preview/previewSelection.js";
9
- import { StudioNoticeContext } from "../preview/studioNotice.js";
10
- import { ContentEditsContext } from "../../core/editing/contentEdits.js";
11
- import { restyle } from "../preview/components/previewStyles.js";
12
6
  import { PreviewDocumentContext } from "../runtime/PreviewDocumentContext.js";
7
+ import { restyle } from "../preview/components/previewStyles.js";
13
8
  import { FrameContext } from "../preview/components/Frame.js";
9
+ import { PreviewEditingOwnerContext } from "../preview/previewSelection.js";
10
+ import { ContentEditsContext } from "../../core/editing/contentEdits.js";
11
+ import { EditableSurfaceContext } from "../../core/editing/useFieldSlot.js";
12
+ import { CanvasWorkspaceContext } from "../canvas/canvasZoom.js";
13
+ import { StudioNoticeContext } from "../preview/studioNotice.js";
14
14
  import { DerivedPageContent } from "../page/DerivedPageContent.js";
15
15
  import { EditablePageContent } from "../preview/EditablePageContent.js";
16
16
  import { c } from "react/compiler-runtime";
@@ -5,10 +5,10 @@ import { runtimePath } from "../runtime/navigationTarget.js";
5
5
  import { useViewPreparer } from "../../lib/view-switch.js";
6
6
  import { useStudioCache } from "../../lib/studio-reads.js";
7
7
  import { ArtifactStudioContext, ArtifactsRunContext } from "../../core/hooks/useArtifactHost.js";
8
- import { EditableSurfaceContext } from "../../core/hooks/useIsEditable.js";
8
+ import { PreviewEditingOwnerContext } from "../preview/previewSelection.js";
9
+ import { EditableSurfaceContext } from "../../core/editing/useFieldSlot.js";
9
10
  import { RevisionsContext } from "../../lib/revisions.js";
10
11
  import { CanvasWorkspaceContext } from "../canvas/canvasZoom.js";
11
- import { PreviewEditingOwnerContext } from "../preview/previewSelection.js";
12
12
  import { showNotice } from "../preview/studioNotice.js";
13
13
  import { RENDERER_DOCUMENT_PATH, STUDIO_HANDLE_KEY } from "./protocol.js";
14
14
  import { createRendererHost } from "./rendererHost.js";
@@ -1,7 +1,7 @@
1
1
  import { canvasStore } from "../canvas/canvasStore.js";
2
2
  import { previewStore } from "../preview/previewStore.js";
3
- import { SDK_VERSION, deferredInbox } from "./protocol.js";
4
3
  import { createContentEditor } from "../../core/editing/contentEdits.js";
4
+ import { SDK_VERSION, deferredInbox } from "./protocol.js";
5
5
  import { LIVE_KEY_SCOPE, scopeOfKey } from "@camox/api-contract/query-keys";
6
6
 
7
7
  //#region src/features/renderer/rendererHost.ts
@@ -1,10 +1,11 @@
1
- import { LocaleView } from "../../lib/locale-path.js";
1
+ import "../../lib/locale-path.js";
2
2
  import "../../core/layoutContext.js";
3
3
  import "../../core/structuredData.js";
4
4
  import "../../core/createLayout.js";
5
5
  import "../../core/createApp.js";
6
+ import "../runtime/reader.js";
6
7
  import "../../lib/studio-reads.js";
7
- import { EndedDraft } from "@camox/api-contract/drafts";
8
+ import { ArchivedDraft, EndedDraft } from "@camox/api-contract/drafts";
8
9
  import { QueryClient } from "@tanstack/react-query";
9
10
  import { Router } from "@camox/api-contract/router";
10
11
  import { RouterClient } from "@orpc/server";
@@ -22,8 +23,8 @@ export type PageStructure = {
22
23
  recordStructuredData?: PageWithBlocks["recordStructuredData"];
23
24
  };
24
25
  /**
25
- * Why a member can't see the draft a URL names: it doesn't exist, it isn't open to them, or it
26
- * was published or deleted (ADR 0011).
26
+ * Why a member can't see the draft a URL names: it doesn't exist, it isn't open to them, it
27
+ * was published or deleted (ADR 0011), or it's archived (ADR 0046).
27
28
  */
28
29
  export type DraftNotice = {
29
30
  reason: "missing";
@@ -31,7 +32,9 @@ export type DraftNotice = {
31
32
  reason: "forbidden";
32
33
  } | ({
33
34
  reason: "ended";
34
- } & EndedDraft);
35
+ } & EndedDraft) | ({
36
+ reason: "archived";
37
+ } & ArchivedDraft);
35
38
  /** Which draft a render read: the URL's, or Live when there was none or it couldn't be read. */
36
39
  export interface DraftRead {
37
40
  /** The draft the render read. Absent for Live. */
@@ -44,13 +47,6 @@ export interface DraftRead {
44
47
  notice: DraftNotice | null;
45
48
  };
46
49
  }
47
- export declare function createServerApiClient(apiUrl: string, options?: {
48
- authCookieHeader?: string;
49
- /** A look token, which reads the draft it was signed for without a session (ADR 0032). */
50
- lookToken?: string;
51
- draftId?: string;
52
- locale?: LocaleView;
53
- }): RouterClient<Router>;
54
50
  export type PageHead = {
55
51
  meta?: Array<Record<string, string>>;
56
52
  links?: Array<Record<string, string>>;
@@ -6,7 +6,7 @@ import { PAGE_IMAGE_SIZE, pageImageUrl } from "../og/pageImage.js";
6
6
  import { curatedLayoutContext } from "../../core/layoutContext.js";
7
7
  import { isArticle } from "../../core/structuredData.js";
8
8
  import { publishingDraft } from "../runtime/publishingDraft.js";
9
- import { DRAFT_ENDED_ERROR_CODE, DRAFT_HEADER, LOOK_TOKEN_HEADER, PUBLISHING_DRAFT_HEADER } from "@camox/api-contract/drafts";
9
+ import { DRAFT_ARCHIVED_ERROR_CODE, DRAFT_ENDED_ERROR_CODE, DRAFT_HEADER, LOOK_TOKEN_HEADER, PUBLISHING_DRAFT_HEADER } from "@camox/api-contract/drafts";
10
10
  import { createORPCClient } from "@orpc/client";
11
11
  import { RPCLink } from "@orpc/client/fetch";
12
12
 
@@ -36,10 +36,16 @@ function parseMarkdownPath(pathname) {
36
36
  if (pathname === "/index.md") return "/";
37
37
  return pathname.slice(0, -3) || null;
38
38
  }
39
- function createServerApiClient(apiUrl, options) {
40
- const headers = localeHeaders(options?.locale);
41
- if (options?.lookToken) headers[LOOK_TOKEN_HEADER] = options.lookToken;
42
- if (options?.draftId && (options.authCookieHeader || options.lookToken)) headers[DRAFT_HEADER] = options.draftId;
39
+ /**
40
+ * The API's client for one kind of read: with a credential, in a draft or Live, in a locale. Only
41
+ * a credential can read a draft, so a draft is only ever named with one; a Live read names the
42
+ * draft this Worker publishes, if any.
43
+ */
44
+ function createServerApiClient(apiUrl, { credential, draftId, locale } = {}) {
45
+ const headers = localeHeaders(locale);
46
+ const session = credential && "session" in credential ? credential.session : void 0;
47
+ if (credential && "lookToken" in credential) headers[LOOK_TOKEN_HEADER] = credential.lookToken;
48
+ if (draftId && credential) headers[DRAFT_HEADER] = draftId;
43
49
  else {
44
50
  const publishing = publishingDraft();
45
51
  if (publishing) headers[PUBLISHING_DRAFT_HEADER] = publishing;
@@ -48,7 +54,7 @@ function createServerApiClient(apiUrl, options) {
48
54
  url: `${apiUrl}/rpc`,
49
55
  headers,
50
56
  fetch: (request, init) => {
51
- if (options?.authCookieHeader && request instanceof Request) request.headers.set("Better-Auth-Cookie", options.authCookieHeader);
57
+ if (session && request instanceof Request) request.headers.set("Better-Auth-Cookie", session);
52
58
  return fetch(request, init);
53
59
  }
54
60
  }));
@@ -84,46 +90,24 @@ function draftRefusalNotice(error, draftId) {
84
90
  reason: "ended",
85
91
  ...data
86
92
  };
93
+ if (code === DRAFT_ARCHIVED_ERROR_CODE && data) return {
94
+ reason: "archived",
95
+ ...data
96
+ };
87
97
  if (isNotFoundError(error) && data?.draftId === draftId) return { reason: "missing" };
88
98
  if (code === "FORBIDDEN" || status === 403) return { reason: "forbidden" };
89
99
  if (code === "UNAUTHORIZED" || status === 401) return null;
90
100
  }
91
- /**
92
- * Reads with the draft a URL names, or Live (ADR 0011). Only a member's request names the draft;
93
- * anyone else reads Live. When the API refuses the draft, the same response reads Live instead.
94
- */
95
- async function readDraftOrLive(draftId, member, read) {
96
- if (!draftId) return { value: await read(void 0) };
97
- if (!member) return {
98
- value: await read(void 0),
99
- refusedDraft: { notice: null }
100
- };
101
- try {
102
- return {
103
- value: await read(draftId),
104
- draftId
105
- };
106
- } catch (error) {
107
- const notice = draftRefusalNotice(error, draftId);
108
- if (notice === void 0) throw error;
109
- return {
110
- value: await read(void 0),
111
- refusedDraft: { notice }
112
- };
113
- }
114
- }
115
101
  /** The URL a render shows: without the draft param when the draft couldn't be read. */
116
102
  function renderedHref(url, read) {
117
103
  return read.refusedDraft ? withoutDraft(url) : url;
118
104
  }
119
- async function loadCamoxPage({ apiUrl, authCookieHeader, lookToken, draftId, locale, pathname, projectUuid, queryClient }) {
105
+ async function loadCamoxPage({ reader, draftId, locale, pathname, projectUuid, queryClient }) {
120
106
  const studioCache = createServerStudioCache(queryClient, {
121
- apiUrl,
122
- authCookieHeader,
123
- lookToken,
107
+ reader,
124
108
  locale
125
109
  });
126
- const inDraft = { in: draftId && (authCookieHeader || lookToken) ? draftId : null };
110
+ const inDraft = { in: draftId ?? null };
127
111
  const fallback = locale && linkFallbackLocale(locale);
128
112
  const [page] = await Promise.all([
129
113
  studioCache.ensure(reads.pageStructure({
@@ -139,16 +123,14 @@ async function loadCamoxPage({ apiUrl, authCookieHeader, lookToken, draftId, loc
139
123
  return page;
140
124
  }
141
125
  /**
142
- * The studio's reads for one server request, read with its session. Each operation names the
143
- * draft it acts in, once the request settled which one it reads.
126
+ * The studio's reads for one server request, read as its reader. Each operation names the draft
127
+ * it acts in, once the request settled which one it reads.
144
128
  */
145
- function createServerStudioCache(queryClient, { apiUrl, authCookieHeader, lookToken, locale }) {
129
+ function createServerStudioCache(queryClient, { reader, locale }) {
146
130
  return createStudioCache({
147
131
  queryClient,
148
132
  scope: createDraftScope({ queryClient }),
149
- api: (draft, inLocale) => createServerApiClient(apiUrl, {
150
- authCookieHeader,
151
- lookToken,
133
+ api: (draft, inLocale) => reader.api({
152
134
  draftId: draft ?? void 0,
153
135
  locale: inLocale
154
136
  }),
@@ -156,45 +138,6 @@ function createServerStudioCache(queryClient, { apiUrl, authCookieHeader, lookTo
156
138
  });
157
139
  }
158
140
  /**
159
- * The locales of the scope a request reads: the URL's draft's for a session naming one, else
160
- * Live's. A draft the API refuses reads Live's, as the request's other reads then do. Live's read
161
- * keeps the session: a member's studio routes the locales Live doesn't serve yet, which visitors
162
- * never see (ADR 0043), and a render with a session is private.
163
- */
164
- async function readScopeLocales({ apiUrl, authCookieHeader, draftId, projectUuid }) {
165
- const list = (session, readDraftId) => createServerApiClient(apiUrl, {
166
- authCookieHeader: session,
167
- draftId: readDraftId
168
- }).locales.list({ projectUuid });
169
- const { value } = await readDraftOrLive(draftId, !!authCookieHeader, async (readDraftId) => {
170
- if (readDraftId || !authCookieHeader) return list(authCookieHeader, readDraftId);
171
- try {
172
- return await list(authCookieHeader, void 0);
173
- } catch (error) {
174
- if (!isAuthSessionError(error)) throw error;
175
- return list(void 0, void 0);
176
- }
177
- });
178
- return value;
179
- }
180
- /**
181
- * The project, when the auth cookie is a valid session of one of its members. A successful
182
- * protected project lookup validates both session and project access: cookie presence alone
183
- * is not proof of authentication. An invalid session asks for the cookie to be cleared.
184
- */
185
- async function resolveMemberProject({ apiUrl, authCookieHeader, projectUuid }) {
186
- if (!authCookieHeader) return { clearAuthCookie: false };
187
- try {
188
- return {
189
- project: await createServerApiClient(apiUrl, { authCookieHeader }).projects.getByUuid({ projectUuid }),
190
- clearAuthCookie: false
191
- };
192
- } catch (error) {
193
- if (!isAuthSessionError(error)) throw error;
194
- return { clearAuthCookie: true };
195
- }
196
- }
197
- /**
198
141
  * The project's favicon, which falls back to the default favicon when none was uploaded.
199
142
  * Pass `withDefault: false` to get a 404 instead, to learn whether there is one.
200
143
  */
@@ -213,36 +156,13 @@ function buildCamoxPageLoaderData(apiUrl, page, origin) {
213
156
  faviconUrl: buildFaviconUrl(apiUrl, page.project)
214
157
  };
215
158
  }
216
- async function loadCamoxPageForRequest({ apiUrl, authCookieHeader, draftId, locale, origin, pathname, projectUuid, queryClient }) {
217
- const member = await resolveMemberProject({
218
- apiUrl,
219
- authCookieHeader,
220
- projectUuid
221
- });
222
- if (member.clearAuthCookie) console.warn("[camox] Ignoring stale camox_auth_cookie.");
223
- const { value: page, ...read } = await readDraftOrLive(draftId, !!member.project, (readDraftId) => loadCamoxPage({
224
- apiUrl,
225
- authCookieHeader: member.project ? authCookieHeader : void 0,
226
- draftId: readDraftId,
227
- locale,
228
- pathname,
229
- projectUuid,
230
- queryClient
231
- }));
232
- return {
233
- ...read,
234
- data: buildCamoxPageLoaderData(apiUrl, page, origin),
235
- project: member.project,
236
- shouldClearAuthCookie: member.clearAuthCookie
237
- };
238
- }
239
159
  /**
240
160
  * Reads the Markdown Representation of the page at a runtime path, with its locale's prefix, in
241
161
  * one API read: the API finds the locale the prefix names, the page there, its Markdown and what
242
162
  * its front matter is built from. Markdown is a public output, so it reads Live whoever asks.
243
163
  */
244
- function readPageMarkdown({ apiUrl, path, projectUuid }) {
245
- return createServerApiClient(apiUrl).pages.getMarkdown({
164
+ function readPageMarkdown({ reader, path, projectUuid }) {
165
+ return reader.api().pages.getMarkdown({
246
166
  path,
247
167
  projectUuid
248
168
  });
@@ -362,4 +282,4 @@ function buildCamoxPageHead(camoxApp, loaderData, locale) {
362
282
  }
363
283
 
364
284
  //#endregion
365
- 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 };
285
+ export { buildCamoxPageHead, buildCamoxPageLoaderData, buildDefaultFaviconUrl, buildFaviconUrl, buildPageHead, buildPageMeta, createServerApiClient, createServerStudioCache, curatedMarkdownResponse, curatedPageMetaInput, declarePage, draftRefusalNotice, getMarkdownPath, isAuthSessionError, isNotFoundError, loadCamoxPage, parseMarkdownPath, parseQuality, prefersMarkdown, readPageMarkdown, renderedHref, withFrontMatter };
@@ -2,8 +2,8 @@ import { DraftScopeContext } from "../../lib/draft-scope.js";
2
2
  import { useAuthState, useSignInRedirect } from "../../lib/auth.js";
3
3
  import { ArtifactCaptureContext } from "../../core/hooks/useArtifactHost.js";
4
4
  import { useSignInShortcut } from "../provider/signInShortcut.js";
5
- import { PreviewActivation } from "../preview/components/PreviewActivation.js";
6
5
  import { PreviewDocumentContext } from "./PreviewDocumentContext.js";
6
+ import { PreviewActivation } from "../preview/components/PreviewActivation.js";
7
7
  import { Frame } from "../preview/components/Frame.js";
8
8
  import { PreviewSignInPage } from "../provider/PreviewSignInPage.js";
9
9
  import { CoreCamoxProvider } from "../provider/CoreCamoxProvider.js";
@@ -0,0 +1,105 @@
1
+ import { createRequestContext } from "../../core/requestContext.js";
2
+ import { asCollectionItem } from "../../core/recordItem.js";
3
+ import { matchDerivedLayout } from "../../core/derivedRoutes.js";
4
+ import { layoutPageContext } from "../../core/layoutContext.js";
5
+ import { isNotFoundError } from "../routes/pageRuntime.js";
6
+
7
+ //#region src/features/runtime/pageRead.ts
8
+ /**
9
+ * What a path shows, in the Locale the path's prefix named: a Singleton Layout's page first, then
10
+ * the curated page at the path, then a Derived Layout's page. Each output reads the curated page
11
+ * and the layout's blocks as it needs them; Page Read decides which it shows and assembles a
12
+ * Layout Page. A Layout Page's loader runs in the Draft its blocks are read in: alongside them
13
+ * when the reader knows the Draft, after them when their read settles it.
14
+ */
15
+ async function readPage({ app, path, pathname, reader, curated, layout }) {
16
+ const match = app && matchDerivedLayout(app.getLayouts(), path);
17
+ const layoutPage = () => readLayoutPage({
18
+ app,
19
+ match,
20
+ pathname,
21
+ reader,
22
+ readBlocks: layout
23
+ });
24
+ if (!mayShowCuratedPage(app, path)) return layoutPage();
25
+ try {
26
+ const { value: page, ...read } = await curated();
27
+ if (page !== null) return {
28
+ kind: "curated",
29
+ page,
30
+ ...read
31
+ };
32
+ } catch (error) {
33
+ if (!isNotFoundError(error)) throw error;
34
+ }
35
+ return match ? layoutPage() : { kind: "none" };
36
+ }
37
+ /**
38
+ * Whether a path may show a curated page, so reading one there may be worth starting early: not
39
+ * a Singleton Layout's route.
40
+ */
41
+ function mayShowCuratedPage(app, path) {
42
+ return (app && matchDerivedLayout(app.getLayouts(), path))?.layout._internal.kind !== "singleton";
43
+ }
44
+ async function readLayoutPage({ app, match, pathname, reader, readBlocks }) {
45
+ const layout = match.layout._internal;
46
+ const blocksInScope = () => reader.inScope((draftId) => readBlocks(draftId, layout.id));
47
+ const load = (draftId) => loadLayoutResult(app, match, createRequestContext({ records: reader.records(draftId) }), reader.locale);
48
+ let result;
49
+ let read;
50
+ try {
51
+ if (reader.draft) [result, read] = await Promise.all([load(reader.draft.id), blocksInScope()]);
52
+ else {
53
+ read = await blocksInScope();
54
+ result = await load(read.draftId);
55
+ }
56
+ } catch (error) {
57
+ if (isNotFoundError(error)) return {
58
+ kind: "none",
59
+ layout: match.layout
60
+ };
61
+ throw error;
62
+ }
63
+ const { value: blocks, ...draftRead } = read;
64
+ return {
65
+ kind: "layout",
66
+ layout: match.layout,
67
+ params: match.params,
68
+ blocks,
69
+ result,
70
+ ...layoutPageContext(app, layout, {
71
+ path: pathname,
72
+ locale: reader.locale.tag,
73
+ projectName: blocks.projectName,
74
+ params: match.params,
75
+ result
76
+ }),
77
+ ...draftRead
78
+ };
79
+ }
80
+ const RECORD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
81
+ /** An envelope names its record; it doesn't authorize anything, the API does. */
82
+ function isValidCollectionItem(app, result) {
83
+ const data = result.data;
84
+ return typeof result.collectionId === "string" && !!app.getCollectionById(result.collectionId) && typeof data?.id === "string" && RECORD_ID.test(data.id) && typeof data.content === "object" && data.content !== null;
85
+ }
86
+ /**
87
+ * Runs a Layout Page's loader in the page's locale. The envelope survives SSR, navigation and
88
+ * hydration, and its data renders as returned: Camox never translates it.
89
+ */
90
+ async function loadLayoutResult(app, match, context, locale) {
91
+ const { loader, id } = match.layout._internal;
92
+ if (!loader) return void 0;
93
+ const result = await loader({
94
+ params: match.params,
95
+ context,
96
+ locale: locale.tag
97
+ });
98
+ const serializedResult = result === void 0 ? void 0 : JSON.parse(JSON.stringify(result));
99
+ const record = asCollectionItem(serializedResult);
100
+ if (!(record ? isValidCollectionItem(app, record) : serializedResult?.kind === "data" && Object.hasOwn(serializedResult, "data"))) throw new Error(`Layout "${id}" loader must return { kind: "data", data } with JSON-serializable data, or a collection-item envelope naming a registered collection and a record id`);
101
+ return serializedResult;
102
+ }
103
+
104
+ //#endregion
105
+ export { mayShowCuratedPage, readPage };
@@ -0,0 +1,25 @@
1
+ import { LocaleView } from "../../lib/locale-path.js";
2
+ import "../routes/pageRuntime.js";
3
+ import { Router } from "@camox/api-contract/router";
4
+ import { RouterClient } from "@orpc/server";
5
+ //#region src/features/runtime/reader.d.ts
6
+ /** The API's procedures, as the content port answers them. */
7
+ export type ContentClient = RouterClient<Router>;
8
+ /** What proves a read may see more than Live: a Member's session, or a look token (ADR 0032). */
9
+ export type Credential = {
10
+ session: string;
11
+ } | {
12
+ lookToken: string;
13
+ };
14
+ /** Which reads a client makes: with which credential, in which Draft (Live without), in which Locale. */
15
+ export interface ContentAccess {
16
+ credential?: Credential;
17
+ draftId?: string;
18
+ locale?: LocaleView;
19
+ }
20
+ /**
21
+ * The content port: the API's reads, as one credential sees them, in Live or a Draft, in a
22
+ * Locale. Production reads the API (`apiContent`); tests read an in-memory one.
23
+ */
24
+ export type Content = (access: ContentAccess) => ContentClient;
25
+ //#endregion