@rigel-cms/rigel 0.2.0-beta.1 → 1.0.0-rc.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.
- package/README.md +4 -2
- package/package.json +3 -4
- package/src/article-body.ts +9 -2
- package/src/article-render.ts +27 -1
- package/src/config.ts +8 -0
- package/src/editor/App.svelte +6 -7
- package/src/editor/blocks/embed.ts +21 -0
- package/src/editor/blocks/index.ts +3 -1
- package/src/editor/blocks/video.ts +35 -0
- package/src/editor/lib/ActionDialog.svelte +2 -1
- package/src/editor/lib/BlockEditor.svelte +84 -15
- package/src/editor/lib/CollectionPanel.svelte +4 -3
- package/src/editor/lib/ImagePicker.svelte +141 -47
- package/src/editor/lib/MediaPicker.svelte +73 -0
- package/src/editor/lib/Select.svelte +5 -2
- package/src/editor/lib/VisualEditor.svelte +11 -6
- package/src/editor/lib/embed-preview.ts +57 -0
- package/src/editor/lib/embeds.ts +47 -0
- package/src/editor/lib/github-backend.ts +2 -1
- package/src/editor/lib/icons.ts +13 -0
- package/src/editor/lib/images.ts +20 -8
- package/src/editor/lib/media-library.ts +11 -1
- package/src/editor/lib/settings.ts +2 -2
- package/src/editor/lib/tooltip.ts +23 -4
- package/src/editor/styles.css +29 -12
- package/src/env.d.ts +1 -1
- package/src/index.ts +4 -6
- package/src/json-output.ts +5 -0
- package/src/manifest/types.ts +1 -0
- package/src/media-types.ts +14 -1
- package/src/preview.ts +7 -3
- package/src/routes/local-documents.ts +1 -1
- package/src/routes/local-media.ts +33 -28
- package/src/routes/local-menu.ts +2 -1
- package/src/routes/local-post.ts +2 -1
- package/src/routes/media-files.ts +36 -1
- package/src/routes/media-validation.ts +2 -2
- package/src/schema/store.ts +3 -1
- package/src/vite/manifest-plugin.ts +3 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export type EmbedProvider = "youtube" | "vimeo";
|
|
2
|
+
export type EmbedRef = { provider: EmbedProvider; id: string };
|
|
3
|
+
|
|
4
|
+
const YOUTUBE_ID = /^[A-Za-z0-9_-]{11}$/;
|
|
5
|
+
const VIMEO_ID = /^\d{6,12}$/;
|
|
6
|
+
const YOUTUBE_HOSTS = new Set(["youtube.com", "www.youtube.com", "m.youtube.com", "youtu.be", "music.youtube.com", "youtube-nocookie.com", "www.youtube-nocookie.com"]);
|
|
7
|
+
const VIMEO_HOSTS = new Set(["vimeo.com", "www.vimeo.com", "player.vimeo.com"]);
|
|
8
|
+
|
|
9
|
+
export function parseEmbedUrl(value: string): EmbedRef | undefined {
|
|
10
|
+
const raw = value.trim();
|
|
11
|
+
if (!/^https?:\/\//i.test(raw) || /[\s\\\u0000-\u001f\u007f]/.test(raw)) return undefined;
|
|
12
|
+
let url: URL;
|
|
13
|
+
try { url = new URL(raw); } catch { return undefined; }
|
|
14
|
+
const host = url.hostname.toLowerCase();
|
|
15
|
+
if (YOUTUBE_HOSTS.has(host)) {
|
|
16
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
17
|
+
const candidate = host === "youtu.be" ? parts[0] : url.searchParams.get("v") ?? (["embed", "shorts", "live", "v"].includes(parts[0] ?? "") ? parts[1] : undefined);
|
|
18
|
+
return candidate && YOUTUBE_ID.test(candidate) ? { provider: "youtube", id: candidate } : undefined;
|
|
19
|
+
}
|
|
20
|
+
if (VIMEO_HOSTS.has(host)) {
|
|
21
|
+
const parts = url.pathname.split("/").filter(Boolean);
|
|
22
|
+
const candidate = parts[0] === "video" ? parts[1] : /^\d{6,12}$/.test(parts.at(-1) ?? "") ? parts.at(-1) : undefined;
|
|
23
|
+
return candidate && VIMEO_ID.test(candidate) ? { provider: "vimeo", id: candidate } : undefined;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function embedSrc(ref: EmbedRef, privacy = true): string {
|
|
29
|
+
if (ref.provider === "youtube") return `https://www.${privacy ? "youtube-nocookie.com" : "youtube.com"}/embed/${ref.id}?rel=0`;
|
|
30
|
+
return `https://player.vimeo.com/video/${ref.id}${privacy ? "?dnt=1" : ""}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function embedWatchUrl(ref: EmbedRef): string {
|
|
34
|
+
return ref.provider === "youtube" ? `https://www.youtube.com/watch?v=${ref.id}` : `https://vimeo.com/${ref.id}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function embedProviderLabel(ref: EmbedRef): string {
|
|
38
|
+
return ref.provider === "youtube" ? "YouTube" : "Vimeo";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function safeEmbed(ref: unknown): EmbedRef | undefined {
|
|
42
|
+
if (!ref || typeof ref !== "object") return undefined;
|
|
43
|
+
const { provider, id } = ref as EmbedRef;
|
|
44
|
+
if (provider === "youtube" && typeof id === "string" && YOUTUBE_ID.test(id)) return { provider, id };
|
|
45
|
+
if (provider === "vimeo" && typeof id === "string" && VIMEO_ID.test(id)) return { provider, id };
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Manifest } from "../../manifest/types";
|
|
2
|
+
import { serializeContentJson } from "../../json-output";
|
|
2
3
|
import type { Selection } from "../../manifest/navigation";
|
|
3
4
|
import { validateBody, type ArticleBody } from "../../article-body";
|
|
4
5
|
import { validateMenu } from "../../manifest/menu";
|
|
@@ -289,7 +290,7 @@ async function saveSelection(ctx: GitContext, manifest: Manifest, selection: Sel
|
|
|
289
290
|
data = validateMenu({...values,slug:selection.slug},cfg.menus.maxDepth);
|
|
290
291
|
path = filePath(ctx,manifest,cfg.menus.folder,selection.slug);
|
|
291
292
|
} else throw new Error("Select editable content before saving");
|
|
292
|
-
await mutate(ctx,manifest,path,
|
|
293
|
+
await mutate(ctx,manifest,path,serializeContentJson(data,cfg.jsonOutput),creating);
|
|
293
294
|
}
|
|
294
295
|
|
|
295
296
|
export interface PublishFile { filename: string; status: "added" | "modified" | "removed" | "renamed"; previous_filename?: string }
|
package/src/editor/lib/icons.ts
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
export const ICONS = {
|
|
2
|
+
file: { viewBox: "0 0 256 256", d: "M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Z" },
|
|
3
|
+
fileAudio: { viewBox: "0 0 256 256", d: "M99.06,128.61a8,8,0,0,0-8.72,1.73L68.69,152H48a8,8,0,0,0-8,8v40a8,8,0,0,0,8,8H68.69l21.65,21.66A8,8,0,0,0,104,224V136A8,8,0,0,0,99.06,128.61ZM88,204.69,77.66,194.34A8,8,0,0,0,72,192H56V168H72a8,8,0,0,0,5.66-2.34L88,155.31ZM152,180a40.55,40.55,0,0,1-20,34.91A8,8,0,0,1,124,201.09a24.49,24.49,0,0,0,0-42.18A8,8,0,0,1,132,145.09,40.55,40.55,0,0,1,152,180Zm61.66-97.66-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v80a8,8,0,0,0,16,0V40h88V88a8,8,0,0,0,8,8h48V216H168a8,8,0,0,0,0,16h32a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160Z" },
|
|
4
|
+
fileDoc: { viewBox: "0 0 256 256", d: "M52,144H36a8,8,0,0,0-8,8v56a8,8,0,0,0,8,8H52a36,36,0,0,0,0-72Zm0,56H44V160h8a20,20,0,0,1,0,40Zm169.53-4.91a8,8,0,0,1,.25,11.31A30.06,30.06,0,0,1,200,216c-17.65,0-32-16.15-32-36s14.35-36,32-36a30.06,30.06,0,0,1,21.78,9.6,8,8,0,0,1-11.56,11.06A14.24,14.24,0,0,0,200,160c-8.82,0-16,9-16,20s7.18,20,16,20a14.24,14.24,0,0,0,10.22-4.66A8,8,0,0,1,221.53,195.09ZM128,144c-17.65,0-32,16.15-32,36s14.35,36,32,36,32-16.15,32-36S145.65,144,128,144Zm0,56c-8.82,0-16-9-16-20s7.18-20,16-20,16,9,16,20S136.82,200,128,200ZM48,120a8,8,0,0,0,8-8V40h88V88a8,8,0,0,0,8,8h48v16a8,8,0,0,0,16,0V88a8,8,0,0,0-2.34-5.66l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v72A8,8,0,0,0,48,120ZM160,51.31,188.69,80H160Z" },
|
|
5
|
+
filePdf: { viewBox: "0 0 256 256", d: "M224,152a8,8,0,0,1-8,8H192v16h16a8,8,0,0,1,0,16H192v16a8,8,0,0,1-16,0V152a8,8,0,0,1,8-8h32A8,8,0,0,1,224,152ZM92,172a28,28,0,0,1-28,28H56v8a8,8,0,0,1-16,0V152a8,8,0,0,1,8-8H64A28,28,0,0,1,92,172Zm-16,0a12,12,0,0,0-12-12H56v24h8A12,12,0,0,0,76,172Zm88,8a36,36,0,0,1-36,36H112a8,8,0,0,1-8-8V152a8,8,0,0,1,8-8h16A36,36,0,0,1,164,180Zm-16,0a20,20,0,0,0-20-20h-8v40h8A20,20,0,0,0,148,180ZM40,112V40A16,16,0,0,1,56,24h96a8,8,0,0,1,5.66,2.34l56,56A8,8,0,0,1,216,88v24a8,8,0,0,1-16,0V96H152a8,8,0,0,1-8-8V40H56v72a8,8,0,0,1-16,0ZM160,80h28.69L160,51.31Z" },
|
|
6
|
+
filePpt: { viewBox: "0 0 256 256", d: "M224,152a8,8,0,0,1-8,8H204v48a8,8,0,0,1-16,0V160H176a8,8,0,0,1,0-16h40A8,8,0,0,1,224,152ZM92,172a28,28,0,0,1-28,28H56v8a8,8,0,0,1-16,0V152a8,8,0,0,1,8-8H64A28,28,0,0,1,92,172Zm-16,0a12,12,0,0,0-12-12H56v24h8A12,12,0,0,0,76,172Zm84,0a28,28,0,0,1-28,28h-8v8a8,8,0,0,1-16,0V152a8,8,0,0,1,8-8h16A28,28,0,0,1,160,172Zm-16,0a12,12,0,0,0-12-12h-8v24h8A12,12,0,0,0,144,172ZM40,112V40A16,16,0,0,1,56,24h96a8,8,0,0,1,5.66,2.34l56,56A8,8,0,0,1,216,88v24a8,8,0,0,1-16,0V96H152a8,8,0,0,1-8-8V40H56v72a8,8,0,0,1-16,0ZM160,80h28.69L160,51.31Z" },
|
|
7
|
+
fileText: { viewBox: "0 0 256 256", d: "M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40V216a16,16,0,0,0,16,16H200a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM200,216H56V40h88V88a8,8,0,0,0,8,8h48V216Zm-32-80a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,136Zm0,32a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,168Z" },
|
|
8
|
+
fileTxt: { viewBox: "0 0 256 256", d: "M48,120a8,8,0,0,0,8-8V40h88V88a8,8,0,0,0,8,8h48v16a8,8,0,0,0,16,0V88a8,8,0,0,0-2.34-5.66l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v72A8,8,0,0,0,48,120ZM160,51.31,188.69,80H160Zm-5.49,105.34L137.83,180l16.68,23.35a8,8,0,0,1-13,9.3L128,193.76l-13.49,18.89a8,8,0,1,1-13-9.3L118.17,180l-16.68-23.35a8,8,0,1,1,13-9.3L128,166.24l13.49-18.89a8,8,0,0,1,13,9.3ZM92,152a8,8,0,0,1-8,8H72v48a8,8,0,0,1-16,0V160H44a8,8,0,0,1,0-16H84A8,8,0,0,1,92,152Zm128,0a8,8,0,0,1-8,8H200v48a8,8,0,0,1-16,0V160H172a8,8,0,0,1,0-16h40A8,8,0,0,1,220,152Z" },
|
|
9
|
+
fileVideo: { viewBox: "0 0 256 256", d: "M213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v72a8,8,0,0,0,16,0V40h88V88a8,8,0,0,0,8,8h48V216h-8a8,8,0,0,0,0,16h8a16,16,0,0,0,16-16V88A8,8,0,0,0,213.66,82.34ZM160,51.31,188.69,80H160ZM155.88,145a8,8,0,0,0-8.12.22l-19.95,12.46A16,16,0,0,0,112,144H48a16,16,0,0,0-16,16v48a16,16,0,0,0,16,16h64a16,16,0,0,0,15.81-13.68l19.95,12.46A8,8,0,0,0,160,216V152A8,8,0,0,0,155.88,145ZM112,208H48V160h64v48Zm32-6.43-16-10V176.43l16-10Z" },
|
|
10
|
+
fileXls: { viewBox: "0 0 256 256", d: "M156,208a8,8,0,0,1-8,8H120a8,8,0,0,1-8-8V152a8,8,0,0,1,16,0v48h20A8,8,0,0,1,156,208ZM92.65,145.49a8,8,0,0,0-11.16,1.86L68,166.24,54.51,147.35a8,8,0,1,0-13,9.3L58.17,180,41.49,203.35a8,8,0,0,0,13,9.3L68,193.76l13.49,18.89a8,8,0,0,0,13-9.3L77.83,180l16.68-23.35A8,8,0,0,0,92.65,145.49Zm98.94,25.82c-4-1.16-8.14-2.35-10.45-3.84-1.25-.82-1.23-1-1.12-1.9a4.54,4.54,0,0,1,2-3.67c4.6-3.12,15.34-1.72,19.82-.56a8,8,0,0,0,4.07-15.48c-2.11-.55-21-5.22-32.83,2.76a20.58,20.58,0,0,0-8.95,14.95c-2,15.88,13.65,20.41,23,23.11,12.06,3.49,13.12,4.92,12.78,7.59-.31,2.41-1.26,3.33-2.15,3.93-4.6,3.06-15.16,1.55-19.54.35A8,8,0,0,0,173.93,214a60.63,60.63,0,0,0,15.19,2c5.82,0,12.3-1,17.49-4.46a20.81,20.81,0,0,0,9.18-15.23C218,179,201.48,174.17,191.59,171.31ZM40,112V40A16,16,0,0,1,56,24h96a8,8,0,0,1,5.66,2.34l56,56A8,8,0,0,1,216,88v24a8,8,0,1,1-16,0V96H152a8,8,0,0,1-8-8V40H56v72a8,8,0,0,1-16,0ZM160,80h28.68L160,51.31Z" },
|
|
11
|
+
fileZip: { viewBox: "0 0 256 256", d: "M184,144H168a8,8,0,0,0-8,8v56a8,8,0,0,0,16,0v-8h8a28,28,0,0,0,0-56Zm0,40h-8V160h8a12,12,0,0,1,0,24Zm-48-32v56a8,8,0,0,1-16,0V152a8,8,0,0,1,16,0ZM96,208a8,8,0,0,1-8,8H56a8,8,0,0,1-7-12l25.16-44H56a8,8,0,0,1,0-16H88a8,8,0,0,1,7,12L69.79,200H88A8,8,0,0,1,96,208ZM213.66,82.34l-56-56A8,8,0,0,0,152,24H56A16,16,0,0,0,40,40v72a8,8,0,0,0,16,0V40h88V88a8,8,0,0,0,8,8h48v16a8,8,0,0,0,16,0V88A8,8,0,0,0,213.66,82.34ZM160,80V51.31L188.69,80Z" },
|
|
2
12
|
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
13
|
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
14
|
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" },
|
|
@@ -100,6 +110,9 @@ export const ICONS = {
|
|
|
100
110
|
viewBox: "0 0 256 256",
|
|
101
111
|
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
112
|
},
|
|
113
|
+
filmStrip: { viewBox: "0 0 256 256", 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,40ZM40,88h80v80H40Zm96-16V56h32V72Zm-16,0H88V56h32Zm0,112v16H88V184Zm16,0h32v16H136Zm0-16V88h80v80Zm80-96H184V56h32ZM72,56V72H40V56ZM40,184H72v16H40Zm176,16H184V184h32v16Z" },
|
|
114
|
+
youtubeLogo: { viewBox: "0 0 256 256", d: "M164.44,121.34l-48-32A8,8,0,0,0,104,96v64a8,8,0,0,0,12.44,6.66l48-32a8,8,0,0,0,0-13.32ZM120,145.05V111l25.58,17ZM234.33,69.52a24,24,0,0,0-14.49-16.4C185.56,39.88,131,40,128,40s-57.56-.12-91.84,13.12a24,24,0,0,0-14.49,16.4C19.08,79.5,16,97.74,16,128s3.08,48.5,5.67,58.48a24,24,0,0,0,14.49,16.41C69,215.56,120.4,216,127.34,216h1.32c6.94,0,58.37-.44,91.18-13.11a24,24,0,0,0,14.49-16.41c2.59-10,5.67-28.22,5.67-58.48S236.92,79.5,234.33,69.52Zm-15.49,113a8,8,0,0,1-4.77,5.49c-31.65,12.22-85.48,12-86,12H128c-.54,0-54.33.2-86-12a8,8,0,0,1-4.77-5.49C34.8,173.39,32,156.57,32,128s2.8-45.39,5.16-54.47A8,8,0,0,1,41.93,68c30.52-11.79,81.66-12,85.85-12h.27c.54,0,54.38-.18,86,12a8,8,0,0,1,4.77,5.49C221.2,82.61,224,99.43,224,128S221.2,173.39,218.84,182.47Z" },
|
|
115
|
+
playCircle: { viewBox: "0 0 256 256", 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,216Zm48.24-94.78-64-40A8,8,0,0,0,100,88v80a8,8,0,0,0,12.24,6.78l64-40a8,8,0,0,0,0-13.56ZM116,153.57V102.43L156.91,128Z" },
|
|
103
116
|
image: {
|
|
104
117
|
viewBox: "0 0 256 256",
|
|
105
118
|
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",
|
package/src/editor/lib/images.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getSettings, setSettings } from "./settings";
|
|
2
|
-
import { mediaMetadata, safeMediaName } from "../../media-types";
|
|
2
|
+
import { DEFAULT_MAX_FILE_SIZE, mediaMetadata, safeMediaName, storedMediaFile } from "../../media-types";
|
|
3
|
+
import config from "virtual:rigel-config";
|
|
3
4
|
export function getKeys(): StockKeys { return getSettings().apiKeys; }
|
|
4
5
|
export function setKeys(keys: StockKeys) { setSettings({ apiKeys: keys }); }
|
|
5
6
|
|
|
@@ -19,6 +20,9 @@ export interface StockResult {
|
|
|
19
20
|
thumb: string;
|
|
20
21
|
credit: string;
|
|
21
22
|
creditURL?: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
alt?: string;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
export function picsum(query: string): StockResult[] {
|
|
@@ -64,6 +68,9 @@ export async function pexels(query: string, key: string): Promise<StockResult[]>
|
|
|
64
68
|
thumb: p.src?.medium,
|
|
65
69
|
credit: `${p.photographer ?? "?"} / Pexels`,
|
|
66
70
|
creditURL: p.url,
|
|
71
|
+
title: typeof p.alt === "string" ? p.alt : "",
|
|
72
|
+
description: typeof p.alt === "string" ? p.alt : "",
|
|
73
|
+
alt: typeof p.alt === "string" ? p.alt : "",
|
|
67
74
|
}));
|
|
68
75
|
}
|
|
69
76
|
|
|
@@ -80,6 +87,9 @@ export async function pixabay(query: string, key: string): Promise<StockResult[]
|
|
|
80
87
|
thumb: p.webformatURL,
|
|
81
88
|
credit: `${p.user ?? "?"} / Pixabay`,
|
|
82
89
|
creditURL: p.pageURL,
|
|
90
|
+
title: typeof p.tags === "string" ? p.tags : "",
|
|
91
|
+
description: typeof p.tags === "string" ? p.tags : "",
|
|
92
|
+
alt: typeof p.tags === "string" ? p.tags : "",
|
|
83
93
|
}));
|
|
84
94
|
}
|
|
85
95
|
|
|
@@ -134,8 +144,8 @@ export async function importImageUrl(value: string): Promise<string> {
|
|
|
134
144
|
try { response = await fetch(value, { credentials: "omit", referrerPolicy: "no-referrer" }); }
|
|
135
145
|
catch { throw new Error("Image download failed. The source must allow browser downloads (CORS); alternatively upload the image file."); }
|
|
136
146
|
if (!response.ok) throw new Error(`Photo download failed (${response.status})`);
|
|
137
|
-
const limit =
|
|
138
|
-
if (Number(response.headers.get("Content-Length")) > limit) throw new Error(
|
|
147
|
+
const limit = config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
148
|
+
if (Number(response.headers.get("Content-Length")) > limit) throw new Error(`Photo exceeds the ${limit / 1024 / 1024} MB import limit`);
|
|
139
149
|
const type = response.headers.get("Content-Type")?.split(";")[0].trim().toLowerCase();
|
|
140
150
|
if (!["image/jpeg", "image/png", "image/webp"].includes(type ?? "")) throw new Error("Choose a JPEG, PNG or WebP image.");
|
|
141
151
|
if (!response.body) throw new Error("Empty image download");
|
|
@@ -147,7 +157,7 @@ export async function importImageUrl(value: string): Promise<string> {
|
|
|
147
157
|
const { done, value: chunk } = await reader.read();
|
|
148
158
|
if (done) break;
|
|
149
159
|
size += chunk.byteLength;
|
|
150
|
-
if (size > limit) { await reader.cancel(); throw new Error(
|
|
160
|
+
if (size > limit) { await reader.cancel(); throw new Error(`Photo exceeds the ${limit / 1024 / 1024} MB import limit`); }
|
|
151
161
|
chunks.push(new Uint8Array(chunk));
|
|
152
162
|
}
|
|
153
163
|
} finally { reader.releaseLock(); }
|
|
@@ -174,21 +184,23 @@ export function currentAuthor(): string {
|
|
|
174
184
|
|
|
175
185
|
export async function uploadMedia(file: File): Promise<string> {
|
|
176
186
|
const metadata = mediaMetadata(file.name);
|
|
177
|
-
|
|
187
|
+
const limit = config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
188
|
+
if (!safeMediaName(file.name) || !metadata || file.size > limit) throw new Error(`Choose a supported file up to ${limit / 1024 / 1024} MB with a safe filename.`);
|
|
178
189
|
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
190
|
if (!response.ok) throw new Error(`Upload failed (${response.status}): ${await response.text()}`);
|
|
180
191
|
const result = await response.json();
|
|
181
|
-
if (typeof result.url !== "string" || !safeImageUrl(result.url) || !result.url.startsWith("/") ||
|
|
192
|
+
if (typeof result.url !== "string" || !safeImageUrl(result.url) || !result.url.startsWith("/") || !storedMediaFile.test(result.url.split("/").pop() ?? "") || !mediaMetadata(result.url)) throw new Error("Invalid uploaded asset URL");
|
|
182
193
|
return result.url;
|
|
183
194
|
}
|
|
184
195
|
|
|
185
196
|
export async function uploadImage(blob: Blob): Promise<string> {
|
|
186
|
-
|
|
197
|
+
const limit = config.maxFileSize ?? DEFAULT_MAX_FILE_SIZE;
|
|
198
|
+
if (blob.size > limit) throw new Error(`Photo exceeds the ${limit / 1024 / 1024} MB import limit`);
|
|
187
199
|
const saved = await fetch("/rigel/api/local/media", { method: "POST", body: blob });
|
|
188
200
|
if (saved.status === 404) throw new Error("Photo import requires the local dev server");
|
|
189
201
|
if (!saved.ok) throw new Error(`Photo import failed (${saved.status})`);
|
|
190
202
|
const result = await saved.json();
|
|
191
|
-
if (typeof result.url !== "string" || !safeImageUrl(result.url) || !result.url.startsWith("/") ||
|
|
203
|
+
if (typeof result.url !== "string" || !safeImageUrl(result.url) || !result.url.startsWith("/") || !storedMediaFile.test(result.url.split("/").pop() ?? "") || !/\.(jpg|png|webp)$/.test(result.url)) {
|
|
192
204
|
throw new Error("Invalid imported photo URL");
|
|
193
205
|
}
|
|
194
206
|
return result.url;
|
|
@@ -1,4 +1,14 @@
|
|
|
1
1
|
import { mediaMetadata } from "../../media-types";
|
|
2
|
+
import type { ICONS } from "./icons";
|
|
3
|
+
|
|
4
|
+
const FILE_ICONS: Record<string, keyof typeof ICONS> = {
|
|
5
|
+
pdf: "filePdf", txt: "fileTxt", doc: "fileDoc", docx: "fileDoc", ppt: "filePpt", pptx: "filePpt",
|
|
6
|
+
Documents: "fileText", Spreadsheets: "fileXls", Archives: "fileZip", Audio: "fileAudio", Video: "fileVideo",
|
|
7
|
+
};
|
|
8
|
+
export function mediaIcon(file: string): keyof typeof ICONS {
|
|
9
|
+
const extension = file.split(".").pop()?.toLowerCase() ?? "";
|
|
10
|
+
return FILE_ICONS[extension] ?? FILE_ICONS[mediaMetadata(file)?.category ?? ""] ?? "file";
|
|
11
|
+
}
|
|
2
12
|
|
|
3
13
|
export interface MediaAsset {
|
|
4
14
|
url: string;
|
|
@@ -27,7 +37,7 @@ export function filterMedia(assets: MediaAsset[], search: string, type: string,
|
|
|
27
37
|
&& (!type || asset.type === type || assetCategory(asset) === type) && (!month || modifiedMonth(asset.modified) === month));
|
|
28
38
|
}
|
|
29
39
|
function assetCategory(asset: MediaAsset): string {
|
|
30
|
-
const category = mediaMetadata(asset.
|
|
40
|
+
const category = mediaMetadata(assetFile(asset.url))?.category ?? '';
|
|
31
41
|
if (!category) return 'Other';
|
|
32
42
|
return CURATED.has(category) ? category : 'Other';
|
|
33
43
|
}
|
|
@@ -26,7 +26,7 @@ const prefs = (theme: "system" | "light" | "dark" = "system", language: "en" | "
|
|
|
26
26
|
const DEFAULTS: RigelSettings = {
|
|
27
27
|
...prefs(),
|
|
28
28
|
apiKeys: {},
|
|
29
|
-
userPref: { previewSplit: 0.5, mediaView: "grid", sidebarCollapsed: false, editorExperience: "
|
|
29
|
+
userPref: { previewSplit: 0.5, mediaView: "grid", sidebarCollapsed: false, editorExperience: "inline" },
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
const PROVIDERS = ["pexels", "pixabay", "unsplash"] as const;
|
|
@@ -46,7 +46,7 @@ export function getSettings(): RigelSettings {
|
|
|
46
46
|
apiKeys: mergedKeys,
|
|
47
47
|
userPref: {
|
|
48
48
|
sidebarCollapsed: saved?.userPref?.sidebarCollapsed === true,
|
|
49
|
-
editorExperience: saved?.userPref?.editorExperience === "
|
|
49
|
+
editorExperience: saved?.userPref?.editorExperience === "sidebar" ? "sidebar" : DEFAULTS.userPref.editorExperience,
|
|
50
50
|
previewSplit: typeof saved?.userPref?.previewSplit === "number" && Number.isFinite(saved.userPref.previewSplit) && saved.userPref.previewSplit >= 0.2 && saved.userPref.previewSplit <= 0.8
|
|
51
51
|
? saved.userPref.previewSplit
|
|
52
52
|
: typeof saved?.previewSplit === "number" && Number.isFinite(saved.previewSplit) && saved.previewSplit >= 0.2 && saved.previewSplit <= 0.8
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
-
export
|
|
1
|
+
export type TooltipSide = "top" | "bottom" | "left" | "right";
|
|
2
|
+
export type TooltipAlign = "start" | "center" | "end";
|
|
3
|
+
|
|
4
|
+
export function tooltipPosition(rect: Pick<DOMRect, "left" | "right" | "top" | "bottom">, width: number, height: number, viewportWidth: number, viewportHeight: number, position: TooltipSide = "top", align: TooltipAlign = "center") {
|
|
5
|
+
const sides: TooltipSide[] = ["top", "bottom", "left", "right"];
|
|
6
|
+
const preferred = sides.includes(position) ? position : "top";
|
|
7
|
+
const opposite: Record<TooltipSide, TooltipSide> = { top: "bottom", bottom: "top", left: "right", right: "left" };
|
|
8
|
+
const candidates = [preferred, opposite[preferred], ...sides.filter(side => side !== preferred && side !== opposite[preferred])];
|
|
9
|
+
const space = { top: rect.top - 8, bottom: viewportHeight - rect.bottom - 8, left: rect.left - 8, right: viewportWidth - rect.right - 8 };
|
|
10
|
+
const needed = { top: height + 8, bottom: height + 8, left: width + 8, right: width + 8 };
|
|
11
|
+
const side = candidates.find(side => space[side] >= needed[side]) ?? candidates.reduce((best, side) => space[side] / needed[side] > space[best] / needed[best] ? side : best);
|
|
12
|
+
const vertical = side === "top" || side === "bottom";
|
|
13
|
+
const start = vertical ? rect.left : rect.top, end = vertical ? rect.right : rect.bottom, size = vertical ? width : height;
|
|
14
|
+
const cross = align === "start" ? start : align === "end" ? end - size : (start + end - size) / 2;
|
|
15
|
+
const left = vertical ? cross : side === "left" ? rect.left - width - 8 : rect.right + 8;
|
|
16
|
+
const top = !vertical ? cross : side === "top" ? rect.top - height - 8 : rect.bottom + 8;
|
|
2
17
|
return {
|
|
3
|
-
left: Math.max(8, Math.min(
|
|
4
|
-
top: Math.max(8, Math.min(
|
|
18
|
+
left: Math.max(8, Math.min(left, viewportWidth - width - 8)),
|
|
19
|
+
top: Math.max(8, Math.min(top, viewportHeight - height - 8)),
|
|
20
|
+
side,
|
|
5
21
|
};
|
|
6
22
|
}
|
|
7
23
|
|
|
@@ -41,7 +57,10 @@ function createTooltips() {
|
|
|
41
57
|
if (!anchor || !tip || !view) return;
|
|
42
58
|
if (!anchor.isConnected || !anchor.checkVisibility() || !anchor.dataset.tooltip) { hide(); return; }
|
|
43
59
|
if (tip.textContent !== anchor.dataset.tooltip) tip.textContent = anchor.dataset.tooltip;
|
|
44
|
-
const
|
|
60
|
+
const preferred = anchor.closest<HTMLElement>("[data-tooltip-position]")?.dataset.tooltipPosition as TooltipSide | undefined;
|
|
61
|
+
const align = anchor.closest<HTMLElement>("[data-tooltip-align]")?.dataset.tooltipAlign as TooltipAlign | undefined;
|
|
62
|
+
const position = tooltipPosition(anchor.getBoundingClientRect(), tip.offsetWidth, tip.offsetHeight, view.innerWidth, view.innerHeight, preferred ?? (anchor.closest(".body-toolbar, .visual-toolbar") ? "bottom" : "top"), align);
|
|
63
|
+
tip.dataset.side = position.side;
|
|
45
64
|
tip.style.left = `${position.left}px`;
|
|
46
65
|
tip.style.top = `${position.top}px`;
|
|
47
66
|
frame = view.requestAnimationFrame(update);
|
package/src/editor/styles.css
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
.rigel, .rigel *, .rigel *::before, .rigel *::after { box-sizing: border-box; }
|
|
2
|
-
.rigel .rigel-tooltip { position: fixed; pointer-events: auto; inset: auto; margin: 0; padding:
|
|
2
|
+
.rigel .rigel-tooltip { position: fixed; pointer-events: auto; inset: auto; margin: 0; padding: 4px 10px; max-width: min(17.5rem, calc(100vw - 16px)); max-height: calc(100dvh - 16px); overflow: auto; border: 0; border-radius: 0; background: var(--text); color: var(--panel); box-shadow: 0 2px 8px rgb(0 0 0 / 0.15); font: 500 11px/1.4 -apple-system, BlinkMacSystemFont, system-ui, sans-serif; overflow-wrap: anywhere; text-align: start; white-space: normal; }
|
|
3
3
|
.rigel .rigel-tooltip::backdrop { background: transparent; pointer-events: none; }
|
|
4
|
-
@media (forced-colors: active) { .rigel .rigel-tooltip { background: Canvas; color: CanvasText; border
|
|
4
|
+
@media (forced-colors: active) { .rigel .rigel-tooltip { background: Canvas; color: CanvasText; border: 1px solid CanvasText; } }
|
|
5
5
|
.rigel {
|
|
6
6
|
--rigel-50: #EFFBFF;
|
|
7
7
|
--rigel-100: #D9F4FF;
|
|
@@ -840,9 +840,9 @@
|
|
|
840
840
|
.rigel .outlined:hover:not(:disabled), .rigel .image-picker input::file-selector-button:hover { background: var(--surface-raised); }
|
|
841
841
|
.rigel .outlined:disabled, .rigel .image-picker .tabs button:disabled, .rigel .image-picker .thumb:disabled { opacity: 0.5; cursor: default; }
|
|
842
842
|
.rigel .image-picker input::file-selector-button { margin-right: 0.75rem; }
|
|
843
|
-
.rigel.image-dialog { width: 100%; height: 100%; max-width: none; max-height: none; margin: 0; padding: 1rem; border: none; }
|
|
844
|
-
.rigel.image-dialog:not([open]) { display: none; }
|
|
845
|
-
.rigel.image-dialog::backdrop { background: transparent; }
|
|
843
|
+
.rigel:is(.image-dialog, .media-insert) { width: 100%; height: 100%; max-width: none; max-height: none; margin: 0; padding: 1rem; border: none; }
|
|
844
|
+
.rigel:is(.image-dialog, .media-insert):not([open]) { display: none; }
|
|
845
|
+
.rigel:is(.image-dialog, .media-insert)::backdrop { background: transparent; }
|
|
846
846
|
.rigel .image-picker { width: min(760px, 100%); }
|
|
847
847
|
.rigel .media-head, .rigel .media-tools, .rigel .media-actions { display: flex; align-items: center; gap: 0.75rem; }
|
|
848
848
|
.rigel .media-head { justify-content: space-between; margin: calc(1rem * -1) calc(1.25rem * -1) 0; padding: 1rem 1.25rem; border-bottom: 1px solid var(--border); }
|
|
@@ -860,8 +860,10 @@
|
|
|
860
860
|
.rigel .media-table :is(th, td) { padding: 0.625rem; overflow-wrap: anywhere; }
|
|
861
861
|
.rigel .media-table th:first-child { width: 48%; }
|
|
862
862
|
.rigel .media-file { display: flex; align-items: center; gap: 0.625rem; text-align: left; width: 100%; }
|
|
863
|
-
.rigel .media-file
|
|
863
|
+
.rigel .media-file .media-thumbnail { display: flex; align-items: center; justify-content: center; width: 48px; height: 40px; flex: 0 0 auto; overflow: hidden; background: var(--surface-raised); color: var(--text-muted); border: 1px solid var(--border-subtle); }
|
|
864
|
+
.rigel .media-thumbnail img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
864
865
|
.rigel .media-file-icon { display: flex; min-height: 120px; flex-direction: column; align-items: center; justify-content: center; gap: 0.75rem; padding: 1rem; color: var(--text-muted); overflow-wrap: anywhere; }
|
|
866
|
+
.rigel .thumb .media-file-icon { height: 100%; min-height: 0; background: var(--surface-raised); }
|
|
865
867
|
.rigel .media-file span { min-width: 0; overflow-wrap: anywhere; }
|
|
866
868
|
.rigel .media-file[aria-pressed=true] { color: var(--on-accent-soft); background: var(--accent-soft); }
|
|
867
869
|
.rigel :is(.image-picker, .media-library) .thumb[aria-pressed=true] { border-color: var(--accent); }
|
|
@@ -871,12 +873,16 @@
|
|
|
871
873
|
.rigel .media-table th:first-child { width: 40%; }
|
|
872
874
|
.rigel .media-file { flex-direction: column; align-items: flex-start; }
|
|
873
875
|
}
|
|
876
|
+
.rigel .media-feedback { position: absolute; right: 0; bottom: calc(100% + 0.25rem); max-width: 100%; margin: 0; color: var(--success); font-size: 0.8125rem; text-align: right; }
|
|
874
877
|
.rigel .media-actions {
|
|
878
|
+
position: relative;
|
|
875
879
|
justify-content: flex-end; gap: 0.75rem;
|
|
876
880
|
margin-top: 1rem; padding-top: 0.75rem; border-top: 1px solid var(--border);
|
|
877
881
|
}
|
|
878
882
|
.rigel .media-selection { margin-top: 1rem; }
|
|
879
|
-
.rigel .media-preview > img { display: block; max-width: 100%; max-height: 300px; margin: 0 auto 1rem; }
|
|
883
|
+
.rigel .media-preview > :is(img, video) { display: block; max-width: 100%; max-height: 300px; margin: 0 auto 1rem; }
|
|
884
|
+
.rigel .media-preview > video { width: 100%; object-fit: contain; background: #000; }
|
|
885
|
+
.rigel .media-preview > iframe { display: block; width: 100%; height: min(60dvh, 36rem); border: 1px solid var(--border-subtle); }
|
|
880
886
|
.rigel .media-button-actions { display: flex; align-items: center; gap: 0.75rem; margin-left: auto; flex-shrink: 0; }
|
|
881
887
|
.rigel .media-actions.with-media-path { flex-wrap: wrap; }
|
|
882
888
|
.rigel .media-download { flex-shrink: 0; white-space: nowrap; text-decoration: none; }
|
|
@@ -886,16 +892,19 @@
|
|
|
886
892
|
.rigel .media-meta { border-top: 1px solid var(--border-subtle); padding-top: 0.5rem; }
|
|
887
893
|
.rigel .media-meta label { margin-top: 0.75rem; }
|
|
888
894
|
.rigel .media-details { min-width: 0; }
|
|
895
|
+
.rigel .media-details .hint { overflow-wrap: anywhere; }
|
|
889
896
|
.rigel .image-picker:has(.media-editing) { width: min(1040px, 100%); }
|
|
890
897
|
.rigel .media-selection.media-editing { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 1.5rem; }
|
|
891
898
|
.rigel .media-editing .media-preview { min-width: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
|
892
|
-
.rigel .media-editing .media-preview > img { max-height: 60dvh; }
|
|
899
|
+
.rigel .media-editing .media-preview > :is(img, video) { max-height: 60dvh; }
|
|
893
900
|
.rigel .media-editing .media-meta { border-top: 0; padding-top: 0; }
|
|
894
901
|
.rigel .media-editing .media-meta label:first-child { margin-top: 0; }
|
|
895
902
|
.rigel .media-editing > .media-actions { grid-column: 1 / -1; margin-top: 0; }
|
|
896
903
|
@media (max-width: 700px) {
|
|
897
904
|
.rigel .media-selection.media-editing { grid-template-columns: minmax(0, 1fr); }
|
|
898
|
-
.rigel .media-editing .media-preview > img { max-height: 35dvh; }
|
|
905
|
+
.rigel .media-editing .media-preview > :is(img, video) { max-height: 35dvh; }
|
|
906
|
+
.rigel .media-editing .media-preview > iframe { height: 35dvh; }
|
|
907
|
+
.rigel .with-media-path .media-path { flex-basis: 100%; }
|
|
899
908
|
.rigel .with-media-path .media-button-actions { flex-basis: 100%; justify-content: flex-end; }
|
|
900
909
|
}
|
|
901
910
|
.rigel .image-picker input[type=file] { width: 100%; color: var(--text); }
|
|
@@ -927,12 +936,15 @@
|
|
|
927
936
|
display: grid; grid-template-columns: repeat(auto-fill, minmax(min(160px, 100%), 1fr)); gap: 1rem; margin-top: 1rem;
|
|
928
937
|
}
|
|
929
938
|
.rigel :is(.image-picker, .media-library) .thumb {
|
|
939
|
+
position: relative;
|
|
930
940
|
border: 2px solid transparent; border-radius: 0; overflow: hidden;
|
|
931
941
|
padding: 0; cursor: pointer; background: none; aspect-ratio: 3 / 2;
|
|
932
942
|
transition: border-color 0.1s, transform 0.1s;
|
|
933
943
|
}
|
|
934
944
|
.rigel :is(.image-picker, .media-library) .thumb:hover { border-color: var(--accent); transform: translateY(-1px); }
|
|
935
|
-
.rigel :is(.image-picker, .media-library) .thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
945
|
+
.rigel :is(.image-picker, .media-library) .thumb :is(img, video) { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
946
|
+
.rigel :is(.image-picker, .media-library) .thumb video { pointer-events: none; }
|
|
947
|
+
.rigel .media-extension { position: absolute; top: 0.25rem; right: 0.25rem; padding: 0.0625rem 0.25rem; border: 1px solid var(--border-control); background: var(--panel); color: var(--text); font-size: 0.625rem; font-weight: 600; line-height: 1.2; pointer-events: none; }
|
|
936
948
|
.rigel :is(.image-picker, .media-library) .grid figure { margin: 0; min-width: 0; }
|
|
937
949
|
.rigel :is(.image-picker, .media-library) .grid .thumb { width: 100%; }
|
|
938
950
|
.rigel :is(.image-picker, .media-library) .grid figcaption { font-size: 0.75rem; color: var(--text-2); overflow-wrap: anywhere; }
|
|
@@ -1154,7 +1166,7 @@
|
|
|
1154
1166
|
.rigel.rigel-inline { display: block; position: fixed; top: 0; left: 0; width: 100%; min-height: 0; height: 0; padding: 0; background: transparent; pointer-events: none; z-index: 120; }
|
|
1155
1167
|
.rigel.rigel-inline .block-editor { margin: 0; border: 0; }
|
|
1156
1168
|
.rigel.rigel-inline .block-editor > [role="alert"] { position: fixed; inset: 8px 8px auto; padding: 0.75rem; background: var(--danger-soft); color: var(--danger); pointer-events: auto; }
|
|
1157
|
-
.rigel.rigel-inline :is(.body-toolbar, .block-handle, .block-insert, .block-menu, .block-slash, .markdown-link, .block-markdown-view) { pointer-events: auto; }
|
|
1169
|
+
.rigel.rigel-inline :is(.body-toolbar, .block-handle, .block-insert, .block-menu, .block-slash, .markdown-link, .block-markdown-view, .media-insert, .image-dialog) { pointer-events: auto; }
|
|
1158
1170
|
.rigel.rigel-inline .block-editor .body-toolbar, .rigel .block-editor .body-toolbar.image-toolbar { position: fixed; inset: auto; width: max-content; max-width: calc(100vw - 16px); margin: 0; padding: 0.25rem; gap: 0.25rem; border: 1px solid var(--border-control); box-shadow: var(--shadow-lg); z-index: 120; }
|
|
1159
1171
|
.rigel.rigel-inline :where(.block-editor .body-toolbar) > button { width: 32px; height: 32px; padding: 0; background: transparent; color: var(--text); border: 0; }
|
|
1160
1172
|
.rigel.rigel-inline .block-handle { position: fixed; width: 24px; height: 28px; }
|
|
@@ -1209,7 +1221,7 @@
|
|
|
1209
1221
|
.rigel .block-table-controls button:hover:not(:disabled) { background: var(--accent-soft); color: var(--on-accent-soft); }
|
|
1210
1222
|
.rigel .block-table-controls button:disabled { opacity: 0.35; cursor: default; }
|
|
1211
1223
|
.rigel .block-table-controls label.inline { display: flex; align-items: center; gap: 0.5rem; margin: 0.25rem 0 0; padding: 0.5rem; border-top: 1px solid var(--border-subtle); font-size: 0.8125rem; }
|
|
1212
|
-
|
|
1224
|
+
|
|
1213
1225
|
.rigel .block-content > p:has(> br:only-child)::before { content: "Type / to choose a block"; position: absolute; color: var(--text-2); pointer-events: none; }
|
|
1214
1226
|
.rigel .block-handle { position: absolute; left: 2px; width: 28px; height: 32px; padding: 0; display: flex; align-items: center; justify-content: center; background: var(--panel); border: 1px solid var(--border-control); color: var(--text); cursor: grab; touch-action: none; }
|
|
1215
1227
|
.rigel .block-insert { position: fixed; z-index: 119; display: flex; align-items: center; justify-content: center; width: 28px; height: 28px; padding: 0; background: var(--panel); border: 1px solid var(--border-control); color: var(--text); cursor: pointer; }
|
|
@@ -1303,3 +1315,8 @@
|
|
|
1303
1315
|
.rigel main.visual-pane { padding: 0.75rem; }
|
|
1304
1316
|
.rigel .visual-editor.with-panel { grid-template-rows: minmax(0, 1fr) 50%; }
|
|
1305
1317
|
}
|
|
1318
|
+
|
|
1319
|
+
.rigel .media-menu > button { display: flex; align-items: center; gap: 0.5rem; }
|
|
1320
|
+
.rigel .media-insert .media-button-actions { justify-content: space-between; margin: 1rem 0 0; }
|
|
1321
|
+
|
|
1322
|
+
.rigel .block-content figure.media-video video, .rigel .block-content figure.media-embed iframe { display: block; width: 100%; aspect-ratio: 16 / 9; border: 0; background: #000; }
|
package/src/env.d.ts
CHANGED
|
@@ -17,6 +17,6 @@ declare module "virtual:rigel-content" {
|
|
|
17
17
|
|
|
18
18
|
declare module "virtual:rigel-config" {
|
|
19
19
|
import type { RigelConfig } from "@rigel-cms/rigel";
|
|
20
|
-
const config: Pick<RigelConfig, "copyDir" | "backend" | "demo" | "collections" | "menus" | "mediaDir" | "schema" | "preview"> & { publicDir?: string; root?: string };
|
|
20
|
+
const config: Pick<RigelConfig, "copyDir" | "backend" | "jsonOutput" | "demo" | "collections" | "menus" | "mediaDir" | "maxFileSize" | "schema" | "preview"> & { publicDir?: string; root?: string };
|
|
21
21
|
export default config;
|
|
22
22
|
}
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { AstroIntegration } from "astro";
|
|
|
2
2
|
import { fileURLToPath } from "node:url";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { lstat, readFile } from "node:fs/promises";
|
|
5
|
-
import {
|
|
5
|
+
import { mediaHeaders, safeMediaName, storedMediaFile } from "./media-types";
|
|
6
6
|
import { mediaDirectory } from "./routes/media-files";
|
|
7
7
|
import { parseConfig, type RigelConfig } from "./config";
|
|
8
8
|
import { rigelManifestPlugin } from "./vite/manifest-plugin";
|
|
@@ -48,10 +48,7 @@ export function rigel(options: Partial<RigelConfig> = {}): AstroIntegration {
|
|
|
48
48
|
}
|
|
49
49
|
const file = path.join(directory, filename), info = await lstat(file);
|
|
50
50
|
if (!info.isFile() || info.isSymbolicLink()) throw new Error();
|
|
51
|
-
const
|
|
52
|
-
response.setHeader("Content-Type", metadata.type);
|
|
53
|
-
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
54
|
-
response.setHeader("Content-Disposition", `${metadata.category === "Images" ? "inline" : "attachment"}; filename="${filename}"`);
|
|
51
|
+
for (const [name, value] of Object.entries(mediaHeaders(filename, new URL(request.url ?? "/", "http://localhost").searchParams.get("preview") === "1"))) response.setHeader(name, value);
|
|
55
52
|
response.end(request.method === "HEAD" ? undefined : await readFile(file));
|
|
56
53
|
} catch { response.statusCode = 404; response.end("Not found"); }
|
|
57
54
|
});
|
|
@@ -60,7 +57,7 @@ export function rigel(options: Partial<RigelConfig> = {}): AstroIntegration {
|
|
|
60
57
|
...(cfg.backend === "local" && command === "dev" ? {
|
|
61
58
|
server: { watch: { ignored: [(file: string) =>
|
|
62
59
|
path.dirname(file) === path.resolve(fileURLToPath(config.publicDir), cfg.mediaDir ?? "rigel-media")
|
|
63
|
-
&&
|
|
60
|
+
&& storedMediaFile.test(path.basename(file))
|
|
64
61
|
] } },
|
|
65
62
|
} : {}),
|
|
66
63
|
},
|
|
@@ -119,6 +116,7 @@ export type { MenuItem, MenuDocument } from "./manifest/menu";
|
|
|
119
116
|
export { defineCopy } from "./manifest/types";
|
|
120
117
|
export type { CopyField, CopyGroup, CopyPage, Manifest, CollectionField } from "./manifest/types";
|
|
121
118
|
export type { RigelConfig, CollectionConfig } from "./config";
|
|
119
|
+
export type { JsonOutput } from "./json-output";
|
|
122
120
|
export type { ReferenceOption } from "./manifest/references";
|
|
123
121
|
export type { SidebarEntry, SidebarTarget, SidebarIconName } from "./manifest/navigation";
|
|
124
122
|
export { defineSchema, defineDocument, defineObject, defineField } from "./schema/types";
|
package/src/manifest/types.ts
CHANGED
package/src/media-types.ts
CHANGED
|
@@ -3,11 +3,24 @@ for (const [category, entries] of Object.entries({
|
|
|
3
3
|
Images: { jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png', webp: 'image/webp', gif: 'image/gif', avif: 'image/avif' },
|
|
4
4
|
Audio: { mp3: 'audio/mpeg', wav: 'audio/wav', ogg: 'audio/ogg', m4a: 'audio/mp4' },
|
|
5
5
|
Video: { mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime' },
|
|
6
|
-
Documents: { pdf: 'application/pdf', doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', rtf: 'application/rtf', odt: 'application/vnd.oasis.opendocument.text', ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' },
|
|
6
|
+
Documents: { txt: 'text/plain', pdf: 'application/pdf', doc: 'application/msword', docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', rtf: 'application/rtf', odt: 'application/vnd.oasis.opendocument.text', ppt: 'application/vnd.ms-powerpoint', pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation' },
|
|
7
7
|
Spreadsheets: { xls: 'application/vnd.ms-excel', xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', csv: 'text/csv', ods: 'application/vnd.oasis.opendocument.spreadsheet' },
|
|
8
8
|
Archives: { zip: 'application/zip', gz: 'application/gzip', tar: 'application/x-tar', '7z': 'application/x-7z-compressed', rar: 'application/vnd.rar' },
|
|
9
9
|
})) for (const [extension, type] of Object.entries(entries)) MEDIA_TYPES[extension] = { type, category };
|
|
10
10
|
|
|
11
|
+
export function mediaHeaders(filename: string, preview = false) {
|
|
12
|
+
const metadata = mediaMetadata(filename)!;
|
|
13
|
+
const inline = ['Images', 'Video'].includes(metadata.category) || (preview && ['pdf', 'txt'].includes(metadata.extension));
|
|
14
|
+
return {
|
|
15
|
+
'Content-Type': metadata.type === 'text/plain' ? 'text/plain; charset=utf-8' : metadata.type,
|
|
16
|
+
'Content-Disposition': `${inline ? 'inline' : 'attachment'}; filename="${filename}"`,
|
|
17
|
+
'X-Content-Type-Options': 'nosniff',
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const storedMediaFile = /^(?:[a-f0-9]{64}|[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12})\.[a-z0-9]+$/;
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_MAX_FILE_SIZE = 2 * 1024 * 1024;
|
|
11
24
|
export const MEDIA_CATEGORIES = ['Images', 'Audio', 'Video', 'Documents', 'Spreadsheets', 'Archives', 'Other'];
|
|
12
25
|
export function mediaMetadata(name: string) {
|
|
13
26
|
const extension = name.split('.').pop()?.toLowerCase() ?? '';
|
package/src/preview.ts
CHANGED
|
@@ -3,6 +3,7 @@ import DOMPurify from "dompurify";
|
|
|
3
3
|
import { pageRoute } from "./schema/paths";
|
|
4
4
|
import { slugify } from "./slugs";
|
|
5
5
|
import { renderArticle } from "./article-render";
|
|
6
|
+
import { createEmbedPreview } from "./editor/lib/embed-preview";
|
|
6
7
|
import { safePreviewUrl, previewBaseUrl } from "./preview-urls";
|
|
7
8
|
export { safePreviewUrl } from "./preview-urls";
|
|
8
9
|
|
|
@@ -158,11 +159,14 @@ export function patchPreview(doc: Document, values: Record<string, unknown>, bod
|
|
|
158
159
|
const source = blocks ? JSON.stringify(result.value) : typeof result.value === "string" ? result.value : "";
|
|
159
160
|
if (rendered.get(el) === source) continue;
|
|
160
161
|
rendered.set(el, source);
|
|
161
|
-
const fragment = DOMPurify.sanitize(blocks ? (result.value ? renderArticle(result.value as import("./article-body").ArticleBody) : "") : markdown.render(source), {
|
|
162
|
+
const fragment = DOMPurify.sanitize(blocks ? (result.value ? renderArticle(result.value as import("./article-body").ArticleBody, { preview: true }) : "") : markdown.render(source), {
|
|
162
163
|
RETURN_DOM_FRAGMENT: true,
|
|
163
|
-
ALLOWED_TAGS: ["p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "blockquote", "aside", "figure", "figcaption", "pre", "code", "strong", "em", "s", "u", "mark", "sub", "sup", "a", "img", "br", "hr", "table", "thead", "tbody", "tr", "th", "td"],
|
|
164
|
-
ALLOWED_ATTR: ["href", "src", "alt", "title", "start", "scope"],
|
|
164
|
+
ALLOWED_TAGS: ["p", "h1", "h2", "h3", "h4", "h5", "h6", "ul", "ol", "li", "blockquote", "aside", "figure", "figcaption", "pre", "code", "strong", "em", "s", "u", "mark", "sub", "sup", "a", "img", "br", "hr", "table", "thead", "tbody", "tr", "th", "td", ...(blocks ? ["video"] : [])],
|
|
165
|
+
ALLOWED_ATTR: ["href", "src", "alt", "title", "start", "scope", ...(blocks ? ["controls", "muted", "preload", "playsinline", "poster", "class"] : [])],
|
|
165
166
|
});
|
|
167
|
+
if (blocks) for (const marker of fragment.querySelectorAll<HTMLElement>("[data-rigel-embed]")) {
|
|
168
|
+
marker.replaceWith(createEmbedPreview({ provider: marker.dataset.rigelEmbed, id: marker.dataset.rigelVideoId, title: marker.dataset.rigelVideoTitle }, doc));
|
|
169
|
+
}
|
|
166
170
|
for (const link of fragment.querySelectorAll("[href], [src]")) {
|
|
167
171
|
for (const attr of ["href", "src"]) if (link.hasAttribute(attr) && !safePreviewUrl(link.getAttribute(attr)!, attr === "src")) link.removeAttribute(attr);
|
|
168
172
|
}
|
|
@@ -69,7 +69,7 @@ const handle: APIRoute = async ({ request, params }) => {
|
|
|
69
69
|
const documents = await readDocuments(guarded.schema, guarded.root, config.preview);
|
|
70
70
|
const next = { ...data, _id: id, _type: target.type.name };
|
|
71
71
|
validateDocument(guarded.schema, target.type.name, target.slug, next, documents);
|
|
72
|
-
await writeDocument(guarded.schema, guarded.root, target.type.name, target.slug, next, request.method === "POST" ? "create" : "update", config.preview);
|
|
72
|
+
await writeDocument(guarded.schema, guarded.root, target.type.name, target.slug, next, request.method === "POST" ? "create" : "update", config.preview, config.jsonOutput);
|
|
73
73
|
return new Response(null, { status: 204 });
|
|
74
74
|
} catch (error) {
|
|
75
75
|
if (error instanceof DocumentStoreError) return new Response(error.message, { status: error.status });
|