feat(ui): uniform bordered button kit; retire ghost variants
Every x-btn variant now carries a persistent border + background. Replace the borderless `ghost`/`ghost-danger` (the latter only reddened on hover) with the bordered `secondary` and a new `danger-soft` (red-tinted border+bg, always). Migrate all 10 usages across servers/settings/files. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>feat/v1-foundation
parent
50b18aa45f
commit
e26921f33a
|
|
@ -1,13 +1,15 @@
|
||||||
@props(['variant' => 'secondary', 'icon' => false, 'href' => null, 'size' => 'sm'])
|
@props(['variant' => 'secondary', 'icon' => false, 'href' => null, 'size' => 'sm'])
|
||||||
@php
|
@php
|
||||||
// One button style for the whole app — compact, consistent (R10).
|
// One button style for the whole app — EVERY variant carries a persistent
|
||||||
|
// border + background (R10). No borderless variants: low-emphasis actions use
|
||||||
|
// `secondary`, destructive ones use `danger-soft` (red-tinted, always — not
|
||||||
|
// only on hover). Unknown variants fall back to the bordered `secondary`.
|
||||||
$variants = [
|
$variants = [
|
||||||
'primary' => 'bg-accent text-void hover:bg-accent-bright',
|
'primary' => 'bg-accent text-void hover:bg-accent-bright',
|
||||||
'accent' => 'border border-accent/25 bg-accent/10 text-accent-text hover:bg-accent/15',
|
'accent' => 'border border-accent/25 bg-accent/10 text-accent-text hover:bg-accent/15',
|
||||||
'secondary' => 'border border-line bg-inset text-ink-2 hover:bg-raised hover:text-ink',
|
'secondary' => 'border border-line bg-inset text-ink-2 hover:bg-raised hover:text-ink',
|
||||||
'danger' => 'bg-offline text-void hover:bg-offline/90',
|
'danger' => 'bg-offline text-void hover:bg-offline/90',
|
||||||
'ghost' => 'text-ink-2 hover:bg-raised hover:text-ink',
|
'danger-soft' => 'border border-offline/25 bg-offline/10 text-offline hover:bg-offline/15',
|
||||||
'ghost-danger' => 'text-ink-3 hover:bg-offline/10 hover:text-offline',
|
|
||||||
];
|
];
|
||||||
// sm = compact toolbar/row buttons; lg = full-height primary CTA (≥44px touch target, R7).
|
// sm = compact toolbar/row buttons; lg = full-height primary CTA (≥44px touch target, R7).
|
||||||
$sizes = [
|
$sizes = [
|
||||||
|
|
|
||||||
|
|
@ -119,10 +119,10 @@
|
||||||
{{-- Row actions --}}
|
{{-- Row actions --}}
|
||||||
<div class="flex shrink-0 items-center gap-1 lg:justify-end">
|
<div class="flex shrink-0 items-center gap-1 lg:justify-end">
|
||||||
@unless ($isDir)
|
@unless ($isDir)
|
||||||
<x-btn variant="ghost" wire:click="download({{ $loop->index }})">{{ __('files.download') }}</x-btn>
|
<x-btn variant="secondary" wire:click="download({{ $loop->index }})">{{ __('files.download') }}</x-btn>
|
||||||
<x-btn variant="ghost" wire:click="edit({{ $loop->index }})">{{ __('common.edit') }}</x-btn>
|
<x-btn variant="secondary" wire:click="edit({{ $loop->index }})">{{ __('common.edit') }}</x-btn>
|
||||||
@endunless
|
@endunless
|
||||||
<x-btn variant="ghost-danger" wire:click="confirmDelete({{ $loop->index }})">{{ __('common.delete') }}</x-btn>
|
<x-btn variant="danger-soft" wire:click="confirmDelete({{ $loop->index }})">{{ __('common.delete') }}</x-btn>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,7 @@
|
||||||
<svg wire:loading wire:target="toggleCredential" 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>
|
<svg wire:loading wire:target="toggleCredential" 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>
|
||||||
{{ $cred->disabled ? __('common.unlock') : __('common.lock') }}
|
{{ $cred->disabled ? __('common.unlock') : __('common.lock') }}
|
||||||
</x-btn>
|
</x-btn>
|
||||||
<x-btn variant="ghost-danger" wire:click="confirmDeleteCredential">
|
<x-btn variant="danger-soft" wire:click="confirmDeleteCredential">
|
||||||
<x-icon name="trash" class="h-3.5 w-3.5" /> {{ __('common.delete') }}
|
<x-icon name="trash" class="h-3.5 w-3.5" /> {{ __('common.delete') }}
|
||||||
</x-btn>
|
</x-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -207,7 +207,7 @@
|
||||||
</div>
|
</div>
|
||||||
@if ($supported)
|
@if ($supported)
|
||||||
@if ($check['key'] === 'fail2ban')
|
@if ($check['key'] === 'fail2ban')
|
||||||
<x-btn variant="ghost" size="sm" icon class="shrink-0" title="{{ __('servers.fail2ban_configure') }}"
|
<x-btn variant="secondary" size="sm" icon class="shrink-0" title="{{ __('servers.fail2ban_configure') }}"
|
||||||
wire:click="$dispatch('openModal', { component: 'modals.fail2ban-config', arguments: { serverId: {{ $server->id }} } })">
|
wire:click="$dispatch('openModal', { component: 'modals.fail2ban-config', arguments: { serverId: {{ $server->id }} } })">
|
||||||
<x-icon name="settings" class="h-3.5 w-3.5" />
|
<x-icon name="settings" class="h-3.5 w-3.5" />
|
||||||
</x-btn>
|
</x-btn>
|
||||||
|
|
@ -320,7 +320,7 @@
|
||||||
'text-offline' => $ruleTone === 'offline',
|
'text-offline' => $ruleTone === 'offline',
|
||||||
])>{{ strtolower($rule['action'] ?? 'allow') }}</span>
|
])>{{ strtolower($rule['action'] ?? 'allow') }}</span>
|
||||||
@unless ($fwReadOnly)
|
@unless ($fwReadOnly)
|
||||||
<x-btn variant="ghost-danger" size="sm" icon class="shrink-0" title="{{ __('servers.firewall_remove_rule') }}"
|
<x-btn variant="danger-soft" size="sm" icon class="shrink-0" title="{{ __('servers.firewall_remove_rule') }}"
|
||||||
wire:click="confirmDeleteRule({{ $loop->index }})">
|
wire:click="confirmDeleteRule({{ $loop->index }})">
|
||||||
<x-icon name="trash" class="h-3.5 w-3.5" />
|
<x-icon name="trash" class="h-3.5 w-3.5" />
|
||||||
</x-btn>
|
</x-btn>
|
||||||
|
|
@ -351,7 +351,7 @@
|
||||||
:padded="false">
|
:padded="false">
|
||||||
@if ($f2bSupported && $f2bInstalled && $f2bActive)
|
@if ($f2bSupported && $f2bInstalled && $f2bActive)
|
||||||
<x-slot:actions>
|
<x-slot:actions>
|
||||||
<x-btn variant="ghost" size="sm" icon title="{{ __('servers.fail2ban_configure') }}"
|
<x-btn variant="secondary" size="sm" icon title="{{ __('servers.fail2ban_configure') }}"
|
||||||
wire:click="$dispatch('openModal', { component: 'modals.fail2ban-config', arguments: { serverId: {{ $server->id }} } })">
|
wire:click="$dispatch('openModal', { component: 'modals.fail2ban-config', arguments: { serverId: {{ $server->id }} } })">
|
||||||
<x-icon name="settings" class="h-3.5 w-3.5" />
|
<x-icon name="settings" class="h-3.5 w-3.5" />
|
||||||
</x-btn>
|
</x-btn>
|
||||||
|
|
@ -395,7 +395,7 @@
|
||||||
@foreach ($jail['bannedIps'] as $ip)
|
@foreach ($jail['bannedIps'] as $ip)
|
||||||
<div class="flex items-center justify-between gap-2">
|
<div class="flex items-center justify-between gap-2">
|
||||||
<span class="font-mono text-[11px] text-ink-2">{{ $ip }}</span>
|
<span class="font-mono text-[11px] text-ink-2">{{ $ip }}</span>
|
||||||
<x-btn variant="ghost" size="sm" wire:click="unbanIp({{ \Illuminate\Support\Js::from($jail['name']) }}, {{ \Illuminate\Support\Js::from($ip) }})">{{ __('common.unlock') }}</x-btn>
|
<x-btn variant="secondary" size="sm" wire:click="unbanIp({{ \Illuminate\Support\Js::from($jail['name']) }}, {{ \Illuminate\Support\Js::from($ip) }})">{{ __('common.unlock') }}</x-btn>
|
||||||
</div>
|
</div>
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -504,7 +504,7 @@
|
||||||
</div>
|
</div>
|
||||||
<p class="truncate font-mono text-[11px] text-ink-3">{{ $key['fingerprint'] }}</p>
|
<p class="truncate font-mono text-[11px] text-ink-3">{{ $key['fingerprint'] }}</p>
|
||||||
</div>
|
</div>
|
||||||
<x-btn variant="ghost-danger" icon wire:click="confirmKeyRemoval({{ $loop->index }})" title="{{ __('servers.ssh_keys_remove') }}">
|
<x-btn variant="danger-soft" icon wire:click="confirmKeyRemoval({{ $loop->index }})" title="{{ __('servers.ssh_keys_remove') }}">
|
||||||
<x-icon name="trash" class="h-3.5 w-3.5" />
|
<x-icon name="trash" class="h-3.5 w-3.5" />
|
||||||
</x-btn>
|
</x-btn>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -106,7 +106,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if ($twoFactorEnabled)
|
@if ($twoFactorEnabled)
|
||||||
<x-btn variant="ghost-danger" wire:click="confirmDisableTwoFactor">{{ __('common.disable') }}</x-btn>
|
<x-btn variant="danger-soft" wire:click="confirmDisableTwoFactor">{{ __('common.disable') }}</x-btn>
|
||||||
@else
|
@else
|
||||||
<x-btn variant="accent" :href="route('two-factor.setup')" wire:navigate>
|
<x-btn variant="accent" :href="route('two-factor.setup')" wire:navigate>
|
||||||
<x-icon name="shield" class="h-3.5 w-3.5" /> {{ __('settings.twofa_setup') }}
|
<x-icon name="shield" class="h-3.5 w-3.5" /> {{ __('settings.twofa_setup') }}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Blade;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ButtonComponentTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_danger_soft_has_persistent_border_and_bg(): void
|
||||||
|
{
|
||||||
|
$html = Blade::render('<x-btn variant="danger-soft">x</x-btn>');
|
||||||
|
$this->assertStringContainsString('border border-offline/25', $html);
|
||||||
|
$this->assertStringContainsString('bg-offline/10', $html);
|
||||||
|
$this->assertStringContainsString('text-offline', $html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_no_borderless_ghost_variants_remain(): void
|
||||||
|
{
|
||||||
|
// Unknown variants fall back to secondary (bordered) — never borderless.
|
||||||
|
$html = Blade::render('<x-btn variant="ghost">x</x-btn>');
|
||||||
|
$this->assertStringContainsString('border border-line', $html);
|
||||||
|
$this->assertStringContainsString('bg-inset', $html);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue