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.
- package/CHANGELOG.md +71 -0
- package/ROADMAP.md +33 -0
- package/dist/admin/api/rest/handler.d.ts +7 -0
- package/dist/admin/api/rest/handler.js +116 -0
- package/dist/admin/api/rest/middleware/apiKey.d.ts +6 -0
- package/dist/admin/api/rest/middleware/apiKey.js +45 -0
- package/dist/admin/api/rest/routes/collections.d.ts +5 -0
- package/dist/admin/api/rest/routes/collections.js +104 -0
- package/dist/admin/api/rest/routes/entries.d.ts +2 -0
- package/dist/admin/api/rest/routes/entries.js +37 -0
- package/dist/admin/api/rest/routes/languages.d.ts +1 -0
- package/dist/admin/api/rest/routes/languages.js +5 -0
- package/dist/admin/api/rest/routes/schema.d.ts +2 -0
- package/dist/admin/api/rest/routes/schema.js +78 -0
- package/dist/admin/api/rest/routes/singletons.d.ts +3 -0
- package/dist/admin/api/rest/routes/singletons.js +60 -0
- package/dist/admin/auth-client.d.ts +48 -48
- package/dist/admin/client/admin/admin-after-login-layout.svelte +5 -3
- package/dist/admin/client/admin/admin-layout.svelte +0 -2
- package/dist/admin/client/collection/bulk-actions-bar.svelte +3 -1
- package/dist/admin/client/collection/bulk-actions-bar.svelte.d.ts +1 -1
- package/dist/admin/client/collection/collection-entries.svelte +334 -166
- package/dist/admin/client/collection/collection-page.svelte +6 -3
- package/dist/admin/client/collection/data-table.svelte +127 -18
- package/dist/admin/client/collection/data-table.svelte.d.ts +2 -0
- package/dist/admin/client/collection/row-actions.svelte +3 -1
- package/dist/admin/client/collection/row-actions.svelte.d.ts +1 -1
- package/dist/admin/client/entry/entry-page.svelte +5 -4
- package/dist/admin/client/form/form-config-wrapper.svelte +5 -3
- package/dist/admin/client/form/form-page.svelte +5 -5
- package/dist/admin/client/form/form-submission/form-submission-page.svelte +30 -32
- package/dist/admin/components/accessibility/accessibility-overview.svelte +7 -5
- package/dist/admin/components/accessibility/accessibility-overview.svelte.d.ts +2 -17
- package/dist/admin/components/dashboard/a11y-gauge.svelte +7 -4
- package/dist/admin/components/dashboard/a11y-gauge.svelte.d.ts +2 -17
- package/dist/admin/components/dashboard/accessibility-hub.svelte +7 -4
- package/dist/admin/components/dashboard/accessibility-hub.svelte.d.ts +2 -17
- package/dist/admin/components/dashboard/form-submissions-widget.svelte +8 -3
- package/dist/admin/components/dashboard/form-submissions-widget.svelte.d.ts +2 -17
- package/dist/admin/components/dashboard/recent-activity.svelte +8 -3
- package/dist/admin/components/dashboard/recent-activity.svelte.d.ts +2 -17
- package/dist/admin/components/dashboard/recent-entries.svelte +8 -3
- package/dist/admin/components/dashboard/recent-entries.svelte.d.ts +2 -17
- package/dist/admin/components/fields/image-field.svelte +21 -20
- package/dist/admin/components/fields/media-field.svelte +45 -44
- package/dist/admin/components/fields/relation-field.svelte +428 -149
- package/dist/admin/components/fields/relation-picker-dialog.svelte +112 -0
- package/dist/admin/components/fields/relation-picker-dialog.svelte.d.ts +22 -0
- package/dist/admin/components/fields/seo-field.svelte +66 -10
- package/dist/admin/components/icons/icon-map.d.ts +4 -0
- package/dist/admin/components/icons/icon-map.js +143 -0
- package/dist/admin/components/layout/header-actions.svelte +0 -10
- package/dist/admin/components/layout/nav-collections.svelte +55 -57
- package/dist/admin/components/layout/nav-collections.svelte.d.ts +2 -17
- package/dist/admin/components/layout/nav-forms.svelte +49 -49
- package/dist/admin/components/layout/nav-forms.svelte.d.ts +2 -17
- package/dist/admin/components/layout/nav-search.svelte +15 -22
- package/dist/admin/components/layout/nav-singletons.svelte +35 -38
- package/dist/admin/components/layout/nav-singletons.svelte.d.ts +2 -17
- package/dist/admin/components/media/file/file-preview.svelte +92 -91
- package/dist/admin/components/media/file-preview.svelte +17 -18
- package/dist/admin/components/media/media-library.svelte +39 -41
- package/dist/admin/components/media/media-selector.svelte +8 -8
- package/dist/admin/components/tiptap/InlineBlockNodeView.svelte +6 -8
- package/dist/admin/components/tiptap/content-editor.svelte +2 -2
- package/dist/admin/components/tiptap/editor-toolbar.svelte +1 -1
- package/dist/admin/components/tiptap/inline-block-node.d.ts +1 -0
- package/dist/admin/components/tiptap/inline-block-node.js +12 -1
- package/dist/admin/components/tiptap/tiptap-editor.svelte +1 -1
- package/dist/admin/remote/entry.remote.d.ts +20 -1
- package/dist/admin/remote/entry.remote.js +24 -3
- package/dist/admin/remote/invite.d.ts +2 -2
- package/dist/cli/scaffold/admin.js +8 -0
- package/dist/cms/runtime/remote.d.ts +8 -0
- package/dist/cms/runtime/remote.js +7 -0
- package/dist/cms/runtime/types.d.ts +1 -1
- package/dist/components/ui/input/input.svelte.d.ts +1 -1
- package/dist/components/ui/input-group/input-group-input.svelte.d.ts +1 -1
- package/dist/components/ui/sidebar/sidebar-input.svelte.d.ts +1 -1
- package/dist/components/ui/sonner/sonner.svelte +1 -2
- package/dist/core/cms.d.ts +2 -1
- package/dist/core/cms.js +2 -0
- package/dist/core/server/entries/operations/get.d.ts +11 -0
- package/dist/core/server/entries/operations/get.js +63 -1
- package/dist/core/server/entries/operations/update.d.ts +1 -0
- package/dist/core/server/entries/operations/update.js +5 -1
- package/dist/core/server/fields/resolveUrlFields.js +49 -18
- package/dist/db-postgres/schema/entry.d.ts +17 -0
- package/dist/db-postgres/schema/entry.js +4 -2
- package/dist/server/auth.d.ts +4 -4
- package/dist/sveltekit/components/structured-content.svelte +18 -5
- package/dist/sveltekit/server/handle.js +1 -0
- package/dist/types/cms.d.ts +7 -0
- package/dist/types/collections.d.ts +2 -0
- package/dist/types/config.d.ts +2 -2
- package/dist/types/entries.d.ts +7 -1
- package/dist/types/forms.d.ts +2 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.js +1 -0
- package/dist/updates/0.5.4/index.d.ts +2 -0
- package/dist/updates/0.5.4/index.js +15 -0
- package/dist/updates/0.5.5/index.d.ts +2 -0
- package/dist/updates/0.5.5/index.js +20 -0
- package/dist/updates/0.5.6/index.d.ts +2 -0
- package/dist/updates/0.5.6/index.js +17 -0
- package/dist/updates/0.5.7/index.d.ts +2 -0
- package/dist/updates/0.5.7/index.js +14 -0
- package/dist/updates/index.js +5 -1
- 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:
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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,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
|
-
|
|
16
|
+
const pathname = page.url.pathname;
|
|
17
|
+
return pathname === url || pathname.startsWith(url + '/');
|
|
16
18
|
}
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
{#
|
|
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
|
-
{:
|
|
45
|
-
|
|
46
|
-
<Sidebar.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
{
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<Sidebar.
|
|
57
|
-
|
|
58
|
-
{
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
{
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
2
|
-
|
|
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;
|