@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigel-cms/rigel",
3
- "version": "1.3.0",
3
+ "version": "1.4.0-rc.1",
4
4
  "description": "A CMS for Astro sites. You edit the text, Git keeps the history.",
5
5
  "keywords": [
6
6
  "astro",
@@ -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
- const pageCategory = $derived(sel?.kind === "pages" ? sel.category : undefined);
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 && (pageCategory === undefined || p.category === pageCategory))
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 => pageCategory === undefined || doc.category === pageCategory),
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 !== undefined ? $t("shell.allNamedEntries", { name: pageCategory }) : $t("shell.allPages") : coll?.listTitle?.trim() || (coll?.label !== undefined ? $t("shell.allNamedEntries", { name: coll.label }) : $t("shell.allEntries")));
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
- onconfirm: (title) => {
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 === type.name && doc.slug === slug)) throw new Error($t("shell.slugAlreadyUsed", { slug }));
737
- const id = `${type.name}:${slug}`;
738
- const data = newDocumentFromTemplate(schema, type.name, title, slug);
739
- validateDocumentSlugs(schema, [...(manifest.documents ?? []), { id, type: type.name, slug, data }], manifest.preview, $t);
740
- newDocument = { type: type.name, id, slug };
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: type.name, id });
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 : { kind: "pages", category: page.category })}>{$t("shell.backTo", { name: page.shared ? $t("shell.welcome") : page.category ?? $t("shell.pages") })}</button></p>
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
- {@const backSel: Sel | null = activeDocType?.singleton ? null : sel.type === pageDocType?.name ? { kind: "pages" } : { kind: "documents", type: sel.type }}
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 (sel?.kind === "collection" && coll) || (sel?.kind === "pages" && pageDocType?.creatable !== false && pageDocType) || (sel?.kind === "documents" && activeDocType && activeDocType.creatable !== false && !activeDocType.singleton)}
980
- <button class="primary" onclick={() => (sel?.kind === "collection" ? newPost(coll!) : newPage(sel?.kind === "documents" ? activeDocType : pageDocType))}>{$t("shell.addNew")}</button>
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,4 +1,7 @@
1
1
  {
2
+ "shell.coreCategory": "Standard",
3
+ "shell.template": "Vorlage",
4
+ "shell.chooseTemplate": "Vorlage auswählen",
2
5
  "shell.search": "Suchen",
3
6
  "shell.searchShortcut": "⌘/Ctrl + K",
4
7
  "shell.closeDialog": "Dialog schließen",
@@ -1,4 +1,7 @@
1
1
  {
2
+ "shell.coreCategory": "Core",
3
+ "shell.template": "Template",
4
+ "shell.chooseTemplate": "Choose a template",
2
5
  "shell.search": "Search",
3
6
  "shell.searchShortcut": "⌘/Ctrl + K",
4
7
  "shell.closeDialog": "Close dialog",
@@ -1,4 +1,7 @@
1
1
  {
2
+ "shell.coreCategory": "Principales",
3
+ "shell.template": "Plantilla",
4
+ "shell.chooseTemplate": "Elige una plantilla",
2
5
  "shell.search": "Buscar",
3
6
  "shell.searchShortcut": "⌘/Ctrl + K",
4
7
  "shell.closeDialog": "Cerrar diálogo",
@@ -1,4 +1,7 @@
1
1
  {
2
+ "shell.coreCategory": "Principales",
3
+ "shell.template": "Modèle",
4
+ "shell.chooseTemplate": "Choisir un modèle",
2
5
  "shell.search": "Rechercher",
3
6
  "shell.searchShortcut": "⌘/Ctrl + K",
4
7
  "shell.closeDialog": "Fermer la boîte de dialogue",
@@ -1,4 +1,7 @@
1
1
  {
2
+ "shell.coreCategory": "Principali",
3
+ "shell.template": "Modello",
4
+ "shell.chooseTemplate": "Scegli un modello",
2
5
  "shell.search": "Cerca",
3
6
  "shell.searchShortcut": "⌘/Ctrl + K",
4
7
  "shell.closeDialog": "Chiudi finestra",
@@ -1,4 +1,7 @@
1
1
  {
2
+ "shell.coreCategory": "基本",
3
+ "shell.template": "テンプレート",
4
+ "shell.chooseTemplate": "テンプレートを選択",
2
5
  "shell.search": "検索",
3
6
  "shell.searchShortcut": "⌘/Ctrl + K",
4
7
  "shell.closeDialog": "ダイアログを閉じる",
@@ -1,4 +1,7 @@
1
1
  {
2
+ "shell.coreCategory": "Standaard",
3
+ "shell.template": "Sjabloon",
4
+ "shell.chooseTemplate": "Kies een sjabloon",
2
5
  "shell.search": "Zoeken",
3
6
  "shell.searchShortcut": "⌘/Ctrl + K",
4
7
  "shell.closeDialog": "Dialoogvenster sluiten",
@@ -1,4 +1,7 @@
1
1
  {
2
+ "shell.coreCategory": "Основные",
3
+ "shell.template": "Шаблон",
4
+ "shell.chooseTemplate": "Выберите шаблон",
2
5
  "shell.search": "Поиск",
3
6
  "shell.searchShortcut": "⌘/Ctrl + K",
4
7
  "shell.closeDialog": "Закрыть диалог",
@@ -1,4 +1,7 @@
1
1
  {
2
+ "shell.coreCategory": "基础",
3
+ "shell.template": "模板",
4
+ "shell.chooseTemplate": "选择模板",
2
5
  "shell.search": "搜索",
3
6
  "shell.searchShortcut": "⌘/Ctrl + K",
4
7
  "shell.closeDialog": "关闭对话框",
@@ -1,9 +1,13 @@
1
1
  <script lang="ts">
2
2
  import { t } from "../i18n";
3
3
  import Icon from "./Icon.svelte";
4
- let { title, description, confirmLabel, fieldLabel, danger = false, onconfirm, onclose }: {
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
- onconfirm: (value: string) => void | Promise<void>; onclose: () => void;
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 { await onconfirm(value.trim()); onclose(); }
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
- view = new EditorView(host, {
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(true);
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); element.replaceChildren(scope); bodyScope = scope;
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<HTMLElement>('[contenteditable="true"], textarea')?.focus());
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 = next === "edit";
398
+ panelOpen = false;
397
399
  if (next === "browse") {
398
400
  clearHighlight();
399
401
  clearHover();
@@ -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 pages = manifest.pages.filter(page => !page.shared);
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
+ }