trackops 2.0.3 → 2.0.5
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/LICENSE +21 -21
- package/README.md +695 -402
- package/bin/trackops.js +116 -116
- package/lib/config.js +326 -326
- package/lib/control.js +208 -208
- package/lib/env.js +244 -244
- package/lib/init.js +325 -325
- package/lib/locale.js +24 -0
- package/lib/opera-bootstrap.js +941 -874
- package/lib/opera.js +494 -477
- package/lib/preferences.js +74 -74
- package/lib/registry.js +214 -196
- package/lib/release.js +56 -56
- package/lib/runtime-state.js +144 -144
- package/lib/server.js +312 -207
- package/lib/skills.js +74 -57
- package/lib/workspace.js +260 -260
- package/locales/en.json +192 -166
- package/locales/es.json +192 -166
- package/package.json +61 -58
- package/scripts/postinstall-locale.js +21 -21
- package/scripts/skills-marketplace-smoke.js +124 -124
- package/scripts/smoke-tests.js +558 -554
- package/scripts/sync-skill-version.js +21 -21
- package/scripts/validate-skill.js +103 -103
- package/skills/trackops/SKILL.md +126 -122
- package/skills/trackops/agents/openai.yaml +7 -7
- package/skills/trackops/locales/en/SKILL.md +126 -122
- package/skills/trackops/locales/en/references/activation.md +94 -75
- package/skills/trackops/locales/en/references/troubleshooting.md +73 -55
- package/skills/trackops/locales/en/references/workflow.md +55 -32
- package/skills/trackops/references/activation.md +94 -75
- package/skills/trackops/references/troubleshooting.md +73 -55
- package/skills/trackops/references/workflow.md +55 -32
- package/skills/trackops/skill.json +29 -29
- package/templates/hooks/post-checkout +2 -2
- package/templates/hooks/post-commit +2 -2
- package/templates/hooks/post-merge +2 -2
- package/templates/opera/agent.md +28 -27
- package/templates/opera/architecture/dependency-graph.md +24 -24
- package/templates/opera/architecture/runtime-automation.md +24 -24
- package/templates/opera/architecture/runtime-operations.md +34 -34
- package/templates/opera/en/agent.md +22 -21
- package/templates/opera/en/architecture/dependency-graph.md +24 -24
- package/templates/opera/en/architecture/runtime-automation.md +24 -24
- package/templates/opera/en/architecture/runtime-operations.md +34 -34
- package/templates/opera/en/reviews/delivery-audit.md +18 -18
- package/templates/opera/en/reviews/integration-audit.md +18 -18
- package/templates/opera/en/router.md +24 -19
- package/templates/opera/references/autonomy-and-recovery.md +117 -117
- package/templates/opera/references/opera-cycle.md +193 -193
- package/templates/opera/registry.md +28 -28
- package/templates/opera/reviews/delivery-audit.md +18 -18
- package/templates/opera/reviews/integration-audit.md +18 -18
- package/templates/opera/router.md +54 -49
- package/templates/skills/changelog-updater/SKILL.md +69 -69
- package/templates/skills/commiter/SKILL.md +99 -99
- package/templates/skills/opera-contract-auditor/SKILL.md +38 -38
- package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -38
- package/templates/skills/opera-policy-guard/SKILL.md +26 -26
- package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -26
- package/templates/skills/opera-skill/SKILL.md +279 -0
- package/templates/skills/opera-skill/locales/en/SKILL.md +279 -0
- package/templates/skills/opera-skill/locales/en/references/phase-dod.md +138 -0
- package/templates/skills/opera-skill/references/phase-dod.md +138 -0
- package/templates/skills/project-starter-skill/SKILL.md +150 -131
- package/templates/skills/project-starter-skill/locales/en/SKILL.md +143 -105
- package/templates/skills/project-starter-skill/references/opera-cycle.md +195 -193
- package/ui/css/base.css +284 -266
- package/ui/css/charts.css +425 -327
- package/ui/css/components.css +1107 -570
- package/ui/css/onboarding.css +133 -0
- package/ui/css/panels.css +345 -406
- package/ui/css/terminal.css +125 -0
- package/ui/css/timeline.css +58 -0
- package/ui/css/tokens.css +284 -227
- package/ui/favicon.svg +5 -5
- package/ui/index.html +99 -96
- package/ui/js/api.js +49 -13
- package/ui/js/app.js +28 -32
- package/ui/js/charts.js +526 -0
- package/ui/js/console-logger.js +172 -172
- package/ui/js/filters.js +247 -0
- package/ui/js/icons.js +129 -104
- package/ui/js/keyboard.js +229 -0
- package/ui/js/onboarding.js +33 -42
- package/ui/js/router.js +142 -125
- package/ui/js/theme.js +100 -100
- package/ui/js/time-tracker.js +248 -248
- package/ui/js/views/board.js +84 -114
- package/ui/js/views/dashboard.js +870 -0
- package/ui/js/views/flash.js +47 -47
- package/ui/js/views/projects.js +745 -0
- package/ui/js/views/scrum.js +476 -0
- package/ui/js/views/settings.js +153 -203
- package/ui/js/views/sidebar.js +37 -31
- package/ui/js/views/tasks.js +218 -101
- package/ui/js/views/timeline.js +265 -0
- package/ui/js/views/topbar.js +94 -107
- package/ui/app.js +0 -950
- package/ui/js/views/insights.js +0 -340
- package/ui/js/views/overview.js +0 -369
- package/ui/styles.css +0 -688
package/ui/js/onboarding.js
CHANGED
|
@@ -28,7 +28,7 @@ const STEPS = [
|
|
|
28
28
|
titleKey: 'ui.onboarding.nav.title',
|
|
29
29
|
descKey: 'ui.onboarding.nav.desc',
|
|
30
30
|
target: '#sidebar',
|
|
31
|
-
view: '
|
|
31
|
+
view: 'dashboard',
|
|
32
32
|
pos: 'right',
|
|
33
33
|
},
|
|
34
34
|
|
|
@@ -36,8 +36,8 @@ const STEPS = [
|
|
|
36
36
|
{
|
|
37
37
|
titleKey: 'ui.onboarding.kpi.title',
|
|
38
38
|
descKey: 'ui.onboarding.kpi.desc',
|
|
39
|
-
target: '.
|
|
40
|
-
view: '
|
|
39
|
+
target: '.grid-4',
|
|
40
|
+
view: 'dashboard',
|
|
41
41
|
pos: 'bottom',
|
|
42
42
|
},
|
|
43
43
|
|
|
@@ -46,7 +46,7 @@ const STEPS = [
|
|
|
46
46
|
titleKey: 'ui.onboarding.activity.title',
|
|
47
47
|
descKey: 'ui.onboarding.activity.desc',
|
|
48
48
|
target: '.chart-card',
|
|
49
|
-
view: '
|
|
49
|
+
view: 'dashboard',
|
|
50
50
|
pos: 'bottom',
|
|
51
51
|
},
|
|
52
52
|
|
|
@@ -55,71 +55,62 @@ const STEPS = [
|
|
|
55
55
|
titleKey: 'ui.onboarding.progress.title',
|
|
56
56
|
descKey: 'ui.onboarding.progress.desc',
|
|
57
57
|
target: '.donut-wrapper',
|
|
58
|
-
view: '
|
|
58
|
+
view: 'dashboard',
|
|
59
59
|
pos: 'left',
|
|
60
60
|
},
|
|
61
61
|
|
|
62
|
-
// 5 —
|
|
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
|
|
62
|
+
// 5 — Topbar: búsqueda
|
|
72
63
|
{
|
|
73
64
|
titleKey: 'ui.onboarding.search.title',
|
|
74
65
|
descKey: 'ui.onboarding.search.desc',
|
|
75
66
|
target: '.topbar-search',
|
|
76
|
-
view: '
|
|
67
|
+
view: 'dashboard',
|
|
77
68
|
pos: 'bottom',
|
|
78
69
|
},
|
|
79
70
|
|
|
80
|
-
// 7 —
|
|
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
|
|
71
|
+
// 7 — Tasks (Kanban mode)
|
|
90
72
|
{
|
|
91
73
|
titleKey: 'ui.onboarding.tasks.title',
|
|
92
74
|
descKey: 'ui.onboarding.tasks.desc',
|
|
93
|
-
target: '
|
|
75
|
+
target: '#task-list',
|
|
94
76
|
view: 'tasks',
|
|
95
77
|
pos: 'right',
|
|
96
78
|
},
|
|
97
79
|
|
|
98
|
-
//
|
|
80
|
+
// 8 — Terminal
|
|
99
81
|
{
|
|
100
82
|
titleKey: 'ui.onboarding.execution.title',
|
|
101
83
|
descKey: 'ui.onboarding.execution.desc',
|
|
102
84
|
target: '.terminal-surface',
|
|
103
|
-
view: '
|
|
85
|
+
view: 'terminal',
|
|
104
86
|
pos: 'top',
|
|
105
87
|
},
|
|
106
88
|
|
|
107
|
-
//
|
|
89
|
+
// 9 — Timeline
|
|
108
90
|
{
|
|
109
|
-
titleKey: 'ui.onboarding.
|
|
110
|
-
descKey: 'ui.onboarding.
|
|
111
|
-
target: '.
|
|
112
|
-
view: '
|
|
113
|
-
pos: '
|
|
91
|
+
titleKey: 'ui.onboarding.timeline.title',
|
|
92
|
+
descKey: 'ui.onboarding.timeline.desc',
|
|
93
|
+
target: '.timeline-container',
|
|
94
|
+
view: 'timeline',
|
|
95
|
+
pos: 'top',
|
|
114
96
|
},
|
|
115
97
|
|
|
116
|
-
//
|
|
98
|
+
// 10 — Projects
|
|
117
99
|
{
|
|
118
|
-
titleKey: 'ui.onboarding.
|
|
119
|
-
descKey: 'ui.onboarding.
|
|
120
|
-
target: '
|
|
121
|
-
view: '
|
|
122
|
-
pos: '
|
|
100
|
+
titleKey: 'ui.onboarding.projects.title',
|
|
101
|
+
descKey: 'ui.onboarding.projects.desc',
|
|
102
|
+
target: '.projects-grid',
|
|
103
|
+
view: 'projects',
|
|
104
|
+
pos: 'top',
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
// 11 — Keyboard shortcuts
|
|
108
|
+
{
|
|
109
|
+
titleKey: 'ui.onboarding.keyboard.title',
|
|
110
|
+
descKey: 'ui.onboarding.keyboard.desc',
|
|
111
|
+
target: null,
|
|
112
|
+
view: null,
|
|
113
|
+
pos: 'center',
|
|
123
114
|
},
|
|
124
115
|
|
|
125
116
|
// 12 — Theme toggle
|
|
@@ -127,7 +118,7 @@ const STEPS = [
|
|
|
127
118
|
titleKey: 'ui.onboarding.theme.title',
|
|
128
119
|
descKey: 'ui.onboarding.theme.desc',
|
|
129
120
|
target: '#theme-toggle-btn',
|
|
130
|
-
view: '
|
|
121
|
+
view: 'dashboard',
|
|
131
122
|
pos: 'bottom',
|
|
132
123
|
},
|
|
133
124
|
|
package/ui/js/router.js
CHANGED
|
@@ -1,125 +1,142 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* router.js — Navegación entre vistas
|
|
3
|
-
* Gestiona qué vista está activa en #view-container
|
|
4
|
-
* y sincroniza el estado activo en el sidebar.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import * as state from './state.js';
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
*
|
|
25
|
-
* @param {HTMLElement}
|
|
26
|
-
*/
|
|
27
|
-
export function
|
|
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
|
-
|
|
1
|
+
/**
|
|
2
|
+
* router.js — Navegación entre vistas
|
|
3
|
+
* Gestiona qué vista está activa en #view-container
|
|
4
|
+
* y sincroniza el estado activo en el sidebar.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as state from './state.js';
|
|
8
|
+
|
|
9
|
+
const _redirects = {
|
|
10
|
+
overview: 'dashboard',
|
|
11
|
+
insights: 'dashboard',
|
|
12
|
+
board: 'tasks',
|
|
13
|
+
skills: 'settings',
|
|
14
|
+
execution: 'terminal',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** @type {Map<string, function(): HTMLElement|string>} */
|
|
18
|
+
const _registry = new Map();
|
|
19
|
+
let _currentView = null;
|
|
20
|
+
let _container = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Registra una vista
|
|
24
|
+
* @param {string} id - identificador único de la vista
|
|
25
|
+
* @param {function(): Promise<HTMLElement|string>} factory - función que devuelve el contenido
|
|
26
|
+
*/
|
|
27
|
+
export function register(id, factory) {
|
|
28
|
+
_registry.set(id, factory);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Inicializa el router
|
|
33
|
+
* @param {HTMLElement} container - #view-container
|
|
34
|
+
*/
|
|
35
|
+
export function init(container) {
|
|
36
|
+
_container = container;
|
|
37
|
+
|
|
38
|
+
// Navegación por hash
|
|
39
|
+
window.addEventListener('hashchange', _handleHash);
|
|
40
|
+
|
|
41
|
+
// Clic en nav-items del sidebar (delegación)
|
|
42
|
+
document.addEventListener('click', e => {
|
|
43
|
+
const item = e.target.closest('[data-view]');
|
|
44
|
+
if (!item || !item.dataset.view) return;
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
navigate(item.dataset.view);
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Navega a una vista
|
|
52
|
+
* @param {string} viewId
|
|
53
|
+
* @param {boolean} [updateHash=true]
|
|
54
|
+
*/
|
|
55
|
+
export async function navigate(viewId, updateHash = true) {
|
|
56
|
+
if (!_registry.has(viewId)) {
|
|
57
|
+
console.warn(`[router] Vista desconocida: "${viewId}"`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_currentView = viewId;
|
|
62
|
+
state.update('activeView', viewId);
|
|
63
|
+
|
|
64
|
+
// Actualizar hash sin trigger de hashchange
|
|
65
|
+
if (updateHash && window.location.hash !== `#${viewId}`) {
|
|
66
|
+
history.replaceState(null, '', `#${viewId}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Actualizar clases activas en sidebar
|
|
70
|
+
document.querySelectorAll('[data-view]').forEach(item => {
|
|
71
|
+
item.classList.toggle('is-active', item.dataset.view === viewId);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Renderizar contenido
|
|
75
|
+
if (_container) {
|
|
76
|
+
_container.innerHTML = '<div style="padding:2rem"><div class="spinner"></div></div>';
|
|
77
|
+
try {
|
|
78
|
+
const factory = _registry.get(viewId);
|
|
79
|
+
const content = await factory();
|
|
80
|
+
if (typeof content === 'string') {
|
|
81
|
+
_container.innerHTML = content;
|
|
82
|
+
} else if (content instanceof HTMLElement) {
|
|
83
|
+
_container.innerHTML = '';
|
|
84
|
+
_container.appendChild(content);
|
|
85
|
+
}
|
|
86
|
+
// Scroll to top
|
|
87
|
+
_container.scrollTop = 0;
|
|
88
|
+
// Focus para a11y (skip-link target)
|
|
89
|
+
_container.focus({ preventScroll: true });
|
|
90
|
+
} catch (err) {
|
|
91
|
+
console.error('[router] Error cargando vista:', err);
|
|
92
|
+
_container.innerHTML = `
|
|
93
|
+
<div class="empty-state" style="margin:2rem">
|
|
94
|
+
<p>Error al cargar la vista <strong>${viewId}</strong></p>
|
|
95
|
+
<p style="font-size:0.8rem;font-family:var(--font-mono)">${err.message}</p>
|
|
96
|
+
</div>`;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Refresca la vista actual (re-renderizado)
|
|
103
|
+
*/
|
|
104
|
+
export async function refresh() {
|
|
105
|
+
if (_currentView) {
|
|
106
|
+
await navigate(_currentView, false);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Obtiene la vista activa
|
|
112
|
+
*/
|
|
113
|
+
export function current() { return _currentView; }
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Handler de hashchange
|
|
117
|
+
*/
|
|
118
|
+
function _handleHash() {
|
|
119
|
+
let hash = window.location.hash.slice(1);
|
|
120
|
+
// Redirect old routes
|
|
121
|
+
if (_redirects[hash]) {
|
|
122
|
+
hash = _redirects[hash];
|
|
123
|
+
history.replaceState(null, '', `#${hash}`);
|
|
124
|
+
}
|
|
125
|
+
if (hash && _registry.has(hash) && hash !== _currentView) {
|
|
126
|
+
navigate(hash, false);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Carga la vista inicial desde el hash o usa la vista por defecto
|
|
132
|
+
* @param {string} [defaultView='overview']
|
|
133
|
+
*/
|
|
134
|
+
export async function start(defaultView = 'dashboard') {
|
|
135
|
+
let hash = window.location.hash.slice(1);
|
|
136
|
+
if (_redirects[hash]) {
|
|
137
|
+
hash = _redirects[hash];
|
|
138
|
+
history.replaceState(null, '', `#${hash}`);
|
|
139
|
+
}
|
|
140
|
+
const initial = hash && _registry.has(hash) ? hash : defaultView;
|
|
141
|
+
await navigate(initial, true);
|
|
142
|
+
}
|
package/ui/js/theme.js
CHANGED
|
@@ -1,100 +1,100 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* theme.js — Gestor de tema claro / oscuro
|
|
3
|
-
* Persiste en localStorage. Si no hay preferencia guardada, usa tema claro.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
const STORAGE_KEY = 'trackops-theme';
|
|
7
|
-
const THEMES = { dark: 'dark', light: 'light' };
|
|
8
|
-
|
|
9
|
-
/** Inicializa el tema aplicando el guardado o el preferido por el sistema */
|
|
10
|
-
export function init() {
|
|
11
|
-
const saved = localStorage.getItem(STORAGE_KEY);
|
|
12
|
-
apply(saved || THEMES.light, false);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Aplica un tema
|
|
16
|
-
* @param {'dark'|'light'} theme
|
|
17
|
-
* @param {boolean} [save=true] - persistir en localStorage
|
|
18
|
-
*/
|
|
19
|
-
export function apply(theme, save = true) {
|
|
20
|
-
const root = document.documentElement;
|
|
21
|
-
|
|
22
|
-
if (theme === THEMES.light) {
|
|
23
|
-
root.setAttribute('data-theme', 'light');
|
|
24
|
-
} else {
|
|
25
|
-
root.removeAttribute('data-theme');
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
if (save) {
|
|
29
|
-
localStorage.setItem(STORAGE_KEY, theme);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Actualizar icono del botón del topbar
|
|
33
|
-
_updateButton(theme);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Alterna entre claro y oscuro */
|
|
37
|
-
export function toggle() {
|
|
38
|
-
const current = document.documentElement.getAttribute('data-theme') === 'light'
|
|
39
|
-
? THEMES.light
|
|
40
|
-
: THEMES.dark;
|
|
41
|
-
apply(current === THEMES.light ? THEMES.dark : THEMES.light);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** Obtiene el tema activo */
|
|
45
|
-
export function current() {
|
|
46
|
-
return document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/** Renderiza el botón toggle para el topbar */
|
|
50
|
-
export function renderButton() {
|
|
51
|
-
const isDark = current() === 'dark';
|
|
52
|
-
return `
|
|
53
|
-
<button
|
|
54
|
-
class="btn btn-ghost btn-sm btn-icon theme-toggle"
|
|
55
|
-
id="theme-toggle-btn"
|
|
56
|
-
type="button"
|
|
57
|
-
aria-label="${isDark ? 'Cambiar a tema claro' : 'Cambiar a tema oscuro'}"
|
|
58
|
-
title="${isDark ? 'Tema claro' : 'Tema oscuro'}"
|
|
59
|
-
>
|
|
60
|
-
${isDark ? _iconSun() : _iconMoon()}
|
|
61
|
-
</button>
|
|
62
|
-
`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
/** Vincula el botón toggle (llamar tras renderizar el topbar) */
|
|
66
|
-
export function bindButton() {
|
|
67
|
-
const btn = document.getElementById('theme-toggle-btn');
|
|
68
|
-
btn?.addEventListener('click', toggle);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// ─────────────────────────────── PRIVADO ────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
function _updateButton(theme) {
|
|
74
|
-
const btn = document.getElementById('theme-toggle-btn');
|
|
75
|
-
if (!btn) return;
|
|
76
|
-
const isDark = theme === 'dark';
|
|
77
|
-
btn.setAttribute('aria-label', isDark ? 'Cambiar a tema claro' : 'Cambiar a tema oscuro');
|
|
78
|
-
btn.setAttribute('title', isDark ? 'Tema claro' : 'Tema oscuro');
|
|
79
|
-
btn.innerHTML = isDark ? _iconSun() : _iconMoon();
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function _iconSun() {
|
|
83
|
-
return `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
84
|
-
<circle cx="12" cy="12" r="5"/>
|
|
85
|
-
<line x1="12" y1="1" x2="12" y2="3"/>
|
|
86
|
-
<line x1="12" y1="21" x2="12" y2="23"/>
|
|
87
|
-
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
|
88
|
-
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
|
89
|
-
<line x1="1" y1="12" x2="3" y2="12"/>
|
|
90
|
-
<line x1="21" y1="12" x2="23" y2="12"/>
|
|
91
|
-
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
|
92
|
-
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
|
93
|
-
</svg>`;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function _iconMoon() {
|
|
97
|
-
return `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
98
|
-
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
99
|
-
</svg>`;
|
|
100
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* theme.js — Gestor de tema claro / oscuro
|
|
3
|
+
* Persiste en localStorage. Si no hay preferencia guardada, usa tema claro.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const STORAGE_KEY = 'trackops-theme';
|
|
7
|
+
const THEMES = { dark: 'dark', light: 'light' };
|
|
8
|
+
|
|
9
|
+
/** Inicializa el tema aplicando el guardado o el preferido por el sistema */
|
|
10
|
+
export function init() {
|
|
11
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
12
|
+
apply(saved || THEMES.light, false);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Aplica un tema
|
|
16
|
+
* @param {'dark'|'light'} theme
|
|
17
|
+
* @param {boolean} [save=true] - persistir en localStorage
|
|
18
|
+
*/
|
|
19
|
+
export function apply(theme, save = true) {
|
|
20
|
+
const root = document.documentElement;
|
|
21
|
+
|
|
22
|
+
if (theme === THEMES.light) {
|
|
23
|
+
root.setAttribute('data-theme', 'light');
|
|
24
|
+
} else {
|
|
25
|
+
root.removeAttribute('data-theme');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (save) {
|
|
29
|
+
localStorage.setItem(STORAGE_KEY, theme);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Actualizar icono del botón del topbar
|
|
33
|
+
_updateButton(theme);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Alterna entre claro y oscuro */
|
|
37
|
+
export function toggle() {
|
|
38
|
+
const current = document.documentElement.getAttribute('data-theme') === 'light'
|
|
39
|
+
? THEMES.light
|
|
40
|
+
: THEMES.dark;
|
|
41
|
+
apply(current === THEMES.light ? THEMES.dark : THEMES.light);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Obtiene el tema activo */
|
|
45
|
+
export function current() {
|
|
46
|
+
return document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Renderiza el botón toggle para el topbar */
|
|
50
|
+
export function renderButton() {
|
|
51
|
+
const isDark = current() === 'dark';
|
|
52
|
+
return `
|
|
53
|
+
<button
|
|
54
|
+
class="btn btn-ghost btn-sm btn-icon theme-toggle"
|
|
55
|
+
id="theme-toggle-btn"
|
|
56
|
+
type="button"
|
|
57
|
+
aria-label="${isDark ? 'Cambiar a tema claro' : 'Cambiar a tema oscuro'}"
|
|
58
|
+
title="${isDark ? 'Tema claro' : 'Tema oscuro'}"
|
|
59
|
+
>
|
|
60
|
+
${isDark ? _iconSun() : _iconMoon()}
|
|
61
|
+
</button>
|
|
62
|
+
`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Vincula el botón toggle (llamar tras renderizar el topbar) */
|
|
66
|
+
export function bindButton() {
|
|
67
|
+
const btn = document.getElementById('theme-toggle-btn');
|
|
68
|
+
btn?.addEventListener('click', toggle);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ─────────────────────────────── PRIVADO ────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
function _updateButton(theme) {
|
|
74
|
+
const btn = document.getElementById('theme-toggle-btn');
|
|
75
|
+
if (!btn) return;
|
|
76
|
+
const isDark = theme === 'dark';
|
|
77
|
+
btn.setAttribute('aria-label', isDark ? 'Cambiar a tema claro' : 'Cambiar a tema oscuro');
|
|
78
|
+
btn.setAttribute('title', isDark ? 'Tema claro' : 'Tema oscuro');
|
|
79
|
+
btn.innerHTML = isDark ? _iconSun() : _iconMoon();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function _iconSun() {
|
|
83
|
+
return `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
84
|
+
<circle cx="12" cy="12" r="5"/>
|
|
85
|
+
<line x1="12" y1="1" x2="12" y2="3"/>
|
|
86
|
+
<line x1="12" y1="21" x2="12" y2="23"/>
|
|
87
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/>
|
|
88
|
+
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
|
89
|
+
<line x1="1" y1="12" x2="3" y2="12"/>
|
|
90
|
+
<line x1="21" y1="12" x2="23" y2="12"/>
|
|
91
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
|
92
|
+
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
|
93
|
+
</svg>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function _iconMoon() {
|
|
97
|
+
return `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
98
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
99
|
+
</svg>`;
|
|
100
|
+
}
|