@rigel-cms/rigel 1.3.0 → 1.4.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/docs/content-locales.md +8 -0
- package/package.json +1 -1
- package/src/editor/App.svelte +43 -21
- package/src/editor/i18n/de/shell.json +3 -0
- package/src/editor/i18n/en/shell.json +3 -0
- package/src/editor/i18n/es/shell.json +3 -0
- package/src/editor/i18n/fr/shell.json +3 -0
- package/src/editor/i18n/it/shell.json +3 -0
- package/src/editor/i18n/ja/shell.json +3 -0
- package/src/editor/i18n/nl/shell.json +3 -0
- package/src/editor/i18n/ru/shell.json +3 -0
- package/src/editor/i18n/zh/shell.json +3 -0
- package/src/editor/lib/ActionDialog.svelte +23 -3
- package/src/editor/lib/MarkdownEditor.svelte +7 -5
- package/src/editor/lib/VisualEditor.svelte +6 -4
- package/src/editor/styles.css +4 -0
- package/src/manifest/navigation.ts +2 -2
- package/src/manifest/pages.ts +17 -0
package/docs/content-locales.md
CHANGED
|
@@ -51,3 +51,11 @@ Content without a `locale` remains visible in every language. Use this for commo
|
|
|
51
51
|
Without `contentLocales`, the editor keeps its existing navigation and shows no content-language dropdown. A single configured locale also needs no dropdown.
|
|
52
52
|
|
|
53
53
|
Your Astro routes and templates still own website localization, including public language links, HTML `lang`, translated navigation and fallback behavior. Rigel does not translate text, pair translated entries or generate localized routes. Keep each type's URL mapping consistent with the website routes, and prepare its preview bindings as usual.
|
|
54
|
+
|
|
55
|
+
## Page categories and templates
|
|
56
|
+
|
|
57
|
+
Set `category` on a nonshared document definition to group its pages, for example `category: "Articles"`. This is schema metadata, not an editable field in stored documents. Uncategorised native documents and legacy copy pages remain in **Core** without a migration. When categories exist, the Pages table offers Core, All pages and each category visible in the selected content language. Core is the initial filter when uncategorised pages or types exist; category-only sites initially show All pages. Back returns to the originating table/filter, not a filter inferred from the opened page.
|
|
58
|
+
|
|
59
|
+
Add new asks for a page name and a category from the locale-eligible, creatable document types. When a category contains multiple types, the editor also requires a Template choice using each definition's title. Shared, singleton and `creatable: false` types are not creation choices. A single uncategorised type keeps the name-only dialog. Creation uses the selected type's identity, initial values and section template; nothing is written until Save.
|
|
60
|
+
|
|
61
|
+
A visual variant can be another document definition with its own category, defaults and template—for example a Resources category with a dark-theme initial value. Its native type, folder, preview mapping and host renderer must be configured normally. A category does not change the editor theme, rewrite existing document types or generate website routes.
|
package/package.json
CHANGED
package/src/editor/App.svelte
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import TopBar from "./lib/TopBar.svelte";
|
|
16
16
|
import Select from "./lib/Select.svelte";
|
|
17
17
|
import { contentForLocale, selectionInContent } from "../manifest/content-locales";
|
|
18
|
+
import { pageCategories, pageTemplates, inPageCategory, defaultPageCategory } from "../manifest/pages";
|
|
18
19
|
import { validateDocumentSlugs } from "../slugs";
|
|
19
20
|
import Shortcuts from "./lib/Shortcuts.svelte";
|
|
20
21
|
import CollectionPanel from "./lib/CollectionPanel.svelte";
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
import VisualEditor from "./lib/VisualEditor.svelte";
|
|
39
40
|
import { newDocumentFromTemplate } from "../schema/paths";
|
|
40
41
|
import { previewUrl, previewPath } from "../preview";
|
|
41
|
-
import type { ContentDocument, Schema } from "../schema/types";
|
|
42
|
+
import type { ContentDocument, DocumentDef, Schema } from "../schema/types";
|
|
42
43
|
import { buildSidebar, navigationKey, type Selection } from "../manifest/navigation";
|
|
43
44
|
import { startGitHubLogin, checkGitHubAccess, type GitHubAccess } from "./lib/github-auth";
|
|
44
45
|
import { loadManifestRemote, saveRemoteSelection, deleteRemotePost, reviewRemotePublish, confirmRemotePublish, PublishReviewChangedError, type GitContext, type PublishReview } from "./lib/github-backend";
|
|
@@ -142,7 +143,7 @@
|
|
|
142
143
|
}
|
|
143
144
|
let pendingNavigation = $state<(() => void) | null>(null);
|
|
144
145
|
let pendingNavigationCancel: (() => void) | null = null;
|
|
145
|
-
let actionDialog = $state<{ title: string; description: string; confirmLabel: string; fieldLabel?: string; danger?: boolean; onconfirm: (value: string) => void | Promise<void> } | null>(null);
|
|
146
|
+
let actionDialog = $state<{ title: string; description: string; confirmLabel: string; fieldLabel?: string; danger?: boolean; templates?: DocumentDef[]; templateCategory?: string; onconfirm: (value: string, type?: string) => void | Promise<void> } | null>(null);
|
|
146
147
|
let newEntry = $state<{ collection: string; post: CollectionPost } | null>(null);
|
|
147
148
|
let newDocument = $state<{ type: string; id: string; slug: string } | null>(null);
|
|
148
149
|
let docBase = $state<Record<string, unknown> | null>(null);
|
|
@@ -159,6 +160,7 @@
|
|
|
159
160
|
if (!Array.isArray(data.pages) || (data.backend === "github" && !data.github)) throw new Error($t("shell.configurationIncomplete"));
|
|
160
161
|
blueprint = data;
|
|
161
162
|
manifest = data;
|
|
163
|
+
pagesFilter = defaultPageCategory(contentForLocale(data, data.contentLocales?.defaultLocale));
|
|
162
164
|
ready = true;
|
|
163
165
|
if (data.backend === "github" && data.github) {
|
|
164
166
|
const session = readSession(data.github);
|
|
@@ -478,11 +480,18 @@
|
|
|
478
480
|
});
|
|
479
481
|
|
|
480
482
|
const sidebar = $derived(buildSidebar(visibleContent, $t));
|
|
481
|
-
|
|
483
|
+
let pagesFilter = $state<string | null>("");
|
|
484
|
+
let entryList = $state<Sel | null>(null);
|
|
485
|
+
const categories = $derived(pageCategories(visibleContent));
|
|
486
|
+
const pageCategory = $derived(sel?.kind === "pages" && sel.category !== undefined ? sel.category : pagesFilter);
|
|
482
487
|
const schema = $derived<Schema | undefined>(manifest.schema);
|
|
483
488
|
const docTypes = $derived(visibleContent.schema?.documents ?? []);
|
|
484
489
|
|
|
485
490
|
const pageDocType = $derived(docTypes.find((type) => !type.shared && !type.singleton));
|
|
491
|
+
const templates = $derived(pageTemplates(visibleContent));
|
|
492
|
+
const pageBack = $derived<Extract<Sel, { kind: "pages" }>>(entryList?.kind === "pages" ? entryList : { kind: "pages" });
|
|
493
|
+
const documentBack = $derived<Sel | null>(entryList?.kind === "documents" && entryList.type === activeDocType?.name ? entryList : !activeDocType?.shared ? pageBack : activeDocType.singleton ? null : { kind: "documents", type: activeDocType.name });
|
|
494
|
+
const pageBackLabel = $derived((pageBack.category ?? pagesFilter) || (pagesFilter === "" && categories.length ? $t("shell.coreCategory") : $t("shell.pages")));
|
|
486
495
|
const pageDocuments = $derived((visibleContent.documents ?? []).filter((doc) => docTypes.some(type => type.name === doc.type && !type.shared)));
|
|
487
496
|
function docTypeName(s: Sel | null): string | null {
|
|
488
497
|
return s && (s.kind === "document" || s.kind === "documents") ? s.type : null;
|
|
@@ -509,11 +518,11 @@
|
|
|
509
518
|
|
|
510
519
|
const sortedPages = $derived.by(() => {
|
|
511
520
|
const rows = [
|
|
512
|
-
...visibleContent.pages.filter((p) => !p.shared && (
|
|
521
|
+
...visibleContent.pages.filter((p) => !p.shared && inPageCategory(p.category, pageCategory))
|
|
513
522
|
.map((p) => ({ slug: p.slug, path: previewPath(manifest.preview ?? {}, { kind: "page", slug: p.slug }), title: p.label, category: p.category, draft: false, target: { kind: "page", slug: p.slug } as Sel })),
|
|
514
523
|
...pageDocuments
|
|
515
524
|
.map((doc) => ({ slug: effectiveSlug(doc.data, doc.slug), path: previewPath(manifest.preview ?? {}, { kind: "document", type: doc.type, slug: effectiveSlug(doc.data, doc.slug) }), title: String(doc.data.title ?? doc.slug), category: docTypes.find(type => type.name === doc.type)?.category, draft: doc.data.draft === true, target: { kind: "document", type: doc.type, id: doc.id } as Sel }))
|
|
516
|
-
.filter(doc =>
|
|
525
|
+
.filter(doc => inPageCategory(doc.category, pageCategory)),
|
|
517
526
|
];
|
|
518
527
|
const key = sortBy === "slug" || sortBy === "category" || sortBy === "path" ? sortBy : "title";
|
|
519
528
|
return rows.sort((a, b) => String(a[key] ?? "").localeCompare(String(b[key] ?? "")) * sortDir);
|
|
@@ -526,13 +535,13 @@
|
|
|
526
535
|
|
|
527
536
|
const menu = $derived.by(() => { const slug = sel?.kind === "menu" ? sel.slug : null; return manifest.menus?.entries.find((m) => m.slug === slug); });
|
|
528
537
|
const sortedMenus = $derived([...(manifest.menus?.entries ?? [])].sort((a, b) => (sortBy === "slug" ? a.slug.localeCompare(b.slug) : a.title.localeCompare(b.title)) * sortDir));
|
|
529
|
-
const listTitle = $derived(sel?.kind === "menus" ? $t("shell.allMenus") : sel?.kind === "documents" ? activeDocType?.title ?? $t("shell.documents") : sel?.kind === "pages" ? pageCategory !==
|
|
538
|
+
const listTitle = $derived(sel?.kind === "menus" ? $t("shell.allMenus") : sel?.kind === "documents" ? activeDocType?.title ?? $t("shell.documents") : sel?.kind === "pages" ? pageCategory !== null && categories.length ? $t("shell.allNamedEntries", { name: pageCategory || $t("shell.coreCategory") }) : $t("shell.allPages") : coll?.listTitle?.trim() || (coll?.label !== undefined ? $t("shell.allNamedEntries", { name: coll.label }) : $t("shell.allEntries")));
|
|
530
539
|
const listRows = $derived(sel?.kind === "menus"
|
|
531
540
|
? sortedMenus.map((m) => ({ slug: m.slug, cells: { slug: m.slug, title: m.title } as Record<string, string>, draft: false, target: { kind: "menu", slug: m.slug } as Sel }))
|
|
532
541
|
: sel?.kind === "documents"
|
|
533
542
|
? typeDocuments.map((doc) => ({ slug: doc.slug, cells: Object.fromEntries(columns.map(column => [column.key, documentCell(doc, column.key)])), draft: doc.data.draft === true, target: { kind: "document", type: doc.type, id: doc.id } as Sel }))
|
|
534
543
|
: sel?.kind === "pages"
|
|
535
|
-
? sortedPages.map((p) => ({ slug: p.slug, cells: { label: p.title, slug: p.slug, path: p.path ?? "—", category: p.category ?? "—" } as Record<string, string>, draft: p.draft, target: p.target }))
|
|
544
|
+
? sortedPages.map((p) => ({ slug: p.slug, cells: { label: p.title, slug: p.slug, path: p.path ?? "—", category: p.category ?? (categories.length ? $t("shell.coreCategory") : "—") } as Record<string, string>, draft: p.draft, target: p.target }))
|
|
536
545
|
: sortedPosts.map((p) => ({ slug: p.slug, cells: Object.fromEntries(columns.map((c) => [c.key, cellValue(p, c.key)])), draft: Boolean(p.frontmatter.draft), target: { kind: "post", collection: coll!.name, slug: p.slug } as Sel })));
|
|
537
546
|
const titleColumn = $derived(sel?.kind === "pages" ? "label" : "title");
|
|
538
547
|
|
|
@@ -637,6 +646,11 @@
|
|
|
637
646
|
}
|
|
638
647
|
|
|
639
648
|
function openSelection(next: Sel | null) {
|
|
649
|
+
if (next?.kind === "page" || next?.kind === "document") {
|
|
650
|
+
if (sel?.kind === "pages" || sel?.kind === "documents") entryList = sel;
|
|
651
|
+
else if (sel?.kind !== "page" && sel?.kind !== "document") entryList = null;
|
|
652
|
+
}
|
|
653
|
+
if (next?.kind === "pages" && next.category !== undefined) pagesFilter = next.category;
|
|
640
654
|
sel = next;
|
|
641
655
|
saveError = "";
|
|
642
656
|
sortBy = "";
|
|
@@ -686,6 +700,8 @@
|
|
|
686
700
|
requestNavigation(() => {
|
|
687
701
|
const destination = contentForLocale(manifest, value);
|
|
688
702
|
selectedContentLocale = value;
|
|
703
|
+
if (pagesFilter && !pageCategories(destination).includes(pagesFilter)) pagesFilter = defaultPageCategory(destination);
|
|
704
|
+
if (!selectionInContent(destination, entryList)) entryList = null;
|
|
689
705
|
if (!selectionInContent(destination, sel)) openSelection(null);
|
|
690
706
|
focusContentLocale();
|
|
691
707
|
}, focusContentLocale);
|
|
@@ -726,21 +742,25 @@
|
|
|
726
742
|
return String(v);
|
|
727
743
|
}
|
|
728
744
|
|
|
729
|
-
function newPage(type = pageDocType) {
|
|
745
|
+
function newPage(type = pageDocType, chooseTemplate = false) {
|
|
746
|
+
if (chooseTemplate) type = templates.find(type => inPageCategory(type.category, pageCategory)) ?? templates[0];
|
|
730
747
|
if (!type || !schema || type.creatable === false || type.singleton) return;
|
|
731
748
|
actionDialog = {
|
|
732
|
-
title: $t("shell.addDocument", { name: type.title }), description: $t("shell.newDocumentDescription"), fieldLabel: type.shared ? $t("shell.name") : $t("shell.pageName"), confirmLabel: $t("shell.createDraft"),
|
|
733
|
-
|
|
749
|
+
title: $t("shell.addDocument", { name: chooseTemplate ? $t("shell.pages") : type.title }), description: $t("shell.newDocumentDescription"), fieldLabel: type.shared ? $t("shell.name") : $t("shell.pageName"), confirmLabel: $t("shell.createDraft"),
|
|
750
|
+
...(chooseTemplate ? { templates, templateCategory: type.category ?? "" } : {}),
|
|
751
|
+
onconfirm: (title, selectedType) => {
|
|
752
|
+
const selected = chooseTemplate ? templates.find(entry => entry.name === selectedType) : type;
|
|
753
|
+
if (!selected) throw new Error($t("shell.chooseTemplate"));
|
|
734
754
|
const slug = slugify(title);
|
|
735
755
|
if (!slug) throw new Error($t("shell.titleRequired"));
|
|
736
|
-
if (manifest.documents?.some(doc => doc.type ===
|
|
737
|
-
const id = `${
|
|
738
|
-
const data = newDocumentFromTemplate(schema,
|
|
739
|
-
validateDocumentSlugs(schema, [...(manifest.documents ?? []), { id, type:
|
|
740
|
-
newDocument = { type:
|
|
756
|
+
if (manifest.documents?.some(doc => doc.type === selected.name && doc.slug === slug)) throw new Error($t("shell.slugAlreadyUsed", { slug }));
|
|
757
|
+
const id = `${selected.name}:${slug}`;
|
|
758
|
+
const data = newDocumentFromTemplate(schema, selected.name, title, slug);
|
|
759
|
+
validateDocumentSlugs(schema, [...(manifest.documents ?? []), { id, type: selected.name, slug, data }], manifest.preview, $t);
|
|
760
|
+
newDocument = { type: selected.name, id, slug };
|
|
741
761
|
draft = JSON.parse(JSON.stringify(data));
|
|
742
762
|
docBase = null;
|
|
743
|
-
openSelection({ kind: "document", type:
|
|
763
|
+
openSelection({ kind: "document", type: selected.name, id });
|
|
744
764
|
},
|
|
745
765
|
};
|
|
746
766
|
}
|
|
@@ -957,10 +977,9 @@
|
|
|
957
977
|
{#if sel.kind === "page" || sel.kind === "post" || sel.kind === "menu" || sel.kind === "document"}
|
|
958
978
|
<div class="editor-actions">
|
|
959
979
|
{#if sel.kind === "page" && page}
|
|
960
|
-
<p class="back"><button onclick={() => select(page.shared ? null :
|
|
980
|
+
<p class="back"><button onclick={() => select(page.shared ? null : pageBack)}>{$t("shell.backTo", { name: page.shared ? $t("shell.welcome") : pageBackLabel })}</button></p>
|
|
961
981
|
{:else if sel.kind === "document"}
|
|
962
|
-
{
|
|
963
|
-
<p class="back"><button onclick={() => select(backSel)}>{$t("shell.backTo", { name: activeDocType?.singleton ? $t("shell.welcome") : sel.type === pageDocType?.name ? $t("shell.pages") : activeDocType?.title ?? $t("shell.documents") })}</button></p>
|
|
982
|
+
<p class="back"><button onclick={() => select(documentBack)}>{$t("shell.backTo", { name: !documentBack ? $t("shell.welcome") : documentBack.kind === "pages" ? pageBackLabel : activeDocType?.title ?? $t("shell.documents") })}</button></p>
|
|
964
983
|
{:else if sel.kind === "post" && coll}
|
|
965
984
|
<p class="back"><button onclick={() => select({ kind: "collection", name: coll.name })}>{$t("shell.backTo", { name: coll.label })}</button></p>
|
|
966
985
|
{:else if sel.kind === "menu"}
|
|
@@ -976,8 +995,11 @@
|
|
|
976
995
|
{/if}
|
|
977
996
|
<div class="titlebar">
|
|
978
997
|
<h2>{sel.kind === "page" ? page?.label : sel.kind === "menu" ? String(draft.title || menu?.slug || "") : sel.kind === "post" ? String(draft.title || post?.slug || "") : sel.kind === "document" ? activeDocType?.singleton ? activeDocType.title : String(draft.title || draft.name || activeDocument?.slug || sel.id) : listTitle}</h2>
|
|
979
|
-
{#if
|
|
980
|
-
<
|
|
998
|
+
{#if sel.kind === "pages" && categories.length}
|
|
999
|
+
<Select label={$t("shell.category")} value={JSON.stringify(pageCategory)} options={[{ value: "null", label: $t("shell.allPages") }, { value: JSON.stringify(""), label: $t("shell.coreCategory") }, ...categories.map(category => ({ value: JSON.stringify(category), label: category }))]} onchange={(value) => { if (typeof value === "string") { pagesFilter = JSON.parse(value); select({ kind: "pages" }); } }} />
|
|
1000
|
+
{/if}
|
|
1001
|
+
{#if (sel?.kind === "collection" && coll) || (sel?.kind === "pages" && templates.length) || (sel?.kind === "documents" && activeDocType && activeDocType.creatable !== false && !activeDocType.singleton)}
|
|
1002
|
+
<button class="primary" onclick={() => (sel?.kind === "collection" ? newPost(coll!) : newPage(sel?.kind === "documents" ? activeDocType : pageDocType, sel?.kind === "pages"))}>{$t("shell.addNew")}</button>
|
|
981
1003
|
{/if}
|
|
982
1004
|
{#if sel?.kind === "post" && post}
|
|
983
1005
|
<button class="icon-btn" aria-label={$t("shell.deletePost")} data-tooltip={$t("shell.deletePost")} disabled={saveState === "saving"} onclick={deletePost}>
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { t } from "../i18n";
|
|
3
3
|
import Icon from "./Icon.svelte";
|
|
4
|
-
|
|
4
|
+
import Select from "./Select.svelte";
|
|
5
|
+
import { untrack } from "svelte";
|
|
6
|
+
import type { DocumentDef } from "../../schema/types";
|
|
7
|
+
let { title, description, confirmLabel, fieldLabel, danger = false, templates, templateCategory, onconfirm, onclose }: {
|
|
5
8
|
title: string; description: string; confirmLabel: string; fieldLabel?: string; danger?: boolean;
|
|
6
|
-
|
|
9
|
+
templates?: DocumentDef[]; templateCategory?: string;
|
|
10
|
+
onconfirm: (value: string, type?: string) => void | Promise<void>; onclose: () => void;
|
|
7
11
|
} = $props();
|
|
8
12
|
let dialog: HTMLDialogElement;
|
|
9
13
|
let cancel: HTMLButtonElement;
|
|
@@ -11,6 +15,11 @@
|
|
|
11
15
|
let value = $state("");
|
|
12
16
|
let busy = $state(false);
|
|
13
17
|
let error = $state("");
|
|
18
|
+
let category = $state(untrack(() => templateCategory ?? ""));
|
|
19
|
+
let selectedTemplate = $state("");
|
|
20
|
+
const categories = $derived([...new Set((templates ?? []).map(type => type.category ?? ""))].sort());
|
|
21
|
+
const candidates = $derived((templates ?? []).filter(type => (type.category ?? "") === category));
|
|
22
|
+
const template = $derived(candidates.length === 1 ? candidates[0].name : selectedTemplate);
|
|
14
23
|
$effect(() => {
|
|
15
24
|
dialog.showModal();
|
|
16
25
|
(fieldLabel ? input : cancel).focus();
|
|
@@ -20,7 +29,10 @@
|
|
|
20
29
|
if (busy) return;
|
|
21
30
|
busy = true;
|
|
22
31
|
error = "";
|
|
23
|
-
try {
|
|
32
|
+
try {
|
|
33
|
+
if (templates && !candidates.some(type => type.name === template)) throw new Error($t("shell.chooseTemplate"));
|
|
34
|
+
await onconfirm(value.trim(), template); onclose();
|
|
35
|
+
}
|
|
24
36
|
catch (cause) { error = cause instanceof Error ? cause.message : $t("connection.actionFailed"); }
|
|
25
37
|
finally { busy = false; }
|
|
26
38
|
}
|
|
@@ -31,6 +43,14 @@
|
|
|
31
43
|
<div class="action-head"><h2 id="action-title">{title}</h2><button type="button" class="icon-btn" aria-label={$t("connection.cancel")} disabled={busy} onclick={onclose}><Icon name="x" size={16} /></button></div>
|
|
32
44
|
<p id="action-description">{description}</p>
|
|
33
45
|
{#if fieldLabel}<label><span>{fieldLabel}</span><input type="text" bind:this={input} bind:value required disabled={busy} /></label>{/if}
|
|
46
|
+
{#if templates}
|
|
47
|
+
{#if categories.some(category => category !== "")}
|
|
48
|
+
<Select label={$t("shell.category")} value={category} options={categories.map(value => ({ value, label: value || $t("shell.coreCategory") }))} disabled={busy} onchange={(value) => { if (typeof value === "string") { category = value; selectedTemplate = ""; error = ""; } }} />
|
|
49
|
+
{/if}
|
|
50
|
+
{#if candidates.length > 1}
|
|
51
|
+
<Select label={$t("shell.template")} value={selectedTemplate} options={[{ value: "", label: $t("shell.chooseTemplate"), disabled: true }, ...candidates.map(type => ({ value: type.name, label: type.title }))]} disabled={busy} onchange={(value) => { if (typeof value === "string") selectedTemplate = value; }} />
|
|
52
|
+
{/if}
|
|
53
|
+
{/if}
|
|
34
54
|
{#if error}<p class="error" role="alert">{error}</p>{/if}
|
|
35
55
|
<div class="action-actions">
|
|
36
56
|
<button type="button" bind:this={cancel} disabled={busy} onclick={onclose}>{$t("connection.cancel")}</button>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import type { SelectionBookmark } from "prosemirror-state";
|
|
15
15
|
import { parseVisualMarkdown, serializeMarkdown, safeMarkdownUrl, schema } from "./rich-markdown";
|
|
16
16
|
|
|
17
|
-
let { value, label, oninput }: { value: string; label: string; oninput: (value: string) => void } = $props();
|
|
17
|
+
let { value, label, oninput, editableRoot }: { value: string; label: string; oninput: (value: string) => void; editableRoot?: HTMLElement } = $props();
|
|
18
18
|
const id = $props.id();
|
|
19
19
|
let host: HTMLDivElement;
|
|
20
20
|
let mode = $state<"visual" | "source">("visual");
|
|
@@ -66,9 +66,10 @@
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
$effect(() => {
|
|
69
|
-
if (!host) return;
|
|
69
|
+
if (!editableRoot && !host) return;
|
|
70
70
|
if (!view) {
|
|
71
|
-
|
|
71
|
+
if (editableRoot) editableRoot.classList.add("rigel-markdown-content");
|
|
72
|
+
view = new EditorView(editableRoot ? { mount: editableRoot } : host, {
|
|
72
73
|
state: EditorState.create({ schema }),
|
|
73
74
|
attributes: { role: "textbox", "aria-label": label, "aria-multiline": "true" },
|
|
74
75
|
dispatchTransaction(transaction) {
|
|
@@ -92,6 +93,7 @@
|
|
|
92
93
|
load(value);
|
|
93
94
|
}
|
|
94
95
|
});
|
|
96
|
+
$effect(() => { const editable = mode === "visual"; view?.setProps({ editable: () => editable }); });
|
|
95
97
|
onDestroy(() => view?.destroy());
|
|
96
98
|
|
|
97
99
|
function activeMark(type: string) {
|
|
@@ -162,7 +164,7 @@
|
|
|
162
164
|
|
|
163
165
|
<svelte:window onpointerdown={event => { if (linkOpen && !document.getElementById(`${id}-link`)?.contains(event.target as Node) && !linkButton?.contains(event.target as Node)) linkOpen = false; }} />
|
|
164
166
|
|
|
165
|
-
<div class="markdown-editor body-editor">
|
|
167
|
+
<div class="markdown-editor body-editor" class:markdown-inline={!!editableRoot}>
|
|
166
168
|
<div class="markdown-heading">
|
|
167
169
|
<div class="field-label" id={`${id}-label`}>{label}</div>
|
|
168
170
|
<div class="markdown-modes" role="group" aria-label={$t("richtext.editingMode", { label })}>
|
|
@@ -199,7 +201,7 @@
|
|
|
199
201
|
{#if linkError}<p id={`${id}-error`} role="alert">{linkError}</p>{/if}
|
|
200
202
|
</div>
|
|
201
203
|
{/if}
|
|
202
|
-
<div class="md-preview rich-text" bind:this={host}></div>
|
|
204
|
+
{#if !editableRoot}<div class="md-preview rich-text" bind:this={host}></div>{/if}
|
|
203
205
|
</div>
|
|
204
206
|
{#if mode === "source"}
|
|
205
207
|
<textarea class="mono" rows="14" aria-label={$t("richtext.markdownSource", { label })} value={value} oninput={(event) => { warning = ""; emit(event.currentTarget.value); }}></textarea>
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
let bodyScope: HTMLElement | null = null;
|
|
53
53
|
let removeTooltips: (() => void) | undefined;
|
|
54
54
|
let mode = $state<"browse" | "edit">("edit");
|
|
55
|
-
let panelOpen = $state(
|
|
55
|
+
let panelOpen = $state(false);
|
|
56
56
|
let editorWidth = $state(0);
|
|
57
57
|
let requestedWidth = $state(340);
|
|
58
58
|
let resizing = $state(false);
|
|
@@ -262,19 +262,21 @@
|
|
|
262
262
|
clearHover(); clearHighlight();
|
|
263
263
|
editingBody = element; bodyContents = [...element.childNodes];
|
|
264
264
|
const scope = doc.createElement("div");
|
|
265
|
+
bodyAttributes = ["class", "style", "role", "aria-label", "aria-multiline", "contenteditable", "tabindex", "translate"].map(name => [name, element.getAttribute(name)]);
|
|
265
266
|
const syncTheme = () => { scope.dataset.theme = document.documentElement.dataset.theme; scope.dataset.contrast = document.documentElement.dataset.contrast; };
|
|
266
267
|
syncTheme();
|
|
267
268
|
const host = doc.createElement("div"); host.className = "rigel rigel-inline";
|
|
268
|
-
scope.append(host);
|
|
269
|
+
scope.append(host); doc.body.append(scope); bodyScope = scope;
|
|
269
270
|
removeTooltips = tooltipRoot(host).destroy;
|
|
270
271
|
bodyEditor = mount(MarkdownEditor, { target: host, props: {
|
|
272
|
+
editableRoot: element,
|
|
271
273
|
label: def.labelKey ? $t(def.labelKey) : def.label,
|
|
272
274
|
get value() { return String(anchorValue(values, field.path).value ?? ""); },
|
|
273
275
|
oninput: (next: string) => onchange(replaceAt(values, field.path, next)),
|
|
274
276
|
} });
|
|
275
277
|
bodyThemeObserver = new MutationObserver(syncTheme);
|
|
276
278
|
bodyThemeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "data-contrast"] });
|
|
277
|
-
void tick().then(() => host.querySelector<
|
|
279
|
+
void tick().then(() => (element.isContentEditable ? element : host.querySelector<HTMLTextAreaElement>("textarea"))?.focus());
|
|
278
280
|
return true;
|
|
279
281
|
}
|
|
280
282
|
|
|
@@ -393,7 +395,7 @@
|
|
|
393
395
|
|
|
394
396
|
function setMode(next: "browse" | "edit") {
|
|
395
397
|
mode = next;
|
|
396
|
-
panelOpen =
|
|
398
|
+
panelOpen = false;
|
|
397
399
|
if (next === "browse") {
|
|
398
400
|
clearHighlight();
|
|
399
401
|
clearHover();
|
package/src/editor/styles.css
CHANGED
|
@@ -1183,6 +1183,9 @@
|
|
|
1183
1183
|
.rigel.rigel-inline .markdown-link, .rigel .block-editor .markdown-link { position: fixed; inset: auto; width: 340px; max-width: calc(100vw - 16px); z-index: 121; }
|
|
1184
1184
|
.rigel.rigel-inline .block-markdown-view { position: fixed; inset: 12px; max-height: calc(100vh - 24px); overflow: auto; padding: 0.5rem; border: 1px solid var(--border-control); background: var(--panel); }
|
|
1185
1185
|
.rigel-article-content:focus { outline: 0; }
|
|
1186
|
+
.rigel.rigel-inline .markdown-inline { position: fixed; top: 8px; left: 8px; width: max-content; max-width: calc(100vw - 16px); max-height: calc(100vh - 16px); overflow: auto; background: var(--panel); border: 1px solid var(--border-control); box-shadow: var(--shadow-lg); pointer-events: auto; }
|
|
1187
|
+
.rigel .markdown-editor [hidden] { display: none !important; }
|
|
1188
|
+
.rigel-markdown-content { white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
1186
1189
|
:is(.rigel .block-content, .rigel-article-content)::highlight(rigel-link-selection), :is(.rigel .block-content, .rigel-article-content) ::highlight(rigel-link-selection) { background-color: Highlight; color: HighlightText; }
|
|
1187
1190
|
.rigel-article-content:focus-visible > [data-rigel-active-block] { outline: 3px solid Highlight; outline-offset: 3px; }
|
|
1188
1191
|
.rigel-article-content .block-bold { font-weight: 700; }
|
|
@@ -1316,6 +1319,7 @@
|
|
|
1316
1319
|
.rigel .reorder-status { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
1317
1320
|
.rigel .structure-list li.active { border-color: var(--accent); }
|
|
1318
1321
|
.rigel .structure-row { flex: 1; min-width: 0; display: flex; justify-content: space-between; gap: 0.5rem; align-items: center; border: 0; border-radius: 0; background: transparent; color: var(--text); padding: 0.5rem; text-align: left; cursor: pointer; font-size: 0.8125rem; }
|
|
1322
|
+
.rigel .structure-row > span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
1319
1323
|
.rigel .structure-actions, .rigel .schema-item-actions { display: flex; align-items: center; }
|
|
1320
1324
|
.rigel .structure-actions button, .rigel .schema-item-actions button { border: 0; border-left: 1px solid var(--border); border-radius: 0; background: transparent; color: var(--text-2); cursor: pointer; }
|
|
1321
1325
|
.rigel .structure-actions button:disabled, .rigel .schema-item-actions button:disabled { opacity: 0.35; cursor: default; }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Manifest } from "./types";
|
|
2
|
+
import { pageCategories } from "./pages";
|
|
2
3
|
import { systemMessage, type Translator } from "../editor/i18n/format";
|
|
3
4
|
|
|
4
5
|
export type Selection =
|
|
@@ -31,8 +32,7 @@ export function navigationKey(target: Selection): string {
|
|
|
31
32
|
export function buildSidebar(manifest: Manifest, message: Translator = systemMessage): SidebarNode[] {
|
|
32
33
|
const link = (target: SidebarTarget, label: string): SidebarLink => ({ kind: "link", target, label });
|
|
33
34
|
const types = manifest.schema?.documents ?? [];
|
|
34
|
-
const
|
|
35
|
-
const categories = [...new Set([...pages, ...types.filter(type => !type.shared)].map(entry => entry.category).filter((category): category is string => !!category))].sort();
|
|
35
|
+
const categories = pageCategories(manifest);
|
|
36
36
|
const defaults: SidebarNode[] = [
|
|
37
37
|
{ kind: "heading", label: message("system.sidebar.website") },
|
|
38
38
|
link({ kind: "pages" }, message("system.sidebar.pages")),
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Manifest } from "./types";
|
|
2
|
+
|
|
3
|
+
export function pageCategories(manifest: Manifest): string[] {
|
|
4
|
+
return [...new Set([...manifest.pages, ...(manifest.schema?.documents ?? [])].filter(entry => !entry.shared).map(entry => entry.category).filter((category): category is string => !!category))].sort();
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function pageTemplates(manifest: Manifest) {
|
|
8
|
+
return (manifest.schema?.documents ?? []).filter(type => !type.shared && !type.singleton && type.creatable !== false);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function defaultPageCategory(manifest: Manifest): "" | null {
|
|
12
|
+
return [...manifest.pages, ...(manifest.schema?.documents ?? [])].some(entry => !entry.shared && entry.category === undefined) ? "" : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function inPageCategory(category: string | undefined, filter: string | null): boolean {
|
|
16
|
+
return filter === null || (category ?? "") === filter;
|
|
17
|
+
}
|