Files
picpeak/frontend/src/index.css
T
Luca a7e16e7bf6 feat(crm): frontend code — pages + services + components
Brings in the full frontend CRM stack: admin authoring pages,
customer-portal surfaces, public response flows, typed services,
and the supporting component library. i18n locale JSON is the next
commit (kept separate so reviewers can read it as data).

Pages
  - Quotes: list / editor / detail / public accept-decline
  - Invoices: list / editor / detail / public payment-check
  - Contracts: list / editor / detail / block library / public sign
  - Calendar (FullCalendar — admin-only v1)
  - Tax report (period picker + CSV/PDF export)
  - Hours (logged time entries, per-customer)
  - Deals lineage (DocumentLineageCard surfaces)
  - CRM Development (admin dev tools, gated by crmDevelopment flag)
  - Customer-portal pages for quotes / invoices / contracts
  - Settings reorg: CRM-Settings group + dedicated tabs for Business
    Profile, CRM behaviour, Contracts block library, Reminder emails
  - BrandingPage typography (PDF font picker)
  - EventDetailsPage / CustomerDetailPage / CreateEventPage extensions
    (event-time fields, hours toggle, per-event reminder override)

Services (typed)
  - quotes.service, bills.service, contracts.service
  - customerAdmin.service, deals.service, calendar.service,
    taxReport.service, contracts-blocks.service
  - businessProfile.service (timezone, font picker, bank accounts)
  - useInstallmentDefaults hook, useLocalizedDate dateInputLang extension

Components (admin)
  - CustomerPicker (shared across quote/invoice/contract editors)
  - LineItemsTable (hierarchy + details_text, memoised pricing)
  - InstallmentsPanel (simple + advanced toggle, fixed-date vs trigger)
  - DocumentLineageCard (deal_uuid grouped view)
  - EditInstallmentPlanModal (atomic post-spawn plan reshape)
  - EventReminderOverrideCard, EmailTemplateEditor (tiptap),
    PdfFontPicker, IntegrityCheckCard
  - Feature-flag context + RequireFeature wrapper + AdminSidebar
    featureFlagsAny derivation + UI-hiding sweep

Build infra
  - vite.config: fullcalendar chunk carved off (~200 KB lazy-loaded)
  - frontend/package.json: tiptap, fullcalendar, signature_pad,
    react-international-phone, et al.
  - tailwind + prose styles updated for editor surfaces

3-way merge note: 1 conflict (CustomerDetailPage.tsx) hand-resolved
to keep upstream's SUPPORTED_LANGUAGES.map() data-driven pattern
over feat/crm's hardcoded option list; feat/crm's DecimalInput
import preserved alongside.
2026-05-26 18:19:32 +02:00

719 lines
20 KiB
CSS

/*
* Self-hosted Inter is bootstrapped here so the very first paint —
* before React mounts and runs the dynamic @font-face injector — has
* the default body font available. Replaces the previous Google Fonts
* @import that leaked visitor IPs to fonts.googleapis.com (LG München
* 2022 GDPR ruling). All other families are injected on-demand by
* frontend/src/contexts/ThemeContext.tsx based on the active theme.
*/
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 400; font-display: swap; src: url('/fonts/Inter/400.woff2') format('woff2'); }
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 600; font-display: swap; src: url('/fonts/Inter/600.woff2') format('woff2'); }
@font-face { font-family: 'Inter'; font-style: normal; font-weight: 700; font-display: swap; src: url('/fonts/Inter/700.woff2') format('woff2'); }
/* Import image protection styles */
@import './styles/image-protection.css';
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/*
* Theme CSS Variables — 8-token CI palette.
* Token names are kept aligned with frontend/src/types/theme.types.ts
* (ThemeConfig). ThemeContext.applyTheme() writes these from the active
* theme/branding settings; values here are the "Classic Grid" defaults.
*/
--color-background: #fafafa; /* page base */
--color-surface: #ffffff; /* cards, nav, alternating sections */
--color-elevated: #f5f5f5; /* raised panels */
--color-surface-border: #e5e5e5; /* dividers, borders (a.k.a. border token) */
--color-text: #171717; /* primary text */
--color-muted-text: #737373; /* secondary text */
--color-accent: #22c55e; /* links, focus rings, hover */
--color-accent-dark: #5C8762; /* primary CTA fill */
/* Legacy aliases — kept for any consumer that still reads --color-primary.
* Both resolve to the accent-dark token (the previous "primary" CTA color). */
--color-primary: #5C8762;
--color-primary-light: #7aa583;
--color-primary-dark: #4a6f4f;
--font-family: 'Inter', 'Noto Sans', system-ui, -apple-system, sans-serif;
--heading-font-family: 'Inter', 'Noto Sans', system-ui, -apple-system, sans-serif;
--border-radius: 0.5rem;
--font-size-base: 16px;
--shadow-default: 0 4px 6px rgba(0,0,0,0.1);
/* Tailwind RGB values for primary color (legacy, used by primary-* utilities) */
--tw-color-primary: 92 135 98;
--radius: 0.5rem;
}
/*
* Admin dark mode unification.
* AdminDarkModeContext toggles `.dark` on <html> without going through
* applyTheme(). Previously this only flipped the components that had
* explicit `.dark .x` overrides; everything else (cards/inputs/buttons
* that now read CSS variables) stayed light. We re-declare the 8-token
* defaults under `.dark` so the same variables resolve to a dark palette
* whenever the class is present. Gallery applyTheme() still wins because
* it writes inline `--color-*` styles on the html element, which beat the
* .dark stylesheet rule in the cascade.
*/
.dark {
--color-background: #0a0a0a;
--color-surface: #171717;
--color-elevated: #1f1f1f;
--color-surface-border: #262626;
--color-text: #f5f5f5;
--color-muted-text: #a3a3a3;
--color-accent: #22c55e;
--color-accent-dark: #5C8762;
}
* {
font-family: var(--font-family);
}
body {
background-color: var(--color-background);
color: var(--color-text);
font-size: var(--font-size-base);
@apply antialiased;
overflow-x: hidden;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: var(--background-pattern);
background-size: var(--background-pattern-size);
opacity: 1;
pointer-events: none;
z-index: -1;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
@apply bg-neutral-100;
}
::-webkit-scrollbar-thumb {
@apply bg-neutral-300 rounded-full;
}
::-webkit-scrollbar-thumb:hover {
@apply bg-neutral-400;
}
.dark ::-webkit-scrollbar-track {
@apply bg-neutral-800;
}
.dark ::-webkit-scrollbar-thumb {
@apply bg-neutral-600;
}
.dark ::-webkit-scrollbar-thumb:hover {
@apply bg-neutral-500;
}
}
@layer components {
/*
* Button styles.
*
* Migration note: .btn-primary now binds to --color-accent-dark instead
* of --color-primary. The two tokens are kept in lockstep by the
* customizer (handleChange syncs primaryColor → accentDarkColor) and by
* the migration helper (legacy themes get accentDarkColor = primaryColor),
* so existing instances render identically after upgrade — but new themes
* that set only accentDarkColor (the 8-token CI flow) now drive primary
* buttons correctly. White text + accent-dark fill matches the rest of
* the selected-state visual language (sidebar, tile-selected, segmented
* buttons). Hover stays on --color-primary-dark for the auto-darkened
* legacy behaviour.
*/
.btn {
@apply inline-flex items-center justify-center font-medium whitespace-nowrap transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50;
border-radius: var(--border-radius);
--tw-ring-color: var(--color-accent);
--tw-ring-offset-color: var(--color-background);
}
.btn-primary {
background-color: var(--color-accent-dark);
color: white;
@apply hover:opacity-90;
}
.btn-primary:hover {
background-color: var(--color-primary-dark);
}
.btn-alt {
background-color: transparent;
color: var(--color-text);
border: 2px solid var(--color-text);
}
.btn-alt:hover {
background-color: var(--color-text);
color: var(--color-background);
}
.btn-secondary {
background-color: var(--color-surface-border);
color: var(--color-text);
@apply hover:opacity-80;
}
.btn-outline {
border-color: var(--color-surface-border);
background-color: transparent;
color: var(--color-muted-text);
@apply border hover:opacity-80;
}
.btn-sm {
@apply h-9 px-3 text-sm;
}
.btn-md {
@apply h-10 px-4 py-2;
}
.btn-lg {
@apply h-11 px-8 text-lg;
}
/*
* Input styles - Admin inputs keep explicit Tailwind colors so the visual
* contract is byte-for-byte identical to pre-migration (border-neutral-300
* vs the lighter neutral-200/surface-border). Dark mode is handled by the
* .dark .input override below. Gallery inputs use .input-themed which
* binds to the 8-token palette via CSS variables.
*/
.input {
@apply flex h-10 w-full rounded-lg border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-600 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
@apply bg-white border-neutral-300 text-neutral-900;
}
.input::placeholder {
@apply text-neutral-500;
}
/* Gallery-specific input that uses theme variables */
.input-themed {
@apply flex h-10 w-full rounded-lg border px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-600 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50;
background-color: var(--color-surface);
border-color: var(--color-surface-border);
color: var(--color-text);
}
.input-themed::placeholder {
color: var(--color-muted-text);
}
/* Card styles - Admin cards keep their explicit Tailwind colors for
* migration safety (visible border lightness change otherwise); the
* .dark .card override below handles admin dark mode. Gallery uses
* .card-themed which binds to the 8-token palette. */
.card {
@apply rounded-xl border bg-white border-neutral-200;
box-shadow: var(--shadow-default);
}
.card-hover {
@apply card transition-all duration-200 hover:translate-y-[-2px];
}
/* Gallery-specific card that uses theme variables */
.card-themed {
@apply rounded-xl border;
background-color: var(--color-surface);
border-color: var(--color-surface-border);
box-shadow: var(--shadow-default);
}
/* Headings with custom font */
h1, h2, h3, h4, h5, h6 {
font-family: var(--heading-font-family);
}
/* Container */
.container {
@apply mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl w-full;
}
/*
* Theme-token utility classes — exposed so any component (admin or gallery)
* can opt into the 8-token palette without an inline style. The Tailwind
* config also exposes these as full color aliases (bg-surface, text-theme,
* etc.) but these single-purpose classes are kept for back-compat with
* existing call sites.
*/
.bg-background {
background-color: var(--color-background);
}
.bg-surface {
background-color: var(--color-surface);
}
.bg-elevated {
background-color: var(--color-elevated);
}
.bg-accent {
background-color: var(--color-accent);
}
.bg-accent-dark {
background-color: var(--color-accent-dark);
}
.border-surface {
border-color: var(--color-surface-border);
}
.text-theme {
color: var(--color-text);
}
.text-muted-theme {
color: var(--color-muted-text);
}
.text-accent {
color: var(--color-accent);
}
.text-accent-dark {
color: var(--color-accent-dark);
}
/*
* Low-opacity accent fills used as "icon tile" / "tinted chip"
* backgrounds (Features tab feature icons, Sidebar Preview pills,
* Customer dashboard branding card header). Picks up the admin's CI
* accent automatically — previously these places used Tailwind's
* hardcoded `primary-50` palette which stayed green regardless of
* branding settings.
*
* Mix weights bumped from 12% / 28% to 18% / 55% so the tint reads
* as "tinted" rather than "barely there" on dark backgrounds. The
* pill foreground intentionally falls back to a high-contrast theme
* colour rather than the accent itself (.text-accent-dark on an
* accent-soft bg disappears because both come from the same
* source).
*
* color-mix is supported on every browser we ship (Chromium 111+,
* Safari 16.2+, Firefox 113+ — see baseline). Older browsers fall
* back to the var(--color-accent-dark) below.
*/
.bg-accent-soft {
background-color: var(--color-accent-dark);
background-color: color-mix(in srgb, var(--color-accent-dark) 18%, transparent);
}
.dark .bg-accent-soft {
background-color: var(--color-accent-dark);
background-color: color-mix(in srgb, var(--color-accent-dark) 55%, transparent);
}
.border-accent-soft {
border-color: var(--color-accent-dark);
border-color: color-mix(in srgb, var(--color-accent-dark) 45%, transparent);
}
.dark .border-accent-soft {
border-color: color-mix(in srgb, var(--color-accent-dark) 70%, transparent);
}
/*
* Foreground for content sitting on .bg-accent-soft. Uses the
* theme's text token (already high-contrast against both light
* backgrounds and dark surfaces) instead of the accent itself —
* accent-on-accent-soft was the reason the Sidebar Preview pills
* read as "barely visible".
*/
.text-on-accent-soft {
color: var(--color-accent-dark);
}
.dark .text-on-accent-soft {
color: #ffffff;
}
/*
* Selected-tile state for picker grids (Gallery Layout, Filter Bar Style,
* Header Style, Hero Divider, Theme Presets). Used in place of the dim
* "border + light tint + accent text" pattern which had poor contrast on
* dark themes (accent text on dim accent bg). The full accent-dark fill
* with white descendants gives a strong, accessible selection cue and
* follows the user's CI palette.
*/
.tile-selected {
background-color: var(--color-accent-dark) !important;
border-color: var(--color-accent-dark) !important;
color: #ffffff !important;
}
.tile-selected *,
.tile-selected svg {
color: #ffffff !important;
}
/*
* Inline hover tooltip used by the colour-picker info icons.
* Pure CSS — no library, no JS state. Wrap an `<Info>` icon (or any
* trigger) in <span class="info-tooltip" data-tooltip="..."> and a
* positioned bubble fades in on hover/focus. The native HTML `title`
* attribute has a ~1.5s delay and is suppressed in some browsers, which
* is why earlier iterations appeared not to work for users.
*/
.info-tooltip {
position: relative;
display: inline-flex;
cursor: help;
}
.info-tooltip::after {
content: attr(data-tooltip);
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
z-index: 50;
min-width: 200px;
max-width: 320px;
padding: 0.5rem 0.625rem;
border-radius: 0.375rem;
background-color: #171717;
color: #fafafa;
font-size: 0.75rem;
line-height: 1.35;
font-weight: 400;
text-align: left;
white-space: normal;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.35);
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease-out;
}
.info-tooltip:hover::after,
.info-tooltip:focus-visible::after {
opacity: 1;
}
.dark .info-tooltip::after {
background-color: #fafafa;
color: #171717;
}
/* Image loading skeleton */
.skeleton {
@apply animate-pulse rounded-lg bg-neutral-200;
}
/* Gallery grid - Mobile optimized */
.gallery-grid {
@apply grid gap-2 sm:gap-3 md:gap-4 grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6;
}
/* Modal overlay */
.modal-overlay {
@apply fixed inset-0 bg-black/50 backdrop-blur-sm animate-fade-in;
}
/* Badge */
.badge {
@apply inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium;
}
.badge-success {
@apply bg-green-100 text-green-800;
}
.badge-warning {
@apply bg-amber-100 text-amber-800;
}
.badge-danger {
@apply bg-red-100 text-red-800;
}
/* Admin dark mode overrides (via Tailwind dark: class on html) */
.dark .card {
@apply bg-neutral-800 border-neutral-700;
}
.dark .card-themed {
@apply bg-neutral-800 border-neutral-700;
}
.dark .input {
@apply bg-neutral-800 border-neutral-700 text-neutral-100;
}
.dark .input::placeholder {
@apply text-neutral-500;
}
/*
* Customer-surface scope (#354): the default .input and .card classes
* hard-code bg-white, and the customer surface uses theme variables
* rather than the admin's `.dark` class trigger. Overriding here so
* every <Input> and <Card> rendered inside the customer surface picks
* up var(--color-surface) / var(--color-text) automatically — no
* per-page wrapper needed. Same treatment for native <select>
* elements which share the .input mental model on the profile and
* accept-invite forms.
*
* The .customer-surface marker is set on the root <div> of every
* customer page (CustomerLayout, CustomerLoginPage, CustomerAcceptInvitePage,
* CustomerResetPasswordPage). Pages outside this scope (admin, public
* gallery) keep their original styling.
*/
.customer-surface .card,
.customer-surface .card-hover {
background-color: var(--color-surface);
border-color: var(--color-surface-border);
}
.customer-surface .input,
.customer-surface select.input,
.customer-surface input.input {
background-color: var(--color-surface);
border-color: var(--color-surface-border);
color: var(--color-text);
}
.customer-surface .input::placeholder {
color: var(--color-muted-text);
opacity: 0.7;
}
.customer-surface .input:disabled {
background-color: var(--color-elevated, var(--color-surface));
color: var(--color-muted-text);
opacity: 0.7;
}
.dark .input-themed {
@apply bg-neutral-800 border-neutral-700 text-neutral-100;
}
.dark .input-themed::placeholder {
@apply text-neutral-500;
}
.dark .skeleton {
@apply bg-neutral-700;
}
.dark .badge-success {
@apply bg-green-900/40 text-green-300;
}
.dark .badge-warning {
@apply bg-amber-900/40 text-amber-300;
}
.dark .badge-danger {
@apply bg-red-900/40 text-red-300;
}
/* Secondary and outline buttons for admin dark mode — restore explicit
* neutral overrides so existing admin pages render exactly as before. */
.dark .btn-secondary {
@apply bg-neutral-700 border-neutral-600 text-neutral-100;
}
.dark .btn-outline {
@apply border-neutral-600 text-neutral-300;
}
.dark .btn-outline:hover {
@apply bg-neutral-800;
}
/* Table styles for admin dark mode */
.dark table {
@apply bg-neutral-800;
}
.dark thead {
@apply bg-neutral-900/50;
}
.dark th {
@apply text-neutral-300 border-neutral-700;
}
.dark td {
@apply text-neutral-200 border-neutral-700;
}
.dark tbody tr {
@apply border-neutral-700;
}
.dark tbody tr:hover {
@apply bg-neutral-700/50;
}
/* Select/dropdown for admin dark mode */
.dark select {
@apply bg-neutral-800 border-neutral-700 text-neutral-100;
}
/* Tag/chip styles for admin dark mode */
.dark .tag,
.dark [class*="tag-"],
.dark .chip {
@apply bg-neutral-700 text-neutral-200 border-neutral-600;
}
/* Alert/info boxes for admin dark mode */
.dark .alert-info {
@apply bg-blue-900/30 border-blue-800 text-blue-200;
}
.dark .alert-warning {
@apply bg-amber-900/30 border-amber-800 text-amber-200;
}
.dark .alert-success {
@apply bg-green-900/30 border-green-800 text-green-200;
}
.dark .alert-danger {
@apply bg-red-900/30 border-red-800 text-red-200;
}
}
@layer utilities {
/* Hide scrollbar for Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Hide scrollbar for IE, Edge and Firefox */
.no-scrollbar {
-ms-overflow-style: none;
scrollbar-width: none;
}
/* Text gradient */
.text-gradient {
@apply bg-clip-text text-transparent bg-gradient-to-r from-primary-600 to-primary-800;
}
/* Smooth scroll */
.smooth-scroll {
scroll-behavior: smooth;
}
/* Custom range slider styles - Updated */
input[type="range"].slider {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 100%;
height: 8px;
background: #d4d4d4 !important;
border-radius: 4px;
outline: none;
cursor: pointer;
position: relative;
}
/* Webkit browsers like Chrome/Safari */
input[type="range"].slider::-webkit-slider-track {
width: 100%;
height: 8px;
background: #d4d4d4 !important;
border-radius: 4px;
border: none;
}
input[type="range"].slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 20px;
height: 20px;
background: var(--color-primary);
border-radius: 50%;
cursor: pointer;
margin-top: -6px; /* Center the thumb vertically */
transition: all 0.2s ease;
}
input[type="range"].slider::-webkit-slider-thumb:hover {
background: var(--color-primary-dark);
transform: scale(1.1);
}
/* Firefox */
input[type="range"].slider::-moz-range-track {
width: 100%;
height: 8px;
background: #d4d4d4 !important;
border-radius: 4px;
border: none;
}
input[type="range"].slider::-moz-range-thumb {
border: none;
width: 20px;
height: 20px;
background: var(--color-primary);
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
}
input[type="range"].slider::-moz-range-thumb:hover {
background: var(--color-primary-dark);
transform: scale(1.1);
}
/* IE/Edge */
input[type="range"].slider::-ms-track {
width: 100%;
height: 8px;
background: transparent;
border-color: transparent;
color: transparent;
}
input[type="range"].slider::-ms-fill-lower {
background: #d4d4d4;
border-radius: 4px;
}
input[type="range"].slider::-ms-fill-upper {
background: #d4d4d4;
border-radius: 4px;
}
input[type="range"].slider::-ms-thumb {
width: 20px;
height: 20px;
background: var(--color-primary);
border-radius: 50%;
cursor: pointer;
margin-top: 0;
}
}