@rigel-cms/rigel 1.4.0-rc.1 → 1.4.0-rc.2

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.
@@ -4,6 +4,7 @@ Content languages are optional. Configure them separately from `languages`, whic
4
4
 
5
5
  ```js
6
6
  rigel({
7
+ search: { scope: "all" },
7
8
  contentLocales: {
8
9
  defaultLocale: "en",
9
10
  locales: [
@@ -44,7 +45,15 @@ defineDocument({
44
45
 
45
46
  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
 
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
+ With multiple configured locales, the editor shows a Content language dropdown. It filters navigation, page lists and collections. Switching away from unsaved changes offers the existing Save, Discard and Keep editing choices. New sessions start with `defaultLocale`.
49
+
50
+ ## Global search
51
+
52
+ Sidebar search (⌘/Ctrl + K) searches **all content languages** by default. Set `search: { scope: "currentLocale" }` in your Astro integration to default to the selected content language plus shared resources instead. Missing or invalid scope values fall back to `"all"`. This setting does not change the Pages table's language filter or its live search.
53
+
54
+ In **Settings → Content → Search content languages**, editors can choose **Website default**, **All languages**, or **Current content language**, alongside the article editing preferences. The override applies immediately and persists in `rigelcms.settings.userPref.searchScope` in this browser. Website default removes the override; Erase All Data resets it with the other browser preferences. It is independent of the editor interface language, which lives in **Settings → Interface** after Theme and Accessibility. Current content language is disabled when there are fewer than two configured content languages.
55
+
56
+ Results include legacy pages, native documents, collections, posts and menus. Shared resources appear once. Localized results show two-character language codes when multiple content languages exist, with the full configured label available on hover and to screen readers. Opening another language's result switches content language and opens that exact resource through the existing unsaved-changes guard; Keep editing leaves the language and draft untouched. Search still shows at most 12 results.
48
57
 
49
58
  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
59
 
@@ -54,7 +63,7 @@ Your Astro routes and templates still own website localization, including public
54
63
 
55
64
  ## Page categories and templates
56
65
 
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.
66
+ 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. Pages initially shows **All pages**. When categories exist, the table offers Core, All pages and each category visible in the selected content language. The live search matches title/name, slug and category substrings, ignores case and surrounding whitespace, and combines with the selected category. Its clear button restores input focus. Back preserves both the query and originating table/filter, not a filter inferred from the opened page. Content-language switches retain the query and reset unavailable categories to All pages.
58
67
 
59
68
  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
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rigel-cms/rigel",
3
- "version": "1.4.0-rc.1",
3
+ "version": "1.4.0-rc.2",
4
4
  "description": "A CMS for Astro sites. You edit the text, Git keeps the history.",
5
5
  "keywords": [
6
6
  "astro",
package/src/config.ts CHANGED
@@ -9,12 +9,15 @@ 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
11
  import { validateContentLocales, validateContentLocale } from "./manifest/content-locales";
12
+ import { effectiveSearchScope } from "./manifest/search";
12
13
 
13
14
  export interface ContentLocalesConfig {
14
15
  defaultLocale: string;
15
16
  locales: { code: string; label: string }[];
16
17
  }
17
18
 
19
+ export type SearchScope = "all" | "currentLocale";
20
+
18
21
  export interface RigelConfig {
19
22
  copyDir: string;
20
23
  mediaDir?: string;
@@ -25,6 +28,7 @@ export interface RigelConfig {
25
28
  demo?: boolean;
26
29
  languages?: Locale[];
27
30
  contentLocales?: ContentLocalesConfig;
31
+ search?: { scope: SearchScope };
28
32
  extensions?: string[];
29
33
  collections?: CollectionConfig[];
30
34
  menus?: { folder: string; maxDepth?: number };
@@ -56,6 +60,7 @@ const DEFAULTS: Pick<RigelConfig, "copyDir" | "backend"> = {
56
60
 
57
61
  export function parseConfig(options: Partial<RigelConfig> = {}): RigelConfig {
58
62
  const cfg = { ...DEFAULTS, ...options };
63
+ cfg.search = { scope: effectiveSearchScope(cfg.search, undefined) };
59
64
  if (cfg.contentLocales !== undefined) {
60
65
  validateContentLocales(cfg.contentLocales);
61
66
  cfg.contentLocales = { ...cfg.contentLocales, locales: cfg.contentLocales.locales.map(locale => ({ ...locale })) };
@@ -12,10 +12,11 @@
12
12
  import SitePreview from "./lib/SitePreview.svelte";
13
13
  import ImagePicker from "./lib/ImagePicker.svelte";
14
14
  import Settings from "./lib/Settings.svelte";
15
+ import { effectiveSearchScope } from "../manifest/search";
15
16
  import TopBar from "./lib/TopBar.svelte";
16
17
  import Select from "./lib/Select.svelte";
17
18
  import { contentForLocale, selectionInContent } from "../manifest/content-locales";
18
- import { pageCategories, pageTemplates, inPageCategory, defaultPageCategory } from "../manifest/pages";
19
+ import { pageCategories, pageTemplates, inPageCategory } from "../manifest/pages";
19
20
  import { validateDocumentSlugs } from "../slugs";
20
21
  import Shortcuts from "./lib/Shortcuts.svelte";
21
22
  import CollectionPanel from "./lib/CollectionPanel.svelte";
@@ -160,7 +161,7 @@
160
161
  if (!Array.isArray(data.pages) || (data.backend === "github" && !data.github)) throw new Error($t("shell.configurationIncomplete"));
161
162
  blueprint = data;
162
163
  manifest = data;
163
- pagesFilter = defaultPageCategory(contentForLocale(data, data.contentLocales?.defaultLocale));
164
+ pagesFilter = null;
164
165
  ready = true;
165
166
  if (data.backend === "github" && data.github) {
166
167
  const session = readSession(data.github);
@@ -480,7 +481,9 @@
480
481
  });
481
482
 
482
483
  const sidebar = $derived(buildSidebar(visibleContent, $t));
483
- let pagesFilter = $state<string | null>("");
484
+ let pagesFilter = $state<string | null>(null);
485
+ let pagesQuery = $state("");
486
+ let pagesSearch: HTMLInputElement;
484
487
  let entryList = $state<Sel | null>(null);
485
488
  const categories = $derived(pageCategories(visibleContent));
486
489
  const pageCategory = $derived(sel?.kind === "pages" && sel.category !== undefined ? sel.category : pagesFilter);
@@ -521,11 +524,12 @@
521
524
  ...visibleContent.pages.filter((p) => !p.shared && inPageCategory(p.category, pageCategory))
522
525
  .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 })),
523
526
  ...pageDocuments
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 }))
527
+ .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.data.name ?? 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 }))
525
528
  .filter(doc => inPageCategory(doc.category, pageCategory)),
526
529
  ];
527
530
  const key = sortBy === "slug" || sortBy === "category" || sortBy === "path" ? sortBy : "title";
528
- return rows.sort((a, b) => String(a[key] ?? "").localeCompare(String(b[key] ?? "")) * sortDir);
531
+ const query = pagesQuery.trim().toLocaleLowerCase();
532
+ return rows.filter(row => !query || [row.title, row.slug, row.category ?? (categories.length ? $t("shell.coreCategory") : "")].some(value => value.toLocaleLowerCase().includes(query))).sort((a, b) => String(a[key] ?? "").localeCompare(String(b[key] ?? "")) * sortDir);
529
533
  });
530
534
  function documentCell(doc: ContentDocument, key: string): string {
531
535
  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] ?? "");
@@ -694,19 +698,25 @@
694
698
  void tick().then(() => document.querySelector<HTMLButtonElement>(".content-language button[role=combobox]")?.focus({ preventScroll: true }));
695
699
  }
696
700
 
697
- function switchContentLocale(value: string | string[]) {
701
+ function switchContentLocale(value: string | string[], target?: Sel) {
698
702
  localeControlRevision += 1;
699
703
  if (typeof value !== "string" || value === contentLocale || !manifest.contentLocales?.locales.some(locale => locale.code === value)) return;
700
704
  requestNavigation(() => {
701
705
  const destination = contentForLocale(manifest, value);
702
706
  selectedContentLocale = value;
703
- if (pagesFilter && !pageCategories(destination).includes(pagesFilter)) pagesFilter = defaultPageCategory(destination);
707
+ if (pagesFilter && !pageCategories(destination).includes(pagesFilter)) pagesFilter = null;
704
708
  if (!selectionInContent(destination, entryList)) entryList = null;
705
709
  if (!selectionInContent(destination, sel)) openSelection(null);
710
+ if (target && selectionInContent(destination, target)) openSelection(target);
706
711
  focusContentLocale();
707
712
  }, focusContentLocale);
708
713
  }
709
714
 
715
+ function selectSearch(next: Sel, locale?: string) {
716
+ if (locale && locale !== contentLocale && manifest.contentLocales?.locales.some(entry => entry.code === locale)) switchContentLocale(locale, next);
717
+ else select(next);
718
+ }
719
+
710
720
  function sort(key: string) {
711
721
  if (sortBy === key) sortDir = sortDir === 1 ? -1 : 1;
712
722
  else {
@@ -877,7 +887,7 @@
877
887
  <img src={logo} alt="" width="32" height="32" />
878
888
  <span>Rigel <span class="brand-suffix">CMS</span></span>
879
889
  </a>
880
- <TopBar manifest={visibleContent} {sel} onselect={select} />
890
+ <TopBar {manifest} {sel} {contentLocale} scope={effectiveSearchScope(manifest.search, settings.userPref.searchScope)} onselect={selectSearch} />
881
891
  {#if manifest.contentLocales && manifest.contentLocales.locales.length > 1}
882
892
  <div class="content-language" data-locale={contentLocale}>
883
893
  {#key localeControlRevision}
@@ -995,10 +1005,24 @@
995
1005
  {/if}
996
1006
  <div class="titlebar">
997
1007
  <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>
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" }); } }} />
1008
+ {#if sel.kind === "pages"}
1009
+ {#if templates.length}
1010
+ <button class="primary" onclick={() => newPage(pageDocType, true)}>{$t("shell.addNew")}</button>
1011
+ {/if}
1012
+ <div class="pages-controls">
1013
+ {#if categories.length}
1014
+ <Select ariaLabel={$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" }); } }} />
1015
+ {/if}
1016
+ <div class="pages-search">
1017
+ <Icon name="magnifier" size={18} />
1018
+ <input type="search" aria-label={$t("shell.searchPages")} placeholder={$t("shell.searchPages")} bind:value={pagesQuery} bind:this={pagesSearch} />
1019
+ {#if pagesQuery}
1020
+ <button type="button" class="icon-btn" aria-label={$t("fields.clear")} onclick={() => { pagesQuery = ""; pagesSearch.focus(); }}><Icon name="x" size={16} /></button>
1021
+ {/if}
1022
+ </div>
1023
+ </div>
1000
1024
  {/if}
1001
- {#if (sel?.kind === "collection" && coll) || (sel?.kind === "pages" && templates.length) || (sel?.kind === "documents" && activeDocType && activeDocType.creatable !== false && !activeDocType.singleton)}
1025
+ {#if (sel?.kind === "collection" && coll) || (sel?.kind === "documents" && activeDocType && activeDocType.creatable !== false && !activeDocType.singleton)}
1002
1026
  <button class="primary" onclick={() => (sel?.kind === "collection" ? newPost(coll!) : newPage(sel?.kind === "documents" ? activeDocType : pageDocType, sel?.kind === "pages"))}>{$t("shell.addNew")}</button>
1003
1027
  {/if}
1004
1028
  {#if sel?.kind === "post" && post}
@@ -1025,6 +1049,7 @@
1025
1049
  {:else if sel?.kind === "media"}
1026
1050
  <ImagePicker inline />
1027
1051
  {:else if sel?.kind === "pages" || sel?.kind === "documents" || sel?.kind === "menus" || (sel?.kind === "collection" && coll)}
1052
+
1028
1053
  <div class="table-card">
1029
1054
  <table>
1030
1055
  <thead>
@@ -1061,7 +1086,7 @@
1061
1086
  {/each}
1062
1087
  </tr>
1063
1088
  {:else}
1064
- <tr><td colspan={columns.length}>{$t("shell.noEntries")}</td></tr>
1089
+ <tr><td colspan={columns.length}>{$t(sel?.kind === "pages" && pagesQuery.trim() ? "shell.noResults" : "shell.noEntries")}</td></tr>
1065
1090
  {/each}
1066
1091
  </tbody>
1067
1092
  </table>
@@ -1166,7 +1191,7 @@
1166
1191
  {/if}
1167
1192
 
1168
1193
  {#if settingsOpen}
1169
- <Settings bind:s={settings} github={githubMode ? manifest.github : undefined} user={githubAccess?.user} error={settingsError} saving={saveState === "saving"} onerase={eraseBrowserData} onclose={() => (settingsOpen = false)} />
1194
+ <Settings bind:s={settings} search={manifest.search} contentLocales={manifest.contentLocales} github={githubMode ? manifest.github : undefined} user={githubAccess?.user} error={settingsError} saving={saveState === "saving"} onerase={eraseBrowserData} onclose={() => (settingsOpen = false)} />
1170
1195
  {/if}
1171
1196
  {#if shortcutsOpen}
1172
1197
  <Shortcuts onclose={() => (shortcutsOpen = false)} />
@@ -1,5 +1,10 @@
1
1
  {
2
+ "shell.searchScope": "Inhaltssprachen durchsuchen",
3
+ "shell.searchScopeDefault": "Website-Standard ({scope})",
4
+ "shell.searchScopeAll": "Alle Sprachen",
5
+ "shell.searchScopeCurrent": "Aktuelle Inhaltssprache",
2
6
  "shell.coreCategory": "Standard",
7
+ "shell.searchPages": "Seiten suchen",
3
8
  "shell.template": "Vorlage",
4
9
  "shell.chooseTemplate": "Vorlage auswählen",
5
10
  "shell.search": "Suchen",
@@ -24,7 +29,7 @@
24
29
  "shell.searchNavigate": "navigieren",
25
30
  "shell.searchOpen": "öffnen",
26
31
  "shell.searchClose": "schließen",
27
- "shell.appearance": "Darstellung",
32
+ "shell.interface": "Benutzeroberfläche",
28
33
  "shell.editor": "Editor",
29
34
  "shell.connection": "Verbindung",
30
35
  "shell.media": "Medien",
@@ -1,5 +1,10 @@
1
1
  {
2
+ "shell.searchScope": "Search content languages",
3
+ "shell.searchScopeDefault": "Website default ({scope})",
4
+ "shell.searchScopeAll": "All languages",
5
+ "shell.searchScopeCurrent": "Current content language",
2
6
  "shell.coreCategory": "Core",
7
+ "shell.searchPages": "Search pages",
3
8
  "shell.template": "Template",
4
9
  "shell.chooseTemplate": "Choose a template",
5
10
  "shell.search": "Search",
@@ -24,7 +29,7 @@
24
29
  "shell.searchNavigate": "navigate",
25
30
  "shell.searchOpen": "open",
26
31
  "shell.searchClose": "close",
27
- "shell.appearance": "Appearance",
32
+ "shell.interface": "Interface",
28
33
  "shell.editor": "Editor",
29
34
  "shell.connection": "Connection",
30
35
  "shell.media": "Media",
@@ -1,5 +1,10 @@
1
1
  {
2
+ "shell.searchScope": "Idiomas del contenido de búsqueda",
3
+ "shell.searchScopeDefault": "Predeterminado del sitio ({scope})",
4
+ "shell.searchScopeAll": "Todos los idiomas",
5
+ "shell.searchScopeCurrent": "Idioma actual del contenido",
2
6
  "shell.coreCategory": "Principales",
7
+ "shell.searchPages": "Buscar páginas",
3
8
  "shell.template": "Plantilla",
4
9
  "shell.chooseTemplate": "Elige una plantilla",
5
10
  "shell.search": "Buscar",
@@ -24,7 +29,7 @@
24
29
  "shell.searchNavigate": "navegar",
25
30
  "shell.searchOpen": "abrir",
26
31
  "shell.searchClose": "cerrar",
27
- "shell.appearance": "Apariencia",
32
+ "shell.interface": "Interfaz",
28
33
  "shell.editor": "Editor",
29
34
  "shell.connection": "Conexión",
30
35
  "shell.media": "Archivos multimedia",
@@ -1,5 +1,10 @@
1
1
  {
2
+ "shell.searchScope": "Langues du contenu recherché",
3
+ "shell.searchScopeDefault": "Par défaut du site ({scope})",
4
+ "shell.searchScopeAll": "Toutes les langues",
5
+ "shell.searchScopeCurrent": "Langue actuelle du contenu",
2
6
  "shell.coreCategory": "Principales",
7
+ "shell.searchPages": "Rechercher des pages",
3
8
  "shell.template": "Modèle",
4
9
  "shell.chooseTemplate": "Choisir un modèle",
5
10
  "shell.search": "Rechercher",
@@ -24,7 +29,7 @@
24
29
  "shell.searchNavigate": "parcourir",
25
30
  "shell.searchOpen": "ouvrir",
26
31
  "shell.searchClose": "fermer",
27
- "shell.appearance": "Apparence",
32
+ "shell.interface": "Interface",
28
33
  "shell.editor": "Éditeur",
29
34
  "shell.connection": "Connexion",
30
35
  "shell.media": "Médias",
@@ -1,5 +1,10 @@
1
1
  {
2
+ "shell.searchScope": "Lingue dei contenuti da cercare",
3
+ "shell.searchScopeDefault": "Predefinito del sito ({scope})",
4
+ "shell.searchScopeAll": "Tutte le lingue",
5
+ "shell.searchScopeCurrent": "Lingua attuale del contenuto",
2
6
  "shell.coreCategory": "Principali",
7
+ "shell.searchPages": "Cerca pagine",
3
8
  "shell.template": "Modello",
4
9
  "shell.chooseTemplate": "Scegli un modello",
5
10
  "shell.search": "Cerca",
@@ -24,7 +29,7 @@
24
29
  "shell.searchNavigate": "naviga",
25
30
  "shell.searchOpen": "apri",
26
31
  "shell.searchClose": "chiudi",
27
- "shell.appearance": "Aspetto",
32
+ "shell.interface": "Interfaccia",
28
33
  "shell.editor": "Editor",
29
34
  "shell.connection": "Connessione",
30
35
  "shell.media": "Media",
@@ -1,5 +1,10 @@
1
1
  {
2
+ "shell.searchScope": "検索するコンテンツの言語",
3
+ "shell.searchScopeDefault": "サイトの既定値({scope})",
4
+ "shell.searchScopeAll": "すべての言語",
5
+ "shell.searchScopeCurrent": "現在のコンテンツ言語",
2
6
  "shell.coreCategory": "基本",
7
+ "shell.searchPages": "ページを検索",
3
8
  "shell.template": "テンプレート",
4
9
  "shell.chooseTemplate": "テンプレートを選択",
5
10
  "shell.search": "検索",
@@ -24,7 +29,7 @@
24
29
  "shell.searchNavigate": "移動",
25
30
  "shell.searchOpen": "開く",
26
31
  "shell.searchClose": "閉じる",
27
- "shell.appearance": "外観",
32
+ "shell.interface": "インターフェース",
28
33
  "shell.editor": "エディター",
29
34
  "shell.connection": "接続",
30
35
  "shell.media": "メディア",
@@ -1,5 +1,10 @@
1
1
  {
2
+ "shell.searchScope": "Inhoudstalen doorzoeken",
3
+ "shell.searchScopeDefault": "Websitestandaard ({scope})",
4
+ "shell.searchScopeAll": "Alle talen",
5
+ "shell.searchScopeCurrent": "Huidige inhoudstaal",
2
6
  "shell.coreCategory": "Standaard",
7
+ "shell.searchPages": "Pagina’s zoeken",
3
8
  "shell.template": "Sjabloon",
4
9
  "shell.chooseTemplate": "Kies een sjabloon",
5
10
  "shell.search": "Zoeken",
@@ -24,7 +29,7 @@
24
29
  "shell.searchNavigate": "navigeren",
25
30
  "shell.searchOpen": "openen",
26
31
  "shell.searchClose": "sluiten",
27
- "shell.appearance": "Weergave",
32
+ "shell.interface": "Interface",
28
33
  "shell.editor": "Editor",
29
34
  "shell.connection": "Verbinding",
30
35
  "shell.media": "Media",
@@ -1,5 +1,10 @@
1
1
  {
2
+ "shell.searchScope": "Языки содержимого для поиска",
3
+ "shell.searchScopeDefault": "По умолчанию на сайте ({scope})",
4
+ "shell.searchScopeAll": "Все языки",
5
+ "shell.searchScopeCurrent": "Текущий язык содержимого",
2
6
  "shell.coreCategory": "Основные",
7
+ "shell.searchPages": "Поиск страниц",
3
8
  "shell.template": "Шаблон",
4
9
  "shell.chooseTemplate": "Выберите шаблон",
5
10
  "shell.search": "Поиск",
@@ -24,7 +29,7 @@
24
29
  "shell.searchNavigate": "перемещение",
25
30
  "shell.searchOpen": "открыть",
26
31
  "shell.searchClose": "закрыть",
27
- "shell.appearance": "Оформление",
32
+ "shell.interface": "Интерфейс",
28
33
  "shell.editor": "Редактор",
29
34
  "shell.connection": "Подключение",
30
35
  "shell.media": "Медиафайлы",
@@ -1,5 +1,10 @@
1
1
  {
2
+ "shell.searchScope": "搜索内容语言",
3
+ "shell.searchScopeDefault": "网站默认({scope})",
4
+ "shell.searchScopeAll": "所有语言",
5
+ "shell.searchScopeCurrent": "当前内容语言",
2
6
  "shell.coreCategory": "基础",
7
+ "shell.searchPages": "搜索页面",
3
8
  "shell.template": "模板",
4
9
  "shell.chooseTemplate": "选择模板",
5
10
  "shell.search": "搜索",
@@ -24,7 +29,7 @@
24
29
  "shell.searchNavigate": "切换",
25
30
  "shell.searchOpen": "打开",
26
31
  "shell.searchClose": "关闭",
27
- "shell.appearance": "外观",
32
+ "shell.interface": "界面",
28
33
  "shell.editor": "编辑器",
29
34
  "shell.connection": "连接",
30
35
  "shell.media": "媒体",
@@ -5,9 +5,13 @@
5
5
  import type { Manifest } from "../../manifest/types";
6
6
  import type { GitHubAccess } from "./github-auth";
7
7
  import Icon from "./Icon.svelte";
8
+ import Select from "./Select.svelte";
9
+ import { effectiveSearchScope } from "../../manifest/search";
8
10
 
9
- let { s = $bindable(), github, user, error, saving, onerase, onclose }: {
11
+ let { s = $bindable(), search, contentLocales, github, user, error, saving, onerase, onclose }: {
10
12
  s: RigelSettings;
13
+ search?: Manifest["search"];
14
+ contentLocales?: Manifest["contentLocales"];
11
15
  github?: Manifest["github"];
12
16
  user?: GitHubAccess["user"];
13
17
  error: string;
@@ -16,8 +20,8 @@
16
20
  onclose: () => void;
17
21
  } = $props();
18
22
 
19
- type PaneId = "appearance" | "editor" | "connection" | "media" | "language" | "advanced";
20
- let active: PaneId = $state("appearance");
23
+ type PaneId = "interface" | "content" | "connection" | "media" | "advanced";
24
+ let active: PaneId = $state("interface");
21
25
 
22
26
  const providerIds = Object.keys(STOCK_PROVIDERS) as (keyof typeof STOCK_PROVIDERS)[];
23
27
  let keys = $state(getKeys());
@@ -26,20 +30,20 @@
26
30
  let eraseError = $state("");
27
31
 
28
32
  const panes: { id: PaneId; label: string }[] = $derived([
29
- { id: "appearance", label: $t("shell.appearance") },
30
- { id: "editor", label: $t("shell.editor") },
33
+ { id: "interface", label: $t("shell.interface") },
34
+ { id: "content", label: $t("shell.content") },
31
35
  { id: "connection", label: $t("shell.connection") },
32
36
  { id: "media", label: $t("shell.media") },
33
- { id: "language", label: $t("shell.language") },
37
+
34
38
  { id: "advanced", label: $t("shell.advanced") },
35
39
  ]);
36
40
 
37
41
  const PANE_ICONS = {
38
- appearance: "palette",
39
- editor: "textB",
42
+ interface: "palette",
43
+ content: "fileText",
40
44
  connection: "user",
41
45
  media: "image",
42
- language: "translate",
46
+
43
47
  advanced: "wrench",
44
48
  } as const;
45
49
 
@@ -71,6 +75,7 @@
71
75
  {#each panes as p (p.id)}
72
76
  <button
73
77
  class:active={active === p.id}
78
+ aria-current={active === p.id ? "true" : undefined}
74
79
  onclick={() => (active = p.id)}
75
80
  >
76
81
  <span class="pico"><Icon name={PANE_ICONS[p.id]} size={24} /></span>
@@ -79,9 +84,9 @@
79
84
  {/each}
80
85
  </nav>
81
86
 
82
- <div class="settings-pane">
83
- {#if active === "appearance"}
84
- <h3>{$t("shell.appearance")}</h3>
87
+ <div class="settings-pane" role="region" aria-label={panes.find(p => p.id === active)?.label}>
88
+ {#if active === "interface"}
89
+
85
90
  <fieldset>
86
91
  <legend>{$t("shell.theme")}</legend>
87
92
  <div class="theme-options">
@@ -101,17 +106,30 @@
101
106
  </label>
102
107
  <p class="hint" id="high-contrast-hint">{$t("shell.highContrastHint")}</p>
103
108
  </fieldset>
104
-
105
- {:else if active === "editor"}
106
- <h3>{$t("shell.editorExperience")}</h3>
107
109
  <fieldset>
108
- <legend>{$t("shell.articleBodyEditing")}</legend>
109
- <label class="radio"><input type="radio" bind:group={s.userPref.editorExperience} value="sidebar" /><span>{$t("shell.sidebar")}</span></label>
110
- <label class="radio"><input type="radio" bind:group={s.userPref.editorExperience} value="inline" /><span>{$t("shell.inlinePreview")}</span></label>
110
+ <legend>{$t("shell.editorInterfaceLanguage")}</legend>
111
+ {#each availableLocales as lang}
112
+ <label class="radio">
113
+ <input type="radio" bind:group={s.language} value={lang} />
114
+ <span>{localeNames[lang]}</span>
115
+ </label>
116
+ {/each}
111
117
  </fieldset>
118
+
119
+ {:else if active === "content"}
120
+
121
+ <Select label={$t("shell.articleBodyEditing")} value={s.userPref.editorExperience ?? "inline"} options={[
122
+ { value: "sidebar", label: $t("shell.sidebar") },
123
+ { value: "inline", label: $t("shell.inlinePreview") },
124
+ ]} onchange={value => s.userPref.editorExperience = value === "sidebar" ? "sidebar" : "inline"} />
112
125
  <p class="hint">{$t("shell.editorExperienceHint")}</p>
126
+ <Select label={$t("shell.searchScope")} value={s.userPref.searchScope ?? "inherit"} options={[
127
+ { value: "inherit", label: $t("shell.searchScopeDefault", { scope: $t(effectiveSearchScope(search, undefined) === "all" ? "shell.searchScopeAll" : "shell.searchScopeCurrent") }) },
128
+ { value: "all", label: $t("shell.searchScopeAll") },
129
+ { value: "currentLocale", label: $t("shell.searchScopeCurrent"), disabled: !contentLocales || contentLocales.locales.length < 2 },
130
+ ]} onchange={value => s.userPref.searchScope = value === "all" || value === "currentLocale" ? value : undefined} />
113
131
  {:else if active === "connection"}
114
- <h3>{$t("shell.connection")}</h3>
132
+
115
133
  {#if github}
116
134
  <dl class="connection-details">
117
135
  <dt>{$t("shell.githubAccount")}</dt>
@@ -165,17 +183,6 @@
165
183
 
166
184
  </section>
167
185
 
168
- {:else if active === "language"}
169
- <h3>{$t("shell.language")}</h3>
170
- <fieldset>
171
- <legend>{$t("shell.editorInterfaceLanguage")}</legend>
172
- {#each availableLocales as lang}
173
- <label class="radio">
174
- <input type="radio" bind:group={s.language} value={lang} />
175
- <span>{localeNames[lang]}</span>
176
- </label>
177
- {/each}
178
- </fieldset>
179
186
 
180
187
  {:else}
181
188
  <h3>{$t("shell.developerMode")}</h3>
@@ -4,6 +4,8 @@
4
4
  import SidebarIcon from "./SidebarIcon.svelte";
5
5
  import { tick } from "svelte";
6
6
  import type { Manifest } from "../../manifest/types";
7
+ import { searchIndex, type SearchHit } from "../../manifest/search";
8
+ import type { SearchScope } from "../../config";
7
9
 
8
10
  type Sel =
9
11
  | { kind: "media" }
@@ -20,10 +22,14 @@
20
22
  manifest,
21
23
  sel,
22
24
  onselect,
25
+ scope = "all",
26
+ contentLocale,
23
27
  }: {
24
28
  manifest: Manifest;
25
29
  sel: Sel | null;
26
- onselect: (s: Sel) => void;
30
+ onselect: (s: Sel, locale?: string) => void;
31
+ scope?: SearchScope;
32
+ contentLocale?: string;
27
33
  } = $props();
28
34
 
29
35
  let open = $state(false);
@@ -35,53 +41,11 @@
35
41
  if (open) inputEl?.focus();
36
42
  });
37
43
 
38
- interface Hit {
39
- label: string;
40
- hint: string;
41
- action: () => void;
42
- }
43
-
44
- const hits = $derived.by((): Hit[] => {
45
- const q = query.trim().toLowerCase();
46
- const all: Hit[] = [];
47
- for (const p of manifest.pages) {
48
- all.push({
49
- label: p.label,
50
- hint: $t("shell.page"),
51
- action: () => onselect({ kind: "page", slug: p.slug }),
52
- });
53
- }
54
- for (const doc of manifest.documents ?? []) {
55
- all.push({
56
- label: String(doc.data.title ?? doc.data.name ?? doc.slug),
57
- hint: $t("shell.searchItemHint", { name: doc.type, slug: doc.slug }),
58
- action: () => onselect({ kind: "document", type: doc.type, id: doc.id }),
59
- });
60
- }
61
- for (const c of manifest.collections ?? []) {
62
- all.push({
63
- label: c.label,
64
- hint: $t("shell.collection"),
65
- action: () => onselect({ kind: "collection", name: c.name }),
66
- });
67
- for (const po of c.posts) {
68
- all.push({
69
- label: String(po.frontmatter.title ?? po.slug),
70
- hint: $t("shell.searchItemHint", { name: c.label, slug: po.slug }),
71
- action: () => onselect({ kind: "post", collection: c.name, slug: po.slug }),
72
- });
73
- }
74
- }
75
- if (manifest.menus) {
76
- all.push({ label: $t("shell.menus"), hint: $t("shell.collection"), action: () => onselect({ kind: "menus" }) });
77
- for (const m of manifest.menus.entries) all.push({ label: m.title, hint: $t("shell.searchMenuHint", { slug: m.slug }), action: () => onselect({ kind: "menu", slug: m.slug }) });
78
- }
79
- if (!q) return all.slice(0, 12);
80
- return all.filter((h) => h.label.toLowerCase().includes(q) || h.hint.toLowerCase().includes(q)).slice(0, 12);
81
- });
44
+ const index = $derived(searchIndex(manifest, scope, contentLocale, $t));
45
+ const hits = $derived(index.filter(hit => [hit.label, hit.hint, hit.language ?? ""].some(value => value.toLowerCase().includes(query.trim().toLowerCase()))).slice(0, 12));
82
46
 
83
- function trigger(h: Hit) {
84
- h.action();
47
+ function trigger(h: SearchHit) {
48
+ onselect(h.selection, h.locale);
85
49
  open = false;
86
50
  query = "";
87
51
  active = -1;
@@ -145,10 +109,10 @@
145
109
  <kbd>{$t("shell.escapeKeyLowercase")}</kbd>
146
110
  </div>
147
111
  <ul id="rigel-search-results" role="listbox" aria-label={$t("shell.searchResults")}>
148
- {#each hits as h, i (h.hint + h.label)}
112
+ {#each hits as h, i (h.id)}
149
113
  <li role="presentation">
150
114
  <button id={`rigel-search-hit-${i}`} role="option" aria-selected={active === i} onfocus={() => active = i} onclick={() => trigger(h)}>
151
- <span>{h.label}</span>
115
+ <span>{h.label}{#if h.language}<span class="badge search-language" title={h.language} aria-label={h.language}>{h.locale?.slice(0, 2).toUpperCase()}</span>{/if}</span>
152
116
  <span class="hint">{h.hint}</span>
153
117
  </button>
154
118
  </li>
@@ -3,6 +3,7 @@ import type { StockKeys } from "./images";
3
3
  import { availableLocales, setLocale, type Locale } from "../i18n";
4
4
 
5
5
  export interface RigelUserPref {
6
+ searchScope?: "all" | "currentLocale";
6
7
  sidebarCollapsed?: boolean;
7
8
  editorExperience?: "sidebar" | "inline";
8
9
  previewSplit: number;
@@ -46,6 +47,7 @@ export function getSettings(): RigelSettings {
46
47
  highContrast: typeof saved?.highContrast === "boolean" ? saved.highContrast : DEFAULTS.highContrast,
47
48
  apiKeys: mergedKeys,
48
49
  userPref: {
50
+ searchScope: saved?.userPref?.searchScope === "all" || saved?.userPref?.searchScope === "currentLocale" ? saved.userPref.searchScope : undefined,
49
51
  sidebarCollapsed: saved?.userPref?.sidebarCollapsed === true,
50
52
  editorExperience: saved?.userPref?.editorExperience === "sidebar" ? "sidebar" : DEFAULTS.userPref.editorExperience,
51
53
  previewSplit: typeof saved?.userPref?.previewSplit === "number" && Number.isFinite(saved.userPref.previewSplit) && saved.userPref.previewSplit >= 0.2 && saved.userPref.previewSplit <= 0.8
@@ -408,6 +408,15 @@
408
408
  .rigel .titlebar { display: flex; align-items: center; flex-wrap: wrap; gap: 0.75rem 1.5rem; }
409
409
  .rigel .titlebar h2 { min-width: 0; overflow-wrap: anywhere; }
410
410
  .rigel .titlebar > button { flex-shrink: 0; }
411
+ .rigel .pages-controls button[role="combobox"], .rigel .pages-search input[type="search"] { padding-top: 0.375rem; padding-bottom: 0.375rem; font-size: 0.8125rem; line-height: normal; }
412
+ .rigel .pages-controls { display: flex; align-items: center; justify-content: flex-end; flex-wrap: wrap; gap: 0.75rem; margin-left: auto; min-width: 0; max-width: 100%; }
413
+ .rigel .pages-controls .custom-select { max-width: 100%; }
414
+ .rigel .pages-search { position: relative; width: 240px; max-width: 100%; color: var(--text-muted); }
415
+ .rigel .pages-search > svg { position: absolute; left: 0.75rem; top: 50%; transform: translateY(-50%); pointer-events: none; }
416
+ .rigel .pages-search input[type="search"] { padding-left: 2.25rem; padding-right: 2.75rem; }
417
+ .rigel .pages-search input::-webkit-search-cancel-button { appearance: none; }
418
+ .rigel .pages-search .icon-btn { position: absolute; right: 0; top: 0; height: 100%; width: 2.5rem; }
419
+
411
420
  .rigel .editor-actions { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 1rem; }
412
421
  .rigel .editor-actions .back { display: flex; min-width: 0; margin: 0; }
413
422
  .rigel .editor-actions .back button, .rigel .editor-actions .save-button { height: 2.25rem; padding: 0.5rem 0.75rem; border-width: 1px; border-style: solid; font-family: inherit; font-size: 0.8125rem; font-weight: 500; line-height: 1.25; }
@@ -567,6 +576,7 @@
567
576
  .rigel .palette li button:hover,
568
577
  .rigel .palette li button:focus-visible { background: var(--accent-soft); color: var(--on-accent-soft); }
569
578
  .rigel .palette .hint { color: var(--text-muted); font-size: 0.75rem; font-weight: 400; }
579
+ .rigel .palette .search-language { margin-left: 0.5rem; }
570
580
  .rigel .palette li button[aria-selected="true"] .hint,
571
581
  .rigel .palette li button:hover .hint,
572
582
  .rigel .palette li button:focus-visible .hint { color: var(--on-accent-soft); }
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, ContentLocalesConfig } from "./config";
118
+ export type { RigelConfig, CollectionConfig, ContentLocalesConfig, SearchScope } 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";
@@ -8,10 +8,6 @@ export function pageTemplates(manifest: Manifest) {
8
8
  return (manifest.schema?.documents ?? []).filter(type => !type.shared && !type.singleton && type.creatable !== false);
9
9
  }
10
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
11
  export function inPageCategory(category: string | undefined, filter: string | null): boolean {
16
12
  return filter === null || (category ?? "") === filter;
17
13
  }
@@ -0,0 +1,41 @@
1
+ import type { SearchScope } from "../config";
2
+ import type { Manifest } from "./types";
3
+ import type { Selection } from "./navigation";
4
+ import { contentForLocale } from "./content-locales";
5
+
6
+ export interface SearchHit {
7
+ id: string;
8
+ label: string;
9
+ hint: string;
10
+ locale?: string;
11
+ language?: string;
12
+ selection: Selection;
13
+ }
14
+
15
+ export function effectiveSearchScope(config: Manifest["search"], override: unknown): SearchScope {
16
+ return override === "all" || override === "currentLocale" ? override : config?.scope === "currentLocale" ? "currentLocale" : "all";
17
+ }
18
+
19
+ export function searchIndex(manifest: Manifest, scope: SearchScope, locale: string | undefined, t: (key: string, params?: Record<string, string>) => string): SearchHit[] {
20
+ const content = scope === "currentLocale" ? contentForLocale(manifest, locale) : manifest;
21
+ const hits = new Map<string, SearchHit>();
22
+ function add(selection: Selection, label: string, hint: string, code?: string) {
23
+ const id = JSON.stringify(selection);
24
+ const language = manifest.contentLocales && manifest.contentLocales.locales.length > 1 ? manifest.contentLocales.locales.find(entry => entry.code === code)?.label : undefined;
25
+ if (!hits.has(id)) hits.set(id, { id, label, hint, selection, locale: code, language });
26
+ }
27
+ for (const page of content.pages) add({ kind: "page", slug: page.slug }, page.label, t("shell.page"), page.locale);
28
+ for (const doc of content.documents ?? []) {
29
+ const type = content.schema?.documents.find(type => type.name === doc.type);
30
+ add({ kind: "document", type: doc.type, id: doc.id }, String(doc.data.title ?? doc.data.name ?? doc.slug), t("shell.searchItemHint", { name: doc.type, slug: doc.slug }), type?.locale);
31
+ }
32
+ for (const collection of content.collections ?? []) {
33
+ add({ kind: "collection", name: collection.name }, collection.label, t("shell.collection"), collection.locale);
34
+ for (const post of collection.posts) add({ kind: "post", collection: collection.name, slug: post.slug }, String(post.frontmatter.title ?? post.slug), t("shell.searchItemHint", { name: collection.label, slug: post.slug }), collection.locale);
35
+ }
36
+ if (content.menus) {
37
+ add({ kind: "menus" }, t("shell.menus"), t("shell.collection"));
38
+ for (const menu of content.menus.entries) add({ kind: "menu", slug: menu.slug }, menu.title, t("shell.searchMenuHint", { slug: menu.slug }));
39
+ }
40
+ return [...hits.values()];
41
+ }
@@ -38,6 +38,7 @@ export interface GitHubBackendConfig {
38
38
 
39
39
  export interface Manifest {
40
40
  contentLocales?: import("../config").ContentLocalesConfig;
41
+ search?: { scope: import("../config").SearchScope };
41
42
  backend?: "github";
42
43
  github?: GitHubBackendConfig;
43
44
  sidebar?: import("./navigation").SidebarEntry[];
@@ -169,6 +169,7 @@ export const menus = {
169
169
  demo: cfg.demo,
170
170
  languages: cfg.languages,
171
171
  contentLocales: cfg.contentLocales,
172
+ search: cfg.search,
172
173
  publicDir: publicDir?.href,
173
174
  mediaDir: cfg.mediaDir,
174
175
  maxFileSize: cfg.maxFileSize,
@@ -218,7 +219,7 @@ ${collectionCode}
218
219
  ];
219
220
  for (const collection of collections) __rigelAssertUniqueUrlSlugs(collection.posts.map(post => ({ slug: post.slug, values: post.frontmatter })));
220
221
  ${menuCode}
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) : ""} };
222
+ export default { pages, collections, documents, search: ${JSON.stringify(cfg.search)}${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) : ""} };
222
223
  `;
223
224
  },
224
225
  };