Phase 4: power UX (palette, shortcuts, mentions, mobile, PWA)

Command palette (cmd+K) with fuzzy nav, ticket search, people lookup
and action entries (new ticket, logout, show shortcuts). Opens from
keyboard or user dropdown.

Global keyboard shortcuts via a small useShortcut/useLeaderShortcut
hook: `?` help overlay, `c` new ticket, `g d|t|m|n|s` leader nav.
Tickets list: j/k cursor, Enter open, x toggle select. TicketDetail:
`e` edit, `r` focus comment composer. All guarded against firing
inside text fields.

@mention autocomplete in the comment composer (MentionTextarea) with
arrow-key nav and Tab/Enter insert. Rendered comments and audit log
rewrite @username tokens to links pointing at that user's assignee
filter; unknown usernames left as plain text.

Mobile sweep: TicketDetail sidebar stacks below content on <md,
Settings profile grid collapses to one column, admin tables get
horizontal scroll with a 640px min width, CTI 3-column grid stacks
vertically on <md, New ticket severity/assignee grid same.

PWA: manifest.webmanifest, SVG icon, minimal network-first service
worker for the app shell (never caches /api/*), registered in
production builds only. Theme-color meta + manifest link in index.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-18 16:36:28 -04:00
parent 4bade22410
commit ef22e92ac8
21 changed files with 976 additions and 28 deletions
+4
View File
@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<rect width="512" height="512" rx="96" fill="#2563eb"/>
<path fill="#ffffff" d="M128 144c0-8.8 7.2-16 16-16h224c8.8 0 16 7.2 16 16v48c0 8.8-7.2 16-16 16-8.8 0-16-7.2-16-16v-32H280v224h40c8.8 0 16 7.2 16 16s-7.2 16-16 16H192c-8.8 0-16-7.2-16-16s7.2-16 16-16h40V160h-72v32c0 8.8-7.2 16-16 16s-16-7.2-16-16v-48z"/>
</svg>

After

Width:  |  Height:  |  Size: 386 B

+13
View File
@@ -0,0 +1,13 @@
{
"name": "Ticketing System",
"short_name": "Tickets",
"description": "Homelab ticketing with CTI routing, severity triage and n8n integration.",
"start_url": "/dashboard",
"scope": "/",
"display": "standalone",
"background_color": "#0a0a0a",
"theme_color": "#2563eb",
"icons": [
{ "src": "/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable" }
]
}
+58
View File
@@ -0,0 +1,58 @@
// Minimal service worker: offline shell only. No data caching.
const CACHE = 'ticketing-shell-v1';
const SHELL = ['/', '/icon.svg', '/manifest.webmanifest'];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE).then((cache) => cache.addAll(SHELL)),
);
self.skipWaiting();
});
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((keys) =>
Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))),
),
);
self.clients.claim();
});
self.addEventListener('fetch', (event) => {
const req = event.request;
if (req.method !== 'GET') return;
const url = new URL(req.url);
// Never cache API calls — they must always hit the server
if (url.pathname.startsWith('/api/')) return;
// Network-first for navigation, fall back to cached shell
if (req.mode === 'navigate') {
event.respondWith(
fetch(req)
.then((res) => {
const clone = res.clone();
caches.open(CACHE).then((c) => c.put('/', clone));
return res;
})
.catch(() => caches.match('/')),
);
return;
}
// Cache-first for static assets
if (url.origin === self.location.origin) {
event.respondWith(
caches.match(req).then((cached) => {
if (cached) return cached;
return fetch(req).then((res) => {
if (res.ok && res.type === 'basic') {
const clone = res.clone();
caches.open(CACHE).then((c) => c.put(req, clone));
}
return res;
});
}),
);
}
});