É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
426 lines
13 KiB
CSS
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;
|
|
}
|
|
}
|