@codepress/blog 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +118 -2
  2. package/dist/blog.css +227 -0
  3. package/dist/chunks/BlogEditor-EDAPNNP5.js +2553 -0
  4. package/dist/chunks/chunk-3GCZFCUK.js +51 -0
  5. package/dist/chunks/chunk-3WRTL63O.js +106 -0
  6. package/dist/chunks/chunk-AEQDRBA3.js +128 -0
  7. package/dist/chunks/chunk-AJVVTSKZ.js +90 -0
  8. package/dist/chunks/chunk-EICE5ONB.js +197 -0
  9. package/dist/chunks/chunk-HWR3R5MU.js +48 -0
  10. package/dist/chunks/chunk-QX6276YA.js +0 -0
  11. package/dist/chunks/chunk-TYX2HJZC.js +68 -0
  12. package/dist/chunks/chunk-UA243KAQ.js +104 -0
  13. package/dist/chunks/chunk-WJVZD4UI.js +114 -0
  14. package/dist/chunks/chunk-XLJBCIXI.js +93 -0
  15. package/dist/cli.js +440 -0
  16. package/dist/content-source.d.ts +31 -0
  17. package/dist/editor/BlogEditor.d.ts +40 -0
  18. package/dist/editor/BlogEditorMount.d.ts +71 -0
  19. package/dist/editor/DocumentActions.d.ts +39 -0
  20. package/dist/editor/EditDesignLink.d.ts +30 -0
  21. package/dist/editor/NewPostForm.d.ts +10 -0
  22. package/dist/editor/SlashMenu.d.ts +5 -0
  23. package/dist/editor/Toolbar.d.ts +39 -0
  24. package/dist/editor/blocks.d.ts +5 -0
  25. package/dist/editor/client.d.ts +88 -0
  26. package/dist/editor/client.js +6 -0
  27. package/dist/editor/component-node.d.ts +14 -0
  28. package/dist/editor/config.d.ts +24 -0
  29. package/dist/editor/controller.d.ts +17 -0
  30. package/dist/editor/document.d.ts +47 -0
  31. package/dist/editor/extensions.d.ts +19 -0
  32. package/dist/editor/header.d.ts +20 -0
  33. package/dist/editor/index.d.ts +12 -0
  34. package/dist/editor/index.js +211 -0
  35. package/dist/editor/paste-guard.d.ts +14 -0
  36. package/dist/editor/primitives.d.ts +17 -0
  37. package/dist/editor/props-form.d.ts +17 -0
  38. package/dist/editor/serialize.d.ts +34 -0
  39. package/dist/editor/slash-command.d.ts +21 -0
  40. package/dist/feed.d.ts +4 -0
  41. package/dist/index.d.ts +8 -0
  42. package/dist/index.js +72 -0
  43. package/dist/next/app.d.ts +34 -0
  44. package/dist/next/config.d.ts +44 -0
  45. package/dist/next/index.d.ts +2 -0
  46. package/dist/next/index.js +94 -0
  47. package/dist/next/pages.d.ts +33 -0
  48. package/dist/next/pages.js +167 -0
  49. package/dist/next/routes.d.ts +14 -0
  50. package/dist/next/routes.js +37 -0
  51. package/dist/next/views.d.ts +18 -0
  52. package/dist/react/components.d.ts +18 -0
  53. package/dist/react/index.d.ts +2 -0
  54. package/dist/react/index.js +15 -0
  55. package/dist/react/render.d.ts +7 -0
  56. package/dist/registry.d.ts +95 -0
  57. package/dist/sanitize.d.ts +33 -0
  58. package/dist/schema.d.ts +24 -0
  59. package/dist/seo.d.ts +63 -0
  60. package/dist/sitemap.d.ts +4 -0
  61. package/dist/xml.d.ts +1 -0
  62. package/package.json +92 -4
@@ -0,0 +1,39 @@
1
+ import { type Editor } from "@tiptap/react";
2
+ import { type ReactElement, type ReactNode } from "react";
3
+ import { type BlogEditorHeader } from "./header.js";
4
+ /** A 32px control: icon buttons, menu triggers and the document actions share it. */
5
+ export declare const toolClass = "inline-flex h-8 min-w-8 shrink-0 items-center justify-center gap-1.5 rounded-md px-2 text-[13px] font-medium text-[#4A4740] transition-colors hover:bg-[#F1EFE9] hover:text-[#111110] focus-visible:ring-2 focus-visible:ring-black/40 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-40 data-[active=true]:bg-[#E8E5DC] data-[active=true]:text-[#111110] data-[state=open]:bg-[#F1EFE9] data-[state=open]:text-[#111110]";
6
+ /** The menus match the site header's dropdown. */
7
+ export declare const menuClass = "rounded-xl border-[#E6E3DA] bg-white p-1.5 text-[#111110] shadow-[0_8px_24px_-8px_rgba(10,10,10,0.16)]";
8
+ export declare const menuItemClass = "flex cursor-pointer items-center gap-2.5 rounded-lg px-2.5 py-2 text-[13px] focus:bg-[#F1EFE9] focus:text-[#111110]";
9
+ export declare function ToolbarSeparator({ className }: {
10
+ className?: string;
11
+ }): import("react/jsx-runtime").JSX.Element;
12
+ /** A hover and focus label, with the keyboard shortcut when there is one. */
13
+ export declare function Tip({ label, shortcut, children, }: {
14
+ label: string;
15
+ shortcut?: string;
16
+ children: ReactElement;
17
+ }): import("react/jsx-runtime").JSX.Element;
18
+ /** `Mod-Shift-8` as the platform writes it: `⌘⇧8` on Apple devices, `Ctrl+Shift+8` elsewhere. */
19
+ export declare function formatShortcut(combo: string, mac: boolean): string;
20
+ export interface ToolbarProps {
21
+ editor: Editor;
22
+ /** Publishing or discarding: nothing may change the post. */
23
+ frozen: boolean;
24
+ /** Focus is in the title or the metadata, which are plain text. */
25
+ formattingOff: boolean;
26
+ linkOpen: boolean;
27
+ onLinkOpenChange: (open: boolean) => void;
28
+ /** The document actions (save status, Ask CodePress, Publish…), at the end of the row. */
29
+ actions?: ReactNode;
30
+ /** The host page's header the toolbar pins below. */
31
+ header?: BlogEditorHeader;
32
+ }
33
+ /**
34
+ * The editor's one toolbar. It spans the page directly under the site header and
35
+ * stays pinned there: its `top` is the header's measured height, so it can never
36
+ * slide under it. Formatting sits over the reading column; the document actions
37
+ * line up with the header's own right edge.
38
+ */
39
+ export declare function Toolbar({ editor, frozen, formattingOff, linkOpen, onLinkOpenChange, actions, header, }: ToolbarProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import type { SlashItem } from "./slash-command.js";
2
+ /** Every block the `/` menu offers. */
3
+ export declare const SLASH_ITEMS: SlashItem[];
4
+ /** The toolbar's Insert menu: the same items as the `/` menu, minus the text blocks. */
5
+ export declare const INSERT_ITEMS: SlashItem[];
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The draft API client, with no editor code in it: a host can read what the
3
+ * caller may do, or publish, without downloading Tiptap.
4
+ */
5
+ /** Supplies the caller's CodePress token; the client never reads storage itself. */
6
+ export interface BlogEditorAuth {
7
+ getToken(): Promise<string | null>;
8
+ /** Sees every response, for a host that refreshes its token from a response header. */
9
+ onResponse?(response: Response): void;
10
+ }
11
+ /** The draft API's shapes; see `apps/backend/apps/content_sites/schemas.py`. */
12
+ export interface DraftFile {
13
+ slug: string;
14
+ /** `draft` (the branch's file), `published` (the default branch's) or `new`. */
15
+ source: "draft" | "published" | "new";
16
+ html: string;
17
+ /** A git site's file blob sha; a database site's revision id. */
18
+ base_sha: string | null;
19
+ branch: string;
20
+ head_sha: string | null;
21
+ pr_number: number | null;
22
+ pr_url: string | null;
23
+ /** Where the site keeps its posts; absent (an older backend) means git. */
24
+ storage?: "git" | "db";
25
+ /** A database site's per-post revision count, from 1; null on a git site. */
26
+ revision?: number | null;
27
+ }
28
+ export interface Violation {
29
+ kind: string;
30
+ reason: string;
31
+ tag?: string | null;
32
+ attribute?: string | null;
33
+ prop?: string | null;
34
+ }
35
+ export interface Capabilities {
36
+ can_edit: boolean;
37
+ can_publish: boolean;
38
+ /** The site's organization; present when the caller can edit. */
39
+ organization_id?: string | null;
40
+ /** The org that owns the site and the repo its posts live in; set for editors only. */
41
+ organization_slug?: string | null;
42
+ repository_full_name?: string | null;
43
+ }
44
+ export type SaveResult = {
45
+ kind: "saved";
46
+ draft: DraftFile;
47
+ } | {
48
+ kind: "conflict";
49
+ current: DraftFile;
50
+ } | {
51
+ kind: "rejected";
52
+ detail: string;
53
+ violations: Violation[];
54
+ } | {
55
+ kind: "error";
56
+ message: string;
57
+ };
58
+ export type ActionResult = {
59
+ kind: "ok";
60
+ } | {
61
+ kind: "rejected";
62
+ detail: string;
63
+ violations: Violation[];
64
+ } | {
65
+ kind: "error";
66
+ message: string;
67
+ status?: number;
68
+ };
69
+ export interface DraftClient {
70
+ getCapabilities(site: string): Promise<Capabilities | null>;
71
+ getDraft(site: string, slug: string): Promise<{
72
+ draft: DraftFile;
73
+ } | {
74
+ error: string;
75
+ }>;
76
+ saveDraft(site: string, slug: string, html: string, baseSha: string | null): Promise<SaveResult>;
77
+ publishDraft(site: string, slug: string): Promise<ActionResult>;
78
+ discardDraft(site: string, slug: string): Promise<ActionResult>;
79
+ }
80
+ /**
81
+ * A client for the content-site draft API at `apiBase` (the backend's `/v1`
82
+ * root). Every request carries `auth.getToken()` as its bearer token, and only
83
+ * that one.
84
+ */
85
+ export declare function createDraftClient({ apiBase, auth, }: {
86
+ apiBase: string;
87
+ auth: BlogEditorAuth;
88
+ }): DraftClient;
@@ -0,0 +1,6 @@
1
+ import {
2
+ createDraftClient
3
+ } from "../chunks/chunk-3WRTL63O.js";
4
+ export {
5
+ createDraftClient
6
+ };
@@ -0,0 +1,14 @@
1
+ import { Node } from "@tiptap/react";
2
+ import { type ComponentTag } from "../index.js";
3
+ /** Props a component starts with when inserted from the slash menu. */
4
+ export declare const DEFAULT_PROPS: Record<ComponentTag, Record<string, unknown>>;
5
+ /** `cp-callout` -> `cpCallout`. */
6
+ export declare function componentNodeName(tag: string): string;
7
+ /**
8
+ * One editor node per registry component: parsed from and rendered to the
9
+ * same `<cp-*>` tag the post file uses, with the props JSON kept in one
10
+ * attribute and edited through a form generated from the zod schema.
11
+ */
12
+ export declare function componentNode(tag: ComponentTag): Node<{
13
+ tag: "cp-callout" | "cp-figure";
14
+ }, any>;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Editor timings and layout defaults. Every timing has a production default
3
+ * and a prop override (`timings`), so a host or QA can compress the clock.
4
+ */
5
+ export interface BlogEditorTimings {
6
+ /** Quiet time after the last keystroke before the draft is saved. */
7
+ autosaveMs: number;
8
+ /** How often an open editor checks whether the draft changed elsewhere. */
9
+ draftPollMs: number;
10
+ /** How long the "published" notice shows before `onPublished` runs. */
11
+ publishReloadMs: number;
12
+ }
13
+ export declare const DEFAULT_TIMINGS: BlogEditorTimings;
14
+ /** The defaults, with each override that is a positive number. */
15
+ export declare function resolveTimings(timings: Partial<BlogEditorTimings> | undefined): BlogEditorTimings;
16
+ /** The reading column the editor lays its title and body out in. */
17
+ export declare const DEFAULT_COLUMN = "mx-auto w-full max-w-[720px] px-5 sm:px-8";
18
+ /** Where the CodePress app lives: Ask CodePress, Edit design and History open there. */
19
+ export declare const DEFAULT_APP_URL = "https://codepress.dev";
20
+ /**
21
+ * CSS variable the toolbar sets on the document root while it is mounted: the
22
+ * viewport y of its bottom edge. Floating panels (Ask CodePress) keep below it.
23
+ */
24
+ export declare const BLOG_TOOLBAR_BOTTOM_VAR = "--blog-toolbar-bottom";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * What a host holds to leave the editor without losing work. The editor guards a page unload
3
+ * itself; a client-side route change unmounts it without warning, so a host that navigates in
4
+ * place routes its navigation through `leave`.
5
+ */
6
+ export interface BlogEditorController {
7
+ /**
8
+ * Leaves the way Done does: settles and saves anything unsaved, then calls `go`. When the
9
+ * document cannot be saved (offline, rejected, conflict) the editor stays and asks: "Leave
10
+ * without saving" calls `go`, "Keep editing" does not. Resolves true when `go` ran straight away.
11
+ */
12
+ leave(go: () => void): Promise<boolean>;
13
+ }
14
+ /** Filled while an editor with a loaded draft is open, and null otherwise. */
15
+ export interface BlogEditorControllerRef {
16
+ current: BlogEditorController | null;
17
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The post file as text, split so an edit only rewrites the blocks that
3
+ * changed. Everything the author wrote around an unchanged block (its own
4
+ * line wrapping, the blank lines between blocks) is kept byte for byte, so a
5
+ * draft PR's diff shows the edited blocks and nothing else.
6
+ */
7
+ export interface PostSource {
8
+ /** Text before the metadata block (normally empty). */
9
+ prefix: string;
10
+ /** The metadata block exactly as written, or `""` when there is none. */
11
+ metaText: string;
12
+ meta: Record<string, unknown>;
13
+ blocks: SourceBlock[];
14
+ /** `gaps[i]` is the text before `blocks[i]`; the last entry trails the file. */
15
+ gaps: string[];
16
+ /** Reasons this file cannot be edited in place; empty when it can. */
17
+ problems: string[];
18
+ }
19
+ export interface SourceBlock {
20
+ id: string | null;
21
+ tag: string;
22
+ text: string;
23
+ }
24
+ /** Splits a post file into its metadata block and top-level body blocks. */
25
+ export declare function parsePostSource(source: string): PostSource;
26
+ /** The metadata block in the layout the seed post uses. */
27
+ export declare function serializeMeta(meta: Record<string, unknown>): string;
28
+ /**
29
+ * A body block after editing. `sourceIndex` is set when it is unchanged;
30
+ * `replaces` is the source block an edited block came from (unset for a new one).
31
+ */
32
+ export interface EditedBlock {
33
+ html: string;
34
+ sourceIndex: number | null;
35
+ replaces?: number | null;
36
+ }
37
+ /**
38
+ * Writes the post file. Unchanged blocks and the metadata block (when its
39
+ * values did not change) keep their original text and spacing.
40
+ */
41
+ export declare function assemblePostFile(source: PostSource | null, meta: Record<string, unknown>, blocks: EditedBlock[]): string;
42
+ /**
43
+ * A comparison form of a block: the same for two spellings of the same
44
+ * content (whitespace, attribute order, quoting, entity form), and ignoring
45
+ * `data-cp-id`, which the editor assigns to blocks that have none.
46
+ */
47
+ export declare function canonicalBlock(html: string): string;
@@ -0,0 +1,19 @@
1
+ import { type Extensions } from "@tiptap/react";
2
+ import { type SlashItem, type SlashState } from "./slash-command.js";
3
+ export declare function generateBlockId(): string;
4
+ /** UniqueID names the node attribute `cp-id` and writes it to HTML as `data-cp-id`. */
5
+ export declare const BLOCK_ID_NODE_ATTRIBUTE: string;
6
+ export interface EditorHooks {
7
+ isFrozen?: () => boolean;
8
+ onSlash?: (state: SlashState | null) => void;
9
+ onPasteRejected?: (reasons: string[]) => void;
10
+ slashItems?: SlashItem[];
11
+ onLinkShortcut?: () => void;
12
+ }
13
+ /**
14
+ * Everything a post may contain and nothing else: the elements the registry
15
+ * allows, one node per registered component, and a stable `data-cp-id` on each
16
+ * block. Marks and nodes outside the schema (strike, underline, tables) are
17
+ * turned off so the editor cannot produce content the backend would reject.
18
+ */
19
+ export declare function editorExtensions(hooks?: EditorHooks): Extensions;
@@ -0,0 +1,20 @@
1
+ import { type RefObject } from "react";
2
+ /** The host page's header, which the toolbar pins directly below. */
3
+ export interface BlogEditorHeader {
4
+ /** The header element; defaults to `[data-blog-header]`, then the page's first `header`. */
5
+ selector?: string;
6
+ /** A fixed distance from the top of the viewport, used instead of measuring a header. */
7
+ offset?: number;
8
+ }
9
+ export declare const DEFAULT_HEADER_SELECTOR = "[data-blog-header]";
10
+ /**
11
+ * The host's header: the selector's match, else the first `header` element
12
+ * outside the editor itself (its title block is a `header` too), else none.
13
+ */
14
+ export declare function findHostHeader(selector: string | undefined, editorRoot: Element | null): HTMLElement | null;
15
+ /**
16
+ * How far from the top the toolbar pins: the host header's height, measured
17
+ * and followed as it changes (a mobile menu opening), or `offset` when given,
18
+ * or 0 when the page has no header.
19
+ */
20
+ export declare function useHeaderOffset(header: BlogEditorHeader | undefined, own: RefObject<HTMLElement | null>): number;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * `@codepress/blog/editor`: the in-place editor. Client-only; the Tiptap
3
+ * editor itself is a lazy chunk that loads when someone who can edit clicks
4
+ * Edit, so importing this entry downloads no ProseMirror code.
5
+ */
6
+ export { BlogEditorMount, appPageUrl, type AskPanelProps, type BlogEditorMountProps, type DesignLinkContext, } from "./BlogEditorMount.js";
7
+ export type { BlogEditorController, BlogEditorControllerRef, } from "./controller.js";
8
+ export { NewPostForm, isValidNewSlug, type NewPostFormProps, } from "./NewPostForm.js";
9
+ export { BLOG_TOOLBAR_BOTTOM_VAR, DEFAULT_TIMINGS, type BlogEditorTimings, } from "./config.js";
10
+ export { DEFAULT_HEADER_SELECTOR, type BlogEditorHeader } from "./header.js";
11
+ export { designUrl, type DesignTarget } from "./EditDesignLink.js";
12
+ export * from "./client.js";
@@ -0,0 +1,211 @@
1
+ import {
2
+ createDraftClient
3
+ } from "../chunks/chunk-3WRTL63O.js";
4
+ import {
5
+ BLOG_TOOLBAR_BOTTOM_VAR,
6
+ DEFAULT_APP_URL,
7
+ DEFAULT_COLUMN,
8
+ DEFAULT_HEADER_SELECTOR,
9
+ DEFAULT_TIMINGS,
10
+ designTargetOf,
11
+ designUrl
12
+ } from "../chunks/chunk-UA243KAQ.js";
13
+ import {
14
+ SLUG_PATTERN
15
+ } from "../chunks/chunk-XLJBCIXI.js";
16
+
17
+ // src/editor/BlogEditorMount.tsx
18
+ import {
19
+ Suspense,
20
+ lazy,
21
+ useEffect,
22
+ useMemo,
23
+ useState
24
+ } from "react";
25
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
26
+ var BlogEditor = lazy(() => import("../chunks/BlogEditor-EDAPNNP5.js"));
27
+ function appPageUrl(appUrl, organizationSlug, site, slug, tab) {
28
+ if (!organizationSlug) return `${appUrl}/dashboard`;
29
+ const query = new URLSearchParams({ site, post: slug });
30
+ if (tab) query.set("tab", tab);
31
+ return `${appUrl}/dashboard/${encodeURIComponent(organizationSlug)}/apps?${query.toString()}`;
32
+ }
33
+ function BlogEditorMount({
34
+ site,
35
+ slug,
36
+ apiBase,
37
+ auth,
38
+ isNew = false,
39
+ header,
40
+ column = DEFAULT_COLUMN,
41
+ appUrl = DEFAULT_APP_URL,
42
+ timings,
43
+ onEditingChange,
44
+ onPublished,
45
+ onLeave,
46
+ controller,
47
+ slots,
48
+ actions
49
+ }) {
50
+ const client = useMemo(
51
+ () => createDraftClient({ apiBase, auth }),
52
+ [apiBase, auth]
53
+ );
54
+ const [capabilities, setCapabilities] = useState(null);
55
+ const [checked, setChecked] = useState(false);
56
+ const [editing, setEditing] = useState(isNew);
57
+ const [ask, setAsk] = useState("closed");
58
+ useEffect(() => {
59
+ let cancelled = false;
60
+ void client.getCapabilities(site).then((result) => {
61
+ if (cancelled) return;
62
+ setCapabilities(result);
63
+ setChecked(true);
64
+ });
65
+ return () => {
66
+ cancelled = true;
67
+ };
68
+ }, [client, site]);
69
+ const changeEditing = (next) => {
70
+ setEditing(next);
71
+ onEditingChange?.(next);
72
+ };
73
+ if (!checked) return null;
74
+ if (!capabilities?.can_edit) {
75
+ return isNew ? /* @__PURE__ */ jsx("p", { "data-testid": "blog-no-access", children: "You do not have access to write blog posts." }) : null;
76
+ }
77
+ const organizationId = capabilities.organization_id;
78
+ const organizationSlug = capabilities.organization_slug;
79
+ const askPanel = ask !== "closed" && organizationId && slots?.askPanel ? slots.askPanel({
80
+ site,
81
+ slug,
82
+ organizationId,
83
+ open: ask === "open",
84
+ onClose: () => setAsk("hidden")
85
+ }) : null;
86
+ const target = designTargetOf(capabilities);
87
+ const designHref = target ? slots?.designLink?.({ slug, ...target }) ?? designUrl(target, slug, appUrl) : null;
88
+ if (!editing) {
89
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
90
+ /* @__PURE__ */ jsx(
91
+ "button",
92
+ {
93
+ type: "button",
94
+ className: "fixed right-6 bottom-6 z-40 rounded-full bg-[#111110] px-5 py-2.5 text-sm font-medium text-white shadow-lg",
95
+ "data-testid": "blog-edit-button",
96
+ onClick: () => changeEditing(true),
97
+ children: "Edit"
98
+ }
99
+ ),
100
+ askPanel
101
+ ] });
102
+ }
103
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
104
+ /* @__PURE__ */ jsx(
105
+ Suspense,
106
+ {
107
+ fallback: /* @__PURE__ */ jsx(
108
+ "p",
109
+ {
110
+ className: `${column} pt-10 pb-20 sm:pt-14`,
111
+ "data-testid": "blog-editor-loading",
112
+ children: "Loading the editor\u2026"
113
+ }
114
+ ),
115
+ children: /* @__PURE__ */ jsx(
116
+ BlogEditor,
117
+ {
118
+ site,
119
+ slug,
120
+ client,
121
+ timings,
122
+ column,
123
+ header,
124
+ canPublish: capabilities.can_publish,
125
+ designHref,
126
+ historyHref: appPageUrl(
127
+ appUrl,
128
+ organizationSlug,
129
+ site,
130
+ slug,
131
+ "history"
132
+ ),
133
+ onAsk: organizationId && slots?.askPanel ? () => setAsk((current) => current === "open" ? "hidden" : "open") : void 0,
134
+ askHref: actions?.askCodePress?.href ?? appPageUrl(appUrl, organizationSlug, site, slug),
135
+ askIconSrc: `${appUrl}/logo-mark-square.png`,
136
+ askOpen: ask === "open",
137
+ controller,
138
+ onExit: () => isNew ? onLeave?.() : changeEditing(false),
139
+ onPublished: () => onPublished ? onPublished(slug) : window.location.reload(),
140
+ onDiscarded: () => isNew ? onLeave?.() : window.location.reload()
141
+ }
142
+ )
143
+ }
144
+ ),
145
+ askPanel
146
+ ] });
147
+ }
148
+
149
+ // src/editor/NewPostForm.tsx
150
+ import { useState as useState2 } from "react";
151
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
152
+ var RESERVED_SLUGS = /* @__PURE__ */ new Set(["new"]);
153
+ function isValidNewSlug(slug) {
154
+ return SLUG_PATTERN.test(slug) && !RESERVED_SLUGS.has(slug);
155
+ }
156
+ function NewPostForm({ onStart, prefix = "/blog/" }) {
157
+ const [draftSlug, setDraftSlug] = useState2("");
158
+ const valid = isValidNewSlug(draftSlug);
159
+ return /* @__PURE__ */ jsxs2(
160
+ "form",
161
+ {
162
+ "data-testid": "blog-new-form",
163
+ onSubmit: (event) => {
164
+ event.preventDefault();
165
+ if (valid) onStart(draftSlug);
166
+ },
167
+ children: [
168
+ /* @__PURE__ */ jsx2("h1", { className: "text-[34px] font-medium tracking-[-0.03em] text-[#111110]", children: "New post" }),
169
+ /* @__PURE__ */ jsxs2("label", { className: "mt-6 block text-sm text-[#6B6760]", children: [
170
+ "Address",
171
+ /* @__PURE__ */ jsxs2("div", { className: "mt-1 flex items-center gap-1 text-[#111110]", children: [
172
+ /* @__PURE__ */ jsx2("span", { children: prefix }),
173
+ /* @__PURE__ */ jsx2(
174
+ "input",
175
+ {
176
+ className: "w-full rounded-md border border-[#D9D5CC] px-2 py-1",
177
+ placeholder: "my-first-post",
178
+ "aria-label": "Post address",
179
+ "data-testid": "blog-new-slug",
180
+ value: draftSlug,
181
+ onChange: (event) => setDraftSlug(event.target.value)
182
+ }
183
+ )
184
+ ] })
185
+ ] }),
186
+ draftSlug && !valid ? /* @__PURE__ */ jsx2("p", { className: "mt-2 text-sm text-red-600", children: "Use lowercase letters, numbers and single hyphens." }) : null,
187
+ /* @__PURE__ */ jsx2(
188
+ "button",
189
+ {
190
+ type: "submit",
191
+ className: "mt-4 rounded-md bg-[#111110] px-4 py-2 text-sm text-white disabled:opacity-50",
192
+ "data-testid": "blog-new-start",
193
+ disabled: !valid,
194
+ children: "Start writing"
195
+ }
196
+ )
197
+ ]
198
+ }
199
+ );
200
+ }
201
+ export {
202
+ BLOG_TOOLBAR_BOTTOM_VAR,
203
+ BlogEditorMount,
204
+ DEFAULT_HEADER_SELECTOR,
205
+ DEFAULT_TIMINGS,
206
+ NewPostForm,
207
+ appPageUrl,
208
+ createDraftClient,
209
+ designUrl,
210
+ isValidNewSlug
211
+ };
@@ -0,0 +1,14 @@
1
+ import { Extension } from "@tiptap/react";
2
+ /**
3
+ * What in a pasted fragment the editor refuses rather than flattens. Ordinary
4
+ * web markup (`div`, `span`, styles) is normalized by the schema like any
5
+ * paste; scripts, event handlers, unsafe URLs, invalid component props and
6
+ * unregistered `cp-*` tags are rejected out loud so nobody wonders where they
7
+ * went.
8
+ */
9
+ export declare function pasteRejections(html: string): string[];
10
+ interface PasteGuardOptions {
11
+ onReject: (reasons: string[]) => void;
12
+ }
13
+ export declare const PasteGuard: Extension<PasteGuardOptions, any>;
14
+ export {};
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The menu and tooltip primitives the toolbar uses: Radix, with the classes
3
+ * and markup of the CodePress UI kit's own wrappers, so the package needs no
4
+ * private dependency and the toolbar renders exactly as it does in the app.
5
+ */
6
+ import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
7
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
8
+ import type { ComponentProps } from "react";
9
+ export declare function DropdownMenu(props: ComponentProps<typeof DropdownMenuPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
10
+ export declare function DropdownMenuTrigger(props: ComponentProps<typeof DropdownMenuPrimitive.Trigger>): import("react/jsx-runtime").JSX.Element;
11
+ export declare function DropdownMenuContent({ className, sideOffset, ...props }: ComponentProps<typeof DropdownMenuPrimitive.Content>): import("react/jsx-runtime").JSX.Element;
12
+ export declare function DropdownMenuItem({ className, ...props }: ComponentProps<typeof DropdownMenuPrimitive.Item>): import("react/jsx-runtime").JSX.Element;
13
+ export declare function DropdownMenuSeparator({ className, ...props }: ComponentProps<typeof DropdownMenuPrimitive.Separator>): import("react/jsx-runtime").JSX.Element;
14
+ /** Each tooltip carries its own provider, so a host needs no provider around the editor. */
15
+ export declare function Tooltip(props: ComponentProps<typeof TooltipPrimitive.Root>): import("react/jsx-runtime").JSX.Element;
16
+ export declare function TooltipTrigger(props: ComponentProps<typeof TooltipPrimitive.Trigger>): import("react/jsx-runtime").JSX.Element;
17
+ export declare function TooltipContent({ className, sideOffset, children, ...props }: ComponentProps<typeof TooltipPrimitive.Content>): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,17 @@
1
+ import { z } from "zod";
2
+ interface Field {
3
+ name: string;
4
+ kind: "enum" | "text" | "number" | "boolean";
5
+ options: string[];
6
+ required: boolean;
7
+ }
8
+ /** The editable fields of a component's props, read from its zod schema. */
9
+ export declare function fieldsFor(schema: z.ZodType): Field[];
10
+ interface PropsFormProps {
11
+ schema: z.ZodType;
12
+ value: Record<string, unknown>;
13
+ onChange: (next: Record<string, unknown>) => void;
14
+ }
15
+ /** A form generated from a props schema; it edits the props object in place. */
16
+ export declare function PropsForm({ schema, value, onChange }: PropsFormProps): import("react/jsx-runtime").JSX.Element;
17
+ export {};
@@ -0,0 +1,34 @@
1
+ import { type Node as ProseMirrorNode, type Schema } from "@tiptap/pm/model";
2
+ import { type EditedBlock, type PostSource } from "./document.js";
3
+ /** The ProseMirror schema the editor uses; built once. */
4
+ export declare function postSchema(): Schema;
5
+ /** The HTML of one top-level editor node, in the file's own conventions. */
6
+ export declare function nodeToHtml(schema: Schema, node: ProseMirrorNode): string;
7
+ /**
8
+ * Why a source file cannot be edited without loss: structural problems from
9
+ * parsing, and each block that would come out different after the editor
10
+ * loads and writes it back (unknown markup, unsupported elements).
11
+ */
12
+ export declare function editabilityProblems(source: PostSource): string[];
13
+ /**
14
+ * The source body as the editor's initial content, with an id on every block.
15
+ * Blocks the author left without a `data-cp-id` get one here, so each loaded
16
+ * block maps to its source block by id from the first keystroke; the injected
17
+ * id only reaches the file if that block is edited.
18
+ */
19
+ export declare function sourceBodyHtml(source: PostSource): {
20
+ html: string;
21
+ indexById: Map<string, number>;
22
+ };
23
+ /** Maps each block's `data-cp-id` in the loaded document to its source block. */
24
+ export declare function blockIndexById(doc: ProseMirrorNode): Map<string, number>;
25
+ /** The current document as blocks, reusing source text for the unchanged ones. */
26
+ export declare function editedBlocks(doc: ProseMirrorNode, source: PostSource | null, indexById: Map<string, number>): EditedBlock[];
27
+ export interface LocalProblem {
28
+ message: string;
29
+ }
30
+ /**
31
+ * What the backend would reject, found before saving: the same registry
32
+ * checks the sanitizer applies, plus the metadata shape.
33
+ */
34
+ export declare function localProblems(meta: Record<string, unknown>, blocks: EditedBlock[]): LocalProblem[];
@@ -0,0 +1,21 @@
1
+ import { Extension, type Editor, type Range } from "@tiptap/react";
2
+ export interface SlashItem {
3
+ id: string;
4
+ title: string;
5
+ description: string;
6
+ run: (editor: Editor, range: Range) => void;
7
+ }
8
+ /** What the menu component needs to draw itself and pick an item. */
9
+ export interface SlashState {
10
+ items: SlashItem[];
11
+ index: number;
12
+ rect: DOMRect | null;
13
+ choose: (index: number) => void;
14
+ }
15
+ interface SlashOptions {
16
+ items: SlashItem[];
17
+ onState: (state: SlashState | null) => void;
18
+ }
19
+ /** Typing `/` at the start of a block opens a menu of blocks to insert. */
20
+ export declare const SlashCommand: Extension<SlashOptions, any>;
21
+ export {};
package/dist/feed.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import type { PostSummary } from "./content-source.js";
2
+ import { type BlogSite } from "./seo.js";
3
+ /** An RSS 2.0 feed of `posts`, newest first. */
4
+ export declare function feedXml(site: BlogSite, posts: PostSummary[]): string;
@@ -0,0 +1,8 @@
1
+ export * from "./registry.js";
2
+ export * from "./sanitize.js";
3
+ export * from "./schema.js";
4
+ export * from "./seo.js";
5
+ export * from "./feed.js";
6
+ export * from "./sitemap.js";
7
+ export * from "./content-source.js";
8
+ export { escapeXml } from "./xml.js";