includio-cms 0.13.0 → 0.13.2
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/CHANGELOG.md +36 -0
- package/ROADMAP.md +24 -0
- package/dist/admin/api/handler.js +2 -0
- package/dist/admin/api/replace.js +2 -1
- package/dist/admin/api/rest/routes/upload.js +2 -1
- package/dist/admin/api/upload-limit.d.ts +2 -0
- package/dist/admin/api/upload-limit.js +7 -0
- package/dist/admin/api/upload.js +2 -1
- package/dist/admin/client/collection/collection-entries.svelte +58 -11
- package/dist/admin/client/users/users-page.svelte +5 -6
- package/dist/admin/client/users/users-page.svelte.d.ts +1 -4
- package/dist/admin/components/fields/block-picker-modal.svelte +13 -4
- package/dist/admin/components/fields/blocks-field.svelte +31 -9
- package/dist/admin/components/fields/simple-array-field.svelte +22 -11
- package/dist/admin/components/layout/layout-renderer.svelte +10 -4
- package/dist/admin/components/media/file-preview.svelte +10 -1
- package/dist/admin/components/media/file-upload.svelte +66 -9
- package/dist/admin/components/media/files-list.svelte +12 -3
- package/dist/admin/components/media/media-selector.svelte +11 -5
- package/dist/admin/components/tiptap/FigureNodeView.svelte +15 -10
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +32 -1
- package/dist/admin/components/tiptap/SlashCommandPopup.svelte +8 -3
- package/dist/admin/components/tiptap/editor-toolbar.svelte +28 -23
- package/dist/admin/components/tiptap/image-dialog.svelte +12 -7
- package/dist/admin/components/tiptap/lang.d.ts +77 -0
- package/dist/admin/components/tiptap/lang.js +170 -0
- package/dist/admin/components/tiptap/link-dialog.svelte +22 -18
- package/dist/admin/components/tiptap/slash-command.js +26 -22
- package/dist/admin/components/tiptap/table-dialog.svelte +9 -4
- package/dist/admin/components/tiptap/video-dialog.svelte +6 -1
- package/dist/admin/remote/email.remote.d.ts +1 -0
- package/dist/admin/remote/email.remote.js +5 -0
- package/dist/admin/remote/entry.remote.d.ts +1 -0
- package/dist/admin/remote/entry.remote.js +6 -4
- package/dist/admin/remote/index.d.ts +1 -0
- package/dist/admin/remote/index.js +1 -0
- package/dist/admin/remote/reorder.d.ts +1 -0
- package/dist/admin/remote/reorder.js +33 -0
- package/dist/core/server/entries/operations/get.js +15 -3
- package/dist/core/server/fields/utils/imageStyles.js +7 -3
- package/dist/core/server/generator/fields.js +2 -2
- package/dist/core/server/generator/generator.js +4 -2
- package/dist/core/server/media/styles/operations/createMediaStyle.d.ts +2 -2
- package/dist/core/server/media/styles/operations/createMediaStyle.js +5 -3
- package/dist/core/server/media/styles/operations/generateDefaultStyles.js +33 -6
- package/dist/core/server/media/styles/operations/getImageStyle.d.ts +1 -0
- package/dist/core/server/media/styles/operations/getImageStyle.js +3 -0
- package/dist/core/server/media/styles/sharp/generateImageStyle.d.ts +2 -1
- package/dist/core/server/media/styles/sharp/generateImageStyle.js +5 -3
- package/dist/core/server/media/uploadLimit.d.ts +2 -0
- package/dist/core/server/media/uploadLimit.js +26 -0
- package/dist/types/entries.d.ts +1 -0
- package/dist/types/layout.d.ts +0 -1
- package/dist/updates/0.13.1/index.d.ts +2 -0
- package/dist/updates/0.13.1/index.js +20 -0
- package/dist/updates/0.13.2/index.d.ts +2 -0
- package/dist/updates/0.13.2/index.js +20 -0
- package/dist/updates/index.js +3 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { getRemotes } from '../../context/remotes.js';
|
|
3
3
|
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
4
4
|
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
5
|
+
import { toast } from 'svelte-sonner';
|
|
5
6
|
import Upload from '@tabler/icons-svelte/icons/upload';
|
|
6
7
|
import X from '@tabler/icons-svelte/icons/x';
|
|
7
8
|
|
|
@@ -9,21 +10,36 @@
|
|
|
9
10
|
const interfaceLanguage = useInterfaceLanguage();
|
|
10
11
|
const lang: Record<
|
|
11
12
|
InterfaceLanguage,
|
|
12
|
-
{
|
|
13
|
+
{
|
|
14
|
+
addFiles: string;
|
|
15
|
+
dropFiles: string;
|
|
16
|
+
dropHint: string;
|
|
17
|
+
uploading: string;
|
|
18
|
+
uploadComplete: string;
|
|
19
|
+
fileTooLarge: (maxMB: number) => string;
|
|
20
|
+
uploadError: string;
|
|
21
|
+
skippedFiles: (count: number) => string;
|
|
22
|
+
}
|
|
13
23
|
> = {
|
|
14
24
|
pl: {
|
|
15
25
|
addFiles: 'Prześlij pliki',
|
|
16
26
|
dropFiles: 'Upuść pliki tutaj',
|
|
17
27
|
dropHint: 'Przeciągnij pliki tutaj',
|
|
18
28
|
uploading: 'Wysyłanie...',
|
|
19
|
-
uploadComplete: 'Ukończono'
|
|
29
|
+
uploadComplete: 'Ukończono',
|
|
30
|
+
fileTooLarge: (maxMB) => `Plik za duży (max ${maxMB} MB)`,
|
|
31
|
+
uploadError: 'Błąd przesyłania',
|
|
32
|
+
skippedFiles: (count) => `Pominięto ${count} plik(ów) przekraczających limit`
|
|
20
33
|
},
|
|
21
34
|
en: {
|
|
22
35
|
addFiles: 'Upload files',
|
|
23
36
|
dropFiles: 'Drop files here',
|
|
24
37
|
dropHint: 'Drag files here',
|
|
25
38
|
uploading: 'Uploading...',
|
|
26
|
-
uploadComplete: 'Complete'
|
|
39
|
+
uploadComplete: 'Complete',
|
|
40
|
+
fileTooLarge: (maxMB) => `File too large (max ${maxMB} MB)`,
|
|
41
|
+
uploadError: 'Upload error',
|
|
42
|
+
skippedFiles: (count) => `Skipped ${count} file(s) exceeding the limit`
|
|
27
43
|
}
|
|
28
44
|
};
|
|
29
45
|
|
|
@@ -39,7 +55,17 @@
|
|
|
39
55
|
|
|
40
56
|
let inputElement: HTMLInputElement;
|
|
41
57
|
let isDragging = $state(false);
|
|
42
|
-
let uploadProgress = $state<{ name: string; progress: number; complete: boolean }[]>([]);
|
|
58
|
+
let uploadProgress = $state<{ name: string; progress: number; complete: boolean; error?: string }[]>([]);
|
|
59
|
+
let maxUploadSize = $state<number>(50 * 1024 * 1024);
|
|
60
|
+
|
|
61
|
+
$effect(() => {
|
|
62
|
+
fetch('/admin/api/upload-limit')
|
|
63
|
+
.then((r) => r.json())
|
|
64
|
+
.then((data) => {
|
|
65
|
+
if (data.maxUploadSize) maxUploadSize = data.maxUploadSize;
|
|
66
|
+
})
|
|
67
|
+
.catch(() => {});
|
|
68
|
+
});
|
|
43
69
|
|
|
44
70
|
async function uploadFile(file: File, index: number) {
|
|
45
71
|
const form = new FormData();
|
|
@@ -58,13 +84,26 @@
|
|
|
58
84
|
};
|
|
59
85
|
|
|
60
86
|
xhr.onload = () => {
|
|
61
|
-
|
|
62
|
-
|
|
87
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
88
|
+
uploadProgress[index].complete = true;
|
|
89
|
+
uploadProgress[index].progress = 100;
|
|
90
|
+
} else {
|
|
91
|
+
const t = lang[interfaceLanguage.current];
|
|
92
|
+
const error =
|
|
93
|
+
xhr.status === 413
|
|
94
|
+
? t.fileTooLarge(Math.round(maxUploadSize / 1024 / 1024))
|
|
95
|
+
: t.uploadError;
|
|
96
|
+
uploadProgress[index].progress = -1;
|
|
97
|
+
uploadProgress[index].error = error;
|
|
98
|
+
toast.error(error, { description: file.name });
|
|
99
|
+
}
|
|
63
100
|
resolve();
|
|
64
101
|
};
|
|
65
102
|
|
|
66
103
|
xhr.onerror = () => {
|
|
67
104
|
uploadProgress[index].progress = -1;
|
|
105
|
+
uploadProgress[index].error = lang[interfaceLanguage.current].uploadError;
|
|
106
|
+
toast.error(lang[interfaceLanguage.current].uploadError, { description: file.name });
|
|
68
107
|
resolve();
|
|
69
108
|
};
|
|
70
109
|
|
|
@@ -76,9 +115,27 @@
|
|
|
76
115
|
async function handleFiles(files: File[]) {
|
|
77
116
|
if (!files.length) return;
|
|
78
117
|
|
|
79
|
-
|
|
118
|
+
const maxMB = Math.round(maxUploadSize / 1024 / 1024);
|
|
119
|
+
uploadProgress = files.map((f) => ({
|
|
120
|
+
name: f.name,
|
|
121
|
+
progress: f.size > maxUploadSize ? -1 : 0,
|
|
122
|
+
complete: false,
|
|
123
|
+
error: f.size > maxUploadSize ? lang[interfaceLanguage.current].fileTooLarge(maxMB) : undefined
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
const validFiles = files
|
|
127
|
+
.map((file, i) => ({ file, i }))
|
|
128
|
+
.filter(({ file }) => file.size <= maxUploadSize);
|
|
129
|
+
|
|
130
|
+
const rejectedCount = files.length - validFiles.length;
|
|
131
|
+
if (rejectedCount > 0) {
|
|
132
|
+
const t = lang[interfaceLanguage.current];
|
|
133
|
+
toast.error(t.fileTooLarge(maxMB), {
|
|
134
|
+
description: t.skippedFiles(rejectedCount)
|
|
135
|
+
});
|
|
136
|
+
}
|
|
80
137
|
|
|
81
|
-
await Promise.all(
|
|
138
|
+
await Promise.all(validFiles.map(({ file, i }) => uploadFile(file, i)));
|
|
82
139
|
|
|
83
140
|
remotes.getMedia().refresh();
|
|
84
141
|
|
|
@@ -210,7 +267,7 @@
|
|
|
210
267
|
{#if item.complete}
|
|
211
268
|
{lang[interfaceLanguage.current].uploadComplete}
|
|
212
269
|
{:else if item.progress === -1}
|
|
213
|
-
|
|
270
|
+
{item.error || lang[interfaceLanguage.current].uploadError}
|
|
214
271
|
{:else}
|
|
215
272
|
{item.progress}%
|
|
216
273
|
{/if}
|
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
import Music from '@tabler/icons-svelte/icons/music';
|
|
10
10
|
import Pdf from '@tabler/icons-svelte/icons/pdf';
|
|
11
11
|
import File from '@tabler/icons-svelte/icons/file';
|
|
12
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
13
|
+
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
14
|
+
|
|
15
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
16
|
+
const filesLang: Record<InterfaceLanguage, { noFiles: string; uploadOrFilter: string; tags: string }> = {
|
|
17
|
+
pl: { noFiles: 'Brak plików', uploadOrFilter: 'Prześlij pliki lub zmień filtry', tags: 'Tagi' },
|
|
18
|
+
en: { noFiles: 'No files', uploadOrFilter: 'Upload files or change filters', tags: 'Tags' }
|
|
19
|
+
};
|
|
20
|
+
const ft = $derived(filesLang[interfaceLanguage.current]);
|
|
12
21
|
|
|
13
22
|
type Props = {
|
|
14
23
|
files: MediaFile[];
|
|
@@ -94,8 +103,8 @@
|
|
|
94
103
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
|
95
104
|
</svg>
|
|
96
105
|
</div>
|
|
97
|
-
<p class="text-sm font-medium text-foreground">
|
|
98
|
-
<p class="text-xs text-muted-foreground mt-1">
|
|
106
|
+
<p class="text-sm font-medium text-foreground">{ft.noFiles}</p>
|
|
107
|
+
<p class="text-xs text-muted-foreground mt-1">{ft.uploadOrFilter}</p>
|
|
99
108
|
</div>
|
|
100
109
|
{:else}
|
|
101
110
|
{#each sortedFiles as file, i (file.id)}
|
|
@@ -146,7 +155,7 @@
|
|
|
146
155
|
<span class="text-[11px] font-medium text-text-light">{formatFileSize(file.size)}</span>
|
|
147
156
|
{/if}
|
|
148
157
|
{#if file.tags && file.tags.length > 0}
|
|
149
|
-
<span class="flex items-center gap-0.5 ml-auto" role="list" aria-label=
|
|
158
|
+
<span class="flex items-center gap-0.5 ml-auto" role="list" aria-label={ft.tags}>
|
|
150
159
|
{#each file.tags as tag (tag.id)}
|
|
151
160
|
<span
|
|
152
161
|
class="h-2 w-2 rounded-full"
|
|
@@ -29,6 +29,8 @@
|
|
|
29
29
|
cancel: string;
|
|
30
30
|
confirm: string;
|
|
31
31
|
selectedCount: string;
|
|
32
|
+
clickToSelect: string;
|
|
33
|
+
selectedFile: string;
|
|
32
34
|
}
|
|
33
35
|
> = {
|
|
34
36
|
pl: {
|
|
@@ -39,7 +41,9 @@
|
|
|
39
41
|
selectionResetLabel: 'Resetuj wybór',
|
|
40
42
|
cancel: 'Anuluj',
|
|
41
43
|
confirm: 'Potwierdź',
|
|
42
|
-
selectedCount: 'Wybrano'
|
|
44
|
+
selectedCount: 'Wybrano',
|
|
45
|
+
clickToSelect: '{lang[interfaceLanguage.current].clickToSelect}',
|
|
46
|
+
selectedFile: 'Wybrany plik:'
|
|
43
47
|
},
|
|
44
48
|
en: {
|
|
45
49
|
fileDeletedToast: 'File has been deleted',
|
|
@@ -49,7 +53,9 @@
|
|
|
49
53
|
selectionResetLabel: 'Reset selection',
|
|
50
54
|
cancel: 'Cancel',
|
|
51
55
|
confirm: 'Confirm',
|
|
52
|
-
selectedCount: 'Selected'
|
|
56
|
+
selectedCount: 'Selected',
|
|
57
|
+
clickToSelect: 'Click a file to select it',
|
|
58
|
+
selectedFile: 'Selected file:'
|
|
53
59
|
}
|
|
54
60
|
};
|
|
55
61
|
|
|
@@ -283,15 +289,15 @@
|
|
|
283
289
|
{/each}
|
|
284
290
|
|
|
285
291
|
{#if stagedSelection.length === 0}
|
|
286
|
-
<p class="text-sm text-muted-foreground py-8 text-center">
|
|
292
|
+
<p class="text-sm text-muted-foreground py-8 text-center">{lang[interfaceLanguage.current].clickToSelect}</p>
|
|
287
293
|
{/if}
|
|
288
294
|
{:else}
|
|
289
|
-
<p class="text-sm font-medium text-muted-foreground">
|
|
295
|
+
<p class="text-sm font-medium text-muted-foreground">{lang[interfaceLanguage.current].selectedFile}</p>
|
|
290
296
|
|
|
291
297
|
{#if stagedSelection}
|
|
292
298
|
<FilePreview fileId={stagedSelection} />
|
|
293
299
|
{:else}
|
|
294
|
-
<p class="text-sm text-muted-foreground py-8 text-center">
|
|
300
|
+
<p class="text-sm text-muted-foreground py-8 text-center">{lang[interfaceLanguage.current].clickToSelect}</p>
|
|
295
301
|
{/if}
|
|
296
302
|
{/if}
|
|
297
303
|
</div>
|
|
@@ -5,6 +5,11 @@
|
|
|
5
5
|
import AlignCenter from '@tabler/icons-svelte/icons/align-center';
|
|
6
6
|
import AlignRight from '@tabler/icons-svelte/icons/align-right';
|
|
7
7
|
import AlertTriangle from '@tabler/icons-svelte/icons/alert-triangle';
|
|
8
|
+
import { tiptapLang } from './lang.js';
|
|
9
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
10
|
+
|
|
11
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
12
|
+
const t = $derived(tiptapLang[interfaceLanguage.current]);
|
|
8
13
|
|
|
9
14
|
let { node, updateAttributes, selected }: NodeViewProps = $props();
|
|
10
15
|
|
|
@@ -91,8 +96,8 @@
|
|
|
91
96
|
class="toolbar-btn"
|
|
92
97
|
class:active={node.attrs.align === 'left'}
|
|
93
98
|
onclick={() => setAlign('left')}
|
|
94
|
-
aria-label=
|
|
95
|
-
title=
|
|
99
|
+
aria-label={t.alignLeftLabel}
|
|
100
|
+
title={t.alignLeftLabel}
|
|
96
101
|
>
|
|
97
102
|
<AlignLeft size={16} />
|
|
98
103
|
</button>
|
|
@@ -101,8 +106,8 @@
|
|
|
101
106
|
class="toolbar-btn"
|
|
102
107
|
class:active={node.attrs.align === 'center'}
|
|
103
108
|
onclick={() => setAlign('center')}
|
|
104
|
-
aria-label=
|
|
105
|
-
title=
|
|
109
|
+
aria-label={t.alignCenterLabel}
|
|
110
|
+
title={t.alignCenterLabel}
|
|
106
111
|
>
|
|
107
112
|
<AlignCenter size={16} />
|
|
108
113
|
</button>
|
|
@@ -111,8 +116,8 @@
|
|
|
111
116
|
class="toolbar-btn"
|
|
112
117
|
class:active={node.attrs.align === 'right'}
|
|
113
118
|
onclick={() => setAlign('right')}
|
|
114
|
-
aria-label=
|
|
115
|
-
title=
|
|
119
|
+
aria-label={t.alignRightLabel}
|
|
120
|
+
title={t.alignRightLabel}
|
|
116
121
|
>
|
|
117
122
|
<AlignRight size={16} />
|
|
118
123
|
</button>
|
|
@@ -131,7 +136,7 @@
|
|
|
131
136
|
bind:value={altInput}
|
|
132
137
|
onblur={saveAlt}
|
|
133
138
|
onkeydown={handleAltKeydown}
|
|
134
|
-
placeholder=
|
|
139
|
+
placeholder={t.imageAltPlaceholder}
|
|
135
140
|
/>
|
|
136
141
|
{:else if hasAlt}
|
|
137
142
|
<button type="button" class="figure-alt-display" onclick={startEditAlt}>
|
|
@@ -140,7 +145,7 @@
|
|
|
140
145
|
{:else}
|
|
141
146
|
<button type="button" class="figure-alt-display figure-alt-missing" onclick={startEditAlt}>
|
|
142
147
|
<AlertTriangle size={14} />
|
|
143
|
-
<span>
|
|
148
|
+
<span>{t.addAltMessage}</span>
|
|
144
149
|
</button>
|
|
145
150
|
{/if}
|
|
146
151
|
</div>
|
|
@@ -154,11 +159,11 @@
|
|
|
154
159
|
bind:value={captionInput}
|
|
155
160
|
onblur={saveCaption}
|
|
156
161
|
onkeydown={handleKeydown}
|
|
157
|
-
placeholder=
|
|
162
|
+
placeholder={t.captionPlaceholder}
|
|
158
163
|
/>
|
|
159
164
|
{:else}
|
|
160
165
|
<button type="button" class="caption-display" ondblclick={() => (editing = true)}>
|
|
161
|
-
{node.attrs.caption ||
|
|
166
|
+
{node.attrs.caption || t.captionDefault}
|
|
162
167
|
</button>
|
|
163
168
|
{/if}
|
|
164
169
|
</div>
|
|
@@ -78,6 +78,24 @@
|
|
|
78
78
|
const blockLabel = $derived(blockDef?.label ? (typeof blockDef.label === 'string' ? blockDef.label : Object.values(blockDef.label)[0] ?? blockDef.slug) : node.attrs.blockType);
|
|
79
79
|
const supportedFields = $derived(blockDef?.fields.filter((f) => !SKIP_TYPES.has(f.type)) ?? []);
|
|
80
80
|
|
|
81
|
+
const accordionLabel = $derived.by(() => {
|
|
82
|
+
const field = blockDef?.accordionLabelField;
|
|
83
|
+
if (!field || typeof field !== 'string' || !field.trim()) return '';
|
|
84
|
+
const val = $formStore[field] as string | Record<string, string> | undefined;
|
|
85
|
+
if (!val) return '';
|
|
86
|
+
if (typeof val === 'string') return val.trim();
|
|
87
|
+
if (typeof val === 'object') {
|
|
88
|
+
if ('url' in val) {
|
|
89
|
+
const urlData = val as { url: string | Record<string, string>; text?: string | Record<string, string> };
|
|
90
|
+
const display = urlData.text || urlData.url;
|
|
91
|
+
if (typeof display === 'string') return display.trim();
|
|
92
|
+
if (typeof display === 'object' && display !== null) return display[contentLanguage.current] ?? '';
|
|
93
|
+
}
|
|
94
|
+
return (val as Record<string, string>)[contentLanguage.current] ?? '';
|
|
95
|
+
}
|
|
96
|
+
return '';
|
|
97
|
+
});
|
|
98
|
+
|
|
81
99
|
function parseBlockData(raw: unknown): Record<string, unknown> {
|
|
82
100
|
if (typeof raw === 'string') {
|
|
83
101
|
try { return JSON.parse(raw); } catch { return {}; }
|
|
@@ -192,9 +210,12 @@
|
|
|
192
210
|
class="inline-block-collapse-toggle"
|
|
193
211
|
onclick={() => (collapsed = !collapsed)}
|
|
194
212
|
aria-expanded={!collapsed}
|
|
195
|
-
aria-label={collapsed ?
|
|
213
|
+
aria-label={collapsed ? `Rozwiń blok ${blockLabel}${accordionLabel ? ` — ${accordionLabel}` : ''}` : `Zwiń blok ${blockLabel}${accordionLabel ? ` — ${accordionLabel}` : ''}`}
|
|
196
214
|
>
|
|
197
215
|
<span class="inline-block-label">{blockLabel}</span>
|
|
216
|
+
{#if accordionLabel}
|
|
217
|
+
<span class="inline-block-accordion-label">— {accordionLabel}</span>
|
|
218
|
+
{/if}
|
|
198
219
|
<span class="collapse-chevron" class:collapsed>
|
|
199
220
|
<ChevronDown size={14} />
|
|
200
221
|
</span>
|
|
@@ -298,6 +319,16 @@
|
|
|
298
319
|
color: var(--foreground);
|
|
299
320
|
}
|
|
300
321
|
|
|
322
|
+
.inline-block-accordion-label {
|
|
323
|
+
font-size: 0.8125rem;
|
|
324
|
+
font-weight: 400;
|
|
325
|
+
color: var(--muted-foreground);
|
|
326
|
+
overflow: hidden;
|
|
327
|
+
text-overflow: ellipsis;
|
|
328
|
+
white-space: nowrap;
|
|
329
|
+
max-width: 200px;
|
|
330
|
+
}
|
|
331
|
+
|
|
301
332
|
.collapse-chevron {
|
|
302
333
|
display: flex;
|
|
303
334
|
align-items: center;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { SlashCommandItem } from './slash-command.js';
|
|
3
3
|
import { onMount } from 'svelte';
|
|
4
|
+
import { tiptapLang } from './lang.js';
|
|
5
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
6
|
+
|
|
7
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
8
|
+
const t = $derived(tiptapLang[interfaceLanguage.current]);
|
|
4
9
|
|
|
5
10
|
type Props = {
|
|
6
11
|
items: SlashCommandItem[];
|
|
@@ -79,9 +84,9 @@
|
|
|
79
84
|
});
|
|
80
85
|
</script>
|
|
81
86
|
|
|
82
|
-
<div class="slash-popup" bind:this={listEl} data-slash-popup role="listbox" aria-label=
|
|
87
|
+
<div class="slash-popup" bind:this={listEl} data-slash-popup role="listbox" aria-label={t.commandsLabel}>
|
|
83
88
|
{#if items.length === 0}
|
|
84
|
-
<div class="slash-empty">
|
|
89
|
+
<div class="slash-empty">{t.noResults}</div>
|
|
85
90
|
{:else}
|
|
86
91
|
{@const groups = grouped()}
|
|
87
92
|
{#each [...groups.entries()] as [group, groupItems]}
|
|
@@ -114,7 +119,7 @@
|
|
|
114
119
|
class="slash-more-btn"
|
|
115
120
|
onclick={() => (showTier2 = true)}
|
|
116
121
|
>
|
|
117
|
-
|
|
122
|
+
{t.more}
|
|
118
123
|
</button>
|
|
119
124
|
{/if}
|
|
120
125
|
{/if}
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
import ToolbarButton from './toolbar-button.svelte';
|
|
4
4
|
import * as Tooltip from '../../../components/ui/tooltip/index.js';
|
|
5
5
|
import Separator from '../../../components/ui/separator/separator.svelte';
|
|
6
|
+
import { tiptapLang } from './lang.js';
|
|
7
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
8
|
+
|
|
9
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
10
|
+
const t = $derived(tiptapLang[interfaceLanguage.current]);
|
|
6
11
|
|
|
7
12
|
// Icons
|
|
8
13
|
import H_2 from '@tabler/icons-svelte/icons/h-2';
|
|
@@ -60,21 +65,21 @@
|
|
|
60
65
|
<div class="flex flex-wrap items-center gap-0.5 border-b p-1 sticky top-0 z-10 bg-muted/50">
|
|
61
66
|
<!-- Headings -->
|
|
62
67
|
<ToolbarButton
|
|
63
|
-
label=
|
|
68
|
+
label={t.heading2}
|
|
64
69
|
active={ed.isActive('heading', { level: 2 })}
|
|
65
70
|
onclick={() => ed.chain().focus().toggleHeading({ level: 2 }).run()}
|
|
66
71
|
>
|
|
67
72
|
<H_2 class="h-4 w-4" />
|
|
68
73
|
</ToolbarButton>
|
|
69
74
|
<ToolbarButton
|
|
70
|
-
label=
|
|
75
|
+
label={t.heading3}
|
|
71
76
|
active={ed.isActive('heading', { level: 3 })}
|
|
72
77
|
onclick={() => ed.chain().focus().toggleHeading({ level: 3 }).run()}
|
|
73
78
|
>
|
|
74
79
|
<H_3 class="h-4 w-4" />
|
|
75
80
|
</ToolbarButton>
|
|
76
81
|
<ToolbarButton
|
|
77
|
-
label=
|
|
82
|
+
label={t.paragraph}
|
|
78
83
|
active={ed.isActive('paragraph') && !ed.isActive('heading')}
|
|
79
84
|
onclick={() => ed.chain().focus().setParagraph().run()}
|
|
80
85
|
>
|
|
@@ -85,35 +90,35 @@
|
|
|
85
90
|
|
|
86
91
|
<!-- Text formatting -->
|
|
87
92
|
<ToolbarButton
|
|
88
|
-
label=
|
|
93
|
+
label={t.bold}
|
|
89
94
|
active={ed.isActive('bold')}
|
|
90
95
|
onclick={() => ed.chain().focus().toggleBold().run()}
|
|
91
96
|
>
|
|
92
97
|
<Bold class="h-4 w-4" />
|
|
93
98
|
</ToolbarButton>
|
|
94
99
|
<ToolbarButton
|
|
95
|
-
label=
|
|
100
|
+
label={t.italic}
|
|
96
101
|
active={ed.isActive('italic')}
|
|
97
102
|
onclick={() => ed.chain().focus().toggleItalic().run()}
|
|
98
103
|
>
|
|
99
104
|
<Italic class="h-4 w-4" />
|
|
100
105
|
</ToolbarButton>
|
|
101
106
|
<ToolbarButton
|
|
102
|
-
label=
|
|
107
|
+
label={t.underline}
|
|
103
108
|
active={ed.isActive('underline')}
|
|
104
109
|
onclick={() => ed.chain().focus().toggleUnderline().run()}
|
|
105
110
|
>
|
|
106
111
|
<Underline class="h-4 w-4" />
|
|
107
112
|
</ToolbarButton>
|
|
108
113
|
<ToolbarButton
|
|
109
|
-
label=
|
|
114
|
+
label={t.strikethrough}
|
|
110
115
|
active={ed.isActive('strike')}
|
|
111
116
|
onclick={() => ed.chain().focus().toggleStrike().run()}
|
|
112
117
|
>
|
|
113
118
|
<Strikethrough class="h-4 w-4" />
|
|
114
119
|
</ToolbarButton>
|
|
115
120
|
<ToolbarButton
|
|
116
|
-
label=
|
|
121
|
+
label={t.highlight}
|
|
117
122
|
active={ed.isActive('highlight')}
|
|
118
123
|
onclick={() => ed.chain().focus().toggleHighlight().run()}
|
|
119
124
|
>
|
|
@@ -124,28 +129,28 @@
|
|
|
124
129
|
|
|
125
130
|
<!-- Alignment -->
|
|
126
131
|
<ToolbarButton
|
|
127
|
-
label=
|
|
132
|
+
label={t.alignLeft}
|
|
128
133
|
active={ed.isActive({ textAlign: 'left' })}
|
|
129
134
|
onclick={() => ed.chain().focus().setTextAlign('left').run()}
|
|
130
135
|
>
|
|
131
136
|
<AlignLeft class="h-4 w-4" />
|
|
132
137
|
</ToolbarButton>
|
|
133
138
|
<ToolbarButton
|
|
134
|
-
label=
|
|
139
|
+
label={t.alignCenter}
|
|
135
140
|
active={ed.isActive({ textAlign: 'center' })}
|
|
136
141
|
onclick={() => ed.chain().focus().setTextAlign('center').run()}
|
|
137
142
|
>
|
|
138
143
|
<AlignCenter class="h-4 w-4" />
|
|
139
144
|
</ToolbarButton>
|
|
140
145
|
<ToolbarButton
|
|
141
|
-
label=
|
|
146
|
+
label={t.alignRight}
|
|
142
147
|
active={ed.isActive({ textAlign: 'right' })}
|
|
143
148
|
onclick={() => ed.chain().focus().setTextAlign('right').run()}
|
|
144
149
|
>
|
|
145
150
|
<AlignRight class="h-4 w-4" />
|
|
146
151
|
</ToolbarButton>
|
|
147
152
|
<ToolbarButton
|
|
148
|
-
label=
|
|
153
|
+
label={t.alignJustify}
|
|
149
154
|
active={ed.isActive({ textAlign: 'justify' })}
|
|
150
155
|
onclick={() => ed.chain().focus().setTextAlign('justify').run()}
|
|
151
156
|
>
|
|
@@ -156,21 +161,21 @@
|
|
|
156
161
|
|
|
157
162
|
<!-- Lists & Quote -->
|
|
158
163
|
<ToolbarButton
|
|
159
|
-
label=
|
|
164
|
+
label={t.bulletList}
|
|
160
165
|
active={ed.isActive('bulletList')}
|
|
161
166
|
onclick={() => ed.chain().focus().toggleBulletList().run()}
|
|
162
167
|
>
|
|
163
168
|
<List class="h-4 w-4" />
|
|
164
169
|
</ToolbarButton>
|
|
165
170
|
<ToolbarButton
|
|
166
|
-
label=
|
|
171
|
+
label={t.orderedList}
|
|
167
172
|
active={ed.isActive('orderedList')}
|
|
168
173
|
onclick={() => ed.chain().focus().toggleOrderedList().run()}
|
|
169
174
|
>
|
|
170
175
|
<ListNumbers class="h-4 w-4" />
|
|
171
176
|
</ToolbarButton>
|
|
172
177
|
<ToolbarButton
|
|
173
|
-
label=
|
|
178
|
+
label={t.blockquote}
|
|
174
179
|
active={ed.isActive('blockquote')}
|
|
175
180
|
onclick={() => ed.chain().focus().toggleBlockquote().run()}
|
|
176
181
|
>
|
|
@@ -180,16 +185,16 @@
|
|
|
180
185
|
<Separator orientation="vertical" class="mx-1 h-6" />
|
|
181
186
|
|
|
182
187
|
<!-- Link, Image, Table -->
|
|
183
|
-
<ToolbarButton label=
|
|
188
|
+
<ToolbarButton label={t.link} active={ed.isActive('link')} onclick={onLinkDialog}>
|
|
184
189
|
<LinkIcon class="h-4 w-4" />
|
|
185
190
|
</ToolbarButton>
|
|
186
|
-
<ToolbarButton label=
|
|
191
|
+
<ToolbarButton label={t.image} active={false} onclick={onImageDialog}>
|
|
187
192
|
<Photo class="h-4 w-4" />
|
|
188
193
|
</ToolbarButton>
|
|
189
|
-
<ToolbarButton label=
|
|
194
|
+
<ToolbarButton label={t.video} active={false} onclick={onVideoDialog}>
|
|
190
195
|
<VideoIcon class="h-4 w-4" />
|
|
191
196
|
</ToolbarButton>
|
|
192
|
-
<ToolbarButton label=
|
|
197
|
+
<ToolbarButton label={t.table} active={false} onclick={onTableDialog}>
|
|
193
198
|
<Table class="h-4 w-4" />
|
|
194
199
|
</ToolbarButton>
|
|
195
200
|
|
|
@@ -197,14 +202,14 @@
|
|
|
197
202
|
|
|
198
203
|
<!-- Code -->
|
|
199
204
|
<ToolbarButton
|
|
200
|
-
label=
|
|
205
|
+
label={t.inlineCode}
|
|
201
206
|
active={ed.isActive('code')}
|
|
202
207
|
onclick={() => ed.chain().focus().toggleCode().run()}
|
|
203
208
|
>
|
|
204
209
|
<Code class="h-4 w-4" />
|
|
205
210
|
</ToolbarButton>
|
|
206
211
|
<ToolbarButton
|
|
207
|
-
label=
|
|
212
|
+
label={t.codeBlock}
|
|
208
213
|
active={ed.isActive('codeBlock')}
|
|
209
214
|
onclick={() => ed.chain().focus().toggleCodeBlock().run()}
|
|
210
215
|
>
|
|
@@ -213,7 +218,7 @@
|
|
|
213
218
|
|
|
214
219
|
{#if showInsertBlock && onInsertBlock}
|
|
215
220
|
<Separator orientation="vertical" class="mx-1 h-6" />
|
|
216
|
-
<ToolbarButton label=
|
|
221
|
+
<ToolbarButton label={t.insertBlock} active={false} onclick={onInsertBlock}>
|
|
217
222
|
<Plus class="h-4 w-4" />
|
|
218
223
|
</ToolbarButton>
|
|
219
224
|
{/if}
|
|
@@ -222,7 +227,7 @@
|
|
|
222
227
|
<Separator orientation="vertical" class="mx-1 h-6" />
|
|
223
228
|
|
|
224
229
|
<!-- HTML View -->
|
|
225
|
-
<ToolbarButton label=
|
|
230
|
+
<ToolbarButton label={t.htmlView} active={isCodeViewActive} onclick={onToggleCodeView}>
|
|
226
231
|
<SourceCode class="h-4 w-4" />
|
|
227
232
|
</ToolbarButton>
|
|
228
233
|
{/if}
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
import { getRemotes } from '../../context/remotes.js';
|
|
5
5
|
import type { Editor } from '@tiptap/core';
|
|
6
6
|
import AlertTriangle from '@tabler/icons-svelte/icons/alert-triangle';
|
|
7
|
+
import { tiptapLang } from './lang.js';
|
|
8
|
+
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
9
|
+
|
|
10
|
+
const interfaceLanguage = useInterfaceLanguage();
|
|
11
|
+
const t = $derived(tiptapLang[interfaceLanguage.current]);
|
|
7
12
|
|
|
8
13
|
type Props = {
|
|
9
14
|
open: boolean;
|
|
@@ -78,7 +83,7 @@
|
|
|
78
83
|
<Dialog.Root bind:open {onOpenChange}>
|
|
79
84
|
<Dialog.Content class="max-w-5xl! sm:max-w-5xl!">
|
|
80
85
|
<Dialog.Header>
|
|
81
|
-
<Dialog.Title>
|
|
86
|
+
<Dialog.Title>{t.insertImage}</Dialog.Title>
|
|
82
87
|
</Dialog.Header>
|
|
83
88
|
<MediaSelector bind:selected accept="image/*" onConfirm={handleConfirm} />
|
|
84
89
|
</Dialog.Content>
|
|
@@ -88,26 +93,26 @@
|
|
|
88
93
|
<Dialog.Root bind:open={altOpen}>
|
|
89
94
|
<Dialog.Content class="max-w-lg! sm:max-w-lg!">
|
|
90
95
|
<Dialog.Header>
|
|
91
|
-
<Dialog.Title>
|
|
96
|
+
<Dialog.Title>{t.imageDescription}</Dialog.Title>
|
|
92
97
|
</Dialog.Header>
|
|
93
98
|
<div class="image-alt-prompt">
|
|
94
99
|
{#if !altText.trim()}
|
|
95
100
|
<div class="image-alt-banner">
|
|
96
101
|
<AlertTriangle class="size-4 shrink-0" />
|
|
97
|
-
<span>
|
|
102
|
+
<span>{t.addAltBanner}</span>
|
|
98
103
|
</div>
|
|
99
104
|
{/if}
|
|
100
105
|
<div class="image-alt-preview">
|
|
101
106
|
<img src={fileUrl} alt="" class="image-alt-thumb" />
|
|
102
107
|
</div>
|
|
103
|
-
<label class="image-alt-label" for="image-alt-input">
|
|
108
|
+
<label class="image-alt-label" for="image-alt-input">{t.altLabel}</label>
|
|
104
109
|
<input
|
|
105
110
|
id="image-alt-input"
|
|
106
111
|
type="text"
|
|
107
112
|
class="image-alt-input"
|
|
108
113
|
bind:value={altText}
|
|
109
114
|
onkeydown={handleAltKeydown}
|
|
110
|
-
placeholder=
|
|
115
|
+
placeholder={t.altPlaceholder}
|
|
111
116
|
/>
|
|
112
117
|
<div class="image-alt-actions">
|
|
113
118
|
<button
|
|
@@ -115,14 +120,14 @@
|
|
|
115
120
|
class="image-alt-btn-primary"
|
|
116
121
|
onclick={() => insertImage(altText.trim())}
|
|
117
122
|
>
|
|
118
|
-
|
|
123
|
+
{t.addAndInsert}
|
|
119
124
|
</button>
|
|
120
125
|
<button
|
|
121
126
|
type="button"
|
|
122
127
|
class="image-alt-btn-ghost"
|
|
123
128
|
onclick={handleAltCancel}
|
|
124
129
|
>
|
|
125
|
-
|
|
130
|
+
{t.skip}
|
|
126
131
|
</button>
|
|
127
132
|
</div>
|
|
128
133
|
</div>
|