feat(portal): design foundation — Tailwind v3, tokens, self-hosted fonts

- Downgrade Tailwind v4 -> v3 (user decision): postcss.config.js,
  tailwind.config.js mapping framework-neutral CSS-var tokens onto utilities.
- portal-tokens.css: light enterprise palette, single orange accent, IBM Plex
  type scale, radius/shadow/motion/focus (design handoff §6). AA-safe accent
  text/fill tokens (accent-active/-press/-text) — #f97316 alone fails AA.
- Self-hosted IBM Plex Sans+Mono via @fontsource, Vite-bundled (R14, no CDN).
- app.css: v3 layers, base type, uniform :focus-visible, reduced-motion.
- layouts/portal.blade.php base layout; welcome page retokenised (guarded
  login CTA, DE/EN via lang/common) — no v4-only classes.
- Reviewed with Codex (R15): 5 rounds, all findings fixed, final pass clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-25 00:43:01 +02:00
parent 2ae7595aff
commit 53c2a12d6d
11 changed files with 1283 additions and 547 deletions

11
lang/de/common.php Normal file
View File

@ -0,0 +1,11 @@
<?php
return [
'tagline' => 'Kontrollzentrum für Ihre Cloud.',
'sign_in' => 'Anmelden',
'save' => 'Speichern',
'cancel' => 'Abbrechen',
'online' => 'Online',
'offline' => 'Offline',
'loading' => 'Wird geladen …',
];

11
lang/en/common.php Normal file
View File

@ -0,0 +1,11 @@
<?php
return [
'tagline' => 'Control center for your cloud.',
'sign_in' => 'Sign in',
'save' => 'Save',
'cancel' => 'Cancel',
'online' => 'Online',
'offline' => 'Offline',
'loading' => 'Loading …',
];

1363
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -7,13 +7,16 @@
"dev": "vite"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"autoprefixer": "^10.5.4",
"concurrently": "^9.0.1",
"laravel-vite-plugin": "^3.1",
"tailwindcss": "^4.0.0",
"postcss": "^8.5.23",
"tailwindcss": "^3.4.19",
"vite": "^8.0.0"
},
"dependencies": {
"@fontsource/ibm-plex-mono": "^5.3.0",
"@fontsource/ibm-plex-sans": "^5.3.0",
"laravel-echo": "^2.4.0",
"pusher-js": "^8.6.0"
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

View File

@ -1,9 +1,52 @@
@import 'tailwindcss';
/* Self-hosted fonts (R14) @fontsource ships the .woff2 locally; Vite bundles
them and serves them from our own origin. No Google Fonts / CDN request. */
@import '@fontsource/ibm-plex-sans/400.css';
@import '@fontsource/ibm-plex-sans/500.css';
@import '@fontsource/ibm-plex-sans/600.css';
@import '@fontsource/ibm-plex-sans/700.css';
@import '@fontsource/ibm-plex-mono/400.css';
@import '@fontsource/ibm-plex-mono/500.css';
@import '@fontsource/ibm-plex-mono/600.css';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
/* Design tokens, then Tailwind v3 layers. */
@import './portal-tokens.css';
@theme {
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
'Segoe UI Symbol', 'Noto Color Emoji';
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
html {
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
}
body {
background: var(--bg);
color: var(--text);
font-size: var(--text-base);
line-height: var(--lh-normal);
}
/* Uniform, visible focus for every interactive element (a11y §6.4/§9). */
:focus-visible {
outline: none;
box-shadow: var(--focus-ring);
border-color: var(--accent);
}
/* Data/IDs/IPs use tabular figures. */
.font-mono {
font-variant-numeric: tabular-nums;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
}
}

View File

@ -0,0 +1,62 @@
/* CluPilot customer-portal design tokens refined enterprise, light, single
orange accent. Framework-neutral CSS custom properties (work in Tailwind v3
or v4). Values calibrated for AA contrast. See design handoff §6. */
:root {
/* Surfaces (light, slightly cool) */
--bg: #f6f7f9;
--surface: #ffffff;
--surface-2: #f1f3f5;
--surface-hover: #f8f9fb;
/* Lines */
--border: #e4e7ec;
--border-strong: #d0d5dd;
/* Text */
--text-strong: #101828;
--text: #344054;
--text-muted: #667085;
--text-faint: #98a2b3;
/* Accent = the single brand / interaction tone: orange */
--accent: #f97316; /* brand tone: decorative fills, borders, tints, focus */
--accent-hover: #ea6a0c;
--accent-active: #c2560a; /* AA-safe fill for white text on orange (~5:1) */
--accent-press: #a8480a; /* darker press/hover for accent-fill buttons */
--accent-subtle: #fff4ec;
--accent-border: #fed7aa;
--accent-ring: rgba(249, 115, 22, .35);
--on-accent: #ffffff;
/* Accent as TEXT on light surfaces (links, small labels) must be dark enough
for AA #f97316 on white is only ~2.9:1, so text uses --accent-active. */
--accent-text: #c2560a;
/* Status semantics — only in badges/alerts, never a second decorative tone */
--success: #067a48; --success-bg: #ecfdf3; --success-border: #abefc6;
--warning: #b54708; --warning-bg: #fffaeb; --warning-border: #fedf89;
--danger: #b42318; --danger-bg: #fef3f2; --danger-border: #fecdca;
--info: #175cd3; --info-bg: #eff8ff; --info-border: #b2ddff;
/* Typography */
--font-sans: "IBM Plex Sans", ui-sans-serif, system-ui, sans-serif;
--font-mono: "IBM Plex Mono", ui-monospace, monospace;
--text-xs: 12px; --text-sm: 13px; --text-base: 14px; --text-md: 15px;
--text-lg: 18px; --text-xl: 22px; --text-2xl: 28px; --text-3xl: 34px;
--lh-tight: 1.2; --lh-normal: 1.5;
--tracking-tight: -0.02em;
--tracking-label: 0.06em;
/* Radius / shadow / motion / focus */
--radius-sm: 6px; --radius: 8px; --radius-lg: 12px; --radius-pill: 999px;
--shadow-xs: 0 1px 2px rgba(16, 24, 40, .05);
--shadow-sm: 0 1px 3px rgba(16, 24, 40, .10), 0 1px 2px rgba(16, 24, 40, .06);
--shadow-md: 0 4px 8px -2px rgba(16, 24, 40, .10), 0 2px 4px -2px rgba(16, 24, 40, .06);
--dur-fast: 120ms; --dur: 180ms; --dur-slow: 240ms;
--ease: cubic-bezier(.2, .6, .2, 1);
--focus-ring: 0 0 0 3px var(--accent-ring);
}

View File

@ -0,0 +1,16 @@
<!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">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? config('app.name') }}</title>
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
</head>
<body class="min-h-full bg-bg text-body antialiased">
{{ $slot }}
@livewireScripts
</body>
</html>

File diff suppressed because one or more lines are too long

60
tailwind.config.js Normal file
View File

@ -0,0 +1,60 @@
/** @type {import('tailwindcss').Config} */
// Tailwind v3. Design tokens live as CSS custom properties in
// resources/css/portal-tokens.css (framework-neutral); this config only maps
// them onto utility names so a token change restyles every utility.
export default {
content: [
'./resources/**/*.blade.php',
'./resources/**/*.js',
'./app/Livewire/**/*.php',
'./app/View/**/*.php',
'./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
'./vendor/wire-elements/modal/resources/views/**/*.blade.php',
],
theme: {
extend: {
colors: {
bg: 'var(--bg)',
surface: 'var(--surface)',
'surface-2': 'var(--surface-2)',
'surface-hover': 'var(--surface-hover)',
line: 'var(--border)',
'line-strong': 'var(--border-strong)',
ink: 'var(--text-strong)',
body: 'var(--text)',
muted: 'var(--text-muted)',
faint: 'var(--text-faint)',
accent: {
DEFAULT: 'var(--accent)',
hover: 'var(--accent-hover)',
active: 'var(--accent-active)',
press: 'var(--accent-press)',
subtle: 'var(--accent-subtle)',
border: 'var(--accent-border)',
text: 'var(--accent-text)',
},
'on-accent': 'var(--on-accent)',
success: { DEFAULT: 'var(--success)', bg: 'var(--success-bg)', border: 'var(--success-border)' },
warning: { DEFAULT: 'var(--warning)', bg: 'var(--warning-bg)', border: 'var(--warning-border)' },
danger: { DEFAULT: 'var(--danger)', bg: 'var(--danger-bg)', border: 'var(--danger-border)' },
info: { DEFAULT: 'var(--info)', bg: 'var(--info-bg)', border: 'var(--info-border)' },
},
fontFamily: {
sans: 'var(--font-sans)',
mono: 'var(--font-mono)',
},
fontSize: {
xs: 'var(--text-xs)', sm: 'var(--text-sm)', base: 'var(--text-base)', md: 'var(--text-md)',
lg: 'var(--text-lg)', xl: 'var(--text-xl)', '2xl': 'var(--text-2xl)', '3xl': 'var(--text-3xl)',
},
borderRadius: {
sm: 'var(--radius-sm)', DEFAULT: 'var(--radius)', lg: 'var(--radius-lg)', pill: 'var(--radius-pill)',
},
boxShadow: {
xs: 'var(--shadow-xs)', sm: 'var(--shadow-sm)', md: 'var(--shadow-md)',
},
ringColor: { accent: 'var(--accent-ring)' },
},
},
plugins: [],
};

View File

@ -1,6 +1,6 @@
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import tailwindcss from '@tailwindcss/vite';
// Tailwind v3 runs via PostCSS (postcss.config.js) — no Vite plugin.
// Fonts are self-hosted locally (R14) — declared via @font-face in app.css,
// NOT fetched from a CDN. No bunny()/Google Fonts provider here.
@ -17,7 +17,6 @@ export default defineConfig({
input: ['resources/css/app.css', 'resources/js/app.js'],
refresh: true,
}),
tailwindcss(),
],
server: {
host: '0.0.0.0',