@ampless/admin 1.0.0-alpha.73 → 1.0.0-alpha.75

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,18 +1,18 @@
1
1
  'use client';
2
2
  import {
3
3
  PostForm
4
- } from "./chunk-ECN2MBJN.js";
4
+ } from "./chunk-LZHJTCRU.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({ renderPreviewAction }) {
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, { renderPreviewAction })
16
16
  ] });
17
17
  }
18
18
 
@@ -1,7 +1,7 @@
1
1
  'use client';
2
2
  import {
3
3
  PostForm
4
- } from "./chunk-ECN2MBJN.js";
4
+ } from "./chunk-LZHJTCRU.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, renderPreviewAction }) {
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, renderPreviewAction })
32
32
  ] });
33
33
  }
34
34
 
@@ -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
+ renderPreviewAction
667
+ }) {
660
668
  const t = useT();
661
669
  const config = getAdminCmsConfig();
662
670
  const timezone = config?.timezone ?? "UTC";
@@ -668,6 +676,19 @@ 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
+ useEffect3(() => {
681
+ if (!selected || !renderPreviewAction || selected.format === "static") return;
682
+ const ctrl = new AbortController();
683
+ renderPreviewAction(revisionAsPost(selected)).then((html) => {
684
+ if (!ctrl.signal.aborted) setPreviewHtml(html);
685
+ }).catch((err) => {
686
+ if (!ctrl.signal.aborted) {
687
+ console.error("[ampless admin] revision preview action failed:", err);
688
+ }
689
+ });
690
+ return () => ctrl.abort();
691
+ }, [selected, renderPreviewAction]);
671
692
  const loadPage = useCallback(
672
693
  async (token) => {
673
694
  setLoading(true);
@@ -784,13 +805,19 @@ function PostHistoryPanel({ postId, onRestore }) {
784
805
  /* @__PURE__ */ jsx6("span", { className: "font-mono", children: selected.slug })
785
806
  ] })
786
807
  ] }),
787
- 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",
808
+ 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") }) : renderPreviewAction ? /* @__PURE__ */ jsx6(
809
+ "iframe",
789
810
  {
790
- className: "prose prose-neutral dark:prose-invert max-w-none text-sm",
791
- dangerouslySetInnerHTML: { __html: renderBody(revisionAsPost(selected)) }
811
+ title: "revision-preview",
812
+ srcDoc: previewHtml,
813
+ sandbox: "allow-scripts",
814
+ className: "prose prose-neutral dark:prose-invert max-w-none min-h-[300px] w-full rounded-md border text-sm"
792
815
  }
793
- )
816
+ ) : /* @__PURE__ */ jsxs6("p", { className: "text-sm text-muted-foreground", children: [
817
+ "Preview unavailable: pass ",
818
+ /* @__PURE__ */ jsx6("code", { children: "renderPreviewAction" }),
819
+ " prop from the template's server action."
820
+ ] })
794
821
  ] })
795
822
  ] })
796
823
  ] });
@@ -1263,7 +1290,7 @@ function formatDraftTime(epochMs, timezone, locale) {
1263
1290
  return d.toLocaleString();
1264
1291
  }
1265
1292
  }
1266
- function PostForm({ post }) {
1293
+ function PostForm({ post, renderPreviewAction }) {
1267
1294
  const router = useRouter();
1268
1295
  const t = useT();
1269
1296
  const isEdit = !!post;
@@ -1280,6 +1307,7 @@ function PostForm({ post }) {
1280
1307
  const [saving, setSaving] = useState5(false);
1281
1308
  const [error, setError] = useState5(null);
1282
1309
  const [view, setView] = useState5("edit");
1310
+ const [previewHtml, setPreviewHtml] = useState5("");
1283
1311
  const [editorEpoch, setEditorEpoch] = useState5(0);
1284
1312
  const dirtyRef = useRef3(false);
1285
1313
  function markDirty() {
@@ -1558,6 +1586,35 @@ function PostForm({ post }) {
1558
1586
  publishedAt: resolvedPublishedAt,
1559
1587
  tags: parseTags(tagsInput)
1560
1588
  };
1589
+ useEffect4(() => {
1590
+ if (view !== "preview" || !renderPreviewAction) return;
1591
+ if (format === "static") return;
1592
+ const ctrl = new AbortController();
1593
+ const tid = window.setTimeout(() => {
1594
+ renderPreviewAction(previewPost).then((html) => {
1595
+ if (!ctrl.signal.aborted) setPreviewHtml(html);
1596
+ }).catch((err) => {
1597
+ if (!ctrl.signal.aborted) {
1598
+ console.error("[ampless admin] renderPreviewAction failed:", err);
1599
+ }
1600
+ });
1601
+ }, 250);
1602
+ return () => {
1603
+ ctrl.abort();
1604
+ window.clearTimeout(tid);
1605
+ };
1606
+ }, [
1607
+ view,
1608
+ renderPreviewAction,
1609
+ format,
1610
+ title,
1611
+ slug,
1612
+ excerpt,
1613
+ body,
1614
+ status,
1615
+ resolvedPublishedAt,
1616
+ tagsInput
1617
+ ]);
1561
1618
  return /* @__PURE__ */ jsxs8("form", { onSubmit: save, className: "space-y-6", children: [
1562
1619
  /* @__PURE__ */ jsxs8("div", { className: "flex gap-1 border-b", children: [
1563
1620
  /* @__PURE__ */ jsx8(
@@ -1591,13 +1648,29 @@ function PostForm({ post }) {
1591
1648
  ] }),
1592
1649
  excerpt && /* @__PURE__ */ jsx8("p", { className: "mt-3 text-base text-muted-foreground", children: excerpt })
1593
1650
  ] }),
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
- }
1600
- ),
1651
+ format === "static" ? /* @__PURE__ */ jsx8("p", { className: "text-sm text-muted-foreground", children: t("posts.form.static.previewHint") }) : renderPreviewAction ? (
1652
+ // Phase 7: preview is rendered via the template's
1653
+ // `'use server'` action so `ampless.renderBody` (now async
1654
+ // ReactNode) + `ampless.publicPostScriptsForPage([draft])`
1655
+ // can run server-side. The result is injected into an
1656
+ // iframe with sandbox=`allow-scripts` only (no
1657
+ // `allow-same-origin`) so 3rd-party widget scripts
1658
+ // (YouTube iframes, x.com widgets.js) can hydrate without
1659
+ // crossing the admin's same-origin boundary.
1660
+ /* @__PURE__ */ jsx8(
1661
+ "iframe",
1662
+ {
1663
+ title: "post-preview",
1664
+ srcDoc: previewHtml,
1665
+ sandbox: "allow-scripts",
1666
+ className: "prose prose-neutral dark:prose-invert max-w-none min-h-[400px] w-full rounded-md border"
1667
+ }
1668
+ )
1669
+ ) : /* @__PURE__ */ jsxs8("p", { className: "text-sm text-muted-foreground", children: [
1670
+ "Preview unavailable: pass ",
1671
+ /* @__PURE__ */ jsx8("code", { children: "renderPreviewAction" }),
1672
+ " prop from the template's server action."
1673
+ ] }),
1601
1674
  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
1675
  "span",
1603
1676
  {
@@ -1821,7 +1894,14 @@ function PostForm({ post }) {
1821
1894
  /* @__PURE__ */ jsx8(Button7, { type: "submit", disabled: saving, children: saving ? t("common.saving") : isEdit ? t("posts.form.saveChanges") : t("posts.form.createPost") }),
1822
1895
  isEdit && /* @__PURE__ */ jsx8(Button7, { type: "button", variant: "destructive", onClick: handleDelete, disabled: saving, children: t("posts.form.delete") })
1823
1896
  ] }),
1824
- isEdit && post && /* @__PURE__ */ jsx8(PostHistoryPanel, { postId: post.postId, onRestore: restoreRevision })
1897
+ isEdit && post && /* @__PURE__ */ jsx8(
1898
+ PostHistoryPanel,
1899
+ {
1900
+ postId: post.postId,
1901
+ onRestore: restoreRevision,
1902
+ renderPreviewAction
1903
+ }
1904
+ )
1825
1905
  ] })
1826
1906
  ] });
1827
1907
  }
@@ -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-LZHJTCRU.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
@@ -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,9 +1,16 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
+ import { Post } from 'ampless';
2
3
 
3
- declare function EditPostPage({ params }: {
4
+ interface EditPostPageProps {
4
5
  params: Promise<{
5
6
  postId: string;
6
7
  }>;
7
- }): react_jsx_runtime.JSX.Element;
8
+ /**
9
+ * Phase 7: server action threaded from `createEditPostPage` factory
10
+ * down into `<PostForm>` for iframe-based preview rendering.
11
+ */
12
+ renderPreviewAction?: (draft: Post) => Promise<string>;
13
+ }
14
+ declare function EditPostPage({ params, renderPreviewAction }: 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-AKQAEPUJ.js";
5
+ import "../chunk-LZHJTCRU.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,21 @@ declare function Sidebar({ email, isAdmin, }: {
99
99
 
100
100
  interface PostFormProps {
101
101
  post?: Post;
102
+ /**
103
+ * Phase 7: server action that renders the draft post into a complete
104
+ * HTML string (body + page-level scripts). Templates wire this from
105
+ * `_actions/render-preview.ts` and thread it down via the
106
+ * `createEditPostPage` / `createNewPostPage` factory options. The
107
+ * resulting HTML is shown in an `<iframe srcDoc>` (sandbox =
108
+ * `allow-scripts` only) so YouTube iframes / x.com `widgets.js` can
109
+ * hydrate without crossing the admin's same-origin boundary.
110
+ *
111
+ * When omitted, the preview pane shows a fallback message explaining
112
+ * how to wire the prop. Alpha 7 onwards templates always pass it.
113
+ */
114
+ renderPreviewAction?: (draft: Post) => Promise<string>;
102
115
  }
103
- declare function PostForm({ post }: PostFormProps): react_jsx_runtime.JSX.Element;
116
+ declare function PostForm({ post, renderPreviewAction }: PostFormProps): react_jsx_runtime.JSX.Element;
104
117
 
105
118
  interface SiteSettingsFormValues {
106
119
  '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-RL6EOFCU.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-LZHJTCRU.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
+ import { Post } from 'ampless';
2
3
 
3
- declare function NewPostPage(): react_jsx_runtime.JSX.Element;
4
+ interface NewPostPageProps {
5
+ /**
6
+ * Phase 7: server action threaded from `createNewPostPage` factory
7
+ * down into `<PostForm>` for iframe-based preview rendering.
8
+ */
9
+ renderPreviewAction?: (draft: Post) => Promise<string>;
10
+ }
11
+ declare function NewPostPage({ renderPreviewAction }: 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-AKH24LFB.js";
5
+ import "../chunk-LZHJTCRU.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 server actions (e.g. `_actions/render-preview.ts`)
93
+ * use this to obtain the runtime without depending on the
94
+ * thunk/eager dual 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