@rigel-cms/rigel 1.1.0 → 1.2.0
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 -0
- package/docs/content-locales.md +53 -0
- package/package.json +3 -2
- package/src/config.ts +14 -0
- package/src/editor/App.svelte +53 -12
- package/src/editor/i18n/de/shell.json +1 -0
- package/src/editor/i18n/en/shell.json +1 -0
- package/src/editor/i18n/es/shell.json +1 -0
- package/src/editor/i18n/fr/shell.json +1 -0
- package/src/editor/i18n/it/shell.json +1 -0
- package/src/editor/i18n/ja/shell.json +1 -0
- package/src/editor/i18n/nl/shell.json +1 -0
- package/src/editor/i18n/ru/shell.json +1 -0
- package/src/editor/i18n/zh/shell.json +1 -0
- package/src/editor/lib/Select.svelte +2 -2
- package/src/editor/lib/VisualEditor.svelte +6 -6
- package/src/editor/styles.css +9 -0
- package/src/index.ts +1 -1
- package/src/manifest/content-locales.ts +45 -0
- package/src/manifest/navigation.ts +4 -0
- package/src/manifest/types.ts +3 -0
- package/src/preview.ts +14 -0
- package/src/schema/types.ts +1 -0
- package/src/vite/manifest-plugin.ts +5 -1
package/README.md
CHANGED
|
@@ -22,6 +22,10 @@ Use the [Astro starter](https://github.com/rigel-cms/rigel-starter), or follow t
|
|
|
22
22
|
|
|
23
23
|
[Documentation](https://rigelcms.dev/docs/) · [Content model](https://rigelcms.dev/docs/content-model/) · [Previews](https://rigelcms.dev/docs/preview/) · [GitHub setup](https://rigelcms.dev/docs/github-publishing/)
|
|
24
24
|
|
|
25
|
+
## Content languages
|
|
26
|
+
|
|
27
|
+
Declare language-specific page types and article collections, then use an optional content-language selector in the editor. See [Content languages](docs/content-locales.md) for configuration. Website routing stays in your Astro templates.
|
|
28
|
+
|
|
25
29
|
## Extensions
|
|
26
30
|
|
|
27
31
|
Register your own slash commands, toolbar buttons and saved article blocks with Svelte components. The [extension guide](docs/extensions.md) covers registration, editable blocks and published rendering.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Content languages
|
|
2
|
+
|
|
3
|
+
Content languages are optional. Configure them separately from `languages`, which selects the editor's interface translations.
|
|
4
|
+
|
|
5
|
+
```js
|
|
6
|
+
rigel({
|
|
7
|
+
contentLocales: {
|
|
8
|
+
defaultLocale: "en",
|
|
9
|
+
locales: [
|
|
10
|
+
{ code: "en", label: "English" },
|
|
11
|
+
{ code: "fr", label: "Français" },
|
|
12
|
+
],
|
|
13
|
+
},
|
|
14
|
+
schema,
|
|
15
|
+
collections: [
|
|
16
|
+
{ name: "blogEn", label: "Blog", locale: "en", folder: "src/content/blog/en", fields },
|
|
17
|
+
{ name: "blogFr", label: "Articles", locale: "fr", folder: "src/content/blog/fr", fields },
|
|
18
|
+
],
|
|
19
|
+
preview: {
|
|
20
|
+
documents: {
|
|
21
|
+
pageEn: { url: "/en/{slug}/" },
|
|
22
|
+
pageFr: { url: "/fr/{slug}/" },
|
|
23
|
+
},
|
|
24
|
+
collections: {
|
|
25
|
+
blogEn: { url: "/en/blog/{slug}/", new: "/preview/blog-en/" },
|
|
26
|
+
blogFr: { url: "/fr/blog/{slug}/", new: "/preview/blog-fr/" },
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Here `schema` and `fields` are your existing document schema and article fields. Add `locale` to each document type that belongs to one content language:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
defineDocument({
|
|
36
|
+
name: "pageFr",
|
|
37
|
+
title: "Pages françaises",
|
|
38
|
+
type: "document",
|
|
39
|
+
locale: "fr",
|
|
40
|
+
folder: "src/content/pages/fr",
|
|
41
|
+
fields: pageFields,
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Each type can have different fields, allowed sections and initial content. You can also annotate an existing `defineCopy` page with `locale`. Rigel reads the annotation, not the folder name.
|
|
46
|
+
|
|
47
|
+
With multiple configured locales, the editor shows a Content language dropdown. It filters navigation, page lists, collections and search. Switching away from unsaved changes offers the existing Save, Discard and Keep editing choices. New sessions start with `defaultLocale`.
|
|
48
|
+
|
|
49
|
+
Content without a `locale` remains visible in every language. Use this for common authors, media and other shared records. A shared document type can also have a locale when its records are language-specific. The language filter is an editing convenience, not an access-control boundary: the manifest and publishing batch still contain the site's complete content.
|
|
50
|
+
|
|
51
|
+
Without `contentLocales`, the editor keeps its existing navigation and shows no content-language dropdown. A single configured locale also needs no dropdown.
|
|
52
|
+
|
|
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.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rigel-cms/rigel",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "A CMS for Astro sites. You edit the text, Git keeps the history.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"astro",
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
"src",
|
|
54
54
|
"!src/**/*.test.ts",
|
|
55
55
|
"README.md",
|
|
56
|
-
"docs/extensions.md"
|
|
56
|
+
"docs/extensions.md",
|
|
57
|
+
"docs/content-locales.md"
|
|
57
58
|
],
|
|
58
59
|
"peerDependencies": {
|
|
59
60
|
"astro": "^7.3.0",
|
package/src/config.ts
CHANGED
|
@@ -8,6 +8,12 @@ import type { JsonOutput } from "./json-output";
|
|
|
8
8
|
import { DEFAULT_MAX_FILE_SIZE } from "./media-types";
|
|
9
9
|
import { systemMessage } from "./editor/i18n/format";
|
|
10
10
|
import { localeNames, type Locale } from "./editor/i18n/locales";
|
|
11
|
+
import { validateContentLocales, validateContentLocale } from "./manifest/content-locales";
|
|
12
|
+
|
|
13
|
+
export interface ContentLocalesConfig {
|
|
14
|
+
defaultLocale: string;
|
|
15
|
+
locales: { code: string; label: string }[];
|
|
16
|
+
}
|
|
11
17
|
|
|
12
18
|
export interface RigelConfig {
|
|
13
19
|
copyDir: string;
|
|
@@ -18,6 +24,7 @@ export interface RigelConfig {
|
|
|
18
24
|
jsonOutput?: JsonOutput;
|
|
19
25
|
demo?: boolean;
|
|
20
26
|
languages?: Locale[];
|
|
27
|
+
contentLocales?: ContentLocalesConfig;
|
|
21
28
|
extensions?: string[];
|
|
22
29
|
collections?: CollectionConfig[];
|
|
23
30
|
menus?: { folder: string; maxDepth?: number };
|
|
@@ -34,6 +41,7 @@ export interface RigelConfig {
|
|
|
34
41
|
|
|
35
42
|
export interface CollectionConfig {
|
|
36
43
|
name: string;
|
|
44
|
+
locale?: string;
|
|
37
45
|
label?: string;
|
|
38
46
|
listTitle?: string;
|
|
39
47
|
folder: string;
|
|
@@ -48,6 +56,12 @@ const DEFAULTS: Pick<RigelConfig, "copyDir" | "backend"> = {
|
|
|
48
56
|
|
|
49
57
|
export function parseConfig(options: Partial<RigelConfig> = {}): RigelConfig {
|
|
50
58
|
const cfg = { ...DEFAULTS, ...options };
|
|
59
|
+
if (cfg.contentLocales !== undefined) {
|
|
60
|
+
validateContentLocales(cfg.contentLocales);
|
|
61
|
+
cfg.contentLocales = { ...cfg.contentLocales, locales: cfg.contentLocales.locales.map(locale => ({ ...locale })) };
|
|
62
|
+
}
|
|
63
|
+
for (const type of cfg.schema?.documents ?? []) validateContentLocale(type.locale, cfg.contentLocales, type.name);
|
|
64
|
+
for (const collection of cfg.collections ?? []) validateContentLocale(collection.locale, cfg.contentLocales, collection.name);
|
|
51
65
|
if (cfg.extensions === undefined) cfg.extensions = [];
|
|
52
66
|
if (!Array.isArray(cfg.extensions) || Array.from(cfg.extensions).some(module => typeof module !== "string" || !module.startsWith("./") || !module.slice(2).split("/").every(part => /^[a-zA-Z0-9_-][a-zA-Z0-9_.-]*$/.test(part) && part !== "node_modules" && part !== "." && part !== "..")) || new Set(cfg.extensions).size !== cfg.extensions.length) throw new Error("rigel: extensions must be unique explicit ./module paths inside the site project");
|
|
53
67
|
cfg.extensions = [...cfg.extensions];
|
package/src/editor/App.svelte
CHANGED
|
@@ -13,6 +13,9 @@
|
|
|
13
13
|
import ImagePicker from "./lib/ImagePicker.svelte";
|
|
14
14
|
import Settings from "./lib/Settings.svelte";
|
|
15
15
|
import TopBar from "./lib/TopBar.svelte";
|
|
16
|
+
import Select from "./lib/Select.svelte";
|
|
17
|
+
import { contentForLocale, selectionInContent } from "../manifest/content-locales";
|
|
18
|
+
import { validateDocumentSlugs } from "../slugs";
|
|
16
19
|
import Shortcuts from "./lib/Shortcuts.svelte";
|
|
17
20
|
import CollectionPanel from "./lib/CollectionPanel.svelte";
|
|
18
21
|
import { emptyBody, type ArticleBody } from "../article-body";
|
|
@@ -43,6 +46,10 @@
|
|
|
43
46
|
type Sel = Selection;
|
|
44
47
|
|
|
45
48
|
let manifest = $state<Manifest>({ pages: [] });
|
|
49
|
+
let selectedContentLocale = $state<string | undefined>(undefined);
|
|
50
|
+
let localeControlRevision = $state(0);
|
|
51
|
+
const contentLocale = $derived(selectedContentLocale ?? manifest.contentLocales?.defaultLocale);
|
|
52
|
+
const visibleContent = $derived(contentForLocale(manifest, contentLocale));
|
|
46
53
|
let ready = $state(false);
|
|
47
54
|
let blueprint: Manifest | null = null;
|
|
48
55
|
let gitContext: GitContext | null = null;
|
|
@@ -134,6 +141,7 @@
|
|
|
134
141
|
} finally { publishBusy = false; }
|
|
135
142
|
}
|
|
136
143
|
let pendingNavigation = $state<(() => void) | null>(null);
|
|
144
|
+
let pendingNavigationCancel: (() => void) | null = null;
|
|
137
145
|
let actionDialog = $state<{ title: string; description: string; confirmLabel: string; fieldLabel?: string; danger?: boolean; onconfirm: (value: string) => void | Promise<void> } | null>(null);
|
|
138
146
|
let newEntry = $state<{ collection: string; post: CollectionPost } | null>(null);
|
|
139
147
|
let newDocument = $state<{ type: string; id: string; slug: string } | null>(null);
|
|
@@ -414,7 +422,7 @@
|
|
|
414
422
|
});
|
|
415
423
|
|
|
416
424
|
function findColl(name: string): ManifestCollection | undefined {
|
|
417
|
-
return
|
|
425
|
+
return visibleContent.collections?.find((x) => x.name === name);
|
|
418
426
|
}
|
|
419
427
|
|
|
420
428
|
function collName(s: Sel | null): string | null {
|
|
@@ -469,13 +477,13 @@
|
|
|
469
477
|
});
|
|
470
478
|
});
|
|
471
479
|
|
|
472
|
-
const sidebar = $derived(buildSidebar(
|
|
480
|
+
const sidebar = $derived(buildSidebar(visibleContent, $t));
|
|
473
481
|
const pageCategory = $derived(sel?.kind === "pages" ? sel.category : undefined);
|
|
474
482
|
const schema = $derived<Schema | undefined>(manifest.schema);
|
|
475
|
-
const docTypes = $derived(schema?.documents ?? []);
|
|
483
|
+
const docTypes = $derived(visibleContent.schema?.documents ?? []);
|
|
476
484
|
|
|
477
485
|
const pageDocType = $derived(docTypes.find((type) => !type.shared && !type.singleton));
|
|
478
|
-
const pageDocuments = $derived((
|
|
486
|
+
const pageDocuments = $derived((visibleContent.documents ?? []).filter((doc) => docTypes.some(type => type.name === doc.type && !type.shared)));
|
|
479
487
|
function docTypeName(s: Sel | null): string | null {
|
|
480
488
|
return s && (s.kind === "document" || s.kind === "documents") ? s.type : null;
|
|
481
489
|
}
|
|
@@ -493,14 +501,15 @@
|
|
|
493
501
|
});
|
|
494
502
|
function documentPreview(mode: "draft" | "website") {
|
|
495
503
|
if (sel?.kind !== "document" || (mode === "draft" && newDocument?.id === sel.id)) return null;
|
|
496
|
-
|
|
504
|
+
const storedSlug = activeDocument ? effectiveSlug(activeDocument.data, activeDocument.slug) : sel.id.slice(sel.type.length + 1);
|
|
505
|
+
return previewUrl(manifest.preview ?? {}, { kind: "document", type: sel.type, slug: storedSlug, urlSlug: Object.hasOwn(draft, "slug") ? String(draft.slug) : undefined }, mode);
|
|
497
506
|
}
|
|
498
507
|
const standaloneDocument = $derived(sel?.kind === "document" && activeDocType?.shared === true);
|
|
499
508
|
const visualSelection = $derived(sel?.kind === "post" || (sel?.kind === "document" && !standaloneDocument));
|
|
500
509
|
|
|
501
510
|
const sortedPages = $derived.by(() => {
|
|
502
511
|
const rows = [
|
|
503
|
-
...
|
|
512
|
+
...visibleContent.pages.filter((p) => !p.shared && (pageCategory === undefined || p.category === pageCategory))
|
|
504
513
|
.map((p) => ({ slug: p.slug, title: p.label, category: p.category, draft: false, target: { kind: "page", slug: p.slug } as Sel })),
|
|
505
514
|
...(pageCategory === undefined ? pageDocuments
|
|
506
515
|
.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 })) : []),
|
|
@@ -511,7 +520,7 @@
|
|
|
511
520
|
function documentCell(doc: ContentDocument, key: string): string {
|
|
512
521
|
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] ?? "");
|
|
513
522
|
}
|
|
514
|
-
const typeDocuments = $derived((
|
|
523
|
+
const typeDocuments = $derived((visibleContent.documents ?? []).filter((doc) => doc.type === docTypeName(sel)).sort((a, b) => documentCell(a, sortBy).localeCompare(documentCell(b, sortBy)) * sortDir));
|
|
515
524
|
|
|
516
525
|
|
|
517
526
|
const menu = $derived.by(() => { const slug = sel?.kind === "menu" ? sel.slug : null; return manifest.menus?.entries.find((m) => m.slug === slug); });
|
|
@@ -587,22 +596,31 @@
|
|
|
587
596
|
return false;
|
|
588
597
|
}
|
|
589
598
|
|
|
590
|
-
function requestNavigation(action: () => void) {
|
|
599
|
+
function requestNavigation(action: () => void, oncancel: (() => void) | null = null) {
|
|
591
600
|
if (publishOpen || publishBusy) return;
|
|
601
|
+
pendingNavigationCancel = oncancel;
|
|
592
602
|
if (dirty || saveState === "saving") pendingNavigation = action;
|
|
593
603
|
else action();
|
|
594
604
|
}
|
|
595
605
|
|
|
606
|
+
function cancelNavigation() {
|
|
607
|
+
pendingNavigation = null;
|
|
608
|
+
pendingNavigationCancel?.();
|
|
609
|
+
pendingNavigationCancel = null;
|
|
610
|
+
}
|
|
611
|
+
|
|
596
612
|
async function saveAndLeave() {
|
|
597
613
|
if (!(await save())) return;
|
|
598
614
|
const action = pendingNavigation;
|
|
599
615
|
pendingNavigation = null;
|
|
616
|
+
pendingNavigationCancel = null;
|
|
600
617
|
action?.();
|
|
601
618
|
}
|
|
602
619
|
|
|
603
620
|
function discardAndLeave() {
|
|
604
621
|
const action = pendingNavigation;
|
|
605
622
|
pendingNavigation = null;
|
|
623
|
+
pendingNavigationCancel = null;
|
|
606
624
|
const fallback: Sel | null = newEntry ? { kind: "collection", name: newEntry.collection } : newDocument ? { kind: "pages" } : sel;
|
|
607
625
|
newEntry = null;
|
|
608
626
|
newDocument = null;
|
|
@@ -611,6 +629,7 @@
|
|
|
611
629
|
}
|
|
612
630
|
|
|
613
631
|
function select(next: Sel | null) {
|
|
632
|
+
if (manifest.contentLocales && !selectionInContent(visibleContent, next)) return;
|
|
614
633
|
if (sel && next && sameSel(sel, next)) return;
|
|
615
634
|
requestNavigation(() => openSelection(next));
|
|
616
635
|
}
|
|
@@ -655,6 +674,21 @@
|
|
|
655
674
|
|
|
656
675
|
}
|
|
657
676
|
|
|
677
|
+
function focusContentLocale() {
|
|
678
|
+
void tick().then(() => document.querySelector<HTMLButtonElement>(".content-language button[role=combobox]")?.focus({ preventScroll: true }));
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function switchContentLocale(value: string | string[]) {
|
|
682
|
+
localeControlRevision += 1;
|
|
683
|
+
if (typeof value !== "string" || value === contentLocale || !manifest.contentLocales?.locales.some(locale => locale.code === value)) return;
|
|
684
|
+
requestNavigation(() => {
|
|
685
|
+
const destination = contentForLocale(manifest, value);
|
|
686
|
+
selectedContentLocale = value;
|
|
687
|
+
if (!selectionInContent(destination, sel)) openSelection(null);
|
|
688
|
+
focusContentLocale();
|
|
689
|
+
}, focusContentLocale);
|
|
690
|
+
}
|
|
691
|
+
|
|
658
692
|
function sort(key: string) {
|
|
659
693
|
if (sortBy === key) sortDir = sortDir === 1 ? -1 : 1;
|
|
660
694
|
else {
|
|
@@ -697,10 +731,10 @@
|
|
|
697
731
|
onconfirm: (title) => {
|
|
698
732
|
const slug = slugify(title);
|
|
699
733
|
if (!slug) throw new Error($t("shell.titleRequired"));
|
|
700
|
-
|
|
701
|
-
if (taken.includes(slug)) throw new Error($t("shell.slugAlreadyUsed", { slug }));
|
|
734
|
+
if (manifest.documents?.some(doc => doc.type === type.name && doc.slug === slug)) throw new Error($t("shell.slugAlreadyUsed", { slug }));
|
|
702
735
|
const id = `${type.name}:${slug}`;
|
|
703
736
|
const data = newDocumentFromTemplate(schema, type.name, title, slug);
|
|
737
|
+
validateDocumentSlugs(schema, [...(manifest.documents ?? []), { id, type: type.name, slug, data }], manifest.preview, $t);
|
|
704
738
|
newDocument = { type: type.name, id, slug };
|
|
705
739
|
draft = JSON.parse(JSON.stringify(data));
|
|
706
740
|
docBase = null;
|
|
@@ -821,7 +855,14 @@
|
|
|
821
855
|
<img src={logo} alt="" width="32" height="32" />
|
|
822
856
|
<span>Rigel <span class="brand-suffix">CMS</span></span>
|
|
823
857
|
</a>
|
|
824
|
-
<TopBar {
|
|
858
|
+
<TopBar manifest={visibleContent} {sel} onselect={select} />
|
|
859
|
+
{#if manifest.contentLocales && manifest.contentLocales.locales.length > 1}
|
|
860
|
+
<div class="content-language" data-locale={contentLocale}>
|
|
861
|
+
{#key localeControlRevision}
|
|
862
|
+
<Select label={$t("shell.contentLanguage")} value={contentLocale} options={manifest.contentLocales.locales.map(locale => ({ value: locale.code, label: locale.label }))} disabled={publishOpen || publishBusy} onchange={switchContentLocale} />
|
|
863
|
+
{/key}
|
|
864
|
+
</div>
|
|
865
|
+
{/if}
|
|
825
866
|
|
|
826
867
|
<nav id="rigel-sidebar-navigation" class="rail-scroll" aria-label={$t("shell.content")}>
|
|
827
868
|
{#each sidebar as node}
|
|
@@ -1107,7 +1148,7 @@
|
|
|
1107
1148
|
<Shortcuts onclose={() => (shortcutsOpen = false)} />
|
|
1108
1149
|
{/if}
|
|
1109
1150
|
{#if pendingNavigation}
|
|
1110
|
-
<UnsavedChanges busy={saveState === "saving"} error={saveError} onsave={saveAndLeave} ondiscard={discardAndLeave} oncancel={
|
|
1151
|
+
<UnsavedChanges busy={saveState === "saving"} error={saveError} onsave={saveAndLeave} ondiscard={discardAndLeave} oncancel={cancelNavigation} />
|
|
1111
1152
|
{/if}
|
|
1112
1153
|
{#if actionDialog}
|
|
1113
1154
|
<ActionDialog {...actionDialog} onclose={() => { actionDialog = null; }} />
|
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
"shell.privacyPolicy": "Datenschutzerklärung",
|
|
140
140
|
"shell.welcome": "Willkommen",
|
|
141
141
|
"shell.content": "Inhalte",
|
|
142
|
+
"shell.contentLanguage": "Inhaltssprache",
|
|
142
143
|
"shell.publish": "Veröffentlichen",
|
|
143
144
|
"shell.saveBeforePublishing": "Änderungen vor dem Veröffentlichen speichern",
|
|
144
145
|
"shell.publishSavedChanges": "Gespeicherte Branch-Änderungen veröffentlichen",
|
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
"shell.privacyPolicy": "Privacy Policy",
|
|
140
140
|
"shell.welcome": "Welcome",
|
|
141
141
|
"shell.content": "Content",
|
|
142
|
+
"shell.contentLanguage": "Content language",
|
|
142
143
|
"shell.publish": "Publish",
|
|
143
144
|
"shell.saveBeforePublishing": "Save your edits before publishing",
|
|
144
145
|
"shell.publishSavedChanges": "Publish saved branch changes",
|
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
"shell.privacyPolicy": "Política de privacidad",
|
|
140
140
|
"shell.welcome": "Bienvenido",
|
|
141
141
|
"shell.content": "Contenido",
|
|
142
|
+
"shell.contentLanguage": "Idioma del contenido",
|
|
142
143
|
"shell.publish": "Publicar",
|
|
143
144
|
"shell.saveBeforePublishing": "Guardar los cambios antes de publicar",
|
|
144
145
|
"shell.publishSavedChanges": "Publicar los cambios guardados de la rama",
|
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
"shell.privacyPolicy": "Politique de confidentialité",
|
|
140
140
|
"shell.welcome": "Bienvenue",
|
|
141
141
|
"shell.content": "Contenu",
|
|
142
|
+
"shell.contentLanguage": "Langue du contenu",
|
|
142
143
|
"shell.publish": "Publier",
|
|
143
144
|
"shell.saveBeforePublishing": "Enregistrer vos modifications avant de publier",
|
|
144
145
|
"shell.publishSavedChanges": "Publier les modifications enregistrées de la branche",
|
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
"shell.privacyPolicy": "Informativa sulla privacy",
|
|
140
140
|
"shell.welcome": "Benvenuto",
|
|
141
141
|
"shell.content": "Contenuti",
|
|
142
|
+
"shell.contentLanguage": "Lingua dei contenuti",
|
|
142
143
|
"shell.publish": "Pubblica",
|
|
143
144
|
"shell.saveBeforePublishing": "Salva le modifiche prima di pubblicare",
|
|
144
145
|
"shell.publishSavedChanges": "Pubblica le modifiche salvate nel branch",
|
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
"shell.privacyPolicy": "プライバシーポリシー",
|
|
140
140
|
"shell.welcome": "ようこそ",
|
|
141
141
|
"shell.content": "コンテンツ",
|
|
142
|
+
"shell.contentLanguage": "コンテンツの言語",
|
|
142
143
|
"shell.publish": "公開",
|
|
143
144
|
"shell.saveBeforePublishing": "公開する前に変更を保存してください",
|
|
144
145
|
"shell.publishSavedChanges": "ブランチに保存済みの変更を公開",
|
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
"shell.privacyPolicy": "Privacybeleid",
|
|
140
140
|
"shell.welcome": "Welkom",
|
|
141
141
|
"shell.content": "Inhoud",
|
|
142
|
+
"shell.contentLanguage": "Inhoudstaal",
|
|
142
143
|
"shell.publish": "Publiceren",
|
|
143
144
|
"shell.saveBeforePublishing": "Sla je wijzigingen op voordat je publiceert",
|
|
144
145
|
"shell.publishSavedChanges": "Opgeslagen branchwijzigingen publiceren",
|
|
@@ -139,6 +139,7 @@
|
|
|
139
139
|
"shell.privacyPolicy": "Политика конфиденциальности",
|
|
140
140
|
"shell.welcome": "Добро пожаловать",
|
|
141
141
|
"shell.content": "Содержимое",
|
|
142
|
+
"shell.contentLanguage": "Язык содержимого",
|
|
142
143
|
"shell.publish": "Опубликовать",
|
|
143
144
|
"shell.saveBeforePublishing": "Сохраните правки перед публикацией",
|
|
144
145
|
"shell.publishSavedChanges": "Опубликовать сохранённые изменения ветки",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
position();
|
|
61
61
|
await tick();
|
|
62
62
|
position();
|
|
63
|
-
menu
|
|
63
|
+
menu?.querySelector(`#${CSS.escape(id)}-option-${active}`)?.scrollIntoView({ block: "nearest" });
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
function choose(index: number) {
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
if (match >= 0) active = match;
|
|
102
102
|
} else return;
|
|
103
103
|
await tick();
|
|
104
|
-
menu
|
|
104
|
+
menu?.querySelector(`#${CSS.escape(id)}-option-${active}`)?.scrollIntoView({ block: "nearest" });
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
$effect(() => {
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
|
|
95
95
|
function anchorSelector(path: string) {
|
|
96
96
|
const escaped = CSS.escape(path);
|
|
97
|
-
return `[data-rigel-text="${escaped}"],[data-rigel-richtext="${escaped}"],[data-rigel-image="${escaped}"],[data-rigel-alt="${escaped}"],[data-rigel-href="${escaped}"]`;
|
|
97
|
+
return `[data-rigel-tags="${escaped}"],[data-rigel-text="${escaped}"],[data-rigel-richtext="${escaped}"],[data-rigel-image="${escaped}"],[data-rigel-alt="${escaped}"],[data-rigel-href="${escaped}"]`;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
function applySelection(scroll = false) {
|
|
@@ -166,7 +166,7 @@
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
function fieldPathFrom(element: HTMLElement): { path: string; doc: string } | null {
|
|
169
|
-
for (const attr of ["data-rigel-text", "data-rigel-richtext", "data-rigel-image", "data-rigel-alt", "data-rigel-href"]) {
|
|
169
|
+
for (const attr of ["data-rigel-tags", "data-rigel-text", "data-rigel-richtext", "data-rigel-image", "data-rigel-alt", "data-rigel-href"]) {
|
|
170
170
|
const value = element.getAttribute(attr);
|
|
171
171
|
if (value) return { path: value, doc: element.getAttribute("data-rigel-doc") ?? docId };
|
|
172
172
|
}
|
|
@@ -264,7 +264,7 @@
|
|
|
264
264
|
if (!target) return;
|
|
265
265
|
const articleBody = target.closest<HTMLElement>('[data-rigel-richtext="$body"]');
|
|
266
266
|
if (inlineBodyEnabled && articleBody) { void startBodyEdit(articleBody, event); return; }
|
|
267
|
-
const element = target.closest<HTMLElement>("[data-rigel-text],[data-rigel-richtext],[data-rigel-image],[data-rigel-alt],[data-rigel-href],[data-rigel-section]");
|
|
267
|
+
const element = target.closest<HTMLElement>("[data-rigel-tags],[data-rigel-text],[data-rigel-richtext],[data-rigel-image],[data-rigel-alt],[data-rigel-href],[data-rigel-section]");
|
|
268
268
|
if (element) selectFromElement(element);
|
|
269
269
|
}
|
|
270
270
|
|
|
@@ -315,7 +315,7 @@
|
|
|
315
315
|
const target = eventElement(event.target);
|
|
316
316
|
if (target && (editingBody?.contains(target) || bodyScope?.contains(target))) return;
|
|
317
317
|
if (!target) return;
|
|
318
|
-
const element = target.closest<HTMLElement>("[data-rigel-text],[data-rigel-richtext],[data-rigel-image],[data-rigel-alt],[data-rigel-href],[data-rigel-section]");
|
|
318
|
+
const element = target.closest<HTMLElement>("[data-rigel-tags],[data-rigel-text],[data-rigel-richtext],[data-rigel-image],[data-rigel-alt],[data-rigel-href],[data-rigel-section]");
|
|
319
319
|
if (!element || element === highlighted) return;
|
|
320
320
|
if (hovered && hovered !== element) clearHover();
|
|
321
321
|
hovered = element;
|
|
@@ -325,7 +325,7 @@
|
|
|
325
325
|
function onOut(event: MouseEvent) {
|
|
326
326
|
const target = eventElement(event.target);
|
|
327
327
|
if (!target) return;
|
|
328
|
-
const element = target.closest<HTMLElement>("[data-rigel-text],[data-rigel-richtext],[data-rigel-image],[data-rigel-alt],[data-rigel-href],[data-rigel-section]");
|
|
328
|
+
const element = target.closest<HTMLElement>("[data-rigel-tags],[data-rigel-text],[data-rigel-richtext],[data-rigel-image],[data-rigel-alt],[data-rigel-href],[data-rigel-section]");
|
|
329
329
|
if (element && element === hovered) clearHover();
|
|
330
330
|
}
|
|
331
331
|
|
|
@@ -349,7 +349,7 @@
|
|
|
349
349
|
doc.addEventListener("mouseout", onOut, true);
|
|
350
350
|
doc.addEventListener("submit", (event) => event.preventDefault(), true);
|
|
351
351
|
doc.defaultView?.addEventListener("scroll", () => closeInline(), { passive: true });
|
|
352
|
-
if (!doc.querySelector(schema ? `[data-rigel-doc="${docId}"]` : "[data-rigel-text],[data-rigel-richtext],[data-rigel-image]")) throw new Error($t("richtext.liveEditingUnavailable"));
|
|
352
|
+
if (!doc.querySelector(schema ? `[data-rigel-doc="${docId}"]` : "[data-rigel-tags],[data-rigel-text],[data-rigel-richtext],[data-rigel-image]")) throw new Error($t("richtext.liveEditingUnavailable"));
|
|
353
353
|
loaded = true;
|
|
354
354
|
bodyAvailable = !schema && !!body && !!doc.querySelector('[data-rigel-richtext="$body"]');
|
|
355
355
|
patch();
|
package/src/editor/styles.css
CHANGED
|
@@ -1161,6 +1161,15 @@
|
|
|
1161
1161
|
.rigel .schema-panel { border-left: 1px solid var(--border); overflow-y: auto; min-height: 0; padding: 1rem; background: var(--panel); }
|
|
1162
1162
|
.rigel .schema-panel section { border: 0; padding: 0 0 1rem; }
|
|
1163
1163
|
.rigel .custom-select { min-width: 0; max-width: 100%; }
|
|
1164
|
+
.rigel .content-language { position: relative; margin: -0.75rem 0.75rem 1.5rem; flex-shrink: 0; }
|
|
1165
|
+
.rigel .content-language .select-label { margin-top: 0; }
|
|
1166
|
+
.rigel .content-language .select-trigger { min-height: 36px; }
|
|
1167
|
+
@media (min-width: 768px) {
|
|
1168
|
+
.rigel.sidebar-collapsed .content-language .select-label { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
|
|
1169
|
+
.rigel.sidebar-collapsed .content-language .select-trigger > * { visibility: hidden; }
|
|
1170
|
+
.rigel.sidebar-collapsed .content-language .select-trigger { gap: 0; padding-inline: 0; }
|
|
1171
|
+
.rigel.sidebar-collapsed .content-language::after { content: attr(data-locale); position: absolute; inset: 0; overflow: hidden; line-height: 36px; text-align: center; text-transform: uppercase; font-size: 0.6875rem; pointer-events: none; }
|
|
1172
|
+
}
|
|
1164
1173
|
.rigel .block-editor { position: relative; margin-top: 1rem; border: 1px solid var(--border-control); }
|
|
1165
1174
|
.rigel .block-editor [hidden] { display: none !important; }
|
|
1166
1175
|
.rigel.rigel-inline { display: block; position: fixed; top: 0; left: 0; width: 100%; min-height: 0; height: 0; padding: 0; background: transparent; pointer-events: none; z-index: 120; }
|
package/src/index.ts
CHANGED
|
@@ -115,7 +115,7 @@ export { validateMenu } from "./manifest/menu";
|
|
|
115
115
|
export type { MenuItem, MenuDocument } from "./manifest/menu";
|
|
116
116
|
export { defineCopy } from "./manifest/types";
|
|
117
117
|
export type { CopyField, CopyGroup, CopyPage, Manifest, CollectionField } from "./manifest/types";
|
|
118
|
-
export type { RigelConfig, CollectionConfig } from "./config";
|
|
118
|
+
export type { RigelConfig, CollectionConfig, ContentLocalesConfig } from "./config";
|
|
119
119
|
export type { JsonOutput } from "./json-output";
|
|
120
120
|
export type { ReferenceOption } from "./manifest/references";
|
|
121
121
|
export type { SidebarEntry, SidebarTarget, SidebarIconName } from "./manifest/navigation";
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { ContentLocalesConfig } from "../config";
|
|
2
|
+
import type { Manifest } from "./types";
|
|
3
|
+
import type { Selection } from "./navigation";
|
|
4
|
+
|
|
5
|
+
const CODE = /^[A-Za-z]{2,8}(?:-[A-Za-z0-9]{1,8})*$/;
|
|
6
|
+
|
|
7
|
+
export function validateContentLocales(value: unknown): asserts value is ContentLocalesConfig {
|
|
8
|
+
const config = value as ContentLocalesConfig | null;
|
|
9
|
+
if (!config || typeof config !== "object" || !Array.isArray(config.locales) || !config.locales.length || Array.from(config.locales).some(locale => !locale || typeof locale.code !== "string" || !CODE.test(locale.code) || typeof locale.label !== "string" || !locale.label.trim()) || new Set(config.locales.map(locale => locale.code.toLowerCase())).size !== config.locales.length || !config.locales.some(locale => locale.code === config.defaultLocale)) {
|
|
10
|
+
throw new Error("rigel: contentLocales requires unique locale codes, non-empty labels and a defaultLocale matching a configured code");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function validateContentLocale(locale: unknown, config: ContentLocalesConfig | undefined, scope: string): void {
|
|
15
|
+
if (locale === undefined) return;
|
|
16
|
+
if (typeof locale !== "string" || !CODE.test(locale)) throw new Error(`rigel: ${scope}: invalid content locale`);
|
|
17
|
+
if (config && !config.locales.some(entry => entry.code === locale)) throw new Error(`rigel: ${scope}: unknown content locale ${locale}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function contentForLocale(manifest: Manifest, locale: string | undefined): Manifest {
|
|
21
|
+
if (!manifest.contentLocales) return manifest;
|
|
22
|
+
const selected = locale ?? manifest.contentLocales.defaultLocale;
|
|
23
|
+
const visible = (entry: { locale?: string }) => entry.locale === undefined || entry.locale === selected;
|
|
24
|
+
const types = manifest.schema?.documents.filter(visible);
|
|
25
|
+
return {
|
|
26
|
+
...manifest,
|
|
27
|
+
pages: manifest.pages.filter(visible),
|
|
28
|
+
...(manifest.collections ? { collections: manifest.collections.filter(visible) } : {}),
|
|
29
|
+
...(manifest.schema ? { schema: { ...manifest.schema, documents: types! } } : {}),
|
|
30
|
+
...(manifest.documents ? { documents: manifest.documents.filter(doc => types?.some(type => type.name === doc.type)) } : {}),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function selectionInContent(manifest: Manifest, selection: Selection | null): boolean {
|
|
35
|
+
if (!selection) return true;
|
|
36
|
+
switch (selection.kind) {
|
|
37
|
+
case "page": return manifest.pages.some(page => page.slug === selection.slug);
|
|
38
|
+
case "pages": return !selection.category || manifest.pages.some(page => !page.shared && page.category === selection.category);
|
|
39
|
+
case "document":
|
|
40
|
+
case "documents": return !!manifest.schema?.documents.some(type => type.name === selection.type);
|
|
41
|
+
case "post": return !!manifest.collections?.some(collection => collection.name === selection.collection);
|
|
42
|
+
case "collection": return !!manifest.collections?.some(collection => collection.name === selection.name);
|
|
43
|
+
default: return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -54,6 +54,10 @@ export function buildSidebar(manifest: Manifest, message: Translator = systemMes
|
|
|
54
54
|
];
|
|
55
55
|
if (!manifest.sidebar) return defaults;
|
|
56
56
|
const available = new Map(defaults.filter((node): node is SidebarLink => node.kind === "link").map(node => [navigationKey(node.target), node]));
|
|
57
|
+
for (const type of types) {
|
|
58
|
+
const target: SidebarTarget = { kind: "documents", type: type.name };
|
|
59
|
+
available.set(navigationKey(target), link(target, type.title));
|
|
60
|
+
}
|
|
57
61
|
for (const page of manifest.pages) available.set(navigationKey({ kind: "page", slug: page.slug }), link({ kind: "page", slug: page.slug }, page.label));
|
|
58
62
|
for (const doc of manifest.documents ?? []) {
|
|
59
63
|
const target: SidebarTarget = { kind: "document", type: doc.type, id: doc.id };
|
package/src/manifest/types.ts
CHANGED
|
@@ -15,6 +15,7 @@ export interface CopyGroup {
|
|
|
15
15
|
|
|
16
16
|
export interface CopyPage {
|
|
17
17
|
slug: string;
|
|
18
|
+
locale?: string;
|
|
18
19
|
label: string;
|
|
19
20
|
shared?: boolean;
|
|
20
21
|
category?: string;
|
|
@@ -36,6 +37,7 @@ export interface GitHubBackendConfig {
|
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export interface Manifest {
|
|
40
|
+
contentLocales?: import("../config").ContentLocalesConfig;
|
|
39
41
|
backend?: "github";
|
|
40
42
|
github?: GitHubBackendConfig;
|
|
41
43
|
sidebar?: import("./navigation").SidebarEntry[];
|
|
@@ -66,6 +68,7 @@ export interface CollectionPost {
|
|
|
66
68
|
|
|
67
69
|
export interface ManifestCollection {
|
|
68
70
|
name: string;
|
|
71
|
+
locale?: string;
|
|
69
72
|
label: string;
|
|
70
73
|
listTitle?: string;
|
|
71
74
|
fields: CollectionField[];
|
package/src/preview.ts
CHANGED
|
@@ -100,6 +100,7 @@ function rebindTree(root: Element | DocumentFragment, update: (element: Element)
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
export function patchPreview(doc: Document, values: Record<string, unknown>, body: "" | import("./article-body").ArticleBody, options: PatchDocuments = {}) {
|
|
103
|
+
if (!doc.documentElement) return;
|
|
103
104
|
let previews = extensionPreviews.get(doc);
|
|
104
105
|
if (!previews) {
|
|
105
106
|
previews = new Map();
|
|
@@ -172,6 +173,19 @@ export function patchPreview(doc: Document, values: Record<string, unknown>, bod
|
|
|
172
173
|
const result = get(element, element.getAttribute("data-rigel-if")!);
|
|
173
174
|
if (result.found) element.hidden = !result.value;
|
|
174
175
|
}
|
|
176
|
+
for (const el of doc.querySelectorAll<HTMLElement>("[data-rigel-tags]")) {
|
|
177
|
+
if (el.tagName !== "UL" && el.tagName !== "OL") continue;
|
|
178
|
+
const result = get(el, el.dataset.rigelTags!);
|
|
179
|
+
if (!result.found) continue;
|
|
180
|
+
const source = typeof result.value === "string" ? result.value : "";
|
|
181
|
+
if (rendered.get(el) === source) continue;
|
|
182
|
+
el.replaceChildren(...source.split(",").map(tag => tag.trim()).filter(Boolean).map(tag => {
|
|
183
|
+
const item = doc.createElement("li");
|
|
184
|
+
item.textContent = tag;
|
|
185
|
+
return item;
|
|
186
|
+
}));
|
|
187
|
+
rendered.set(el, source);
|
|
188
|
+
}
|
|
175
189
|
for (const el of doc.querySelectorAll<HTMLElement>("[data-rigel-text]")) {
|
|
176
190
|
const result = get(el, el.dataset.rigelText!);
|
|
177
191
|
if (!result.found) continue;
|
package/src/schema/types.ts
CHANGED
|
@@ -44,6 +44,7 @@ const __rigelReservedSlugs = new Set(documents.filter((doc) => !documentTypes.fi
|
|
|
44
44
|
.map(
|
|
45
45
|
(c) => ` {
|
|
46
46
|
name: ${JSON.stringify(c.name)},
|
|
47
|
+
${c.locale !== undefined ? `locale: ${JSON.stringify(c.locale)},` : ""}
|
|
47
48
|
label: ${JSON.stringify(c.label ?? c.name)},
|
|
48
49
|
listTitle: ${JSON.stringify(c.listTitle)},
|
|
49
50
|
fields: ${JSON.stringify(c.fields)},
|
|
@@ -167,6 +168,7 @@ export const menus = {
|
|
|
167
168
|
jsonOutput: cfg.jsonOutput,
|
|
168
169
|
demo: cfg.demo,
|
|
169
170
|
languages: cfg.languages,
|
|
171
|
+
contentLocales: cfg.contentLocales,
|
|
170
172
|
publicDir: publicDir?.href,
|
|
171
173
|
mediaDir: cfg.mediaDir,
|
|
172
174
|
maxFileSize: cfg.maxFileSize,
|
|
@@ -205,8 +207,10 @@ export function derefDocument(value) { return deref(value, __rigelIndex); }
|
|
|
205
207
|
return `
|
|
206
208
|
import { validateBody as __rigelValidateBody } from ${JSON.stringify(fileURLToPath(new URL("../article-body.ts", import.meta.url)))};
|
|
207
209
|
import { validateCollectionValues as __rigelValidateCollectionValues } from ${JSON.stringify(fileURLToPath(new URL("../manifest/collections.ts", import.meta.url)))};
|
|
210
|
+
import { validateContentLocale as __rigelValidateContentLocale } from ${JSON.stringify(fileURLToPath(new URL("../manifest/content-locales.ts", import.meta.url)))};
|
|
208
211
|
const modules = import.meta.glob(${JSON.stringify(pattern)}, { eager: true });
|
|
209
212
|
const loaded = Object.values(modules).map((m) => m.default);
|
|
213
|
+
for (const page of loaded) __rigelValidateContentLocale(page.locale, ${JSON.stringify(cfg.contentLocales)}, page.slug);
|
|
210
214
|
${documentsSrc}
|
|
211
215
|
export const pages = loaded.filter((page) => !__rigelReservedSlugs.has(page.slug)).sort((a, b) => a.slug.localeCompare(b.slug));
|
|
212
216
|
export const collections = [
|
|
@@ -214,7 +218,7 @@ ${collectionCode}
|
|
|
214
218
|
];
|
|
215
219
|
for (const collection of collections) __rigelAssertUniqueUrlSlugs(collection.posts.map(post => ({ slug: post.slug, values: post.frontmatter })));
|
|
216
220
|
${menuCode}
|
|
217
|
-
export default { pages, collections, documents${github ? ', backend: "github", github: ' + JSON.stringify(github) : ""}${cfg.menus ? ", menus" : ""}${cfg.schema ? ", schema: " + JSON.stringify(cfg.schema) : ""}${cfg.preview ? ", preview: " + JSON.stringify(cfg.preview) : ""}${cfg.sidebar ? ", sidebar: " + JSON.stringify(cfg.sidebar) : ""} };
|
|
221
|
+
export default { pages, collections, documents${cfg.contentLocales ? ", contentLocales: " + JSON.stringify(cfg.contentLocales) : ""}${github ? ', backend: "github", github: ' + JSON.stringify(github) : ""}${cfg.menus ? ", menus" : ""}${cfg.schema ? ", schema: " + JSON.stringify(cfg.schema) : ""}${cfg.preview ? ", preview: " + JSON.stringify(cfg.preview) : ""}${cfg.sidebar ? ", sidebar: " + JSON.stringify(cfg.sidebar) : ""} };
|
|
218
222
|
`;
|
|
219
223
|
},
|
|
220
224
|
};
|