trackops 1.0.1 → 1.1.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/README.md +326 -270
- package/bin/trackops.js +102 -70
- package/lib/config.js +260 -35
- package/lib/control.js +517 -475
- package/lib/env.js +227 -0
- package/lib/i18n.js +61 -53
- package/lib/init.js +135 -46
- package/lib/locale.js +63 -0
- package/lib/opera-bootstrap.js +523 -0
- package/lib/opera.js +319 -170
- package/lib/registry.js +27 -13
- package/lib/release.js +56 -0
- package/lib/resources.js +42 -0
- package/lib/server.js +907 -554
- package/lib/skills.js +148 -124
- package/lib/workspace.js +260 -0
- package/locales/en.json +331 -139
- package/locales/es.json +331 -139
- package/package.json +7 -9
- package/scripts/skills-marketplace-smoke.js +124 -0
- package/scripts/smoke-tests.js +445 -0
- package/scripts/sync-skill-version.js +21 -0
- package/scripts/validate-skill.js +88 -0
- package/skills/trackops/SKILL.md +64 -0
- package/skills/trackops/agents/openai.yaml +3 -0
- package/skills/trackops/references/activation.md +39 -0
- package/skills/trackops/references/troubleshooting.md +34 -0
- package/skills/trackops/references/workflow.md +20 -0
- package/skills/trackops/scripts/bootstrap-trackops.js +201 -0
- package/skills/trackops/skill.json +29 -0
- package/templates/opera/en/agent.md +26 -0
- package/templates/opera/en/genesis.md +79 -0
- package/templates/opera/en/references/autonomy-and-recovery.md +23 -0
- package/templates/opera/en/references/opera-cycle.md +62 -0
- package/templates/opera/en/registry.md +28 -0
- package/templates/opera/en/router.md +39 -0
- package/templates/opera/genesis.md +79 -94
- package/templates/skills/changelog-updater/locales/en/SKILL.md +11 -0
- package/templates/skills/commiter/locales/en/SKILL.md +11 -0
- package/templates/skills/project-starter-skill/locales/en/SKILL.md +24 -0
- package/ui/css/panels.css +956 -953
- package/ui/index.html +1 -1
- package/ui/js/api.js +211 -194
- package/ui/js/app.js +200 -199
- package/ui/js/i18n.js +14 -0
- package/ui/js/onboarding.js +439 -437
- package/ui/js/state.js +130 -129
- package/ui/js/utils.js +175 -172
- package/ui/js/views/board.js +255 -254
- package/ui/js/views/execution.js +256 -256
- package/ui/js/views/insights.js +340 -339
- package/ui/js/views/overview.js +365 -364
- package/ui/js/views/settings.js +340 -202
- package/ui/js/views/sidebar.js +131 -132
- package/ui/js/views/skills.js +163 -162
- package/ui/js/views/tasks.js +406 -405
- package/ui/js/views/topbar.js +239 -183
package/ui/js/onboarding.js
CHANGED
|
@@ -1,437 +1,439 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* onboarding.js — Tour interactivo tipo spotlight
|
|
3
|
-
*
|
|
4
|
-
* Técnica: un div con box-shadow gigante rodea el target,
|
|
5
|
-
* dejando solo ese elemento visible. El tooltip-bocadillo
|
|
6
|
-
* tiene una flecha CSS que apunta directamente al target.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import * as state from './state.js';
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
let
|
|
147
|
-
let
|
|
148
|
-
let
|
|
149
|
-
let
|
|
150
|
-
let
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
_escHandler
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
document.getElementById('ob-
|
|
214
|
-
document.getElementById('ob-
|
|
215
|
-
document.getElementById('ob-
|
|
216
|
-
document.getElementById('ob-prev').
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
_ring.
|
|
299
|
-
_ring.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
const
|
|
340
|
-
const
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
tooltip
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (btn
|
|
404
|
-
if (btn.id === 'ob-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
1
|
+
/**
|
|
2
|
+
* onboarding.js — Tour interactivo tipo spotlight
|
|
3
|
+
*
|
|
4
|
+
* Técnica: un div con box-shadow gigante rodea el target,
|
|
5
|
+
* dejando solo ese elemento visible. El tooltip-bocadillo
|
|
6
|
+
* tiene una flecha CSS que apunta directamente al target.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as state from './state.js';
|
|
10
|
+
import { t } from './i18n.js';
|
|
11
|
+
|
|
12
|
+
const STORAGE_KEY = 'trackops-onboarding-v2';
|
|
13
|
+
|
|
14
|
+
/* ── PASOS ──────────────────────────────────────────────────────────────── */
|
|
15
|
+
|
|
16
|
+
const STEPS = [
|
|
17
|
+
// 0 — Bienvenida (sin target)
|
|
18
|
+
{
|
|
19
|
+
titleKey: 'ui.onboarding.welcome.title',
|
|
20
|
+
descKey: 'ui.onboarding.welcome.desc',
|
|
21
|
+
target: null,
|
|
22
|
+
view: null,
|
|
23
|
+
pos: 'center',
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
// 1 — Sidebar / navegación
|
|
27
|
+
{
|
|
28
|
+
titleKey: 'ui.onboarding.nav.title',
|
|
29
|
+
descKey: 'ui.onboarding.nav.desc',
|
|
30
|
+
target: '#sidebar',
|
|
31
|
+
view: 'overview',
|
|
32
|
+
pos: 'right',
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// 2 — KPI cards
|
|
36
|
+
{
|
|
37
|
+
titleKey: 'ui.onboarding.kpi.title',
|
|
38
|
+
descKey: 'ui.onboarding.kpi.desc',
|
|
39
|
+
target: '.kpi-grid',
|
|
40
|
+
view: 'overview',
|
|
41
|
+
pos: 'bottom',
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
// 3 — Gráfico de actividad
|
|
45
|
+
{
|
|
46
|
+
titleKey: 'ui.onboarding.activity.title',
|
|
47
|
+
descKey: 'ui.onboarding.activity.desc',
|
|
48
|
+
target: '.chart-card',
|
|
49
|
+
view: 'overview',
|
|
50
|
+
pos: 'bottom',
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
// 4 — Donut de progreso
|
|
54
|
+
{
|
|
55
|
+
titleKey: 'ui.onboarding.progress.title',
|
|
56
|
+
descKey: 'ui.onboarding.progress.desc',
|
|
57
|
+
target: '.donut-wrapper',
|
|
58
|
+
view: 'overview',
|
|
59
|
+
pos: 'left',
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
// 5 — Time Tracker
|
|
63
|
+
{
|
|
64
|
+
titleKey: 'ui.onboarding.time.title',
|
|
65
|
+
descKey: 'ui.onboarding.time.desc',
|
|
66
|
+
target: '.time-tracker-card',
|
|
67
|
+
view: 'overview',
|
|
68
|
+
pos: 'top',
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// 6 — Topbar: búsqueda
|
|
72
|
+
{
|
|
73
|
+
titleKey: 'ui.onboarding.search.title',
|
|
74
|
+
descKey: 'ui.onboarding.search.desc',
|
|
75
|
+
target: '.topbar-search',
|
|
76
|
+
view: 'overview',
|
|
77
|
+
pos: 'bottom',
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
// 7 — Board (Kanban)
|
|
81
|
+
{
|
|
82
|
+
titleKey: 'ui.onboarding.board.title',
|
|
83
|
+
descKey: 'ui.onboarding.board.desc',
|
|
84
|
+
target: '.board-grid',
|
|
85
|
+
view: 'board',
|
|
86
|
+
pos: 'top',
|
|
87
|
+
},
|
|
88
|
+
|
|
89
|
+
// 8 — Editor de tareas
|
|
90
|
+
{
|
|
91
|
+
titleKey: 'ui.onboarding.tasks.title',
|
|
92
|
+
descKey: 'ui.onboarding.tasks.desc',
|
|
93
|
+
target: '.task-list',
|
|
94
|
+
view: 'tasks',
|
|
95
|
+
pos: 'right',
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
// 9 — Ejecución / Consola
|
|
99
|
+
{
|
|
100
|
+
titleKey: 'ui.onboarding.execution.title',
|
|
101
|
+
descKey: 'ui.onboarding.execution.desc',
|
|
102
|
+
target: '.terminal-surface',
|
|
103
|
+
view: 'execution',
|
|
104
|
+
pos: 'top',
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// 10 — Analytics
|
|
108
|
+
{
|
|
109
|
+
titleKey: 'ui.onboarding.insights.title',
|
|
110
|
+
descKey: 'ui.onboarding.insights.desc',
|
|
111
|
+
target: '.health-grid',
|
|
112
|
+
view: 'insights',
|
|
113
|
+
pos: 'bottom',
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
// 11 — AI Skill Hub
|
|
117
|
+
{
|
|
118
|
+
titleKey: 'ui.onboarding.skills.title',
|
|
119
|
+
descKey: 'ui.onboarding.skills.desc',
|
|
120
|
+
target: '#view-skills',
|
|
121
|
+
view: 'skills',
|
|
122
|
+
pos: 'right',
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
// 12 — Theme toggle
|
|
126
|
+
{
|
|
127
|
+
titleKey: 'ui.onboarding.theme.title',
|
|
128
|
+
descKey: 'ui.onboarding.theme.desc',
|
|
129
|
+
target: '#theme-toggle-btn',
|
|
130
|
+
view: 'overview',
|
|
131
|
+
pos: 'bottom',
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
// 13 — Fin
|
|
135
|
+
{
|
|
136
|
+
titleKey: 'ui.onboarding.done.title',
|
|
137
|
+
descKey: 'ui.onboarding.done.desc',
|
|
138
|
+
target: null,
|
|
139
|
+
view: null,
|
|
140
|
+
pos: 'center',
|
|
141
|
+
},
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
/* ── ESTADO ──────────────────────────────────────────────────────────────── */
|
|
145
|
+
|
|
146
|
+
let _step = 0;
|
|
147
|
+
let _spotlight = null;
|
|
148
|
+
let _tooltip = null;
|
|
149
|
+
let _active = false;
|
|
150
|
+
let _ring = null;
|
|
151
|
+
let _escHandler = null;
|
|
152
|
+
|
|
153
|
+
/* ── API PÚBLICA ─────────────────────────────────────────────────────────── */
|
|
154
|
+
|
|
155
|
+
export function init() {
|
|
156
|
+
_spotlight = document.getElementById('onboarding-spotlight');
|
|
157
|
+
_tooltip = document.getElementById('onboarding-tooltip');
|
|
158
|
+
if (!_spotlight || !_tooltip) return;
|
|
159
|
+
|
|
160
|
+
_bindStaticEvents();
|
|
161
|
+
|
|
162
|
+
if (!localStorage.getItem(STORAGE_KEY)) {
|
|
163
|
+
setTimeout(show, 900);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function show() {
|
|
168
|
+
_step = 0;
|
|
169
|
+
_active = true;
|
|
170
|
+
_spotlight.classList.remove('is-hidden');
|
|
171
|
+
_tooltip.classList.remove('is-hidden');
|
|
172
|
+
_renderStep();
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function finish() {
|
|
176
|
+
_active = false;
|
|
177
|
+
localStorage.setItem(STORAGE_KEY, '1');
|
|
178
|
+
_spotlight.classList.add('is-hidden');
|
|
179
|
+
_tooltip.classList.add('is-hidden');
|
|
180
|
+
_clearRing();
|
|
181
|
+
_clearSpotlight();
|
|
182
|
+
if (_escHandler) {
|
|
183
|
+
document.removeEventListener('keydown', _escHandler);
|
|
184
|
+
_escHandler = null;
|
|
185
|
+
}
|
|
186
|
+
state.update('onboardingDone', true);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function reset() {
|
|
190
|
+
localStorage.removeItem(STORAGE_KEY);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* ── RENDERIZADO ─────────────────────────────────────────────────────────── */
|
|
194
|
+
|
|
195
|
+
async function _renderStep() {
|
|
196
|
+
if (!_active) return;
|
|
197
|
+
const step = STEPS[_step];
|
|
198
|
+
const total = STEPS.length;
|
|
199
|
+
const isLast = _step === total - 1;
|
|
200
|
+
const isFirst = _step === 0;
|
|
201
|
+
|
|
202
|
+
// Navegar a la vista correcta antes de medir el target
|
|
203
|
+
if (step.view) {
|
|
204
|
+
const router = await import('./router.js');
|
|
205
|
+
if (router.current() !== step.view) {
|
|
206
|
+
await router.navigate(step.view);
|
|
207
|
+
// Esperar al siguiente frame para que el DOM esté pintado
|
|
208
|
+
await _nextFrame(120);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Actualizar contenido del tooltip
|
|
213
|
+
document.getElementById('ob-step-label').textContent = t('ui.onboarding.step', { current: _step + 1, total }, `Step ${_step + 1} of ${total}`);
|
|
214
|
+
document.getElementById('ob-title').textContent = t(step.titleKey, {}, step.titleKey);
|
|
215
|
+
document.getElementById('ob-desc').textContent = t(step.descKey, {}, step.descKey);
|
|
216
|
+
document.getElementById('ob-prev').textContent = t('ui.onboarding.prev', {}, 'Previous');
|
|
217
|
+
document.getElementById('ob-next').textContent = isLast ? t('ui.onboarding.start', {}, 'Start') : t('ui.onboarding.next', {}, 'Next →');
|
|
218
|
+
document.getElementById('ob-prev').style.visibility = isFirst ? 'hidden' : 'visible';
|
|
219
|
+
|
|
220
|
+
// Dots
|
|
221
|
+
const dotsEl = document.getElementById('ob-dots');
|
|
222
|
+
if (dotsEl) {
|
|
223
|
+
dotsEl.innerHTML = STEPS.map((_, i) =>
|
|
224
|
+
`<span class="ob-dot ${i === _step ? 'is-active' : ''}" aria-hidden="true"></span>`
|
|
225
|
+
).join('');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Mostrar tooltip con animación de entrada oculta temporalmente (para no saltar)
|
|
229
|
+
_tooltip.classList.remove('ob-enter');
|
|
230
|
+
_clearRing();
|
|
231
|
+
|
|
232
|
+
// Posicionar
|
|
233
|
+
if (step.target) {
|
|
234
|
+
const targetEl = _findTarget(step.target, step.view);
|
|
235
|
+
if (targetEl) {
|
|
236
|
+
// Hacer scroll suave para centrar el elemento en el viewport
|
|
237
|
+
targetEl.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'center' });
|
|
238
|
+
await _nextFrame(400); // Esperar que termine el scroll
|
|
239
|
+
_applySpotlight(targetEl);
|
|
240
|
+
_applyRing(targetEl);
|
|
241
|
+
_positionTooltip(targetEl, step.pos);
|
|
242
|
+
_mostrarTooltip();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Sin target → centrado
|
|
248
|
+
_clearSpotlight();
|
|
249
|
+
_positionCenter();
|
|
250
|
+
_mostrarTooltip();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
function _mostrarTooltip() {
|
|
254
|
+
void _tooltip.offsetWidth; // reflow
|
|
255
|
+
_tooltip.classList.add('ob-enter');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* ── SPOTLIGHT ───────────────────────────────────────────────────────────── */
|
|
259
|
+
|
|
260
|
+
function _applySpotlight(el) {
|
|
261
|
+
const PADDING = 10;
|
|
262
|
+
const rect = el.getBoundingClientRect();
|
|
263
|
+
|
|
264
|
+
// El spotlight es el propio elemento; usamos box-shadow inmenso para oscurecer el resto
|
|
265
|
+
_spotlight.style.cssText = `
|
|
266
|
+
position: fixed;
|
|
267
|
+
left: ${rect.left - PADDING}px;
|
|
268
|
+
top: ${rect.top - PADDING}px;
|
|
269
|
+
width: ${rect.width + PADDING * 2}px;
|
|
270
|
+
height: ${rect.height + PADDING * 2}px;
|
|
271
|
+
border-radius: 12px;
|
|
272
|
+
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.72);
|
|
273
|
+
z-index: var(--z-onboard);
|
|
274
|
+
pointer-events: none;
|
|
275
|
+
transition: all 280ms cubic-bezier(0.16, 1, 0.3, 1);
|
|
276
|
+
`;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function _clearSpotlight() {
|
|
280
|
+
_spotlight.style.cssText = `
|
|
281
|
+
position: fixed;
|
|
282
|
+
inset: 0;
|
|
283
|
+
box-shadow: 0 0 0 9999px rgba(0,0,0,0.72);
|
|
284
|
+
z-index: var(--z-onboard);
|
|
285
|
+
pointer-events: none;
|
|
286
|
+
background: transparent;
|
|
287
|
+
border-radius: 0;
|
|
288
|
+
`;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/* ── RING ANIMADO alrededor del target ───────────────────────────────────── */
|
|
292
|
+
|
|
293
|
+
function _applyRing(el) {
|
|
294
|
+
_clearRing();
|
|
295
|
+
const PADDING = 10;
|
|
296
|
+
const rect = el.getBoundingClientRect();
|
|
297
|
+
|
|
298
|
+
_ring = document.createElement('div');
|
|
299
|
+
_ring.className = 'onboarding-ring';
|
|
300
|
+
_ring.setAttribute('aria-hidden', 'true');
|
|
301
|
+
_ring.style.cssText = `
|
|
302
|
+
position: fixed;
|
|
303
|
+
left: ${rect.left - PADDING}px;
|
|
304
|
+
top: ${rect.top - PADDING}px;
|
|
305
|
+
width: ${rect.width + PADDING * 2}px;
|
|
306
|
+
height: ${rect.height + PADDING * 2}px;
|
|
307
|
+
border-radius: 14px;
|
|
308
|
+
border: 2px solid var(--accent);
|
|
309
|
+
z-index: calc(var(--z-onboard) + 1);
|
|
310
|
+
pointer-events: none;
|
|
311
|
+
animation: ob-ring-pulse 1.8s ease-in-out infinite;
|
|
312
|
+
`;
|
|
313
|
+
document.body.appendChild(_ring);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function _clearRing() {
|
|
317
|
+
if (_ring) {
|
|
318
|
+
_ring.remove();
|
|
319
|
+
_ring = null;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/* ── POSICIONAMIENTO DEL TOOLTIP ─────────────────────────────────────────── */
|
|
324
|
+
|
|
325
|
+
function _positionCenter() {
|
|
326
|
+
_tooltip.removeAttribute('data-pos');
|
|
327
|
+
_tooltip.style.cssText = `
|
|
328
|
+
position: fixed;
|
|
329
|
+
left: 50%;
|
|
330
|
+
top: 50%;
|
|
331
|
+
transform: translate(-50%, -50%);
|
|
332
|
+
z-index: calc(var(--z-onboard) + 2);
|
|
333
|
+
`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function _positionTooltip(el, preferredPos) {
|
|
337
|
+
const MARGIN = 18;
|
|
338
|
+
const TW = 420; // Más ancho para acomodar navegación + botones
|
|
339
|
+
const TH = 220;
|
|
340
|
+
const PADDING = 10;
|
|
341
|
+
const rect = el.getBoundingClientRect();
|
|
342
|
+
const vw = window.innerWidth;
|
|
343
|
+
const vh = window.innerHeight;
|
|
344
|
+
|
|
345
|
+
// Determinar la mejor posición disponible
|
|
346
|
+
const available = {
|
|
347
|
+
right: vw - rect.right - PADDING >= TW + MARGIN,
|
|
348
|
+
left: rect.left - PADDING >= TW + MARGIN,
|
|
349
|
+
bottom: vh - rect.bottom - PADDING >= TH + MARGIN,
|
|
350
|
+
top: rect.top - PADDING >= TH + MARGIN,
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
let pos = preferredPos;
|
|
354
|
+
if (!available[pos]) {
|
|
355
|
+
// Fallback en orden de preferencia
|
|
356
|
+
pos = ['right', 'left', 'bottom', 'top'].find(p => available[p]) || 'bottom';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
let left, top;
|
|
360
|
+
|
|
361
|
+
switch (pos) {
|
|
362
|
+
case 'right':
|
|
363
|
+
left = rect.right + PADDING + MARGIN;
|
|
364
|
+
top = rect.top + rect.height / 2 - TH / 2;
|
|
365
|
+
break;
|
|
366
|
+
case 'left':
|
|
367
|
+
left = rect.left - PADDING - MARGIN - TW;
|
|
368
|
+
top = rect.top + rect.height / 2 - TH / 2;
|
|
369
|
+
break;
|
|
370
|
+
case 'bottom':
|
|
371
|
+
left = rect.left + rect.width / 2 - TW / 2;
|
|
372
|
+
top = rect.bottom + PADDING + MARGIN;
|
|
373
|
+
break;
|
|
374
|
+
case 'top':
|
|
375
|
+
left = rect.left + rect.width / 2 - TW / 2;
|
|
376
|
+
top = rect.top - PADDING - MARGIN - TH;
|
|
377
|
+
break;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Clamp dentro del viewport
|
|
381
|
+
left = Math.max(MARGIN, Math.min(left, vw - TW - MARGIN));
|
|
382
|
+
top = Math.max(MARGIN, Math.min(top, vh - TH - MARGIN));
|
|
383
|
+
|
|
384
|
+
_tooltip.setAttribute('data-pos', pos);
|
|
385
|
+
_tooltip.style.cssText = `
|
|
386
|
+
position: fixed;
|
|
387
|
+
left: ${left}px;
|
|
388
|
+
top: ${top}px;
|
|
389
|
+
width: ${TW}px;
|
|
390
|
+
z-index: calc(var(--z-onboard) + 2);
|
|
391
|
+
transform: none;
|
|
392
|
+
`;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/* ── EVENTOS ─────────────────────────────────────────────────────────────── */
|
|
396
|
+
|
|
397
|
+
function _bindStaticEvents() {
|
|
398
|
+
const tooltip = _tooltip;
|
|
399
|
+
if (!tooltip) return;
|
|
400
|
+
|
|
401
|
+
tooltip.addEventListener('click', e => {
|
|
402
|
+
const btn = e.target.closest('button[id]');
|
|
403
|
+
if (!btn) return;
|
|
404
|
+
if (btn.id === 'ob-next') _advance(1);
|
|
405
|
+
if (btn.id === 'ob-prev') _advance(-1);
|
|
406
|
+
if (btn.id === 'ob-skip') finish();
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
// Escape para cerrar
|
|
410
|
+
_escHandler = e => {
|
|
411
|
+
if (e.key === 'Escape' && _active) finish();
|
|
412
|
+
};
|
|
413
|
+
document.addEventListener('keydown', _escHandler);
|
|
414
|
+
|
|
415
|
+
// Clic en el backdrop (fuera del tooltip) → cerrar
|
|
416
|
+
_spotlight.addEventListener('click', finish);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
function _advance(dir) {
|
|
420
|
+
if (dir > 0 && _step === STEPS.length - 1) {
|
|
421
|
+
finish();
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
_step = Math.max(0, Math.min(_step + dir, STEPS.length - 1));
|
|
425
|
+
_renderStep();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/* ── UTILS ───────────────────────────────────────────────────────────────── */
|
|
429
|
+
|
|
430
|
+
function _findTarget(selector, _view) {
|
|
431
|
+
// Intentar el selector tal cual
|
|
432
|
+
let el = document.querySelector(selector);
|
|
433
|
+
if (el) return el;
|
|
434
|
+
return null;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function _nextFrame(ms = 60) {
|
|
438
|
+
return new Promise(res => setTimeout(res, ms));
|
|
439
|
+
}
|