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.
- package/CHANGELOG.md +18 -0
- package/ROADMAP.md +14 -0
- package/dist/admin/client/collection/collection-entries.svelte +17 -8
- 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/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.js +1 -1
- package/dist/admin/remote/index.d.ts +1 -0
- package/dist/admin/remote/index.js +1 -0
- package/dist/core/server/generator/fields.js +2 -2
- package/dist/core/server/generator/generator.js +1 -1
- 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/index.js +2 -1
- 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
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
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>
|
|
43
|
-
<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=
|
|
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">
|
|
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}
|
|
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
|
-
}}>
|
|
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
|
-
}}>
|
|
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">
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
|
|
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">
|
|
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;
|