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.
Files changed (59) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/ROADMAP.md +24 -0
  3. package/dist/admin/api/handler.js +2 -0
  4. package/dist/admin/api/replace.js +2 -1
  5. package/dist/admin/api/rest/routes/upload.js +2 -1
  6. package/dist/admin/api/upload-limit.d.ts +2 -0
  7. package/dist/admin/api/upload-limit.js +7 -0
  8. package/dist/admin/api/upload.js +2 -1
  9. package/dist/admin/client/collection/collection-entries.svelte +58 -11
  10. package/dist/admin/client/users/users-page.svelte +5 -6
  11. package/dist/admin/client/users/users-page.svelte.d.ts +1 -4
  12. package/dist/admin/components/fields/block-picker-modal.svelte +13 -4
  13. package/dist/admin/components/fields/blocks-field.svelte +31 -9
  14. package/dist/admin/components/fields/simple-array-field.svelte +22 -11
  15. package/dist/admin/components/layout/layout-renderer.svelte +10 -4
  16. package/dist/admin/components/media/file-preview.svelte +10 -1
  17. package/dist/admin/components/media/file-upload.svelte +66 -9
  18. package/dist/admin/components/media/files-list.svelte +12 -3
  19. package/dist/admin/components/media/media-selector.svelte +11 -5
  20. package/dist/admin/components/tiptap/FigureNodeView.svelte +15 -10
  21. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +32 -1
  22. package/dist/admin/components/tiptap/SlashCommandPopup.svelte +8 -3
  23. package/dist/admin/components/tiptap/editor-toolbar.svelte +28 -23
  24. package/dist/admin/components/tiptap/image-dialog.svelte +12 -7
  25. package/dist/admin/components/tiptap/lang.d.ts +77 -0
  26. package/dist/admin/components/tiptap/lang.js +170 -0
  27. package/dist/admin/components/tiptap/link-dialog.svelte +22 -18
  28. package/dist/admin/components/tiptap/slash-command.js +26 -22
  29. package/dist/admin/components/tiptap/table-dialog.svelte +9 -4
  30. package/dist/admin/components/tiptap/video-dialog.svelte +6 -1
  31. package/dist/admin/remote/email.remote.d.ts +1 -0
  32. package/dist/admin/remote/email.remote.js +5 -0
  33. package/dist/admin/remote/entry.remote.d.ts +1 -0
  34. package/dist/admin/remote/entry.remote.js +6 -4
  35. package/dist/admin/remote/index.d.ts +1 -0
  36. package/dist/admin/remote/index.js +1 -0
  37. package/dist/admin/remote/reorder.d.ts +1 -0
  38. package/dist/admin/remote/reorder.js +33 -0
  39. package/dist/core/server/entries/operations/get.js +15 -3
  40. package/dist/core/server/fields/utils/imageStyles.js +7 -3
  41. package/dist/core/server/generator/fields.js +2 -2
  42. package/dist/core/server/generator/generator.js +4 -2
  43. package/dist/core/server/media/styles/operations/createMediaStyle.d.ts +2 -2
  44. package/dist/core/server/media/styles/operations/createMediaStyle.js +5 -3
  45. package/dist/core/server/media/styles/operations/generateDefaultStyles.js +33 -6
  46. package/dist/core/server/media/styles/operations/getImageStyle.d.ts +1 -0
  47. package/dist/core/server/media/styles/operations/getImageStyle.js +3 -0
  48. package/dist/core/server/media/styles/sharp/generateImageStyle.d.ts +2 -1
  49. package/dist/core/server/media/styles/sharp/generateImageStyle.js +5 -3
  50. package/dist/core/server/media/uploadLimit.d.ts +2 -0
  51. package/dist/core/server/media/uploadLimit.js +26 -0
  52. package/dist/types/entries.d.ts +1 -0
  53. package/dist/types/layout.d.ts +0 -1
  54. package/dist/updates/0.13.1/index.d.ts +2 -0
  55. package/dist/updates/0.13.1/index.js +20 -0
  56. package/dist/updates/0.13.2/index.d.ts +2 -0
  57. package/dist/updates/0.13.2/index.js +20 -0
  58. package/dist/updates/index.js +3 -1
  59. 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
- { addFiles: string; dropFiles: string; dropHint: string; uploading: string; uploadComplete: string }
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
- uploadProgress[index].complete = true;
62
- uploadProgress[index].progress = 100;
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
- uploadProgress = files.map((f) => ({ name: f.name, progress: 0, complete: false }));
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(files.map((file, i) => uploadFile(file, i)));
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
- Error
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">Brak plików</p>
98
- <p class="text-xs text-muted-foreground mt-1">Prześlij pliki lub zmień filtry</p>
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="Tagi">
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">Kliknij na plik aby go wybrać</p>
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">Wybrany plik:</p>
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">Kliknij na plik aby go wybrać</p>
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="Wyrównaj do lewej"
95
- title="Wyrównaj do lewej"
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="Wyśrodkuj"
105
- title="Wyśrodkuj"
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="Wyrównaj do prawej"
115
- title="Wyrównaj do prawej"
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="Opis zdjęcia..."
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>Dodaj opis zdjęcia, żeby każdy mógł je zrozumieć</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="Dodaj podpis..."
162
+ placeholder={t.captionPlaceholder}
158
163
  />
159
164
  {:else}
160
165
  <button type="button" class="caption-display" ondblclick={() => (editing = true)}>
161
- {node.attrs.caption || 'Kliknij dwukrotnie, by dodać podpis'}
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 ? 'Rozwiń blok' : 'Zwiń blok'}
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="Polecenia">
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">Brak wyników</div>
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
- Więcej...
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="Nagłówek 2"
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="Nagłówek 3"
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="Paragraf"
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="Pogrubienie"
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="Kursywa"
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="Podkreślenie"
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="Przekreślenie"
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="Wyróżnienie"
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="Do lewej"
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="Wyśrodkuj"
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="Do prawej"
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="Wyjustuj"
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="Lista punktowa"
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="Lista numerowana"
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="Cytat"
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="Link" active={ed.isActive('link')} onclick={onLinkDialog}>
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="Obrazek" active={false} onclick={onImageDialog}>
191
+ <ToolbarButton label={t.image} active={false} onclick={onImageDialog}>
187
192
  <Photo class="h-4 w-4" />
188
193
  </ToolbarButton>
189
- <ToolbarButton label="Wideo" active={false} onclick={onVideoDialog}>
194
+ <ToolbarButton label={t.video} active={false} onclick={onVideoDialog}>
190
195
  <VideoIcon class="h-4 w-4" />
191
196
  </ToolbarButton>
192
- <ToolbarButton label="Tabela" active={false} onclick={onTableDialog}>
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="Kod inline"
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="Blok kodu"
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="Wstaw blok" active={false} onclick={onInsertBlock}>
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="Widok HTML" active={isCodeViewActive} onclick={onToggleCodeView}>
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>Wstaw obrazek</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>Opis zdjęcia</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>Dodaj opis zdjęcia, żeby każdy mógł je zrozumieć</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">Opis alternatywny (alt text)</label>
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="Np. Zdjęcie zespołu na konferencji..."
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
- Dodaj i wstaw
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
- Pomiń
130
+ {t.skip}
126
131
  </button>
127
132
  </div>
128
133
  </div>