@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
|
@@ -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",
|
|
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",
|
|
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
|
-
|
|
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
|
-
|
|
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={
|
|
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
|
-
|
|
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}
|
|
@@ -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}
|
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
import type { Manifest } from "../../manifest/types";
|
|
6
6
|
import type { GitHubAccess } from "./github-auth";
|
|
7
7
|
import Icon from "./Icon.svelte";
|
|
8
|
+
import Select from "./Select.svelte";
|
|
9
|
+
import { effectiveSearchScope } from "../../manifest/search";
|
|
8
10
|
|
|
9
|
-
let { s = $bindable(), github, user, error, saving, onerase, onclose }: {
|
|
11
|
+
let { s = $bindable(), search, contentLocales, github, user, error, saving, onerase, onclose }: {
|
|
10
12
|
s: RigelSettings;
|
|
13
|
+
search?: Manifest["search"];
|
|
14
|
+
contentLocales?: Manifest["contentLocales"];
|
|
11
15
|
github?: Manifest["github"];
|
|
12
16
|
user?: GitHubAccess["user"];
|
|
13
17
|
error: string;
|
|
@@ -16,8 +20,8 @@
|
|
|
16
20
|
onclose: () => void;
|
|
17
21
|
} = $props();
|
|
18
22
|
|
|
19
|
-
type PaneId = "
|
|
20
|
-
let active: PaneId = $state("
|
|
23
|
+
type PaneId = "interface" | "content" | "connection" | "media" | "advanced";
|
|
24
|
+
let active: PaneId = $state("interface");
|
|
21
25
|
|
|
22
26
|
const providerIds = Object.keys(STOCK_PROVIDERS) as (keyof typeof STOCK_PROVIDERS)[];
|
|
23
27
|
let keys = $state(getKeys());
|
|
@@ -26,20 +30,20 @@
|
|
|
26
30
|
let eraseError = $state("");
|
|
27
31
|
|
|
28
32
|
const panes: { id: PaneId; label: string }[] = $derived([
|
|
29
|
-
{ id: "
|
|
30
|
-
{ id: "
|
|
33
|
+
{ id: "interface", label: $t("shell.interface") },
|
|
34
|
+
{ id: "content", label: $t("shell.content") },
|
|
31
35
|
{ id: "connection", label: $t("shell.connection") },
|
|
32
36
|
{ id: "media", label: $t("shell.media") },
|
|
33
|
-
|
|
37
|
+
|
|
34
38
|
{ id: "advanced", label: $t("shell.advanced") },
|
|
35
39
|
]);
|
|
36
40
|
|
|
37
41
|
const PANE_ICONS = {
|
|
38
|
-
|
|
39
|
-
|
|
42
|
+
interface: "palette",
|
|
43
|
+
content: "fileText",
|
|
40
44
|
connection: "user",
|
|
41
45
|
media: "image",
|
|
42
|
-
|
|
46
|
+
|
|
43
47
|
advanced: "wrench",
|
|
44
48
|
} as const;
|
|
45
49
|
|
|
@@ -71,6 +75,7 @@
|
|
|
71
75
|
{#each panes as p (p.id)}
|
|
72
76
|
<button
|
|
73
77
|
class:active={active === p.id}
|
|
78
|
+
aria-current={active === p.id ? "true" : undefined}
|
|
74
79
|
onclick={() => (active = p.id)}
|
|
75
80
|
>
|
|
76
81
|
<span class="pico"><Icon name={PANE_ICONS[p.id]} size={24} /></span>
|
|
@@ -79,9 +84,9 @@
|
|
|
79
84
|
{/each}
|
|
80
85
|
</nav>
|
|
81
86
|
|
|
82
|
-
<div class="settings-pane">
|
|
83
|
-
{#if active === "
|
|
84
|
-
|
|
87
|
+
<div class="settings-pane" role="region" aria-label={panes.find(p => p.id === active)?.label}>
|
|
88
|
+
{#if active === "interface"}
|
|
89
|
+
|
|
85
90
|
<fieldset>
|
|
86
91
|
<legend>{$t("shell.theme")}</legend>
|
|
87
92
|
<div class="theme-options">
|
|
@@ -101,17 +106,30 @@
|
|
|
101
106
|
</label>
|
|
102
107
|
<p class="hint" id="high-contrast-hint">{$t("shell.highContrastHint")}</p>
|
|
103
108
|
</fieldset>
|
|
104
|
-
|
|
105
|
-
{:else if active === "editor"}
|
|
106
|
-
<h3>{$t("shell.editorExperience")}</h3>
|
|
107
109
|
<fieldset>
|
|
108
|
-
<legend>{$t("shell.
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
<legend>{$t("shell.editorInterfaceLanguage")}</legend>
|
|
111
|
+
{#each availableLocales as lang}
|
|
112
|
+
<label class="radio">
|
|
113
|
+
<input type="radio" bind:group={s.language} value={lang} />
|
|
114
|
+
<span>{localeNames[lang]}</span>
|
|
115
|
+
</label>
|
|
116
|
+
{/each}
|
|
111
117
|
</fieldset>
|
|
118
|
+
|
|
119
|
+
{:else if active === "content"}
|
|
120
|
+
|
|
121
|
+
<Select label={$t("shell.articleBodyEditing")} value={s.userPref.editorExperience ?? "inline"} options={[
|
|
122
|
+
{ value: "sidebar", label: $t("shell.sidebar") },
|
|
123
|
+
{ value: "inline", label: $t("shell.inlinePreview") },
|
|
124
|
+
]} onchange={value => s.userPref.editorExperience = value === "sidebar" ? "sidebar" : "inline"} />
|
|
112
125
|
<p class="hint">{$t("shell.editorExperienceHint")}</p>
|
|
126
|
+
<Select label={$t("shell.searchScope")} value={s.userPref.searchScope ?? "inherit"} options={[
|
|
127
|
+
{ value: "inherit", label: $t("shell.searchScopeDefault", { scope: $t(effectiveSearchScope(search, undefined) === "all" ? "shell.searchScopeAll" : "shell.searchScopeCurrent") }) },
|
|
128
|
+
{ value: "all", label: $t("shell.searchScopeAll") },
|
|
129
|
+
{ value: "currentLocale", label: $t("shell.searchScopeCurrent"), disabled: !contentLocales || contentLocales.locales.length < 2 },
|
|
130
|
+
]} onchange={value => s.userPref.searchScope = value === "all" || value === "currentLocale" ? value : undefined} />
|
|
113
131
|
{:else if active === "connection"}
|
|
114
|
-
|
|
132
|
+
|
|
115
133
|
{#if github}
|
|
116
134
|
<dl class="connection-details">
|
|
117
135
|
<dt>{$t("shell.githubAccount")}</dt>
|
|
@@ -165,17 +183,6 @@
|
|
|
165
183
|
|
|
166
184
|
</section>
|
|
167
185
|
|
|
168
|
-
{:else if active === "language"}
|
|
169
|
-
<h3>{$t("shell.language")}</h3>
|
|
170
|
-
<fieldset>
|
|
171
|
-
<legend>{$t("shell.editorInterfaceLanguage")}</legend>
|
|
172
|
-
{#each availableLocales as lang}
|
|
173
|
-
<label class="radio">
|
|
174
|
-
<input type="radio" bind:group={s.language} value={lang} />
|
|
175
|
-
<span>{localeNames[lang]}</span>
|
|
176
|
-
</label>
|
|
177
|
-
{/each}
|
|
178
|
-
</fieldset>
|
|
179
186
|
|
|
180
187
|
{:else}
|
|
181
188
|
<h3>{$t("shell.developerMode")}</h3>
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
import SidebarIcon from "./SidebarIcon.svelte";
|
|
5
5
|
import { tick } from "svelte";
|
|
6
6
|
import type { Manifest } from "../../manifest/types";
|
|
7
|
+
import { searchIndex, type SearchHit } from "../../manifest/search";
|
|
8
|
+
import type { SearchScope } from "../../config";
|
|
7
9
|
|
|
8
10
|
type Sel =
|
|
9
11
|
| { kind: "media" }
|
|
@@ -20,10 +22,14 @@
|
|
|
20
22
|
manifest,
|
|
21
23
|
sel,
|
|
22
24
|
onselect,
|
|
25
|
+
scope = "all",
|
|
26
|
+
contentLocale,
|
|
23
27
|
}: {
|
|
24
28
|
manifest: Manifest;
|
|
25
29
|
sel: Sel | null;
|
|
26
|
-
onselect: (s: Sel) => void;
|
|
30
|
+
onselect: (s: Sel, locale?: string) => void;
|
|
31
|
+
scope?: SearchScope;
|
|
32
|
+
contentLocale?: string;
|
|
27
33
|
} = $props();
|
|
28
34
|
|
|
29
35
|
let open = $state(false);
|
|
@@ -35,53 +41,11 @@
|
|
|
35
41
|
if (open) inputEl?.focus();
|
|
36
42
|
});
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
hint: string;
|
|
41
|
-
action: () => void;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
const hits = $derived.by((): Hit[] => {
|
|
45
|
-
const q = query.trim().toLowerCase();
|
|
46
|
-
const all: Hit[] = [];
|
|
47
|
-
for (const p of manifest.pages) {
|
|
48
|
-
all.push({
|
|
49
|
-
label: p.label,
|
|
50
|
-
hint: $t("shell.page"),
|
|
51
|
-
action: () => onselect({ kind: "page", slug: p.slug }),
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
for (const doc of manifest.documents ?? []) {
|
|
55
|
-
all.push({
|
|
56
|
-
label: String(doc.data.title ?? doc.data.name ?? doc.slug),
|
|
57
|
-
hint: $t("shell.searchItemHint", { name: doc.type, slug: doc.slug }),
|
|
58
|
-
action: () => onselect({ kind: "document", type: doc.type, id: doc.id }),
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
for (const c of manifest.collections ?? []) {
|
|
62
|
-
all.push({
|
|
63
|
-
label: c.label,
|
|
64
|
-
hint: $t("shell.collection"),
|
|
65
|
-
action: () => onselect({ kind: "collection", name: c.name }),
|
|
66
|
-
});
|
|
67
|
-
for (const po of c.posts) {
|
|
68
|
-
all.push({
|
|
69
|
-
label: String(po.frontmatter.title ?? po.slug),
|
|
70
|
-
hint: $t("shell.searchItemHint", { name: c.label, slug: po.slug }),
|
|
71
|
-
action: () => onselect({ kind: "post", collection: c.name, slug: po.slug }),
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (manifest.menus) {
|
|
76
|
-
all.push({ label: $t("shell.menus"), hint: $t("shell.collection"), action: () => onselect({ kind: "menus" }) });
|
|
77
|
-
for (const m of manifest.menus.entries) all.push({ label: m.title, hint: $t("shell.searchMenuHint", { slug: m.slug }), action: () => onselect({ kind: "menu", slug: m.slug }) });
|
|
78
|
-
}
|
|
79
|
-
if (!q) return all.slice(0, 12);
|
|
80
|
-
return all.filter((h) => h.label.toLowerCase().includes(q) || h.hint.toLowerCase().includes(q)).slice(0, 12);
|
|
81
|
-
});
|
|
44
|
+
const index = $derived(searchIndex(manifest, scope, contentLocale, $t));
|
|
45
|
+
const hits = $derived(index.filter(hit => [hit.label, hit.hint, hit.language ?? ""].some(value => value.toLowerCase().includes(query.trim().toLowerCase()))).slice(0, 12));
|
|
82
46
|
|
|
83
|
-
function trigger(h:
|
|
84
|
-
h.
|
|
47
|
+
function trigger(h: SearchHit) {
|
|
48
|
+
onselect(h.selection, h.locale);
|
|
85
49
|
open = false;
|
|
86
50
|
query = "";
|
|
87
51
|
active = -1;
|
|
@@ -145,10 +109,10 @@
|
|
|
145
109
|
<kbd>{$t("shell.escapeKeyLowercase")}</kbd>
|
|
146
110
|
</div>
|
|
147
111
|
<ul id="rigel-search-results" role="listbox" aria-label={$t("shell.searchResults")}>
|
|
148
|
-
{#each hits as h, i (h.
|
|
112
|
+
{#each hits as h, i (h.id)}
|
|
149
113
|
<li role="presentation">
|
|
150
114
|
<button id={`rigel-search-hit-${i}`} role="option" aria-selected={active === i} onfocus={() => active = i} onclick={() => trigger(h)}>
|
|
151
|
-
<span>{h.label}</span>
|
|
115
|
+
<span>{h.label}{#if h.language}<span class="badge search-language" title={h.language} aria-label={h.language}>{h.locale?.slice(0, 2).toUpperCase()}</span>{/if}</span>
|
|
152
116
|
<span class="hint">{h.hint}</span>
|
|
153
117
|
</button>
|
|
154
118
|
</li>
|