includio-cms 0.0.68 → 0.1.0

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 (55) hide show
  1. package/CHANGELOG.md +181 -0
  2. package/ROADMAP.md +92 -0
  3. package/dist/admin/client/account/lang.d.ts +0 -5
  4. package/dist/admin/client/account/lang.js +2 -12
  5. package/dist/admin/client/account/preferences-section.svelte +27 -24
  6. package/dist/admin/client/account/profile-section.svelte +11 -15
  7. package/dist/admin/client/account/security-section.svelte +0 -11
  8. package/dist/admin/client/account/sessions-section.svelte +2 -1
  9. package/dist/admin/client/collection/collection-entries.svelte +168 -43
  10. package/dist/admin/client/collection/collection-view.svelte.d.ts +5 -0
  11. package/dist/admin/client/collection/collection-view.svelte.js +22 -5
  12. package/dist/admin/client/collection/collection.svelte +2 -2
  13. package/dist/admin/client/collection/data-table.svelte +15 -3
  14. package/dist/admin/client/collection/data-table.svelte.d.ts +3 -0
  15. package/dist/admin/client/entry/entry.svelte +11 -7
  16. package/dist/admin/client/entry/header/status-badge.svelte +6 -3
  17. package/dist/admin/client/entry/header/version-history-sheet.svelte +6 -3
  18. package/dist/admin/client/form/form-submissions.svelte +3 -26
  19. package/dist/admin/components/dashboard/form-submissions-widget.svelte +2 -12
  20. package/dist/admin/components/dashboard/recent-activity.svelte +2 -12
  21. package/dist/admin/components/fields/array-field.svelte +126 -71
  22. package/dist/admin/components/fields/relation-field.svelte +6 -10
  23. package/dist/admin/components/layout/header-actions.svelte +4 -3
  24. package/dist/admin/components/layout/nav-search.svelte +43 -31
  25. package/dist/admin/components/media/media-library.svelte +17 -6
  26. package/dist/admin/remote/entry.remote.d.ts +10 -1
  27. package/dist/admin/remote/entry.remote.js +16 -4
  28. package/dist/admin/state/interface-language.svelte.d.ts +4 -7
  29. package/dist/admin/state/interface-language.svelte.js +19 -18
  30. package/dist/admin/utils/arrayMove.d.ts +5 -0
  31. package/dist/admin/utils/arrayMove.js +12 -0
  32. package/dist/admin/utils/formatDate.d.ts +1 -0
  33. package/dist/admin/utils/formatDate.js +5 -1
  34. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  35. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  36. package/dist/core/server/entries/operations/get.d.ts +2 -0
  37. package/dist/core/server/entries/operations/get.js +6 -0
  38. package/dist/db-postgres/index.js +26 -1
  39. package/dist/sveltekit/components/hybrid-context.d.ts +4 -0
  40. package/dist/sveltekit/components/hybrid-context.js +9 -0
  41. package/dist/sveltekit/components/hybrid-target.svelte +4 -1
  42. package/dist/sveltekit/components/image.svelte +5 -2
  43. package/dist/sveltekit/components/preview.svelte +3 -0
  44. package/dist/sveltekit/components/video.svelte +4 -1
  45. package/dist/sveltekit/index.d.ts +1 -0
  46. package/dist/sveltekit/index.js +1 -0
  47. package/dist/types/adapters/db.d.ts +3 -1
  48. package/dist/types/entries.d.ts +10 -2
  49. package/dist/types/languages.d.ts +2 -1
  50. package/dist/updates/0.0.69/index.d.ts +2 -0
  51. package/dist/updates/0.0.69/index.js +12 -0
  52. package/dist/updates/0.1.0/index.d.ts +2 -0
  53. package/dist/updates/0.1.0/index.js +25 -0
  54. package/dist/updates/index.js +3 -1
  55. package/package.json +7 -2
@@ -30,6 +30,10 @@
30
30
  import RequiredLabel from './required-label.svelte';
31
31
  import { cn } from '../../../utils.js';
32
32
  import BlockPickerModal from './block-picker-modal.svelte';
33
+ import GripVertical from '@tabler/icons-svelte/icons/grip-vertical';
34
+ import { droppable, draggable, dndState } from '@thisux/sveltednd';
35
+ import { flip } from 'svelte/animate';
36
+ import { arrayMove } from '../../utils/arrayMove.js';
33
37
 
34
38
  const contentLanguage = getContentLanguage();
35
39
  const interfaceLanguage = useInterfaceLanguage();
@@ -119,6 +123,17 @@
119
123
  });
120
124
  }
121
125
 
126
+ let dropProcessing = false;
127
+
128
+ function moveItem(from: number, to: number) {
129
+ if (!$value || from === to) return;
130
+ $value = arrayMove($value, from, to);
131
+
132
+ tick().then(() => {
133
+ openAndCloseOthers(to);
134
+ });
135
+ }
136
+
122
137
  function removeItem(index: number) {
123
138
  if (!$value) return;
124
139
 
@@ -208,80 +223,120 @@
208
223
  <Accordion.Root type="multiple" class="w-full space-y-4" bind:value={accordionOpenState}>
209
224
  {#if $value && $value.length > 0}
210
225
  {#each $value as item, index (item._id ?? index)}
211
- {#if $value[index].data && $value[index].slug}
212
- {@const item = $value[index]}
213
- {@const objectField = field.of.find((option) => option.slug === item.slug)}
214
-
215
- {#if objectField}
216
- <Accordion.Item value={index.toString()} class="border-0" data-depth={depth + 1}>
217
- <Accordion.Trigger
218
- class="items-center border px-4 text-base font-normal data-[state=open]:rounded-b-none dark:bg-slate-800/30 dark:hover:bg-slate-700/40 dark:border-white/[0.08]"
219
- >
220
- <div class="flex grow items-center justify-between gap-4">
221
- <div class="flex items-center gap-4">
222
- <span>{index < 10 ? '0' : ''}{index + 1}</span>
223
- <Badge variant="outline">{getLocalizedLabel(objectField.label, interfaceLanguage.current) || objectField.slug}</Badge>
224
- <span>{getAccordionLabel($value[index])}</span>
225
- </div>
226
-
227
- <DropdownMenu.Root>
228
- <DropdownMenu.Trigger
229
- class="data-[state=open]:bg-muted text-muted-foreground flex size-8"
230
- >
231
- {#snippet child({ props })}
232
- <Button variant="ghost" size="icon" {...props}>
233
- <DotsVerticalIcon />
234
- <span class="sr-only">Open menu</span>
235
- </Button>
236
- {/snippet}
237
- </DropdownMenu.Trigger>
238
- <DropdownMenu.Content align="end" class="w-32">
239
- <DropdownMenu.Item onclick={() => duplicateItem(index)}>
240
- Duplicate
241
- </DropdownMenu.Item>
242
- <DropdownMenu.Item onclick={() => moveItemUp(index)} disabled={index === 0}>
243
- Move up
244
- </DropdownMenu.Item>
245
- <DropdownMenu.Item
246
- onclick={() => moveItemDown(index)}
247
- disabled={$value && index === $value.length - 1}
226
+ <div
227
+ use:droppable={{
228
+ container: index.toString(),
229
+ callbacks: {
230
+ onDrop: (state) => {
231
+ if (dropProcessing) return;
232
+ dropProcessing = true;
233
+
234
+ const dragIndex = parseInt(state.sourceContainer ?? '');
235
+ const dropIndex = parseInt(state.targetContainer ?? '');
236
+ if (!isNaN(dragIndex) && !isNaN(dropIndex)) {
237
+ moveItem(dragIndex, dropIndex);
238
+ }
239
+
240
+ // {#key index} destroys the old draggable before dragend fires,
241
+ // leaving isDragging stuck at true. Reset manually.
242
+ dndState.isDragging = false;
243
+ dndState.draggedItem = null;
244
+ dndState.sourceContainer = '';
245
+ dndState.targetContainer = null;
246
+ dndState.targetElement = null;
247
+
248
+ setTimeout(() => { dropProcessing = false; }, 50);
249
+ }
250
+ }
251
+ }}
252
+ animate:flip={{ duration: 200 }}
253
+ >
254
+ {#key index}
255
+ {#if $value[index].data && $value[index].slug}
256
+ {@const item = $value[index]}
257
+ {@const objectField = field.of.find((option) => option.slug === item.slug)}
258
+
259
+ {#if objectField}
260
+ <Accordion.Item value={index.toString()} class="border-0" data-depth={depth + 1}>
261
+ <Accordion.Trigger
262
+ class="items-center border px-4 text-base font-normal data-[state=open]:rounded-b-none dark:bg-slate-800/30 dark:hover:bg-slate-700/40 dark:border-white/[0.08]"
263
+ >
264
+ <div class="flex grow items-center justify-between gap-4">
265
+ <div class="flex items-center gap-4">
266
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
267
+ <div
268
+ use:draggable={{ container: index.toString(), dragData: { id: item._id } }}
269
+ class="cursor-grab text-muted-foreground hover:text-foreground"
270
+ onmousedown={(e) => e.stopPropagation()}
271
+ onclick={(e) => e.stopPropagation()}
248
272
  >
249
- Move down
250
- </DropdownMenu.Item>
251
- <DropdownMenu.Item variant="destructive" onclick={() => removeItem(index)}>
252
- Delete
253
- </DropdownMenu.Item>
254
- </DropdownMenu.Content>
255
- </DropdownMenu.Root>
256
- </div>
257
- </Accordion.Trigger>
258
- <Accordion.Content
259
- class="space-y-4 rounded-b-md border border-t-0 dark:bg-slate-900/30 dark:shadow-[inset_0_2px_4px_rgb(0_0_0/0.1)] dark:border-white/[0.08]"
260
- style="padding: {Math.max(4, 16 - depth * 3)}px;"
261
- >
262
- {@const itemPath = joinPath(path, index)}
263
- <div data-field-path={itemPath}>
264
- <FieldRenderer
265
- objectFieldType="inline"
266
- field={objectField}
267
- form={form as SuperForm<Record<string, unknown>>}
268
- path={itemPath as FormPathLeaves<T, ObjectFieldData>}
269
- {focusedPath}
270
- {flashingPath}
271
- depth={depth + 1}
272
- />
273
- </div>
274
- </Accordion.Content>
275
- </Accordion.Item>
273
+ <GripVertical class="h-4 w-4" />
274
+ </div>
275
+ <span>{index < 10 ? '0' : ''}{index + 1}</span>
276
+ <Badge variant="outline">{getLocalizedLabel(objectField.label, interfaceLanguage.current) || objectField.slug}</Badge>
277
+ <span>{getAccordionLabel($value[index])}</span>
278
+ </div>
279
+
280
+ <DropdownMenu.Root>
281
+ <DropdownMenu.Trigger
282
+ class="data-[state=open]:bg-muted text-muted-foreground flex size-8"
283
+ >
284
+ {#snippet child({ props })}
285
+ <Button variant="ghost" size="icon" {...props}>
286
+ <DotsVerticalIcon />
287
+ <span class="sr-only">Open menu</span>
288
+ </Button>
289
+ {/snippet}
290
+ </DropdownMenu.Trigger>
291
+ <DropdownMenu.Content align="end" class="w-32">
292
+ <DropdownMenu.Item onclick={() => duplicateItem(index)}>
293
+ Duplicate
294
+ </DropdownMenu.Item>
295
+ <DropdownMenu.Item onclick={() => moveItemUp(index)} disabled={index === 0}>
296
+ Move up
297
+ </DropdownMenu.Item>
298
+ <DropdownMenu.Item
299
+ onclick={() => moveItemDown(index)}
300
+ disabled={$value && index === $value.length - 1}
301
+ >
302
+ Move down
303
+ </DropdownMenu.Item>
304
+ <DropdownMenu.Item variant="destructive" onclick={() => removeItem(index)}>
305
+ Delete
306
+ </DropdownMenu.Item>
307
+ </DropdownMenu.Content>
308
+ </DropdownMenu.Root>
309
+ </div>
310
+ </Accordion.Trigger>
311
+ <Accordion.Content
312
+ class="space-y-4 rounded-b-md border border-t-0 dark:bg-slate-900/30 dark:shadow-[inset_0_2px_4px_rgb(0_0_0/0.1)] dark:border-white/[0.08]"
313
+ style="padding: {Math.max(4, 16 - depth * 3)}px;"
314
+ >
315
+ {@const itemPath = joinPath(path, index)}
316
+ <div data-field-path={itemPath}>
317
+ <FieldRenderer
318
+ objectFieldType="inline"
319
+ field={objectField}
320
+ form={form as SuperForm<Record<string, unknown>>}
321
+ path={itemPath as FormPathLeaves<T, ObjectFieldData>}
322
+ {focusedPath}
323
+ {flashingPath}
324
+ depth={depth + 1}
325
+ />
326
+ </div>
327
+ </Accordion.Content>
328
+ </Accordion.Item>
329
+ {:else}
330
+ <p class="text-red-500">
331
+ Invalid field configuration. Unknown slug:
332
+ {$value[index].slug}
333
+ </p>
334
+ {/if}
276
335
  {:else}
277
- <p class="text-red-500">
278
- Invalid field configuration. Unknown slug:
279
- {$value[index].slug}
280
- </p>
336
+ <p class="text-red-500">Invalid field configuration. Index: {index}</p>
281
337
  {/if}
282
- {:else}
283
- <p class="text-red-500">Invalid field configuration. Index: {index}</p>
284
- {/if}
338
+ {/key}
339
+ </div>
285
340
  {/each}
286
341
  {/if}
287
342
  </Accordion.Root>
@@ -27,8 +27,8 @@
27
27
  import type { InterfaceLanguage } from '../../../types/languages.js';
28
28
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
29
29
  import { getLocalizedLabel } from '../../utils/collectionLabel.js';
30
- import { droppable } from '@thisux/sveltednd';
31
- import { draggable } from '@thisux/sveltednd';
30
+ import { droppable, draggable } from '@thisux/sveltednd';
31
+ import { arrayMove } from '../../utils/arrayMove.js';
32
32
  import { flip } from 'svelte/animate';
33
33
  import { fade } from 'svelte/transition';
34
34
 
@@ -195,15 +195,11 @@
195
195
  container: index.toString(),
196
196
  callbacks: {
197
197
  onDrop: (state) => {
198
- const { targetContainer, sourceContainer } = state;
199
- const dragIndex = parseInt(sourceContainer ?? '0');
200
- const dropIndex = parseInt(targetContainer ?? '0');
198
+ const dragIndex = parseInt(state.sourceContainer ?? '');
199
+ const dropIndex = parseInt(state.targetContainer ?? '');
201
200
 
202
- if (dragIndex !== -1 && !isNaN(dropIndex)) {
203
- const arr = getArrayValue();
204
- const [moved] = arr.splice(dragIndex, 1);
205
- arr.splice(dropIndex, 0, moved);
206
- $value = [...arr];
201
+ if (!isNaN(dragIndex) && !isNaN(dropIndex)) {
202
+ $value = [...arrayMove(getArrayValue(), dragIndex, dropIndex)];
207
203
  }
208
204
  }
209
205
  }
@@ -2,12 +2,13 @@
2
2
  import { toggleMode } from 'mode-watcher';
3
3
  import Sun from '@tabler/icons-svelte/icons/sun';
4
4
  import Moon from '@tabler/icons-svelte/icons/moon';
5
- import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
5
+ import { useInterfaceLanguage, locales } from '../../state/interface-language.svelte.js';
6
6
 
7
7
  const interfaceLanguage = useInterfaceLanguage();
8
8
 
9
9
  function toggleLanguage() {
10
- interfaceLanguage.current = interfaceLanguage.current === 'en' ? 'pl' : 'en';
10
+ const idx = locales.indexOf(interfaceLanguage.current);
11
+ interfaceLanguage.current = locales[(idx + 1) % locales.length];
11
12
  }
12
13
  </script>
13
14
 
@@ -23,6 +24,6 @@
23
24
  onclick={toggleLanguage}
24
25
  class="text-foreground/70 hover:text-foreground flex size-8 items-center justify-center rounded-md border border-black/10 text-xs font-semibold transition-colors hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/10"
25
26
  >
26
- {interfaceLanguage.current === 'en' ? 'EN' : 'PL'}
27
+ {interfaceLanguage.current.toUpperCase()}
27
28
  </button>
28
29
  </div>
@@ -28,34 +28,49 @@
28
28
  open = false;
29
29
  goto(url);
30
30
  }
31
+
32
+ async function getData() {
33
+ const [singles, collections, forms] = await Promise.all([
34
+ remotes.getSingles(),
35
+ remotes.getCollections(),
36
+ remotes.getForms()
37
+ ]);
38
+
39
+ return { singles, collections, forms };
40
+ }
31
41
  </script>
32
42
 
33
43
  <svelte:window {onkeydown} />
34
44
 
35
- <button
36
- onclick={() => (open = true)}
37
- class="text-muted-foreground mx-2 flex w-[calc(100%-1rem)] items-center gap-2 rounded-md border px-2.5 py-1.5 text-sm transition-colors hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/10"
38
- >
39
- <SearchIcon class="size-4 shrink-0" />
40
- <span class="flex-1 truncate text-left">{sidebarLang[interfaceLanguage.current].search.placeholder}</span>
41
- <kbd class="bg-muted text-muted-foreground shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium">⌘K</kbd>
42
- </button>
45
+ {#await getData() then { singles, collections, forms }}
46
+ <button
47
+ onclick={() => (open = true)}
48
+ class="text-muted-foreground mx-2 flex w-[calc(100%-1rem)] items-center gap-2 rounded-md border px-2.5 py-1.5 text-sm transition-colors hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/10"
49
+ >
50
+ <SearchIcon class="size-4 shrink-0" />
51
+ <span class="flex-1 truncate text-left"
52
+ >{sidebarLang[interfaceLanguage.current].search.placeholder}</span
53
+ >
54
+ <kbd
55
+ class="bg-muted text-muted-foreground shrink-0 rounded px-1.5 py-0.5 text-[10px] font-medium"
56
+ >⌘K</kbd
57
+ >
58
+ </button>
43
59
 
44
- <Command.Dialog bind:open title={sidebarLang[interfaceLanguage.current].search.placeholder}>
45
- <Command.Input placeholder={sidebarLang[interfaceLanguage.current].search.placeholder} />
46
- <Command.List>
47
- <Command.Empty>{sidebarLang[interfaceLanguage.current].search.noResults}</Command.Empty>
60
+ <Command.Dialog bind:open title={sidebarLang[interfaceLanguage.current].search.placeholder}>
61
+ <Command.Input placeholder={sidebarLang[interfaceLanguage.current].search.placeholder} />
62
+ <Command.List>
63
+ <Command.Empty>{sidebarLang[interfaceLanguage.current].search.noResults}</Command.Empty>
48
64
 
49
- <Command.Group heading={sidebarLang[interfaceLanguage.current].main.platform}>
50
- <Command.Item onSelect={() => navigate('/admin')}>
51
- <DashboardIcon class="mr-2 size-4" />
52
- {sidebarLang[interfaceLanguage.current].main.dashboard}
53
- </Command.Item>
54
- <Command.Item onSelect={() => navigate('/admin/media')}>
55
- <CameraIcon class="mr-2 size-4" />
56
- {sidebarLang[interfaceLanguage.current].main.media}
57
- </Command.Item>
58
- {#await remotes.getSingles() then singles}
65
+ <Command.Group heading={sidebarLang[interfaceLanguage.current].main.platform}>
66
+ <Command.Item onSelect={() => navigate('/admin')}>
67
+ <DashboardIcon class="mr-2 size-4" />
68
+ {sidebarLang[interfaceLanguage.current].main.dashboard}
69
+ </Command.Item>
70
+ <Command.Item onSelect={() => navigate('/admin/media')}>
71
+ <CameraIcon class="mr-2 size-4" />
72
+ {sidebarLang[interfaceLanguage.current].main.media}
73
+ </Command.Item>
59
74
  {#each singles as item (item.slug)}
60
75
  {@const name = getLocalizedLabel(item.label, interfaceLanguage.current) ?? item.slug}
61
76
  <Command.Item onSelect={() => navigate(`/admin/entries/${item.slug}`)}>
@@ -67,14 +82,13 @@
67
82
  {name}
68
83
  </Command.Item>
69
84
  {/each}
70
- {/await}
71
- </Command.Group>
85
+ </Command.Group>
72
86
 
73
- {#await remotes.getCollections() then collections}
74
87
  {#if collections.length > 0}
75
88
  <Command.Group heading={sidebarLang[interfaceLanguage.current].collections.title}>
76
89
  {#each collections as item (item.slug)}
77
- {@const name = getLocalizedLabel(item.labels?.plural, interfaceLanguage.current) ?? item.slug}
90
+ {@const name =
91
+ getLocalizedLabel(item.labels?.plural, interfaceLanguage.current) ?? item.slug}
78
92
  <Command.Item onSelect={() => navigate(`/admin/collections/${item.slug}`)}>
79
93
  {#if item.sidebarIcon}
80
94
  <item.sidebarIcon class="mr-2 size-4" />
@@ -86,9 +100,7 @@
86
100
  {/each}
87
101
  </Command.Group>
88
102
  {/if}
89
- {/await}
90
103
 
91
- {#await remotes.getForms() then forms}
92
104
  {#if forms.length > 0}
93
105
  <Command.Group heading={sidebarLang[interfaceLanguage.current].forms.title}>
94
106
  {#each forms as item (item.slug)}
@@ -100,6 +112,6 @@
100
112
  {/each}
101
113
  </Command.Group>
102
114
  {/if}
103
- {/await}
104
- </Command.List>
105
- </Command.Dialog>
115
+ </Command.List>
116
+ </Command.Dialog>
117
+ {/await}
@@ -65,6 +65,10 @@
65
65
  })
66
66
  );
67
67
 
68
+ const allFilesQuery = $derived(
69
+ remotes.getMediaFiles({ data: { mimeTypes: accept?.split(',') } })
70
+ );
71
+
68
72
  let tagsQuery = $derived(remotes.getMediaTags());
69
73
 
70
74
  // Multi-select with shift/ctrl
@@ -99,6 +103,7 @@
99
103
  await remotes.deleteMediaFile(currentFile.id);
100
104
  toast.success(lang[interfaceLanguage.current].fileDeletedToast);
101
105
  filesQuery.refresh();
106
+ allFilesQuery.refresh();
102
107
  currentFile = null;
103
108
  }
104
109
  }
@@ -109,6 +114,7 @@
109
114
  const tags = (await tagsQuery) as MediaTag[];
110
115
  currentFile = { ...currentFile, tags: tags.filter((t) => tagIds.includes(t.id)) };
111
116
  await filesQuery.refresh();
117
+ await allFilesQuery.refresh();
112
118
  }
113
119
  }
114
120
 
@@ -133,12 +139,14 @@
133
139
  await remotes.updateMediaTag({ id, name, color });
134
140
  await tagsQuery.refresh();
135
141
  await filesQuery.refresh();
142
+ await allFilesQuery.refresh();
136
143
  }
137
144
 
138
145
  async function handleDeleteTag(id: string) {
139
146
  await remotes.deleteMediaTag(id);
140
147
  await tagsQuery.refresh();
141
148
  await filesQuery.refresh();
149
+ await allFilesQuery.refresh();
142
150
  }
143
151
 
144
152
  // Bulk operations
@@ -146,6 +154,7 @@
146
154
  const fileIds = selectedFileIds;
147
155
  await remotes.bulkSetMediaFileTags({ fileIds, tagIds });
148
156
  await filesQuery.refresh();
157
+ await allFilesQuery.refresh();
149
158
  }
150
159
 
151
160
  async function handleBulkDelete() {
@@ -154,6 +163,7 @@
154
163
  selectedFileIds = [];
155
164
  currentFile = null;
156
165
  await filesQuery.refresh();
166
+ await allFilesQuery.refresh();
157
167
  }
158
168
 
159
169
  onMount(() => {
@@ -168,10 +178,10 @@
168
178
  <div class="flex h-full" bind:this={dropZoneRef}>
169
179
  <!-- Tag sidebar -->
170
180
  <div class="w-48 shrink-0 border-r p-3">
171
- {#await tagsQuery then tags}
181
+ {#await Promise.all([tagsQuery, allFilesQuery]) then [tags, allFiles]}
172
182
  <TagSidebar
173
183
  {tags}
174
- files={[]}
184
+ files={allFiles}
175
185
  activeFilter={activeTagFilter}
176
186
  onFilterChange={(f) => (activeTagFilter = f)}
177
187
  onCreateTag={handleCreateTag}
@@ -187,7 +197,7 @@
187
197
  <MediaSearch bind:value={searchQuery} />
188
198
  <MediaSort />
189
199
  <FileUpload
190
- onUpload={() => filesQuery.refresh()}
200
+ onUpload={() => { filesQuery.refresh(); allFilesQuery.refresh(); }}
191
201
  {accept}
192
202
  bind:dropZoneRef
193
203
  />
@@ -234,6 +244,7 @@
234
244
  onReplace={(updated) => {
235
245
  currentFile = updated;
236
246
  filesQuery.refresh();
247
+ allFilesQuery.refresh();
237
248
  }}
238
249
  {onTagUpdate}
239
250
  {onNameUpdate}
@@ -256,15 +267,15 @@
256
267
 
257
268
  <!-- Bulk action bar -->
258
269
  {#if selectedFileIds.length > 0}
259
- {#await tagsQuery then tags}
270
+ {#await Promise.all([tagsQuery, allFilesQuery]) then [tags, allFiles]}
260
271
  <BulkActionBar
261
272
  selectedCount={selectedFileIds.length}
262
- totalCount={0}
273
+ totalCount={allFiles.length}
263
274
  {tags}
264
275
  onBulkTag={handleBulkTag}
265
276
  onBulkDelete={handleBulkDelete}
266
277
  onClear={() => (selectedFileIds = [])}
267
- onSelectAll={() => {}}
278
+ onSelectAll={() => { selectedFileIds = allFiles.map((f) => f.id); }}
268
279
  onCreateTag={handleCreateTag}
269
280
  onUpdateTagColor={async (id, color) => {
270
281
  const tag = tags.find((t) => t.id === id);
@@ -3,7 +3,16 @@ export declare const getRawEntries: import("@sveltejs/kit").RemoteQueryFunction<
3
3
  ids?: string[] | undefined;
4
4
  slug?: string | undefined;
5
5
  onlyArchived?: boolean | undefined;
6
- }, RawEntry[]>;
6
+ limit?: number | undefined;
7
+ offset?: number | undefined;
8
+ orderBy?: {
9
+ column: "createdAt" | "updatedAt";
10
+ direction: "asc" | "desc";
11
+ } | undefined;
12
+ }, {
13
+ entries: RawEntry[];
14
+ total: number;
15
+ }>;
7
16
  export declare const getEntries: import("@sveltejs/kit").RemoteQueryFunction<{
8
17
  ids?: string[] | undefined;
9
18
  dataValues?: Record<string, unknown> | undefined;
@@ -1,6 +1,6 @@
1
1
  import { command, query } from '$app/server';
2
2
  import { createEntry as createEntryOperation, createEntrySchema, createEntryVersion } from '../../core/server/entries/operations/create.js';
3
- import { getRawEntries as getRawEntriesOperation, getRawEntry as getRawEntryOperation, getRawEntryOrThrow, getDbEntry, getDbEntryOrThrow, getEntries as getEntriesOperation, getEntry as getEntryOperation, getEntryVersion as getEntryVersionOperation } from '../../core/server/entries/operations/get.js';
3
+ import { getRawEntries as getRawEntriesOperation, countRawEntries as countRawEntriesOperation, getRawEntry as getRawEntryOperation, getRawEntryOrThrow, getDbEntry, getDbEntryOrThrow, getEntries as getEntriesOperation, getEntry as getEntryOperation, getEntryVersion as getEntryVersionOperation } from '../../core/server/entries/operations/get.js';
4
4
  import { getCMS } from '../../core/cms.js';
5
5
  import { pruneOldDraftVersions, unpublishEntry, updateEntry, updateEntrySchema, updateEntryVersionCommandTypes } from '../../core/server/entries/operations/update.js';
6
6
  import z from 'zod';
@@ -9,10 +9,22 @@ import { entryStatuses } from '../../types/entries.js';
9
9
  export const getRawEntries = query(z.object({
10
10
  ids: z.array(z.string().uuid()).optional(),
11
11
  slug: z.string().optional(),
12
- onlyArchived: z.boolean().optional()
12
+ onlyArchived: z.boolean().optional(),
13
+ limit: z.number().int().positive().optional(),
14
+ offset: z.number().int().nonnegative().optional(),
15
+ orderBy: z
16
+ .object({
17
+ column: z.enum(['createdAt', 'updatedAt']),
18
+ direction: z.enum(['asc', 'desc'])
19
+ })
20
+ .optional()
13
21
  }), async (input) => {
14
- // We return RawEntry[] here, transformation to populated Entry[] is done in the client
15
- return getRawEntriesOperation(input);
22
+ const { limit, offset, orderBy, ...filterOptions } = input;
23
+ const [entries, total] = await Promise.all([
24
+ getRawEntriesOperation(input),
25
+ countRawEntriesOperation(filterOptions)
26
+ ]);
27
+ return { entries, total };
16
28
  });
17
29
  export const getEntries = query(z.object({
18
30
  ids: z.array(z.string().uuid()).optional(),
@@ -1,8 +1,5 @@
1
1
  import type { InterfaceLanguage as InterfaceLanguageType } from '../../types/languages.js';
2
- export declare class InterfaceLanguage {
3
- #private;
4
- constructor();
5
- get current(): InterfaceLanguageType;
6
- set current(value: InterfaceLanguageType);
7
- }
8
- export declare function useInterfaceLanguage(): InterfaceLanguage;
2
+ export declare function useInterfaceLanguage(): {
3
+ current: InterfaceLanguageType;
4
+ };
5
+ export { locales } from '../../paraglide/runtime.js';
@@ -1,25 +1,26 @@
1
1
  import { PersistedState } from 'runed';
2
- import { createContext } from 'svelte';
3
- const [getInterfaceLanguage, setInterfaceLanguage] = createContext();
4
- export class InterfaceLanguage {
5
- #state;
6
- constructor() {
7
- const state = new PersistedState('interface-language', 'pl');
8
- this.#state = state;
2
+ import { locales, baseLocale } from '../../paraglide/runtime.js';
3
+ function detectDefaultLocale() {
4
+ if (typeof navigator === 'undefined')
5
+ return baseLocale;
6
+ const langs = navigator.languages ?? [navigator.language];
7
+ for (const lang of langs) {
8
+ const base = lang.split('-')[0].toLowerCase();
9
+ if (locales.includes(base))
10
+ return base;
9
11
  }
12
+ return baseLocale;
13
+ }
14
+ const state = new PersistedState('interface-language', detectDefaultLocale());
15
+ const interfaceLanguage = {
10
16
  get current() {
11
- return this.#state.current;
12
- }
17
+ return state.current;
18
+ },
13
19
  set current(value) {
14
- this.#state.current = value;
20
+ state.current = value;
15
21
  }
16
- }
22
+ };
17
23
  export function useInterfaceLanguage() {
18
- try {
19
- return getInterfaceLanguage();
20
- }
21
- catch {
22
- setInterfaceLanguage(new InterfaceLanguage());
23
- return getInterfaceLanguage();
24
- }
24
+ return interfaceLanguage;
25
25
  }
26
+ export { locales } from '../../paraglide/runtime.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Move an element from one index to another via splice, returning a new array.
3
+ * Returns the original array reference (unchanged) when `from === to`.
4
+ */
5
+ export declare function arrayMove<T>(arr: T[], from: number, to: number): T[];
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Move an element from one index to another via splice, returning a new array.
3
+ * Returns the original array reference (unchanged) when `from === to`.
4
+ */
5
+ export function arrayMove(arr, from, to) {
6
+ if (from === to)
7
+ return arr;
8
+ const copy = [...arr];
9
+ const [moved] = copy.splice(from, 1);
10
+ copy.splice(to, 0, moved);
11
+ return copy;
12
+ }
@@ -1,3 +1,4 @@
1
1
  import type { InterfaceLanguage } from '../../types/languages.js';
2
+ export declare function toLocaleCode(lang: string): string;
2
3
  export declare function formatRelativeDate(date: Date | string, lang?: InterfaceLanguage): string;
3
4
  export declare function formatAbsoluteDate(date: Date | string, lang?: InterfaceLanguage): string;
@@ -1,3 +1,7 @@
1
+ export function toLocaleCode(lang) {
2
+ const map = { pl: 'pl-PL', en: 'en-US' };
3
+ return map[lang] ?? lang;
4
+ }
1
5
  const units = [
2
6
  { unit: 'year', ms: 365 * 24 * 60 * 60 * 1000 },
3
7
  { unit: 'month', ms: 30 * 24 * 60 * 60 * 1000 },
@@ -84,7 +88,7 @@ export function formatRelativeDate(date, lang = 'en') {
84
88
  }
85
89
  export function formatAbsoluteDate(date, lang = 'en') {
86
90
  const d = typeof date === 'string' ? new Date(date) : date;
87
- return d.toLocaleString(lang === 'pl' ? 'pl-PL' : 'en-US', {
91
+ return d.toLocaleString(toLocaleCode(lang), {
88
92
  year: 'numeric',
89
93
  month: 'short',
90
94
  day: 'numeric',