@ampless/admin 1.0.0-alpha.74 → 1.0.0-alpha.76

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.
@@ -5,6 +5,9 @@ import {
5
5
  getMediaProcessingDefaults,
6
6
  uploadProcessedImage
7
7
  } from "./chunk-DDAGBC4N.js";
8
+ import {
9
+ getAdminEditorExtensions
10
+ } from "./chunk-WUYJ42LP.js";
8
11
  import {
9
12
  publicMediaUrl
10
13
  } from "./chunk-2ITWLRYF.js";
@@ -165,7 +168,6 @@ import {
165
168
  formatDate
166
169
  } from "ampless";
167
170
  import {
168
- renderBody as renderBody2,
169
171
  tiptapToHtml,
170
172
  tiptapToMarkdown,
171
173
  markdownToHtml,
@@ -581,6 +583,7 @@ var EDITOR_STYLES = `
581
583
  }
582
584
  `;
583
585
  function TiptapEditor({ initialContent, onChange, onUserEdit }) {
586
+ const installed = getAdminEditorExtensions();
584
587
  const editor = useEditor({
585
588
  extensions: [
586
589
  StarterKit,
@@ -594,7 +597,9 @@ function TiptapEditor({ initialContent, onChange, onUserEdit }) {
594
597
  TaskItem.configure({ nested: true }),
595
598
  Underline2,
596
599
  Highlight.configure({ multicolor: false }),
597
- TextAlign.configure({ types: ["heading", "paragraph"] })
600
+ TextAlign.configure({ types: ["heading", "paragraph"] }),
601
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
602
+ ...installed
598
603
  ],
599
604
  content: initialContent ?? { type: "doc", content: [{ type: "paragraph" }] },
600
605
  immediatelyRender: false,
@@ -622,7 +627,6 @@ function TiptapEditor({ initialContent, onChange, onUserEdit }) {
622
627
  // src/components/post-history-panel.tsx
623
628
  import { useCallback, useEffect as useEffect3, useState as useState3 } from "react";
624
629
  import { listPostHistory } from "ampless";
625
- import { renderBody } from "@ampless/runtime";
626
630
  import { Button as Button5 } from "@ampless/runtime/ui";
627
631
  import { Fragment as Fragment2, jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
628
632
  var PAGE_SIZE = 20;
@@ -656,7 +660,11 @@ function revisionAsPost(rev) {
656
660
  metadata: rev.metadata
657
661
  };
658
662
  }
659
- function PostHistoryPanel({ postId, onRestore }) {
663
+ function PostHistoryPanel({
664
+ postId,
665
+ onRestore,
666
+ previewEndpoint = "/admin/preview"
667
+ }) {
660
668
  const t = useT();
661
669
  const config = getAdminCmsConfig();
662
670
  const timezone = config?.timezone ?? "UTC";
@@ -668,6 +676,29 @@ function PostHistoryPanel({ postId, onRestore }) {
668
676
  const [revisions, setRevisions] = useState3([]);
669
677
  const [nextToken, setNextToken] = useState3(void 0);
670
678
  const [selected, setSelected] = useState3(null);
679
+ const [previewHtml, setPreviewHtml] = useState3("");
680
+ const selectedHistoryId = selected?.postHistoryId;
681
+ const selectedFormat = selected?.format;
682
+ useEffect3(() => {
683
+ if (!selected) return;
684
+ if (selected.format === "static") return;
685
+ const ctrl = new AbortController();
686
+ fetch(previewEndpoint, {
687
+ method: "POST",
688
+ headers: { "Content-Type": "application/json" },
689
+ body: JSON.stringify(revisionAsPost(selected)),
690
+ signal: ctrl.signal
691
+ }).then((r) => {
692
+ if (!r.ok) throw new Error(`preview ${r.status}`);
693
+ return r.text();
694
+ }).then((html) => {
695
+ if (!ctrl.signal.aborted) setPreviewHtml(html);
696
+ }).catch((err) => {
697
+ if (ctrl.signal.aborted) return;
698
+ console.error("[ampless admin] revision preview fetch failed:", err);
699
+ });
700
+ return () => ctrl.abort();
701
+ }, [selectedHistoryId, selectedFormat, previewEndpoint]);
671
702
  const loadPage = useCallback(
672
703
  async (token) => {
673
704
  setLoading(true);
@@ -785,10 +816,12 @@ function PostHistoryPanel({ postId, onRestore }) {
785
816
  ] })
786
817
  ] }),
787
818
  selected.format === "static" ? /* @__PURE__ */ jsx6("p", { className: "rounded-md bg-amber-50 p-2 text-xs text-amber-700 dark:bg-amber-950 dark:text-amber-400", children: t("posts.history.staticCaveat") }) : /* @__PURE__ */ jsx6(
788
- "div",
819
+ "iframe",
789
820
  {
790
- className: "prose prose-neutral dark:prose-invert max-w-none text-sm",
791
- dangerouslySetInnerHTML: { __html: renderBody(revisionAsPost(selected)) }
821
+ title: "revision-preview",
822
+ srcDoc: previewHtml,
823
+ sandbox: "allow-scripts",
824
+ className: "prose prose-neutral dark:prose-invert max-w-none min-h-[300px] w-full rounded-md border text-sm"
792
825
  }
793
826
  )
794
827
  ] })
@@ -1263,7 +1296,7 @@ function formatDraftTime(epochMs, timezone, locale) {
1263
1296
  return d.toLocaleString();
1264
1297
  }
1265
1298
  }
1266
- function PostForm({ post }) {
1299
+ function PostForm({ post, previewEndpoint = "/admin/preview" }) {
1267
1300
  const router = useRouter();
1268
1301
  const t = useT();
1269
1302
  const isEdit = !!post;
@@ -1280,6 +1313,7 @@ function PostForm({ post }) {
1280
1313
  const [saving, setSaving] = useState5(false);
1281
1314
  const [error, setError] = useState5(null);
1282
1315
  const [view, setView] = useState5("edit");
1316
+ const [previewHtml, setPreviewHtml] = useState5("");
1283
1317
  const [editorEpoch, setEditorEpoch] = useState5(0);
1284
1318
  const dirtyRef = useRef3(false);
1285
1319
  function markDirty() {
@@ -1558,6 +1592,42 @@ function PostForm({ post }) {
1558
1592
  publishedAt: resolvedPublishedAt,
1559
1593
  tags: parseTags(tagsInput)
1560
1594
  };
1595
+ useEffect4(() => {
1596
+ if (view !== "preview") return;
1597
+ if (format === "static") return;
1598
+ const ctrl = new AbortController();
1599
+ const tid = window.setTimeout(() => {
1600
+ fetch(previewEndpoint, {
1601
+ method: "POST",
1602
+ headers: { "Content-Type": "application/json" },
1603
+ body: JSON.stringify(previewPost),
1604
+ signal: ctrl.signal
1605
+ }).then((r) => {
1606
+ if (!r.ok) throw new Error(`preview ${r.status}`);
1607
+ return r.text();
1608
+ }).then((html) => {
1609
+ if (!ctrl.signal.aborted) setPreviewHtml(html);
1610
+ }).catch((err) => {
1611
+ if (ctrl.signal.aborted) return;
1612
+ console.error("[ampless admin] preview fetch failed:", err);
1613
+ });
1614
+ }, 250);
1615
+ return () => {
1616
+ ctrl.abort();
1617
+ window.clearTimeout(tid);
1618
+ };
1619
+ }, [
1620
+ view,
1621
+ previewEndpoint,
1622
+ format,
1623
+ title,
1624
+ slug,
1625
+ excerpt,
1626
+ body,
1627
+ status,
1628
+ resolvedPublishedAt,
1629
+ tagsInput
1630
+ ]);
1561
1631
  return /* @__PURE__ */ jsxs8("form", { onSubmit: save, className: "space-y-6", children: [
1562
1632
  /* @__PURE__ */ jsxs8("div", { className: "flex gap-1 border-b", children: [
1563
1633
  /* @__PURE__ */ jsx8(
@@ -1591,12 +1661,24 @@ function PostForm({ post }) {
1591
1661
  ] }),
1592
1662
  excerpt && /* @__PURE__ */ jsx8("p", { className: "mt-3 text-base text-muted-foreground", children: excerpt })
1593
1663
  ] }),
1594
- format === "static" ? /* @__PURE__ */ jsx8("p", { className: "text-sm text-muted-foreground", children: t("posts.form.static.previewHint") }) : /* @__PURE__ */ jsx8(
1595
- "div",
1596
- {
1597
- className: "prose prose-neutral dark:prose-invert max-w-none",
1598
- dangerouslySetInnerHTML: { __html: renderBody2(previewPost) }
1599
- }
1664
+ format === "static" ? /* @__PURE__ */ jsx8("p", { className: "text-sm text-muted-foreground", children: t("posts.form.static.previewHint") }) : (
1665
+ // Phase 7: preview HTML is fetched from the template's
1666
+ // `/admin/preview` Route Handler so `ampless.renderBody`
1667
+ // (async ReactNode) + `ampless.publicPostScriptsForPage(
1668
+ // [draft])` can run server-side. The result is injected
1669
+ // into an iframe with sandbox=`allow-scripts` only (no
1670
+ // `allow-same-origin`) so 3rd-party widget scripts
1671
+ // (YouTube iframes, x.com widgets.js) can hydrate without
1672
+ // crossing the admin's same-origin boundary.
1673
+ /* @__PURE__ */ jsx8(
1674
+ "iframe",
1675
+ {
1676
+ title: "post-preview",
1677
+ srcDoc: previewHtml,
1678
+ sandbox: "allow-scripts",
1679
+ className: "prose prose-neutral dark:prose-invert max-w-none min-h-[400px] w-full rounded-md border"
1680
+ }
1681
+ )
1600
1682
  ),
1601
1683
  previewPost.tags && previewPost.tags.length > 0 && /* @__PURE__ */ jsx8("div", { className: "flex flex-wrap gap-2 border-t pt-4 text-sm", children: previewPost.tags.map((tag) => /* @__PURE__ */ jsxs8(
1602
1684
  "span",
@@ -1821,7 +1903,14 @@ function PostForm({ post }) {
1821
1903
  /* @__PURE__ */ jsx8(Button7, { type: "submit", disabled: saving, children: saving ? t("common.saving") : isEdit ? t("posts.form.saveChanges") : t("posts.form.createPost") }),
1822
1904
  isEdit && /* @__PURE__ */ jsx8(Button7, { type: "button", variant: "destructive", onClick: handleDelete, disabled: saving, children: t("posts.form.delete") })
1823
1905
  ] }),
1824
- isEdit && post && /* @__PURE__ */ jsx8(PostHistoryPanel, { postId: post.postId, onRestore: restoreRevision })
1906
+ isEdit && post && /* @__PURE__ */ jsx8(
1907
+ PostHistoryPanel,
1908
+ {
1909
+ postId: post.postId,
1910
+ onRestore: restoreRevision,
1911
+ previewEndpoint
1912
+ }
1913
+ )
1825
1914
  ] })
1826
1915
  ] });
1827
1916
  }
@@ -4,16 +4,16 @@ import {
4
4
  } from "./chunk-D72XF3Q3.js";
5
5
  import {
6
6
  clearAllDrafts
7
- } from "./chunk-ECN2MBJN.js";
8
- import {
9
- setMcpTokenStore
10
- } from "./chunk-C7G5AQ3L.js";
7
+ } from "./chunk-6MDPOMKE.js";
11
8
  import {
12
9
  setAdminCmsConfigClient
13
10
  } from "./chunk-DDAGBC4N.js";
14
11
  import {
15
12
  setAdminMediaContext
16
13
  } from "./chunk-2ITWLRYF.js";
14
+ import {
15
+ setMcpTokenStore
16
+ } from "./chunk-C7G5AQ3L.js";
17
17
  import {
18
18
  useLocale,
19
19
  useT
@@ -1,18 +1,18 @@
1
1
  'use client';
2
2
  import {
3
3
  PostForm
4
- } from "./chunk-ECN2MBJN.js";
4
+ } from "./chunk-6MDPOMKE.js";
5
5
  import {
6
6
  useT
7
7
  } from "./chunk-DUSEHGSV.js";
8
8
 
9
9
  // src/components/new-post-view.tsx
10
10
  import { jsx, jsxs } from "react/jsx-runtime";
11
- function NewPostPage() {
11
+ function NewPostPage({ previewEndpoint }) {
12
12
  const t = useT();
13
13
  return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-7xl p-4 md:p-8", children: [
14
14
  /* @__PURE__ */ jsx("h1", { className: "mb-6 text-2xl font-bold md:mb-8 md:text-3xl", children: t("posts.form.newTitle") }),
15
- /* @__PURE__ */ jsx(PostForm, {})
15
+ /* @__PURE__ */ jsx(PostForm, { previewEndpoint })
16
16
  ] });
17
17
  }
18
18
 
@@ -0,0 +1,28 @@
1
+ 'use client';
2
+ // src/editor/admin-editor-extensions.ts
3
+ var extensions = [];
4
+ var installed = false;
5
+ function installAdminEditorExtensions(list) {
6
+ if (installed) return;
7
+ const seen = /* @__PURE__ */ new Set();
8
+ for (const ext of list) {
9
+ const n = typeof ext.name === "string" ? ext.name : "";
10
+ if (!n) continue;
11
+ if (seen.has(n)) {
12
+ throw new Error(
13
+ `[ampless admin editor] duplicate tiptap extension name "${n}" in installAdminEditorExtensions(). Each extension may be registered at most once.`
14
+ );
15
+ }
16
+ seen.add(n);
17
+ }
18
+ extensions = list;
19
+ installed = true;
20
+ }
21
+ function getAdminEditorExtensions() {
22
+ return extensions;
23
+ }
24
+
25
+ export {
26
+ installAdminEditorExtensions,
27
+ getAdminEditorExtensions
28
+ };
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import {
3
3
  PostForm
4
- } from "./chunk-ECN2MBJN.js";
4
+ } from "./chunk-6MDPOMKE.js";
5
5
  import {
6
6
  useT
7
7
  } from "./chunk-DUSEHGSV.js";
@@ -11,7 +11,7 @@ import { useEffect, useState, use } from "react";
11
11
  import { notFound } from "next/navigation";
12
12
  import { getPostById } from "ampless";
13
13
  import { jsx, jsxs } from "react/jsx-runtime";
14
- function EditPostPage({ params }) {
14
+ function EditPostPage({ params, previewEndpoint }) {
15
15
  const t = useT();
16
16
  const { postId } = use(params);
17
17
  const [post, setPost] = useState(null);
@@ -28,7 +28,7 @@ function EditPostPage({ params }) {
28
28
  if (missing) notFound();
29
29
  return /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-7xl p-4 md:p-8", children: [
30
30
  /* @__PURE__ */ jsx("h1", { className: "mb-6 text-2xl font-bold md:mb-8 md:text-3xl", children: t("posts.form.editTitle") }),
31
- post && /* @__PURE__ */ jsx(PostForm, { post })
31
+ post && /* @__PURE__ */ jsx(PostForm, { post, previewEndpoint })
32
32
  ] });
33
33
  }
34
34
 
@@ -1,9 +1,16 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
 
3
- declare function EditPostPage({ params }: {
3
+ interface EditPostPageProps {
4
4
  params: Promise<{
5
5
  postId: string;
6
6
  }>;
7
- }): react_jsx_runtime.JSX.Element;
7
+ /**
8
+ * Endpoint that `<PostForm>` / `<PostHistoryPanel>` POST the draft to
9
+ * for preview HTML. Threaded down from `createEditPostPage`. Defaults
10
+ * to `/admin/preview` inside `<PostForm>` when omitted.
11
+ */
12
+ previewEndpoint?: string;
13
+ }
14
+ declare function EditPostPage({ params, previewEndpoint }: EditPostPageProps): react_jsx_runtime.JSX.Element;
8
15
 
9
16
  export { EditPostPage };
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
  import {
3
3
  EditPostPage
4
- } from "../chunk-7ATGSNSZ.js";
5
- import "../chunk-ECN2MBJN.js";
4
+ } from "../chunk-WXUHQNZB.js";
5
+ import "../chunk-6MDPOMKE.js";
6
6
  import "../chunk-DDAGBC4N.js";
7
+ import "../chunk-WUYJ42LP.js";
7
8
  import "../chunk-2ITWLRYF.js";
8
9
  import "../chunk-DUSEHGSV.js";
9
10
  import "../chunk-7TAT4AHW.js";
@@ -99,8 +99,22 @@ declare function Sidebar({ email, isAdmin, }: {
99
99
 
100
100
  interface PostFormProps {
101
101
  post?: Post;
102
+ /**
103
+ * Phase 7: endpoint that the form POSTs the in-flight draft to for
104
+ * server-rendered preview HTML (body + page-level scripts). Defaults
105
+ * to `/admin/preview`, the Route Handler shipped by the template
106
+ * scaffold at `app/(admin)/admin/preview/route.tsx`. The factory
107
+ * (`createEditPostPage` / `createNewPostPage`) exposes a
108
+ * `previewEndpoint?: string` option that threads down to here for
109
+ * non-default admin mount paths (e.g. Next.js `basePath`).
110
+ *
111
+ * The resulting HTML is shown in an `<iframe srcDoc>` (sandbox =
112
+ * `allow-scripts` only) so YouTube iframes / x.com `widgets.js` can
113
+ * hydrate without crossing the admin's same-origin boundary.
114
+ */
115
+ previewEndpoint?: string;
102
116
  }
103
- declare function PostForm({ post }: PostFormProps): react_jsx_runtime.JSX.Element;
117
+ declare function PostForm({ post, previewEndpoint }: PostFormProps): react_jsx_runtime.JSX.Element;
104
118
 
105
119
  interface SiteSettingsFormValues {
106
120
  'site.name'?: string;
@@ -4,27 +4,28 @@ import {
4
4
  Sidebar,
5
5
  SiteSettingsForm,
6
6
  ThemeSettingsForm
7
- } from "../chunk-O7VJHAYM.js";
7
+ } from "../chunk-IJVACP4G.js";
8
+ import {
9
+ MediaUploader
10
+ } from "../chunk-DSCNWIBN.js";
8
11
  import {
9
12
  invalidateSiteSettingsCache
10
13
  } from "../chunk-D72XF3Q3.js";
11
14
  import {
12
15
  MediaPicker,
13
16
  PostForm
14
- } from "../chunk-ECN2MBJN.js";
15
- import "../chunk-C7G5AQ3L.js";
16
- import {
17
- MediaUploader
18
- } from "../chunk-DSCNWIBN.js";
17
+ } from "../chunk-6MDPOMKE.js";
19
18
  import {
20
19
  ImageUploadDialog,
21
20
  sanitizeName,
22
21
  uploadProcessedImage
23
22
  } from "../chunk-DDAGBC4N.js";
23
+ import "../chunk-WUYJ42LP.js";
24
24
  import {
25
25
  publicMediaUrl,
26
26
  setAdminMediaContext
27
27
  } from "../chunk-2ITWLRYF.js";
28
+ import "../chunk-C7G5AQ3L.js";
28
29
  import {
29
30
  I18nProvider,
30
31
  useLocale,
@@ -1,5 +1,13 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
2
 
3
- declare function NewPostPage(): react_jsx_runtime.JSX.Element;
3
+ interface NewPostPageProps {
4
+ /**
5
+ * Endpoint that `<PostForm>` POSTs the draft to for preview HTML.
6
+ * Threaded down from `createNewPostPage`. Defaults to `/admin/preview`
7
+ * inside `<PostForm>` when omitted.
8
+ */
9
+ previewEndpoint?: string;
10
+ }
11
+ declare function NewPostPage({ previewEndpoint }: NewPostPageProps): react_jsx_runtime.JSX.Element;
4
12
 
5
13
  export { NewPostPage };
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
  import {
3
3
  NewPostPage
4
- } from "../chunk-GNTGWI43.js";
5
- import "../chunk-ECN2MBJN.js";
4
+ } from "../chunk-JUNHTZDL.js";
5
+ import "../chunk-6MDPOMKE.js";
6
6
  import "../chunk-DDAGBC4N.js";
7
+ import "../chunk-WUYJ42LP.js";
7
8
  import "../chunk-2ITWLRYF.js";
8
9
  import "../chunk-DUSEHGSV.js";
9
10
  import "../chunk-7TAT4AHW.js";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Structural type for a tiptap extension instance. The admin can't
3
+ * import `@tiptap/core` here (the package is a peerDep of admin and
4
+ * not all consumers have it installed), so we accept anything with a
5
+ * `name` string and pass it through to <TiptapEditor>'s
6
+ * `useEditor({ extensions: [...] })` array spread.
7
+ */
8
+ interface TiptapExtensionLike {
9
+ readonly name?: string;
10
+ readonly [key: string]: unknown;
11
+ }
12
+ /**
13
+ * Register a list of tiptap extensions to be appended to the admin's
14
+ * built-in extension list. Idempotent — subsequent calls are ignored
15
+ * so multiple `EditorBootstrap` renders (e.g. layout re-mounts)
16
+ * don't replace the registry with a stale empty array.
17
+ *
18
+ * Calling with a list whose entries have duplicate `name` fields
19
+ * (within the call) throws — same extension can't register twice.
20
+ */
21
+ declare function installAdminEditorExtensions(list: readonly TiptapExtensionLike[]): void;
22
+ /**
23
+ * Read the currently installed extensions. <TiptapEditor> calls this
24
+ * at render time and spreads the result onto its built-in extension
25
+ * list. The list is intentionally readonly — consumers should not
26
+ * mutate it in place.
27
+ */
28
+ declare function getAdminEditorExtensions(): readonly TiptapExtensionLike[];
29
+
30
+ export { type TiptapExtensionLike, getAdminEditorExtensions, installAdminEditorExtensions };
package/dist/editor.js ADDED
@@ -0,0 +1,9 @@
1
+ 'use client';
2
+ import {
3
+ getAdminEditorExtensions,
4
+ installAdminEditorExtensions
5
+ } from "./chunk-WUYJ42LP.js";
6
+ export {
7
+ getAdminEditorExtensions,
8
+ installAdminEditorExtensions
9
+ };
package/dist/index.d.ts CHANGED
@@ -82,6 +82,18 @@ interface Admin {
82
82
  * the `ampless` opt — throws if it wasn't supplied.
83
83
  */
84
84
  getMediaBySrc(src: string): Promise<ResolvedMedia | null>;
85
+ /**
86
+ * Async accessor for the underlying `Ampless` runtime instance.
87
+ * Resolves the thunk form (the canonical scaffold shape) and caches
88
+ * the result, so multiple callers within one request share the same
89
+ * instance. Throws when `createAdmin` was called without the
90
+ * `ampless` option — same error as the other passthroughs.
91
+ *
92
+ * Template-side Route Handlers (e.g. `preview/route.tsx`) use this
93
+ * to obtain the runtime without depending on the thunk/eager dual
94
+ * form of `admin.ampless`.
95
+ */
96
+ getAmpless(): Promise<Ampless>;
85
97
  readonly outputs: AmplessOutputs;
86
98
  readonly cmsConfig: Config;
87
99
  readonly ampless: Ampless | null;
package/dist/index.js CHANGED
@@ -91,6 +91,7 @@ function createAdmin(opts) {
91
91
  },
92
92
  publicMediaUrl: media.publicMediaUrl,
93
93
  getMediaBySrc: async (src) => (await resolveAmpless()).getMediaBySrc(src),
94
+ getAmpless: () => resolveAmpless(),
94
95
  outputs,
95
96
  cmsConfig,
96
97
  ampless: eagerAmpless