@r2digisolutions/ui 0.22.7 → 0.23.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/container/TabsStepper/TabsStepper.svelte +252 -0
- package/dist/components/container/TabsStepper/TabsStepper.svelte.d.ts +28 -0
- package/dist/components/container/TabsStepper/core/types.d.ts +9 -0
- package/dist/components/container/TabsStepper/core/types.js +1 -0
- package/dist/components/container/index.d.ts +3 -1
- package/dist/components/container/index.js +3 -1
- package/package.json +3 -3
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Button from '../../ui/Button/Button.svelte';
|
|
3
|
+
import type { Snippet } from 'svelte';
|
|
4
|
+
import type { StepDef } from './core/types.js';
|
|
5
|
+
|
|
6
|
+
type StepState = 'pending' | 'active' | 'done';
|
|
7
|
+
type Mode = 'create' | 'edit';
|
|
8
|
+
type ForwardTabBehavior = 'blocked' | 'validate' | 'free';
|
|
9
|
+
|
|
10
|
+
type FooterApi = {
|
|
11
|
+
current: number;
|
|
12
|
+
total: number;
|
|
13
|
+
next: () => Promise<void>;
|
|
14
|
+
prev: () => Promise<void>;
|
|
15
|
+
goTo: (idx: number) => Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type Props = {
|
|
19
|
+
steps: StepDef[];
|
|
20
|
+
current?: number;
|
|
21
|
+
onChange?: (idx: number) => void;
|
|
22
|
+
canGoNext?: (idx: number) => Promise<boolean> | boolean;
|
|
23
|
+
canGoPrev?: (idx: number) => Promise<boolean> | boolean;
|
|
24
|
+
|
|
25
|
+
mode?: Mode;
|
|
26
|
+
forwardTabBehavior?: ForwardTabBehavior; // NUEVO
|
|
27
|
+
stickyFooter?: boolean;
|
|
28
|
+
showHeader?: boolean;
|
|
29
|
+
showProgressBar?: boolean;
|
|
30
|
+
showConnectors?: boolean;
|
|
31
|
+
footer?: Snippet<[FooterApi]>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
let {
|
|
35
|
+
steps,
|
|
36
|
+
current = $bindable(1),
|
|
37
|
+
onChange,
|
|
38
|
+
canGoNext,
|
|
39
|
+
canGoPrev,
|
|
40
|
+
mode = 'create',
|
|
41
|
+
forwardTabBehavior = 'blocked',
|
|
42
|
+
stickyFooter = true,
|
|
43
|
+
showHeader = true,
|
|
44
|
+
showProgressBar = true,
|
|
45
|
+
showConnectors = true,
|
|
46
|
+
footer
|
|
47
|
+
}: Props = $props();
|
|
48
|
+
|
|
49
|
+
const total = $derived(steps.length);
|
|
50
|
+
const pct = $derived(Math.max(0, Math.min(100, (current / total) * 100)));
|
|
51
|
+
|
|
52
|
+
let maxReached = $state(current);
|
|
53
|
+
|
|
54
|
+
// Si avanzamos, actualizamos maxReached
|
|
55
|
+
$effect(() => {
|
|
56
|
+
if (current > maxReached) maxReached = current;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Ajustar si la cantidad de pasos cambia
|
|
60
|
+
$effect(() => {
|
|
61
|
+
if (maxReached > steps.length) maxReached = steps.length;
|
|
62
|
+
if (current > steps.length) current = steps.length;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
function stepState(i: number): StepState {
|
|
66
|
+
const idx = i + 1;
|
|
67
|
+
if (idx < current) return 'done';
|
|
68
|
+
if (idx === current) return 'active';
|
|
69
|
+
return 'pending';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function canJumpTo(idx: number) {
|
|
73
|
+
if (steps[idx - 1]?.disabled) return false;
|
|
74
|
+
if (mode === 'edit') return true;
|
|
75
|
+
|
|
76
|
+
if (idx <= maxReached) return true;
|
|
77
|
+
|
|
78
|
+
if (forwardTabBehavior === 'free') return true;
|
|
79
|
+
if (forwardTabBehavior === 'validate') return idx === current + 1;
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function goTo(idx: number) {
|
|
84
|
+
if (idx < 1 || idx > total) return;
|
|
85
|
+
|
|
86
|
+
if (!canJumpTo(idx)) return;
|
|
87
|
+
|
|
88
|
+
const goingForward = idx > current;
|
|
89
|
+
if (mode !== 'edit' && goingForward && forwardTabBehavior === 'validate') {
|
|
90
|
+
const ok = canGoNext ? await canGoNext(current) : true;
|
|
91
|
+
if (!ok) return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
current = idx;
|
|
95
|
+
if (current > maxReached) maxReached = current;
|
|
96
|
+
onChange?.(idx);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function next() {
|
|
100
|
+
if (current >= total) return;
|
|
101
|
+
|
|
102
|
+
if (canGoNext) {
|
|
103
|
+
const ok = await canGoNext(current);
|
|
104
|
+
if (!ok) return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const target = current + 1;
|
|
108
|
+
current = target;
|
|
109
|
+
if (current > maxReached) maxReached = current;
|
|
110
|
+
onChange?.(target);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function prev() {
|
|
114
|
+
if (current <= 1) return;
|
|
115
|
+
if (canGoPrev) {
|
|
116
|
+
const ok = await canGoPrev(current);
|
|
117
|
+
if (!ok) return;
|
|
118
|
+
}
|
|
119
|
+
await goTo(current - 1);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function onHeaderKeydown(e: KeyboardEvent) {
|
|
123
|
+
const { key } = e;
|
|
124
|
+
if (key !== 'ArrowLeft' && key !== 'ArrowRight') return;
|
|
125
|
+
e.preventDefault();
|
|
126
|
+
if (key === 'ArrowLeft') prev();
|
|
127
|
+
else next();
|
|
128
|
+
}
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<div class="w-full">
|
|
132
|
+
{#if showHeader}
|
|
133
|
+
<div class="mb-5">
|
|
134
|
+
<div
|
|
135
|
+
class="flex flex-wrap items-center justify-start gap-2"
|
|
136
|
+
role="tablist"
|
|
137
|
+
tabindex="0"
|
|
138
|
+
aria-label="Progreso del formulario"
|
|
139
|
+
onkeydown={onHeaderKeydown}
|
|
140
|
+
>
|
|
141
|
+
{#each steps as s, i (s.id)}
|
|
142
|
+
{@const state = stepState(i)}
|
|
143
|
+
{@const idx = i + 1}
|
|
144
|
+
{@const allowed = canJumpTo(idx)}
|
|
145
|
+
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
class={[
|
|
149
|
+
'group relative inline-flex items-center gap-3 rounded-xl border px-3 py-2 transition',
|
|
150
|
+
state === 'active'
|
|
151
|
+
? 'border-purple-600 bg-purple-50 text-purple-700 shadow-sm'
|
|
152
|
+
: state === 'done'
|
|
153
|
+
? 'border-emerald-500/50 bg-emerald-50 text-emerald-700'
|
|
154
|
+
: 'border-gray-300 bg-white text-gray-600 hover:bg-gray-50',
|
|
155
|
+
allowed ? 'cursor-pointer' : 'pointer-events-none cursor-not-allowed opacity-60'
|
|
156
|
+
].join(' ')}
|
|
157
|
+
aria-current={state === 'active' ? 'step' : undefined}
|
|
158
|
+
aria-disabled={allowed ? 'false' : 'true'}
|
|
159
|
+
tabindex={allowed ? 0 : -1}
|
|
160
|
+
title={allowed ? (s.tooltip ?? s.label) : 'Completa el paso anterior'}
|
|
161
|
+
onclick={() => allowed && goTo(idx)}
|
|
162
|
+
>
|
|
163
|
+
<div
|
|
164
|
+
class={[
|
|
165
|
+
'flex h-8 w-8 shrink-0 items-center justify-center rounded-full border text-sm font-semibold',
|
|
166
|
+
state === 'active'
|
|
167
|
+
? 'border-purple-600 bg-white text-purple-700'
|
|
168
|
+
: state === 'done'
|
|
169
|
+
? 'border-emerald-500 bg-white text-emerald-700'
|
|
170
|
+
: 'border-gray-300 bg-white text-gray-600'
|
|
171
|
+
].join(' ')}
|
|
172
|
+
>
|
|
173
|
+
{#if state === 'done'}
|
|
174
|
+
<svg viewBox="0 0 20 20" class="h-4 w-4" aria-hidden="true">
|
|
175
|
+
<path d="M7.5 11.5l-2-2 -1 1 3 3 7-7 -1-1 -6 6z" />
|
|
176
|
+
</svg>
|
|
177
|
+
{:else}
|
|
178
|
+
{idx}
|
|
179
|
+
{/if}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<!-- Texto -->
|
|
183
|
+
<div class="min-w-0 text-left">
|
|
184
|
+
<div class="truncate text-sm font-medium">{s.label}</div>
|
|
185
|
+
{#if state === 'active'}
|
|
186
|
+
<div class="truncate text-xs text-purple-700/70">En curso</div>
|
|
187
|
+
{:else if state === 'done'}
|
|
188
|
+
<div class="truncate text-xs text-emerald-700/70">Completado</div>
|
|
189
|
+
{:else}
|
|
190
|
+
<div class="truncate text-xs text-gray-500">Pendiente</div>
|
|
191
|
+
{/if}
|
|
192
|
+
</div>
|
|
193
|
+
</button>
|
|
194
|
+
|
|
195
|
+
{#if showConnectors && i < steps.length - 1}
|
|
196
|
+
<div class="h-px w-6 bg-gradient-to-r from-gray-300 to-gray-200 sm:w-8"></div>
|
|
197
|
+
{/if}
|
|
198
|
+
{/each}
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
{#if showProgressBar}
|
|
202
|
+
<div class="mt-3 h-1.5 w-full rounded-full bg-gray-200">
|
|
203
|
+
<div
|
|
204
|
+
class="h-1.5 rounded-full bg-gradient-to-r from-purple-600 to-blue-600 transition-all duration-300"
|
|
205
|
+
style:width={`${pct}%`}
|
|
206
|
+
></div>
|
|
207
|
+
</div>
|
|
208
|
+
{/if}
|
|
209
|
+
</div>
|
|
210
|
+
{/if}
|
|
211
|
+
|
|
212
|
+
<div class="relative">
|
|
213
|
+
{#each steps as s, i (s.id)}
|
|
214
|
+
<section
|
|
215
|
+
class="animate-[fadeIn_160ms_ease-out] data-[active=false]:hidden"
|
|
216
|
+
data-active={current === i + 1}
|
|
217
|
+
aria-hidden={current === i + 1 ? 'false' : 'true'}
|
|
218
|
+
inert={current === i + 1 ? undefined : true}
|
|
219
|
+
>
|
|
220
|
+
{@render s.content()}
|
|
221
|
+
</section>
|
|
222
|
+
{/each}
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
<div
|
|
226
|
+
class={stickyFooter
|
|
227
|
+
? 'sticky right-0 bottom-0 left-0 bg-gradient-to-t from-white via-white to-transparent pt-4'
|
|
228
|
+
: 'mt-6'}
|
|
229
|
+
>
|
|
230
|
+
<div class="flex w-full gap-3 border-t border-gray-200 pt-4">
|
|
231
|
+
{#if footer}
|
|
232
|
+
{@render footer({ current, total, next, prev, goTo })}
|
|
233
|
+
{:else}
|
|
234
|
+
<Button type="button" onclick={prev} disabled={current === 1}>Anterior</Button>
|
|
235
|
+
<Button type="button" onclick={next} disabled={current === total}>Siguiente</Button>
|
|
236
|
+
{/if}
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
</div>
|
|
240
|
+
|
|
241
|
+
<style>
|
|
242
|
+
@keyframes fadeIn {
|
|
243
|
+
from {
|
|
244
|
+
opacity: 0;
|
|
245
|
+
transform: translateY(2px);
|
|
246
|
+
}
|
|
247
|
+
to {
|
|
248
|
+
opacity: 1;
|
|
249
|
+
transform: translateY(0);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { StepDef } from './core/types.js';
|
|
3
|
+
type Mode = 'create' | 'edit';
|
|
4
|
+
type ForwardTabBehavior = 'blocked' | 'validate' | 'free';
|
|
5
|
+
type FooterApi = {
|
|
6
|
+
current: number;
|
|
7
|
+
total: number;
|
|
8
|
+
next: () => Promise<void>;
|
|
9
|
+
prev: () => Promise<void>;
|
|
10
|
+
goTo: (idx: number) => Promise<void>;
|
|
11
|
+
};
|
|
12
|
+
type Props = {
|
|
13
|
+
steps: StepDef[];
|
|
14
|
+
current?: number;
|
|
15
|
+
onChange?: (idx: number) => void;
|
|
16
|
+
canGoNext?: (idx: number) => Promise<boolean> | boolean;
|
|
17
|
+
canGoPrev?: (idx: number) => Promise<boolean> | boolean;
|
|
18
|
+
mode?: Mode;
|
|
19
|
+
forwardTabBehavior?: ForwardTabBehavior;
|
|
20
|
+
stickyFooter?: boolean;
|
|
21
|
+
showHeader?: boolean;
|
|
22
|
+
showProgressBar?: boolean;
|
|
23
|
+
showConnectors?: boolean;
|
|
24
|
+
footer?: Snippet<[FooterApi]>;
|
|
25
|
+
};
|
|
26
|
+
declare const TabsStepper: import("svelte").Component<Props, {}, "current">;
|
|
27
|
+
type TabsStepper = ReturnType<typeof TabsStepper>;
|
|
28
|
+
export default TabsStepper;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@r2digisolutions/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"packageManager": "bun@1.2.
|
|
5
|
+
"packageManager": "bun@1.2.23",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"svelte-check": "^4.3.2",
|
|
82
82
|
"tailwindcss": "^4.1.13",
|
|
83
83
|
"typescript": "^5.9.2",
|
|
84
|
-
"typescript-eslint": "^8.
|
|
84
|
+
"typescript-eslint": "^8.45.0",
|
|
85
85
|
"vite": "^7.1.7",
|
|
86
86
|
"vitest": "^3.2.4"
|
|
87
87
|
},
|