141 lines
7.4 KiB
PHP
141 lines
7.4 KiB
PHP
{{--
|
|
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
|
|
// Every navigable page (mirrors the 3-group sidebar), each ROLE-GATED so the palette + its
|
|
// "g <key>" shortcut only ever offer pages the user can actually open — go() resolves a chord
|
|
// via this list, so a gated-out page has neither an entry nor a live shortcut (no 403 dead-end).
|
|
// [ lang key, href, chord letter (null = search-only), required ability (null = always) ]
|
|
$u = auth()->user();
|
|
$navDefs = [
|
|
// Fleet
|
|
['shell.nav_dashboard', '/', 'd', null],
|
|
['shell.nav_servers', '/servers', 's', null],
|
|
['shell.nav_services', '/services', 'i', null],
|
|
['shell.nav_commands', '/commands', 'c', 'operate'],
|
|
['shell.nav_files', '/files', 'f', null],
|
|
['shell.nav_audit', '/audit', 'l', null],
|
|
// Host
|
|
['shell.nav_docker', '/docker', 'k', 'manage-fleet'],
|
|
['shell.nav_terminal', '/terminal', 't', 'manage-fleet'],
|
|
['shell.nav_wireguard', '/wireguard', 'w', 'manage-network'],
|
|
['shell.nav_system', '/system', 'y', null],
|
|
['shell.nav_posture', '/posture', 'p', 'operate'],
|
|
['shell.nav_patch', '/patch', 'u', 'operate'],
|
|
['shell.nav_certs', '/certs', 'r', 'operate'],
|
|
['shell.nav_health', '/health', 'o', 'operate'],
|
|
// Account
|
|
['shell.nav_settings', '/settings', 'e', null],
|
|
['shell.nav_alerts', '/alerts', 'a', 'manage-panel'],
|
|
['shell.nav_threats', '/threats', 'b', 'manage-panel'],
|
|
['shell.nav_versions', '/versions', 'v', null],
|
|
['shell.nav_help', config('clusev.docs_url'), 'h', null],
|
|
];
|
|
|
|
$nav = [];
|
|
foreach ($navDefs as [$key, $href, $chord, $gate]) {
|
|
if ($gate !== null && ! ($u?->can($gate) ?? false)) {
|
|
continue;
|
|
}
|
|
$nav[] = ['label' => __($key), 'href' => $href, 'hint' => $chord ? 'g '.$chord : ''];
|
|
}
|
|
|
|
// Help panel: the fixed utility keys + one "g <key>" row per shortcut the user actually has.
|
|
$chords = [
|
|
['Strg / ⌘ K', __('shell.cmdk_open')],
|
|
['/', __('shell.cmdk_focus_search')],
|
|
['?', __('shell.cmdk_show_help')],
|
|
];
|
|
foreach ($nav as $n) {
|
|
if ($n['hint'] !== '') {
|
|
$chords[] = [str_replace('g ', 'g ', $n['hint']), $n['label']];
|
|
}
|
|
}
|
|
|
|
$actions = [
|
|
['label' => __('servers.add_server'), 'action' => 'create-server', 'hint' => ''],
|
|
];
|
|
|
|
// Fleet servers become palette entries so "Strg/⌘ K → name/IP" jumps straight to a
|
|
// server's details. `search` carries name + IP so either matches; the hint shows the IP.
|
|
$servers = \App\Models\Server::orderBy('name')->get(['uuid', 'name', 'ip'])
|
|
->map(fn ($s) => [
|
|
'label' => $s->name,
|
|
'href' => '/servers/'.$s->uuid,
|
|
'hint' => $s->ip,
|
|
'search' => $s->name.' '.$s->ip,
|
|
])->all();
|
|
|
|
$kbd = 'rounded border border-line bg-inset px-1.5 py-0.5 font-mono text-[10px] text-ink-2';
|
|
@endphp
|
|
<div x-data="cmdk(@js($nav), @js($actions), @js($servers), @js(__('shell.toast_modal_failed')))"
|
|
@keydown.window="onKey($event)"
|
|
@keydown.escape.window="close()"
|
|
@cmdk-open.window="toggle(true)">
|
|
|
|
{{-- Command palette --}}
|
|
<div x-show="open" x-cloak class="fixed inset-0 z-50 flex items-start justify-center px-4 pt-[12vh]" role="dialog" aria-modal="true">
|
|
<div class="fixed inset-0 bg-void/70 backdrop-blur-sm" @click="close()" aria-hidden="true"></div>
|
|
|
|
<div x-show="open" x-transition
|
|
class="relative w-full max-w-xl overflow-hidden rounded-lg border border-line bg-surface shadow-pop">
|
|
<div class="flex items-center gap-2.5 border-b border-line px-4">
|
|
<x-icon name="search" class="h-4 w-4 shrink-0 text-ink-3" />
|
|
<input x-ref="input" x-model="query" type="text" autocomplete="off" spellcheck="false"
|
|
placeholder="{{ __('shell.cmdk_placeholder') }}"
|
|
@input="active = 0"
|
|
@keydown.down.prevent="move(1)"
|
|
@keydown.up.prevent="move(-1)"
|
|
@keydown.enter.prevent="run()"
|
|
class="h-12 w-full bg-transparent font-mono text-sm text-ink placeholder:text-ink-4 focus:outline-none" />
|
|
<span class="shrink-0 font-mono text-[10px] uppercase tracking-wider text-ink-4">{{ __('shell.cmdk_esc') }}</span>
|
|
</div>
|
|
|
|
<ul class="max-h-80 overflow-y-auto py-1.5">
|
|
<template x-for="(item, idx) in items" :key="item.href || item.label">
|
|
<li>
|
|
<button type="button"
|
|
@click="run(item)"
|
|
@mouseenter="active = idx"
|
|
:class="active === idx ? 'bg-accent/10 text-accent-text' : 'text-ink-2'"
|
|
class="flex w-full items-center justify-between gap-3 px-4 py-2 text-left">
|
|
<span class="truncate font-mono text-sm" x-text="item.label"></span>
|
|
<span class="shrink-0 font-mono text-[10px] uppercase tracking-wider text-ink-4" x-text="item.hint"></span>
|
|
</button>
|
|
</li>
|
|
</template>
|
|
<li x-show="items.length === 0" class="px-4 py-3 font-mono text-[11px] text-ink-4">{{ __('shell.cmdk_no_match') }}</li>
|
|
</ul>
|
|
|
|
<div class="flex items-center justify-between border-t border-line px-4 py-2 font-mono text-[10px] text-ink-4">
|
|
<span class="flex items-center gap-1.5"><x-icon name="corner-down-left" class="h-3 w-3" /> {{ __('shell.cmdk_open_hint') }}</span>
|
|
<button type="button" @click="open = false; help = true" class="hover:text-ink-2">{{ __('shell.cmdk_shortcuts_hint') }}</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{{-- Shortcut help --}}
|
|
<div x-show="help" x-cloak class="fixed inset-0 z-50 flex items-start justify-center px-4 pt-[12vh]" role="dialog" aria-modal="true">
|
|
<div class="fixed inset-0 bg-void/70 backdrop-blur-sm" @click="help = false" aria-hidden="true"></div>
|
|
<div x-show="help" x-transition class="relative w-full max-w-md overflow-hidden rounded-lg border border-line bg-surface shadow-pop">
|
|
<div class="flex items-center gap-2.5 border-b border-line px-4 py-3">
|
|
<x-icon name="command" class="h-4 w-4 shrink-0 text-accent-text" />
|
|
<h2 class="font-display text-sm font-semibold text-ink">{{ __('shell.shortcuts_heading') }}</h2>
|
|
<button type="button" @click="help = false" class="ml-auto text-ink-4 hover:text-ink" aria-label="{{ __('common.close') }}">
|
|
<x-icon name="x" class="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
<dl class="divide-y divide-line">
|
|
@foreach ($chords as [$chord, $desc])
|
|
<div class="flex items-center justify-between gap-3 px-4 py-2">
|
|
<dt class="text-sm text-ink-2">{{ $desc }}</dt>
|
|
<dd class="shrink-0"><span class="{{ $kbd }}">{{ $chord }}</span></dd>
|
|
</div>
|
|
@endforeach
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|