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
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { RelationFieldData, RelationField } from '../../../types/fields.js';
|
|
3
|
-
import type { CollectionConfig } from '../../../types/collections.js';
|
|
4
3
|
import { getRemotes } from '../../../sveltekit/index.js';
|
|
5
4
|
import { buttonVariants } from '../../../components/ui/button/button.svelte';
|
|
6
|
-
import { getCollectionEntryLabel } from '../../utils/entryLabel.js';
|
|
7
5
|
import { onMount, tick, untrack } from 'svelte';
|
|
8
6
|
import { useId } from 'bits-ui';
|
|
9
7
|
import * as Popover from '../../../components/ui/popover/index.js';
|
|
10
8
|
import * as Command from '../../../components/ui/command/index.js';
|
|
11
9
|
import { cn } from '../../../utils.js';
|
|
12
|
-
import Selector from '@tabler/icons-svelte/icons/selector';
|
|
13
10
|
import Check from '@tabler/icons-svelte/icons/check';
|
|
14
11
|
import X from '@tabler/icons-svelte/icons/x';
|
|
12
|
+
import Plus from '@tabler/icons-svelte/icons/plus';
|
|
15
13
|
import GripVertical from '@tabler/icons-svelte/icons/grip-vertical';
|
|
16
|
-
import
|
|
14
|
+
import ChevronUp from '@tabler/icons-svelte/icons/chevron-up';
|
|
15
|
+
import ChevronDown from '@tabler/icons-svelte/icons/chevron-down';
|
|
16
|
+
import Spinner from '../../../components/ui/spinner/spinner.svelte';
|
|
17
17
|
import type { InterfaceLanguage } from '../../../types/languages.js';
|
|
18
18
|
import { useInterfaceLanguage } from '../../state/interface-language.svelte.js';
|
|
19
19
|
import { getLocalizedLabel } from '../../utils/collectionLabel.js';
|
|
@@ -21,14 +21,62 @@
|
|
|
21
21
|
import { arrayMove } from '../../utils/arrayMove.js';
|
|
22
22
|
import { flip } from 'svelte/animate';
|
|
23
23
|
import { fade } from 'svelte/transition';
|
|
24
|
+
import RelationPickerDialog from './relation-picker-dialog.svelte';
|
|
24
25
|
|
|
25
|
-
const lang: Record<
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
const lang: Record<
|
|
27
|
+
InterfaceLanguage,
|
|
28
|
+
{
|
|
29
|
+
select: string;
|
|
30
|
+
search: string;
|
|
31
|
+
selected: string;
|
|
32
|
+
add: string;
|
|
33
|
+
noResults: string;
|
|
34
|
+
clear: string;
|
|
35
|
+
remove: string;
|
|
36
|
+
reorder: string;
|
|
37
|
+
moveUp: string;
|
|
38
|
+
moveDown: string;
|
|
39
|
+
movedTo: string;
|
|
40
|
+
loading: string;
|
|
41
|
+
dialogTitle: string;
|
|
42
|
+
closeDialog: string;
|
|
43
|
+
}
|
|
44
|
+
> = {
|
|
45
|
+
en: {
|
|
46
|
+
select: 'Select',
|
|
47
|
+
search: 'Search',
|
|
48
|
+
selected: 'selected',
|
|
49
|
+
add: 'Add',
|
|
50
|
+
noResults: 'No results found',
|
|
51
|
+
clear: 'Clear selection',
|
|
52
|
+
remove: 'Remove',
|
|
53
|
+
reorder: 'Reorder',
|
|
54
|
+
moveUp: 'Move up',
|
|
55
|
+
moveDown: 'Move down',
|
|
56
|
+
movedTo: 'Moved to position',
|
|
57
|
+
loading: 'Loading...',
|
|
58
|
+
dialogTitle: 'Select',
|
|
59
|
+
closeDialog: 'Close'
|
|
60
|
+
},
|
|
61
|
+
pl: {
|
|
62
|
+
select: 'Wybierz',
|
|
63
|
+
search: 'Szukaj',
|
|
64
|
+
selected: 'wybranych',
|
|
65
|
+
add: 'Dodaj',
|
|
66
|
+
noResults: 'Brak wyników',
|
|
67
|
+
clear: 'Wyczyść wybór',
|
|
68
|
+
remove: 'Usuń',
|
|
69
|
+
reorder: 'Zmień kolejność',
|
|
70
|
+
moveUp: 'Przenieś wyżej',
|
|
71
|
+
moveDown: 'Przenieś niżej',
|
|
72
|
+
movedTo: 'Przeniesiono na pozycję',
|
|
73
|
+
loading: 'Ładowanie...',
|
|
74
|
+
dialogTitle: 'Wybierz',
|
|
75
|
+
closeDialog: 'Zamknij'
|
|
76
|
+
}
|
|
28
77
|
};
|
|
29
78
|
|
|
30
79
|
const remotes = getRemotes();
|
|
31
|
-
const contentLanguage = getContentLanguage();
|
|
32
80
|
const interfaceLanguage = useInterfaceLanguage();
|
|
33
81
|
|
|
34
82
|
type Props = {
|
|
@@ -39,6 +87,7 @@
|
|
|
39
87
|
let { field, value = $bindable(), ...props }: Props = $props();
|
|
40
88
|
|
|
41
89
|
const multiple = $derived(field.multiple === true);
|
|
90
|
+
const t = $derived(lang[interfaceLanguage.current]);
|
|
42
91
|
|
|
43
92
|
// Ensure value is array when multiple
|
|
44
93
|
onMount(() => {
|
|
@@ -47,7 +96,7 @@
|
|
|
47
96
|
}
|
|
48
97
|
});
|
|
49
98
|
|
|
50
|
-
//
|
|
99
|
+
// --- Value helpers ---
|
|
51
100
|
function getArrayValue(): string[] {
|
|
52
101
|
if (Array.isArray(value)) return value;
|
|
53
102
|
return value ? [value] : [];
|
|
@@ -58,11 +107,16 @@
|
|
|
58
107
|
}
|
|
59
108
|
|
|
60
109
|
function toggleItem(id: string) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
110
|
+
if (multiple) {
|
|
111
|
+
const arr = getArrayValue();
|
|
112
|
+
if (arr.includes(id)) {
|
|
113
|
+
value = arr.filter((v) => v !== id);
|
|
114
|
+
} else {
|
|
115
|
+
value = [...arr, id];
|
|
116
|
+
}
|
|
64
117
|
} else {
|
|
65
|
-
value =
|
|
118
|
+
value = id;
|
|
119
|
+
pickerOpen = false;
|
|
66
120
|
}
|
|
67
121
|
}
|
|
68
122
|
|
|
@@ -70,195 +124,420 @@
|
|
|
70
124
|
value = getArrayValue().filter((v) => v !== id);
|
|
71
125
|
}
|
|
72
126
|
|
|
73
|
-
|
|
74
|
-
|
|
127
|
+
function clearValue() {
|
|
128
|
+
value = multiple ? [] : '';
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// --- Labels cache (for displaying selected items) ---
|
|
132
|
+
type LabelEntry = { id: string; label: string };
|
|
133
|
+
let labelCache = $state<Map<string, string>>(new Map());
|
|
75
134
|
|
|
76
|
-
|
|
77
|
-
|
|
135
|
+
// Resolve labels for selected IDs not yet in cache
|
|
136
|
+
async function resolveLabels(ids: string[]) {
|
|
137
|
+
const missing = ids.filter((id) => !labelCache.has(id));
|
|
138
|
+
if (missing.length === 0) return;
|
|
78
139
|
|
|
140
|
+
const results = await remotes.getEntryLabels({
|
|
141
|
+
slug: field.collection,
|
|
142
|
+
ids: missing
|
|
143
|
+
});
|
|
144
|
+
const newCache = new Map(labelCache);
|
|
145
|
+
for (const r of results) {
|
|
146
|
+
newCache.set(r.id, r.label);
|
|
147
|
+
}
|
|
148
|
+
labelCache = newCache;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Resolve labels whenever value changes
|
|
79
152
|
$effect(() => {
|
|
80
|
-
const
|
|
81
|
-
|
|
153
|
+
const ids = getArrayValue();
|
|
154
|
+
if (ids.length > 0) {
|
|
155
|
+
untrack(() => resolveLabels(ids));
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
function getLabel(id: string): string {
|
|
160
|
+
return labelCache.get(id) ?? id;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// --- Picker state ---
|
|
164
|
+
let pickerOpen = $state(false);
|
|
165
|
+
let searchValue = $state('');
|
|
166
|
+
let pickerOptions = $state<LabelEntry[]>([]);
|
|
167
|
+
let pickerLoading = $state(false);
|
|
168
|
+
let totalCount = $state(0);
|
|
169
|
+
let initialLoading = $state(true);
|
|
170
|
+
|
|
171
|
+
// Auto-detect mode: <=30 published → inline popover, >30 → dialog
|
|
172
|
+
let useDialog = $state(false);
|
|
173
|
+
|
|
174
|
+
// Debounced search
|
|
175
|
+
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
176
|
+
|
|
177
|
+
function debouncedSearch(query: string) {
|
|
178
|
+
if (searchTimeout) clearTimeout(searchTimeout);
|
|
179
|
+
searchTimeout = setTimeout(() => fetchOptions(query), 300);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function fetchOptions(search?: string) {
|
|
183
|
+
pickerLoading = true;
|
|
184
|
+
try {
|
|
185
|
+
const results = await remotes.getEntryLabels({
|
|
186
|
+
slug: field.collection,
|
|
187
|
+
search: search || undefined,
|
|
188
|
+
status: 'published',
|
|
189
|
+
limit: 100
|
|
190
|
+
});
|
|
191
|
+
pickerOptions = results.map((r) => ({ id: r.id, label: r.label }));
|
|
192
|
+
if (results.length > 0) {
|
|
193
|
+
totalCount = results[0].total;
|
|
194
|
+
} else if (!search) {
|
|
195
|
+
totalCount = 0;
|
|
196
|
+
}
|
|
197
|
+
} finally {
|
|
198
|
+
pickerLoading = false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
82
201
|
|
|
202
|
+
// Initial load to determine mode + count
|
|
203
|
+
onMount(async () => {
|
|
204
|
+
try {
|
|
205
|
+
const results = await remotes.getEntryLabels({
|
|
206
|
+
slug: field.collection,
|
|
207
|
+
status: 'published',
|
|
208
|
+
limit: 100
|
|
209
|
+
});
|
|
210
|
+
pickerOptions = results.map((r) => ({ id: r.id, label: r.label }));
|
|
211
|
+
totalCount = results.length > 0 ? results[0].total : 0;
|
|
212
|
+
useDialog = totalCount > 30;
|
|
213
|
+
|
|
214
|
+
// Populate label cache from initial load
|
|
215
|
+
const newCache = new Map(labelCache);
|
|
216
|
+
for (const r of results) {
|
|
217
|
+
newCache.set(r.id, r.label);
|
|
218
|
+
}
|
|
219
|
+
labelCache = newCache;
|
|
220
|
+
} finally {
|
|
221
|
+
initialLoading = false;
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
function handleSearchChange(val: string) {
|
|
226
|
+
searchValue = val;
|
|
227
|
+
debouncedSearch(val);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function openPicker() {
|
|
231
|
+
searchValue = '';
|
|
232
|
+
fetchOptions();
|
|
233
|
+
pickerOpen = true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function closePicker() {
|
|
237
|
+
pickerOpen = false;
|
|
238
|
+
searchValue = '';
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// --- Collection config ---
|
|
242
|
+
let collectionLabel = $state('');
|
|
243
|
+
let singularLabel = $state('');
|
|
244
|
+
|
|
245
|
+
$effect(() => {
|
|
246
|
+
const slug = field.collection;
|
|
83
247
|
untrack(() => {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const options = entries.map((entry) => ({
|
|
90
|
-
label: getCollectionEntryLabel(entry, collectionConfig),
|
|
91
|
-
value: entry.id
|
|
92
|
-
}));
|
|
93
|
-
relationData = { collectionConfig, options };
|
|
94
|
-
loading = false;
|
|
248
|
+
remotes.getCollection(slug).then((config) => {
|
|
249
|
+
collectionLabel =
|
|
250
|
+
getLocalizedLabel(config.labels?.plural, interfaceLanguage.current) || config.slug;
|
|
251
|
+
singularLabel =
|
|
252
|
+
getLocalizedLabel(config.labels?.singular, interfaceLanguage.current) || config.slug;
|
|
95
253
|
});
|
|
96
254
|
});
|
|
97
255
|
});
|
|
98
256
|
|
|
99
|
-
|
|
257
|
+
// Popover state (inline mode)
|
|
258
|
+
let popoverOpen = $state(false);
|
|
259
|
+
const triggerId = useId();
|
|
100
260
|
|
|
101
|
-
function
|
|
102
|
-
|
|
261
|
+
function closePopoverAndFocus() {
|
|
262
|
+
popoverOpen = false;
|
|
103
263
|
tick().then(() => {
|
|
104
264
|
document.getElementById(triggerId)?.focus();
|
|
105
265
|
});
|
|
106
266
|
}
|
|
107
|
-
const triggerId = useId();
|
|
108
267
|
|
|
109
|
-
|
|
110
|
-
|
|
268
|
+
// --- Keyboard reorder ---
|
|
269
|
+
let liveMessage = $state('');
|
|
270
|
+
|
|
271
|
+
function moveItem(fromIndex: number, toIndex: number) {
|
|
272
|
+
const arr = getArrayValue();
|
|
273
|
+
if (toIndex < 0 || toIndex >= arr.length) return;
|
|
274
|
+
value = [...arrayMove(arr, fromIndex, toIndex)];
|
|
275
|
+
const label = getLabel(arr[fromIndex]);
|
|
276
|
+
liveMessage = `${label}: ${t.movedTo} ${toIndex + 1}`;
|
|
111
277
|
}
|
|
112
278
|
|
|
113
|
-
// Selected items for multi-select display
|
|
279
|
+
// Selected items for multi-select chip display
|
|
114
280
|
let selectedItems = $derived(
|
|
115
|
-
multiple ? getArrayValue().map((id) => ({ id, label:
|
|
281
|
+
multiple ? getArrayValue().map((id) => ({ id, label: getLabel(id) })) : []
|
|
116
282
|
);
|
|
283
|
+
|
|
284
|
+
// Single-select display
|
|
285
|
+
let singleLabel = $derived(!multiple && value ? getLabel(value as string) : '');
|
|
117
286
|
</script>
|
|
118
287
|
|
|
119
|
-
{#if
|
|
120
|
-
<div class="h-10 w-full
|
|
121
|
-
|
|
122
|
-
|
|
288
|
+
{#if initialLoading}
|
|
289
|
+
<div class="flex h-10 w-full items-center justify-center rounded-md border bg-muted/30">
|
|
290
|
+
<Spinner class="size-4" />
|
|
291
|
+
</div>
|
|
292
|
+
{:else if multiple}
|
|
293
|
+
<!-- Multi-select mode -->
|
|
294
|
+
|
|
295
|
+
<!-- Selected items list with drag reorder -->
|
|
296
|
+
{#if selectedItems.length > 0}
|
|
297
|
+
<div class="flex flex-col gap-1.5">
|
|
298
|
+
{#each selectedItems as item, index (item.id)}
|
|
299
|
+
<div
|
|
300
|
+
use:droppable={{
|
|
301
|
+
container: index.toString(),
|
|
302
|
+
callbacks: {
|
|
303
|
+
onDrop: (state) => {
|
|
304
|
+
const dragIndex = parseInt(state.sourceContainer ?? '');
|
|
305
|
+
const dropIndex = parseInt(state.targetContainer ?? '');
|
|
306
|
+
|
|
307
|
+
if (!isNaN(dragIndex) && !isNaN(dropIndex)) {
|
|
308
|
+
value = [...arrayMove(getArrayValue(), dragIndex, dropIndex)];
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}}
|
|
313
|
+
animate:flip={{ duration: 200 }}
|
|
314
|
+
in:fade={{ duration: 150 }}
|
|
315
|
+
out:fade={{ duration: 150 }}
|
|
316
|
+
>
|
|
317
|
+
<div
|
|
318
|
+
class="flex items-center gap-1 rounded-md border bg-background px-2 py-1.5 text-sm"
|
|
319
|
+
>
|
|
320
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
321
|
+
<div
|
|
322
|
+
use:draggable={{ container: index.toString(), dragData: { id: item.id } }}
|
|
323
|
+
class="cursor-grab text-muted-foreground hover:text-foreground"
|
|
324
|
+
aria-label="{t.reorder} {item.label}"
|
|
325
|
+
ondragstart={(e) => e.stopPropagation()}
|
|
326
|
+
onpointerdown={(e) => e.stopPropagation()}
|
|
327
|
+
>
|
|
328
|
+
<GripVertical class="h-3.5 w-3.5" />
|
|
329
|
+
</div>
|
|
330
|
+
{#if index > 0}
|
|
331
|
+
<button
|
|
332
|
+
type="button"
|
|
333
|
+
class="shrink-0 text-muted-foreground hover:text-foreground"
|
|
334
|
+
onclick={() => moveItem(index, index - 1)}
|
|
335
|
+
aria-label="{t.moveUp} {item.label}"
|
|
336
|
+
>
|
|
337
|
+
<ChevronUp class="h-3.5 w-3.5" />
|
|
338
|
+
</button>
|
|
339
|
+
{/if}
|
|
340
|
+
{#if index < selectedItems.length - 1}
|
|
341
|
+
<button
|
|
342
|
+
type="button"
|
|
343
|
+
class="shrink-0 text-muted-foreground hover:text-foreground"
|
|
344
|
+
onclick={() => moveItem(index, index + 1)}
|
|
345
|
+
aria-label="{t.moveDown} {item.label}"
|
|
346
|
+
>
|
|
347
|
+
<ChevronDown class="h-3.5 w-3.5" />
|
|
348
|
+
</button>
|
|
349
|
+
{/if}
|
|
350
|
+
<span class="flex-1 truncate">{item.label}</span>
|
|
351
|
+
<button
|
|
352
|
+
type="button"
|
|
353
|
+
class="shrink-0 text-muted-foreground hover:text-destructive"
|
|
354
|
+
onclick={() => removeItem(item.id)}
|
|
355
|
+
aria-label="{t.remove} {item.label}"
|
|
356
|
+
>
|
|
357
|
+
<X class="h-3.5 w-3.5" />
|
|
358
|
+
</button>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
{/each}
|
|
362
|
+
</div>
|
|
363
|
+
{/if}
|
|
364
|
+
|
|
365
|
+
<!-- Add trigger -->
|
|
366
|
+
{#if useDialog}
|
|
367
|
+
<button
|
|
368
|
+
type="button"
|
|
369
|
+
class="mt-1.5 flex w-full items-center justify-center gap-1.5 rounded-md border border-dashed px-3 py-2 text-sm text-muted-foreground transition-colors hover:border-primary hover:text-primary"
|
|
370
|
+
onclick={openPicker}
|
|
371
|
+
aria-haspopup="dialog"
|
|
372
|
+
>
|
|
373
|
+
<Plus class="h-4 w-4" />
|
|
374
|
+
{t.add} {singularLabel}
|
|
375
|
+
</button>
|
|
123
376
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
377
|
+
<RelationPickerDialog
|
|
378
|
+
bind:open={pickerOpen}
|
|
379
|
+
title="{t.dialogTitle} {collectionLabel}"
|
|
380
|
+
searchPlaceholder="{t.search} {collectionLabel}..."
|
|
381
|
+
noResults={t.noResults}
|
|
382
|
+
closeLabel={t.closeDialog}
|
|
383
|
+
{multiple}
|
|
384
|
+
selected={getArrayValue()}
|
|
385
|
+
options={pickerOptions}
|
|
386
|
+
loading={pickerLoading}
|
|
387
|
+
bind:searchValue
|
|
388
|
+
onSearchChange={handleSearchChange}
|
|
389
|
+
onSelect={toggleItem}
|
|
390
|
+
onClose={closePicker}
|
|
391
|
+
/>
|
|
392
|
+
{:else}
|
|
393
|
+
<Popover.Root bind:open={popoverOpen}>
|
|
127
394
|
<Popover.Trigger
|
|
128
395
|
id={triggerId}
|
|
129
|
-
class=
|
|
130
|
-
buttonVariants({ variant: 'outline' }),
|
|
131
|
-
'justify-between',
|
|
132
|
-
'text-muted-foreground'
|
|
133
|
-
)}
|
|
396
|
+
class="mt-1.5 flex w-full items-center justify-center gap-1.5 rounded-md border border-dashed px-3 py-2 text-sm text-muted-foreground transition-colors hover:border-primary hover:text-primary"
|
|
134
397
|
role="combobox"
|
|
135
|
-
{
|
|
398
|
+
aria-expanded={popoverOpen}
|
|
136
399
|
>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
{:else}
|
|
140
|
-
{lang[interfaceLanguage.current].select} {getLocalizedLabel(collectionConfig.labels?.singular, interfaceLanguage.current) || collectionConfig.slug}
|
|
141
|
-
{/if}
|
|
142
|
-
<Selector class="opacity-50" />
|
|
400
|
+
<Plus class="h-4 w-4" />
|
|
401
|
+
{t.add} {singularLabel}
|
|
143
402
|
</Popover.Trigger>
|
|
144
403
|
|
|
145
|
-
<Popover.Content class="w-[
|
|
404
|
+
<Popover.Content class="min-w-[280px] p-0">
|
|
146
405
|
<Command.Root>
|
|
147
406
|
<Command.Input
|
|
148
407
|
autofocus
|
|
149
|
-
placeholder="{
|
|
150
|
-
collectionConfig.labels?.plural,
|
|
151
|
-
interfaceLanguage.current
|
|
152
|
-
) || collectionConfig.slug}..."
|
|
408
|
+
placeholder="{t.search} {collectionLabel}..."
|
|
153
409
|
class="h-9"
|
|
154
410
|
/>
|
|
155
|
-
<Command.
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
toggleItem(option.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
411
|
+
<Command.List>
|
|
412
|
+
<Command.Empty>{t.noResults}</Command.Empty>
|
|
413
|
+
<Command.Group>
|
|
414
|
+
{#each pickerOptions as option (option.id)}
|
|
415
|
+
<Command.Item
|
|
416
|
+
value={option.label}
|
|
417
|
+
onSelect={() => toggleItem(option.id)}
|
|
418
|
+
class="flex items-center gap-2"
|
|
419
|
+
>
|
|
420
|
+
<div
|
|
421
|
+
class={cn(
|
|
422
|
+
'flex h-4 w-4 shrink-0 items-center justify-center rounded border',
|
|
423
|
+
isSelected(option.id)
|
|
424
|
+
? 'border-primary bg-primary text-primary-foreground'
|
|
425
|
+
: 'border-muted-foreground/30'
|
|
426
|
+
)}
|
|
427
|
+
>
|
|
428
|
+
{#if isSelected(option.id)}
|
|
429
|
+
<Check class="h-3 w-3" />
|
|
430
|
+
{/if}
|
|
431
|
+
</div>
|
|
432
|
+
<span class="flex-1 truncate">{option.label}</span>
|
|
433
|
+
</Command.Item>
|
|
434
|
+
{/each}
|
|
435
|
+
</Command.Group>
|
|
436
|
+
</Command.List>
|
|
171
437
|
</Command.Root>
|
|
172
438
|
</Popover.Content>
|
|
173
439
|
</Popover.Root>
|
|
440
|
+
{/if}
|
|
174
441
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
442
|
+
<!-- Live region for screen readers -->
|
|
443
|
+
<div class="sr-only" aria-live="polite" role="status">
|
|
444
|
+
{liveMessage || `${getArrayValue().length} ${t.selected}`}
|
|
445
|
+
</div>
|
|
446
|
+
{:else}
|
|
447
|
+
<!-- Single-select mode -->
|
|
448
|
+
{#if value}
|
|
449
|
+
<!-- Show selected chip -->
|
|
450
|
+
<div class="flex items-center gap-2 rounded-md border bg-background px-3 py-2 text-sm">
|
|
451
|
+
<span class="flex-1 truncate">{singleLabel}</span>
|
|
452
|
+
<button
|
|
453
|
+
type="button"
|
|
454
|
+
class="shrink-0 text-muted-foreground hover:text-destructive"
|
|
455
|
+
onclick={clearValue}
|
|
456
|
+
aria-label={t.clear}
|
|
457
|
+
>
|
|
458
|
+
<X class="h-3.5 w-3.5" />
|
|
459
|
+
</button>
|
|
460
|
+
</div>
|
|
461
|
+
{/if}
|
|
462
|
+
|
|
463
|
+
{#if useDialog}
|
|
464
|
+
<button
|
|
465
|
+
type="button"
|
|
466
|
+
class={cn(
|
|
467
|
+
buttonVariants({ variant: 'outline' }),
|
|
468
|
+
'w-full justify-between',
|
|
469
|
+
!value && 'text-muted-foreground',
|
|
470
|
+
value && 'mt-1.5'
|
|
471
|
+
)}
|
|
472
|
+
onclick={openPicker}
|
|
473
|
+
role="combobox"
|
|
474
|
+
aria-expanded={pickerOpen}
|
|
475
|
+
aria-haspopup="dialog"
|
|
476
|
+
{...props}
|
|
477
|
+
>
|
|
478
|
+
{t.select} {singularLabel}
|
|
479
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
|
|
480
|
+
</button>
|
|
481
|
+
|
|
482
|
+
<RelationPickerDialog
|
|
483
|
+
bind:open={pickerOpen}
|
|
484
|
+
title="{t.dialogTitle} {singularLabel}"
|
|
485
|
+
searchPlaceholder="{t.search} {collectionLabel}..."
|
|
486
|
+
noResults={t.noResults}
|
|
487
|
+
closeLabel={t.closeDialog}
|
|
488
|
+
{multiple}
|
|
489
|
+
selected={value ? [value as string] : []}
|
|
490
|
+
options={pickerOptions}
|
|
491
|
+
loading={pickerLoading}
|
|
492
|
+
bind:searchValue
|
|
493
|
+
onSearchChange={handleSearchChange}
|
|
494
|
+
onSelect={toggleItem}
|
|
495
|
+
onClose={closePicker}
|
|
496
|
+
/>
|
|
219
497
|
{:else}
|
|
220
|
-
|
|
221
|
-
<Popover.Root bind:open>
|
|
498
|
+
<Popover.Root bind:open={popoverOpen}>
|
|
222
499
|
<Popover.Trigger
|
|
223
500
|
id={triggerId}
|
|
224
501
|
class={cn(
|
|
225
502
|
buttonVariants({ variant: 'outline' }),
|
|
226
|
-
'justify-between',
|
|
227
|
-
!value && 'text-muted-foreground'
|
|
503
|
+
'w-full justify-between',
|
|
504
|
+
!value && 'text-muted-foreground',
|
|
505
|
+
value && 'mt-1.5'
|
|
228
506
|
)}
|
|
229
507
|
role="combobox"
|
|
508
|
+
aria-expanded={popoverOpen}
|
|
230
509
|
{...props}
|
|
231
510
|
>
|
|
232
|
-
{
|
|
233
|
-
|
|
234
|
-
<Selector class="opacity-50" />
|
|
511
|
+
{t.select} {singularLabel}
|
|
512
|
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 opacity-50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
|
|
235
513
|
</Popover.Trigger>
|
|
236
514
|
|
|
237
|
-
<Popover.Content class="w-[
|
|
515
|
+
<Popover.Content class="min-w-[280px] p-0">
|
|
238
516
|
<Command.Root>
|
|
239
517
|
<Command.Input
|
|
240
518
|
autofocus
|
|
241
|
-
placeholder="{
|
|
242
|
-
collectionConfig.labels?.plural,
|
|
243
|
-
interfaceLanguage.current
|
|
244
|
-
) || collectionConfig.slug}..."
|
|
519
|
+
placeholder="{t.search} {collectionLabel}..."
|
|
245
520
|
class="h-9"
|
|
246
521
|
/>
|
|
247
|
-
<Command.
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
522
|
+
<Command.List>
|
|
523
|
+
<Command.Empty>{t.noResults}</Command.Empty>
|
|
524
|
+
<Command.Group>
|
|
525
|
+
{#each pickerOptions as option (option.id)}
|
|
526
|
+
<Command.Item
|
|
527
|
+
value={option.label}
|
|
528
|
+
onSelect={() => {
|
|
529
|
+
value = option.id;
|
|
530
|
+
closePopoverAndFocus();
|
|
531
|
+
}}
|
|
532
|
+
>
|
|
533
|
+
{option.label}
|
|
534
|
+
<Check
|
|
535
|
+
class={cn('ml-auto', option.id !== value && 'text-transparent')}
|
|
536
|
+
/>
|
|
537
|
+
</Command.Item>
|
|
538
|
+
{/each}
|
|
539
|
+
</Command.Group>
|
|
540
|
+
</Command.List>
|
|
262
541
|
</Command.Root>
|
|
263
542
|
</Popover.Content>
|
|
264
543
|
</Popover.Root>
|