@codepress/blog 0.0.0-stage → 0.1.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 (61) hide show
  1. package/README.md +118 -2
  2. package/dist/blog.css +227 -0
  3. package/dist/chunks/BlogEditor-VF3UAJB6.js +2527 -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 +37 -0
  18. package/dist/editor/BlogEditorMount.d.ts +64 -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/document.d.ts +47 -0
  30. package/dist/editor/extensions.d.ts +19 -0
  31. package/dist/editor/header.d.ts +20 -0
  32. package/dist/editor/index.d.ts +11 -0
  33. package/dist/editor/index.js +209 -0
  34. package/dist/editor/paste-guard.d.ts +14 -0
  35. package/dist/editor/primitives.d.ts +17 -0
  36. package/dist/editor/props-form.d.ts +17 -0
  37. package/dist/editor/serialize.d.ts +34 -0
  38. package/dist/editor/slash-command.d.ts +21 -0
  39. package/dist/feed.d.ts +4 -0
  40. package/dist/index.d.ts +8 -0
  41. package/dist/index.js +72 -0
  42. package/dist/next/app.d.ts +34 -0
  43. package/dist/next/config.d.ts +44 -0
  44. package/dist/next/index.d.ts +2 -0
  45. package/dist/next/index.js +94 -0
  46. package/dist/next/pages.d.ts +33 -0
  47. package/dist/next/pages.js +167 -0
  48. package/dist/next/routes.d.ts +14 -0
  49. package/dist/next/routes.js +37 -0
  50. package/dist/next/views.d.ts +18 -0
  51. package/dist/react/components.d.ts +18 -0
  52. package/dist/react/index.d.ts +2 -0
  53. package/dist/react/index.js +15 -0
  54. package/dist/react/render.d.ts +7 -0
  55. package/dist/registry.d.ts +95 -0
  56. package/dist/sanitize.d.ts +33 -0
  57. package/dist/schema.d.ts +24 -0
  58. package/dist/seo.d.ts +63 -0
  59. package/dist/sitemap.d.ts +4 -0
  60. package/dist/xml.d.ts +1 -0
  61. package/package.json +92 -4
package/dist/cli.js ADDED
@@ -0,0 +1,440 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/bin.ts
4
+ import { readFileSync as readFileSync2 } from "node:fs";
5
+
6
+ // src/cli/init.ts
7
+ import {
8
+ existsSync,
9
+ mkdirSync,
10
+ readFileSync,
11
+ readdirSync,
12
+ statSync,
13
+ writeFileSync
14
+ } from "node:fs";
15
+ import path from "node:path";
16
+ import { parseArgs } from "node:util";
17
+ var USAGE = `Usage: npx @codepress/blog init --site <slug> [options]
18
+
19
+ Adds a CodePress blog to the Next.js project in the current directory.
20
+
21
+ Options:
22
+ --site <slug> The CodePress content site to read posts from (required)
23
+ --base-url <url> The site's public origin, e.g. https://acme.com
24
+ (default: process.env.NEXT_PUBLIC_SITE_URL at runtime)
25
+ --base-path <path> Where the blog is mounted (default: /blog)
26
+ --api-base <url> The CodePress API root (default: https://api.codepress.dev/v1)
27
+ --revalidate <secs> Seconds between ISR regenerations (default: 60)
28
+ --router <app|pages> Which router to use (default: detected)
29
+ --force Overwrite the files init writes when they already exist.
30
+ A /blog route served by another file (the other router,
31
+ a route group, pages/blog.tsx) still stops init: move or
32
+ delete it first.
33
+ --cwd <dir> The project directory (default: the current directory)
34
+ -h, --help Show this help
35
+ `;
36
+ var SITE_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
37
+ var PATH_SEGMENT = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/;
38
+ var CONFIG_BASENAME = "codepress-blog";
39
+ var InitError = class extends Error {
40
+ };
41
+ function readPackageJson(root) {
42
+ const file = path.join(root, "package.json");
43
+ if (!existsSync(file)) throw new InitError(`No package.json in ${root}.`);
44
+ return JSON.parse(readFileSync(file, "utf8"));
45
+ }
46
+ function isDirectory(file) {
47
+ return existsSync(file) && statSync(file).isDirectory();
48
+ }
49
+ function detectLayout(root, forced) {
50
+ const pkg = readPackageJson(root);
51
+ const deps = {
52
+ ...pkg.dependencies,
53
+ ...pkg.devDependencies
54
+ };
55
+ if (!deps.next) {
56
+ throw new InitError(
57
+ `${root} is not a Next.js project: package.json has no "next" dependency.`
58
+ );
59
+ }
60
+ const find = (name) => [path.join(root, name), path.join(root, "src", name)].find(isDirectory);
61
+ const app = find("app");
62
+ const pages = find("pages");
63
+ if (forced && forced !== "app" && forced !== "pages") {
64
+ throw new InitError(`--router must be "app" or "pages", not "${forced}".`);
65
+ }
66
+ const router = forced ?? (app ? "app" : pages ? "pages" : void 0);
67
+ if (!router) {
68
+ throw new InitError(
69
+ `No app/ or pages/ directory found in ${root} (or its src/).`
70
+ );
71
+ }
72
+ const routesDir = (router === "app" ? app : pages) ?? path.join(root, router);
73
+ const allRoutesDirs = ["app", "pages"].flatMap(
74
+ (name) => [path.join(root, name), path.join(root, "src", name)].filter(isDirectory).map((dir) => ({ dir, router: name }))
75
+ );
76
+ return {
77
+ root,
78
+ router,
79
+ routesDir,
80
+ allRoutesDirs,
81
+ configDir: path.dirname(routesDir),
82
+ typescript: existsSync(path.join(root, "tsconfig.json"))
83
+ };
84
+ }
85
+ var BLOG_ROUTE_KEYS = /* @__PURE__ */ new Set(["", "[]", "feed.xml", "sitemap.xml"]);
86
+ function routeKey(segments) {
87
+ if (segments.some((segment) => segment.startsWith("[[..."))) return "*";
88
+ return segments.map((segment) => segment.startsWith("[") ? "[]" : segment).join("/");
89
+ }
90
+ function existingRoutes(dir, router, basePath) {
91
+ const found = [];
92
+ const segments = basePath.split("/").filter(Boolean);
93
+ const walk = (current, url) => {
94
+ for (const entry of readdirSync(current)) {
95
+ const full = path.join(current, entry);
96
+ if (isDirectory(full)) {
97
+ if (entry === "node_modules") continue;
98
+ const invisible = /^\(.*\)$/.test(entry) || entry.startsWith("@");
99
+ walk(full, invisible ? url : [...url, entry]);
100
+ continue;
101
+ }
102
+ if (!/\.(mdx?|[cm]?[jt]sx?)$/.test(entry)) continue;
103
+ const routeUrl = router === "app" ? /^(page|route)\.[cm]?[jt]sx?$|^page\.mdx?$/.test(entry) ? url : null : [...url, entry.replace(/\.[^.]+$/, "")].filter(
104
+ (s, i, all) => !(s === "index" && i === all.length - 1)
105
+ );
106
+ if (!routeUrl) continue;
107
+ if (segments.every((segment, index) => routeUrl[index] === segment)) {
108
+ found.push({
109
+ file: full,
110
+ key: routeKey(routeUrl.slice(segments.length))
111
+ });
112
+ }
113
+ }
114
+ };
115
+ walk(dir, []);
116
+ return found;
117
+ }
118
+ function classifyConflicts(layout, basePath, files) {
119
+ const overwrite = [];
120
+ const migrate = [];
121
+ const other = [];
122
+ const relative = (file) => path.relative(layout.root, file);
123
+ for (const { dir, router } of layout.allRoutesDirs) {
124
+ for (const route of existingRoutes(dir, router, basePath)) {
125
+ if (files.has(route.file)) overwrite.push(relative(route.file));
126
+ else if (route.key === "*" || BLOG_ROUTE_KEYS.has(route.key))
127
+ migrate.push(relative(route.file));
128
+ else other.push(relative(route.file));
129
+ }
130
+ }
131
+ for (const file of files.keys()) {
132
+ const name = relative(file);
133
+ if (existsSync(file) && !overwrite.includes(name)) overwrite.push(name);
134
+ }
135
+ return { overwrite, migrate, other };
136
+ }
137
+ function importPath(fromFile, toFile) {
138
+ const relative = path.relative(path.dirname(fromFile), toFile).replace(/\\/g, "/").replace(/\.[jt]sx?$/, "");
139
+ return relative.startsWith(".") ? relative : `./${relative}`;
140
+ }
141
+ function configSource(layout, options) {
142
+ const module = layout.router === "app" ? "@codepress/blog/next" : "@codepress/blog/next/pages";
143
+ const lines = [
144
+ ` site: ${JSON.stringify(options.site)},`,
145
+ options.baseUrl ? ` baseUrl: ${JSON.stringify(options.baseUrl)},` : ` baseUrl: process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000",`,
146
+ ` basePath: ${JSON.stringify(options.basePath)},`,
147
+ ...options.apiBase ? [` apiBase: ${JSON.stringify(options.apiBase)},`] : [],
148
+ ` revalidate: ${options.revalidate},`
149
+ ];
150
+ const typed = layout.typescript;
151
+ return `// Written by \`npx @codepress/blog init\`. Posts come from the CodePress
152
+ // content site ${JSON.stringify(options.site)}; see https://www.npmjs.com/package/@codepress/blog.
153
+ ${typed ? `import type { BlogConfig } from "${module}";
154
+
155
+ ` : ""}export const blogConfig${typed ? ": BlogConfig" : ""} = {
156
+ ${lines.join("\n")}
157
+ };
158
+ `;
159
+ }
160
+ function routeFiles(layout, options, configFile) {
161
+ const ext = layout.typescript ? "tsx" : "jsx";
162
+ const codeExt = layout.typescript ? "ts" : "js";
163
+ const base = path.join(
164
+ layout.routesDir,
165
+ ...options.basePath.split("/").filter(Boolean)
166
+ );
167
+ const files = /* @__PURE__ */ new Map();
168
+ const add = (file, body) => files.set(file, body(importPath(file, configFile)));
169
+ const css = `import "@codepress/blog/blog.css";
170
+ `;
171
+ const r = options.revalidate;
172
+ if (layout.router === "app") {
173
+ add(
174
+ path.join(base, `page.${ext}`),
175
+ (config) => `${css}import { createAppBlog } from "@codepress/blog/next";
176
+ import { blogConfig } from "${config}";
177
+
178
+ const blog = createAppBlog(blogConfig);
179
+
180
+ export const revalidate = ${r};
181
+ export const generateMetadata = blog.generateIndexMetadata;
182
+ export default blog.IndexPage;
183
+ `
184
+ );
185
+ add(
186
+ path.join(base, "[slug]", `page.${ext}`),
187
+ (config) => `${css}import { createAppBlog } from "@codepress/blog/next";
188
+ import { blogConfig } from "${config}";
189
+
190
+ const blog = createAppBlog(blogConfig);
191
+
192
+ export const revalidate = ${r};
193
+ export const dynamicParams = true;
194
+ export const generateStaticParams = blog.generateStaticParams;
195
+ export const generateMetadata = blog.generatePostMetadata;
196
+ export default blog.PostPage;
197
+ `
198
+ );
199
+ add(
200
+ path.join(base, "feed.xml", `route.${codeExt}`),
201
+ (config) => `import { createAppBlogRoutes } from "@codepress/blog/next/routes";
202
+ import { blogConfig } from "${config}";
203
+
204
+ export const revalidate = ${r};
205
+ export const GET = createAppBlogRoutes(blogConfig).feed;
206
+ `
207
+ );
208
+ add(
209
+ path.join(base, "sitemap.xml", `route.${codeExt}`),
210
+ (config) => `import { createAppBlogRoutes } from "@codepress/blog/next/routes";
211
+ import { blogConfig } from "${config}";
212
+
213
+ export const revalidate = ${r};
214
+ export const GET = createAppBlogRoutes(blogConfig).sitemap;
215
+ `
216
+ );
217
+ } else {
218
+ add(
219
+ path.join(base, `index.${ext}`),
220
+ (config) => `${css}import { createPagesBlog } from "@codepress/blog/next/pages";
221
+ import { blogConfig } from "${config}";
222
+
223
+ const blog = createPagesBlog(blogConfig);
224
+
225
+ export const getStaticProps = blog.getIndexStaticProps;
226
+ export default blog.IndexPage;
227
+ `
228
+ );
229
+ add(
230
+ path.join(base, `[slug].${ext}`),
231
+ (config) => `${css}import { createPagesBlog } from "@codepress/blog/next/pages";
232
+ import { blogConfig } from "${config}";
233
+
234
+ const blog = createPagesBlog(blogConfig);
235
+
236
+ export const getStaticPaths = blog.getPostStaticPaths;
237
+ export const getStaticProps = blog.getPostStaticProps;
238
+ export default blog.PostPage;
239
+ `
240
+ );
241
+ add(
242
+ path.join(base, `feed.xml.${ext}`),
243
+ (config) => `import { createPagesBlog } from "@codepress/blog/next/pages";
244
+ import { blogConfig } from "${config}";
245
+
246
+ const blog = createPagesBlog(blogConfig);
247
+
248
+ export const getServerSideProps = blog.getFeedServerSideProps;
249
+ export default blog.EmptyPage;
250
+ `
251
+ );
252
+ add(
253
+ path.join(base, `sitemap.xml.${ext}`),
254
+ (config) => `import { createPagesBlog } from "@codepress/blog/next/pages";
255
+ import { blogConfig } from "${config}";
256
+
257
+ const blog = createPagesBlog(blogConfig);
258
+
259
+ export const getServerSideProps = blog.getSitemapServerSideProps;
260
+ export default blog.EmptyPage;
261
+ `
262
+ );
263
+ }
264
+ return files;
265
+ }
266
+ function parseOptions(argv) {
267
+ const { values, positionals } = parseArgs({
268
+ args: argv,
269
+ allowPositionals: true,
270
+ options: {
271
+ site: { type: "string" },
272
+ "base-url": { type: "string" },
273
+ "base-path": { type: "string", default: "/blog" },
274
+ "api-base": { type: "string" },
275
+ revalidate: { type: "string", default: "60" },
276
+ router: { type: "string" },
277
+ force: { type: "boolean", default: false },
278
+ cwd: { type: "string" },
279
+ help: { type: "boolean", short: "h", default: false }
280
+ }
281
+ });
282
+ return { values, positionals };
283
+ }
284
+ function validate(values) {
285
+ const site = values.site;
286
+ if (!site || !SITE_PATTERN.test(site)) {
287
+ throw new InitError(
288
+ "--site <slug> is required: lowercase letters, digits and hyphens."
289
+ );
290
+ }
291
+ const segments = (values["base-path"] ?? "/blog").split("/").filter(Boolean);
292
+ if (segments.length === 0 || !segments.every((segment) => PATH_SEGMENT.test(segment))) {
293
+ throw new InitError(
294
+ `--base-path must be a path such as /blog, not "${values["base-path"]}".`
295
+ );
296
+ }
297
+ for (const [flag, value] of [
298
+ ["--base-url", values["base-url"]],
299
+ ["--api-base", values["api-base"]]
300
+ ]) {
301
+ if (value !== void 0 && !/^https?:\/\/[^/]/.test(value)) {
302
+ throw new InitError(`${flag} must be an http(s) URL, not "${value}".`);
303
+ }
304
+ }
305
+ const revalidate = Number(values.revalidate);
306
+ if (!Number.isInteger(revalidate) || revalidate < 1) {
307
+ throw new InitError(
308
+ `--revalidate must be a whole number of seconds, not "${values.revalidate}".`
309
+ );
310
+ }
311
+ return {
312
+ site,
313
+ baseUrl: values["base-url"]?.replace(/\/+$/, ""),
314
+ basePath: `/${segments.join("/")}`,
315
+ apiBase: values["api-base"],
316
+ revalidate
317
+ };
318
+ }
319
+ function writeFile(file, content) {
320
+ mkdirSync(path.dirname(file), { recursive: true });
321
+ writeFileSync(file, content);
322
+ }
323
+ function runInit(argv, {
324
+ version: version2,
325
+ cwd = process.cwd(),
326
+ io = { log: console.log, error: console.error }
327
+ }) {
328
+ try {
329
+ const { values, positionals } = parseOptions(argv);
330
+ if (values.help) {
331
+ io.log(USAGE);
332
+ return 0;
333
+ }
334
+ if (positionals.length > 0)
335
+ throw new InitError(`Unexpected argument "${positionals[0]}".`);
336
+ const options = validate(values);
337
+ const layout = detectLayout(
338
+ path.resolve(cwd, values.cwd ?? "."),
339
+ values.router
340
+ );
341
+ const configFile = path.join(
342
+ layout.configDir,
343
+ `${CONFIG_BASENAME}.${layout.typescript ? "ts" : "js"}`
344
+ );
345
+ const files = routeFiles(layout, options, configFile);
346
+ files.set(configFile, configSource(layout, options));
347
+ const { overwrite, migrate, other } = classifyConflicts(
348
+ layout,
349
+ options.basePath,
350
+ files
351
+ );
352
+ if (migrate.length > 0 || !values.force && overwrite.length + other.length > 0) {
353
+ io.error(`${options.basePath} already exists; nothing was changed.`);
354
+ if (migrate.length > 0) {
355
+ io.error(
356
+ `These files serve ${options.basePath} URLs that init writes, from a different path. Move or delete them first; --force does not remove them:`
357
+ );
358
+ for (const file of migrate) io.error(` ${file}`);
359
+ }
360
+ if (overwrite.length > 0) {
361
+ io.error(
362
+ values.force ? "--force would overwrite:" : "Re-run with --force to overwrite:"
363
+ );
364
+ for (const file of overwrite) io.error(` ${file}`);
365
+ }
366
+ if (other.length > 0) {
367
+ io.error(
368
+ values.force ? `--force would keep these other ${options.basePath} routes:` : `Other ${options.basePath} routes (--force keeps them):`
369
+ );
370
+ for (const file of other) io.error(` ${file}`);
371
+ }
372
+ return 1;
373
+ }
374
+ const pkgFile = path.join(layout.root, "package.json");
375
+ const pkg = readPackageJson(layout.root);
376
+ const dependencies = {
377
+ ...pkg.dependencies
378
+ };
379
+ const dependencyChanged = !dependencies["@codepress/blog"];
380
+ if (dependencyChanged) {
381
+ dependencies["@codepress/blog"] = `^${version2}`;
382
+ pkg.dependencies = Object.fromEntries(
383
+ Object.entries(dependencies).sort(([a], [b]) => a.localeCompare(b))
384
+ );
385
+ }
386
+ io.log(
387
+ `Adding a CodePress blog at ${options.basePath} (${layout.router === "app" ? "App" : "Pages"} Router):`
388
+ );
389
+ for (const [file, content] of files) {
390
+ const verb = existsSync(file) ? "overwrote" : "created";
391
+ writeFile(file, content);
392
+ io.log(` ${verb} ${path.relative(layout.root, file)}`);
393
+ }
394
+ for (const file of other) io.log(` kept ${file}`);
395
+ if (dependencyChanged) {
396
+ writeFileSync(pkgFile, `${JSON.stringify(pkg, null, 2)}
397
+ `);
398
+ io.log(` added @codepress/blog@^${version2} to package.json`);
399
+ }
400
+ io.log("");
401
+ io.log(
402
+ "The pages render inside your root layout and import @codepress/blog/blog.css."
403
+ );
404
+ if (dependencyChanged)
405
+ io.log("Next: install dependencies (npm install, pnpm install or yarn).");
406
+ if (!options.baseUrl) {
407
+ io.log(
408
+ "Set NEXT_PUBLIC_SITE_URL to your site's origin, or edit baseUrl in the config, so feed and canonical links are absolute."
409
+ );
410
+ }
411
+ return 0;
412
+ } catch (error) {
413
+ if (error instanceof InitError || error.code?.startsWith("ERR_PARSE_ARGS")) {
414
+ io.error(error.message);
415
+ io.error("");
416
+ io.error(USAGE);
417
+ return 1;
418
+ }
419
+ throw error;
420
+ }
421
+ }
422
+ function runCli(argv, options) {
423
+ const [command, ...rest] = argv;
424
+ if (command === "init") return runInit(rest, options);
425
+ const io = options.io ?? { log: console.log, error: console.error };
426
+ if (command === void 0 || command === "-h" || command === "--help") {
427
+ io.log(USAGE);
428
+ return command === void 0 ? 1 : 0;
429
+ }
430
+ io.error(`Unknown command "${command}".
431
+
432
+ ${USAGE}`);
433
+ return 1;
434
+ }
435
+
436
+ // src/cli/bin.ts
437
+ var { version } = JSON.parse(
438
+ readFileSync2(new URL("../package.json", import.meta.url), "utf8")
439
+ );
440
+ process.exitCode = runCli(process.argv.slice(2), { version });
@@ -0,0 +1,31 @@
1
+ import { type PostMeta } from "./registry.js";
2
+ export interface PostSummary {
3
+ slug: string;
4
+ meta: PostMeta;
5
+ }
6
+ export interface Post extends PostSummary {
7
+ /** Sanitized body HTML. */
8
+ html: string;
9
+ }
10
+ /** Where posts come from. Pages depend on this interface only. */
11
+ export interface ContentSource {
12
+ /** Published posts, newest first. */
13
+ listPosts(): Promise<PostSummary[]>;
14
+ getPost(slug: string): Promise<Post | null>;
15
+ }
16
+ export interface HttpContentSourceOptions {
17
+ /** The CodePress API root including `/v1`, for example `https://api.codepress.dev/v1`. */
18
+ apiBase: string;
19
+ /** The content site's slug. */
20
+ site: string;
21
+ /** Timeout for each request. Defaults to 10 seconds. */
22
+ timeoutMs?: number;
23
+ }
24
+ /**
25
+ * Reads a content site's published posts from the CodePress read API
26
+ * (`GET {apiBase}/content-sites/{site}/posts[/{slug}]`), which serves sanitized
27
+ * posts. A 404 is "no such post" (an empty blog for the list); any other
28
+ * failure throws, so during ISR regeneration the last good page keeps being
29
+ * served instead of being replaced with a 404.
30
+ */
31
+ export declare function createHttpContentSource({ apiBase, site, timeoutMs, }: HttpContentSourceOptions): ContentSource;
@@ -0,0 +1,37 @@
1
+ import type { DraftClient, DraftFile } from "./client.js";
2
+ import { type BlogEditorTimings } from "./config.js";
3
+ import type { BlogEditorHeader } from "./header.js";
4
+ /** A database site's revision, as the save status names it; null for a git site. */
5
+ export declare function revisionOf(draft: DraftFile): {
6
+ revision: number | null;
7
+ } | null;
8
+ export interface BlogEditorProps {
9
+ site: string;
10
+ slug: string;
11
+ /** The draft API, carrying the host's auth. */
12
+ client: DraftClient;
13
+ canPublish: boolean;
14
+ /** Overrides of the autosave, poll and publish-notice timings. */
15
+ timings?: Partial<BlogEditorTimings>;
16
+ /** The reading column's classes; defaults to a 720px column. */
17
+ column?: string;
18
+ /** The host page's header the toolbar pins below. */
19
+ header?: BlogEditorHeader;
20
+ /** Opens or hides the "Ask CodePress" chat; the mount owns the panel so it survives editor reloads. */
21
+ onAsk?: () => void;
22
+ /** Without a panel, Ask CodePress opens this page in a new tab. */
23
+ askHref?: string | null;
24
+ /** The CodePress agent's mark on the Ask CodePress button. */
25
+ askIconSrc?: string;
26
+ /** The Ask CodePress panel is showing. */
27
+ askOpen?: boolean;
28
+ /** Where "Edit design" opens; without it the link is not shown. */
29
+ designHref?: string | null;
30
+ /** A database site's revision history, which its saved status links to. */
31
+ historyHref?: string | null;
32
+ onExit: () => void;
33
+ onPublished: () => void;
34
+ onDiscarded: () => void;
35
+ }
36
+ /** The in-place editor for one post; loaded lazily, only for people who can edit. */
37
+ export default function BlogEditor({ timings: timingOverrides, column, ...props }: BlogEditorProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,64 @@
1
+ import { type ReactNode } from "react";
2
+ import { type BlogEditorAuth } from "./client.js";
3
+ import { type BlogEditorTimings } from "./config.js";
4
+ import type { BlogEditorHeader } from "./header.js";
5
+ /** What the host's Ask CodePress panel is given. */
6
+ export interface AskPanelProps {
7
+ site: string;
8
+ slug: string;
9
+ /** The site's organization: the chat runs in it. */
10
+ organizationId: string;
11
+ open: boolean;
12
+ onClose: () => void;
13
+ }
14
+ /** What a host's Edit design link is built from: the site's owner and repository. */
15
+ export interface DesignLinkContext {
16
+ slug: string;
17
+ organizationSlug: string;
18
+ repository: string;
19
+ }
20
+ export interface BlogEditorMountProps {
21
+ /** The content site's slug. */
22
+ site: string;
23
+ slug: string;
24
+ /** The CodePress API root, `…/v1`. */
25
+ apiBase: string;
26
+ auth: BlogEditorAuth;
27
+ /** A post that does not exist yet: open the editor straight away. */
28
+ isNew?: boolean;
29
+ /** The host page's header the toolbar pins below. */
30
+ header?: BlogEditorHeader;
31
+ /** The reading column's classes; defaults to a 720px column. */
32
+ column?: string;
33
+ /** The CodePress app's origin, for its links and the agent's mark; empty when this page is on it. */
34
+ appUrl?: string;
35
+ timings?: Partial<BlogEditorTimings>;
36
+ onEditingChange?: (editing: boolean) => void;
37
+ /** After a publish; the default reloads the page. */
38
+ onPublished?: (slug: string) => void;
39
+ /** A new post was closed or discarded, so there is no reader page to return to. */
40
+ onLeave?: () => void;
41
+ slots?: {
42
+ /** An Ask CodePress chat beside the editor; kept mounted (hidden) once opened. */
43
+ askPanel?: (props: AskPanelProps) => ReactNode;
44
+ /** Where Edit design opens; defaults to the CodePress Design page for the site's repository. */
45
+ designLink?: (context: DesignLinkContext) => string;
46
+ };
47
+ actions?: {
48
+ /** Where Ask CodePress goes without a panel; defaults to the post in the site's App page. */
49
+ askCodePress?: {
50
+ href: string;
51
+ };
52
+ };
53
+ }
54
+ /**
55
+ * The site's App page in CodePress, opened on a post. `tab` picks the page's
56
+ * History; without the organization it is the dashboard.
57
+ */
58
+ export declare function appPageUrl(appUrl: string, organizationSlug: string | null | undefined, site: string, slug: string, tab?: "history"): string;
59
+ /**
60
+ * Asks the backend what the caller may do and shows an Edit button only when
61
+ * the answer is `can_edit`; everyone else gets nothing and never downloads the
62
+ * editor.
63
+ */
64
+ export declare function BlogEditorMount({ site, slug, apiBase, auth, isNew, header, column, appUrl, timings, onEditingChange, onPublished, onLeave, slots, actions, }: BlogEditorMountProps): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,39 @@
1
+ export type Status = "idle" | "dirty" | "saving" | "saved" | "conflict" | "rejected" | "invalid" | "error";
2
+ export declare const STATUS_LABEL: Record<Status, string>;
3
+ /** A database site's draft: no pull request, a revision count instead. */
4
+ export interface DbRevision {
5
+ revision: number | null;
6
+ }
7
+ /** What the status says: a database site names the revision a save made. */
8
+ export declare function statusLabel(status: Status, db: DbRevision | null): string;
9
+ export interface DocumentActionsProps {
10
+ status: Status;
11
+ prUrl: string | null;
12
+ /** Set for a database site, whose draft has no pull request, only a history. */
13
+ db?: DbRevision | null;
14
+ /** Where a database site's revision history is. */
15
+ historyHref?: string | null;
16
+ /** Where Edit design opens; null when the site has no connected repository. */
17
+ designHref: string | null;
18
+ /** Opens or hides the Ask CodePress panel; absent when the mount cannot host it. */
19
+ onAsk?: () => void;
20
+ /** Without a panel, Ask CodePress opens this page (the post in the CodePress app) in a new tab. */
21
+ askHref?: string | null;
22
+ /** The CodePress agent's mark. */
23
+ askIconSrc?: string;
24
+ askOpen: boolean;
25
+ canPublish: boolean;
26
+ busy: null | "publishing" | "discarding";
27
+ publishDisabled: boolean;
28
+ confirmDiscard: boolean;
29
+ onConfirmDiscardChange: (open: boolean) => void;
30
+ onDiscard: () => void;
31
+ onDone: () => void;
32
+ onPublish: () => void;
33
+ }
34
+ /**
35
+ * The right-hand side of the toolbar: where the draft stands and what can be
36
+ * done with it. Below `xl` the secondary actions move into a More menu so the
37
+ * row never wraps.
38
+ */
39
+ export declare function DocumentActions({ status, prUrl, db, historyHref, designHref, onAsk, askHref, askIconSrc, askOpen, canPublish, busy, publishDisabled, confirmDiscard, onConfirmDiscardChange, onDiscard, onDone, onPublish, }: DocumentActionsProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,30 @@
1
+ import type { ComponentPropsWithRef, ReactNode } from "react";
2
+ /** Where a site's posts are edited: the org that owns the site and its repository. */
3
+ export interface DesignTarget {
4
+ organizationSlug: string;
5
+ repository: string;
6
+ }
7
+ /** The target from the site's `/me` answer, or null when the site has no connected repository. */
8
+ export declare function designTargetOf(capabilities: {
9
+ organization_slug?: string | null;
10
+ repository_full_name?: string | null;
11
+ }): DesignTarget | null;
12
+ /**
13
+ * The dashboard's Design page (the Live Dev Server) of the organization that
14
+ * owns the site, opened on the site's repository at the post's route. The
15
+ * organization comes from the backend, not from the caller's organization list:
16
+ * someone in several organizations must land in the one where the repository is
17
+ * connected. The preview runs on an isolated origin inside the dashboard, so
18
+ * nothing from the preview runtime is ever loaded on this site. `appUrl` is the
19
+ * CodePress app's origin; empty means this page is on it.
20
+ */
21
+ export declare function designUrl({ organizationSlug, repository }: DesignTarget, slug: string, appUrl?: string): string;
22
+ /**
23
+ * Renders nothing without a link: without the owning organization and its
24
+ * repository there is no Design page to open, and the dashboard's entry point
25
+ * would pick the caller's first organization, which may not own the site.
26
+ */
27
+ export declare function EditDesignLink({ href, children, ...rest }: {
28
+ href: string | null;
29
+ children?: ReactNode;
30
+ } & Omit<ComponentPropsWithRef<"a">, "href" | "target" | "rel">): import("react/jsx-runtime").JSX.Element | null;
@@ -0,0 +1,10 @@
1
+ /** Whether `slug` can address a new post: the schema's slug rules, and not a reserved route. */
2
+ export declare function isValidNewSlug(slug: string): boolean;
3
+ export interface NewPostFormProps {
4
+ /** Called with a valid slug when the author starts writing. */
5
+ onStart: (slug: string) => void;
6
+ /** Shown before the slug field: where posts live on the site. */
7
+ prefix?: string;
8
+ }
9
+ /** Start a post: choose its address, then write it in the editor. */
10
+ export declare function NewPostForm({ onStart, prefix }: NewPostFormProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,5 @@
1
+ import type { SlashState } from "./slash-command.js";
2
+ /** The `/` menu, drawn under the caret while the slash command is active. */
3
+ export declare function SlashMenu({ state }: {
4
+ state: SlashState | null;
5
+ }): import("react/jsx-runtime").JSX.Element | null;