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
package/CHANGELOG.md CHANGED
@@ -3,6 +3,33 @@
3
3
  All notable changes to includio-cms are documented here.
4
4
  Generated from `src/lib/updates/` — do not edit manually.
5
5
 
6
+ ## 0.5.2 — 2026-02-25
7
+
8
+ Update system: split migration into sql + notes
9
+
10
+ ### Fixed
11
+ - CmsUpdate.migration split into sql (executable SQL) and notes (manual steps) — fixes CLI crash on text descriptions
12
+ - Changelog dialog: SQL accordion hidden when no SQL, notes shown as inline text
13
+ - Changelog script: SQL in ```sql blocks, notes as plain text
14
+
15
+ ### Breaking
16
+ - CmsUpdate interface: migration field replaced by sql + notes fields
17
+
18
+ ## 0.5.1 — 2026-02-24
19
+
20
+ Restore archived entries, dashboard redesign, translation fixes
21
+
22
+ ### Added
23
+ - Restore archived entries from collection list (single + bulk)
24
+ - Archived entry page: read-only with restore banner
25
+ - Dashboard: changelog modal, orphaned entries redesign, grid layout
26
+
27
+ ### Fixed
28
+ - Opening archived entry no longer returns 500 (getRawEntry now supports includeArchived)
29
+ - Translation flow: reactive status, switcher UX, dynamic ref, copyFrom crash, panel scroll
30
+ - False "Niezapisane zmiany" alert on entry load
31
+ - Hide translation dots for non-required empty fields
32
+
6
33
  ## 0.5.0 — 2026-02-22
7
34
 
8
35
  Frontend rendering for structured content
@@ -13,11 +40,9 @@ Frontend rendering for structured content
13
40
  - Query helpers: `extractBlocks()`, `extractInlineBlocks()`, `extractText()`, `extractMediaRefs()`
14
41
  - Media resolution in population pipeline for `content` field — figure/video nodes enriched with `_media`
15
42
 
16
- ### Migration
43
+ ### Notes
17
44
 
18
- ```sql
19
45
  No migration needed. Import `StructuredContent` from `includio/sveltekit` to render content fields.
20
- ```
21
46
 
22
47
  ## 0.2.2 — 2026-02-21
23
48
 
@@ -28,11 +53,9 @@ Structured Content field (TipTap JSON)
28
53
  - Extracted shared editor toolbar component
29
54
  - Content field link resolution in resolve pipeline
30
55
 
31
- ### Migration
56
+ ### Notes
32
57
 
33
- ```sql
34
58
  No migration needed. Add type:"content" fields to collection/single configs. Existing richtext fields unchanged.
35
- ```
36
59
 
37
60
  ## 0.2.0 — 2026-02-21
38
61
 
@@ -44,11 +67,9 @@ Array→Blocks rename, new simple array field
44
67
  ### Breaking
45
68
  - type:"array" with objects renamed to type:"blocks". Update collection/single configs.
46
69
 
47
- ### Migration
70
+ ### Notes
48
71
 
49
- ```sql
50
72
  Config-only: rename type:"array" to type:"blocks" in field definitions. Stored data unchanged.
51
- ```
52
73
 
53
74
  ## 0.1.5 — 2026-02-21
54
75
 
@@ -65,11 +86,9 @@ Simplified entry versioning
65
86
  - Auto-save no longer creates dozens of empty versions
66
87
  - Pruning no longer deletes important versions to make room for empty ones
67
88
 
68
- ### Migration
89
+ ### Notes
69
90
 
70
- ```sql
71
91
  Removes duplicate entry versions where data is identical to the previous version. Preserves published, scheduled, and latest draft versions.
72
- ```
73
92
 
74
93
  ## 0.1.4 — 2026-02-19
75
94
 
package/ROADMAP.md CHANGED
@@ -64,6 +64,18 @@
64
64
  - **0.4.1** — Accessibility layer (ATAG Part B)
65
65
  - **0.5.0** — Frontend rendering
66
66
 
67
+ ## 0.5.1 — Patches & dashboard
68
+
69
+ - [x] `[feature]` `[P1]` Restore archived entries — banner, read-only mode, list actions
70
+ - [x] `[feature]` `[P2]` Dashboard: changelog modal, orphaned entries redesign, grid layout
71
+ - [x] `[fix]` `[P1]` Translation flow — reactive status, switcher UX, dynamic ref, copyFrom crash, panel scroll
72
+ - [x] `[fix]` `[P1]` False "Niezapisane zmiany" on entry load
73
+ - [x] `[fix]` `[P2]` Hide translation dots for non-required empty fields
74
+
75
+ ## 0.5.2 — Update system fix
76
+
77
+ - [x] `[fix]` `[P1]` Split `migration` field into `sql` + `notes` — fixes CLI crash on text descriptions
78
+
67
79
  ## 0.6.0 — Plugin system _(deferred from 0.2.0)_
68
80
 
69
81
  - [ ] `[feature]` `[P0]` Wire plugin hooks into CRUD operations (before/afterCreate, Update, Delete) <!-- files: src/lib/types/plugins.ts, src/lib/core/server/entries/operations/ -->
@@ -1,5 +1,5 @@
1
1
  <script lang="ts">
2
- import UpdatesBanner from '../../components/dashboard/updates-banner.svelte';
2
+ import OrphanedEntriesNotice from '../../components/dashboard/orphaned-entries-notice.svelte';
3
3
  import WelcomeHeader from '../../components/dashboard/welcome-header.svelte';
4
4
  import RecentEntries from '../../components/dashboard/recent-entries.svelte';
5
5
  import FormSubmissionsWidget from '../../components/dashboard/form-submissions-widget.svelte';
@@ -9,12 +9,8 @@
9
9
  import { sidebarLang } from '../../components/layout/lang.js';
10
10
  import { getBreadcrumbs } from '../../state/breadcrumbs.svelte.js';
11
11
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
12
- import * as Alert from '../../../components/ui/alert/index.js';
13
- import { Button } from '../../../components/ui/button/index.js';
14
12
 
15
13
  import type { InterfaceLanguage } from '../../../types/languages.js';
16
- import AlertTriangleIcon from '@tabler/icons-svelte/icons/alert-triangle';
17
- import TrashIcon from '@tabler/icons-svelte/icons/trash';
18
14
 
19
15
  type OrphanedEntry = { id: string; slug: string; createdAt: string };
20
16
 
@@ -39,7 +35,11 @@
39
35
  }
40
36
 
41
37
  async function deleteOrphaned() {
42
- if (!confirm(orphanedLang[interfaceLanguage.current].confirmDelete)) return;
38
+ const msg: Record<InterfaceLanguage, string> = {
39
+ pl: 'Czy na pewno chcesz usunąć te wpisy? Tej operacji nie można cofnąć.',
40
+ en: 'Are you sure you want to delete these entries? This cannot be undone.'
41
+ };
42
+ if (!confirm(msg[interfaceLanguage.current])) return;
43
43
 
44
44
  deletingOrphaned = true;
45
45
  try {
@@ -65,78 +65,32 @@
65
65
  }
66
66
  ];
67
67
  });
68
-
69
- const orphanedLang: Record<
70
- InterfaceLanguage,
71
- { title: string; description: string; deleteBtn: string; confirmDelete: string }
72
- > = {
73
- pl: {
74
- title: 'Znaleziono osirocone wpisy',
75
- description:
76
- 'W bazie danych znajdują się wpisy, których konfiguracja została usunięta. Zalecane jest ich usunięcie.',
77
- deleteBtn: 'Usuń osirocone wpisy',
78
- confirmDelete: 'Czy na pewno chcesz usunąć wszystkie osirocone wpisy? Ta operacja jest nieodwracalna.'
79
- },
80
- en: {
81
- title: 'Orphaned entries found',
82
- description:
83
- 'The database contains entries whose configuration has been removed. It is recommended to delete them.',
84
- deleteBtn: 'Delete orphaned entries',
85
- confirmDelete: 'Are you sure you want to delete all orphaned entries? This operation is irreversible.'
86
- }
87
- };
88
68
  </script>
89
69
 
90
70
  <main class="dash-main">
91
- <UpdatesBanner />
92
-
93
- {#if !loadingOrphaned && orphanedEntries.length > 0}
94
- <Alert.Root
95
- variant="destructive"
96
- class="mb-5 rounded-2xl border-orange-300/50 bg-orange-50 dark:border-orange-500/30 dark:bg-orange-950/60"
97
- >
98
- <AlertTriangleIcon class="text-orange-600 dark:text-orange-400" />
99
- <Alert.Title class="text-orange-800 dark:text-orange-200">
100
- {orphanedLang[interfaceLanguage.current].title} ({orphanedEntries.length})
101
- </Alert.Title>
102
- <Alert.Description class="text-orange-700 dark:text-orange-300">
103
- <p class="mb-3">{orphanedLang[interfaceLanguage.current].description}</p>
104
- <details class="mb-3">
105
- <summary class="cursor-pointer text-sm font-medium">Slugs</summary>
106
- <ul class="mt-2 list-inside list-disc text-sm">
107
- {#each orphanedEntries as entry}
108
- <li><code class="rounded bg-orange-200/50 px-1 dark:bg-orange-800/50">{entry.slug}</code></li>
109
- {/each}
110
- </ul>
111
- </details>
112
- <Button
113
- variant="destructive"
114
- size="sm"
115
- onclick={deleteOrphaned}
116
- disabled={deletingOrphaned}
117
- >
118
- <TrashIcon class="mr-1 h-4 w-4" />
119
- {orphanedLang[interfaceLanguage.current].deleteBtn}
120
- </Button>
121
- </Alert.Description>
122
- </Alert.Root>
71
+ {#if !loadingOrphaned}
72
+ <OrphanedEntriesNotice
73
+ entries={orphanedEntries}
74
+ deleting={deletingOrphaned}
75
+ ondelete={deleteOrphaned}
76
+ />
123
77
  {/if}
124
78
 
125
79
  <WelcomeHeader />
126
80
 
127
- <div class="dashboard-grid">
128
- <div class="dashboard-grid-main">
129
- <div class="content-duo">
81
+ <div class="flex gap-5">
82
+ <div class="flex-1">
83
+ <div class="grid grid-cols-2 gap-5">
130
84
  <RecentEntries />
131
85
  <FormSubmissionsWidget />
132
86
  </div>
133
- <div style="margin-top:20px">
87
+ <div class="mt-5">
134
88
  <RecentActivity />
135
89
  </div>
136
90
  </div>
137
- <div class="dashboard-grid-aside">
91
+ <div class="max-w-80 flex-1 shrink-0">
138
92
  <A11yGauge />
139
- <div style="margin-top:20px">
93
+ <div class="mt-5">
140
94
  <TipOfTheDay />
141
95
  </div>
142
96
  </div>
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import Archive from '@tabler/icons-svelte/icons/archive';
3
+ import ArchiveOff from '@tabler/icons-svelte/icons/archive-off';
3
4
  import Trash from '@tabler/icons-svelte/icons/trash';
4
5
  import X from '@tabler/icons-svelte/icons/x';
5
6
  import Button from '../../../components/ui/button/button.svelte';
@@ -12,25 +13,29 @@
12
13
  onArchive: () => void;
13
14
  onDelete: () => void;
14
15
  onClear: () => void;
16
+ onRestore?: () => void;
15
17
  };
16
18
 
17
- let { selectedCount, onArchive, onDelete, onClear }: Props = $props();
19
+ let { selectedCount, onArchive, onDelete, onClear, onRestore }: Props = $props();
18
20
 
19
21
  const interfaceLanguage = useInterfaceLanguage();
20
22
 
21
23
  const lang: Record<InterfaceLanguage, {
22
24
  selected: (n: number) => string;
23
25
  archive: string;
26
+ restore: string;
24
27
  delete: string;
25
28
  }> = {
26
29
  en: {
27
30
  selected: (n) => `${n} selected`,
28
31
  archive: 'Archive',
32
+ restore: 'Restore',
29
33
  delete: 'Delete'
30
34
  },
31
35
  pl: {
32
36
  selected: (n) => `Zaznaczono: ${n}`,
33
37
  archive: 'Archiwizuj',
38
+ restore: 'Przywróć',
34
39
  delete: 'Usuń'
35
40
  }
36
41
  };
@@ -52,6 +57,17 @@
52
57
 
53
58
  <div class="h-5 w-px bg-white/20"></div>
54
59
 
60
+ {#if onRestore}
61
+ <Button
62
+ variant="ghost"
63
+ size="sm"
64
+ class="text-white hover:bg-white/10 hover:text-white"
65
+ onclick={onRestore}
66
+ >
67
+ <ArchiveOff class="mr-1.5 size-3.5" />
68
+ {t.restore}
69
+ </Button>
70
+ {:else}
55
71
  <Button
56
72
  variant="ghost"
57
73
  size="sm"
@@ -61,6 +77,7 @@
61
77
  <Archive class="mr-1.5 size-3.5" />
62
78
  {t.archive}
63
79
  </Button>
80
+ {/if}
64
81
  <Button
65
82
  variant="ghost"
66
83
  size="sm"
@@ -3,6 +3,7 @@ type Props = {
3
3
  onArchive: () => void;
4
4
  onDelete: () => void;
5
5
  onClear: () => void;
6
+ onRestore?: () => void;
6
7
  };
7
8
  declare const BulkActionsBar: import("svelte").Component<Props, {}, "">;
8
9
  type BulkActionsBar = ReturnType<typeof BulkActionsBar>;
@@ -47,6 +47,8 @@
47
47
  selectAll: string;
48
48
  selectRow: string;
49
49
  entriesArchived: string;
50
+ entryRestored: string;
51
+ entriesRestored: string;
50
52
  entryDeleted: string;
51
53
  deleteConfirmTitle: string;
52
54
  deleteConfirmDescription: string;
@@ -65,6 +67,8 @@
65
67
  selectAll: 'Select all',
66
68
  selectRow: 'Select row',
67
69
  entriesArchived: 'Entries archived',
70
+ entryRestored: 'Entry restored',
71
+ entriesRestored: 'Entries restored',
68
72
  entryDeleted: 'Entry permanently deleted',
69
73
  deleteConfirmTitle: 'Delete entry permanently?',
70
74
  deleteConfirmDescription:
@@ -83,6 +87,8 @@
83
87
  selectAll: 'Zaznacz wszystkie',
84
88
  selectRow: 'Zaznacz wiersz',
85
89
  entriesArchived: 'Wpisy zarchiwizowane',
90
+ entryRestored: 'Wpis przywrócony',
91
+ entriesRestored: 'Wpisy przywrócone',
86
92
  entryDeleted: 'Wpis trwale usunięty',
87
93
  deleteConfirmTitle: 'Usunąć wpis na stałe?',
88
94
  deleteConfirmDescription: 'Ta akcja jest nieodwracalna. Wpis zostanie trwale usunięty.',
@@ -242,7 +248,8 @@
242
248
  entryUrl: info.row.original.url,
243
249
  entryName: info.row.original.name,
244
250
  onArchive: () => handleArchiveSingle(info.row.original.id),
245
- onDelete: () => handleDelete(info.row.original.id)
251
+ onDelete: () => handleDelete(info.row.original.id),
252
+ ...(isArchivedFilter ? { onRestore: () => handleRestoreSingle(info.row.original.id) } : {})
246
253
  }),
247
254
  enableSorting: false,
248
255
  size: 50
@@ -259,6 +266,12 @@
259
266
  refreshQueries();
260
267
  }
261
268
 
269
+ async function handleRestoreSingle(id: string) {
270
+ await remotes.unarchiveEntryCommand(id);
271
+ toast.success(lang[interfaceLanguage.current].entryRestored);
272
+ refreshQueries();
273
+ }
274
+
262
275
  function handleDelete(id: string) {
263
276
  pendingDeleteId = id;
264
277
  deleteDialogOpen = true;
@@ -381,6 +394,16 @@
381
394
  refreshQueries();
382
395
  }
383
396
 
397
+ async function handleBulkRestore(items: CollectionDataTableRow[]) {
398
+ const idsToRestore = selectedIndices.map((idx) => items[idx]?.id).filter(Boolean);
399
+ for (const id of idsToRestore) {
400
+ await remotes.unarchiveEntryCommand(id);
401
+ }
402
+ toast.success(lang[interfaceLanguage.current].entriesRestored);
403
+ rowSelection = {};
404
+ refreshQueries();
405
+ }
406
+
384
407
  async function handleBulkDelete(items: CollectionDataTableRow[]) {
385
408
  const idsToDelete = selectedIndices.map((idx) => items[idx]?.id).filter(Boolean);
386
409
  for (const id of idsToDelete) {
@@ -543,6 +566,7 @@
543
566
  onArchive={() => handleBulkArchive(displayItems)}
544
567
  onDelete={() => handleBulkDelete(displayItems)}
545
568
  onClear={() => (rowSelection = {})}
569
+ onRestore={isArchivedFilter ? () => handleBulkRestore(displayItems) : undefined}
546
570
  />
547
571
  {/await}
548
572
  {/key}
@@ -2,6 +2,7 @@
2
2
  import DotsVertical from '@tabler/icons-svelte/icons/dots-vertical';
3
3
  import ExternalLink from '@tabler/icons-svelte/icons/external-link';
4
4
  import Archive from '@tabler/icons-svelte/icons/archive';
5
+ import ArchiveOff from '@tabler/icons-svelte/icons/archive-off';
5
6
  import Trash from '@tabler/icons-svelte/icons/trash';
6
7
  import Button from '../../../components/ui/button/button.svelte';
7
8
  import * as DropdownMenu from '../../../components/ui/dropdown-menu/index.js';
@@ -13,15 +14,16 @@
13
14
  entryName: string;
14
15
  onArchive: () => void;
15
16
  onDelete: () => void;
17
+ onRestore?: () => void;
16
18
  };
17
19
 
18
- let { entryUrl, entryName, onArchive, onDelete }: Props = $props();
20
+ let { entryUrl, entryName, onArchive, onDelete, onRestore }: Props = $props();
19
21
 
20
22
  const interfaceLanguage = useInterfaceLanguage();
21
23
 
22
- const lang: Record<InterfaceLanguage, { open: string; archive: string; delete: string; moreActions: string }> = {
23
- en: { open: 'Open', archive: 'Archive', delete: 'Delete', moreActions: 'More actions for' },
24
- pl: { open: 'Otwórz', archive: 'Archiwizuj', delete: 'Usuń', moreActions: 'Więcej akcji dla' }
24
+ const lang: Record<InterfaceLanguage, { open: string; archive: string; restore: string; delete: string; moreActions: string }> = {
25
+ en: { open: 'Open', archive: 'Archive', restore: 'Restore', delete: 'Delete', moreActions: 'More actions for' },
26
+ pl: { open: 'Otwórz', archive: 'Archiwizuj', restore: 'Przywróć', delete: 'Usuń', moreActions: 'Więcej akcji dla' }
25
27
  };
26
28
 
27
29
  const t = $derived(lang[interfaceLanguage.current]);
@@ -48,10 +50,17 @@
48
50
  {t.open}
49
51
  </DropdownMenu.Item>
50
52
  <DropdownMenu.Separator />
53
+ {#if onRestore}
54
+ <DropdownMenu.Item onclick={onRestore}>
55
+ <ArchiveOff class="mr-2 size-4" />
56
+ {t.restore}
57
+ </DropdownMenu.Item>
58
+ {:else}
51
59
  <DropdownMenu.Item onclick={onArchive}>
52
60
  <Archive class="mr-2 size-4" />
53
61
  {t.archive}
54
62
  </DropdownMenu.Item>
63
+ {/if}
55
64
  <DropdownMenu.Item class="text-destructive focus:text-destructive" onclick={onDelete}>
56
65
  <Trash class="mr-2 size-4" />
57
66
  {t.delete}
@@ -3,6 +3,7 @@ type Props = {
3
3
  entryName: string;
4
4
  onArchive: () => void;
5
5
  onDelete: () => void;
6
+ onRestore?: () => void;
6
7
  };
7
8
  declare const RowActions: import("svelte").Component<Props, {}, "">;
8
9
  type RowActions = ReturnType<typeof RowActions>;
@@ -11,10 +11,12 @@
11
11
  import LayoutSidebar from '@tabler/icons-svelte/icons/layout-sidebar';
12
12
  import SendIcon from '@tabler/icons-svelte/icons/send';
13
13
  import ChevronDownIcon from '@tabler/icons-svelte/icons/chevron-down';
14
+ import LanguageIcon from '@tabler/icons-svelte/icons/language';
14
15
  import * as DropdownMenu from '../../../components/ui/dropdown-menu/index.js';
15
16
  import { hasHybridContext, getHybridContext } from './hybrid/hybrid-context.svelte.js';
16
17
  import { getEntryStatus } from './utils.js';
17
18
  import { onMount } from 'svelte';
19
+ import type { LangStatus } from '../../utils/translationStatus.js';
18
20
 
19
21
  const contentLanguage = getContentLanguage();
20
22
  const interfaceLanguage = useInterfaceLanguage();
@@ -50,7 +52,9 @@
50
52
  onSaveDraft: () => void;
51
53
  onArchive: () => void;
52
54
  saveStatus?: SaveStatus;
55
+ isArchived?: boolean;
53
56
  onScrollToIssue?: (fieldSlug: string, nodePos: number) => void;
57
+ translationStatus?: Record<string, LangStatus> | null;
54
58
  };
55
59
 
56
60
  let {
@@ -62,7 +66,9 @@
62
66
  fields = [],
63
67
  getFormData,
64
68
  saveStatus = 'idle',
65
- onScrollToIssue
69
+ isArchived = false,
70
+ onScrollToIssue,
71
+ translationStatus = null
66
72
  }: Props = $props();
67
73
  let { collection } = entry;
68
74
 
@@ -86,6 +92,31 @@
86
92
  isMac = /Mac|iPhone|iPad|iPod/.test(navigator.platform);
87
93
  });
88
94
  const shortcutLabel = $derived(isMac ? '⌘S' : 'Ctrl+S');
95
+
96
+ function statusDotClass(status: LangStatus | undefined): string {
97
+ if (!status) return 'bg-[var(--text-light)]';
98
+ if (status.status === 'complete') return 'bg-[var(--success)]';
99
+ if (status.status === 'partial') return 'bg-[var(--warning)]';
100
+ return 'bg-[var(--text-light)]';
101
+ }
102
+
103
+ function statusLabel(lang: string, status: LangStatus | undefined): string {
104
+ if (!status) return lang.toUpperCase();
105
+ if (status.status === 'complete') return `${lang.toUpperCase()} \u2713`;
106
+ if (status.status === 'partial') return `${lang.toUpperCase()} \u26A0`;
107
+ return lang.toUpperCase();
108
+ }
109
+
110
+ function statusTooltip(lang: string, status: LangStatus | undefined): string {
111
+ if (!status) return lang.toUpperCase();
112
+ if (status.status === 'complete') return `${lang.toUpperCase()}: 100%`;
113
+ const missing = status.missingFields.map((f) => f.label).join(', ');
114
+ return `${lang.toUpperCase()}: ${status.percentage}% — ${interfaceLanguage.current === 'pl' ? 'brakuje' : 'missing'}: ${missing}`;
115
+ }
116
+
117
+ const isNonDefaultLang = $derived(
118
+ contentLanguage.all.length > 1 && contentLanguage.current !== contentLanguage.all[0]
119
+ );
89
120
  </script>
90
121
 
91
122
  <div
@@ -103,24 +134,39 @@
103
134
  {#if contentLanguage.all.length > 1}
104
135
  <div class="border-border bg-muted inline-flex overflow-hidden rounded-lg border">
105
136
  {#each contentLanguage.all as lang}
137
+ {@const langStatus = translationStatus?.[lang]}
106
138
  <button
107
139
  type="button"
108
140
  role="tab"
109
141
  aria-selected={lang === contentLanguage.current}
110
- class="px-2.5 py-1 text-xs font-semibold transition-colors {lang ===
142
+ title={statusTooltip(lang, langStatus)}
143
+ class="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-semibold transition-colors {lang ===
111
144
  contentLanguage.current
112
- ? 'text-primary bg-white shadow-sm'
145
+ ? 'text-primary bg-white shadow-sm ring-1 ring-primary/20'
113
146
  : 'text-muted-foreground hover:text-foreground bg-transparent'}"
114
147
  onclick={() => (contentLanguage.current = lang)}
115
148
  >
116
- {lang.toUpperCase()}
149
+ <span class="size-2 shrink-0 rounded-full {statusDotClass(langStatus)}"></span>
150
+ {statusLabel(lang, langStatus)}
117
151
  </button>
118
152
  {/each}
119
153
  </div>
120
154
 
155
+ <!-- Reference mode toggle -->
156
+ <Button
157
+ variant="ghost"
158
+ size="icon-sm"
159
+ class={contentLanguage.referenceMode ? 'text-primary bg-[var(--lavender-lighter)]' : ''}
160
+ onclick={() => (contentLanguage.referenceMode = !contentLanguage.referenceMode)}
161
+ title={interfaceLanguage.current === 'pl' ? 'Tryb referencyjny' : 'Reference mode'}
162
+ >
163
+ <LanguageIcon class="size-4" />
164
+ </Button>
165
+
121
166
  <div class="bg-border mx-1 h-5 w-px shrink-0"></div>
122
167
  {/if}
123
168
 
169
+ {#if !isArchived}
124
170
  <!-- Split button: Publish + Save Draft -->
125
171
  <div class="inline-flex items-stretch rounded-lg shadow-[0_1px_3px_rgba(91,74,158,0.3)]">
126
172
  <button
@@ -155,6 +201,7 @@
155
201
  </div>
156
202
 
157
203
  <div class="bg-border mx-1 h-5 w-px shrink-0"></div>
204
+ {/if}
158
205
 
159
206
  <!-- Panel triggers + Hybrid toggle -->
160
207
  {#await import('./header/publish-panel.svelte') then { default: PublishPanel }}
@@ -1,6 +1,7 @@
1
1
  import type { DbEntryVersion, RawEntry } from '../../../types/entries.js';
2
2
  import type { UpdateEntryVersionCommandType } from '../../../core/server/entries/operations/update.js';
3
3
  import type { Field } from '../../../types/fields.js';
4
+ import type { LangStatus } from '../../utils/translationStatus.js';
4
5
  type SaveStatus = 'idle' | 'saving' | 'saved' | 'unsaved' | 'error';
5
6
  type Props = {
6
7
  entry: RawEntry;
@@ -11,7 +12,9 @@ type Props = {
11
12
  onSaveDraft: () => void;
12
13
  onArchive: () => void;
13
14
  saveStatus?: SaveStatus;
15
+ isArchived?: boolean;
14
16
  onScrollToIssue?: (fieldSlug: string, nodePos: number) => void;
17
+ translationStatus?: Record<string, LangStatus> | null;
15
18
  };
16
19
  declare const EntryHeader: import("svelte").Component<Props, {}, "">;
17
20
  type EntryHeader = ReturnType<typeof EntryHeader>;