@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
@@ -471,7 +471,9 @@
471
471
 
472
472
  const sortedPosts = $derived.by(() => {
473
473
  if (!coll) return [];
474
- const posts = [...coll.posts];
474
+ const query = (Object.hasOwn(collectionQueries, coll.name) ? collectionQueries[coll.name] : "").trim().toLocaleLowerCase();
475
+ const fields = coll.fields.filter(field => (field.key === "description" && (field.type === "string" || field.type === "text")) || (field.key === "tags" && field.type === "tags"));
476
+ const posts = coll.posts.filter(post => !query || [post.frontmatter.title, effectiveSlug(post.frontmatter, post.slug), ...fields.flatMap(field => post.frontmatter[field.key] ?? [])].some(value => typeof value === "string" && value.toLocaleLowerCase().includes(query)));
475
477
  if (!sortBy) return posts;
476
478
  return posts.sort((a, b) => {
477
479
  const va = cellValue(a, sortBy);
@@ -483,6 +485,13 @@
483
485
  const sidebar = $derived(buildSidebar(visibleContent, $t));
484
486
  let pagesFilter = $state<string | null>(null);
485
487
  let pagesQuery = $state("");
488
+ let collectionQueries = $state<Record<string, string>>({});
489
+ const tableQuery = $derived(sel?.kind === "collection" ? Object.hasOwn(collectionQueries, sel.name) ? collectionQueries[sel.name] : "" : pagesQuery);
490
+ const searchLabel = $derived(sel?.kind === "collection" && coll ? $t("shell.searchNamedEntries", { name: coll.label }) : $t("shell.searchPages"));
491
+ function setTableQuery(value: string) {
492
+ if (sel?.kind === "collection") collectionQueries[sel.name] = value;
493
+ else pagesQuery = value;
494
+ }
486
495
  let pagesSearch: HTMLInputElement;
487
496
  let entryList = $state<Sel | null>(null);
488
497
  const categories = $derived(pageCategories(visibleContent));
@@ -643,10 +652,19 @@
643
652
  action?.();
644
653
  }
645
654
 
646
- function select(next: Sel | null) {
655
+ function select(next: Sel | null, fromSidebar = false) {
647
656
  if (manifest.contentLocales && !selectionInContent(visibleContent, next)) return;
648
- if (sel && next && sameSel(sel, next)) return;
649
- requestNavigation(() => openSelection(next));
657
+ const resetPages = fromSidebar && next?.kind === "pages";
658
+ const resetCollection = fromSidebar && next?.kind === "collection";
659
+ if (sel && next && sameSel(sel, next) && !resetPages && !resetCollection) return;
660
+ requestNavigation(() => {
661
+ if (resetPages) {
662
+ pagesFilter = next.category ?? null;
663
+ pagesQuery = "";
664
+ }
665
+ if (resetCollection) collectionQueries[next.name] = "";
666
+ openSelection(next);
667
+ });
650
668
  }
651
669
 
652
670
  function openSelection(next: Sel | null) {
@@ -915,7 +933,7 @@
915
933
  data-tooltip={node.label}
916
934
  class:active={navActive(node.target)}
917
935
  aria-current={navActive(node.target) ? "page" : undefined}
918
- onclick={() => select(node.target)}
936
+ onclick={() => select(node.target, true)}
919
937
  >
920
938
  <SidebarIcon name={node.icon ?? ({ pages: "files", page: "file-text", collection: "newspaper", documents: "squares-four", document: "file-text", menus: "list", media: "images" }[node.target.kind])} /><span class="sidebar-label">{node.label}</span>
921
939
  </button>
@@ -1009,22 +1027,24 @@
1009
1027
  {#if templates.length}
1010
1028
  <button class="primary" onclick={() => newPage(pageDocType, true)}>{$t("shell.addNew")}</button>
1011
1029
  {/if}
1030
+ {/if}
1031
+ {#if (sel?.kind === "collection" && coll) || (sel?.kind === "documents" && activeDocType && activeDocType.creatable !== false && !activeDocType.singleton)}
1032
+ <button class="primary" onclick={() => (sel?.kind === "collection" ? newPost(coll!) : newPage(activeDocType))}>{$t("shell.addNew")}</button>
1033
+ {/if}
1034
+ {#if sel.kind === "pages" || (sel.kind === "collection" && coll)}
1012
1035
  <div class="pages-controls">
1013
- {#if categories.length}
1036
+ {#if sel.kind === "pages" && categories.length}
1014
1037
  <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
1038
  {/if}
1016
1039
  <div class="pages-search">
1017
1040
  <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>
1041
+ <input type="search" aria-label={searchLabel} placeholder={searchLabel} value={tableQuery} oninput={(event) => setTableQuery(event.currentTarget.value)} bind:this={pagesSearch} />
1042
+ {#if tableQuery}
1043
+ <button type="button" class="icon-btn" aria-label={$t("fields.clear")} onclick={() => { setTableQuery(""); pagesSearch.focus(); }}><Icon name="x" size={16} /></button>
1021
1044
  {/if}
1022
1045
  </div>
1023
1046
  </div>
1024
1047
  {/if}
1025
- {#if (sel?.kind === "collection" && coll) || (sel?.kind === "documents" && activeDocType && activeDocType.creatable !== false && !activeDocType.singleton)}
1026
- <button class="primary" onclick={() => (sel?.kind === "collection" ? newPost(coll!) : newPage(sel?.kind === "documents" ? activeDocType : pageDocType, sel?.kind === "pages"))}>{$t("shell.addNew")}</button>
1027
- {/if}
1028
1048
  {#if sel?.kind === "post" && post}
1029
1049
  <button class="icon-btn" aria-label={$t("shell.deletePost")} data-tooltip={$t("shell.deletePost")} disabled={saveState === "saving"} onclick={deletePost}>
1030
1050
  <Icon name="trash" size={15} />
@@ -1086,7 +1106,7 @@
1086
1106
  {/each}
1087
1107
  </tr>
1088
1108
  {:else}
1089
- <tr><td colspan={columns.length}>{$t(sel?.kind === "pages" && pagesQuery.trim() ? "shell.noResults" : "shell.noEntries")}</td></tr>
1109
+ <tr><td colspan={columns.length}>{$t((sel?.kind === "pages" || sel?.kind === "collection") && tableQuery.trim() ? "shell.noResults" : "shell.noEntries")}</td></tr>
1090
1110
  {/each}
1091
1111
  </tbody>
1092
1112
  </table>
@@ -1106,6 +1126,7 @@
1106
1126
  documents={draftDocuments}
1107
1127
  previewPath={documentPreview("draft")}
1108
1128
  websitePath={documentPreview("website")}
1129
+ bodyInline={settings.userPref.editorExperience === "inline"}
1109
1130
  creating={newDocument?.id === sel.id}
1110
1131
  devMode={settings.devMode}
1111
1132
  reloadToken={documentReload}
@@ -1161,8 +1182,8 @@
1161
1182
  devMode={settings.devMode}
1162
1183
  onchange={(next) => { draft = next; touch(); }}
1163
1184
  >
1164
- {#snippet properties(selected, focusToken, bodyInline, oneditbody, bodyLoading)}
1165
- <CollectionPanel fields={coll.fields} values={draft} storageSlug={post.slug} documents={manifest.documents ?? []} body={bodyDraft as ArticleBody} {bodyInline} {bodyLoading} {oneditbody} {selected} {focusToken} devMode={settings.devMode} onchange={(key, value) => { draft[key] = value; touch(); }} onbody={(value) => { bodyDraft = value; touch(); }} />
1185
+ {#snippet properties(selected, focusToken, bodyInline, oneditbody, bodyLoading, inlineFields, oneditfield)}
1186
+ <CollectionPanel fields={coll.fields} values={draft} storageSlug={post.slug} documents={manifest.documents ?? []} body={bodyDraft as ArticleBody} {bodyInline} {bodyLoading} {inlineFields} {oneditbody} {oneditfield} {selected} {focusToken} devMode={settings.devMode} onchange={(key, value) => { draft[key] = value; touch(); }} onbody={(value) => { bodyDraft = value; touch(); }} />
1166
1187
  {/snippet}
1167
1188
  </VisualEditor>
1168
1189
  {/key}
@@ -50,6 +50,8 @@
50
50
  "fields.editArticleBody": "Artikeltext in der Vorschau bearbeiten",
51
51
  "fields.loadingPreview": "Vorschau wird geladen…",
52
52
  "fields.editArticleBodyHint": "Klicken Sie zum Bearbeiten auf den Artikeltext in der Vorschau. Änderungen an den Inhalten müssen gespeichert werden.",
53
+ "fields.editRichText": "In der Vorschau bearbeiten",
54
+ "fields.editRichTextHint": "Klicke in der Vorschau auf diesen Text, um ihn zu bearbeiten. Änderungen müssen gespeichert werden.",
53
55
  "fields.seoNotSet": "Nicht festgelegt",
54
56
  "fields.seoTooShort": "Zu kurz",
55
57
  "fields.seoTooLong": "Zu lang",
@@ -5,6 +5,7 @@
5
5
  "shell.searchScopeCurrent": "Aktuelle Inhaltssprache",
6
6
  "shell.coreCategory": "Standard",
7
7
  "shell.searchPages": "Seiten suchen",
8
+ "shell.searchNamedEntries": "In {name} suchen",
8
9
  "shell.template": "Vorlage",
9
10
  "shell.chooseTemplate": "Vorlage auswählen",
10
11
  "shell.search": "Suchen",
@@ -49,7 +50,7 @@
49
50
  "shell.articleBodyEditing": "Artikeltext bearbeiten",
50
51
  "shell.sidebar": "Seitenleiste",
51
52
  "shell.inlinePreview": "Inline-Vorschau",
52
- "shell.editorExperienceHint": "Mit der Inline-Vorschau kannst du auf einen Artikeltext klicken und direkt auf der Seite schreiben. Andere Felder bleiben in der Seitenleiste. Diese Einstellung wird in diesem Browser gespeichert; Inhalte müssen weiterhin mit Speichern gesichert werden.",
53
+ "shell.editorExperienceHint": "Mit der Inline-Vorschau kannst du auf Rich-Text-Bereiche klicken und direkt auf der Seite schreiben. Felder ohne Vorschau-Bindung bleiben in der Seitenleiste. Diese Einstellung wird in diesem Browser gespeichert; Inhalte müssen weiterhin mit Speichern gesichert werden.",
53
54
  "shell.githubAccount": "GitHub-Konto",
54
55
  "shell.noGithubAccount": "Kein GitHub-Konto verbunden.",
55
56
  "shell.repository": "Repository",
@@ -50,6 +50,8 @@
50
50
  "fields.editArticleBody": "Edit article body in preview",
51
51
  "fields.loadingPreview": "Loading preview…",
52
52
  "fields.editArticleBodyHint": "Click the article body in the preview to edit. Content changes require Save.",
53
+ "fields.editRichText": "Edit in preview",
54
+ "fields.editRichTextHint": "Click this text in the preview to edit. Content changes require Save.",
53
55
  "fields.seoNotSet": "Not set",
54
56
  "fields.seoTooShort": "Too short",
55
57
  "fields.seoTooLong": "Too long",
@@ -5,6 +5,7 @@
5
5
  "shell.searchScopeCurrent": "Current content language",
6
6
  "shell.coreCategory": "Core",
7
7
  "shell.searchPages": "Search pages",
8
+ "shell.searchNamedEntries": "Search {name}",
8
9
  "shell.template": "Template",
9
10
  "shell.chooseTemplate": "Choose a template",
10
11
  "shell.search": "Search",
@@ -49,7 +50,7 @@
49
50
  "shell.articleBodyEditing": "Article body editing",
50
51
  "shell.sidebar": "Sidebar",
51
52
  "shell.inlinePreview": "Inline preview",
52
- "shell.editorExperienceHint": "Inline preview lets you click an article body and write directly on the page. Other fields stay in the sidebar. This preference saves in this browser; content still requires Save.",
53
+ "shell.editorExperienceHint": "Inline preview lets you click bound rich text and write directly on the page. Fields without a preview binding stay in the sidebar. This preference saves in this browser; content still requires Save.",
53
54
  "shell.githubAccount": "GitHub account",
54
55
  "shell.noGithubAccount": "No GitHub account connected.",
55
56
  "shell.repository": "Repository",
@@ -50,6 +50,8 @@
50
50
  "fields.editArticleBody": "Editar el cuerpo del artículo en la vista previa",
51
51
  "fields.loadingPreview": "Cargando vista previa…",
52
52
  "fields.editArticleBodyHint": "Haz clic en el cuerpo del artículo en la vista previa para editarlo. Los cambios en el contenido requieren pulsar Guardar.",
53
+ "fields.editRichText": "Editar en la vista previa",
54
+ "fields.editRichTextHint": "Haz clic en este texto de la vista previa para editarlo. Los cambios requieren pulsar Guardar.",
53
55
  "fields.seoNotSet": "Sin definir",
54
56
  "fields.seoTooShort": "Demasiado corto",
55
57
  "fields.seoTooLong": "Demasiado largo",
@@ -5,6 +5,7 @@
5
5
  "shell.searchScopeCurrent": "Idioma actual del contenido",
6
6
  "shell.coreCategory": "Principales",
7
7
  "shell.searchPages": "Buscar páginas",
8
+ "shell.searchNamedEntries": "Buscar en {name}",
8
9
  "shell.template": "Plantilla",
9
10
  "shell.chooseTemplate": "Elige una plantilla",
10
11
  "shell.search": "Buscar",
@@ -49,7 +50,7 @@
49
50
  "shell.articleBodyEditing": "Edición del cuerpo del artículo",
50
51
  "shell.sidebar": "Barra lateral",
51
52
  "shell.inlinePreview": "Vista previa en línea",
52
- "shell.editorExperienceHint": "La vista previa en línea permite hacer clic en el cuerpo de un artículo y escribir directamente en la página. Los demás campos permanecen en la barra lateral. Esta preferencia se guarda en este navegador; para guardar el contenido sigue siendo necesario pulsar Guardar.",
53
+ "shell.editorExperienceHint": "La vista previa en línea permite hacer clic en los textos enriquecidos vinculados y escribir directamente en la página. Los campos sin vínculo de vista previa permanecen en la barra lateral. Esta preferencia se guarda en este navegador; para guardar el contenido sigue siendo necesario pulsar Guardar.",
53
54
  "shell.githubAccount": "Cuenta de GitHub",
54
55
  "shell.noGithubAccount": "No hay ninguna cuenta de GitHub conectada.",
55
56
  "shell.repository": "Repositorio",
@@ -50,6 +50,8 @@
50
50
  "fields.editArticleBody": "Modifier le corps de l’article dans l’aperçu",
51
51
  "fields.loadingPreview": "Chargement de l’aperçu…",
52
52
  "fields.editArticleBodyHint": "Cliquez sur le corps de l’article dans l’aperçu pour le modifier. Enregistrez pour conserver les modifications du contenu.",
53
+ "fields.editRichText": "Modifier dans l’aperçu",
54
+ "fields.editRichTextHint": "Cliquez sur ce texte dans l’aperçu pour le modifier. Enregistrez pour conserver les modifications du contenu.",
53
55
  "fields.seoNotSet": "Non défini",
54
56
  "fields.seoTooShort": "Trop court",
55
57
  "fields.seoTooLong": "Trop long",
@@ -5,6 +5,7 @@
5
5
  "shell.searchScopeCurrent": "Langue actuelle du contenu",
6
6
  "shell.coreCategory": "Principales",
7
7
  "shell.searchPages": "Rechercher des pages",
8
+ "shell.searchNamedEntries": "Rechercher dans {name}",
8
9
  "shell.template": "Modèle",
9
10
  "shell.chooseTemplate": "Choisir un modèle",
10
11
  "shell.search": "Rechercher",
@@ -49,7 +50,7 @@
49
50
  "shell.articleBodyEditing": "Édition du corps des articles",
50
51
  "shell.sidebar": "Barre latérale",
51
52
  "shell.inlinePreview": "Aperçu avec édition intégrée",
52
- "shell.editorExperienceHint": "L’aperçu avec édition intégrée permet de cliquer sur le corps d’un article pour écrire directement sur la page. Les autres champs restent dans la barre latérale. Cette préférence est enregistrée dans ce navigateur ; le contenu nécessite toujours un clic sur Enregistrer.",
53
+ "shell.editorExperienceHint": "L’aperçu avec édition intégrée permet de cliquer sur les textes enrichis liés pour écrire directement sur la page. Les champs sans liaison d’aperçu restent dans la barre latérale. Cette préférence est enregistrée dans ce navigateur ; le contenu nécessite toujours un clic sur Enregistrer.",
53
54
  "shell.githubAccount": "Compte GitHub",
54
55
  "shell.noGithubAccount": "Aucun compte GitHub connecté.",
55
56
  "shell.repository": "Dépôt",
@@ -50,6 +50,8 @@
50
50
  "fields.editArticleBody": "Modifica il corpo dell’articolo nell’anteprima",
51
51
  "fields.loadingPreview": "Caricamento dell’anteprima…",
52
52
  "fields.editArticleBodyHint": "Fai clic sul corpo dell’articolo nell’anteprima per modificarlo. Premi Salva per salvare le modifiche ai contenuti.",
53
+ "fields.editRichText": "Modifica nell’anteprima",
54
+ "fields.editRichTextHint": "Fai clic su questo testo nell’anteprima per modificarlo. Premi Salva per salvare le modifiche ai contenuti.",
53
55
  "fields.seoNotSet": "Non impostato",
54
56
  "fields.seoTooShort": "Troppo breve",
55
57
  "fields.seoTooLong": "Troppo lungo",
@@ -5,6 +5,7 @@
5
5
  "shell.searchScopeCurrent": "Lingua attuale del contenuto",
6
6
  "shell.coreCategory": "Principali",
7
7
  "shell.searchPages": "Cerca pagine",
8
+ "shell.searchNamedEntries": "Cerca in {name}",
8
9
  "shell.template": "Modello",
9
10
  "shell.chooseTemplate": "Scegli un modello",
10
11
  "shell.search": "Cerca",
@@ -49,7 +50,7 @@
49
50
  "shell.articleBodyEditing": "Modifica del corpo dell’articolo",
50
51
  "shell.sidebar": "Barra laterale",
51
52
  "shell.inlinePreview": "Anteprima con modifica diretta",
52
- "shell.editorExperienceHint": "L’anteprima con modifica diretta permette di fare clic sul corpo di un articolo e scrivere direttamente nella pagina. Gli altri campi restano nella barra laterale. Questa preferenza viene salvata in questo browser; per salvare i contenuti devi comunque premere Salva.",
53
+ "shell.editorExperienceHint": "L’anteprima con modifica diretta permette di fare clic sui testi formattati collegati e scrivere direttamente nella pagina. I campi senza collegamento all’anteprima restano nella barra laterale. Questa preferenza viene salvata in questo browser; per salvare i contenuti devi comunque premere Salva.",
53
54
  "shell.githubAccount": "Account GitHub",
54
55
  "shell.noGithubAccount": "Nessun account GitHub collegato.",
55
56
  "shell.repository": "Repository",
@@ -50,6 +50,8 @@
50
50
  "fields.editArticleBody": "プレビューで記事本文を編集",
51
51
  "fields.loadingPreview": "プレビューを読み込み中…",
52
52
  "fields.editArticleBodyHint": "プレビューの記事本文をクリックすると編集できます。コンテンツの変更には保存が必要です。",
53
+ "fields.editRichText": "プレビューで編集",
54
+ "fields.editRichTextHint": "プレビューのこのテキストをクリックすると編集できます。変更には保存が必要です。",
53
55
  "fields.seoNotSet": "未設定",
54
56
  "fields.seoTooShort": "短すぎます",
55
57
  "fields.seoTooLong": "長すぎます",
@@ -5,6 +5,7 @@
5
5
  "shell.searchScopeCurrent": "現在のコンテンツ言語",
6
6
  "shell.coreCategory": "基本",
7
7
  "shell.searchPages": "ページを検索",
8
+ "shell.searchNamedEntries": "{name}内を検索",
8
9
  "shell.template": "テンプレート",
9
10
  "shell.chooseTemplate": "テンプレートを選択",
10
11
  "shell.search": "検索",
@@ -49,7 +50,7 @@
49
50
  "shell.articleBodyEditing": "記事本文の編集",
50
51
  "shell.sidebar": "サイドバー",
51
52
  "shell.inlinePreview": "インラインプレビュー",
52
- "shell.editorExperienceHint": "インラインプレビューでは、記事本文をクリックしてページ上で直接編集できます。他のフィールドはサイドバーに表示されます。この設定はこのブラウザーに保存されますが、コンテンツには引き続き「保存」が必要です。",
53
+ "shell.editorExperienceHint": "インラインプレビューでは、プレビューに紐づいたリッチテキストをクリックしてページ上で直接編集できます。プレビューに紐づかないフィールドはサイドバーに表示されます。この設定はこのブラウザーに保存されますが、コンテンツには引き続き「保存」が必要です。",
53
54
  "shell.githubAccount": "GitHubアカウント",
54
55
  "shell.noGithubAccount": "GitHubアカウントが接続されていません。",
55
56
  "shell.repository": "リポジトリ",
@@ -50,6 +50,8 @@
50
50
  "fields.editArticleBody": "Artikeltekst in het voorbeeld bewerken",
51
51
  "fields.loadingPreview": "Voorbeeld laden…",
52
52
  "fields.editArticleBodyHint": "Klik op de artikeltekst in het voorbeeld om deze te bewerken. Wijzigingen in de inhoud moeten worden opgeslagen.",
53
+ "fields.editRichText": "Bewerken in het voorbeeld",
54
+ "fields.editRichTextHint": "Klik op deze tekst in het voorbeeld om deze te bewerken. Wijzigingen moeten worden opgeslagen.",
53
55
  "fields.seoNotSet": "Niet ingesteld",
54
56
  "fields.seoTooShort": "Te kort",
55
57
  "fields.seoTooLong": "Te lang",
@@ -5,6 +5,7 @@
5
5
  "shell.searchScopeCurrent": "Huidige inhoudstaal",
6
6
  "shell.coreCategory": "Standaard",
7
7
  "shell.searchPages": "Pagina’s zoeken",
8
+ "shell.searchNamedEntries": "Zoeken in {name}",
8
9
  "shell.template": "Sjabloon",
9
10
  "shell.chooseTemplate": "Kies een sjabloon",
10
11
  "shell.search": "Zoeken",
@@ -49,7 +50,7 @@
49
50
  "shell.articleBodyEditing": "Artikeltekst bewerken",
50
51
  "shell.sidebar": "Zijbalk",
51
52
  "shell.inlinePreview": "Inline voorbeeld",
52
- "shell.editorExperienceHint": "Met het inline voorbeeld kun je op een artikeltekst klikken en direct op de pagina schrijven. Andere velden blijven in de zijbalk. Deze voorkeur wordt in deze browser opgeslagen; inhoud moet je nog steeds opslaan met Opslaan.",
53
+ "shell.editorExperienceHint": "Met het inline voorbeeld kun je op gekoppelde rich text klikken en direct op de pagina schrijven. Velden zonder koppeling met het voorbeeld blijven in de zijbalk. Deze voorkeur wordt in deze browser opgeslagen; inhoud moet je nog steeds opslaan met Opslaan.",
53
54
  "shell.githubAccount": "GitHub-account",
54
55
  "shell.noGithubAccount": "Er is geen GitHub-account gekoppeld.",
55
56
  "shell.repository": "Repository",
@@ -52,6 +52,8 @@
52
52
  "fields.editArticleBody": "Редактировать текст статьи в предпросмотре",
53
53
  "fields.loadingPreview": "Загрузка предпросмотра…",
54
54
  "fields.editArticleBodyHint": "Нажмите на текст статьи в предпросмотре, чтобы редактировать его. Изменения содержимого нужно сохранить.",
55
+ "fields.editRichText": "Редактировать в предпросмотре",
56
+ "fields.editRichTextHint": "Нажмите на этот текст в предпросмотре, чтобы редактировать его. Изменения нужно сохранить.",
55
57
  "fields.seoNotSet": "Не задано",
56
58
  "fields.seoTooShort": "Слишком коротко",
57
59
  "fields.seoTooLong": "Слишком длинно",
@@ -5,6 +5,7 @@
5
5
  "shell.searchScopeCurrent": "Текущий язык содержимого",
6
6
  "shell.coreCategory": "Основные",
7
7
  "shell.searchPages": "Поиск страниц",
8
+ "shell.searchNamedEntries": "Поиск в {name}",
8
9
  "shell.template": "Шаблон",
9
10
  "shell.chooseTemplate": "Выберите шаблон",
10
11
  "shell.search": "Поиск",
@@ -49,7 +50,7 @@
49
50
  "shell.articleBodyEditing": "Редактирование текста статьи",
50
51
  "shell.sidebar": "Боковая панель",
51
52
  "shell.inlinePreview": "Прямо в предпросмотре",
52
- "shell.editorExperienceHint": "Этот режим позволяет нажать на текст статьи и редактировать его прямо на странице. Остальные поля остаются на боковой панели. Настройка сохраняется в этом браузере; для сохранения содержимого по-прежнему нужно нажать «Сохранить».",
53
+ "shell.editorExperienceHint": "Этот режим позволяет нажать на связанный форматированный текст и редактировать его прямо на странице. Поля без привязки к предпросмотру остаются на боковой панели. Настройка сохраняется в этом браузере; для сохранения содержимого по-прежнему нужно нажать «Сохранить».",
53
54
  "shell.githubAccount": "Учётная запись GitHub",
54
55
  "shell.noGithubAccount": "Учётная запись GitHub не подключена.",
55
56
  "shell.repository": "Репозиторий",
@@ -50,6 +50,8 @@
50
50
  "fields.editArticleBody": "在预览中编辑文章正文",
51
51
  "fields.loadingPreview": "正在加载预览…",
52
52
  "fields.editArticleBodyHint": "点击预览中的文章正文即可编辑。内容更改后需保存。",
53
+ "fields.editRichText": "在预览中编辑",
54
+ "fields.editRichTextHint": "点击预览中的这段文字即可编辑。更改后需保存。",
53
55
  "fields.seoNotSet": "未设置",
54
56
  "fields.seoTooShort": "过短",
55
57
  "fields.seoTooLong": "过长",
@@ -5,6 +5,7 @@
5
5
  "shell.searchScopeCurrent": "当前内容语言",
6
6
  "shell.coreCategory": "基础",
7
7
  "shell.searchPages": "搜索页面",
8
+ "shell.searchNamedEntries": "在{name}中搜索",
8
9
  "shell.template": "模板",
9
10
  "shell.chooseTemplate": "选择模板",
10
11
  "shell.search": "搜索",
@@ -49,7 +50,7 @@
49
50
  "shell.articleBodyEditing": "文章正文编辑",
50
51
  "shell.sidebar": "侧边栏",
51
52
  "shell.inlinePreview": "行内预览",
52
- "shell.editorExperienceHint": "行内预览让你点击文章正文,直接在页面上编辑。其他字段仍在侧边栏中。此偏好设置保存在当前浏览器中;内容仍需点击“保存”才会保存。",
53
+ "shell.editorExperienceHint": "行内预览让你点击预览中绑定的富文本,直接在页面上编辑。没有预览绑定的字段仍在侧边栏中。此偏好设置保存在当前浏览器中;内容仍需点击“保存”才会保存。",
53
54
  "shell.githubAccount": "GitHub 账号",
54
55
  "shell.noGithubAccount": "未连接 GitHub 账号。",
55
56
  "shell.repository": "仓库",
@@ -25,8 +25,9 @@
25
25
  import { articleMarkdown } from "../../article-render";
26
26
  import Icon from "./Icon.svelte";
27
27
  import Select from "./Select.svelte";
28
- let { value, oninput, editableRoot }: { value: ArticleBody; oninput: (value: ArticleBody) => void; editableRoot?: HTMLElement } = $props();
28
+ let { value, oninput, editableRoot, label }: { value: ArticleBody; oninput: (value: ArticleBody) => void; editableRoot?: HTMLElement; label?: string } = $props();
29
29
  const inline = $derived(!!editableRoot);
30
+ const regionLabel = $derived(label ?? $t("richtext.body"));
30
31
  let root: HTMLElement;
31
32
  let surface: HTMLDivElement;
32
33
  let menu: HTMLDivElement;
@@ -203,7 +204,7 @@
203
204
  onMount(() => {
204
205
  if (editableRoot) {
205
206
  root = editableRoot; root.classList.add("block-content", "rigel-article-content"); root.contentEditable = "true";
206
- root.setAttribute("role", "textbox"); root.setAttribute("aria-label", $t("richtext.body")); root.setAttribute("aria-multiline", "true");
207
+ root.setAttribute("role", "textbox"); root.setAttribute("aria-label", regionLabel); root.setAttribute("aria-multiline", "true");
207
208
  root.setAttribute("aria-keyshortcuts", "Alt+F10");
208
209
  }
209
210
  view = root.ownerDocument.defaultView!;
@@ -278,7 +279,7 @@
278
279
  };
279
280
  });
280
281
 
281
- $effect(() => { if (editableRoot) editableRoot.setAttribute("aria-label", $t("richtext.body")); });
282
+ $effect(() => { if (editableRoot) editableRoot.setAttribute("aria-label", regionLabel); });
282
283
 
283
284
  $effect(() => { if (!linkOpen) view?.CSS.highlights?.delete("rigel-link-selection"); });
284
285
 
@@ -16,14 +16,16 @@
16
16
  import type { ContentDocument } from "../../schema/types";
17
17
  import { slugify } from "../../slugs";
18
18
 
19
- let { fields, values, storageSlug, body, selected, focusToken, devMode, documents = [], bodyInline = false, bodyLoading = false, oneditbody = () => {}, onchange, onbody }: {
19
+ let { fields, values, storageSlug, body, selected, focusToken, devMode, documents = [], bodyInline = false, bodyLoading = false, inlineFields = [], oneditbody = () => {}, oneditfield = () => {}, onchange, onbody }: {
20
20
  fields: CollectionField[];
21
21
  values: Record<string, unknown>;
22
22
  storageSlug: string;
23
23
  body: ArticleBody;
24
24
  bodyInline?: boolean;
25
25
  bodyLoading?: boolean;
26
+ inlineFields?: string[];
26
27
  oneditbody?: () => void;
28
+ oneditfield?: (path: string) => void;
27
29
  selected: SelectedField | null;
28
30
  focusToken: number;
29
31
  devMode: boolean;
@@ -66,7 +68,13 @@
66
68
  <span>{label}{devMode ? ` (${field.key})` : ""}</span>
67
69
  </label>
68
70
  {:else if field.type === "markdown"}
69
- <MarkdownEditor {label} value={String(values[field.key] ?? "")} oninput={(value) => onchange(field.key, value)} />
71
+ {#if inlineFields.includes(field.key)}
72
+ <span class="schema-label">{label}{devMode ? ` (${field.key})` : ""}</span>
73
+ <button type="button" class="outlined" onclick={() => oneditfield(field.key)}>{$t("fields.editRichText")}</button>
74
+ <p class="hint">{$t("fields.editRichTextHint")}</p>
75
+ {:else}
76
+ <MarkdownEditor {label} value={String(values[field.key] ?? "")} oninput={(value) => onchange(field.key, value)} />
77
+ {/if}
70
78
  {:else if field.type === "image"}
71
79
  <label><span>{label}{devMode ? ` (${field.key})` : ""}</span><ImagePicker value={String(values[field.key] ?? "")} onpick={(value) => onchange(field.key, value)} /></label>
72
80
  {:else if field.type === "tags"}
@@ -28,6 +28,28 @@
28
28
  let view: EditorView | undefined;
29
29
  let lastValue: string | undefined;
30
30
  let imageSelection: SelectionBookmark | undefined;
31
+ let toolbar: HTMLDivElement | undefined;
32
+ let floating = $state("");
33
+ let linkPosition = $state("");
34
+ let releaseFloating: (() => void) | undefined;
35
+ const inline = $derived(!!editableRoot);
36
+ function positionFloating() {
37
+ if (!inline || !editableRoot || !view || mode !== "visual") { floating = ""; linkPosition = ""; return; }
38
+ const owner = editableRoot.ownerDocument;
39
+ const active = owner.activeElement;
40
+ if (!editableRoot.contains(active) && !toolbar?.contains(active)) { floating = ""; linkPosition = ""; return; }
41
+ const anchor = view.state.selection.$from;
42
+ const block = anchor.depth > 0 ? view.nodeDOM(anchor.before(1)) : null;
43
+ const rect = block instanceof HTMLElement ? block.getBoundingClientRect() : editableRoot.getBoundingClientRect();
44
+ const { innerWidth, innerHeight } = owner.defaultView!;
45
+ if (rect.bottom < 0 || rect.top > innerHeight) { floating = ""; linkPosition = ""; return; }
46
+ const size = toolbar?.getBoundingClientRect();
47
+ const height = size?.height || 44, width = size?.width || 360;
48
+ const top = Math.max(8, Math.min(rect.top >= height + 12 ? rect.top - height - 8 : rect.bottom + 8, innerHeight - height - 8));
49
+ const left = Math.max(8, Math.min(rect.left, innerWidth - width - 8));
50
+ floating = `left:${left}px;top:${top}px`;
51
+ linkPosition = `left:${Math.max(8, Math.min(left, innerWidth - 356))}px;top:${Math.max(8, Math.min(top + height + 4, innerHeight - 128))}px`;
52
+ }
31
53
  function insertImage(url: string, alt: string, credit?: StockResult) {
32
54
  if (!view || !imageSelection || !safeImageUrl(url)) return;
33
55
  let tr = view.state.tr.setSelection(imageSelection.resolve(view.state.doc));
@@ -76,6 +98,7 @@
76
98
  state = view!.state.apply(transaction);
77
99
  view!.updateState(state);
78
100
  if (transaction.docChanged) emit(serializeMarkdown(state.doc));
101
+ positionFloating();
79
102
  },
80
103
  handlePaste(editor, event) {
81
104
  event.preventDefault();
@@ -85,8 +108,14 @@
85
108
  },
86
109
  handleDrop(_editor, _event, _slice, moved) { return !moved; },
87
110
  handleClickOn(_editor, _pos, node) { return node.type.name === "image"; },
88
- handleDOMEvents: { click: (_editor, event) => { if ((event.target as Element).closest("a")) { event.preventDefault(); return true; } return false; } },
111
+ handleDOMEvents: { click: (_editor, event) => { if ((event.target as Element).closest("a")) { event.preventDefault(); return true; } return false; }, focus: () => { positionFloating(); return false; }, blur: () => { setTimeout(positionFloating); return false; } },
89
112
  });
113
+ const target = editableRoot ?? host;
114
+ const owner = target.ownerDocument;
115
+ const reposition = () => positionFloating();
116
+ owner.defaultView?.addEventListener("scroll", reposition, true);
117
+ owner.defaultView?.addEventListener("resize", reposition);
118
+ releaseFloating = () => { owner.defaultView?.removeEventListener("scroll", reposition, true); owner.defaultView?.removeEventListener("resize", reposition); };
90
119
  }
91
120
  if (value !== lastValue) {
92
121
  lastValue = value;
@@ -94,7 +123,8 @@
94
123
  }
95
124
  });
96
125
  $effect(() => { const editable = mode === "visual"; view?.setProps({ editable: () => editable }); });
97
- onDestroy(() => view?.destroy());
126
+ $effect(() => { void mode; void inline; positionFloating(); });
127
+ onDestroy(() => { releaseFloating?.(); view?.destroy(); });
98
128
 
99
129
  function activeMark(type: string) {
100
130
  if (!state) return false;
@@ -164,7 +194,8 @@
164
194
 
165
195
  <svelte:window onpointerdown={event => { if (linkOpen && !document.getElementById(`${id}-link`)?.contains(event.target as Node) && !linkButton?.contains(event.target as Node)) linkOpen = false; }} />
166
196
 
167
- <div class="markdown-editor body-editor" class:markdown-inline={!!editableRoot}>
197
+ <div class="markdown-editor body-editor" class:markdown-inline={inline} class:markdown-floating={inline && mode === "visual"}>
198
+ {#if !inline || mode === "source"}
168
199
  <div class="markdown-heading">
169
200
  <div class="field-label" id={`${id}-label`}>{label}</div>
170
201
  <div class="markdown-modes" role="group" aria-label={$t("richtext.editingMode", { label })}>
@@ -172,9 +203,10 @@
172
203
  <button type="button" aria-pressed={mode === "source"} onclick={() => { mode = "source"; linkOpen = false; }}>{$t("richtext.markdown")}</button>
173
204
  </div>
174
205
  </div>
206
+ {/if}
175
207
  {#if warning}<p class="markdown-warning" role="status">{warning}</p>{/if}
176
208
  <div class="markdown-visual" hidden={mode !== "visual"}>
177
- <div class="body-toolbar formatting-toolbar" role="group" aria-label={$t("richtext.formatting", { label })}>
209
+ <div class="body-toolbar formatting-toolbar" bind:this={toolbar} role="group" aria-label={$t("richtext.formatting", { label })} style={inline ? floating : undefined} hidden={inline && !floating}>
178
210
  <Select ariaLabel={$t("richtext.textStyle")} data-tooltip={$t("richtext.textStyle")} value={format} options={[{ value: "paragraph", label: $t("richtext.paragraph") }, ...[1, 2, 3, 4, 5, 6].map(level => ({ value: `h${level}`, label: $t("richtext.heading", { level }) })), { value: "code_block", label: $t("richtext.codeBlock") }].map(option => ({ ...option, disabled: !!state && format !== option.value && !formatCommand(option.value)(state) }))} onchange={next => command(formatCommand(String(next)))} />
179
211
  <div class="format-group" role="group" aria-label={$t("richtext.textFormatting")}>
180
212
  {#each marks as item}
@@ -188,9 +220,10 @@
188
220
  </div>
189
221
  <button bind:this={linkButton} type="button" aria-label={$t("richtext.link")} data-tooltip={$t("richtext.link")} aria-pressed={activeMark("link")} aria-expanded={linkOpen} aria-controls={`${id}-link`} disabled={!state || !toggleMark(schema.marks.link)(state)} onmousedown={event => event.preventDefault()} onclick={openLink}><Icon name="link" size={18} /></button>
190
222
  <ImagePicker toolbar onopen={() => { imageSelection = view?.state.selection.getBookmark(); linkOpen = false; }} onpick={insertImage} onclose={() => { imageSelection = undefined; }} />
223
+ {#if inline}<button type="button" class="markdown-toggle" aria-label={$t("richtext.markdown")} data-tooltip={$t("richtext.markdown")} onmousedown={event => event.preventDefault()} onclick={() => { mode = "source"; linkOpen = false; }}>{$t("richtext.markdown")}</button>{/if}
191
224
  </div>
192
225
  {#if linkOpen}
193
- <div class="markdown-link" id={`${id}-link`} role="group" aria-label={$t("richtext.editLink")} onkeydown={event => { if (event.key === "Escape") { event.stopPropagation(); linkOpen = false; linkButton.focus(); } if (event.key === "Enter") { event.preventDefault(); link(); } }}>
226
+ <div class="markdown-link" id={`${id}-link`} role="group" aria-label={$t("richtext.editLink")} style={inline ? linkPosition : undefined} onkeydown={event => { if (event.key === "Escape") { event.stopPropagation(); linkOpen = false; linkButton.focus(); } if (event.key === "Enter") { event.preventDefault(); link(); } }}>
194
227
  <label for={`${id}-url`}>{$t("richtext.linkUrl")}</label>
195
228
  <div class="markdown-link-controls">
196
229
  <input id={`${id}-url`} type="text" placeholder="https://…" bind:this={linkInput} bind:value={linkUrl} aria-invalid={!!linkError} aria-describedby={linkError ? `${id}-error` : undefined} />
@@ -1,6 +1,13 @@
1
1
  <script lang="ts">
2
2
  import { t } from "../i18n";
3
3
  let { tab = $bindable("content") }: { tab?: "content" | "seo" } = $props();
4
+ let tabs: HTMLDivElement;
5
+ let height = $state(0);
6
+ $effect(() => {
7
+ const panel = tabs?.parentElement;
8
+ panel?.style.setProperty("--properties-tabs-height", `${height}px`);
9
+ return () => panel?.style.removeProperty("--properties-tabs-height");
10
+ });
4
11
  function onkey(event: KeyboardEvent) {
5
12
  if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key)) return;
6
13
  event.preventDefault();
@@ -9,7 +16,7 @@
9
16
  }
10
17
  </script>
11
18
 
12
- <div class="panel-tabs" role="tablist" aria-label={$t("fields.properties")}>
19
+ <div class="panel-tabs" role="tablist" aria-label={$t("fields.properties")} bind:this={tabs} bind:offsetHeight={height}>
13
20
  {#each ["content", "seo"] as value}
14
21
  <button type="button" role="tab" id={`properties-tab-${value}`} aria-controls={`properties-${value}`} aria-selected={tab === value} tabindex={tab === value ? 0 : -1} data-tab={value} onclick={() => (tab = value as "content" | "seo")} onkeydown={onkey}>{$t(value === "content" ? "fields.content" : "fields.seo")}</button>
15
22
  {/each}
@@ -18,7 +18,7 @@
18
18
  import type { ContentDocument, FieldDef, Schema } from "../../schema/types";
19
19
 
20
20
  let {
21
- schema, field, value, base, documents, devMode = false, onvalue,
21
+ schema, field, value, base, documents, devMode = false, inlineFields = [], oneditfield = () => {}, onvalue,
22
22
  }: {
23
23
  schema: Schema;
24
24
  field: FieldDef;
@@ -26,6 +26,8 @@
26
26
  base: string;
27
27
  documents: ContentDocument[];
28
28
  devMode?: boolean;
29
+ inlineFields?: string[];
30
+ oneditfield?: (path: string) => void;
29
31
  onvalue: (next: unknown) => void;
30
32
  } = $props();
31
33
 
@@ -85,7 +87,15 @@
85
87
  <textarea rows="3" value={typeof value === "string" ? value : ""} oninput={(event) => onvalue(event.currentTarget.value)}></textarea>
86
88
  </label>
87
89
  {:else if field.type === "markdown"}
88
- <MarkdownEditor label={fieldLabel} value={typeof value === "string" ? value : ""} oninput={(next) => onvalue(next)} />
90
+ {#if !field.sidebarOnly && inlineFields.includes(base)}
91
+ <div class="schema-field">
92
+ <span class="schema-label">{label}</span>
93
+ <button type="button" class="outlined" onclick={() => oneditfield(base)}>{$t("fields.editRichText")}</button>
94
+ <p class="hint">{$t("fields.editRichTextHint")}</p>
95
+ </div>
96
+ {:else}
97
+ <MarkdownEditor label={fieldLabel} value={typeof value === "string" ? value : ""} oninput={(next) => onvalue(next)} />
98
+ {/if}
89
99
  {:else if field.type === "image" || field.type === "file"}
90
100
  <div class="schema-field">
91
101
  <span class="schema-label">{label}</span>
@@ -113,6 +123,8 @@
113
123
  {schema}
114
124
  {documents}
115
125
  {devMode}
126
+ {inlineFields}
127
+ {oneditfield}
116
128
  field={sub}
117
129
  base={`${base}.${sub.key}`}
118
130
  value={value && typeof value === "object" ? (value as Record<string, unknown>)[sub.key] : undefined}
@@ -141,6 +153,8 @@
141
153
  {schema}
142
154
  {documents}
143
155
  {devMode}
156
+ {inlineFields}
157
+ {oneditfield}
144
158
  field={sub}
145
159
  base={`${base}.${(item as Record<string, unknown>)._key}.${sub.key}`}
146
160
  value={item && typeof item === "object" ? (item as Record<string, unknown>)[sub.key] : undefined}