One wordmark, one typeface, and the address the server actually issues
Reported as "logo and font are different everywhere" — maintenance page, customer panel, console, homepage. They were. Counting them up: the site header set the name in ink at -0.02em, the sidebar split it into "Clu" plus an orange "Pilot", the sign-in plate set it white at a third size, the customer two-factor page set it in MONO with no mark at all, the error pages and the placeholder each inlined their own — and the maintenance screen had no wordmark whatsoever. Nine surfaces, seven ideas of the brand. There is one now: resources/views/components/ui/brand.blade.php. Mark plus wordmark in one piece, in ink, with an optional muted "Cloud". No orange half — that made the accent a permanent fixture instead of something used sparingly. The four places that cannot use a Blade component (the two self-contained pages, the error layout, the mail layout) inline it and are held to the same shape by a test. The typeface. The console loads IBM Plex through Vite; the 503 page has no stylesheet at all, so the maintenance screen — the one page shown when somebody is already worried — was rendering in the system sans. It declares the fonts itself now. And the bold weight was missing from public/fonts entirely: the placeholder and the error pages both asked for ibm-plex-sans-latin-700-normal.woff2, which was not there. A @font-face pointing at nothing fails silently, which is why nobody noticed. The error pages carried the same two faults the placeholder shipped with: an empty gradient tile where the mark belongs, and `gap` on an inline-flex row spacing "Clu", "Pilot" and "Cloud" apart as three separate words. And the price sheet promised "Adresse auf clupilot.cloud" — a domain the company does not own — while provisioning was issuing addresses under whatever CLUPILOT_DNS_ZONE says. It is assembled from that setting now, the same reason the prices are read from the catalogue rather than typed into the page. welcome.blade.php removed: Laravel's leftover, reachable from no route. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>feature/betriebsmodus v1.3.10
parent
4ef5b9519c
commit
17a6fabcc2
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\Billing\PlanCatalogue;
|
||||
use App\Support\ProvisioningSettings;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Throwable;
|
||||
|
|
@ -38,7 +39,12 @@ class LandingController extends Controller
|
|||
'managed_updates' => 'Updates & Wartung',
|
||||
'daily_backups' => 'Tägliche Sicherung',
|
||||
'monitoring' => 'Überwachung rund um die Uhr',
|
||||
'subdomain' => 'Adresse auf clupilot.cloud',
|
||||
// Filled in from the configured zone by label(), because a
|
||||
// hard-coded domain here is the same two-sources-of-truth mistake
|
||||
// as a hard-coded price — and it shipped naming clupilot.cloud, a
|
||||
// domain the company does not own, while provisioning was issuing
|
||||
// addresses under whatever CLUPILOT_DNS_ZONE said.
|
||||
'subdomain' => 'Adresse auf :zone',
|
||||
'custom_domain' => 'Eigene Domain',
|
||||
'office' => 'Office im Browser',
|
||||
'branding' => 'Ihr Logo & Ihre Farben',
|
||||
|
|
@ -73,9 +79,13 @@ class LandingController extends Controller
|
|||
{
|
||||
$offered = array_merge(...array_column($plans, 'features')) ?: [];
|
||||
|
||||
// Through label(), like the plans themselves. Compared against the raw
|
||||
// constant this stopped matching the moment one label gained a
|
||||
// placeholder, and the row for it would simply have vanished from the
|
||||
// comparison table — with nothing anywhere saying why.
|
||||
return array_values(array_filter(
|
||||
self::FEATURES,
|
||||
fn (string $label) => in_array($label, $offered, true),
|
||||
array_map(fn (string $key) => $this->label($key), array_keys(self::FEATURES)),
|
||||
fn (?string $label) => $label !== null && in_array($label, $offered, true),
|
||||
));
|
||||
}
|
||||
|
||||
|
|
@ -114,6 +124,22 @@ class LandingController extends Controller
|
|||
return $plans;
|
||||
}
|
||||
|
||||
/**
|
||||
* One feature, in the words a customer uses.
|
||||
*
|
||||
* The address the platform hands out is a setting, not a constant — a
|
||||
* server can be installed against any zone — so the sentence naming it is
|
||||
* assembled rather than written down twice.
|
||||
*/
|
||||
private function label(string $key): ?string
|
||||
{
|
||||
$label = self::FEATURES[$key] ?? null;
|
||||
|
||||
return $label === null
|
||||
? null
|
||||
: str_replace(':zone', ProvisioningSettings::dnsZone(), $label);
|
||||
}
|
||||
|
||||
/** Currencies we have a symbol for; anything else prints its ISO code. */
|
||||
private const SYMBOLS = ['EUR' => '€', 'CHF' => 'CHF', 'USD' => '$', 'GBP' => '£'];
|
||||
|
||||
|
|
@ -156,7 +182,7 @@ class LandingController extends Controller
|
|||
// the catalogue without a translation would otherwise appear on the
|
||||
// public page as "premium_sla".
|
||||
return array_values(array_filter(array_map(
|
||||
fn (string $key) => self::FEATURES[$key] ?? null,
|
||||
fn (string $key) => $this->label($key),
|
||||
$keys,
|
||||
)));
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -73,9 +73,9 @@
|
|||
and " Cloud", it spaced the brand out into three separate words — which is
|
||||
how the page went out reading "Clu Pilot Cloud". The wordmark is a single
|
||||
element now, and the gap only ever separates it from the mark. */
|
||||
.brand{display:flex;align-items:center;justify-content:center;gap:12px;margin-top:34px}
|
||||
.brand svg{width:44px;height:44px;flex:none;border-radius:11px}
|
||||
.word{font-size:22px;font-weight:700;letter-spacing:-.025em;color:var(--ink)}
|
||||
.brand{display:flex;align-items:center;justify-content:center;gap:10px;margin-top:34px}
|
||||
.brand svg{width:36px;height:36px;flex:none}
|
||||
.word{font-size:20px;font-weight:700;letter-spacing:-.025em;color:var(--ink)}
|
||||
.word span{color:var(--muted);font-weight:600}
|
||||
|
||||
h1{margin-top:26px;font-weight:700;font-size:clamp(2rem,6vw,2.9rem);
|
||||
|
|
|
|||
|
|
@ -18,10 +18,7 @@
|
|||
<div aria-hidden="true" class="pointer-events-none absolute inset-0"
|
||||
style="background:radial-gradient(ellipse 52% 46% at 24% 18%,rgba(249,115,22,.20),transparent 68%);"></div>
|
||||
|
||||
<div class="relative flex items-center gap-2.5">
|
||||
<x-ui.logo class="size-8" />
|
||||
<span class="text-xl font-bold tracking-tight text-white">CluPilot</span>
|
||||
</div>
|
||||
<div class="relative"><x-ui.brand size="lg" tone="white" /></div>
|
||||
|
||||
<div class="relative">
|
||||
<p class="mb-7 inline-flex items-center gap-2 rounded-pill px-3.5 py-1.5 text-xs font-semibold"
|
||||
|
|
|
|||
|
|
@ -39,10 +39,7 @@
|
|||
exactly what scrolls under this bar. --}}
|
||||
<div class="mx-auto max-w-[1120px] rounded-xl border border-line bg-surface shadow-sm">
|
||||
<div class="flex h-14 items-center gap-8 pl-4 pr-3">
|
||||
<a href="{{ route('home') }}" class="flex items-center gap-2.5">
|
||||
<x-ui.logo class="size-7" />
|
||||
<span class="font-bold tracking-[-0.02em] text-ink">CluPilot</span>
|
||||
</a>
|
||||
<a href="{{ route('home') }}"><x-ui.brand size="md" /></a>
|
||||
|
||||
<nav class="hidden items-center gap-1 text-sm text-muted lg:flex">
|
||||
@foreach (['#produkt' => 'Produkt', '#ablauf' => 'Ablauf', '#sicherheit' => 'Sicherheit', '#preise' => 'Preise', '#fragen' => 'Fragen'] as $href => $label)
|
||||
|
|
@ -76,10 +73,7 @@
|
|||
<div class="mx-auto max-w-[1120px] px-5 py-14 sm:px-6">
|
||||
<div class="grid gap-10 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<div class="lg:col-span-2">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<x-ui.logo class="size-7" />
|
||||
<span class="font-bold tracking-[-0.02em] text-ink">CluPilot Cloud</span>
|
||||
</div>
|
||||
<x-ui.brand size="md" suffix />
|
||||
<p class="mt-4 max-w-[38ch] text-sm leading-relaxed text-muted">
|
||||
Eine eigene, isolierte Cloud für Ihr Unternehmen — eingerichtet, gesichert und
|
||||
überwacht von uns, mit Serverstandort in der EU.
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@
|
|||
</table>
|
||||
</td>
|
||||
<td style="font-size:17px;line-height:26px;color:#17171c;letter-spacing:-0.2px;">
|
||||
<span style="font-weight:700;">Clu</span><span style="font-weight:700;color:#b8500a;">Pilot</span>
|
||||
<span style="font-weight:700;color:#17171c;">CluPilot</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
|
|
|||
|
|
@ -22,10 +22,7 @@
|
|||
>
|
||||
<div class="flex shrink-0 items-center justify-between gap-2 px-2.5 pb-5">
|
||||
<a href="{{ $console ? \App\Support\AdminArea::home() : route('dashboard') }}" class="flex items-center gap-2.5">
|
||||
<x-ui.logo class="size-7" />
|
||||
{{-- One element: as separate flex children the gap lands inside the
|
||||
word and reads as "Clu Pilot". --}}
|
||||
<span class="whitespace-nowrap text-lg font-bold tracking-tight text-ink">Clu<span class="text-accent-text">Pilot</span></span>
|
||||
<x-ui.brand size="md" />
|
||||
@if ($console)
|
||||
<span class="rounded-sm bg-accent-subtle px-1.5 py-0.5 font-mono text-[9.5px] font-semibold uppercase tracking-[0.1em] text-accent-text">{{ __('admin.badge') }}</span>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
@props([
|
||||
'size' => 'md', // sm | md | lg
|
||||
'suffix' => false, // append "Cloud" in the muted tone
|
||||
'tone' => 'ink', // ink | white — for the dark sign-in plate
|
||||
])
|
||||
{{--
|
||||
The lockup. Mark plus wordmark, one definition.
|
||||
|
||||
It had five. The website header set the word in ink at -0.02em, the
|
||||
sidebar split it into "Clu" plus an orange "Pilot", the sign-in plate set
|
||||
it white at a third size, the placeholder inlined its own, and the
|
||||
maintenance screen had no wordmark at all — five surfaces of one product,
|
||||
each with its own idea of the brand.
|
||||
|
||||
The ink version wins because the website is the newest and most considered
|
||||
of them, and because a two-tone wordmark makes the accent a permanent
|
||||
fixture rather than something used sparingly.
|
||||
|
||||
The mark and the word are the only two children of the flex row, and the
|
||||
word is a single element. `gap` applies between EVERY child of a flex
|
||||
container and a bare text node is a child — written the other way this
|
||||
shipped reading "Clu Pilot Cloud".
|
||||
--}}
|
||||
@php
|
||||
$marks = ['sm' => 'size-6', 'md' => 'size-7', 'lg' => 'size-9'];
|
||||
$words = ['sm' => 'text-base', 'md' => 'text-lg', 'lg' => 'text-xl'];
|
||||
@endphp
|
||||
<span {{ $attributes->merge(['class' => 'inline-flex items-center gap-2.5']) }}>
|
||||
<x-ui.logo class="{{ $marks[$size] ?? $marks['md'] }} shrink-0" />
|
||||
{{-- The directive sits on its own line on purpose. Blade only compiles a
|
||||
directive at a non-word boundary, so `CluPilot@if (…)` leaves the @if
|
||||
as literal text and the matching @endif then blows up the compiled
|
||||
view with a parse error. The newline also supplies the space before
|
||||
"Cloud". --}}
|
||||
<span class="whitespace-nowrap {{ $words[$size] ?? $words['md'] }} font-bold tracking-[-0.025em] {{ $tone === 'white' ? 'text-white' : 'text-ink' }}">CluPilot
|
||||
@if ($suffix)<span class="font-semibold {{ $tone === 'white' ? 'text-white/70' : 'text-muted' }}">Cloud</span>@endif</span>
|
||||
</span>
|
||||
|
|
@ -39,6 +39,7 @@
|
|||
<style>
|
||||
@font-face{font-family:"Plex Sans";src:url("/fonts/ibm-plex-sans-latin-400-normal.woff2") format("woff2");font-weight:400;font-display:swap}
|
||||
@font-face{font-family:"Plex Sans";src:url("/fonts/ibm-plex-sans-latin-600-normal.woff2") format("woff2");font-weight:600;font-display:swap}
|
||||
@font-face{font-family:"Plex Sans";src:url("/fonts/ibm-plex-sans-latin-700-normal.woff2") format("woff2");font-weight:700;font-display:swap}
|
||||
@font-face{font-family:"Plex Mono";src:url("/fonts/ibm-plex-mono-latin-400-normal.woff2") format("woff2");font-weight:400;font-display:swap}
|
||||
|
||||
:root{
|
||||
|
|
@ -66,8 +67,9 @@
|
|||
.body{padding:30px 24px 28px}
|
||||
.brand{display:inline-flex;align-items:center;gap:9px;font-size:17px;font-weight:700;
|
||||
letter-spacing:-.02em;color:var(--ink)}
|
||||
.brand i{font-style:normal;color:var(--accent-ink)}
|
||||
.mark{width:26px;height:26px;border-radius:7px;flex:none;
|
||||
.word{font-size:20px;font-weight:700;letter-spacing:-.025em;color:var(--ink)}
|
||||
.word span{font-weight:600;color:var(--muted)}
|
||||
.mark{width:28px;height:28px;flex:none;
|
||||
background:linear-gradient(135deg,#fb923c,#f97316 55%,#c2560a)}
|
||||
h1{margin-top:18px;font-weight:700;font-size:clamp(1.6rem,4vw,2.1rem);
|
||||
letter-spacing:-.032em;line-height:1.1;color:var(--ink)}
|
||||
|
|
@ -96,7 +98,21 @@
|
|||
<div class="card">
|
||||
<div class="head"><span>{{ __('errors.heading') }}</span><b>{{ $code }}</b></div>
|
||||
<div class="body">
|
||||
<span class="brand"><span class="mark" aria-hidden="true"></span>Clu<i>Pilot</i></span>
|
||||
<span class="brand">
|
||||
<svg class="mark" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<defs><linearGradient id="err-mark" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FB923C"/><stop offset="0.55" stop-color="#F97316"/><stop offset="1" stop-color="#C2560A"/>
|
||||
</linearGradient></defs>
|
||||
<rect width="64" height="64" rx="15" fill="url(#err-mark)"/>
|
||||
<path d="M22 41h20a8 8 0 0 0 .7-15.97A11 11 0 0 0 21.4 27.2 8.5 8.5 0 0 0 22 44z" fill="#ffffff" fill-opacity="0.22"/>
|
||||
<path d="M32 17 44 44l-12-6-12 6z" fill="#ffffff"/>
|
||||
</svg>
|
||||
{{-- One element. `gap` applies between every child of a flex row and a
|
||||
bare text node is a child, so "Clu", <i>Pilot</i> and the space
|
||||
between them were being spaced apart — the same fault the
|
||||
placeholder shipped with. --}}
|
||||
<span class="word">CluPilot <span>Cloud</span></span>
|
||||
</span>
|
||||
<h1>{{ $title }}</h1>
|
||||
@if ($body)
|
||||
<p>{{ $body }}</p>
|
||||
|
|
|
|||
|
|
@ -12,10 +12,7 @@
|
|||
{{-- Form panel --}}
|
||||
<main class="flex flex-1 items-center justify-center px-6 py-12">
|
||||
<div class="w-full max-w-sm animate-rise">
|
||||
<div class="mb-9 flex items-center gap-2.5 lg:hidden">
|
||||
<x-ui.logo class="size-8" />
|
||||
<span class="text-lg font-bold tracking-tight text-ink">Clu<span class="text-accent-text">Pilot</span></span>
|
||||
</div>
|
||||
<x-ui.brand size="md" class="mb-9 lg:hidden" />
|
||||
|
||||
<h1 class="text-3xl font-bold leading-tight tracking-tight text-ink">{{ __('auth.login_title') }}</h1>
|
||||
<p class="mt-2 text-sm text-muted">{{ __('auth.login_subtitle') }}</p>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
<div class="flex min-h-screen items-center justify-center bg-bg px-6 py-12">
|
||||
<div class="w-full max-w-sm animate-rise">
|
||||
<div class="mb-9 flex items-center gap-2.5">
|
||||
<x-ui.logo class="size-8" />
|
||||
<span class="text-lg font-bold tracking-tight text-ink">Clu<span class="text-accent-text">Pilot</span></span>
|
||||
</div>
|
||||
<x-ui.brand size="md" class="mb-9" />
|
||||
|
||||
<h1 class="text-2xl font-bold leading-tight tracking-tight text-ink">{{ __('auth.console_login_title') }}</h1>
|
||||
<p class="mt-2 text-sm text-muted">{{ __('auth.console_login_subtitle') }}</p>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
<div class="flex min-h-screen items-center justify-center bg-bg px-6 py-12">
|
||||
<div class="w-full max-w-sm animate-rise">
|
||||
<div class="mb-9 flex items-center gap-2.5">
|
||||
<x-ui.logo class="size-8" />
|
||||
<span class="text-lg font-bold tracking-tight text-ink">Clu<span class="text-accent-text">Pilot</span></span>
|
||||
</div>
|
||||
<x-ui.brand size="md" class="mb-9" />
|
||||
|
||||
<h1 class="text-2xl font-bold leading-tight tracking-tight text-ink">{{ __('auth.twofa_title') }}</h1>
|
||||
<p class="mt-2 text-sm text-muted">
|
||||
|
|
|
|||
|
|
@ -12,10 +12,7 @@
|
|||
{{-- Form panel --}}
|
||||
<main class="flex flex-1 items-center justify-center px-6 py-12">
|
||||
<div class="w-full max-w-sm animate-rise">
|
||||
<div class="mb-9 flex items-center gap-2.5 lg:hidden">
|
||||
<x-ui.logo class="size-8" />
|
||||
<span class="text-lg font-bold tracking-tight text-ink">Clu<span class="text-accent-text">Pilot</span></span>
|
||||
</div>
|
||||
<x-ui.brand size="md" class="mb-9 lg:hidden" />
|
||||
|
||||
<h1 class="text-3xl font-bold leading-tight tracking-tight text-ink">{{ __('auth.register_title') }}</h1>
|
||||
<p class="mt-2 text-sm text-muted">{{ __('auth.register_subtitle') }}</p>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
<div class="flex min-h-screen items-center justify-center p-6" x-data="{ recovery: false }">
|
||||
<div class="w-full max-w-sm">
|
||||
<div class="mb-6 text-center">
|
||||
<span class="font-mono text-xl font-semibold tracking-tight text-ink">CluPilot</span>
|
||||
</div>
|
||||
<div class="mb-6 flex justify-center"><x-ui.brand size="md" /></div>
|
||||
|
||||
<x-ui.card>
|
||||
<h1 class="text-lg font-semibold text-ink">{{ __('auth.twofa_title') }}</h1>
|
||||
|
|
|
|||
|
|
@ -28,6 +28,17 @@
|
|||
for one page is not a dark mode; it is one page that does not match.
|
||||
--}}
|
||||
<style>
|
||||
/* The fonts, declared here too. The console loads them through Vite, but
|
||||
the 503 page has no stylesheet at all — so the maintenance screen was the
|
||||
one surface rendering in the system sans while every other page was IBM
|
||||
Plex. Same files the placeholder uses; if they are missing the page
|
||||
degrades to a system sans and still reads. */
|
||||
@font-face{font-family:"Plex Sans";src:url("/fonts/ibm-plex-sans-latin-400-normal.woff2") format("woff2");font-weight:400;font-display:swap}
|
||||
@font-face{font-family:"Plex Sans";src:url("/fonts/ibm-plex-sans-latin-500-normal.woff2") format("woff2");font-weight:500;font-display:swap}
|
||||
@font-face{font-family:"Plex Sans";src:url("/fonts/ibm-plex-sans-latin-600-normal.woff2") format("woff2");font-weight:600;font-display:swap}
|
||||
@font-face{font-family:"Plex Sans";src:url("/fonts/ibm-plex-sans-latin-700-normal.woff2") format("woff2");font-weight:700;font-display:swap}
|
||||
@font-face{font-family:"Plex Mono";src:url("/fonts/ibm-plex-mono-latin-400-normal.woff2") format("woff2");font-weight:400;font-display:swap}
|
||||
|
||||
.cpu-wrap {
|
||||
/* portal-tokens.css: --bg, --text-strong, --text-muted, --accent,
|
||||
--accent-text, --surface, --border, --surface-2 */
|
||||
|
|
@ -47,7 +58,10 @@
|
|||
font-family: "IBM Plex Sans", "Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
}
|
||||
.cpu-main { max-width: 34rem; width: 100%; }
|
||||
.cpu-mark { width: 2.75rem; height: 2.75rem; margin: 0 auto; display: block; }
|
||||
.cpu-brand { display: flex; align-items: center; justify-content: center; gap: 0.625rem; }
|
||||
.cpu-mark { width: 2.25rem; height: 2.25rem; flex: none; }
|
||||
.cpu-word { font-size: 1.25rem; font-weight: 700; letter-spacing: -0.025em; color: var(--cpu-fg); }
|
||||
.cpu-word span { font-weight: 600; color: var(--cpu-muted); }
|
||||
.cpu-badge { display: inline-flex; align-items: center; gap: 0.5rem; margin-top: 1.5rem;
|
||||
padding: 0.4rem 0.85rem; border: 1px solid #e8cdb2; border-radius: 9999px;
|
||||
background: #fff3e9; color: var(--cpu-accent-ink);
|
||||
|
|
@ -79,7 +93,7 @@
|
|||
reads as an image that failed to load, and this panel is shown at
|
||||
the one moment somebody is already wondering whether something is
|
||||
broken. --}}
|
||||
<svg class="cpu-mark" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<div class="cpu-brand"><svg class="cpu-mark" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<defs>
|
||||
<linearGradient id="cpu-mark-grad" x1="0" y1="0" x2="64" y2="64" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FB923C"/><stop offset="0.55" stop-color="#F97316"/><stop offset="1" stop-color="#C2560A"/>
|
||||
|
|
@ -89,6 +103,8 @@
|
|||
<path d="M22 41h20a8 8 0 0 0 .7-15.97A11 11 0 0 0 21.4 27.2 8.5 8.5 0 0 0 22 44z" fill="#ffffff" fill-opacity="0.22"/>
|
||||
<path d="M32 17 44 44l-12-6-12 6z" fill="#ffffff"/>
|
||||
</svg>
|
||||
<span class="cpu-word">CluPilot <span>Cloud</span></span>
|
||||
</div>
|
||||
|
||||
<div class="cpu-badge"><span class="cpu-dot"></span>{{ __('updating.badge') }}</div>
|
||||
<h1 class="cpu-title">{{ __('updating.title') }}</h1>
|
||||
|
|
|
|||
|
|
@ -1,21 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="h-full">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>{{ config('app.name') }}</title>
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
</head>
|
||||
<body class="flex min-h-full items-center justify-center bg-bg p-6 text-body antialiased">
|
||||
<main class="text-center">
|
||||
<h1 class="font-mono text-2xl font-semibold tracking-tight text-ink">CluPilot</h1>
|
||||
<p class="mt-2 text-sm text-muted">{{ __('common.tagline') }}</p>
|
||||
@if (Route::has('login'))
|
||||
<a href="{{ route('login') }}"
|
||||
class="mt-6 inline-flex items-center justify-center rounded bg-accent-active px-4 py-2 text-sm font-semibold text-on-accent transition hover:bg-accent-press">
|
||||
{{ __('common.sign_in') }}
|
||||
</a>
|
||||
@endif
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -124,3 +124,92 @@ it('draws the actual mark on the placeholder, not an empty tile', function () {
|
|||
// rounded rectangle.
|
||||
->and($source)->toContain('M32 17 44 44l-12-6-12 6z');
|
||||
});
|
||||
|
||||
it('draws the wordmark from one definition, not five', function () {
|
||||
// It had five: the site header set it in ink, the sidebar split it into
|
||||
// "Clu" plus an orange "Pilot", the sign-in plate set it white at another
|
||||
// size, the placeholder inlined its own, and the maintenance screen had no
|
||||
// wordmark at all. Five surfaces of one product, each with its own idea of
|
||||
// the brand.
|
||||
$offenders = [];
|
||||
|
||||
foreach (File::allFiles(resource_path('views')) as $file) {
|
||||
// The two self-contained pages cannot use a Blade component's compiled
|
||||
// output — they render when the asset build may not exist — and the
|
||||
// component itself obviously spells the word out.
|
||||
// The self-contained pages cannot use a Blade component's compiled
|
||||
// output — they render when the asset build may not exist — and an
|
||||
// email has no stylesheet of ours at all, so its lockup is inline by
|
||||
// necessity. They still have to AGREE with the component, which is
|
||||
// what the next assertion checks.
|
||||
if (in_array($file->getFilename(), ['coming-soon.blade.php', 'updating-panel.blade.php', 'brand.blade.php', 'layout.blade.php'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$source = preg_replace('/\{\{--.*?--\}\}/s', '', File::get($file->getPathname()));
|
||||
|
||||
// The wordmark is markup around the name; a plain sentence mentioning
|
||||
// the company is not. `>Clu` catches exactly the lockup forms.
|
||||
if (preg_match('/>\s*Clu(Pilot|<)/', $source)) {
|
||||
$offenders[] = str_replace(resource_path().'/', '', $file->getPathname());
|
||||
}
|
||||
}
|
||||
|
||||
expect($offenders)->toBeEmpty();
|
||||
|
||||
// The four that must inline it write the name in one piece, in ink, with
|
||||
// no orange half — the shape the component defines.
|
||||
foreach ([
|
||||
'coming-soon.blade.php',
|
||||
'partials/updating-panel.blade.php',
|
||||
'errors/layout.blade.php',
|
||||
'components/mail/layout.blade.php',
|
||||
] as $page) {
|
||||
expect(File::get(resource_path('views/'.$page)))
|
||||
->toContain('CluPilot')
|
||||
->not->toContain('Clu<i>Pilot</i>')
|
||||
->not->toContain('>Clu</span>');
|
||||
}
|
||||
});
|
||||
|
||||
it('gives the maintenance screen the same typeface as everything else', function () {
|
||||
// The console loads IBM Plex through Vite. The 503 page has no stylesheet
|
||||
// at all, so the maintenance screen — the one page shown when somebody is
|
||||
// already worried — was rendering in the system sans.
|
||||
$panel = File::get(resource_path('views/partials/updating-panel.blade.php'));
|
||||
|
||||
expect($panel)->toContain('@font-face')
|
||||
->and($panel)->toContain('/fonts/ibm-plex-sans-latin-700-normal.woff2');
|
||||
|
||||
// And the file it names is actually there. A @font-face pointing at
|
||||
// nothing fails silently, which is how the bold weight went missing from
|
||||
// the placeholder without anyone noticing.
|
||||
foreach (['400', '500', '600', '700'] as $weight) {
|
||||
expect(public_path("fonts/ibm-plex-sans-latin-{$weight}-normal.woff2"))->toBeFile();
|
||||
}
|
||||
});
|
||||
|
||||
it('names the address zone the installation is configured for', function () {
|
||||
// It said "Adresse auf clupilot.cloud" — a domain the company does not own
|
||||
// — while provisioning issued addresses under whatever CLUPILOT_DNS_ZONE
|
||||
// said. The same two-sources-of-truth mistake as a hard-coded price.
|
||||
App\Support\Settings::set('provisioning.dns_zone', 'example.test');
|
||||
|
||||
// Comments stripped: the controller explains at length that it used to
|
||||
// name clupilot.cloud, and a raw scan reads the explanation as the
|
||||
// offence. Third time this suite has had to learn it.
|
||||
$source = implode('', array_map(
|
||||
fn ($token) => is_array($token) && in_array($token[0], [T_COMMENT, T_DOC_COMMENT], true) ? '' : (is_array($token) ? $token[1] : $token),
|
||||
token_get_all(File::get(app_path('Http/Controllers/LandingController.php'))),
|
||||
));
|
||||
|
||||
expect($source)->not->toContain('clupilot.cloud');
|
||||
|
||||
$page = $this->get('/');
|
||||
|
||||
$page->assertOk();
|
||||
|
||||
// Only where the catalogue actually sells the feature; either way the old
|
||||
// literal must be gone.
|
||||
$page->assertDontSee('clupilot.cloud');
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue