includio-cms 0.13.0 → 0.13.1

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 (35) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/ROADMAP.md +14 -0
  3. package/dist/admin/client/collection/collection-entries.svelte +17 -8
  4. package/dist/admin/client/users/users-page.svelte +5 -6
  5. package/dist/admin/client/users/users-page.svelte.d.ts +1 -4
  6. package/dist/admin/components/fields/block-picker-modal.svelte +13 -4
  7. package/dist/admin/components/fields/blocks-field.svelte +31 -9
  8. package/dist/admin/components/fields/simple-array-field.svelte +22 -11
  9. package/dist/admin/components/layout/layout-renderer.svelte +10 -4
  10. package/dist/admin/components/media/file-preview.svelte +10 -1
  11. package/dist/admin/components/media/files-list.svelte +12 -3
  12. package/dist/admin/components/media/media-selector.svelte +11 -5
  13. package/dist/admin/components/tiptap/FigureNodeView.svelte +15 -10
  14. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +32 -1
  15. package/dist/admin/components/tiptap/SlashCommandPopup.svelte +8 -3
  16. package/dist/admin/components/tiptap/editor-toolbar.svelte +28 -23
  17. package/dist/admin/components/tiptap/image-dialog.svelte +12 -7
  18. package/dist/admin/components/tiptap/lang.d.ts +77 -0
  19. package/dist/admin/components/tiptap/lang.js +170 -0
  20. package/dist/admin/components/tiptap/link-dialog.svelte +22 -18
  21. package/dist/admin/components/tiptap/slash-command.js +26 -22
  22. package/dist/admin/components/tiptap/table-dialog.svelte +9 -4
  23. package/dist/admin/components/tiptap/video-dialog.svelte +6 -1
  24. package/dist/admin/remote/email.remote.d.ts +1 -0
  25. package/dist/admin/remote/email.remote.js +5 -0
  26. package/dist/admin/remote/entry.remote.js +1 -1
  27. package/dist/admin/remote/index.d.ts +1 -0
  28. package/dist/admin/remote/index.js +1 -0
  29. package/dist/core/server/generator/fields.js +2 -2
  30. package/dist/core/server/generator/generator.js +1 -1
  31. package/dist/types/layout.d.ts +0 -1
  32. package/dist/updates/0.13.1/index.d.ts +2 -0
  33. package/dist/updates/0.13.1/index.js +20 -0
  34. package/dist/updates/index.js +2 -1
  35. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,24 @@
3
3
  All notable changes to includio-cms are documented here.
4
4
  Generated from `src/lib/updates/` — do not edit manually.
5
5
 
6
+ ## 0.13.1 — 2026-03-20
7
+
8
+ Admin UI i18n, codegen cleanup, docs rewrite
9
+
10
+ ### Added
11
+ - Admin UI i18n — TipTap editor, blocks field, array field, media components support pl/en interface language
12
+ - Inline block accordion labels — show field value in collapsed block header
13
+ - Blocks field UrlFieldData label support
14
+ - Email configuration remote endpoint
15
+
16
+ ### Fixed
17
+ - Codegen: remove unused FlatImageFieldData/FlatVideoFieldData, use ImageFieldData/VideoFieldData
18
+ - Layout renderer: hide card header when label is empty
19
+ - Collection entries: improved relation label fetching with UUID validation and multi-version support
20
+ - Users page: client-side email config check, remove server load dependency
21
+ - Entry remote: stricter UUID validation for ids parameter
22
+ - Layout type: remove unused label property from layout node
23
+
6
24
  ## 0.13.0 — 2026-03-19
7
25
 
8
26
  Private file uploads for form submissions
package/ROADMAP.md CHANGED
@@ -205,6 +205,20 @@
205
205
 
206
206
  - [x] `[feature]` `[P1]` Private file uploads — form submissions can upload files to non-public directory
207
207
 
208
+ ## 0.13.1 — Admin i18n, codegen cleanup, docs
209
+
210
+ - [x] `[feature]` `[P1]` Admin UI i18n — TipTap, blocks, array, media components support pl/en <!-- files: src/lib/admin/components/tiptap/lang.ts -->
211
+ - [x] `[feature]` `[P2]` Inline block accordion labels — show field value in collapsed header <!-- files: src/lib/admin/components/tiptap/InlineBlockNodeView.svelte -->
212
+ - [x] `[feature]` `[P2]` Blocks field UrlFieldData label support <!-- files: src/lib/admin/components/fields/blocks-field.svelte -->
213
+ - [x] `[feature]` `[P2]` Email configuration remote endpoint <!-- files: src/lib/admin/remote/email.remote.ts -->
214
+ - [x] `[fix]` `[P1]` Codegen: remove unused Flat*FieldData types, use ImageFieldData/VideoFieldData <!-- files: src/lib/core/server/generator/fields.ts, src/lib/core/server/generator/generator.ts -->
215
+ - [x] `[fix]` `[P1]` Layout renderer: hide card header when label empty <!-- files: src/lib/admin/components/layout/layout-renderer.svelte -->
216
+ - [x] `[fix]` `[P1]` Collection entries: improved relation label fetching (UUID validation, multi-version) <!-- files: src/lib/admin/client/collection/collection-entries.svelte -->
217
+ - [x] `[fix]` `[P1]` Users page: client-side email config, remove server load <!-- files: src/lib/admin/client/users/users-page.svelte -->
218
+ - [x] `[fix]` `[P2]` Entry remote: stricter UUID validation for ids <!-- files: src/lib/admin/remote/entry.remote.ts -->
219
+ - [x] `[fix]` `[P2]` Layout type: remove unused label property <!-- files: src/lib/types/layout.ts -->
220
+ - [x] `[chore]` `[P1]` Documentation rewrite — new pages for blocks, content, media-field, layout; expanded API, auth, entries, forms, plugins docs
221
+
208
222
  ## 0.14.0 — SEO module
209
223
 
210
224
  - [ ] `[feature]` `[P1]` SERP preview + character limits for title/description <!-- files: src/lib/admin/components/fields/seo-field.svelte -->
@@ -433,6 +433,8 @@
433
433
  return issues.filter((i) => i.type === 'warning').length;
434
434
  }
435
435
 
436
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
437
+
436
438
  async function fetchRelationLabels(entries: RawEntry[]): Promise<Record<string, string>> {
437
439
  if (relationListFields.length === 0) return {};
438
440
 
@@ -441,14 +443,21 @@
441
443
  uuidsByCollection.set(field.collection, new Set());
442
444
  }
443
445
  for (const entry of entries) {
444
- const data = getVersionData(entry);
445
- if (!data) continue;
446
- for (const field of relationListFields) {
447
- const raw = (data as Record<string, unknown>)[field.slug];
448
- if (!raw) continue;
449
- const set = uuidsByCollection.get(field.collection)!;
450
- if (typeof raw === 'string') set.add(raw);
451
- if (Array.isArray(raw)) raw.forEach((v) => typeof v === 'string' && set.add(v));
446
+ // Collect UUIDs from all version sources (draft + published) to match mapEntryToRow
447
+ const lang = contentLanguage.current;
448
+ const dataSources = [
449
+ entry.publishedVersions[lang]?.data,
450
+ entry.draftVersions[lang]?.data,
451
+ getVersionData(entry)
452
+ ].filter(Boolean) as Record<string, unknown>[];
453
+ for (const data of dataSources) {
454
+ for (const field of relationListFields) {
455
+ const raw = data[field.slug];
456
+ if (!raw) continue;
457
+ const set = uuidsByCollection.get(field.collection)!;
458
+ if (typeof raw === 'string' && UUID_RE.test(raw)) set.add(raw);
459
+ if (Array.isArray(raw)) raw.forEach((v) => typeof v === 'string' && UUID_RE.test(v) && set.add(v));
460
+ }
452
461
  }
453
462
  }
454
463
 
@@ -32,12 +32,7 @@
32
32
  import UserSessionsSheet from './user-sessions-sheet.svelte';
33
33
  import InviteUserDialog from './invite-user-dialog.svelte';
34
34
  import PendingInvitations from './pending-invitations.svelte';
35
-
36
- type Props = {
37
- emailConfigured?: boolean;
38
- };
39
-
40
- let { emailConfigured = false }: Props = $props();
35
+ import { getRemotes } from '../../helpers/index.js';
41
36
 
42
37
  type User = {
43
38
  id: string;
@@ -47,6 +42,10 @@
47
42
  createdAt: Date;
48
43
  };
49
44
 
45
+ const remotes = getRemotes();
46
+ const emailQuery = $derived(remotes.getEmailConfigured());
47
+ const emailConfigured = $derived(emailQuery.data === true);
48
+
50
49
  const interfaceLanguage = useInterfaceLanguage();
51
50
  const lang = $derived(usersLang[interfaceLanguage.current]);
52
51
  const session = authClient.useSession();
@@ -1,6 +1,3 @@
1
- type Props = {
2
- emailConfigured?: boolean;
3
- };
4
- declare const UsersPage: import("svelte").Component<Props, {}, "">;
1
+ declare const UsersPage: import("svelte").Component<Record<string, never>, {}, "">;
5
2
  type UsersPage = ReturnType<typeof UsersPage>;
6
3
  export default UsersPage;
@@ -9,6 +9,15 @@
9
9
 
10
10
  const interfaceLanguage = useInterfaceLanguage();
11
11
 
12
+ import type { InterfaceLanguage } from '../../../types/languages.js';
13
+ const pickerLang: Record<InterfaceLanguage, {
14
+ addBlock: string; chooseBlockType: string; searchBlocks: string; noBlocksFound: string;
15
+ }> = {
16
+ pl: { addBlock: 'Dodaj blok', chooseBlockType: 'Wybierz typ bloku', searchBlocks: 'Szukaj bloków...', noBlocksFound: 'Nie znaleziono bloków' },
17
+ en: { addBlock: 'Add block', chooseBlockType: 'Choose a block type to add', searchBlocks: 'Search blocks...', noBlocksFound: 'No blocks found' }
18
+ };
19
+ const pt = $derived(pickerLang[interfaceLanguage.current]);
20
+
12
21
  type Props = {
13
22
  open: boolean;
14
23
  options: ObjectField[];
@@ -39,15 +48,15 @@
39
48
  <Dialog.Root bind:open>
40
49
  <Dialog.Content class="max-w-3xl">
41
50
  <Dialog.Header>
42
- <Dialog.Title>Add Block</Dialog.Title>
43
- <Dialog.Description>Choose a block type to add</Dialog.Description>
51
+ <Dialog.Title>{pt.addBlock}</Dialog.Title>
52
+ <Dialog.Description>{pt.chooseBlockType}</Dialog.Description>
44
53
  </Dialog.Header>
45
54
 
46
55
  <div class="relative">
47
56
  <SearchIcon class="text-muted-foreground absolute left-3 top-1/2 size-4 -translate-y-1/2" />
48
57
  <Input
49
58
  type="text"
50
- placeholder="Search blocks..."
59
+ placeholder={pt.searchBlocks}
51
60
  class="pl-10"
52
61
  bind:value={searchQuery}
53
62
  />
@@ -90,7 +99,7 @@
90
99
 
91
100
  {#if filteredOptions.length === 0}
92
101
  <div class="py-8 text-center">
93
- <p class="text-muted-foreground">No blocks found</p>
102
+ <p class="text-muted-foreground">{pt.noBlocksFound}</p>
94
103
  </div>
95
104
  {/if}
96
105
  </Dialog.Content>
@@ -32,6 +32,16 @@
32
32
  const contentLanguage = getContentLanguage();
33
33
  const interfaceLanguage = useInterfaceLanguage();
34
34
 
35
+ import type { InterfaceLanguage } from '../../../types/languages.js';
36
+ const blocksLang: Record<InterfaceLanguage, {
37
+ collapseAll: string; showAll: string; openMenu: string; duplicate: string;
38
+ moveUp: string; moveDown: string; delete: string; addBlock: string; elements: string;
39
+ }> = {
40
+ pl: { collapseAll: 'Zwiń wszystko', showAll: 'Rozwiń wszystko', openMenu: 'Otwórz menu', duplicate: 'Duplikuj', moveUp: 'Przenieś wyżej', moveDown: 'Przenieś niżej', delete: 'Usuń', addBlock: 'Dodaj blok', elements: 'elementów' },
41
+ en: { collapseAll: 'Collapse all', showAll: 'Show all', openMenu: 'Open menu', duplicate: 'Duplicate', moveUp: 'Move up', moveDown: 'Move down', delete: 'Delete', addBlock: 'Add block', elements: 'elements' }
42
+ };
43
+ const bt = $derived(blocksLang[interfaceLanguage.current]);
44
+
35
45
  function generateId(): string {
36
46
  if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
37
47
  return crypto.randomUUID();
@@ -174,6 +184,18 @@
174
184
  }
175
185
 
176
186
  if (typeof label === 'object' && label !== null) {
187
+ // UrlFieldData — has `url` property
188
+ if ('url' in label) {
189
+ const urlData = label as { url: string | Record<string, string>; text?: string | Record<string, string> };
190
+ const displayValue = urlData.text || urlData.url;
191
+ if (typeof displayValue === 'string' && displayValue.trim().length > 0) {
192
+ return displayValue;
193
+ }
194
+ if (typeof displayValue === 'object' && displayValue !== null) {
195
+ return displayValue[contentLanguage.current] ?? '';
196
+ }
197
+ }
198
+
177
199
  const objectLabel = label as Record<string, string>;
178
200
  return `${objectLabel[contentLanguage.current]}`;
179
201
  }
@@ -229,7 +251,7 @@
229
251
  >{getLocalizedLabel(field.label, interfaceLanguage.current)}</RequiredLabel
230
252
  >
231
253
  {#if isFixedLength}
232
- <span class="text-muted-foreground text-xs">{fixedCount} elementów</span>
254
+ <span class="text-muted-foreground text-xs">{fixedCount} {bt.elements}</span>
233
255
  {:else if field.maxItems !== undefined}
234
256
  <span class="text-xs {atMax ? 'text-destructive' : 'text-muted-foreground'}"
235
257
  >{$value?.length ?? 0} / {field.maxItems}</span
@@ -244,7 +266,7 @@
244
266
  variant="ghost"
245
267
  onclick={() => {
246
268
  accordionOpenState = [];
247
- }}>Collapse All</Button
269
+ }}>{bt.collapseAll}</Button
248
270
  >
249
271
  <Button
250
272
  size="sm"
@@ -254,7 +276,7 @@
254
276
  if ($value) {
255
277
  accordionOpenState = $value.map((_, i) => i.toString());
256
278
  }
257
- }}>Show All</Button
279
+ }}>{bt.showAll}</Button
258
280
  >
259
281
  </div>
260
282
  </div>
@@ -334,7 +356,7 @@
334
356
  {#snippet child({ props })}
335
357
  <Button variant="ghost" size="icon" {...props}>
336
358
  <DotsVerticalIcon />
337
- <span class="sr-only">Open menu</span>
359
+ <span class="sr-only">{bt.openMenu}</span>
338
360
  </Button>
339
361
  {/snippet}
340
362
  </DropdownMenu.Trigger>
@@ -344,27 +366,27 @@
344
366
  onclick={() => duplicateItem(index)}
345
367
  disabled={atMax}
346
368
  >
347
- Duplicate
369
+ {bt.duplicate}
348
370
  </DropdownMenu.Item>
349
371
  {/if}
350
372
  <DropdownMenu.Item
351
373
  onclick={() => moveItemUp(index)}
352
374
  disabled={index === 0}
353
375
  >
354
- Move up
376
+ {bt.moveUp}
355
377
  </DropdownMenu.Item>
356
378
  <DropdownMenu.Item
357
379
  onclick={() => moveItemDown(index)}
358
380
  disabled={$value && index === $value.length - 1}
359
381
  >
360
- Move down
382
+ {bt.moveDown}
361
383
  </DropdownMenu.Item>
362
384
  {#if !isFixedLength}
363
385
  <DropdownMenu.Item
364
386
  variant="destructive"
365
387
  onclick={() => removeItem(index)}
366
388
  >
367
- Delete
389
+ {bt.delete}
368
390
  </DropdownMenu.Item>
369
391
  {/if}
370
392
  </DropdownMenu.Content>
@@ -413,7 +435,7 @@
413
435
  onclick={() => (blockPickerOpen = true)}
414
436
  >
415
437
  <CirclePlus />
416
- Add Block
438
+ {bt.addBlock}
417
439
  </Button>
418
440
  </div>
419
441
  <BlockPickerModal
@@ -11,6 +11,17 @@
11
11
  const contentLanguage = getContentLanguage();
12
12
  const interfaceLanguage = useInterfaceLanguage();
13
13
 
14
+ import type { InterfaceLanguage } from '../../../types/languages.js';
15
+ const arrayLang: Record<InterfaceLanguage, {
16
+ typeAndEnter: string; add: string; typeNumber: string; addLink: string;
17
+ urlPlaceholder: string; linkText: string; newTab: string;
18
+ removeItem: string; removeLink: string;
19
+ }> = {
20
+ pl: { typeAndEnter: 'Wpisz i naciśnij Enter...', add: 'Dodaj', typeNumber: 'Wpisz liczbę...', addLink: 'Dodaj link', urlPlaceholder: 'URL...', linkText: 'Tekst linku...', newTab: 'Nowa karta', removeItem: 'Usuń element', removeLink: 'Usuń link' },
21
+ en: { typeAndEnter: 'Type and press Enter...', add: 'Add', typeNumber: 'Enter a number...', addLink: 'Add link', urlPlaceholder: 'URL...', linkText: 'Link text...', newTab: 'New tab', removeItem: 'Remove item', removeLink: 'Remove link' }
22
+ };
23
+ const at = $derived(arrayLang[interfaceLanguage.current]);
24
+
14
25
  type Props = {
15
26
  field: ArrayField;
16
27
  value: unknown[] | undefined;
@@ -122,7 +133,7 @@
122
133
  type="button"
123
134
  class="text-[#555566] hover:text-[#C44B4B] transition-colors"
124
135
  onclick={() => removeItem(index)}
125
- aria-label="Usuń element"
136
+ aria-label={at.removeItem}
126
137
  >
127
138
  <X class="h-3.5 w-3.5" />
128
139
  </button>
@@ -134,7 +145,7 @@
134
145
  <div class="flex items-center gap-2">
135
146
  <Input
136
147
  type="text"
137
- placeholder="Wpisz i naciśnij Enter..."
148
+ placeholder={at.typeAndEnter}
138
149
  bind:value={textInput}
139
150
  onkeydown={handleTextKeydown}
140
151
  disabled={atMax}
@@ -142,7 +153,7 @@
142
153
  />
143
154
  <Button size="sm" type="button" variant="outline" disabled={atMax || !textInput.trim()} onclick={addTextItem}>
144
155
  <CirclePlus class="h-4 w-4" />
145
- Dodaj
156
+ {at.add}
146
157
  </Button>
147
158
  </div>
148
159
 
@@ -167,7 +178,7 @@
167
178
  type="button"
168
179
  class="text-[#555566] hover:text-[#C44B4B] transition-colors"
169
180
  onclick={() => removeItem(index)}
170
- aria-label="Usuń element"
181
+ aria-label={at.removeItem}
171
182
  >
172
183
  <X class="h-3.5 w-3.5" />
173
184
  </button>
@@ -179,7 +190,7 @@
179
190
  <div class="flex items-center gap-2">
180
191
  <Input
181
192
  type="number"
182
- placeholder="Wpisz liczbę..."
193
+ placeholder={at.typeNumber}
183
194
  bind:value={numberInput}
184
195
  onkeydown={handleNumberKeydown}
185
196
  disabled={atMax}
@@ -187,7 +198,7 @@
187
198
  />
188
199
  <Button size="sm" type="button" variant="outline" disabled={atMax || numberInput === ''} onclick={addNumberItem}>
189
200
  <CirclePlus class="h-4 w-4" />
190
- Dodaj
201
+ {at.add}
191
202
  </Button>
192
203
  </div>
193
204
 
@@ -209,7 +220,7 @@
209
220
  <div class="flex-1 space-y-2">
210
221
  <Input
211
222
  type="url"
212
- placeholder="URL..."
223
+ placeholder={at.urlPlaceholder}
213
224
  value={typeof urlItem.url === 'string' ? urlItem.url : ''}
214
225
  oninput={(e) => {
215
226
  const val = e.currentTarget.value;
@@ -222,7 +233,7 @@
222
233
  <div class="flex items-center gap-2">
223
234
  <Input
224
235
  type="text"
225
- placeholder="Tekst linku..."
236
+ placeholder={at.linkText}
226
237
  class="flex-1"
227
238
  value={typeof urlItem.text === 'string' ? urlItem.text : ''}
228
239
  oninput={(e) => {
@@ -245,7 +256,7 @@
245
256
  }}
246
257
  class="accent-[#5B4A9E]"
247
258
  />
248
- Nowa karta
259
+ {at.newTab}
249
260
  </label>
250
261
  </div>
251
262
  </div>
@@ -253,7 +264,7 @@
253
264
  type="button"
254
265
  class="mt-1.5 text-[#555566] hover:text-[#C44B4B] transition-colors"
255
266
  onclick={() => removeItem(index)}
256
- aria-label="Usuń link"
267
+ aria-label={at.removeLink}
257
268
  >
258
269
  <X class="h-4 w-4" />
259
270
  </button>
@@ -264,7 +275,7 @@
264
275
 
265
276
  <Button size="sm" type="button" variant="outline" disabled={atMax} onclick={addUrlItem}>
266
277
  <CirclePlus class="h-4 w-4" />
267
- Dodaj link
278
+ {at.addLink}
268
279
  </Button>
269
280
 
270
281
  {#if field.maxItems !== undefined}
@@ -150,10 +150,12 @@
150
150
  </div>
151
151
 
152
152
  {:else if node.type === 'card'}
153
- <div role="group" aria-label={getLabel(node)} class="layout-card">
154
- <div class="layout-card-header">
155
- {getLabel(node)}
156
- </div>
153
+ <div role="group" aria-label={getLabel(node) || undefined} class="layout-card" class:no-header={!getLabel(node)}>
154
+ {#if getLabel(node)}
155
+ <div class="layout-card-header">
156
+ {getLabel(node)}
157
+ </div>
158
+ {/if}
157
159
  <div class="layout-card-body">
158
160
  {#if isLayoutLeaf(node) && node.autoGrid}
159
161
  <div class="layout-auto-grid">
@@ -268,6 +270,10 @@
268
270
  padding: 10px 16px 16px;
269
271
  }
270
272
 
273
+ .layout-card.no-header .layout-card-body {
274
+ padding-top: 16px;
275
+ }
276
+
271
277
  /* ═══════════ ACCORDION ═══════════ */
272
278
  .layout-accordion-wrapper {
273
279
  background: var(--card);
@@ -5,6 +5,15 @@
5
5
  import VideoOff from '@tabler/icons-svelte/icons/video-off';
6
6
 
7
7
  import { getRemotes } from '../../context/remotes.js';
8
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
9
+ import type { InterfaceLanguage } from '../../../types/languages.js';
10
+
11
+ const interfaceLanguage = useInterfaceLanguage();
12
+ const previewLang: Record<InterfaceLanguage, { loading: string }> = {
13
+ pl: { loading: 'Ładowanie...' },
14
+ en: { loading: 'Loading...' }
15
+ };
16
+ const pvt = $derived(previewLang[interfaceLanguage.current]);
8
17
 
9
18
  const remotes = getRemotes();
10
19
 
@@ -20,7 +29,7 @@
20
29
 
21
30
  <div class="border">
22
31
  {#if !fileQuery.ready}
23
- Loading...
32
+ {pvt.loading}
24
33
  {:else if fileQuery.current}
25
34
  {@const file = fileQuery.current}
26
35
  {#if file.type === 'image'}
@@ -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;