trackops 2.0.3 → 2.0.4
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 +238 -0
- package/lib/init.js +2 -2
- package/lib/locale.js +41 -17
- package/lib/opera-bootstrap.js +68 -7
- package/lib/opera.js +10 -2
- package/lib/registry.js +18 -0
- package/lib/server.js +312 -207
- package/locales/en.json +4 -0
- package/locales/es.json +4 -0
- package/package.json +1 -1
- package/skills/trackops/locales/en/references/activation.md +15 -0
- package/skills/trackops/locales/en/references/troubleshooting.md +12 -0
- package/skills/trackops/references/activation.md +15 -0
- package/skills/trackops/references/troubleshooting.md +12 -0
- package/skills/trackops/skill.json +2 -2
- package/ui/css/base.css +19 -1
- package/ui/css/charts.css +106 -8
- package/ui/css/components.css +554 -17
- 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 +170 -113
- package/ui/index.html +3 -0
- package/ui/js/api.js +49 -13
- package/ui/js/app.js +28 -32
- package/ui/js/charts.js +526 -0
- package/ui/js/filters.js +247 -0
- package/ui/js/icons.js +82 -57
- package/ui/js/keyboard.js +229 -0
- package/ui/js/onboarding.js +33 -42
- package/ui/js/router.js +20 -3
- package/ui/js/views/board.js +84 -114
- package/ui/js/views/dashboard.js +870 -0
- package/ui/js/views/projects.js +745 -0
- package/ui/js/views/scrum.js +476 -0
- package/ui/js/views/settings.js +197 -247
- 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/views/settings.js
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* settings.js — Vista de configuración del proyecto
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { icon } from '../icons.js';
|
|
6
|
-
import * as state from '../state.js';
|
|
7
|
-
import * as api from '../api.js';
|
|
8
|
-
import { flash } from './flash.js';
|
|
9
|
-
import { esc, formatDate } from '../utils.js';
|
|
10
|
-
import { t } from '../i18n.js';
|
|
11
|
-
|
|
1
|
+
/**
|
|
2
|
+
* settings.js — Vista de configuración del proyecto
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { icon } from '../icons.js';
|
|
6
|
+
import * as state from '../state.js';
|
|
7
|
+
import * as api from '../api.js';
|
|
8
|
+
import { flash } from './flash.js';
|
|
9
|
+
import { esc, formatDate } from '../utils.js';
|
|
10
|
+
import { t } from '../i18n.js';
|
|
11
|
+
import * as skillsView from './skills.js';
|
|
12
|
+
|
|
12
13
|
function applyLocaleState(payload) {
|
|
13
14
|
if (!payload) return;
|
|
14
15
|
state.update('payload', payload);
|
|
15
|
-
if (payload.i18n) {
|
|
16
|
-
state.update({
|
|
17
|
-
phases: payload.i18n.phases || [],
|
|
18
|
-
statusLabels: payload.i18n.statusLabels || {},
|
|
19
|
-
locale: payload.i18n.locale || 'es',
|
|
20
|
-
messages: payload.i18n.messages || {},
|
|
21
|
-
});
|
|
16
|
+
if (payload.i18n) {
|
|
17
|
+
state.update({
|
|
18
|
+
phases: payload.i18n.phases || [],
|
|
19
|
+
statusLabels: payload.i18n.statusLabels || {},
|
|
20
|
+
locale: payload.i18n.locale || 'es',
|
|
21
|
+
messages: payload.i18n.messages || {},
|
|
22
|
+
});
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
25
|
|
|
@@ -27,7 +28,7 @@ function renderWorkspacePanel(payload) {
|
|
|
27
28
|
if (!project) return '';
|
|
28
29
|
|
|
29
30
|
return `
|
|
30
|
-
<div class="panel">
|
|
31
|
+
<div class="glass-card panel">
|
|
31
32
|
<div class="panel-header">
|
|
32
33
|
<p class="panel-title">${t('ui.settings.workspaceTitle', {}, 'Workspace')}</p>
|
|
33
34
|
<span class="badge badge-muted">${esc(project.layout || 'legacy')}</span>
|
|
@@ -59,7 +60,7 @@ function renderEnvPanel(envState) {
|
|
|
59
60
|
const files = envState.files || {};
|
|
60
61
|
|
|
61
62
|
return `
|
|
62
|
-
<div class="panel">
|
|
63
|
+
<div class="glass-card panel">
|
|
63
64
|
<div class="panel-header">
|
|
64
65
|
<p class="panel-title">${t('ui.settings.envTitle', {}, 'Environment')}</p>
|
|
65
66
|
<span class="badge badge-${missing.length ? 'warning' : 'success'}">${missing.length ? t('ui.settings.envMissingBadge', { count: missing.length }, `${missing.length} missing`) : t('ui.settings.envHealthyBadge', {}, 'Ready')}</span>
|
|
@@ -107,68 +108,66 @@ function renderEnvPanel(envState) {
|
|
|
107
108
|
|
|
108
109
|
export async function render() {
|
|
109
110
|
const payload = state.getPayload();
|
|
110
|
-
const projects = state.get('projects');
|
|
111
|
-
const currentId = state.get('currentProjectId');
|
|
112
111
|
const control = payload?.control;
|
|
113
112
|
const runtime = payload?.runtime;
|
|
114
113
|
const docsDirty = payload?.docsDirty || [];
|
|
115
114
|
const envState = payload?.env;
|
|
116
|
-
|
|
117
|
-
return `
|
|
118
|
-
<div class="view-enter">
|
|
119
|
-
<div class="section-header">
|
|
120
|
-
<div class="section-header-left">
|
|
121
|
-
<p class="eyebrow">${t('ui.settings.eyebrow', {}, 'Settings')}</p>
|
|
122
|
-
<h2>${t('ui.settings.title', {}, 'Project settings')}</h2>
|
|
123
|
-
</div>
|
|
124
|
-
</div>
|
|
125
|
-
|
|
126
|
-
<div class="grid-2" style="align-items:start">
|
|
127
|
-
|
|
128
|
-
<!-- Col izquierda -->
|
|
129
|
-
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
|
|
130
|
-
|
|
131
|
-
<!-- Info del proyecto activo -->
|
|
115
|
+
|
|
116
|
+
return `
|
|
117
|
+
<div class="view-enter">
|
|
118
|
+
<div class="section-header">
|
|
119
|
+
<div class="section-header-left">
|
|
120
|
+
<p class="eyebrow">${t('ui.settings.eyebrow', {}, 'Settings')}</p>
|
|
121
|
+
<h2>${t('ui.settings.title', {}, 'Project settings')}</h2>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div class="grid-2" style="align-items:start">
|
|
126
|
+
|
|
127
|
+
<!-- Col izquierda -->
|
|
128
|
+
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
|
|
129
|
+
|
|
130
|
+
<!-- Info del proyecto activo -->
|
|
132
131
|
${control ? `
|
|
133
|
-
<div class="panel">
|
|
132
|
+
<div class="glass-card panel">
|
|
134
133
|
<div class="panel-header">
|
|
135
134
|
<p class="panel-title">Proyecto Activo</p>
|
|
136
|
-
<span class="badge badge-success">Activo</span>
|
|
137
|
-
</div>
|
|
138
|
-
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
139
|
-
<div class="info-row">
|
|
140
|
-
<p class="label-sm">Nombre</p>
|
|
141
|
-
<p class="value">${esc(control.meta?.projectName || '—')}</p>
|
|
142
|
-
</div>
|
|
143
|
-
<div class="info-row">
|
|
144
|
-
<p class="label-sm">Ruta</p>
|
|
145
|
-
<p class="value">${esc(payload?.project?.root || '—')}</p>
|
|
146
|
-
</div>
|
|
147
|
-
<div class="info-row">
|
|
148
|
-
<p class="label-sm">Fase activa</p>
|
|
149
|
-
<p class="value">${esc(payload?.derived?.activePhase?.id || '—')} — ${esc(payload?.derived?.activePhase?.label || '')}</p>
|
|
150
|
-
</div>
|
|
151
|
-
<div class="info-row">
|
|
152
|
-
<p class="label-sm">Foco actual</p>
|
|
153
|
-
<p class="value">${esc(control.meta?.currentFocus || '—')}</p>
|
|
154
|
-
</div>
|
|
155
|
-
<div class="info-row">
|
|
156
|
-
<p class="label-sm">Entrega objetivo</p>
|
|
157
|
-
<p class="value">${esc(control.meta?.deliveryTarget || '—')}</p>
|
|
158
|
-
</div>
|
|
159
|
-
<div class="info-row">
|
|
160
|
-
<p class="label-sm">Última actualización</p>
|
|
161
|
-
<p class="value">${formatDate(control.meta?.updatedAt || '', 'date')}</p>
|
|
162
|
-
</div>
|
|
163
|
-
<div class="info-row">
|
|
164
|
-
<p class="label-sm">${t('ui.topbar.language', {}, 'Language')}</p>
|
|
165
|
-
<div class="project-select-wrapper locale-select-wrapper">
|
|
166
|
-
<select id="settings-locale-select" aria-label="${t('ui.topbar.languageAria', {}, 'Select dashboard language')}">
|
|
167
|
-
<option value="es" ${(payload?.i18n?.locale || 'es') === 'es' ? 'selected' : ''}>${t('ui.topbar.languageEs', {}, 'ES')}</option>
|
|
168
|
-
<option value="en" ${(payload?.i18n?.locale || 'es') === 'en' ? 'selected' : ''}>${t('ui.topbar.languageEn', {}, 'EN')}</option>
|
|
169
|
-
</select>
|
|
170
|
-
</div>
|
|
171
|
-
</div>
|
|
135
|
+
<span class="badge badge-success">Activo</span>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
138
|
+
<div class="info-row">
|
|
139
|
+
<p class="label-sm">Nombre</p>
|
|
140
|
+
<p class="value">${esc(control.meta?.projectName || '—')}</p>
|
|
141
|
+
</div>
|
|
142
|
+
<div class="info-row">
|
|
143
|
+
<p class="label-sm">Ruta</p>
|
|
144
|
+
<p class="value">${esc(payload?.project?.root || '—')}</p>
|
|
145
|
+
</div>
|
|
146
|
+
<div class="info-row">
|
|
147
|
+
<p class="label-sm">Fase activa</p>
|
|
148
|
+
<p class="value">${esc(payload?.derived?.activePhase?.id || '—')} — ${esc(payload?.derived?.activePhase?.label || '')}</p>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="info-row">
|
|
151
|
+
<p class="label-sm">Foco actual</p>
|
|
152
|
+
<p class="value">${esc(control.meta?.currentFocus || '—')}</p>
|
|
153
|
+
</div>
|
|
154
|
+
<div class="info-row">
|
|
155
|
+
<p class="label-sm">Entrega objetivo</p>
|
|
156
|
+
<p class="value">${esc(control.meta?.deliveryTarget || '—')}</p>
|
|
157
|
+
</div>
|
|
158
|
+
<div class="info-row">
|
|
159
|
+
<p class="label-sm">Última actualización</p>
|
|
160
|
+
<p class="value">${formatDate(control.meta?.updatedAt || '', 'date')}</p>
|
|
161
|
+
</div>
|
|
162
|
+
<div class="info-row">
|
|
163
|
+
<p class="label-sm">${t('ui.topbar.language', {}, 'Language')}</p>
|
|
164
|
+
<div class="project-select-wrapper locale-select-wrapper">
|
|
165
|
+
<select id="settings-locale-select" aria-label="${t('ui.topbar.languageAria', {}, 'Select dashboard language')}">
|
|
166
|
+
<option value="es" ${(payload?.i18n?.locale || 'es') === 'es' ? 'selected' : ''}>${t('ui.topbar.languageEs', {}, 'ES')}</option>
|
|
167
|
+
<option value="en" ${(payload?.i18n?.locale || 'es') === 'en' ? 'selected' : ''}>${t('ui.topbar.languageEn', {}, 'EN')}</option>
|
|
168
|
+
</select>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
172
171
|
</div>
|
|
173
172
|
</div>
|
|
174
173
|
` : `<p class="text-muted">${t('ui.settings.noProject', {}, 'No project loaded.')}</p>`}
|
|
@@ -177,103 +176,60 @@ export async function render() {
|
|
|
177
176
|
|
|
178
177
|
<!-- Estado del Repo -->
|
|
179
178
|
${runtime ? `
|
|
180
|
-
<div class="panel">
|
|
181
|
-
<div class="panel-header">
|
|
182
|
-
<p class="panel-title">Repositorio</p>
|
|
183
|
-
<span class="badge badge-${runtime.clean ? 'success' : 'warning'}">${runtime.clean ? 'Limpio' : 'Con cambios'}</span>
|
|
184
|
-
</div>
|
|
185
|
-
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
186
|
-
<div class="info-row">
|
|
187
|
-
<p class="label-sm">Rama</p>
|
|
188
|
-
<p class="value">${esc(runtime.branch || '—')}</p>
|
|
189
|
-
</div>
|
|
190
|
-
<div class="info-row">
|
|
191
|
-
<p class="label-sm">Estado</p>
|
|
192
|
-
<p class="value">${runtime.staged}s ${runtime.unstaged}u ${runtime.untracked}?</p>
|
|
193
|
-
</div>
|
|
194
|
-
${runtime.lastCommit ? `
|
|
195
|
-
<div class="info-row">
|
|
196
|
-
<p class="label-sm">Último commit</p>
|
|
197
|
-
<p class="value">${esc(runtime.lastCommit.shortHash)} · ${esc(runtime.lastCommit.subject)}</p>
|
|
198
|
-
</div>
|
|
199
|
-
<div class="info-row">
|
|
200
|
-
<p class="label-sm">Fecha commit</p>
|
|
201
|
-
<p class="value">${formatDate(runtime.lastCommit.date, 'date')}</p>
|
|
202
|
-
</div>
|
|
203
|
-
` : ''}
|
|
204
|
-
<div class="info-row">
|
|
205
|
-
<p class="label-sm">Divergencia</p>
|
|
206
|
-
<p class="value">↑ ${runtime.ahead || 0} por delante · ↓ ${runtime.behind || 0} por detrás</p>
|
|
207
|
-
</div>
|
|
208
|
-
</div>
|
|
209
|
-
</div>
|
|
210
|
-
` : ''}
|
|
211
|
-
|
|
212
|
-
</div>
|
|
213
|
-
|
|
214
|
-
<!-- Col derecha -->
|
|
215
|
-
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
|
|
216
|
-
|
|
217
|
-
<!--
|
|
218
|
-
<div class="panel">
|
|
219
|
-
<div class="panel-header">
|
|
220
|
-
<p class="panel-title">
|
|
221
|
-
<span class="badge badge
|
|
222
|
-
</div>
|
|
223
|
-
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
224
|
-
${
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
? `<span class="badge badge-success">Disponible</span>`
|
|
235
|
-
: `<span class="badge badge-warning" title="No se puede cargar el control">No disponible</span>`}
|
|
236
|
-
${p.id === currentId
|
|
237
|
-
? `<span class="badge badge-accent">Activo</span>`
|
|
238
|
-
: p.available ? `<button class="btn btn-ghost btn-sm" type="button" data-switch="${esc(p.id)}">Abrir</button>` : ''
|
|
239
|
-
}
|
|
240
|
-
</div>
|
|
241
|
-
</div>
|
|
242
|
-
`).join('')
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
<!-- Registrar nuevo -->
|
|
246
|
-
<div style="margin-top:var(--space-2)">
|
|
247
|
-
<div class="field">
|
|
248
|
-
<label for="new-project-path">Registrar proyecto existente</label>
|
|
249
|
-
<input id="new-project-path" type="text" placeholder="/ruta/al/proyecto" />
|
|
250
|
-
</div>
|
|
251
|
-
<div class="form-actions" style="margin-top:var(--space-2)">
|
|
252
|
-
<button class="btn btn-ghost btn-sm" type="button" id="register-project-btn">
|
|
253
|
-
${icon('plus', 14)} Registrar
|
|
254
|
-
</button>
|
|
255
|
-
</div>
|
|
256
|
-
</div>
|
|
257
|
-
</div>
|
|
258
|
-
</div>
|
|
259
|
-
|
|
260
|
-
<!-- Docs Drift -->
|
|
261
|
-
<div class="panel">
|
|
262
|
-
<div class="panel-header">
|
|
263
|
-
<p class="panel-title">Documentación</p>
|
|
264
|
-
<span class="badge badge-${docsDirty.length ? 'warning' : 'success'}">${docsDirty.length ? `${docsDirty.length} desfasados` : 'Sincronizados'}</span>
|
|
265
|
-
</div>
|
|
266
|
-
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
267
|
-
${docsDirty.length > 0 ? `
|
|
268
|
-
<p style="font-size:var(--text-sm);color:var(--text-secondary)">Los siguientes archivos de documentación están desactualizados:</p>
|
|
269
|
-
<div class="stack stack-sm">
|
|
270
|
-
${docsDirty.map(d => `
|
|
271
|
-
<div class="info-row"><p class="value">${esc(d)}</p></div>
|
|
272
|
-
`).join('')}
|
|
273
|
-
</div>
|
|
274
|
-
` : `
|
|
275
|
-
<p style="font-size:var(--text-sm);color:var(--success)">✓ Todos los archivos de documentación están sincronizados.</p>
|
|
276
|
-
`}
|
|
179
|
+
<div class="glass-card panel">
|
|
180
|
+
<div class="panel-header">
|
|
181
|
+
<p class="panel-title">Repositorio</p>
|
|
182
|
+
<span class="badge badge-${runtime.clean ? 'success' : 'warning'}">${runtime.clean ? 'Limpio' : 'Con cambios'}</span>
|
|
183
|
+
</div>
|
|
184
|
+
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
185
|
+
<div class="info-row">
|
|
186
|
+
<p class="label-sm">Rama</p>
|
|
187
|
+
<p class="value">${esc(runtime.branch || '—')}</p>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="info-row">
|
|
190
|
+
<p class="label-sm">Estado</p>
|
|
191
|
+
<p class="value">${runtime.staged}s ${runtime.unstaged}u ${runtime.untracked}?</p>
|
|
192
|
+
</div>
|
|
193
|
+
${runtime.lastCommit ? `
|
|
194
|
+
<div class="info-row">
|
|
195
|
+
<p class="label-sm">Último commit</p>
|
|
196
|
+
<p class="value">${esc(runtime.lastCommit.shortHash)} · ${esc(runtime.lastCommit.subject)}</p>
|
|
197
|
+
</div>
|
|
198
|
+
<div class="info-row">
|
|
199
|
+
<p class="label-sm">Fecha commit</p>
|
|
200
|
+
<p class="value">${formatDate(runtime.lastCommit.date, 'date')}</p>
|
|
201
|
+
</div>
|
|
202
|
+
` : ''}
|
|
203
|
+
<div class="info-row">
|
|
204
|
+
<p class="label-sm">Divergencia</p>
|
|
205
|
+
<p class="value">↑ ${runtime.ahead || 0} por delante · ↓ ${runtime.behind || 0} por detrás</p>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
</div>
|
|
209
|
+
` : ''}
|
|
210
|
+
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<!-- Col derecha -->
|
|
214
|
+
<div style="display:flex;flex-direction:column;gap:var(--space-4)">
|
|
215
|
+
|
|
216
|
+
<!-- Docs Drift -->
|
|
217
|
+
<div class="glass-card panel">
|
|
218
|
+
<div class="panel-header">
|
|
219
|
+
<p class="panel-title">Documentación</p>
|
|
220
|
+
<span class="badge badge-${docsDirty.length ? 'warning' : 'success'}">${docsDirty.length ? `${docsDirty.length} desfasados` : 'Sincronizados'}</span>
|
|
221
|
+
</div>
|
|
222
|
+
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
223
|
+
${docsDirty.length > 0 ? `
|
|
224
|
+
<p style="font-size:var(--text-sm);color:var(--text-secondary)">Los siguientes archivos de documentación están desactualizados:</p>
|
|
225
|
+
<div class="stack stack-sm">
|
|
226
|
+
${docsDirty.map(d => `
|
|
227
|
+
<div class="info-row"><p class="value">${esc(d)}</p></div>
|
|
228
|
+
`).join('')}
|
|
229
|
+
</div>
|
|
230
|
+
` : `
|
|
231
|
+
<p style="font-size:var(--text-sm);color:var(--success)">✓ Todos los archivos de documentación están sincronizados.</p>
|
|
232
|
+
`}
|
|
277
233
|
<button class="btn btn-ghost btn-sm" type="button" id="sync-docs-btn">
|
|
278
234
|
${icon('sync', 14)} Sincronizar ahora
|
|
279
235
|
</button>
|
|
@@ -284,64 +240,49 @@ export async function render() {
|
|
|
284
240
|
|
|
285
241
|
<!-- Milestones -->
|
|
286
242
|
${control?.milestones?.length ? `
|
|
287
|
-
<div class="panel">
|
|
288
|
-
<div class="panel-header">
|
|
289
|
-
<p class="panel-title">Milestones</p>
|
|
290
|
-
<span class="badge badge-accent">${control.milestones.length}</span>
|
|
291
|
-
</div>
|
|
292
|
-
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
293
|
-
${control.milestones.map(m => `
|
|
294
|
-
<div class="finding-item" style="border-left-color:var(--accent)">
|
|
295
|
-
<p style="font-weight:700;font-size:var(--text-sm)">${esc(m.title)}</p>
|
|
296
|
-
<p style="font-size:var(--text-xs);color:var(--text-muted)">${esc(m.date)}</p>
|
|
297
|
-
<ul style="margin-top:var(--space-2);padding-left:1rem">
|
|
298
|
-
${(m.items || []).map(item => `<li style="font-size:var(--text-xs);color:var(--text-secondary)">${esc(item)}</li>`).join('')}
|
|
299
|
-
</ul>
|
|
300
|
-
</div>
|
|
301
|
-
`).join('')}
|
|
302
|
-
</div>
|
|
303
|
-
</div>
|
|
304
|
-
` : ''}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
//
|
|
324
|
-
document.getElementById('register-project-btn')?.addEventListener('click', async () => {
|
|
325
|
-
const input = document.getElementById('new-project-path');
|
|
326
|
-
const root = input?.value.trim();
|
|
327
|
-
if (!root) { flash(t('ui.settings.enterPath', {}, 'Enter the project path.'), 'warning'); return; }
|
|
328
|
-
try {
|
|
329
|
-
await api.registerProject(root);
|
|
330
|
-
flash('Proyecto registrado.', 'success');
|
|
331
|
-
if (input) input.value = '';
|
|
332
|
-
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
333
|
-
} catch (err) {
|
|
334
|
-
flash(err.message, 'error');
|
|
335
|
-
}
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
// Sync docs
|
|
243
|
+
<div class="glass-card panel">
|
|
244
|
+
<div class="panel-header">
|
|
245
|
+
<p class="panel-title">Milestones</p>
|
|
246
|
+
<span class="badge badge-accent">${control.milestones.length}</span>
|
|
247
|
+
</div>
|
|
248
|
+
<div class="panel-body" style="display:flex;flex-direction:column;gap:var(--space-3)">
|
|
249
|
+
${control.milestones.map(m => `
|
|
250
|
+
<div class="finding-item" style="border-left-color:var(--accent)">
|
|
251
|
+
<p style="font-weight:700;font-size:var(--text-sm)">${esc(m.title)}</p>
|
|
252
|
+
<p style="font-size:var(--text-xs);color:var(--text-muted)">${esc(m.date)}</p>
|
|
253
|
+
<ul style="margin-top:var(--space-2);padding-left:1rem">
|
|
254
|
+
${(m.items || []).map(item => `<li style="font-size:var(--text-xs);color:var(--text-secondary)">${esc(item)}</li>`).join('')}
|
|
255
|
+
</ul>
|
|
256
|
+
</div>
|
|
257
|
+
`).join('')}
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
` : ''}
|
|
261
|
+
|
|
262
|
+
<!-- Skills & Extensions -->
|
|
263
|
+
<div class="glass-card panel">
|
|
264
|
+
<div class="panel-header">
|
|
265
|
+
<h3 class="panel-title">${icon('zap', 16)} ${t('ui.settings.skills', {}, 'Skills & Extensions')}</h3>
|
|
266
|
+
</div>
|
|
267
|
+
<div class="panel-body" id="settings-skills-container">
|
|
268
|
+
<p style="color:var(--text-muted);font-size:var(--text-sm)">${t('ui.settings.skillsLoading', {}, 'Loading skills...')}</p>
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
</div>
|
|
275
|
+
`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export async function bindEvents() {
|
|
279
|
+
// Sync docs
|
|
339
280
|
document.getElementById('sync-docs-btn')?.addEventListener('click', async () => {
|
|
340
281
|
try {
|
|
341
282
|
await api.syncDocs();
|
|
342
283
|
flash('Documentación sincronizada.', 'success');
|
|
343
284
|
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
344
|
-
} catch (err) {
|
|
285
|
+
} catch (err) {
|
|
345
286
|
flash(err.message, 'error');
|
|
346
287
|
}
|
|
347
288
|
});
|
|
@@ -357,25 +298,34 @@ export function bindEvents() {
|
|
|
357
298
|
});
|
|
358
299
|
|
|
359
300
|
document.getElementById('settings-locale-select')?.addEventListener('change', async e => {
|
|
360
|
-
const select = e.target;
|
|
361
|
-
const previousLocale = state.get('locale') || 'es';
|
|
362
|
-
const nextLocale = select.value;
|
|
363
|
-
|
|
364
|
-
if (nextLocale === previousLocale) return;
|
|
365
|
-
|
|
366
|
-
select.disabled = true;
|
|
367
|
-
try {
|
|
368
|
-
const result = await api.updateProjectLocale(nextLocale);
|
|
369
|
-
applyLocaleState(result.state);
|
|
370
|
-
flash(t('ui.topbar.localeUpdated', {}, 'Language updated.'), 'success');
|
|
371
|
-
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
372
|
-
} catch (err) {
|
|
373
|
-
select.value = previousLocale;
|
|
374
|
-
flash(err.message, 'error');
|
|
375
|
-
} finally {
|
|
376
|
-
select.disabled = false;
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
301
|
+
const select = e.target;
|
|
302
|
+
const previousLocale = state.get('locale') || 'es';
|
|
303
|
+
const nextLocale = select.value;
|
|
304
|
+
|
|
305
|
+
if (nextLocale === previousLocale) return;
|
|
306
|
+
|
|
307
|
+
select.disabled = true;
|
|
308
|
+
try {
|
|
309
|
+
const result = await api.updateProjectLocale(nextLocale);
|
|
310
|
+
applyLocaleState(result.state);
|
|
311
|
+
flash(t('ui.topbar.localeUpdated', {}, 'Language updated.'), 'success');
|
|
312
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
313
|
+
} catch (err) {
|
|
314
|
+
select.value = previousLocale;
|
|
315
|
+
flash(err.message, 'error');
|
|
316
|
+
} finally {
|
|
317
|
+
select.disabled = false;
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
// Load skills into the container
|
|
322
|
+
try {
|
|
323
|
+
const skillsHtml = await skillsView.render();
|
|
324
|
+
const container = document.getElementById('settings-skills-container');
|
|
325
|
+
if (container) container.innerHTML = skillsHtml;
|
|
326
|
+
skillsView.bindEvents();
|
|
327
|
+
if (typeof skillsView.loadData === 'function') skillsView.loadData();
|
|
328
|
+
} catch (e) { console.warn('Skills load failed:', e); }
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export { bindEvents as bind };
|
package/ui/js/views/sidebar.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sidebar.js —
|
|
2
|
+
* sidebar.js — Glassmorphism sidebar with collapsed icon-strip layout
|
|
3
|
+
*
|
|
4
|
+
* 6 main nav items + 2 footer actions.
|
|
5
|
+
* CSS handles collapse/expand on hover; JS renders structure and manages badges.
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
import { icon } from '../icons.js';
|
|
@@ -8,23 +11,24 @@ import * as consoleLogger from '../console-logger.js';
|
|
|
8
11
|
import * as onboarding from '../onboarding.js';
|
|
9
12
|
import { t } from '../i18n.js';
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
const NAV_ITEMS = [
|
|
15
|
+
{ id: 'projects', label: 'Projects', icon: 'folder' },
|
|
16
|
+
{ id: 'dashboard', label: 'Dashboard', icon: 'dashboard' },
|
|
17
|
+
{ id: 'tasks', label: 'Tasks', icon: 'tasks', badge: true },
|
|
18
|
+
{ id: 'timeline', label: 'Timeline', icon: 'timeline' },
|
|
19
|
+
{ id: 'terminal', label: 'Terminal', icon: 'terminal2' },
|
|
20
|
+
{ id: 'settings', label: 'Settings', icon: 'settings' },
|
|
21
|
+
];
|
|
22
|
+
|
|
23
|
+
const FOOTER_ITEMS = [
|
|
24
|
+
{ id: 'help', label: 'Help & Tour', icon: 'help', action: 'tour' },
|
|
25
|
+
{ id: 'console', label: 'Logs', icon: 'console', action: 'console', badge: 'error' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** Renders the full sidebar */
|
|
12
29
|
export function render() {
|
|
13
30
|
const el = document.getElementById('sidebar');
|
|
14
31
|
if (!el) return;
|
|
15
|
-
const navItems = [
|
|
16
|
-
{ id: 'overview', label: t('ui.nav.overview', {}, 'Overview'), icon: 'dashboard', section: 'menu' },
|
|
17
|
-
{ id: 'tasks', label: t('ui.nav.tasks', {}, 'Tasks'), icon: 'tasks', section: 'menu', badge: true },
|
|
18
|
-
{ id: 'board', label: t('ui.nav.board', {}, 'Board'), icon: 'board', section: 'menu' },
|
|
19
|
-
{ id: 'execution', label: t('ui.nav.execution', {}, 'Execution'), icon: 'execution', section: 'menu' },
|
|
20
|
-
{ id: 'skills', label: t('ui.nav.skills', {}, 'Skills'), icon: 'zap', section: 'menu' },
|
|
21
|
-
{ id: 'insights', label: t('ui.nav.insights', {}, 'Insights'), icon: 'insights', section: 'menu' },
|
|
22
|
-
];
|
|
23
|
-
const generalItems = [
|
|
24
|
-
{ id: 'settings', label: t('ui.nav.settings', {}, 'Settings'), icon: 'settings' },
|
|
25
|
-
{ id: 'help', label: t('ui.nav.help', {}, 'Help & Tour'), icon: 'help', action: 'tour' },
|
|
26
|
-
{ id: 'console', label: t('ui.nav.logs', {}, 'Logs'), icon: 'console', action: 'console', badge: 'error' },
|
|
27
|
-
];
|
|
28
32
|
|
|
29
33
|
el.innerHTML = `
|
|
30
34
|
<nav class="sidebar" aria-label="${t('ui.sidebar.aria', {}, 'Main navigation')}">
|
|
@@ -36,18 +40,17 @@ export function render() {
|
|
|
36
40
|
<span class="sidebar-logo-name">TrackOps</span>
|
|
37
41
|
</a>
|
|
38
42
|
|
|
39
|
-
<!--
|
|
43
|
+
<!-- Menu -->
|
|
40
44
|
<div class="sidebar-section">
|
|
41
45
|
<p class="sidebar-section-label">${t('ui.sidebar.menu', {}, 'Menu')}</p>
|
|
42
46
|
<ul class="sidebar-nav" role="list">
|
|
43
|
-
${
|
|
47
|
+
${NAV_ITEMS.map(item => _renderNavItem(item)).join('')}
|
|
44
48
|
</ul>
|
|
45
49
|
</div>
|
|
46
50
|
|
|
47
|
-
<!--
|
|
51
|
+
<!-- Footer -->
|
|
48
52
|
<div class="sidebar-footer">
|
|
49
|
-
|
|
50
|
-
${generalItems.map(item => _renderGeneralItem(item)).join('')}
|
|
53
|
+
${FOOTER_ITEMS.map(item => _renderFooterItem(item)).join('')}
|
|
51
54
|
</div>
|
|
52
55
|
</nav>
|
|
53
56
|
`;
|
|
@@ -58,37 +61,41 @@ export function render() {
|
|
|
58
61
|
function _renderNavItem(item) {
|
|
59
62
|
const activeView = state.get('activeView');
|
|
60
63
|
const isActive = activeView === item.id;
|
|
64
|
+
const label = t(`ui.nav.${item.id}`, {}, item.label);
|
|
61
65
|
const pendingCount = _getPendingCount(item);
|
|
62
66
|
|
|
63
67
|
return `
|
|
64
68
|
<li role="listitem">
|
|
65
69
|
<button
|
|
66
|
-
class="nav-item
|
|
70
|
+
class="nav-item${isActive ? ' is-active' : ''}"
|
|
67
71
|
data-view="${item.id}"
|
|
72
|
+
data-tooltip="${label}"
|
|
68
73
|
type="button"
|
|
69
74
|
aria-current="${isActive ? 'page' : 'false'}"
|
|
70
|
-
aria-label="${
|
|
75
|
+
aria-label="${label}${pendingCount ? `, ${t('ui.sidebar.pendingCount', { count: pendingCount }, `${pendingCount} pending`)}` : ''}"
|
|
71
76
|
>
|
|
72
77
|
<span class="nav-item-icon" aria-hidden="true">${icon(item.icon, 18)}</span>
|
|
73
|
-
<span class="nav-item-label">${
|
|
78
|
+
<span class="nav-item-label">${label}</span>
|
|
74
79
|
${pendingCount ? `<span class="nav-item-badge" aria-label="${t('ui.sidebar.tasksBadge', { count: pendingCount }, `${pendingCount} tasks`)}">${pendingCount}</span>` : ''}
|
|
75
80
|
</button>
|
|
76
81
|
</li>
|
|
77
82
|
`;
|
|
78
83
|
}
|
|
79
84
|
|
|
80
|
-
function
|
|
85
|
+
function _renderFooterItem(item) {
|
|
86
|
+
const label = t(`ui.nav.${item.id}`, {}, item.label);
|
|
87
|
+
|
|
81
88
|
return `
|
|
82
89
|
<button
|
|
83
90
|
class="nav-item"
|
|
84
|
-
data-action="${item.action
|
|
85
|
-
data-
|
|
91
|
+
data-action="${item.action}"
|
|
92
|
+
data-tooltip="${label}"
|
|
86
93
|
type="button"
|
|
87
|
-
aria-label="${
|
|
88
|
-
${item.
|
|
94
|
+
aria-label="${label}"
|
|
95
|
+
${item.id === 'console' ? 'id="sidebar-console-btn"' : ''}
|
|
89
96
|
>
|
|
90
97
|
<span class="nav-item-icon" aria-hidden="true">${icon(item.icon, 18)}</span>
|
|
91
|
-
<span class="nav-item-label">${
|
|
98
|
+
<span class="nav-item-label">${label}</span>
|
|
92
99
|
${item.badge === 'error' ? `<span class="nav-item-badge danger" id="sidebar-console-badge" aria-label="${t('ui.sidebar.errors', {}, 'errors')}" style="display:none">0</span>` : ''}
|
|
93
100
|
</button>
|
|
94
101
|
`;
|
|
@@ -103,7 +110,6 @@ function _getPendingCount(item) {
|
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
function _bindEvents(el) {
|
|
106
|
-
// Acciones especiales (no nav-view)
|
|
107
113
|
el.addEventListener('click', e => {
|
|
108
114
|
const btn = e.target.closest('[data-action]');
|
|
109
115
|
if (!btn) return;
|
|
@@ -117,7 +123,7 @@ function _bindEvents(el) {
|
|
|
117
123
|
});
|
|
118
124
|
}
|
|
119
125
|
|
|
120
|
-
/**
|
|
126
|
+
/** Refresh badges without full re-render */
|
|
121
127
|
export function updateBadges() {
|
|
122
128
|
const payload = state.getPayload();
|
|
123
129
|
if (!payload) return;
|