@rigel-cms/rigel 0.0.0-stage → 0.1.0-beta.1

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 (117) hide show
  1. package/LICENSE +203 -0
  2. package/README.md +8 -2
  3. package/package.json +78 -4
  4. package/src/article-body.ts +60 -0
  5. package/src/article-render.ts +87 -0
  6. package/src/config.ts +97 -0
  7. package/src/content.ts +5 -0
  8. package/src/editor/App.svelte +972 -0
  9. package/src/editor/assets/rigelcms-icon.png +0 -0
  10. package/src/editor/assets/sidebar/LICENSE +21 -0
  11. package/src/editor/assets/sidebar/book-open.svg +1 -0
  12. package/src/editor/assets/sidebar/calendar.svg +1 -0
  13. package/src/editor/assets/sidebar/chart-bar.svg +1 -0
  14. package/src/editor/assets/sidebar/file-text.svg +1 -0
  15. package/src/editor/assets/sidebar/files.svg +1 -0
  16. package/src/editor/assets/sidebar/folder.svg +1 -0
  17. package/src/editor/assets/sidebar/folders.svg +1 -0
  18. package/src/editor/assets/sidebar/gear.svg +1 -0
  19. package/src/editor/assets/sidebar/globe.svg +1 -0
  20. package/src/editor/assets/sidebar/heart.svg +1 -0
  21. package/src/editor/assets/sidebar/house.svg +1 -0
  22. package/src/editor/assets/sidebar/images.svg +1 -0
  23. package/src/editor/assets/sidebar/layout.svg +1 -0
  24. package/src/editor/assets/sidebar/list.svg +1 -0
  25. package/src/editor/assets/sidebar/magnifying-glass.svg +1 -0
  26. package/src/editor/assets/sidebar/megaphone.svg +1 -0
  27. package/src/editor/assets/sidebar/newspaper.svg +1 -0
  28. package/src/editor/assets/sidebar/pencil-simple.svg +1 -0
  29. package/src/editor/assets/sidebar/shopping-bag.svg +1 -0
  30. package/src/editor/assets/sidebar/sidebar-simple.svg +1 -0
  31. package/src/editor/assets/sidebar/square-half.svg +1 -0
  32. package/src/editor/assets/sidebar/squares-four.svg +1 -0
  33. package/src/editor/assets/sidebar/tag.svg +1 -0
  34. package/src/editor/assets/sidebar/user.svg +1 -0
  35. package/src/editor/assets/sidebar/users.svg +1 -0
  36. package/src/editor/blocks/bulleted-list.ts +2 -0
  37. package/src/editor/blocks/code.ts +1 -0
  38. package/src/editor/blocks/divider.ts +14 -0
  39. package/src/editor/blocks/h1.ts +2 -0
  40. package/src/editor/blocks/h2.ts +2 -0
  41. package/src/editor/blocks/h3.ts +2 -0
  42. package/src/editor/blocks/h4.ts +2 -0
  43. package/src/editor/blocks/h5.ts +2 -0
  44. package/src/editor/blocks/h6.ts +2 -0
  45. package/src/editor/blocks/image.ts +34 -0
  46. package/src/editor/blocks/index.ts +40 -0
  47. package/src/editor/blocks/numbered-list.ts +2 -0
  48. package/src/editor/blocks/paragraph.ts +1 -0
  49. package/src/editor/blocks/quote.ts +1 -0
  50. package/src/editor/blocks/table.ts +5 -0
  51. package/src/editor/lib/ActionDialog.svelte +51 -0
  52. package/src/editor/lib/BlockEditor.svelte +614 -0
  53. package/src/editor/lib/CollectionPanel.svelte +103 -0
  54. package/src/editor/lib/DatePicker.svelte +106 -0
  55. package/src/editor/lib/Field.svelte +80 -0
  56. package/src/editor/lib/Icon.svelte +34 -0
  57. package/src/editor/lib/ImagePicker.svelte +264 -0
  58. package/src/editor/lib/MarkdownEditor.svelte +206 -0
  59. package/src/editor/lib/MenuEditor.svelte +209 -0
  60. package/src/editor/lib/PropertiesTabs.svelte +15 -0
  61. package/src/editor/lib/ReferenceSelect.svelte +17 -0
  62. package/src/editor/lib/SchemaField.svelte +156 -0
  63. package/src/editor/lib/SchemaPanel.svelte +189 -0
  64. package/src/editor/lib/Select.svelte +138 -0
  65. package/src/editor/lib/SeoGuidance.svelte +21 -0
  66. package/src/editor/lib/Settings.svelte +183 -0
  67. package/src/editor/lib/Shortcuts.svelte +29 -0
  68. package/src/editor/lib/SidebarIcon.svelte +8 -0
  69. package/src/editor/lib/SitePreview.svelte +50 -0
  70. package/src/editor/lib/TopBar.svelte +165 -0
  71. package/src/editor/lib/UnsavedChanges.svelte +38 -0
  72. package/src/editor/lib/VisualEditor.svelte +494 -0
  73. package/src/editor/lib/calendar.ts +55 -0
  74. package/src/editor/lib/github-auth.ts +117 -0
  75. package/src/editor/lib/github-backend.ts +263 -0
  76. package/src/editor/lib/icons.ts +111 -0
  77. package/src/editor/lib/images.ts +195 -0
  78. package/src/editor/lib/markdown.ts +71 -0
  79. package/src/editor/lib/media-library.ts +39 -0
  80. package/src/editor/lib/menu-tree.ts +37 -0
  81. package/src/editor/lib/rich-markdown.ts +30 -0
  82. package/src/editor/lib/schema-edit.ts +110 -0
  83. package/src/editor/lib/settings.ts +103 -0
  84. package/src/editor/lib/slugify.ts +10 -0
  85. package/src/editor/lib/storage.ts +17 -0
  86. package/src/editor/lib/store.svelte.ts +33 -0
  87. package/src/editor/main.ts +9 -0
  88. package/src/editor/styles.css +1280 -0
  89. package/src/env.d.ts +22 -0
  90. package/src/index.ts +128 -0
  91. package/src/manifest/frontmatter.ts +56 -0
  92. package/src/manifest/loader.ts +14 -0
  93. package/src/manifest/menu.ts +46 -0
  94. package/src/manifest/navigation.ts +94 -0
  95. package/src/manifest/references.ts +39 -0
  96. package/src/manifest/seo.ts +15 -0
  97. package/src/manifest/types.ts +75 -0
  98. package/src/media-types.ts +21 -0
  99. package/src/preview-urls.ts +3 -0
  100. package/src/preview.ts +167 -0
  101. package/src/routes/local-api.ts +42 -0
  102. package/src/routes/local-documents.ts +83 -0
  103. package/src/routes/local-media.ts +124 -0
  104. package/src/routes/local-menu.ts +81 -0
  105. package/src/routes/local-post.ts +80 -0
  106. package/src/routes/manifest.json.ts +15 -0
  107. package/src/routes/media-files.ts +126 -0
  108. package/src/routes/media-validation.ts +37 -0
  109. package/src/routes/media.json.ts +12 -0
  110. package/src/routes/rigel-page.astro +25 -0
  111. package/src/routes/serialize.ts +139 -0
  112. package/src/schema/docs.ts +33 -0
  113. package/src/schema/paths.ts +136 -0
  114. package/src/schema/store.ts +141 -0
  115. package/src/schema/types.ts +81 -0
  116. package/src/schema/validate.ts +327 -0
  117. package/src/vite/manifest-plugin.ts +180 -0
@@ -0,0 +1,263 @@
1
+ import type { Manifest } from "../../manifest/types";
2
+ import type { Selection } from "../../manifest/navigation";
3
+ import { validateBody, type ArticleBody } from "../../article-body";
4
+ import { validateMenu } from "../../manifest/menu";
5
+ import { validateCollectionReferences } from "../../manifest/references";
6
+ import { documentIndex, isPlainObject, validateDocument, validateDocumentId, validateSchema } from "../../schema/validate";
7
+ import { applyValues, readValues } from "../../routes/serialize";
8
+
9
+ export interface GitContext { token: string; repo: string; branch: string }
10
+
11
+ type FileSnapshot = { sha: string; text: string };
12
+ type PendingWrite = { sha: string; text: string | null; creating: boolean };
13
+ type Snapshot = { target: string; files: Map<string, FileSnapshot>; paths: Set<string>; pending: Map<string, PendingWrite> };
14
+ const REMOTE_SNAPSHOT = Symbol("rigel-remote-snapshot");
15
+ const SHA = /^[a-f0-9]{40}$/;
16
+ const SLUG = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
17
+ const POST_SLUG = /^[a-z0-9][a-z0-9_-]*$/;
18
+ const MAX_BYTES = 1024 * 1024;
19
+
20
+ class GitError extends Error {
21
+ constructor(message: string, public status: number) { super(message); }
22
+ }
23
+
24
+ function safePath(path: string, empty = false): string {
25
+ if (typeof path !== "string" || (!path && !empty) || (path && !path.split("/").every(part => /^[A-Za-z0-9_-][A-Za-z0-9_. -]*$/.test(part)))) throw new Error("Unsafe GitHub content path");
26
+ return path;
27
+ }
28
+
29
+ function branch(name: string): string {
30
+ if (typeof name !== "string" || !/^[A-Za-z0-9_-][A-Za-z0-9_./-]*$/.test(name) || name.includes("..") || name.includes("//") || name.endsWith("/") || name.endsWith(".") || name.split("/").some(part => part.startsWith(".") || part.endsWith(".lock"))) throw new Error("Invalid GitHub branch");
31
+ return name;
32
+ }
33
+
34
+ function config(ctx: GitContext, manifest: Manifest) {
35
+ const cfg = manifest.github;
36
+ if (!cfg || !/^[A-Za-z0-9_-]+\/[A-Za-z0-9_.-]+$/.test(ctx.repo) || ctx.repo !== cfg.repo || !ctx.token.trim()) throw new Error("Connect the configured GitHub repository first");
37
+ branch(ctx.branch); branch(cfg.publishBranch);
38
+ if (ctx.branch !== (cfg.branch || "drafts") || ctx.branch === cfg.publishBranch || ctx.branch === "main" || ctx.branch === "master") throw new Error("GitHub content changes require a dedicated drafts branch, never the publishing branch");
39
+ safePath(cfg.repoRoot, true); safePath(cfg.copyDir);
40
+ const folders = [cfg.copyDir, ...cfg.collections.map(c => c.folder), ...manifest.schema?.documents.map(d => d.folder) ?? [], ...(cfg.menus ? [cfg.menus.folder] : [])];
41
+ folders.forEach(folder => safePath(folder));
42
+ if (folders.some((folder, index) => folders.some((other, j) => j !== index && (folder === other || folder.startsWith(other + "/"))))) throw new Error("GitHub content directories must not overlap");
43
+ if (manifest.schema) validateSchema(manifest.schema);
44
+ return cfg;
45
+ }
46
+
47
+ function target(ctx: GitContext, manifest: Manifest) { return JSON.stringify([ctx.repo, ctx.branch, manifest.github, manifest.schema]); }
48
+ function filePath(ctx: GitContext, manifest: Manifest, folder: string, slug: string, extension = "json", pattern = SLUG) {
49
+ const cfg = config(ctx, manifest);
50
+ if (!pattern.test(slug)) throw new Error("Invalid content filename");
51
+ return [cfg.repoRoot, safePath(folder), `${slug}.${extension}`].filter(Boolean).join("/");
52
+ }
53
+
54
+ async function gh(ctx: GitContext, path: string, init: RequestInit = {}): Promise<any> {
55
+ let response: Response;
56
+ try {
57
+ response = await fetch(`https://api.github.com/repos/${ctx.repo}${path}`, {
58
+ ...init, headers: {Authorization: `Bearer ${ctx.token}`, Accept: "application/vnd.github+json", "X-GitHub-Api-Version":"2022-11-28", "Content-Type":"application/json"}, redirect:"error", cache:"no-store", credentials:"omit", signal:AbortSignal.timeout(15000),
59
+ });
60
+ } catch { throw new Error("GitHub request failed; check your connection and reconnect"); }
61
+ if (!response.ok) {
62
+ const rateLimited = response.status === 429 || response.headers.get("x-ratelimit-remaining") === "0" || response.headers.has("retry-after");
63
+ const action = rateLimited ? "GitHub rate limit reached; wait before retrying" : ({401:"GitHub session expired; reconnect",403:"GitHub access denied; check repository installation permissions",404:"GitHub repository, branch or file unavailable; check access and reload",409:"GitHub content conflict; reload before saving",422:"GitHub rejected this change; the path or branch may already exist; reload"} as Record<number,string>)[response.status] ?? "GitHub request failed; retry after checking repository access";
64
+ throw new GitError(`${action} (${response.status})`, response.status);
65
+ }
66
+ try { return await response.json(); } catch { throw new Error("Malformed GitHub API response"); }
67
+ }
68
+
69
+ function sha(value: unknown): string {
70
+ if (typeof value !== "string" || !SHA.test(value)) throw new Error("Malformed GitHub SHA");
71
+ return value;
72
+ }
73
+
74
+ async function draftHead(ctx: GitContext, manifest: Manifest): Promise<string> {
75
+ const cfg = config(ctx, manifest);
76
+ const ref = `/git/ref/heads/${encodeURIComponent(ctx.branch)}`;
77
+ try { return sha((await gh(ctx, ref))?.object?.sha); }
78
+ catch (error) { if (!(error instanceof GitError) || error.status !== 404) throw error; }
79
+ const base = sha((await gh(ctx, `/git/ref/heads/${encodeURIComponent(cfg.publishBranch)}`))?.object?.sha);
80
+ try { await gh(ctx, "/git/refs", {method:"POST", body:JSON.stringify({ref:`refs/heads/${ctx.branch}`,sha:base})}); }
81
+ catch (error) { if (!(error instanceof GitError) || ![409,422].includes(error.status)) throw error; }
82
+ return sha((await gh(ctx, ref))?.object?.sha);
83
+ }
84
+
85
+ function decode(payload: any): string {
86
+ if (!payload || payload.encoding !== "base64" || typeof payload.content !== "string") throw new Error("Malformed GitHub content payload");
87
+ const base64 = payload.content.replace(/[\r\n]/g, "");
88
+ if (base64.length > Math.ceil(MAX_BYTES / 3) * 4 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(base64)) throw new Error("Invalid or oversized GitHub content payload");
89
+ try {
90
+ const bytes = Uint8Array.from(atob(base64), char => char.charCodeAt(0));
91
+ if (bytes.length > MAX_BYTES) throw new Error();
92
+ return new TextDecoder("utf-8", {fatal:true}).decode(bytes);
93
+ } catch { throw new Error("Invalid UTF-8 GitHub content payload"); }
94
+ }
95
+
96
+ function encode(text: string): string {
97
+ const bytes = new TextEncoder().encode(text);
98
+ if (bytes.length > MAX_BYTES) throw new Error("Content exceeds the 1 MiB limit");
99
+ let binary = "";
100
+ for (const byte of bytes) binary += String.fromCharCode(byte);
101
+ return btoa(binary);
102
+ }
103
+
104
+ function parseJson(text: string): unknown {
105
+ try { return JSON.parse(text); } catch { throw new Error("Invalid remote content JSON"); }
106
+ }
107
+
108
+ export async function loadManifestRemote(ctx: GitContext, blueprint: Manifest): Promise<Manifest> {
109
+ const cfg = config(ctx, blueprint);
110
+ const head = await draftHead(ctx, blueprint);
111
+ const tree = await gh(ctx, `/git/trees/${head}?recursive=1`);
112
+ if (!tree || tree.truncated !== false || !Array.isArray(tree.tree)) throw new Error("Incomplete GitHub tree; content cannot be loaded safely");
113
+ const manifest = structuredClone(blueprint);
114
+ const snapshot: Snapshot = {target:target(ctx,manifest),files:new Map(),paths:new Set(),pending:new Map()};
115
+ const entries = new Map<string, any>();
116
+ for (const entry of tree.tree) {
117
+ if (!entry || typeof entry.path !== "string" || entry.path.split("/").some((part: string) => !part || part === "." || part === "..") || /[\\\u0000-\u001f]/.test(entry.path) || !["blob","tree","commit"].includes(entry.type) || !SHA.test(entry.sha) || !["100644","100755","120000","040000","160000"].includes(entry.mode)) throw new Error("Malformed GitHub tree entry");
118
+ if (entries.has(entry.path)) throw new Error("Duplicate GitHub tree path");
119
+ entries.set(entry.path, entry); snapshot.paths.add(entry.path);
120
+ }
121
+ const folders = [cfg.copyDir, ...cfg.collections.map(c => c.folder), ...(manifest.schema?.documents.map(d => d.folder) ?? []), ...(cfg.menus ? [cfg.menus.folder] : [])];
122
+ for (const folder of folders) {
123
+ const parts = [cfg.repoRoot,folder].filter(Boolean).join("/").split("/");
124
+ for (let count = 1; count <= parts.length; count++) {
125
+ const entry = entries.get(parts.slice(0,count).join("/"));
126
+ if (entry && (entry.type !== "tree" || entry.mode !== "040000")) throw new Error("Content directory must be a real GitHub directory");
127
+ }
128
+ }
129
+ const read = async (path: string): Promise<string> => {
130
+ const entry = entries.get(path);
131
+ if (!entry || entry.type !== "blob" || !["100644","100755"].includes(entry.mode)) throw new Error("Content must be an existing regular GitHub file");
132
+ const expected = sha(entry.sha);
133
+ const blob = await gh(ctx, `/git/blobs/${expected}`);
134
+ if (sha(blob?.sha) !== expected) throw new Error("GitHub blob SHA mismatch");
135
+ const text = decode(blob); snapshot.files.set(path,{sha:expected,text}); return text;
136
+ };
137
+ const list = (folder: string, pattern = SLUG) => {
138
+ const prefix = [cfg.repoRoot,folder].filter(Boolean).join("/") + "/";
139
+ return [...entries.keys()].filter(path => path.startsWith(prefix) && !path.slice(prefix.length).includes("/") && path.endsWith(".json")).sort().map(path => {
140
+ const slug = path.slice(prefix.length,-5);
141
+ if (!pattern.test(slug)) throw new Error("Invalid remote content filename");
142
+ return {path,slug};
143
+ });
144
+ };
145
+ for (const page of manifest.pages) {
146
+ const path = filePath(ctx,manifest,cfg.copyDir,page.slug,"ts");
147
+ const values = readValues(await read(path));
148
+ if (!Object.values(values).every(isPlainObject)) throw new Error("Copy groups must contain value objects");
149
+ page.values = values as typeof page.values;
150
+ }
151
+ manifest.documents = [];
152
+ for (const type of manifest.schema?.documents ?? []) for (const file of list(type.folder)) {
153
+ const data = parseJson(await read(file.path));
154
+ if (!isPlainObject(data)) throw new Error("Remote document must be an object");
155
+ manifest.documents.push({id:`${type.name}:${file.slug}`,type:type.name,slug:file.slug,data});
156
+ }
157
+ documentIndex(manifest.documents);
158
+ for (const doc of manifest.documents) validateDocument(manifest.schema!,doc.type,doc.slug,doc.data,manifest.documents);
159
+ for (const collection of manifest.collections ?? []) {
160
+ const folder = cfg.collections.find(c => c.name === collection.name)?.folder;
161
+ if (!folder) throw new Error("Missing configured GitHub collection");
162
+ collection.posts = [];
163
+ for (const file of list(folder,POST_SLUG)) {
164
+ const post = parseJson(await read(file.path));
165
+ if (!isPlainObject(post) || !isPlainObject(post.frontmatter)) throw new Error("Invalid remote article");
166
+ validateBody(post.body); validateCollectionReferences(collection.fields,post.frontmatter,manifest.documents);
167
+ collection.posts.push({slug:file.slug,frontmatter:post.frontmatter,body:post.body});
168
+ }
169
+ }
170
+ if (cfg.menus) {
171
+ manifest.menus = {maxDepth:cfg.menus.maxDepth,entries:[]};
172
+ for (const file of list(cfg.menus.folder)) {
173
+ const menu = validateMenu(parseJson(await read(file.path)),cfg.menus.maxDepth);
174
+ if (menu.slug !== file.slug) throw new Error("Remote menu slug does not match filename");
175
+ manifest.menus.entries.push(menu);
176
+ }
177
+ }
178
+ Object.defineProperty(manifest,REMOTE_SNAPSHOT,{value:snapshot});
179
+ return manifest;
180
+ }
181
+
182
+ function loaded(ctx: GitContext, manifest: Manifest): Snapshot {
183
+ config(ctx,manifest);
184
+ const snapshot = (manifest as Manifest & { [REMOTE_SNAPSHOT]?: Snapshot })[REMOTE_SNAPSHOT];
185
+ if (!snapshot || snapshot.target !== target(ctx,manifest)) throw new Error("Reload draft content before saving to this repository");
186
+ return snapshot;
187
+ }
188
+
189
+ async function mutate(ctx: GitContext, manifest: Manifest, path: string, text: string | null, creating: boolean) {
190
+ const snapshot = loaded(ctx,manifest);
191
+ const endpoint = `/contents/${path.split("/").map(encodeURIComponent).join("/")}`;
192
+ const verify = async (pending: PendingWrite) => {
193
+ let verified;
194
+ try { verified = await gh(ctx,`${endpoint}?ref=${encodeURIComponent(ctx.branch)}`); }
195
+ catch (error) {
196
+ if (pending.text !== null || !(error instanceof GitError) || error.status !== 404) throw error;
197
+ }
198
+ if (pending.text === null) {
199
+ if (verified !== undefined) throw new Error("GitHub deletion could not be verified; reload");
200
+ snapshot.files.delete(path); snapshot.paths.delete(path);
201
+ } else {
202
+ if (verified?.type !== "file" || sha(verified.sha) !== pending.sha || decode(verified) !== pending.text) throw new Error("GitHub save changed before verification; reload");
203
+ snapshot.files.set(path,{sha:pending.sha,text:pending.text}); snapshot.paths.add(path);
204
+ }
205
+ snapshot.pending.delete(path);
206
+ };
207
+ const uncertain = snapshot.pending.get(path);
208
+ if (uncertain) {
209
+ await verify(uncertain);
210
+ if (uncertain.text === text && (!creating || uncertain.creating)) return;
211
+ if (creating && uncertain.creating) creating = false;
212
+ }
213
+ const previous = snapshot.files.get(path);
214
+ if (creating ? snapshot.paths.has(path) : !previous) throw new Error(creating ? "Content already exists; choose another slug" : "Reload this content before changing it");
215
+ const payload = {message:`rigel: ${text === null ? "delete" : "save"} ${path}`,branch:ctx.branch,...(!creating ? {sha:previous!.sha} : {}),...(text === null ? {} : {content:encode(text)})};
216
+ const result = await gh(ctx,endpoint,{method:text === null ? "DELETE" : "PUT",body:JSON.stringify(payload)});
217
+ const pending = {sha:text === null ? previous!.sha : sha(result?.content?.sha),text,creating};
218
+ snapshot.pending.set(path,pending);
219
+ await verify(pending);
220
+ }
221
+
222
+ export async function saveRemoteSelection(ctx: GitContext, manifest: Manifest, selection: Selection, values: Record<string, unknown>, body: ArticleBody | "", creating: boolean): Promise<void> {
223
+ const cfg = config(ctx,manifest);
224
+ const snapshot = loaded(ctx,manifest);
225
+ if (!isPlainObject(values)) throw new Error("Content values must be an object");
226
+ let path: string;
227
+ let data: unknown;
228
+ if (selection.kind === "page") {
229
+ if (creating || !manifest.pages.some(page => page.slug === selection.slug)) throw new Error("Unknown copy page");
230
+ if (!Object.values(values).every(isPlainObject)) throw new Error("Copy groups must contain value objects");
231
+ path = filePath(ctx,manifest,cfg.copyDir,selection.slug,"ts");
232
+ const source = snapshot.files.get(path)?.text;
233
+ if (!source) throw new Error("Reload the copy page before saving");
234
+ await mutate(ctx,manifest,path,applyValues(source,values),false); return;
235
+ } else if (selection.kind === "post") {
236
+ const folder = cfg.collections.find(c => c.name === selection.collection)?.folder;
237
+ const collection = manifest.collections?.find(c => c.name === selection.collection);
238
+ if (!folder || !collection) throw new Error("Unknown collection");
239
+ validateBody(body); validateCollectionReferences(collection.fields,values,manifest.documents ?? []);
240
+ path = filePath(ctx,manifest,folder,selection.slug,"json",POST_SLUG);
241
+ data = {frontmatter:{...values,slug:selection.slug},body};
242
+ } else if (selection.kind === "document") {
243
+ if (!manifest.schema) throw new Error("No document schema configured");
244
+ const document = validateDocumentId(manifest.schema,selection.id);
245
+ if (!document || document.type.name !== selection.type) throw new Error("Unknown document type or slug");
246
+ if (creating && (document.type.creatable === false || document.type.singleton)) throw new Error("Creating entries is disabled for this document type");
247
+ data = {...values,_id:selection.id,_type:selection.type,slug:document.slug};
248
+ validateDocument(manifest.schema,selection.type,document.slug,data as Record<string,unknown>,manifest.documents ?? []);
249
+ path = filePath(ctx,manifest,document.type.folder,document.slug);
250
+ } else if (selection.kind === "menu") {
251
+ if (!cfg.menus || creating) throw new Error("Menus must be configured by the site developer");
252
+ data = validateMenu({...values,slug:selection.slug},cfg.menus.maxDepth);
253
+ path = filePath(ctx,manifest,cfg.menus.folder,selection.slug);
254
+ } else throw new Error("Select editable content before saving");
255
+ await mutate(ctx,manifest,path,JSON.stringify(data,null,2) + "\n",creating);
256
+ }
257
+
258
+ export async function deleteRemotePost(ctx: GitContext, manifest: Manifest, collection: string, slug: string): Promise<void> {
259
+ const cfg = config(ctx,manifest);
260
+ const folder = cfg.collections.find(c => c.name === collection)?.folder;
261
+ if (!folder || !manifest.collections?.some(c => c.name === collection)) throw new Error("Unknown collection");
262
+ await mutate(ctx,manifest,filePath(ctx,manifest,folder,slug,"json",POST_SLUG),null,false);
263
+ }
@@ -0,0 +1,111 @@
1
+ export const ICONS = {
2
+ calendar: { viewBox: "0 0 256 256", d: "M208,32H184V24a8,8,0,0,0-16,0v8H88V24a8,8,0,0,0-16,0v8H48A16,16,0,0,0,32,48V208a16,16,0,0,0,16,16H208a16,16,0,0,0,16-16V48A16,16,0,0,0,208,32ZM72,48v8a8,8,0,0,0,16,0V48h80v8a8,8,0,0,0,16,0V48h24V80H48V48ZM208,208H48V96H208V208Zm-96-88v64a8,8,0,0,1-16,0V132.94l-4.42,2.22a8,8,0,0,1-7.16-14.32l16-8A8,8,0,0,1,112,120Zm59.16,30.45L152,176h16a8,8,0,0,1,0,16H136a8,8,0,0,1-6.4-12.8l28.78-38.37A8,8,0,1,0,145.07,132a8,8,0,1,1-13.85-8A24,24,0,0,1,176,136,23.76,23.76,0,0,1,171.16,150.45Z" },
3
+ textUnderline: { viewBox: "0 0 256 256", d: "M200,224a8,8,0,0,1-8,8H64a8,8,0,0,1,0-16H192A8,8,0,0,1,200,224Zm-72-24a64.07,64.07,0,0,0,64-64V56a8,8,0,0,0-16,0v80a48,48,0,0,1-96,0V56a8,8,0,0,0-16,0v80A64.07,64.07,0,0,0,128,200Z" },
4
+ highlighter: { viewBox: "0 0 256 256", d: "M253.66,106.34a8,8,0,0,0-11.32,0L192,156.69,107.31,72l50.35-50.34a8,8,0,1,0-11.32-11.32L96,60.69A16,16,0,0,0,93.18,79.5L72,100.69a16,16,0,0,0,0,22.62L76.69,128,18.34,186.34a8,8,0,0,0,3.13,13.25l72,24A7.88,7.88,0,0,0,96,224a8,8,0,0,0,5.66-2.34L136,187.31l4.69,4.69a16,16,0,0,0,22.62,0l21.19-21.18A16,16,0,0,0,203.31,168l50.35-50.34A8,8,0,0,0,253.66,106.34ZM93.84,206.85l-55-18.35L88,139.31,124.69,176ZM152,180.69,83.31,112,104,91.31,172.69,160Z" },
5
+ textSubscript: { viewBox: "0 0 256 256", d: "M248,208a8,8,0,0,1-8,8H192a8,8,0,0,1-6.4-12.8l43.17-57.56a16,16,0,1,0-27.86-15,8,8,0,0,1-15.09-5.34,32.18,32.18,0,0,1,4.63-8.59,32,32,0,0,1,51.11,38.52L208,200h32A8,8,0,0,1,248,208ZM149.24,50a8,8,0,0,0-11.29.81L92,103.78l-45.95-53A8,8,0,0,0,34,61.24L81.41,116,34,170.76a8,8,0,0,0,12.1,10.48l46-53,45.95,53a8,8,0,1,0,12.1-10.48L102.59,116l47.46-54.76A8,8,0,0,0,149.24,50Z" },
6
+ textSuperscript: { viewBox: "0 0 256 256", d: "M248,144a8,8,0,0,1-8,8H192a8,8,0,0,1-6.4-12.8l43.17-57.55a16,16,0,1,0-27.86-15,8,8,0,0,1-15.09-5.33,32,32,0,1,1,55.74,29.92L208,136h32A8,8,0,0,1,248,144ZM149.24,74a8,8,0,0,0-11.29.8L92,127.79l-45.95-53A8,8,0,0,0,34,85.24L81.41,140,34,194.76a8,8,0,0,0,12.1,10.48l46-53,45.95,53a8,8,0,1,0,12.1-10.48L102.59,140l47.46-54.76A8,8,0,0,0,149.24,74Z" },
7
+ heading1: { viewBox: "0 0 256 256", d: "M152,56V176a8,8,0,0,1-16,0V124H48v52a8,8,0,0,1-16,0V56a8,8,0,0,1,16,0v52h88V56a8,8,0,0,1,16,0Zm75.77,49a8,8,0,0,0-8.21.39l-24,16a8,8,0,1,0,8.88,13.32L216,127V208a8,8,0,0,0,16,0V112A8,8,0,0,0,227.77,105Z" },
8
+ heading5: { viewBox: "0 0 256 256", d: "M152,56V176a8,8,0,0,1-16,0V124H48v52a8,8,0,0,1-16,0V56a8,8,0,0,1,16,0v52h88V56a8,8,0,0,1,16,0Zm60,88a38.8,38.8,0,0,0-9.41,1.14L206.78,120H240a8,8,0,0,0,0-16H200a8,8,0,0,0-7.89,6.68l-8,48a8,8,0,0,0,13.6,6.92A19.73,19.73,0,0,1,212,160a20,20,0,0,1,0,40,19.73,19.73,0,0,1-14.29-5.6,8,8,0,1,0-11.42,11.2A35.54,35.54,0,0,0,212,216a36,36,0,0,0,0-72Z" },
9
+ heading6: { viewBox: "0 0 256 256", d: "M152,56V176a8,8,0,0,1-16,0V124H48v52a8,8,0,0,1-16,0V56a8,8,0,0,1,16,0v52h88V56a8,8,0,0,1,16,0Zm96,124a36,36,0,1,1-67.34-17.68c.07-.14.14-.28.22-.42l32.25-54a8,8,0,0,1,13.74,8.2l-16.69,28c.6,0,1.21-.05,1.82-.05A36,36,0,0,1,248,180Zm-16,0a20,20,0,1,0-20,20A20,20,0,0,0,232,180Z" },
10
+ paragraph: { viewBox: "0 0 256 256", d: "M208,40H96a64,64,0,0,0,0,128h40v40a8,8,0,0,0,16,0V56h24V208a8,8,0,0,0,16,0V56h16a8,8,0,0,0,0-16ZM136,152H96a48,48,0,0,1,0-96h40Z" },
11
+ heading2: { viewBox: "0 0 256 256", d: "M152,56V176a8,8,0,0,1-16,0V124H48v52a8,8,0,0,1-16,0V56a8,8,0,0,1,16,0v52h88V56a8,8,0,0,1,16,0Zm88,144H208l33.55-44.74a32,32,0,1,0-55.73-29.93,8,8,0,1,0,15.08,5.34,16.28,16.28,0,0,1,2.32-4.3,16,16,0,1,1,25.54,19.27L185.6,203.2A8,8,0,0,0,192,216h48a8,8,0,0,0,0-16Z" },
12
+ heading3: { viewBox: "0 0 256 256", d: "M152,56V176a8,8,0,0,1-16,0V124H48v52a8,8,0,0,1-16,0V56a8,8,0,0,1,16,0v52h88V56a8,8,0,0,1,16,0Zm73.52,90.63,21-30A8,8,0,0,0,240,104H192a8,8,0,0,0,0,16h32.63l-19.18,27.41A8,8,0,0,0,212,160a20,20,0,1,1-14.29,34,8,8,0,1,0-11.42,11.19A36,36,0,0,0,248,180,36.07,36.07,0,0,0,225.52,146.63Z" },
13
+ heading4: { viewBox: "0 0 256 256", d: "M152,56V176a8,8,0,0,1-16,0V124H48v52a8,8,0,0,1-16,0V56a8,8,0,0,1,16,0v52h88V56a8,8,0,0,1,16,0ZM256,184a8,8,0,0,1-8,8h-8v16a8,8,0,0,1-16,0V192H176a8,8,0,0,1-6.31-12.91l56-72A8,8,0,0,1,240,112v64h8A8,8,0,0,1,256,184Zm-32-48.68L192.36,176H224Z" },
14
+ check: { viewBox: "0 0 256 256", d: "M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z" },
15
+ linkBreak: { viewBox: "0 0 256 256", d: "M198.63,57.37a32,32,0,0,0-45.19-.06L141.79,69.52a8,8,0,0,1-11.58-11l11.72-12.29a1.59,1.59,0,0,1,.13-.13,48,48,0,0,1,67.88,67.88,1.59,1.59,0,0,1-.13.13l-12.29,11.72a8,8,0,0,1-11-11.58l12.21-11.65A32,32,0,0,0,198.63,57.37ZM114.21,186.48l-11.65,12.21a32,32,0,0,1-45.25-45.25l12.21-11.65a8,8,0,0,0-11-11.58L46.19,141.93a1.59,1.59,0,0,0-.13.13,48,48,0,0,0,67.88,67.88,1.59,1.59,0,0,0,.13-.13l11.72-12.29a8,8,0,1,0-11.58-11ZM216,152H192a8,8,0,0,0,0,16h24a8,8,0,0,0,0-16ZM40,104H64a8,8,0,0,0,0-16H40a8,8,0,0,0,0,16Zm120,80a8,8,0,0,0-8,8v24a8,8,0,0,0,16,0V192A8,8,0,0,0,160,184ZM96,72a8,8,0,0,0,8-8V40a8,8,0,0,0-16,0V64A8,8,0,0,0,96,72Z" },
16
+ minus: { viewBox: "0 0 256 256", d: "M224,128a8,8,0,0,1-8,8H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128Z" },
17
+ table: { viewBox: "0 0 256 256", d: "M224,48H32a8,8,0,0,0-8,8V192a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A8,8,0,0,0,224,48ZM40,112H80v32H40Zm56,0H216v32H96ZM216,64V96H40V64ZM40,160H80v32H40Zm176,32H96V160H216v32Z" },
18
+ textStrikethrough: { viewBox: "0 0 256 256", d: "M224,128a8,8,0,0,1-8,8H175.93c9.19,7.11,16.07,17.2,16.07,32,0,13.34-7,25.7-19.75,34.79C160.33,211.31,144.61,216,128,216s-32.33-4.69-44.25-13.21C71,193.7,64,181.34,64,168a8,8,0,0,1,16,0c0,17.35,22,32,48,32s48-14.65,48-32c0-14.85-10.54-23.58-38.77-32H40a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM76.33,104a8,8,0,0,0,7.61-10.49A17.3,17.3,0,0,1,83.11,88c0-18.24,19.3-32,44.89-32,18.84,0,34.16,7.42,41,19.85a8,8,0,0,0,14-7.7C173.33,50.52,152.77,40,128,40,93.29,40,67.11,60.63,67.11,88a33.73,33.73,0,0,0,1.62,10.49A8,8,0,0,0,76.33,104Z" },
19
+ squaresFour: { viewBox: "0 0 256 256", d: "M104,40H56A16,16,0,0,0,40,56v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V56A16,16,0,0,0,104,40Zm0,64H56V56h48v48Zm96-64H152a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V56A16,16,0,0,0,200,40Zm0,64H152V56h48v48Zm-96,32H56a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V152A16,16,0,0,0,104,136Zm0,64H56V152h48v48Zm96-64H152a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V152A16,16,0,0,0,200,136Zm0,64H152V152h48v48Z" },
20
+ textB: { viewBox: "0 0 256 256", d: "M178.48,115.7A44,44,0,0,0,148,40H80a8,8,0,0,0-8,8V200a8,8,0,0,0,8,8h80a48,48,0,0,0,18.48-92.3ZM88,56h60a28,28,0,0,1,0,56H88Zm72,136H88V128h72a32,32,0,0,1,0,64Z" },
21
+ textItalic: { viewBox: "0 0 256 256", d: "M200,56a8,8,0,0,1-8,8H157.77L115.1,192H144a8,8,0,0,1,0,16H64a8,8,0,0,1,0-16H98.23L140.9,64H112a8,8,0,0,1,0-16h80A8,8,0,0,1,200,56Z" },
22
+ code: { viewBox: "0 0 256 256", d: "M69.12,94.15,28.5,128l40.62,33.85a8,8,0,1,1-10.24,12.29l-48-40a8,8,0,0,1,0-12.29l48-40a8,8,0,0,1,10.24,12.3Zm176,27.7-48-40a8,8,0,1,0-10.24,12.3L227.5,128l-40.62,33.85a8,8,0,1,0,10.24,12.29l48-40a8,8,0,0,0,0-12.29ZM162.73,32.48a8,8,0,0,0-10.25,4.79l-64,176a8,8,0,0,0,4.79,10.26A8.14,8.14,0,0,0,96,224a8,8,0,0,0,7.52-5.27l64-176A8,8,0,0,0,162.73,32.48Z" },
23
+ listBullets: { viewBox: "0 0 256 256", d: "M80,64a8,8,0,0,1,8-8H216a8,8,0,0,1,0,16H88A8,8,0,0,1,80,64Zm136,56H88a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16Zm0,64H88a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16ZM44,52A12,12,0,1,0,56,64,12,12,0,0,0,44,52Zm0,64a12,12,0,1,0,12,12A12,12,0,0,0,44,116Zm0,64a12,12,0,1,0,12,12A12,12,0,0,0,44,180Z" },
24
+ listNumbers: { viewBox: "0 0 256 256", d: "M224,128a8,8,0,0,1-8,8H104a8,8,0,0,1,0-16H216A8,8,0,0,1,224,128ZM104,72H216a8,8,0,0,0,0-16H104a8,8,0,0,0,0,16ZM216,184H104a8,8,0,0,0,0,16H216a8,8,0,0,0,0-16ZM43.58,55.16,48,52.94V104a8,8,0,0,0,16,0V40a8,8,0,0,0-11.58-7.16l-16,8a8,8,0,0,0,7.16,14.32ZM79.77,156.72a23.73,23.73,0,0,0-9.6-15.95,24.86,24.86,0,0,0-34.11,4.7,23.63,23.63,0,0,0-3.57,6.46,8,8,0,1,0,15,5.47,7.84,7.84,0,0,1,1.18-2.13,8.76,8.76,0,0,1,12-1.59A7.91,7.91,0,0,1,63.93,159a7.64,7.64,0,0,1-1.57,5.78,1,1,0,0,0-.08.11L33.59,203.21A8,8,0,0,0,40,216H72a8,8,0,0,0,0-16H56l19.08-25.53A23.47,23.47,0,0,0,79.77,156.72Z" },
25
+ quotes: { viewBox: "0 0 256 256", d: "M100,56H40A16,16,0,0,0,24,72v64a16,16,0,0,0,16,16h60v8a32,32,0,0,1-32,32,8,8,0,0,0,0,16,48.05,48.05,0,0,0,48-48V72A16,16,0,0,0,100,56Zm0,80H40V72h60ZM216,56H156a16,16,0,0,0-16,16v64a16,16,0,0,0,16,16h60v8a32,32,0,0,1-32,32,8,8,0,0,0,0,16,48.05,48.05,0,0,0,48-48V72A16,16,0,0,0,216,56Zm0,80H156V72h60Z" },
26
+ link: { viewBox: "0 0 256 256", d: "M240,88.23a54.43,54.43,0,0,1-16,37L189.25,160a54.27,54.27,0,0,1-38.63,16h-.05A54.63,54.63,0,0,1,96,119.84a8,8,0,0,1,16,.45A38.62,38.62,0,0,0,150.58,160h0a38.39,38.39,0,0,0,27.31-11.31l34.75-34.75a38.63,38.63,0,0,0-54.63-54.63l-11,11A8,8,0,0,1,135.7,59l11-11A54.65,54.65,0,0,1,224,48,54.86,54.86,0,0,1,240,88.23ZM109,185.66l-11,11A38.41,38.41,0,0,1,70.6,208h0a38.63,38.63,0,0,1-27.29-65.94L78,107.31A38.63,38.63,0,0,1,144,135.71a8,8,0,0,0,16,.45A54.86,54.86,0,0,0,144,96a54.65,54.65,0,0,0-77.27,0L32,130.75A54.62,54.62,0,0,0,70.56,224h0a54.28,54.28,0,0,0,38.64-16l11-11A8,8,0,0,0,109,185.66Z" },
27
+ caretDown: {
28
+ viewBox: "0 0 256 256",
29
+ d: "M213.66,101.66l-80,80a8,8,0,0,1-11.32,0l-80-80A8,8,0,0,1,53.66,90.34L128,164.69l74.34-74.35a8,8,0,0,1,11.32,11.32Z",
30
+ },
31
+ caretRight: {
32
+ viewBox: "0 0 256 256",
33
+ d: "M181.66,122.34l-80-80A8,8,0,0,0,88,48V208a8,8,0,0,0,13.66,5.66l80-80A8,8,0,0,0,181.66,122.34Z",
34
+ },
35
+ user: {
36
+ viewBox: "0 0 256 256",
37
+ d: "M230.92,212c-15.23-26.33-38.7-45.21-66.09-54.16a72,72,0,1,0-73.66,0C63.78,166.78,40.31,185.66,25.08,212a8,8,0,1,0,13.85,8c18.84-32.56,52.14-52,89.07-52s70.23,19.44,89.07,52a8,8,0,1,0,13.85-8ZM72,96a56,56,0,1,1,56,56A56.06,56.06,0,0,1,72,96Z",
38
+ },
39
+ eye: {
40
+ viewBox: "0 0 256 256",
41
+ d: "M247.31,124.76c-.35-.79-8.82-19.58-27.65-38.41C194.57,61.26,162.88,48,128,48S61.43,61.26,36.34,86.35C17.51,105.18,9,124,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208s66.57-13.26,91.66-38.34c18.83-18.83,27.3-37.61,27.65-38.4A8,8,0,0,0,247.31,124.76ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.47,133.47,0,0,1,25,128,133.33,133.33,0,0,1,48.07,97.25C70.33,75.19,97.22,64,128,64s57.67,11.19,79.93,33.25A133.46,133.46,0,0,1,231.05,128C223.84,141.46,192.43,192,128,192Zm0-112a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Z",
42
+ },
43
+ eyeSlash: {
44
+ viewBox: "0 0 256 256",
45
+ d: "M53.92,34.62A8,8,0,1,0,42.08,45.38L61.32,66.55C25,88.84,9.38,123.2,8.69,124.76a8,8,0,0,0,0,6.5c.35.79,8.82,19.57,27.65,38.4C61.43,194.74,93.12,208,128,208a127.11,127.11,0,0,0,52.07-10.83l22,24.21a8,8,0,1,0,11.84-10.76Zm47.33,75.84,41.67,45.85a32,32,0,0,1-41.67-45.85ZM128,192c-30.78,0-57.67-11.19-79.93-33.25A133.16,133.16,0,0,1,25,128c4.69-8.79,19.66-33.39,47.35-49.38l18,19.75a48,48,0,0,0,63.66,70l14.73,16.2A112,112,0,0,1,128,192Zm6-95.43a8,8,0,0,1,3-15.72,48.16,48.16,0,0,1,38.77,42.64,8,8,0,0,1-7.22,8.71,6.39,6.39,0,0,1-.75,0,8,8,0,0,1-8-7.26A32.09,32.09,0,0,0,134,96.57Zm113.28,34.69c-.42.94-10.55,23.37-33.36,43.8a8,8,0,1,1-10.67-11.92A132.77,132.77,0,0,0,231.05,128a133.15,133.15,0,0,0-23.12-30.77C185.67,75.19,158.78,64,128,64a118.37,118.37,0,0,0-19.36,1.57A8,8,0,1,1,106,49.79,134,134,0,0,1,128,48c34.88,0,66.57,13.26,91.66,38.35,18.83,18.83,27.3,37.62,27.65,38.41A8,8,0,0,1,247.31,131.26Z",
46
+ },
47
+ magnifier: {
48
+ viewBox: "0 0 256 256",
49
+ d: "M229.66,218.34l-50.07-50.06a88.11,88.11,0,1,0-11.31,11.31l50.06,50.07a8,8,0,0,0,11.32-11.32ZM40,112a72,72,0,1,1,72,72A72.08,72.08,0,0,1,40,112Z",
50
+ },
51
+ gear: {
52
+ viewBox: "0 0 256 256",
53
+ d: "M128,80a48,48,0,1,0,48,48A48.05,48.05,0,0,0,128,80Zm0,80a32,32,0,1,1,32-32A32,32,0,0,1,128,160Zm88-29.84q.06-2.16,0-4.32l14.92-18.64a8,8,0,0,0,1.48-7.06,107.21,107.21,0,0,0-10.88-26.25,8,8,0,0,0-6-3.93l-23.72-2.64q-1.48-1.56-3-3L186,40.54a8,8,0,0,0-3.94-6,107.71,107.71,0,0,0-26.25-10.87,8,8,0,0,0-7.06,1.49L130.16,40Q128,40,125.84,40L107.2,25.11a8,8,0,0,0-7.06-1.48A107.6,107.6,0,0,0,73.89,34.51a8,8,0,0,0-3.93,6L67.32,64.27q-1.56,1.49-3,3L40.54,70a8,8,0,0,0-6,3.94,107.71,107.71,0,0,0-10.87,26.25,8,8,0,0,0,1.49,7.06L40,125.84Q40,128,40,130.16L25.11,148.8a8,8,0,0,0-1.48,7.06,107.21,107.21,0,0,0,10.88,26.25,8,8,0,0,0,6,3.93l23.72,2.64q1.49,1.56,3,3L70,215.46a8,8,0,0,0,3.94,6,107.71,107.71,0,0,0,26.25,10.87,8,8,0,0,0,7.06-1.49L125.84,216q2.16.06,4.32,0l18.64,14.92a8,8,0,0,0,7.06,1.48,107.21,107.21,0,0,0,26.25-10.88,8,8,0,0,0,3.93-6l2.64-23.72q1.56-1.48,3-3L215.46,186a8,8,0,0,0,6-3.94,107.71,107.71,0,0,0,10.87-26.25,8,8,0,0,0-1.49-7.06Zm-16.1-6.5a73.93,73.93,0,0,1,0,8.68,8,8,0,0,0,1.74,5.48l14.19,17.73a91.57,91.57,0,0,1-6.23,15L187,173.11a8,8,0,0,0-5.1,2.64,74.11,74.11,0,0,1-6.14,6.14,8,8,0,0,0-2.64,5.1l-2.51,22.58a91.32,91.32,0,0,1-15,6.23l-17.74-14.19a8,8,0,0,0-5-1.75h-.48a73.93,73.93,0,0,1-8.68,0,8,8,0,0,0-5.48,1.74L100.45,215.8a91.57,91.57,0,0,1-15-6.23L82.89,187a8,8,0,0,0-2.64-5.1,74.11,74.11,0,0,1-6.14-6.14,8,8,0,0,0-5.1-2.64L46.43,170.6a91.32,91.32,0,0,1-6.23-15l14.19-17.74a8,8,0,0,0,1.74-5.48,73.93,73.93,0,0,1,0-8.68,8,8,0,0,0-1.74-5.48L40.2,100.45a91.57,91.57,0,0,1,6.23-15L69,82.89a8,8,0,0,0,5.1-2.64,74.11,74.11,0,0,1,6.14-6.14A8,8,0,0,0,82.89,69L85.4,46.43a91.32,91.32,0,0,1,15-6.23l17.74,14.19a8,8,0,0,0,5.48,1.74,73.93,73.93,0,0,1,8.68,0,8,8,0,0,0,5.48-1.74L155.55,40.2a91.57,91.57,0,0,1,15,6.23L173.11,69a8,8,0,0,0,2.64,5.1,74.11,74.11,0,0,1,6.14,6.14,8,8,0,0,0,5.1,2.64l22.58,2.51a91.32,91.32,0,0,1,6.23,15l-14.19,17.74A8,8,0,0,0,199.87,123.66Z",
54
+ },
55
+ info: {
56
+ viewBox: "0 0 256 256",
57
+ d: "M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z",
58
+ },
59
+ external: {
60
+ viewBox: "0 0 256 256",
61
+ d: "M224,104a8,8,0,0,1-16,0V59.32l-66.33,66.34a8,8,0,0,1-11.32-11.32L196.68,48H152a8,8,0,0,1,0-16h64a8,8,0,0,1,8,8Zm-40,24a8,8,0,0,0-8,8v72H48V80h72a8,8,0,0,0,0-16H48A16,16,0,0,0,32,80V208a16,16,0,0,0,16,16H176a16,16,0,0,0,16-16V136A8,8,0,0,0,184,128Z",
62
+ },
63
+ dotsThreeOutlineVertical: {
64
+ viewBox: "0 0 256 256",
65
+ d: "M128,96a32,32,0,1,0,32,32A32,32,0,0,0,128,96Zm0,48a16,16,0,1,1,16-16A16,16,0,0,1,128,144Zm0-64A32,32,0,1,0,96,48,32,32,0,0,0,128,80Zm0-48a16,16,0,1,1-16,16A16,16,0,0,1,128,32Zm0,144a32,32,0,1,0,32,32A32,32,0,0,0,128,176Zm0,48a16,16,0,1,1,16-16A16,16,0,0,1,128,224Z",
66
+ },
67
+ keyboard: {
68
+ viewBox: "0 0 256 256",
69
+ d: "M224,48H32A16,16,0,0,0,16,64V192a16,16,0,0,0,16,16H224a16,16,0,0,0,16-16V64A16,16,0,0,0,224,48Zm0,144H32V64H224V192Zm-16-64a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,128Zm0-32a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16H200A8,8,0,0,1,208,96ZM72,160a8,8,0,0,1-8,8H56a8,8,0,0,1,0-16h8A8,8,0,0,1,72,160Zm96,0a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,160Zm40,0a8,8,0,0,1-8,8h-8a8,8,0,0,1,0-16h8A8,8,0,0,1,208,160Z",
70
+ },
71
+ signOut: {
72
+ viewBox: "0 0 256 256",
73
+ d: "M120,216a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V40a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H56V208h56A8,8,0,0,1,120,216Zm109.66-93.66-40-40a8,8,0,0,0-11.32,11.32L204.69,120H112a8,8,0,0,0,0,16h92.69l-26.35,26.34a8,8,0,0,0,11.32,11.32l40-40A8,8,0,0,0,229.66,122.34Z",
74
+ },
75
+ trash: {
76
+ viewBox: "0 0 256 256",
77
+ d: "M216,48H176V40a24,24,0,0,0-24-24H104A24,24,0,0,0,80,40v8H40a8,8,0,0,0,0,16h8V208a16,16,0,0,0,16,16H192a16,16,0,0,0,16-16V64h8a8,8,0,0,0,0-16ZM96,40a8,8,0,0,1,8-8h48a8,8,0,0,1,8,8v8H96Zm96,168H64V64H192ZM112,104v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Zm48,0v64a8,8,0,0,1-16,0V104a8,8,0,0,1,16,0Z",
78
+ },
79
+ plus: {
80
+ viewBox: "0 0 256 256",
81
+ d: "M224,128a8,8,0,0,1-8,8H136v80a8,8,0,0,1-16,0V136H40a8,8,0,0,1,0-16h80V40a8,8,0,0,1,16,0v80h80A8,8,0,0,1,224,128Z",
82
+ },
83
+ x: {
84
+ viewBox: "0 0 256 256",
85
+ d: "M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z",
86
+ },
87
+ monitor: {
88
+ viewBox: "0 0 256 256",
89
+ d: "M208,40H48A24,24,0,0,0,24,64V176a24,24,0,0,0,24,24H208a24,24,0,0,0,24-24V64A24,24,0,0,0,208,40Zm8,136a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V64a8,8,0,0,1,8-8H208a8,8,0,0,1,8,8Zm-48,48a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,224Z",
90
+ },
91
+ palette: {
92
+ viewBox: "0 0 256 256",
93
+ d: "M200.77,53.89A103.27,103.27,0,0,0,128,24h-1.07A104,104,0,0,0,24,128c0,43,26.58,79.06,69.36,94.17A32,32,0,0,0,136,192a16,16,0,0,1,16-16h46.21a31.81,31.81,0,0,0,31.2-24.88,104.43,104.43,0,0,0,2.59-24A103.28,103.28,0,0,0,200.77,53.89Zm13,93.71A15.89,15.89,0,0,1,198.21,160H152a32,32,0,0,0-32,32,16,16,0,0,1-21.31,15.07C62.49,194.3,40,164,40,128a88,88,0,0,1,87.09-88h.9a88.35,88.35,0,0,1,88,87.25A88.86,88.86,0,0,1,213.81,147.6ZM140,76a12,12,0,1,1-12-12A12,12,0,0,1,140,76ZM96,100A12,12,0,1,1,84,88,12,12,0,0,1,96,100Zm0,56a12,12,0,1,1-12-12A12,12,0,0,1,96,156Zm88-56a12,12,0,1,1-12-12A12,12,0,0,1,184,100Z",
94
+ },
95
+ translate: {
96
+ viewBox: "0 0 256 256",
97
+ d: "M247.15,212.42l-56-112a8,8,0,0,0-14.31,0l-21.71,43.43A88,88,0,0,1,108,126.93,103.65,103.65,0,0,0,135.69,64H160a8,8,0,0,0,0-16H104V32a8,8,0,0,0-16,0V48H32a8,8,0,0,0,0,16h87.63A87.76,87.76,0,0,1,96,116.35a87.74,87.74,0,0,1-19-31,8,8,0,1,0-15.08,5.34A103.63,103.63,0,0,0,84,127a87.55,87.55,0,0,1-52,17,8,8,0,0,0,0,16,103.46,103.46,0,0,0,64-22.08,104.18,104.18,0,0,0,51.44,21.31l-26.6,53.19a8,8,0,0,0,14.31,7.16L148.94,192h70.11l13.79,27.58A8,8,0,0,0,240,224a8,8,0,0,0,7.15-11.58ZM156.94,176,184,121.89,211.05,176Z",
98
+ },
99
+ wrench: {
100
+ viewBox: "0 0 256 256",
101
+ d: "M226.76,69a8,8,0,0,0-12.84-2.88l-40.3,37.19-17.23-3.7-3.7-17.23,37.19-40.3A8,8,0,0,0,187,29.24,72,72,0,0,0,88,96,72.34,72.34,0,0,0,94,124.94L33.79,177c-.15.12-.29.26-.43.39a32,32,0,0,0,45.26,45.26c.13-.13.27-.28.39-.42L131.06,162A72,72,0,0,0,232,96,71.56,71.56,0,0,0,226.76,69ZM160,152a56.14,56.14,0,0,1-27.07-7,8,8,0,0,0-9.92,1.77L67.11,211.51a16,16,0,0,1-22.62-22.62L109.18,133a8,8,0,0,0,1.77-9.93,56,56,0,0,1,58.36-82.31l-31.2,33.81a8,8,0,0,0-1.94,7.1L141.83,108a8,8,0,0,0,6.14,6.14l26.35,5.66a8,8,0,0,0,7.1-1.94l33.81-31.2A56.06,56.06,0,0,1,160,152Z",
102
+ },
103
+ image: {
104
+ viewBox: "0 0 256 256",
105
+ d: "M216,40H40A16,16,0,0,0,24,56V200a16,16,0,0,0,16,16H216a16,16,0,0,0,16-16V56A16,16,0,0,0,216,40Zm0,16V158.75l-26.07-26.06a16,16,0,0,0-22.63,0l-20,20-44-44a16,16,0,0,0-22.62,0L40,149.37V56ZM40,172l52-52,80,80H40Zm176,28H194.63l-36-36,20-20L216,181.38V200ZM144,100a12,12,0,1,1,12,12A12,12,0,0,1,144,100Z",
106
+ },
107
+ textAa: {
108
+ viewBox: "0 0 256 256",
109
+ d: "M87.24,52.59a8,8,0,0,0-14.48,0l-64,136a8,8,0,1,0,14.48,6.81L39.9,160h80.2l16.66,35.4a8,8,0,1,0,14.48-6.81ZM47.43,144,80,74.79,112.57,144ZM200,96c-12.76,0-22.73,3.47-29.63,10.32a8,8,0,0,0,11.26,11.36c3.8-3.77,10-5.68,18.37-5.68,13.23,0,24,9,24,20v3.22A42.76,42.76,0,0,0,200,128c-22.06,0-40,16.15-40,36s17.94,36,40,36a42.73,42.73,0,0,0,24-7.25,8,8,0,0,0,16-.75V132C240,112.15,222.06,96,200,96Zm0,88c-13.23,0-24-9-24-20s10.77-20,24-20,24,9,24,20S213.23,184,200,184Z",
110
+ },
111
+ } as const;
@@ -0,0 +1,195 @@
1
+ import { getSettings, setSettings } from "./settings";
2
+ import { mediaMetadata, safeMediaName } from "../../media-types";
3
+ export function getKeys(): StockKeys { return getSettings().apiKeys; }
4
+ export function setKeys(keys: StockKeys) { setSettings({ apiKeys: keys }); }
5
+
6
+ export const STOCK_PROVIDERS = {
7
+ pexels: { label: "Pexels", keyURL: "https://www.pexels.com/api/new/", pattern: "[a-zA-Z0-9]{56}", placeholder: "e.g. Ab3dE6fG9hJ2…" },
8
+ pixabay: { label: "Pixabay", keyURL: "https://pixabay.com/api/docs/#api_key", pattern: "[0-9]+-[a-f0-9]{25}", placeholder: "e.g. 12345678-abcdef012345…" },
9
+ unsplash: { label: "Unsplash", keyURL: "https://unsplash.com/oauth/applications", pattern: "[a-zA-Z0-9_-]{40,}", placeholder: "e.g. AbCdEf12_GhIjKl34…" },
10
+ } as const;
11
+
12
+ export type StockSource = "picsum" | keyof typeof STOCK_PROVIDERS;
13
+ export type StockKeys = Partial<Record<keyof typeof STOCK_PROVIDERS, string>>;
14
+
15
+ export interface StockResult {
16
+ source: StockSource;
17
+ id?: string;
18
+ url: string;
19
+ thumb: string;
20
+ credit: string;
21
+ creditURL?: string;
22
+ }
23
+
24
+ export function picsum(query: string): StockResult[] {
25
+ const seed = (query || "rigel").toLowerCase().replace(/[^a-z0-9]+/g, "-");
26
+ return Array.from({ length: 8 }, (_, i) => {
27
+ const s = `${seed}-${i}`;
28
+ return {
29
+ source: "picsum",
30
+ url: `https://picsum.photos/seed/${s}/1200/800`,
31
+ thumb: `https://picsum.photos/seed/${s}/300/200`,
32
+ credit: `Lorem Picsum · seed ${s}`,
33
+ };
34
+ });
35
+ }
36
+
37
+ export async function unsplash(query: string, key: string): Promise<StockResult[]> {
38
+ const r = await fetch(
39
+ query ? `https://api.unsplash.com/search/photos?query=${encodeURIComponent(query)}&per_page=24` : "https://api.unsplash.com/photos?per_page=24",
40
+ { headers: { Authorization: `Client-ID ${key}` } },
41
+ );
42
+ if (!r.ok) throw new Error(`Unsplash ${r.status}`);
43
+ const data = await r.json();
44
+ return (query ? data.results ?? [] : data).map((p: Record<string, any>) => ({
45
+ source: "unsplash",
46
+ id: String(p.id),
47
+ url: p.urls?.regular ?? p.urls?.full,
48
+ thumb: p.urls?.small,
49
+ credit: `${p.user?.name ?? "?"} / Unsplash`,
50
+ creditURL: `https://unsplash.com/photos/${encodeURIComponent(p.id)}?utm_source=rigel-cms&utm_medium=referral`,
51
+ }));
52
+ }
53
+
54
+ export async function pexels(query: string, key: string): Promise<StockResult[]> {
55
+ const r = await fetch(
56
+ query ? `https://api.pexels.com/v1/search?query=${encodeURIComponent(query)}&per_page=24` : "https://api.pexels.com/v1/curated?per_page=24",
57
+ { headers: { Authorization: key } },
58
+ );
59
+ if (!r.ok) throw new Error(`Pexels ${r.status}`);
60
+ const data = await r.json();
61
+ return (data.photos ?? []).map((p: Record<string, any>) => ({
62
+ source: "pexels",
63
+ url: p.src?.large2x ?? p.src?.large ?? p.src?.original,
64
+ thumb: p.src?.medium,
65
+ credit: `${p.photographer ?? "?"} / Pexels`,
66
+ creditURL: p.url,
67
+ }));
68
+ }
69
+
70
+ export async function pixabay(query: string, key: string): Promise<StockResult[]> {
71
+ const params = new URLSearchParams({ key, image_type: "photo", safesearch: "true", per_page: "24" });
72
+ if (query) params.set("q", query);
73
+ else params.set("editors_choice", "true");
74
+ const r = await fetch(`https://pixabay.com/api/?${params}`);
75
+ if (!r.ok) throw new Error(`Pixabay ${r.status}`);
76
+ const data = await r.json();
77
+ return (data.hits ?? []).map((p: Record<string, any>) => ({
78
+ source: "pixabay",
79
+ url: p.largeImageURL,
80
+ thumb: p.webformatURL,
81
+ credit: `${p.user ?? "?"} / Pixabay`,
82
+ creditURL: p.pageURL,
83
+ }));
84
+ }
85
+
86
+ export async function searchImages(
87
+ source: StockSource,
88
+ query: string,
89
+ ): Promise<StockResult[]> {
90
+ query = query.trim();
91
+ if (source === "picsum") return picsum(query);
92
+ const keys = getKeys();
93
+ if (source === "pixabay") {
94
+ if (!keys.pixabay) throw new Error("Add a Pixabay API key in Settings → Media");
95
+ return pixabay(query, keys.pixabay);
96
+ }
97
+ if (source === "unsplash") {
98
+ if (!keys.unsplash) throw new Error("Add an Unsplash API key in Settings");
99
+ return unsplash(query, keys.unsplash);
100
+ }
101
+ if (source === "pexels") {
102
+ if (!keys.pexels) throw new Error("Add a Pexels API key in Settings → Media");
103
+ return pexels(query, keys.pexels);
104
+ }
105
+ throw new Error("Unknown stock photo provider");
106
+ }
107
+
108
+ export async function importStockImage(image: StockResult): Promise<string> {
109
+ const url = new URL(image.url);
110
+ const hosts = {
111
+ picsum: ["picsum.photos"], unsplash: ["images.unsplash.com"],
112
+ pexels: ["images.pexels.com"], pixabay: ["pixabay.com", "cdn.pixabay.com"],
113
+ };
114
+ if (url.protocol !== "https:" || url.username || url.password || !hosts[image.source]?.includes(url.hostname)) {
115
+ throw new Error("Unexpected stock photo URL");
116
+ }
117
+ if (image.source === "unsplash") {
118
+ const key = getKeys().unsplash;
119
+ if (!key || !image.id) throw new Error("Unsplash photo or API key is missing");
120
+ const tracked = await fetch(`https://api.unsplash.com/photos/${encodeURIComponent(image.id)}/download`, {
121
+ headers: { Authorization: "Client-" + "ID " + key },
122
+ });
123
+ if (!tracked.ok) throw new Error(`Unsplash download tracking failed (${tracked.status})`);
124
+ return image.url;
125
+ }
126
+ return importImageUrl(image.url);
127
+ }
128
+
129
+ export async function importImageUrl(value: string): Promise<string> {
130
+ if (!safeImageUrl(value)) throw new Error("Unsafe image URL");
131
+ if (!value.startsWith("/") && new URL(value).protocol !== "https:") throw new Error("Remote imports require HTTPS");
132
+ if (!value.startsWith("/") && new URL(value).hostname === "images.unsplash.com") throw new Error("Unsplash API images must stay hotlinked. Use Unsplash in the content image picker.");
133
+ let response: Response;
134
+ try { response = await fetch(value, { credentials: "omit", referrerPolicy: "no-referrer" }); }
135
+ catch { throw new Error("Image download failed. The source must allow browser downloads (CORS); alternatively upload the image file."); }
136
+ if (!response.ok) throw new Error(`Photo download failed (${response.status})`);
137
+ const limit = 10 * 1024 * 1024;
138
+ if (Number(response.headers.get("Content-Length")) > limit) throw new Error("Photo exceeds the 10 MB import limit");
139
+ const type = response.headers.get("Content-Type")?.split(";")[0].trim().toLowerCase();
140
+ if (!["image/jpeg", "image/png", "image/webp"].includes(type ?? "")) throw new Error("Choose a JPEG, PNG or WebP image.");
141
+ if (!response.body) throw new Error("Empty image download");
142
+ const reader = response.body.getReader();
143
+ const chunks: Uint8Array<ArrayBuffer>[] = [];
144
+ let size = 0;
145
+ try {
146
+ while (true) {
147
+ const { done, value: chunk } = await reader.read();
148
+ if (done) break;
149
+ size += chunk.byteLength;
150
+ if (size > limit) { await reader.cancel(); throw new Error("Photo exceeds the 10 MB import limit"); }
151
+ chunks.push(new Uint8Array(chunk));
152
+ }
153
+ } finally { reader.releaseLock(); }
154
+ return uploadImage(new Blob(chunks, { type }));
155
+ }
156
+
157
+ export function safeImageUrl(value: string): boolean {
158
+ if (!value || /[\s\u0000-\u001f\u007f\\]/.test(value) || value.startsWith("//")) return false;
159
+ try {
160
+ const url = new URL(value, "https://rigel.invalid");
161
+ return (value.startsWith("/") || /^https?:\/\//i.test(value)) && ["http:", "https:"].includes(url.protocol) && !url.username && !url.password;
162
+ } catch { return false; }
163
+ }
164
+
165
+ export function currentAuthor(): string {
166
+ try {
167
+ const raw = localStorage.getItem("rigelcms.user");
168
+ if (!raw) return "Local user";
169
+ const parsed = JSON.parse(raw);
170
+ const name = typeof parsed?.name === "string" ? parsed.name : typeof parsed?.nickname === "string" ? parsed.nickname : "";
171
+ return name.trim() || "Local user";
172
+ } catch { return "Local user"; }
173
+ }
174
+
175
+ export async function uploadMedia(file: File): Promise<string> {
176
+ const metadata = mediaMetadata(file.name);
177
+ if (!safeMediaName(file.name) || !metadata || file.size > 10 * 1024 * 1024) throw new Error("Choose a supported file up to 10 MB with a safe filename.");
178
+ const response = await fetch("/rigel/api/local/media", { method: "POST", headers: { "Content-Type": metadata.type, "X-Rigel-Filename": file.name, "X-Rigel-Author": currentAuthor() }, body: file });
179
+ if (!response.ok) throw new Error(`Upload failed (${response.status}): ${await response.text()}`);
180
+ const result = await response.json();
181
+ if (typeof result.url !== "string" || !safeImageUrl(result.url) || !result.url.startsWith("/") || !/\/[a-f0-9]{64}\.[a-z0-9]+$/.test(result.url) || !mediaMetadata(result.url)) throw new Error("Invalid uploaded asset URL");
182
+ return result.url;
183
+ }
184
+
185
+ export async function uploadImage(blob: Blob): Promise<string> {
186
+ if (blob.size > 10 * 1024 * 1024) throw new Error("Photo exceeds the 10 MB import limit");
187
+ const saved = await fetch("/rigel/api/local/media", { method: "POST", body: blob });
188
+ if (saved.status === 404) throw new Error("Photo import requires the local dev server");
189
+ if (!saved.ok) throw new Error(`Photo import failed (${saved.status})`);
190
+ const result = await saved.json();
191
+ if (typeof result.url !== "string" || !safeImageUrl(result.url) || !result.url.startsWith("/") || !/\/[a-f0-9]{64}\.(jpg|png|webp)$/.test(result.url)) {
192
+ throw new Error("Invalid imported photo URL");
193
+ }
194
+ return result.url;
195
+ }