@r2digisolutions/ui 0.29.2 → 0.31.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.
- package/dist/components/index.d.ts +3 -1
- package/dist/components/index.js +3 -1
- package/dist/components/ui/MobileSheet/MobileSheet.svelte +317 -0
- package/dist/components/ui/MobileSheet/MobileSheet.svelte.d.ts +30 -0
- package/dist/components/ui/SelectTile/SelectTile.svelte +207 -0
- package/dist/components/ui/SelectTile/SelectTile.svelte.d.ts +20 -0
- package/package.json +10 -10
|
@@ -23,4 +23,6 @@ import NoContent from './ui/NoContent/NoContent.svelte';
|
|
|
23
23
|
import Tag from './ui/Tag/Tag.svelte';
|
|
24
24
|
export * from './ui/Dialog/index.js';
|
|
25
25
|
import Selector from './ui/Selector/Selector.svelte';
|
|
26
|
-
|
|
26
|
+
import SelectTile from './ui/SelectTile/SelectTile.svelte';
|
|
27
|
+
import MobileSheet from './ui/MobileSheet/MobileSheet.svelte';
|
|
28
|
+
export { MobileSheet, SelectTile, Selector, Tag, NoContent, Alert, Avatar, Button, Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, Container, Checkbox, Field, Section, Loading, TableList, Heading, Label, Input, InputRadio, Textarea, };
|
package/dist/components/index.js
CHANGED
|
@@ -23,4 +23,6 @@ import NoContent from './ui/NoContent/NoContent.svelte';
|
|
|
23
23
|
import Tag from './ui/Tag/Tag.svelte';
|
|
24
24
|
export * from './ui/Dialog/index.js';
|
|
25
25
|
import Selector from './ui/Selector/Selector.svelte';
|
|
26
|
-
|
|
26
|
+
import SelectTile from './ui/SelectTile/SelectTile.svelte';
|
|
27
|
+
import MobileSheet from './ui/MobileSheet/MobileSheet.svelte';
|
|
28
|
+
export { MobileSheet, SelectTile, Selector, Tag, NoContent, Alert, Avatar, Button, Badge, Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter, Container, Checkbox, Field, Section, Loading, TableList, Heading, Label, Input, InputRadio, Textarea, };
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { fade, slide } from 'svelte/transition';
|
|
4
|
+
import { cubicOut, cubicIn } from 'svelte/easing';
|
|
5
|
+
|
|
6
|
+
type SheetContext = { close: (reason?: string) => void };
|
|
7
|
+
|
|
8
|
+
type Props = {
|
|
9
|
+
open: boolean;
|
|
10
|
+
onOpenChange?: (open: boolean) => void;
|
|
11
|
+
|
|
12
|
+
header?: Snippet<[SheetContext]>;
|
|
13
|
+
content?: Snippet<[SheetContext]>;
|
|
14
|
+
footer?: Snippet<[SheetContext]>;
|
|
15
|
+
|
|
16
|
+
snapPoints?: number[];
|
|
17
|
+
initialSnap?: number;
|
|
18
|
+
|
|
19
|
+
closeOnOverlay?: boolean;
|
|
20
|
+
closeOnEscape?: boolean;
|
|
21
|
+
|
|
22
|
+
maxHeight?: string;
|
|
23
|
+
|
|
24
|
+
overlayClass?: string;
|
|
25
|
+
panelClass?: string;
|
|
26
|
+
|
|
27
|
+
/** Evita que el sheet se cierre arrastrando hacia abajo (sólo cambia de snap) */
|
|
28
|
+
disableDragClose?: boolean;
|
|
29
|
+
|
|
30
|
+
/** Hace scroll-lock del body mientras el sheet está abierto (por defecto true) */
|
|
31
|
+
lockScroll?: boolean;
|
|
32
|
+
|
|
33
|
+
/** Callback cuando cambia el snap */
|
|
34
|
+
onSnapChange?: (snap: { index: number; height: number }) => void;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const props: Props = $props();
|
|
38
|
+
|
|
39
|
+
// --- Snap inicial: índice del snapPoint más pequeño, o initialSnap si es válido ---
|
|
40
|
+
let snapIndex = 0;
|
|
41
|
+
if (props.snapPoints && props.snapPoints.length) {
|
|
42
|
+
let min = props.snapPoints[0];
|
|
43
|
+
let minIndex = 0;
|
|
44
|
+
for (let i = 1; i < props.snapPoints.length; i++) {
|
|
45
|
+
if (props.snapPoints[i] < min) {
|
|
46
|
+
min = props.snapPoints[i];
|
|
47
|
+
minIndex = i;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (
|
|
51
|
+
typeof props.initialSnap === 'number' &&
|
|
52
|
+
props.initialSnap >= 0 &&
|
|
53
|
+
props.initialSnap < props.snapPoints.length
|
|
54
|
+
) {
|
|
55
|
+
snapIndex = props.initialSnap;
|
|
56
|
+
} else {
|
|
57
|
+
snapIndex = minIndex;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- Estado drag / altura ---
|
|
62
|
+
|
|
63
|
+
let dragHeight = $state<number | null>(null);
|
|
64
|
+
let isDragging = $state(false);
|
|
65
|
+
let dragStartY = 0;
|
|
66
|
+
let dragStartHeight = 0;
|
|
67
|
+
let dragDelta = $state(0);
|
|
68
|
+
|
|
69
|
+
const snapPoints = $derived(props.snapPoints ?? [0.4, 0.9]); // por defecto compacto
|
|
70
|
+
const minSnap = $derived(Math.min(...snapPoints));
|
|
71
|
+
const maxSnap = $derived(Math.max(...snapPoints));
|
|
72
|
+
|
|
73
|
+
function baseHeight() {
|
|
74
|
+
return snapPoints[snapIndex] ?? snapPoints[0] ?? 0.4;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const currentHeight = $derived(dragHeight ?? baseHeight());
|
|
78
|
+
const handleStretch = $derived(Math.min(Math.abs(dragDelta) / 160, 0.45));
|
|
79
|
+
const handleOffset = $derived(Math.max(Math.min(dragDelta / 12, 10), -6));
|
|
80
|
+
|
|
81
|
+
let isOpening = $state(false);
|
|
82
|
+
|
|
83
|
+
// Animación elástica al abrir
|
|
84
|
+
$effect(() => {
|
|
85
|
+
if (!props.open || typeof window === 'undefined') {
|
|
86
|
+
isOpening = false;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
isOpening = true;
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
isOpening = false;
|
|
93
|
+
}, 260);
|
|
94
|
+
|
|
95
|
+
return () => clearTimeout(timer);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Scroll-lock del body mientras está abierto
|
|
99
|
+
$effect(() => {
|
|
100
|
+
if (typeof document === 'undefined') return;
|
|
101
|
+
|
|
102
|
+
if (props.open && props.lockScroll !== false) {
|
|
103
|
+
const prev = document.body.style.overflow;
|
|
104
|
+
document.body.style.overflow = 'hidden';
|
|
105
|
+
|
|
106
|
+
return () => {
|
|
107
|
+
document.body.style.overflow = prev;
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
function setOpen(next: boolean, _reason?: string) {
|
|
113
|
+
props.onOpenChange?.(next);
|
|
114
|
+
if (!next) {
|
|
115
|
+
dragHeight = null;
|
|
116
|
+
dragDelta = 0;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function close(reason?: string) {
|
|
121
|
+
if (!props.open) return;
|
|
122
|
+
setOpen(false, reason);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const sheetContext: SheetContext = {
|
|
126
|
+
close
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
function handleOverlayClick() {
|
|
130
|
+
if (props.closeOnOverlay === false) return;
|
|
131
|
+
close('overlay');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Escape global
|
|
135
|
+
$effect(() => {
|
|
136
|
+
if (!props.open || props.closeOnEscape === false || typeof window === 'undefined') return;
|
|
137
|
+
|
|
138
|
+
function onKey(e: KeyboardEvent) {
|
|
139
|
+
if (e.key === 'Escape') {
|
|
140
|
+
e.stopPropagation();
|
|
141
|
+
close('escape');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
window.addEventListener('keydown', onKey);
|
|
146
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
function startDrag(e: PointerEvent) {
|
|
150
|
+
if (!props.open || typeof window === 'undefined') return;
|
|
151
|
+
isDragging = true;
|
|
152
|
+
dragStartY = e.clientY;
|
|
153
|
+
dragStartHeight = baseHeight();
|
|
154
|
+
dragDelta = 0;
|
|
155
|
+
|
|
156
|
+
window.addEventListener('pointermove', handleWindowPointerMove);
|
|
157
|
+
window.addEventListener('pointerup', handleWindowPointerUp);
|
|
158
|
+
window.addEventListener('pointercancel', handleWindowPointerUp);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function onHandlePointerDown(e: PointerEvent) {
|
|
162
|
+
// Evita selección de texto y gesto accidental
|
|
163
|
+
e.preventDefault();
|
|
164
|
+
startDrag(e);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function handleWindowPointerMove(e: PointerEvent) {
|
|
168
|
+
if (!isDragging || !props.open) return;
|
|
169
|
+
|
|
170
|
+
const deltaY = e.clientY - dragStartY;
|
|
171
|
+
dragDelta = deltaY;
|
|
172
|
+
|
|
173
|
+
const vh = window.innerHeight || 1;
|
|
174
|
+
let next = dragStartHeight - deltaY / vh; // arriba => más alto
|
|
175
|
+
|
|
176
|
+
// “Esponja” debajo del mínimo
|
|
177
|
+
if (next < minSnap) {
|
|
178
|
+
const overshoot = minSnap - next;
|
|
179
|
+
next = minSnap - overshoot * 0.25;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// “Esponja” por encima del máximo
|
|
183
|
+
if (next > maxSnap) {
|
|
184
|
+
const overshoot = next - maxSnap;
|
|
185
|
+
next = maxSnap + overshoot * 0.25;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
dragHeight = next;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function handleWindowPointerUp(e: PointerEvent) {
|
|
192
|
+
if (!isDragging) return;
|
|
193
|
+
isDragging = false;
|
|
194
|
+
|
|
195
|
+
if (typeof window !== 'undefined') {
|
|
196
|
+
window.removeEventListener('pointermove', handleWindowPointerMove);
|
|
197
|
+
window.removeEventListener('pointerup', handleWindowPointerUp);
|
|
198
|
+
window.removeEventListener('pointercancel', handleWindowPointerUp);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const vh = typeof window !== 'undefined' ? window.innerHeight || 1 : 1;
|
|
202
|
+
const deltaY = e.clientY - dragStartY;
|
|
203
|
+
const final = dragHeight ?? baseHeight();
|
|
204
|
+
dragHeight = null;
|
|
205
|
+
dragDelta = 0;
|
|
206
|
+
|
|
207
|
+
const pulledDownHard = deltaY > vh * 0.18;
|
|
208
|
+
const allowDragClose = props.disableDragClose !== true;
|
|
209
|
+
|
|
210
|
+
if (allowDragClose && (pulledDownHard || final < minSnap * 0.7)) {
|
|
211
|
+
close('drag-down');
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Snap al punto más cercano
|
|
216
|
+
let targetIndex = 0;
|
|
217
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
218
|
+
|
|
219
|
+
snapPoints.forEach((p, i) => {
|
|
220
|
+
const d = Math.abs(p - final);
|
|
221
|
+
if (d < bestDist) {
|
|
222
|
+
bestDist = d;
|
|
223
|
+
targetIndex = i;
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
snapIndex = targetIndex;
|
|
228
|
+
props.onSnapChange?.({ index: targetIndex, height: snapPoints[targetIndex] });
|
|
229
|
+
}
|
|
230
|
+
</script>
|
|
231
|
+
|
|
232
|
+
{#if props.open}
|
|
233
|
+
<div class="fixed inset-0 z-99 flex flex-col md:hidden">
|
|
234
|
+
<!-- Overlay -->
|
|
235
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
236
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
237
|
+
<div
|
|
238
|
+
in:fade={{ duration: 150 }}
|
|
239
|
+
out:fade={{ duration: 120 }}
|
|
240
|
+
class={['flex-1 bg-neutral-950/70 backdrop-blur-sm', props.overlayClass ?? '']}
|
|
241
|
+
onclick={handleOverlayClick}
|
|
242
|
+
></div>
|
|
243
|
+
|
|
244
|
+
<!-- Panel -->
|
|
245
|
+
<div
|
|
246
|
+
role="dialog"
|
|
247
|
+
aria-modal="true"
|
|
248
|
+
in:slide={{ duration: 220, easing: cubicOut }}
|
|
249
|
+
out:slide={{ duration: 190, easing: cubicIn }}
|
|
250
|
+
class={[
|
|
251
|
+
'relative w-full overflow-hidden rounded-t-3xl border-t border-neutral-800/70 bg-neutral-950/95 text-neutral-50 shadow-[0_-18px_45px_rgba(0,0,0,0.7)]',
|
|
252
|
+
props.panelClass ?? ''
|
|
253
|
+
]}
|
|
254
|
+
style={`max-height:${props.maxHeight ?? '80vh'};height:${currentHeight * 100}vh;`}
|
|
255
|
+
>
|
|
256
|
+
<div class={['flex h-full flex-col', isOpening ? 'sheet-inner-opening' : '']}>
|
|
257
|
+
<!-- Handle drag -->
|
|
258
|
+
<div
|
|
259
|
+
class="flex items-center justify-center pt-2 pb-1 select-none"
|
|
260
|
+
style="touch-action:none;"
|
|
261
|
+
onpointerdown={onHandlePointerDown}
|
|
262
|
+
>
|
|
263
|
+
<div
|
|
264
|
+
class="h-1.5 w-10 rounded-full bg-neutral-700/80 transition-transform duration-150"
|
|
265
|
+
style={`transform: translateY(${handleOffset}px) scaleX(${
|
|
266
|
+
1 + handleStretch
|
|
267
|
+
}); opacity:${0.9 - handleStretch * 0.3};`}
|
|
268
|
+
></div>
|
|
269
|
+
</div>
|
|
270
|
+
|
|
271
|
+
<!-- Header opcional -->
|
|
272
|
+
{#if props.header}
|
|
273
|
+
<div class="px-4 pb-2">
|
|
274
|
+
{@render props.header(sheetContext)}
|
|
275
|
+
</div>
|
|
276
|
+
{/if}
|
|
277
|
+
|
|
278
|
+
<!-- Body -->
|
|
279
|
+
<div class="flex min-h-0 flex-1 flex-col overflow-hidden px-3 pb-3">
|
|
280
|
+
{#if props.content}
|
|
281
|
+
<div class="min-h-0 flex-1 overflow-y-auto">
|
|
282
|
+
{@render props.content(sheetContext)}
|
|
283
|
+
</div>
|
|
284
|
+
{/if}
|
|
285
|
+
</div>
|
|
286
|
+
|
|
287
|
+
<!-- Footer siempre abajo, con pequeño safe-area -->
|
|
288
|
+
{#if props.footer}
|
|
289
|
+
<div
|
|
290
|
+
class="border-t border-neutral-800/70 px-3 py-3 pb-[max(0.75rem,env(safe-area-inset-bottom,0px))]"
|
|
291
|
+
>
|
|
292
|
+
{@render props.footer(sheetContext)}
|
|
293
|
+
</div>
|
|
294
|
+
{/if}
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
{/if}
|
|
299
|
+
|
|
300
|
+
<style>
|
|
301
|
+
@keyframes sheet-inner-elastic-in {
|
|
302
|
+
0% {
|
|
303
|
+
transform: scaleY(0.9);
|
|
304
|
+
}
|
|
305
|
+
60% {
|
|
306
|
+
transform: scaleY(1.03);
|
|
307
|
+
}
|
|
308
|
+
100% {
|
|
309
|
+
transform: scaleY(1);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.sheet-inner-opening {
|
|
314
|
+
transform-origin: bottom;
|
|
315
|
+
animation: sheet-inner-elastic-in 260ms cubic-bezier(0.22, 1, 0.36, 1);
|
|
316
|
+
}
|
|
317
|
+
</style>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
type SheetContext = {
|
|
3
|
+
close: (reason?: string) => void;
|
|
4
|
+
};
|
|
5
|
+
type Props = {
|
|
6
|
+
open: boolean;
|
|
7
|
+
onOpenChange?: (open: boolean) => void;
|
|
8
|
+
header?: Snippet<[SheetContext]>;
|
|
9
|
+
content?: Snippet<[SheetContext]>;
|
|
10
|
+
footer?: Snippet<[SheetContext]>;
|
|
11
|
+
snapPoints?: number[];
|
|
12
|
+
initialSnap?: number;
|
|
13
|
+
closeOnOverlay?: boolean;
|
|
14
|
+
closeOnEscape?: boolean;
|
|
15
|
+
maxHeight?: string;
|
|
16
|
+
overlayClass?: string;
|
|
17
|
+
panelClass?: string;
|
|
18
|
+
/** Evita que el sheet se cierre arrastrando hacia abajo (sólo cambia de snap) */
|
|
19
|
+
disableDragClose?: boolean;
|
|
20
|
+
/** Hace scroll-lock del body mientras el sheet está abierto (por defecto true) */
|
|
21
|
+
lockScroll?: boolean;
|
|
22
|
+
/** Callback cuando cambia el snap */
|
|
23
|
+
onSnapChange?: (snap: {
|
|
24
|
+
index: number;
|
|
25
|
+
height: number;
|
|
26
|
+
}) => void;
|
|
27
|
+
};
|
|
28
|
+
declare const MobileSheet: import("svelte").Component<Props, {}, "">;
|
|
29
|
+
type MobileSheet = ReturnType<typeof MobileSheet>;
|
|
30
|
+
export default MobileSheet;
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { i18n } from '../../../settings/index.js';
|
|
3
|
+
import { Check, Plus, ChevronRight } from 'lucide-svelte';
|
|
4
|
+
import type { Snippet } from 'svelte';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
label: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
|
|
10
|
+
selected?: boolean;
|
|
11
|
+
hasChildren?: boolean;
|
|
12
|
+
|
|
13
|
+
size?: 'sm' | 'md';
|
|
14
|
+
accent?: 'purple' | 'indigo' | 'sky' | 'neutral';
|
|
15
|
+
|
|
16
|
+
showLeadingCircle?: boolean;
|
|
17
|
+
showSelectedBadge?: boolean;
|
|
18
|
+
showChevronForChildren?: boolean;
|
|
19
|
+
|
|
20
|
+
compact?: boolean;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
|
|
23
|
+
onclick?: () => void;
|
|
24
|
+
|
|
25
|
+
meta?: Snippet;
|
|
26
|
+
right?: Snippet;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const {
|
|
30
|
+
label,
|
|
31
|
+
description = '',
|
|
32
|
+
selected = false,
|
|
33
|
+
hasChildren = false,
|
|
34
|
+
size = 'md',
|
|
35
|
+
accent = 'purple',
|
|
36
|
+
|
|
37
|
+
showLeadingCircle = true,
|
|
38
|
+
showSelectedBadge = true,
|
|
39
|
+
showChevronForChildren = true,
|
|
40
|
+
|
|
41
|
+
compact = false,
|
|
42
|
+
disabled = false,
|
|
43
|
+
|
|
44
|
+
onclick,
|
|
45
|
+
meta,
|
|
46
|
+
right
|
|
47
|
+
}: Props = $props();
|
|
48
|
+
|
|
49
|
+
const sizeClasses = $derived(
|
|
50
|
+
{
|
|
51
|
+
sm: 'px-2.5 py-1.5 text-[13px]',
|
|
52
|
+
md: 'px-3 py-2 text-sm'
|
|
53
|
+
}[size]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const leadingSizeClasses = $derived(
|
|
57
|
+
{
|
|
58
|
+
sm: 'h-6 w-6',
|
|
59
|
+
md: 'h-7 w-7'
|
|
60
|
+
}[size]
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const labelTextSize = $derived(compact ? 'text-[13px]' : 'text-[13px]');
|
|
64
|
+
const descriptionTextSize = $derived(compact ? 'text-[11px]' : 'text-[11px]');
|
|
65
|
+
|
|
66
|
+
const accentMap = {
|
|
67
|
+
purple: {
|
|
68
|
+
selectedBorder: 'border-purple-400/70 dark:border-purple-500/60',
|
|
69
|
+
selectedBg: 'bg-purple-50 dark:bg-purple-950/40',
|
|
70
|
+
selectedText: 'text-neutral-900 dark:text-neutral-50',
|
|
71
|
+
|
|
72
|
+
badgeBg: 'bg-white/85 dark:bg-purple-200/10',
|
|
73
|
+
badgeText: 'text-purple-700 dark:text-purple-100',
|
|
74
|
+
|
|
75
|
+
leadingSelectedBg: 'bg-purple-500/90 dark:bg-purple-500',
|
|
76
|
+
leadingSelectedBorder: 'border-purple-400/70 dark:border-purple-400/80'
|
|
77
|
+
},
|
|
78
|
+
indigo: {
|
|
79
|
+
selectedBorder: 'border-indigo-400/70 dark:border-indigo-500/60',
|
|
80
|
+
selectedBg: 'bg-indigo-50 dark:bg-indigo-950/40',
|
|
81
|
+
selectedText: 'text-neutral-900 dark:text-neutral-50',
|
|
82
|
+
|
|
83
|
+
badgeBg: 'bg-white/85 dark:bg-indigo-200/10',
|
|
84
|
+
badgeText: 'text-indigo-700 dark:text-indigo-100',
|
|
85
|
+
|
|
86
|
+
leadingSelectedBg: 'bg-indigo-500/90 dark:bg-indigo-500',
|
|
87
|
+
leadingSelectedBorder: 'border-indigo-400/70 dark:border-indigo-400/80'
|
|
88
|
+
},
|
|
89
|
+
sky: {
|
|
90
|
+
selectedBorder: 'border-sky-400/70 dark:border-sky-500/60',
|
|
91
|
+
selectedBg: 'bg-sky-50 dark:bg-sky-950/40',
|
|
92
|
+
selectedText: 'text-neutral-900 dark:text-neutral-50',
|
|
93
|
+
|
|
94
|
+
badgeBg: 'bg-white/85 dark:bg-sky-200/10',
|
|
95
|
+
badgeText: 'text-sky-700 dark:text-sky-100',
|
|
96
|
+
|
|
97
|
+
leadingSelectedBg: 'bg-sky-500/90 dark:bg-sky-500',
|
|
98
|
+
leadingSelectedBorder: 'border-sky-400/70 dark:border-sky-400/80'
|
|
99
|
+
},
|
|
100
|
+
neutral: {
|
|
101
|
+
selectedBorder: 'border-neutral-400/70 dark:border-neutral-500/60',
|
|
102
|
+
selectedBg: 'bg-neutral-100 dark:bg-neutral-900',
|
|
103
|
+
selectedText: 'text-neutral-900 dark:text-neutral-50',
|
|
104
|
+
|
|
105
|
+
badgeBg: 'bg-white/85 dark:bg-neutral-800',
|
|
106
|
+
badgeText: 'text-neutral-800 dark:text-neutral-100',
|
|
107
|
+
|
|
108
|
+
leadingSelectedBg: 'bg-neutral-700 dark:bg-neutral-700',
|
|
109
|
+
leadingSelectedBorder: 'border-neutral-500/70 dark:border-neutral-500/80'
|
|
110
|
+
}
|
|
111
|
+
} as const;
|
|
112
|
+
|
|
113
|
+
const accentClasses = $derived(accentMap[accent]);
|
|
114
|
+
</script>
|
|
115
|
+
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
{disabled}
|
|
119
|
+
onclick={() => onclick?.()}
|
|
120
|
+
class={[
|
|
121
|
+
'flex w-full items-center justify-between gap-3 rounded-lg border text-left transition-colors',
|
|
122
|
+
sizeClasses,
|
|
123
|
+
'focus-visible:ring-2 focus-visible:ring-purple-500/40 focus-visible:outline-none',
|
|
124
|
+
!disabled && 'cursor-pointer',
|
|
125
|
+
'disabled:cursor-not-allowed disabled:opacity-60',
|
|
126
|
+
|
|
127
|
+
selected
|
|
128
|
+
? [
|
|
129
|
+
accentClasses.selectedBorder,
|
|
130
|
+
accentClasses.selectedBg,
|
|
131
|
+
accentClasses.selectedText,
|
|
132
|
+
'shadow-sm',
|
|
133
|
+
'hover:brightness-[0.98] dark:hover:brightness-[1.05]'
|
|
134
|
+
]
|
|
135
|
+
: [
|
|
136
|
+
'border-neutral-200 bg-neutral-50 text-neutral-800',
|
|
137
|
+
'hover:border-neutral-300 hover:bg-neutral-100',
|
|
138
|
+
'dark:border-neutral-700 dark:bg-neutral-900 dark:text-neutral-100',
|
|
139
|
+
'dark:hover:border-neutral-600 dark:hover:bg-neutral-800'
|
|
140
|
+
]
|
|
141
|
+
]}
|
|
142
|
+
>
|
|
143
|
+
<div class="flex min-w-0 flex-1 items-center gap-3">
|
|
144
|
+
{#if showLeadingCircle}
|
|
145
|
+
<div
|
|
146
|
+
class={[
|
|
147
|
+
'flex shrink-0 items-center justify-center rounded-full border text-xs font-medium shadow-sm',
|
|
148
|
+
leadingSizeClasses,
|
|
149
|
+
selected
|
|
150
|
+
? [accentClasses.leadingSelectedBg, accentClasses.leadingSelectedBorder, 'text-white']
|
|
151
|
+
: 'border-neutral-300 bg-white text-neutral-500 dark:border-neutral-600 dark:bg-neutral-900 dark:text-neutral-300'
|
|
152
|
+
]}
|
|
153
|
+
>
|
|
154
|
+
{#if selected}
|
|
155
|
+
<Check class="h-3.5 w-3.5" />
|
|
156
|
+
{:else}
|
|
157
|
+
<Plus class="h-3.5 w-3.5" />
|
|
158
|
+
{/if}
|
|
159
|
+
</div>
|
|
160
|
+
{/if}
|
|
161
|
+
|
|
162
|
+
<div class="flex min-w-0 flex-col">
|
|
163
|
+
<span class={`truncate font-medium ${labelTextSize}`}>
|
|
164
|
+
{label}
|
|
165
|
+
</span>
|
|
166
|
+
|
|
167
|
+
{#if description}
|
|
168
|
+
<span
|
|
169
|
+
class={`mt-0.5 truncate ${descriptionTextSize} ${
|
|
170
|
+
selected
|
|
171
|
+
? 'text-neutral-600 dark:text-neutral-300'
|
|
172
|
+
: 'text-neutral-500 dark:text-neutral-400'
|
|
173
|
+
}`}
|
|
174
|
+
>
|
|
175
|
+
{description}
|
|
176
|
+
</span>
|
|
177
|
+
{/if}
|
|
178
|
+
|
|
179
|
+
{@render meta?.()}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div class="flex shrink-0 items-center gap-2">
|
|
184
|
+
{#if selected && showSelectedBadge}
|
|
185
|
+
<span
|
|
186
|
+
class={[
|
|
187
|
+
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-[11px] font-medium shadow-sm backdrop-blur',
|
|
188
|
+
accentClasses.badgeBg,
|
|
189
|
+
accentClasses.badgeText
|
|
190
|
+
]}
|
|
191
|
+
>
|
|
192
|
+
<Check class="h-3 w-3" />
|
|
193
|
+
<span>{i18n.t('common.selected')}</span>
|
|
194
|
+
</span>
|
|
195
|
+
{/if}
|
|
196
|
+
|
|
197
|
+
{@render right?.()}
|
|
198
|
+
|
|
199
|
+
{#if hasChildren && showChevronForChildren}
|
|
200
|
+
<div
|
|
201
|
+
class="flex h-7 w-7 items-center justify-center rounded-full bg-neutral-900/5 text-neutral-400 dark:bg-neutral-50/5 dark:text-neutral-500"
|
|
202
|
+
>
|
|
203
|
+
<ChevronRight class="h-4 w-4" />
|
|
204
|
+
</div>
|
|
205
|
+
{/if}
|
|
206
|
+
</div>
|
|
207
|
+
</button>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
interface Props {
|
|
3
|
+
label: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
selected?: boolean;
|
|
6
|
+
hasChildren?: boolean;
|
|
7
|
+
size?: 'sm' | 'md';
|
|
8
|
+
accent?: 'purple' | 'indigo' | 'sky' | 'neutral';
|
|
9
|
+
showLeadingCircle?: boolean;
|
|
10
|
+
showSelectedBadge?: boolean;
|
|
11
|
+
showChevronForChildren?: boolean;
|
|
12
|
+
compact?: boolean;
|
|
13
|
+
disabled?: boolean;
|
|
14
|
+
onclick?: () => void;
|
|
15
|
+
meta?: Snippet;
|
|
16
|
+
right?: Snippet;
|
|
17
|
+
}
|
|
18
|
+
declare const SelectTile: import("svelte").Component<Props, {}, "">;
|
|
19
|
+
type SelectTile = ReturnType<typeof SelectTile>;
|
|
20
|
+
export default SelectTile;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@r2digisolutions/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.31.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"packageManager": "bun@1.3.4",
|
|
6
6
|
"publishConfig": {
|
|
@@ -55,11 +55,11 @@
|
|
|
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.6",
|
|
59
|
+
"@storybook/sveltekit": "10.1.6",
|
|
60
60
|
"@storybook/test": "8.6.14",
|
|
61
61
|
"@sveltejs/adapter-static": "3.0.10",
|
|
62
|
-
"@sveltejs/kit": "2.49.
|
|
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.17",
|
|
@@ -70,18 +70,18 @@
|
|
|
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.3.0",
|
|
74
|
+
"lucide-svelte": "0.560.0",
|
|
75
75
|
"prettier": "3.7.4",
|
|
76
76
|
"prettier-plugin-svelte": "3.4.0",
|
|
77
77
|
"prettier-plugin-tailwindcss": "0.7.2",
|
|
78
|
-
"publint": "0.3.
|
|
79
|
-
"storybook": "10.1.
|
|
80
|
-
"svelte": "5.45.
|
|
78
|
+
"publint": "0.3.16",
|
|
79
|
+
"storybook": "10.1.6",
|
|
80
|
+
"svelte": "5.45.8",
|
|
81
81
|
"svelte-check": "4.3.4",
|
|
82
82
|
"tailwindcss": "4.1.17",
|
|
83
83
|
"typescript": "5.9.3",
|
|
84
|
-
"typescript-eslint": "8.
|
|
84
|
+
"typescript-eslint": "8.49.0",
|
|
85
85
|
"vite": "7.2.7",
|
|
86
86
|
"vitest": "4.0.15"
|
|
87
87
|
},
|