Files
planflow/src/app/globals.css
T
Claude d9f55f866f WP-04 (suite) : vues par poste et par mois, duplication, édition, impression
Édition
- Le panneau de créneau sert aussi à la modification : déplacer un créneau,
  c'est changer son jour, son salarié ou ses heures — exactement les champs de
  la création. Deux écrans distincts finiraient par diverger.
- Duplication d'une semaine vers une autre, refusée si la destination contient
  déjà des créneaux. Le report se fait **par heure locale**, pas par décalage de
  sept jours : entre mars et avril, sept jours d'écart ne redonnent pas la même
  heure.

Vues
- `/planning/etiquettes` : la semaine par poste. C'est la question du
  responsable d'ouverture — « la caisse est-elle tenue samedi ? » — illisible
  sur une grille dont les lignes sont des personnes. Les postes sans créneau
  restent affichés : un poste vide toute la semaine est une information.
- `/planning/mois` : les heures par salarié et par jour, avec les journées de
  plus de 10 h signalées. Vue de contrôle, pas d'édition.
- Les deux lisent `Shift` directement. Un test e2e pose un créneau et le
  retrouve identique dans les quatre vues — c'est ce qui empêche l'une d'elles
  de dériver vers son propre calcul.

Impression
- Feuille d'impression A4 paysage : les zones à défilement se déplient, les
  lignes ne se coupent pas entre deux pages, et les aplats de poste sortent
  (`print-color-adjust: exact`) sans être nécessaires à la lecture, puisque le
  code du poste est imprimé dans chaque bloc.
- Pas de moteur PDF côté serveur : le navigateur sait paginer et enregistrer en
  PDF. Un second moteur de rendu serait un second endroit où la grille peut
  diverger de ce qui est à l'écran.

Accessibilité
- La grille porte enfin ses rôles ARIA (`grid`, `row`, `columnheader`,
  `rowheader`, `gridcell`). Elle est faite de div pour la mise en page, mais se
  lit comme un tableau : un lecteur d'écran doit pouvoir annoncer « ligne
  Camille Ferrand, colonne mercredi ».

Cette structure a d'ailleurs révélé un test faux : `locator('div')` remontait au
conteneur de la grille, et le créneau du test atterrissait sur le premier
salarié de l'équipe au lieu de celui visé — une erreur invisible à l'écran.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Cr9dkEHwbDgkWPnyGj1Rjv
2026-08-08 07:20:36 +00:00

426 lines
13 KiB
CSS

@import 'tailwindcss';
/* ==========================================================================
PlanFlow — jetons de design
Portés depuis l'export Claude Design « PlanFlow Design System ».
Les valeurs de couleur sont en oklch : les paliers de clarté restent
perceptuellement réguliers d'une teinte à l'autre, ce dont dépend la palette
catégorielle de postes (§ plus bas).
Déclarés dans @theme pour que Tailwind génère les utilitaires correspondants
(bg-surface, text-ink-1, border-line-1…). Le thème sombre redéfinit les mêmes
variables : les utilitaires compilent vers var(--color-…) et suivent donc.
========================================================================== */
@theme {
--font-ui: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, 'Cascadia Mono', 'Segoe UI Mono',
'Liberation Mono', Menlo, monospace;
--text-micro: 11px;
--text-xs: 12px;
--text-sm: 13px;
--text-md: 14px;
--text-lg: 16px;
--text-xl: 20px;
--text-2xl: 26px;
--text-3xl: 32px;
--radius-1: 3px;
--radius-2: 6px;
--radius-3: 10px;
--radius-4: 14px;
--radius-full: 999px;
--ease-organic: cubic-bezier(0.2, 0.7, 0.3, 1);
--ease-organic-out: cubic-bezier(0, 0.6, 0.35, 1);
/* Neutres chauds — la base « organique » du thème. */
--color-canvas: oklch(0.976 0.008 85);
--color-surface: oklch(0.996 0.005 85);
--color-surface-2: oklch(0.958 0.01 85);
--color-surface-3: oklch(0.932 0.012 85);
--color-line-1: oklch(0.905 0.012 85);
--color-line-2: oklch(0.852 0.014 85);
--color-line-3: oklch(0.735 0.016 85);
--color-ink-1: oklch(0.255 0.02 140);
--color-ink-2: oklch(0.445 0.016 135);
--color-ink-3: oklch(0.545 0.014 120);
--color-accent: oklch(0.44 0.078 158);
--color-accent-hover: oklch(0.385 0.08 158);
--color-accent-press: oklch(0.335 0.072 158);
--color-accent-ink: oklch(0.985 0.008 158);
--color-accent-soft: oklch(0.935 0.03 158);
--color-accent-soft-ink: oklch(0.315 0.06 158);
--color-focus: oklch(0.52 0.16 252);
--color-danger: oklch(0.475 0.145 30);
--color-danger-hover: oklch(0.415 0.145 30);
--color-danger-ink: oklch(0.985 0.006 30);
--color-danger-soft: oklch(0.94 0.035 30);
--color-danger-soft-ink: oklch(0.355 0.105 30);
--color-warn: oklch(0.745 0.125 78);
--color-warn-ink: oklch(0.255 0.055 60);
--color-warn-soft: oklch(0.945 0.045 82);
--color-warn-soft-ink: oklch(0.375 0.085 60);
--color-ok: oklch(0.475 0.095 148);
--color-ok-soft: oklch(0.935 0.035 148);
--color-ok-soft-ink: oklch(0.315 0.07 148);
--color-info: oklch(0.475 0.105 245);
--color-info-soft: oklch(0.935 0.035 245);
--color-info-soft-ink: oklch(0.325 0.08 245);
--shadow-e1: 0 1px 2px oklch(0.3 0.02 85 / 0.08);
--shadow-e2:
0 1px 2px oklch(0.3 0.02 85 / 0.06), 0 6px 14px oklch(0.3 0.02 85 / 0.07);
--shadow-e3:
0 2px 6px oklch(0.3 0.02 85 / 0.08), 0 16px 40px oklch(0.3 0.02 85 / 0.14);
}
:root {
--focus-halo: oklch(0.52 0.16 252 / 0.22);
--lh-tight: 1.2;
--lh-ui: 1.35;
--lh-table: 1.4;
--lh-prose: 1.55;
--d-1: 80ms;
--d-2: 130ms;
--d-3: 200ms;
--d-4: 280ms;
/* Hauteur d'une ligne de grille. Change avec la densité. */
--row-h: 38px;
/* ---- Palette catégorielle de postes -----------------------------------
Douze teintes dérivées d'une même formule : la clarté et le chroma sont
communs, seule la teinte varie, plus un palier alterné (--post-*-t) qui
écarte les teintes voisines. Deux postes ne peuvent donc pas se retrouver
à la même clarté avec des teintes proches — c'est ce qui les garde
distinguables en vision daltonienne, où la teinte seule ne suffit pas.
La couleur ne porte jamais l'information seule : le code du poste est
toujours écrit dans le bloc (voir ShiftChip).
--------------------------------------------------------------------- */
--tier-dir: -1;
--chip-bg-l: 0.905;
--chip-bg-c: 0.042;
--chip-fg-l: 0.315;
--chip-fg-c: 0.055;
--chip-edge-l: 0.52;
--chip-edge-c: 0.12;
--post-cai-h: 250;
--post-cai-t: 0;
--post-vte-h: 162;
--post-vte-t: 0.045;
--post-res-h: 30;
--post-res-t: 0;
--post-acc-h: 205;
--post-acc-t: 0.045;
--post-cab-h: 100;
--post-cab-t: 0;
--post-rss-h: 308;
--post-rss-t: 0.045;
--post-inv-h: 138;
--post-inv-t: 0;
--post-liv-h: 52;
--post-liv-t: 0.045;
--post-enc-h: 276;
--post-enc-t: 0;
--post-clc-h: 340;
--post-clc-t: 0.045;
--post-net-h: 185;
--post-net-t: 0;
--post-for-h: 68;
--post-for-t: 0.045;
/* Dérivation : une seule formule pour les douze postes. Ajouter un poste
revient à déclarer sa teinte et son palier, puis ces trois lignes. */
--post-cai-bg: oklch(
calc(var(--chip-bg-l) + var(--tier-dir) * var(--post-cai-t)) var(--chip-bg-c)
var(--post-cai-h)
);
--post-cai-fg: oklch(var(--chip-fg-l) var(--chip-fg-c) var(--post-cai-h));
--post-cai-edge: oklch(
calc(var(--chip-edge-l) + var(--tier-dir) * var(--post-cai-t)) var(--chip-edge-c)
var(--post-cai-h)
);
--post-vte-bg: oklch(
calc(var(--chip-bg-l) + var(--tier-dir) * var(--post-vte-t)) var(--chip-bg-c)
var(--post-vte-h)
);
--post-vte-fg: oklch(var(--chip-fg-l) var(--chip-fg-c) var(--post-vte-h));
--post-vte-edge: oklch(
calc(var(--chip-edge-l) + var(--tier-dir) * var(--post-vte-t)) var(--chip-edge-c)
var(--post-vte-h)
);
--post-res-bg: oklch(
calc(var(--chip-bg-l) + var(--tier-dir) * var(--post-res-t)) var(--chip-bg-c)
var(--post-res-h)
);
--post-res-fg: oklch(var(--chip-fg-l) var(--chip-fg-c) var(--post-res-h));
--post-res-edge: oklch(
calc(var(--chip-edge-l) + var(--tier-dir) * var(--post-res-t)) var(--chip-edge-c)
var(--post-res-h)
);
--post-acc-bg: oklch(
calc(var(--chip-bg-l) + var(--tier-dir) * var(--post-acc-t)) var(--chip-bg-c)
var(--post-acc-h)
);
--post-acc-fg: oklch(var(--chip-fg-l) var(--chip-fg-c) var(--post-acc-h));
--post-acc-edge: oklch(
calc(var(--chip-edge-l) + var(--tier-dir) * var(--post-acc-t)) var(--chip-edge-c)
var(--post-acc-h)
);
--post-cab-bg: oklch(
calc(var(--chip-bg-l) + var(--tier-dir) * var(--post-cab-t)) var(--chip-bg-c)
var(--post-cab-h)
);
--post-cab-fg: oklch(var(--chip-fg-l) var(--chip-fg-c) var(--post-cab-h));
--post-cab-edge: oklch(
calc(var(--chip-edge-l) + var(--tier-dir) * var(--post-cab-t)) var(--chip-edge-c)
var(--post-cab-h)
);
--post-rss-bg: oklch(
calc(var(--chip-bg-l) + var(--tier-dir) * var(--post-rss-t)) var(--chip-bg-c)
var(--post-rss-h)
);
--post-rss-fg: oklch(var(--chip-fg-l) var(--chip-fg-c) var(--post-rss-h));
--post-rss-edge: oklch(
calc(var(--chip-edge-l) + var(--tier-dir) * var(--post-rss-t)) var(--chip-edge-c)
var(--post-rss-h)
);
--post-inv-bg: oklch(
calc(var(--chip-bg-l) + var(--tier-dir) * var(--post-inv-t)) var(--chip-bg-c)
var(--post-inv-h)
);
--post-inv-fg: oklch(var(--chip-fg-l) var(--chip-fg-c) var(--post-inv-h));
--post-inv-edge: oklch(
calc(var(--chip-edge-l) + var(--tier-dir) * var(--post-inv-t)) var(--chip-edge-c)
var(--post-inv-h)
);
--post-liv-bg: oklch(
calc(var(--chip-bg-l) + var(--tier-dir) * var(--post-liv-t)) var(--chip-bg-c)
var(--post-liv-h)
);
--post-liv-fg: oklch(var(--chip-fg-l) var(--chip-fg-c) var(--post-liv-h));
--post-liv-edge: oklch(
calc(var(--chip-edge-l) + var(--tier-dir) * var(--post-liv-t)) var(--chip-edge-c)
var(--post-liv-h)
);
--post-enc-bg: oklch(
calc(var(--chip-bg-l) + var(--tier-dir) * var(--post-enc-t)) var(--chip-bg-c)
var(--post-enc-h)
);
--post-enc-fg: oklch(var(--chip-fg-l) var(--chip-fg-c) var(--post-enc-h));
--post-enc-edge: oklch(
calc(var(--chip-edge-l) + var(--tier-dir) * var(--post-enc-t)) var(--chip-edge-c)
var(--post-enc-h)
);
--post-clc-bg: oklch(
calc(var(--chip-bg-l) + var(--tier-dir) * var(--post-clc-t)) var(--chip-bg-c)
var(--post-clc-h)
);
--post-clc-fg: oklch(var(--chip-fg-l) var(--chip-fg-c) var(--post-clc-h));
--post-clc-edge: oklch(
calc(var(--chip-edge-l) + var(--tier-dir) * var(--post-clc-t)) var(--chip-edge-c)
var(--post-clc-h)
);
--post-net-bg: oklch(
calc(var(--chip-bg-l) + var(--tier-dir) * var(--post-net-t)) var(--chip-bg-c)
var(--post-net-h)
);
--post-net-fg: oklch(var(--chip-fg-l) var(--chip-fg-c) var(--post-net-h));
--post-net-edge: oklch(
calc(var(--chip-edge-l) + var(--tier-dir) * var(--post-net-t)) var(--chip-edge-c)
var(--post-net-h)
);
--post-for-bg: oklch(
calc(var(--chip-bg-l) + var(--tier-dir) * var(--post-for-t)) var(--chip-bg-c)
var(--post-for-h)
);
--post-for-fg: oklch(var(--chip-fg-l) var(--chip-fg-c) var(--post-for-h));
--post-for-edge: oklch(
calc(var(--chip-edge-l) + var(--tier-dir) * var(--post-for-t)) var(--chip-edge-c)
var(--post-for-h)
);
}
/* Thème sombre : redéfinit les mêmes variables. --tier-dir s'inverse pour que
le palier de clarté pousse vers le clair au lieu du sombre. */
:root[data-theme='dark'] {
--tier-dir: 1;
--chip-bg-l: 0.315;
--chip-bg-c: 0.048;
--chip-fg-l: 0.905;
--chip-fg-c: 0.032;
--chip-edge-l: 0.66;
--chip-edge-c: 0.105;
--color-canvas: oklch(0.185 0.01 150);
--color-surface: oklch(0.225 0.012 150);
--color-surface-2: oklch(0.265 0.012 150);
--color-surface-3: oklch(0.305 0.013 150);
--color-line-1: oklch(0.325 0.012 150);
--color-line-2: oklch(0.395 0.014 150);
--color-line-3: oklch(0.52 0.016 150);
--color-ink-1: oklch(0.955 0.008 95);
--color-ink-2: oklch(0.815 0.012 100);
--color-ink-3: oklch(0.67 0.013 105);
--color-accent: oklch(0.735 0.105 158);
--color-accent-hover: oklch(0.795 0.095 158);
--color-accent-press: oklch(0.68 0.1 158);
--color-accent-ink: oklch(0.195 0.04 158);
--color-accent-soft: oklch(0.3 0.045 158);
--color-accent-soft-ink: oklch(0.87 0.07 158);
--color-focus: oklch(0.76 0.14 252);
--focus-halo: oklch(0.76 0.14 252 / 0.28);
--color-danger: oklch(0.69 0.15 32);
--color-danger-hover: oklch(0.745 0.14 32);
--color-danger-ink: oklch(0.185 0.045 32);
--color-danger-soft: oklch(0.305 0.06 32);
--color-danger-soft-ink: oklch(0.885 0.075 32);
--color-warn: oklch(0.79 0.13 80);
--color-warn-ink: oklch(0.21 0.05 60);
--color-warn-soft: oklch(0.32 0.055 75);
--color-warn-soft-ink: oklch(0.895 0.08 82);
--color-ok: oklch(0.71 0.1 148);
--color-ok-soft: oklch(0.295 0.05 148);
--color-ok-soft-ink: oklch(0.885 0.075 148);
--color-info: oklch(0.705 0.11 245);
--color-info-soft: oklch(0.3 0.05 245);
--color-info-soft-ink: oklch(0.88 0.07 245);
--shadow-e1: 0 1px 2px oklch(0 0 0 / 0.4);
--shadow-e2: 0 1px 2px oklch(0 0 0 / 0.35), 0 6px 14px oklch(0 0 0 / 0.4);
--shadow-e3: 0 2px 6px oklch(0 0 0 / 0.35), 0 16px 40px oklch(0 0 0 / 0.55);
}
/* Densité confortable : une seule variable pilote la hauteur de ligne. */
:root[data-density='confortable'] {
--row-h: 46px;
}
/* ==========================================================================
Base
========================================================================== */
html {
color-scheme: light;
}
html[data-theme='dark'] {
color-scheme: dark;
}
body {
margin: 0;
background: var(--color-canvas);
color: var(--color-ink-1);
font-family: var(--font-ui);
font-size: var(--text-md);
line-height: var(--lh-ui);
-webkit-font-smoothing: antialiased;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Anneau de focus unique, contraste ≥ 3:1 sur tous les fonds y compris les
aplats de poste. La grille se pilote au clavier : il doit rester visible. */
:focus-visible {
outline: 2px solid var(--color-focus);
outline-offset: 2px;
}
/* Compteurs, horaires, écarts : sans chiffres tabulaires les colonnes dansent
d'une ligne à l'autre et la comparaison devient pénible. */
.tnum {
font-variant-numeric: tabular-nums;
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 1ms !important;
transition-duration: 1ms !important;
}
}
/* ---------------------------------------------------------------------------
Impression
---------------------------------------------------------------------------
Un planning affiché reste affiché ; un planning imprimé est le document que
l'équipe lit dans la réserve. Deux exigences en découlent :
- la couleur ne doit pas être nécessaire (les codes de poste sont déjà
imprimés dans chaque bloc), mais les aplats doivent sortir quand même,
d'où `print-color-adjust: exact` ;
- rien ne doit être coupé : la grille tient en paysage et les lignes ne se
scindent pas entre deux pages.
*/
@media print {
@page {
size: A4 landscape;
margin: 10mm;
}
:root {
/* L'impression part toujours du thème clair : un aplat sombre consomme
l'encre sans rien apporter. */
color-scheme: light;
}
html,
body {
background: #fff;
color: #000;
}
* {
print-color-adjust: exact;
-webkit-print-color-adjust: exact;
}
/* Les zones à défilement horizontal doivent se déplier, sinon la fin de la
semaine n'est tout simplement pas sur la feuille. */
.overflow-x-auto {
overflow: visible !important;
}
.min-w-\[1040px\] {
min-width: 0 !important;
}
[data-print='hide'],
nav,
aside {
display: none !important;
}
section,
tr,
.print-keep {
break-inside: avoid;
}
}