@rigel-cms/rigel 1.0.0 → 1.1.0
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 -0
- package/docs/extensions.md +194 -0
- package/package.json +15 -3
- package/src/article-body.ts +49 -37
- package/src/article-render.ts +25 -2
- package/src/article.ts +44 -0
- package/src/config.ts +12 -2
- package/src/editor/App.svelte +98 -97
- package/src/editor/blocks/extension.ts +29 -0
- package/src/editor/blocks/index.ts +17 -15
- package/src/editor/blocks/video.ts +2 -1
- package/src/editor/i18n/README.md +23 -0
- package/src/editor/i18n/de/connection.json +136 -0
- package/src/editor/i18n/de/defaults.json +6 -0
- package/src/editor/i18n/de/fields.json +130 -0
- package/src/editor/i18n/de/local.json +49 -0
- package/src/editor/i18n/de/media.json +166 -0
- package/src/editor/i18n/de/richtext.json +116 -0
- package/src/editor/i18n/de/shell.json +181 -0
- package/src/editor/i18n/de/system.json +14 -0
- package/src/editor/i18n/de/validation.json +82 -0
- package/src/editor/i18n/en/connection.json +136 -0
- package/src/editor/i18n/en/defaults.json +6 -0
- package/src/editor/i18n/en/fields.json +130 -0
- package/src/editor/i18n/en/local.json +49 -0
- package/src/editor/i18n/en/media.json +166 -0
- package/src/editor/i18n/en/richtext.json +116 -0
- package/src/editor/i18n/en/shell.json +181 -0
- package/src/editor/i18n/en/system.json +14 -0
- package/src/editor/i18n/en/validation.json +82 -0
- package/src/editor/i18n/es/connection.json +136 -0
- package/src/editor/i18n/es/defaults.json +6 -0
- package/src/editor/i18n/es/fields.json +130 -0
- package/src/editor/i18n/es/local.json +49 -0
- package/src/editor/i18n/es/media.json +166 -0
- package/src/editor/i18n/es/richtext.json +116 -0
- package/src/editor/i18n/es/shell.json +181 -0
- package/src/editor/i18n/es/system.json +14 -0
- package/src/editor/i18n/es/validation.json +82 -0
- package/src/editor/i18n/format.ts +26 -0
- package/src/editor/i18n/fr/connection.json +136 -0
- package/src/editor/i18n/fr/defaults.json +6 -0
- package/src/editor/i18n/fr/fields.json +130 -0
- package/src/editor/i18n/fr/local.json +49 -0
- package/src/editor/i18n/fr/media.json +166 -0
- package/src/editor/i18n/fr/richtext.json +116 -0
- package/src/editor/i18n/fr/shell.json +181 -0
- package/src/editor/i18n/fr/system.json +14 -0
- package/src/editor/i18n/fr/validation.json +82 -0
- package/src/editor/i18n/index.ts +18 -0
- package/src/editor/i18n/it/connection.json +136 -0
- package/src/editor/i18n/it/defaults.json +6 -0
- package/src/editor/i18n/it/fields.json +130 -0
- package/src/editor/i18n/it/local.json +49 -0
- package/src/editor/i18n/it/media.json +166 -0
- package/src/editor/i18n/it/richtext.json +116 -0
- package/src/editor/i18n/it/shell.json +181 -0
- package/src/editor/i18n/it/system.json +14 -0
- package/src/editor/i18n/it/validation.json +82 -0
- package/src/editor/i18n/ja/connection.json +136 -0
- package/src/editor/i18n/ja/defaults.json +6 -0
- package/src/editor/i18n/ja/fields.json +130 -0
- package/src/editor/i18n/ja/local.json +49 -0
- package/src/editor/i18n/ja/media.json +166 -0
- package/src/editor/i18n/ja/richtext.json +116 -0
- package/src/editor/i18n/ja/shell.json +181 -0
- package/src/editor/i18n/ja/system.json +14 -0
- package/src/editor/i18n/ja/validation.json +82 -0
- package/src/editor/i18n/locales.ts +13 -0
- package/src/editor/i18n/nl/connection.json +136 -0
- package/src/editor/i18n/nl/defaults.json +6 -0
- package/src/editor/i18n/nl/fields.json +130 -0
- package/src/editor/i18n/nl/local.json +49 -0
- package/src/editor/i18n/nl/media.json +166 -0
- package/src/editor/i18n/nl/richtext.json +116 -0
- package/src/editor/i18n/nl/shell.json +181 -0
- package/src/editor/i18n/nl/system.json +14 -0
- package/src/editor/i18n/nl/validation.json +82 -0
- package/src/editor/i18n/ru/connection.json +138 -0
- package/src/editor/i18n/ru/defaults.json +6 -0
- package/src/editor/i18n/ru/fields.json +132 -0
- package/src/editor/i18n/ru/local.json +49 -0
- package/src/editor/i18n/ru/media.json +166 -0
- package/src/editor/i18n/ru/richtext.json +116 -0
- package/src/editor/i18n/ru/shell.json +181 -0
- package/src/editor/i18n/ru/system.json +14 -0
- package/src/editor/i18n/ru/validation.json +82 -0
- package/src/editor/i18n/server.ts +13 -0
- package/src/editor/i18n/zh/connection.json +136 -0
- package/src/editor/i18n/zh/defaults.json +6 -0
- package/src/editor/i18n/zh/fields.json +130 -0
- package/src/editor/i18n/zh/local.json +49 -0
- package/src/editor/i18n/zh/media.json +166 -0
- package/src/editor/i18n/zh/richtext.json +116 -0
- package/src/editor/i18n/zh/shell.json +181 -0
- package/src/editor/i18n/zh/system.json +14 -0
- package/src/editor/i18n/zh/validation.json +82 -0
- package/src/editor/lib/ActionDialog.svelte +5 -4
- package/src/editor/lib/BlockEditor.svelte +305 -88
- package/src/editor/lib/CollectionPanel.svelte +17 -15
- package/src/editor/lib/DatePicker.svelte +15 -12
- package/src/editor/lib/ExtensionMount.svelte +12 -0
- package/src/editor/lib/Field.svelte +3 -2
- package/src/editor/lib/ImagePicker.svelte +82 -81
- package/src/editor/lib/MarkdownEditor.svelte +25 -24
- package/src/editor/lib/MediaPicker.svelte +6 -5
- package/src/editor/lib/MenuEditor.svelte +31 -30
- package/src/editor/lib/PreviewFrame.svelte +11 -6
- package/src/editor/lib/PropertiesTabs.svelte +3 -2
- package/src/editor/lib/PublishDialog.svelte +15 -14
- package/src/editor/lib/ReferenceSelect.svelte +3 -2
- package/src/editor/lib/SchemaField.svelte +15 -13
- package/src/editor/lib/SchemaPanel.svelte +20 -19
- package/src/editor/lib/Select.svelte +3 -2
- package/src/editor/lib/SeoGuidance.svelte +6 -5
- package/src/editor/lib/Settings.svelte +54 -54
- package/src/editor/lib/Shortcuts.svelte +7 -6
- package/src/editor/lib/SitePreview.svelte +10 -9
- package/src/editor/lib/TopBar.svelte +18 -17
- package/src/editor/lib/UnsavedChanges.svelte +7 -6
- package/src/editor/lib/VisualEditor.svelte +14 -13
- package/src/editor/lib/calendar.ts +10 -4
- package/src/editor/lib/embed-preview.ts +23 -5
- package/src/editor/lib/extension-preview.ts +121 -0
- package/src/editor/lib/github-auth.ts +23 -22
- package/src/editor/lib/github-backend.ts +96 -91
- package/src/editor/lib/images.ts +33 -31
- package/src/editor/lib/menu-tree.ts +5 -4
- package/src/editor/lib/preview-frame.ts +4 -3
- package/src/editor/lib/schema-edit.ts +3 -2
- package/src/editor/lib/settings.ts +5 -3
- package/src/editor/lib/slash-command.ts +1 -0
- package/src/editor/lib/tooltip.ts +1 -1
- package/src/editor/main.ts +9 -0
- package/src/editor/styles.css +6 -3
- package/src/env.d.ts +11 -1
- package/src/extensions.ts +160 -0
- package/src/manifest/collections.ts +6 -5
- package/src/manifest/menu.ts +12 -10
- package/src/manifest/navigation.ts +9 -8
- package/src/manifest/references.ts +8 -7
- package/src/manifest/seo.ts +4 -3
- package/src/manifest/types.ts +1 -0
- package/src/preview.ts +22 -2
- package/src/routes/local-api.ts +5 -3
- package/src/routes/local-documents.ts +20 -16
- package/src/routes/local-media.ts +21 -16
- package/src/routes/local-menu.ts +9 -7
- package/src/routes/local-post.ts +18 -12
- package/src/routes/media-files.ts +10 -8
- package/src/routes/rigel-page.astro +2 -1
- package/src/schema/store.ts +22 -20
- package/src/schema/types.ts +1 -0
- package/src/schema/validate.ts +73 -69
- package/src/slugs.ts +11 -10
- package/src/vite/manifest-plugin.ts +33 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { t } from "../i18n";
|
|
2
3
|
import SchemaField from "./SchemaField.svelte";
|
|
3
4
|
import Icon from "./Icon.svelte";
|
|
4
5
|
import PropertiesTabs from "./PropertiesTabs.svelte";
|
|
@@ -92,15 +93,15 @@
|
|
|
92
93
|
|
|
93
94
|
<div class="schema-panel" bind:this={host}>
|
|
94
95
|
{#if docType.shared}
|
|
95
|
-
<p class="shared-badge"><Icon name="info" size={12} /> {docType.demo ? "
|
|
96
|
+
<p class="shared-badge"><Icon name="info" size={12} /> {$t(docType.demo ? "fields.demoContentNotice" : "fields.sharedContentNotice")}</p>
|
|
96
97
|
{/if}
|
|
97
98
|
<div class:page-properties-header={!docType.shared}>
|
|
98
99
|
{#if seoFields.length}<PropertiesTabs bind:tab />{/if}
|
|
99
100
|
<section class="document-properties" hidden={tab !== "content"}>
|
|
100
|
-
<h3>{docType.shared ? "
|
|
101
|
+
<h3>{$t(docType.shared ? "fields.content" : "fields.pageProperties")}</h3>
|
|
101
102
|
{#if !docType.shared}
|
|
102
|
-
<label><span>
|
|
103
|
-
<p class="hint">
|
|
103
|
+
<label><span>{$t("fields.urlSlug")}</span><input type="text" value={String(values.slug ?? docId.slice(docType.name.length + 1))} oninput={(event) => onchange({ ...values, slug: event.currentTarget.value })} onblur={() => { if (Object.hasOwn(values, "slug")) onchange({ ...values, slug: slugify(String(values.slug)) }); }} /></label>
|
|
104
|
+
<p class="hint">{$t("fields.documentSlugHint")}</p>
|
|
104
105
|
{/if}
|
|
105
106
|
{#each docFields.filter(field => docType.shared || !isSeoField(field.key)) as field (field.key)}
|
|
106
107
|
<div data-schema-path={field.key}>
|
|
@@ -111,40 +112,40 @@
|
|
|
111
112
|
</div>
|
|
112
113
|
<div id="properties-content" role={seoFields.length ? "tabpanel" : undefined} aria-labelledby={seoFields.length ? "properties-tab-content" : undefined} hidden={tab !== "content"}>
|
|
113
114
|
{#if creating}
|
|
114
|
-
<p class="panel-note">
|
|
115
|
+
<p class="panel-note">{$t("fields.newPageNotice")}</p>
|
|
115
116
|
{/if}
|
|
116
117
|
{#if sections.length || values.sections !== undefined}
|
|
117
118
|
<section>
|
|
118
|
-
<h3>
|
|
119
|
+
<h3>{$t("fields.structure")}</h3>
|
|
119
120
|
<ul class="structure-list">
|
|
120
121
|
{#each sections as section, index (section._key)}
|
|
121
122
|
<li class:active={selected?.sectionKey === section._key}>
|
|
122
123
|
<button type="button" class="structure-row" onclick={() => onselect({ docId, path: "", sectionKey: String(section._key) })}>
|
|
123
|
-
<span>{sectionLabel(schema, section._type)}</span>
|
|
124
|
+
<span>{sectionLabel(schema, section._type, $t("fields.section"))}</span>
|
|
124
125
|
{#if devMode}<span class="schema-key">{section._key}</span>{/if}
|
|
125
126
|
</button>
|
|
126
127
|
<div class="structure-actions">
|
|
127
|
-
<button type="button" aria-label={
|
|
128
|
-
<button type="button" aria-label={
|
|
129
|
-
<button type="button" aria-label={
|
|
130
|
-
<button type="button" aria-label={
|
|
128
|
+
<button type="button" aria-label={$t("fields.moveSectionUp", { label: sectionLabel(schema, section._type) })} data-tooltip={$t("fields.moveUp")} disabled={index === 0} onclick={() => setSections(moveItem(sections, index, -1) as Record<string, unknown>[])}>↑</button>
|
|
129
|
+
<button type="button" aria-label={$t("fields.moveSectionDown", { label: sectionLabel(schema, section._type) })} data-tooltip={$t("fields.moveDown")} disabled={index === sections.length - 1} onclick={() => setSections(moveItem(sections, index, 1) as Record<string, unknown>[])}>↓</button>
|
|
130
|
+
<button type="button" aria-label={$t("fields.duplicateSection", { label: sectionLabel(schema, section._type) })} data-tooltip={$t("fields.duplicate")} onclick={() => setSections(duplicateItem(sections, index) as Record<string, unknown>[])}>⧉</button>
|
|
131
|
+
<button type="button" aria-label={$t("fields.removeSection", { label: sectionLabel(schema, section._type) })} data-tooltip={$t("fields.remove")} onclick={() => removeSection(index)}>×</button>
|
|
131
132
|
</div>
|
|
132
133
|
</li>
|
|
133
134
|
{/each}
|
|
134
135
|
</ul>
|
|
135
136
|
<div class="add-section">
|
|
136
|
-
<Select ariaLabel="
|
|
137
|
-
<button type="button" class="outlined" disabled={!addType} onclick={addSection}>
|
|
137
|
+
<Select ariaLabel={$t("fields.sectionTypeToAdd")} value={addType} onchange={next => { addType = String(next); }} options={[{ value: "", label: $t("fields.chooseSection") }, ...allowedTypes.map(type => ({ value: type.name, label: type.title }))]} />
|
|
138
|
+
<button type="button" class="outlined" disabled={!addType} onclick={addSection}>{$t("fields.addSection")}</button>
|
|
138
139
|
</div>
|
|
139
|
-
{#if pending.length}<p class="panel-note" role="status">{
|
|
140
|
+
{#if pending.length}<p class="panel-note" role="status">{$t("fields.pendingSections", { count: pending.length })}</p>{/if}
|
|
140
141
|
</section>
|
|
141
142
|
{/if}
|
|
142
143
|
|
|
143
144
|
{#if selectedSection || (selectedField && selected?.path.includes("."))}
|
|
144
145
|
<section class="section-properties">
|
|
145
|
-
<h3>
|
|
146
|
+
<h3>{$t("fields.sectionProperties")}</h3>
|
|
146
147
|
{#if selectedSection && selected?.sectionKey}
|
|
147
|
-
<p class="selected-path">{selectedField ? `${selectedField.label} · ` : ""}{selectedSection.title}</p>
|
|
148
|
+
<p class="selected-path">{selectedField ? `${selectedField.labelKey ? $t(selectedField.labelKey) : selectedField.label} · ` : ""}{selectedSection.title}</p>
|
|
148
149
|
{#each selectedSection.fields as field (field.key)}
|
|
149
150
|
{@const reference = field.type === "reference" ? sections.find((section) => section._key === selected?.sectionKey)?.[field.key] as { _ref?: string } | undefined : undefined}
|
|
150
151
|
<div data-schema-path={`sections.${selected.sectionKey}.${field.key}`}>
|
|
@@ -158,12 +159,12 @@
|
|
|
158
159
|
onvalue={(next) => onchange(replaceAt(values, `sections.${selected!.sectionKey}.${field.key}`, next))}
|
|
159
160
|
/>
|
|
160
161
|
{#if reference?._ref && documents.some((doc) => doc.id === reference._ref)}
|
|
161
|
-
<button type="button" class="outlined" onclick={() => oneditdocument(reference._ref!)}>
|
|
162
|
+
<button type="button" class="outlined" onclick={() => oneditdocument(reference._ref!)}>{$t("fields.editReference", { label: field.label.toLowerCase() })}</button>
|
|
162
163
|
{/if}
|
|
163
164
|
</div>
|
|
164
165
|
{/each}
|
|
165
166
|
{:else if selectedField && selected}
|
|
166
|
-
<p class="selected-path">{selectedField.label}{selected.sectionKey ? ` · ${sectionLabel(schema, sections.find((section) => section._key === selected.sectionKey)?._type)}` : ""}</p>
|
|
167
|
+
<p class="selected-path">{selectedField.labelKey ? $t(selectedField.labelKey) : selectedField.label}{selected.sectionKey ? ` · ${sectionLabel(schema, sections.find((section) => section._key === selected.sectionKey)?._type, $t("fields.section"))}` : ""}</p>
|
|
167
168
|
<SchemaField
|
|
168
169
|
{schema}
|
|
169
170
|
{documents}
|
|
@@ -181,7 +182,7 @@
|
|
|
181
182
|
{#if seoFields.length}
|
|
182
183
|
<div id="properties-seo" role="tabpanel" aria-labelledby="properties-tab-seo" hidden={tab !== "seo"}>
|
|
183
184
|
<section class="seo-fields">
|
|
184
|
-
<h3>
|
|
185
|
+
<h3>{$t("fields.seo")}</h3>
|
|
185
186
|
{#each seoFields as field (field.key)}
|
|
186
187
|
<div data-schema-path={field.key}>
|
|
187
188
|
<SchemaField {schema} {documents} {devMode} {field} base={field.key} value={values[field.key]} onvalue={(next) => onchange(replaceAt(values, field.key, next))} />
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { t } from "../i18n";
|
|
2
3
|
import { tick } from "svelte";
|
|
3
4
|
import Icon from "./Icon.svelte";
|
|
4
5
|
import type { ICONS } from "./icons";
|
|
@@ -125,7 +126,7 @@
|
|
|
125
126
|
<div class="custom-select" onfocusout={() => { if (open) requestAnimationFrame(() => { if (trigger.ownerDocument.activeElement?.tagName === "IFRAME") close(); }); }}>
|
|
126
127
|
{#if label}<span class="select-label" id={`${id}-label`}>{label}</span>{/if}
|
|
127
128
|
<button bind:this={trigger} type="button" class="select-trigger" class:icon-only={!!icon} role="combobox" aria-label={label ? undefined : ariaLabel} aria-labelledby={label ? `${id}-label` : undefined} aria-haspopup="listbox" aria-expanded={open} aria-controls={`${id}-list`} aria-activedescendant={open && active >= 0 ? `${id}-option-${active}` : undefined} popovertarget={`${id}-list`} data-tooltip={tooltip} data-tooltip-position={tooltipPosition} data-tooltip-align={tooltipAlign} {disabled} onclick={event => { event.preventDefault(); menu.matches(":popover-open") ? close() : void reveal(); }} onkeydown={onkey}>
|
|
128
|
-
{#if icon}<Icon name={icon} size={18} />{:else}<span>{text || (multiple ?
|
|
129
|
+
{#if icon}<Icon name={icon} size={18} />{:else}<span>{text || (multiple ? label ? $t("fields.selectLabel", { label: label.toLowerCase() }) : $t("fields.selectOptions") : $t("fields.choose"))}</span><Icon name="caretDown" size={12} />{/if}
|
|
129
130
|
</button>
|
|
130
131
|
<select hidden aria-hidden="true" tabindex="-1" aria-label={ariaLabel} {multiple} {disabled} {value} onchange={event => { value = multiple ? Array.from(event.currentTarget.selectedOptions, option => option.value) : event.currentTarget.value; onchange(value); }}>
|
|
131
132
|
{#each options as option (option.value)}<option value={option.value} disabled={option.disabled}>{option.label}</option>{/each}
|
|
@@ -137,6 +138,6 @@
|
|
|
137
138
|
<span>{option.label}</span><span class="select-check" aria-hidden="true">{selected.includes(option.value) ? "✓" : ""}</span>
|
|
138
139
|
</div>
|
|
139
140
|
{/each}
|
|
140
|
-
{#if !options.length}<p class="select-empty">
|
|
141
|
+
{#if !options.length}<p class="select-empty">{$t("fields.noOptions")}</p>{/if}
|
|
141
142
|
</div>
|
|
142
143
|
</div>
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { t } from "../i18n";
|
|
2
3
|
let { field, value }: { field: string; value: unknown } = $props();
|
|
3
|
-
const limits = $derived(field === "metaTitle" ? { min: 50, max: 60
|
|
4
|
+
const limits = $derived(field === "metaTitle" ? { min: 50, max: 60 } : { min: 120, max: 160 });
|
|
4
5
|
const count = $derived(typeof value === "string" ? [...value].length : 0);
|
|
5
|
-
const status = $derived(!count ? "
|
|
6
|
+
const status = $derived($t(!count ? "fields.seoNotSet" : count < limits.min ? "fields.seoTooShort" : count > limits.max ? "fields.seoTooLong" : "fields.seoRecommendedLength"));
|
|
6
7
|
const tone = $derived(!count ? "empty" : count > limits.max || count < limits.min / 2 ? "danger" : count < limits.min ? "warning" : "success");
|
|
7
8
|
</script>
|
|
8
9
|
|
|
9
10
|
{#if field === "metaTitle" || field === "description"}
|
|
10
11
|
<div class="seo-length" data-seo-length={field} data-tone={tone}>
|
|
11
|
-
<div class="seo-length-track" role="meter" aria-label={
|
|
12
|
+
<div class="seo-length-track" role="meter" aria-label={$t(field === "metaTitle" ? "fields.seoTitleLength" : "fields.seoDescriptionLength")} aria-valuemin={0} aria-valuemax={Math.max(limits.max, count)} aria-valuenow={count} aria-valuetext={$t("fields.seoLengthDetails", { count, status, min: limits.min, max: limits.max })} data-tooltip={status}>
|
|
12
13
|
<span class="seo-length-fill" style:width={`${Math.min(count / limits.max * 100, 100)}%`}></span>
|
|
13
14
|
</div>
|
|
14
15
|
<div class="seo-length-summary hint">
|
|
15
|
-
<span>
|
|
16
|
+
<span>{$t("fields.seoRecommendedCharacters", { min: limits.min, max: limits.max })}</span>
|
|
16
17
|
<span class="seo-length-count" aria-hidden="true">{count}/{limits.max}</span>
|
|
17
18
|
</div>
|
|
18
19
|
</div>
|
|
19
20
|
{:else if field === "ogImage"}
|
|
20
|
-
<p class="hint">
|
|
21
|
+
<p class="hint">{$t("fields.seoRecommendedImageSize")}</p>
|
|
21
22
|
{/if}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { t, availableLocales, localeNames } from "../i18n";
|
|
2
3
|
import { getKeys, setKeys, STOCK_PROVIDERS } from "./images";
|
|
3
4
|
import type { RigelSettings } from "./settings";
|
|
4
5
|
import type { Manifest } from "../../manifest/types";
|
|
@@ -24,14 +25,14 @@
|
|
|
24
25
|
let keysError = $state("");
|
|
25
26
|
let eraseError = $state("");
|
|
26
27
|
|
|
27
|
-
const panes: { id: PaneId; label: string }[] = [
|
|
28
|
-
{ id: "appearance", label: "
|
|
29
|
-
{ id: "editor", label: "
|
|
30
|
-
{ id: "connection", label: "
|
|
31
|
-
{ id: "media", label: "
|
|
32
|
-
{ id: "language", label: "
|
|
33
|
-
{ id: "advanced", label: "
|
|
34
|
-
];
|
|
28
|
+
const panes: { id: PaneId; label: string }[] = $derived([
|
|
29
|
+
{ id: "appearance", label: $t("shell.appearance") },
|
|
30
|
+
{ id: "editor", label: $t("shell.editor") },
|
|
31
|
+
{ id: "connection", label: $t("shell.connection") },
|
|
32
|
+
{ id: "media", label: $t("shell.media") },
|
|
33
|
+
{ id: "language", label: $t("shell.language") },
|
|
34
|
+
{ id: "advanced", label: $t("shell.advanced") },
|
|
35
|
+
]);
|
|
35
36
|
|
|
36
37
|
const PANE_ICONS = {
|
|
37
38
|
appearance: "palette",
|
|
@@ -47,7 +48,7 @@
|
|
|
47
48
|
setKeys(Object.fromEntries(providerIds.map((id) => [id, keys[id]?.trim() || undefined])));
|
|
48
49
|
keysError = "";
|
|
49
50
|
} catch {
|
|
50
|
-
keysError = "
|
|
51
|
+
keysError = $t("shell.apiKeysStorageError");
|
|
51
52
|
}
|
|
52
53
|
});
|
|
53
54
|
</script>
|
|
@@ -58,10 +59,10 @@
|
|
|
58
59
|
onclick={onclose}
|
|
59
60
|
onkeydown={(e) => e.key === "Escape" && onclose()}
|
|
60
61
|
>
|
|
61
|
-
<div class="rigel-modal settings-modal" role="dialog" aria-label="
|
|
62
|
+
<div class="rigel-modal settings-modal" role="dialog" aria-label={$t("shell.settings")} onclick={(e) => e.stopPropagation()}>
|
|
62
63
|
<div class="settings-head">
|
|
63
|
-
<h2>
|
|
64
|
-
<button class="close" onclick={onclose} aria-label="
|
|
64
|
+
<h2>{$t("shell.settings")}</h2>
|
|
65
|
+
<button class="close" onclick={onclose} aria-label={$t("shell.close")}>
|
|
65
66
|
<Icon name="x" size={14} weight="bold" />
|
|
66
67
|
</button>
|
|
67
68
|
</div>
|
|
@@ -80,67 +81,67 @@
|
|
|
80
81
|
|
|
81
82
|
<div class="settings-pane">
|
|
82
83
|
{#if active === "appearance"}
|
|
83
|
-
<h3>
|
|
84
|
+
<h3>{$t("shell.appearance")}</h3>
|
|
84
85
|
<fieldset>
|
|
85
|
-
<legend>
|
|
86
|
+
<legend>{$t("shell.theme")}</legend>
|
|
86
87
|
<div class="theme-options">
|
|
87
|
-
{#each ["system", "light", "dark"] as const as
|
|
88
|
+
{#each ["system", "light", "dark"] as const as theme}
|
|
88
89
|
<label class="theme-option">
|
|
89
|
-
<input type="radio" bind:group={s.theme} value={
|
|
90
|
-
<span>{t
|
|
90
|
+
<input type="radio" bind:group={s.theme} value={theme} />
|
|
91
|
+
<span>{$t({ system: "shell.themeSystem", light: "shell.themeLight", dark: "shell.themeDark" }[theme])}</span>
|
|
91
92
|
</label>
|
|
92
93
|
{/each}
|
|
93
94
|
</div>
|
|
94
95
|
</fieldset>
|
|
95
96
|
<fieldset class="accessibility-options">
|
|
96
|
-
<legend>
|
|
97
|
+
<legend>{$t("shell.accessibility")}</legend>
|
|
97
98
|
<label class="inline">
|
|
98
99
|
<input type="checkbox" role="switch" class="switch" bind:checked={s.highContrast} aria-describedby="high-contrast-hint" />
|
|
99
|
-
<span>
|
|
100
|
+
<span>{$t("shell.highContrastMode")}</span>
|
|
100
101
|
</label>
|
|
101
|
-
<p class="hint" id="high-contrast-hint">
|
|
102
|
+
<p class="hint" id="high-contrast-hint">{$t("shell.highContrastHint")}</p>
|
|
102
103
|
</fieldset>
|
|
103
104
|
|
|
104
105
|
{:else if active === "editor"}
|
|
105
|
-
<h3>
|
|
106
|
+
<h3>{$t("shell.editorExperience")}</h3>
|
|
106
107
|
<fieldset>
|
|
107
|
-
<legend>
|
|
108
|
-
<label class="radio"><input type="radio" bind:group={s.userPref.editorExperience} value="sidebar" /><span>
|
|
109
|
-
<label class="radio"><input type="radio" bind:group={s.userPref.editorExperience} value="inline" /><span>
|
|
108
|
+
<legend>{$t("shell.articleBodyEditing")}</legend>
|
|
109
|
+
<label class="radio"><input type="radio" bind:group={s.userPref.editorExperience} value="sidebar" /><span>{$t("shell.sidebar")}</span></label>
|
|
110
|
+
<label class="radio"><input type="radio" bind:group={s.userPref.editorExperience} value="inline" /><span>{$t("shell.inlinePreview")}</span></label>
|
|
110
111
|
</fieldset>
|
|
111
|
-
<p class="hint">
|
|
112
|
+
<p class="hint">{$t("shell.editorExperienceHint")}</p>
|
|
112
113
|
{:else if active === "connection"}
|
|
113
|
-
<h3>
|
|
114
|
+
<h3>{$t("shell.connection")}</h3>
|
|
114
115
|
{#if github}
|
|
115
116
|
<dl class="connection-details">
|
|
116
|
-
<dt>
|
|
117
|
-
<dd>{#if user}{user.name ?? user.login}<small>@{user.login}</small>{:else}
|
|
118
|
-
<dt>
|
|
117
|
+
<dt>{$t("shell.githubAccount")}</dt>
|
|
118
|
+
<dd>{#if user}{user.name ?? user.login}<small>@{user.login}</small>{:else}{$t("shell.noGithubAccount")}{/if}</dd>
|
|
119
|
+
<dt>{$t("shell.repository")}</dt>
|
|
119
120
|
<dd>{github.repo}</dd>
|
|
120
|
-
<dt>
|
|
121
|
+
<dt>{$t("shell.editingBranch")}</dt>
|
|
121
122
|
<dd>{github.branch}</dd>
|
|
122
123
|
{#if github.publishBranch}
|
|
123
|
-
<dt>
|
|
124
|
+
<dt>{$t("shell.publishingBranch")}</dt>
|
|
124
125
|
<dd>{github.publishBranch}</dd>
|
|
125
126
|
{/if}
|
|
126
127
|
</dl>
|
|
127
128
|
{:else}
|
|
128
|
-
<p>
|
|
129
|
-
<p class="hint">
|
|
129
|
+
<p>{$t("shell.localMode")}</p>
|
|
130
|
+
<p class="hint">{$t("shell.localModeHint")}</p>
|
|
130
131
|
{/if}
|
|
131
132
|
{:else if active === "media"}
|
|
132
133
|
|
|
133
134
|
<section class="media-settings-section">
|
|
134
|
-
<h3>
|
|
135
|
-
<p class="hint">
|
|
135
|
+
<h3>{$t("shell.stockPhotoApiKeys")}</h3>
|
|
136
|
+
<p class="hint">{$t("shell.stockPhotoApiKeysHint")}</p>
|
|
136
137
|
{#each providerIds as id (id)}
|
|
137
138
|
{@const provider = STOCK_PROVIDERS[id]}
|
|
138
|
-
{@const keyLabel =
|
|
139
|
+
{@const keyLabel = $t(id === "unsplash" ? "shell.providerAccessKey" : "shell.providerApiKey", { name: provider.label })}
|
|
139
140
|
{@const invalid = !!keys[id]?.trim() && !new RegExp(`^${provider.pattern}$`).test(keys[id]!.trim())}
|
|
140
141
|
<div class="stock-key">
|
|
141
142
|
<div class="stock-key-head">
|
|
142
143
|
<h4><label for={`rigel-key-${id}`}>{keyLabel}</label></h4>
|
|
143
|
-
<a href={provider.keyURL} target="_blank" rel="noopener noreferrer">
|
|
144
|
+
<a href={provider.keyURL} target="_blank" rel="noopener noreferrer">{$t(id === "unsplash" ? "shell.getAccessKey" : "shell.getApiKey")}</a>
|
|
144
145
|
</div>
|
|
145
146
|
<div class="secret-input">
|
|
146
147
|
<input
|
|
@@ -154,47 +155,46 @@
|
|
|
154
155
|
aria-invalid={invalid}
|
|
155
156
|
aria-describedby={invalid ? `rigel-key-${id}-error` : undefined}
|
|
156
157
|
/>
|
|
157
|
-
<button type="button" aria-label={
|
|
158
|
+
<button type="button" aria-label={$t(revealed[id] ? "shell.hideKey" : "shell.showKey", { name: keyLabel })} aria-controls={`rigel-key-${id}`} aria-pressed={revealed[id] ?? false} onclick={() => revealed[id] = !revealed[id]}>
|
|
158
159
|
<Icon name={revealed[id] ? "eyeSlash" : "eye"} size={18} />
|
|
159
160
|
</button>
|
|
160
161
|
</div>
|
|
161
|
-
{#if invalid}<p class="error" id={`rigel-key-${id}-error`}>
|
|
162
|
+
{#if invalid}<p class="error" id={`rigel-key-${id}-error`}>{$t("shell.keyFormatError", { name: keyLabel })}</p>{/if}
|
|
162
163
|
</div>
|
|
163
164
|
{/each}
|
|
164
165
|
|
|
165
166
|
</section>
|
|
166
167
|
|
|
167
168
|
{:else if active === "language"}
|
|
168
|
-
<h3>
|
|
169
|
+
<h3>{$t("shell.language")}</h3>
|
|
169
170
|
<fieldset>
|
|
170
|
-
<legend>
|
|
171
|
-
{#each
|
|
171
|
+
<legend>{$t("shell.editorInterfaceLanguage")}</legend>
|
|
172
|
+
{#each availableLocales as lang}
|
|
172
173
|
<label class="radio">
|
|
173
174
|
<input type="radio" bind:group={s.language} value={lang} />
|
|
174
|
-
<span>{
|
|
175
|
+
<span>{localeNames[lang]}</span>
|
|
175
176
|
</label>
|
|
176
177
|
{/each}
|
|
177
178
|
</fieldset>
|
|
178
|
-
<p class="hint">English is the only shipped locale today.</p>
|
|
179
179
|
|
|
180
180
|
{:else}
|
|
181
|
-
<h3>
|
|
181
|
+
<h3>{$t("shell.developerMode")}</h3>
|
|
182
182
|
<label class="inline">
|
|
183
183
|
<input type="checkbox" role="switch" class="switch" bind:checked={s.devMode} />
|
|
184
|
-
<span>
|
|
184
|
+
<span>{$t("shell.enableDevMode")}</span>
|
|
185
185
|
</label>
|
|
186
|
-
<p class="hint">
|
|
186
|
+
<p class="hint">{$t("shell.devModeHint")}</p>
|
|
187
187
|
<div class="clear-data">
|
|
188
|
-
<h3>
|
|
189
|
-
<p class="hint">
|
|
190
|
-
<button type="button" class="danger-btn" disabled>
|
|
191
|
-
<p class="hint">
|
|
188
|
+
<h3>{$t("shell.clearData")}</h3>
|
|
189
|
+
<p class="hint">{$t("shell.fileCacheHint")}</p>
|
|
190
|
+
<button type="button" class="danger-btn" disabled>{$t("shell.clearFileCache")}</button>
|
|
191
|
+
<p class="hint">{$t("shell.eraseAllDataHint")}</p>
|
|
192
192
|
<button type="button" class="danger-btn" disabled={saving} onclick={() => {
|
|
193
193
|
eraseError = "";
|
|
194
194
|
try { onerase(); }
|
|
195
|
-
catch { eraseError = "
|
|
196
|
-
}}>
|
|
197
|
-
{#if saving}<p class="hint" role="status">
|
|
195
|
+
catch { eraseError = $t("shell.eraseDataError"); }
|
|
196
|
+
}}>{$t("shell.eraseAllData")}</button>
|
|
197
|
+
{#if saving}<p class="hint" role="status">{$t("shell.waitForSave")}</p>{/if}
|
|
198
198
|
{#if eraseError}<p class="error" role="alert">{eraseError}</p>{/if}
|
|
199
199
|
</div>
|
|
200
200
|
{/if}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { t } from "../i18n";
|
|
2
3
|
let { onclose }: { onclose: () => void } = $props();
|
|
3
4
|
|
|
4
|
-
const rows: [string, string][] = [
|
|
5
|
-
["
|
|
6
|
-
["
|
|
7
|
-
];
|
|
5
|
+
const rows: [string, string][] = $derived([
|
|
6
|
+
[$t("shell.search"), $t("shell.searchShortcut")],
|
|
7
|
+
[$t("shell.closeDialog"), $t("shell.escapeKey")],
|
|
8
|
+
]);
|
|
8
9
|
</script>
|
|
9
10
|
|
|
10
11
|
<div
|
|
@@ -13,8 +14,8 @@
|
|
|
13
14
|
onclick={onclose}
|
|
14
15
|
onkeydown={(e) => e.key === "Escape" && onclose()}
|
|
15
16
|
>
|
|
16
|
-
<div class="rigel-modal" role="dialog" aria-label="
|
|
17
|
-
<h2>
|
|
17
|
+
<div class="rigel-modal" role="dialog" aria-label={$t("shell.keyboardShortcuts")} onclick={(e) => e.stopPropagation()}>
|
|
18
|
+
<h2>{$t("shell.keyboardShortcuts")}</h2>
|
|
18
19
|
<table class="keys">
|
|
19
20
|
<tbody>
|
|
20
21
|
{#each rows as [action, key]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { t } from "../i18n";
|
|
2
3
|
import { untrack } from "svelte";
|
|
3
4
|
import Icon from "./Icon.svelte";
|
|
4
5
|
import { patchPreview, previewUrl, type PreviewConfig, type PreviewTarget } from "../../preview";
|
|
@@ -25,27 +26,27 @@
|
|
|
25
26
|
try {
|
|
26
27
|
const doc = frame?.contentDocument;
|
|
27
28
|
doc?.addEventListener("contextmenu", event => { if (!devMode) event.preventDefault(); }, true);
|
|
28
|
-
if (!doc) throw new Error("
|
|
29
|
-
if (!doc.querySelector('[data-rigel-text], [data-rigel-richtext], [data-rigel-image]')) throw new Error("
|
|
29
|
+
if (!doc) throw new Error($t("richtext.preparedPageUnavailable"));
|
|
30
|
+
if (!doc.querySelector('[data-rigel-text], [data-rigel-richtext], [data-rigel-image]')) throw new Error($t("richtext.noPreparedAnchors"));
|
|
30
31
|
doc.addEventListener("click", event => event.preventDefault(), true);
|
|
31
32
|
doc.addEventListener("submit", event => event.preventDefault(), true);
|
|
32
33
|
loaded = true;
|
|
33
34
|
patch();
|
|
34
|
-
} catch (cause) { error = cause instanceof Error ? cause.message : "
|
|
35
|
+
} catch (cause) { error = cause instanceof Error ? cause.message : $t("richtext.previewUnavailable"); }
|
|
35
36
|
}
|
|
36
37
|
$effect(() => { JSON.stringify(values); void body; patch(); });
|
|
37
38
|
</script>
|
|
38
39
|
|
|
39
|
-
<div class="site-preview" aria-label="
|
|
40
|
+
<div class="site-preview" aria-label={$t("richtext.websitePreview")}>
|
|
40
41
|
<div class="preview-heading">
|
|
41
|
-
<strong>
|
|
42
|
-
{#if websitePath}<a class="icon-btn visual-live" href={websitePath} target="_blank" rel="noopener noreferrer" aria-label="
|
|
42
|
+
<strong>{$t("richtext.websitePreview")}</strong>
|
|
43
|
+
{#if websitePath}<a class="icon-btn visual-live" href={websitePath} target="_blank" rel="noopener noreferrer" aria-label={$t("richtext.previewOnWebsite")} data-tooltip={$t("richtext.previewOnWebsite")}><Icon name="external" size={18} /></a>{/if}
|
|
43
44
|
</div>
|
|
44
45
|
{#if error}<p role="status" class="preview-error">{error}</p>{/if}
|
|
45
46
|
{#if path}
|
|
46
|
-
{#key key}<PreviewFrame bind:frame url={path} title="
|
|
47
|
-
<small>
|
|
47
|
+
{#key key}<PreviewFrame bind:frame url={path} title={$t("richtext.liveDraftPreview")} {onload} onloading={() => { loaded = false; error = ""; }} onerror={(message) => { loaded = false; error = message; }} />{/key}
|
|
48
|
+
<small>{$t("richtext.preparedTemplateHint")}</small>
|
|
48
49
|
{:else}
|
|
49
|
-
<p role="status">{target.kind === "menu" ? "
|
|
50
|
+
<p role="status">{target.kind === "menu" ? $t("richtext.menuPreviewUnsupported") : $t("richtext.noContentPreview")}</p>
|
|
50
51
|
{/if}
|
|
51
52
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { t } from "../i18n";
|
|
2
3
|
import Icon from "./Icon.svelte";
|
|
3
4
|
import SidebarIcon from "./SidebarIcon.svelte";
|
|
4
5
|
import { tick } from "svelte";
|
|
@@ -46,34 +47,34 @@
|
|
|
46
47
|
for (const p of manifest.pages) {
|
|
47
48
|
all.push({
|
|
48
49
|
label: p.label,
|
|
49
|
-
hint: "
|
|
50
|
+
hint: $t("shell.page"),
|
|
50
51
|
action: () => onselect({ kind: "page", slug: p.slug }),
|
|
51
52
|
});
|
|
52
53
|
}
|
|
53
54
|
for (const doc of manifest.documents ?? []) {
|
|
54
55
|
all.push({
|
|
55
56
|
label: String(doc.data.title ?? doc.data.name ?? doc.slug),
|
|
56
|
-
hint:
|
|
57
|
+
hint: $t("shell.searchItemHint", { name: doc.type, slug: doc.slug }),
|
|
57
58
|
action: () => onselect({ kind: "document", type: doc.type, id: doc.id }),
|
|
58
59
|
});
|
|
59
60
|
}
|
|
60
61
|
for (const c of manifest.collections ?? []) {
|
|
61
62
|
all.push({
|
|
62
63
|
label: c.label,
|
|
63
|
-
hint: "
|
|
64
|
+
hint: $t("shell.collection"),
|
|
64
65
|
action: () => onselect({ kind: "collection", name: c.name }),
|
|
65
66
|
});
|
|
66
67
|
for (const po of c.posts) {
|
|
67
68
|
all.push({
|
|
68
69
|
label: String(po.frontmatter.title ?? po.slug),
|
|
69
|
-
hint:
|
|
70
|
+
hint: $t("shell.searchItemHint", { name: c.label, slug: po.slug }),
|
|
70
71
|
action: () => onselect({ kind: "post", collection: c.name, slug: po.slug }),
|
|
71
72
|
});
|
|
72
73
|
}
|
|
73
74
|
}
|
|
74
75
|
if (manifest.menus) {
|
|
75
|
-
all.push({ label: "
|
|
76
|
-
for (const m of manifest.menus.entries) all.push({ label: m.title, hint:
|
|
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 }) });
|
|
77
78
|
}
|
|
78
79
|
if (!q) return all.slice(0, 12);
|
|
79
80
|
return all.filter((h) => h.label.toLowerCase().includes(q) || h.hint.toLowerCase().includes(q)).slice(0, 12);
|
|
@@ -112,9 +113,9 @@
|
|
|
112
113
|
|
|
113
114
|
<svelte:window onkeydown={onKeydown} />
|
|
114
115
|
|
|
115
|
-
<button class="search-trigger" aria-label="
|
|
116
|
+
<button class="search-trigger" aria-label={$t("shell.searchContent")} data-tooltip={$t("shell.searchContentTooltip")} onclick={() => { active = -1; open = true; }}>
|
|
116
117
|
<SidebarIcon name="magnifying-glass" />
|
|
117
|
-
<span class="sidebar-label">
|
|
118
|
+
<span class="sidebar-label">{$t("shell.searchPlaceholder")}</span>
|
|
118
119
|
<kbd>⌘K</kbd>
|
|
119
120
|
</button>
|
|
120
121
|
|
|
@@ -125,7 +126,7 @@
|
|
|
125
126
|
onclick={() => (open = false)}
|
|
126
127
|
onkeydown={(e) => e.key === "Escape" && (open = false)}
|
|
127
128
|
>
|
|
128
|
-
<div class="palette" role="dialog" aria-label="
|
|
129
|
+
<div class="palette" role="dialog" aria-label={$t("shell.searchContent")} onclick={(e) => e.stopPropagation()}>
|
|
129
130
|
<div class="palette-search">
|
|
130
131
|
<Icon name="magnifier" size={20} />
|
|
131
132
|
<input
|
|
@@ -135,15 +136,15 @@
|
|
|
135
136
|
aria-controls="rigel-search-results"
|
|
136
137
|
aria-autocomplete="list"
|
|
137
138
|
aria-activedescendant={active >= 0 && hits[active] ? `rigel-search-hit-${active}` : undefined}
|
|
138
|
-
aria-label="
|
|
139
|
-
placeholder="
|
|
139
|
+
aria-label={$t("shell.searchPagesCollectionsPosts")}
|
|
140
|
+
placeholder={$t("shell.searchPagesCollectionsPostsPlaceholder")}
|
|
140
141
|
bind:value={query}
|
|
141
142
|
oninput={() => active = -1}
|
|
142
143
|
bind:this={inputEl}
|
|
143
144
|
/>
|
|
144
|
-
<kbd>
|
|
145
|
+
<kbd>{$t("shell.escapeKeyLowercase")}</kbd>
|
|
145
146
|
</div>
|
|
146
|
-
<ul id="rigel-search-results" role="listbox" aria-label="
|
|
147
|
+
<ul id="rigel-search-results" role="listbox" aria-label={$t("shell.searchResults")}>
|
|
147
148
|
{#each hits as h, i (h.hint + h.label)}
|
|
148
149
|
<li role="presentation">
|
|
149
150
|
<button id={`rigel-search-hit-${i}`} role="option" aria-selected={active === i} onfocus={() => active = i} onclick={() => trigger(h)}>
|
|
@@ -152,13 +153,13 @@
|
|
|
152
153
|
</button>
|
|
153
154
|
</li>
|
|
154
155
|
{:else}
|
|
155
|
-
<li class="empty" role="presentation">
|
|
156
|
+
<li class="empty" role="presentation">{$t("shell.noResults")}</li>
|
|
156
157
|
{/each}
|
|
157
158
|
</ul>
|
|
158
159
|
<div class="palette-footer">
|
|
159
|
-
<span><kbd>↑</kbd><kbd>↓</kbd>
|
|
160
|
-
<span><kbd>↵</kbd>
|
|
161
|
-
<span><kbd>
|
|
160
|
+
<span><kbd>↑</kbd><kbd>↓</kbd> {$t("shell.searchNavigate")}</span>
|
|
161
|
+
<span><kbd>↵</kbd> {$t("shell.searchOpen")}</span>
|
|
162
|
+
<span><kbd>{$t("shell.escapeKeyLowercase")}</kbd> {$t("shell.searchClose")}</span>
|
|
162
163
|
</div>
|
|
163
164
|
</div>
|
|
164
165
|
</div>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { t } from "../i18n";
|
|
2
3
|
import Icon from "./Icon.svelte";
|
|
3
4
|
let { busy, error, onsave, ondiscard, oncancel }: { busy: boolean; error: string; onsave: () => void; ondiscard: () => void; oncancel: () => void } = $props();
|
|
4
5
|
let dialog: HTMLDialogElement;
|
|
@@ -13,15 +14,15 @@
|
|
|
13
14
|
<dialog bind:this={dialog} class="rigel rigel-modal-backdrop unsaved-dialog" aria-labelledby="unsaved-title" aria-describedby="unsaved-description" oncancel={(event) => { event.preventDefault(); if (!busy) oncancel(); }}>
|
|
14
15
|
<div class="rigel-modal">
|
|
15
16
|
<div class="unsaved-head">
|
|
16
|
-
<h2 id="unsaved-title">
|
|
17
|
-
<button type="button" class="icon-btn" aria-label="
|
|
17
|
+
<h2 id="unsaved-title">{$t("connection.unsavedTitle")}</h2>
|
|
18
|
+
<button type="button" class="icon-btn" aria-label={$t("connection.keepEditing")} disabled={busy} onclick={oncancel}><Icon name="x" size={16} /></button>
|
|
18
19
|
</div>
|
|
19
|
-
<p id="unsaved-description">
|
|
20
|
+
<p id="unsaved-description">{$t("connection.unsavedDescription")}</p>
|
|
20
21
|
{#if error}<p role="alert">{error}</p>{/if}
|
|
21
22
|
<div class="unsaved-actions">
|
|
22
|
-
<button type="button" bind:this={cancel} disabled={busy} onclick={oncancel}>
|
|
23
|
-
<button type="button" disabled={busy} onclick={ondiscard}>
|
|
24
|
-
<button type="button" class="primary" disabled={busy} onclick={onsave}>{busy ?
|
|
23
|
+
<button type="button" bind:this={cancel} disabled={busy} onclick={oncancel}>{$t("connection.keepEditing")}</button>
|
|
24
|
+
<button type="button" disabled={busy} onclick={ondiscard}>{$t("connection.discardChanges")}</button>
|
|
25
|
+
<button type="button" class="primary" disabled={busy} onclick={onsave}>{busy ? $t("connection.saving") : $t("connection.saveAndLeave")}</button>
|
|
25
26
|
</div>
|
|
26
27
|
</div>
|
|
27
28
|
</dialog>
|