diff --git a/client/src/pages/Tickets.tsx b/client/src/pages/Tickets.tsx
index cdb4b1f..9ef2cc6 100644
--- a/client/src/pages/Tickets.tsx
+++ b/client/src/pages/Tickets.tsx
@@ -1,5 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
-import { Link, useSearchParams } from 'react-router-dom';
+import { Link, useNavigate, useSearchParams } from 'react-router-dom';
+import { useShortcut } from '../hooks/useShortcuts';
import { ChevronLeft, ChevronRight, Trash2, Save } from 'lucide-react';
import { formatDistanceToNow } from 'date-fns';
import { toast } from 'sonner';
@@ -69,6 +70,7 @@ function sevColor(severity: number) {
export default function Tickets() {
const [params, setParams] = useSearchParams();
+ const navigate = useNavigate();
const { user: authUser } = useAuth();
const { data: users = [] } = useUsers();
@@ -83,6 +85,7 @@ export default function Tickets() {
const [searchInput, setSearchInput] = useState(search);
const [selected, setSelected] = useState>(new Set());
+ const [cursor, setCursor] = useState(-1);
const [saveOpen, setSaveOpen] = useState(false);
const [newViewName, setNewViewName] = useState('');
const [confirmBulk, setConfirmBulk] = useState<
@@ -213,6 +216,35 @@ export default function Tickets() {
const agentUsers = users.filter((u) => u.role !== 'SERVICE');
+ // Keyboard navigation
+ useEffect(() => {
+ setCursor((c) => (c >= tickets.length ? tickets.length - 1 : c));
+ }, [tickets.length]);
+
+ useShortcut('j', (e) => {
+ if (tickets.length === 0) return;
+ e.preventDefault();
+ setCursor((c) => Math.min(tickets.length - 1, c < 0 ? 0 : c + 1));
+ }, [tickets.length]);
+
+ useShortcut('k', (e) => {
+ if (tickets.length === 0) return;
+ e.preventDefault();
+ setCursor((c) => Math.max(0, c < 0 ? 0 : c - 1));
+ }, [tickets.length]);
+
+ useShortcut('enter', (e) => {
+ if (cursor < 0 || cursor >= tickets.length) return;
+ e.preventDefault();
+ navigate(`/${tickets[cursor].displayId}`);
+ }, [cursor, tickets]);
+
+ useShortcut('x', (e) => {
+ if (cursor < 0 || cursor >= tickets.length) return;
+ e.preventDefault();
+ toggleOne(tickets[cursor].id);
+ }, [cursor, tickets]);
+
return (
{/* Status tabs */}
@@ -453,10 +485,14 @@ export default function Tickets() {
- {tickets.map((ticket) => (
+ {tickets.map((ticket, idx) => (
-
-
+
{/* Categories */}
diff --git a/client/src/pages/admin/Users.tsx b/client/src/pages/admin/Users.tsx
index 968a64d..e4ce438 100644
--- a/client/src/pages/admin/Users.tsx
+++ b/client/src/pages/admin/Users.tsx
@@ -193,8 +193,8 @@ export default function AdminUsers() {
}
>
-
-
+
+
diff --git a/client/src/pages/admin/Webhooks.tsx b/client/src/pages/admin/Webhooks.tsx
index 3d450af..6a1373b 100644
--- a/client/src/pages/admin/Webhooks.tsx
+++ b/client/src/pages/admin/Webhooks.tsx
@@ -116,8 +116,8 @@ export default function AdminWebhooks() {
No webhooks configured. Add one to push events to n8n, Slack, or any HTTP receiver.
) : (
-
-
+
+
| Name |
diff --git a/client/src/vite-env.d.ts b/client/src/vite-env.d.ts
new file mode 100644
index 0000000..11f02fe
--- /dev/null
+++ b/client/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
|