bearminion 1.4.0 → 1.6.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.
- package/CHANGELOG.md +22 -0
- package/README.md +2 -2
- package/dist/astro.d.ts +2 -2
- package/dist/astro.js +234 -13
- package/dist/content.d.ts +1 -1
- package/dist/content.js +3 -3
- package/dist/index.d.ts +35 -23
- package/dist/index.js +9 -7
- package/dist/{migrate-B56wxCiC.js → migrate-b408EaLp.js} +3 -1
- package/dist/{options-qFsKXOuE.d.ts → options-DdCyRbPs.d.ts} +35 -2
- package/dist/{page-C87bm-bf.d.ts → page-BFuPEq6Q.d.ts} +56 -3
- package/dist/{page-ON4Jkcm-.js → page-NOFVEqn1.js} +125 -3
- package/dist/{preview-CTVEBdEY.js → preview--S1QUrMM.js} +1 -1
- package/dist/{reload-Cq434rtI.js → reload-D0vwI0OT.js} +1 -1
- package/dist/rich-text.d.ts +17 -8
- package/dist/rich-text.js +45 -24
- package/dist/{theme-DMpt3hcT.js → theme-DFjHjgFO.js} +1 -1
- package/dist/{theme-colors-zICpze42.js → theme-colors-CHROGZkI.js} +24 -49
- package/dist/ui/mount.js +902 -245
- package/package.json +5 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# bearminion
|
|
2
2
|
|
|
3
|
+
## 1.6.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- eec6f3d: New starter sections and editor features.
|
|
8
|
+
|
|
9
|
+
- **Gallery** section: grid, masonry and featured layouts, a keyboard-friendly lightbox, an optional box around each picture, and required alt text.
|
|
10
|
+
- **Text** section: long-form rich text for an About or policy page, with a narrow, readable column on by default. Rich-text fields can opt into **H2** and **H3** (`data-rich-headings`); `renderRichText`, `sanitizeRichHtml` and `stripPlainRich` take a `{ headings }` option.
|
|
11
|
+
- **Media** now covers video: the separate Video section is gone, and Media gains `playback` (`controls` or `loop`). Content `schemaVersion` 2 migrates existing Video sections to Media automatically.
|
|
12
|
+
- **Video uploads**: upload an mp4, webm, mov or ogv to `public/videos` from the editor (`videosDir`, `maxVideoBytes`, default 50 MB), pick an uploaded one, and see what a pasted link is or why it will not play. New API routes `GET`/`POST /videos` and `DELETE /videos/:filename`.
|
|
13
|
+
- `parseMediaSource` and `VIDEO_FILE` are exported from `bearminion`.
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- eec6f3d: Clearer package descriptions and READMEs: what BearMinion is, who it is for, and what `create-bearminion` refuses (a folder that is not new or empty).
|
|
18
|
+
|
|
19
|
+
## 1.5.0
|
|
20
|
+
|
|
21
|
+
### Minor Changes
|
|
22
|
+
|
|
23
|
+
- 8a428e7: Sitewide JSON-LD. `site.seo.jsonLd` is optional (up to 10 blocks, 50 KB, so no migration) and the default template emits its blocks on every page. A page's own `seo.jsonLd` still replaces only the generated `WebPage` block, and a block with the same `@id` is emitted once, the page's first. With the conventional ids `<url>/#organization` and `<url>/#website` in the site's blocks, the generated `WebPage` gets `isPartOf` and `publisher` as `@id` references. The Site tab has a Structured data group with an Edit JSON-LD dialog and a Generate button that fills an `Organization` and a `WebSite` from the site name, address and language, replacing blocks with the same `@id`. The logo is added only for a full https address. A warning shows while the site address is still `example.com`. `siteJsonLdIds`, `mergeJsonLd`, `hasJsonLdId`, `generateSiteJsonLd`, `upsertJsonLd` and `isPlaceholderSiteUrl` are exported; themes that emit JSON-LD should use `mergeJsonLd` (see the theme docs).
|
|
24
|
+
|
|
3
25
|
## 1.4.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
5
|
<p align="center">
|
|
6
|
-
A website you edit by clicking on it
|
|
6
|
+
The BearMinion for the bare minimum. A website you edit by clicking on it: no database to patch at 3 AM, no admin panel, no server.<br />
|
|
7
7
|
Built on <a href="https://astro.build">Astro</a>.
|
|
8
8
|
</p>
|
|
9
9
|
|
|
@@ -34,7 +34,7 @@ It asks for the site's name and web address. Open the address `npm run dev` prin
|
|
|
34
34
|
## What you can change
|
|
35
35
|
|
|
36
36
|
- **Text** right on the page, with bold, italic, code, links and lists
|
|
37
|
-
- **Sections**:
|
|
37
|
+
- **Sections**: fourteen ready-made types, each with two or three layouts, added from a visual picker
|
|
38
38
|
- **Pages, menus and footer**, with titles and descriptions for search and sharing
|
|
39
39
|
- **Images**: upload, choose how they fill their frame and which part stays in view
|
|
40
40
|
- **Colors**: pick three, and the editor builds a full light and dark palette that stays readable
|
package/dist/astro.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { a as StoreKeyError, c as createFileStore, i as FileStoreOptions, n as EditorContext, o as StoreOperation, r as ContentStore, s as UploadStore, t as BearminionOptions } from "./options-
|
|
1
|
+
import { A as SectionRegistry, J as SiteDefinition, Y as SiteOf, Yt as ContentMigrator, k as ContentModel, t as Page, u as SectionOf } from "./page-BFuPEq6Q.js";
|
|
2
|
+
import { a as StoreKeyError, c as createFileStore, i as FileStoreOptions, n as EditorContext, o as StoreOperation, r as ContentStore, s as UploadStore, t as BearminionOptions } from "./options-DdCyRbPs.js";
|
|
3
3
|
import { AstroIntegration } from "astro";
|
|
4
4
|
//#region src/integration.d.ts
|
|
5
5
|
interface BearminionIntegration extends AstroIntegration {
|
package/dist/astro.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { a as imageFields, c as ContentReadError, l as ContentValidationError, r as loadThemePreview, t as PreviewUnavailableError } from "./preview
|
|
2
|
-
import { t as defaultTheme } from "./theme-
|
|
3
|
-
import {
|
|
4
|
-
import { a as DEFAULT_UPLOADS_DIR, i as DEFAULT_CONTENT_DIR, n as createMigrator, r as API_PREFIX, t as SCHEMA_VERSION_KEY } from "./migrate-
|
|
5
|
-
import { a as JSON_BODY_LIMIT, c as isInside, d as resolveDir, f as resolveInside, i as IMAGE_BYTE_LIMIT, l as isLocalRequest, n as contentReloadPlugin, o as MULTIPART_BODY_LIMIT, r as dropEarlyReloadAfterSave, s as asDirectory, u as readSlug } from "./reload-
|
|
1
|
+
import { a as imageFields, c as ContentReadError, l as ContentValidationError, r as loadThemePreview, t as PreviewUnavailableError } from "./preview--S1QUrMM.js";
|
|
2
|
+
import { t as defaultTheme } from "./theme-DFjHjgFO.js";
|
|
3
|
+
import { i as isSystemPage, n as createPageSchema$1, r as createTolerantPageSchema, s as withoutOrphanSections, u as isOrphanSection } from "./page-NOFVEqn1.js";
|
|
4
|
+
import { a as DEFAULT_UPLOADS_DIR, i as DEFAULT_CONTENT_DIR, n as createMigrator, o as DEFAULT_VIDEOS_DIR, r as API_PREFIX, t as SCHEMA_VERSION_KEY } from "./migrate-b408EaLp.js";
|
|
5
|
+
import { a as JSON_BODY_LIMIT, c as isInside, d as resolveDir, f as resolveInside, i as IMAGE_BYTE_LIMIT, l as isLocalRequest, n as contentReloadPlugin, o as MULTIPART_BODY_LIMIT, r as dropEarlyReloadAfterSave, s as asDirectory, u as readSlug } from "./reload-D0vwI0OT.js";
|
|
6
6
|
import { S as isAssetPath, b as describeSiteRef, c as projectId, v as assetRefs, y as describePageRef } from "./draft-store-1Y4-sn45.js";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import fs from "node:fs";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
10
10
|
import path from "node:path";
|
|
11
|
-
import { access, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
12
11
|
import { randomBytes } from "node:crypto";
|
|
12
|
+
import { access, mkdir, readFile, readdir, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
|
|
13
13
|
import { execFile } from "node:child_process";
|
|
14
14
|
import { promisify } from "node:util";
|
|
15
15
|
//#region src/editor-client.ts
|
|
@@ -35,6 +35,112 @@ function editorClientPlugin() {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
//#endregion
|
|
38
|
+
//#region src/videos.ts
|
|
39
|
+
const EXTENSION$1 = {
|
|
40
|
+
mp4: ".mp4",
|
|
41
|
+
mov: ".mov",
|
|
42
|
+
webm: ".webm",
|
|
43
|
+
ogv: ".ogv"
|
|
44
|
+
};
|
|
45
|
+
const MIME_TO_KIND$1 = {
|
|
46
|
+
"video/mp4": "mp4",
|
|
47
|
+
"video/x-m4v": "mp4",
|
|
48
|
+
"video/quicktime": "mov",
|
|
49
|
+
"video/webm": "webm",
|
|
50
|
+
"video/ogg": "ogv"
|
|
51
|
+
};
|
|
52
|
+
/** Room for multipart boundaries around a max-size video. */
|
|
53
|
+
function videoBodyLimit(maxVideoBytes) {
|
|
54
|
+
return maxVideoBytes + 262144;
|
|
55
|
+
}
|
|
56
|
+
/** The container a file really is, from its first bytes. The file's name and declared type are not trusted. */
|
|
57
|
+
function sniffVideo(bytes) {
|
|
58
|
+
if (bytes.length >= 12 && ascii$1(bytes, 4, 8) === "ftyp") {
|
|
59
|
+
const brand = ascii$1(bytes, 8, 12);
|
|
60
|
+
if (brand === "avif" || brand === "avis") return null;
|
|
61
|
+
return brand === "qt " ? "mov" : "mp4";
|
|
62
|
+
}
|
|
63
|
+
if (bytes.length >= 4 && bytes[0] === 26 && bytes[1] === 69 && bytes[2] === 223 && bytes[3] === 163) return ascii$1(bytes, 0, Math.min(bytes.length, 64)).includes("webm") ? "webm" : null;
|
|
64
|
+
if (bytes.length >= 4 && ascii$1(bytes, 0, 4) === "OggS") return "ogv";
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
function videoKindFromMime(mime) {
|
|
68
|
+
const base = mime.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
69
|
+
return MIME_TO_KIND$1[base] ?? null;
|
|
70
|
+
}
|
|
71
|
+
/** The address a file in `public/` is served at. */
|
|
72
|
+
function publicUrl(root, filePath) {
|
|
73
|
+
return `/${path.relative(path.resolve(root, "public"), filePath).split(path.sep).join("/")}`;
|
|
74
|
+
}
|
|
75
|
+
async function saveVideo(options) {
|
|
76
|
+
await mkdir(options.videosDir, { recursive: true });
|
|
77
|
+
const extension = EXTENSION$1[options.kind];
|
|
78
|
+
const stem = safeStem$1(options.originalName);
|
|
79
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
80
|
+
const filename = `${stem}-${randomBytes(4).toString("hex")}${extension}`;
|
|
81
|
+
const filePath = resolveInside(options.videosDir, filename);
|
|
82
|
+
if (filePath === null) throw new Error("Resolved upload path escaped the videos directory.");
|
|
83
|
+
try {
|
|
84
|
+
await writeFile(filePath, options.bytes, { flag: "wx" });
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error instanceof Error && "code" in error && error.code === "EEXIST") continue;
|
|
87
|
+
throw error;
|
|
88
|
+
}
|
|
89
|
+
if (!isInside(options.root, filePath)) throw new Error("Upload path is outside the project root.");
|
|
90
|
+
return {
|
|
91
|
+
filename,
|
|
92
|
+
path: path.relative(options.root, filePath).split(path.sep).join("/"),
|
|
93
|
+
url: publicUrl(options.root, filePath),
|
|
94
|
+
bytes: options.bytes.byteLength
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
throw new Error("Unable to allocate a unique upload filename.");
|
|
98
|
+
}
|
|
99
|
+
async function listVideos(root, videosDir) {
|
|
100
|
+
let entries;
|
|
101
|
+
try {
|
|
102
|
+
entries = await readdir(videosDir, { withFileTypes: true });
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
const videos = [];
|
|
107
|
+
for (const entry of entries) {
|
|
108
|
+
if (!entry.isFile() || entry.name.startsWith(".")) continue;
|
|
109
|
+
if (!Object.values(EXTENSION$1).includes(path.extname(entry.name).toLowerCase())) continue;
|
|
110
|
+
const filePath = resolveInside(videosDir, entry.name);
|
|
111
|
+
if (filePath === null) continue;
|
|
112
|
+
const info = await stat(filePath).catch(() => null);
|
|
113
|
+
videos.push({
|
|
114
|
+
filename: entry.name,
|
|
115
|
+
path: path.relative(root, filePath).split(path.sep).join("/"),
|
|
116
|
+
url: publicUrl(root, filePath),
|
|
117
|
+
...info ? { bytes: info.size } : {}
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
videos.sort((a, b) => a.filename.localeCompare(b.filename));
|
|
121
|
+
return videos;
|
|
122
|
+
}
|
|
123
|
+
/** Delete one uploaded video by file name. False when there is no such file or the name is not a plain video file name. */
|
|
124
|
+
async function deleteVideo(videosDir, filename) {
|
|
125
|
+
if (filename !== path.basename(filename) || filename.startsWith(".")) return false;
|
|
126
|
+
if (!Object.values(EXTENSION$1).includes(path.extname(filename).toLowerCase())) return false;
|
|
127
|
+
const filePath = resolveInside(videosDir, filename);
|
|
128
|
+
if (filePath === null) return false;
|
|
129
|
+
try {
|
|
130
|
+
if (!(await stat(filePath)).isFile()) return false;
|
|
131
|
+
await unlink(filePath);
|
|
132
|
+
return true;
|
|
133
|
+
} catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function safeStem$1(originalName) {
|
|
138
|
+
return path.win32.basename(path.basename(originalName)).replace(/\.[^.]+$/, "").normalize("NFKD").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "video";
|
|
139
|
+
}
|
|
140
|
+
function ascii$1(bytes, from, to) {
|
|
141
|
+
return Buffer.from(bytes.subarray(from, to)).toString("latin1");
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
38
144
|
//#region src/asset-refs.ts
|
|
39
145
|
/**
|
|
40
146
|
* Where an image path may live on disk: under `src/assets` or the uploads directory. The absolute path, or null
|
|
@@ -237,7 +343,7 @@ function pageKey(slug) {
|
|
|
237
343
|
return `${PAGES_DIR}/${slug}.json`;
|
|
238
344
|
}
|
|
239
345
|
/** The content store for a local project: content as JSON files, uploads in a folder. */
|
|
240
|
-
function createFileStore({ root, contentDir, uploadsDir }) {
|
|
346
|
+
function createFileStore({ root, contentDir, uploadsDir, videosDir }) {
|
|
241
347
|
function resolve(key) {
|
|
242
348
|
const parts = key.split("/");
|
|
243
349
|
if (key === "" || parts.some((part) => part === "" || part === "." || part === "..")) throw new StoreKeyError(key);
|
|
@@ -282,6 +388,17 @@ function createFileStore({ root, contentDir, uploadsDir }) {
|
|
|
282
388
|
if (filePath === null) return false;
|
|
283
389
|
return stat(filePath).then((info) => info.isFile(), () => false);
|
|
284
390
|
},
|
|
391
|
+
...videosDir === void 0 ? {} : { videos: {
|
|
392
|
+
list: () => listVideos(root, videosDir),
|
|
393
|
+
save: ({ originalName, bytes, kind }) => saveVideo({
|
|
394
|
+
root,
|
|
395
|
+
videosDir,
|
|
396
|
+
originalName,
|
|
397
|
+
bytes,
|
|
398
|
+
kind
|
|
399
|
+
}),
|
|
400
|
+
remove: (filename) => deleteVideo(videosDir, filename)
|
|
401
|
+
} },
|
|
285
402
|
uploads: {
|
|
286
403
|
async list() {
|
|
287
404
|
return listImages(root, uploadsDir);
|
|
@@ -312,17 +429,21 @@ function resolveEditorContext(options, rootInput) {
|
|
|
312
429
|
const root = asDirectory(rootInput);
|
|
313
430
|
const contentDir = resolveDir(root, options?.contentDir, DEFAULT_CONTENT_DIR);
|
|
314
431
|
const uploadsDir = resolveDir(root, options?.uploadsDir, DEFAULT_UPLOADS_DIR);
|
|
432
|
+
const videosDir = resolveDir(root, options?.videosDir, DEFAULT_VIDEOS_DIR);
|
|
315
433
|
return {
|
|
316
434
|
root,
|
|
317
435
|
contentDir,
|
|
318
436
|
uploadsDir,
|
|
437
|
+
videosDir,
|
|
438
|
+
maxVideoBytes: options?.maxVideoBytes ?? 52428800,
|
|
319
439
|
sections: options.sections,
|
|
320
440
|
site: options.site,
|
|
321
441
|
theme: options.theme ?? defaultTheme,
|
|
322
442
|
store: options.store ?? createFileStore({
|
|
323
443
|
root,
|
|
324
444
|
contentDir,
|
|
325
|
-
uploadsDir
|
|
445
|
+
uploadsDir,
|
|
446
|
+
videosDir
|
|
326
447
|
}),
|
|
327
448
|
migrator: createMigrator(options),
|
|
328
449
|
git: options?.git === true
|
|
@@ -452,7 +573,11 @@ async function git(root, args) {
|
|
|
452
573
|
}
|
|
453
574
|
/** The folders a commit may touch: the content and the uploads. Relative to the project root. */
|
|
454
575
|
function trackedPaths(context) {
|
|
455
|
-
return [
|
|
576
|
+
return [
|
|
577
|
+
context.contentDir,
|
|
578
|
+
context.uploadsDir,
|
|
579
|
+
context.videosDir
|
|
580
|
+
].filter((dir) => isInside(context.root, dir)).map((dir) => path.relative(context.root, dir).split(path.sep).join("/") || ".");
|
|
456
581
|
}
|
|
457
582
|
function statusOf(code) {
|
|
458
583
|
if (code === "??") return "untracked";
|
|
@@ -606,6 +731,15 @@ async function handleEditorRequest(request, context, meta = {}) {
|
|
|
606
731
|
if (method === "DELETE") return removeImage(context, decodeSegment(parts[1] ?? ""));
|
|
607
732
|
return methodNotAllowed(["DELETE"]);
|
|
608
733
|
}
|
|
734
|
+
if (parts.length === 1 && parts[0] === "videos") {
|
|
735
|
+
if (method === "GET") return getVideos(context);
|
|
736
|
+
if (method === "POST") return postVideo(request, context);
|
|
737
|
+
return methodNotAllowed(["GET", "POST"]);
|
|
738
|
+
}
|
|
739
|
+
if (parts.length === 2 && parts[0] === "videos") {
|
|
740
|
+
if (method === "DELETE") return removeVideo(context, decodeSegment(parts[1] ?? ""));
|
|
741
|
+
return methodNotAllowed(["DELETE"]);
|
|
742
|
+
}
|
|
609
743
|
if (parts.length === 1 && parts[0] === "preview") {
|
|
610
744
|
if (method === "POST") return postPreview(request, context, meta);
|
|
611
745
|
return methodNotAllowed(["POST"]);
|
|
@@ -1149,6 +1283,87 @@ async function removeImage(context, filename) {
|
|
|
1149
1283
|
filename
|
|
1150
1284
|
});
|
|
1151
1285
|
}
|
|
1286
|
+
const NO_VIDEOS = "This content store does not take video uploads.";
|
|
1287
|
+
async function getVideos(context) {
|
|
1288
|
+
if (!context.store.videos) return json(200, {
|
|
1289
|
+
videos: [],
|
|
1290
|
+
maxBytes: context.maxVideoBytes
|
|
1291
|
+
});
|
|
1292
|
+
return json(200, {
|
|
1293
|
+
videos: await context.store.videos.list(),
|
|
1294
|
+
maxBytes: context.maxVideoBytes
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
/** Every string in a JSON value. */
|
|
1298
|
+
function* strings(value) {
|
|
1299
|
+
if (typeof value === "string") yield value;
|
|
1300
|
+
else if (Array.isArray(value)) for (const item of value) yield* strings(item);
|
|
1301
|
+
else if (typeof value === "object" && value !== null) for (const item of Object.values(value)) yield* strings(item);
|
|
1302
|
+
}
|
|
1303
|
+
/** Where saved content links the video at `url`, named like the image usages: "Home page › Media". */
|
|
1304
|
+
async function videoUsages(context, url) {
|
|
1305
|
+
const labels = Object.fromEntries(Object.values(context.sections).map((definition) => [definition.type, definition.label]));
|
|
1306
|
+
const usages = [];
|
|
1307
|
+
if ([...strings(await readStoredJson(context, "site.json", "site"))].includes(url)) usages.push("Site settings");
|
|
1308
|
+
for (const name of await context.store.list("pages") ?? []) {
|
|
1309
|
+
if (!name.endsWith(".json") || name.startsWith(".")) continue;
|
|
1310
|
+
const page = await readStoredJson(context, `${PAGES_DIR}/${name}`, "page");
|
|
1311
|
+
if (typeof page !== "object" || page === null) continue;
|
|
1312
|
+
const title = "title" in page && typeof page.title === "string" ? page.title : name.replace(/\.json$/, "");
|
|
1313
|
+
const sections = "sections" in page && Array.isArray(page.sections) ? page.sections : [];
|
|
1314
|
+
for (const section of sections) if ([...strings(section)].includes(url)) usages.push(`${title} › ${labels[section.type ?? ""] ?? section.type ?? "section"}`);
|
|
1315
|
+
}
|
|
1316
|
+
return usages;
|
|
1317
|
+
}
|
|
1318
|
+
async function removeVideo(context, filename) {
|
|
1319
|
+
const videos = context.store.videos;
|
|
1320
|
+
if (!videos) return problem(404, "not_found", NO_VIDEOS);
|
|
1321
|
+
const video = (await videos.list()).find((item) => item.filename === filename);
|
|
1322
|
+
if (!video) return problem(404, "not_found", "No uploaded video has that name.");
|
|
1323
|
+
const usages = await videoUsages(context, video.url);
|
|
1324
|
+
if (usages.length > 0) return json(409, {
|
|
1325
|
+
error: "in_use",
|
|
1326
|
+
message: `${filename} is still used: ${usages.join("; ")}. Choose another video there and save, then delete it.`,
|
|
1327
|
+
usages
|
|
1328
|
+
});
|
|
1329
|
+
if (!await videos.remove(filename)) return problem(404, "not_found", "No uploaded video has that name.");
|
|
1330
|
+
return json(200, {
|
|
1331
|
+
deleted: true,
|
|
1332
|
+
filename
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
async function postVideo(request, context) {
|
|
1336
|
+
const videos = context.store.videos;
|
|
1337
|
+
if (!videos) return problem(404, "not_found", NO_VIDEOS);
|
|
1338
|
+
const limitMb = Math.round(context.maxVideoBytes / 1048576);
|
|
1339
|
+
const tooLarge = () => problem(413, "too_large", `Videos must be ${limitMb}MB or smaller. Link a hosted video instead, or compress this one.`);
|
|
1340
|
+
const length = contentLength(request);
|
|
1341
|
+
if (length !== null && length > videoBodyLimit(context.maxVideoBytes)) return tooLarge();
|
|
1342
|
+
if (!(request.headers.get("content-type") ?? "").toLowerCase().includes("multipart/form-data")) return problem(415, "unsupported_media_type", "Upload the video as multipart/form-data.");
|
|
1343
|
+
let form;
|
|
1344
|
+
try {
|
|
1345
|
+
form = await request.formData();
|
|
1346
|
+
} catch {
|
|
1347
|
+
return problem(400, "invalid_upload", "The upload body could not be read.");
|
|
1348
|
+
}
|
|
1349
|
+
const entry = form.get("file");
|
|
1350
|
+
if (!(entry instanceof File)) return problem(400, "invalid_upload", "Expected a multipart file field named \"file\".");
|
|
1351
|
+
if (entry.size > context.maxVideoBytes) return tooLarge();
|
|
1352
|
+
const bytes = new Uint8Array(await entry.arrayBuffer());
|
|
1353
|
+
if (bytes.byteLength > context.maxVideoBytes) return tooLarge();
|
|
1354
|
+
const declared = videoKindFromMime(entry.type);
|
|
1355
|
+
const sniffed = sniffVideo(bytes);
|
|
1356
|
+
if (declared === null || sniffed === null || declared !== sniffed) return problem(415, "unsupported_media_type", "Upload an mp4, webm, mov or ogv video whose contents match the declared type.");
|
|
1357
|
+
try {
|
|
1358
|
+
return json(201, await videos.save({
|
|
1359
|
+
originalName: entry.name,
|
|
1360
|
+
bytes,
|
|
1361
|
+
kind: sniffed
|
|
1362
|
+
}));
|
|
1363
|
+
} catch (error) {
|
|
1364
|
+
return problem(500, "write_failed", error instanceof Error ? error.message : "Upload failed.");
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1152
1367
|
async function postImage(request, context) {
|
|
1153
1368
|
const length = contentLength(request);
|
|
1154
1369
|
if (length !== null && length > 5505024) return problem(413, "too_large", "Upload exceeds the 5MB image limit.");
|
|
@@ -1450,10 +1665,12 @@ async function dispatch(req, res, context, rawUrl, hooks) {
|
|
|
1450
1665
|
const headers = headersFrom(req);
|
|
1451
1666
|
const method = (req.method ?? "GET").toUpperCase();
|
|
1452
1667
|
const multipart = (headers.get("content-type") ?? "").toLowerCase().includes("multipart/form-data");
|
|
1453
|
-
const
|
|
1668
|
+
const videoUpload = multipart && stripTrailingSlash(rawUrl.split("?")[0] ?? "") === `/__bearminion/api/videos`;
|
|
1669
|
+
const limit = videoUpload ? videoBodyLimit(context.maxVideoBytes) : multipart ? MULTIPART_BODY_LIMIT : JSON_BODY_LIMIT;
|
|
1670
|
+
const tooLargeMessage = videoUpload ? "Upload exceeds the video size limit." : multipart ? "Upload exceeds the 5MB image limit." : "JSON body exceeds 1MB.";
|
|
1454
1671
|
const advertised = Number(headers.get("content-length") ?? "");
|
|
1455
1672
|
if (Number.isFinite(advertised) && advertised > limit) {
|
|
1456
|
-
await send(res, jsonProblem(413, "too_large",
|
|
1673
|
+
await send(res, jsonProblem(413, "too_large", tooLargeMessage));
|
|
1457
1674
|
return;
|
|
1458
1675
|
}
|
|
1459
1676
|
let body;
|
|
@@ -1461,7 +1678,7 @@ async function dispatch(req, res, context, rawUrl, hooks) {
|
|
|
1461
1678
|
body = await readBody(req, limit);
|
|
1462
1679
|
} catch (error) {
|
|
1463
1680
|
if (error instanceof PayloadTooLargeError) {
|
|
1464
|
-
await send(res, jsonProblem(413, "too_large",
|
|
1681
|
+
await send(res, jsonProblem(413, "too_large", tooLargeMessage));
|
|
1465
1682
|
return;
|
|
1466
1683
|
}
|
|
1467
1684
|
throw error;
|
|
@@ -1627,7 +1844,11 @@ function bearminion(options) {
|
|
|
1627
1844
|
if (command !== "dev") return;
|
|
1628
1845
|
const context = resolveEditorContext(options, rootInput(config.root));
|
|
1629
1846
|
updateConfig({ vite: {
|
|
1630
|
-
server: { watch: { ignored: [
|
|
1847
|
+
server: { watch: { ignored: [
|
|
1848
|
+
`${context.uploadsDir.replaceAll("\\", "/")}/**`,
|
|
1849
|
+
`${context.videosDir.replaceAll("\\", "/")}/**`,
|
|
1850
|
+
`${context.root.replaceAll("\\", "/")}/.editor/**`
|
|
1851
|
+
] } },
|
|
1631
1852
|
plugins: [contentReloadPlugin([context.contentDir]), editorClientPlugin()]
|
|
1632
1853
|
} });
|
|
1633
1854
|
injectScript("page", "import \"virtual:bearminion-editor\"");
|
package/dist/content.d.ts
CHANGED
package/dist/content.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as createPageSchema, r as createTolerantPageSchema } from "./page-
|
|
2
|
-
import { n as createMigrator } from "./migrate-
|
|
3
|
-
import { t as CONTENT_COLLECTIONS_MARKER } from "./reload-
|
|
1
|
+
import { n as createPageSchema, r as createTolerantPageSchema } from "./page-NOFVEqn1.js";
|
|
2
|
+
import { n as createMigrator } from "./migrate-b408EaLp.js";
|
|
3
|
+
import { t as CONTENT_COLLECTIONS_MARKER } from "./reload-D0vwI0OT.js";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
6
|
import path from "node:path";
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as brandLogoSchema, $t as createMigrator, A as SectionRegistry, At as wireframeIssues, B as FooterColumn, Bt as themeModes, C as jsonLdWarnings, Ct as SectionDefinition, D as upsertJsonLd, Dt as Wireframe, E as siteJsonLdIds, Et as WireRect, F as defineTheme, Ft as NumberOption, G as SiteAnalytics, Gt as FieldIcons, H as Header, Ht as tones, I as BRAND_LOGO_HEIGHT, It as Option, J as SiteDefinition, Jt as ContentMigration, K as SiteConfig, Kt as ShowCondition, L as DEFAULT_MAX_FOOTER_COLUMNS, Lt as StringOption, M as ThemeDefinition, Mt as BooleanOption, N as ThemeFont, Nt as CountRule, O as API_PREFIX, Ot as defineWireframe, P as defaultTheme, Pt as EnumOption, Q as Theme, Qt as SCHEMA_VERSION_KEY, R as DEFAULT_MAX_HEADER_ACTIONS, Rt as buttonStyles, S as jsonLdSchema, St as SectionConfig, T as parseJsonLdInput, Tt as WireKind, U as HeaderConfig, Ut as variantOption, V as FooterConfig, Vt as toneOption, W as Site, Wt as FieldChoices, X as SiteSeo, Xt as ContentVersioning, Y as SiteOf, Yt as ContentMigrator, Z as SiteWith, Zt as DocumentKind, _ as SiteJsonLdSource, _t as textMarkSchema, a as isSystemPage, at as themeSchema, b as hasJsonLdId, bt as trackingIdSchema, c as BaseSection, ct as Link, d as anchorSchema, dt as Text, et as defineSite, f as createSectionSchema, ft as Tone, g as SiteJsonLdIds, gt as richTextSchema, h as JsonLdResult, ht as linkSchema, i as createPageSchema, it as themeColorsSchema, j as ThemeConfig, jt as OptionalContent, k as ContentModel, kt as wireKinds, l as PropsOf, lt as MAX_NAV_DEPTH, m as JsonLdBlock, mt as imageSchema, n as SYSTEM_PAGES, nt as siteConsentSchema, o as seoSchema, ot as ButtonStyle, p as sectionIdSchema, pt as htmlIdSchema, q as SiteConsent, qt as ShowWhen, r as Seo, rt as siteSeoSchema, s as withoutOrphanSections, st as Image, t as Page, tt as siteAnalyticsSchema, u as SectionOf, ut as RichText, v as formatJsonLd, vt as textSchema, w as mergeJsonLd, wt as defineSection, x as isPlaceholderSiteUrl, xt as withEmphasis, y as generateSiteJsonLd, yt as toneSchema, z as Footer, zt as motionOption } from "./page-BFuPEq6Q.js";
|
|
2
2
|
import { _ as renderMarkedHtml, a as WeightMark, f as markClass, g as reconcileMarks, i as TextRun, l as fontMarks, m as marksFromEmphasis, o as applyTextMark, r as TextMark, t as FontMark, v as splitMarkedText, y as weightMarks } from "./marks-BzLa48Y9.js";
|
|
3
|
-
import "./options-
|
|
3
|
+
import "./options-DdCyRbPs.js";
|
|
4
4
|
//#region src/errors.d.ts
|
|
5
5
|
/** One schema problem, addressed by a dot path such as `sections.0.props.heading`. */
|
|
6
6
|
interface ContentIssue {
|
|
@@ -70,6 +70,29 @@ interface ThemePreview<TModel extends ContentModel = ContentModel> {
|
|
|
70
70
|
*/
|
|
71
71
|
export declare function defineThemePreview<TModel extends ContentModel = ContentModel>(preview: ThemePreview<TModel>): ThemePreview<TModel>;
|
|
72
72
|
//#endregion
|
|
73
|
+
//#region src/media-source.d.ts
|
|
74
|
+
/**
|
|
75
|
+
* What a Media section's video link points at. YouTube and Vimeo links become privacy-friendly embeds
|
|
76
|
+
* (youtube-nocookie.com, Vimeo with Do Not Track) that load only after the visitor presses play. A link to a video file
|
|
77
|
+
* plays in a `<video>` element.
|
|
78
|
+
*/
|
|
79
|
+
type MediaSource = {
|
|
80
|
+
kind: 'youtube';
|
|
81
|
+
id: string;
|
|
82
|
+
embed: string;
|
|
83
|
+
thumbnail: string;
|
|
84
|
+
} | {
|
|
85
|
+
kind: 'vimeo';
|
|
86
|
+
id: string;
|
|
87
|
+
embed: string;
|
|
88
|
+
} | {
|
|
89
|
+
kind: 'file';
|
|
90
|
+
src: string;
|
|
91
|
+
};
|
|
92
|
+
/** A path in `public/` (`/videos/demo.webm`) or an `https://` link to a video file. */
|
|
93
|
+
export declare const VIDEO_FILE: RegExp;
|
|
94
|
+
export declare function parseMediaSource(value: string): MediaSource | null;
|
|
95
|
+
//#endregion
|
|
73
96
|
//#region src/git.d.ts
|
|
74
97
|
type ChangeStatus = 'added' | 'modified' | 'deleted' | 'renamed' | 'untracked';
|
|
75
98
|
interface ChangedFile {
|
|
@@ -120,6 +143,15 @@ interface UploadedImage {
|
|
|
120
143
|
path: string;
|
|
121
144
|
bytes?: number;
|
|
122
145
|
}
|
|
146
|
+
/** `GET /videos` entries and the body of a successful `POST /videos`. */
|
|
147
|
+
interface UploadedVideo {
|
|
148
|
+
filename: string;
|
|
149
|
+
/** Project-relative path of the file. */
|
|
150
|
+
path: string;
|
|
151
|
+
/** What a Media section's `video` field holds, such as `/videos/demo-1a2b3c4d.mp4`. */
|
|
152
|
+
url: string;
|
|
153
|
+
bytes?: number;
|
|
154
|
+
}
|
|
123
155
|
/** `GET /history/site` and `GET /history/pages/:slug` entries. */
|
|
124
156
|
interface HistoryEntry {
|
|
125
157
|
id: string;
|
|
@@ -162,26 +194,6 @@ interface SitePreview {
|
|
|
162
194
|
footer: string;
|
|
163
195
|
}
|
|
164
196
|
//#endregion
|
|
165
|
-
//#region src/json-ld.d.ts
|
|
166
|
-
/** JSON-LD blocks as a page stores them: parsed, so diffs stay readable and output can be escaped safely. */
|
|
167
|
-
type JsonLdBlock = Record<string, unknown>;
|
|
168
|
-
type JsonLdResult = {
|
|
169
|
-
ok: true;
|
|
170
|
-
value: JsonLdBlock[];
|
|
171
|
-
} | {
|
|
172
|
-
ok: false;
|
|
173
|
-
error: string;
|
|
174
|
-
};
|
|
175
|
-
/**
|
|
176
|
-
* Parses what a person pastes into the JSON-LD box: an object, an array of objects, or either one inside a
|
|
177
|
-
* `<script type="application/ld+json">` tag. Empty input is valid and means "no custom JSON-LD".
|
|
178
|
-
*/
|
|
179
|
-
export declare function parseJsonLdInput(raw: string): JsonLdResult;
|
|
180
|
-
/** The text shown in the editor box: one block as an object, several as an array, none as empty. */
|
|
181
|
-
export declare function formatJsonLd(blocks: readonly JsonLdBlock[] | undefined): string;
|
|
182
|
-
/** Advice, not errors: a block without `@type` or `@context` is usually a paste mistake, but Google decides what it accepts. */
|
|
183
|
-
export declare function jsonLdWarnings(blocks: readonly JsonLdBlock[]): string[];
|
|
184
|
-
//#endregion
|
|
185
197
|
//#region src/consent.d.ts
|
|
186
198
|
/** Longest copy the consent banner takes, in characters. The editor shows the same limits. */
|
|
187
199
|
export declare const CONSENT_LIMITS: {
|
|
@@ -292,4 +304,4 @@ interface ContrastCheck {
|
|
|
292
304
|
/** Text pairs a visitor reads: text on its background, label on the accent fill, link on its background. */
|
|
293
305
|
export declare function contrastChecks(colors: ThemeColors | undefined): ContrastCheck[];
|
|
294
306
|
//#endregion
|
|
295
|
-
export { API_PREFIX, type ApiIssue, type ApiProblem, BRAND_LOGO_HEIGHT, type BaseSection, type BooleanOption, type ButtonStyle, type ChangedFile, type CommitRequest, type CommitResult, type ConsentCopy, type ConsentReopen, type ConsentText, type ConsentTextKey, type ContentIssue, type ContentMigration, type ContentMigrator, type ContentModel, type ContentVersioning, type ContrastCheck, type CountRule, type CreatePageRequest, DEFAULT_MAX_FOOTER_COLUMNS, DEFAULT_MAX_HEADER_ACTIONS, type DocumentKind, type EnumOption, type FieldChoices, type FieldIcons, type FontMark, type Footer, type FooterColumn, type FooterConfig, type GitInfo, type Header, type HeaderConfig, type HistoryEntry, type Image, type ImageFit, type ImagePosition, type JsonLdBlock, type JsonLdResult, type Link, MAX_NAV_DEPTH, type NumberOption, type Option, type OptionalContent, type Page, type PageListItem, type PropsOf, type RichText, SCHEMA_VERSION_KEY, SYSTEM_PAGES, type SaveRequest, type SaveResponse, type SectionConfig, type SectionDefinition, type SectionOf, type SectionRegistry, type Seo, type ShowCondition, type ShowWhen, type Site, type SiteAnalytics, type SiteConfig, type SiteConsent, type SiteDefinition, type SiteOf, type SitePreview, type SitePreviewHtml, type SiteSeo, type SiteWith, type StringOption, type Text, type TextMark, type TextRun, type Theme, type ThemeColors, type ThemeConfig, type ThemeDefinition, type ThemeFont, type ThemePreview, type Tone, type UploadedImage, type WeightMark, type WireKind, type WireRect, type Wireframe, anchorSchema, applyTextMark, brandLogoSchema, buttonStyles, createMigrator, createPageSchema, createSectionSchema, defaultTheme, defineSection, defineSite, defineTheme, defineWireframe, fontMarks, htmlIdSchema, imageSchema, isSystemPage, jsonLdSchema, linkSchema, markClass, marksFromEmphasis, motionOption, reconcileMarks, renderMarkedHtml, richTextSchema, sectionIdSchema, seoSchema, siteAnalyticsSchema, siteConsentSchema, siteSeoSchema, splitMarkedText, textMarkSchema, textSchema, themeColorsSchema, themeModes, themeSchema, toneOption, toneSchema, tones, trackingIdSchema, variantOption, weightMarks, wireKinds, wireframeIssues, withEmphasis, withoutOrphanSections };
|
|
307
|
+
export { API_PREFIX, type ApiIssue, type ApiProblem, BRAND_LOGO_HEIGHT, type BaseSection, type BooleanOption, type ButtonStyle, type ChangedFile, type CommitRequest, type CommitResult, type ConsentCopy, type ConsentReopen, type ConsentText, type ConsentTextKey, type ContentIssue, type ContentMigration, type ContentMigrator, type ContentModel, type ContentVersioning, type ContrastCheck, type CountRule, type CreatePageRequest, DEFAULT_MAX_FOOTER_COLUMNS, DEFAULT_MAX_HEADER_ACTIONS, type DocumentKind, type EnumOption, type FieldChoices, type FieldIcons, type FontMark, type Footer, type FooterColumn, type FooterConfig, type GitInfo, type Header, type HeaderConfig, type HistoryEntry, type Image, type ImageFit, type ImagePosition, type JsonLdBlock, type JsonLdResult, type Link, MAX_NAV_DEPTH, type MediaSource, type NumberOption, type Option, type OptionalContent, type Page, type PageListItem, type PropsOf, type RichText, SCHEMA_VERSION_KEY, SYSTEM_PAGES, type SaveRequest, type SaveResponse, type SectionConfig, type SectionDefinition, type SectionOf, type SectionRegistry, type Seo, type ShowCondition, type ShowWhen, type Site, type SiteAnalytics, type SiteConfig, type SiteConsent, type SiteDefinition, type SiteJsonLdIds, type SiteJsonLdSource, type SiteOf, type SitePreview, type SitePreviewHtml, type SiteSeo, type SiteWith, type StringOption, type Text, type TextMark, type TextRun, type Theme, type ThemeColors, type ThemeConfig, type ThemeDefinition, type ThemeFont, type ThemePreview, type Tone, type UploadedImage, type UploadedVideo, type WeightMark, type WireKind, type WireRect, type Wireframe, anchorSchema, applyTextMark, brandLogoSchema, buttonStyles, createMigrator, createPageSchema, createSectionSchema, defaultTheme, defineSection, defineSite, defineTheme, defineWireframe, fontMarks, formatJsonLd, generateSiteJsonLd, hasJsonLdId, htmlIdSchema, imageSchema, isPlaceholderSiteUrl, isSystemPage, jsonLdSchema, jsonLdWarnings, linkSchema, markClass, marksFromEmphasis, mergeJsonLd, motionOption, parseJsonLdInput, reconcileMarks, renderMarkedHtml, richTextSchema, sectionIdSchema, seoSchema, siteAnalyticsSchema, siteConsentSchema, siteJsonLdIds, siteSeoSchema, splitMarkedText, textMarkSchema, textSchema, themeColorsSchema, themeModes, themeSchema, toneOption, toneSchema, tones, trackingIdSchema, upsertJsonLd, variantOption, weightMarks, wireKinds, wireframeIssues, withEmphasis, withoutOrphanSections };
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { c as ContentReadError, i as enumFields, l as ContentValidationError, n as defineThemePreview, o as schemaProps, s as ContentError, u as formatContentIssues } from "./preview
|
|
2
|
-
import { n as defineTheme, t as defaultTheme } from "./theme-
|
|
3
|
-
import { A as
|
|
1
|
+
import { c as ContentReadError, i as enumFields, l as ContentValidationError, n as defineThemePreview, o as schemaProps, s as ContentError, u as formatContentIssues } from "./preview--S1QUrMM.js";
|
|
2
|
+
import { n as defineTheme, t as defaultTheme } from "./theme-DFjHjgFO.js";
|
|
3
|
+
import { A as textSchema, C as MAX_NAV_DEPTH, D as linkSchema, E as imageSchema, F as DEFAULT_IMAGE_POSITION, G as tones, H as motionOption, I as IMAGE_FITS, K as variantOption, L as IMAGE_POSITIONS, M as trackingIdSchema, N as withEmphasis, O as richTextSchema, P as DEFAULT_IMAGE_FIT, R as isImageFit, S as upsertJsonLd, T as htmlIdSchema, U as themeModes, V as buttonStyles, W as toneOption, _ as jsonLdSchema, a as seoSchema, b as parseJsonLdInput, c as anchorSchema, d as sectionIdSchema, f as walkLinkIds, g as isPlaceholderSiteUrl, h as hasJsonLdId, i as isSystemPage, j as toneSchema, k as textMarkSchema, l as createSectionSchema, m as generateSiteJsonLd, n as createPageSchema, p as formatJsonLd, s as withoutOrphanSections, t as SYSTEM_PAGES, v as jsonLdWarnings, w as addMarkIssues, x as siteJsonLdIds, y as mergeJsonLd, z as isImagePosition } from "./page-NOFVEqn1.js";
|
|
4
4
|
import { applyTextMark, fontMarks, markClass, marksFromEmphasis, reconcileMarks, renderMarkedHtml, splitMarkedText, weightMarks } from "./marks.js";
|
|
5
|
-
import { n as createMigrator, r as API_PREFIX, t as SCHEMA_VERSION_KEY } from "./migrate-
|
|
6
|
-
import { _ as consentCopy, a as contrastRatio, b as
|
|
5
|
+
import { n as createMigrator, r as API_PREFIX, t as SCHEMA_VERSION_KEY } from "./migrate-b408EaLp.js";
|
|
6
|
+
import { _ as consentCopy, a as contrastRatio, b as parseMediaSource, c as hasThemeColors, d as tokenToHex, f as CONSENT_COPY, g as CONSENT_REOPEN, h as CONSENT_LIMITS, i as contrastChecks, l as hexToToken, m as CONSENT_HREF_MESSAGE, n as HEX_COLOR, o as derivePalette, p as CONSENT_HREF, s as hasOverrides, u as themeCss, v as consentTextIssues, y as VIDEO_FILE } from "./theme-colors-CHROGZkI.js";
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
//#region src/wireframe.ts
|
|
9
9
|
/**
|
|
@@ -239,7 +239,9 @@ const siteSeoSchema = z.object({
|
|
|
239
239
|
/** `%s` is replaced with the page SEO title. */
|
|
240
240
|
titleTemplate: z.string().min(1).optional(),
|
|
241
241
|
description: z.string().optional(),
|
|
242
|
-
ogImage: z.string().min(1).optional()
|
|
242
|
+
ogImage: z.string().min(1).optional(),
|
|
243
|
+
/** Structured data emitted on every page, such as `Organization` and `WebSite`. A page's own blocks add to it. */
|
|
244
|
+
jsonLd: jsonLdSchema.optional()
|
|
243
245
|
}).strict();
|
|
244
246
|
/**
|
|
245
247
|
* Raw HTML snippets for analytics and similar tags. A theme renders `head` at the end of `<head>` and
|
|
@@ -439,4 +441,4 @@ function defineSite(config) {
|
|
|
439
441
|
};
|
|
440
442
|
}
|
|
441
443
|
//#endregion
|
|
442
|
-
export { API_PREFIX, BRAND_LOGO_HEIGHT, CONSENT_COPY, CONSENT_HREF, CONSENT_LIMITS, CONSENT_REOPEN, ContentError, ContentReadError, ContentValidationError, DEFAULT_IMAGE_FIT, DEFAULT_IMAGE_POSITION, DEFAULT_MAX_FOOTER_COLUMNS, DEFAULT_MAX_HEADER_ACTIONS, HEX_COLOR, IMAGE_FITS, IMAGE_POSITIONS, MAX_NAV_DEPTH, SCHEMA_VERSION_KEY, SYSTEM_PAGES, anchorSchema, applyTextMark, brandLogoSchema, buttonStyles, consentCopy, consentTextIssues, contrastChecks, contrastRatio, createMigrator, createPageSchema, createSectionSchema, defaultTheme, defineSection, defineSite, defineTheme, defineThemePreview, defineWireframe, derivePalette, fontMarks, formatJsonLd, hasOverrides, hasThemeColors, hexToToken, htmlIdSchema, imageSchema, isImageFit, isImagePosition, isSystemPage, jsonLdSchema, jsonLdWarnings, linkSchema, markClass, marksFromEmphasis, motionOption, parseJsonLdInput, reconcileMarks, renderMarkedHtml, richTextSchema, sectionIdSchema, seoSchema, siteAnalyticsSchema, siteConsentSchema, siteSeoSchema, splitMarkedText, textMarkSchema, textSchema, themeColorsSchema, themeCss, themeModes, themeSchema, tokenToHex, toneOption, toneSchema, tones, trackingIdSchema, variantOption, weightMarks, wireKinds, wireframeIssues, withEmphasis, withoutOrphanSections };
|
|
444
|
+
export { API_PREFIX, BRAND_LOGO_HEIGHT, CONSENT_COPY, CONSENT_HREF, CONSENT_LIMITS, CONSENT_REOPEN, ContentError, ContentReadError, ContentValidationError, DEFAULT_IMAGE_FIT, DEFAULT_IMAGE_POSITION, DEFAULT_MAX_FOOTER_COLUMNS, DEFAULT_MAX_HEADER_ACTIONS, HEX_COLOR, IMAGE_FITS, IMAGE_POSITIONS, MAX_NAV_DEPTH, SCHEMA_VERSION_KEY, SYSTEM_PAGES, VIDEO_FILE, anchorSchema, applyTextMark, brandLogoSchema, buttonStyles, consentCopy, consentTextIssues, contrastChecks, contrastRatio, createMigrator, createPageSchema, createSectionSchema, defaultTheme, defineSection, defineSite, defineTheme, defineThemePreview, defineWireframe, derivePalette, fontMarks, formatJsonLd, generateSiteJsonLd, hasJsonLdId, hasOverrides, hasThemeColors, hexToToken, htmlIdSchema, imageSchema, isImageFit, isImagePosition, isPlaceholderSiteUrl, isSystemPage, jsonLdSchema, jsonLdWarnings, linkSchema, markClass, marksFromEmphasis, mergeJsonLd, motionOption, parseJsonLdInput, parseMediaSource, reconcileMarks, renderMarkedHtml, richTextSchema, sectionIdSchema, seoSchema, siteAnalyticsSchema, siteConsentSchema, siteJsonLdIds, siteSeoSchema, splitMarkedText, textMarkSchema, textSchema, themeColorsSchema, themeCss, themeModes, themeSchema, tokenToHex, toneOption, toneSchema, tones, trackingIdSchema, upsertJsonLd, variantOption, weightMarks, wireKinds, wireframeIssues, withEmphasis, withoutOrphanSections };
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
const API_PREFIX = "/__bearminion/api";
|
|
4
4
|
const DEFAULT_CONTENT_DIR = "content";
|
|
5
5
|
const DEFAULT_UPLOADS_DIR = "src/assets/uploads";
|
|
6
|
+
/** Videos are served as they are, so they live in `public/`. */
|
|
7
|
+
const DEFAULT_VIDEOS_DIR = "public/videos";
|
|
6
8
|
//#endregion
|
|
7
9
|
//#region src/migrate.ts
|
|
8
10
|
/** The key that stores a document's version. It is stripped on read and stamped on write; schemas never see it. */
|
|
@@ -56,4 +58,4 @@ function createMigrator(versioning = {}) {
|
|
|
56
58
|
};
|
|
57
59
|
}
|
|
58
60
|
//#endregion
|
|
59
|
-
export { DEFAULT_UPLOADS_DIR as a, DEFAULT_CONTENT_DIR as i, createMigrator as n, API_PREFIX as r, SCHEMA_VERSION_KEY as t };
|
|
61
|
+
export { DEFAULT_UPLOADS_DIR as a, DEFAULT_CONTENT_DIR as i, createMigrator as n, DEFAULT_VIDEOS_DIR as o, API_PREFIX as r, SCHEMA_VERSION_KEY as t };
|