@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.
- package/README.md +4 -2
- package/docs/README.md +28 -0
- package/docs/authors.md +133 -0
- package/docs/content-locales.md +12 -3
- package/docs/content-model.md +131 -0
- package/docs/getting-started.md +139 -0
- package/docs/github.md +112 -0
- package/docs/media.md +75 -0
- package/docs/menus.md +95 -0
- package/docs/preview-hosts.md +78 -0
- package/docs/preview.md +100 -0
- package/docs/seo.md +87 -0
- package/docs/sidebar.md +97 -0
- package/package.json +2 -3
- package/src/article-render.ts +12 -5
- package/src/config.ts +5 -0
- package/src/editor/App.svelte +67 -21
- package/src/editor/i18n/de/fields.json +2 -0
- package/src/editor/i18n/de/shell.json +8 -2
- package/src/editor/i18n/en/fields.json +2 -0
- package/src/editor/i18n/en/shell.json +8 -2
- package/src/editor/i18n/es/fields.json +2 -0
- package/src/editor/i18n/es/shell.json +8 -2
- package/src/editor/i18n/fr/fields.json +2 -0
- package/src/editor/i18n/fr/shell.json +8 -2
- package/src/editor/i18n/it/fields.json +2 -0
- package/src/editor/i18n/it/shell.json +8 -2
- package/src/editor/i18n/ja/fields.json +2 -0
- package/src/editor/i18n/ja/shell.json +8 -2
- package/src/editor/i18n/nl/fields.json +2 -0
- package/src/editor/i18n/nl/shell.json +8 -2
- package/src/editor/i18n/ru/fields.json +2 -0
- package/src/editor/i18n/ru/shell.json +8 -2
- package/src/editor/i18n/zh/fields.json +2 -0
- package/src/editor/i18n/zh/shell.json +8 -2
- package/src/editor/lib/BlockEditor.svelte +4 -3
- package/src/editor/lib/CollectionPanel.svelte +10 -2
- package/src/editor/lib/MarkdownEditor.svelte +38 -5
- package/src/editor/lib/PropertiesTabs.svelte +8 -1
- package/src/editor/lib/SchemaField.svelte +16 -2
- package/src/editor/lib/SchemaPanel.svelte +9 -3
- package/src/editor/lib/Settings.svelte +37 -30
- package/src/editor/lib/TopBar.svelte +13 -49
- package/src/editor/lib/VisualEditor.svelte +64 -13
- package/src/editor/lib/markdown-blocks.ts +69 -0
- package/src/editor/lib/media-library.ts +2 -1
- package/src/editor/lib/settings.ts +2 -0
- package/src/editor/styles.css +17 -1
- package/src/index.ts +1 -1
- package/src/manifest/pages.ts +0 -4
- package/src/manifest/search.ts +41 -0
- package/src/manifest/types.ts +1 -0
- package/src/schema/types.ts +1 -0
- package/src/schema/validate.ts +1 -0
- 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 =
|
|
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 =
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
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
|
package/src/editor/styles.css
CHANGED
|
@@ -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:
|
|
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";
|
package/src/manifest/pages.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/manifest/types.ts
CHANGED
|
@@ -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[];
|
package/src/schema/types.ts
CHANGED
package/src/schema/validate.ts
CHANGED
|
@@ -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
|
};
|