@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/README.md
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
A CMS for Astro sites. You edit the text, git keeps the history.
|
|
4
4
|
|
|
5
|
-
`@rigel-cms/rigel` is a beta package and a work in progress. Expect bugs and breaking changes.
|
|
6
|
-
|
|
7
5
|
## Collection validation
|
|
8
6
|
|
|
9
7
|
New collection drafts initialize every configured field, including Rigel's automatic `description`, `metaTitle`, and `ogImage` strings. Existing content is never filled in automatically; unknown properties are preserved and missing optional fields remain allowed.
|
|
@@ -12,6 +10,10 @@ Rigel validates configured collection field types and `required: true` on load a
|
|
|
12
10
|
|
|
13
11
|
Astro content schemas remain the host site's contract: Rigel does not discover or execute them. Align Rigel field types and requiredness with the host schema. For example, Astro's `z.string()` accepts an initialized empty description; `z.string().min(1)` also needs Rigel's `required: true`.
|
|
14
12
|
|
|
13
|
+
## Media uploads
|
|
14
|
+
|
|
15
|
+
Set `maxFileSize` in the `rigel()` options in `astro.config.mjs`. The value is bytes and defaults to `2 * 1024 * 1024` (2 MB). For example, `rigel({ maxFileSize: 50 * 1024 * 1024 })` allows 50 MB per file. The limit applies to uploads and downloaded image imports, not existing files or linked videos. Your host may impose its own limit.
|
|
16
|
+
|
|
15
17
|
## License
|
|
16
18
|
|
|
17
19
|
[Apache-2.0](LICENSE). Copyright 2026 [Alex Zappa](https://alex.zappa.dev/) ([FreshJuice](https://freshjuice.dev/)).
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigel-cms/rigel",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0-rc.1",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
|
-
"tag": "
|
|
6
|
+
"tag": "rc"
|
|
7
7
|
},
|
|
8
8
|
"description": "Rigel CMS — the copy editor for Astro sites. Clients edit text, git keeps the history.",
|
|
9
9
|
"author": {
|
|
@@ -32,8 +32,7 @@
|
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "pnpm typecheck",
|
|
34
34
|
"test": "vitest run",
|
|
35
|
-
"typecheck": "tsc --noEmit"
|
|
36
|
-
"publish:beta": "npm publish --access public --tag beta"
|
|
35
|
+
"typecheck": "tsc --noEmit"
|
|
37
36
|
},
|
|
38
37
|
"exports": {
|
|
39
38
|
".": "./src/index.ts",
|
package/src/article-body.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SerializedEditorState } from "lexical";
|
|
2
2
|
import { safePreviewUrl } from "./preview-urls";
|
|
3
|
+
import { safeEmbed } from "./editor/lib/embeds";
|
|
3
4
|
|
|
4
5
|
export type ArticleBody = SerializedEditorState;
|
|
5
6
|
export type ArticleNode = { type: string; version: number; children?: ArticleNode[]; text?: string; format?: number | string; tag?: string; listType?: string; start?: number; url?: string; src?: string; alt?: string; credit?: string; creditURL?: string; [key: string]: unknown };
|
|
@@ -12,7 +13,7 @@ export function emptyBody(): ArticleBody {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
export function validateBody(value: unknown): asserts value is ArticleBody {
|
|
15
|
-
const types = new Set(["root", "paragraph", "heading", "quote", "list", "listitem", "code", "text", "linebreak", "link", "image", "divider", "table", "tablerow", "tablecell"]);
|
|
16
|
+
const types = new Set(["root", "paragraph", "heading", "quote", "list", "listitem", "code", "text", "linebreak", "link", "image", "video", "embed", "divider", "table", "tablerow", "tablecell"]);
|
|
16
17
|
let count = 0;
|
|
17
18
|
let textSize = 0;
|
|
18
19
|
function check(raw: unknown, parent = "", depth = 0) {
|
|
@@ -43,9 +44,15 @@ export function validateBody(value: unknown): asserts value is ArticleBody {
|
|
|
43
44
|
if (node.type === "image" && !safePreviewUrl(node.src!, true)) throw new Error("Unsafe image URL");
|
|
44
45
|
if (node.type === "image" && node.url !== undefined && (typeof node.url !== "string" || node.url && !safePreviewUrl(node.url))) throw new Error("Unsafe image link URL");
|
|
45
46
|
if (node.type === "link" && !safePreviewUrl(node.url!)) throw new Error("Unsafe link URL");
|
|
47
|
+
if (node.type === "video" && (typeof node.src !== "string" || !safePreviewUrl(node.src, true))) throw new Error("Unsafe video URL");
|
|
48
|
+
if (node.type === "video" && node.poster !== undefined && (typeof node.poster !== "string" || !safePreviewUrl(node.poster, true))) throw new Error("Unsafe video poster URL");
|
|
49
|
+
if (node.type === "video" && node.caption !== undefined && typeof node.caption !== "string") throw new Error("Invalid video caption");
|
|
50
|
+
if (node.type === "video") for (const key of ["muted", "controls"]) if (node[key] !== undefined && typeof node[key] !== "boolean") throw new Error("Invalid video playback setting");
|
|
51
|
+
if (node.type === "embed" && (!safeEmbed(node) || typeof node.privacy !== "boolean")) throw new Error("Invalid video embed");
|
|
52
|
+
if (node.type === "embed" && node.title !== undefined && typeof node.title !== "string") throw new Error("Invalid embed title");
|
|
46
53
|
for (const key of ["credit", "creditURL"]) if (node[key] !== undefined && typeof node[key] !== "string") throw new Error("Invalid image credit");
|
|
47
54
|
if (node.creditURL && !safePreviewUrl(node.creditURL)) throw new Error("Unsafe image credit URL");
|
|
48
|
-
if (!["text", "linebreak", "image", "divider"].includes(node.type)) {
|
|
55
|
+
if (!["text", "linebreak", "image", "video", "embed", "divider"].includes(node.type)) {
|
|
49
56
|
if (!Array.isArray(node.children)) throw new Error("Article block requires children");
|
|
50
57
|
for (const child of node.children) check(child, node.type, depth + 1);
|
|
51
58
|
if (["table", "tablerow", "tablecell"].includes(node.type) && !node.children.length) throw new Error("Table elements cannot be empty");
|
package/src/article-render.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { validateBody, type ArticleBody, type ArticleNode } from "./article-body";
|
|
2
2
|
import { safePreviewUrl } from "./preview-urls";
|
|
3
|
+
import { embedSrc, embedWatchUrl, safeEmbed, embedProviderLabel } from "./editor/lib/embeds";
|
|
3
4
|
|
|
4
5
|
const escape = (text: unknown) => String(text ?? "").replace(/[&<>"']/g, character => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[character]!);
|
|
5
6
|
|
|
@@ -10,7 +11,7 @@ function renderText(node: ArticleNode): string {
|
|
|
10
11
|
return text;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
|
-
export function renderArticle(body: ArticleBody): string {
|
|
14
|
+
export function renderArticle(body: ArticleBody, options: { preview?: boolean } = {}): string {
|
|
14
15
|
validateBody(body);
|
|
15
16
|
function walk(node: ArticleNode): string {
|
|
16
17
|
const content = () => (node.children ?? []).map(walk).join("");
|
|
@@ -32,6 +33,20 @@ export function renderArticle(body: ArticleBody): string {
|
|
|
32
33
|
if (node.type === "tablecell") return Number(node.headerState) ? `<th scope="${Number(node.headerState) & 1 ? "col" : "row"}">${content()}</th>` : `<td>${content()}</td>`;
|
|
33
34
|
if (node.type === "link") return safePreviewUrl(node.url!) ? `<a href="${escape(node.url)}">${content()}</a>` : content();
|
|
34
35
|
if (node.type === "divider") return "<hr>";
|
|
36
|
+
if (node.type === "video") {
|
|
37
|
+
if (!safePreviewUrl(node.src!, true)) return "";
|
|
38
|
+
const poster = typeof node.poster === "string" && safePreviewUrl(node.poster, true) ? ` poster="${escape(node.poster)}"` : "";
|
|
39
|
+
const caption = node.caption ? `<figcaption>${escape(node.caption)}</figcaption>` : "";
|
|
40
|
+
return `<figure class="media-video"><video${node.controls !== false ? " controls" : ""}${node.muted === true ? " muted" : ""} preload="metadata" playsinline src="${escape(node.src)}"${poster}></video>${caption}</figure>`;
|
|
41
|
+
}
|
|
42
|
+
if (node.type === "embed") {
|
|
43
|
+
const ref = safeEmbed(node);
|
|
44
|
+
if (!ref) return "";
|
|
45
|
+
const title = escape(typeof node.title === "string" && node.title ? node.title : `${embedProviderLabel(ref)} video`);
|
|
46
|
+
if (options.preview) return `<figure data-rigel-embed="${ref.provider}" data-rigel-video-id="${ref.id}" data-rigel-video-title="${title}"></figure>`;
|
|
47
|
+
const src = escape(embedSrc(ref, node.privacy !== false));
|
|
48
|
+
return `<figure class="media-embed"><iframe src="${src}" title="${title}" loading="lazy" allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen referrerpolicy="strict-origin-when-cross-origin"></iframe></figure>`;
|
|
49
|
+
}
|
|
35
50
|
if (node.type === "image") {
|
|
36
51
|
if (!safePreviewUrl(node.src!, true)) return "";
|
|
37
52
|
const credit = node.credit ? (node.creditURL && safePreviewUrl(node.creditURL) ? `<a href="${escape(node.creditURL)}">${escape(node.credit)}</a>` : escape(node.credit)) : "";
|
|
@@ -81,6 +96,17 @@ export function articleMarkdown(body: ArticleBody): string {
|
|
|
81
96
|
return (node.url ? `[${image}](<${node.url.replace(/[<>]/g, character => encodeURIComponent(character))}>)` : image) + "\n\n";
|
|
82
97
|
}
|
|
83
98
|
if (node.type === "divider") return "---\n\n";
|
|
99
|
+
if (node.type === "video") {
|
|
100
|
+
if (!safePreviewUrl(node.src!, true)) return "";
|
|
101
|
+
const label = node.caption ? `Video: ${node.caption}` : "Video";
|
|
102
|
+
return `[${label}](<${node.src!.replace(/[<>]/g, character => encodeURIComponent(character))}>)\n\n`;
|
|
103
|
+
}
|
|
104
|
+
if (node.type === "embed") {
|
|
105
|
+
const ref = safeEmbed(node);
|
|
106
|
+
if (!ref) return "";
|
|
107
|
+
const label = typeof node.title === "string" && node.title ? node.title : `${embedProviderLabel(ref)} video`;
|
|
108
|
+
return `[${escaped(label)}](<${embedWatchUrl(ref)}>)\n\n`;
|
|
109
|
+
}
|
|
84
110
|
return content();
|
|
85
111
|
}
|
|
86
112
|
return walk(body.root as unknown as ArticleNode).trimEnd() + "\n";
|
package/src/config.ts
CHANGED
|
@@ -4,12 +4,16 @@ import type { Schema } from "./schema/types";
|
|
|
4
4
|
import { validateSidebar, type SidebarEntry } from "./manifest/navigation";
|
|
5
5
|
import { withSeoFields } from "./manifest/seo";
|
|
6
6
|
import { previewBaseUrl } from "./preview-urls";
|
|
7
|
+
import type { JsonOutput } from "./json-output";
|
|
8
|
+
import { DEFAULT_MAX_FILE_SIZE } from "./media-types";
|
|
7
9
|
|
|
8
10
|
export interface RigelConfig {
|
|
9
11
|
copyDir: string;
|
|
10
12
|
mediaDir?: string;
|
|
13
|
+
maxFileSize?: number;
|
|
11
14
|
preview?: import("./preview").PreviewConfig;
|
|
12
15
|
backend: "local" | "github";
|
|
16
|
+
jsonOutput?: JsonOutput;
|
|
13
17
|
demo?: boolean;
|
|
14
18
|
collections?: CollectionConfig[];
|
|
15
19
|
menus?: { folder: string; maxDepth?: number };
|
|
@@ -40,6 +44,10 @@ const DEFAULTS: Pick<RigelConfig, "copyDir" | "backend"> = {
|
|
|
40
44
|
|
|
41
45
|
export function parseConfig(options: Partial<RigelConfig> = {}): RigelConfig {
|
|
42
46
|
const cfg = { ...DEFAULTS, ...options };
|
|
47
|
+
if (cfg.maxFileSize === undefined) cfg.maxFileSize = DEFAULT_MAX_FILE_SIZE;
|
|
48
|
+
if (!Number.isSafeInteger(cfg.maxFileSize) || cfg.maxFileSize <= 0) throw new Error("rigel: maxFileSize must be a positive integer in bytes");
|
|
49
|
+
if (cfg.jsonOutput === undefined) cfg.jsonOutput = "pretty";
|
|
50
|
+
if (cfg.jsonOutput !== "pretty" && cfg.jsonOutput !== "minified") throw new Error("rigel: jsonOutput must be pretty or minified");
|
|
43
51
|
if (cfg.demo === undefined) cfg.demo = true;
|
|
44
52
|
if (typeof cfg.demo !== "boolean") throw new Error("rigel: demo must be boolean");
|
|
45
53
|
if (cfg.preview) {
|
package/src/content.ts
CHANGED
|
@@ -2,4 +2,6 @@ export type { MenuItem, MenuDocument } from "./manifest/menu";
|
|
|
2
2
|
export { defineCopy } from "./manifest/types";
|
|
3
3
|
export type { CopyField, CopyGroup, CopyPage, Manifest, ManifestCollection } from "./manifest/types";
|
|
4
4
|
export { defineSchema, defineDocument, defineObject, defineField } from "./schema/types";
|
|
5
|
-
export type { Schema, DocumentDef, ObjectDef, FieldDef, ContentDocument } from "./schema/types";
|
|
5
|
+
export type { Schema, DocumentDef, ObjectDef, FieldDef, ContentDocument } from "./schema/types";
|
|
6
|
+
export { effectiveSlug } from "./slugs";
|
|
7
|
+
export { pageRoute } from "./schema/paths";
|
package/src/editor/App.svelte
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import { onMount } from "svelte";
|
|
2
|
+
import { onMount, tick } from "svelte";
|
|
3
3
|
import { tooltipRoot } from "./lib/tooltip";
|
|
4
4
|
onMount(() => tooltipRoot(document.body).destroy);
|
|
5
5
|
import type {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { isSeoField } from "../manifest/seo";
|
|
21
21
|
import { initialCollectionValues } from "../manifest/collections";
|
|
22
22
|
import { slugify } from "./lib/slugify";
|
|
23
|
+
import { effectiveSlug, normalizeUrlSlug } from "../slugs";
|
|
23
24
|
import Icon from "./lib/Icon.svelte";
|
|
24
25
|
import SidebarIcon from "./lib/SidebarIcon.svelte";
|
|
25
26
|
import logo from "./assets/rigelcms-icon.png?url";
|
|
@@ -29,13 +30,14 @@
|
|
|
29
30
|
import type { MenuItem } from "../manifest/menu";
|
|
30
31
|
import UnsavedChanges from "./lib/UnsavedChanges.svelte";
|
|
31
32
|
import ActionDialog from "./lib/ActionDialog.svelte";
|
|
33
|
+
import PublishDialog from "./lib/PublishDialog.svelte";
|
|
32
34
|
import VisualEditor from "./lib/VisualEditor.svelte";
|
|
33
35
|
import { newDocumentFromTemplate } from "../schema/paths";
|
|
34
36
|
import { previewUrl } from "../preview";
|
|
35
37
|
import type { ContentDocument, Schema } from "../schema/types";
|
|
36
38
|
import { buildSidebar, navigationKey, type Selection } from "../manifest/navigation";
|
|
37
39
|
import { startGitHubLogin, checkGitHubAccess, type GitHubAccess } from "./lib/github-auth";
|
|
38
|
-
import { loadManifestRemote, saveRemoteSelection, deleteRemotePost, type GitContext } from "./lib/github-backend";
|
|
40
|
+
import { loadManifestRemote, saveRemoteSelection, deleteRemotePost, reviewRemotePublish, confirmRemotePublish, type GitContext, type PublishReview } from "./lib/github-backend";
|
|
39
41
|
|
|
40
42
|
type Sel = Selection;
|
|
41
43
|
|
|
@@ -61,6 +63,75 @@
|
|
|
61
63
|
let saveState = $state<"idle" | "saving" | "saved" | "error">("idle");
|
|
62
64
|
let saveTask: Promise<boolean> | null = null;
|
|
63
65
|
let saveError = $state("");
|
|
66
|
+
let publishOpen = $state(false);
|
|
67
|
+
let publishLoading = $state(false);
|
|
68
|
+
let publishBusy = $state(false);
|
|
69
|
+
let publishReview = $state<PublishReview | null>(null);
|
|
70
|
+
let publishError = $state("");
|
|
71
|
+
let publishMessage = $state("");
|
|
72
|
+
let publishRevision = 0;
|
|
73
|
+
|
|
74
|
+
function closePublish() {
|
|
75
|
+
publishRevision += 1;
|
|
76
|
+
publishOpen = false;
|
|
77
|
+
publishReview = null;
|
|
78
|
+
void tick().then(() => document.querySelector<HTMLButtonElement>("#publish-button")?.focus());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function publishGuard(context: GitContext, revision: number, auth: number) {
|
|
82
|
+
if (!publishOpen || revision !== publishRevision || auth !== authRevision || gitContext !== context) throw new Error("Publish review is no longer active. Reconnect and review again.");
|
|
83
|
+
requireGitContext();
|
|
84
|
+
if (dirty || saveTask || saveState === "saving") throw new Error("Save your unsaved edits first, then review publishing. Your edits are still here.");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function reviewPublish() {
|
|
88
|
+
if (publishBusy || publishLoading) return;
|
|
89
|
+
publishOpen = true;
|
|
90
|
+
publishReview = null;
|
|
91
|
+
publishError = "";
|
|
92
|
+
publishMessage = "";
|
|
93
|
+
publishLoading = true;
|
|
94
|
+
const revision = ++publishRevision, auth = authRevision;
|
|
95
|
+
try {
|
|
96
|
+
const context = requireGitContext();
|
|
97
|
+
const guard = () => publishGuard(context, revision, auth);
|
|
98
|
+
guard();
|
|
99
|
+
const review = await reviewRemotePublish(context, manifest, guard);
|
|
100
|
+
guard();
|
|
101
|
+
publishReview = review;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
handleAuthError(error);
|
|
104
|
+
if (revision === publishRevision) publishError = error instanceof Error ? error.message : "Could not review saved changes.";
|
|
105
|
+
} finally { publishLoading = false; }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function publish() {
|
|
109
|
+
if (publishBusy || publishLoading || !publishReview) return;
|
|
110
|
+
publishBusy = true;
|
|
111
|
+
publishError = "";
|
|
112
|
+
const review = publishReview, revision = publishRevision, auth = authRevision;
|
|
113
|
+
try {
|
|
114
|
+
const context = requireGitContext();
|
|
115
|
+
const guard = () => publishGuard(context, revision, auth);
|
|
116
|
+
guard();
|
|
117
|
+
const result = await confirmRemotePublish(context, manifest, review, guard);
|
|
118
|
+
guard();
|
|
119
|
+
publishReview = null;
|
|
120
|
+
if (result === "synced") {
|
|
121
|
+
const remote = await loadManifestRemote(context, blueprint!, false);
|
|
122
|
+
guard();
|
|
123
|
+
manifest = remote;
|
|
124
|
+
openSelection(sel);
|
|
125
|
+
publishMessage = "Drafts synced. Nothing was published. Close this dialog and review again before publishing.";
|
|
126
|
+
} else publishMessage = `Published to ${review.publishBranch}. Your configured deployment can now start; the site is not instantly live. The drafts branch is preserved.`;
|
|
127
|
+
} catch (error) {
|
|
128
|
+
handleAuthError(error);
|
|
129
|
+
if (revision === publishRevision) {
|
|
130
|
+
publishError = error instanceof Error ? error.message : "Could not verify publishing. Check GitHub before retrying.";
|
|
131
|
+
if (/changed|no longer matches/.test(publishError)) publishReview = null;
|
|
132
|
+
}
|
|
133
|
+
} finally { publishBusy = false; }
|
|
134
|
+
}
|
|
64
135
|
let pendingNavigation = $state<(() => void) | null>(null);
|
|
65
136
|
let actionDialog = $state<{ title: string; description: string; confirmLabel: string; fieldLabel?: string; danger?: boolean; onconfirm: (value: string) => void | Promise<void> } | null>(null);
|
|
66
137
|
let newEntry = $state<{ collection: string; post: CollectionPost } | null>(null);
|
|
@@ -129,6 +200,7 @@
|
|
|
129
200
|
}
|
|
130
201
|
|
|
131
202
|
function invalidateSession() {
|
|
203
|
+
closePublish();
|
|
132
204
|
authRevision += 1;
|
|
133
205
|
clearSession();
|
|
134
206
|
gitContext = null;
|
|
@@ -142,7 +214,8 @@
|
|
|
142
214
|
}
|
|
143
215
|
|
|
144
216
|
async function connectGitHub() {
|
|
145
|
-
if (connectionBusy || saveState === "saving" || !manifest.github) return;
|
|
217
|
+
if (connectionBusy || saveState === "saving" || publishBusy || !manifest.github) return;
|
|
218
|
+
closePublish();
|
|
146
219
|
connectionBusy = true;
|
|
147
220
|
connectionError = "";
|
|
148
221
|
const revision = ++authRevision;
|
|
@@ -173,6 +246,8 @@
|
|
|
173
246
|
function signOut() {
|
|
174
247
|
if (saveState === "saving" || connectionBusy) return;
|
|
175
248
|
userOpen = false;
|
|
249
|
+
if (publishBusy) { invalidateSession(); connected = false; return; }
|
|
250
|
+
closePublish();
|
|
176
251
|
requestNavigation(() => {
|
|
177
252
|
invalidateSession();
|
|
178
253
|
connected = false;
|
|
@@ -202,6 +277,7 @@
|
|
|
202
277
|
});
|
|
203
278
|
|
|
204
279
|
function save(): Promise<boolean> {
|
|
280
|
+
if (publishOpen || publishBusy) return Promise.resolve(false);
|
|
205
281
|
if (saveTask) return saveTask;
|
|
206
282
|
saveTask = (async () => {
|
|
207
283
|
if (dirty) {
|
|
@@ -215,6 +291,10 @@
|
|
|
215
291
|
const docSel = sel?.kind === "document" ? sel : null;
|
|
216
292
|
const creatingDoc = docSel !== null && newDocument?.id === docSel.id;
|
|
217
293
|
try {
|
|
294
|
+
if ((selection?.kind === "post" || (docSel && !activeDocType?.shared)) && Object.hasOwn(values, "slug")) {
|
|
295
|
+
values.slug = normalizeUrlSlug(values.slug);
|
|
296
|
+
draft.slug = values.slug;
|
|
297
|
+
}
|
|
218
298
|
if (githubMode) {
|
|
219
299
|
if (!selection) throw new Error("Select content before saving.");
|
|
220
300
|
await saveRemoteSelection(requireGitContext(), manifest, selection, values, body, !!creating || creatingDoc);
|
|
@@ -234,7 +314,7 @@
|
|
|
234
314
|
newDocument = null;
|
|
235
315
|
docBase = JSON.parse(JSON.stringify(values));
|
|
236
316
|
} else if (m) { m.title = values.title; m.items = values.items; }
|
|
237
|
-
else if (po) { po.frontmatter =
|
|
317
|
+
else if (po) { po.frontmatter = values; po.body = body; bodyBase = body; }
|
|
238
318
|
else if (p) p.values = values;
|
|
239
319
|
if (creating && po && c) c.posts.push(po);
|
|
240
320
|
newEntry = null;
|
|
@@ -411,8 +491,8 @@
|
|
|
411
491
|
return map;
|
|
412
492
|
});
|
|
413
493
|
function documentPreview(mode: "draft" | "website") {
|
|
414
|
-
if (sel?.kind !== "document" || newDocument?.id === sel.id) return null;
|
|
415
|
-
return previewUrl(manifest.preview ?? {}, { kind: "document", type: sel.type, slug: sel.id.slice(sel.type.length + 1) }, mode);
|
|
494
|
+
if (sel?.kind !== "document" || (mode === "draft" && newDocument?.id === sel.id)) return null;
|
|
495
|
+
return previewUrl(manifest.preview ?? {}, { kind: "document", type: sel.type, slug: sel.id.slice(sel.type.length + 1), urlSlug: Object.hasOwn(draft, "slug") ? String(draft.slug) : undefined }, mode);
|
|
416
496
|
}
|
|
417
497
|
const standaloneDocument = $derived(sel?.kind === "document" && activeDocType?.shared === true);
|
|
418
498
|
const visualSelection = $derived(sel?.kind === "post" || (sel?.kind === "document" && !standaloneDocument));
|
|
@@ -422,13 +502,13 @@
|
|
|
422
502
|
...manifest.pages.filter((p) => !p.shared && (pageCategory === undefined || p.category === pageCategory))
|
|
423
503
|
.map((p) => ({ slug: p.slug, title: p.label, category: p.category, draft: false, target: { kind: "page", slug: p.slug } as Sel })),
|
|
424
504
|
...(pageCategory === undefined ? pageDocuments
|
|
425
|
-
.map((doc) => ({ slug: doc.slug, title: String(doc.data.title ?? doc.slug), category: undefined, draft: doc.data.draft === true, target: { kind: "document", type: doc.type, id: doc.id } as Sel })) : []),
|
|
505
|
+
.map((doc) => ({ slug: effectiveSlug(doc.data, doc.slug), title: String(doc.data.title ?? doc.slug), category: undefined, draft: doc.data.draft === true, target: { kind: "document", type: doc.type, id: doc.id } as Sel })) : []),
|
|
426
506
|
];
|
|
427
507
|
const key = sortBy === "slug" || sortBy === "category" ? sortBy : "title";
|
|
428
508
|
return rows.sort((a, b) => String(a[key] ?? "").localeCompare(String(b[key] ?? "")) * sortDir);
|
|
429
509
|
});
|
|
430
510
|
function documentCell(doc: ContentDocument, key: string): string {
|
|
431
|
-
return String(key === "title" ? doc.data.title ?? doc.data.name ?? doc.slug : key === "slug" ? doc.slug : doc.data[key] ?? "");
|
|
511
|
+
return String(key === "title" ? doc.data.title ?? doc.data.name ?? doc.slug : key === "slug" ? activeDocType?.shared ? doc.slug : effectiveSlug(doc.data, doc.slug) : doc.data[key] ?? "");
|
|
432
512
|
}
|
|
433
513
|
const typeDocuments = $derived((manifest.documents ?? []).filter((doc) => doc.type === docTypeName(sel)).sort((a, b) => documentCell(a, sortBy).localeCompare(documentCell(b, sortBy)) * sortDir));
|
|
434
514
|
|
|
@@ -507,6 +587,7 @@
|
|
|
507
587
|
}
|
|
508
588
|
|
|
509
589
|
function requestNavigation(action: () => void) {
|
|
590
|
+
if (publishOpen || publishBusy) return;
|
|
510
591
|
if (dirty || saveState === "saving") pendingNavigation = action;
|
|
511
592
|
else action();
|
|
512
593
|
}
|
|
@@ -564,7 +645,6 @@
|
|
|
564
645
|
const po = post;
|
|
565
646
|
if (!po) return;
|
|
566
647
|
const fm: Record<string, unknown> = { ...po.frontmatter };
|
|
567
|
-
delete fm.slug;
|
|
568
648
|
draft = JSON.parse(JSON.stringify(fm));
|
|
569
649
|
bodyDraft = po.body;
|
|
570
650
|
bodyBase = po.body;
|
|
@@ -590,9 +670,7 @@
|
|
|
590
670
|
}
|
|
591
671
|
const ref =
|
|
592
672
|
sel?.kind === "menu" && menu ? { title: menu.title, items: menu.items } : sel?.kind === "post" && post
|
|
593
|
-
?
|
|
594
|
-
Object.entries(post.frontmatter).filter(([k]) => k !== "slug"),
|
|
595
|
-
)
|
|
673
|
+
? post.frontmatter
|
|
596
674
|
: page?.values;
|
|
597
675
|
dirty =
|
|
598
676
|
newEntry !== null ||
|
|
@@ -602,7 +680,7 @@
|
|
|
602
680
|
}
|
|
603
681
|
|
|
604
682
|
function cellValue(p: { slug: string; frontmatter: Record<string, unknown> }, key: string): string {
|
|
605
|
-
const v = key === "slug" ? p.slug : p.frontmatter[key];
|
|
683
|
+
const v = key === "slug" ? effectiveSlug(p.frontmatter, p.slug) : p.frontmatter[key];
|
|
606
684
|
const field = coll?.fields.find(field => field.key === key);
|
|
607
685
|
if (field?.type === "reference") return referenceLabel(v, referenceOptions(field, manifest.documents)) || "—";
|
|
608
686
|
if (v === true) return "✓";
|
|
@@ -618,7 +696,7 @@
|
|
|
618
696
|
onconfirm: (title) => {
|
|
619
697
|
const slug = slugify(title);
|
|
620
698
|
if (!slug) throw new Error("Enter a title containing letters or numbers.");
|
|
621
|
-
const taken = [...(manifest.documents ?? []).filter(doc => !type.shared || doc.type === type.name).
|
|
699
|
+
const taken = [...(manifest.documents ?? []).filter(doc => !type.shared || doc.type === type.name).flatMap((doc) => [doc.slug, ...(type.shared || docTypes.find(type => type.name === doc.type)?.shared ? [] : [effectiveSlug(doc.data, doc.slug)])]), ...(!type.shared ? manifest.pages.map((page) => page.slug) : [])];
|
|
622
700
|
if (taken.includes(slug)) throw new Error(`The slug "${slug}" is already used by another document or page.`);
|
|
623
701
|
const id = `${type.name}:${slug}`;
|
|
624
702
|
const data = newDocumentFromTemplate(schema, type.name, title, slug);
|
|
@@ -634,15 +712,16 @@
|
|
|
634
712
|
actionDialog = {
|
|
635
713
|
title: `Add to ${c.label}`, description: "Enter a title to start a draft. Nothing is written until you press Save.", fieldLabel: "Title", confirmLabel: "Create draft",
|
|
636
714
|
onconfirm: (title) => {
|
|
637
|
-
const slug = slugify(title, c.posts.
|
|
715
|
+
const slug = slugify(title, c.posts.flatMap((post) => [post.slug, effectiveSlug(post.frontmatter, post.slug)]));
|
|
638
716
|
if (!slug) throw new Error("Enter a title containing letters or numbers.");
|
|
639
|
-
newEntry = { collection: c.name, post: { slug, frontmatter: initialCollectionValues(c.fields, title), body: emptyBody() } };
|
|
717
|
+
newEntry = { collection: c.name, post: { slug, frontmatter: { ...initialCollectionValues(c.fields, title), slug }, body: emptyBody() } };
|
|
640
718
|
openSelection({ kind: "post", collection: c.name, slug });
|
|
641
719
|
},
|
|
642
720
|
};
|
|
643
721
|
}
|
|
644
722
|
|
|
645
723
|
function deletePost() {
|
|
724
|
+
if (publishOpen || publishBusy) return;
|
|
646
725
|
const current = post, collection = coll, unsaved = newEntry !== null;
|
|
647
726
|
if (!current || !collection) return;
|
|
648
727
|
actionDialog = {
|
|
@@ -689,7 +768,7 @@
|
|
|
689
768
|
onresize={endPreviewResize}
|
|
690
769
|
onblur={endPreviewResize}
|
|
691
770
|
oncontextmenu={(event) => { if (!settings.devMode) event.preventDefault(); }}
|
|
692
|
-
onbeforeunload={(event) => { if (dirty || saveState === "saving") { event.preventDefault(); event.returnValue = ""; } }}
|
|
771
|
+
onbeforeunload={(event) => { if (dirty || saveState === "saving" || publishBusy) { event.preventDefault(); event.returnValue = ""; } }}
|
|
693
772
|
onclick={(e) => {
|
|
694
773
|
if (!(e.target instanceof Element) || !e.target.closest('.user-menu-wrap')) userOpen = false;
|
|
695
774
|
}}
|
|
@@ -736,11 +815,10 @@
|
|
|
736
815
|
</div>
|
|
737
816
|
{:else}
|
|
738
817
|
<div class="rigel" class:sidebar-collapsed={settings.userPref.sidebarCollapsed}>
|
|
739
|
-
<aside id="rigel-sidebar">
|
|
740
|
-
<a class="brand" href="/rigel/" role="button" aria-label="Welcome"
|
|
818
|
+
<aside id="rigel-sidebar" data-tooltip-position="right">
|
|
819
|
+
<a class="brand" href="/rigel/" role="button" aria-label="Welcome" onclick={(event) => { event.preventDefault(); goWelcome(); }}>
|
|
741
820
|
<img src={logo} alt="" width="32" height="32" />
|
|
742
821
|
<span>Rigel <span class="brand-suffix">CMS</span></span>
|
|
743
|
-
<span id="rigel-beta" class="badge">Beta</span>
|
|
744
822
|
</a>
|
|
745
823
|
<TopBar {manifest} {sel} onselect={select} />
|
|
746
824
|
|
|
@@ -771,6 +849,11 @@
|
|
|
771
849
|
{/each}
|
|
772
850
|
</nav>
|
|
773
851
|
|
|
852
|
+
{#if githubMode && manifest.github?.publishBranch}
|
|
853
|
+
<button id="publish-button" class="sidebar-publish" aria-label="Publish" data-tooltip={dirty ? "Save your edits before publishing" : "Publish saved branch changes"} disabled={publishBusy || publishLoading || saveState === "saving" || connectionBusy || tokenExpired || githubAccess?.status !== "ready"} onclick={reviewPublish}>
|
|
854
|
+
<Icon name="external" size={18} /><span class="sidebar-label">Publish</span>
|
|
855
|
+
</button>
|
|
856
|
+
{/if}
|
|
774
857
|
<button class="sidebar-toggle" aria-expanded={!settings.userPref.sidebarCollapsed} aria-controls="rigel-sidebar-navigation" aria-label={settings.userPref.sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} data-tooltip={settings.userPref.sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"} onclick={() => { settings.userPref.sidebarCollapsed = !settings.userPref.sidebarCollapsed; }}>
|
|
775
858
|
<SidebarIcon name="sidebar-simple" /><span class="sidebar-label">Collapse sidebar</span>
|
|
776
859
|
</button>
|
|
@@ -793,11 +876,11 @@
|
|
|
793
876
|
Live Site
|
|
794
877
|
</a>
|
|
795
878
|
<div class="divider"></div>
|
|
796
|
-
<button role="menuitem" onclick={() => { userOpen = false;
|
|
879
|
+
<button role="menuitem" onclick={() => { userOpen = false; settingsOpen = true; }}>
|
|
797
880
|
<Icon name="gear" size={13} />
|
|
798
881
|
Settings
|
|
799
882
|
</button>
|
|
800
|
-
<button role="menuitem" onclick={() => { userOpen = false;
|
|
883
|
+
<button role="menuitem" onclick={() => { userOpen = false; shortcutsOpen = true; }}>
|
|
801
884
|
<Icon name="keyboard" size={13} />
|
|
802
885
|
Keyboard Shortcuts
|
|
803
886
|
</button>
|
|
@@ -951,6 +1034,9 @@
|
|
|
951
1034
|
<MenuEditor items={draft.items as MenuItem[]} maxDepth={manifest.menus.maxDepth} oninput={(items) => { draft.items = items; touch(); }} />
|
|
952
1035
|
|
|
953
1036
|
{:else if sel?.kind === "page" && page}
|
|
1037
|
+
{#if !page.shared}
|
|
1038
|
+
<section><label><span>Website route</span><input type="text" value={manifest.preview?.pages?.[page.slug] ?? "Not configured"} readonly /></label><p class="hint">This copy page uses a fixed Astro route. Its URL can only be changed in the website code; slug metadata cannot move it.</p></section>
|
|
1039
|
+
{/if}
|
|
954
1040
|
{#each page.groups as group (group.id)}
|
|
955
1041
|
<section>
|
|
956
1042
|
<h3>{group.label}</h3>
|
|
@@ -979,13 +1065,13 @@
|
|
|
979
1065
|
bodyInline={settings.userPref.editorExperience === "inline"}
|
|
980
1066
|
onbody={(value) => { bodyDraft = value; touch(); }}
|
|
981
1067
|
previewPath={manifest.preview ? previewUrl(manifest.preview, sel, "draft", !!newEntry) : null}
|
|
982
|
-
websitePath={manifest.preview ? previewUrl(manifest.preview, sel, "website") : null}
|
|
1068
|
+
websitePath={manifest.preview ? previewUrl(manifest.preview, { ...sel, urlSlug: Object.hasOwn(draft, "slug") ? String(draft.slug) : undefined }, "website") : null}
|
|
983
1069
|
creating={!!newEntry}
|
|
984
1070
|
devMode={settings.devMode}
|
|
985
1071
|
onchange={(next) => { draft = next; touch(); }}
|
|
986
1072
|
>
|
|
987
|
-
{#snippet properties(selected, focusToken, bodyInline, oneditbody)}
|
|
988
|
-
<CollectionPanel fields={coll.fields} values={draft} documents={manifest.documents ?? []} body={bodyDraft as ArticleBody} {bodyInline} {oneditbody} {selected} {focusToken} devMode={settings.devMode} onchange={(key, value) => { draft[key] = value; touch(); }} onbody={(value) => { bodyDraft = value; touch(); }} />
|
|
1073
|
+
{#snippet properties(selected, focusToken, bodyInline, oneditbody, bodyLoading)}
|
|
1074
|
+
<CollectionPanel fields={coll.fields} values={draft} storageSlug={post.slug} documents={manifest.documents ?? []} body={bodyDraft as ArticleBody} {bodyInline} {bodyLoading} {oneditbody} {selected} {focusToken} devMode={settings.devMode} onchange={(key, value) => { draft[key] = value; touch(); }} onbody={(value) => { bodyDraft = value; touch(); }} />
|
|
989
1075
|
{/snippet}
|
|
990
1076
|
</VisualEditor>
|
|
991
1077
|
{/key}
|
|
@@ -1025,3 +1111,6 @@
|
|
|
1025
1111
|
{#if actionDialog}
|
|
1026
1112
|
<ActionDialog {...actionDialog} onclose={() => { actionDialog = null; }} />
|
|
1027
1113
|
{/if}
|
|
1114
|
+
{#if publishOpen}
|
|
1115
|
+
<PublishDialog review={publishReview} loading={publishLoading} busy={publishBusy} error={publishError} message={publishMessage} onrefresh={reviewPublish} onconfirm={publish} onclose={closePublish} />
|
|
1116
|
+
{/if}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DecoratorNode, type NodeKey } from "lexical";
|
|
2
|
+
import type { EmbedProvider } from "../lib/embeds";
|
|
3
|
+
import { createEmbedPreview } from "../lib/embed-preview";
|
|
4
|
+
|
|
5
|
+
export type EmbedData = { provider: EmbedProvider; id: string; title?: string; privacy: boolean };
|
|
6
|
+
export class EmbedNode extends DecoratorNode<null> {
|
|
7
|
+
__embed: EmbedData;
|
|
8
|
+
static getType() { return "embed"; }
|
|
9
|
+
static clone(node: EmbedNode) { return new EmbedNode(node.__embed, node.__key); }
|
|
10
|
+
constructor(embed: EmbedData = { provider: "youtube", id: "", privacy: true }, key?: NodeKey) { super(key); this.__embed = embed; }
|
|
11
|
+
static importJSON(raw: unknown) { const embed = raw as EmbedData; return new EmbedNode({ provider: embed.provider, id: embed.id, title: embed.title, privacy: embed.privacy !== false }); }
|
|
12
|
+
exportJSON() { return { ...this.__embed, type: "embed", version: 1 }; }
|
|
13
|
+
createDOM() {
|
|
14
|
+
return createEmbedPreview(this.__embed, document, true);
|
|
15
|
+
}
|
|
16
|
+
updateDOM() { return true; }
|
|
17
|
+
decorate() { return null; }
|
|
18
|
+
isInline() { return false; }
|
|
19
|
+
getTextContent() { return this.__embed.title ?? ""; }
|
|
20
|
+
}
|
|
21
|
+
export const create = (embed?: EmbedData) => new EmbedNode(embed);
|
|
@@ -7,6 +7,8 @@ import { LinkNode } from "@lexical/link";
|
|
|
7
7
|
import { CodeNode } from "@lexical/code";
|
|
8
8
|
import { TableNode, TableRowNode, TableCellNode } from "@lexical/table";
|
|
9
9
|
import { ImageNode, create as image } from "./image";
|
|
10
|
+
import { VideoNode } from "./video";
|
|
11
|
+
import { EmbedNode } from "./embed";
|
|
10
12
|
import { DividerNode, create as divider } from "./divider";
|
|
11
13
|
import { create as quote } from "./quote";
|
|
12
14
|
import { create as bullet } from "./bulleted-list";
|
|
@@ -37,4 +39,4 @@ export const blockTypes: { value: string; label: string; icon: keyof typeof ICON
|
|
|
37
39
|
{ value: "divider", label: "Divider", icon: "minus", create: divider },
|
|
38
40
|
{ value: "table", label: "Table", icon: "table", create: table },
|
|
39
41
|
];
|
|
40
|
-
export const blockNodes = [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode, CodeNode, ImageNode, DividerNode, TableNode, TableRowNode, TableCellNode];
|
|
42
|
+
export const blockNodes = [HeadingNode, QuoteNode, ListNode, ListItemNode, LinkNode, CodeNode, ImageNode, VideoNode, EmbedNode, DividerNode, TableNode, TableRowNode, TableCellNode];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DecoratorNode, type NodeKey } from "lexical";
|
|
2
|
+
import { safePreviewUrl } from "../../preview-urls";
|
|
3
|
+
import { createVideoOverlay } from "../lib/embed-preview";
|
|
4
|
+
|
|
5
|
+
export type VideoData = { src: string; poster?: string; caption?: string; muted?: boolean; controls?: boolean };
|
|
6
|
+
export class VideoNode extends DecoratorNode<null> {
|
|
7
|
+
__video: VideoData;
|
|
8
|
+
static getType() { return "video"; }
|
|
9
|
+
static clone(node: VideoNode) { return new VideoNode(node.__video, node.__key); }
|
|
10
|
+
constructor(video: VideoData = { src: "" }, key?: NodeKey) { super(key); this.__video = video; }
|
|
11
|
+
static importJSON(raw: unknown) { const video = raw as VideoData; return new VideoNode({ src: video.src, poster: video.poster, caption: video.caption, muted: video.muted, controls: video.controls }); }
|
|
12
|
+
exportJSON() { return { ...this.__video, type: "video", version: 1 }; }
|
|
13
|
+
createDOM() {
|
|
14
|
+
const figure = document.createElement("figure");
|
|
15
|
+
figure.className = "media-video";
|
|
16
|
+
const video = document.createElement("video");
|
|
17
|
+
if (safePreviewUrl(this.__video.src, true)) video.src = this.__video.src;
|
|
18
|
+
if (this.__video.poster && safePreviewUrl(this.__video.poster, true)) video.poster = this.__video.poster;
|
|
19
|
+
video.controls = this.__video.controls !== false;
|
|
20
|
+
video.muted = video.defaultMuted = this.__video.muted === true;
|
|
21
|
+
video.preload = "metadata";
|
|
22
|
+
video.playsInline = true;
|
|
23
|
+
const preview = document.createElement("div");
|
|
24
|
+
preview.style.cssText = "position:relative;min-height:220px";
|
|
25
|
+
preview.append(video, createVideoOverlay(document, "Video", this.__video.src, true));
|
|
26
|
+
figure.append(preview);
|
|
27
|
+
if (this.__video.caption) { const caption = document.createElement("figcaption"); caption.textContent = this.__video.caption; figure.append(caption); }
|
|
28
|
+
return figure;
|
|
29
|
+
}
|
|
30
|
+
updateDOM() { return true; }
|
|
31
|
+
decorate() { return null; }
|
|
32
|
+
isInline() { return false; }
|
|
33
|
+
getTextContent() { return this.__video.caption ?? ""; }
|
|
34
|
+
}
|
|
35
|
+
export const create = (video?: VideoData) => new VideoNode(video);
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
}
|
|
26
26
|
</script>
|
|
27
27
|
|
|
28
|
-
<dialog bind:this={dialog} class="rigel rigel-modal-backdrop action-dialog" aria-labelledby="action-title" aria-describedby="action-description" oncancel={(event) => { event.preventDefault(); if (!busy) onclose(); }}>
|
|
28
|
+
<dialog bind:this={dialog} class="rigel rigel-modal-backdrop action-dialog" aria-labelledby="action-title" aria-describedby="action-description" oncancel={(event) => { event.preventDefault(); event.stopPropagation(); if (!busy) onclose(); }} onkeydown={(event) => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); if (!busy) onclose(); } }}>
|
|
29
29
|
<form class="rigel-modal" onsubmit={(event) => { event.preventDefault(); void submit(); }}>
|
|
30
30
|
<div class="action-head"><h2 id="action-title">{title}</h2><button type="button" class="icon-btn" aria-label="Cancel" disabled={busy} onclick={onclose}><Icon name="x" size={16} /></button></div>
|
|
31
31
|
<p id="action-description">{description}</p>
|
|
@@ -45,6 +45,7 @@
|
|
|
45
45
|
.action-dialog::backdrop { background: transparent; }
|
|
46
46
|
.action-head, .action-actions { display: flex; align-items: center; gap: 0.75rem; }
|
|
47
47
|
.action-head { justify-content: space-between; }
|
|
48
|
+
.action-head h2 { min-width: 0; overflow-wrap: anywhere; }
|
|
48
49
|
.action-actions { justify-content: flex-end; flex-wrap: wrap; margin-top: 1.5rem; }
|
|
49
50
|
.action-actions button[type=button] { padding: 0.4375rem 0.75rem; border: 1px solid var(--border-control); border-radius: 0; background: var(--panel); color: var(--text); font: inherit; font-size: 0.8125rem; cursor: pointer; }
|
|
50
51
|
button:disabled { opacity: 0.5; cursor: wait; }
|