includio-cms 0.5.3 → 0.5.7

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 (109) hide show
  1. package/CHANGELOG.md +71 -0
  2. package/ROADMAP.md +33 -0
  3. package/dist/admin/api/rest/handler.d.ts +7 -0
  4. package/dist/admin/api/rest/handler.js +116 -0
  5. package/dist/admin/api/rest/middleware/apiKey.d.ts +6 -0
  6. package/dist/admin/api/rest/middleware/apiKey.js +45 -0
  7. package/dist/admin/api/rest/routes/collections.d.ts +5 -0
  8. package/dist/admin/api/rest/routes/collections.js +104 -0
  9. package/dist/admin/api/rest/routes/entries.d.ts +2 -0
  10. package/dist/admin/api/rest/routes/entries.js +37 -0
  11. package/dist/admin/api/rest/routes/languages.d.ts +1 -0
  12. package/dist/admin/api/rest/routes/languages.js +5 -0
  13. package/dist/admin/api/rest/routes/schema.d.ts +2 -0
  14. package/dist/admin/api/rest/routes/schema.js +78 -0
  15. package/dist/admin/api/rest/routes/singletons.d.ts +3 -0
  16. package/dist/admin/api/rest/routes/singletons.js +60 -0
  17. package/dist/admin/auth-client.d.ts +48 -48
  18. package/dist/admin/client/admin/admin-after-login-layout.svelte +5 -3
  19. package/dist/admin/client/admin/admin-layout.svelte +0 -2
  20. package/dist/admin/client/collection/bulk-actions-bar.svelte +3 -1
  21. package/dist/admin/client/collection/bulk-actions-bar.svelte.d.ts +1 -1
  22. package/dist/admin/client/collection/collection-entries.svelte +334 -166
  23. package/dist/admin/client/collection/collection-page.svelte +6 -3
  24. package/dist/admin/client/collection/data-table.svelte +127 -18
  25. package/dist/admin/client/collection/data-table.svelte.d.ts +2 -0
  26. package/dist/admin/client/collection/row-actions.svelte +3 -1
  27. package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -1
  28. package/dist/admin/client/entry/entry-page.svelte +5 -4
  29. package/dist/admin/client/form/form-config-wrapper.svelte +5 -3
  30. package/dist/admin/client/form/form-page.svelte +5 -5
  31. package/dist/admin/client/form/form-submission/form-submission-page.svelte +30 -32
  32. package/dist/admin/components/accessibility/accessibility-overview.svelte +7 -5
  33. package/dist/admin/components/accessibility/accessibility-overview.svelte.d.ts +2 -17
  34. package/dist/admin/components/dashboard/a11y-gauge.svelte +7 -4
  35. package/dist/admin/components/dashboard/a11y-gauge.svelte.d.ts +2 -17
  36. package/dist/admin/components/dashboard/accessibility-hub.svelte +7 -4
  37. package/dist/admin/components/dashboard/accessibility-hub.svelte.d.ts +2 -17
  38. package/dist/admin/components/dashboard/form-submissions-widget.svelte +8 -3
  39. package/dist/admin/components/dashboard/form-submissions-widget.svelte.d.ts +2 -17
  40. package/dist/admin/components/dashboard/recent-activity.svelte +8 -3
  41. package/dist/admin/components/dashboard/recent-activity.svelte.d.ts +2 -17
  42. package/dist/admin/components/dashboard/recent-entries.svelte +8 -3
  43. package/dist/admin/components/dashboard/recent-entries.svelte.d.ts +2 -17
  44. package/dist/admin/components/fields/image-field.svelte +21 -20
  45. package/dist/admin/components/fields/media-field.svelte +45 -44
  46. package/dist/admin/components/fields/relation-field.svelte +428 -149
  47. package/dist/admin/components/fields/relation-picker-dialog.svelte +112 -0
  48. package/dist/admin/components/fields/relation-picker-dialog.svelte.d.ts +22 -0
  49. package/dist/admin/components/fields/seo-field.svelte +66 -10
  50. package/dist/admin/components/icons/icon-map.d.ts +4 -0
  51. package/dist/admin/components/icons/icon-map.js +143 -0
  52. package/dist/admin/components/layout/header-actions.svelte +0 -10
  53. package/dist/admin/components/layout/nav-collections.svelte +55 -57
  54. package/dist/admin/components/layout/nav-collections.svelte.d.ts +2 -17
  55. package/dist/admin/components/layout/nav-forms.svelte +49 -49
  56. package/dist/admin/components/layout/nav-forms.svelte.d.ts +2 -17
  57. package/dist/admin/components/layout/nav-search.svelte +15 -22
  58. package/dist/admin/components/layout/nav-singletons.svelte +35 -38
  59. package/dist/admin/components/layout/nav-singletons.svelte.d.ts +2 -17
  60. package/dist/admin/components/media/file/file-preview.svelte +92 -91
  61. package/dist/admin/components/media/file-preview.svelte +17 -18
  62. package/dist/admin/components/media/media-library.svelte +39 -41
  63. package/dist/admin/components/media/media-selector.svelte +8 -8
  64. package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +6 -8
  65. package/dist/admin/components/tiptap/content-editor.svelte +2 -2
  66. package/dist/admin/components/tiptap/editor-toolbar.svelte +1 -1
  67. package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
  68. package/dist/admin/components/tiptap/inline-block-node.js +12 -1
  69. package/dist/admin/components/tiptap/tiptap-editor.svelte +1 -1
  70. package/dist/admin/remote/entry.remote.d.ts +20 -1
  71. package/dist/admin/remote/entry.remote.js +24 -3
  72. package/dist/admin/remote/invite.d.ts +2 -2
  73. package/dist/cli/scaffold/admin.js +8 -0
  74. package/dist/cms/runtime/remote.d.ts +8 -0
  75. package/dist/cms/runtime/remote.js +7 -0
  76. package/dist/cms/runtime/types.d.ts +1 -1
  77. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  78. package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
  79. package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
  80. package/dist/components/ui/sonner/sonner.svelte +1 -2
  81. package/dist/core/cms.d.ts +2 -1
  82. package/dist/core/cms.js +2 -0
  83. package/dist/core/server/entries/operations/get.d.ts +11 -0
  84. package/dist/core/server/entries/operations/get.js +63 -1
  85. package/dist/core/server/entries/operations/update.d.ts +1 -0
  86. package/dist/core/server/entries/operations/update.js +5 -1
  87. package/dist/core/server/fields/resolveUrlFields.js +49 -18
  88. package/dist/db-postgres/schema/entry.d.ts +17 -0
  89. package/dist/db-postgres/schema/entry.js +4 -2
  90. package/dist/server/auth.d.ts +4 -4
  91. package/dist/sveltekit/components/structured-content.svelte +18 -5
  92. package/dist/sveltekit/server/handle.js +1 -0
  93. package/dist/types/cms.d.ts +7 -0
  94. package/dist/types/collections.d.ts +2 -0
  95. package/dist/types/config.d.ts +2 -2
  96. package/dist/types/entries.d.ts +7 -1
  97. package/dist/types/forms.d.ts +2 -0
  98. package/dist/types/index.d.ts +2 -1
  99. package/dist/types/index.js +1 -0
  100. package/dist/updates/0.5.4/index.d.ts +2 -0
  101. package/dist/updates/0.5.4/index.js +15 -0
  102. package/dist/updates/0.5.5/index.d.ts +2 -0
  103. package/dist/updates/0.5.5/index.js +20 -0
  104. package/dist/updates/0.5.6/index.d.ts +2 -0
  105. package/dist/updates/0.5.6/index.js +17 -0
  106. package/dist/updates/0.5.7/index.d.ts +2 -0
  107. package/dist/updates/0.5.7/index.js +14 -0
  108. package/dist/updates/index.js +5 -1
  109. package/package.json +6 -2
@@ -0,0 +1,112 @@
1
+ <script lang="ts">
2
+ import * as Dialog from '../../../components/ui/dialog/index.js';
3
+ import * as Command from '../../../components/ui/command/index.js';
4
+ import Check from '@tabler/icons-svelte/icons/check';
5
+ import { cn } from '../../../utils.js';
6
+ import Spinner from '../../../components/ui/spinner/spinner.svelte';
7
+ import type { InterfaceLanguage } from '../../../types/languages.js';
8
+
9
+ type LabelEntry = { id: string; label: string };
10
+
11
+ type Props = {
12
+ open: boolean;
13
+ title: string;
14
+ searchPlaceholder: string;
15
+ noResults: string;
16
+ closeLabel: string;
17
+ multiple: boolean;
18
+ selected: string[];
19
+ options: LabelEntry[];
20
+ loading: boolean;
21
+ searchValue: string;
22
+ onSearchChange: (value: string) => void;
23
+ onSelect: (id: string) => void;
24
+ onClose: () => void;
25
+ };
26
+
27
+ let {
28
+ open = $bindable(),
29
+ title,
30
+ searchPlaceholder,
31
+ noResults,
32
+ closeLabel,
33
+ multiple,
34
+ selected,
35
+ options,
36
+ loading,
37
+ searchValue = $bindable(),
38
+ onSearchChange,
39
+ onSelect,
40
+ onClose
41
+ }: Props = $props();
42
+
43
+ function isSelected(id: string): boolean {
44
+ return selected.includes(id);
45
+ }
46
+
47
+ function handleOpenChange(next: boolean) {
48
+ if (!next) {
49
+ onClose();
50
+ }
51
+ open = next;
52
+ }
53
+ </script>
54
+
55
+ <Dialog.Root bind:open onOpenChange={handleOpenChange}>
56
+ <Dialog.Content class="flex max-h-[80vh] flex-col gap-0 p-0 sm:max-w-md">
57
+ <Dialog.Header class="border-b px-4 py-3">
58
+ <Dialog.Title class="text-base font-semibold">{title}</Dialog.Title>
59
+ <Dialog.Description class="sr-only">{title}</Dialog.Description>
60
+ </Dialog.Header>
61
+
62
+ <Command.Root shouldFilter={false} class="flex flex-1 flex-col overflow-hidden">
63
+ <Command.Input
64
+ autofocus
65
+ placeholder={searchPlaceholder}
66
+ value={searchValue}
67
+ onValueChange={(v) => onSearchChange(v)}
68
+ class="h-10"
69
+ />
70
+ <Command.List class="max-h-[50vh] flex-1 overflow-y-auto">
71
+ {#if loading}
72
+ <div class="flex items-center justify-center py-6">
73
+ <Spinner class="size-5" />
74
+ </div>
75
+ {:else if options.length === 0}
76
+ <Command.Empty>{noResults}</Command.Empty>
77
+ {:else}
78
+ <Command.Group>
79
+ {#each options as option (option.id)}
80
+ <Command.Item
81
+ value={option.id}
82
+ onSelect={() => onSelect(option.id)}
83
+ class="flex items-center gap-2"
84
+ aria-selected={isSelected(option.id)}
85
+ >
86
+ <div
87
+ class={cn(
88
+ 'flex h-4 w-4 shrink-0 items-center justify-center rounded border',
89
+ isSelected(option.id)
90
+ ? 'border-primary bg-primary text-primary-foreground'
91
+ : 'border-muted-foreground/30'
92
+ )}
93
+ >
94
+ {#if isSelected(option.id)}
95
+ <Check class="h-3 w-3" />
96
+ {/if}
97
+ </div>
98
+ <span class="flex-1 truncate">{option.label}</span>
99
+ </Command.Item>
100
+ {/each}
101
+ </Command.Group>
102
+ {/if}
103
+ </Command.List>
104
+ </Command.Root>
105
+
106
+ {#if multiple && selected.length > 0}
107
+ <div class="border-t px-4 py-2 text-sm text-muted-foreground">
108
+ <span aria-live="polite">{selected.length} wybranych</span>
109
+ </div>
110
+ {/if}
111
+ </Dialog.Content>
112
+ </Dialog.Root>
@@ -0,0 +1,22 @@
1
+ type LabelEntry = {
2
+ id: string;
3
+ label: string;
4
+ };
5
+ type Props = {
6
+ open: boolean;
7
+ title: string;
8
+ searchPlaceholder: string;
9
+ noResults: string;
10
+ closeLabel: string;
11
+ multiple: boolean;
12
+ selected: string[];
13
+ options: LabelEntry[];
14
+ loading: boolean;
15
+ searchValue: string;
16
+ onSearchChange: (value: string) => void;
17
+ onSelect: (id: string) => void;
18
+ onClose: () => void;
19
+ };
20
+ declare const RelationPickerDialog: import("svelte").Component<Props, {}, "searchValue" | "open">;
21
+ type RelationPickerDialog = ReturnType<typeof RelationPickerDialog>;
22
+ export default RelationPickerDialog;
@@ -15,6 +15,7 @@
15
15
  import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
16
16
  import { getContentLanguage } from '../../state/content-language.svelte.js';
17
17
  import { getLocalizedLabel } from '../../utils/collectionLabel.js';
18
+ import { Switch } from '../../../components/ui/switch/index.js';
18
19
 
19
20
  const interfaceLanguage = useInterfaceLanguage();
20
21
  const contentLanguage = getContentLanguage();
@@ -86,7 +87,7 @@
86
87
  slug: 'slug',
87
88
  label: labels.slug.label,
88
89
  description: labels.slug.description,
89
- required: true
90
+ required: false
90
91
  };
91
92
 
92
93
  const canonicalUrlField: TextField = {
@@ -141,7 +142,6 @@
141
142
  };
142
143
 
143
144
  const fields: Field[] = [
144
- slugField,
145
145
  canonicalUrlField,
146
146
  titleField,
147
147
  descriptionField,
@@ -187,9 +187,30 @@
187
187
  let lastAutoSlugs: Record<string, string> = {};
188
188
  let lastAutoTitles: Record<string, string> = {};
189
189
 
190
+ // Auto slug toggle: ON by default when slugSource is set
191
+ // Initializes to false if existing slug differs from what auto-gen would produce
192
+ let autoSlug = $state((() => {
193
+ if (!field.slugSource) return false;
194
+ const sourceRaw = ($formData as Record<string, unknown>)[field.slugSource];
195
+ if (!sourceRaw) return true;
196
+ const pairs: [string, string][] =
197
+ typeof sourceRaw === 'string'
198
+ ? [[contentLanguage.current, sourceRaw]]
199
+ : typeof sourceRaw === 'object'
200
+ ? Object.entries(sourceRaw as Record<string, string>).filter(([, v]) => typeof v === 'string' && v)
201
+ : [];
202
+ for (const [lang, text] of pairs) {
203
+ const targetPath = joinPath(String(path), 'slug', lang);
204
+ const current = getAtPath($formData as Record<string, unknown>, targetPath) as string | undefined;
205
+ const expected = slugify(String(text), { lower: true, strict: true, trim: true });
206
+ if (current != null && current !== expected) return false;
207
+ }
208
+ return true;
209
+ })());
210
+
190
211
  // slugSource → auto-gen seo.slug per language
191
212
  $effect(() => {
192
- if (!field.slugSource) return;
213
+ if (!field.slugSource || !autoSlug) return;
193
214
  const sourceRaw = ($formData as Record<string, unknown>)[field.slugSource];
194
215
  if (!sourceRaw) return;
195
216
 
@@ -205,19 +226,39 @@
205
226
  for (const [lang, text] of pairs) {
206
227
  const targetPath = joinPath(String(path), 'slug', lang);
207
228
  const current = getAtPath($formData as Record<string, unknown>, targetPath) as string | undefined;
208
- if (!current || current === lastAutoSlugs[lang]) {
209
- const newSlug = slugify(String(text), { lower: true, strict: true, trim: true });
210
- if (newSlug !== current) {
211
- setAtPath($formData as Record<string, unknown>, targetPath, newSlug);
212
- changed = true;
213
- }
214
- lastAutoSlugs[lang] = newSlug;
229
+ const newSlug = slugify(String(text), { lower: true, strict: true, trim: true });
230
+ if (newSlug !== current) {
231
+ setAtPath($formData as Record<string, unknown>, targetPath, newSlug);
232
+ changed = true;
215
233
  }
234
+ lastAutoSlugs[lang] = newSlug;
216
235
  }
217
236
  if (changed) $formData = $formData;
218
237
  });
219
238
  });
220
239
 
240
+ function onAutoSlugToggle(checked: boolean) {
241
+ if (!checked || !field.slugSource) return;
242
+ // Regenerate slug from current source when toggling ON
243
+ const sourceRaw = ($formData as Record<string, unknown>)[field.slugSource];
244
+ if (!sourceRaw) return;
245
+ const pairs: [string, string][] =
246
+ typeof sourceRaw === 'string'
247
+ ? [[contentLanguage.current, sourceRaw]]
248
+ : typeof sourceRaw === 'object'
249
+ ? Object.entries(sourceRaw as Record<string, string>).filter(([, v]) => typeof v === 'string' && v)
250
+ : [];
251
+ let changed = false;
252
+ for (const [lang, text] of pairs) {
253
+ const targetPath = joinPath(String(path), 'slug', lang);
254
+ const newSlug = slugify(String(text), { lower: true, strict: true, trim: true });
255
+ setAtPath($formData as Record<string, unknown>, targetPath, newSlug);
256
+ lastAutoSlugs[lang] = newSlug;
257
+ changed = true;
258
+ }
259
+ if (changed) $formData = $formData;
260
+ }
261
+
221
262
  // titleSource → auto-fill seo.title per language
222
263
  $effect(() => {
223
264
  if (!field.titleSource) return;
@@ -250,6 +291,21 @@
250
291
  </script>
251
292
 
252
293
  <div class="space-y-4">
294
+ <!-- Slug field with auto/manual toggle -->
295
+ <div>
296
+ {#if field.slugSource}
297
+ <div class="mb-1.5 flex items-center gap-2">
298
+ <span class="text-sm font-medium text-muted-foreground">Auto</span>
299
+ <Switch bind:checked={autoSlug} onCheckedChange={onAutoSlugToggle} />
300
+ </div>
301
+ {/if}
302
+ <FieldRenderer
303
+ field={slugField}
304
+ {form}
305
+ path={joinPath(path, 'slug')}
306
+ readonly={autoSlug}
307
+ />
308
+ </div>
253
309
  {#each fields as f}
254
310
  <div>
255
311
  <FieldRenderer
@@ -0,0 +1,4 @@
1
+ import type { Icon } from '@tabler/icons-svelte';
2
+ export declare const iconMap: Record<string, Icon>;
3
+ export type IconName = keyof typeof iconMap;
4
+ export declare function getIcon(name: IconName | string | undefined): Icon | undefined;
@@ -0,0 +1,143 @@
1
+ // Documents & Content
2
+ import IconFile from '@tabler/icons-svelte/icons/file';
3
+ import IconFileText from '@tabler/icons-svelte/icons/file-text';
4
+ import IconFiles from '@tabler/icons-svelte/icons/files';
5
+ import IconArticle from '@tabler/icons-svelte/icons/article';
6
+ import IconNews from '@tabler/icons-svelte/icons/news';
7
+ import IconBook from '@tabler/icons-svelte/icons/book';
8
+ import IconNotebook from '@tabler/icons-svelte/icons/notebook';
9
+ import IconBlockquote from '@tabler/icons-svelte/icons/blockquote';
10
+ // Navigation & Layout
11
+ import IconHome from '@tabler/icons-svelte/icons/home';
12
+ import IconMenu from '@tabler/icons-svelte/icons/menu-2';
13
+ import IconLayout from '@tabler/icons-svelte/icons/layout';
14
+ import IconSitemap from '@tabler/icons-svelte/icons/sitemap';
15
+ import IconLink from '@tabler/icons-svelte/icons/link';
16
+ // People & Users
17
+ import IconUser from '@tabler/icons-svelte/icons/user';
18
+ import IconUsers from '@tabler/icons-svelte/icons/users';
19
+ import IconUserCircle from '@tabler/icons-svelte/icons/user-circle';
20
+ // E-commerce
21
+ import IconShoppingCart from '@tabler/icons-svelte/icons/shopping-cart';
22
+ import IconShoppingBag from '@tabler/icons-svelte/icons/shopping-bag';
23
+ import IconTag from '@tabler/icons-svelte/icons/tag';
24
+ import IconTags from '@tabler/icons-svelte/icons/tags';
25
+ import IconReceipt from '@tabler/icons-svelte/icons/receipt';
26
+ import IconCreditCard from '@tabler/icons-svelte/icons/credit-card';
27
+ // Media
28
+ import IconPhoto from '@tabler/icons-svelte/icons/photo';
29
+ import IconCamera from '@tabler/icons-svelte/icons/camera';
30
+ import IconVideo from '@tabler/icons-svelte/icons/video';
31
+ import IconMusic from '@tabler/icons-svelte/icons/music';
32
+ import IconMicrophone from '@tabler/icons-svelte/icons/microphone';
33
+ // Communication
34
+ import IconMail from '@tabler/icons-svelte/icons/mail';
35
+ import IconMessage from '@tabler/icons-svelte/icons/message';
36
+ import IconMessages from '@tabler/icons-svelte/icons/messages';
37
+ import IconSend from '@tabler/icons-svelte/icons/send';
38
+ import IconPhone from '@tabler/icons-svelte/icons/phone';
39
+ // Organization
40
+ import IconFolder from '@tabler/icons-svelte/icons/folder';
41
+ import IconCategory from '@tabler/icons-svelte/icons/category';
42
+ import IconBookmark from '@tabler/icons-svelte/icons/bookmark';
43
+ import IconArchive from '@tabler/icons-svelte/icons/archive';
44
+ import IconClipboardList from '@tabler/icons-svelte/icons/clipboard-list';
45
+ // Settings & System
46
+ import IconSettings from '@tabler/icons-svelte/icons/settings';
47
+ import IconAdjustments from '@tabler/icons-svelte/icons/adjustments';
48
+ import IconTool from '@tabler/icons-svelte/icons/tool';
49
+ import IconDatabase from '@tabler/icons-svelte/icons/database';
50
+ // Misc
51
+ import IconCalendar from '@tabler/icons-svelte/icons/calendar';
52
+ import IconClock from '@tabler/icons-svelte/icons/clock';
53
+ import IconMap from '@tabler/icons-svelte/icons/map';
54
+ import IconMapPin from '@tabler/icons-svelte/icons/map-pin';
55
+ import IconGlobe from '@tabler/icons-svelte/icons/world';
56
+ import IconStar from '@tabler/icons-svelte/icons/star';
57
+ import IconHeart from '@tabler/icons-svelte/icons/heart';
58
+ import IconBulb from '@tabler/icons-svelte/icons/bulb';
59
+ import IconPalette from '@tabler/icons-svelte/icons/palette';
60
+ import IconCode from '@tabler/icons-svelte/icons/code';
61
+ import IconBriefcase from '@tabler/icons-svelte/icons/briefcase';
62
+ import IconBuilding from '@tabler/icons-svelte/icons/building';
63
+ import IconSchool from '@tabler/icons-svelte/icons/school';
64
+ import IconTrophy from '@tabler/icons-svelte/icons/trophy';
65
+ import IconChartBar from '@tabler/icons-svelte/icons/chart-bar';
66
+ import IconLock from '@tabler/icons-svelte/icons/lock';
67
+ import IconShield from '@tabler/icons-svelte/icons/shield';
68
+ import IconHelp from '@tabler/icons-svelte/icons/help';
69
+ export const iconMap = {
70
+ // Documents & Content
71
+ file: IconFile,
72
+ 'file-text': IconFileText,
73
+ files: IconFiles,
74
+ article: IconArticle,
75
+ news: IconNews,
76
+ book: IconBook,
77
+ notebook: IconNotebook,
78
+ blockquote: IconBlockquote,
79
+ // Navigation & Layout
80
+ home: IconHome,
81
+ menu: IconMenu,
82
+ layout: IconLayout,
83
+ sitemap: IconSitemap,
84
+ link: IconLink,
85
+ // People & Users
86
+ user: IconUser,
87
+ users: IconUsers,
88
+ 'user-circle': IconUserCircle,
89
+ // E-commerce
90
+ 'shopping-cart': IconShoppingCart,
91
+ 'shopping-bag': IconShoppingBag,
92
+ tag: IconTag,
93
+ tags: IconTags,
94
+ receipt: IconReceipt,
95
+ 'credit-card': IconCreditCard,
96
+ // Media
97
+ photo: IconPhoto,
98
+ camera: IconCamera,
99
+ video: IconVideo,
100
+ music: IconMusic,
101
+ microphone: IconMicrophone,
102
+ // Communication
103
+ mail: IconMail,
104
+ message: IconMessage,
105
+ messages: IconMessages,
106
+ send: IconSend,
107
+ phone: IconPhone,
108
+ // Organization
109
+ folder: IconFolder,
110
+ category: IconCategory,
111
+ bookmark: IconBookmark,
112
+ archive: IconArchive,
113
+ 'clipboard-list': IconClipboardList,
114
+ // Settings & System
115
+ settings: IconSettings,
116
+ adjustments: IconAdjustments,
117
+ tool: IconTool,
118
+ database: IconDatabase,
119
+ // Misc
120
+ calendar: IconCalendar,
121
+ clock: IconClock,
122
+ map: IconMap,
123
+ 'map-pin': IconMapPin,
124
+ globe: IconGlobe,
125
+ star: IconStar,
126
+ heart: IconHeart,
127
+ bulb: IconBulb,
128
+ palette: IconPalette,
129
+ code: IconCode,
130
+ briefcase: IconBriefcase,
131
+ building: IconBuilding,
132
+ school: IconSchool,
133
+ trophy: IconTrophy,
134
+ 'chart-bar': IconChartBar,
135
+ lock: IconLock,
136
+ shield: IconShield,
137
+ help: IconHelp
138
+ };
139
+ export function getIcon(name) {
140
+ if (!name)
141
+ return undefined;
142
+ return iconMap[name];
143
+ }
@@ -1,7 +1,4 @@
1
1
  <script lang="ts">
2
- import { toggleMode } from 'mode-watcher';
3
- import Sun from '@tabler/icons-svelte/icons/sun';
4
- import Moon from '@tabler/icons-svelte/icons/moon';
5
2
  import { useInterfaceLanguage, locales } from '../../state/interface-language.svelte.js';
6
3
 
7
4
  const interfaceLanguage = useInterfaceLanguage();
@@ -13,13 +10,6 @@
13
10
  </script>
14
11
 
15
12
  <div class="flex items-center gap-1">
16
- <button
17
- onclick={toggleMode}
18
- class="text-foreground/70 hover:text-foreground flex size-8 items-center justify-center rounded-md border border-black/10 transition-colors hover:bg-black/5 dark:border-white/10 dark:hover:bg-white/10"
19
- >
20
- <Sun class="size-4 dark:hidden" />
21
- <Moon class="hidden size-4 dark:block" />
22
- </button>
23
13
  <button
24
14
  onclick={toggleLanguage}
25
15
  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"
@@ -7,29 +7,35 @@
7
7
  import Skeleton from '../../../components/ui/skeleton/skeleton.svelte';
8
8
  import { page } from '$app/state';
9
9
  import FolderIcon from '@tabler/icons-svelte/icons/folder';
10
+ import { getIcon } from '../icons/icon-map.js';
10
11
 
11
12
  const interfaceLanguage = useInterfaceLanguage();
12
13
  const remotes = getRemotes();
13
14
 
14
15
  function isActive(url: string) {
15
- return page.url.pathname.startsWith(url);
16
+ const pathname = page.url.pathname;
17
+ return pathname === url || pathname.startsWith(url + '/');
16
18
  }
17
19
 
18
- async function loadCollections() {
19
- const collections = await remotes.getCollections();
20
- const counts = await Promise.all(
21
- collections.map((c: { slug: string }) =>
22
- remotes
23
- .getRawEntries({ slug: c.slug, limit: 1, offset: 0 })
24
- .then((r: { total: number }) => r.total)
25
- .catch(() => 0)
26
- )
27
- );
28
- return collections.map((c: any, i: number) => ({ ...c, entryCount: counts[i] }));
29
- }
20
+ const collectionsQuery = $derived(remotes.getCollections());
21
+
22
+ let countsMap: Map<string, number> = $state(new Map());
23
+
24
+ $effect(() => {
25
+ const collections = collectionsQuery.current;
26
+ if (!collections) return;
27
+ const map = new Map<string, number>();
28
+ for (const col of collections) {
29
+ const q = remotes.getRawEntries({ slug: col.slug, limit: 1, offset: 0 });
30
+ if (q.ready && q.current) {
31
+ map.set(col.slug, q.current.total);
32
+ }
33
+ }
34
+ countsMap = map;
35
+ });
30
36
  </script>
31
37
 
32
- {#await loadCollections()}
38
+ {#if !collectionsQuery.ready}
33
39
  <Sidebar.Group class="border-sidebar-border mt-1 border-t px-0 pt-2 pb-1">
34
40
  <Sidebar.GroupLabel
35
41
  >{sidebarLang[interfaceLanguage.current].collections.title}</Sidebar.GroupLabel
@@ -41,47 +47,39 @@
41
47
  </div>
42
48
  </Sidebar.Menu>
43
49
  </Sidebar.Group>
44
- {:then items}
45
- {#if items.length > 0}
46
- <Sidebar.Group class="border-sidebar-border mt-1 border-t px-0 pt-2 pb-1">
47
- <Sidebar.GroupLabel class="group-data-[collapsible=icon]:hidden"
48
- >{sidebarLang[interfaceLanguage.current].collections.title}</Sidebar.GroupLabel
49
- >
50
- <Sidebar.Menu>
51
- {#each items as item (item.slug)}
52
- {@const name =
53
- getLocalizedLabel(item.labels?.plural, interfaceLanguage.current) ?? item.slug}
54
- {@const url = `/admin/collections/${item.slug}`}
55
- {@const active = isActive(url)}
56
- <Sidebar.MenuItem>
57
- <Sidebar.MenuButton isActive={active} tooltipContent={name}>
58
- {#snippet child({ props })}
59
- <a {...props} href={url}>
60
- {#if item.sidebarIcon}
61
- <item.sidebarIcon
62
- class="size-5! shrink-0 {active ? 'text-primary' : 'text-muted-foreground'}"
63
- />
64
- {:else}
65
- <FolderIcon
66
- class="size-5! shrink-0 {active ? 'text-primary' : 'text-muted-foreground'}"
67
- />
68
- {/if}
69
- <span class="flex-1 truncate {active ? 'text-primary' : 'text-muted-foreground'}"
70
- >{name}</span
50
+ {:else if collectionsQuery.current && collectionsQuery.current.length > 0}
51
+ <Sidebar.Group class="border-sidebar-border mt-1 border-t px-0 pt-2 pb-1">
52
+ <Sidebar.GroupLabel class="group-data-[collapsible=icon]:hidden"
53
+ >{sidebarLang[interfaceLanguage.current].collections.title}</Sidebar.GroupLabel
54
+ >
55
+ <Sidebar.Menu>
56
+ {#each collectionsQuery.current as item (item.slug)}
57
+ {@const name =
58
+ getLocalizedLabel(item.labels?.plural, interfaceLanguage.current) ?? item.slug}
59
+ {@const url = `/admin/collections/${item.slug}`}
60
+ {@const active = isActive(url)}
61
+ <Sidebar.MenuItem>
62
+ <Sidebar.MenuButton isActive={active} tooltipContent={name}>
63
+ {#snippet child({ props })}
64
+ {@const ResolvedIcon = getIcon(item.sidebarIcon) ?? FolderIcon}
65
+ <a {...props} href={url}>
66
+ <ResolvedIcon
67
+ class="size-5! shrink-0 {active ? 'text-primary' : 'text-muted-foreground'}"
68
+ />
69
+ <span class="flex-1 truncate {active ? 'text-primary' : 'text-muted-foreground'}"
70
+ >{name}</span
71
+ >
72
+ {#if countsMap.get(item.slug)}
73
+ <span
74
+ class="bg-lavender-lighter text-primary inline-flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full px-[5px] text-[10px] font-bold group-data-[collapsible=icon]:hidden"
75
+ >{countsMap.get(item.slug)}</span
71
76
  >
72
- {#if item.entryCount > 0}
73
- <span
74
- class="bg-lavender-lighter text-primary inline-flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full px-[5px] text-[10px] font-bold"
75
- >
76
- {item.entryCount}
77
- </span>
78
- {/if}
79
- </a>
80
- {/snippet}
81
- </Sidebar.MenuButton>
82
- </Sidebar.MenuItem>
83
- {/each}
84
- </Sidebar.Menu>
85
- </Sidebar.Group>
86
- {/if}
87
- {/await}
77
+ {/if}
78
+ </a>
79
+ {/snippet}
80
+ </Sidebar.MenuButton>
81
+ </Sidebar.MenuItem>
82
+ {/each}
83
+ </Sidebar.Menu>
84
+ </Sidebar.Group>
85
+ {/if}
@@ -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 NavCollections: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
- [evt: string]: CustomEvent<any>;
16
- }, {}, {}, string>;
17
- type NavCollections = InstanceType<typeof NavCollections>;
1
+ declare const NavCollections: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type NavCollections = ReturnType<typeof NavCollections>;
18
3
  export default NavCollections;