includio-cms 0.7.2 → 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 (185) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/ROADMAP.md +54 -2
  3. package/dist/admin/api/generate-styles.d.ts +2 -0
  4. package/dist/admin/api/generate-styles.js +32 -0
  5. package/dist/admin/api/handler.js +33 -0
  6. package/dist/admin/api/media-gc.js +10 -4
  7. package/dist/admin/api/rest/handler.js +17 -0
  8. package/dist/admin/api/rest/routes/collections.js +25 -13
  9. package/dist/admin/api/rest/routes/entries.d.ts +1 -1
  10. package/dist/admin/api/rest/routes/entries.js +10 -10
  11. package/dist/admin/api/rest/routes/media.d.ts +2 -0
  12. package/dist/admin/api/rest/routes/media.js +9 -0
  13. package/dist/admin/api/rest/routes/schema.d.ts +5 -0
  14. package/dist/admin/api/rest/routes/schema.js +152 -0
  15. package/dist/admin/api/rest/routes/singletons.d.ts +1 -1
  16. package/dist/admin/api/rest/routes/singletons.js +8 -7
  17. package/dist/admin/api/rest/routes/upload.d.ts +2 -0
  18. package/dist/admin/api/rest/routes/upload.js +28 -0
  19. package/dist/admin/api/upload.js +13 -0
  20. package/dist/admin/client/collection/collection-entries.svelte +35 -13
  21. package/dist/admin/client/entry/entry.svelte +21 -23
  22. package/dist/admin/client/entry/header/a11y-validator.js +2 -2
  23. package/dist/admin/client/entry/header/publish-panel.svelte +33 -85
  24. package/dist/admin/client/entry/header/status-badge.svelte +2 -2
  25. package/dist/admin/client/entry/header/version-history-sheet.svelte +9 -9
  26. package/dist/admin/client/entry/header/visibility.svelte +16 -10
  27. package/dist/admin/client/entry/utils.d.ts +3 -0
  28. package/dist/admin/client/entry/utils.js +22 -4
  29. package/dist/admin/client/form/form-submission/form-submission-page.svelte +4 -1
  30. package/dist/admin/client/form/form-submission/submission-field.svelte +10 -0
  31. package/dist/admin/client/index.d.ts +1 -0
  32. package/dist/admin/client/index.js +1 -0
  33. package/dist/admin/client/maintenance/maintenance-page.svelte +146 -2
  34. package/dist/admin/client/users/users-page.svelte +5 -6
  35. package/dist/admin/client/users/users-page.svelte.d.ts +1 -4
  36. package/dist/admin/components/fields/block-picker-modal.svelte +13 -4
  37. package/dist/admin/components/fields/blocks-field.svelte +40 -19
  38. package/dist/admin/components/fields/field-renderer.svelte +4 -8
  39. package/dist/admin/components/fields/object-field.svelte +7 -12
  40. package/dist/admin/components/fields/select-field.svelte +8 -2
  41. package/dist/admin/components/fields/seo-field.svelte +40 -93
  42. package/dist/admin/components/fields/simple-array-field.svelte +27 -16
  43. package/dist/admin/components/fields/text-field-wrapper.svelte +52 -197
  44. package/dist/admin/components/fields/text-field-wrapper.svelte.d.ts +2 -2
  45. package/dist/admin/components/fields/url-field-wrapper.svelte +15 -25
  46. package/dist/admin/components/fields/url-field.svelte +61 -72
  47. package/dist/admin/components/layout/layout-renderer.svelte +10 -4
  48. package/dist/admin/components/media/file-preview.svelte +10 -1
  49. package/dist/admin/components/media/file-upload.svelte +5 -1
  50. package/dist/admin/components/media/file-upload.svelte.d.ts +1 -0
  51. package/dist/admin/components/media/files-list.svelte +12 -3
  52. package/dist/admin/components/media/media-library.svelte +109 -37
  53. package/dist/admin/components/media/media-selector.svelte +90 -16
  54. package/dist/admin/components/media/tag-sidebar.svelte +10 -6
  55. package/dist/admin/components/media/tag-sidebar.svelte.d.ts +7 -2
  56. package/dist/admin/components/tiptap/FigureNodeView.svelte +15 -10
  57. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +53 -94
  58. package/dist/admin/components/tiptap/SlashCommandPopup.svelte +8 -3
  59. package/dist/admin/components/tiptap/editor-toolbar.svelte +28 -23
  60. package/dist/admin/components/tiptap/image-dialog.svelte +12 -7
  61. package/dist/admin/components/tiptap/inline-block-node.js +6 -5
  62. package/dist/admin/components/tiptap/lang.d.ts +77 -0
  63. package/dist/admin/components/tiptap/lang.js +170 -0
  64. package/dist/admin/components/tiptap/link-dialog.svelte +31 -28
  65. package/dist/admin/components/tiptap/slash-command.js +27 -23
  66. package/dist/admin/components/tiptap/table-dialog.svelte +9 -4
  67. package/dist/admin/components/tiptap/video-dialog.svelte +6 -1
  68. package/dist/admin/remote/email.remote.d.ts +1 -0
  69. package/dist/admin/remote/email.remote.js +5 -0
  70. package/dist/admin/remote/entry.remote.d.ts +2 -5
  71. package/dist/admin/remote/entry.remote.js +23 -28
  72. package/dist/admin/remote/index.d.ts +1 -0
  73. package/dist/admin/remote/index.js +1 -0
  74. package/dist/admin/remote/media.remote.d.ts +15 -0
  75. package/dist/admin/remote/media.remote.js +18 -2
  76. package/dist/admin/remote/preview.remote.js +3 -1
  77. package/dist/admin/utils/entryLabel.js +9 -6
  78. package/dist/admin/utils/translationStatus.js +1 -2
  79. package/dist/cli/scaffold/admin.js +34 -2
  80. package/dist/cms/runtime/api.d.ts +16 -12
  81. package/dist/cms/runtime/api.js +7 -6
  82. package/dist/cms/runtime/remote.js +2 -2
  83. package/dist/cms/runtime/schemas.d.ts +1 -1
  84. package/dist/cms/runtime/schemas.js +1 -1
  85. package/dist/cms/runtime/types.d.ts +118 -112
  86. package/dist/cms/runtime/types.js +0 -12
  87. package/dist/core/cms.d.ts +3 -1
  88. package/dist/core/cms.js +30 -0
  89. package/dist/core/fields/fieldSchemaToTs.js +9 -15
  90. package/dist/core/fields/formFieldSchemaToTs.js +7 -0
  91. package/dist/core/server/entries/operations/create.js +10 -4
  92. package/dist/core/server/entries/operations/get.d.ts +1 -0
  93. package/dist/core/server/entries/operations/get.js +186 -191
  94. package/dist/core/server/entries/operations/update.d.ts +6 -7
  95. package/dist/core/server/entries/operations/update.js +20 -38
  96. package/dist/core/server/fields/populateEntry.js +16 -52
  97. package/dist/core/server/fields/resolveImageFields.js +69 -120
  98. package/dist/core/server/fields/resolveRelationFields.js +30 -51
  99. package/dist/core/server/fields/resolveRichtextLinks.js +46 -100
  100. package/dist/core/server/fields/resolveTypographyOrphans.bench.d.ts +1 -0
  101. package/dist/core/server/fields/resolveTypographyOrphans.bench.js +87 -0
  102. package/dist/core/server/fields/resolveTypographyOrphans.d.ts +3 -0
  103. package/dist/core/server/fields/resolveTypographyOrphans.js +128 -0
  104. package/dist/core/server/fields/resolveUrlFields.js +47 -56
  105. package/dist/core/server/fields/utils/fixOrphans.d.ts +5 -0
  106. package/dist/core/server/fields/utils/fixOrphans.js +12 -0
  107. package/dist/core/server/fields/utils/imageStyles.d.ts +4 -2
  108. package/dist/core/server/fields/utils/imageStyles.js +41 -25
  109. package/dist/core/server/fields/utils/resolveMedia.js +1 -6
  110. package/dist/core/server/forms/submissions/operations/delete.js +26 -2
  111. package/dist/core/server/forms/submissions/utils/parseMultipart.d.ts +2 -0
  112. package/dist/core/server/forms/submissions/utils/parseMultipart.js +75 -0
  113. package/dist/core/server/generator/fields.d.ts +6 -0
  114. package/dist/core/server/generator/fields.js +43 -5
  115. package/dist/core/server/generator/formFieldSchemaToString.js +10 -0
  116. package/dist/core/server/generator/formFields.js +1 -0
  117. package/dist/core/server/generator/generator.js +98 -30
  118. package/dist/core/server/media/operations/getFiles.d.ts +5 -0
  119. package/dist/core/server/media/operations/getFiles.js +6 -0
  120. package/dist/core/server/media/operations/uploadPrivateFile.d.ts +4 -0
  121. package/dist/core/server/media/operations/uploadPrivateFile.js +8 -0
  122. package/dist/core/server/media/styles/operations/batchGenerateStyles.d.ts +16 -0
  123. package/dist/core/server/media/styles/operations/batchGenerateStyles.js +144 -0
  124. package/dist/db-postgres/index.js +303 -37
  125. package/dist/db-postgres/schema/entry.d.ts +0 -94
  126. package/dist/db-postgres/schema/entry.js +0 -6
  127. package/dist/db-postgres/schema/entryVersion.d.ts +17 -0
  128. package/dist/db-postgres/schema/entryVersion.js +1 -0
  129. package/dist/entity/index.d.ts +9 -4
  130. package/dist/entity/index.js +24 -24
  131. package/dist/files-local/index.js +43 -0
  132. package/dist/paraglide/messages/_index.d.ts +36 -3
  133. package/dist/paraglide/messages/_index.js +71 -3
  134. package/dist/paraglide/messages/en.d.ts +5 -0
  135. package/dist/paraglide/messages/en.js +14 -0
  136. package/dist/paraglide/messages/pl.d.ts +5 -0
  137. package/dist/paraglide/messages/pl.js +14 -0
  138. package/dist/sveltekit/components/preview.svelte +2 -326
  139. package/dist/sveltekit/components/preview.svelte.d.ts +5 -16
  140. package/dist/sveltekit/server/index.d.ts +2 -1
  141. package/dist/sveltekit/server/index.js +2 -1
  142. package/dist/sveltekit/server/preview.js +4 -7
  143. package/dist/types/adapters/db.d.ts +15 -1
  144. package/dist/types/adapters/files.d.ts +6 -0
  145. package/dist/types/cms.d.ts +5 -0
  146. package/dist/types/entries.d.ts +54 -18
  147. package/dist/types/fields.d.ts +14 -24
  148. package/dist/types/formFields.d.ts +7 -2
  149. package/dist/types/index.d.ts +2 -2
  150. package/dist/types/layout.d.ts +0 -1
  151. package/dist/types/structured-content.d.ts +5 -0
  152. package/dist/updates/0.10.0/index.d.ts +2 -0
  153. package/dist/updates/0.10.0/index.js +15 -0
  154. package/dist/updates/0.11.0/index.d.ts +2 -0
  155. package/dist/updates/0.11.0/index.js +12 -0
  156. package/dist/updates/0.12.0/index.d.ts +2 -0
  157. package/dist/updates/0.12.0/index.js +12 -0
  158. package/dist/updates/0.13.0/index.d.ts +2 -0
  159. package/dist/updates/0.13.0/index.js +10 -0
  160. package/dist/updates/0.13.1/index.d.ts +2 -0
  161. package/dist/updates/0.13.1/index.js +20 -0
  162. package/dist/updates/0.7.3/index.d.ts +2 -0
  163. package/dist/updates/0.7.3/index.js +10 -0
  164. package/dist/updates/0.8.0/index.d.ts +2 -0
  165. package/dist/updates/0.8.0/index.js +18 -0
  166. package/dist/updates/0.8.0/migrate.d.ts +2 -0
  167. package/dist/updates/0.8.0/migrate.js +101 -0
  168. package/dist/updates/0.9.0/index.d.ts +2 -0
  169. package/dist/updates/0.9.0/index.js +38 -0
  170. package/dist/updates/index.js +9 -1
  171. package/package.json +7 -6
  172. package/dist/admin/components/fields/image-field.svelte +0 -198
  173. package/dist/admin/components/fields/image-field.svelte.d.ts +0 -8
  174. package/dist/admin/components/fields/richtext-field.svelte +0 -13
  175. package/dist/admin/components/fields/richtext-field.svelte.d.ts +0 -8
  176. package/dist/admin/components/tiptap.svelte +0 -11
  177. package/dist/admin/components/tiptap.svelte.d.ts +0 -6
  178. package/dist/core/server/entries/utils/getEntryTranslation.d.ts +0 -1
  179. package/dist/core/server/entries/utils/getEntryTranslation.js +0 -18
  180. package/dist/paraglide/messages/hello_world.d.ts +0 -5
  181. package/dist/paraglide/messages/hello_world.js +0 -33
  182. package/dist/paraglide/messages/login_hello.d.ts +0 -16
  183. package/dist/paraglide/messages/login_hello.js +0 -34
  184. package/dist/paraglide/messages/login_please_login.d.ts +0 -16
  185. package/dist/paraglide/messages/login_please_login.js +0 -34
@@ -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();
@@ -66,8 +76,8 @@
66
76
  }
67
77
 
68
78
  $value = $value.reduce((acc: ObjectFieldData[], item: ObjectFieldData) => {
69
- if (!item.slug) return acc;
70
- if (field.of.find((o) => o.slug === item.slug) === undefined) return acc;
79
+ if (!item._slug) return acc;
80
+ if (field.of.find((o) => o.slug === item._slug) === undefined) return acc;
71
81
  acc.push(item);
72
82
  return acc;
73
83
  }, [] as ObjectFieldData[]);
@@ -81,7 +91,7 @@
81
91
  if (defaults && defaults[i]) {
82
92
  current.push({ _id: generateId(), ...JSON.parse(JSON.stringify(defaults[i])) });
83
93
  } else {
84
- current.push({ _id: generateId(), slug: field.of[0].slug, data: {} });
94
+ current.push({ _id: generateId(), _slug: field.of[0].slug });
85
95
  }
86
96
  }
87
97
  } else {
@@ -97,8 +107,7 @@
97
107
  ...($value ?? []),
98
108
  {
99
109
  _id: generateId(),
100
- slug: field.slug,
101
- data: {}
110
+ _slug: field.slug
102
111
  }
103
112
  ];
104
113
 
@@ -161,20 +170,32 @@
161
170
  }
162
171
 
163
172
  function getAccordionLabel(item: ObjectFieldData) {
164
- const objectConfig = field.of.find((o) => o.slug === item.slug) as ObjectFieldType;
173
+ const objectConfig = field.of.find((o) => o.slug === item._slug) as ObjectFieldType;
165
174
 
166
175
  if (
167
176
  objectConfig.accordionLabelField &&
168
177
  typeof objectConfig.accordionLabelField === 'string' &&
169
178
  objectConfig.accordionLabelField.trim().length > 0
170
179
  ) {
171
- const label = item.data[objectConfig.accordionLabelField] as string | Record<string, string>;
180
+ const label = item[objectConfig.accordionLabelField] as string | Record<string, string>;
172
181
 
173
182
  if (typeof label === 'string' && label.trim().length > 0) {
174
183
  return `${label}`;
175
184
  }
176
185
 
177
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
+
178
199
  const objectLabel = label as Record<string, string>;
179
200
  return `${objectLabel[contentLanguage.current]}`;
180
201
  }
@@ -230,7 +251,7 @@
230
251
  >{getLocalizedLabel(field.label, interfaceLanguage.current)}</RequiredLabel
231
252
  >
232
253
  {#if isFixedLength}
233
- <span class="text-muted-foreground text-xs">{fixedCount} elementów</span>
254
+ <span class="text-muted-foreground text-xs">{fixedCount} {bt.elements}</span>
234
255
  {:else if field.maxItems !== undefined}
235
256
  <span class="text-xs {atMax ? 'text-destructive' : 'text-muted-foreground'}"
236
257
  >{$value?.length ?? 0} / {field.maxItems}</span
@@ -245,7 +266,7 @@
245
266
  variant="ghost"
246
267
  onclick={() => {
247
268
  accordionOpenState = [];
248
- }}>Collapse All</Button
269
+ }}>{bt.collapseAll}</Button
249
270
  >
250
271
  <Button
251
272
  size="sm"
@@ -255,7 +276,7 @@
255
276
  if ($value) {
256
277
  accordionOpenState = $value.map((_, i) => i.toString());
257
278
  }
258
- }}>Show All</Button
279
+ }}>{bt.showAll}</Button
259
280
  >
260
281
  </div>
261
282
  </div>
@@ -294,9 +315,9 @@
294
315
  animate:flip={{ duration: 200 }}
295
316
  >
296
317
  {#key index}
297
- {#if $value[index].data && $value[index].slug}
318
+ {#if $value[index]._slug}
298
319
  {@const item = $value[index]}
299
- {@const objectField = field.of.find((option) => option.slug === item.slug)}
320
+ {@const objectField = field.of.find((option) => option.slug === item._slug)}
300
321
 
301
322
  {#if objectField}
302
323
  <Accordion.Item value={index.toString()} class="overflow-hidden rounded-md border-0" data-depth={depth + 1}>
@@ -335,7 +356,7 @@
335
356
  {#snippet child({ props })}
336
357
  <Button variant="ghost" size="icon" {...props}>
337
358
  <DotsVerticalIcon />
338
- <span class="sr-only">Open menu</span>
359
+ <span class="sr-only">{bt.openMenu}</span>
339
360
  </Button>
340
361
  {/snippet}
341
362
  </DropdownMenu.Trigger>
@@ -345,27 +366,27 @@
345
366
  onclick={() => duplicateItem(index)}
346
367
  disabled={atMax}
347
368
  >
348
- Duplicate
369
+ {bt.duplicate}
349
370
  </DropdownMenu.Item>
350
371
  {/if}
351
372
  <DropdownMenu.Item
352
373
  onclick={() => moveItemUp(index)}
353
374
  disabled={index === 0}
354
375
  >
355
- Move up
376
+ {bt.moveUp}
356
377
  </DropdownMenu.Item>
357
378
  <DropdownMenu.Item
358
379
  onclick={() => moveItemDown(index)}
359
380
  disabled={$value && index === $value.length - 1}
360
381
  >
361
- Move down
382
+ {bt.moveDown}
362
383
  </DropdownMenu.Item>
363
384
  {#if !isFixedLength}
364
385
  <DropdownMenu.Item
365
386
  variant="destructive"
366
387
  onclick={() => removeItem(index)}
367
388
  >
368
- Delete
389
+ {bt.delete}
369
390
  </DropdownMenu.Item>
370
391
  {/if}
371
392
  </DropdownMenu.Content>
@@ -391,7 +412,7 @@
391
412
  {:else}
392
413
  <p class="text-red-500">
393
414
  Invalid field configuration. Unknown slug:
394
- {$value[index].slug}
415
+ {$value[index]._slug}
395
416
  </p>
396
417
  {/if}
397
418
  {:else}
@@ -414,7 +435,7 @@
414
435
  onclick={() => (blockPickerOpen = true)}
415
436
  >
416
437
  <CirclePlus />
417
- Add Block
438
+ {bt.addBlock}
418
439
  </Button>
419
440
  </div>
420
441
  <BlockPickerModal
@@ -41,10 +41,10 @@
41
41
  const fieldsWithNoDescription: FieldType[] = ['boolean', 'object', 'blocks', 'seo'];
42
42
  const fieldsWithNoLabel: FieldType[] = ['boolean', 'object', 'blocks', 'seo'];
43
43
 
44
- const fieldsWithAlternativeDescription: FieldType[] = ['image', 'media', 'object', 'blocks'];
44
+ const fieldsWithAlternativeDescription: FieldType[] = ['media', 'object', 'blocks'];
45
45
 
46
- function isTextField(field: Field): field is Extract<Field, { type: 'text' | 'richtext' | 'content' }> {
47
- return ['text', 'richtext', 'content'].includes(field.type);
46
+ function isTextField(field: Field): field is Extract<Field, { type: 'text' | 'content' }> {
47
+ return ['text', 'content'].includes(field.type);
48
48
  }
49
49
 
50
50
  function isRadioField(field: Field): field is Extract<Field, { type: 'radio' }> {
@@ -121,11 +121,7 @@
121
121
  <Form.Description>{getLocalizedLabel(field.description, interfaceLanguage.current)}</Form.Description>
122
122
  {/if}
123
123
 
124
- {#if field.type === 'image'}
125
- {#await import('./image-field.svelte') then { default: ImageField }}
126
- <ImageField {field} bind:value={$value} />
127
- {/await}
128
- {:else if field.type === 'media'}
124
+ {#if field.type === 'media'}
129
125
  {#await import('./media-field.svelte') then { default: MediaField }}
130
126
  <MediaField {field} bind:value={$value} />
131
127
  {/await}
@@ -32,21 +32,16 @@
32
32
 
33
33
  onMount(() => {
34
34
  if (!$value) {
35
- $value = { slug: field.slug, data: {} };
35
+ $value = { _slug: field.slug };
36
36
  return;
37
37
  }
38
38
 
39
- if (!$value.slug) {
40
- $value.slug = field.slug;
39
+ if (!$value._slug) {
40
+ $value._slug = field.slug;
41
41
  }
42
42
 
43
- if ($value.slug !== field.slug) {
44
- $value.slug = field.slug;
45
- $value.data = {};
46
- }
47
-
48
- if (typeof $value.data !== 'object' || $value.data === null) {
49
- $value.data = {};
43
+ if ($value._slug !== field.slug) {
44
+ $value = { _slug: field.slug };
50
45
  }
51
46
  });
52
47
 
@@ -59,8 +54,8 @@
59
54
  {#snippet content()}
60
55
  <div class="space-y-4">
61
56
  {#each field.fields as f}
62
- {#if evaluateCondition(f.showWhen, (slug) => $value?.data?.[slug] ?? $formData[slug])}
63
- {@const fieldPath = joinPath(path, 'data', f.slug)}
57
+ {#if evaluateCondition(f.showWhen, (slug) => $value?.[slug] ?? $formData[slug])}
58
+ {@const fieldPath = joinPath(path, f.slug)}
64
59
  {@const showFlash = isFlashing(fieldPath)}
65
60
  <div
66
61
  data-field-path={fieldPath}
@@ -30,8 +30,14 @@
30
30
  }
31
31
 
32
32
  onMount(() => {
33
- if (value === undefined) {
34
- value = field.multiple ? [] : '';
33
+ if (field.multiple) {
34
+ if (value === undefined) {
35
+ value = [];
36
+ } else if (typeof value === 'string') {
37
+ value = value ? [value] : [];
38
+ }
39
+ } else if (value === undefined) {
40
+ value = '';
35
41
  }
36
42
  });
37
43
  </script>
@@ -5,7 +5,7 @@
5
5
  import { getAtPath, setAtPath } from '../../utils/objectPath.js';
6
6
  import type {
7
7
  Field,
8
- ImageField,
8
+ MediaField,
9
9
  SeoField,
10
10
  SeoFieldData,
11
11
  TextField
@@ -13,12 +13,10 @@
13
13
  import { untrack } from 'svelte';
14
14
  import slugify from '../../imports/slugify.js';
15
15
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
16
- import { getContentLanguage } from '../../state/content-language.svelte.js';
17
16
  import { getLocalizedLabel } from '../../utils/collectionLabel.js';
18
17
  import { Switch } from '../../../components/ui/switch/index.js';
19
18
 
20
19
  const interfaceLanguage = useInterfaceLanguage();
21
- const contentLanguage = getContentLanguage();
22
20
 
23
21
  type Props = {
24
22
  field: SeoField;
@@ -124,11 +122,12 @@
124
122
  multiline: true
125
123
  };
126
124
 
127
- const ogImageField: ImageField = {
128
- type: 'image',
125
+ const ogImageField: MediaField = {
126
+ type: 'media',
129
127
  slug: 'ogImage',
130
128
  label: labels.ogImage.label,
131
129
  description: labels.ogImage.description,
130
+ accept: 'image/*',
132
131
  required: false
133
132
  };
134
133
 
@@ -150,29 +149,19 @@
150
149
  customCodeField
151
150
  ];
152
151
 
153
- // Helper: read a value from formData, handling both localized (object) and plain (string) values
154
- function readSourceValue(sourcePath: string): string | undefined {
155
- const raw = ($formData as Record<string, unknown>)[sourcePath];
156
- if (typeof raw === 'string') return raw;
157
- if (raw && typeof raw === 'object') {
158
- return (raw as Record<string, string>)[contentLanguage.current];
159
- }
160
- return undefined;
161
- }
162
-
163
152
  // Helper: read value at a dot-path from formData
164
153
  function readPath(dotPath: string): string | undefined {
165
154
  const val = getAtPath($formData as Record<string, unknown>, dotPath);
166
155
  return typeof val === 'string' ? val : undefined;
167
156
  }
168
157
 
169
- // Character count for current language
158
+ // Character count data is flat, no lang nesting
170
159
  let titleLength = $derived.by(() => {
171
- const val = readPath(joinPath(String(path), 'title', contentLanguage.current));
160
+ const val = readPath(joinPath(String(path), 'title'));
172
161
  return val?.length ?? 0;
173
162
  });
174
163
  let descLength = $derived.by(() => {
175
- const val = readPath(joinPath(String(path), 'description', contentLanguage.current));
164
+ const val = readPath(joinPath(String(path), 'description'));
176
165
  return val?.length ?? 0;
177
166
  });
178
167
 
@@ -183,109 +172,67 @@
183
172
  return 'text-destructive';
184
173
  }
185
174
 
186
- // Auto-gen: track last auto-generated values per language
187
- let lastAutoSlugs: Record<string, string> = {};
188
- let lastAutoTitles: Record<string, string> = {};
175
+ // Auto-gen: track last auto-generated value
176
+ let lastAutoSlug = '';
177
+ let lastAutoTitle = '';
189
178
 
190
- // Auto slug toggle: ON by default when slugSource is set
191
- // Initializes to false if existing slug differs from what auto-gen would produce
179
+ // Auto slug toggle
192
180
  let autoSlug = $state((() => {
193
181
  if (!field.slugSource) return false;
194
182
  const sourceRaw = ($formData as Record<string, unknown>)[field.slugSource];
195
- if (!sourceRaw) return true;
196
- const pairs: [string, string][] =
197
- typeof sourceRaw === 'string'
198
- ? [[contentLanguage.current, sourceRaw]]
199
- : typeof sourceRaw === 'object'
200
- ? Object.entries(sourceRaw as Record<string, string>).filter(([, v]) => typeof v === 'string' && v)
201
- : [];
202
- for (const [lang, text] of pairs) {
203
- const targetPath = joinPath(String(path), 'slug', lang);
204
- const current = getAtPath($formData as Record<string, unknown>, targetPath) as string | undefined;
205
- const expected = slugify(String(text), { lower: true, strict: true, trim: true });
206
- if (current != null && current !== expected) return false;
207
- }
183
+ if (!sourceRaw || typeof sourceRaw !== 'string') return true;
184
+ const slugPath = joinPath(String(path), 'slug');
185
+ const current = getAtPath($formData as Record<string, unknown>, slugPath) as string | undefined;
186
+ const expected = slugify(sourceRaw, { lower: true, strict: true, trim: true });
187
+ if (current != null && current !== expected) return false;
208
188
  return true;
209
189
  })());
210
190
 
211
- // slugSource → auto-gen seo.slug per language
191
+ // slugSource → auto-gen seo.slug (flat, no per-lang)
212
192
  $effect(() => {
213
193
  if (!field.slugSource || !autoSlug) return;
214
194
  const sourceRaw = ($formData as Record<string, unknown>)[field.slugSource];
215
- if (!sourceRaw) return;
195
+ if (!sourceRaw || typeof sourceRaw !== 'string') return;
216
196
 
217
197
  untrack(() => {
218
- const pairs: [string, string][] =
219
- typeof sourceRaw === 'string'
220
- ? [[contentLanguage.current, sourceRaw]]
221
- : typeof sourceRaw === 'object'
222
- ? Object.entries(sourceRaw as Record<string, string>).filter(([, v]) => typeof v === 'string' && v)
223
- : [];
224
-
225
- let changed = false;
226
- for (const [lang, text] of pairs) {
227
- const targetPath = joinPath(String(path), 'slug', lang);
228
- const current = getAtPath($formData as Record<string, unknown>, targetPath) as string | undefined;
229
- const newSlug = slugify(String(text), { lower: true, strict: true, trim: true });
230
- if (newSlug !== current) {
231
- setAtPath($formData as Record<string, unknown>, targetPath, newSlug);
232
- changed = true;
233
- }
234
- lastAutoSlugs[lang] = newSlug;
198
+ const slugPath = joinPath(String(path), 'slug');
199
+ const current = getAtPath($formData as Record<string, unknown>, slugPath) as string | undefined;
200
+ const newSlug = slugify(sourceRaw, { lower: true, strict: true, trim: true });
201
+ if (newSlug !== current) {
202
+ setAtPath($formData as Record<string, unknown>, slugPath, newSlug);
203
+ $formData = $formData;
235
204
  }
236
- if (changed) $formData = $formData;
205
+ lastAutoSlug = newSlug;
237
206
  });
238
207
  });
239
208
 
240
209
  function onAutoSlugToggle(checked: boolean) {
241
210
  if (!checked || !field.slugSource) return;
242
- // Regenerate slug from current source when toggling ON
243
211
  const sourceRaw = ($formData as Record<string, unknown>)[field.slugSource];
244
- if (!sourceRaw) return;
245
- const pairs: [string, string][] =
246
- typeof sourceRaw === 'string'
247
- ? [[contentLanguage.current, sourceRaw]]
248
- : typeof sourceRaw === 'object'
249
- ? Object.entries(sourceRaw as Record<string, string>).filter(([, v]) => typeof v === 'string' && v)
250
- : [];
251
- let changed = false;
252
- for (const [lang, text] of pairs) {
253
- const targetPath = joinPath(String(path), 'slug', lang);
254
- const newSlug = slugify(String(text), { lower: true, strict: true, trim: true });
255
- setAtPath($formData as Record<string, unknown>, targetPath, newSlug);
256
- lastAutoSlugs[lang] = newSlug;
257
- changed = true;
258
- }
259
- if (changed) $formData = $formData;
212
+ if (!sourceRaw || typeof sourceRaw !== 'string') return;
213
+ const slugPath = joinPath(String(path), 'slug');
214
+ const newSlug = slugify(sourceRaw, { lower: true, strict: true, trim: true });
215
+ setAtPath($formData as Record<string, unknown>, slugPath, newSlug);
216
+ lastAutoSlug = newSlug;
217
+ $formData = $formData;
260
218
  }
261
219
 
262
- // titleSource → auto-fill seo.title per language
220
+ // titleSource → auto-fill seo.title (flat)
263
221
  $effect(() => {
264
222
  if (!field.titleSource) return;
265
223
  const sourceRaw = ($formData as Record<string, unknown>)[field.titleSource];
266
- if (!sourceRaw) return;
224
+ if (!sourceRaw || typeof sourceRaw !== 'string') return;
267
225
 
268
226
  untrack(() => {
269
- const pairs: [string, string][] =
270
- typeof sourceRaw === 'string'
271
- ? [[contentLanguage.current, sourceRaw]]
272
- : typeof sourceRaw === 'object'
273
- ? Object.entries(sourceRaw as Record<string, string>).filter(([, v]) => typeof v === 'string' && v)
274
- : [];
275
-
276
- let changed = false;
277
- for (const [lang, text] of pairs) {
278
- const targetPath = joinPath(String(path), 'title', lang);
279
- const current = getAtPath($formData as Record<string, unknown>, targetPath) as string | undefined;
280
- if (!current || current === lastAutoTitles[lang]) {
281
- if (text !== current) {
282
- setAtPath($formData as Record<string, unknown>, targetPath, text);
283
- changed = true;
284
- }
285
- lastAutoTitles[lang] = text;
227
+ const titlePath = joinPath(String(path), 'title');
228
+ const current = getAtPath($formData as Record<string, unknown>, titlePath) as string | undefined;
229
+ if (!current || current === lastAutoTitle) {
230
+ if (sourceRaw !== current) {
231
+ setAtPath($formData as Record<string, unknown>, titlePath, sourceRaw);
232
+ $formData = $formData;
286
233
  }
234
+ lastAutoTitle = sourceRaw;
287
235
  }
288
- if (changed) $formData = $formData;
289
236
  });
290
237
  });
291
238
  </script>
@@ -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;
@@ -68,7 +79,7 @@
68
79
  // --- URL ---
69
80
  function addUrlItem() {
70
81
  if (atMax) return;
71
- const item: UrlFieldData = { url: {}, text: {}, newTab: false };
82
+ const item: UrlFieldData = { url: '', text: '', newTab: false };
72
83
  value = [...(value ?? []), item];
73
84
  }
74
85
 
@@ -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,11 +220,11 @@
209
220
  <div class="flex-1 space-y-2">
210
221
  <Input
211
222
  type="url"
212
- placeholder="URL..."
213
- value={urlItem.url?.[contentLanguage.current] ?? ''}
223
+ placeholder={at.urlPlaceholder}
224
+ value={typeof urlItem.url === 'string' ? urlItem.url : ''}
214
225
  oninput={(e) => {
215
226
  const val = e.currentTarget.value;
216
- const updated = { ...urlItem, url: { ...(urlItem.url ?? {}), [contentLanguage.current]: val } };
227
+ const updated = { ...urlItem, url: val };
217
228
  const arr = [...(value ?? [])];
218
229
  arr[index] = updated;
219
230
  value = arr;
@@ -222,12 +233,12 @@
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
- value={urlItem.text?.[contentLanguage.current] ?? ''}
238
+ value={typeof urlItem.text === 'string' ? urlItem.text : ''}
228
239
  oninput={(e) => {
229
240
  const val = e.currentTarget.value;
230
- const updated = { ...urlItem, text: { ...(urlItem.text ?? {}), [contentLanguage.current]: val } };
241
+ const updated = { ...urlItem, text: val };
231
242
  const arr = [...(value ?? [])];
232
243
  arr[index] = updated;
233
244
  value = arr;
@@ -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}