@rigel-cms/rigel 0.1.0-beta.1 → 0.1.4-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/package.json +7 -2
- package/src/config.ts +6 -0
- package/src/editor/App.svelte +113 -58
- package/src/editor/lib/BlockEditor.svelte +18 -18
- package/src/editor/lib/ImagePicker.svelte +7 -7
- package/src/editor/lib/MarkdownEditor.svelte +7 -7
- package/src/editor/lib/PreviewFrame.svelte +45 -0
- package/src/editor/lib/SchemaField.svelte +4 -4
- package/src/editor/lib/SchemaPanel.svelte +4 -4
- package/src/editor/lib/Select.svelte +3 -2
- package/src/editor/lib/SeoGuidance.svelte +1 -1
- package/src/editor/lib/Settings.svelte +27 -2
- package/src/editor/lib/SitePreview.svelte +9 -8
- package/src/editor/lib/TopBar.svelte +1 -1
- package/src/editor/lib/VisualEditor.svelte +10 -8
- package/src/editor/lib/github-auth.ts +4 -3
- package/src/editor/lib/github-backend.ts +3 -3
- package/src/editor/lib/preview-frame.ts +29 -0
- package/src/editor/lib/slugify.ts +7 -2
- package/src/editor/lib/storage.ts +30 -2
- package/src/editor/lib/tooltip.ts +120 -0
- package/src/editor/styles.css +11 -0
- package/src/manifest/collections.ts +27 -0
- package/src/manifest/types.ts +1 -0
- package/src/preview-urls.ts +9 -0
- package/src/preview.ts +12 -4
- package/src/routes/local-post.ts +11 -5
- package/src/vite/manifest-plugin.ts +2 -0
package/README.md
CHANGED
|
@@ -4,6 +4,14 @@ A CMS for Astro sites. You edit the text, git keeps the history.
|
|
|
4
4
|
|
|
5
5
|
`@rigel-cms/rigel` is a beta package and a work in progress. Expect bugs and breaking changes.
|
|
6
6
|
|
|
7
|
+
## Collection validation
|
|
8
|
+
|
|
9
|
+
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.
|
|
10
|
+
|
|
11
|
+
Rigel validates configured collection field types and `required: true` on load and before local or GitHub saves. Required strings must contain non-whitespace text, required arrays must contain an item, and `false` is a valid required boolean. Dates use real `YYYY-MM-DD` calendar dates; optional dates may be empty. Tags are arrays of strings and references must select configured entries.
|
|
12
|
+
|
|
13
|
+
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
|
+
|
|
7
15
|
## License
|
|
8
16
|
|
|
9
17
|
[Apache-2.0](LICENSE). Copyright 2026 [Alex Zappa](https://alex.zappa.dev/) ([FreshJuice](https://freshjuice.dev/)).
|
package/package.json
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigel-cms/rigel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4-beta.1",
|
|
4
|
+
"publishConfig": {
|
|
5
|
+
"access": "public",
|
|
6
|
+
"tag": "beta"
|
|
7
|
+
},
|
|
4
8
|
"description": "Rigel CMS — the copy editor for Astro sites. Clients edit text, git keeps the history.",
|
|
5
9
|
"author": {
|
|
6
10
|
"name": "FreshJuice",
|
|
@@ -28,7 +32,8 @@
|
|
|
28
32
|
"scripts": {
|
|
29
33
|
"build": "pnpm typecheck",
|
|
30
34
|
"test": "vitest run",
|
|
31
|
-
"typecheck": "tsc --noEmit"
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"publish:beta": "npm publish --access public --tag beta"
|
|
32
37
|
},
|
|
33
38
|
"exports": {
|
|
34
39
|
".": "./src/index.ts",
|
package/src/config.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { validateSchema } from "./schema/validate";
|
|
|
3
3
|
import type { Schema } from "./schema/types";
|
|
4
4
|
import { validateSidebar, type SidebarEntry } from "./manifest/navigation";
|
|
5
5
|
import { withSeoFields } from "./manifest/seo";
|
|
6
|
+
import { previewBaseUrl } from "./preview-urls";
|
|
6
7
|
|
|
7
8
|
export interface RigelConfig {
|
|
8
9
|
copyDir: string;
|
|
@@ -41,6 +42,10 @@ export function parseConfig(options: Partial<RigelConfig> = {}): RigelConfig {
|
|
|
41
42
|
const cfg = { ...DEFAULTS, ...options };
|
|
42
43
|
if (cfg.demo === undefined) cfg.demo = true;
|
|
43
44
|
if (typeof cfg.demo !== "boolean") throw new Error("rigel: demo must be boolean");
|
|
45
|
+
if (cfg.preview) {
|
|
46
|
+
cfg.preview = { ...cfg.preview };
|
|
47
|
+
for (const key of ["liveUrl", "draftUrl"] as const) if (cfg.preview[key] !== undefined) cfg.preview[key] = previewBaseUrl(cfg.preview[key]);
|
|
48
|
+
}
|
|
44
49
|
if (cfg.mediaDir !== undefined && (typeof cfg.mediaDir !== "string" || !cfg.mediaDir.split("/").every(part => /^[a-zA-Z0-9_-][a-zA-Z0-9_. -]*$/.test(part)))) throw new Error("rigel: mediaDir must be a relative folder inside publicDir");
|
|
45
50
|
if (cfg.backend === "github") {
|
|
46
51
|
if (typeof cfg.repo !== "string" || !/^[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\/[a-zA-Z0-9_.-]+$/.test(cfg.repo) || [".", ".."].includes(cfg.repo.split("/")[1])) throw new Error("rigel: github backend requires `repo: owner/name`");
|
|
@@ -82,6 +87,7 @@ export function parseConfig(options: Partial<RigelConfig> = {}): RigelConfig {
|
|
|
82
87
|
if (cfg.collections) cfg.collections = cfg.collections.map(collection => ({ ...collection, fields: withSeoFields(collection.fields) }));
|
|
83
88
|
for (const collection of cfg.collections ?? []) {
|
|
84
89
|
for (const field of collection.fields) {
|
|
90
|
+
if (field.required !== undefined && typeof field.required !== "boolean") throw new Error(`rigel: ${collection.name}.${field.key}: required must be boolean`);
|
|
85
91
|
if (field.type !== "reference") continue;
|
|
86
92
|
if (field.multiple !== undefined && typeof field.multiple !== "boolean") throw new Error(`rigel: ${collection.name}.${field.key}: multiple must be boolean`);
|
|
87
93
|
if ((field.to !== undefined ? 1 : 0) + (field.options !== undefined ? 1 : 0) !== 1) throw new Error(`rigel: ${collection.name}.${field.key}: reference requires either to or options`);
|
package/src/editor/App.svelte
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { onMount } from "svelte";
|
|
3
|
+
import { tooltipRoot } from "./lib/tooltip";
|
|
4
|
+
onMount(() => tooltipRoot(document.body).destroy);
|
|
2
5
|
import type {
|
|
3
6
|
Manifest,
|
|
4
7
|
ManifestCollection,
|
|
@@ -15,19 +18,20 @@
|
|
|
15
18
|
import SchemaPanel from "./lib/SchemaPanel.svelte";
|
|
16
19
|
import { collectionPreviewValues, referenceLabel, referenceOptions } from "../manifest/references";
|
|
17
20
|
import { isSeoField } from "../manifest/seo";
|
|
21
|
+
import { initialCollectionValues } from "../manifest/collections";
|
|
18
22
|
import { slugify } from "./lib/slugify";
|
|
19
23
|
import Icon from "./lib/Icon.svelte";
|
|
20
24
|
import SidebarIcon from "./lib/SidebarIcon.svelte";
|
|
21
25
|
import logo from "./assets/rigelcms-icon.png?url";
|
|
22
26
|
import { getSettings, setSettings } from "./lib/settings";
|
|
23
|
-
import { eraseData } from "./lib/storage";
|
|
27
|
+
import { eraseData, readSession, writeSession, clearSession } from "./lib/storage";
|
|
24
28
|
import MenuEditor from "./lib/MenuEditor.svelte";
|
|
25
29
|
import type { MenuItem } from "../manifest/menu";
|
|
26
30
|
import UnsavedChanges from "./lib/UnsavedChanges.svelte";
|
|
27
31
|
import ActionDialog from "./lib/ActionDialog.svelte";
|
|
28
32
|
import VisualEditor from "./lib/VisualEditor.svelte";
|
|
29
33
|
import { newDocumentFromTemplate } from "../schema/paths";
|
|
30
|
-
import {
|
|
34
|
+
import { previewUrl } from "../preview";
|
|
31
35
|
import type { ContentDocument, Schema } from "../schema/types";
|
|
32
36
|
import { buildSidebar, navigationKey, type Selection } from "../manifest/navigation";
|
|
33
37
|
import { startGitHubLogin, checkGitHubAccess, type GitHubAccess } from "./lib/github-auth";
|
|
@@ -39,6 +43,7 @@
|
|
|
39
43
|
let ready = $state(false);
|
|
40
44
|
let blueprint: Manifest | null = null;
|
|
41
45
|
let gitContext: GitContext | null = null;
|
|
46
|
+
let authRevision = 0;
|
|
42
47
|
let connected = $state(false);
|
|
43
48
|
let connectionBusy = $state(false);
|
|
44
49
|
let connectionError = $state("");
|
|
@@ -75,6 +80,14 @@
|
|
|
75
80
|
blueprint = data;
|
|
76
81
|
manifest = data;
|
|
77
82
|
ready = true;
|
|
83
|
+
if (data.backend === "github" && data.github) {
|
|
84
|
+
const session = readSession(data.github);
|
|
85
|
+
if (session) {
|
|
86
|
+
gitContext = { token: session.accessToken, repo: data.github.repo, branch: data.github.branch };
|
|
87
|
+
sessionExpiresAt = session.expiresAt;
|
|
88
|
+
await recheckGitHub();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
78
91
|
} catch (error) {
|
|
79
92
|
loadError = error instanceof Error ? error.message : "Could not load the editor configuration.";
|
|
80
93
|
}
|
|
@@ -84,7 +97,7 @@
|
|
|
84
97
|
if (connectionBusy) throw new Error("Wait for GitHub access verification before saving.");
|
|
85
98
|
if (!gitContext || !connected || githubAccess?.status !== "ready") throw new Error("Connect GitHub and verify repository access before saving.");
|
|
86
99
|
if (Date.now() >= sessionExpiresAt - 30_000) {
|
|
87
|
-
|
|
100
|
+
invalidateSession();
|
|
88
101
|
throw new Error("Your GitHub session expired. Reconnect GitHub, then retry. Your unsaved edits are still here.");
|
|
89
102
|
}
|
|
90
103
|
return gitContext;
|
|
@@ -92,27 +105,54 @@
|
|
|
92
105
|
|
|
93
106
|
async function verifyGitHub() {
|
|
94
107
|
const config = blueprint?.github;
|
|
95
|
-
|
|
96
|
-
if (
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
const context = gitContext;
|
|
109
|
+
if (!config || !context) return;
|
|
110
|
+
try {
|
|
111
|
+
if (Date.now() >= sessionExpiresAt - 30_000) {
|
|
112
|
+
invalidateSession();
|
|
113
|
+
throw new Error("Your GitHub session expired. Sign in again to check repository access.");
|
|
114
|
+
}
|
|
115
|
+
const access = await checkGitHubAccess(context.token, config.repo, config.appSlug);
|
|
116
|
+
if (gitContext !== context) return;
|
|
117
|
+
githubAccess = access;
|
|
118
|
+
if (access.status !== "ready") return;
|
|
119
|
+
if (!connected && blueprint) {
|
|
120
|
+
const remote = await loadManifestRemote(context, blueprint);
|
|
121
|
+
if (gitContext !== context) return;
|
|
122
|
+
manifest = remote;
|
|
123
|
+
connected = true;
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
if (gitContext === context) handleAuthError(error);
|
|
127
|
+
throw error;
|
|
103
128
|
}
|
|
104
129
|
}
|
|
105
130
|
|
|
131
|
+
function invalidateSession() {
|
|
132
|
+
authRevision += 1;
|
|
133
|
+
clearSession();
|
|
134
|
+
gitContext = null;
|
|
135
|
+
sessionExpiresAt = 0;
|
|
136
|
+
githubAccess = null;
|
|
137
|
+
tokenExpired = true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function handleAuthError(error: unknown) {
|
|
141
|
+
if (error instanceof Error && "status" in error && error.status === 401) invalidateSession();
|
|
142
|
+
}
|
|
143
|
+
|
|
106
144
|
async function connectGitHub() {
|
|
107
145
|
if (connectionBusy || saveState === "saving" || !manifest.github) return;
|
|
108
146
|
connectionBusy = true;
|
|
109
147
|
connectionError = "";
|
|
148
|
+
const revision = ++authRevision;
|
|
110
149
|
try {
|
|
111
150
|
const session = await startGitHubLogin(manifest.github.authOrigin);
|
|
112
|
-
if (
|
|
113
|
-
|
|
151
|
+
if (revision !== authRevision) return;
|
|
152
|
+
gitContext = { token: session.accessToken, repo: manifest.github.repo, branch: manifest.github.branch };
|
|
114
153
|
githubAccess = null;
|
|
115
154
|
sessionExpiresAt = session.expiresAt;
|
|
155
|
+
writeSession(manifest.github, session);
|
|
116
156
|
await verifyGitHub();
|
|
117
157
|
} catch (error) {
|
|
118
158
|
connectionError = error instanceof Error ? error.message : "Could not connect GitHub. Try signing in again.";
|
|
@@ -134,9 +174,7 @@
|
|
|
134
174
|
if (saveState === "saving" || connectionBusy) return;
|
|
135
175
|
userOpen = false;
|
|
136
176
|
requestNavigation(() => {
|
|
137
|
-
|
|
138
|
-
sessionExpiresAt = 0;
|
|
139
|
-
githubAccess = null;
|
|
177
|
+
invalidateSession();
|
|
140
178
|
connected = false;
|
|
141
179
|
connectionError = "";
|
|
142
180
|
settingsOpen = false;
|
|
@@ -157,9 +195,9 @@
|
|
|
157
195
|
|
|
158
196
|
$effect(() => {
|
|
159
197
|
const expiry = sessionExpiresAt;
|
|
160
|
-
tokenExpired =
|
|
198
|
+
tokenExpired = !expiry || Date.now() >= expiry - 30_000;
|
|
161
199
|
if (!expiry) return;
|
|
162
|
-
const timer = setTimeout(
|
|
200
|
+
const timer = setTimeout(invalidateSession, Math.max(0, expiry - Date.now() - 30_000));
|
|
163
201
|
return () => clearTimeout(timer);
|
|
164
202
|
});
|
|
165
203
|
|
|
@@ -185,7 +223,7 @@
|
|
|
185
223
|
? `/rigel/api/local/documents/${encodeURIComponent(docSel.id)}`
|
|
186
224
|
: m ? `/rigel/api/local/menus/${m.slug}` : po && c ? `/rigel/api/local/${c.name}/${encodeURIComponent(po.slug)}` : "/rigel/api/local/save";
|
|
187
225
|
const payload = docSel ? { data: values } : m ? { slug: m.slug, title: values.title, items: values.items } : po ? { frontmatter: values, body } : { slug: p?.slug, values };
|
|
188
|
-
const res = await fetch(url, { method: docSel ? (creatingDoc ? "POST" : "PUT") : m ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload) });
|
|
226
|
+
const res = await fetch(url, { method: docSel ? (creatingDoc ? "POST" : "PUT") : m ? "PUT" : "POST", headers: { "Content-Type": "application/json", ...(po && creating ? { "If-None-Match": "*" } : {}) }, body: JSON.stringify(payload) });
|
|
189
227
|
if (!res.ok) throw new Error((await res.text()) || `Save failed (${res.status})`);
|
|
190
228
|
}
|
|
191
229
|
if (docSel) {
|
|
@@ -204,6 +242,7 @@
|
|
|
204
242
|
saveState = dirty ? "idle" : "saved";
|
|
205
243
|
saveError = "";
|
|
206
244
|
} catch (error) {
|
|
245
|
+
handleAuthError(error);
|
|
207
246
|
saveState = "error";
|
|
208
247
|
saveError = error instanceof Error ? error.message : "Could not save changes";
|
|
209
248
|
return false;
|
|
@@ -219,8 +258,10 @@
|
|
|
219
258
|
|
|
220
259
|
let settings = $state(getSettings());
|
|
221
260
|
let settingsError = $state("");
|
|
222
|
-
const userName = $derived(githubMode ? githubAccess?.user.login ?? "GitHub user" : "Local user");
|
|
223
|
-
const
|
|
261
|
+
const userName = $derived(githubMode ? githubAccess?.user.name ?? githubAccess?.user.login ?? "GitHub user" : "Local user");
|
|
262
|
+
const userAvatar = $derived(githubMode && /^https:\/\/avatars\.githubusercontent\.com\//.test(githubAccess?.user.avatar_url ?? "") ? githubAccess!.user.avatar_url : "");
|
|
263
|
+
let failedAvatar = $state("");
|
|
264
|
+
const userLogin = $derived(githubMode && githubAccess ? `@${githubAccess.user.login}` : "Local mode");
|
|
224
265
|
let content: HTMLDivElement;
|
|
225
266
|
let contentWidth = $state(0);
|
|
226
267
|
let resizing = $state(false);
|
|
@@ -270,7 +311,7 @@
|
|
|
270
311
|
function eraseBrowserData() {
|
|
271
312
|
actionDialog = {
|
|
272
313
|
title: "Erase browser data?", confirmLabel: "Erase data", danger: true,
|
|
273
|
-
description: "This removes Rigel settings, API keys and unsaved drafts from this browser. Saved repository files will not change. The editor will reload.",
|
|
314
|
+
description: "This removes Rigel settings, API keys, GitHub login and unsaved drafts from this browser. Saved repository files will not change. The editor will reload.",
|
|
274
315
|
onconfirm: () => {
|
|
275
316
|
if (saveState === "saving") throw new Error("Wait for the current save to finish.");
|
|
276
317
|
eraseData();
|
|
@@ -369,10 +410,10 @@
|
|
|
369
410
|
if (current?.kind === "document") map[current.id] = draft;
|
|
370
411
|
return map;
|
|
371
412
|
});
|
|
372
|
-
|
|
413
|
+
function documentPreview(mode: "draft" | "website") {
|
|
373
414
|
if (sel?.kind !== "document" || newDocument?.id === sel.id) return null;
|
|
374
|
-
return
|
|
375
|
-
}
|
|
415
|
+
return previewUrl(manifest.preview ?? {}, { kind: "document", type: sel.type, slug: sel.id.slice(sel.type.length + 1) }, mode);
|
|
416
|
+
}
|
|
376
417
|
const standaloneDocument = $derived(sel?.kind === "document" && activeDocType?.shared === true);
|
|
377
418
|
const visualSelection = $derived(sel?.kind === "post" || (sel?.kind === "document" && !standaloneDocument));
|
|
378
419
|
|
|
@@ -593,10 +634,9 @@
|
|
|
593
634
|
actionDialog = {
|
|
594
635
|
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",
|
|
595
636
|
onconfirm: (title) => {
|
|
596
|
-
const slug = slugify(title);
|
|
637
|
+
const slug = slugify(title, c.posts.map((post) => post.slug));
|
|
597
638
|
if (!slug) throw new Error("Enter a title containing letters or numbers.");
|
|
598
|
-
|
|
599
|
-
newEntry = { collection: c.name, post: { slug, frontmatter: { title, date: new Date().toISOString().slice(0, 10), author: "", draft: true, ...Object.fromEntries(c.fields.filter(field => field.type === "reference" && field.multiple).map(field => [field.key, []])) }, body: emptyBody() } };
|
|
639
|
+
newEntry = { collection: c.name, post: { slug, frontmatter: initialCollectionValues(c.fields, title), body: emptyBody() } };
|
|
600
640
|
openSelection({ kind: "post", collection: c.name, slug });
|
|
601
641
|
},
|
|
602
642
|
};
|
|
@@ -610,7 +650,10 @@
|
|
|
610
650
|
description: unsaved ? "This discards the new post and its unsaved changes." : `This deletes ${current.slug}.json${githubMode ? ` from ${manifest.github?.branch}` : ""} and discards any unsaved changes. This cannot be undone in the editor.`,
|
|
611
651
|
onconfirm: async () => {
|
|
612
652
|
if (!unsaved) {
|
|
613
|
-
if (githubMode)
|
|
653
|
+
if (githubMode) {
|
|
654
|
+
try { await deleteRemotePost(requireGitContext(), manifest, collection.name, current.slug); }
|
|
655
|
+
catch (error) { handleAuthError(error); throw error; }
|
|
656
|
+
}
|
|
614
657
|
else {
|
|
615
658
|
const res = await fetch(`/rigel/api/local/${collection.name}/${encodeURIComponent(current.slug)}`, { method: "DELETE" });
|
|
616
659
|
if (!res.ok) throw new Error((await res.text()) || `Delete failed (${res.status})`);
|
|
@@ -626,7 +669,23 @@
|
|
|
626
669
|
|
|
627
670
|
</script>
|
|
628
671
|
|
|
672
|
+
{#snippet avatar()}
|
|
673
|
+
<span class="avatar">
|
|
674
|
+
{#if userAvatar && failedAvatar !== userAvatar}
|
|
675
|
+
<img src={userAvatar} alt="" width="36" height="36" referrerpolicy="no-referrer" onerror={() => { failedAvatar = userAvatar; }} />
|
|
676
|
+
{:else}
|
|
677
|
+
<Icon name="user" size={20} />
|
|
678
|
+
{/if}
|
|
679
|
+
</span>
|
|
680
|
+
{/snippet}
|
|
681
|
+
|
|
629
682
|
<svelte:window
|
|
683
|
+
onstorage={(event) => {
|
|
684
|
+
if ((event.key === "rigelcms.session" || event.key === null) && blueprint?.github && !readSession(blueprint.github)) {
|
|
685
|
+
invalidateSession();
|
|
686
|
+
connectionError = "Signed out in another tab. Reconnect GitHub to save. Your unsaved edits are still here.";
|
|
687
|
+
}
|
|
688
|
+
}}
|
|
630
689
|
onresize={endPreviewResize}
|
|
631
690
|
onblur={endPreviewResize}
|
|
632
691
|
oncontextmenu={(event) => { if (!settings.devMode) event.preventDefault(); }}
|
|
@@ -645,37 +704,32 @@
|
|
|
645
704
|
{#if !ready || (githubMode && !connected)}
|
|
646
705
|
<div class="rigel connection-shell">
|
|
647
706
|
<main id="github-connection" class="connection-card" aria-busy={connectionBusy}>
|
|
648
|
-
<img src={logo} alt="" width="64" height="64"
|
|
649
|
-
<h1>{!ready ? "
|
|
707
|
+
<div class="connection-brand"><img src={logo} alt="" width="64" height="64" /><span>Rigel <span class="brand-suffix">CMS</span></span></div>
|
|
708
|
+
<h1>{!ready ? "Loading editor" : "Connect GitHub"}</h1>
|
|
650
709
|
{#if !ready}
|
|
651
710
|
{#if loadError}<p class="connection-error" role="alert">{loadError}</p><button class="primary" onclick={loadBlueprint}>Retry loading</button>
|
|
652
711
|
{:else}<p role="status">Loading the editor configuration…</p>{/if}
|
|
653
712
|
{:else if manifest.github}
|
|
654
|
-
<p>Content repository <strong>{manifest.github.repo}</strong></p>
|
|
655
713
|
{#if githubAccess}
|
|
656
714
|
<p>Signed in as <strong>{githubAccess.user.login}</strong>.</p>
|
|
657
|
-
{#if githubAccess.status === "read-only"}
|
|
658
|
-
<p>
|
|
659
|
-
{:else if githubAccess.status === "installation-required"}
|
|
660
|
-
<p>Install or configure the Rigel GitHub App for this repository. On GitHub, choose your account or organization, select <strong>Only select repositories</strong>, and include <strong>{manifest.github.repo}</strong>.</p>
|
|
661
|
-
<p>If your organization requires approval, its administrator must approve the installation first.</p>
|
|
715
|
+
{#if githubAccess.status === "read-only" || githubAccess.status === "installation-required"}
|
|
716
|
+
<p>You don’t have editing access. Contact the website administrator.</p>
|
|
662
717
|
{:else}
|
|
663
|
-
<p role="status">{connectionBusy ? "
|
|
718
|
+
<p role="status">{connectionBusy ? "Opening the editor…" : "Access confirmed. Try opening the editor again."}</p>
|
|
664
719
|
{/if}
|
|
665
720
|
{#if githubAccess.status !== "ready"}
|
|
666
|
-
<a id="github-install" class="primary" href={githubAccess.installUrl} target="_blank" rel="noopener noreferrer">
|
|
667
|
-
<p>Keep this tab open. Return here after configuring access on GitHub.</p>
|
|
721
|
+
<a id="github-install" class="primary" href={githubAccess.installUrl} target="_blank" rel="noopener noreferrer">Manage GitHub access</a>
|
|
668
722
|
{/if}
|
|
669
723
|
{:else}
|
|
670
|
-
<p>Sign in
|
|
724
|
+
<p>Sign in to edit this website.</p>
|
|
671
725
|
{/if}
|
|
672
726
|
<div class="connection-actions">
|
|
673
727
|
{#if sessionExpiresAt && !tokenExpired}
|
|
674
|
-
<button id="github-check-access" class="primary" disabled={connectionBusy} onclick={recheckGitHub}>{connectionBusy ? "Checking access…" : githubAccess?.status === "ready" ? "
|
|
728
|
+
<button id="github-check-access" class="primary" disabled={connectionBusy} onclick={recheckGitHub}>{connectionBusy ? "Checking access…" : githubAccess?.status === "ready" ? "Open editor" : "Check access"}</button>
|
|
675
729
|
{/if}
|
|
676
730
|
<button id="github-sign-in" class:primary={!sessionExpiresAt || tokenExpired} class:outlined={sessionExpiresAt > 0 && !tokenExpired} disabled={connectionBusy} onclick={connectGitHub}>{connectionBusy && !sessionExpiresAt ? "Signing in…" : sessionExpiresAt ? "Sign in again" : "Sign in with GitHub"}</button>
|
|
677
731
|
</div>
|
|
678
|
-
<p class="connection-note">
|
|
732
|
+
<p class="connection-note">By continuing, you agree to the <a href="https://rigelcms.dev/terms" target="_blank" rel="noopener noreferrer">Terms of Use</a> and acknowledge the <a href="https://rigelcms.dev/privacy" target="_blank" rel="noopener noreferrer">Privacy Policy</a>.</p>
|
|
679
733
|
{/if}
|
|
680
734
|
{#if connectionError}<p class="connection-error" role="alert">{connectionError}</p>{/if}
|
|
681
735
|
</main>
|
|
@@ -685,7 +739,7 @@
|
|
|
685
739
|
<aside id="rigel-sidebar">
|
|
686
740
|
<a class="brand" href="/rigel/" role="button" aria-label="Welcome" aria-describedby="rigel-beta" onclick={(event) => { event.preventDefault(); goWelcome(); }}>
|
|
687
741
|
<img src={logo} alt="" width="32" height="32" />
|
|
688
|
-
<span>
|
|
742
|
+
<span>Rigel <span class="brand-suffix">CMS</span></span>
|
|
689
743
|
<span id="rigel-beta" class="badge">Beta</span>
|
|
690
744
|
</a>
|
|
691
745
|
<TopBar {manifest} {sel} onselect={select} />
|
|
@@ -706,7 +760,7 @@
|
|
|
706
760
|
data-document={node.target.kind === "document" ? node.target.id : undefined}
|
|
707
761
|
data-page={node.target.kind === "page" ? node.target.slug : undefined}
|
|
708
762
|
aria-label={node.label}
|
|
709
|
-
|
|
763
|
+
data-tooltip={node.label}
|
|
710
764
|
class:active={navActive(node.target)}
|
|
711
765
|
aria-current={navActive(node.target) ? "page" : undefined}
|
|
712
766
|
onclick={() => select(node.target)}
|
|
@@ -717,21 +771,21 @@
|
|
|
717
771
|
{/each}
|
|
718
772
|
</nav>
|
|
719
773
|
|
|
720
|
-
<button class="sidebar-toggle" aria-expanded={!settings.userPref.sidebarCollapsed} aria-controls="rigel-sidebar-navigation" aria-label={settings.userPref.sidebarCollapsed ? "Expand sidebar" : "Collapse sidebar"}
|
|
774
|
+
<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; }}>
|
|
721
775
|
<SidebarIcon name="sidebar-simple" /><span class="sidebar-label">Collapse sidebar</span>
|
|
722
776
|
</button>
|
|
723
777
|
<div class="aside-foot">
|
|
724
778
|
<div class="user-menu-wrap">
|
|
725
|
-
<button class="user-btn" aria-label={githubMode ? "GitHub user menu" : "Local user menu"}
|
|
726
|
-
|
|
727
|
-
<span class="user-details"><span class="uname">{userName}</span><small>{
|
|
779
|
+
<button class="user-btn" aria-label={githubMode ? "GitHub user menu" : "Local user menu"} data-tooltip={githubMode ? "GitHub user menu" : "Local user menu"} aria-haspopup="menu" aria-expanded={userOpen} onclick={() => (userOpen = !userOpen)}>
|
|
780
|
+
{@render avatar()}
|
|
781
|
+
<span class="user-details"><span class="uname">{userName}</span><small>{userLogin}</small></span>
|
|
728
782
|
<span class="user-menu-icon"><Icon name="dotsThreeOutlineVertical" size={18} /></span>
|
|
729
783
|
</button>
|
|
730
784
|
{#if userOpen}
|
|
731
785
|
<div class="user-menu" role="menu">
|
|
732
786
|
<div class="user-menu-head">
|
|
733
|
-
|
|
734
|
-
<div class="user-details"><strong>{userName}</strong><small>{
|
|
787
|
+
{@render avatar()}
|
|
788
|
+
<div class="user-details"><strong>{userName}</strong><small>{userLogin}</small></div>
|
|
735
789
|
</div>
|
|
736
790
|
<div class="divider"></div>
|
|
737
791
|
<a role="menuitem" href="/" target="_blank" rel="noopener">
|
|
@@ -752,7 +806,7 @@
|
|
|
752
806
|
<a role="menuitem" href={githubAccess?.installUrl} target="_blank" rel="noopener noreferrer">Configure GitHub access</a>
|
|
753
807
|
<button role="menuitem" class="github-reconnect" disabled={connectionBusy || saveState === "saving"} onclick={() => { userOpen = false; void connectGitHub(); }}>Reconnect GitHub</button>
|
|
754
808
|
{/if}
|
|
755
|
-
<button id={githubMode ? "github-sign-out" : undefined} role="menuitem" class:muted={!githubMode} disabled={!githubMode || saveState === "saving" || connectionBusy}
|
|
809
|
+
<button id={githubMode ? "github-sign-out" : undefined} role="menuitem" class:muted={!githubMode} disabled={!githubMode || saveState === "saving" || connectionBusy} data-tooltip={githubMode ? "Sign out of this editor" : "Local mode"} onclick={signOut}>
|
|
756
810
|
<Icon name="signOut" size={13} />
|
|
757
811
|
Sign Out
|
|
758
812
|
{#if !githubMode}<span class="soon">backend: github</span>{/if}
|
|
@@ -799,7 +853,7 @@
|
|
|
799
853
|
<button class="primary" onclick={() => (sel?.kind === "collection" ? newPost(coll!) : newPage(sel?.kind === "documents" ? activeDocType : pageDocType))}>Add new</button>
|
|
800
854
|
{/if}
|
|
801
855
|
{#if sel?.kind === "post" && post}
|
|
802
|
-
<button class="icon-btn"
|
|
856
|
+
<button class="icon-btn" aria-label="Delete post" data-tooltip="Delete post" disabled={saveState === "saving"} onclick={deletePost}>
|
|
803
857
|
<Icon name="trash" size={15} />
|
|
804
858
|
</button>
|
|
805
859
|
{/if}
|
|
@@ -876,7 +930,8 @@
|
|
|
876
930
|
docType={activeDocType}
|
|
877
931
|
values={draft}
|
|
878
932
|
documents={draftDocuments}
|
|
879
|
-
previewPath={
|
|
933
|
+
previewPath={documentPreview("draft")}
|
|
934
|
+
websitePath={documentPreview("website")}
|
|
880
935
|
creating={newDocument?.id === sel.id}
|
|
881
936
|
devMode={settings.devMode}
|
|
882
937
|
reloadToken={documentReload}
|
|
@@ -923,8 +978,8 @@
|
|
|
923
978
|
body={bodyDraft}
|
|
924
979
|
bodyInline={settings.userPref.editorExperience === "inline"}
|
|
925
980
|
onbody={(value) => { bodyDraft = value; touch(); }}
|
|
926
|
-
previewPath={manifest.preview ?
|
|
927
|
-
|
|
981
|
+
previewPath={manifest.preview ? previewUrl(manifest.preview, sel, "draft", !!newEntry) : null}
|
|
982
|
+
websitePath={manifest.preview ? previewUrl(manifest.preview, sel, "website") : null}
|
|
928
983
|
creating={!!newEntry}
|
|
929
984
|
devMode={settings.devMode}
|
|
930
985
|
onchange={(next) => { draft = next; touch(); }}
|
|
@@ -938,7 +993,7 @@
|
|
|
938
993
|
{:else}
|
|
939
994
|
<div class="welcome">
|
|
940
995
|
<img src={logo} alt="" width="64" height="64" />
|
|
941
|
-
<h2>Welcome to
|
|
996
|
+
<h2>Welcome to Rigel CMS</h2>
|
|
942
997
|
<p>The best CMS in the whole space.</p>
|
|
943
998
|
<p class="welcome-hint">Select a page or collection from the sidebar to start editing.</p>
|
|
944
999
|
</div>
|
|
@@ -959,7 +1014,7 @@
|
|
|
959
1014
|
{/if}
|
|
960
1015
|
|
|
961
1016
|
{#if settingsOpen}
|
|
962
|
-
<Settings bind:s={settings} error={settingsError} saving={saveState === "saving"} onerase={eraseBrowserData} onclose={() => (settingsOpen = false)} />
|
|
1017
|
+
<Settings bind:s={settings} github={githubMode ? manifest.github : undefined} user={githubAccess?.user} error={settingsError} saving={saveState === "saving"} onerase={eraseBrowserData} onclose={() => (settingsOpen = false)} />
|
|
963
1018
|
{/if}
|
|
964
1019
|
{#if shortcutsOpen}
|
|
965
1020
|
<Shortcuts onclose={() => (shortcutsOpen = false)} />
|
|
@@ -509,10 +509,10 @@
|
|
|
509
509
|
|
|
510
510
|
{#snippet formatButtons()}
|
|
511
511
|
{#each marks.slice(0, 2) as mark}
|
|
512
|
-
<button type="button" aria-label={mark.label}
|
|
512
|
+
<button type="button" aria-label={mark.label} data-tooltip={mark.label} aria-pressed={activeMarks.includes(mark.type)} onpointerdown={event => event.preventDefault()} onclick={() => format(mark.type)}><Icon name={mark.icon} size={18} /></button>
|
|
513
513
|
{/each}
|
|
514
|
-
<button type="button" aria-label="Link"
|
|
515
|
-
<button type="button" aria-label="More formatting"
|
|
514
|
+
<button type="button" aria-label="Link" data-tooltip="Link" aria-expanded={linkOpen} aria-controls={`${id}-link`} onpointerdown={event => event.preventDefault()} onclick={openLink}><Icon name="link" size={18} /></button>
|
|
515
|
+
<button type="button" aria-label="More formatting" data-tooltip="More formatting" aria-haspopup="menu" aria-expanded={formatsOpen} aria-controls={`${id}-formats`} popovertarget={`${id}-formats`} onpointerdown={event => event.preventDefault()} onclick={event => { event.preventDefault(); formatTrigger = event.currentTarget; formatMenu.matches(':popover-open') ? closeMenu() : openMenu(event, formatMenu); }}><Icon name="caretDown" size={18} /></button>
|
|
516
516
|
{/snippet}
|
|
517
517
|
{#snippet tableControls()}
|
|
518
518
|
<div class="block-table-controls" role="group" aria-label="Table controls">
|
|
@@ -526,24 +526,24 @@
|
|
|
526
526
|
{#snippet toolbar(image: boolean)}
|
|
527
527
|
<div class="body-toolbar formatting-toolbar" class:image-toolbar={image} role="group" aria-label={image ? "Image editing" : "Body formatting"} style={inline || image ? floating : undefined} hidden={markdownView || ((inline || image) && !floating) || (image && !imageData)}>
|
|
528
528
|
{#if image}
|
|
529
|
-
<button type="button" aria-label="Edit alt text"
|
|
530
|
-
<button type="button" aria-label={imageData?.url ? "Edit image link" : "Add image link"}
|
|
529
|
+
<button type="button" aria-label="Edit alt text" data-tooltip="Edit alt text" onpointerdown={event => event.preventDefault()} onclick={openAlt}><Icon name="textAa" size={18} /></button>
|
|
530
|
+
<button type="button" aria-label={imageData?.url ? "Edit image link" : "Add image link"} data-tooltip={imageData?.url ? "Edit image link" : "Add image link"} onpointerdown={event => event.preventDefault()} onclick={openLink}><Icon name="link" size={18} /></button>
|
|
531
531
|
{:else}
|
|
532
|
-
<Select ariaLabel="Block type"
|
|
532
|
+
<Select ariaLabel="Block type" data-tooltip={activeBlock?.label ?? "Block type"} icon={activeBlock?.icon ?? "paragraph"} value={imageData ? "" : blockType} options={textBlocks} disabled={!!imageData} onchange={next => { hoverKey = ""; changeBlock(String(next)); }} />
|
|
533
533
|
{#if !inline || !["image", "divider"].includes(blockType)}<span class="toolbar-separator" aria-hidden="true"></span>{@render formatButtons()}<span class="toolbar-separator" aria-hidden="true"></span>{/if}
|
|
534
534
|
{/if}
|
|
535
535
|
<ImagePicker toolbar value={image ? imageData?.src ?? "" : ""} altText={image ? imageData?.alt ?? "" : ""} triggerLabel={image ? "Replace image" : "Insert image"} onopen={() => { if (!imageJob) imageJob = { key: activeKey, replace: image }; linkOpen = false; altOpen = false; }} onpick={pickImage} onclose={() => { imageJob = null; refocus(); }} />
|
|
536
536
|
{#if !image}
|
|
537
|
-
<button type="button" aria-label="Table"
|
|
538
|
-
<button type="button" aria-label="Bulleted list"
|
|
539
|
-
<button type="button" aria-label="Numbered list"
|
|
537
|
+
<button type="button" aria-label="Table" data-tooltip="Table" aria-pressed={blockType === "table"} popovertarget={`${id}-table`} onpointerdown={event => event.preventDefault()} onclick={event => { event.preventDefault(); tableMenu.matches(':popover-open') ? closeMenu() : openMenu(event, tableMenu); }}><Icon name="table" size={18} /></button>
|
|
538
|
+
<button type="button" aria-label="Bulleted list" data-tooltip="Bulleted list" aria-pressed={listType === "bullet"} disabled={["image", "divider"].includes(blockType)} onpointerdown={event => event.preventDefault()} onclick={() => toggleList("bullet")}><Icon name="listBullets" size={18} /></button>
|
|
539
|
+
<button type="button" aria-label="Numbered list" data-tooltip="Numbered list" aria-pressed={listType === "number"} disabled={["image", "divider"].includes(blockType)} onpointerdown={event => event.preventDefault()} onclick={() => toggleList("number")}><Icon name="listNumbers" size={18} /></button>
|
|
540
540
|
{#if !inline}
|
|
541
541
|
<button type="button" class="block-history" disabled={!canUndo} onclick={() => editor.dispatchCommand(L.UNDO_COMMAND, undefined)}>Undo</button>
|
|
542
542
|
<button type="button" class="block-history" disabled={!canRedo} onclick={() => editor.dispatchCommand(L.REDO_COMMAND, undefined)}>Redo</button>
|
|
543
|
-
<button type="button" class="block-add" aria-label="Add block"
|
|
543
|
+
<button type="button" class="block-add" aria-label="Add block" data-tooltip="Add block" onclick={addBlock}>Add block</button>
|
|
544
544
|
{/if}
|
|
545
545
|
{/if}
|
|
546
|
-
{#if inline || image}<span class="toolbar-separator" aria-hidden="true"></span><button type="button" aria-label="More actions"
|
|
546
|
+
{#if inline || image}<span class="toolbar-separator" aria-hidden="true"></span><button type="button" aria-label="More actions" data-tooltip="More actions" aria-haspopup="true" popovertarget={`${id}-actions`} onpointerdown={event => event.preventDefault()} onclick={event => { event.preventDefault(); menu.matches(':popover-open') ? closeMenu() : openMenu(event); }}><Icon name="dotsThreeOutlineVertical" size={18} /></button>{/if}
|
|
547
547
|
</div>
|
|
548
548
|
{/snippet}
|
|
549
549
|
<div class="block-editor" bind:this={editorHost}>
|
|
@@ -556,9 +556,9 @@
|
|
|
556
556
|
<label for={`${id}-url`}>Link URL</label>
|
|
557
557
|
<div class="markdown-link-controls">
|
|
558
558
|
<input id={`${id}-url`} type="text" placeholder="https://…" bind:this={linkInput} bind:value={linkUrl} aria-invalid={!!linkError} />
|
|
559
|
-
<button type="button" aria-label="Apply link"
|
|
560
|
-
<button type="button" aria-label="Remove link"
|
|
561
|
-
<button type="button" aria-label="Close link"
|
|
559
|
+
<button type="button" aria-label="Apply link" data-tooltip="Apply link" onclick={() => applyLink()}><Icon name="check" size={16} /></button>
|
|
560
|
+
<button type="button" aria-label="Remove link" data-tooltip="Remove link" onclick={() => applyLink(true)}><Icon name="linkBreak" size={16} /></button>
|
|
561
|
+
<button type="button" aria-label="Close link" data-tooltip="Close link" onclick={() => { linkOpen = false; refocus(); }}><Icon name="x" size={16} /></button>
|
|
562
562
|
</div>
|
|
563
563
|
{#if linkError}<p role="alert">{linkError}</p>{/if}
|
|
564
564
|
</div>
|
|
@@ -568,8 +568,8 @@
|
|
|
568
568
|
<label for={`${id}-alt`}>Alt text (leave empty if decorative)</label>
|
|
569
569
|
<div class="markdown-link-controls">
|
|
570
570
|
<input id={`${id}-alt`} type="text" aria-label="Image alt text" bind:this={altInput} bind:value={altText} />
|
|
571
|
-
<button type="button" aria-label="Apply alt text"
|
|
572
|
-
<button type="button" aria-label="Close alt text"
|
|
571
|
+
<button type="button" aria-label="Apply alt text" data-tooltip="Apply alt text" onclick={applyAlt}><Icon name="check" size={16} /></button>
|
|
572
|
+
<button type="button" aria-label="Close alt text" data-tooltip="Close alt text" onclick={() => { altOpen = false; refocus(); }}><Icon name="x" size={16} /></button>
|
|
573
573
|
</div>
|
|
574
574
|
</div>
|
|
575
575
|
{/if}
|
|
@@ -578,9 +578,9 @@
|
|
|
578
578
|
<div class="block-markdown-view">{#if inline}<button type="button" class="outlined" onclick={async () => { markdownView = false; await tick(); editor.focus(); }}>Close Markdown view</button>{/if}<pre class="block-markdown" aria-label="Body as Markdown" tabindex="0">{articleMarkdown(value)}</pre></div>
|
|
579
579
|
{/if}
|
|
580
580
|
<div class="block-surface" bind:this={surface} hidden={markdownView} onpointermove={hover} role="group" aria-label="Article blocks">
|
|
581
|
-
{#if insertion && !markdownView}<button type="button" class="block-insert" style={`top:${insertion.top}px;left:${insertion.left}px`} aria-label="Insert paragraph here"
|
|
581
|
+
{#if insertion && !markdownView}<button type="button" class="block-insert" style={`top:${insertion.top}px;left:${insertion.left}px`} aria-label="Insert paragraph here" data-tooltip="Insert paragraph" onpointerdown={event => event.preventDefault()} onpointerleave={leaveInsertion} onblur={() => { insertion = null; }} onclick={insertParagraph}><Icon name="plus" size={16} /></button>{/if}
|
|
582
582
|
{#if !inline}<div class="block-content" contenteditable="true" role="textbox" aria-label="Body" aria-multiline="true" bind:this={root}></div>{/if}
|
|
583
|
-
{#if hoverKey || activeKey}<button bind:this={handle} type="button" class="block-handle" style={`top:${handleTop}px;${inline ? `left:${handleLeft}px` : ''}`} aria-label="Block options and drag handle"
|
|
583
|
+
{#if hoverKey || activeKey}<button bind:this={handle} type="button" class="block-handle" style={`top:${handleTop}px;${inline ? `left:${handleLeft}px` : ''}`} aria-label="Block options and drag handle" data-tooltip="Drag to reorder; click for block options" onpointerdown={startDrag} onpointermove={moveDrag} onpointerup={() => endDrag()} onpointercancel={() => endDrag(true)} onlostpointercapture={() => endDrag(true)} onclick={openMenu}><Icon name="dotsThreeOutlineVertical" size={16} /></button>{/if}
|
|
584
584
|
{#if drop}<div class="block-drop-indicator" style={`top:${drop.top}px;${inline ? `position:fixed;left:${root.getBoundingClientRect().left}px;width:${root.getBoundingClientRect().width}px;right:auto` : ''}`}></div>{/if}
|
|
585
585
|
<div bind:this={slashMenu} class="block-slash" id={`${id}-slash`} popover="manual" style={slashPosition} role="listbox" aria-label="Insert block" onkeydown={slashKeydown}>
|
|
586
586
|
{#if slash !== null}
|