@r2digisolutions/ui 0.28.4 → 0.29.1
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/dist/components/container/SelectMultiple/SelectMultiple.svelte +329 -0
- package/dist/components/container/SelectMultiple/SelectMultiple.svelte.d.ts +4 -0
- package/dist/components/container/SelectMultiple/type.d.ts +23 -0
- package/dist/components/container/SelectMultiple/type.js +1 -0
- package/dist/components/container/index.d.ts +2 -1
- package/dist/components/container/index.js +2 -1
- package/dist/components/ui/Tag/Tag.svelte +2 -1
- package/dist/components/ui/Tag/type.d.ts +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Option, Props } from './type.js';
|
|
3
|
+
import { BoxSelect, Check, ChevronDown, Save, Search, X, Plus } from 'lucide-svelte';
|
|
4
|
+
import { SvelteMap } from 'svelte/reactivity';
|
|
5
|
+
import Tag from '../../ui/Tag/Tag.svelte';
|
|
6
|
+
import Dialog from '../../ui/Dialog/Dialog.svelte';
|
|
7
|
+
import DialogHeader from '../../ui/Dialog/DialogHeader.svelte';
|
|
8
|
+
import DialogTitle from '../../ui/Dialog/DialogTitle.svelte';
|
|
9
|
+
import DialogDescription from '../../ui/Dialog/DialogDescription.svelte';
|
|
10
|
+
import Input from '../../ui/Input/Input.svelte';
|
|
11
|
+
import DialogContent from '../../ui/Dialog/DialogContent.svelte';
|
|
12
|
+
import Checkbox from '../../ui/Checkbox/Checkbox.svelte';
|
|
13
|
+
import DialogFooter from '../../ui/Dialog/DialogFooter.svelte';
|
|
14
|
+
import Button from '../../ui/Button/Button.svelte';
|
|
15
|
+
import { i18n } from '../../../settings/index.js';
|
|
16
|
+
import NoContent from '../../ui/NoContent/NoContent.svelte';
|
|
17
|
+
import Field from '../../ui/Field/Field.svelte';
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
options,
|
|
21
|
+
value = $bindable(),
|
|
22
|
+
label,
|
|
23
|
+
name,
|
|
24
|
+
placeholder,
|
|
25
|
+
multiple = true,
|
|
26
|
+
onConfirm,
|
|
27
|
+
onCancel,
|
|
28
|
+
required,
|
|
29
|
+
errors = [],
|
|
30
|
+
...props
|
|
31
|
+
}: Props = $props();
|
|
32
|
+
|
|
33
|
+
let open = $state(false);
|
|
34
|
+
let search = $state('');
|
|
35
|
+
|
|
36
|
+
let selected_items = $state(new SvelteMap<string, Option>());
|
|
37
|
+
let pending_selection = $state(new SvelteMap<string, Option>());
|
|
38
|
+
|
|
39
|
+
$effect(() => {
|
|
40
|
+
selected_items = new SvelteMap((value ?? []).map((v) => [v.value, v] as [string, Option]));
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function openDialog() {
|
|
44
|
+
pending_selection = new SvelteMap(selected_items);
|
|
45
|
+
search = '';
|
|
46
|
+
open = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function toggleItem(item: Option) {
|
|
50
|
+
if (!multiple) {
|
|
51
|
+
pending_selection = new SvelteMap([[item.value, item]]);
|
|
52
|
+
confirmSelection();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const next = new SvelteMap(pending_selection);
|
|
57
|
+
|
|
58
|
+
if (next.has(item.value)) {
|
|
59
|
+
next.delete(item.value);
|
|
60
|
+
} else {
|
|
61
|
+
next.set(item.value, item);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
pending_selection = next;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function confirmSelection() {
|
|
68
|
+
value = [...pending_selection.values()];
|
|
69
|
+
selected_items = new SvelteMap(pending_selection);
|
|
70
|
+
open = false;
|
|
71
|
+
onConfirm?.(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function cancelSelection() {
|
|
75
|
+
pending_selection = new SvelteMap(selected_items);
|
|
76
|
+
open = false;
|
|
77
|
+
onCancel?.();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function removeSelected(v: string) {
|
|
81
|
+
const next = new SvelteMap(selected_items);
|
|
82
|
+
next.delete(v);
|
|
83
|
+
selected_items = next;
|
|
84
|
+
value = [...selected_items.values()];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const filtered_options = $derived.by(() => {
|
|
88
|
+
if (!search) return options;
|
|
89
|
+
const term = search.toLowerCase();
|
|
90
|
+
return options.filter((option) => option.label.toLowerCase().includes(term));
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
const single_selected: Option | undefined = $derived.by(() => {
|
|
94
|
+
if (!selected_items.size) return;
|
|
95
|
+
return selected_items.values().next().value;
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const footerMessage = $derived.by(() => {
|
|
99
|
+
if (multiple) {
|
|
100
|
+
if (pending_selection.size === 0) return 'No hay elementos seleccionados.';
|
|
101
|
+
if (pending_selection.size === 1) return '1 elemento seleccionado.';
|
|
102
|
+
return `${pending_selection.size} elementos seleccionados.`;
|
|
103
|
+
} else {
|
|
104
|
+
if (single_selected) return `Seleccionado: ${single_selected.label}`;
|
|
105
|
+
return '';
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
</script>
|
|
109
|
+
|
|
110
|
+
<div class={['flex w-full flex-col gap-2', props.parentClass].join(' ')}>
|
|
111
|
+
<Field {name} {label} {errors} {required}>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
onclick={openDialog}
|
|
115
|
+
class="flex h-11 w-full items-center gap-2 rounded-2xl border border-neutral-200/70 bg-neutral-50/80 px-3.5 text-left text-sm text-neutral-800 shadow-xs transition-all hover:border-neutral-300 hover:bg-neutral-50 focus-visible:ring-2 focus-visible:ring-indigo-500/60 focus-visible:outline-none dark:border-neutral-700/70 dark:bg-neutral-900/70 dark:text-neutral-100 dark:hover:border-neutral-500 dark:hover:bg-neutral-900"
|
|
116
|
+
>
|
|
117
|
+
<BoxSelect class="h-4 w-4 text-neutral-400 dark:text-neutral-500" />
|
|
118
|
+
|
|
119
|
+
<span class="truncate text-[13px] text-neutral-600 dark:text-neutral-300">
|
|
120
|
+
{#if multiple}
|
|
121
|
+
{#if selected_items.size > 0}
|
|
122
|
+
{selected_items.size === 1
|
|
123
|
+
? Array.from(selected_items.values())[0].label
|
|
124
|
+
: `${selected_items.size} seleccionados`}
|
|
125
|
+
{:else}
|
|
126
|
+
{placeholder}
|
|
127
|
+
{/if}
|
|
128
|
+
{:else if selected_items.size > 0}
|
|
129
|
+
{single_selected?.label ?? placeholder}
|
|
130
|
+
{:else}
|
|
131
|
+
{placeholder}
|
|
132
|
+
{/if}
|
|
133
|
+
</span>
|
|
134
|
+
|
|
135
|
+
{#if multiple && selected_items.size > 0}
|
|
136
|
+
<span
|
|
137
|
+
class="ml-1 rounded-full bg-neutral-200/80 px-2 py-[2px] text-[11px] text-neutral-700 tabular-nums dark:bg-neutral-800/90 dark:text-neutral-200"
|
|
138
|
+
>
|
|
139
|
+
{selected_items.size}
|
|
140
|
+
</span>
|
|
141
|
+
{/if}
|
|
142
|
+
|
|
143
|
+
<ChevronDown
|
|
144
|
+
class={[
|
|
145
|
+
'ml-auto h-4 w-4 text-neutral-400 transition-transform dark:text-neutral-500',
|
|
146
|
+
open ? 'rotate-180' : ''
|
|
147
|
+
].join(' ')}
|
|
148
|
+
/>
|
|
149
|
+
</button>
|
|
150
|
+
</Field>
|
|
151
|
+
|
|
152
|
+
{#if multiple}
|
|
153
|
+
<div class="flex flex-wrap gap-1.5">
|
|
154
|
+
{#each Array.from(selected_items.values()) as item, index (item.value)}
|
|
155
|
+
<input type="hidden" name="{name}[{index}]" value={item.value} />
|
|
156
|
+
<Tag
|
|
157
|
+
onclose={() => removeSelected(item.value)}
|
|
158
|
+
variant="solid"
|
|
159
|
+
color="indigo"
|
|
160
|
+
class="rounded-full bg-indigo-500/10 text-[11px] text-indigo-700 ring-1 ring-indigo-500/30 dark:bg-indigo-500/15 dark:text-indigo-200"
|
|
161
|
+
>
|
|
162
|
+
{item.label}
|
|
163
|
+
</Tag>
|
|
164
|
+
{/each}
|
|
165
|
+
</div>
|
|
166
|
+
{:else}
|
|
167
|
+
<input type="hidden" {name} value={single_selected?.value ?? ''} />
|
|
168
|
+
{/if}
|
|
169
|
+
</div>
|
|
170
|
+
|
|
171
|
+
<Dialog {open} onclose={cancelSelection}>
|
|
172
|
+
<DialogHeader>
|
|
173
|
+
<DialogTitle>{label}</DialogTitle>
|
|
174
|
+
{#if placeholder}
|
|
175
|
+
<DialogDescription>{placeholder}</DialogDescription>
|
|
176
|
+
{/if}
|
|
177
|
+
|
|
178
|
+
<div class="mt-4 space-y-2">
|
|
179
|
+
<div class="relative">
|
|
180
|
+
<Input
|
|
181
|
+
type="search"
|
|
182
|
+
class="w-full"
|
|
183
|
+
placeholder="Buscar..."
|
|
184
|
+
name="search"
|
|
185
|
+
autofocus
|
|
186
|
+
value={search}
|
|
187
|
+
oninput={(e) => (search = e.currentTarget.value)}
|
|
188
|
+
/>
|
|
189
|
+
<div class="top absolute left-2">
|
|
190
|
+
<Search class="h-4 w-4 text-neutral-400" />
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
{#if multiple}
|
|
195
|
+
<div
|
|
196
|
+
class="flex items-center justify-between text-[11px] text-neutral-500 dark:text-neutral-400"
|
|
197
|
+
>
|
|
198
|
+
<span>
|
|
199
|
+
Seleccionados:
|
|
200
|
+
<span class="ml-1 font-semibold text-neutral-700 dark:text-neutral-200">
|
|
201
|
+
{pending_selection.size}
|
|
202
|
+
</span>
|
|
203
|
+
{#if options.length}
|
|
204
|
+
<span class="ml-1 text-neutral-400">/ {options.length}</span>
|
|
205
|
+
{/if}
|
|
206
|
+
</span>
|
|
207
|
+
{#if pending_selection.size > 0}
|
|
208
|
+
<button
|
|
209
|
+
type="button"
|
|
210
|
+
class="inline-flex items-center gap-1 rounded-full px-2 py-[2px] text-[11px] text-neutral-500 hover:bg-neutral-100 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
|
211
|
+
onclick={() => (pending_selection = new SvelteMap())}
|
|
212
|
+
>
|
|
213
|
+
<X class="h-3 w-3" />
|
|
214
|
+
Limpiar
|
|
215
|
+
</button>
|
|
216
|
+
{/if}
|
|
217
|
+
</div>
|
|
218
|
+
{/if}
|
|
219
|
+
</div>
|
|
220
|
+
</DialogHeader>
|
|
221
|
+
|
|
222
|
+
<DialogContent class="max-h-[70dvh] gap-1 py-2">
|
|
223
|
+
{#each filtered_options as item (item.value)}
|
|
224
|
+
{#if multiple}
|
|
225
|
+
<label class="flex flex-row gap-1">
|
|
226
|
+
<Checkbox
|
|
227
|
+
class={[
|
|
228
|
+
'group flex items-center gap-3 rounded-2xl border px-3.5 py-2.5 text-sm transition-all',
|
|
229
|
+
pending_selection.has(item.value)
|
|
230
|
+
? 'border-indigo-500/70 bg-indigo-500/8 shadow-sm shadow-indigo-500/20 dark:border-indigo-400/70 dark:bg-indigo-500/10'
|
|
231
|
+
: 'border-transparent bg-neutral-100/80 hover:bg-neutral-200/80 dark:bg-neutral-900/70 dark:hover:bg-neutral-800'
|
|
232
|
+
]}
|
|
233
|
+
value={item.value}
|
|
234
|
+
label={item.label}
|
|
235
|
+
checked={pending_selection.has(item.value)}
|
|
236
|
+
onchange={() => toggleItem(item)}
|
|
237
|
+
/>
|
|
238
|
+
<div class="flex flex-col">
|
|
239
|
+
<span>{item.label}</span>
|
|
240
|
+
{#if item.description}
|
|
241
|
+
<span
|
|
242
|
+
class="block text-[11px] text-neutral-500 group-hover:text-neutral-600 dark:text-neutral-400 dark:group-hover:text-neutral-300"
|
|
243
|
+
>
|
|
244
|
+
{item.description}
|
|
245
|
+
</span>
|
|
246
|
+
{/if}
|
|
247
|
+
</div>
|
|
248
|
+
</label>
|
|
249
|
+
{:else}
|
|
250
|
+
<button
|
|
251
|
+
type="button"
|
|
252
|
+
onclick={() => toggleItem(item)}
|
|
253
|
+
class={[
|
|
254
|
+
'flex w-full cursor-pointer items-center gap-3 rounded-2xl border px-3.5 py-2.5 text-sm transition-all',
|
|
255
|
+
pending_selection.has(item.value)
|
|
256
|
+
? 'border-indigo-500/70 bg-indigo-500/8 shadow-sm shadow-indigo-500/20 dark:border-indigo-400/70 dark:bg-indigo-500/10'
|
|
257
|
+
: 'border-transparent bg-neutral-100/80 hover:bg-neutral-200/80 dark:bg-neutral-900/70 dark:hover:bg-neutral-800'
|
|
258
|
+
].join(' ')}
|
|
259
|
+
>
|
|
260
|
+
<!-- icono left -->
|
|
261
|
+
<div
|
|
262
|
+
class={[
|
|
263
|
+
'flex h-7 w-7 items-center justify-center rounded-full border text-neutral-500 transition-all',
|
|
264
|
+
pending_selection.has(item.value)
|
|
265
|
+
? 'border-transparent bg-linear-to-tr from-indigo-500 via-violet-500 to-blue-500 text-white shadow-sm shadow-indigo-500/40'
|
|
266
|
+
: 'border-neutral-300 bg-white/90 dark:border-neutral-600 dark:bg-neutral-900/90'
|
|
267
|
+
].join(' ')}
|
|
268
|
+
>
|
|
269
|
+
{#if pending_selection.has(item.value)}
|
|
270
|
+
<Check class="h-3.5 w-3.5" />
|
|
271
|
+
{:else}
|
|
272
|
+
<Plus class="h-3.5 w-3.5" />
|
|
273
|
+
{/if}
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
<div class="flex flex-1 flex-col text-left">
|
|
277
|
+
<span class="text-[13px] font-medium text-neutral-800 dark:text-neutral-100">
|
|
278
|
+
{item.label}
|
|
279
|
+
</span>
|
|
280
|
+
{#if item.description}
|
|
281
|
+
<span class="text-[11px] text-neutral-500 dark:text-neutral-400">
|
|
282
|
+
{item.description}
|
|
283
|
+
</span>
|
|
284
|
+
{/if}
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
{#if pending_selection.has(item.value)}
|
|
288
|
+
<div
|
|
289
|
+
class="inline-flex items-center gap-1 rounded-full bg-neutral-900/90 px-2.5 py-[3px] text-[10px] font-medium text-neutral-50 shadow-sm shadow-black/40 dark:bg-neutral-50/95 dark:text-neutral-900"
|
|
290
|
+
>
|
|
291
|
+
<Check class="h-3 w-3" />
|
|
292
|
+
<span>Seleccionado</span>
|
|
293
|
+
</div>
|
|
294
|
+
{/if}
|
|
295
|
+
</button>
|
|
296
|
+
{/if}
|
|
297
|
+
{:else}
|
|
298
|
+
<NoContent
|
|
299
|
+
icon={Search}
|
|
300
|
+
title="No se encontraron resultados"
|
|
301
|
+
subtitle="Prueba con otros términos o limpia el buscador."
|
|
302
|
+
/>
|
|
303
|
+
{/each}
|
|
304
|
+
</DialogContent>
|
|
305
|
+
|
|
306
|
+
<DialogFooter class="flex items-center justify-between gap-2">
|
|
307
|
+
{#if footerMessage}
|
|
308
|
+
<div class="text-[11px] text-neutral-500 dark:text-neutral-400">
|
|
309
|
+
{footerMessage}
|
|
310
|
+
</div>
|
|
311
|
+
{/if}
|
|
312
|
+
|
|
313
|
+
<div class="ml-auto flex gap-2">
|
|
314
|
+
<Button variant="secondary" onclick={cancelSelection}>
|
|
315
|
+
<X class="h-4 w-4" />
|
|
316
|
+
{i18n.t('common.cancel')}
|
|
317
|
+
</Button>
|
|
318
|
+
<Button variant="primary" onclick={confirmSelection}>
|
|
319
|
+
<Save class="h-4 w-4" />
|
|
320
|
+
{i18n.t('common.confirm')}
|
|
321
|
+
{#if multiple}
|
|
322
|
+
<span class="ml-1 text-[11px] tabular-nums">
|
|
323
|
+
({pending_selection.size})
|
|
324
|
+
</span>
|
|
325
|
+
{/if}
|
|
326
|
+
</Button>
|
|
327
|
+
</div>
|
|
328
|
+
</DialogFooter>
|
|
329
|
+
</Dialog>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { ClassValue } from 'svelte/elements';
|
|
3
|
+
export interface Option {
|
|
4
|
+
label: string;
|
|
5
|
+
value: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
}
|
|
9
|
+
export interface Props {
|
|
10
|
+
parentClass?: ClassValue;
|
|
11
|
+
class?: ClassValue;
|
|
12
|
+
required?: boolean;
|
|
13
|
+
multiple?: boolean;
|
|
14
|
+
onConfirm?: (selected: Option[]) => void;
|
|
15
|
+
onCancel?: () => void;
|
|
16
|
+
item?: Snippet<[option: Option]>;
|
|
17
|
+
options: Option[];
|
|
18
|
+
value?: Option[];
|
|
19
|
+
label: string;
|
|
20
|
+
name: string;
|
|
21
|
+
placeholder: string;
|
|
22
|
+
errors?: string[];
|
|
23
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -2,6 +2,7 @@ import DataTable from './DataTable/DataTable.svelte';
|
|
|
2
2
|
import TabsStepper from './TabsStepper/TabsStepper.svelte';
|
|
3
3
|
import DataTableShell from './DataTableShell/DataTableShell.svelte';
|
|
4
4
|
import { DataTableController } from './DataTableShell/core/DataTableController.svelte.js';
|
|
5
|
+
import SelectMultiple from './SelectMultiple/SelectMultiple.svelte';
|
|
5
6
|
export * from './DataTable/core/types.js';
|
|
6
7
|
export * from './TabsStepper/core/types.js';
|
|
7
|
-
export { DataTable, TabsStepper, DataTableShell, DataTableController };
|
|
8
|
+
export { DataTable, TabsStepper, DataTableShell, DataTableController, SelectMultiple };
|
|
@@ -2,6 +2,7 @@ import DataTable from './DataTable/DataTable.svelte';
|
|
|
2
2
|
import TabsStepper from './TabsStepper/TabsStepper.svelte';
|
|
3
3
|
import DataTableShell from './DataTableShell/DataTableShell.svelte';
|
|
4
4
|
import { DataTableController } from './DataTableShell/core/DataTableController.svelte.js';
|
|
5
|
+
import SelectMultiple from './SelectMultiple/SelectMultiple.svelte';
|
|
5
6
|
export * from './DataTable/core/types.js';
|
|
6
7
|
export * from './TabsStepper/core/types.js';
|
|
7
|
-
export { DataTable, TabsStepper, DataTableShell, DataTableController };
|
|
8
|
+
export { DataTable, TabsStepper, DataTableShell, DataTableController, SelectMultiple };
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
white: `${type}-white text-black`,
|
|
31
31
|
info: `${type}-blue-500 text-whit`,
|
|
32
32
|
outline: `${type}-gray-200 text-gray-800`,
|
|
33
|
-
default: `${type}-gray-800 text-white
|
|
33
|
+
default: `${type}-gray-800 text-white`,
|
|
34
|
+
indigo: `${type}-indigo-500 text-white`
|
|
34
35
|
});
|
|
35
36
|
|
|
36
37
|
const variants: Record<typeof variant, string> = $derived({
|
|
@@ -4,7 +4,7 @@ export interface Props {
|
|
|
4
4
|
onclick?(): void;
|
|
5
5
|
href?: string;
|
|
6
6
|
variant?: 'solid' | 'outline';
|
|
7
|
-
color?: 'primary' | 'secondary' | 'danger' | 'white' | 'teal' | 'info' | 'outline' | 'default';
|
|
7
|
+
color?: 'primary' | 'secondary' | 'danger' | 'white' | 'teal' | 'info' | 'outline' | 'default' | 'indigo';
|
|
8
8
|
shadow?: 'none' | 'sm' | 'md' | 'lg' | 'xl';
|
|
9
9
|
onclose?(): void;
|
|
10
10
|
class?: ClassValue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@r2digisolutions/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.29.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"packageManager": "bun@1.3.4",
|
|
6
6
|
"publishConfig": {
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"tailwindcss": "4.1.17",
|
|
83
83
|
"typescript": "5.9.3",
|
|
84
84
|
"typescript-eslint": "8.48.1",
|
|
85
|
-
"vite": "7.2.
|
|
85
|
+
"vite": "7.2.7",
|
|
86
86
|
"vitest": "4.0.15"
|
|
87
87
|
},
|
|
88
88
|
"dependencies": {
|