includio-cms 0.5.0 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/CHANGELOG.md +31 -12
  2. package/ROADMAP.md +12 -0
  3. package/dist/admin/client/admin/dashboard-page.svelte +18 -64
  4. package/dist/admin/client/collection/bulk-actions-bar.svelte +18 -1
  5. package/dist/admin/client/collection/bulk-actions-bar.svelte.d.ts +1 -0
  6. package/dist/admin/client/collection/collection-entries.svelte +25 -1
  7. package/dist/admin/client/collection/row-actions.svelte +13 -4
  8. package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -0
  9. package/dist/admin/client/entry/entry-header.svelte +51 -4
  10. package/dist/admin/client/entry/entry-header.svelte.d.ts +3 -0
  11. package/dist/admin/client/entry/entry.svelte +106 -6
  12. package/dist/admin/client/entry/header/a11y-validator.d.ts +3 -2
  13. package/dist/admin/client/entry/header/a11y-validator.js +50 -9
  14. package/dist/admin/client/entry/header/publish-panel.svelte +164 -4
  15. package/dist/admin/client/entry/header/version-history-sheet.svelte +9 -1
  16. package/dist/admin/components/dashboard/changelog-dialog.svelte +167 -0
  17. package/dist/admin/components/dashboard/changelog-dialog.svelte.d.ts +6 -0
  18. package/dist/admin/components/dashboard/orphaned-entries-notice.svelte +240 -0
  19. package/dist/admin/components/dashboard/orphaned-entries-notice.svelte.d.ts +13 -0
  20. package/dist/admin/components/fields/text-field-wrapper.svelte +134 -2
  21. package/dist/admin/components/layout/nav-footer.svelte +11 -4
  22. package/dist/admin/components/layout/nav-footer.svelte.d.ts +2 -17
  23. package/dist/admin/remote/entry.remote.d.ts +1 -0
  24. package/dist/admin/remote/entry.remote.js +5 -4
  25. package/dist/admin/state/content-language.svelte.d.ts +3 -0
  26. package/dist/admin/state/content-language.svelte.js +8 -0
  27. package/dist/admin/utils/translationStatus.d.ts +17 -0
  28. package/dist/admin/utils/translationStatus.js +134 -0
  29. package/dist/core/server/entries/operations/get.js +2 -1
  30. package/dist/db-postgres/index.js +10 -6
  31. package/dist/types/entries.d.ts +3 -0
  32. package/dist/updates/0.0.65/index.js +1 -1
  33. package/dist/updates/0.0.67/index.js +1 -1
  34. package/dist/updates/0.1.2/index.js +1 -1
  35. package/dist/updates/0.1.5/index.js +1 -1
  36. package/dist/updates/0.2.0/index.js +1 -1
  37. package/dist/updates/0.2.2/index.js +1 -1
  38. package/dist/updates/0.5.0/index.js +1 -1
  39. package/dist/updates/0.5.1/index.d.ts +2 -0
  40. package/dist/updates/0.5.1/index.js +17 -0
  41. package/dist/updates/0.5.2/index.d.ts +2 -0
  42. package/dist/updates/0.5.2/index.js +14 -0
  43. package/dist/updates/index.d.ts +2 -1
  44. package/dist/updates/index.js +3 -1
  45. package/package.json +1 -1
  46. package/dist/admin/components/dashboard/updates-banner.svelte +0 -170
  47. package/dist/admin/components/dashboard/updates-banner.svelte.d.ts +0 -3
  48. package/dist/updates/0.0.65/migration.sql +0 -55
  49. package/dist/updates/0.0.67/migration.sql +0 -9
@@ -0,0 +1,167 @@
1
+ <script lang="ts">
2
+ import { updates } from '../../../updates/index.js';
3
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
4
+ import * as Dialog from '../../../components/ui/dialog/index.js';
5
+ import { Badge } from '../../../components/ui/badge/index.js';
6
+ import type { InterfaceLanguage } from '../../../types/languages.js';
7
+ import SparklesIcon from '@tabler/icons-svelte/icons/sparkles';
8
+ import AlertTriangleIcon from '@tabler/icons-svelte/icons/alert-triangle';
9
+ import BugIcon from '@tabler/icons-svelte/icons/bug';
10
+ import StarIcon from '@tabler/icons-svelte/icons/star';
11
+
12
+ type Props = {
13
+ open: boolean;
14
+ };
15
+
16
+ let { open = $bindable(false) }: Props = $props();
17
+
18
+ const interfaceLanguage = useInterfaceLanguage();
19
+
20
+ const sorted = [...updates].sort((a, b) => {
21
+ const ap = a.version.split('.').map(Number);
22
+ const bp = b.version.split('.').map(Number);
23
+ for (let i = 0; i < 3; i++) {
24
+ if (bp[i] !== ap[i]) return bp[i] - ap[i];
25
+ }
26
+ return 0;
27
+ });
28
+
29
+ const lang: Record<
30
+ InterfaceLanguage,
31
+ {
32
+ title: string;
33
+ features: string;
34
+ fixes: string;
35
+ breaking: string;
36
+ sql: string;
37
+ notes: string;
38
+ empty: string;
39
+ }
40
+ > = {
41
+ pl: {
42
+ title: 'Historia zmian',
43
+ features: 'Nowe funkcje',
44
+ fixes: 'Poprawki',
45
+ breaking: 'Zmiany wymagające uwagi',
46
+ sql: 'Migracja SQL',
47
+ notes: 'Uwagi do aktualizacji',
48
+ empty: 'Brak zmian w tej wersji.'
49
+ },
50
+ en: {
51
+ title: 'Changelog',
52
+ features: 'Features',
53
+ fixes: 'Fixes',
54
+ breaking: 'Breaking changes',
55
+ sql: 'SQL Migration',
56
+ notes: 'Update notes',
57
+ empty: 'No changes in this version.'
58
+ }
59
+ };
60
+ </script>
61
+
62
+ <Dialog.Root bind:open>
63
+ <Dialog.Content class="max-h-[80vh] max-w-2xl! overflow-hidden sm:max-w-2xl!">
64
+ <Dialog.Header>
65
+ <Dialog.Title class="flex items-center gap-2">
66
+ <SparklesIcon class="text-primary size-5" />
67
+ {lang[interfaceLanguage.current].title}
68
+ </Dialog.Title>
69
+ </Dialog.Header>
70
+
71
+ <div class="custom-scrollbar -mx-1 max-h-[60vh] space-y-6 overflow-y-auto px-1 py-2">
72
+ {#each sorted as update, i}
73
+ {@const t = lang[interfaceLanguage.current]}
74
+ {@const hasBreaking = update.breakingChanges.length > 0}
75
+ {@const hasContent =
76
+ update.features.length > 0 ||
77
+ update.fixes.length > 0 ||
78
+ update.breakingChanges.length > 0}
79
+
80
+ <div class="relative">
81
+ {#if i > 0}
82
+ <div class="border-border mb-6 border-t"></div>
83
+ {/if}
84
+
85
+ <div class="mb-2 flex items-center gap-2">
86
+ <Badge variant={i === 0 ? 'default' : 'secondary'} class="font-mono text-xs">
87
+ v{update.version}
88
+ </Badge>
89
+ <span class="text-text-light text-xs">{update.date}</span>
90
+ {#if hasBreaking}
91
+ <Badge variant="destructive" class="text-xs">
92
+ <AlertTriangleIcon class="mr-0.5 size-3" />
93
+ {t.breaking}
94
+ </Badge>
95
+ {/if}
96
+ </div>
97
+
98
+ <p class="text-sm text-foreground/80">{update.description}</p>
99
+
100
+ {#if hasContent}
101
+ <div class="mt-3 space-y-3">
102
+ {#if update.features.length > 0}
103
+ <div>
104
+ <p class="text-primary mb-1 flex items-center gap-1 text-xs font-semibold">
105
+ <StarIcon class="size-3.5" />
106
+ {t.features}
107
+ </p>
108
+ <ul class="list-inside list-disc text-sm text-foreground/70">
109
+ {#each update.features as feature}
110
+ <li>{feature}</li>
111
+ {/each}
112
+ </ul>
113
+ </div>
114
+ {/if}
115
+
116
+ {#if update.fixes.length > 0}
117
+ <div>
118
+ <p class="mb-1 flex items-center gap-1 text-xs font-semibold text-emerald-600 dark:text-emerald-400">
119
+ <BugIcon class="size-3.5" />
120
+ {t.fixes}
121
+ </p>
122
+ <ul class="list-inside list-disc text-sm text-foreground/70">
123
+ {#each update.fixes as fix}
124
+ <li>{fix}</li>
125
+ {/each}
126
+ </ul>
127
+ </div>
128
+ {/if}
129
+
130
+ {#if update.breakingChanges.length > 0}
131
+ <div>
132
+ <p class="mb-1 flex items-center gap-1 text-xs font-semibold text-orange-600 dark:text-orange-400">
133
+ <AlertTriangleIcon class="size-3.5" />
134
+ {t.breaking}
135
+ </p>
136
+ <ul class="list-inside list-disc text-sm text-orange-700 dark:text-orange-300">
137
+ {#each update.breakingChanges as change}
138
+ <li>{change}</li>
139
+ {/each}
140
+ </ul>
141
+ </div>
142
+ {/if}
143
+
144
+ {#if update.sql}
145
+ <details>
146
+ <summary class="cursor-pointer text-xs font-semibold text-foreground/60">
147
+ {t.sql}
148
+ </summary>
149
+ <pre class="mt-1 overflow-x-auto rounded-lg bg-muted p-2 text-xs">{update.sql}</pre>
150
+ </details>
151
+ {/if}
152
+
153
+ {#if update.notes}
154
+ <p class="text-xs text-foreground/60">
155
+ <span class="font-semibold">{t.notes}:</span>
156
+ {update.notes}
157
+ </p>
158
+ {/if}
159
+ </div>
160
+ {:else}
161
+ <p class="mt-1 text-xs text-foreground/50 italic">{t.empty}</p>
162
+ {/if}
163
+ </div>
164
+ {/each}
165
+ </div>
166
+ </Dialog.Content>
167
+ </Dialog.Root>
@@ -0,0 +1,6 @@
1
+ type Props = {
2
+ open: boolean;
3
+ };
4
+ declare const ChangelogDialog: import("svelte").Component<Props, {}, "open">;
5
+ type ChangelogDialog = ReturnType<typeof ChangelogDialog>;
6
+ export default ChangelogDialog;
@@ -0,0 +1,240 @@
1
+ <script lang="ts">
2
+ import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
3
+ import type { InterfaceLanguage } from '../../../types/languages.js';
4
+
5
+ type OrphanedEntry = { id: string; slug: string; createdAt: string };
6
+
7
+ type Props = {
8
+ entries: OrphanedEntry[];
9
+ deleting: boolean;
10
+ ondelete: () => void;
11
+ };
12
+
13
+ let { entries, deleting, ondelete }: Props = $props();
14
+
15
+ const interfaceLanguage = useInterfaceLanguage();
16
+
17
+ const lang: Record<
18
+ InterfaceLanguage,
19
+ {
20
+ title: string;
21
+ description: (n: number) => string;
22
+ details: string;
23
+ deleteBtn: string;
24
+ deletingBtn: string;
25
+ }
26
+ > = {
27
+ pl: {
28
+ title: 'Wpisy do uporządkowania',
29
+ description: (n) =>
30
+ n === 1
31
+ ? 'Masz 1 wpis, którego typ został usunięty z konfiguracji. Możesz go bezpiecznie usunąć.'
32
+ : `Masz ${n} wpisów, których typ został usunięty z konfiguracji. Możesz je bezpiecznie usunąć.`,
33
+ details: 'Pokaż szczegóły',
34
+ deleteBtn: 'Usuń niepotrzebne wpisy',
35
+ deletingBtn: 'Usuwanie…'
36
+ },
37
+ en: {
38
+ title: 'Entries to clean up',
39
+ description: (n) =>
40
+ n === 1
41
+ ? 'You have 1 entry whose content type was removed. You can safely delete it.'
42
+ : `You have ${n} entries whose content type was removed. You can safely delete them.`,
43
+ details: 'Show details',
44
+ deleteBtn: 'Delete unused entries',
45
+ deletingBtn: 'Deleting…'
46
+ }
47
+ };
48
+ </script>
49
+
50
+ {#if entries.length > 0}
51
+ {@const t = lang[interfaceLanguage.current]}
52
+ <div class="orphaned-notice" role="status">
53
+ <div class="orphaned-notice-header">
54
+ <div class="orphaned-notice-icon" aria-hidden="true">
55
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 4H8l-7 8 7 8h13a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2"/><line x1="18" x2="12" y1="9" y2="15"/><line x1="12" x2="18" y1="9" y2="15"/></svg>
56
+ </div>
57
+ <div class="orphaned-notice-content">
58
+ <h3 class="orphaned-notice-title">{t.title}</h3>
59
+ <p class="orphaned-notice-desc">{t.description(entries.length)}</p>
60
+ </div>
61
+ </div>
62
+
63
+ <details class="orphaned-notice-details">
64
+ <summary class="orphaned-notice-summary">{t.details}</summary>
65
+ <ul class="orphaned-notice-slugs">
66
+ {#each entries as entry}
67
+ <li><code>{entry.slug}</code></li>
68
+ {/each}
69
+ </ul>
70
+ </details>
71
+
72
+ <div class="orphaned-notice-actions">
73
+ <button
74
+ type="button"
75
+ class="orphaned-notice-btn"
76
+ onclick={ondelete}
77
+ disabled={deleting}
78
+ >
79
+ {#if deleting}
80
+ <svg class="orphaned-notice-spinner" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
81
+ {t.deletingBtn}
82
+ {:else}
83
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>
84
+ {t.deleteBtn}
85
+ {/if}
86
+ </button>
87
+ </div>
88
+ </div>
89
+ {/if}
90
+
91
+ <style>
92
+ .orphaned-notice {
93
+ background: var(--warning-bg);
94
+ border: 1px solid color-mix(in oklch, var(--warning) 20%, transparent);
95
+ border-radius: var(--radius-lg);
96
+ box-shadow: var(--shadow-sm);
97
+ margin-bottom: 20px;
98
+ animation: dash-fade-up 0.3s ease both;
99
+ animation-delay: 0.06s;
100
+ }
101
+
102
+ .orphaned-notice-header {
103
+ display: flex;
104
+ gap: 12px;
105
+ padding: 18px 20px 0;
106
+ }
107
+
108
+ .orphaned-notice-icon {
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ width: 32px;
113
+ height: 32px;
114
+ border-radius: var(--radius);
115
+ background: color-mix(in oklch, var(--warning) 14%, transparent);
116
+ color: var(--warning);
117
+ flex-shrink: 0;
118
+ }
119
+
120
+ .orphaned-notice-content {
121
+ flex: 1;
122
+ min-width: 0;
123
+ }
124
+
125
+ .orphaned-notice-title {
126
+ font-size: 14px;
127
+ font-weight: 600;
128
+ color: var(--foreground);
129
+ margin: 0 0 4px;
130
+ line-height: 1.4;
131
+ }
132
+
133
+ .orphaned-notice-desc {
134
+ font-size: 13px;
135
+ font-weight: 400;
136
+ color: var(--muted-foreground);
137
+ line-height: 1.5;
138
+ margin: 0;
139
+ }
140
+
141
+ .orphaned-notice-details {
142
+ padding: 0 20px;
143
+ margin-top: 12px;
144
+ }
145
+
146
+ .orphaned-notice-summary {
147
+ font-size: 12px;
148
+ font-weight: 600;
149
+ color: var(--warning);
150
+ cursor: pointer;
151
+ user-select: none;
152
+ list-style: none;
153
+ display: inline-flex;
154
+ align-items: center;
155
+ gap: 4px;
156
+ }
157
+
158
+ .orphaned-notice-summary::-webkit-details-marker {
159
+ display: none;
160
+ }
161
+
162
+ .orphaned-notice-summary::before {
163
+ content: '›';
164
+ display: inline-block;
165
+ font-size: 14px;
166
+ line-height: 1;
167
+ transition: transform 0.15s ease;
168
+ }
169
+
170
+ details[open] > .orphaned-notice-summary::before {
171
+ transform: rotate(90deg);
172
+ }
173
+
174
+ .orphaned-notice-summary:focus-visible {
175
+ outline: 2px solid var(--primary);
176
+ outline-offset: 2px;
177
+ border-radius: 2px;
178
+ }
179
+
180
+ .orphaned-notice-slugs {
181
+ list-style: none;
182
+ padding: 0;
183
+ margin: 8px 0 0;
184
+ display: flex;
185
+ flex-wrap: wrap;
186
+ gap: 6px;
187
+ }
188
+
189
+ .orphaned-notice-slugs code {
190
+ font-size: 11px;
191
+ font-family: ui-monospace, 'SF Mono', 'Cascadia Code', monospace;
192
+ background: color-mix(in oklch, var(--warning) 10%, var(--card));
193
+ border: 1px solid color-mix(in oklch, var(--warning) 14%, transparent);
194
+ color: var(--foreground);
195
+ padding: 2px 8px;
196
+ border-radius: 4px;
197
+ }
198
+
199
+ .orphaned-notice-actions {
200
+ padding: 14px 20px 16px;
201
+ }
202
+
203
+ .orphaned-notice-btn {
204
+ display: inline-flex;
205
+ align-items: center;
206
+ gap: 6px;
207
+ padding: 7px 14px;
208
+ border: 1px solid color-mix(in oklch, var(--warning) 30%, transparent);
209
+ border-radius: var(--radius);
210
+ background: var(--card);
211
+ font-size: 13px;
212
+ font-weight: 600;
213
+ color: var(--warning);
214
+ cursor: pointer;
215
+ transition: 0.15s ease;
216
+ }
217
+
218
+ .orphaned-notice-btn:hover:not(:disabled) {
219
+ background: color-mix(in oklch, var(--warning) 8%, var(--card));
220
+ border-color: color-mix(in oklch, var(--warning) 40%, transparent);
221
+ }
222
+
223
+ .orphaned-notice-btn:focus-visible {
224
+ outline: 2px solid var(--primary);
225
+ outline-offset: 2px;
226
+ }
227
+
228
+ .orphaned-notice-btn:disabled {
229
+ opacity: 0.6;
230
+ cursor: not-allowed;
231
+ }
232
+
233
+ @keyframes notice-spin {
234
+ to { transform: rotate(360deg); }
235
+ }
236
+
237
+ .orphaned-notice-spinner {
238
+ animation: notice-spin 0.8s linear infinite;
239
+ }
240
+ </style>
@@ -0,0 +1,13 @@
1
+ type OrphanedEntry = {
2
+ id: string;
3
+ slug: string;
4
+ createdAt: string;
5
+ };
6
+ type Props = {
7
+ entries: OrphanedEntry[];
8
+ deleting: boolean;
9
+ ondelete: () => void;
10
+ };
11
+ declare const OrphanedEntriesNotice: import("svelte").Component<Props, {}, "">;
12
+ type OrphanedEntriesNotice = ReturnType<typeof OrphanedEntriesNotice>;
13
+ export default OrphanedEntriesNotice;
@@ -12,11 +12,13 @@
12
12
  import * as Form from '../../../components/ui/form/index.js';
13
13
  import { type FormPathLeaves, type SuperForm } from 'sveltekit-superforms';
14
14
  import TextField from './text-field.svelte';
15
- import { joinPath } from '../../utils/objectPath.js';
15
+ import { joinPath, setAtPath } from '../../utils/objectPath.js';
16
16
  import { getContentLanguage } from '../../state/content-language.svelte.js';
17
17
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
18
18
  import { getLocalizedLabel } from '../../utils/collectionLabel.js';
19
19
  import RequiredLabel from './required-label.svelte';
20
+ import ClipboardIcon from '@tabler/icons-svelte/icons/clipboard';
21
+ import type { InterfaceLanguage } from '../../../types/languages.js';
20
22
 
21
23
  const contentLanguage = getContentLanguage();
22
24
  const interfaceLanguage = useInterfaceLanguage();
@@ -35,11 +37,83 @@
35
37
  const hasConstraints = $derived(
36
38
  isText && (field.minLength !== undefined || field.maxLength !== undefined || field.pattern !== undefined)
37
39
  );
40
+ const isMultiLang = $derived(contentLanguage.all.length > 1);
41
+ const defaultLang = $derived(contentLanguage.all[0]);
42
+ const isEditingNonDefault = $derived(isMultiLang && contentLanguage.current !== defaultLang);
43
+
44
+ const copyLang: Record<InterfaceLanguage, { copyFrom: (lang: string) => string }> = {
45
+ en: { copyFrom: (lang: string) => `Copy from ${lang.toUpperCase()}` },
46
+ pl: { copyFrom: (lang: string) => `Kopiuj z ${lang.toUpperCase()}` }
47
+ };
48
+
49
+ const refLang: Record<InterfaceLanguage, { reference: (lang: string) => string }> = {
50
+ en: { reference: (lang: string) => `${lang.toUpperCase()} (reference)` },
51
+ pl: { reference: (lang: string) => `${lang.toUpperCase()} (referencja)` }
52
+ };
38
53
 
39
54
  function resolvePathValue(data: Record<string, unknown>, dotPath: string): unknown {
40
55
  return dotPath.split('.').reduce<unknown>((obj, key) => (obj as Record<string, unknown>)?.[key], data);
41
56
  }
42
57
 
58
+ /** Check if a value is empty for copy-from logic. */
59
+ function isValueEmpty(value: unknown): boolean {
60
+ if (value == null) return true;
61
+ if (typeof value === 'string') return value.length === 0;
62
+ if (typeof value === 'object' && 'type' in (value as Record<string, unknown>)) {
63
+ const doc = value as { type: string; content?: unknown[] };
64
+ return doc.type === 'doc' && (!doc.content || doc.content.length === 0);
65
+ }
66
+ return false;
67
+ }
68
+
69
+ /** Get source language value for a given field. */
70
+ function getSourceValue(lang: string): unknown {
71
+ return resolvePathValue($formData, joinPath(path, lang));
72
+ }
73
+
74
+ /** Find first language that has content (for copy-from). */
75
+ function findSourceLang(currentLang: string): string | null {
76
+ for (const l of contentLanguage.all) {
77
+ if (l === currentLang) continue;
78
+ const val = getSourceValue(l);
79
+ if (!isValueEmpty(val)) return l;
80
+ }
81
+ return null;
82
+ }
83
+
84
+ /** Copy value from source language to current language. */
85
+ function copyFrom(sourceLang: string, targetLang: string) {
86
+ const sourceVal = getSourceValue(sourceLang);
87
+ if (sourceVal == null) return;
88
+
89
+ const targetPath = joinPath(path, targetLang);
90
+ const value =
91
+ field.type === 'content' && typeof sourceVal === 'object'
92
+ ? structuredClone(sourceVal)
93
+ : sourceVal;
94
+ setAtPath($formData as Record<string, unknown>, targetPath, value);
95
+ $formData = $formData; // trigger reactivity
96
+ }
97
+
98
+ /** Get reference text for display. */
99
+ function getReferenceText(lang: string): string {
100
+ const val = getSourceValue(lang);
101
+ if (typeof val === 'string') return val;
102
+ if (val && typeof val === 'object' && 'type' in (val as Record<string, unknown>)) {
103
+ // Content field — show placeholder
104
+ return interfaceLanguage.current === 'pl'
105
+ ? '(treść w edytorze)'
106
+ : '(editor content)';
107
+ }
108
+ return '';
109
+ }
110
+
111
+ /** Get per-language fill status for status dots. */
112
+ function getLangFillStatus(lang: string): boolean {
113
+ const val = getSourceValue(lang);
114
+ return !isValueEmpty(val);
115
+ }
116
+
43
117
  function constraintHint(): string {
44
118
  if (field.type !== 'text') return '';
45
119
  const parts: string[] = [];
@@ -52,6 +126,18 @@
52
126
  }
53
127
  return parts.join('');
54
128
  }
129
+
130
+ function dotTooltip(): string {
131
+ const parts = contentLanguage.all.map((l) => {
132
+ const filled = getLangFillStatus(l);
133
+ const statusText =
134
+ interfaceLanguage.current === 'pl'
135
+ ? filled ? 'uzupełnione' : 'puste'
136
+ : filled ? 'filled' : 'empty';
137
+ return `${l.toUpperCase()}: ${statusText}`;
138
+ });
139
+ return parts.join(', ');
140
+ }
55
141
  </script>
56
142
 
57
143
  <Tabs.Root
@@ -66,7 +152,37 @@
66
152
  <Form.Control>
67
153
  {#snippet children({ props })}
68
154
  {#if field.label}
69
- <RequiredLabel required={field.required}>{getLocalizedLabel(field.label, interfaceLanguage.current)}</RequiredLabel>
155
+ <div class="flex items-center gap-1.5">
156
+ <RequiredLabel required={field.required}>{getLocalizedLabel(field.label, interfaceLanguage.current)}</RequiredLabel>
157
+ <!-- Status dots -->
158
+ {#if isMultiLang && (field.required || contentLanguage.all.some(l => getLangFillStatus(l)))}
159
+ <span
160
+ class="inline-flex items-center gap-1"
161
+ title={dotTooltip()}
162
+ aria-label={dotTooltip()}
163
+ >
164
+ {#each contentLanguage.all as l}
165
+ <span
166
+ class="inline-block size-1.5 rounded-full {getLangFillStatus(l) ? 'bg-[var(--success)]' : 'bg-[var(--warning)]'}"
167
+ ></span>
168
+ {/each}
169
+ </span>
170
+ {/if}
171
+ </div>
172
+ {/if}
173
+
174
+ <!-- Reference block -->
175
+ {#if contentLanguage.referenceMode && isMultiLang}
176
+ {@const refSource = findSourceLang(lang)}
177
+ {#if refSource}
178
+ {@const refText = getReferenceText(refSource)}
179
+ {#if refText}
180
+ <div class="mb-2 rounded-lg border-l-2 border-[var(--primary)]/20 bg-[var(--muted)]/50 p-3 text-sm text-[var(--muted-foreground)]">
181
+ <div class="mb-1 text-xs font-semibold text-[var(--text-light)]">{refLang[interfaceLanguage.current].reference(refSource)}</div>
182
+ <div class="whitespace-pre-wrap">{refText}</div>
183
+ </div>
184
+ {/if}
185
+ {/if}
70
186
  {/if}
71
187
 
72
188
  {#if field.type === 'text'}
@@ -99,6 +215,22 @@
99
215
  <div class="h-32 animate-pulse rounded-md bg-accent"></div>
100
216
  {/await}
101
217
  {/if}
218
+
219
+ <!-- Copy from button -->
220
+ {#if isMultiLang}
221
+ {@const currentVal = resolvePathValue($formData, joinPath(path, lang))}
222
+ {@const sourceLang = isValueEmpty(currentVal) ? findSourceLang(lang) : null}
223
+ {#if sourceLang}
224
+ <button
225
+ type="button"
226
+ class="mt-1 inline-flex items-center gap-1 text-xs text-[var(--muted-foreground)] transition-colors hover:text-[var(--primary)]"
227
+ onclick={() => copyFrom(sourceLang, lang)}
228
+ >
229
+ <ClipboardIcon class="size-3" />
230
+ {copyLang[interfaceLanguage.current].copyFrom(sourceLang)}
231
+ </button>
232
+ {/if}
233
+ {/if}
102
234
  {/snippet}
103
235
  </Form.Control>
104
236
 
@@ -3,20 +3,25 @@
3
3
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
4
4
  import { updates } from '../../../updates/index.js';
5
5
  import { sidebarLang } from './lang.js';
6
+ import ChangelogDialog from '../dashboard/changelog-dialog.svelte';
6
7
 
7
8
  const interfaceLanguage = useInterfaceLanguage();
8
- const version = `v${updates[0].version}`;
9
+ const version = `v${updates[updates.length - 1].version}`;
10
+
11
+ let changelogOpen = $state(false);
9
12
  </script>
10
13
 
11
14
  <div class="border-sidebar-border border-t p-2">
12
15
  <div
13
16
  class="flex items-center justify-between gap-2 px-2.5 py-2 group-data-[collapsible=icon]:px-0"
14
17
  >
15
- <span
16
- class="bg-muted text-text-light shrink-0 rounded-[3px] px-1.5 py-0.5 text-[10px] font-semibold group-data-[collapsible=icon]:hidden"
18
+ <button
19
+ type="button"
20
+ onclick={() => (changelogOpen = true)}
21
+ class="bg-muted text-text-light hover:bg-lavender-lighter hover:text-primary shrink-0 cursor-pointer rounded-[3px] px-1.5 py-0.5 text-[10px] font-semibold transition-colors group-data-[collapsible=icon]:hidden"
17
22
  >
18
23
  {version}
19
- </span>
24
+ </button>
20
25
  <a
21
26
  href="/admin/help"
22
27
  class="border-sidebar-border text-text-light hover:bg-lavender-lighter hover:text-primary hover:border-lavender inline-flex items-center gap-1.5 rounded-md border px-2.5 py-[5px] text-xs font-medium transition-colors group-data-[collapsible=icon]:border-0"
@@ -29,3 +34,5 @@
29
34
  </a>
30
35
  </div>
31
36
  </div>
37
+
38
+ <ChangelogDialog bind:open={changelogOpen} />
@@ -1,18 +1,3 @@
1
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: {
6
- $$events?: Events;
7
- $$slots?: Slots;
8
- }): Exports & {
9
- $set?: any;
10
- $on?: any;
11
- };
12
- z_$$bindings?: Bindings;
13
- }
14
- declare const NavFooter: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
- [evt: string]: CustomEvent<any>;
16
- }, {}, {}, string>;
17
- type NavFooter = InstanceType<typeof NavFooter>;
1
+ declare const NavFooter: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type NavFooter = ReturnType<typeof NavFooter>;
18
3
  export default NavFooter;
@@ -36,6 +36,7 @@ export declare const createEntry: import("@sveltejs/kit").RemoteCommand<{
36
36
  export declare const getRawEntry: import("@sveltejs/kit").RemoteQueryFunction<{
37
37
  id?: string | undefined;
38
38
  slug?: string | undefined;
39
+ includeArchived?: boolean | undefined;
39
40
  }, RawEntry | null>;
40
41
  export declare const getEntryForEntryPage: import("@sveltejs/kit").RemoteQueryFunction<string, RawEntry>;
41
42
  export declare const updateEntryVersionCommand: import("@sveltejs/kit").RemoteCommand<{