@rigel-cms/rigel 1.4.0-rc.2 → 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 (46) 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 +1 -1
  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/editor/App.svelte +36 -15
  17. package/src/editor/i18n/de/fields.json +2 -0
  18. package/src/editor/i18n/de/shell.json +2 -1
  19. package/src/editor/i18n/en/fields.json +2 -0
  20. package/src/editor/i18n/en/shell.json +2 -1
  21. package/src/editor/i18n/es/fields.json +2 -0
  22. package/src/editor/i18n/es/shell.json +2 -1
  23. package/src/editor/i18n/fr/fields.json +2 -0
  24. package/src/editor/i18n/fr/shell.json +2 -1
  25. package/src/editor/i18n/it/fields.json +2 -0
  26. package/src/editor/i18n/it/shell.json +2 -1
  27. package/src/editor/i18n/ja/fields.json +2 -0
  28. package/src/editor/i18n/ja/shell.json +2 -1
  29. package/src/editor/i18n/nl/fields.json +2 -0
  30. package/src/editor/i18n/nl/shell.json +2 -1
  31. package/src/editor/i18n/ru/fields.json +2 -0
  32. package/src/editor/i18n/ru/shell.json +2 -1
  33. package/src/editor/i18n/zh/fields.json +2 -0
  34. package/src/editor/i18n/zh/shell.json +2 -1
  35. package/src/editor/lib/BlockEditor.svelte +4 -3
  36. package/src/editor/lib/CollectionPanel.svelte +10 -2
  37. package/src/editor/lib/MarkdownEditor.svelte +38 -5
  38. package/src/editor/lib/PropertiesTabs.svelte +8 -1
  39. package/src/editor/lib/SchemaField.svelte +16 -2
  40. package/src/editor/lib/SchemaPanel.svelte +9 -3
  41. package/src/editor/lib/VisualEditor.svelte +64 -13
  42. package/src/editor/lib/markdown-blocks.ts +69 -0
  43. package/src/editor/lib/media-library.ts +2 -1
  44. package/src/editor/styles.css +7 -1
  45. package/src/schema/types.ts +1 -0
  46. package/src/schema/validate.ts +1 -0
@@ -23,7 +23,7 @@
23
23
  import { slugify } from "../../slugs";
24
24
 
25
25
  let {
26
- schema, docType, values, docId, selected, creating, pending, documents, focusToken, devMode, onchange, onselect, oneditdocument,
26
+ schema, docType, values, docId, selected, creating, pending, documents, focusToken, devMode, inlineFields = [], oneditfield = () => {}, onchange, onselect, oneditdocument,
27
27
  }: {
28
28
  schema: Schema;
29
29
  docType: DocumentDef;
@@ -35,6 +35,8 @@
35
35
  documents: ContentDocument[];
36
36
  focusToken: number;
37
37
  devMode: boolean;
38
+ inlineFields?: string[];
39
+ oneditfield?: (path: string) => void;
38
40
  onchange: (next: Record<string, unknown>) => void;
39
41
  onselect: (next: SelectedField | null) => void;
40
42
  oneditdocument: (id: string) => void;
@@ -176,7 +178,7 @@
176
178
  {/if}
177
179
  {#each docFields.filter(field => docType.shared || !isSeoField(field.key)) as field (field.key)}
178
180
  <div data-schema-path={field.key}>
179
- <SchemaField {schema} {documents} {devMode} {field} base={field.key} value={values[field.key]} onvalue={(next) => onchange(replaceAt(values, field.key, next))} />
181
+ <SchemaField {schema} {documents} {devMode} {inlineFields} {oneditfield} {field} base={field.key} value={values[field.key]} onvalue={(next) => onchange(replaceAt(values, field.key, next))} />
180
182
  </div>
181
183
  {/each}
182
184
  </section>
@@ -223,6 +225,8 @@
223
225
  {schema}
224
226
  {documents}
225
227
  {devMode}
228
+ {inlineFields}
229
+ {oneditfield}
226
230
  {field}
227
231
  base={`sections.${selected.sectionKey}.${field.key}`}
228
232
  value={sections.find((section) => section._key === selected?.sectionKey)?.[field.key]}
@@ -239,6 +243,8 @@
239
243
  {schema}
240
244
  {documents}
241
245
  {devMode}
246
+ {inlineFields}
247
+ {oneditfield}
242
248
  field={selectedField}
243
249
  base={selected.path}
244
250
  value={selectedValue}
@@ -255,7 +261,7 @@
255
261
  <h3>{$t("fields.seo")}</h3>
256
262
  {#each seoFields as field (field.key)}
257
263
  <div data-schema-path={field.key}>
258
- <SchemaField {schema} {documents} {devMode} {field} base={field.key} value={values[field.key]} onvalue={(next) => onchange(replaceAt(values, field.key, next))} />
264
+ <SchemaField {schema} {documents} {devMode} {inlineFields} {oneditfield} {field} base={field.key} value={values[field.key]} onvalue={(next) => onchange(replaceAt(values, field.key, next))} />
259
265
  <SeoGuidance field={field.key} value={values[field.key]} />
260
266
  </div>
261
267
  {/each}
@@ -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 {
@@ -1171,6 +1171,7 @@
1171
1171
  .rigel .visual-error { margin: 1.5rem; font-size: 0.875rem; color: var(--text-2); max-width: 44ch; }
1172
1172
  .rigel .schema-panel { border-left: 1px solid var(--border); overflow-y: auto; min-height: 0; padding: 1rem; background: var(--panel); }
1173
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; }
1174
1175
  .rigel .custom-select { min-width: 0; max-width: 100%; }
1175
1176
  .rigel .content-language { position: relative; margin: -0.75rem 0.75rem 1.5rem; flex-shrink: 0; }
1176
1177
  .rigel .content-language .select-label { margin-top: 0; }
@@ -1193,7 +1194,12 @@
1193
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; }
1194
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); }
1195
1196
  .rigel-article-content:focus { outline: 0; }
1197
+ .rigel-markdown-content:focus { outline: 0; }
1196
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; }
1197
1203
  .rigel .markdown-editor [hidden] { display: none !important; }
1198
1204
  .rigel-markdown-content { white-space: pre-wrap; overflow-wrap: anywhere; }
1199
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; }
@@ -1303,7 +1309,7 @@
1303
1309
  .rigel .add-section .custom-select { flex: 1; }
1304
1310
  @media (max-width: 767px) { .rigel .select-option { min-height: 2.75rem; } }
1305
1311
  .rigel .schema-panel:has(> .panel-tabs) { scroll-padding-top: 4rem; }
1306
- .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; }
1307
1313
  .rigel .schema-panel > .panel-tabs { position: sticky; top: 0; z-index: 10; padding-top: 0.5rem; margin-top: -0.5rem; background: var(--panel); }
1308
1314
  .rigel .schema-panel > .panel-tabs::before { content: ""; position: absolute; inset: -1rem -1rem 0; z-index: -1; background: var(--panel); }
1309
1315
  .rigel .panel-tabs { display: flex; gap: 0.25rem; border-bottom: 1px solid var(--border-control); }
@@ -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);