One definition of the navigation, and the country instead of the building
The breadcrumb said Übersicht on every page. The sidebar and the breadcrumb each built their own copy of the structure, so the layout had no way to know which entry was current and fell back to the first one. Navigation::portal() and ::console() are now the single definition, and currentLabel() matches on the route name — which matters for the console, whose PATH changes between host-bound and fallback mode while its route names do not. The datacentre name is out of everything a customer sees, for the third time and now at the source rather than in a template. Falkenstein is how an operator places an instance; a customer's processing record names the jurisdiction, and putting the building in customer copy means editing that copy every time the estate grows. Also swept the views onto the new scale: --text-faint is 2.8:1 and a decoration token, but it was carrying table headers, hints and timestamps — text people have to read. All of it moved to --muted, which is 5.0:1 and passes AA. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feat/portal-design
parent
6376f5e9fa
commit
21d8dc310a
|
|
@ -86,7 +86,7 @@ class Dashboard extends Component
|
|||
* has to be the place their instance really runs on, not the place we
|
||||
* usually sell.
|
||||
*
|
||||
* @return array{name: string, note: ?string}|null
|
||||
* @return array{name: string, note: null}|null
|
||||
*/
|
||||
private function location(?Instance $instance): ?array
|
||||
{
|
||||
|
|
@ -98,9 +98,13 @@ class Dashboard extends Component
|
|||
|
||||
$datacenter = Datacenter::query()->where('code', $code)->first();
|
||||
|
||||
// The country, not the site. "Falkenstein" is how an operator places an
|
||||
// instance; a customer's processing record says which jurisdiction the
|
||||
// data sits in, and naming the building means editing customer-facing
|
||||
// copy every time the estate grows.
|
||||
return [
|
||||
'name' => $datacenter?->name ?: $code,
|
||||
'note' => $datacenter?->location,
|
||||
'name' => $datacenter?->location ?: $code,
|
||||
'note' => null,
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
/**
|
||||
* The navigation of both shells, in one place.
|
||||
*
|
||||
* The sidebar and the breadcrumb describe the same structure, and when each
|
||||
* built its own copy the breadcrumb quietly said "Übersicht" on every page —
|
||||
* the layout had no way to know which entry was current, so it fell back to the
|
||||
* first one. One definition, two readers.
|
||||
*
|
||||
* Each entry is [route name, icon, translation key, capability or null].
|
||||
*/
|
||||
final class Navigation
|
||||
{
|
||||
/** @return array<int, array{label: ?string, items: array<int, array{0:string,1:string,2:string,3:?string}>}> */
|
||||
public static function portal(): array
|
||||
{
|
||||
return [
|
||||
['label' => null, 'items' => [
|
||||
['dashboard', 'gauge', 'overview', null],
|
||||
['cloud', 'cloud', 'cloud', null],
|
||||
['users', 'users', 'users', null],
|
||||
['backups', 'database', 'backups', null],
|
||||
]],
|
||||
['label' => __('dashboard.nav_group.contract'), 'items' => [
|
||||
['billing', 'box', 'billing', null],
|
||||
['invoices', 'receipt', 'invoices', null],
|
||||
['settings', 'settings', 'settings', null],
|
||||
['support', 'life-buoy', 'support', null],
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
/** @return array<int, array{label: ?string, items: array<int, array{0:string,1:string,2:string,3:?string}>}> */
|
||||
public static function console(): array
|
||||
{
|
||||
return [
|
||||
['label' => null, 'items' => [
|
||||
['admin.overview', 'gauge', 'overview', null],
|
||||
['admin.customers', 'users', 'customers', null],
|
||||
['admin.instances', 'box', 'instances', null],
|
||||
['admin.hosts', 'server', 'hosts', null],
|
||||
['admin.datacenters', 'database', 'datacenters', null],
|
||||
['admin.plans', 'tag', 'plans', 'plans.manage'],
|
||||
]],
|
||||
['label' => __('admin.nav_group.operations'), 'items' => [
|
||||
['admin.provisioning', 'activity', 'provisioning', null],
|
||||
['admin.maintenance', 'alert-triangle', 'maintenance', null],
|
||||
['admin.vpn', 'shield', 'vpn', null],
|
||||
['admin.revenue', 'trending-up', 'revenue', null],
|
||||
]],
|
||||
['label' => __('admin.nav_group.system'), 'items' => [
|
||||
['admin.secrets', 'lock', 'secrets', 'secrets.manage'],
|
||||
['admin.settings', 'settings', 'settings', null],
|
||||
]],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The label of the entry the current request belongs to.
|
||||
*
|
||||
* Matched on the route rather than the path, because the console's path
|
||||
* changes between host-bound and fallback mode while its route names do
|
||||
* not. Null when the page is not in the navigation at all — a detail view,
|
||||
* a modal — and the caller then supplies its own title.
|
||||
*/
|
||||
public static function currentLabel(bool $console = false): ?string
|
||||
{
|
||||
$prefix = $console ? 'admin.nav.' : 'dashboard.nav.';
|
||||
|
||||
foreach ($console ? self::console() : self::portal() as $group) {
|
||||
foreach ($group['items'] as [$route, , $key]) {
|
||||
$matches = $console
|
||||
? AdminArea::routeIs($route)
|
||||
: request()->routeIs($route);
|
||||
|
||||
if ($matches) {
|
||||
return __($prefix.$key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ return [
|
|||
'title' => 'Meine Cloud',
|
||||
'subtitle' => 'Details und Ressourcen Ihrer Instanz.',
|
||||
'plan_line' => ':plan · :price €/Monat',
|
||||
'datacenter' => 'EU · Rechenzentrum Falkenstein',
|
||||
'datacenter' => 'EU — Serverstandort im Angebot festgehalten',
|
||||
'now' => 'jetzt',
|
||||
'instance_label' => 'Instanz',
|
||||
'open' => 'Cloud öffnen',
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ return [
|
|||
'title' => 'My cloud',
|
||||
'subtitle' => 'Details and resources of your instance.',
|
||||
'plan_line' => ':plan · €:price/mo',
|
||||
'datacenter' => 'EU · Falkenstein data center',
|
||||
'datacenter' => 'EU — server location fixed in your offer',
|
||||
'now' => 'now',
|
||||
'instance_label' => 'Instance',
|
||||
'open' => 'Open cloud',
|
||||
|
|
|
|||
|
|
@ -18,30 +18,11 @@
|
|||
prefix="admin.nav"
|
||||
console
|
||||
:footer="$release->label()"
|
||||
:groups="[
|
||||
['label' => null, 'items' => [
|
||||
['admin.overview', 'gauge', 'overview', null],
|
||||
['admin.customers', 'users', 'customers', null],
|
||||
['admin.instances', 'box', 'instances', null],
|
||||
['admin.hosts', 'server', 'hosts', null],
|
||||
['admin.datacenters', 'database', 'datacenters', null],
|
||||
['admin.plans', 'tag', 'plans', 'plans.manage'],
|
||||
]],
|
||||
['label' => __('admin.nav_group.operations'), 'items' => [
|
||||
['admin.provisioning', 'activity', 'provisioning', null],
|
||||
['admin.maintenance', 'alert-triangle', 'maintenance', null],
|
||||
['admin.vpn', 'shield', 'vpn', null],
|
||||
['admin.revenue', 'trending-up', 'revenue', null],
|
||||
]],
|
||||
['label' => __('admin.nav_group.system'), 'items' => [
|
||||
['admin.secrets', 'lock', 'secrets', 'secrets.manage'],
|
||||
['admin.settings', 'settings', 'settings', null],
|
||||
]],
|
||||
]"
|
||||
:groups="\App\Support\Navigation::console()"
|
||||
/>
|
||||
|
||||
<div class="flex min-w-0 flex-1 flex-col lg:h-screen lg:overflow-hidden">
|
||||
<x-shell.topbar :crumbs="[__('admin.console'), $title ?? __('admin.nav.overview')]">
|
||||
<x-shell.topbar :crumbs="array_values(array_filter([__('admin.console'), $title ?? \App\Support\Navigation::currentLabel(console: true)]))">
|
||||
@auth
|
||||
<div class="relative" x-data="{ open: false }" @keydown.escape="open = false">
|
||||
<button type="button" class="flex min-h-11 items-center gap-2.5 rounded px-2 hover:bg-surface-hover" @click="open = !open" :aria-expanded="open" aria-haspopup="menu">
|
||||
|
|
|
|||
|
|
@ -31,25 +31,11 @@
|
|||
<div class="flex min-h-screen lg:h-screen lg:overflow-hidden">
|
||||
<x-shell.nav
|
||||
prefix="dashboard.nav"
|
||||
:footer="__('dashboard.nav_footer')"
|
||||
:groups="[
|
||||
['label' => null, 'items' => [
|
||||
['dashboard', 'gauge', 'overview', null],
|
||||
['cloud', 'cloud', 'cloud', null],
|
||||
['users', 'users', 'users', null],
|
||||
['backups', 'database', 'backups', null],
|
||||
]],
|
||||
['label' => __('dashboard.nav_group.contract'), 'items' => [
|
||||
['billing', 'box', 'billing', null],
|
||||
['invoices', 'receipt', 'invoices', null],
|
||||
['settings', 'settings', 'settings', null],
|
||||
['support', 'life-buoy', 'support', null],
|
||||
]],
|
||||
]"
|
||||
:groups="\App\Support\Navigation::portal()"
|
||||
/>
|
||||
|
||||
<div class="flex min-w-0 flex-1 flex-col lg:h-screen lg:overflow-hidden">
|
||||
<x-shell.topbar :crumbs="array_values(array_filter([$portalCustomer?->name, $title ?? __('dashboard.title')]))">
|
||||
<x-shell.topbar :crumbs="array_values(array_filter([$portalCustomer?->name, $title ?? \App\Support\Navigation::currentLabel()]))">
|
||||
{{-- Impersonation is a MODE, not a notice: it stays visible
|
||||
rather than hiding in a dropdown, because acting as someone
|
||||
else without noticing is how mistakes happen. --}}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="rounded-xl bg-surface p-6">
|
||||
<div class="rounded-lg bg-surface p-6">
|
||||
@if ($hostCount > 0)
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="grid size-10 shrink-0 place-items-center rounded-lg bg-warning-bg text-warning">
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="rounded-xl bg-surface p-6">
|
||||
<div class="rounded-lg bg-surface p-6">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="grid size-10 shrink-0 place-items-center rounded-lg bg-danger-bg text-danger">
|
||||
<x-ui.icon name="alert-triangle" class="size-5" />
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
<div class="space-y-5">
|
||||
<div class="animate-rise">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('admin.nav.customers') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('admin.nav.customers') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('admin.customers_sub') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-5 lg:grid-cols-[1fr_300px] lg:items-start">
|
||||
<div class="overflow-hidden rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs uppercase tracking-wide text-faint">
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs font-semibold text-muted">
|
||||
<th class="px-4 py-3 font-semibold">{{ __('admin.col.customer') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('admin.col.plan') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('admin.col.mrr') }}</th>
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<h2 class="font-semibold text-ink">{{ __('admin.by_plan') }}</h2>
|
||||
<div class="mt-3 h-56" wire:ignore><x-ui.chart :config="$plansChart" class="h-56" :label="__('admin.by_plan')" /></div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
<div class="space-y-5">
|
||||
<div class="animate-rise">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('datacenters.title') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('datacenters.title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('datacenters.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-5 lg:grid-cols-[1fr_320px] lg:items-start">
|
||||
{{-- List --}}
|
||||
<div class="overflow-hidden rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
@if ($datacenters->isEmpty())
|
||||
<p class="p-8 text-center text-sm text-muted">{{ __('datacenters.empty') }}</p>
|
||||
@else
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs uppercase tracking-wide text-faint">
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs font-semibold text-muted">
|
||||
<th class="px-4 py-3 font-semibold">{{ __('datacenters.code') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('datacenters.name') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('datacenters.hosts') }}</th>
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
<tr wire:key="dc-{{ $dc->uuid }}" class="border-b border-line last:border-0">
|
||||
<td class="px-4 py-3 font-mono font-semibold text-ink">{{ $dc->code }}</td>
|
||||
<td class="px-4 py-3 text-body">{{ $dc->name }}
|
||||
@if ($dc->location)<span class="text-xs text-faint">· {{ $countries[$dc->location] ?? $dc->location }}</span>@endif
|
||||
@if ($dc->location)<span class="text-xs text-muted">· {{ $countries[$dc->location] ?? $dc->location }}</span>@endif
|
||||
</td>
|
||||
<td class="px-4 py-3 font-mono text-xs text-muted">{{ $dc->hosts_count }}</td>
|
||||
<td class="px-4 py-3">
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
</div>
|
||||
|
||||
{{-- Add form --}}
|
||||
<form wire:submit="save" class="space-y-4 rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<form wire:submit="save" class="space-y-4 rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<h2 class="font-semibold text-ink">{{ __('datacenters.add') }}</h2>
|
||||
<x-ui.input name="code" wire:model="code" :label="__('datacenters.code')" :hint="__('datacenters.code_hint')" />
|
||||
<x-ui.input name="name" wire:model="name" :label="__('datacenters.name')" placeholder="Falkenstein" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="rounded-xl bg-surface p-6">
|
||||
<div class="rounded-lg bg-surface p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-base font-semibold text-ink">{{ __('datacenters.edit_title') }}</h3>
|
||||
<span class="rounded bg-surface-2 px-2 py-0.5 font-mono text-xs text-muted">{{ $code }}</span>
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
<a href="{{ route('admin.hosts') }}" wire:navigate class="inline-flex items-center gap-1.5 text-xs font-medium text-muted hover:text-body">
|
||||
<x-ui.icon name="arrow-left" class="size-4" />{{ __('hosts.back') }}
|
||||
</a>
|
||||
<h1 class="mt-2 text-2xl font-semibold tracking-tight text-ink">{{ __('hosts.create_title') }}</h1>
|
||||
<h1 class="mt-2 text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('hosts.create_title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('hosts.create_sub') }}</p>
|
||||
</div>
|
||||
|
||||
<form wire:submit="save" class="space-y-5 rounded-xl border border-line bg-surface p-6 shadow-xs animate-rise">
|
||||
<form wire:submit="save" class="space-y-5 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise">
|
||||
<x-ui.input name="name" wire:model="name" :label="__('hosts.field.name')" :hint="__('hosts.field.name_hint')" autofocus />
|
||||
|
||||
<div class="space-y-1.5">
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
<x-ui.icon name="arrow-left" class="size-4" />{{ __('hosts.back') }}
|
||||
</a>
|
||||
<div class="mt-2 flex items-center gap-3">
|
||||
<h1 class="font-mono text-2xl font-semibold tracking-tight text-ink">{{ $host->name }}</h1>
|
||||
<h1 class="font-mono text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ $host->name }}</h1>
|
||||
<x-ui.badge :status="$badge">{{ __('hosts.status.'.$host->status) }}</x-ui.badge>
|
||||
</div>
|
||||
<p class="mt-1 font-mono text-xs text-muted">{{ $host->public_ip }} · {{ $host->datacenter }}</p>
|
||||
|
|
@ -41,8 +41,8 @@
|
|||
@endphp
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3 animate-rise">
|
||||
{{-- Health --}}
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-faint">{{ __('hosts.detail.health') }}</p>
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs">
|
||||
<p class="text-xs font-semibold text-muted">{{ __('hosts.detail.health') }}</p>
|
||||
<p class="mt-2 flex items-center gap-2 text-lg font-semibold {{ $htone }}">
|
||||
<span class="size-2.5 rounded-pill {{ $hdot }} {{ $health === 'online' ? 'animate-pulse' : '' }}" aria-hidden="true"></span>
|
||||
{{ __('hosts.health.'.$health) }}
|
||||
|
|
@ -53,8 +53,8 @@
|
|||
</div>
|
||||
|
||||
{{-- Storage capacity --}}
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-faint">{{ __('hosts.detail.storage') }}</p>
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs">
|
||||
<p class="text-xs font-semibold text-muted">{{ __('hosts.detail.storage') }}</p>
|
||||
@if ($host->total_gb)
|
||||
<p class="mt-2 font-mono text-lg font-semibold text-ink">{{ $host->availableGb() }} <span class="text-sm font-normal text-muted">/ {{ $host->freeGb() }} GB {{ __('hosts.free') }}</span></p>
|
||||
<div class="mt-2 h-2 overflow-hidden rounded-pill bg-surface-2">
|
||||
|
|
@ -73,8 +73,8 @@
|
|||
</div>
|
||||
|
||||
{{-- Compute --}}
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-faint">{{ __('hosts.detail.compute') }}</p>
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs">
|
||||
<p class="text-xs font-semibold text-muted">{{ __('hosts.detail.compute') }}</p>
|
||||
<div class="mt-2 grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<p class="font-mono text-lg font-semibold text-ink">{{ $host->cpu_cores ?? '—' }}</p>
|
||||
|
|
@ -96,7 +96,7 @@
|
|||
['meta.version', $host->pve_version ?? __('hosts.unknown')],
|
||||
['detail.instances', (string) $instances->count()],
|
||||
] as [$key, $value])
|
||||
<div class="rounded-xl border border-line bg-surface p-4 shadow-xs">
|
||||
<div class="rounded-lg border border-line bg-surface p-4 shadow-xs">
|
||||
<p class="text-xs text-muted">{{ __('hosts.'.$key) }}</p>
|
||||
<p class="mt-1 truncate font-mono text-sm font-semibold text-ink">{{ $value }}</p>
|
||||
</div>
|
||||
|
|
@ -104,7 +104,7 @@
|
|||
</div>
|
||||
|
||||
{{-- Hosted instances --}}
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:100ms]">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:100ms]">
|
||||
<h2 class="mb-3 text-sm font-semibold text-ink">{{ __('hosts.detail.hosted') }}</h2>
|
||||
@if ($instances->isEmpty())
|
||||
<p class="text-sm text-muted">{{ __('hosts.detail.no_instances') }}</p>
|
||||
|
|
@ -128,7 +128,7 @@
|
|||
</div>
|
||||
|
||||
@if ($run && $run->status === 'failed')
|
||||
<div class="flex items-start gap-3 rounded-xl border border-danger-border bg-danger-bg p-4 animate-rise">
|
||||
<div class="flex items-start gap-3 rounded-lg border border-danger-border bg-danger-bg p-4 animate-rise">
|
||||
<x-ui.icon name="alert-triangle" class="size-5 shrink-0 text-danger" />
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-semibold text-danger">{{ __('hosts.error_title') }}</p>
|
||||
|
|
@ -138,7 +138,7 @@
|
|||
@endif
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-5">
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise lg:col-span-3">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise lg:col-span-3">
|
||||
<h2 class="mb-4 text-sm font-semibold text-ink">{{ __('hosts.progress') }}</h2>
|
||||
@if ($run)
|
||||
<x-ui.progress-stepper :steps="$steps" />
|
||||
|
|
@ -147,7 +147,7 @@
|
|||
@endif
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise lg:col-span-2">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise lg:col-span-2">
|
||||
<h2 class="mb-4 text-sm font-semibold text-ink">{{ __('hosts.events') }}</h2>
|
||||
@if ($events->isEmpty())
|
||||
<p class="text-sm text-muted">{{ __('hosts.no_run') }}</p>
|
||||
|
|
@ -161,7 +161,7 @@
|
|||
<span class="mt-1.5 size-1.5 shrink-0 rounded-pill bg-current {{ $tone }}" aria-hidden="true"></span>
|
||||
<div class="min-w-0">
|
||||
<p class="text-body">{{ __('hosts.step.'.$event->step) }}</p>
|
||||
<p class="text-xs text-faint">
|
||||
<p class="text-xs text-muted">
|
||||
{{ __('hosts.outcome.'.$event->outcome) }}
|
||||
· {{ __('hosts.attempt') }} {{ $event->attempt }}
|
||||
@if ($event->message) · <span class="break-words">{{ $event->message }}</span> @endif
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div class="space-y-5">
|
||||
<div class="flex items-start justify-between gap-4 animate-rise">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('hosts.title') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('hosts.title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('hosts.subtitle') }}</p>
|
||||
</div>
|
||||
<a href="{{ route('admin.hosts.create') }}" wire:navigate>
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
<div class="flex flex-wrap items-center gap-2 animate-rise [animation-delay:40ms]">
|
||||
<div class="relative min-w-48 flex-1">
|
||||
<input type="search" wire:model.live.debounce.300ms="search" placeholder="{{ __('hosts.search_placeholder') }}"
|
||||
class="w-full rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-ink placeholder:text-faint" />
|
||||
class="w-full rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-ink placeholder:text-muted" />
|
||||
</div>
|
||||
<select wire:model.live="datacenter" class="rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-body">
|
||||
<option value="">{{ __('hosts.all_datacenters') }}</option>
|
||||
|
|
@ -34,16 +34,16 @@
|
|||
</div>
|
||||
|
||||
@if ($hosts->isEmpty())
|
||||
<div class="rounded-xl border border-dashed border-line-strong bg-surface p-10 text-center animate-rise">
|
||||
<div class="rounded-lg border border-dashed border-line-strong bg-surface p-10 text-center animate-rise">
|
||||
<span class="mx-auto grid size-12 place-items-center rounded-lg bg-surface-2 text-muted"><x-ui.icon name="server" class="size-6" /></span>
|
||||
<p class="mt-3 text-sm text-muted">{{ $total === 0 ? __('hosts.empty') : __('hosts.no_matches') }}</p>
|
||||
</div>
|
||||
@else
|
||||
<div class="overflow-hidden rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:80ms]">
|
||||
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:80ms]">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs uppercase tracking-wide text-faint">
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs font-semibold text-muted">
|
||||
<th class="px-4 py-3 font-semibold">{{ __('hosts.col.host') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('hosts.col.datacenter') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('hosts.col.health') }}</th>
|
||||
|
|
@ -86,7 +86,7 @@
|
|||
<span class="font-mono text-xs text-muted">{{ $host->availableGb() }} / {{ $host->freeGb() }} GB</span>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-xs text-faint">{{ __('hosts.unknown') }}</span>
|
||||
<span class="text-xs text-muted">{{ __('hosts.unknown') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3"><x-ui.badge :status="$badge">{{ __('hosts.status.'.$host->status) }}</x-ui.badge></td>
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
</div>
|
||||
</dl>
|
||||
|
||||
<p class="mt-3 text-xs text-faint">{{ __('instances.admin_once') }}</p>
|
||||
<p class="mt-3 text-xs text-muted">{{ __('instances.admin_once') }}</p>
|
||||
|
||||
<div class="mt-5 flex justify-end">
|
||||
<x-ui.button variant="secondary" x-on:click="Livewire.dispatch('closeModal')">{{ __('common.close') }}</x-ui.button>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
<div class="space-y-5">
|
||||
<div class="animate-rise">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('admin.nav.instances') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('admin.nav.instances') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('admin.instances_sub') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs uppercase tracking-wide text-faint">
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs font-semibold text-muted">
|
||||
<th class="px-4 py-3 font-semibold">{{ __('admin.col.address') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('admin.col.customer') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('admin.col.host') }}</th>
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
<td class="px-4 py-3"><x-ui.badge :status="$r['status']">{{ $r['status_label'] }}</x-ui.badge></td>
|
||||
</tr>
|
||||
@empty
|
||||
<tr><td colspan="7" class="px-4 py-8 text-center text-sm text-faint">{{ __('admin.instances_empty') }}</td></tr>
|
||||
<tr><td colspan="7" class="px-4 py-8 text-center text-sm text-muted">{{ __('admin.instances_empty') }}</td></tr>
|
||||
@endforelse
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
<div class="space-y-6">
|
||||
<div class="animate-rise">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('maintenance.title') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('maintenance.title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('maintenance.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-[1fr_420px] lg:items-start">
|
||||
{{-- Windows list --}}
|
||||
<div class="overflow-hidden rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
@if ($windows->isEmpty())
|
||||
<p class="p-8 text-center text-sm text-muted">{{ __('maintenance.empty') }}</p>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs uppercase tracking-wide text-faint">
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs font-semibold text-muted">
|
||||
<th class="px-4 py-3 font-semibold">{{ __('maintenance.col_window') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('maintenance.col_when') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('maintenance.col_impact') }}</th>
|
||||
|
|
@ -29,7 +29,7 @@
|
|||
<td class="px-4 py-3 text-xs text-body">{{ $w['starts_at']->isoFormat('DD.MM. HH:mm') }} – {{ $w['ends_at']->isoFormat('HH:mm') }}</td>
|
||||
<td class="px-4 py-3 text-xs text-muted whitespace-nowrap">
|
||||
{{ trans_choice('maintenance.impact_hosts', $w['hosts'], ['count' => $w['hosts']]) }}
|
||||
<span class="text-faint">·</span>
|
||||
<span class="text-muted">·</span>
|
||||
{{ trans_choice('maintenance.impact_customers', $w['affected'], ['count' => $w['affected']]) }}
|
||||
</td>
|
||||
<td class="px-4 py-3"><x-ui.badge :status="$tone">{{ __('maintenance.state_'.$w['state']) }}</x-ui.badge></td>
|
||||
|
|
@ -69,7 +69,7 @@
|
|||
</div>
|
||||
|
||||
{{-- Create form --}}
|
||||
<form wire:submit="publish" class="overflow-hidden rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<form wire:submit="publish" class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div class="border-b border-line bg-surface-2 px-5 py-3.5">
|
||||
<h2 class="font-semibold text-ink">{{ __('maintenance.new_title') }}</h2>
|
||||
<p class="mt-0.5 text-xs text-muted">{{ __('maintenance.new_sub') }}</p>
|
||||
|
|
@ -81,14 +81,14 @@
|
|||
<div>
|
||||
<label class="text-sm font-medium text-body" for="publicDescription">{{ __('maintenance.field_public') }}</label>
|
||||
<textarea id="publicDescription" wire:model="publicDescription" rows="2" placeholder="{{ __('maintenance.field_public_ph') }}"
|
||||
class="mt-1.5 w-full rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-ink placeholder:text-faint focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30"></textarea>
|
||||
<p class="mt-1 text-xs text-faint">{{ __('maintenance.field_public_hint') }}</p>
|
||||
class="mt-1.5 w-full rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-ink placeholder:text-muted focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30"></textarea>
|
||||
<p class="mt-1 text-xs text-muted">{{ __('maintenance.field_public_hint') }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-sm font-medium text-body" for="internalNotes">{{ __('maintenance.field_internal') }}</label>
|
||||
<textarea id="internalNotes" wire:model="internalNotes" rows="2" placeholder="{{ __('maintenance.field_internal_ph') }}"
|
||||
class="mt-1.5 w-full rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-ink placeholder:text-faint focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30"></textarea>
|
||||
class="mt-1.5 w-full rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-ink placeholder:text-muted focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
<div>
|
||||
<label class="text-sm font-medium text-body" for="{{ $field }}">{{ __('maintenance.'.$key) }}</label>
|
||||
<div class="relative mt-1.5">
|
||||
<x-ui.icon name="calendar" class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-faint" />
|
||||
<x-ui.icon name="calendar" class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted" />
|
||||
<input id="{{ $field }}" type="datetime-local" wire:model.live="{{ $field }}"
|
||||
class="w-full rounded-md border border-line-strong bg-surface py-2 pl-9 pr-3 text-sm text-ink focus:border-accent focus:outline-none focus:ring-2 focus:ring-accent/30 [color-scheme:dark]" />
|
||||
</div>
|
||||
|
|
@ -109,7 +109,7 @@
|
|||
{{-- Typing an end time by hand is the fiddliest part of this form;
|
||||
these set it from the start time. --}}
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<span class="text-xs text-faint">{{ __('maintenance.duration') }}</span>
|
||||
<span class="text-xs text-muted">{{ __('maintenance.duration') }}</span>
|
||||
@foreach ([30, 60, 120, 240] as $minutes)
|
||||
<button type="button" wire:click="setDuration({{ $minutes }})"
|
||||
class="rounded-pill border px-2.5 py-0.5 text-xs font-medium transition
|
||||
|
|
@ -127,7 +127,7 @@
|
|||
<div>
|
||||
<div class="flex items-baseline justify-between">
|
||||
<label class="text-sm font-medium text-body">{{ __('maintenance.field_hosts') }}</label>
|
||||
<span class="font-mono text-xs {{ count($hostIds) > 0 ? 'text-accent-text' : 'text-faint' }}">{{ __('maintenance.selected', ['n' => count($hostIds)]) }}</span>
|
||||
<span class="font-mono text-xs {{ count($hostIds) > 0 ? 'text-accent-text' : 'text-muted' }}">{{ __('maintenance.selected', ['n' => count($hostIds)]) }}</span>
|
||||
</div>
|
||||
@error('hostIds')<p class="mt-1 text-xs text-danger">{{ $message }}</p>@enderror
|
||||
<div class="mt-1.5 max-h-64 divide-y divide-line overflow-y-auto rounded-lg border border-line-strong bg-surface-2">
|
||||
|
|
@ -139,7 +139,7 @@
|
|||
@if ($dcHosts->isNotEmpty())
|
||||
<div class="p-2.5">
|
||||
<div class="flex items-center justify-between px-1.5 pb-1.5">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-faint">
|
||||
<p class="text-xs font-semibold text-muted">
|
||||
{{ $dc->name }}
|
||||
@if ($dcSelected)
|
||||
<span class="ml-1 font-mono text-accent-text">{{ $dcSelected }}/{{ $dcHosts->count() }}</span>
|
||||
|
|
@ -158,7 +158,7 @@
|
|||
<input type="checkbox" wire:model.live="hostIds" value="{{ $host->id }}"
|
||||
class="size-4 rounded border-line-strong bg-surface text-accent-active focus:ring-2 focus:ring-accent/30" />
|
||||
<span class="font-mono">{{ $host->name }}</span>
|
||||
<span class="ml-auto font-mono text-xs {{ $checked ? 'text-accent-text' : 'text-faint' }}">{{ $host->public_ip }}</span>
|
||||
<span class="ml-auto font-mono text-xs {{ $checked ? 'text-accent-text' : 'text-muted' }}">{{ $host->public_ip }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
|
|
@ -170,7 +170,7 @@
|
|||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-2 border-t border-line bg-surface-2 px-5 py-3.5">
|
||||
<p class="mr-auto max-w-[14rem] text-xs text-faint">{{ __('maintenance.mail_note') }}</p>
|
||||
<p class="mr-auto max-w-[14rem] text-xs text-muted">{{ __('maintenance.mail_note') }}</p>
|
||||
<x-ui.button variant="secondary" type="button" size="sm" wire:click="saveDraft" wire:loading.attr="disabled" wire:target="saveDraft">{{ __('maintenance.save_draft') }}</x-ui.button>
|
||||
<x-ui.button variant="primary" type="submit" size="sm" wire:loading.attr="disabled" wire:target="publish">{{ __('maintenance.publish_notify') }}</x-ui.button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<div class="space-y-5">
|
||||
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 animate-rise">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('admin.overview_title') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('admin.overview_title') }}</h1>
|
||||
{{-- Derived from the notice list below, never asserted: a green badge
|
||||
that is not computed is the most expensive kind of decoration. --}}
|
||||
@if ($noticeCount === 0)
|
||||
|
|
@ -19,8 +19,8 @@
|
|||
@php $kd = ['[animation-delay:40ms]', '[animation-delay:80ms]', '[animation-delay:120ms]', '[animation-delay:160ms]']; @endphp
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
@foreach ($kpis as $i => $kpi)
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise {{ $kd[$i] ?? '' }}">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-faint">{{ $kpi['label'] }}</p>
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise {{ $kd[$i] ?? '' }}">
|
||||
<p class="text-xs font-semibold text-muted">{{ $kpi['label'] }}</p>
|
||||
<p class="mt-2 font-mono text-2xl font-semibold text-ink">{{ $kpi['value'] }}</p>
|
||||
@if (($kpi['sub'] ?? null) !== null)
|
||||
<p class="mt-1 text-xs text-muted">{{ $kpi['sub'] }}</p>
|
||||
|
|
@ -31,18 +31,18 @@
|
|||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
{{-- New instances per month --}}
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms] lg:col-span-2">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms] lg:col-span-2">
|
||||
<h2 class="font-semibold text-ink">{{ __('admin.new_instances') }}</h2>
|
||||
<p class="text-sm text-muted">{{ __('admin.new_instances_sub') }}</p>
|
||||
@if ($newInstances['empty'])
|
||||
<p class="mt-8 text-center text-sm text-faint">{{ __('admin.no_data_yet') }}</p>
|
||||
<p class="mt-8 text-center text-sm text-muted">{{ __('admin.no_data_yet') }}</p>
|
||||
@else
|
||||
<div class="mt-4 h-56" wire:ignore><x-ui.chart :config="$newInstances['config']" class="h-56" :label="__('admin.new_instances')" /></div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- Committed storage per host --}}
|
||||
<div class="flex flex-col rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div class="flex flex-col rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<h2 class="font-semibold text-ink">{{ __('admin.host_load') }}</h2>
|
||||
<p class="text-sm text-muted">{{ __('admin.host_load_sub') }}</p>
|
||||
<ul class="mt-4 flex-1 space-y-3.5">
|
||||
|
|
@ -58,7 +58,7 @@
|
|||
</div>
|
||||
</li>
|
||||
@empty
|
||||
<li class="text-sm text-faint">{{ __('admin.no_hosts_yet') }}</li>
|
||||
<li class="text-sm text-muted">{{ __('admin.no_hosts_yet') }}</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
{{-- Open provisioning runs --}}
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="flex items-center">
|
||||
<h2 class="font-semibold text-ink">{{ __('admin.active_runs') }}</h2>
|
||||
<a href="{{ route('admin.provisioning') }}" class="ml-auto text-xs font-semibold text-accent-text hover:underline">{{ __('admin.view_all') }} ›</a>
|
||||
|
|
@ -82,13 +82,13 @@
|
|||
<x-ui.badge :status="$r['status'] === 'failed' ? 'error' : 'provisioning'">{{ __('admin.run_status.'.$r['status']) }}</x-ui.badge>
|
||||
</li>
|
||||
@empty
|
||||
<li class="py-3 text-sm text-faint">{{ __('admin.no_open_runs') }}</li>
|
||||
<li class="py-3 text-sm text-muted">{{ __('admin.no_open_runs') }}</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
{{-- Notices --}}
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<h2 class="font-semibold text-ink">{{ __('admin.alerts') }}</h2>
|
||||
<ul class="mt-2 space-y-2">
|
||||
@forelse ($notices as $a)
|
||||
|
|
@ -97,7 +97,7 @@
|
|||
<span class="text-body">{{ $a['text'] }}</span>
|
||||
</li>
|
||||
@empty
|
||||
<li class="py-1 text-sm text-faint">{{ __('admin.no_notices') }}</li>
|
||||
<li class="py-1 text-sm text-muted">{{ __('admin.no_notices') }}</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
<x-ui.icon name="arrow-left" class="size-3.5" />
|
||||
{{ __('plans.back') }}
|
||||
</a>
|
||||
<h1 class="mt-2 text-2xl font-semibold tracking-tight text-ink">{{ $family->name }}</h1>
|
||||
<h1 class="mt-2 text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ $family->name }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('plans.versions_subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -30,7 +30,7 @@
|
|||
$live = $windowOpen && $family->sales_enabled;
|
||||
$scheduled = $version->isPublished() && $version->available_from->greaterThan($now);
|
||||
@endphp
|
||||
<div wire:key="v-{{ $version->uuid }}" class="rounded-xl border border-line bg-surface p-5 shadow-xs
|
||||
<div wire:key="v-{{ $version->uuid }}" class="rounded-lg border border-line bg-surface p-5 shadow-xs
|
||||
{{ $live ? 'border-success-border' : '' }}">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
['plans.f_template', $version->template_vmid ?? '—'],
|
||||
] as [$label, $value])
|
||||
<div>
|
||||
<dt class="text-xs uppercase tracking-wide text-faint">{{ __($label) }}</dt>
|
||||
<dt class="text-xs font-semibold text-muted">{{ __($label) }}</dt>
|
||||
<dd class="text-body">{{ $value }}</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
|
|
@ -114,9 +114,9 @@
|
|||
<div class="mt-4 flex flex-wrap gap-4 border-t border-line pt-3 text-sm">
|
||||
@foreach ($version->prices->sortBy('term') as $price)
|
||||
<span class="text-body">
|
||||
<span class="text-xs uppercase tracking-wide text-faint">{{ __('plans.term_'.$price->term) }}</span>
|
||||
<span class="text-xs font-semibold text-muted">{{ __('plans.term_'.$price->term) }}</span>
|
||||
<span class="ml-1.5 font-semibold text-ink">{{ $eur($price->amount_cents) }}</span>
|
||||
<span class="text-xs text-faint">{{ __('plans.net') }}</span>
|
||||
<span class="text-xs text-muted">{{ __('plans.net') }}</span>
|
||||
</span>
|
||||
@endforeach
|
||||
</div>
|
||||
|
|
@ -132,14 +132,14 @@
|
|||
@endif
|
||||
</div>
|
||||
@empty
|
||||
<p class="rounded-xl border border-line bg-surface p-8 text-center text-sm text-muted shadow-xs">
|
||||
<p class="rounded-lg border border-line bg-surface p-8 text-center text-sm text-muted shadow-xs">
|
||||
{{ __('plans.no_versions') }}
|
||||
</p>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
{{-- New draft --}}
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<h2 class="text-base font-semibold text-ink">{{ __('plans.draft_title') }}</h2>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('plans.draft_body') }}</p>
|
||||
|
||||
|
|
@ -162,7 +162,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<p class="pt-1 text-xs font-semibold uppercase tracking-wide text-faint">{{ __('plans.infra') }}</p>
|
||||
<p class="pt-1 text-xs font-semibold text-muted">{{ __('plans.infra') }}</p>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<x-ui.input name="ramMb" type="number" min="512" max="4194304" :label="__('plans.f_ram')" wire:model="ramMb" />
|
||||
<x-ui.input name="cores" type="number" min="1" max="512" :label="__('plans.f_cores')" wire:model="cores" />
|
||||
|
|
@ -170,13 +170,13 @@
|
|||
<x-ui.input name="templateVmid" type="number" min="100" max="999999999" :label="__('plans.f_template')" wire:model="templateVmid" />
|
||||
</div>
|
||||
|
||||
<p class="pt-1 text-xs font-semibold uppercase tracking-wide text-faint">{{ __('plans.pricing') }}</p>
|
||||
<p class="pt-1 text-xs font-semibold text-muted">{{ __('plans.pricing') }}</p>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<x-ui.input name="monthlyPrice" inputmode="decimal" :label="__('plans.term_monthly')" :hint="__('plans.euro_hint')" wire:model="monthlyPrice" />
|
||||
<x-ui.input name="yearlyPrice" inputmode="decimal" :label="__('plans.term_yearly')" :hint="__('plans.euro_hint')" wire:model="yearlyPrice" />
|
||||
</div>
|
||||
|
||||
<p class="pt-1 text-xs font-semibold uppercase tracking-wide text-faint">{{ __('plans.features') }}</p>
|
||||
<p class="pt-1 text-xs font-semibold text-muted">{{ __('plans.features') }}</p>
|
||||
<div class="space-y-2">
|
||||
@foreach ($featureKeys as $key)
|
||||
<div wire:key="feat-{{ $key }}">
|
||||
|
|
|
|||
|
|
@ -1,19 +1,19 @@
|
|||
<div class="space-y-5">
|
||||
<div class="animate-rise">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('plans.title') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('plans.title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('plans.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-5 lg:grid-cols-[1fr_320px] lg:items-start">
|
||||
{{-- Plan lines --}}
|
||||
<div class="overflow-hidden rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
@if ($families->isEmpty())
|
||||
<p class="p-8 text-center text-sm text-muted">{{ __('plans.empty') }}</p>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs uppercase tracking-wide text-faint">
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs font-semibold text-muted">
|
||||
<th class="px-4 py-3 font-semibold">{{ __('plans.plan') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('plans.tier') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('plans.live_version') }}</th>
|
||||
|
|
@ -26,7 +26,7 @@
|
|||
<tr wire:key="plan-{{ $family['uuid'] }}" class="border-b border-line last:border-0">
|
||||
<td class="px-4 py-3">
|
||||
<span class="font-semibold text-ink">{{ $family['name'] }}</span>
|
||||
<span class="ml-1.5 font-mono text-xs text-faint">{{ $family['key'] }}</span>
|
||||
<span class="ml-1.5 font-mono text-xs text-muted">{{ $family['key'] }}</span>
|
||||
@if ($family['drafts'] > 0)
|
||||
<span class="ml-2 rounded-pill border border-line-strong bg-surface-2 px-2 py-0.5 text-xs text-muted">
|
||||
{{ trans_choice('plans.drafts', $family['drafts'], ['count' => $family['drafts']]) }}
|
||||
|
|
@ -38,12 +38,12 @@
|
|||
@if ($family['live'])
|
||||
v{{ $family['live']->version }}
|
||||
@if ($family['next'])
|
||||
<span class="block text-xs text-faint">
|
||||
<span class="block text-xs text-muted">
|
||||
{{ __('plans.next_from', ['version' => $family['next']->version, 'date' => $family['next']->available_from->isoFormat('L')]) }}
|
||||
</span>
|
||||
@endif
|
||||
@else
|
||||
<span class="text-faint">{{ __('plans.no_live_version') }}</span>
|
||||
<span class="text-muted">{{ __('plans.no_live_version') }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-4 py-3">
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
</div>
|
||||
|
||||
{{-- New plan line --}}
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<h2 class="text-base font-semibold text-ink">{{ __('plans.new_title') }}</h2>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('plans.new_body') }}</p>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
<div class="space-y-5" @if ($hasActive) wire:poll.4s @endif>
|
||||
<div class="animate-rise">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('admin.nav.provisioning') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('admin.nav.provisioning') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('admin.provisioning_sub') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-5 lg:grid-cols-[1fr_300px] lg:items-start">
|
||||
<div class="overflow-hidden rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
@if (count($rows) === 0)
|
||||
<p class="p-8 text-center text-sm text-muted">{{ __('admin.no_runs') }}</p>
|
||||
@else
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs uppercase tracking-wide text-faint">
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs font-semibold text-muted">
|
||||
<th class="px-4 py-3 font-semibold">{{ __('admin.col.customer') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('admin.col.step') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('admin.col.attempt') }}</th>
|
||||
|
|
@ -25,17 +25,17 @@
|
|||
@php $tone = ['running' => 'provisioning', 'done' => 'active', 'failed' => 'failed'][$r['state']] ?? 'info'; @endphp
|
||||
<tr wire:key="run-{{ $r['uuid'] }}" class="border-b border-line last:border-0 hover:bg-surface-hover">
|
||||
<td class="px-4 py-3 font-medium text-ink">{{ $r['customer'] }}
|
||||
<span class="ml-1 font-mono text-xs text-faint">{{ $r['pipeline'] }}</span></td>
|
||||
<span class="ml-1 font-mono text-xs text-muted">{{ $r['pipeline'] }}</span></td>
|
||||
<td class="px-4 py-3">
|
||||
<div class="flex items-baseline gap-1.5">
|
||||
<span class="text-xs text-body">{{ $r['step'] }}</span>
|
||||
<span class="font-mono text-xs text-faint">{{ $r['n'] }}</span>
|
||||
<span class="font-mono text-xs text-muted">{{ $r['n'] }}</span>
|
||||
</div>
|
||||
<div class="mt-1.5 flex items-center gap-2">
|
||||
<div class="h-1 w-24 overflow-hidden rounded-pill bg-surface-2">
|
||||
<div class="h-full rounded-pill {{ $r['failed'] ? 'bg-danger' : ($r['stale'] ? 'bg-warning' : 'bg-accent') }} transition-[width] duration-500" style="width: {{ $r['percent'] }}%"></div>
|
||||
</div>
|
||||
<span class="font-mono text-[0.65rem] {{ $r['stale'] ? 'text-warning' : 'text-faint' }}">{{ $r['activity'] }}</span>
|
||||
<span class="font-mono text-[0.65rem] {{ $r['stale'] ? 'text-warning' : 'text-muted' }}">{{ $r['activity'] }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-4 py-3 font-mono text-xs text-muted">{{ $r['attempt'] }}</td>
|
||||
|
|
@ -49,7 +49,7 @@
|
|||
<x-ui.icon name="rotate-ccw" class="size-3.5" />{{ __('admin.retry') }}
|
||||
</button>
|
||||
@else
|
||||
<span class="text-xs text-faint">—</span>
|
||||
<span class="text-xs text-muted">—</span>
|
||||
@endif
|
||||
</td>
|
||||
</tr>
|
||||
|
|
@ -60,9 +60,9 @@
|
|||
@endif
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div class="flex items-center justify-between">
|
||||
<h2 class="text-xs font-semibold uppercase tracking-wider text-faint">{{ __('admin.run_detail') }}</h2>
|
||||
<h2 class="text-xs font-semibold uppercase tracking-wider text-muted">{{ __('admin.run_detail') }}</h2>
|
||||
@if ($panel)
|
||||
<span class="font-mono text-xs {{ $panel['failed'] ? 'text-danger' : 'text-muted' }}">{{ $panel['percent'] }}%</span>
|
||||
@endif
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
<p class="mt-4 text-sm text-muted">{{ __('admin.no_runs') }}</p>
|
||||
@else
|
||||
<p class="mt-2 truncate font-semibold text-ink">{{ $panel['subject'] }}</p>
|
||||
<p class="font-mono text-xs text-faint">{{ $panel['pipeline'] }} · {{ __('admin.run_attempt', ['n' => $panel['attempt']]) }}</p>
|
||||
<p class="font-mono text-xs text-muted">{{ $panel['pipeline'] }} · {{ __('admin.run_attempt', ['n' => $panel['attempt']]) }}</p>
|
||||
|
||||
{{-- Progress bar (width is the one allowed inline style, R4) --}}
|
||||
<div class="mt-4 h-1.5 overflow-hidden rounded-pill bg-surface-2">
|
||||
|
|
@ -83,9 +83,9 @@
|
|||
|
||||
<dl class="mt-3 space-y-1 text-xs">
|
||||
@if ($panel['started'])
|
||||
<div class="flex justify-between"><dt class="text-faint">{{ __('admin.run_started') }}</dt><dd class="font-mono text-muted">{{ $panel['started'] }}</dd></div>
|
||||
<div class="flex justify-between"><dt class="text-muted">{{ __('admin.run_started') }}</dt><dd class="font-mono text-muted">{{ $panel['started'] }}</dd></div>
|
||||
@endif
|
||||
<div class="flex justify-between"><dt class="text-faint">{{ __('admin.run_activity') }}</dt><dd class="font-mono {{ $panel['stale'] ? 'text-warning' : 'text-muted' }}">{{ $panel['activity'] }}</dd></div>
|
||||
<div class="flex justify-between"><dt class="text-muted">{{ __('admin.run_activity') }}</dt><dd class="font-mono {{ $panel['stale'] ? 'text-warning' : 'text-muted' }}">{{ $panel['activity'] }}</dd></div>
|
||||
</dl>
|
||||
|
||||
@if ($panel['stale'] && ! $panel['failed'])
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
</div>
|
||||
@elseif ($panel['current'])
|
||||
<div class="mt-4">
|
||||
<p class="text-[0.65rem] font-semibold uppercase tracking-wider text-faint">{{ __('admin.run_current') }}</p>
|
||||
<p class="text-[0.65rem] font-semibold uppercase tracking-wider text-muted">{{ __('admin.run_current') }}</p>
|
||||
<div class="mt-1.5 flex items-center gap-2.5">
|
||||
<span class="size-2 shrink-0 rounded-pill bg-info motion-safe:animate-pulse" aria-hidden="true"></span>
|
||||
<span class="text-sm font-medium text-ink">{{ $panel['current'] }}</span>
|
||||
|
|
@ -116,7 +116,7 @@
|
|||
|
||||
@if ($panel['next'] && ! $panel['failed'])
|
||||
<div class="mt-4">
|
||||
<p class="text-[0.65rem] font-semibold uppercase tracking-wider text-faint">{{ __('admin.run_next') }}</p>
|
||||
<p class="text-[0.65rem] font-semibold uppercase tracking-wider text-muted">{{ __('admin.run_next') }}</p>
|
||||
<div class="mt-1.5 flex items-center gap-2.5">
|
||||
<span class="size-2 shrink-0 rounded-pill border border-line-strong" aria-hidden="true"></span>
|
||||
<span class="text-sm text-muted">{{ $panel['next'] }}</span>
|
||||
|
|
|
|||
|
|
@ -1,14 +1,14 @@
|
|||
<div class="space-y-5">
|
||||
<div class="animate-rise">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('admin.nav.revenue') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('admin.nav.revenue') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('admin.revenue_sub') }}</p>
|
||||
</div>
|
||||
|
||||
@php $rd = ['[animation-delay:40ms]', '[animation-delay:80ms]', '[animation-delay:120ms]', '[animation-delay:160ms]']; @endphp
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
||||
@foreach ($kpis as $i => $kpi)
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise {{ $rd[$i] ?? '' }}">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-faint">{{ $kpi['label'] }}</p>
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise {{ $rd[$i] ?? '' }}">
|
||||
<p class="text-xs font-semibold text-muted">{{ $kpi['label'] }}</p>
|
||||
<p class="mt-2 font-mono text-2xl font-semibold text-ink">{{ $kpi['value'] }}</p>
|
||||
<p class="mt-1 text-xs text-muted">{{ $kpi['sub'] }}</p>
|
||||
</div>
|
||||
|
|
@ -16,32 +16,32 @@
|
|||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3">
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<h2 class="font-semibold text-ink">{{ __('admin.mrr_by_plan') }}</h2>
|
||||
@forelse ($planCharts as $chart)
|
||||
{{-- One ring per currency: a doughnut sums its slices, and a
|
||||
total mixing euros with francs is not an amount. --}}
|
||||
@if (count($planCharts) > 1)
|
||||
<p class="mt-3 font-mono text-xs uppercase tracking-wide text-faint">{{ $chart['currency'] }}</p>
|
||||
<p class="mt-3 font-mono text-xs font-semibold text-muted">{{ $chart['currency'] }}</p>
|
||||
@endif
|
||||
<div class="mt-3 h-56" wire:ignore><x-ui.chart :config="$chart['config']" class="h-56" :label="__('admin.mrr_by_plan').' — '.$chart['currency']" /></div>
|
||||
@empty
|
||||
<p class="mt-8 text-center text-sm text-faint">{{ __('admin.no_data_yet') }}</p>
|
||||
<p class="mt-8 text-center text-sm text-muted">{{ __('admin.no_data_yet') }}</p>
|
||||
@endforelse
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:120ms] lg:col-span-2">
|
||||
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:120ms] lg:col-span-2">
|
||||
<div class="border-b border-line px-5 py-3"><h2 class="font-semibold text-ink">{{ __('admin.recent_payments') }}</h2></div>
|
||||
<ul class="divide-y divide-line">
|
||||
@forelse ($payments as $p)
|
||||
<li class="flex items-center gap-3 px-5 py-3 text-sm">
|
||||
<span class="grid size-8 shrink-0 place-items-center rounded-lg bg-surface-2 text-success"><x-ui.icon name="check" class="size-4" /></span>
|
||||
<span class="min-w-0 flex-1 truncate font-medium text-ink">{{ $p['customer'] }}</span>
|
||||
<span class="font-mono text-xs text-faint">{{ $p['when'] }}</span>
|
||||
<span class="font-mono text-xs text-muted">{{ $p['when'] }}</span>
|
||||
<span class="font-mono text-body">{{ $p['amount'] }}</span>
|
||||
</li>
|
||||
@empty
|
||||
<li class="px-5 py-8 text-center text-sm text-faint">{{ __('admin.no_payments_yet') }}</li>
|
||||
<li class="px-5 py-8 text-center text-sm text-muted">{{ __('admin.no_payments_yet') }}</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<div class="mx-auto max-w-3xl space-y-6">
|
||||
<div class="animate-rise">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('secrets.title') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('secrets.title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('secrets.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
{{-- The second gate. Being signed in is not enough to read a payment
|
||||
key: the realistic threat is an unlocked machine, and a session is
|
||||
exactly what that hands over. --}}
|
||||
<form wire:submit="confirmPassword" class="rounded-xl border border-line bg-surface p-6 shadow-xs animate-rise">
|
||||
<form wire:submit="confirmPassword" class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise">
|
||||
<h2 class="font-semibold text-ink">{{ __('secrets.locked_title') }}</h2>
|
||||
<p class="mt-1.5 max-w-xl text-sm text-muted">{{ __('secrets.locked_body') }}</p>
|
||||
|
||||
|
|
@ -34,7 +34,7 @@
|
|||
</div>
|
||||
|
||||
@foreach ($entries as $entry)
|
||||
<div wire:key="secret-{{ $entry['key'] }}" class="space-y-4 rounded-xl border border-line bg-surface p-6 shadow-xs animate-rise">
|
||||
<div wire:key="secret-{{ $entry['key'] }}" class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise">
|
||||
<div class="flex flex-wrap items-start justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="font-semibold text-ink">{{ $entry['label'] }}</h2>
|
||||
|
|
@ -99,7 +99,7 @@
|
|||
@endforeach
|
||||
|
||||
@if ($check !== null)
|
||||
<div class="rounded-xl border border-line bg-surface p-6 shadow-xs animate-rise">
|
||||
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise">
|
||||
<h2 class="font-semibold text-ink">{{ __('secrets.check_title') }}</h2>
|
||||
|
||||
@if (! $check['ok'])
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<div class="mx-auto max-w-3xl space-y-6">
|
||||
<div class="animate-rise">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('admin_settings.title') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('admin_settings.title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('admin_settings.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -9,7 +9,7 @@
|
|||
{{-- Polls itself: an operator watching an update run should not have to
|
||||
reload the page to find out whether it finished. Fast while
|
||||
something is happening, slow when nothing is. --}}
|
||||
<div class="rounded-xl border border-line bg-surface p-6 shadow-xs animate-rise"
|
||||
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise"
|
||||
wire:poll.{{ $update['running'] ? '3s' : '30s' }}>
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
|
|
@ -130,7 +130,7 @@
|
|||
|
||||
{{-- My account --}}
|
||||
@if ($canManageSite)
|
||||
<div class="rounded-xl border border-line bg-surface p-6 shadow-xs animate-rise">
|
||||
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
|
|
@ -144,7 +144,7 @@
|
|||
<p class="mt-1.5 max-w-xl text-sm text-muted">
|
||||
{{ $sitePublic ? __('admin_settings.site_public_body') : __('admin_settings.site_hidden_body') }}
|
||||
</p>
|
||||
<p class="mt-2 text-xs text-faint">
|
||||
<p class="mt-2 text-xs text-muted">
|
||||
{{ __('admin_settings.site_your_view', ['ip' => request()->ip()]) }}
|
||||
<span class="{{ $viewerOnVpn ? 'text-success' : '' }}">
|
||||
{{ $viewerOnVpn ? __('admin_settings.site_via_vpn') : __('admin_settings.site_not_vpn') }}
|
||||
|
|
@ -161,7 +161,7 @@
|
|||
|
||||
{{-- Who may reach this console --}}
|
||||
@if ($canManageSite)
|
||||
<div class="rounded-xl border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:30ms]">
|
||||
<div class="rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:30ms]">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div class="min-w-0">
|
||||
<div class="flex items-center gap-2">
|
||||
|
|
@ -175,7 +175,7 @@
|
|||
<p class="mt-1 max-w-xl text-sm text-muted">
|
||||
{{ $consoleRestricted ? __('admin_settings.console_locked_body') : __('admin_settings.console_open_body') }}
|
||||
</p>
|
||||
<p class="mt-2 font-mono text-xs text-faint">
|
||||
<p class="mt-2 font-mono text-xs text-muted">
|
||||
{{ __('admin_settings.console_your_ip', ['ip' => $viewerIp]) }}
|
||||
</p>
|
||||
</div>
|
||||
|
|
@ -187,15 +187,15 @@
|
|||
</div>
|
||||
|
||||
<div class="mt-5 space-y-2 border-t border-line pt-4">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-faint">{{ __('admin_settings.console_always') }}</p>
|
||||
<p class="text-xs font-semibold text-muted">{{ __('admin_settings.console_always') }}</p>
|
||||
@foreach ($consoleVpnRanges as $range)
|
||||
<div class="flex items-center justify-between rounded-lg border border-line-strong bg-surface-2 px-3 py-2">
|
||||
<span class="font-mono text-sm text-body">{{ $range }}</span>
|
||||
<span class="text-xs text-faint">{{ __('admin_settings.console_vpn_note') }}</span>
|
||||
<span class="text-xs text-muted">{{ __('admin_settings.console_vpn_note') }}</span>
|
||||
</div>
|
||||
@endforeach
|
||||
|
||||
<p class="pt-3 text-xs font-semibold uppercase tracking-wide text-faint">{{ __('admin_settings.console_extra') }}</p>
|
||||
<p class="pt-3 text-xs font-semibold text-muted">{{ __('admin_settings.console_extra') }}</p>
|
||||
@forelse ($consoleIps as $ip)
|
||||
<div wire:key="cip-{{ $ip }}" class="flex items-center justify-between rounded-lg border border-line-strong bg-surface-2 px-3 py-2">
|
||||
<span class="font-mono text-sm text-body">{{ $ip }}</span>
|
||||
|
|
@ -217,7 +217,7 @@
|
|||
</div>
|
||||
<x-ui.button type="submit" wire:loading.attr="disabled" wire:target="addConsoleIp">{{ __('admin_settings.console_add') }}</x-ui.button>
|
||||
</div>
|
||||
<p class="mt-1.5 text-xs text-faint">{{ __('admin_settings.console_ip_hint') }}</p>
|
||||
<p class="mt-1.5 text-xs text-muted">{{ __('admin_settings.console_ip_hint') }}</p>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -226,7 +226,7 @@
|
|||
|
||||
{{-- Own password. There was no way to change one at all: an account created
|
||||
with a generated password kept it until someone opened a shell. --}}
|
||||
<form wire:submit="updateOwnPassword" class="space-y-4 rounded-xl border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:90ms]">
|
||||
<form wire:submit="updateOwnPassword" class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:90ms]">
|
||||
<div>
|
||||
<h2 class="font-semibold text-ink">{{ __('admin_settings.password_title') }}</h2>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('admin_settings.password_sub') }}</p>
|
||||
|
|
@ -246,13 +246,13 @@
|
|||
</div>
|
||||
</form>
|
||||
|
||||
<form wire:submit="saveAccount" class="space-y-4 rounded-xl border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<form wire:submit="saveAccount" class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<h2 class="font-semibold text-ink">{{ __('admin_settings.account_title') }}</h2>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<x-ui.input name="name" wire:model="name" :label="__('admin_settings.name')" />
|
||||
<x-ui.input name="email" wire:model="email" :label="__('admin_settings.email')" type="email" />
|
||||
</div>
|
||||
<p class="text-xs text-faint">{{ __('admin_settings.twofa_hint') }}</p>
|
||||
<p class="text-xs text-muted">{{ __('admin_settings.twofa_hint') }}</p>
|
||||
<div class="flex justify-end">
|
||||
<x-ui.button variant="primary" type="submit" wire:loading.attr="disabled" wire:target="saveAccount">{{ __('admin_settings.save') }}</x-ui.button>
|
||||
</div>
|
||||
|
|
@ -260,7 +260,7 @@
|
|||
|
||||
{{-- Staff & access (Owner-only) --}}
|
||||
@if ($canManageStaff)
|
||||
<div class="space-y-4 rounded-xl border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div>
|
||||
<h2 class="font-semibold text-ink">{{ __('admin_settings.staff_title') }}</h2>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('admin_settings.staff_sub') }}</p>
|
||||
|
|
@ -293,7 +293,7 @@
|
|||
<x-ui.icon name="plus" class="size-4" />{{ __('admin_settings.invite') }}
|
||||
</x-ui.button>
|
||||
</form>
|
||||
<p class="text-xs text-faint">{{ __('admin_settings.invite_hint') }}</p>
|
||||
<p class="text-xs text-muted">{{ __('admin_settings.invite_hint') }}</p>
|
||||
|
||||
@if ($invitedPassword)
|
||||
<div class="rounded-lg border border-warning-border bg-warning-bg p-4">
|
||||
|
|
@ -307,11 +307,11 @@
|
|||
@endif
|
||||
|
||||
{{-- Staff list --}}
|
||||
<div class="overflow-hidden rounded-xl border border-line">
|
||||
<div class="overflow-hidden rounded-lg border border-line">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs uppercase tracking-wide text-faint">
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs font-semibold text-muted">
|
||||
<th class="px-4 py-3 font-semibold">{{ __('admin_settings.col_person') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('admin_settings.role') }}</th>
|
||||
<th class="px-4 py-3 text-right font-semibold">{{ __('admin_settings.col_actions') }}</th>
|
||||
|
|
@ -322,7 +322,7 @@
|
|||
<tr wire:key="staff-{{ $s['id'] }}" class="border-b border-line last:border-0 hover:bg-surface-hover">
|
||||
<td class="px-4 py-3">
|
||||
<p class="font-medium text-ink">{{ $s['name'] }}
|
||||
@if ($s['self'])<span class="ml-1 text-xs text-faint">({{ __('admin_settings.you') }})</span>@endif
|
||||
@if ($s['self'])<span class="ml-1 text-xs text-muted">({{ __('admin_settings.you') }})</span>@endif
|
||||
</p>
|
||||
<p class="text-xs text-muted">{{ $s['email'] }}</p>
|
||||
</td>
|
||||
|
|
@ -344,7 +344,7 @@
|
|||
yourself — and an empty cell reads as a bug rather
|
||||
than as a rule. --}}
|
||||
@if ($s['self'])
|
||||
<span class="text-xs text-faint">—</span>
|
||||
<span class="text-xs text-muted">—</span>
|
||||
@endif
|
||||
@unless ($s['self'])
|
||||
<button type="button" wire:click="revokeStaff({{ $s['id'] }})" class="rounded-md border border-line px-2.5 py-1.5 text-xs font-semibold text-muted hover:border-danger hover:text-danger">{{ __('admin_settings.revoke') }}</button>
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@
|
|||
seconds. Traffic figures can wait the minute someone needs to copy a key. --}}
|
||||
<div class="space-y-5" @if (! $newConfig) wire:poll.5s="refreshPeers" @endif>
|
||||
<div class="animate-rise">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('vpn.title') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('vpn.title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('vpn.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
@if ($hubEndpoint === '' || $hubPublicKey === '')
|
||||
<div class="flex items-start gap-3 rounded-xl border border-warning-border bg-warning-bg p-4 animate-rise">
|
||||
<div class="flex items-start gap-3 rounded-lg border border-warning-border bg-warning-bg p-4 animate-rise">
|
||||
<x-ui.icon name="alert-triangle" class="mt-0.5 size-5 shrink-0 text-warning" />
|
||||
<div class="text-sm">
|
||||
<p class="font-medium text-ink">{{ __('vpn.hub_incomplete_title') }}</p>
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
{{-- Shown once. The private key exists only in this response; a reload loses it. --}}
|
||||
@if ($newConfig)
|
||||
<div class="rounded-xl border border-accent-border bg-accent-bg p-5 animate-rise">
|
||||
<div class="rounded-lg border border-accent-border bg-accent-bg p-5 animate-rise">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 class="font-semibold text-ink">{{ __('vpn.config_ready', ['name' => $newConfigName]) }}</h2>
|
||||
|
|
@ -61,10 +61,10 @@
|
|||
|
||||
<div class="grid grid-cols-1 gap-5 lg:grid-cols-[1fr_320px] lg:items-start">
|
||||
{{-- Peer list --}}
|
||||
<div class="overflow-hidden rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="flex items-center justify-between border-b border-line px-4 py-3">
|
||||
<h2 class="text-sm font-semibold text-ink">{{ __('vpn.peers') }}</h2>
|
||||
<span class="text-xs text-faint">
|
||||
<span class="text-xs text-muted">
|
||||
{{ $lastSync ? __('vpn.last_sync', ['time' => \Illuminate\Support\Carbon::parse($lastSync)->diffForHumans()]) : __('vpn.never_synced') }}
|
||||
</span>
|
||||
</div>
|
||||
|
|
@ -120,7 +120,7 @@
|
|||
<span class="rounded-pill border border-accent-border bg-accent-subtle px-1.5 py-0.5 text-[10px] font-medium text-accent-text">{{ __('vpn.you') }}</span>
|
||||
@endif
|
||||
@elseif (! $peer->host)
|
||||
<span class="text-sm text-faint">{{ __('vpn.no_owner') }}</span>
|
||||
<span class="text-sm text-muted">{{ __('vpn.no_owner') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
|
|
@ -129,7 +129,7 @@
|
|||
<div class="mt-1.5 flex flex-wrap items-center gap-x-5 gap-y-1 font-mono text-xs text-muted">
|
||||
<span class="text-body">{{ $peer->allowed_ip }}</span>
|
||||
@if ($peer->endpoint)
|
||||
<span class="text-faint">{{ $peer->endpoint }}</span>
|
||||
<span class="text-muted">{{ $peer->endpoint }}</span>
|
||||
@endif
|
||||
<span class="whitespace-nowrap">↓ {{ \App\Support\Bytes::human($peer->rx_bytes) }} ↑ {{ \App\Support\Bytes::human($peer->tx_bytes) }}</span>
|
||||
<span class="whitespace-nowrap">{{ __('vpn.handshake') }}: {{ $peer->last_handshake_at?->diffForHumans() ?? __('vpn.never') }}</span>
|
||||
|
|
@ -147,7 +147,7 @@
|
|||
nothing to hand out, and saying so beats an absent button. --}}
|
||||
@if (! $peer->hasStoredConfig() && $peer->kind === \App\Models\VpnPeer::KIND_STAFF && $peer->user_id === auth()->id())
|
||||
<span title="{{ __('vpn.no_stored_config') }}"
|
||||
class="grid size-8 cursor-help place-items-center rounded-md border border-dashed border-line text-faint">
|
||||
class="grid size-8 cursor-help place-items-center rounded-md border border-dashed border-line text-muted">
|
||||
<x-ui.icon name="download" class="size-4" />
|
||||
</span>
|
||||
@endif
|
||||
|
|
@ -193,7 +193,7 @@
|
|||
<div class="space-y-5">
|
||||
{{-- New access --}}
|
||||
@if ($canManage)
|
||||
<form wire:submit="create" class="space-y-4 rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<form wire:submit="create" class="space-y-4 rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<h2 class="font-semibold text-ink">{{ __('vpn.add') }}</h2>
|
||||
<x-ui.input name="name" wire:model="name" :label="__('vpn.name')" placeholder="Notebook Boban" />
|
||||
|
||||
|
|
@ -231,18 +231,18 @@
|
|||
@endif
|
||||
|
||||
{{-- Hub facts an operator needs when configuring a peer by hand --}}
|
||||
<div class="space-y-3 rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:180ms]">
|
||||
<div class="space-y-3 rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:180ms]">
|
||||
<h2 class="font-semibold text-ink">{{ __('vpn.hub') }}</h2>
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-wide text-faint">{{ __('vpn.endpoint') }}</div>
|
||||
<div class="text-xs font-semibold text-muted">{{ __('vpn.endpoint') }}</div>
|
||||
<div class="mt-0.5 break-all font-mono text-xs text-body">{{ $hubEndpoint ?: '—' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-wide text-faint">{{ __('vpn.hub_key') }}</div>
|
||||
<div class="text-xs font-semibold text-muted">{{ __('vpn.hub_key') }}</div>
|
||||
<div class="mt-0.5 break-all font-mono text-xs text-body">{{ $hubPublicKey ?: '—' }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-xs uppercase tracking-wide text-faint">{{ __('vpn.subnet') }}</div>
|
||||
<div class="text-xs font-semibold text-muted">{{ __('vpn.subnet') }}</div>
|
||||
<div class="mt-0.5 font-mono text-xs text-body">{{ config('provisioning.wireguard.subnet') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,24 +1,24 @@
|
|||
<div class="space-y-6" x-data="{ msgs: @js(['restore' => __('backups.restore_toast')]) }">
|
||||
<div class="flex flex-wrap items-center gap-3 animate-rise">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('backups.title') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('backups.title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('backups.subtitle') }}</p>
|
||||
</div>
|
||||
<span class="ml-auto rounded-pill bg-success-bg px-3 py-1.5 text-xs font-semibold text-success">{{ __('backups.schedule') }}</span>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<h2 class="font-semibold text-ink">{{ __('backups.size_trend') }}</h2>
|
||||
<div class="mt-3 h-52" wire:ignore>
|
||||
<x-ui.chart :config="$sizeChart" class="h-52" :label="__('backups.size_trend')" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs uppercase tracking-wide text-faint">
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs font-semibold text-muted">
|
||||
<th class="px-4 py-3 font-semibold">{{ __('backups.col_when') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('backups.col_size') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('backups.col_status') }}</th>
|
||||
|
|
|
|||
|
|
@ -3,15 +3,15 @@
|
|||
@php $loc = app()->getLocale(); $eur = fn ($c) => Number::currency($c / 100, in: 'EUR', locale: $loc); @endphp
|
||||
|
||||
<div class="animate-rise">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('billing.title') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('billing.title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('billing.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
{{-- Current plan --}}
|
||||
<div class="overflow-hidden rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 border-b border-line bg-surface-2 px-6 py-4">
|
||||
<div>
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-faint">{{ __('billing.current_plan') }}</p>
|
||||
<p class="text-xs font-semibold uppercase tracking-wider text-muted">{{ __('billing.current_plan') }}</p>
|
||||
<p class="mt-0.5 text-lg font-semibold text-ink">{{ __('billing.plan.'.$currentKey) }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
{{-- Cart: "5 purchases pending" told nobody what they had ordered, and
|
||||
offered no way to change their mind. --}}
|
||||
@if ($pending->isNotEmpty())
|
||||
<div class="overflow-hidden rounded-xl border border-info-border bg-surface shadow-xs animate-rise">
|
||||
<div class="overflow-hidden rounded-lg border border-info-border bg-surface shadow-xs animate-rise">
|
||||
<div class="flex items-center gap-2 border-b border-line bg-info-bg px-5 py-3">
|
||||
<x-ui.icon name="receipt" class="size-4 shrink-0 text-info" />
|
||||
<h2 class="text-sm font-semibold text-ink">{{ __('billing.cart.title') }}</h2>
|
||||
|
|
@ -56,13 +56,13 @@
|
|||
<li wire:key="order-{{ $order->uuid }}" class="flex flex-wrap items-center gap-3 px-5 py-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-ink">{{ $order->label() }}</p>
|
||||
<p class="mt-0.5 text-xs text-faint">
|
||||
<p class="mt-0.5 text-xs text-muted">
|
||||
{{ __('billing.cart.added', ['when' => $order->created_at->isoFormat('LL')]) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="font-mono text-sm text-body">{{ $eur($order->amount_cents) }}</p>
|
||||
<p class="text-xs text-faint">
|
||||
<p class="text-xs text-muted">
|
||||
{{ __('billing.cart.net') }} ·
|
||||
{{ $order->isRecurring() ? __('billing.cart.per_month') : __('billing.cart.once') }}
|
||||
</p>
|
||||
|
|
@ -103,7 +103,7 @@
|
|||
@if ($recurringGross > 0)
|
||||
{{-- The number that actually matters every month, kept apart
|
||||
from a one-off traffic top-up in the same cart. --}}
|
||||
<p class="pt-1 text-xs text-faint">
|
||||
<p class="pt-1 text-xs text-muted">
|
||||
{{ __('billing.cart.recurring_note', ['amount' => $eur($recurringGross)]) }}
|
||||
</p>
|
||||
@endif
|
||||
|
|
@ -118,7 +118,7 @@
|
|||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
@foreach ($upgrades as $key)
|
||||
@php $p = $plans[$key]; @endphp
|
||||
<div wire:key="upgrade-{{ $key }}" class="flex flex-col rounded-xl border border-line bg-surface p-5 shadow-xs transition hover:border-accent-border">
|
||||
<div wire:key="upgrade-{{ $key }}" class="flex flex-col rounded-lg border border-line bg-surface p-5 shadow-xs transition hover:border-accent-border">
|
||||
<div class="flex items-baseline justify-between">
|
||||
<p class="font-semibold text-ink">{{ __('billing.plan.'.$key) }}</p>
|
||||
<span class="rounded-pill bg-accent-subtle px-2 py-0.5 text-xs font-semibold text-accent-text">{{ __('billing.perf.'.($p['performance'] ?? 'standard')) }}</span>
|
||||
|
|
@ -133,7 +133,7 @@
|
|||
@endforeach
|
||||
</ul>
|
||||
<p class="mt-4 text-xl font-semibold text-ink">{{ $eur($p['price_cents']) }}<span class="text-sm font-normal text-muted"> / {{ __('billing.month_short') }}</span></p>
|
||||
<p class="text-xs text-faint">
|
||||
<p class="text-xs text-muted">
|
||||
{{ $tax->reverseCharge
|
||||
? __('billing.net_reverse_charge')
|
||||
: __('billing.net_hint', ['percent' => $tax->percentLabel()]) }}
|
||||
|
|
@ -150,26 +150,26 @@
|
|||
{{-- Extra storage + Add-ons --}}
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3 xl:grid-cols-4 animate-rise [animation-delay:180ms]">
|
||||
{{-- Storage --}}
|
||||
<div class="flex flex-col rounded-xl border border-accent-border bg-accent-subtle p-5">
|
||||
<div class="flex flex-col rounded-lg border border-accent-border bg-accent-subtle p-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<x-ui.icon name="database" class="size-5 text-accent-active" />
|
||||
<p class="font-semibold text-ink">{{ __('billing.storage_title') }}</p>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-body">{{ __('billing.storage_body', ['gb' => $storage['gb'], 'price' => $eur($storage['price_cents'])]) }}</p>
|
||||
<p class="mt-1 text-xs text-faint">{{ __('billing.net_per_month') }}</p>
|
||||
<p class="mt-1 text-xs text-muted">{{ __('billing.net_per_month') }}</p>
|
||||
<x-ui.button variant="primary" size="sm" class="mt-4 w-full" wire:click="purchase('storage')" wire:target="purchase('storage')" wire:loading.attr="disabled">
|
||||
{{ __('billing.storage_cta', ['gb' => $storage['gb']]) }}
|
||||
</x-ui.button>
|
||||
</div>
|
||||
|
||||
{{-- Traffic --}}
|
||||
<div class="flex flex-col rounded-xl border {{ $trafficMeter && $trafficMeter->state() !== 'ok' ? 'border-warning-border bg-warning-bg' : 'border-line bg-surface shadow-xs' }} p-5">
|
||||
<div class="flex flex-col rounded-lg border {{ $trafficMeter && $trafficMeter->state() !== 'ok' ? 'border-warning-border bg-warning-bg' : 'border-line bg-surface shadow-xs' }} p-5">
|
||||
<div class="flex items-center gap-2">
|
||||
<x-ui.icon name="activity" class="size-5 {{ $trafficMeter && $trafficMeter->state() !== 'ok' ? 'text-warning' : 'text-muted' }}" />
|
||||
<p class="font-semibold text-ink">{{ __('billing.traffic_title') }}</p>
|
||||
</div>
|
||||
<p class="mt-2 text-sm text-body">{{ __('billing.traffic_body', ['gb' => $trafficAddon['gb'], 'price' => $eur($trafficAddon['price_cents'])]) }}</p>
|
||||
<p class="mt-1 text-xs text-faint">{{ __('billing.net_once') }}</p>
|
||||
<p class="mt-1 text-xs text-muted">{{ __('billing.net_once') }}</p>
|
||||
@if ($trafficMeter)
|
||||
<p class="mt-2 font-mono text-xs text-muted">
|
||||
{{ __('billing.traffic_used', [
|
||||
|
|
@ -186,7 +186,7 @@
|
|||
|
||||
{{-- Add-ons --}}
|
||||
@foreach ($addons as $key => $addon)
|
||||
<div wire:key="addon-{{ $key }}" class="flex flex-col rounded-xl border border-line bg-surface p-5 shadow-xs">
|
||||
<div wire:key="addon-{{ $key }}" class="flex flex-col rounded-lg border border-line bg-surface p-5 shadow-xs">
|
||||
<div class="flex items-center gap-2">
|
||||
<x-ui.icon name="box" class="size-5 text-muted" />
|
||||
<p class="font-semibold text-ink">{{ __('billing.addon.'.$key.'.name') }}</p>
|
||||
|
|
@ -200,7 +200,7 @@
|
|||
<span class="text-sm font-normal text-muted">· {{ __('billing.addon_packs', ['count' => $addon['quantity']]) }}</span>
|
||||
@endif
|
||||
</p>
|
||||
<p class="text-xs text-faint">
|
||||
<p class="text-xs text-muted">
|
||||
{{ $tax->reverseCharge
|
||||
? __('billing.net_reverse_charge')
|
||||
: __('billing.net_hint', ['percent' => $tax->percentLabel()]) }}
|
||||
|
|
@ -221,5 +221,5 @@
|
|||
@endforeach
|
||||
</div>
|
||||
|
||||
<p class="text-xs text-faint">{{ __('billing.mock_note') }}</p>
|
||||
<p class="text-xs text-muted">{{ __('billing.mock_note') }}</p>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
<div class="space-y-6" x-data="{ msgs: @js(['soon' => __('cloud.action_toast')]) }">
|
||||
<div class="animate-rise">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('cloud.title') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('cloud.title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('cloud.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<span class="grid size-11 place-items-center rounded-lg bg-ink font-semibold text-on-accent">B</span>
|
||||
<div class="min-w-0">
|
||||
|
|
@ -48,14 +48,14 @@
|
|||
['cloud.storage', $storageLabel],
|
||||
] as [$key, $val])
|
||||
<div>
|
||||
<dt class="text-xs font-semibold uppercase tracking-wide text-faint">{{ __($key) }}</dt>
|
||||
<dt class="text-xs font-semibold text-muted">{{ __($key) }}</dt>
|
||||
<dd class="mt-1 font-mono text-sm text-body">{{ $val }}</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<h2 class="font-semibold text-ink">{{ __('cloud.storage_growth') }}</h2>
|
||||
<p class="text-sm text-muted">{{ __('cloud.storage_growth_sub') }}</p>
|
||||
<div class="mt-4 h-56" wire:ignore>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="rounded-xl bg-surface p-6">
|
||||
<div class="rounded-lg bg-surface p-6">
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="grid size-10 shrink-0 place-items-center rounded-lg bg-warning-bg text-warning">
|
||||
<x-ui.icon name="alert-triangle" class="size-5" />
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class="rounded-xl bg-surface p-6">
|
||||
<div class="rounded-lg bg-surface p-6">
|
||||
@if ($blocked)
|
||||
<div class="flex items-start gap-3">
|
||||
<span class="grid size-10 shrink-0 place-items-center rounded-lg bg-warning-bg text-warning">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<div @if ($polling) wire:poll.4s @endif>
|
||||
@if ($active)
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="size-2 rounded-pill {{ $failed ? 'bg-danger' : 'bg-info animate-pulse' }}" aria-hidden="true"></span>
|
||||
<h2 class="font-semibold text-ink">{{ __('dashboard.provisioning_title') }}</h2>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,15 @@
|
|||
<div class="space-y-6" x-data="{ msgs: @js(['download' => __('invoices.download_toast')]) }">
|
||||
<div class="animate-rise">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('invoices.title') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('invoices.title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('invoices.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-6 lg:grid-cols-[1fr_320px] lg:items-start">
|
||||
<div class="overflow-hidden rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs uppercase tracking-wide text-faint">
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs font-semibold text-muted">
|
||||
<th class="px-4 py-3 font-semibold">{{ __('invoices.col_no') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('invoices.col_date') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('invoices.col_amount') }}</th>
|
||||
|
|
@ -33,12 +33,12 @@
|
|||
</div>
|
||||
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="rounded-xl border border-accent-border bg-accent-subtle p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-faint">{{ __('invoices.next_charge') }}</p>
|
||||
<div class="rounded-lg border border-accent-border bg-accent-subtle p-5 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<p class="text-xs font-semibold text-muted">{{ __('invoices.next_charge') }}</p>
|
||||
<p class="mt-1 font-mono text-2xl font-semibold text-ink">{{ $nextAmount }}</p>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('invoices.on_date', ['date' => $nextCharge]) }}</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:180ms]">
|
||||
<div class="rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:180ms]">
|
||||
<h2 class="font-semibold text-ink">{{ __('invoices.spend') }}</h2>
|
||||
<div class="mt-3 h-48" wire:ignore>
|
||||
<x-ui.chart :config="$spendChart" class="h-48" :label="__('invoices.spend')" />
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<div class="mx-auto max-w-3xl space-y-6">
|
||||
<div class="animate-rise">
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('settings.title') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('settings.title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('settings.subtitle') }}</p>
|
||||
</div>
|
||||
|
||||
{{-- Company / billing profile --}}
|
||||
<form wire:submit="saveProfile" class="space-y-4 rounded-xl border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<form wire:submit="saveProfile" class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:60ms]">
|
||||
<h2 class="font-semibold text-ink">{{ __('settings.company_title') }}</h2>
|
||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||
<x-ui.input name="companyName" wire:model="companyName" :label="__('settings.company_name')" />
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
|
||||
{{-- Own password. There was no way to change one at all — Fortify's
|
||||
updatePasswords feature was switched off. --}}
|
||||
<form wire:submit="updateOwnPassword" class="space-y-4 rounded-xl border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:90ms]">
|
||||
<form wire:submit="updateOwnPassword" class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:90ms]">
|
||||
<div>
|
||||
<h2 class="font-semibold text-ink">{{ __('admin_settings.password_title') }}</h2>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('admin_settings.password_sub') }}</p>
|
||||
|
|
@ -52,7 +52,7 @@
|
|||
{{-- Two-factor. Every action is re-checked server-side against a recent
|
||||
password confirmation — hiding the buttons stops nobody who can post
|
||||
to /livewire/update. --}}
|
||||
<div class="space-y-4 rounded-xl border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:150ms]">
|
||||
<div class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:150ms]">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
@endif
|
||||
</div>
|
||||
|
||||
<form wire:submit="saveBranding" class="space-y-4 rounded-xl border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<form wire:submit="saveBranding" class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div>
|
||||
<h2 class="font-semibold text-ink">{{ __('settings.branding_title') }}</h2>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('settings.branding_sub') }}</p>
|
||||
|
|
@ -162,12 +162,12 @@
|
|||
@elseif ($logoUrl)
|
||||
<img src="{{ $logoUrl }}" alt="" class="max-h-16 max-w-16 object-contain" />
|
||||
@else
|
||||
<span class="text-xs text-faint">{{ __('settings.brand_default') }}</span>
|
||||
<span class="text-xs text-muted">{{ __('settings.brand_default') }}</span>
|
||||
@endif
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<input type="file" wire:model="logo" accept="image/png,image/webp" class="text-sm text-body file:mr-3 file:rounded-md file:border-0 file:bg-surface-2 file:px-3 file:py-1.5 file:text-sm file:font-semibold file:text-body" />
|
||||
<p class="text-xs text-faint">{{ __('settings.brand_logo_hint') }}</p>
|
||||
<p class="text-xs text-muted">{{ __('settings.brand_logo_hint') }}</p>
|
||||
@if ($logoUrl)
|
||||
<button type="button" wire:click="removeLogo" class="self-start text-xs font-semibold text-danger hover:underline">{{ __('settings.brand_logo_remove') }}</button>
|
||||
@endif
|
||||
|
|
@ -177,7 +177,7 @@
|
|||
</div>
|
||||
|
||||
@if ($branding['is_default'] ?? false)
|
||||
<p class="text-xs text-faint">{{ __('settings.brand_using_default') }}</p>
|
||||
<p class="text-xs text-muted">{{ __('settings.brand_using_default') }}</p>
|
||||
@endif
|
||||
|
||||
<div class="flex justify-end">
|
||||
|
|
@ -186,7 +186,7 @@
|
|||
</form>
|
||||
|
||||
{{-- Package & account lifecycle --}}
|
||||
<div class="space-y-4 rounded-xl border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:180ms]">
|
||||
<div class="space-y-4 rounded-lg border border-line bg-surface p-6 shadow-xs animate-rise [animation-delay:180ms]">
|
||||
<h2 class="font-semibold text-ink">{{ __('settings.package_title') }}</h2>
|
||||
|
||||
@if ($cancellationScheduled)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div class="space-y-6" x-data="{ open: null, msgs: @js(['sent' => __('support.new_toast')]) }">
|
||||
<div class="flex flex-wrap items-center gap-3 animate-rise">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('support.title') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('support.title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('support.subtitle') }}</p>
|
||||
</div>
|
||||
<x-ui.button variant="primary" class="ml-auto" @click="$dispatch('notify', { message: msgs.sent })">
|
||||
|
|
@ -17,10 +17,10 @@
|
|||
['external-link', __('support.contact_email'), 'support@clupilot.com'],
|
||||
['shield-check', __('support.contact_sla'), __('support.contact_sla_val')],
|
||||
] as $i => [$icon, $label, $val])
|
||||
<div class="flex items-center gap-3 rounded-xl border border-line bg-surface p-4 shadow-xs animate-rise {{ $delays[$i] }}">
|
||||
<div class="flex items-center gap-3 rounded-lg border border-line bg-surface p-4 shadow-xs animate-rise {{ $delays[$i] }}">
|
||||
<span class="grid size-10 shrink-0 place-items-center rounded-lg bg-surface-2 text-accent-active"><x-ui.icon :name="$icon" class="size-5" /></span>
|
||||
<div class="min-w-0">
|
||||
<p class="text-xs font-semibold uppercase tracking-wide text-faint">{{ $label }}</p>
|
||||
<p class="text-xs font-semibold text-muted">{{ $label }}</p>
|
||||
<p class="mt-0.5 truncate text-sm font-medium text-ink">{{ $val }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
</div>
|
||||
|
||||
{{-- Tickets --}}
|
||||
<div class="overflow-hidden rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:240ms]">
|
||||
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:240ms]">
|
||||
<div class="border-b border-line px-5 py-3"><h2 class="font-semibold text-ink">{{ __('support.tickets') }}</h2></div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
</div>
|
||||
|
||||
{{-- FAQ accordion --}}
|
||||
<div class="rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:300ms]">
|
||||
<div class="rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:300ms]">
|
||||
<div class="border-b border-line px-5 py-3"><h2 class="font-semibold text-ink">{{ __('support.faq') }}</h2></div>
|
||||
<ul class="divide-y divide-line">
|
||||
@foreach ($faqs as $i => $f)
|
||||
|
|
|
|||
|
|
@ -1,18 +1,18 @@
|
|||
<div class="space-y-6">
|
||||
<div class="flex flex-wrap items-end justify-between gap-4 animate-rise">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold tracking-tight text-ink">{{ __('users.title') }}</h1>
|
||||
<h1 class="text-2xl font-bold tracking-tight text-ink sm:text-3xl">{{ __('users.title') }}</h1>
|
||||
<p class="mt-1 text-sm text-muted">{{ __('users.subtitle') }}</p>
|
||||
</div>
|
||||
<div class="text-right">
|
||||
<p class="font-mono text-lg font-semibold text-ink">{{ $used }} / {{ $limit }}</p>
|
||||
<p class="text-xs text-muted">{{ __('users.seats_used') }}</p>
|
||||
<p class="text-xs text-faint">{{ __('users.seats_note') }}</p>
|
||||
<p class="text-xs text-muted">{{ __('users.seats_note') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- Invite --}}
|
||||
<form wire:submit="invite" class="grid grid-cols-1 gap-3 rounded-xl border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms] sm:grid-cols-[1fr_1fr_auto_auto] sm:items-end">
|
||||
<form wire:submit="invite" class="grid grid-cols-1 gap-3 rounded-lg border border-line bg-surface p-5 shadow-xs animate-rise [animation-delay:60ms] sm:grid-cols-[1fr_1fr_auto_auto] sm:items-end">
|
||||
<div>
|
||||
<label class="text-sm font-medium text-body" for="inviteEmail">{{ __('users.invite_email') }}</label>
|
||||
<input id="inviteEmail" type="email" wire:model="inviteEmail" class="mt-1.5 w-full rounded-md border border-line-strong bg-surface px-3 py-2 text-sm text-ink" />
|
||||
|
|
@ -36,11 +36,11 @@
|
|||
</form>
|
||||
|
||||
{{-- Seats --}}
|
||||
<div class="overflow-hidden rounded-xl border border-line bg-surface shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div class="overflow-hidden rounded-lg border border-line bg-surface shadow-xs animate-rise [animation-delay:120ms]">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs uppercase tracking-wide text-faint">
|
||||
<tr class="border-b border-line bg-surface-2 text-left text-xs font-semibold text-muted">
|
||||
<th class="px-4 py-3 font-semibold">{{ __('users.col_person') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('users.role') }}</th>
|
||||
<th class="px-4 py-3 font-semibold">{{ __('users.col_status') }}</th>
|
||||
|
|
|
|||
Loading…
Reference in New Issue