Skip to content

Commit ab587ab

Browse files
fabriziosalmiclaude
andcommitted
feat: keyboard shortcuts for power-user navigation, bump v1.11.1
Adds global keyboard shortcuts module (shortcuts.js): - ? to show/hide shortcut help overlay - / to focus certificate search (or open Cmd+K on other pages) - n to focus new certificate domain input - r to refresh certificate list - t to toggle dark mode - g+h/c/s/a/d for quick navigation (go to page) - Shortcuts suppressed when typing in inputs UI: keyboard icon in navbar, / hint badge in search input. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 413e596 commit ab587ab

File tree

4 files changed

+206
-3
lines changed

4 files changed

+206
-3
lines changed

app.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
CertMate - Modular SSL Certificate Management Application
33
Main application entry point with modular architecture
44
"""
5-
__version__ = '1.11.0'
5+
__version__ = '1.11.1'
66
import os
77
import sys
88
import tempfile

static/js/shortcuts.js

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/**
2+
* Keyboard Shortcuts — static/js/shortcuts.js
3+
* Global keyboard shortcuts for power-user navigation and actions.
4+
* Loaded after certmate.js in base.html.
5+
*/
6+
(function() {
7+
'use strict';
8+
9+
var shortcutOverlay = null;
10+
11+
var shortcuts = [
12+
{ key: '?', desc: 'Show keyboard shortcuts' },
13+
{ key: '/', desc: 'Focus search / filter' },
14+
{ key: 'n', desc: 'New certificate (focus domain input)' },
15+
{ key: 'r', desc: 'Refresh certificate list' },
16+
{ key: 't', desc: 'Toggle dark mode' },
17+
{ key: 'g h', desc: 'Go to Certificates' },
18+
{ key: 'g c', desc: 'Go to Client Certificates' },
19+
{ key: 'g s', desc: 'Go to Settings' },
20+
{ key: 'g a', desc: 'Go to Activity' },
21+
{ key: 'g d', desc: 'Go to API Docs' },
22+
{ key: 'Esc', desc: 'Close panel / overlay' }
23+
];
24+
25+
// "g" prefix state for two-key navigation combos
26+
var gPending = false;
27+
var gTimer = null;
28+
29+
function isInputFocused() {
30+
var el = document.activeElement;
31+
if (!el) return false;
32+
var tag = el.tagName;
33+
if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
34+
if (el.isContentEditable) return true;
35+
return false;
36+
}
37+
38+
function createOverlay() {
39+
var div = document.createElement('div');
40+
div.id = 'shortcutOverlay';
41+
div.className = 'fixed inset-0 z-[101] hidden';
42+
var cols = '';
43+
shortcuts.forEach(function(s) {
44+
var keys = s.key.split(' ');
45+
var kbds = keys.map(function(k) {
46+
return '<kbd class="inline-flex items-center justify-center min-w-[28px] px-2 py-1 text-xs font-mono font-semibold ' +
47+
'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-200 border border-gray-300 dark:border-gray-600 rounded shadow-sm">' +
48+
CertMate.escapeHtml(k) + '</kbd>';
49+
}).join('<span class="mx-1 text-gray-400 text-xs">then</span>');
50+
cols += '<div class="flex items-center justify-between py-1.5">' +
51+
'<span class="text-sm text-gray-700 dark:text-gray-300">' + CertMate.escapeHtml(s.desc) + '</span>' +
52+
'<span class="ml-4 flex items-center gap-1">' + kbds + '</span>' +
53+
'</div>';
54+
});
55+
56+
div.innerHTML =
57+
'<div class="fixed inset-0 bg-black/50 backdrop-blur-sm" id="shortcutOverlayBg"></div>' +
58+
'<div class="fixed inset-x-4 top-[12vh] sm:inset-x-auto sm:left-1/2 sm:-translate-x-1/2 sm:w-full sm:max-w-md ' +
59+
'bg-white dark:bg-gray-800 rounded-xl shadow-2xl border border-gray-200 dark:border-gray-700 overflow-hidden">' +
60+
'<div class="px-5 py-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">' +
61+
'<h3 class="text-sm font-semibold text-gray-900 dark:text-white"><i class="fas fa-keyboard mr-2 text-gray-400"></i>Keyboard Shortcuts</h3>' +
62+
'<button type="button" id="shortcutOverlayClose" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-200" title="Close">' +
63+
'<i class="fas fa-times"></i>' +
64+
'</button>' +
65+
'</div>' +
66+
'<div class="px-5 py-3 divide-y divide-gray-100 dark:divide-gray-700/50">' + cols + '</div>' +
67+
'<div class="px-5 py-2 border-t border-gray-200 dark:border-gray-700 text-xs text-gray-400 text-center">' +
68+
'Press <kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-xs">?</kbd> to toggle &middot; ' +
69+
'<kbd class="px-1.5 py-0.5 bg-gray-100 dark:bg-gray-700 rounded text-xs">Esc</kbd> to close' +
70+
'</div>' +
71+
'</div>';
72+
73+
document.body.appendChild(div);
74+
shortcutOverlay = div;
75+
76+
document.getElementById('shortcutOverlayBg').addEventListener('click', closeOverlay);
77+
document.getElementById('shortcutOverlayClose').addEventListener('click', closeOverlay);
78+
}
79+
80+
function openOverlay() {
81+
if (!shortcutOverlay) createOverlay();
82+
shortcutOverlay.classList.remove('hidden');
83+
}
84+
85+
function closeOverlay() {
86+
if (shortcutOverlay) shortcutOverlay.classList.add('hidden');
87+
}
88+
89+
function isOverlayOpen() {
90+
return shortcutOverlay && !shortcutOverlay.classList.contains('hidden');
91+
}
92+
93+
function cancelG() {
94+
gPending = false;
95+
if (gTimer) {
96+
clearTimeout(gTimer);
97+
gTimer = null;
98+
}
99+
}
100+
101+
document.addEventListener('keydown', function(e) {
102+
// Ignore when modifier keys are held (except Shift for ?)
103+
if (e.ctrlKey || e.metaKey || e.altKey) return;
104+
105+
// Escape always works — close overlays/panels
106+
if (e.key === 'Escape') {
107+
if (isOverlayOpen()) {
108+
e.preventDefault();
109+
closeOverlay();
110+
return;
111+
}
112+
// Let other handlers (cmd-palette, cert detail) handle Escape
113+
cancelG();
114+
return;
115+
}
116+
117+
// All other shortcuts suppressed when typing in inputs
118+
if (isInputFocused()) {
119+
cancelG();
120+
return;
121+
}
122+
123+
// Handle "g" prefix combos
124+
if (gPending) {
125+
cancelG();
126+
e.preventDefault();
127+
switch (e.key) {
128+
case 'h': window.location.href = '/'; break;
129+
case 'c': window.location.href = '/#client'; break;
130+
case 's': window.location.href = '/settings'; break;
131+
case 'a': window.location.href = '/activity'; break;
132+
case 'd': window.location.href = '/redoc'; break;
133+
}
134+
return;
135+
}
136+
137+
// Single-key shortcuts
138+
switch (e.key) {
139+
case '?':
140+
e.preventDefault();
141+
if (isOverlayOpen()) {
142+
closeOverlay();
143+
} else {
144+
openOverlay();
145+
}
146+
break;
147+
148+
case '/':
149+
e.preventDefault();
150+
// Focus certificate search if on dashboard, otherwise open Cmd+K
151+
var searchEl = document.getElementById('certificateSearch');
152+
if (searchEl) {
153+
searchEl.focus();
154+
searchEl.select();
155+
} else {
156+
// Trigger Cmd+K palette on non-dashboard pages
157+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', metaKey: true }));
158+
}
159+
break;
160+
161+
case 'n':
162+
e.preventDefault();
163+
var domainInput = document.getElementById('domain');
164+
if (domainInput) {
165+
domainInput.focus();
166+
domainInput.scrollIntoView({ behavior: 'smooth', block: 'center' });
167+
} else {
168+
// Navigate to dashboard first
169+
window.location.href = '/';
170+
}
171+
break;
172+
173+
case 'r':
174+
e.preventDefault();
175+
if (typeof window.loadCertificates === 'function') {
176+
window.loadCertificates();
177+
if (typeof CertMate !== 'undefined' && CertMate.toast) {
178+
CertMate.toast('Refreshing certificates...', 'info');
179+
}
180+
}
181+
break;
182+
183+
case 't':
184+
e.preventDefault();
185+
if (typeof toggleTheme === 'function') toggleTheme();
186+
break;
187+
188+
case 'g':
189+
e.preventDefault();
190+
gPending = true;
191+
gTimer = setTimeout(cancelG, 1500);
192+
break;
193+
}
194+
});
195+
})();

templates/base.html

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ <h1 class="text-lg md:text-xl font-semibold text-gray-900 dark:text-white">CertM
5959
<span class="text-xs">Search</span>
6060
<kbd class="ml-2 px-1.5 py-0.5 text-[10px] bg-white dark:bg-gray-800 rounded border border-gray-300 dark:border-gray-500">&#8984;K</kbd>
6161
</button>
62+
<!-- Keyboard shortcuts hint -->
63+
<button type="button" onclick="document.dispatchEvent(new KeyboardEvent('keydown',{key:'?'}))" class="hidden sm:inline-flex items-center px-2 py-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition" title="Keyboard shortcuts (?)">
64+
<i class="fas fa-keyboard text-sm"></i>
65+
</button>
6266
<!-- Notification bell -->
6367
<div class="relative">
6468
<button type="button" id="notifBtn" onclick="toggleNotifications()" class="px-2 py-2 rounded-md text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 relative" title="Notifications">
@@ -263,6 +267,7 @@ <h3 class="text-sm font-semibold text-gray-900 dark:text-white">Notifications</h
263267
<script defer src="/static/js/alpine.min.js"></script>
264268
<script src="/static/js/certmate.js"></script>
265269
<script src="/static/js/cmd-palette.js"></script>
270+
<script src="/static/js/shortcuts.js"></script>
266271
<script src="/static/js/setup-wizard.js"></script>
267272

268273
{% block content %}{% endblock %}

templates/index.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,11 +264,14 @@ <h3 class="text-xl font-bold text-gray-900 dark:text-white flex items-center">
264264
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Your Certificates</h3>
265265
<div class="flex flex-col sm:flex-row items-stretch sm:items-center space-y-2 sm:space-y-0 sm:space-x-2 lg:space-x-3">
266266
<div class="relative">
267-
<input type="text" id="certificateSearch" placeholder="Search certificates..."
268-
class="pl-10 pr-4 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm bg-white shadow-sm transition-shadow duration-200 w-full sm:w-48">
267+
<input type="text" id="certificateSearch" placeholder="Search certificates..."
268+
class="pl-10 pr-8 py-2 border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg focus:ring-2 focus:ring-primary focus:border-primary text-sm bg-white shadow-sm transition-shadow duration-200 w-full sm:w-48">
269269
<div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
270270
<i class="fas fa-search text-gray-400"></i>
271271
</div>
272+
<div class="absolute inset-y-0 right-0 pr-2 hidden sm:flex items-center pointer-events-none">
273+
<kbd class="px-1.5 py-0.5 text-[10px] text-gray-400 bg-gray-100 dark:bg-gray-600 rounded border border-gray-300 dark:border-gray-500">/</kbd>
274+
</div>
272275
</div>
273276
<select id="statusFilter" class="border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-white rounded-lg px-3 py-2 text-sm focus:ring-2 focus:ring-primary focus:border-primary bg-white shadow-sm transition-shadow duration-200">
274277
<option value="all">All Status</option>

0 commit comments

Comments
 (0)