@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.
@@ -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,9 @@
1
+ import type { Snippet } from "svelte";
2
+ export type StepDef = {
3
+ id: string;
4
+ label: string;
5
+ icon?: any;
6
+ content: Snippet<[]>;
7
+ tooltip?: string;
8
+ disabled?: boolean;
9
+ };
@@ -1,3 +1,5 @@
1
1
  import DataTable from './DataTable/DataTable.svelte';
2
+ import TabsStepper from './TabsStepper/TabsStepper.svelte';
2
3
  export * from './DataTable/core/types.js';
3
- export { DataTable };
4
+ export * from './TabsStepper/core/types.js';
5
+ export { DataTable, TabsStepper };
@@ -1,3 +1,5 @@
1
1
  import DataTable from './DataTable/DataTable.svelte';
2
+ import TabsStepper from './TabsStepper/TabsStepper.svelte';
2
3
  export * from './DataTable/core/types.js';
3
- export { DataTable };
4
+ export * from './TabsStepper/core/types.js';
5
+ export { DataTable, TabsStepper };
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@r2digisolutions/ui",
3
- "version": "0.22.7",
3
+ "version": "0.23.0",
4
4
  "private": false,
5
- "packageManager": "bun@1.2.22",
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.44.1",
84
+ "typescript-eslint": "^8.45.0",
85
85
  "vite": "^7.1.7",
86
86
  "vitest": "^3.2.4"
87
87
  },