trackops 1.0.0 → 1.0.1
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 +59 -6
- package/lib/control.js +1 -0
- package/lib/init.js +12 -10
- package/lib/opera.js +3 -3
- package/lib/server.js +141 -0
- package/lib/skills.js +1 -1
- package/package.json +16 -3
- package/templates/etapa/agent.md +2 -2
- package/templates/etapa/references/etapa-cycle.md +1 -1
- package/templates/opera/agent.md +1 -1
- package/templates/skills/project-starter-skill/SKILL.md +5 -3
- package/ui/css/base.css +266 -0
- package/ui/css/charts.css +327 -0
- package/ui/css/components.css +570 -0
- package/ui/css/panels.css +953 -0
- package/ui/css/tokens.css +227 -0
- package/ui/favicon.svg +5 -0
- package/ui/index.html +91 -351
- package/ui/js/api.js +203 -0
- package/ui/js/app.js +199 -0
- package/ui/js/console-logger.js +172 -0
- package/ui/js/icons.js +104 -0
- package/ui/js/onboarding.js +437 -0
- package/ui/js/router.js +125 -0
- package/ui/js/state.js +129 -0
- package/ui/js/theme.js +100 -0
- package/ui/js/time-tracker.js +248 -0
- package/ui/js/utils.js +172 -0
- package/ui/js/views/board.js +254 -0
- package/ui/js/views/execution.js +256 -0
- package/ui/js/views/flash.js +47 -0
- package/ui/js/views/insights.js +339 -0
- package/ui/js/views/overview.js +364 -0
- package/ui/js/views/settings.js +243 -0
- package/ui/js/views/sidebar.js +132 -0
- package/ui/js/views/skills.js +162 -0
- package/ui/js/views/tasks.js +405 -0
- package/ui/js/views/topbar.js +183 -0
package/ui/js/app.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* app.js — Orquestador principal del dashboard TrackOps
|
|
3
|
+
* Punto de entrada del módulo ES.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as state from './state.js';
|
|
7
|
+
import * as api from './api.js';
|
|
8
|
+
import * as router from './router.js';
|
|
9
|
+
import * as consoleLogger from './console-logger.js';
|
|
10
|
+
import * as onboarding from './onboarding.js';
|
|
11
|
+
import * as timeTracker from './time-tracker.js';
|
|
12
|
+
import * as theme from './theme.js';
|
|
13
|
+
|
|
14
|
+
// Vistas
|
|
15
|
+
import { render as renderSidebar } from './views/sidebar.js';
|
|
16
|
+
import { render as renderTopbar } from './views/topbar.js';
|
|
17
|
+
import { render as renderOverview } from './views/overview.js';
|
|
18
|
+
import { render as renderBoard } from './views/board.js';
|
|
19
|
+
import { render as renderTasks } from './views/tasks.js';
|
|
20
|
+
import * as executionView from './views/execution.js';
|
|
21
|
+
import { render as renderInsights } from './views/insights.js';
|
|
22
|
+
import * as settingsView from './views/settings.js';
|
|
23
|
+
import * as skillsView from './views/skills.js';
|
|
24
|
+
|
|
25
|
+
// ─────────────────────────────── INIT ───────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
async function init() {
|
|
28
|
+
// 0. Tema (PRIMERO: evitar flash de tema incorrecto)
|
|
29
|
+
theme.init();
|
|
30
|
+
|
|
31
|
+
// 1. Console logger (capturar errores desde el arranque)
|
|
32
|
+
consoleLogger.init();
|
|
33
|
+
|
|
34
|
+
// 2. Registrar rutas en el router
|
|
35
|
+
router.register('overview', renderOverview);
|
|
36
|
+
router.register('board', renderBoard);
|
|
37
|
+
router.register('tasks', renderTasks);
|
|
38
|
+
router.register('execution', async () => {
|
|
39
|
+
const html = await executionView.render();
|
|
40
|
+
setTimeout(() => executionView.bindEvents(), 50);
|
|
41
|
+
return html;
|
|
42
|
+
});
|
|
43
|
+
router.register('insights', renderInsights);
|
|
44
|
+
router.register('settings', async () => {
|
|
45
|
+
const html = await settingsView.render();
|
|
46
|
+
setTimeout(() => settingsView.bindEvents(), 50);
|
|
47
|
+
return html;
|
|
48
|
+
});
|
|
49
|
+
router.register('skills', async () => {
|
|
50
|
+
const html = await skillsView.render();
|
|
51
|
+
setTimeout(() => {
|
|
52
|
+
skillsView.bindEvents();
|
|
53
|
+
skillsView.loadData();
|
|
54
|
+
}, 50);
|
|
55
|
+
return html;
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// 3. Inicializar el router
|
|
59
|
+
router.init(document.getElementById('view-container'));
|
|
60
|
+
|
|
61
|
+
// 4. Cargar estado inicial
|
|
62
|
+
await _loadInitialState();
|
|
63
|
+
|
|
64
|
+
// 5. Renderizar chrome (sidebar + topbar)
|
|
65
|
+
renderSidebar();
|
|
66
|
+
renderTopbar();
|
|
67
|
+
|
|
68
|
+
// 6. Navegar a la vista inicial
|
|
69
|
+
await router.start('overview');
|
|
70
|
+
|
|
71
|
+
// 7. Cargar time entries en background
|
|
72
|
+
timeTracker.loadEntries().catch(err => {
|
|
73
|
+
console.warn('[app] No se pudieron cargar time entries:', err.message);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// 8. Inicializar onboarding
|
|
77
|
+
onboarding.init();
|
|
78
|
+
|
|
79
|
+
// 9. Suscribir refreshes globales
|
|
80
|
+
_bindGlobalEvents();
|
|
81
|
+
|
|
82
|
+
// 10. Auto-refresh cada 60s
|
|
83
|
+
setInterval(_refreshState, 60_000);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─────────────────────────────── CARGA DE ESTADO ────────────────────────────
|
|
87
|
+
|
|
88
|
+
async function _loadInitialState() {
|
|
89
|
+
try {
|
|
90
|
+
// Cargar lista de proyectos
|
|
91
|
+
const projectsResult = await api.getProjects();
|
|
92
|
+
const projects = projectsResult.projects || [];
|
|
93
|
+
state.update('projects', projects);
|
|
94
|
+
|
|
95
|
+
// Determinar proyecto activo (persistido o primero disponible)
|
|
96
|
+
const saved = localStorage.getItem('ops-dashboard-project');
|
|
97
|
+
const first = projects.find(p => p.available);
|
|
98
|
+
const currentId = saved && projects.some(p => p.id === saved)
|
|
99
|
+
? saved
|
|
100
|
+
: first?.id || null;
|
|
101
|
+
|
|
102
|
+
state.update('currentProjectId', currentId);
|
|
103
|
+
|
|
104
|
+
// Cargar el estado del proyecto activo
|
|
105
|
+
if (currentId) {
|
|
106
|
+
await _refreshState();
|
|
107
|
+
}
|
|
108
|
+
} catch (err) {
|
|
109
|
+
console.error('[app] Error cargando estado inicial:', err);
|
|
110
|
+
// Si el endpoint de proyectos falla, intentar cargar el estado directamente
|
|
111
|
+
try {
|
|
112
|
+
await _refreshState();
|
|
113
|
+
} catch (fallbackErr) {
|
|
114
|
+
console.error('[app] Error en fallback de estado:', fallbackErr);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function _refreshState() {
|
|
120
|
+
try {
|
|
121
|
+
const payload = await api.getState();
|
|
122
|
+
state.update('payload', payload);
|
|
123
|
+
|
|
124
|
+
// Actualizar datos de i18n desde el backend
|
|
125
|
+
if (payload.i18n) {
|
|
126
|
+
state.update('phases', payload.i18n.phases || []);
|
|
127
|
+
state.update('statusLabels', payload.i18n.statusLabels || {});
|
|
128
|
+
state.update('locale', payload.i18n.locale || 'es');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Actualizar proyectos si el payload incluye info de proyectos
|
|
132
|
+
if (payload.project && state.get('projects').length === 0) {
|
|
133
|
+
state.update('projects', [{ ...payload.project, available: true }]);
|
|
134
|
+
state.update('currentProjectId', payload.project.id);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Actualizar helper findTask en el state (se accede via get('payload'))
|
|
138
|
+
// NO se puede asignar a state.findTask porque los namespaces de módulos ES son readonly.
|
|
139
|
+
|
|
140
|
+
// Re-renderizar chrome (puede haber cambiado el repo status)
|
|
141
|
+
renderTopbar();
|
|
142
|
+
renderSidebar();
|
|
143
|
+
|
|
144
|
+
// Refrescar la vista actual
|
|
145
|
+
await router.refresh();
|
|
146
|
+
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error('[app] Error actualizando estado:', err);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ─────────────────────────────── EVENTOS GLOBALES ───────────────────────────
|
|
153
|
+
|
|
154
|
+
function _bindGlobalEvents() {
|
|
155
|
+
// Refresh global (disparado por sync, cambio de proyecto, etc.)
|
|
156
|
+
window.addEventListener('ops:refresh', async () => {
|
|
157
|
+
await _refreshState();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Búsqueda global → refrescar la vista actual
|
|
161
|
+
window.addEventListener('ops:search', () => {
|
|
162
|
+
const active = router.current();
|
|
163
|
+
if (active === 'board' || active === 'tasks') {
|
|
164
|
+
router.refresh();
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// Navegación por teclado: Escape cierra modales / deselecciona
|
|
169
|
+
document.addEventListener('keydown', e => {
|
|
170
|
+
if (e.key === 'Escape') {
|
|
171
|
+
// Deseleccionar tarea si no hay modal abierto
|
|
172
|
+
const modalEl = document.querySelector('.modal-overlay:not(.is-hidden)');
|
|
173
|
+
if (!modalEl) {
|
|
174
|
+
// No deseleccionar: permite a las vistas manejar escape internamente
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Actualizar sidebar badges cuando cambia el payload
|
|
180
|
+
state.subscribe('payload', () => {
|
|
181
|
+
import('./views/sidebar.js').then(m => m.updateBadges?.());
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─────────────────────────────── ARRANQUE ───────────────────────────────────
|
|
186
|
+
|
|
187
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
188
|
+
init().catch(err => {
|
|
189
|
+
console.error('[app] Error fatal en la inicialización:', err);
|
|
190
|
+
document.getElementById('view-container').innerHTML = `
|
|
191
|
+
<div class="empty-state" style="margin:4rem auto;max-width:440px">
|
|
192
|
+
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="color:var(--danger)"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>
|
|
193
|
+
<h3 style="margin-top:var(--space-2)">Error al iniciar el dashboard</h3>
|
|
194
|
+
<p style="font-size:var(--text-sm);color:var(--text-secondary)">${err.message}</p>
|
|
195
|
+
<button class="btn btn-primary" onclick="location.reload()">Reintentar</button>
|
|
196
|
+
</div>
|
|
197
|
+
`;
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* console-logger.js — Panel de logs y captura de errores del frontend
|
|
3
|
+
* Intercepta console.error, console.warn y window.onerror.
|
|
4
|
+
* Muestra un panel colapsable en la parte inferior de la pantalla.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as state from './state.js';
|
|
8
|
+
|
|
9
|
+
const MAX_LOGS = 100;
|
|
10
|
+
let _panel = null;
|
|
11
|
+
let _logsList = null;
|
|
12
|
+
let _errorBadge = null;
|
|
13
|
+
let _titleEl = null;
|
|
14
|
+
let _errorCount = 0;
|
|
15
|
+
|
|
16
|
+
/** Inicializa el módulo y vincula el panel del DOM */
|
|
17
|
+
export function init() {
|
|
18
|
+
_panel = document.getElementById('console-panel');
|
|
19
|
+
_logsList = document.getElementById('console-logs');
|
|
20
|
+
_errorBadge = document.getElementById('console-error-count');
|
|
21
|
+
_titleEl = document.getElementById('console-panel-title');
|
|
22
|
+
|
|
23
|
+
const closeBtn = document.getElementById('console-close-btn');
|
|
24
|
+
const clearBtn = document.getElementById('console-clear-btn');
|
|
25
|
+
|
|
26
|
+
closeBtn?.addEventListener('click', close);
|
|
27
|
+
clearBtn?.addEventListener('click', clear);
|
|
28
|
+
|
|
29
|
+
_patchConsole();
|
|
30
|
+
_attachWindowErrors();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Abre el panel de console */
|
|
34
|
+
export function open() {
|
|
35
|
+
_panel?.classList.add('is-open');
|
|
36
|
+
state.update('consolePanelOpen', true);
|
|
37
|
+
// Scroll to bottom
|
|
38
|
+
setTimeout(() => { if (_logsList) _logsList.scrollTop = _logsList.scrollHeight; }, 50);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Cierra el panel */
|
|
42
|
+
export function close() {
|
|
43
|
+
_panel?.classList.remove('is-open');
|
|
44
|
+
state.update('consolePanelOpen', false);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Toggle */
|
|
48
|
+
export function toggle() {
|
|
49
|
+
if (state.get('consolePanelOpen')) close();
|
|
50
|
+
else open();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Limpia los logs */
|
|
54
|
+
export function clear() {
|
|
55
|
+
state.update('consoleLogs', []);
|
|
56
|
+
_errorCount = 0;
|
|
57
|
+
if (_logsList) _logsList.innerHTML = '';
|
|
58
|
+
_updateErrorBadge();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Añade una entrada al log
|
|
63
|
+
* @param {'error'|'warn'|'info'|'debug'} level
|
|
64
|
+
* @param {string} message
|
|
65
|
+
*/
|
|
66
|
+
export function log(level, message) {
|
|
67
|
+
const entry = {
|
|
68
|
+
level,
|
|
69
|
+
message: String(message),
|
|
70
|
+
time: new Date().toISOString(),
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const logs = state.get('consoleLogs');
|
|
74
|
+
logs.push(entry);
|
|
75
|
+
if (logs.length > MAX_LOGS) logs.shift();
|
|
76
|
+
state.update('consoleLogs', logs);
|
|
77
|
+
|
|
78
|
+
_appendEntry(entry);
|
|
79
|
+
|
|
80
|
+
if (level === 'error') {
|
|
81
|
+
_errorCount++;
|
|
82
|
+
_updateErrorBadge();
|
|
83
|
+
// Auto-abrir en error
|
|
84
|
+
if (!state.get('consolePanelOpen')) open();
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function _appendEntry(entry) {
|
|
89
|
+
if (!_logsList) return;
|
|
90
|
+
const time = entry.time.slice(11, 23); // HH:mm:ss.mmm
|
|
91
|
+
const item = document.createElement('div');
|
|
92
|
+
item.className = 'console-log-entry';
|
|
93
|
+
item.setAttribute('role', 'listitem');
|
|
94
|
+
item.innerHTML = `
|
|
95
|
+
<span class="console-log-time">${time}</span>
|
|
96
|
+
<span class="log-level log-${entry.level}">[${entry.level.toUpperCase()}]</span>
|
|
97
|
+
<span class="console-log-msg">${_escapeHtml(entry.message)}</span>
|
|
98
|
+
`;
|
|
99
|
+
_logsList.appendChild(item);
|
|
100
|
+
_logsList.scrollTop = _logsList.scrollHeight;
|
|
101
|
+
|
|
102
|
+
// Limitar nodos en el DOM
|
|
103
|
+
while (_logsList.childElementCount > MAX_LOGS) {
|
|
104
|
+
_logsList.firstElementChild?.remove();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function _updateErrorBadge() {
|
|
109
|
+
if (!_errorBadge) return;
|
|
110
|
+
if (_errorCount > 0) {
|
|
111
|
+
_errorBadge.textContent = _errorCount;
|
|
112
|
+
_errorBadge.style.display = '';
|
|
113
|
+
} else {
|
|
114
|
+
_errorBadge.style.display = 'none';
|
|
115
|
+
}
|
|
116
|
+
// Actualizar badge del sidebar
|
|
117
|
+
const sidebarBadge = document.getElementById('sidebar-console-badge');
|
|
118
|
+
if (sidebarBadge) {
|
|
119
|
+
sidebarBadge.textContent = _errorCount;
|
|
120
|
+
sidebarBadge.style.display = _errorCount > 0 ? '' : 'none';
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function _patchConsole() {
|
|
125
|
+
const _origError = console.error.bind(console);
|
|
126
|
+
const _origWarn = console.warn.bind(console);
|
|
127
|
+
const _origLog = console.log.bind(console);
|
|
128
|
+
|
|
129
|
+
console.error = (...args) => {
|
|
130
|
+
_origError(...args);
|
|
131
|
+
log('error', args.map(_serialize).join(' '));
|
|
132
|
+
};
|
|
133
|
+
console.warn = (...args) => {
|
|
134
|
+
_origWarn(...args);
|
|
135
|
+
log('warn', args.map(_serialize).join(' '));
|
|
136
|
+
};
|
|
137
|
+
// Solo capturar console.log marcados como [ops]
|
|
138
|
+
console.log = (...args) => {
|
|
139
|
+
_origLog(...args);
|
|
140
|
+
const msg = args.map(_serialize).join(' ');
|
|
141
|
+
if (msg.startsWith('[ops]') || msg.startsWith('[router]') || msg.startsWith('[api]')) {
|
|
142
|
+
log('info', msg);
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function _attachWindowErrors() {
|
|
148
|
+
window.onerror = (message, source, lineno, colno, error) => {
|
|
149
|
+
const msg = error?.stack || `${message} (${source}:${lineno}:${colno})`;
|
|
150
|
+
log('error', msg);
|
|
151
|
+
return false; // Dejar que sigan propagándose
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
window.addEventListener('unhandledrejection', event => {
|
|
155
|
+
const msg = event.reason?.stack || String(event.reason);
|
|
156
|
+
log('error', `Unhandled Promise rejection: ${msg}`);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function _serialize(value) {
|
|
161
|
+
if (typeof value === 'string') return value;
|
|
162
|
+
if (value instanceof Error) return value.stack || value.message;
|
|
163
|
+
try { return JSON.stringify(value, null, 0); } catch { return String(value); }
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function _escapeHtml(str) {
|
|
167
|
+
return String(str)
|
|
168
|
+
.replace(/&/g, '&')
|
|
169
|
+
.replace(/</g, '<')
|
|
170
|
+
.replace(/>/g, '>')
|
|
171
|
+
.replace(/"/g, '"');
|
|
172
|
+
}
|
package/ui/js/icons.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* icons.js — SVG paths de React Icons (Flat / Line) como constantes
|
|
3
|
+
* Stack vanilla: sin React, sin dependencias externas.
|
|
4
|
+
* Se usan los paths SVG de heroicons + lucide-style wrappers.
|
|
5
|
+
* Uso: icons.render('dashboard', 20) → <svg>...</svg>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const PATHS = {
|
|
9
|
+
// ── Navegación ──
|
|
10
|
+
dashboard: `<rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/>`,
|
|
11
|
+
tasks: `<path d="M9 5H7a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/><rect x="9" y="3" width="6" height="4" rx="1"/><path d="m9 12 2 2 4-4"/>`,
|
|
12
|
+
board: `<rect x="3" y="3" width="5" height="18" rx="1"/><rect x="11" y="3" width="5" height="12" rx="1"/><rect x="19" y="3" width="5" height="15" rx="1"/>`, // adjusted
|
|
13
|
+
execution: `<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>`,
|
|
14
|
+
insights: `<line x1="18" y1="20" x2="18" y2="10"/><line x1="12" y1="20" x2="12" y2="4"/><line x1="6" y1="20" x2="6" y2="14"/>`,
|
|
15
|
+
settings: `<circle cx="12" cy="12" r="3"/><path d="M19.1 6.9a2.83 2.83 0 0 1 0 4L17 13l-1.4-1.4 2.1-2.1a.83.83 0 0 0 0-1.2L16.4 7l-2.1 2.1L13 7.8l2.1-2.1a.83.83 0 0 0-1.2 0L12.7 6l-1.4 1.4L9.2 5.3a2.83 2.83 0 0 1 4-4L15.3 3.4l1.4 1.4 2.1-2.1L19.1 3a2.83 2.83 0 0 1 4 4l-2.1 2.1 1.4 1.4L24.5 8.8a2.83 2.83 0 0 1-4 4l-2.1-2.1-1.4 1.4 2.1 2.1a.83.83 0 0 1 0 1.2L17.4 17l2.1 2.1a.83.83 0 0 1-1.2 0L16.2 17l-1.4 1.4 2.1 2.1a2.83 2.83 0 0 1-4 4l-2.1-2.1-1.4 1.4 2.1 2.1a.83.83 0 0 1 0 1.2L10.7 26l2.1 2.1a.83.83 0 0 1-1.2 0L9.5 26l-1.4-1.4L5.9 26.7a2.83 2.83 0 0 1-4-4L4 20.6l1.4-1.4-2.1-2.1A2.83 2.83 0 0 1 7.3 13l2.1 2.1 1.4-1.4L8.7 11.6A2.83 2.83 0 0 1 13 7.6l2.1 2.1 1.4-1.4L14.4 6.2A2.83 2.83 0 0 1 19.1 6.9Z"/>`,
|
|
16
|
+
|
|
17
|
+
// ── General ──
|
|
18
|
+
help: `<circle cx="12" cy="12" r="10"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/><line x1="12" y1="17" x2="12.01" y2="17"/>`,
|
|
19
|
+
logout: `<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/>`,
|
|
20
|
+
console: `<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>`,
|
|
21
|
+
infinity: `<path d="M12 12c-2-2.5-4-4-6-4a4 4 0 0 0 0 8c2 0 4-1.5 6-4z"/><path d="M12 12c2 2.5 4 4 6 4a4 4 0 0 0 0-8c-2 0-4 1.5-6 4z"/>`,
|
|
22
|
+
|
|
23
|
+
// ── Acciones ──
|
|
24
|
+
plus: `<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>`,
|
|
25
|
+
check: `<polyline points="20 6 9 17 4 12"/>`,
|
|
26
|
+
x: `<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>`,
|
|
27
|
+
edit: `<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>`,
|
|
28
|
+
trash: `<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/>`,
|
|
29
|
+
copy: `<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>`,
|
|
30
|
+
refresh: `<polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>`,
|
|
31
|
+
sync: `<path d="M21 2v6h-6"/><path d="M3 12a9 9 0 0 1 15-6.7L21 8"/><path d="M3 22v-6h6"/><path d="M21 12a9 9 0 0 1-15 6.7L3 16"/>`,
|
|
32
|
+
search: `<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>`,
|
|
33
|
+
filter: `<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>`,
|
|
34
|
+
sort: `<line x1="21" y1="10" x2="7" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="21" y1="18" x2="7" y2="18"/>`,
|
|
35
|
+
menu: `<line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/>`,
|
|
36
|
+
arrowUp: `<line x1="12" y1="19" x2="12" y2="5"/><polyline points="5 12 12 5 19 12"/>`,
|
|
37
|
+
arrowDown: `<line x1="12" y1="5" x2="12" y2="19"/><polyline points="19 12 12 19 5 12"/>`,
|
|
38
|
+
arrowRight:`<line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/>`,
|
|
39
|
+
chevronDown:`<polyline points="6 9 12 15 18 9"/>`,
|
|
40
|
+
externalLink:`<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" y1="14" x2="21" y2="3"/>`,
|
|
41
|
+
|
|
42
|
+
// ── Tiempo / cronómetro ──
|
|
43
|
+
clock: `<circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/>`,
|
|
44
|
+
play: `<polygon points="5 3 19 12 5 21 5 3"/>`,
|
|
45
|
+
pause: `<rect x="6" y="4" width="4" height="16"/><rect x="14" y="4" width="4" height="16"/>`,
|
|
46
|
+
stop: `<rect x="3" y="3" width="18" height="18" rx="2"/>`,
|
|
47
|
+
timer: `<circle cx="12" cy="13" r="8"/><path d="M12 9v4l2 2"/><path d="M5 3 2 6"/><path d="m22 6-3-3"/><path d="M6.38 18.7 4 21"/><path d="M17.64 18.67 20 21"/>`,
|
|
48
|
+
|
|
49
|
+
// ── Git / Repo ──
|
|
50
|
+
gitBranch: `<line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/>`,
|
|
51
|
+
gitCommit: `<circle cx="12" cy="12" r="4"/><line x1="1.05" y1="12" x2="7" y2="12"/><line x1="17.01" y1="12" x2="22.96" y2="12"/>`,
|
|
52
|
+
gitMerge: `<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><path d="M6 21V9a9 9 0 0 0 9 9"/>`,
|
|
53
|
+
|
|
54
|
+
// ── Información / estado ──
|
|
55
|
+
alertCircle: `<circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/>`,
|
|
56
|
+
alertTriangle:`<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/>`,
|
|
57
|
+
checkCircle: `<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/>`,
|
|
58
|
+
info: `<circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/>`,
|
|
59
|
+
shield: `<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>`,
|
|
60
|
+
zap: `<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>`,
|
|
61
|
+
trending: `<polyline points="23 6 13.5 15.5 8.5 10.5 1 18"/><polyline points="17 6 23 6 23 12"/>`,
|
|
62
|
+
target: `<circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/>`,
|
|
63
|
+
|
|
64
|
+
// ── Ficheros / docs ──
|
|
65
|
+
fileText: `<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><polyline points="10 9 9 9 8 9"/>`,
|
|
66
|
+
folder: `<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>`,
|
|
67
|
+
download: `<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>`,
|
|
68
|
+
|
|
69
|
+
// ── Layout ──
|
|
70
|
+
maximize: `<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>`,
|
|
71
|
+
columns: `<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="12" y1="3" x2="12" y2="21"/>`,
|
|
72
|
+
terminal2: `<polyline points="4 17 10 11 4 5"/><line x1="12" y1="19" x2="20" y2="19"/>`,
|
|
73
|
+
package: `<line x1="16.5" y1="9.4" x2="7.5" y2="4.21"/><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>`,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Renderiza un SVG icon
|
|
78
|
+
* @param {string} name - nombre del icono
|
|
79
|
+
* @param {number} [size=18] - tamaño en px
|
|
80
|
+
* @param {string} [className=''] - clases adicionales
|
|
81
|
+
* @returns {string} HTML string del SVG
|
|
82
|
+
*/
|
|
83
|
+
export function icon(name, size = 18, className = '') {
|
|
84
|
+
const paths = PATHS[name];
|
|
85
|
+
if (!paths) {
|
|
86
|
+
// Icono fallback: círculo vacío
|
|
87
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="${className}" aria-hidden="true"><circle cx="12" cy="12" r="10" opacity="0.3"/></svg>`;
|
|
88
|
+
}
|
|
89
|
+
return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="${className}" aria-hidden="true">${paths}</svg>`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Crea un elemento SVG icon insertado en el DOM
|
|
94
|
+
* @param {string} name
|
|
95
|
+
* @param {number} [size=18]
|
|
96
|
+
* @returns {SVGElement}
|
|
97
|
+
*/
|
|
98
|
+
export function iconEl(name, size = 18) {
|
|
99
|
+
const wrapper = document.createElement('span');
|
|
100
|
+
wrapper.innerHTML = icon(name, size);
|
|
101
|
+
return wrapper.firstElementChild;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default { icon, iconEl };
|