+ {{ $value }} + @if ($unit){{ $unit }}@endif +
+ @if ($foot) +{{ $foot }}
+ @endif +diff --git a/app/Livewire/Support.php b/app/Livewire/Support.php
index 1652e16..ebccf30 100644
--- a/app/Livewire/Support.php
+++ b/app/Livewire/Support.php
@@ -2,7 +2,6 @@
namespace App\Livewire;
-use Illuminate\Support\Carbon;
use Livewire\Attributes\Layout;
use Livewire\Component;
@@ -11,15 +10,7 @@ class Support extends Component
{
public function render()
{
- $locale = app()->getLocale();
- $date = fn (string $iso) => Carbon::parse($iso)->locale($locale)->isoFormat('LL');
-
return view('livewire.support', [
- 'tickets' => [
- ['subject' => __('support.ticket_migration'), 'ref' => '#4821', 'date' => $date('2026-07-22'), 'status' => 'open'],
- ['subject' => __('support.ticket_training'), 'ref' => '#4790', 'date' => $date('2026-07-18'), 'status' => 'progress'],
- ['subject' => __('support.ticket_smtp'), 'ref' => '#4712', 'date' => $date('2026-07-04'), 'status' => 'closed'],
- ],
'faqs' => [
['q' => __('support.faq_q1'), 'a' => __('support.faq_a1')],
['q' => __('support.faq_q2'), 'a' => __('support.faq_a2')],
diff --git a/lang/de/dashboard.php b/lang/de/dashboard.php
index 3c32b4c..0d6ba0e 100644
--- a/lang/de/dashboard.php
+++ b/lang/de/dashboard.php
@@ -36,7 +36,9 @@ return [
'address' => 'Adresse',
'storage' => 'Speicher',
'storage_note' => 'vertraglich zugesichert',
+ 'location_note' => 'kein Drittlandtransfer',
'gb' => ':n GB',
+ 'of_seats' => 'von :total Plätzen',
'of_total' => ':used von :total',
'per_month' => ':amount netto / Monat',
'per_year' => ':amount netto / Jahr',
diff --git a/lang/de/users.php b/lang/de/users.php
index 04bbec8..eab6d7b 100644
--- a/lang/de/users.php
+++ b/lang/de/users.php
@@ -18,6 +18,7 @@ return [
'col_person' => 'Person',
'col_status' => 'Status',
+ 'owner_no_actions' => 'Der Inhaber kann nicht entfernt werden.',
'col_actions' => 'Aktionen',
'status_active' => 'Aktiv',
diff --git a/lang/en/dashboard.php b/lang/en/dashboard.php
index ee14e98..7cb8544 100644
--- a/lang/en/dashboard.php
+++ b/lang/en/dashboard.php
@@ -36,7 +36,9 @@ return [
'address' => 'Address',
'storage' => 'Storage',
'storage_note' => 'contractually guaranteed',
+ 'location_note' => 'no third-country transfer',
'gb' => ':n GB',
+ 'of_seats' => 'of :total seats',
'of_total' => ':used of :total',
'per_month' => ':amount net / month',
'per_year' => ':amount net / year',
diff --git a/lang/en/users.php b/lang/en/users.php
index 705b31a..a53943b 100644
--- a/lang/en/users.php
+++ b/lang/en/users.php
@@ -18,6 +18,7 @@ return [
'col_person' => 'Person',
'col_status' => 'Status',
+ 'owner_no_actions' => 'The owner cannot be removed.',
'col_actions' => 'Actions',
'status_active' => 'Active',
diff --git a/resources/css/app.css b/resources/css/app.css
index 9c71049..8109c48 100644
--- a/resources/css/app.css
+++ b/resources/css/app.css
@@ -81,3 +81,43 @@
transition-duration: 0.001ms !important;
}
}
+
+/* ── Metric visuals ───────────────────────────────────────────────────────
+ * The ring and the sparkline from the approved template. They live here
+ * rather than as utilities because both need keyframes and a per-element
+ * custom property, and Tailwind expresses neither.
+ *
+ * Every var() carries a fallback: an undefined custom property does not make
+ * a browser skip the declaration, it computes to `unset` — which for a
+ * stroke-dashoffset silently draws a full ring at every value.
+ */
+.ring { transform: rotate(-90deg); }
+.ring .track { fill: none; stroke: var(--surface-hover); stroke-width: 7; }
+.ring .value {
+ fill: none;
+ stroke: var(--accent);
+ stroke-width: 7;
+ stroke-linecap: round;
+ stroke-dasharray: var(--c, 157);
+ stroke-dashoffset: var(--c, 157);
+ animation: ring-sweep 1.1s cubic-bezier(.2, .7, .2, 1) .2s forwards;
+}
+@keyframes ring-sweep { to { stroke-dashoffset: var(--o, 0); } }
+
+.spark path { fill: none; stroke-width: 1.75; stroke-linecap: round; stroke-linejoin: round; }
+.spark .line {
+ stroke: var(--accent);
+ stroke-dasharray: 400;
+ stroke-dashoffset: 400;
+ animation: spark-draw 1.3s cubic-bezier(.4, .1, .2, 1) .2s forwards;
+}
+.spark.muted .line { stroke: var(--text-muted); }
+.spark .area { fill: var(--accent); stroke: none; opacity: 0; animation: spark-fade .8s ease .9s forwards; }
+@keyframes spark-draw { to { stroke-dashoffset: 0; } }
+@keyframes spark-fade { to { opacity: .09; } }
+
+@media (prefers-reduced-motion: reduce) {
+ .ring .value { stroke-dashoffset: var(--o, 0) !important; }
+ .spark .line { stroke-dashoffset: 0 !important; }
+ .spark .area { opacity: .09 !important; }
+}
diff --git a/resources/views/components/ui/metric.blade.php b/resources/views/components/ui/metric.blade.php
new file mode 100644
index 0000000..491dc4f
--- /dev/null
+++ b/resources/views/components/ui/metric.blade.php
@@ -0,0 +1,48 @@
+@props([
+ 'label',
+ // The figure, and the unit that follows it in lighter weight — "235" and
+ // "/ 500 GB" read as one measurement rather than as two numbers.
+ 'value',
+ 'unit' => null,
+ 'foot' => null,
+ // A chevron to the page that explains the figure. Omitted where there is
+ // no such page: a chevron that goes nowhere is worse than none.
+ 'href' => null,
+])
+{{--
+ One metric card, exactly as the approved template draws it: small caps
+ label with an optional chevron, the figure in tabular monospace with its
+ unit beside it, a line of context underneath, and the visual on the right.
+
+ The visual is a slot so the card does not have to know whether it is a
+ ring, a bar or a sparkline — and so a metric with nothing to draw simply
+ passes none instead of getting a decorative one.
+--}}
+
+ {{ $value }}
+ @if ($unit){{ $unit }}@endif
+ {{ $foot }}
{{ __('dashboard.sheet', ['when' => $asOf->locale($locale)->isoFormat('LL, LT')]) }} -
-{{ __('dashboard.master.storage') }}
-{{ __('dashboard.master.gb', ['n' => $instance->quota_gb]) }}
- {{-- Consumption is not metered yet. An invented "used" - figure on the sheet a customer forwards to their - auditor is the one thing this page must never do. --}} -{{ __('dashboard.master.storage_note') }}
-{{ __('dashboard.master.users') }}
-- {{ $seats['total'] !== null - ? __('dashboard.master.of_total', ['used' => $seats['used'], 'total' => $seats['total']]) - : $seats['used'] }} -
+{{ __('dashboard.traffic.title') }}
-{{ Bytes::human($traffic->usedBytes) }}
-{{ __('dashboard.traffic.of', ['total' => Bytes::human($traffic->quotaBytes)]) }}
@if ($state !== 'ok') {{-- The offer belongs next to the warning: someone reading that they are nearly out should not have to go looking for the way to fix it. --}} - +{{ __('dashboard.master.location') }}
-{{ $location['name'] }}
- @if ($location['note']) -{{ $location['note'] }}
- @endif -|
- {{ $t['subject'] }} -{{ $t['ref'] }} · {{ $t['date'] }} - |
-
- |
-