Visual - `--radius: 8px` in @theme collapsed Tailwind v4's whole radius scale onto a single value: `rounded` and `rounded-lg` both measured 8px, so a chip, an input and a card could not be told apart. Named steps restore the gradation (6 / 8 / 12px, measured in the browser). - Cards were a 1px border and nothing else. Added warm, brand-tinted elevation tokens — a neutral black shadow over the pink surface reads as dirt — in three steps for cards, dropdowns and overlays, collected behind components/ui/Card.tsx so the 26 hand-copied card class chains have one definition. - KPI tiles lead with the number and carry a tone accent; tables got denser rows, subtle row rules (the full border strength made 800 rows read as a grid), tabular figures in numeric columns and a brand-tinted hover. KPI tiles now link to the view that shows what they count. Making those links honest surfaced two reasons the numbers did not agree with their destinations: - The dashboard read `employees.status`, while every report derives status from entry/exit/karenz dates. A hire whose start date had passed before the cron ran was counted differently on the two pages. The dashboard now uses the same derivation — and one query instead of five. - Eintritte/Austritte counted `entry_date`/`exit_date` while the linked report counts `employee_history`; rehire_employee sets entry_date but logs the event as 'Wiedereintritt', so rehires were missing from the target. Both now count history events. - The employee list filtered on the status column, so it disagreed too. It now filters on derived status in SQL (lib/employee-status-filter.ts). That restates deriveStatusAsOf a second time, in a second language, so an integration test runs both over the full roster and requires identical id sets — drift here is otherwise invisible. Status semantics, per the domain correction: "aktiv" means status Aktiv alone. Karenz is employed but not active, and has its own tile. The active headcount, FTE (Karenz contributes no capacity) and the division bars all follow that; the bars are labelled "Aktive nach Bereich" rather than "Headcount" to say so. The employee filter still offers the combination, named after the two statuses it selects instead of calling the pair active. DEFAULT_STATUSES in lib/reports.ts is deliberately left at Aktiv + Karenz: it governs what the Berichte page shows without an explicit status filter, and therefore what already-saved reports and exports mean.
153 lines
4.5 KiB
CSS
153 lines
4.5 KiB
CSS
@import "tailwindcss";
|
|
|
|
/* Design tokens from spec §7 (ported from tailwind.config.ts for Tailwind v4) */
|
|
@theme {
|
|
--color-brand-50: #fdf3f8;
|
|
--color-brand-100: #fdeaf3;
|
|
--color-brand-200: #f6cfe2;
|
|
--color-brand-500: #d6046e;
|
|
--color-brand-600: #b0035a;
|
|
--color-brand-700: #a30354;
|
|
|
|
--color-surface: #f9f1f5;
|
|
|
|
--color-border: #eedde6;
|
|
--color-border-subtle: #f7e8ef;
|
|
|
|
--color-ink: #2d1c26;
|
|
--color-ink-body: #503b47;
|
|
--color-ink-muted: #7a636f;
|
|
|
|
--color-success-bg: #dff6dd;
|
|
--color-success-text: #0e700e;
|
|
|
|
--color-danger-bg: #fde7e9;
|
|
--color-danger-text: #b10e1c;
|
|
--color-danger-solid: #c50f1f;
|
|
|
|
--color-warning-bg: #fff4ce;
|
|
--color-warning-text: #835b00;
|
|
|
|
--color-info-bg: #d7f0f0;
|
|
--color-info-text: #00666d;
|
|
|
|
--color-purple-bg: #f0ecf7;
|
|
--color-purple-text: #5c2e91;
|
|
|
|
/* A bare `--radius` in Tailwind v4 collapses the whole scale onto one
|
|
value — `rounded` and `rounded-lg` both came out at 8px, so a chip, an
|
|
input and a card could not be told apart. Named steps restore the
|
|
gradation; 8px stays the default so nothing shifts unintentionally. */
|
|
--radius-sm: 4px;
|
|
--radius: 6px;
|
|
--radius-md: 8px;
|
|
--radius-lg: 12px;
|
|
--radius-xl: 16px;
|
|
|
|
/* Warm, brand-tinted rather than neutral grey: a pure black shadow over
|
|
the pink surface reads as dirt. Kept shallow — this is a data-dense
|
|
admin UI, the depth only has to separate a card from the page. */
|
|
--shadow-card: 0 1px 2px rgb(107 33 71 / 0.04), 0 1px 3px rgb(107 33 71 / 0.06);
|
|
--shadow-card-hover: 0 2px 4px rgb(107 33 71 / 0.06), 0 4px 12px rgb(107 33 71 / 0.08);
|
|
--shadow-overlay: 0 8px 32px rgb(45 28 38 / 0.16);
|
|
|
|
--font-sans: var(--font-nunito), system-ui, sans-serif;
|
|
}
|
|
|
|
body {
|
|
background-color: var(--color-surface);
|
|
color: var(--color-ink);
|
|
/* iOS bounce-scrolls the whole document behind fixed overlays (drawer,
|
|
modal) and shows the white page edge; contain keeps the rubber band
|
|
inside the scroller that actually overflowed. */
|
|
overscroll-behavior-y: none;
|
|
}
|
|
|
|
/* Mobile browsers zoom the page when a focused input is under ~16px. Every
|
|
form control in this app is text-sm (14px), so without this, tapping any
|
|
field on iOS Safari jumps the layout. */
|
|
@media (max-width: 767px) {
|
|
input,
|
|
select,
|
|
textarea {
|
|
font-size: 16px;
|
|
}
|
|
}
|
|
|
|
/* Touch devices have no hover, so the grey flash Chrome/Safari paint on tap
|
|
is the only press feedback — but their default is a hard blue box. */
|
|
@media (hover: none) {
|
|
* {
|
|
-webkit-tap-highlight-color: color-mix(in srgb, var(--color-brand-500) 12%, transparent);
|
|
}
|
|
}
|
|
|
|
/* ── React Flow chrome ─────────────────────────────────────────────
|
|
The library ships a generic grey control panel; these bring it onto the
|
|
app's own palette and give the buttons a real touch target. */
|
|
.orgchart-canvas .react-flow__controls {
|
|
gap: 2px;
|
|
border-radius: var(--radius);
|
|
border: 1px solid var(--color-border);
|
|
background: #fff;
|
|
box-shadow: 0 2px 10px rgb(45 28 38 / 0.08);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.orgchart-canvas .react-flow__controls-button {
|
|
width: 32px;
|
|
height: 32px;
|
|
border: none;
|
|
border-bottom: 1px solid var(--color-border-subtle);
|
|
background: #fff;
|
|
color: var(--color-ink-body);
|
|
}
|
|
|
|
.orgchart-canvas .react-flow__controls-button:last-child {
|
|
border-bottom: none;
|
|
}
|
|
|
|
.orgchart-canvas .react-flow__controls-button:hover {
|
|
background: var(--color-brand-100);
|
|
color: var(--color-brand-700);
|
|
}
|
|
|
|
.orgchart-canvas .react-flow__controls-button svg {
|
|
fill: currentColor;
|
|
max-width: 14px;
|
|
max-height: 14px;
|
|
}
|
|
|
|
.orgchart-canvas .react-flow__minimap {
|
|
border-radius: var(--radius);
|
|
border: 1px solid var(--color-border);
|
|
background: #fff;
|
|
box-shadow: 0 2px 10px rgb(45 28 38 / 0.08);
|
|
}
|
|
|
|
/* Node selection has to stay enabled (it is what gives the card pointer
|
|
events at all — see GraphOrgChart), but the card carries its own hover and
|
|
match styling, so React Flow's default selected/focus outline is noise. */
|
|
.orgchart-canvas .react-flow__node.selected,
|
|
.orgchart-canvas .react-flow__node:focus,
|
|
.orgchart-canvas .react-flow__node:focus-visible {
|
|
outline: none;
|
|
box-shadow: none;
|
|
}
|
|
|
|
.orgchart-canvas .react-flow__attribution {
|
|
background: transparent;
|
|
font-size: 10px;
|
|
}
|
|
|
|
.orgchart-canvas .react-flow__attribution a {
|
|
color: var(--color-ink-muted);
|
|
}
|
|
|
|
/* The minimap costs more screen than it earns on a phone. */
|
|
@media (max-width: 1023px) {
|
|
.orgchart-canvas .react-flow__minimap {
|
|
display: none;
|
|
}
|
|
}
|