@rigel-cms/rigel 0.1.4-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/content.ts +3 -1
- package/src/editor/App.svelte +114 -25
- 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 +9 -4
- package/src/editor/lib/ImagePicker.svelte +141 -47
- package/src/editor/lib/MediaPicker.svelte +73 -0
- package/src/editor/lib/PreviewFrame.svelte +64 -22
- package/src/editor/lib/PublishDialog.svelte +65 -0
- package/src/editor/lib/SchemaPanel.svelte +6 -1
- 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 +220 -20
- 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/slugify.ts +1 -15
- package/src/editor/lib/tooltip.ts +23 -4
- package/src/editor/styles.css +44 -13
- 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 +14 -3
- package/src/routes/local-documents.ts +4 -4
- package/src/routes/local-media.ts +33 -28
- package/src/routes/local-menu.ts +2 -1
- package/src/routes/local-post.ts +18 -5
- package/src/routes/media-files.ts +36 -1
- package/src/routes/media-validation.ts +2 -2
- package/src/schema/paths.ts +3 -1
- package/src/schema/store.ts +24 -3
- package/src/slugs.ts +46 -0
- package/src/vite/manifest-plugin.ts +9 -2
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,15 +1 @@
|
|
|
1
|
-
export
|
|
2
|
-
const base = input
|
|
3
|
-
.normalize("NFKD")
|
|
4
|
-
.replace(/[̀-ͯ]/g, "") // combining marks → strip (é→e)
|
|
5
|
-
.toLowerCase()
|
|
6
|
-
.trim()
|
|
7
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
8
|
-
.replace(/^-+|-+$/g, "")
|
|
9
|
-
.replace(/-{2,}/g, "-");
|
|
10
|
-
if (!base) return "";
|
|
11
|
-
const existing = new Set(existingSlugs);
|
|
12
|
-
let slug = base;
|
|
13
|
-
for (let suffix = 2; existing.has(slug); suffix++) slug = `${base}-${suffix}`;
|
|
14
|
-
return slug;
|
|
15
|
-
}
|
|
1
|
+
export { slugify } from "../../slugs";
|
|
@@ -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;
|
|
@@ -175,6 +175,8 @@
|
|
|
175
175
|
.rigel .rail-scroll > button:hover { background: var(--surface-raised); color: var(--text); }
|
|
176
176
|
.rigel .sidebar-asset-icon { display: inline-block; width: 18px; height: 18px; flex-shrink: 0; background-color: currentColor; mask-image: var(--sidebar-icon); mask-position: center; mask-size: contain; mask-repeat: no-repeat; }
|
|
177
177
|
.rigel .rail-scroll > button { gap: 0.625rem; }
|
|
178
|
+
.rigel aside > button.sidebar-publish { gap: 0.625rem; width: calc(100% - 1.5rem); min-height: 44px; margin: 0 0.75rem 0.5rem; padding: 0.625rem; border: 1px solid var(--border-control); color: var(--text); }
|
|
179
|
+
.rigel aside > button.sidebar-publish:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
178
180
|
.rigel aside > button.sidebar-toggle { gap: 0.625rem; width: calc(100% - 1.5rem); margin: 0 0.75rem 0.5rem; padding: 0.625rem; font-size: 0.75rem; }
|
|
179
181
|
@media (min-width: 768px) {
|
|
180
182
|
.rigel.sidebar-collapsed { --sidebar-w: 64px; }
|
|
@@ -186,6 +188,7 @@
|
|
|
186
188
|
.rigel.sidebar-collapsed .user-btn > .user-menu-icon { display: none; }
|
|
187
189
|
.rigel.sidebar-collapsed .rail-scroll > button,
|
|
188
190
|
.rigel.sidebar-collapsed .search-trigger,
|
|
191
|
+
.rigel.sidebar-collapsed .sidebar-publish,
|
|
189
192
|
.rigel.sidebar-collapsed .sidebar-toggle { justify-content: center; }
|
|
190
193
|
.rigel.sidebar-collapsed .sidebar-heading { height: 1px; font-size: 0; padding: 0; margin: 0.75rem 0.625rem; border-top: 1px solid var(--border-subtle); }
|
|
191
194
|
.rigel.sidebar-collapsed .aside-foot { padding: 0.75rem 0.5rem; }
|
|
@@ -365,7 +368,18 @@
|
|
|
365
368
|
.rigel .preview-heading span, .rigel .site-preview small { color: var(--text-muted); font-size: 0.75rem; }
|
|
366
369
|
.rigel .site-preview p { overflow-wrap: anywhere; font-size: 0.8125rem; }
|
|
367
370
|
.rigel .site-preview .preview-error { color: var(--danger); }
|
|
368
|
-
.rigel .
|
|
371
|
+
.rigel .preview-frame { position: relative; display: flex; flex-direction: column; flex: 1; min-height: 0; min-width: 0; background: var(--panel); }
|
|
372
|
+
.rigel .preview-frame iframe { opacity: 0; visibility: hidden; transition: none; }
|
|
373
|
+
.rigel .preview-frame iframe.revealed { opacity: 1; visibility: visible; transition: opacity 200ms ease-out; }
|
|
374
|
+
.rigel .preview-loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 0.625rem; color: var(--text); background: var(--panel); font-size: 0.8125rem; }
|
|
375
|
+
.rigel .preview-spinner { width: 1rem; height: 1rem; border: 2px solid var(--border); border-top-color: currentColor; border-radius: 50%; animation: preview-spin 800ms linear infinite; }
|
|
376
|
+
@keyframes preview-spin { to { transform: rotate(360deg); } }
|
|
377
|
+
@media (prefers-reduced-motion: reduce) {
|
|
378
|
+
.rigel .preview-spinner { animation: none; }
|
|
379
|
+
.rigel .preview-frame iframe.revealed { transition: none; }
|
|
380
|
+
}
|
|
381
|
+
.rigel .site-preview .preview-frame { min-height: 300px; border: 1px solid var(--border); }
|
|
382
|
+
.rigel .site-preview iframe { flex: 1; width: 100%; min-height: 0; border: 0; background: white; }
|
|
369
383
|
.rigel .site-preview small { padding-top: 0.75rem; }
|
|
370
384
|
.rigel .preview-retry { align-self: start; }
|
|
371
385
|
@media (max-width: 1050px) {
|
|
@@ -826,9 +840,9 @@
|
|
|
826
840
|
.rigel .outlined:hover:not(:disabled), .rigel .image-picker input::file-selector-button:hover { background: var(--surface-raised); }
|
|
827
841
|
.rigel .outlined:disabled, .rigel .image-picker .tabs button:disabled, .rigel .image-picker .thumb:disabled { opacity: 0.5; cursor: default; }
|
|
828
842
|
.rigel .image-picker input::file-selector-button { margin-right: 0.75rem; }
|
|
829
|
-
.rigel.image-dialog { width: 100%; height: 100%; max-width: none; max-height: none; margin: 0; padding: 1rem; border: none; }
|
|
830
|
-
.rigel.image-dialog:not([open]) { display: none; }
|
|
831
|
-
.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; }
|
|
832
846
|
.rigel .image-picker { width: min(760px, 100%); }
|
|
833
847
|
.rigel .media-head, .rigel .media-tools, .rigel .media-actions { display: flex; align-items: center; gap: 0.75rem; }
|
|
834
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); }
|
|
@@ -846,8 +860,10 @@
|
|
|
846
860
|
.rigel .media-table :is(th, td) { padding: 0.625rem; overflow-wrap: anywhere; }
|
|
847
861
|
.rigel .media-table th:first-child { width: 48%; }
|
|
848
862
|
.rigel .media-file { display: flex; align-items: center; gap: 0.625rem; text-align: left; width: 100%; }
|
|
849
|
-
.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; }
|
|
850
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); }
|
|
851
867
|
.rigel .media-file span { min-width: 0; overflow-wrap: anywhere; }
|
|
852
868
|
.rigel .media-file[aria-pressed=true] { color: var(--on-accent-soft); background: var(--accent-soft); }
|
|
853
869
|
.rigel :is(.image-picker, .media-library) .thumb[aria-pressed=true] { border-color: var(--accent); }
|
|
@@ -857,12 +873,16 @@
|
|
|
857
873
|
.rigel .media-table th:first-child { width: 40%; }
|
|
858
874
|
.rigel .media-file { flex-direction: column; align-items: flex-start; }
|
|
859
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; }
|
|
860
877
|
.rigel .media-actions {
|
|
878
|
+
position: relative;
|
|
861
879
|
justify-content: flex-end; gap: 0.75rem;
|
|
862
880
|
margin-top: 1rem; padding-top: 0.75rem; border-top: 1px solid var(--border);
|
|
863
881
|
}
|
|
864
882
|
.rigel .media-selection { margin-top: 1rem; }
|
|
865
|
-
.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); }
|
|
866
886
|
.rigel .media-button-actions { display: flex; align-items: center; gap: 0.75rem; margin-left: auto; flex-shrink: 0; }
|
|
867
887
|
.rigel .media-actions.with-media-path { flex-wrap: wrap; }
|
|
868
888
|
.rigel .media-download { flex-shrink: 0; white-space: nowrap; text-decoration: none; }
|
|
@@ -872,16 +892,19 @@
|
|
|
872
892
|
.rigel .media-meta { border-top: 1px solid var(--border-subtle); padding-top: 0.5rem; }
|
|
873
893
|
.rigel .media-meta label { margin-top: 0.75rem; }
|
|
874
894
|
.rigel .media-details { min-width: 0; }
|
|
895
|
+
.rigel .media-details .hint { overflow-wrap: anywhere; }
|
|
875
896
|
.rigel .image-picker:has(.media-editing) { width: min(1040px, 100%); }
|
|
876
897
|
.rigel .media-selection.media-editing { display: grid; grid-template-columns: minmax(0, 3fr) minmax(0, 2fr); gap: 1.5rem; }
|
|
877
898
|
.rigel .media-editing .media-preview { min-width: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; }
|
|
878
|
-
.rigel .media-editing .media-preview > img { max-height: 60dvh; }
|
|
899
|
+
.rigel .media-editing .media-preview > :is(img, video) { max-height: 60dvh; }
|
|
879
900
|
.rigel .media-editing .media-meta { border-top: 0; padding-top: 0; }
|
|
880
901
|
.rigel .media-editing .media-meta label:first-child { margin-top: 0; }
|
|
881
902
|
.rigel .media-editing > .media-actions { grid-column: 1 / -1; margin-top: 0; }
|
|
882
903
|
@media (max-width: 700px) {
|
|
883
904
|
.rigel .media-selection.media-editing { grid-template-columns: minmax(0, 1fr); }
|
|
884
|
-
.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%; }
|
|
885
908
|
.rigel .with-media-path .media-button-actions { flex-basis: 100%; justify-content: flex-end; }
|
|
886
909
|
}
|
|
887
910
|
.rigel .image-picker input[type=file] { width: 100%; color: var(--text); }
|
|
@@ -913,12 +936,15 @@
|
|
|
913
936
|
display: grid; grid-template-columns: repeat(auto-fill, minmax(min(160px, 100%), 1fr)); gap: 1rem; margin-top: 1rem;
|
|
914
937
|
}
|
|
915
938
|
.rigel :is(.image-picker, .media-library) .thumb {
|
|
939
|
+
position: relative;
|
|
916
940
|
border: 2px solid transparent; border-radius: 0; overflow: hidden;
|
|
917
941
|
padding: 0; cursor: pointer; background: none; aspect-ratio: 3 / 2;
|
|
918
942
|
transition: border-color 0.1s, transform 0.1s;
|
|
919
943
|
}
|
|
920
944
|
.rigel :is(.image-picker, .media-library) .thumb:hover { border-color: var(--accent); transform: translateY(-1px); }
|
|
921
|
-
.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; }
|
|
922
948
|
.rigel :is(.image-picker, .media-library) .grid figure { margin: 0; min-width: 0; }
|
|
923
949
|
.rigel :is(.image-picker, .media-library) .grid .thumb { width: 100%; }
|
|
924
950
|
.rigel :is(.image-picker, .media-library) .grid figcaption { font-size: 0.75rem; color: var(--text-2); overflow-wrap: anywhere; }
|
|
@@ -1140,7 +1166,7 @@
|
|
|
1140
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; }
|
|
1141
1167
|
.rigel.rigel-inline .block-editor { margin: 0; border: 0; }
|
|
1142
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; }
|
|
1143
|
-
.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; }
|
|
1144
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; }
|
|
1145
1171
|
.rigel.rigel-inline :where(.block-editor .body-toolbar) > button { width: 32px; height: 32px; padding: 0; background: transparent; color: var(--text); border: 0; }
|
|
1146
1172
|
.rigel.rigel-inline .block-handle { position: fixed; width: 24px; height: 28px; }
|
|
@@ -1195,7 +1221,7 @@
|
|
|
1195
1221
|
.rigel .block-table-controls button:hover:not(:disabled) { background: var(--accent-soft); color: var(--on-accent-soft); }
|
|
1196
1222
|
.rigel .block-table-controls button:disabled { opacity: 0.35; cursor: default; }
|
|
1197
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; }
|
|
1198
|
-
|
|
1224
|
+
|
|
1199
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; }
|
|
1200
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; }
|
|
1201
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; }
|
|
@@ -1289,3 +1315,8 @@
|
|
|
1289
1315
|
.rigel main.visual-pane { padding: 0.75rem; }
|
|
1290
1316
|
.rigel .visual-editor.with-panel { grid-template-rows: minmax(0, 1fr) 50%; }
|
|
1291
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"> & { 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
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import MarkdownIt from "markdown-it";
|
|
2
2
|
import DOMPurify from "dompurify";
|
|
3
3
|
import { pageRoute } from "./schema/paths";
|
|
4
|
+
import { slugify } from "./slugs";
|
|
4
5
|
import { renderArticle } from "./article-render";
|
|
6
|
+
import { createEmbedPreview } from "./editor/lib/embed-preview";
|
|
5
7
|
import { safePreviewUrl, previewBaseUrl } from "./preview-urls";
|
|
6
8
|
export { safePreviewUrl } from "./preview-urls";
|
|
7
9
|
|
|
@@ -20,6 +22,7 @@ export interface PreviewConfig {
|
|
|
20
22
|
export interface PreviewTarget {
|
|
21
23
|
kind: "page" | "post" | "menu" | "document";
|
|
22
24
|
slug: string;
|
|
25
|
+
urlSlug?: string;
|
|
23
26
|
collection?: string;
|
|
24
27
|
type?: string;
|
|
25
28
|
}
|
|
@@ -31,6 +34,11 @@ export function previewPath(config: PreviewConfig, target: PreviewTarget, isNew
|
|
|
31
34
|
}
|
|
32
35
|
|
|
33
36
|
export function previewUrl(config: PreviewConfig, target: PreviewTarget, mode: "live" | "draft" | "website" = "draft", isNew = false): string | null {
|
|
37
|
+
if (mode !== "draft" && target.kind !== "page" && target.urlSlug !== undefined) {
|
|
38
|
+
const slug = slugify(target.urlSlug);
|
|
39
|
+
if (!slug) return null;
|
|
40
|
+
target = { ...target, slug };
|
|
41
|
+
}
|
|
34
42
|
const path = previewPath(config, target, mode === "draft" && (target.kind === "post" || isNew));
|
|
35
43
|
const base = mode === "live" ? config.liveUrl : config.draftUrl;
|
|
36
44
|
if (!path || base === undefined) return path;
|
|
@@ -151,11 +159,14 @@ export function patchPreview(doc: Document, values: Record<string, unknown>, bod
|
|
|
151
159
|
const source = blocks ? JSON.stringify(result.value) : typeof result.value === "string" ? result.value : "";
|
|
152
160
|
if (rendered.get(el) === source) continue;
|
|
153
161
|
rendered.set(el, source);
|
|
154
|
-
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), {
|
|
155
163
|
RETURN_DOM_FRAGMENT: true,
|
|
156
|
-
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"],
|
|
157
|
-
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"] : [])],
|
|
158
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
|
+
}
|
|
159
170
|
for (const link of fragment.querySelectorAll("[href], [src]")) {
|
|
160
171
|
for (const attr of ["href", "src"]) if (link.hasAttribute(attr) && !safePreviewUrl(link.getAttribute(attr)!, attr === "src")) link.removeAttribute(attr);
|
|
161
172
|
}
|
|
@@ -46,7 +46,7 @@ export const GET: APIRoute = async ({ params }) => {
|
|
|
46
46
|
const target = validateDocumentId(guarded.schema, id);
|
|
47
47
|
if (!target) return new Response("Unknown document", { status: 404 });
|
|
48
48
|
try {
|
|
49
|
-
const documents = await readDocuments(guarded.schema, guarded.root);
|
|
49
|
+
const documents = await readDocuments(guarded.schema, guarded.root, config.preview);
|
|
50
50
|
const document = documents.find((entry) => entry.id === id);
|
|
51
51
|
if (!document) return new Response("Unknown document", { status: 404 });
|
|
52
52
|
return Response.json(document);
|
|
@@ -66,10 +66,10 @@ const handle: APIRoute = async ({ request, params }) => {
|
|
|
66
66
|
if (!target) return new Response("Unknown document type or slug", { status: 400 });
|
|
67
67
|
try {
|
|
68
68
|
const data = await readBody(request);
|
|
69
|
-
const documents = await readDocuments(guarded.schema, guarded.root);
|
|
70
|
-
const next = { ...data, _id: id, _type: target.type.name
|
|
69
|
+
const documents = await readDocuments(guarded.schema, guarded.root, config.preview);
|
|
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");
|
|
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 });
|