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.
Files changed (103) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +695 -402
  3. package/bin/trackops.js +116 -116
  4. package/lib/config.js +326 -326
  5. package/lib/control.js +208 -208
  6. package/lib/env.js +244 -244
  7. package/lib/init.js +325 -325
  8. package/lib/locale.js +24 -0
  9. package/lib/opera-bootstrap.js +941 -874
  10. package/lib/opera.js +494 -477
  11. package/lib/preferences.js +74 -74
  12. package/lib/registry.js +214 -196
  13. package/lib/release.js +56 -56
  14. package/lib/runtime-state.js +144 -144
  15. package/lib/server.js +312 -207
  16. package/lib/skills.js +74 -57
  17. package/lib/workspace.js +260 -260
  18. package/locales/en.json +192 -166
  19. package/locales/es.json +192 -166
  20. package/package.json +61 -58
  21. package/scripts/postinstall-locale.js +21 -21
  22. package/scripts/skills-marketplace-smoke.js +124 -124
  23. package/scripts/smoke-tests.js +558 -554
  24. package/scripts/sync-skill-version.js +21 -21
  25. package/scripts/validate-skill.js +103 -103
  26. package/skills/trackops/SKILL.md +126 -122
  27. package/skills/trackops/agents/openai.yaml +7 -7
  28. package/skills/trackops/locales/en/SKILL.md +126 -122
  29. package/skills/trackops/locales/en/references/activation.md +94 -75
  30. package/skills/trackops/locales/en/references/troubleshooting.md +73 -55
  31. package/skills/trackops/locales/en/references/workflow.md +55 -32
  32. package/skills/trackops/references/activation.md +94 -75
  33. package/skills/trackops/references/troubleshooting.md +73 -55
  34. package/skills/trackops/references/workflow.md +55 -32
  35. package/skills/trackops/skill.json +29 -29
  36. package/templates/hooks/post-checkout +2 -2
  37. package/templates/hooks/post-commit +2 -2
  38. package/templates/hooks/post-merge +2 -2
  39. package/templates/opera/agent.md +28 -27
  40. package/templates/opera/architecture/dependency-graph.md +24 -24
  41. package/templates/opera/architecture/runtime-automation.md +24 -24
  42. package/templates/opera/architecture/runtime-operations.md +34 -34
  43. package/templates/opera/en/agent.md +22 -21
  44. package/templates/opera/en/architecture/dependency-graph.md +24 -24
  45. package/templates/opera/en/architecture/runtime-automation.md +24 -24
  46. package/templates/opera/en/architecture/runtime-operations.md +34 -34
  47. package/templates/opera/en/reviews/delivery-audit.md +18 -18
  48. package/templates/opera/en/reviews/integration-audit.md +18 -18
  49. package/templates/opera/en/router.md +24 -19
  50. package/templates/opera/references/autonomy-and-recovery.md +117 -117
  51. package/templates/opera/references/opera-cycle.md +193 -193
  52. package/templates/opera/registry.md +28 -28
  53. package/templates/opera/reviews/delivery-audit.md +18 -18
  54. package/templates/opera/reviews/integration-audit.md +18 -18
  55. package/templates/opera/router.md +54 -49
  56. package/templates/skills/changelog-updater/SKILL.md +69 -69
  57. package/templates/skills/commiter/SKILL.md +99 -99
  58. package/templates/skills/opera-contract-auditor/SKILL.md +38 -38
  59. package/templates/skills/opera-contract-auditor/locales/en/SKILL.md +38 -38
  60. package/templates/skills/opera-policy-guard/SKILL.md +26 -26
  61. package/templates/skills/opera-policy-guard/locales/en/SKILL.md +26 -26
  62. package/templates/skills/opera-skill/SKILL.md +279 -0
  63. package/templates/skills/opera-skill/locales/en/SKILL.md +279 -0
  64. package/templates/skills/opera-skill/locales/en/references/phase-dod.md +138 -0
  65. package/templates/skills/opera-skill/references/phase-dod.md +138 -0
  66. package/templates/skills/project-starter-skill/SKILL.md +150 -131
  67. package/templates/skills/project-starter-skill/locales/en/SKILL.md +143 -105
  68. package/templates/skills/project-starter-skill/references/opera-cycle.md +195 -193
  69. package/ui/css/base.css +284 -266
  70. package/ui/css/charts.css +425 -327
  71. package/ui/css/components.css +1107 -570
  72. package/ui/css/onboarding.css +133 -0
  73. package/ui/css/panels.css +345 -406
  74. package/ui/css/terminal.css +125 -0
  75. package/ui/css/timeline.css +58 -0
  76. package/ui/css/tokens.css +284 -227
  77. package/ui/favicon.svg +5 -5
  78. package/ui/index.html +99 -96
  79. package/ui/js/api.js +49 -13
  80. package/ui/js/app.js +28 -32
  81. package/ui/js/charts.js +526 -0
  82. package/ui/js/console-logger.js +172 -172
  83. package/ui/js/filters.js +247 -0
  84. package/ui/js/icons.js +129 -104
  85. package/ui/js/keyboard.js +229 -0
  86. package/ui/js/onboarding.js +33 -42
  87. package/ui/js/router.js +142 -125
  88. package/ui/js/theme.js +100 -100
  89. package/ui/js/time-tracker.js +248 -248
  90. package/ui/js/views/board.js +84 -114
  91. package/ui/js/views/dashboard.js +870 -0
  92. package/ui/js/views/flash.js +47 -47
  93. package/ui/js/views/projects.js +745 -0
  94. package/ui/js/views/scrum.js +476 -0
  95. package/ui/js/views/settings.js +153 -203
  96. package/ui/js/views/sidebar.js +37 -31
  97. package/ui/js/views/tasks.js +218 -101
  98. package/ui/js/views/timeline.js +265 -0
  99. package/ui/js/views/topbar.js +94 -107
  100. package/ui/app.js +0 -950
  101. package/ui/js/views/insights.js +0 -340
  102. package/ui/js/views/overview.js +0 -369
  103. package/ui/styles.css +0 -688
package/ui/js/icons.js CHANGED
@@ -1,104 +1,129 @@
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 };
1
+ /**
2
+ * icons.js — SVG paths de Remix Icon (Fill) como constantes
3
+ * Stack vanilla: sin React, sin dependencias externas.
4
+ * Paths SVG tomados de Remix Icon (fill variants), licencia MIT.
5
+ * viewBox 0 0 24 24, fill="currentColor", sin stroke.
6
+ * Uso: icon('dashboard', 20) → <svg>...</svg>
7
+ */
8
+
9
+ const PATHS = {
10
+ // ── Navegación ──
11
+ dashboard: `<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z"/>`,
12
+ tasks: `<path d="M21 2H3c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm-9 13l-5-5 1.41-1.41L12 12.17l5.59-5.59L19 8l-7 7z"/>`,
13
+ board: `<path d="M3 3h6v18H3V3zm8 0h6v12h-6V3zm8 0h2v15h-2V3z"/>`,
14
+ execution: `<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/><path d="M13 19h7v2h-7z"/>`,
15
+ insights: `<path d="M5 9.2h3v10.8H5V9.2zm5.6-5.2h3v16h-3V4zm5.6 8h3v8h-3v-8z"/>`,
16
+ settings: `<path d="M19.14 12.94c.04-.31.06-.63.06-.94 0-.31-.02-.63-.06-.94l2.03-1.58a.49.49 0 00.12-.61l-1.92-3.32a.49.49 0 00-.59-.22l-2.39.96a7.03 7.03 0 00-1.62-.94l-.36-2.54a.48.48 0 00-.48-.41h-3.84a.48.48 0 00-.48.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.49.49 0 00-.59.22L2.74 8.87a.48.48 0 00.12.61l2.03 1.58c-.04.31-.06.63-.06.94s.02.63.06.94l-2.03 1.58a.49.49 0 00-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.48-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6A3.6 3.6 0 1112 8.4a3.6 3.6 0 010 7.2z"/>`,
17
+
18
+ // ── General ──
19
+ help: `<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"/>`,
20
+ logout: `<path d="M5 2h14a1 1 0 011 1v18a1 1 0 01-1 1H5a1 1 0 01-1-1v-4h2v3h12V4H7v3H4V3a1 1 0 011-1zm6 7V7l5 5-5 5v-2H4v-6h7z"/>`,
21
+ console: `<path d="M2 4a1 1 0 011-1h18a1 1 0 011 1v16a1 1 0 01-1 1H3a1 1 0 01-1-1V4zm4 13v-2l4-3-4-3V7l6 5-6 5zm6 0h6v-2h-6v2z"/>`,
22
+ infinity: `<path d="M18.6 6.62c-1.44 0-2.8.56-3.77 1.53L12 10.66 9.17 8.15C8.2 7.18 6.84 6.62 5.4 6.62 2.42 6.62 0 9.04 0 12s2.42 5.38 5.4 5.38c1.44 0 2.8-.56 3.77-1.53L12 13.34l2.83 2.51c.97.97 2.33 1.53 3.77 1.53C21.58 17.38 24 14.96 24 12s-2.42-5.38-5.4-5.38zm-13.2 8.56C3.63 15.18 2.2 13.75 2.2 12s1.43-3.18 3.2-3.18c.86 0 1.68.34 2.27.94l2.21 1.96-2.21 2.52c-.59.6-1.41.94-2.27.94zm13.2 0c-.86 0-1.68-.34-2.27-.94l-2.21-1.96 2.21-2.52c.59-.6 1.41-.94 2.27-.94 1.77 0 3.2 1.43 3.2 3.18s-1.43 3.18-3.2 3.18z"/>`,
23
+
24
+ // ── Acciones ──
25
+ plus: `<path d="M11 11V5h2v6h6v2h-6v6h-2v-6H5v-2h6z"/>`,
26
+ check: `<path d="M9.86 18a1 1 0 01-.73-.32l-4.86-5.17a1 1 0 111.46-1.37l4.12 4.39 8.41-9.2a1 1 0 111.48 1.34l-9.14 10a1 1 0 01-.73.33h-.01z"/>`,
27
+ x: `<path d="M12 10.586l4.95-4.95 1.414 1.414L13.414 12l4.95 4.95-1.414 1.414L12 13.414l-4.95 4.95-1.414-1.414L10.586 12 5.636 7.05l1.414-1.414L12 10.586z"/>`,
28
+ edit: `<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04a1 1 0 000-1.41l-2.34-2.34a1 1 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z"/>`,
29
+ trash: `<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM8 9h8v10H8V9zm7.5-5l-1-1h-5l-1 1H5v2h14V4h-3.5z"/>`,
30
+ copy: `<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>`,
31
+ refresh: `<path d="M17.65 6.35A7.958 7.958 0 0012 4c-4.42 0-7.99 3.58-7.99 8s3.57 8 7.99 8c3.73 0 6.84-2.55 7.73-6h-2.08A5.99 5.99 0 0112 18c-3.31 0-6-2.69-6-6s2.69-6 6-6c1.66 0 3.14.69 4.22 1.78L13 11h7V4l-2.35 2.35z"/>`,
32
+ sync: `<path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46A7.93 7.93 0 0020 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74A7.93 7.93 0 004 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/>`,
33
+ search: `<path d="M15.5 14h-.79l-.28-.27A6.47 6.47 0 0016 9.5 6.5 6.5 0 109.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>`,
34
+ filter: `<path d="M10 18h4v-2h-4v2zM3 6v2h18V6H3zm3 7h12v-2H6v2z"/>`,
35
+ sort: `<path d="M3 18h6v-2H3v2zM3 6v2h18V6H3zm0 7h12v-2H3v2z"/>`,
36
+ menu: `<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>`,
37
+ arrowUp: `<path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/>`,
38
+ arrowDown: `<path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/>`,
39
+ arrowRight: `<path d="M12 4l-1.41 1.41L16.17 11H4v2h12.17l-5.58 5.59L12 20l8-8-8-8z"/>`,
40
+ chevronDown: `<path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/>`,
41
+ externalLink: `<path d="M19 19H5V5h7V3H5a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7h-2v7zM14 3v2h3.59l-9.83 9.83 1.41 1.41L19 6.41V10h2V3h-7z"/>`,
42
+
43
+ // ── Tiempo / cronómetro ──
44
+ clock: `<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2zm4.2 14.2L11 13V7h1.5v5.2l4.5 2.7-.8 1.3z"/>`,
45
+ play: `<path d="M8 5v14l11-7L8 5z"/>`,
46
+ pause: `<path d="M6 19h4V5H6v14zm8-14v14h4V5h-4z"/>`,
47
+ stop: `<path d="M6 6h12v12H6z"/>`,
48
+ timer: `<path d="M15 1H9v2h6V1zm-4 13h2V8h-2v6zm8.03-6.61l1.42-1.42c-.43-.51-.9-.99-1.41-1.41l-1.42 1.42A8.962 8.962 0 0012 4c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-2.12-.74-4.07-1.97-5.61zM12 20c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"/>`,
49
+
50
+ // ── Git / Repo ──
51
+ gitBranch: `<path d="M7 5a2 2 0 100 4 2 2 0 000-4zM7 15a2 2 0 100 4 2 2 0 000-4zM17 5a2 2 0 100 4 2 2 0 000-4zM7 11a4 4 0 00-4 4v0a4 4 0 004 4h0V11zm0 0V9m10-2v2a4 4 0 01-4 4H7"/>`,
52
+ gitCommit: `<path d="M12 16a4 4 0 100-8 4 4 0 000 8zm0-10a6.01 6.01 0 015.917 5H22v2h-4.083A6.01 6.01 0 0112 18a6.01 6.01 0 01-5.917-5H2v-2h4.083A6.01 6.01 0 0112 6z"/>`,
53
+ gitMerge: `<path d="M7.105 8.79A3.001 3.001 0 005 12a3 3 0 002.105 2.862V18a3.001 3.001 0 101.99.001v-3.526A8.01 8.01 0 0015 18h1v-2h-1a6.006 6.006 0 01-5.893-4.876A3.001 3.001 0 007.105 8.79zM8 6a1 1 0 11-2 0 1 1 0 012 0zm0 12a1 1 0 11-2 0 1 1 0 012 0zm0-6a1 1 0 11-2 0 1 1 0 012 0z"/>`,
54
+
55
+ // ── Información / estado ──
56
+ alertCircle: `<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>`,
57
+ alertTriangle:`<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>`,
58
+ checkCircle: `<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>`,
59
+ info: `<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>`,
60
+ shield: `<path d="M12 1l-9 4v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>`,
61
+ zap: `<path d="M11 21h-1l1-7H7.5c-.88 0-.33-.75-.31-.78C8.48 10.94 10.42 7.54 13.01 3h1l-1 7h3.51c.4 0 .62.19.4.66C12.97 17.55 11 21 11 21z"/>`,
62
+ trending: `<path d="M16 6l2.29 2.29-4.88 4.88-4-4L2 16.59 3.41 18l6-6 4 4 6.3-6.29L22 12V6h-6z"/>`,
63
+ target: `<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm0-14c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm0 10c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm0-6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>`,
64
+
65
+ // ── Ficheros / docs ──
66
+ fileText: `<path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm2 16H8v-2h8v2zm0-4H8v-2h8v2zm-3-5V3.5L18.5 9H13z"/>`,
67
+ folder: `<path d="M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2h-8l-2-2z"/>`,
68
+ download: `<path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z"/>`,
69
+
70
+ // ── Layout ──
71
+ maximize: `<path d="M7 14H5v5h5v-2H7v-3zm-2-4h2V7h3V5H5v5zm12 7h-3v2h5v-5h-2v3zM14 5v2h3v3h2V5h-5z"/>`,
72
+ columns: `<path d="M10 18h5V5h-5v13zm-6 0h5V5H4v13zM16 5v13h5V5h-5z"/>`,
73
+ terminal2: `<path d="M2 4a1 1 0 011-1h18a1 1 0 011 1v16a1 1 0 01-1 1H3a1 1 0 01-1-1V4zm4 13v-2l4-3-4-3V7l6 5-6 5zm6 0h6v-2h-6v2z"/>`,
74
+ package: `<path d="M20 12V6.5L12 2 4 6.5V12l8 4.5L20 12zM12 4.31l5.74 3.22L12 10.75 6.26 7.53 12 4.31zM6 9.17l5 2.81v5.64l-5-2.81V9.17zm7 8.45v-5.64l5-2.81v5.64l-5 2.81z"/>`,
75
+
76
+ // ── Nuevos iconos ──
77
+ projects: `<path d="M20 6h-8l-2-2H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm-2 10H6v-2h12v2zm0-4H6v-2h12v2z"/>`,
78
+ timeline: `<path d="M2 5h6v4H2V5zm0 10h8v4H2v-4zm0-5h10v4H2v-4zm14-5h6v4h-6V5zm-4 10h10v4H12v-4zm2-5h8v4h-8v-4z"/>`,
79
+ velocity: `<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8zm1-8.41l2.54 2.53 1.41-1.41-3.54-3.54L12 3v6.59z"/>`,
80
+ calendar: `<path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM9 10H7v2h2v-2zm4 0h-2v2h2v-2zm4 0h-2v2h2v-2z"/>`,
81
+ heatmap: `<path d="M4 4h4v4H4V4zm6 0h4v4h-4V4zm6 0h4v4h-4V4zM4 10h4v4H4v-4zm6 0h4v4h-4v-4zm6 0h4v4h-4v-4zM4 16h4v4H4v-4zm6 0h4v4h-4v-4zm6 0h4v4h-4v-4z"/>`,
82
+ keyboard: `<path d="M20 5H4c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm-9 3h2v2h-2V8zm0 3h2v2h-2v-2zM8 8h2v2H8V8zm0 3h2v2H8v-2zm-1 2H5v-2h2v2zm0-3H5V8h2v2zm9 7H8v-2h8v2zm0-4h-2v-2h2v2zm0-3h-2V8h2v2zm3 3h-2v-2h2v2zm0-3h-2V8h2v2z"/>`,
83
+ moon: `<path d="M12.03 4.5C11.44 4.5 10.87 4.56 10.31 4.68 13.31 5.88 15.5 8.88 15.5 12.38c0 3.51-2.19 6.5-5.19 7.7.56.12 1.13.18 1.72.18 4.97 0 9-4.03 9-9s-4.03-9.76-9-9.76z"/>`,
84
+ sun: `<path d="M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.79 1.42-1.41zM4 10.5H1v2h3v-2zm9-9.95h-2V3.5h2V.55zm7.45 3.91l-1.41-1.41-1.79 1.79 1.41 1.41 1.79-1.79zm-3.21 13.7l1.79 1.8 1.41-1.41-1.8-1.79-1.4 1.4zM20 10.5v2h3v-2h-3zm-8-5c-3.31 0-6 2.69-6 6s2.69 6 6 6 6-2.69 6-6-2.69-6-6-6zm-1 16.95h2V19.5h-2v2.95zm-7.45-3.91l1.41 1.41 1.79-1.8-1.41-1.41-1.79 1.8z"/>`,
85
+ user: `<path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/>`,
86
+ lock: `<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>`,
87
+ unlock: `<path d="M12 17c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm6-9h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6h1.9c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm0 12H6V10h12v10z"/>`,
88
+ eye: `<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z"/>`,
89
+ eyeOff: `<path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46A11.8 11.8 0 001 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z"/>`,
90
+ grip: `<path d="M9 3h2v2H9V3zm4 0h2v2h-2V3zM9 7h2v2H9V7zm4 0h2v2h-2V7zM9 11h2v2H9v-2zm4 0h2v2h-2v-2zM9 15h2v2H9v-2zm4 0h2v2h-2v-2zM9 19h2v2H9v-2zm4 0h2v2h-2v-2z"/>`,
91
+ moreVertical: `<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>`,
92
+ moreHorizontal:`<path d="M6 10c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm12 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm-6 0c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"/>`,
93
+ archive: `<path d="M20.54 5.23l-1.39-1.68C18.88 3.21 18.47 3 18 3H6c-.47 0-.88.21-1.16.55L3.46 5.23C3.17 5.57 3 6.02 3 6.5V19c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V6.5c0-.48-.17-.93-.46-1.27zM12 17.5L6.5 12H10v-2h4v2h3.5L12 17.5zM5.12 5l.81-1h12l.94 1H5.12z"/>`,
94
+ star: `<path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/>`,
95
+ bookmark: `<path d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z"/>`,
96
+ link: `<path d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"/>`,
97
+ globe: `<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z"/>`,
98
+ flag: `<path d="M14.4 6L14 4H5v17h2v-7h5.6l.4 2h7V6h-5.6z"/>`,
99
+ };
100
+
101
+ /**
102
+ * Renderiza un SVG icon (fill-based, Remix Icon style)
103
+ * @param {string} name - nombre del icono
104
+ * @param {number} [size=18] - tamaño en px
105
+ * @param {string} [className=''] - clases adicionales
106
+ * @returns {string} HTML string del SVG
107
+ */
108
+ export function icon(name, size = 18, className = '') {
109
+ const paths = PATHS[name];
110
+ if (!paths) {
111
+ // Icono fallback: círculo semitransparente
112
+ return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="currentColor" class="${className}" aria-hidden="true"><circle cx="12" cy="12" r="10" opacity="0.3"/></svg>`;
113
+ }
114
+ return `<svg width="${size}" height="${size}" viewBox="0 0 24 24" fill="currentColor" class="${className}" aria-hidden="true">${paths}</svg>`;
115
+ }
116
+
117
+ /**
118
+ * Crea un elemento SVG icon insertado en el DOM
119
+ * @param {string} name
120
+ * @param {number} [size=18]
121
+ * @returns {SVGElement}
122
+ */
123
+ export function iconEl(name, size = 18) {
124
+ const wrapper = document.createElement('span');
125
+ wrapper.innerHTML = icon(name, size);
126
+ return wrapper.firstElementChild;
127
+ }
128
+
129
+ export default { icon, iconEl };
@@ -0,0 +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">&times;</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 };