@r2digisolutions/ui 0.32.3 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import type { Option, Props } from './type.js';
|
|
3
|
-
import { BoxSelect, Check, ChevronDown, Save, Search, X, Plus } from 'lucide-svelte';
|
|
3
|
+
import { BoxSelect, Check, ChevronDown, Save, Search, X, Plus, ChevronLeft } from 'lucide-svelte';
|
|
4
4
|
import { SvelteMap } from 'svelte/reactivity';
|
|
5
5
|
import Tag from '../../ui/Tag/Tag.svelte';
|
|
6
6
|
import Dialog from '../../ui/Dialog/Dialog.svelte';
|
|
@@ -25,8 +25,20 @@
|
|
|
25
25
|
multiple = true,
|
|
26
26
|
onConfirm,
|
|
27
27
|
onCancel,
|
|
28
|
+
onValueChange, // ✅ NUEVO (para padre “controlado” sin bind)
|
|
28
29
|
required,
|
|
29
30
|
errors = [],
|
|
31
|
+
item,
|
|
32
|
+
|
|
33
|
+
tree = false,
|
|
34
|
+
childrenKey = 'children',
|
|
35
|
+
selectParents = false,
|
|
36
|
+
showPathInSearch = true,
|
|
37
|
+
|
|
38
|
+
// UX opcional: si single y quieres cerrar al elegir
|
|
39
|
+
closeOnPick = false,
|
|
40
|
+
|
|
41
|
+
debug = false,
|
|
30
42
|
...props
|
|
31
43
|
}: Props = $props();
|
|
32
44
|
|
|
@@ -36,39 +48,187 @@
|
|
|
36
48
|
let selected_items = $state(new SvelteMap<string, Option>());
|
|
37
49
|
let pending_selection = $state(new SvelteMap<string, Option>());
|
|
38
50
|
|
|
51
|
+
let navStack = $state<string[]>([]);
|
|
52
|
+
|
|
53
|
+
const keyOf = (v: unknown) => String(v ?? '').trim();
|
|
54
|
+
const getChildren = (opt: Option): Option[] => ((opt as any)?.[childrenKey] as Option[]) ?? [];
|
|
55
|
+
const hasChildren = (opt: Option) => getChildren(opt)?.length > 0;
|
|
56
|
+
|
|
57
|
+
function log(...args: any[]) {
|
|
58
|
+
if (!debug) return;
|
|
59
|
+
const safe = args.map((a) => {
|
|
60
|
+
try {
|
|
61
|
+
return $state.snapshot(a);
|
|
62
|
+
} catch {
|
|
63
|
+
return a;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
console.log(`[Select:${name}]`, ...safe);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ===== indexado árbol
|
|
70
|
+
function flattenTree(
|
|
71
|
+
nodes: Option[],
|
|
72
|
+
parentValue: string | null,
|
|
73
|
+
out: Option[] = [],
|
|
74
|
+
parentBy = new Map<string, string | null>(),
|
|
75
|
+
childrenBy = new Map<string, string[]>(),
|
|
76
|
+
byValue = new Map<string, Option>()
|
|
77
|
+
) {
|
|
78
|
+
for (const n of nodes ?? []) {
|
|
79
|
+
const k = keyOf((n as any)?.value);
|
|
80
|
+
const node: Option = { ...(n as any), value: k };
|
|
81
|
+
|
|
82
|
+
byValue.set(k, node);
|
|
83
|
+
parentBy.set(k, parentValue);
|
|
84
|
+
out.push(node);
|
|
85
|
+
|
|
86
|
+
const children = getChildren(n);
|
|
87
|
+
if (children?.length) {
|
|
88
|
+
childrenBy.set(
|
|
89
|
+
k,
|
|
90
|
+
children.map((c) => keyOf((c as any)?.value))
|
|
91
|
+
);
|
|
92
|
+
flattenTree(children, k, out, parentBy, childrenBy, byValue);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { out, parentBy, childrenBy, byValue };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const treeIndex = $derived.by(() => {
|
|
99
|
+
const normalized = (options ?? []).map((o) => ({
|
|
100
|
+
...(o as any),
|
|
101
|
+
value: keyOf((o as any).value)
|
|
102
|
+
}));
|
|
103
|
+
return flattenTree(normalized, null);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const flatOptions = $derived.by(() => treeIndex.out);
|
|
107
|
+
const byValue = $derived.by(() => treeIndex.byValue);
|
|
108
|
+
const parentByValue = $derived.by(() => treeIndex.parentBy);
|
|
109
|
+
const childrenByValue = $derived.by(() => treeIndex.childrenBy);
|
|
110
|
+
|
|
111
|
+
function canonical(opt: Option) {
|
|
112
|
+
const k = keyOf(opt?.value);
|
|
113
|
+
return byValue.get(k) ?? { ...(opt as any), value: k };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getPathValues(v: string) {
|
|
117
|
+
const path: string[] = [];
|
|
118
|
+
let cur: string | null | undefined = v;
|
|
119
|
+
while (cur) {
|
|
120
|
+
path.push(cur);
|
|
121
|
+
cur = parentByValue.get(cur) ?? null;
|
|
122
|
+
}
|
|
123
|
+
return path.reverse();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getPathLabel(v: string) {
|
|
127
|
+
const parts = getPathValues(v)
|
|
128
|
+
.map((pv) => byValue.get(pv)?.label)
|
|
129
|
+
.filter(Boolean) as string[];
|
|
130
|
+
return parts.join(' › ');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const pendingKeys = $derived.by(() => {
|
|
134
|
+
const s = new Set<string>();
|
|
135
|
+
for (const k of pending_selection.keys()) s.add(keyOf(k));
|
|
136
|
+
return s;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ===== IMPORTANTE:
|
|
140
|
+
// Solo sincronizamos selected_items con value externo (edit inicial / controlado)
|
|
39
141
|
$effect(() => {
|
|
40
|
-
|
|
142
|
+
const next = new SvelteMap<string, Option>();
|
|
143
|
+
for (const v of value ?? []) {
|
|
144
|
+
const ov =
|
|
145
|
+
v && typeof v === 'object'
|
|
146
|
+
? (v as Option)
|
|
147
|
+
: ({ label: String(v), value: String(v) } as Option);
|
|
148
|
+
const c = canonical(ov);
|
|
149
|
+
next.set(keyOf(c.value), c);
|
|
150
|
+
}
|
|
151
|
+
selected_items = next;
|
|
41
152
|
});
|
|
42
153
|
|
|
154
|
+
function firstKey(map: SvelteMap<string, Option>) {
|
|
155
|
+
for (const k of map.keys()) return k;
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function computeNavStackForSelection(map: SvelteMap<string, Option>) {
|
|
160
|
+
if (!tree) return [];
|
|
161
|
+
if (!map.size) return [];
|
|
162
|
+
|
|
163
|
+
const k = firstKey(map);
|
|
164
|
+
if (!k) return [];
|
|
165
|
+
|
|
166
|
+
const kk = keyOf(k);
|
|
167
|
+
const selectedOpt = byValue.get(kk);
|
|
168
|
+
|
|
169
|
+
// si no existe en index, no forzamos nav
|
|
170
|
+
if (!selectedOpt) return [];
|
|
171
|
+
|
|
172
|
+
const path = getPathValues(kk);
|
|
173
|
+
|
|
174
|
+
// si tiene hijos, abrir dentro del seleccionado
|
|
175
|
+
if (hasChildren(selectedOpt)) return path;
|
|
176
|
+
|
|
177
|
+
// si es leaf, abrir en su padre
|
|
178
|
+
return path.slice(0, -1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function rebuildMapFromArray(arr: Option[] | undefined) {
|
|
182
|
+
const next = new SvelteMap<string, Option>();
|
|
183
|
+
for (const v of arr ?? []) {
|
|
184
|
+
const c = canonical(v);
|
|
185
|
+
next.set(keyOf(c.value), c);
|
|
186
|
+
}
|
|
187
|
+
return next;
|
|
188
|
+
}
|
|
189
|
+
|
|
43
190
|
function openDialog() {
|
|
44
|
-
pending_selection = new SvelteMap(selected_items);
|
|
45
191
|
search = '';
|
|
192
|
+
|
|
193
|
+
// Abrimos con lo que nos pasa el padre (value) si existe,
|
|
194
|
+
// si no, con lo último confirmado internamente (selected_items).
|
|
195
|
+
const base = value?.length ? rebuildMapFromArray(value) : new SvelteMap(selected_items);
|
|
196
|
+
pending_selection = base;
|
|
197
|
+
navStack = computeNavStackForSelection(base);
|
|
198
|
+
|
|
46
199
|
open = true;
|
|
47
|
-
}
|
|
48
200
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
201
|
+
if (debug) {
|
|
202
|
+
const k = firstKey(base);
|
|
203
|
+
log('OPEN', {
|
|
204
|
+
valueIn: value,
|
|
205
|
+
pendingKeys: [...base.keys()],
|
|
206
|
+
selectedKey: k,
|
|
207
|
+
path: k ? getPathValues(keyOf(k)) : [],
|
|
208
|
+
navStack,
|
|
209
|
+
flatTotal: flatOptions.length,
|
|
210
|
+
byValueSize: byValue.size
|
|
211
|
+
});
|
|
54
212
|
}
|
|
213
|
+
}
|
|
55
214
|
|
|
56
|
-
|
|
215
|
+
function commitSelection(next: SvelteMap<string, Option>) {
|
|
216
|
+
const arr = [...next.values()];
|
|
57
217
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
} else {
|
|
61
|
-
next.set(item.value, item);
|
|
62
|
-
}
|
|
218
|
+
// 1) actualiza el bind interno (si el padre usa bind, perfecto)
|
|
219
|
+
value = arr;
|
|
63
220
|
|
|
64
|
-
|
|
221
|
+
// 2) guarda interno
|
|
222
|
+
selected_items = new SvelteMap(next);
|
|
223
|
+
|
|
224
|
+
// 3) callback para padre “controlado” (tu caso real)
|
|
225
|
+
onValueChange?.(arr);
|
|
65
226
|
}
|
|
66
227
|
|
|
67
228
|
function confirmSelection() {
|
|
68
|
-
|
|
69
|
-
selected_items = new SvelteMap(pending_selection);
|
|
229
|
+
commitSelection(pending_selection);
|
|
70
230
|
open = false;
|
|
71
|
-
onConfirm?.(
|
|
231
|
+
onConfirm?.([...pending_selection.values()]);
|
|
72
232
|
}
|
|
73
233
|
|
|
74
234
|
function cancelSelection() {
|
|
@@ -78,16 +238,59 @@
|
|
|
78
238
|
}
|
|
79
239
|
|
|
80
240
|
function removeSelected(v: string) {
|
|
241
|
+
const k = keyOf(v);
|
|
81
242
|
const next = new SvelteMap(selected_items);
|
|
82
|
-
next.delete(
|
|
243
|
+
next.delete(k);
|
|
244
|
+
|
|
83
245
|
selected_items = next;
|
|
84
246
|
value = [...selected_items.values()];
|
|
247
|
+
onValueChange?.([...selected_items.values()]);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function enterItem(opt: Option) {
|
|
251
|
+
navStack = [...navStack, keyOf(opt.value)];
|
|
85
252
|
}
|
|
86
253
|
|
|
254
|
+
function goBack() {
|
|
255
|
+
navStack = navStack.slice(0, -1);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const currentLevelOptions = $derived.by(() => {
|
|
259
|
+
if (!tree) return (options ?? []).map(canonical);
|
|
260
|
+
|
|
261
|
+
const currentParent = navStack.length ? navStack[navStack.length - 1] : null;
|
|
262
|
+
|
|
263
|
+
if (!currentParent) {
|
|
264
|
+
return flatOptions
|
|
265
|
+
.filter((o) => (parentByValue.get(keyOf(o.value)) ?? null) === null)
|
|
266
|
+
.map(canonical);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const childValues = childrenByValue.get(currentParent) ?? [];
|
|
270
|
+
return childValues
|
|
271
|
+
.map((v) => byValue.get(v))
|
|
272
|
+
.filter(Boolean)
|
|
273
|
+
.map(canonical) as Option[];
|
|
274
|
+
});
|
|
275
|
+
|
|
87
276
|
const filtered_options = $derived.by(() => {
|
|
88
|
-
if (
|
|
277
|
+
if (tree && search) {
|
|
278
|
+
const term = search.toLowerCase();
|
|
279
|
+
return flatOptions.map(canonical).filter((opt) => {
|
|
280
|
+
const labelHit = opt.label.toLowerCase().includes(term);
|
|
281
|
+
const descHit = (opt.description ?? '').toLowerCase().includes(term);
|
|
282
|
+
const pathHit = showPathInSearch
|
|
283
|
+
? getPathLabel(keyOf(opt.value)).toLowerCase().includes(term)
|
|
284
|
+
: false;
|
|
285
|
+
return labelHit || descHit || pathHit;
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (tree && !search) return currentLevelOptions;
|
|
290
|
+
|
|
291
|
+
if (!search) return (options ?? []).map(canonical);
|
|
89
292
|
const term = search.toLowerCase();
|
|
90
|
-
return options.filter((
|
|
293
|
+
return (options ?? []).map(canonical).filter((opt) => opt.label.toLowerCase().includes(term));
|
|
91
294
|
});
|
|
92
295
|
|
|
93
296
|
const single_selected: Option | undefined = $derived.by(() => {
|
|
@@ -105,6 +308,54 @@
|
|
|
105
308
|
return '';
|
|
106
309
|
}
|
|
107
310
|
});
|
|
311
|
+
|
|
312
|
+
function canSelect(opt: Option) {
|
|
313
|
+
if (!tree) return true;
|
|
314
|
+
const has = hasChildren(opt);
|
|
315
|
+
if (has && !selectParents) return false;
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function toggleItem(optRaw: Option) {
|
|
320
|
+
const opt = canonical(optRaw);
|
|
321
|
+
const k = keyOf(opt.value);
|
|
322
|
+
|
|
323
|
+
const selectable = canSelect(opt);
|
|
324
|
+
const optHasChildren = tree && hasChildren(opt);
|
|
325
|
+
|
|
326
|
+
if (!selectable) {
|
|
327
|
+
if (tree && !search && optHasChildren) enterItem(opt);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// single
|
|
332
|
+
if (!multiple) {
|
|
333
|
+
const next = new SvelteMap<string, Option>([[k, opt]]);
|
|
334
|
+
pending_selection = next;
|
|
335
|
+
|
|
336
|
+
// abrir dentro si tiene hijos
|
|
337
|
+
navStack = computeNavStackForSelection(next);
|
|
338
|
+
|
|
339
|
+
// opcional: commit inmediato
|
|
340
|
+
if (closeOnPick) {
|
|
341
|
+
commitSelection(next);
|
|
342
|
+
open = false;
|
|
343
|
+
onConfirm?.([...next.values()]);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// multi
|
|
350
|
+
const next = new SvelteMap(pending_selection);
|
|
351
|
+
if (next.has(k)) next.delete(k);
|
|
352
|
+
else next.set(k, opt);
|
|
353
|
+
pending_selection = next;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function renderLabelLine(opt: Option) {
|
|
357
|
+
return opt.label;
|
|
358
|
+
}
|
|
108
359
|
</script>
|
|
109
360
|
|
|
110
361
|
<div class={['flex w-full flex-col gap-2', props.parentClass].join(' ')}>
|
|
@@ -151,15 +402,15 @@
|
|
|
151
402
|
|
|
152
403
|
{#if multiple}
|
|
153
404
|
<div class="flex flex-wrap gap-1.5">
|
|
154
|
-
{#each Array.from(selected_items.values()) as
|
|
155
|
-
<input type="hidden" name="{name}[{index}]" value={
|
|
405
|
+
{#each Array.from(selected_items.values()) as sel, index (sel.value)}
|
|
406
|
+
<input type="hidden" name="{name}[{index}]" value={sel.value} />
|
|
156
407
|
<Tag
|
|
157
|
-
onclose={() => removeSelected(
|
|
408
|
+
onclose={() => removeSelected(sel.value)}
|
|
158
409
|
variant="solid"
|
|
159
410
|
color="indigo"
|
|
160
411
|
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
412
|
>
|
|
162
|
-
{
|
|
413
|
+
{sel.label}
|
|
163
414
|
</Tag>
|
|
164
415
|
{/each}
|
|
165
416
|
</div>
|
|
@@ -190,28 +441,42 @@
|
|
|
190
441
|
<Search class="h-4 w-4 text-neutral-400" />
|
|
191
442
|
</div>
|
|
192
443
|
</div>
|
|
193
|
-
|
|
444
|
+
|
|
445
|
+
{#if tree && !search}
|
|
194
446
|
<div
|
|
195
|
-
class="flex items-center justify-between text-[11px] text-neutral-500 dark:text-neutral-400"
|
|
447
|
+
class="flex items-center justify-between gap-2 text-[11px] text-neutral-500 dark:text-neutral-400"
|
|
196
448
|
>
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
449
|
+
<div class="flex min-w-0 items-center gap-2">
|
|
450
|
+
{#if navStack.length > 0}
|
|
451
|
+
<button
|
|
452
|
+
type="button"
|
|
453
|
+
class="inline-flex items-center gap-1 rounded-full px-2 py-[2px] hover:bg-neutral-100 dark:hover:bg-neutral-800"
|
|
454
|
+
onclick={goBack}
|
|
455
|
+
>
|
|
456
|
+
<ChevronLeft class="h-3.5 w-3.5" />
|
|
457
|
+
Atrás
|
|
458
|
+
</button>
|
|
204
459
|
{/if}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
460
|
+
|
|
461
|
+
<span class="truncate">
|
|
462
|
+
{#if navStack.length === 0}
|
|
463
|
+
Raíz
|
|
464
|
+
{:else}
|
|
465
|
+
{getPathLabel(navStack[navStack.length - 1])}
|
|
466
|
+
{/if}
|
|
467
|
+
</span>
|
|
468
|
+
</div>
|
|
469
|
+
|
|
470
|
+
{#if multiple}
|
|
471
|
+
<span>
|
|
472
|
+
Seleccionados:
|
|
473
|
+
<span class="ml-1 font-semibold text-neutral-700 dark:text-neutral-200"
|
|
474
|
+
>{pending_selection.size}</span
|
|
475
|
+
>
|
|
476
|
+
<span class="ml-1 text-neutral-400"
|
|
477
|
+
>/ {tree ? flatOptions.length : options.length}</span
|
|
478
|
+
>
|
|
479
|
+
</span>
|
|
215
480
|
{/if}
|
|
216
481
|
</div>
|
|
217
482
|
{/if}
|
|
@@ -219,71 +484,132 @@
|
|
|
219
484
|
</DialogHeader>
|
|
220
485
|
|
|
221
486
|
<DialogContent class="max-h-[70dvh] gap-1 py-2">
|
|
222
|
-
{#each filtered_options as
|
|
487
|
+
{#each filtered_options as opt (opt.value)}
|
|
488
|
+
{@const k = keyOf(opt.value)}
|
|
489
|
+
{@const optHasChildren = tree && hasChildren(opt)}
|
|
490
|
+
{@const selected = pendingKeys.has(k)}
|
|
491
|
+
{@const selectable = canSelect(opt)}
|
|
492
|
+
|
|
223
493
|
{#if multiple}
|
|
224
|
-
<label class="
|
|
225
|
-
<
|
|
494
|
+
<label class="block">
|
|
495
|
+
<div
|
|
226
496
|
class={[
|
|
227
|
-
'group flex items-
|
|
228
|
-
|
|
497
|
+
'group flex w-full items-start gap-3 rounded-2xl border px-3.5 py-2.5 text-sm transition-all',
|
|
498
|
+
selected
|
|
229
499
|
? 'border-indigo-500/70 bg-indigo-500/8 shadow-sm shadow-indigo-500/20 dark:border-indigo-400/70 dark:bg-indigo-500/10'
|
|
230
|
-
: 'border-transparent bg-neutral-100/80 hover:bg-neutral-200/80 dark:bg-neutral-900/70 dark:hover:bg-neutral-800'
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
500
|
+
: 'border-transparent bg-neutral-100/80 hover:bg-neutral-200/80 dark:bg-neutral-900/70 dark:hover:bg-neutral-800',
|
|
501
|
+
!selectable ? 'opacity-75' : ''
|
|
502
|
+
].join(' ')}
|
|
503
|
+
>
|
|
504
|
+
<Checkbox
|
|
505
|
+
class="mt-0.5"
|
|
506
|
+
value={k}
|
|
507
|
+
label=""
|
|
508
|
+
checked={selected}
|
|
509
|
+
onchange={() => toggleItem(opt)}
|
|
510
|
+
/>
|
|
511
|
+
|
|
512
|
+
<div class="flex min-w-0 flex-1 flex-col">
|
|
513
|
+
{#if item}
|
|
514
|
+
{@render item(opt)}
|
|
515
|
+
{:else}
|
|
516
|
+
<span
|
|
517
|
+
class="truncate text-[13px] font-medium text-neutral-800 dark:text-neutral-100"
|
|
518
|
+
>
|
|
519
|
+
{renderLabelLine(opt)}
|
|
520
|
+
</span>
|
|
521
|
+
|
|
522
|
+
{#if opt.description}
|
|
523
|
+
<span
|
|
524
|
+
class="truncate text-[11px] text-neutral-500 group-hover:text-neutral-600 dark:text-neutral-400 dark:group-hover:text-neutral-300"
|
|
525
|
+
>
|
|
526
|
+
{opt.description}
|
|
527
|
+
</span>
|
|
528
|
+
{/if}
|
|
529
|
+
|
|
530
|
+
{#if tree && search && showPathInSearch}
|
|
531
|
+
<span class="mt-0.5 truncate text-[11px] text-neutral-400">{getPathLabel(k)}</span
|
|
532
|
+
>
|
|
533
|
+
{/if}
|
|
534
|
+
|
|
535
|
+
{#if tree && !search && optHasChildren}
|
|
536
|
+
<span class="mt-0.5 truncate text-[11px] text-neutral-400"
|
|
537
|
+
>{getChildren(opt).length} subcategorías</span
|
|
538
|
+
>
|
|
539
|
+
{/if}
|
|
540
|
+
{/if}
|
|
541
|
+
</div>
|
|
542
|
+
|
|
543
|
+
{#if tree && optHasChildren && !search}
|
|
544
|
+
<button
|
|
545
|
+
type="button"
|
|
546
|
+
class="mt-0.5 inline-flex items-center gap-1 rounded-full px-2 py-[2px] text-[11px] text-neutral-500 hover:bg-neutral-200/70 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
|
547
|
+
onclick={(e) => {
|
|
548
|
+
e.preventDefault();
|
|
549
|
+
e.stopPropagation();
|
|
550
|
+
enterItem(opt);
|
|
551
|
+
}}
|
|
242
552
|
>
|
|
243
|
-
|
|
244
|
-
|
|
553
|
+
<ChevronDown class="h-3.5 w-3.5 -rotate-90" />
|
|
554
|
+
Ver
|
|
555
|
+
</button>
|
|
245
556
|
{/if}
|
|
246
557
|
</div>
|
|
247
558
|
</label>
|
|
248
559
|
{:else}
|
|
249
560
|
<button
|
|
250
561
|
type="button"
|
|
251
|
-
onclick={() => toggleItem(
|
|
562
|
+
onclick={() => toggleItem(opt)}
|
|
252
563
|
class={[
|
|
253
564
|
'flex w-full cursor-pointer items-center gap-3 rounded-2xl border px-3.5 py-2.5 text-sm transition-all',
|
|
254
|
-
|
|
565
|
+
selected
|
|
255
566
|
? 'border-indigo-500/70 bg-indigo-500/8 shadow-sm shadow-indigo-500/20 dark:border-indigo-400/70 dark:bg-indigo-500/10'
|
|
256
|
-
: 'border-transparent bg-neutral-100/80 hover:bg-neutral-200/80 dark:bg-neutral-900/70 dark:hover:bg-neutral-800'
|
|
567
|
+
: 'border-transparent bg-neutral-100/80 hover:bg-neutral-200/80 dark:bg-neutral-900/70 dark:hover:bg-neutral-800',
|
|
568
|
+
!selectable ? 'opacity-75' : ''
|
|
257
569
|
].join(' ')}
|
|
258
570
|
>
|
|
259
|
-
<!-- icono left -->
|
|
260
571
|
<div
|
|
261
572
|
class={[
|
|
262
573
|
'flex h-7 w-7 items-center justify-center rounded-full border text-neutral-500 transition-all',
|
|
263
|
-
|
|
574
|
+
selected
|
|
264
575
|
? 'border-transparent bg-linear-to-tr from-indigo-500 via-violet-500 to-blue-500 text-white shadow-sm shadow-indigo-500/40'
|
|
265
576
|
: 'border-neutral-300 bg-white/90 dark:border-neutral-600 dark:bg-neutral-900/90'
|
|
266
577
|
].join(' ')}
|
|
267
578
|
>
|
|
268
|
-
{#if
|
|
579
|
+
{#if selected}
|
|
269
580
|
<Check class="h-3.5 w-3.5" />
|
|
270
581
|
{:else}
|
|
271
582
|
<Plus class="h-3.5 w-3.5" />
|
|
272
583
|
{/if}
|
|
273
584
|
</div>
|
|
274
585
|
|
|
275
|
-
<div class="flex flex-1 flex-col text-left">
|
|
276
|
-
|
|
277
|
-
{item
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
{item.description}
|
|
586
|
+
<div class="flex min-w-0 flex-1 flex-col text-left">
|
|
587
|
+
{#if item}
|
|
588
|
+
{@render item(opt)}
|
|
589
|
+
{:else}
|
|
590
|
+
<span class="truncate text-[13px] font-medium text-neutral-800 dark:text-neutral-100">
|
|
591
|
+
{renderLabelLine(opt)}
|
|
282
592
|
</span>
|
|
593
|
+
|
|
594
|
+
{#if opt.description}
|
|
595
|
+
<span class="truncate text-[11px] text-neutral-500 dark:text-neutral-400"
|
|
596
|
+
>{opt.description}</span
|
|
597
|
+
>
|
|
598
|
+
{/if}
|
|
599
|
+
|
|
600
|
+
{#if tree && search && showPathInSearch}
|
|
601
|
+
<span class="mt-0.5 truncate text-[11px] text-neutral-400">{getPathLabel(k)}</span>
|
|
602
|
+
{/if}
|
|
603
|
+
|
|
604
|
+
{#if tree && !search && optHasChildren}
|
|
605
|
+
<span class="mt-0.5 truncate text-[11px] text-neutral-400"
|
|
606
|
+
>{getChildren(opt).length} subcategorías</span
|
|
607
|
+
>
|
|
608
|
+
{/if}
|
|
283
609
|
{/if}
|
|
284
610
|
</div>
|
|
285
611
|
|
|
286
|
-
{#if
|
|
612
|
+
{#if selected}
|
|
287
613
|
<div
|
|
288
614
|
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"
|
|
289
615
|
>
|
|
@@ -291,6 +617,21 @@
|
|
|
291
617
|
<span>Seleccionado</span>
|
|
292
618
|
</div>
|
|
293
619
|
{/if}
|
|
620
|
+
|
|
621
|
+
{#if tree && optHasChildren && !search}
|
|
622
|
+
<button
|
|
623
|
+
type="button"
|
|
624
|
+
class="ml-2 inline-flex items-center gap-1 rounded-full px-2 py-[2px] text-[11px] text-neutral-500 hover:bg-neutral-200/70 dark:text-neutral-300 dark:hover:bg-neutral-800"
|
|
625
|
+
onclick={(e) => {
|
|
626
|
+
e.preventDefault();
|
|
627
|
+
e.stopPropagation();
|
|
628
|
+
enterItem(opt);
|
|
629
|
+
}}
|
|
630
|
+
>
|
|
631
|
+
<ChevronDown class="h-3.5 w-3.5 -rotate-90" />
|
|
632
|
+
Ver
|
|
633
|
+
</button>
|
|
634
|
+
{/if}
|
|
294
635
|
</button>
|
|
295
636
|
{/if}
|
|
296
637
|
{:else}
|
|
@@ -304,9 +645,7 @@
|
|
|
304
645
|
|
|
305
646
|
<DialogFooter class="flex items-center justify-between gap-2">
|
|
306
647
|
{#if footerMessage}
|
|
307
|
-
<div class="text-[11px] text-neutral-500 dark:text-neutral-400">
|
|
308
|
-
{footerMessage}
|
|
309
|
-
</div>
|
|
648
|
+
<div class="text-[11px] text-neutral-500 dark:text-neutral-400">{footerMessage}</div>
|
|
310
649
|
{/if}
|
|
311
650
|
|
|
312
651
|
<div class="ml-auto flex gap-2">
|
|
@@ -318,9 +657,7 @@
|
|
|
318
657
|
<Save class="h-4 w-4" />
|
|
319
658
|
{i18n.t('common.confirm')}
|
|
320
659
|
{#if multiple}
|
|
321
|
-
<span class="ml-1 text-[11px] tabular-nums">
|
|
322
|
-
({pending_selection.size})
|
|
323
|
-
</span>
|
|
660
|
+
<span class="ml-1 text-[11px] tabular-nums">({pending_selection.size})</span>
|
|
324
661
|
{/if}
|
|
325
662
|
</Button>
|
|
326
663
|
</div>
|
|
@@ -4,6 +4,7 @@ export interface Option {
|
|
|
4
4
|
label: string;
|
|
5
5
|
value: string;
|
|
6
6
|
description?: string;
|
|
7
|
+
children?: Option[];
|
|
7
8
|
[key: string]: any;
|
|
8
9
|
}
|
|
9
10
|
export interface Props {
|
|
@@ -11,8 +12,14 @@ export interface Props {
|
|
|
11
12
|
class?: ClassValue;
|
|
12
13
|
required?: boolean;
|
|
13
14
|
multiple?: boolean;
|
|
15
|
+
debug?: boolean;
|
|
14
16
|
onConfirm?: (selected: Option[]) => void;
|
|
15
17
|
onCancel?: () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Snippet opcional para personalizar el contenido visual del item.
|
|
20
|
+
* Importante: el componente seguirá controlando el click / navegación / selección,
|
|
21
|
+
* pero tú controlas “qué se pinta” dentro.
|
|
22
|
+
*/
|
|
16
23
|
item?: Snippet<[option: Option]>;
|
|
17
24
|
options: Option[];
|
|
18
25
|
value?: Option[];
|
|
@@ -20,4 +27,12 @@ export interface Props {
|
|
|
20
27
|
name: string;
|
|
21
28
|
placeholder: string;
|
|
22
29
|
errors?: string[];
|
|
30
|
+
/** Activa navegación tipo árbol si los items traen children (o la key indicada). */
|
|
31
|
+
tree?: boolean;
|
|
32
|
+
/** Si tu backend usa otra propiedad distinta a "children", cámbiala aquí. */
|
|
33
|
+
childrenKey?: string;
|
|
34
|
+
/** Si true, permite seleccionar padres además de navegar. */
|
|
35
|
+
selectParents?: boolean;
|
|
36
|
+
/** En modo búsqueda (plano), muestra la ruta padre › hijo debajo del item. */
|
|
37
|
+
showPathInSearch?: boolean;
|
|
23
38
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@r2digisolutions/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"packageManager": "bun@1.3.
|
|
5
|
+
"packageManager": "bun@1.3.5",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
@@ -55,39 +55,39 @@
|
|
|
55
55
|
"@storybook/addon-interactions": "8.6.14",
|
|
56
56
|
"@storybook/addon-svelte-csf": "5.0.10",
|
|
57
57
|
"@storybook/blocks": "8.6.14",
|
|
58
|
-
"@storybook/svelte": "10.1.
|
|
59
|
-
"@storybook/sveltekit": "10.1.
|
|
58
|
+
"@storybook/svelte": "10.1.10",
|
|
59
|
+
"@storybook/sveltekit": "10.1.10",
|
|
60
60
|
"@storybook/test": "8.6.14",
|
|
61
61
|
"@sveltejs/adapter-static": "3.0.10",
|
|
62
62
|
"@sveltejs/kit": "2.49.2",
|
|
63
63
|
"@sveltejs/package": "2.5.7",
|
|
64
64
|
"@sveltejs/vite-plugin-svelte": "6.2.1",
|
|
65
65
|
"@tailwindcss/postcss": "4.1.18",
|
|
66
|
-
"@testing-library/svelte": "5.
|
|
67
|
-
"@vitest/browser": "4.0.
|
|
66
|
+
"@testing-library/svelte": "5.3.1",
|
|
67
|
+
"@vitest/browser": "4.0.16",
|
|
68
68
|
"changeset": "0.2.6",
|
|
69
|
-
"eslint": "9.39.
|
|
69
|
+
"eslint": "9.39.2",
|
|
70
70
|
"eslint-config-prettier": "10.1.8",
|
|
71
71
|
"eslint-plugin-svelte": "3.13.1",
|
|
72
72
|
"globals": "16.5.0",
|
|
73
|
-
"jsdom": "27.
|
|
74
|
-
"lucide-svelte": "0.
|
|
73
|
+
"jsdom": "27.4.0",
|
|
74
|
+
"lucide-svelte": "0.562.0",
|
|
75
75
|
"prettier": "3.7.4",
|
|
76
|
-
"prettier-plugin-svelte": "3.4.
|
|
76
|
+
"prettier-plugin-svelte": "3.4.1",
|
|
77
77
|
"prettier-plugin-tailwindcss": "0.7.2",
|
|
78
78
|
"publint": "0.3.16",
|
|
79
|
-
"storybook": "10.1.
|
|
80
|
-
"svelte": "5.
|
|
81
|
-
"svelte-check": "4.3.
|
|
79
|
+
"storybook": "10.1.10",
|
|
80
|
+
"svelte": "5.46.1",
|
|
81
|
+
"svelte-check": "4.3.5",
|
|
82
82
|
"tailwindcss": "4.1.18",
|
|
83
83
|
"typescript": "5.9.3",
|
|
84
|
-
"typescript-eslint": "8.
|
|
85
|
-
"vite": "7.
|
|
86
|
-
"vitest": "4.0.
|
|
84
|
+
"typescript-eslint": "8.50.1",
|
|
85
|
+
"vite": "7.3.0",
|
|
86
|
+
"vitest": "4.0.16"
|
|
87
87
|
},
|
|
88
88
|
"dependencies": {
|
|
89
89
|
"@tailwindcss/container-queries": "0.1.1",
|
|
90
|
-
"@tailwindcss/forms": "0.5.
|
|
90
|
+
"@tailwindcss/forms": "0.5.11",
|
|
91
91
|
"@tailwindcss/typography": "0.5.19"
|
|
92
92
|
}
|
|
93
93
|
}
|