includio-cms 0.5.2 → 0.5.3

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 (93) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/ROADMAP.md +13 -0
  3. package/dist/admin/client/entry/entry-form.svelte +1 -0
  4. package/dist/admin/client/entry/entry.svelte +130 -123
  5. package/dist/admin/client/entry/hybrid/hybrid-preview.svelte +92 -9
  6. package/dist/admin/components/fields/blocks-field.svelte +142 -112
  7. package/dist/admin/components/fields/blocks-field.svelte.d.ts +10 -30
  8. package/dist/admin/components/fields/boolean-field.svelte +28 -38
  9. package/dist/admin/components/fields/boolean-field.svelte.d.ts +5 -27
  10. package/dist/admin/components/fields/checkboxes-field.svelte +12 -24
  11. package/dist/admin/components/fields/checkboxes-field.svelte.d.ts +5 -27
  12. package/dist/admin/components/fields/content-field.svelte +4 -17
  13. package/dist/admin/components/fields/content-field.svelte.d.ts +5 -27
  14. package/dist/admin/components/fields/date-field.svelte +8 -21
  15. package/dist/admin/components/fields/date-field.svelte.d.ts +5 -27
  16. package/dist/admin/components/fields/datetime-field.svelte +8 -21
  17. package/dist/admin/components/fields/datetime-field.svelte.d.ts +5 -27
  18. package/dist/admin/components/fields/field-renderer.svelte +32 -19
  19. package/dist/admin/components/fields/field-renderer.svelte.d.ts +1 -1
  20. package/dist/admin/components/fields/field-value-bridge.svelte +21 -0
  21. package/dist/admin/components/fields/field-value-bridge.svelte.d.ts +31 -0
  22. package/dist/admin/components/fields/fields-form.svelte +13 -10
  23. package/dist/admin/components/fields/file-field.svelte +12 -27
  24. package/dist/admin/components/fields/file-field.svelte.d.ts +5 -27
  25. package/dist/admin/components/fields/image-field.svelte +13 -28
  26. package/dist/admin/components/fields/image-field.svelte.d.ts +5 -27
  27. package/dist/admin/components/fields/media-field.svelte +15 -30
  28. package/dist/admin/components/fields/media-field.svelte.d.ts +5 -27
  29. package/dist/admin/components/fields/number-field.svelte +6 -20
  30. package/dist/admin/components/fields/number-field.svelte.d.ts +5 -27
  31. package/dist/admin/components/fields/object-field.svelte +26 -29
  32. package/dist/admin/components/fields/object-field.svelte.d.ts +11 -31
  33. package/dist/admin/components/fields/radio-field.svelte +8 -20
  34. package/dist/admin/components/fields/radio-field.svelte.d.ts +5 -27
  35. package/dist/admin/components/fields/relation-field.svelte +15 -30
  36. package/dist/admin/components/fields/relation-field.svelte.d.ts +5 -27
  37. package/dist/admin/components/fields/richtext-field.svelte +4 -17
  38. package/dist/admin/components/fields/richtext-field.svelte.d.ts +5 -27
  39. package/dist/admin/components/fields/select-field.svelte +14 -28
  40. package/dist/admin/components/fields/select-field.svelte.d.ts +5 -27
  41. package/dist/admin/components/fields/seo-field.svelte +5 -12
  42. package/dist/admin/components/fields/seo-field.svelte.d.ts +8 -28
  43. package/dist/admin/components/fields/simple-array-field.svelte +29 -42
  44. package/dist/admin/components/fields/simple-array-field.svelte.d.ts +5 -27
  45. package/dist/admin/components/fields/slug-field.svelte +6 -11
  46. package/dist/admin/components/fields/slug-field.svelte.d.ts +6 -26
  47. package/dist/admin/components/fields/text-field-wrapper.svelte +22 -40
  48. package/dist/admin/components/fields/text-field.svelte +7 -19
  49. package/dist/admin/components/fields/text-field.svelte.d.ts +5 -27
  50. package/dist/admin/components/fields/url-field-wrapper.svelte +8 -3
  51. package/dist/admin/components/fields/url-field.svelte +294 -128
  52. package/dist/admin/components/fields/url-field.svelte.d.ts +5 -27
  53. package/dist/admin/components/layout/layout-renderer.svelte +8 -6
  54. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +221 -31
  55. package/dist/admin/components/tiptap/content-editor.svelte +13 -2
  56. package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
  57. package/dist/admin/components/tiptap/inline-block-node.js +18 -1
  58. package/dist/admin/components/tiptap/slash-command.js +2 -3
  59. package/dist/admin/components/tiptap/standalone-form.d.ts +7 -0
  60. package/dist/admin/components/tiptap/standalone-form.js +31 -0
  61. package/dist/admin/components/tiptap/tiptap-editor.svelte +7 -0
  62. package/dist/admin/remote/entry.remote.js +16 -0
  63. package/dist/admin/styles/admin.css +10 -0
  64. package/dist/admin/utils/fieldCondition.d.ts +6 -0
  65. package/dist/admin/utils/fieldCondition.js +20 -0
  66. package/dist/components/ui/switch/index.d.ts +2 -0
  67. package/dist/components/ui/switch/index.js +4 -0
  68. package/dist/components/ui/switch/switch.svelte +26 -0
  69. package/dist/components/ui/switch/switch.svelte.d.ts +4 -0
  70. package/dist/core/fields/fieldSchemaToTs.js +15 -3
  71. package/dist/core/fields/formFieldSchemaToTs.js +22 -6
  72. package/dist/core/fields/urlUtils.d.ts +14 -0
  73. package/dist/core/fields/urlUtils.js +21 -0
  74. package/dist/core/server/fields/populateEntry.js +43 -0
  75. package/dist/core/server/fields/resolveImageFields.js +33 -1
  76. package/dist/core/server/fields/resolveRelationFields.js +46 -0
  77. package/dist/core/server/fields/resolveRichtextLinks.js +15 -1
  78. package/dist/core/server/fields/resolveUrlFields.js +65 -0
  79. package/dist/core/server/generator/formFieldSchemaToString.js +40 -9
  80. package/dist/core/server/generator/formFields.js +2 -0
  81. package/dist/core/server/generator/generator.js +25 -1
  82. package/dist/schemas/field/url.d.ts +2 -0
  83. package/dist/schemas/field/url.js +4 -2
  84. package/dist/types/fields.d.ts +9 -0
  85. package/dist/types/formFields.d.ts +15 -2
  86. package/dist/types/index.d.ts +1 -0
  87. package/dist/types/index.js +1 -0
  88. package/dist/updates/0.5.3/index.d.ts +2 -0
  89. package/dist/updates/0.5.3/index.js +19 -0
  90. package/dist/updates/index.js +2 -1
  91. package/package.json +2 -1
  92. package/dist/admin/components/fields/standalone-field-renderer.svelte +0 -148
  93. package/dist/admin/components/fields/standalone-field-renderer.svelte.d.ts +0 -9
@@ -1,34 +1,42 @@
1
- <script lang="ts" module>
2
- type T = Record<string, unknown>;
3
- </script>
4
-
5
- <script lang="ts" generics="T extends Record<string, unknown>">
1
+ <script lang="ts">
6
2
  import type { SeoFieldData, UrlField, UrlFieldData } from '../../../types/fields.js';
7
- import {
8
- formFieldProxy,
9
- type FormFieldProxy,
10
- type FormPathLeaves,
11
- type SuperForm
12
- } from 'sveltekit-superforms';
13
3
  import { getRemotes } from '../../../sveltekit/index.js';
14
4
  import type { Entry } from '../../../types/entries.js';
15
- import { joinPath } from '../../utils/objectPath.js';
16
- import Input from '../../../components/ui/input/input.svelte';
5
+ import * as InputGroup from '../../../components/ui/input-group/index.js';
6
+ import Badge from '../../../components/ui/badge/badge.svelte';
17
7
  import Checkbox from '../../../components/ui/checkbox/checkbox.svelte';
8
+ import Label from '../../../components/ui/label/label.svelte';
18
9
  import { getContentLanguage } from '../../state/content-language.svelte.js';
19
10
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
20
11
  import { getLocalizedLabel } from '../../utils/collectionLabel.js';
12
+ import { isExternalUrl } from '../../../core/fields/urlUtils.js';
13
+ import GlobeIcon from '@tabler/icons-svelte/icons/world';
21
14
  import LinkIcon from '@tabler/icons-svelte/icons/link';
22
- import ExternalLinkIcon from '@tabler/icons-svelte/icons/external-link';
15
+ import TextIcon from '@tabler/icons-svelte/icons/typography';
23
16
  import XIcon from '@tabler/icons-svelte/icons/x';
17
+ import ExternalLinkIcon from '@tabler/icons-svelte/icons/external-link';
18
+ import ChevronDown from '@tabler/icons-svelte/icons/chevron-down';
19
+ import ChevronRight from '@tabler/icons-svelte/icons/chevron-right';
24
20
 
25
21
  const remotes = getRemotes();
26
22
  const contentLanguage = getContentLanguage();
27
23
  const interfaceLanguage = useInterfaceLanguage();
28
24
 
29
25
  const labels = {
30
- linkText: { en: 'Link text', pl: 'Tekst linku' },
31
- noTitle: { en: 'No title', pl: 'Bez tytułu' }
26
+ linkText: { en: 'Displayed link text', pl: 'Tekst wyświetlany jako link' },
27
+ noTitle: { en: 'No title', pl: 'Bez tytułu' },
28
+ placeholder: { en: 'Paste URL or search pages...', pl: 'Wklej adres lub szukaj stron...' },
29
+ openNewTab: { en: 'Open in new tab', pl: 'Otwórz w nowej karcie' },
30
+ autoExternal: { en: '(auto for external links)', pl: '(automatycznie dla linków zewnętrznych)' },
31
+ indexing: { en: 'Indexing', pl: 'Indeksowanie' },
32
+ nofollowLabel: { en: 'nofollow', pl: 'nofollow' },
33
+ nofollowDesc: { en: "Don't pass SEO authority. Check for untrusted links.", pl: 'Nie przekazuj autorytetu SEO. Zaznacz dla linków, którym nie chcesz ufać.' },
34
+ sponsoredLabel: { en: 'sponsored', pl: 'sponsored' },
35
+ sponsoredDesc: { en: 'Sponsored or advertising link.', pl: 'Link reklamowy lub sponsorowany.' },
36
+ ugcLabel: { en: 'ugc', pl: 'ugc' },
37
+ ugcDesc: { en: 'User-generated content link (e.g. comment).', pl: 'Link dodany przez użytkownika (np. w komentarzu).' },
38
+ external: { en: 'External', pl: 'Zewnętrzny' },
39
+ internal: { en: 'Internal', pl: 'Wewnętrzny' }
32
40
  };
33
41
 
34
42
  type EntryWithSeo = Entry & {
@@ -40,30 +48,37 @@
40
48
 
41
49
  type Props = {
42
50
  field: UrlField;
43
- form: SuperForm<T>;
44
- path: FormPathLeaves<T, UrlFieldData>;
51
+ value: UrlFieldData;
45
52
  };
46
53
 
47
- let { field, form, path, ...props }: Props = $props();
54
+ let { field, value = $bindable(), ...props }: Props = $props();
48
55
 
49
- const { value } = formFieldProxy(form, path) satisfies FormFieldProxy<UrlFieldData>;
56
+ let isLinked = $derived(value.id != null && value.id.length > 0);
50
57
 
51
- let isLinked = $derived($value.id != null && $value.id.length > 0);
58
+ // Current URL for the active language
59
+ let currentUrl = $derived(value.url[contentLanguage.current] ?? '');
60
+ let currentIsExternal = $derived(currentUrl ? isExternalUrl(currentUrl) : false);
52
61
 
62
+ // Autocomplete state
63
+ let autocompleteSuggestions = $state<EntryWithSeo[]>([]);
64
+ let popoverOpen = $state(false);
65
+ let activeIndex = $state(-1);
66
+ let searchTimeout: ReturnType<typeof setTimeout> | null = null;
67
+ let linkedEntry = $state<(Entry & { data: { seo?: SeoFieldData } }) | null>(null);
68
+
69
+ // Rel collapsible
70
+ let relOpen = $state(false);
71
+ let relNofollow = $derived((value.rel ?? '').includes('nofollow'));
72
+ let relSponsored = $derived((value.rel ?? '').includes('sponsored'));
73
+ let relUgc = $derived((value.rel ?? '').includes('ugc'));
74
+
75
+ // Fetch linked entry when linked
53
76
  $effect(() => {
54
77
  if (isLinked) {
55
- fetchLinkedEntry($value.id).then((entry) => {
78
+ fetchLinkedEntry(value.id).then((entry) => {
56
79
  linkedEntry = entry;
57
80
  });
58
81
  }
59
-
60
- if (!isLinked && $value.url && Object.values($value.url).some((url) => url.length > 2)) {
61
- fetchSuggestions($value.url).then((suggestions) => {
62
- autocompleteSuggestions = suggestions;
63
- });
64
- } else if (!isLinked) {
65
- autocompleteSuggestions = [];
66
- }
67
82
  });
68
83
 
69
84
  async function fetchLinkedEntry(id: string | undefined) {
@@ -71,135 +86,286 @@
71
86
  return remotes.getEntry({ id });
72
87
  }
73
88
 
74
- let linkedEntry = $state<(Entry & { data: { seo?: SeoFieldData } }) | null>(null);
75
-
76
- async function fetchSuggestions(currUrl: Record<string, string>) {
77
- let suggestions: EntryWithSeo[] = [];
78
-
79
- for (let i = 0; i < Object.values(currUrl).length; i++) {
80
- let url = Object.values(currUrl)[i];
81
-
82
- if (url.length > 2) {
83
- const [slugResults, titleResults] = await Promise.all([
84
- remotes.getEntries({ dataLike: { seo: { slug: url } } }),
85
- remotes.getEntries({ dataLike: { seo: { title: url } } })
86
- ]) as [EntryWithSeo[], EntryWithSeo[]];
87
-
88
- suggestions = [...suggestions, ...slugResults, ...titleResults];
89
- suggestions = suggestions.filter(
90
- (entry, index, self) => index === self.findIndex((e) => e.id === entry.id)
91
- );
92
- suggestions = suggestions.slice(0, 5);
93
- }
89
+ function searchEntries() {
90
+ if (searchTimeout) clearTimeout(searchTimeout);
91
+ const url = value.url[contentLanguage.current] ?? '';
92
+ if (isLinked || url.length < 3) {
93
+ autocompleteSuggestions = [];
94
+ popoverOpen = false;
95
+ return;
94
96
  }
95
-
96
- return suggestions;
97
+ searchTimeout = setTimeout(async () => {
98
+ const [slugResults, titleResults] = await Promise.all([
99
+ remotes.getEntries({ dataLike: { seo: { slug: url } } }),
100
+ remotes.getEntries({ dataLike: { seo: { title: url } } })
101
+ ]) as [EntryWithSeo[], EntryWithSeo[]];
102
+ const combined = [...slugResults, ...titleResults];
103
+ const deduped = combined.filter(
104
+ (entry, index, self) => index === self.findIndex((e) => e.id === entry.id)
105
+ );
106
+ autocompleteSuggestions = deduped.slice(0, 5);
107
+ popoverOpen = autocompleteSuggestions.length > 0;
108
+ activeIndex = -1;
109
+ }, 300);
97
110
  }
98
111
 
99
- let autocompleteSuggestions = $state<EntryWithSeo[]>([]);
112
+ function linkEntry(entry: EntryWithSeo) {
113
+ value.id = entry.id;
114
+ linkedEntry = entry;
115
+ autocompleteSuggestions = [];
116
+ popoverOpen = false;
117
+ }
100
118
 
101
119
  function unlinkEntry() {
102
- $value.id = '';
120
+ value.id = '';
103
121
  linkedEntry = null;
104
122
  }
105
123
 
106
- function linkEntry(id: string) {
107
- $value.id = id;
108
- autocompleteSuggestions = [];
124
+ function handleUrlKeydown(e: KeyboardEvent) {
125
+ if (!popoverOpen || autocompleteSuggestions.length === 0) return;
126
+
127
+ switch (e.key) {
128
+ case 'ArrowDown':
129
+ e.preventDefault();
130
+ activeIndex = (activeIndex + 1) % autocompleteSuggestions.length;
131
+ break;
132
+ case 'ArrowUp':
133
+ e.preventDefault();
134
+ activeIndex = activeIndex <= 0 ? autocompleteSuggestions.length - 1 : activeIndex - 1;
135
+ break;
136
+ case 'Enter':
137
+ e.preventDefault();
138
+ if (activeIndex >= 0 && activeIndex < autocompleteSuggestions.length) {
139
+ linkEntry(autocompleteSuggestions[activeIndex]);
140
+ }
141
+ break;
142
+ case 'Escape':
143
+ e.preventDefault();
144
+ popoverOpen = false;
145
+ activeIndex = -1;
146
+ break;
147
+ }
148
+ }
149
+
150
+ function updateRel(token: string, add: boolean) {
151
+ const current = (value.rel ?? '').split(/\s+/).filter(Boolean);
152
+ if (add && !current.includes(token)) {
153
+ current.push(token);
154
+ } else if (!add) {
155
+ const idx = current.indexOf(token);
156
+ if (idx >= 0) current.splice(idx, 1);
157
+ }
158
+ value.rel = current.join(' ');
109
159
  }
110
160
  </script>
111
161
 
112
- <div class="border-border bg-card overflow-hidden rounded-lg border">
113
- <!-- URL row -->
114
- <div class="relative">
115
- {#if isLinked && linkedEntry}
116
- <!-- Linked state -->
117
- <div class="flex items-center gap-2 px-3 py-2">
118
- <LinkIcon class="text-muted-foreground size-4 shrink-0" />
119
- <div class="flex min-w-0 flex-1 items-center gap-2">
120
- <span class="truncate text-sm font-medium">
162
+ <!-- URL input -->
163
+ <div class="relative">
164
+ {#if isLinked && linkedEntry}
165
+ <!-- Linked state: Badge chip -->
166
+ <InputGroup.Root>
167
+ <InputGroup.Addon align="inline-start">
168
+ <LinkIcon class="size-4" />
169
+ </InputGroup.Addon>
170
+ <div class="flex min-w-0 flex-1 items-center gap-2 py-1.5">
171
+ <Badge variant="secondary" class="max-w-full gap-1.5 truncate">
172
+ <span class="truncate font-medium">
121
173
  {linkedEntry.data.seo?.title || linkedEntry.id}
122
174
  </span>
123
- <span class="text-muted-foreground truncate text-xs">
124
- {linkedEntry.data.seo?.slug || ''}
125
- </span>
126
- </div>
175
+ {#if linkedEntry.data.seo?.slug}
176
+ <span class="text-muted-foreground truncate text-[10px] font-normal">
177
+ {linkedEntry.data.seo.slug}
178
+ </span>
179
+ {/if}
180
+ </Badge>
181
+ </div>
182
+ <InputGroup.Addon align="inline-end">
127
183
  <button
128
184
  type="button"
129
- class="text-muted-foreground hover:text-foreground hover:bg-muted -mr-1 rounded-md p-1 transition-colors"
185
+ class="text-muted-foreground hover:text-foreground hover:bg-muted rounded-md p-0.5 transition-colors"
130
186
  onclick={unlinkEntry}
187
+ aria-label={getLocalizedLabel({ en: 'Remove link', pl: 'Usuń powiązanie' }, interfaceLanguage.current)}
131
188
  >
132
189
  <XIcon class="size-3.5" />
133
190
  </button>
191
+ </InputGroup.Addon>
192
+ </InputGroup.Root>
193
+ {:else}
194
+ {#each contentLanguage.all as lang}
195
+ <div class={contentLanguage.current === lang ? '' : 'hidden'}>
196
+ <InputGroup.Root>
197
+ <InputGroup.Addon align="inline-start">
198
+ <GlobeIcon class="size-4" />
199
+ </InputGroup.Addon>
200
+ <InputGroup.Input
201
+ bind:value={value.url[lang]}
202
+ type="text"
203
+ placeholder={field.placeholder?.[lang] || getLocalizedLabel(labels.placeholder, interfaceLanguage.current)}
204
+ oninput={searchEntries}
205
+ onkeydown={handleUrlKeydown}
206
+ role="combobox"
207
+ aria-expanded={popoverOpen}
208
+ aria-controls="url-suggestions"
209
+ aria-autocomplete="list"
210
+ aria-activedescendant={activeIndex >= 0 ? `url-suggestion-${activeIndex}` : undefined}
211
+ />
212
+ {#if currentUrl && contentLanguage.current === lang}
213
+ <InputGroup.Addon align="inline-end">
214
+ {#if currentIsExternal}
215
+ <Badge variant="outline" class="text-[10px]">
216
+ <ExternalLinkIcon class="size-3" />
217
+ {getLocalizedLabel(labels.external, interfaceLanguage.current)}
218
+ </Badge>
219
+ {:else if currentUrl.length > 0}
220
+ <Badge variant="secondary" class="text-[10px]">
221
+ <LinkIcon class="size-3" />
222
+ {getLocalizedLabel(labels.internal, interfaceLanguage.current)}
223
+ </Badge>
224
+ {/if}
225
+ </InputGroup.Addon>
226
+ {/if}
227
+ </InputGroup.Root>
134
228
  </div>
135
- {:else}
136
- <!-- URL input -->
229
+ {/each}
230
+ {/if}
231
+
232
+ <!-- Floating autocomplete dropdown -->
233
+ {#if popoverOpen && autocompleteSuggestions.length > 0}
234
+ <div
235
+ id="url-suggestions"
236
+ role="listbox"
237
+ class="bg-popover text-popover-foreground absolute top-full right-0 left-0 z-50 overflow-hidden rounded-md border shadow-md"
238
+ >
239
+ {#each autocompleteSuggestions as suggestion, i}
240
+ <button
241
+ type="button"
242
+ id="url-suggestion-{i}"
243
+ role="option"
244
+ aria-selected={i === activeIndex}
245
+ class="hover:bg-muted flex w-full items-center gap-2 px-3 py-2 text-left transition-colors {i === activeIndex ? 'bg-muted' : ''} {i < autocompleteSuggestions.length - 1 ? 'border-border/50 border-b' : ''}"
246
+ onclick={() => linkEntry(suggestion)}
247
+ >
248
+ <LinkIcon class="text-muted-foreground size-3.5 shrink-0" />
249
+ <span class="truncate text-sm">
250
+ {suggestion.data.seo?.title || getLocalizedLabel(labels.noTitle, interfaceLanguage.current)}
251
+ </span>
252
+ <span class="text-muted-foreground ml-auto shrink-0 text-xs">
253
+ {suggestion.data.seo?.slug || ''}
254
+ </span>
255
+ </button>
256
+ {/each}
257
+ </div>
258
+ {/if}
259
+ </div>
260
+
261
+ <!-- Options (text, newTab, rel) -->
262
+ {#if (field.text || field.newTab || field.rel) && (field.text && value.text || field.newTab || field.rel)}
263
+ <div class="mt-2 space-y-2">
264
+ <!-- Link text -->
265
+ {#if field.text && value.text}
137
266
  {#each contentLanguage.all as lang}
138
267
  <div class={contentLanguage.current === lang ? '' : 'hidden'}>
139
- <Input
140
- bind:value={$value.url[lang]}
141
- type="text"
142
- placeholder={field.placeholder?.[lang] || 'https://...'}
143
- class="rounded-none border-0 shadow-none focus-visible:ring-0 dark:bg-transparent"
144
- />
268
+ <InputGroup.Root>
269
+ <InputGroup.Addon align="inline-start">
270
+ <TextIcon class="size-4" />
271
+ </InputGroup.Addon>
272
+ <InputGroup.Input
273
+ bind:value={value.text[lang]}
274
+ type="text"
275
+ placeholder={getLocalizedLabel(labels.linkText, interfaceLanguage.current)}
276
+ />
277
+ </InputGroup.Root>
145
278
  </div>
146
279
  {/each}
147
280
  {/if}
148
281
 
149
- <!-- Autocomplete dropdown -->
150
- {#if autocompleteSuggestions.length > 0}
151
- <div class="border-border border-t">
152
- {#each autocompleteSuggestions as suggestion, i}
282
+ <!-- New tab + rel row -->
283
+ {#if field.newTab || field.rel}
284
+ <div class="flex flex-wrap items-center gap-x-4 gap-y-2 px-1">
285
+ {#if field.newTab}
286
+ <label class="flex cursor-pointer items-center gap-2">
287
+ <Checkbox
288
+ checked={value.newTab ?? false}
289
+ onCheckedChange={(v) => (value.newTab = v === true)}
290
+ />
291
+ <span class="text-sm">
292
+ {getLocalizedLabel(labels.openNewTab, interfaceLanguage.current)}
293
+ </span>
294
+ {#if currentIsExternal}
295
+ <span class="text-muted-foreground text-xs">
296
+ {getLocalizedLabel(labels.autoExternal, interfaceLanguage.current)}
297
+ </span>
298
+ {/if}
299
+ </label>
300
+ {/if}
301
+
302
+ {#if field.rel}
153
303
  <button
154
304
  type="button"
155
- class="hover:bg-muted flex w-full items-center gap-2 px-3 py-2 text-left transition-colors {i < autocompleteSuggestions.length - 1 ? 'border-border/50 border-b' : ''}"
156
- onclick={() => linkEntry(suggestion.id)}
305
+ class="text-muted-foreground hover:text-foreground flex items-center gap-1 text-sm transition-colors"
306
+ onclick={() => (relOpen = !relOpen)}
307
+ aria-expanded={relOpen}
157
308
  >
158
- <LinkIcon class="text-muted-foreground size-3.5 shrink-0" />
159
- <span class="truncate text-sm">
160
- {suggestion.data.seo?.title || getLocalizedLabel(labels.noTitle, interfaceLanguage.current)}
161
- </span>
162
- <span class="text-muted-foreground ml-auto shrink-0 text-xs">
163
- {suggestion.data.seo?.slug || ''}
164
- </span>
309
+ {#if relOpen}
310
+ <ChevronDown class="size-3.5" />
311
+ {:else}
312
+ <ChevronRight class="size-3.5" />
313
+ {/if}
314
+ {getLocalizedLabel(labels.indexing, interfaceLanguage.current)}
165
315
  </button>
166
- {/each}
316
+ {/if}
167
317
  </div>
168
318
  {/if}
169
- </div>
170
319
 
171
- <!-- Divider + extra fields -->
172
- {#if field.text || field.newTab}
173
- <div class="border-border flex items-stretch border-t">
174
- {#if field.text && $value.text}
175
- <div class="flex-1">
176
- {#each contentLanguage.all as lang}
177
- <div class={contentLanguage.current === lang ? '' : 'hidden'}>
178
- <Input
179
- bind:value={$value.text[lang]}
180
- type="text"
181
- placeholder={getLocalizedLabel(labels.linkText, interfaceLanguage.current)}
182
- class="rounded-none border-0 shadow-none focus-visible:ring-0 dark:bg-transparent"
183
- />
184
- </div>
185
- {/each}
320
+ <!-- Rel details (collapsible) -->
321
+ {#if field.rel && relOpen}
322
+ <div class="border-border space-y-3 rounded-lg border px-3 py-3">
323
+ <div>
324
+ <div class="flex items-center gap-2">
325
+ <Checkbox
326
+ id="url-rel-nofollow"
327
+ checked={relNofollow}
328
+ onCheckedChange={(v) => updateRel('nofollow', v === true)}
329
+ />
330
+ <Label for="url-rel-nofollow" class="cursor-pointer font-normal">
331
+ {getLocalizedLabel(labels.nofollowLabel, interfaceLanguage.current)}
332
+ </Label>
333
+ </div>
334
+ <p class="text-muted-foreground mt-1 ml-6 text-xs">
335
+ {getLocalizedLabel(labels.nofollowDesc, interfaceLanguage.current)}
336
+ </p>
186
337
  </div>
187
- {/if}
188
-
189
- {#if field.newTab}
190
- <label
191
- class="border-border text-muted-foreground hover:text-foreground flex cursor-pointer items-center gap-1.5 px-3 transition-colors {field.text ? 'border-l' : 'flex-1'}"
192
- >
193
- <Checkbox
194
- bind:checked={
195
- () => $value.newTab ?? false,
196
- (v) => ($value.newTab = v)
197
- }
198
- class="data-[state=checked]:border-blue-600 data-[state=checked]:bg-blue-600 data-[state=checked]:text-white dark:data-[state=checked]:border-blue-700 dark:data-[state=checked]:bg-blue-700"
199
- />
200
- <ExternalLinkIcon class="size-3.5" />
201
- </label>
202
- {/if}
203
- </div>
204
- {/if}
205
- </div>
338
+ <div>
339
+ <div class="flex items-center gap-2">
340
+ <Checkbox
341
+ id="url-rel-sponsored"
342
+ checked={relSponsored}
343
+ onCheckedChange={(v) => updateRel('sponsored', v === true)}
344
+ />
345
+ <Label for="url-rel-sponsored" class="cursor-pointer font-normal">
346
+ {getLocalizedLabel(labels.sponsoredLabel, interfaceLanguage.current)}
347
+ </Label>
348
+ </div>
349
+ <p class="text-muted-foreground mt-1 ml-6 text-xs">
350
+ {getLocalizedLabel(labels.sponsoredDesc, interfaceLanguage.current)}
351
+ </p>
352
+ </div>
353
+ <div>
354
+ <div class="flex items-center gap-2">
355
+ <Checkbox
356
+ id="url-rel-ugc"
357
+ checked={relUgc}
358
+ onCheckedChange={(v) => updateRel('ugc', v === true)}
359
+ />
360
+ <Label for="url-rel-ugc" class="cursor-pointer font-normal">
361
+ {getLocalizedLabel(labels.ugcLabel, interfaceLanguage.current)}
362
+ </Label>
363
+ </div>
364
+ <p class="text-muted-foreground mt-1 ml-6 text-xs">
365
+ {getLocalizedLabel(labels.ugcDesc, interfaceLanguage.current)}
366
+ </p>
367
+ </div>
368
+ </div>
369
+ {/if}
370
+ </div>
371
+ {/if}
@@ -1,30 +1,8 @@
1
1
  import type { UrlField, UrlFieldData } from '../../../types/fields.js';
2
- import { type FormPathLeaves, type SuperForm } from 'sveltekit-superforms';
3
- declare function $$render<T extends Record<string, unknown>>(): {
4
- props: {
5
- field: UrlField;
6
- form: SuperForm<T>;
7
- path: FormPathLeaves<T, UrlFieldData>;
8
- };
9
- exports: {};
10
- bindings: "";
11
- slots: {};
12
- events: {};
2
+ type Props = {
3
+ field: UrlField;
4
+ value: UrlFieldData;
13
5
  };
14
- declare class __sveltets_Render<T extends Record<string, unknown>> {
15
- props(): ReturnType<typeof $$render<T>>['props'];
16
- events(): ReturnType<typeof $$render<T>>['events'];
17
- slots(): ReturnType<typeof $$render<T>>['slots'];
18
- bindings(): "";
19
- exports(): {};
20
- }
21
- interface $$IsomorphicComponent {
22
- new <T extends Record<string, unknown>>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
23
- $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
24
- } & ReturnType<__sveltets_Render<T>['exports']>;
25
- <T extends Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
26
- z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
27
- }
28
- declare const UrlField: $$IsomorphicComponent;
29
- type UrlField<T extends Record<string, unknown>> = InstanceType<typeof UrlField<T>>;
6
+ declare const UrlField: import("svelte").Component<Props, {}, "value">;
7
+ type UrlField = ReturnType<typeof UrlField>;
30
8
  export default UrlField;
@@ -4,6 +4,7 @@
4
4
  import type { SuperForm } from 'sveltekit-superforms';
5
5
  import { isLayoutLeaf, isLayoutBranch } from '../../../types/layout.js';
6
6
  import FieldRenderer from '../fields/field-renderer.svelte';
7
+ import { evaluateCondition } from '../../utils/fieldCondition.js';
7
8
  import * as Accordion from '../../../components/ui/accordion/index.js';
8
9
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
9
10
  import { getLocalizedLabel } from '../../utils/collectionLabel.js';
@@ -28,6 +29,7 @@
28
29
  }: Props = $props();
29
30
 
30
31
  const interfaceLanguage = useInterfaceLanguage();
32
+ const { form: formData } = form;
31
33
 
32
34
  const fieldMap = $derived(new Map(fields.map((f) => [f.slug, f])));
33
35
 
@@ -60,7 +62,7 @@
60
62
  <div class="layout-fields-stack">
61
63
  {#each node.fields as slug (slug)}
62
64
  {@const field = fieldMap.get(slug)}
63
- {#if field}
65
+ {#if field && evaluateCondition(field.showWhen, (s) => $formData[s])}
64
66
  <div
65
67
  data-field-path={slug}
66
68
  class={cn(
@@ -117,7 +119,7 @@
117
119
  <div class="layout-auto-grid">
118
120
  {#each node.fields as slug (slug)}
119
121
  {@const field = fieldMap.get(slug)}
120
- {#if field}
122
+ {#if field && evaluateCondition(field.showWhen, (s) => $formData[s])}
121
123
  <div
122
124
  data-field-path={slug}
123
125
  class={cn(
@@ -135,7 +137,7 @@
135
137
  <div class="layout-fields-stack">
136
138
  {#each node.fields as slug (slug)}
137
139
  {@const field = fieldMap.get(slug)}
138
- {#if field}
140
+ {#if field && evaluateCondition(field.showWhen, (s) => $formData[s])}
139
141
  <div
140
142
  data-field-path={slug}
141
143
  class={cn(
@@ -174,7 +176,7 @@
174
176
  <div class="layout-fields-stack">
175
177
  {#each node.fields as slug (slug)}
176
178
  {@const field = fieldMap.get(slug)}
177
- {#if field}
179
+ {#if field && evaluateCondition(field.showWhen, (s) => $formData[s])}
178
180
  <div
179
181
  data-field-path={slug}
180
182
  class={cn(
@@ -208,7 +210,7 @@
208
210
  {#if isLayoutLeaf(node)}
209
211
  {#each node.fields as slug (slug)}
210
212
  {@const field = fieldMap.get(slug)}
211
- {#if field}
213
+ {#if field && evaluateCondition(field.showWhen, (s) => $formData[s])}
212
214
  <div
213
215
  data-field-path={slug}
214
216
  class={cn(
@@ -334,7 +336,7 @@
334
336
  }
335
337
 
336
338
  /* ═══════════ RESPONSIVE ═══════════ */
337
- @media (max-width: 768px) {
339
+ @container (max-width: 768px) {
338
340
  .layout-columns {
339
341
  grid-template-columns: 1fr !important;
340
342
  }