5a622bc
commit 5a622bc
2 files changed
+224 -1
Showing 2 changed files+224-1
▼src/lib/inbox-shortcuts.ts
+93-0
| @@ -0,0 +1,93 @@ | ||
1 | +/** | |
2 | + * Inbox Keyboard Shortcuts | |
3 | + * Graphite-style keyboard navigation for PR inbox | |
4 | + */ | |
5 | + | |
6 | +// Keyboard shortcut handler | |
7 | +export function initInboxKeyboardShortcuts() { | |
8 | + let selectedIndex = 0; | |
9 | + const prItems = document.querySelectorAll('[data-pr-item]'); | |
10 | + | |
11 | + if (prItems.length === 0) return; | |
12 | + | |
13 | + // Highlight the first item | |
14 | + const highlightPR = (index: number) => { | |
15 | + prItems.forEach((item, i) => { | |
16 | + if (i === index) { | |
17 | + item.classList.add('ring-2', 'ring-primary', 'bg-primary/5'); | |
18 | + item.scrollIntoView({ block: 'nearest', behavior: 'smooth' }); | |
19 | + } else { | |
20 | + item.classList.remove('ring-2', 'ring-primary', 'bg-primary/5'); | |
21 | + } | |
22 | + }); | |
23 | + }; | |
24 | + | |
25 | + // Initialize | |
26 | + highlightPR(0); | |
27 | + | |
28 | + // Add keyboard listener | |
29 | + document.addEventListener('keydown', (e) => { | |
30 | + // Don't trigger if user is typing in an input | |
31 | + if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) { | |
32 | + return; | |
33 | + } | |
34 | + | |
35 | + switch (e.key.toLowerCase()) { | |
36 | + case 'j': // Next PR | |
37 | + e.preventDefault(); | |
38 | + selectedIndex = Math.min(selectedIndex + 1, prItems.length - 1); | |
39 | + highlightPR(selectedIndex); | |
40 | + break; | |
41 | + | |
42 | + case 'k': // Previous PR | |
43 | + e.preventDefault(); | |
44 | + selectedIndex = Math.max(selectedIndex - 1, 0); | |
45 | + highlightPR(selectedIndex); | |
46 | + break; | |
47 | + | |
48 | + case 'o': // Open PR | |
49 | + case 'enter': | |
50 | + e.preventDefault(); | |
51 | + const selectedItem = prItems[selectedIndex] as HTMLAnchorElement; | |
52 | + selectedItem?.click(); | |
53 | + break; | |
54 | + | |
55 | + case 'r': // Refresh | |
56 | + e.preventDefault(); | |
57 | + window.location.reload(); | |
58 | + break; | |
59 | + | |
60 | + case '?': // Show help | |
61 | + e.preventDefault(); | |
62 | + toggleHelpModal(); | |
63 | + break; | |
64 | + | |
65 | + case 'escape': | |
66 | + e.preventDefault(); | |
67 | + closeHelpModal(); | |
68 | + break; | |
69 | + } | |
70 | + }); | |
71 | + | |
72 | + // Help modal | |
73 | + function toggleHelpModal() { | |
74 | + const modal = document.getElementById('keyboard-shortcuts-modal'); | |
75 | + if (modal) { | |
76 | + modal.classList.toggle('hidden'); | |
77 | + } | |
78 | + } | |
79 | + | |
80 | + function closeHelpModal() { | |
81 | + const modal = document.getElementById('keyboard-shortcuts-modal'); | |
82 | + if (modal) { | |
83 | + modal.classList.add('hidden'); | |
84 | + } | |
85 | + } | |
86 | + | |
87 | + console.log('✅ Inbox keyboard shortcuts active'); | |
88 | +} | |
89 | + | |
90 | +// Auto-init on page load | |
91 | +if (typeof window !== 'undefined') { | |
92 | + document.addEventListener('DOMContentLoaded', initInboxKeyboardShortcuts); | |
93 | +} | |
0 | 94 | |
▼src/pages/inbox.astro
+131-1
| @@ -255,8 +255,9 @@ function timeAgo(date: string) { | ||
255 | 255 | ) : ( |
256 | 256 | prsWithStacks.map((pr) => ( |
257 | 257 | <a |
258 | + data-pr-item | |
258 | 259 | href={`/${pr.repository?.name}/pulls/${pr.number}`} |
259 | - class="flex items-start gap-4 p-4 rounded-lg border transition-all hover:bg-muted/50 hover:shadow-md group" | |
260 | + class="flex items-start gap-4 p-4 rounded-lg border transition-all hover:bg-muted/50 hover:shadow-md group cursor-pointer" | |
260 | 261 | > |
261 | 262 | {/* PR Icon */} |
262 | 263 | <div |
| @@ -320,4 +321,133 @@ function timeAgo(date: string) { | ||
320 | 321 | </div> |
321 | 322 | </div> |
322 | 323 | </div> |
324 | + | |
325 | + <!-- Keyboard Shortcuts Help Modal --> | |
326 | + <div | |
327 | + id="keyboard-shortcuts-modal" | |
328 | + class="hidden fixed inset-0 bg-black/50 flex items-center justify-center z-50" | |
329 | + > | |
330 | + <div | |
331 | + class="bg-background border rounded-lg shadow-lg p-6 max-w-md w-full mx-4" | |
332 | + > | |
333 | + <h3 class="text-lg font-semibold mb-4">Keyboard Shortcuts</h3> | |
334 | + <div class="space-y-2 text-sm"> | |
335 | + <div class="flex justify-between"> | |
336 | + <kbd class="px-2 py-1 bg-muted rounded text-xs">j</kbd> | |
337 | + <span>Next PR</span> | |
338 | + </div> | |
339 | + <div class="flex justify-between"> | |
340 | + <kbd class="px-2 py-1 bg-muted rounded text-xs">k</kbd> | |
341 | + <span>Previous PR</span> | |
342 | + </div> | |
343 | + <div class="flex justify-between"> | |
344 | + <kbd class="px-2 py-1 bg-muted rounded text-xs">o</kbd> | |
345 | + <span>Open selected PR</span> | |
346 | + </div> | |
347 | + <div class="flex justify-between"> | |
348 | + <kbd class="px-2 py-1 bg-muted rounded text-xs">r</kbd> | |
349 | + <span>Refresh</span> | |
350 | + </div> | |
351 | + <div class="flex justify-between"> | |
352 | + <kbd class="px-2 py-1 bg-muted rounded text-xs">?</kbd> | |
353 | + <span>Show this help</span> | |
354 | + </div> | |
355 | + <div class="flex justify-between"> | |
356 | + <kbd class="px-2 py-1 bg-muted rounded text-xs">Esc</kbd> | |
357 | + <span>Close dialog</span> | |
358 | + </div> | |
359 | + </div> | |
360 | + <button | |
361 | + onclick="document.getElementById('keyboard-shortcuts-modal').classList.add('hidden')" | |
362 | + class="mt-4 w-full px-4 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90" | |
363 | + > | |
364 | + Got it | |
365 | + </button> | |
366 | + </div> | |
367 | + </div> | |
368 | + | |
369 | + <!-- Keyboard hint --> | |
370 | + <div | |
371 | + class="fixed bottom-4 right-4 text-xs text-muted-foreground bg-background border rounded-lg px-3 py-2 shadow-lg" | |
372 | + > | |
373 | + Press <kbd class="px-1.5 py-0.5 bg-muted rounded text-xs mx-0.5">?</kbd> for keyboard | |
374 | + shortcuts | |
375 | + </div> | |
376 | + | |
377 | + <script> | |
378 | + // Keyboard shortcut handler | |
379 | + function initInboxKeyboardShortcuts() { | |
380 | + let selectedIndex = 0; | |
381 | + const prItems = document.querySelectorAll("[data-pr-item]"); | |
382 | + | |
383 | + if (prItems.length === 0) return; | |
384 | + | |
385 | + // Highlight the first item | |
386 | + const highlightPR = (index) => { | |
387 | + prItems.forEach((item, i) => { | |
388 | + if (i === index) { | |
389 | + item.classList.add("ring-2", "ring-primary", "bg-primary/5"); | |
390 | + item.scrollIntoView({ block: "nearest", behavior: "smooth" }); | |
391 | + } else { | |
392 | + item.classList.remove("ring-2", "ring-primary", "bg-primary/5"); | |
393 | + } | |
394 | + }); | |
395 | + }; | |
396 | + | |
397 | + // Initialize | |
398 | + highlightPR(0); | |
399 | + | |
400 | + // Add keyboard listener | |
401 | + document.addEventListener("keydown", (e) => { | |
402 | + // Don't trigger if user is typing in an input | |
403 | + if (e.target.tagName === "INPUT" || e.target.tagName === "TEXTAREA") { | |
404 | + return; | |
405 | + } | |
406 | + | |
407 | + switch (e.key.toLowerCase()) { | |
408 | + case "j": // Next PR | |
409 | + e.preventDefault(); | |
410 | + selectedIndex = Math.min(selectedIndex + 1, prItems.length - 1); | |
411 | + highlightPR(selectedIndex); | |
412 | + break; | |
413 | + | |
414 | + case "k": // Previous PR | |
415 | + e.preventDefault(); | |
416 | + selectedIndex = Math.max(selectedIndex - 1, 0); | |
417 | + highlightPR(selectedIndex); | |
418 | + break; | |
419 | + | |
420 | + case "o": // Open PR | |
421 | + case "enter": | |
422 | + e.preventDefault(); | |
423 | + prItems[selectedIndex]?.click(); | |
424 | + break; | |
425 | + | |
426 | + case "r": // Refresh | |
427 | + e.preventDefault(); | |
428 | + window.location.reload(); | |
429 | + break; | |
430 | + | |
431 | + case "?": // Show help | |
432 | + e.preventDefault(); | |
433 | + document | |
434 | + .getElementById("keyboard-shortcuts-modal") | |
435 | + ?.classList.toggle("hidden"); | |
436 | + break; | |
437 | + | |
438 | + case "escape": | |
439 | + e.preventDefault(); | |
440 | + document | |
441 | + .getElementById("keyboard-shortcuts-modal") | |
442 | + ?.classList.add("hidden"); | |
443 | + break; | |
444 | + } | |
445 | + }); | |
446 | + | |
447 | + console.log("✅ Inbox keyboard shortcuts active"); | |
448 | + } | |
449 | + | |
450 | + // Auto-init on page load | |
451 | + initInboxKeyboardShortcuts(); | |
452 | + </script> | |
323 | 453 | </BaseLayout> |
324 | 454 | |