polish(ui): in-system design refinements from the design review

Surgical consistency/polish tightening within the existing "Tactical Terminal"
system — no re-theme, no new tokens/fonts/colors, every change uses an existing
@theme utility. From a design review (subagent-cataloged, false-positives rejected).

- nav-item: badge tooltip is now a contextual `badge-title` prop instead of a hardcoded
  "update available" — the Threats count badge showed the wrong tooltip. Sidebar passes a
  real "N login attempts (24h)" title for Threats and "Update available" for Versions.
- empty-state messages (sessions/users/webauthn-keys/login-protection) bumped from the
  faintest ink-4 to ink-3 — when a list is empty that line is the focal point, not meta.
- modal actions: fail2ban-config / hardening-action / system-update submit buttons go from
  the softer `accent` to solid `primary` so the real action reads as primary (cancel stays
  secondary; the done-state close is unchanged).
- servers list: CPU/RAM mini-bar labels ink-4 -> ink-3 to match their values.
- edit-credential: top-level error now uses the standard bordered offline error box
  (border-offline/25 bg-offline/10 + alert icon), matching firewall-rule.
- file-editor: title + path get a native `title=` tooltip so a truncated long path is legible.
- add-ssh-key label rhythm (mb-1.5 -> mb-1); login-protection textarea vertical padding
  (py-2 -> py-2.5, the textarea norm); webauthn-keys add button w-full sm:w-auto so it isn't
  a mobile orphan; app.css font comment de-staled (fonts are self-hosted).

Deliberately NOT changed (would harm the design, not help): mail from_name kept sans vs
from_address mono (name = prose, address = technical token — the convention); the Memory
metric tone kept dynamic/threshold-based (health signal) rather than forced cyan.

Verified: vite build compiles all utilities, 617 tests green, de/en lang parity, DOM render
check of the badge + button variants.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
feat/v1-foundation
boban 2026-07-05 18:06:32 +02:00
parent 0943d56201
commit 930f78efbd
16 changed files with 26 additions and 23 deletions

View File

@ -26,6 +26,7 @@ return [
'nav_versions' => 'Version',
'nav_help' => 'Hilfe',
'update_available' => 'Update verfügbar',
'threats_badge' => ':count Login-Versuche (24h)',
// Sidebar — user / 2FA badge
'twofa_on' => '2FA aktiv',

View File

@ -26,6 +26,7 @@ return [
'nav_versions' => 'Version',
'nav_help' => 'Help',
'update_available' => 'Update available',
'threats_badge' => ':count login attempts (24h)',
// Sidebar — user / 2FA badge
'twofa_on' => '2FA on',

View File

@ -85,7 +85,7 @@
--color-line-soft: rgba(255, 255, 255, 0.055);
--color-line-strong: rgba(255, 255, 255, 0.17);
/* fonts — TODO(§12): self-host the .woff2; falls back until then */
/* fonts — self-hosted .woff2 via the @font-face blocks above (R14); system stack is the fallback */
--font-display: 'Chakra Petch', ui-sans-serif, system-ui, sans-serif;
--font-sans: 'Space Grotesk', ui-sans-serif, system-ui, sans-serif;
--font-mono: 'JetBrains Mono', ui-monospace, 'SF Mono', monospace;

View File

@ -1,4 +1,4 @@
@props(['icon', 'href' => '#', 'active' => false, 'badge' => null])
@props(['icon', 'href' => '#', 'active' => false, 'badge' => null, 'badgeTitle' => null])
<a {{ $attributes->class([
'group flex min-h-11 items-center gap-3 rounded-md border px-3 py-2 text-sm transition-colors',
'border-accent/25 bg-accent/10 text-ink shadow-[inset_2px_0_0_var(--color-accent)]' => $active,
@ -9,6 +9,6 @@
<span class="truncate">{{ $slot }}</span>
@if ($badge !== null && $badge !== '')
<span class="ml-auto inline-flex h-4 min-w-4 shrink-0 items-center justify-center rounded-full bg-accent px-1 font-mono text-[10px] font-semibold tabular-nums text-void"
title="{{ __('shell.update_available') }}">{{ $badge }}</span>
@if ($badgeTitle) title="{{ $badgeTitle }}" @endif>{{ $badge }}</span>
@endif
</a>

View File

@ -51,9 +51,9 @@
<x-nav-item icon="settings" href="/settings" :active="request()->is('settings*')" data-tour="settings">{{ __('shell.nav_settings') }}</x-nav-item>
<x-nav-item icon="shield" href="/system" :active="request()->is('system*')">{{ __('shell.nav_system') }}</x-nav-item>
@can('manage-panel')
<x-nav-item icon="shield-alert" href="/threats" :active="request()->is('threats*')" :badge="$threatCount > 0 ? $threatCount : null">{{ __('shell.nav_threats') }}</x-nav-item>
<x-nav-item icon="shield-alert" href="/threats" :active="request()->is('threats*')" :badge="$threatCount > 0 ? $threatCount : null" :badge-title="$threatCount > 0 ? __('shell.threats_badge', ['count' => $threatCount]) : null">{{ __('shell.nav_threats') }}</x-nav-item>
@endcan
<x-nav-item icon="tag" href="/versions" :active="request()->is('versions*')" :badge="$updateAvailable ? '1' : null">{{ __('shell.nav_versions') }}</x-nav-item>
<x-nav-item icon="tag" href="/versions" :active="request()->is('versions*')" :badge="$updateAvailable ? '1' : null" :badge-title="$updateAvailable ? __('shell.update_available') : null">{{ __('shell.nav_versions') }}</x-nav-item>
@if (config('clusev.release_controls'))
<x-nav-item icon="tag" href="/release" :active="request()->is('release*')">
{{ __('release.nav') }}

View File

@ -10,13 +10,13 @@
</div>
<div class="mt-4">
<label class="mb-1.5 block font-mono text-[11px] uppercase tracking-wider text-ink-3">{{ __('modals.add_ssh_key.name_label') }}</label>
<label class="mb-1 block font-mono text-[11px] uppercase tracking-wider text-ink-3">{{ __('modals.add_ssh_key.name_label') }}</label>
<input wire:model="keyName" type="text" maxlength="64" placeholder="{{ __('modals.add_ssh_key.name_placeholder') }}"
class="h-9 w-full rounded-md border border-line bg-inset px-3 font-mono text-sm text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none" />
</div>
<div class="mt-4">
<div class="mb-1.5 flex items-center justify-between">
<div class="mb-1 flex items-center justify-between">
<label class="font-mono text-[11px] uppercase tracking-wider text-ink-3">{{ __('modals.add_ssh_key.public_key_label') }}</label>
<button type="button" wire:click="generate" wire:target="generate" wire:loading.attr="disabled" class="font-mono text-[11px] text-accent-text transition-colors hover:text-accent disabled:opacity-50">
<span wire:loading.remove wire:target="generate">{{ __('modals.add_ssh_key.generate') }}</span>

View File

@ -47,9 +47,10 @@
@endif
@if ($error)
<p class="flex items-center gap-1.5 font-mono text-[11px] text-offline">
<x-icon name="alert" class="h-3.5 w-3.5 shrink-0" />{{ $error }}
</p>
<div class="flex items-center gap-2.5 rounded-md border border-offline/25 bg-offline/10 px-3.5 py-3">
<x-icon name="alert" class="h-4 w-4 shrink-0 text-offline" />
<p class="text-sm text-ink-2">{{ $error }}</p>
</div>
@endif
</div>

View File

@ -46,7 +46,7 @@
<div class="mt-6 flex items-center justify-end gap-2">
<x-btn variant="secondary" wire:click="$dispatch('closeModal')">{{ $loaded ? __('common.cancel') : __('common.close') }}</x-btn>
@if ($loaded)
<x-btn variant="accent" wire:click="save" wire:target="save" wire:loading.attr="disabled">
<x-btn variant="primary" wire:click="save" wire:target="save" wire:loading.attr="disabled">
<x-icon name="shield" class="h-3.5 w-3.5" wire:loading.remove wire:target="save" />
<svg wire:loading wire:target="save" class="h-3.5 w-3.5 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56" stroke-linecap="round" /></svg>
{{ __('common.save') }}

View File

@ -4,8 +4,8 @@
<x-icon name="file" class="h-5 w-5" />
</span>
<div class="min-w-0 flex-1">
<h2 class="truncate font-display text-base font-semibold text-ink">{{ $name }}</h2>
<p class="mt-1 truncate font-mono text-[11px] text-ink-3">{{ $path }}</p>
<h2 class="truncate font-display text-base font-semibold text-ink" title="{{ $name }}">{{ $name }}</h2>
<p class="mt-1 truncate font-mono text-[11px] text-ink-3" title="{{ $path }}">{{ $path }}</p>
</div>
</div>

View File

@ -38,7 +38,7 @@
@else
<x-btn variant="secondary" wire:click="$dispatch('closeModal')">{{ __('common.cancel') }}</x-btn>
@unless ($error)
<x-btn variant="accent" wire:click="apply" wire:target="apply" wire:loading.attr="disabled">
<x-btn variant="primary" wire:click="apply" wire:target="apply" wire:loading.attr="disabled">
<x-icon name="shield" class="h-3.5 w-3.5" wire:loading.remove wire:target="apply" />
<svg wire:loading wire:target="apply" class="h-3.5 w-3.5 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56" stroke-linecap="round" /></svg>
{{ __('common.apply') }}

View File

@ -78,7 +78,7 @@
@else
<x-btn variant="secondary" wire:click="$dispatch('closeModal')">{{ __('common.cancel') }}</x-btn>
@if (! $error && $supported)
<x-btn variant="accent" wire:click="upgrade" wire:target="upgrade" wire:loading.attr="disabled">
<x-btn variant="primary" wire:click="upgrade" wire:target="upgrade" wire:loading.attr="disabled">
<x-icon name="rotate" class="h-3.5 w-3.5" wire:loading.remove wire:target="upgrade" />
<svg wire:loading wire:target="upgrade" class="h-3.5 w-3.5 animate-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" aria-hidden="true"><path d="M21 12a9 9 0 1 1-6.219-8.56" stroke-linecap="round" /></svg>
{{ __('modals.system_update.submit') }}

View File

@ -71,7 +71,7 @@
<div class="hidden shrink-0 items-center gap-4 md:flex">
<div class="w-20">
<div class="flex items-center justify-between">
<span class="font-mono text-[10px] uppercase text-ink-4">CPU</span>
<span class="font-mono text-[10px] uppercase text-ink-3">CPU</span>
<span class="tabular font-mono text-[10px] text-ink-3">{{ $server->cpu }}%</span>
</div>
<div class="mt-1 h-1 w-full overflow-hidden rounded-full bg-line">
@ -80,7 +80,7 @@
</div>
<div class="w-20">
<div class="flex items-center justify-between">
<span class="font-mono text-[10px] uppercase text-ink-4">RAM</span>
<span class="font-mono text-[10px] uppercase text-ink-3">RAM</span>
<span class="tabular font-mono text-[10px] text-ink-3">{{ $server->mem }}%</span>
</div>
<div class="mt-1 h-1 w-full overflow-hidden rounded-full bg-line">

View File

@ -29,7 +29,7 @@
<div>
<label class="{{ $label }}">{{ __('settings.lp_whitelist') }}</label>
<textarea wire:model="whitelist" rows="4" class="{{ $field }} h-auto py-2 font-mono"></textarea>
<textarea wire:model="whitelist" rows="4" class="{{ $field }} h-auto py-2.5 font-mono"></textarea>
<p class="mt-1 font-mono text-[11px] text-ink-4">{{ __('settings.lp_whitelist_hint') }}</p>
@error('whitelist') <p class="{{ $err }}"><x-icon name="alert" class="h-3.5 w-3.5 shrink-0" />{{ $message }}</p> @enderror
</div>
@ -62,7 +62,7 @@
</x-modal-trigger>
</div>
@empty
<p class="py-3 font-mono text-[11px] text-ink-4">{{ __('settings.lp_no_bans') }}</p>
<p class="py-3 font-mono text-[11px] text-ink-3">{{ __('settings.lp_no_bans') }}</p>
@endforelse
</div>
@if ($bans->isNotEmpty())

View File

@ -25,7 +25,7 @@
</div>
</div>
@empty
<p class="py-3 font-mono text-[11px] text-ink-4">{{ __('sessions.none') }}</p>
<p class="py-3 font-mono text-[11px] text-ink-3">{{ __('sessions.none') }}</p>
@endforelse
</div>

View File

@ -58,7 +58,7 @@
</div>
</div>
@empty
<p class="py-3 font-mono text-[11px] text-ink-4">{{ __('accounts.none') }}</p>
<p class="py-3 font-mono text-[11px] text-ink-3">{{ __('accounts.none') }}</p>
@endforelse
</div>
</x-panel>

View File

@ -24,14 +24,14 @@
</x-modal-trigger>
</div>
@empty
<p class="py-3 font-mono text-[11px] text-ink-4">{{ __('auth.webauthn_none') }}</p>
<p class="py-3 font-mono text-[11px] text-ink-3">{{ __('auth.webauthn_none') }}</p>
@endforelse
</div>
<div class="flex flex-col gap-2 sm:flex-row" x-data>
<input wire:model="newName" type="text" maxlength="60" placeholder="{{ __('auth.webauthn_name_placeholder') }}"
class="h-9 w-full rounded-md border border-line bg-inset px-3 text-sm text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none" />
<x-btn variant="accent" class="shrink-0" x-on:click="window.clusevWebauthn?.register($wire)">
<x-btn variant="accent" class="w-full shrink-0 sm:w-auto" x-on:click="window.clusevWebauthn?.register($wire)">
<x-icon name="plus" class="h-3.5 w-3.5" /> {{ __('auth.webauthn_add') }}
</x-btn>
</div>