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

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.
Files changed (55) hide show
  1. package/README.md +4 -2
  2. package/docs/README.md +28 -0
  3. package/docs/authors.md +133 -0
  4. package/docs/content-locales.md +12 -3
  5. package/docs/content-model.md +131 -0
  6. package/docs/getting-started.md +139 -0
  7. package/docs/github.md +112 -0
  8. package/docs/media.md +75 -0
  9. package/docs/menus.md +95 -0
  10. package/docs/preview-hosts.md +78 -0
  11. package/docs/preview.md +100 -0
  12. package/docs/seo.md +87 -0
  13. package/docs/sidebar.md +97 -0
  14. package/package.json +2 -3
  15. package/src/article-render.ts +12 -5
  16. package/src/config.ts +5 -0
  17. package/src/editor/App.svelte +67 -21
  18. package/src/editor/i18n/de/fields.json +2 -0
  19. package/src/editor/i18n/de/shell.json +8 -2
  20. package/src/editor/i18n/en/fields.json +2 -0
  21. package/src/editor/i18n/en/shell.json +8 -2
  22. package/src/editor/i18n/es/fields.json +2 -0
  23. package/src/editor/i18n/es/shell.json +8 -2
  24. package/src/editor/i18n/fr/fields.json +2 -0
  25. package/src/editor/i18n/fr/shell.json +8 -2
  26. package/src/editor/i18n/it/fields.json +2 -0
  27. package/src/editor/i18n/it/shell.json +8 -2
  28. package/src/editor/i18n/ja/fields.json +2 -0
  29. package/src/editor/i18n/ja/shell.json +8 -2
  30. package/src/editor/i18n/nl/fields.json +2 -0
  31. package/src/editor/i18n/nl/shell.json +8 -2
  32. package/src/editor/i18n/ru/fields.json +2 -0
  33. package/src/editor/i18n/ru/shell.json +8 -2
  34. package/src/editor/i18n/zh/fields.json +2 -0
  35. package/src/editor/i18n/zh/shell.json +8 -2
  36. package/src/editor/lib/BlockEditor.svelte +4 -3
  37. package/src/editor/lib/CollectionPanel.svelte +10 -2
  38. package/src/editor/lib/MarkdownEditor.svelte +38 -5
  39. package/src/editor/lib/PropertiesTabs.svelte +8 -1
  40. package/src/editor/lib/SchemaField.svelte +16 -2
  41. package/src/editor/lib/SchemaPanel.svelte +9 -3
  42. package/src/editor/lib/Settings.svelte +37 -30
  43. package/src/editor/lib/TopBar.svelte +13 -49
  44. package/src/editor/lib/VisualEditor.svelte +64 -13
  45. package/src/editor/lib/markdown-blocks.ts +69 -0
  46. package/src/editor/lib/media-library.ts +2 -1
  47. package/src/editor/lib/settings.ts +2 -0
  48. package/src/editor/styles.css +17 -1
  49. package/src/index.ts +1 -1
  50. package/src/manifest/pages.ts +0 -4
  51. package/src/manifest/search.ts +41 -0
  52. package/src/manifest/types.ts +1 -0
  53. package/src/schema/types.ts +1 -0
  54. package/src/schema/validate.ts +1 -0
  55. package/src/vite/manifest-plugin.ts +2 -1
@@ -3,6 +3,8 @@
3
3
  import { mount, unmount, onDestroy, tick, type Snippet } from "svelte";
4
4
  import BlockEditor from "./BlockEditor.svelte";
5
5
  import MarkdownEditor from "./MarkdownEditor.svelte";
6
+ import { markdownToBody } from "./markdown-blocks";
7
+ import { articleMarkdown } from "../../article-render";
6
8
  import { tooltipRoot } from "./tooltip";
7
9
  import editorStyles from "../styles.css?inline";
8
10
  import type { ArticleBody } from "../../article-body";
@@ -27,7 +29,7 @@
27
29
  previewPath: string | null;
28
30
  websitePath?: string | null;
29
31
  body?: "" | import("../../article-body").ArticleBody;
30
- properties?: Snippet<[SelectedField | null, number, boolean, () => void, boolean]>;
32
+ properties?: Snippet<[SelectedField | null, number, boolean, () => void, boolean, string[], (path: string) => void]>;
31
33
  bodyInline?: boolean;
32
34
  onbody?: (body: ArticleBody) => void;
33
35
  creating: boolean;
@@ -65,6 +67,7 @@
65
67
  let highlighted: HTMLElement | null = null;
66
68
  let hovered: HTMLElement | null = null;
67
69
  let structureSignature = "";
70
+ let richtextPaths = $state<string[]>([]);
68
71
  let inline = $state<{ path: string; value: string; original: string; left: number; top: number; width: number; height: number; fontSize: string } | null>(null);
69
72
  let inlineInput: HTMLInputElement | undefined = $state();
70
73
  const documentList = $derived(Object.entries(documents).map(([id, data]) => ({
@@ -164,6 +167,21 @@
164
167
  applyStructure();
165
168
  }
166
169
  applySelection();
170
+ syncInlineRichText();
171
+ }
172
+
173
+ function inlineMarkdownPath(path: string) {
174
+ const def = schema && docType ? fieldForPath(schema, docType, values, path) : null;
175
+ return def?.type === "markdown" && !def.sidebarOnly;
176
+ }
177
+
178
+ function syncInlineRichText() {
179
+ const doc = docElement();
180
+ const paths = !bodyInline || !doc || !schema || !docType || mode !== "edit" ? [] : [...new Set([...doc.querySelectorAll<HTMLElement>("[data-rigel-richtext]")]
181
+ .filter(element => (element.getAttribute("data-rigel-doc") ?? docId) === docId)
182
+ .map(element => element.getAttribute("data-rigel-richtext")!)
183
+ .filter(path => path !== "$body" && inlineMarkdownPath(path)))];
184
+ if (paths.length !== richtextPaths.length || paths.some((path, index) => path !== richtextPaths[index])) richtextPaths = paths;
167
185
  }
168
186
 
169
187
  function fieldPathFrom(element: HTMLElement): { path: string; doc: string } | null {
@@ -201,6 +219,7 @@
201
219
  editingBody = null; bodyContents = [];
202
220
  patch();
203
221
  }
222
+ const regionAttributes = ["class", "style", "role", "aria-label", "aria-multiline", "aria-keyshortcuts", "contenteditable", "data-lexical-editor", "spellcheck", "tabindex", "translate"];
204
223
  async function startBodyEdit(element: HTMLElement, event?: MouseEvent) {
205
224
  if (!body) return;
206
225
  if (bodyEditor) { element.focus(); return; }
@@ -217,7 +236,7 @@
217
236
  if (editorStyle.textContent !== editorStyles) editorStyle.textContent = editorStyles;
218
237
  clearHover(); clearHighlight();
219
238
  editingBody = element; bodyContents = [...element.childNodes];
220
- bodyAttributes = ["class", "style", "role", "aria-label", "aria-multiline", "aria-keyshortcuts", "contenteditable", "data-lexical-editor", "spellcheck"].map(name => [name, element.getAttribute(name)]);
239
+ bodyAttributes = regionAttributes.map(name => [name, element.getAttribute(name)]);
221
240
  const pre = doc.createElement("pre"), code = doc.createElement("code");
222
241
  pre.append(code); pre.style.cssText = "position:absolute;visibility:hidden"; element.append(pre);
223
242
  for (const [sample, properties] of [[pre, ["background-color", "padding", "margin", "border", "border-radius"]], [code, ["color", "font-family", "font-size", "font-weight", "line-height", "letter-spacing"]]] as const) {
@@ -251,9 +270,9 @@
251
270
  }
252
271
  function startMarkdownEdit(element: HTMLElement) {
253
272
  const field = fieldPathFrom(element);
254
- if (!field || field.doc !== docId || !schema || !docType) return false;
273
+ if (!bodyInline || !field || field.doc !== docId || !schema || !docType) return false;
255
274
  const def = fieldForPath(schema, docType, values, field.path);
256
- if (def?.type !== "markdown") return false;
275
+ if (def?.type !== "markdown" || def.sidebarOnly) return false;
257
276
  stopBodyEdit();
258
277
  const doc = element.ownerDocument;
259
278
  let editorStyle = doc.querySelector<HTMLStyleElement>('style[data-rigel-editor-styles]');
@@ -262,29 +281,59 @@
262
281
  clearHover(); clearHighlight();
263
282
  editingBody = element; bodyContents = [...element.childNodes];
264
283
  const scope = doc.createElement("div");
265
- bodyAttributes = ["class", "style", "role", "aria-label", "aria-multiline", "contenteditable", "tabindex", "translate"].map(name => [name, element.getAttribute(name)]);
284
+ bodyAttributes = regionAttributes.map(name => [name, element.getAttribute(name)]);
266
285
  const syncTheme = () => { scope.dataset.theme = document.documentElement.dataset.theme; scope.dataset.contrast = document.documentElement.dataset.contrast; };
267
286
  syncTheme();
268
287
  const host = doc.createElement("div"); host.className = "rigel rigel-inline";
269
288
  scope.append(host); doc.body.append(scope); bodyScope = scope;
270
289
  removeTooltips = tooltipRoot(host).destroy;
271
- bodyEditor = mount(MarkdownEditor, { target: host, props: {
272
- editableRoot: element,
273
- label: def.labelKey ? $t(def.labelKey) : def.label,
274
- get value() { return String(anchorValue(values, field.path).value ?? ""); },
275
- oninput: (next: string) => onchange(replaceAt(values, field.path, next)),
276
- } });
290
+ const label = def.labelKey ? $t(def.labelKey) : def.label;
291
+ const source = String(anchorValue(values, field.path).value ?? "");
292
+ const blocks = markdownToBody(source);
293
+ if (blocks) {
294
+ const editor = { body: blocks, markdown: source };
295
+ bodyEditor = mount(BlockEditor, { target: host, props: {
296
+ editableRoot: element,
297
+ label,
298
+ get value() { return editor.body as ArticleBody; },
299
+ oninput: (next: ArticleBody) => {
300
+ editor.body = next;
301
+ const markdown = articleMarkdown(next);
302
+ if (markdown === editor.markdown) return;
303
+ editor.markdown = markdown;
304
+ onchange(replaceAt(values, field.path, markdown));
305
+ },
306
+ } });
307
+ } else {
308
+ bodyEditor = mount(MarkdownEditor, { target: host, props: {
309
+ editableRoot: element,
310
+ label,
311
+ get value() { return String(anchorValue(values, field.path).value ?? ""); },
312
+ oninput: (next: string) => onchange(replaceAt(values, field.path, next)),
313
+ } });
314
+ }
277
315
  bodyThemeObserver = new MutationObserver(syncTheme);
278
316
  bodyThemeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ["data-theme", "data-contrast"] });
279
317
  void tick().then(() => (element.isContentEditable ? element : host.querySelector<HTMLTextAreaElement>("textarea"))?.focus());
280
318
  return true;
281
319
  }
282
320
 
321
+ function editField(path: string) {
322
+ const doc = docElement();
323
+ if (!doc) return;
324
+ const element = [...doc.querySelectorAll<HTMLElement>(anchorSelector(path))]
325
+ .find(node => (node.getAttribute("data-rigel-doc") ?? docId) === docId && node.hasAttribute("data-rigel-richtext"));
326
+ if (!element) return;
327
+ element.scrollIntoView({ block: "center" });
328
+ if (editingBody === element) { element.focus(); return; }
329
+ startMarkdownEdit(element);
330
+ }
331
+
283
332
  function focusBody() {
284
333
  const target = docElement()?.querySelector<HTMLElement>('[data-rigel-richtext="$body"]');
285
334
  if (inlineBodyEnabled && target) { target.scrollIntoView({ block: "center" }); void startBodyEdit(target); }
286
335
  }
287
- $effect(() => { if ((!schema && !inlineBodyEnabled) || mode !== "edit") stopBodyEdit(); });
336
+ $effect(() => { if (!bodyInline || (!schema && !inlineBodyEnabled) || mode !== "edit") stopBodyEdit(); });
288
337
  onDestroy(stopBodyEdit);
289
338
 
290
339
  function onClick(event: MouseEvent) {
@@ -516,7 +565,7 @@
516
565
  <div class="properties-divider" role="separator" tabindex="0" aria-label={$t("richtext.propertiesPanelWidth")} aria-orientation="vertical" aria-valuemin="280" aria-valuemax={panelMax} aria-valuenow={panelWidth} aria-valuetext={$t("richtext.panelPixels", { width: Math.round(panelWidth) })} onpointerdown={startResize} onpointermove={moveResize} onpointerup={endResize} onpointercancel={endResize} onlostpointercapture={endResize} onkeydown={keyResize}></div>
517
566
  {/if}
518
567
  {#if panelOpen && properties}
519
- {@render properties(selected, focusToken, inlineBodyEnabled || inlineBodyLoading, focusBody, inlineBodyLoading)}
568
+ {@render properties(selected, focusToken, inlineBodyEnabled || inlineBodyLoading, focusBody, inlineBodyLoading, richtextPaths, editField)}
520
569
  {:else if panelOpen && schema && docType}
521
570
  <SchemaPanel
522
571
  {schema}
@@ -529,6 +578,8 @@
529
578
  {focusToken}
530
579
  {devMode}
531
580
  documents={documentList}
581
+ inlineFields={richtextPaths}
582
+ oneditfield={editField}
532
583
  onchange={onchange}
533
584
  {oneditdocument}
534
585
  onselect={async (next) => { selected = next; focusToken++; await tick(); if (selected?.docId === next?.docId && selected?.sectionKey === next?.sectionKey) applySelection(true); }}
@@ -0,0 +1,69 @@
1
+ import MarkdownIt from "markdown-it";
2
+ import type { Mark, Node as MarkdownNode } from "prosemirror-model";
3
+ import { parseVisualMarkdown } from "./rich-markdown";
4
+ import { articleMarkdown } from "../../article-render";
5
+ import { validateBody, type ArticleBody, type ArticleNode } from "../../article-body";
6
+
7
+ const renderer = new MarkdownIt({ html: true });
8
+
9
+ const FORMATS: Record<string, number> = { strong: 1, em: 2, code: 16 };
10
+
11
+ function textFormat(marks: readonly Mark[]): number {
12
+ return marks.reduce((bits, mark) => bits | (FORMATS[mark.type.name] ?? 0), 0);
13
+ }
14
+
15
+ function linkHref(marks: readonly Mark[]): string | null {
16
+ const link = marks.find(mark => mark.type.name === "link");
17
+ return link && typeof link.attrs.href === "string" ? link.attrs.href : null;
18
+ }
19
+
20
+ function textNode(text: string, format: number): ArticleNode {
21
+ return { type: "text", version: 1, text, format, detail: 0, mode: "normal", style: "" };
22
+ }
23
+
24
+ function inline(nodes: readonly MarkdownNode[]): ArticleNode[] {
25
+ const result: ArticleNode[] = [];
26
+ const parts = nodes.map(node => ({ node, href: node.type.name === "text" ? linkHref(node.marks) : null }));
27
+ for (let index = 0; index < parts.length; index++) {
28
+ const { node, href } = parts[index];
29
+ if (node.type.name === "hard_break") { result.push({ type: "linebreak", version: 1 }); continue; }
30
+ if (node.type.name !== "text") return [];
31
+ const children = [textNode(node.text!, textFormat(node.marks))];
32
+ while (index + 1 < parts.length && parts[index + 1].href === href && parts[index + 1].node.type.name === "text") {
33
+ index++;
34
+ children.push(textNode(parts[index].node.text!, textFormat(parts[index].node.marks)));
35
+ }
36
+ if (href) result.push({ type: "link", version: 1, url: href, children });
37
+ else result.push(...children);
38
+ }
39
+ return result;
40
+ }
41
+
42
+ function block(node: MarkdownNode): ArticleNode | null {
43
+ const children = () => [...node.content.content].map(block).filter((child): child is ArticleNode => !!child);
44
+ const inlineChildren = () => inline([...node.content.content]);
45
+ if (node.type.name === "paragraph") return { type: "paragraph", version: 1, children: inlineChildren(), direction: null, format: "", indent: 0, textFormat: 0, textStyle: "" };
46
+ if (node.type.name === "heading") return { type: "heading", version: 1, tag: `h${node.attrs.level}`, children: inlineChildren(), direction: null, format: "", indent: 0 };
47
+ if (node.type.name === "blockquote") return { type: "quote", version: 1, children: children() };
48
+ if (node.type.name === "bullet_list") return { type: "list", version: 1, listType: "bullet", tag: "ul", start: 1, children: children() };
49
+ if (node.type.name === "ordered_list") return { type: "list", version: 1, listType: "number", tag: "ol", start: Number(node.attrs.order) || 1, children: children() };
50
+ if (node.type.name === "list_item") return { type: "listitem", version: 1, value: 1, children: children() };
51
+ if (node.type.name === "code_block") return { type: "code", version: 1, language: null, children: node.textContent ? [textNode(node.textContent, 0)] : [] };
52
+ if (node.type.name === "horizontal_rule") return { type: "divider", version: 1 };
53
+ if (node.type.name === "image") return { type: "image", version: 1, src: String(node.attrs.src ?? ""), alt: String(node.attrs.alt ?? "") };
54
+ return null;
55
+ }
56
+
57
+ export function markdownToBody(source: string): ArticleBody | null {
58
+ const doc = parseVisualMarkdown(source);
59
+ if (!doc) return null;
60
+ const blocks = [...doc.content.content].map(block);
61
+ if (blocks.some(item => !item)) return null;
62
+ const body = { root: { type: "root", version: 1, children: blocks as ArticleNode[], direction: null, format: "", indent: 0 } } as ArticleBody;
63
+ try {
64
+ validateBody(body);
65
+ } catch {
66
+ return null;
67
+ }
68
+ return renderer.render(source).trimEnd() === renderer.render(articleMarkdown(body)).trimEnd() ? body : null;
69
+ }
@@ -33,7 +33,8 @@ export function modifiedMonth(value?: string): string {
33
33
 
34
34
  const CURATED = new Set(['Images', 'Audio', 'Video', 'Documents', 'Spreadsheets', 'Archives']);
35
35
  export function filterMedia(assets: MediaAsset[], search: string, type: string, month: string): MediaAsset[] {
36
- return assets.filter(asset => asset.name.toLowerCase().includes(search.trim().toLowerCase())
36
+ const query = search.trim().toLowerCase();
37
+ return assets.filter(asset => [asset.name, assetFile(asset.url), asset.title, asset.description, asset.alt].some(value => value?.toLowerCase().includes(query))
37
38
  && (!type || asset.type === type || assetCategory(asset) === type) && (!month || modifiedMonth(asset.modified) === month));
38
39
  }
39
40
  function assetCategory(asset: MediaAsset): string {
@@ -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); }
@@ -1161,6 +1171,7 @@
1161
1171
  .rigel .visual-error { margin: 1.5rem; font-size: 0.875rem; color: var(--text-2); max-width: 44ch; }
1162
1172
  .rigel .schema-panel { border-left: 1px solid var(--border); overflow-y: auto; min-height: 0; padding: 1rem; background: var(--panel); }
1163
1173
  .rigel .schema-panel section { border: 0; padding: 0 0 1rem; }
1174
+ .rigel .schema-panel :is([data-schema-path], [data-collection-field]) + :is([data-schema-path], [data-collection-field]) { margin-top: 1rem; }
1164
1175
  .rigel .custom-select { min-width: 0; max-width: 100%; }
1165
1176
  .rigel .content-language { position: relative; margin: -0.75rem 0.75rem 1.5rem; flex-shrink: 0; }
1166
1177
  .rigel .content-language .select-label { margin-top: 0; }
@@ -1183,7 +1194,12 @@
1183
1194
  .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
1195
  .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
1196
  .rigel-article-content:focus { outline: 0; }
1197
+ .rigel-markdown-content:focus { outline: 0; }
1186
1198
  .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; }
1199
+ .rigel.rigel-inline .markdown-inline.markdown-floating { position: fixed; inset: auto; width: auto; max-width: none; max-height: none; margin: 0; padding: 0; overflow: visible; background: transparent; border: 0; box-shadow: none; pointer-events: none; }
1200
+ .rigel.rigel-inline .markdown-inline.markdown-floating .body-toolbar { position: fixed; inset: auto; width: max-content; max-width: calc(100vw - 16px); margin: 0; padding: 0.25rem; gap: 0.25rem; border: 1px solid var(--border-control); box-shadow: var(--shadow-lg); z-index: 120; pointer-events: auto; }
1201
+ .rigel.rigel-inline .markdown-inline.markdown-floating .body-toolbar > .markdown-toggle { width: auto; padding: 0 0.5rem; font-size: 0.75rem; }
1202
+ .rigel.rigel-inline .markdown-inline.markdown-floating .markdown-link { position: fixed; inset: auto; z-index: 121; pointer-events: auto; }
1187
1203
  .rigel .markdown-editor [hidden] { display: none !important; }
1188
1204
  .rigel-markdown-content { white-space: pre-wrap; overflow-wrap: anywhere; }
1189
1205
  :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; }
@@ -1293,7 +1309,7 @@
1293
1309
  .rigel .add-section .custom-select { flex: 1; }
1294
1310
  @media (max-width: 767px) { .rigel .select-option { min-height: 2.75rem; } }
1295
1311
  .rigel .schema-panel:has(> .panel-tabs) { scroll-padding-top: 4rem; }
1296
- .rigel .schema-panel .block-editor > .body-toolbar:not(.image-toolbar) { position: static; }
1312
+ .rigel .schema-panel .block-editor > .body-toolbar:not(.image-toolbar), .rigel .schema-panel .markdown-editor:not(.markdown-inline) .formatting-toolbar { position: sticky; top: var(--properties-tabs-height, 0px); z-index: 3; }
1297
1313
  .rigel .schema-panel > .panel-tabs { position: sticky; top: 0; z-index: 10; padding-top: 0.5rem; margin-top: -0.5rem; background: var(--panel); }
1298
1314
  .rigel .schema-panel > .panel-tabs::before { content: ""; position: absolute; inset: -1rem -1rem 0; z-index: -1; background: var(--panel); }
1299
1315
  .rigel .panel-tabs { display: flex; gap: 0.25rem; border-bottom: 1px solid var(--border-control); }
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[];
@@ -13,6 +13,7 @@ export interface FieldDef {
13
13
  placeholder?: string;
14
14
  accept?: string;
15
15
  required?: boolean;
16
+ sidebarOnly?: boolean;
16
17
  initialValue?: unknown;
17
18
  fields?: FieldDef[];
18
19
  of?: string;
@@ -91,6 +91,7 @@ function validateFieldsShape(schema: Schema, fields: FieldDef[], scope: string)
91
91
  keys.add(field.key);
92
92
  if (typeof field.label !== "string" || !field.label.trim()) throw new SchemaError(`${scope}.${field.key}: label required`);
93
93
  if (field.accept !== undefined && (field.type !== "file" || typeof field.accept !== "string" || !field.accept.split(",").every(token => /^(?:\.[a-z0-9]+|[a-z0-9!#$&^_.+-]+\/(?:[a-z0-9!#$&^_.+-]+|\*))$/i.test(token.trim())))) throw new SchemaError(`${scope}.${field.key}: accept must contain file extensions or MIME types`);
94
+ if (field.sidebarOnly !== undefined && (field.type !== "markdown" || typeof field.sidebarOnly !== "boolean")) throw new SchemaError(`${scope}.${field.key}: sidebarOnly applies to markdown fields`);
94
95
  if (![...SCALARS, "object", "array", "reference"].includes(field.type)) throw new SchemaError(`${scope}.${field.key}: unknown field type ${field.type}`);
95
96
  if (field.type === "object") {
96
97
  if (field.of) objectDef(schema, field.of);
@@ -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
  };