diff --git a/resources/js/app.js b/resources/js/app.js index 08189de..6d49345 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -66,4 +66,132 @@ document.addEventListener('alpine:init', () => { get memLine() { return this._coords(this.mem); }, get memArea() { return `0,100 ${this._coords(this.mem)} 300,100`; }, })); + + // ── Command palette + keyboard shortcuts ──────────────────────────── + // German nav targets (mirrors the sidebar) with a leader-key hint. Leader + // mnemonics: i=dIenste, l=Log, e=Einstellungen, y=sYstem (d/s already taken). + const CMDK_NAV = [ + { label: 'Dashboard', href: '/', hint: 'g d' }, + { label: 'Server', href: '/servers', hint: 'g s' }, + { label: 'Dienste', href: '/services', hint: 'g i' }, + { label: 'Dateien', href: '/files', hint: 'g f' }, + { label: 'Audit-Log', href: '/audit', hint: 'g l' }, + { label: 'Einstellungen', href: '/settings', hint: 'g e' }, + { label: 'System', href: '/system', hint: 'g y' }, + { label: 'Version', href: '/versions', hint: 'g v' }, + ]; + const CMDK_ACTIONS = [ + { label: 'Server hinzufügen', action: 'create-server', hint: '' }, + ]; + const CMDK_GO = { d: '/', s: '/servers', i: '/services', f: '/files', l: '/audit', e: '/settings', y: '/system', v: '/versions' }; + + window.Alpine.data('cmdk', () => ({ + open: false, + help: false, + query: '', + active: 0, + leader: false, + leaderTimer: null, + nav: CMDK_NAV, + + init() { + // Reset overlays after an SPA navigation. The component is recreated on each + // wire:navigate, so we MUST remove this document listener in destroy() — else + // every navigation would accumulate another permanent callback. + this._onNav = () => { + this.open = false; + this.help = false; + this.leader = false; + }; + document.addEventListener('livewire:navigated', this._onNav); + }, + + destroy() { + document.removeEventListener('livewire:navigated', this._onNav); + }, + + get items() { + const q = this.query.trim().toLowerCase(); + const all = [...CMDK_NAV, ...CMDK_ACTIONS]; + return q === '' ? all : all.filter((i) => i.label.toLowerCase().includes(q)); + }, + + toggle(force) { + this.open = force === true ? true : ! this.open; + if (this.open) { + this.help = false; + this.query = ''; + this.active = 0; + this.$nextTick(() => this.$refs.input?.focus()); + } + }, + + close() { + this.open = false; + this.help = false; + }, + + move(d) { + const n = this.items.length; + if (n) this.active = (this.active + d + n) % n; + }, + + run(item) { + item = item || this.items[this.active] || null; + if (! item) return; + if (item.href) { + this.navigate(item.href); + } else if (item.action === 'create-server') { + this.close(); + window.Livewire?.dispatch('openModal', { component: 'modals.create-server' }); + } + }, + + navigate(href) { + this.close(); + if (window.Livewire?.navigate) window.Livewire.navigate(href); + else window.location.href = href; + }, + + focusSearch() { + const el = document.querySelector('[data-page-search]'); + if (el) { + el.focus(); + + return true; + } + + return false; + }, + + go(key) { + if (CMDK_GO[key]) this.navigate(CMDK_GO[key]); + }, + + onKey(e) { + // Cmd/Ctrl-K toggles the palette even from within an input. + if ((e.key === 'k' || e.key === 'K') && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + this.toggle(); + return; + } + if (this.open || this.help) return; // overlay keys handled in the markup + const t = e.target; + if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.tagName === 'SELECT' || t.isContentEditable)) return; + if (e.metaKey || e.ctrlKey || e.altKey) return; + // Only consume "/" when a page search exists — otherwise leave the browser's quick-find. + if (e.key === '/') { if (this.focusSearch()) e.preventDefault(); return; } + if (e.key === '?') { e.preventDefault(); this.help = true; return; } + if (this.leader) { + this.leader = false; + clearTimeout(this.leaderTimer); + this.go(e.key.toLowerCase()); + return; + } + if (e.key === 'g' || e.key === 'G') { + this.leader = true; + this.leaderTimer = setTimeout(() => { this.leader = false; }, 1200); + } + }, + })); }); diff --git a/resources/views/components/command-palette.blade.php b/resources/views/components/command-palette.blade.php new file mode 100644 index 0000000..04d94c2 --- /dev/null +++ b/resources/views/components/command-palette.blade.php @@ -0,0 +1,89 @@ +{{-- + Command palette + keyboard shortcuts. Mounted ONCE in the persistent layout so it + survives wire:navigate and its global keydown listener is registered only once. + Strg/⌘-K opens it; "g" + a key navigates; "/" focuses the page search; "?" shows help. +--}} +@php + $chords = [ + ['Strg / ⌘ K', 'Befehle öffnen'], + ['/', 'Suche auf der Seite fokussieren'], + ['?', 'Diese Hilfe anzeigen'], + ['g d', 'Dashboard'], + ['g s', 'Server'], + ['g i', 'Dienste'], + ['g f', 'Dateien'], + ['g l', 'Audit-Log'], + ['g e', 'Einstellungen'], + ['g y', 'System'], + ['g v', 'Version'], + ]; + $kbd = 'rounded border border-line bg-inset px-1.5 py-0.5 font-mono text-[10px] text-ink-2'; +@endphp +
+ + {{-- Command palette --}} + + + {{-- Shortcut help --}} + +
diff --git a/resources/views/components/icon.blade.php b/resources/views/components/icon.blade.php index e311e6d..a7497ac 100644 --- a/resources/views/components/icon.blade.php +++ b/resources/views/components/icon.blade.php @@ -15,6 +15,8 @@ 'folder' => '', 'audit' => '', 'file' => '', + 'command' => '', + 'corner-down-left' => '', 'activity' => '', 'switcher' => '', 'search' => '', diff --git a/resources/views/components/topbar.blade.php b/resources/views/components/topbar.blade.php index 2f73c48..dccc356 100644 --- a/resources/views/components/topbar.blade.php +++ b/resources/views/components/topbar.blade.php @@ -9,6 +9,12 @@

{{ $title }}

+ @php $fleetTotal = \App\Models\Server::count(); $fleetOnline = \App\Models\Server::where('status', 'online')->count(); diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index d2a309e..552d236 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -48,6 +48,9 @@
+ {{-- Command palette + keyboard shortcuts (persistent; one global keydown listener) --}} + + @livewireScripts diff --git a/resources/views/livewire/audit/index.blade.php b/resources/views/livewire/audit/index.blade.php index 4a50cde..4f05ca1 100644 --- a/resources/views/livewire/audit/index.blade.php +++ b/resources/views/livewire/audit/index.blade.php @@ -18,6 +18,7 @@