trackops 2.0.4 → 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 -640
- 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 +41 -41
- package/lib/opera-bootstrap.js +942 -936
- package/lib/opera.js +495 -486
- package/lib/preferences.js +74 -74
- package/lib/registry.js +214 -214
- package/lib/release.js +56 -56
- package/lib/runtime-state.js +144 -144
- package/lib/skills.js +74 -57
- package/lib/workspace.js +260 -260
- package/locales/en.json +192 -170
- package/locales/es.json +192 -170
- 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 -90
- package/skills/trackops/locales/en/references/troubleshooting.md +73 -67
- package/skills/trackops/locales/en/references/workflow.md +55 -32
- package/skills/trackops/references/activation.md +94 -90
- package/skills/trackops/references/troubleshooting.md +73 -67
- 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 -284
- package/ui/css/charts.css +425 -425
- package/ui/css/components.css +1107 -1107
- package/ui/css/onboarding.css +133 -133
- package/ui/css/terminal.css +125 -125
- package/ui/css/timeline.css +58 -58
- package/ui/css/tokens.css +284 -284
- package/ui/favicon.svg +5 -5
- package/ui/index.html +99 -99
- package/ui/js/charts.js +526 -526
- package/ui/js/console-logger.js +172 -172
- package/ui/js/filters.js +247 -247
- package/ui/js/icons.js +129 -129
- package/ui/js/keyboard.js +229 -229
- package/ui/js/router.js +142 -142
- package/ui/js/theme.js +100 -100
- package/ui/js/time-tracker.js +248 -248
- package/ui/js/views/dashboard.js +870 -870
- package/ui/js/views/flash.js +47 -47
- package/ui/js/views/projects.js +745 -745
- package/ui/js/views/scrum.js +476 -476
- package/ui/js/views/settings.js +331 -331
- package/ui/js/views/timeline.js +265 -265
package/ui/js/keyboard.js
CHANGED
|
@@ -1,229 +1,229 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* keyboard.js — Sistema de atajos de teclado global para TrackOps Dashboard
|
|
3
|
-
* Secuencias de 2 teclas estilo Vim/GitHub (g d = go dashboard).
|
|
4
|
-
* No activo cuando un input/textarea/select tiene foco.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import * as router from './router.js';
|
|
8
|
-
import * as state from './state.js';
|
|
9
|
-
import { icon } from './icons.js';
|
|
10
|
-
|
|
11
|
-
/** @type {Map<string, {handler: Function, label: string, group: string}>} */
|
|
12
|
-
const _shortcuts = new Map();
|
|
13
|
-
|
|
14
|
-
/** Buffer para secuencias de 2 teclas */
|
|
15
|
-
let _pendingKey = '';
|
|
16
|
-
let _pendingTimer = null;
|
|
17
|
-
const SEQUENCE_TIMEOUT = 600; // ms para completar secuencia
|
|
18
|
-
|
|
19
|
-
/** Elemento del panel de ayuda */
|
|
20
|
-
let _helpVisible = false;
|
|
21
|
-
|
|
22
|
-
// ─────────────────────────────── REGISTRO ────────────────────────────────────
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Registrar un atajo de teclado
|
|
26
|
-
* @param {string} keys — Tecla o secuencia ('g o', '/', '?', 'Escape')
|
|
27
|
-
* @param {Function} handler — Callback
|
|
28
|
-
* @param {string} label — Descripcion para el panel de ayuda
|
|
29
|
-
* @param {string} [group='General'] — Grupo en el panel de ayuda
|
|
30
|
-
*/
|
|
31
|
-
export function register(keys, handler, label, group = 'General') {
|
|
32
|
-
_shortcuts.set(keys, { handler, label, group });
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// ─────────────────────────────── DEFAULTS ────────────────────────────────────
|
|
36
|
-
|
|
37
|
-
function _registerDefaults() {
|
|
38
|
-
// Navegacion
|
|
39
|
-
register('g d', () => router.navigate('dashboard'), 'Go to Dashboard', 'Navigation');
|
|
40
|
-
register('g t', () => router.navigate('tasks'), 'Go to Tasks', 'Navigation');
|
|
41
|
-
register('g l', () => router.navigate('timeline'), 'Go to Timeline', 'Navigation');
|
|
42
|
-
register('g e', () => router.navigate('terminal'), 'Go to Terminal', 'Navigation');
|
|
43
|
-
register('g p', () => router.navigate('projects'), 'Go to Projects', 'Navigation');
|
|
44
|
-
register('g s', () => router.navigate('settings'), 'Go to Settings', 'Navigation');
|
|
45
|
-
|
|
46
|
-
// Acciones
|
|
47
|
-
register('/', () => _focusSearch(), 'Focus search', 'Actions');
|
|
48
|
-
register('r', () => _refreshState(), 'Refresh data', 'Actions');
|
|
49
|
-
register('Escape', () => _closeActiveOverlay(), 'Close overlay', 'Actions');
|
|
50
|
-
|
|
51
|
-
// Ayuda
|
|
52
|
-
register('?', () => toggleHelp(), 'Show shortcuts', 'Help');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ─────────────────────────────── HANDLER ─────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
function _onKeyDown(e) {
|
|
58
|
-
// No interceptar si un input tiene foco
|
|
59
|
-
const tag = document.activeElement?.tagName?.toLowerCase();
|
|
60
|
-
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
61
|
-
// Solo Escape escapa de inputs
|
|
62
|
-
if (e.key === 'Escape') {
|
|
63
|
-
document.activeElement.blur();
|
|
64
|
-
e.preventDefault();
|
|
65
|
-
}
|
|
66
|
-
return;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// No interceptar si hay modificadores (excepto Shift para ?)
|
|
70
|
-
if (e.ctrlKey || e.altKey || e.metaKey) return;
|
|
71
|
-
|
|
72
|
-
const key = e.key;
|
|
73
|
-
|
|
74
|
-
// Buscar atajo directo (1 tecla)
|
|
75
|
-
if (!_pendingKey) {
|
|
76
|
-
const direct = _shortcuts.get(key);
|
|
77
|
-
if (direct) {
|
|
78
|
-
e.preventDefault();
|
|
79
|
-
direct.handler();
|
|
80
|
-
return;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Iniciar secuencia de 2 teclas
|
|
84
|
-
// Solo letras minusculas inician secuencias
|
|
85
|
-
if (/^[a-z]$/.test(key)) {
|
|
86
|
-
_pendingKey = key;
|
|
87
|
-
_pendingTimer = setTimeout(() => {
|
|
88
|
-
_pendingKey = '';
|
|
89
|
-
_pendingTimer = null;
|
|
90
|
-
}, SEQUENCE_TIMEOUT);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
93
|
-
return;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Completar secuencia de 2 teclas
|
|
97
|
-
clearTimeout(_pendingTimer);
|
|
98
|
-
const sequence = `${_pendingKey} ${key}`;
|
|
99
|
-
_pendingKey = '';
|
|
100
|
-
_pendingTimer = null;
|
|
101
|
-
|
|
102
|
-
const seq = _shortcuts.get(sequence);
|
|
103
|
-
if (seq) {
|
|
104
|
-
e.preventDefault();
|
|
105
|
-
seq.handler();
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ─────────────────────────────── ACCIONES ────────────────────────────────────
|
|
110
|
-
|
|
111
|
-
function _focusSearch() {
|
|
112
|
-
const input = document.querySelector('.topbar-search input, .topbar-search-input');
|
|
113
|
-
if (input) {
|
|
114
|
-
input.focus();
|
|
115
|
-
input.select();
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function _refreshState() {
|
|
120
|
-
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function _closeActiveOverlay() {
|
|
124
|
-
// Cerrar help panel
|
|
125
|
-
if (_helpVisible) {
|
|
126
|
-
toggleHelp();
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
// Cerrar modal
|
|
130
|
-
const modal = document.querySelector('.modal-overlay:not(.is-hidden)');
|
|
131
|
-
if (modal) {
|
|
132
|
-
modal.classList.add('is-hidden');
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
// Cerrar console panel
|
|
136
|
-
const consolePanel = document.getElementById('console-panel');
|
|
137
|
-
if (consolePanel?.classList.contains('is-open')) {
|
|
138
|
-
consolePanel.classList.remove('is-open');
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ─────────────────────────────── HELP PANEL ──────────────────────────────────
|
|
143
|
-
|
|
144
|
-
export function toggleHelp() {
|
|
145
|
-
_helpVisible = !_helpVisible;
|
|
146
|
-
let panel = document.getElementById('keyboard-help-panel');
|
|
147
|
-
|
|
148
|
-
if (!_helpVisible && panel) {
|
|
149
|
-
panel.remove();
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (_helpVisible) {
|
|
154
|
-
panel = document.createElement('div');
|
|
155
|
-
panel.id = 'keyboard-help-panel';
|
|
156
|
-
panel.className = 'modal-overlay';
|
|
157
|
-
panel.setAttribute('role', 'dialog');
|
|
158
|
-
panel.setAttribute('aria-modal', 'true');
|
|
159
|
-
panel.setAttribute('aria-label', 'Keyboard shortcuts');
|
|
160
|
-
panel.addEventListener('click', (e) => {
|
|
161
|
-
if (e.target === panel) toggleHelp();
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
// Agrupar shortcuts
|
|
165
|
-
const groups = new Map();
|
|
166
|
-
for (const [keys, { label, group }] of _shortcuts) {
|
|
167
|
-
if (!groups.has(group)) groups.set(group, []);
|
|
168
|
-
groups.get(group).push({ keys, label });
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
let groupsHtml = '';
|
|
172
|
-
for (const [group, items] of groups) {
|
|
173
|
-
groupsHtml += `
|
|
174
|
-
<div class="kb-help-group">
|
|
175
|
-
<h3 class="kb-help-group-title">${group}</h3>
|
|
176
|
-
<div class="kb-help-items">
|
|
177
|
-
${items.map(({ keys, label }) => `
|
|
178
|
-
<div class="kb-help-item">
|
|
179
|
-
<kbd class="kb-help-key">${_formatKey(keys)}</kbd>
|
|
180
|
-
<span class="kb-help-label">${label}</span>
|
|
181
|
-
</div>
|
|
182
|
-
`).join('')}
|
|
183
|
-
</div>
|
|
184
|
-
</div>
|
|
185
|
-
`;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
panel.innerHTML = `
|
|
189
|
-
<div class="modal" style="max-width:520px">
|
|
190
|
-
<div class="modal-header">
|
|
191
|
-
<h2 class="modal-title">${icon('keyboard', 18)} Keyboard shortcuts</h2>
|
|
192
|
-
<button class="modal-close" type="button" aria-label="Close" id="kb-help-close">×</button>
|
|
193
|
-
</div>
|
|
194
|
-
<div class="modal-body" style="padding:var(--space-4) var(--space-6)">
|
|
195
|
-
${groupsHtml}
|
|
196
|
-
</div>
|
|
197
|
-
<div class="modal-footer" style="justify-content:center">
|
|
198
|
-
<p style="font-size:var(--text-xs);color:var(--text-muted)">Press <kbd class="kb-help-key">?</kbd> to toggle this panel</p>
|
|
199
|
-
</div>
|
|
200
|
-
</div>
|
|
201
|
-
`;
|
|
202
|
-
|
|
203
|
-
document.body.appendChild(panel);
|
|
204
|
-
panel.querySelector('#kb-help-close')?.addEventListener('click', () => toggleHelp());
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function _formatKey(keys) {
|
|
209
|
-
return keys.split(' ').map(k => {
|
|
210
|
-
if (k === 'Escape') return 'Esc';
|
|
211
|
-
if (k === '/') return '/';
|
|
212
|
-
if (k === '?') return '?';
|
|
213
|
-
return k.toUpperCase();
|
|
214
|
-
}).join(' <span style="opacity:0.4">then</span> ');
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// ─────────────────────────────── INIT ────────────────────────────────────────
|
|
218
|
-
|
|
219
|
-
export function init() {
|
|
220
|
-
_registerDefaults();
|
|
221
|
-
document.addEventListener('keydown', _onKeyDown);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
export function destroy() {
|
|
225
|
-
document.removeEventListener('keydown', _onKeyDown);
|
|
226
|
-
_shortcuts.clear();
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
export default { init, destroy, register, toggleHelp };
|
|
1
|
+
/**
|
|
2
|
+
* keyboard.js — Sistema de atajos de teclado global para TrackOps Dashboard
|
|
3
|
+
* Secuencias de 2 teclas estilo Vim/GitHub (g d = go dashboard).
|
|
4
|
+
* No activo cuando un input/textarea/select tiene foco.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as router from './router.js';
|
|
8
|
+
import * as state from './state.js';
|
|
9
|
+
import { icon } from './icons.js';
|
|
10
|
+
|
|
11
|
+
/** @type {Map<string, {handler: Function, label: string, group: string}>} */
|
|
12
|
+
const _shortcuts = new Map();
|
|
13
|
+
|
|
14
|
+
/** Buffer para secuencias de 2 teclas */
|
|
15
|
+
let _pendingKey = '';
|
|
16
|
+
let _pendingTimer = null;
|
|
17
|
+
const SEQUENCE_TIMEOUT = 600; // ms para completar secuencia
|
|
18
|
+
|
|
19
|
+
/** Elemento del panel de ayuda */
|
|
20
|
+
let _helpVisible = false;
|
|
21
|
+
|
|
22
|
+
// ─────────────────────────────── REGISTRO ────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Registrar un atajo de teclado
|
|
26
|
+
* @param {string} keys — Tecla o secuencia ('g o', '/', '?', 'Escape')
|
|
27
|
+
* @param {Function} handler — Callback
|
|
28
|
+
* @param {string} label — Descripcion para el panel de ayuda
|
|
29
|
+
* @param {string} [group='General'] — Grupo en el panel de ayuda
|
|
30
|
+
*/
|
|
31
|
+
export function register(keys, handler, label, group = 'General') {
|
|
32
|
+
_shortcuts.set(keys, { handler, label, group });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ─────────────────────────────── DEFAULTS ────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
function _registerDefaults() {
|
|
38
|
+
// Navegacion
|
|
39
|
+
register('g d', () => router.navigate('dashboard'), 'Go to Dashboard', 'Navigation');
|
|
40
|
+
register('g t', () => router.navigate('tasks'), 'Go to Tasks', 'Navigation');
|
|
41
|
+
register('g l', () => router.navigate('timeline'), 'Go to Timeline', 'Navigation');
|
|
42
|
+
register('g e', () => router.navigate('terminal'), 'Go to Terminal', 'Navigation');
|
|
43
|
+
register('g p', () => router.navigate('projects'), 'Go to Projects', 'Navigation');
|
|
44
|
+
register('g s', () => router.navigate('settings'), 'Go to Settings', 'Navigation');
|
|
45
|
+
|
|
46
|
+
// Acciones
|
|
47
|
+
register('/', () => _focusSearch(), 'Focus search', 'Actions');
|
|
48
|
+
register('r', () => _refreshState(), 'Refresh data', 'Actions');
|
|
49
|
+
register('Escape', () => _closeActiveOverlay(), 'Close overlay', 'Actions');
|
|
50
|
+
|
|
51
|
+
// Ayuda
|
|
52
|
+
register('?', () => toggleHelp(), 'Show shortcuts', 'Help');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ─────────────────────────────── HANDLER ─────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
function _onKeyDown(e) {
|
|
58
|
+
// No interceptar si un input tiene foco
|
|
59
|
+
const tag = document.activeElement?.tagName?.toLowerCase();
|
|
60
|
+
if (tag === 'input' || tag === 'textarea' || tag === 'select') {
|
|
61
|
+
// Solo Escape escapa de inputs
|
|
62
|
+
if (e.key === 'Escape') {
|
|
63
|
+
document.activeElement.blur();
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// No interceptar si hay modificadores (excepto Shift para ?)
|
|
70
|
+
if (e.ctrlKey || e.altKey || e.metaKey) return;
|
|
71
|
+
|
|
72
|
+
const key = e.key;
|
|
73
|
+
|
|
74
|
+
// Buscar atajo directo (1 tecla)
|
|
75
|
+
if (!_pendingKey) {
|
|
76
|
+
const direct = _shortcuts.get(key);
|
|
77
|
+
if (direct) {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
direct.handler();
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Iniciar secuencia de 2 teclas
|
|
84
|
+
// Solo letras minusculas inician secuencias
|
|
85
|
+
if (/^[a-z]$/.test(key)) {
|
|
86
|
+
_pendingKey = key;
|
|
87
|
+
_pendingTimer = setTimeout(() => {
|
|
88
|
+
_pendingKey = '';
|
|
89
|
+
_pendingTimer = null;
|
|
90
|
+
}, SEQUENCE_TIMEOUT);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Completar secuencia de 2 teclas
|
|
97
|
+
clearTimeout(_pendingTimer);
|
|
98
|
+
const sequence = `${_pendingKey} ${key}`;
|
|
99
|
+
_pendingKey = '';
|
|
100
|
+
_pendingTimer = null;
|
|
101
|
+
|
|
102
|
+
const seq = _shortcuts.get(sequence);
|
|
103
|
+
if (seq) {
|
|
104
|
+
e.preventDefault();
|
|
105
|
+
seq.handler();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─────────────────────────────── ACCIONES ────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
function _focusSearch() {
|
|
112
|
+
const input = document.querySelector('.topbar-search input, .topbar-search-input');
|
|
113
|
+
if (input) {
|
|
114
|
+
input.focus();
|
|
115
|
+
input.select();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function _refreshState() {
|
|
120
|
+
window.dispatchEvent(new CustomEvent('ops:refresh'));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function _closeActiveOverlay() {
|
|
124
|
+
// Cerrar help panel
|
|
125
|
+
if (_helpVisible) {
|
|
126
|
+
toggleHelp();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
// Cerrar modal
|
|
130
|
+
const modal = document.querySelector('.modal-overlay:not(.is-hidden)');
|
|
131
|
+
if (modal) {
|
|
132
|
+
modal.classList.add('is-hidden');
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
// Cerrar console panel
|
|
136
|
+
const consolePanel = document.getElementById('console-panel');
|
|
137
|
+
if (consolePanel?.classList.contains('is-open')) {
|
|
138
|
+
consolePanel.classList.remove('is-open');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─────────────────────────────── HELP PANEL ──────────────────────────────────
|
|
143
|
+
|
|
144
|
+
export function toggleHelp() {
|
|
145
|
+
_helpVisible = !_helpVisible;
|
|
146
|
+
let panel = document.getElementById('keyboard-help-panel');
|
|
147
|
+
|
|
148
|
+
if (!_helpVisible && panel) {
|
|
149
|
+
panel.remove();
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (_helpVisible) {
|
|
154
|
+
panel = document.createElement('div');
|
|
155
|
+
panel.id = 'keyboard-help-panel';
|
|
156
|
+
panel.className = 'modal-overlay';
|
|
157
|
+
panel.setAttribute('role', 'dialog');
|
|
158
|
+
panel.setAttribute('aria-modal', 'true');
|
|
159
|
+
panel.setAttribute('aria-label', 'Keyboard shortcuts');
|
|
160
|
+
panel.addEventListener('click', (e) => {
|
|
161
|
+
if (e.target === panel) toggleHelp();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Agrupar shortcuts
|
|
165
|
+
const groups = new Map();
|
|
166
|
+
for (const [keys, { label, group }] of _shortcuts) {
|
|
167
|
+
if (!groups.has(group)) groups.set(group, []);
|
|
168
|
+
groups.get(group).push({ keys, label });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let groupsHtml = '';
|
|
172
|
+
for (const [group, items] of groups) {
|
|
173
|
+
groupsHtml += `
|
|
174
|
+
<div class="kb-help-group">
|
|
175
|
+
<h3 class="kb-help-group-title">${group}</h3>
|
|
176
|
+
<div class="kb-help-items">
|
|
177
|
+
${items.map(({ keys, label }) => `
|
|
178
|
+
<div class="kb-help-item">
|
|
179
|
+
<kbd class="kb-help-key">${_formatKey(keys)}</kbd>
|
|
180
|
+
<span class="kb-help-label">${label}</span>
|
|
181
|
+
</div>
|
|
182
|
+
`).join('')}
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
`;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
panel.innerHTML = `
|
|
189
|
+
<div class="modal" style="max-width:520px">
|
|
190
|
+
<div class="modal-header">
|
|
191
|
+
<h2 class="modal-title">${icon('keyboard', 18)} Keyboard shortcuts</h2>
|
|
192
|
+
<button class="modal-close" type="button" aria-label="Close" id="kb-help-close">×</button>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="modal-body" style="padding:var(--space-4) var(--space-6)">
|
|
195
|
+
${groupsHtml}
|
|
196
|
+
</div>
|
|
197
|
+
<div class="modal-footer" style="justify-content:center">
|
|
198
|
+
<p style="font-size:var(--text-xs);color:var(--text-muted)">Press <kbd class="kb-help-key">?</kbd> to toggle this panel</p>
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
`;
|
|
202
|
+
|
|
203
|
+
document.body.appendChild(panel);
|
|
204
|
+
panel.querySelector('#kb-help-close')?.addEventListener('click', () => toggleHelp());
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function _formatKey(keys) {
|
|
209
|
+
return keys.split(' ').map(k => {
|
|
210
|
+
if (k === 'Escape') return 'Esc';
|
|
211
|
+
if (k === '/') return '/';
|
|
212
|
+
if (k === '?') return '?';
|
|
213
|
+
return k.toUpperCase();
|
|
214
|
+
}).join(' <span style="opacity:0.4">then</span> ');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─────────────────────────────── INIT ────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
export function init() {
|
|
220
|
+
_registerDefaults();
|
|
221
|
+
document.addEventListener('keydown', _onKeyDown);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function destroy() {
|
|
225
|
+
document.removeEventListener('keydown', _onKeyDown);
|
|
226
|
+
_shortcuts.clear();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export default { init, destroy, register, toggleHelp };
|