@capacms/sdk 1.0.0-next.7 → 1.0.0-next.9

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEFAULT_API_VERSION = exports.CAPA_ENV_ALIASES = exports.CAPA_ENV = exports.EDIT_CACHE_CONTROL = exports.DRAFT_COOKIE = exports.EDIT_HEADER = exports.EDIT_PARAM = exports.LAYOUT_PAGE = exports.gql = exports.MEDIA_TAG = exports.GRAPHQL_TAG = void 0;
3
+ exports.DRAFT_COOKIE_MAX_AGE = exports.DRAFT_ROBOTS_TAG = exports.CAPA_ADMIN_ORIGIN = exports.DEFAULT_API_VERSION = exports.CAPA_ENV_ALIASES = exports.CAPA_ENV = exports.EDIT_CACHE_CONTROL = exports.DRAFT_COOKIE = exports.EDIT_HEADER = exports.VIEW_PARAM = exports.PREVIEW_PARAM = exports.EDIT_PARAM = exports.LAYOUT_PAGE = exports.gql = exports.MEDIA_TAG = exports.GRAPHQL_TAG = void 0;
4
4
  exports.withCache = withCache;
5
5
  exports.modelTag = modelTag;
6
6
  exports.tagsFor = tagsFor;
@@ -15,6 +15,11 @@ exports.getPublishedClient = getPublishedClient;
15
15
  exports.getCapaClient = getCapaClient;
16
16
  exports.graphql = graphql;
17
17
  exports.safeSitePath = safeSitePath;
18
+ exports.frameAncestors = frameAncestors;
19
+ exports.draftHeaders = draftHeaders;
20
+ exports.capaHeaders = capaHeaders;
21
+ exports.frameDraftCookie = frameDraftCookie;
22
+ exports.clearDraftCookie = clearDraftCookie;
18
23
  exports.createPreviewRoute = createPreviewRoute;
19
24
  exports.exitPreviewRoute = exitPreviewRoute;
20
25
  exports.capaMiddleware = capaMiddleware;
@@ -284,6 +289,10 @@ function pagesFor(client) {
284
289
  * clickable. It never switches the site to draft data.
285
290
  */
286
291
  exports.EDIT_PARAM = "capa-edit";
292
+ /** The query parameter a Capa preview link carries: `?capa-preview=<token>`. */
293
+ exports.PREVIEW_PARAM = "capa-preview";
294
+ /** The query parameter of the editor's Published view: `?capa-view=published`. */
295
+ exports.VIEW_PARAM = "capa-view";
287
296
  /**
288
297
  * The request header `resolveEditRequest` sets once a `capa-edit` token has
289
298
  * been verified, for `editMode()` to read. Any copy a browser sent is removed
@@ -322,6 +331,7 @@ function verifiedEdit(headers) {
322
331
  * const edit = await resolveEditRequest(request, publishedClient());
323
332
  * const response = NextResponse.next({ request: { headers: edit.headers } });
324
333
  * if (edit.cacheControl) response.headers.set("Cache-Control", edit.cacheControl);
334
+ * if (edit.robotsTag) response.headers.set("X-Robots-Tag", edit.robotsTag);
325
335
  * return response;
326
336
  * }
327
337
  *
@@ -348,7 +358,13 @@ async function resolveEditRequest(request, client) {
348
358
  const draft = request.cookies?.has(exports.DRAFT_COOKIE) ??
349
359
  (headers.get("cookie") ?? "").split(/;\s*/).some((c) => c.startsWith(`${exports.DRAFT_COOKIE}=`));
350
360
  const edit = verified || draft;
351
- return { edit, verified, headers, cacheControl: edit ? exports.EDIT_CACHE_CONTROL : null };
361
+ return {
362
+ edit,
363
+ verified,
364
+ headers,
365
+ cacheControl: edit ? exports.EDIT_CACHE_CONTROL : null,
366
+ robotsTag: edit ? exports.DRAFT_ROBOTS_TAG : null,
367
+ };
352
368
  }
353
369
  // ------------------------------------------------- five-minute integration ---
354
370
  /**
@@ -570,26 +586,166 @@ function safeSitePath(value) {
570
586
  return "/";
571
587
  return value;
572
588
  }
589
+ // -------------------------------------------------------- draft responses ---
590
+ /** The Capa admin's origin: what frames a draft in the editor, unless a site names another. */
591
+ exports.CAPA_ADMIN_ORIGIN = "https://app.capacms.com";
592
+ /** `X-Robots-Tag` on every draft and edit-mode response: a draft is never indexed, nor its links followed. */
593
+ exports.DRAFT_ROBOTS_TAG = "noindex, nofollow";
594
+ /** How long the draft cookie lasts, in seconds: one hour, as a preview token does. */
595
+ exports.DRAFT_COOKIE_MAX_AGE = 3600;
596
+ /**
597
+ * `frame-ancestors 'self' <origins>`, the CSP directive that lets the Capa
598
+ * editor frame a draft and nothing else frame it. Each origin is a scheme and
599
+ * a host, with a port when it has one, and is written as the URL parser
600
+ * normalises it, so no `;` or quote can reach the policy. Anything else, a
601
+ * path or a query included, throws a `TypeError`.
602
+ */
603
+ function frameAncestors(adminOrigins = [exports.CAPA_ADMIN_ORIGIN]) {
604
+ const origins = new Set();
605
+ for (const value of adminOrigins)
606
+ origins.add(originOf(value));
607
+ return ["frame-ancestors 'self'", ...origins].join(" ");
608
+ }
609
+ function originOf(value) {
610
+ let url = null;
611
+ try {
612
+ url = typeof value === "string" ? new URL(value) : null;
613
+ }
614
+ catch {
615
+ url = null;
616
+ }
617
+ const bare = url !== null &&
618
+ (url.protocol === "https:" || url.protocol === "http:") &&
619
+ url.pathname === "/" &&
620
+ url.search === "" &&
621
+ url.hash === "" &&
622
+ url.username === "" &&
623
+ url.password === "";
624
+ if (!bare) {
625
+ throw new TypeError(`@capacms/sdk/nextjs: ${JSON.stringify(value)} is not an origin. Pass a scheme and a host, such as ${exports.CAPA_ADMIN_ORIGIN}.`);
626
+ }
627
+ return url.origin;
628
+ }
629
+ /**
630
+ * The headers every draft response carries: `X-Robots-Tag: noindex, nofollow`,
631
+ * and `Content-Security-Policy: frame-ancestors 'self' https://app.capacms.com`
632
+ * (or the `adminOrigins` given). Throws for an origin that is not one.
633
+ */
634
+ function draftHeaders(options = {}) {
635
+ return {
636
+ "X-Robots-Tag": exports.DRAFT_ROBOTS_TAG,
637
+ "Content-Security-Policy": frameAncestors(options.adminOrigins),
638
+ };
639
+ }
640
+ /**
641
+ * Rules for `next.config`'s `headers()` that send `draftHeaders` on draft
642
+ * responses only:
643
+ *
644
+ * // next.config.mjs
645
+ * import { capaHeaders } from "@capacms/sdk/nextjs";
646
+ * export default { async headers() { return [...capaHeaders()]; } };
647
+ *
648
+ * A rule matches a request carrying the draft cookie, or a `capa-preview`,
649
+ * `capa-edit` or `capa-view` query. A visitor's request carries none of
650
+ * them, so its response, cached or not, is exactly what it was. Next checks
651
+ * the cookie by name, not by value, so a forged cookie only adds these
652
+ * headers to the forger's own response.
653
+ *
654
+ * A site that sends its own `frame-ancestors` or `X-Frame-Options` must leave
655
+ * them off draft responses (`missing: [{ type: "cookie", key: DRAFT_COOKIE }]`
656
+ * on its rule): a browser applies every CSP it is sent, so the strictest wins.
657
+ */
658
+ function capaHeaders(options = {}) {
659
+ const headers = () => Object.entries(draftHeaders(options)).map(([key, value]) => ({ key, value }));
660
+ const conditions = [
661
+ { type: "cookie", key: exports.DRAFT_COOKIE },
662
+ { type: "query", key: exports.PREVIEW_PARAM },
663
+ { type: "query", key: exports.EDIT_PARAM },
664
+ { type: "query", key: exports.VIEW_PARAM },
665
+ ];
666
+ return conditions.map((condition) => ({ source: "/:path*", has: [condition], headers: headers() }));
667
+ }
668
+ /** The attributes a draft cookie needs to be sent inside the Capa editor's cross-site frame. */
669
+ const FRAMED = { path: "/", httpOnly: true, secure: true, sameSite: "none", partitioned: true };
670
+ function draftCookieMaxAge(maxAge = exports.DRAFT_COOKIE_MAX_AGE) {
671
+ if (typeof maxAge !== "number" || !Number.isInteger(maxAge) || maxAge <= 0) {
672
+ throw new TypeError(`@capacms/sdk/nextjs: maxAge must be a whole number of seconds above 0, and got ${String(maxAge)}.`);
673
+ }
674
+ return maxAge;
675
+ }
676
+ /**
677
+ * Re-set the cookie `draftMode().enable()` just set, so the Capa editor can
678
+ * use it: `HttpOnly; Secure; SameSite=None; Partitioned; Path=/` and
679
+ * `Max-Age` one hour (`maxAge` seconds). Next sets it with no `Max-Age`, so
680
+ * it unlocks every draft on the site until the browser closes, and without
681
+ * `Partitioned`, which Safari 26.2 and later need to send a cookie into a
682
+ * cross-site frame. Call it after `enable()`, in the same route handler.
683
+ * Resolves false, and sets nothing, when there is no draft cookie to re-set.
684
+ */
685
+ async function frameDraftCookie(cookies, options = {}) {
686
+ const maxAge = draftCookieMaxAge(options.maxAge);
687
+ const jar = await cookies();
688
+ const current = jar.get(exports.DRAFT_COOKIE);
689
+ if (!current?.value)
690
+ return false;
691
+ jar.set({ name: exports.DRAFT_COOKIE, value: current.value, ...FRAMED, maxAge });
692
+ return true;
693
+ }
694
+ /**
695
+ * Delete the draft cookie `frameDraftCookie` set. A partitioned cookie is
696
+ * deleted only by a `Set-Cookie` that is partitioned too, which
697
+ * `draftMode().disable()` is not. Call it after `disable()`; it replaces
698
+ * `disable()`'s own deletion, since a response sets one cookie per name. A
699
+ * draft cookie set without `Partitioned`, before a site used
700
+ * `frameDraftCookie`, ends when the browser closes, as it always did.
701
+ */
702
+ async function clearDraftCookie(cookies) {
703
+ (await cookies()).set({ name: exports.DRAFT_COOKIE, value: "", ...FRAMED, expires: new Date(0) });
704
+ }
705
+ /**
706
+ * The preview and exit routes' own redirect. Next's `redirect()` throws and
707
+ * answers for the route, so it cannot carry headers; this one is a plain
708
+ * `Response`, to which Next appends every cookie the route set. It sets no
709
+ * cookie itself: Next keeps one per name, and the response's own would win.
710
+ */
711
+ function routeRedirect(location, headers) {
712
+ return new Response(null, {
713
+ status: 307,
714
+ headers: { Location: location, "Cache-Control": exports.EDIT_CACHE_CONTROL, "Referrer-Policy": "no-referrer", ...headers },
715
+ });
716
+ }
573
717
  /**
574
718
  * `app/api/capa/preview/route.ts`:
575
719
  *
576
- * import { draftMode } from "next/headers";
577
- * import { redirect } from "next/navigation";
578
- * export const GET = createPreviewRoute({ draftMode, redirect });
720
+ * import { cookies, draftMode } from "next/headers";
721
+ * export const GET = createPreviewRoute({ draftMode, cookies });
579
722
  *
580
723
  * Checks the token with Capa (the site never holds the signing key), turns
581
724
  * draft mode on and lands on the entry's page. A bad or expired token lands on
582
725
  * the page without draft mode and `?preview=expired`; Capa unreachable gives
583
- * `?preview=unavailable`.
726
+ * `?preview=unavailable`. The token is checked with any key the site holds,
727
+ * its legacy key included (`getPublishedClient`, from `CAPA_KEY`).
728
+ *
729
+ * Given `cookies`, the draft cookie is re-set by `frameDraftCookie`: one hour,
730
+ * and sent inside the editor's frame. With no `redirect`, the route answers
731
+ * with its own 307, marked `X-Robots-Tag: noindex, nofollow`,
732
+ * `Referrer-Policy: no-referrer` (the token is in the URL),
733
+ * `Cache-Control: private, no-store` and the editor's `frame-ancestors`.
734
+ * Given Next's `redirect`, it is called instead, as before, and the redirect
735
+ * carries none of those.
584
736
  */
585
737
  function createPreviewRoute(input) {
738
+ // Checked here, so a bad origin or maxAge fails where the route is built.
739
+ const headers = draftHeaders({ adminOrigins: input.adminOrigins });
740
+ const maxAge = draftCookieMaxAge(input.maxAge);
741
+ const go = (location) => input.redirect ? input.redirect(location) : routeRedirect(location, headers);
586
742
  return async (request) => {
587
743
  const url = new URL(request.url);
588
- const token = url.searchParams.get("token") ?? url.searchParams.get("capa-preview") ?? "";
744
+ const token = url.searchParams.get("token") ?? url.searchParams.get(exports.PREVIEW_PARAM) ?? "";
589
745
  // Reached two ways: directly, or through the middleware's rewrite of a page
590
746
  // URL carrying `?capa-preview=`, where Next may hand over the ORIGINAL URL.
591
747
  // Then the page itself is the path.
592
- const path = safeSitePath(url.searchParams.get("path") ?? (url.searchParams.has("capa-preview") ? url.pathname : null));
748
+ const path = safeSitePath(url.searchParams.get("path") ?? (url.searchParams.has(exports.PREVIEW_PARAM) ? url.pathname : null));
593
749
  let claim = null;
594
750
  let failed = false;
595
751
  try {
@@ -601,18 +757,36 @@ function createPreviewRoute(input) {
601
757
  const draft = await input.draftMode();
602
758
  if (!claim) {
603
759
  draft.disable?.();
604
- return input.redirect(`${path}?preview=${failed ? "unavailable" : "expired"}`);
760
+ if (input.cookies)
761
+ await clearDraftCookie(input.cookies);
762
+ return go(`${path}?preview=${failed ? "unavailable" : "expired"}`);
605
763
  }
606
764
  draft.enable?.();
765
+ if (input.cookies)
766
+ await frameDraftCookie(input.cookies, { maxAge });
607
767
  await input.onEnable?.();
608
- return input.redirect(safeSitePath(claim.path ?? path));
768
+ return go(safeSitePath(claim.path ?? path));
609
769
  };
610
770
  }
611
- /** `app/api/capa/exit/route.ts`: `export const GET = exitPreviewRoute({ draftMode, redirect });` */
771
+ /**
772
+ * `app/api/capa/exit/route.ts`:
773
+ *
774
+ * import { cookies, draftMode } from "next/headers";
775
+ * export const GET = exitPreviewRoute({ draftMode, cookies });
776
+ *
777
+ * Turns draft mode off and lands on `?path=`. Given `cookies`, the framed
778
+ * cookie `createPreviewRoute` set is deleted too. With no `redirect`, the route
779
+ * answers with its own 307, marked noindex and never cached.
780
+ */
612
781
  function exitPreviewRoute(input) {
613
782
  return async (request) => {
614
783
  (await input.draftMode()).disable?.();
615
- return input.redirect(safeSitePath(new URL(request.url).searchParams.get("path")));
784
+ if (input.cookies)
785
+ await clearDraftCookie(input.cookies);
786
+ const location = safeSitePath(new URL(request.url).searchParams.get("path"));
787
+ if (input.redirect)
788
+ return input.redirect(location);
789
+ return routeRedirect(location, { "X-Robots-Tag": exports.DRAFT_ROBOTS_TAG });
616
790
  };
617
791
  }
618
792
  /**
@@ -626,12 +800,25 @@ function exitPreviewRoute(input) {
626
800
  * - `?capa-view=published` renders without the draft cookie, for the editor's
627
801
  * Published view;
628
802
  * - `?capa-edit=<token>` turns edit mode on (ids and overlay, published data);
629
- * - every edit-mode response is `private, no-store`.
803
+ * - every edit-mode response is `private, no-store`, and it and every request
804
+ * carrying `capa-edit` or `capa-view` is `X-Robots-Tag: noindex, nofollow`.
805
+ *
806
+ * Give it a `matcher` so a visitor's request never runs it (Next reads
807
+ * `config` from the file itself, so it is written out there):
808
+ *
809
+ * export const config = {
810
+ * matcher: [
811
+ * { source: "/:path*", has: [{ type: "query", key: "capa-preview" }] },
812
+ * { source: "/:path*", has: [{ type: "query", key: "capa-edit" }] },
813
+ * { source: "/:path*", has: [{ type: "query", key: "capa-view" }] },
814
+ * { source: "/:path*", has: [{ type: "cookie", key: "__prerender_bypass" }] },
815
+ * ],
816
+ * };
630
817
  */
631
818
  function capaMiddleware(input) {
632
819
  const previewRoute = input.previewRoute ?? "/api/capa/preview";
633
820
  return async (request) => {
634
- const token = request.nextUrl.searchParams.get("capa-preview");
821
+ const token = request.nextUrl.searchParams.get(exports.PREVIEW_PARAM);
635
822
  if (token) {
636
823
  const target = request.nextUrl.clone();
637
824
  target.pathname = previewRoute;
@@ -641,7 +828,7 @@ function capaMiddleware(input) {
641
828
  return input.NextResponse.rewrite(target);
642
829
  }
643
830
  const headers = new Headers(request.headers);
644
- if (request.nextUrl.searchParams.get("capa-view") === "published") {
831
+ if (request.nextUrl.searchParams.get(exports.VIEW_PARAM) === "published") {
645
832
  const cookies = request.cookies
646
833
  .getAll()
647
834
  .filter((cookie) => cookie.name !== exports.DRAFT_COOKIE)
@@ -658,6 +845,11 @@ function capaMiddleware(input) {
658
845
  const response = input.NextResponse.next({ request: { headers: edit.headers } });
659
846
  if (edit.cacheControl)
660
847
  response.headers.set("Cache-Control", edit.cacheControl);
848
+ // A capa- link is the editor's, verified or not, and never a page to index.
849
+ const capaLink = request.nextUrl.searchParams.has(exports.EDIT_PARAM) || request.nextUrl.searchParams.has(exports.VIEW_PARAM);
850
+ const robots = edit.robotsTag ?? (capaLink ? exports.DRAFT_ROBOTS_TAG : null);
851
+ if (robots)
852
+ response.headers.set("X-Robots-Tag", robots);
661
853
  return response;
662
854
  };
663
855
  }
@@ -1,5 +1,30 @@
1
1
  export interface CapaOverlayProps {
2
2
  /** The Capa admin origins allowed to drive the overlay. */
3
3
  adminOrigins: string[];
4
+ /**
5
+ * How a save shows. `"in-place"` (the default) re-renders the draft with
6
+ * `router.refresh()`, and reloads the page only if that has not landed
7
+ * within `refreshTimeoutMs`. `"reload"` reloads the page on every save.
8
+ * The scroll position is kept either way.
9
+ */
10
+ refresh?: "in-place" | "reload";
11
+ /** How long an in-place refresh may take before the page reloads instead. 10 seconds. */
12
+ refreshTimeoutMs?: number;
4
13
  }
5
- export declare function CapaOverlay({ adminOrigins }: CapaOverlayProps): null;
14
+ /**
15
+ * The refresh runs in a transition, so `isPending` says when the new draft
16
+ * has been committed to the screen, and the overlay settles the refresh
17
+ * then: it puts the scroll position back, and a refresh that has not
18
+ * committed within `refreshTimeoutMs` reloads.
19
+ *
20
+ * While the transition is pending the overlay makes a no-op state update
21
+ * every `REFRESH_NUDGE_MS`. React 19.2 under Next 15.5 can leave a draft
22
+ * refresh suspended after every part of its streamed response has arrived:
23
+ * the page suspends inside an already visible Suspense boundary (a root
24
+ * `loading.tsx` makes one around every page), and the signal that its data
25
+ * is ready is lost, so nothing commits until some other state update. Any
26
+ * state update makes React retry the suspended render, which then completes.
27
+ * A refresh that commits on its own stops the nudges at once. Draft mode
28
+ * only: a visitor never runs this component.
29
+ */
30
+ export declare function CapaOverlay({ adminOrigins, refresh, refreshTimeoutMs }: CapaOverlayProps): null;
@@ -18,18 +18,61 @@ exports.CapaOverlay = CapaOverlay;
18
18
  *
19
19
  * Its own entry point, apart from `@capacms/sdk/nextjs`, because it imports
20
20
  * `react` and `next/navigation` and is a client module: the server helpers must
21
- * stay free of both.
21
+ * stay free of both. A bundler that imports it gets an ES module, so it sees
22
+ * that only `useRouter` is used from `next/navigation` and leaves the chunks a
23
+ * visitor loads as they were.
22
24
  */
23
25
  const react_1 = require("react");
24
26
  const navigation_1 = require("next/navigation");
25
- const overlay_1 = require("../overlay");
26
- function CapaOverlay({ adminOrigins }) {
27
+ const index_js_1 = require("../overlay/index.js");
28
+ /**
29
+ * While a refresh is pending, the overlay updates its own unused state this
30
+ * often. See `CapaOverlay` for why.
31
+ */
32
+ const REFRESH_NUDGE_MS = 300;
33
+ /**
34
+ * The refresh runs in a transition, so `isPending` says when the new draft
35
+ * has been committed to the screen, and the overlay settles the refresh
36
+ * then: it puts the scroll position back, and a refresh that has not
37
+ * committed within `refreshTimeoutMs` reloads.
38
+ *
39
+ * While the transition is pending the overlay makes a no-op state update
40
+ * every `REFRESH_NUDGE_MS`. React 19.2 under Next 15.5 can leave a draft
41
+ * refresh suspended after every part of its streamed response has arrived:
42
+ * the page suspends inside an already visible Suspense boundary (a root
43
+ * `loading.tsx` makes one around every page), and the signal that its data
44
+ * is ready is lost, so nothing commits until some other state update. Any
45
+ * state update makes React retry the suspended render, which then completes.
46
+ * A refresh that commits on its own stops the nudges at once. Draft mode
47
+ * only: a visitor never runs this component.
48
+ */
49
+ function CapaOverlay({ adminOrigins, refresh = "in-place", refreshTimeoutMs }) {
27
50
  const router = (0, navigation_1.useRouter)();
51
+ const [refreshing, startRefresh] = (0, react_1.useTransition)();
52
+ const [, nudge] = (0, react_1.useState)(0);
53
+ /** One resolver per refresh waiting for its transition to commit. */
54
+ const waiting = (0, react_1.useRef)([]);
28
55
  // A string, so a new array with the same origins does not restart it.
29
56
  const origins = adminOrigins.join(",");
30
- (0, react_1.useEffect)(() => (0, overlay_1.startOverlay)({
57
+ (0, react_1.useEffect)(() => (0, index_js_1.startOverlay)({
31
58
  adminOrigins: origins.split(",").filter(Boolean),
32
- onRefresh: () => router.refresh(),
33
- }), [origins, router]);
59
+ onRefresh: refresh === "reload"
60
+ ? undefined
61
+ : () => new Promise((resolve) => {
62
+ waiting.current.push(resolve);
63
+ startRefresh(() => router.refresh());
64
+ }),
65
+ refreshTimeoutMs,
66
+ }), [origins, router, refresh, refreshTimeoutMs]);
67
+ (0, react_1.useEffect)(() => {
68
+ if (!refreshing) {
69
+ // Committed: every refresh started before now is on screen.
70
+ for (const settle of waiting.current.splice(0))
71
+ settle();
72
+ return;
73
+ }
74
+ const timer = window.setInterval(() => nudge((n) => n + 1), REFRESH_NUDGE_MS);
75
+ return () => window.clearInterval(timer);
76
+ }, [refreshing]);
34
77
  return null;
35
78
  }
@@ -1,5 +1,5 @@
1
- export { acceptMessage, hoverMessage, normaliseOrigins, pickCentred, readyMessage, scrollEdge, selectMessage, visibleMessage, ADMIN_SOURCE, PROTOCOL_VERSION, SITE_SOURCE, } from "./protocol";
2
- export type { AdminMessage, MessageLike, SiteMessage, TaggedBox } from "./protocol";
1
+ export { acceptMessage, hoverMessage, normaliseOrigins, pickCentred, readyMessage, scrollEdge, selectMessage, visibleMessage, ADMIN_SOURCE, PROTOCOL_VERSION, SITE_SOURCE, } from "./protocol.js";
2
+ export type { AdminMessage, MessageLike, SiteMessage, TaggedBox } from "./protocol.js";
3
3
  export interface OverlayOptions {
4
4
  /**
5
5
  * The Capa admin origins allowed to drive this page, for example
@@ -9,9 +9,21 @@ export interface OverlayOptions {
9
9
  /**
10
10
  * How to re-render the draft after the editor saves. `router.refresh()` in a
11
11
  * Next app. Without it the page reloads. Scroll position is kept either way.
12
+ *
13
+ * Return a promise that settles once the new draft is on screen, and the
14
+ * overlay waits for it: a promise that rejects, or is still pending after
15
+ * `refreshTimeoutMs`, falls back to a reload, which keeps the scroll
16
+ * position too. A function that returns nothing counts as done at once.
12
17
  */
13
18
  onRefresh?: () => void | Promise<void>;
19
+ /**
20
+ * How long `onRefresh`'s promise may stay pending before the page reloads
21
+ * instead. 10 seconds.
22
+ */
23
+ refreshTimeoutMs?: number;
14
24
  }
25
+ /** How long an in-place refresh may take before the page reloads instead. */
26
+ export declare const REFRESH_TIMEOUT_MS = 10000;
15
27
  /**
16
28
  * Start the overlay. Returns a disposer that removes every listener and the
17
29
  * drawing layer. Calling it again while it runs updates the options and returns