From 3a18ac9eabc7ca9844f41dbdfb2223a7a2bd20be Mon Sep 17 00:00:00 2001 From: IliesAllali Date: Tue, 27 Oct 2026 12:57:34 +0100 Subject: [PATCH] =?UTF-8?q?Fiche=20:=20appara=C3=AEt=20d'un=20coup=20sur?= =?UTF-8?q?=20ordinateur,=20feuille=20en=20240=20ms,=20r=C3=A9sistance=20v?= =?UTF-8?q?ers=20le=20haut,=20coins=20continus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Ordinateur : plus d'entrée animée (fondu et flou de 260 ms) ; on ouvre une fiche des dizaines de fois par visite. - Téléphone : la feuille monte en 240 ms au lieu de 400 ; en mouvements réduits, fondu court seulement sur téléphone. - Geste des feuilles (fiche et menu de la barre) : tirée vers le haut, la feuille cède de moins en moins (40 px au plus) et revient au lâcher, au lieu de buter net. Logique pure sheetOffset, testée. Une ombre pleine comble le dessous. - Coins continus (corner-shape: squircle) sur la fiche, le portrait et les vignettes, seulement là où le navigateur les dessine ; ailleurs rien ne change. Planche validée par Patch le 27/10 : _bmad-output/parenthese/motion/2026-10-27-fiche/ Co-Authored-By: Claude Opus 5.5 --- frontend/src/components/PersonCard.css | 24 ++++++++++++------ frontend/src/components/navbar/NavMenu.css | 3 ++- frontend/src/hooks/useSheetDrag.js | 7 +++--- frontend/src/utils/sheetDrag.js | 16 +++++++++--- frontend/src/utils/sheetDrag.test.js | 29 ++++++++++++++++++++-- 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/frontend/src/components/PersonCard.css b/frontend/src/components/PersonCard.css index 035e51a..a8f5290 100644 --- a/frontend/src/components/PersonCard.css +++ b/frontend/src/components/PersonCard.css @@ -21,15 +21,10 @@ box-shadow: var(--pz-shadow-lg); font-family: var(--pz-sans); color: var(--pz-ink); - animation: pc-in 260ms var(--pz-ease) both; + /* Pas d'entrée animée sur ordinateur : on ouvre une fiche des dizaines de fois par visite, elle apparaît d'un coup (Patch, 27/10) */ } .person-card--edit-mode { border-color: var(--pz-a300); } -@keyframes pc-in { - from { opacity: 0; transform: translateX(-12px) scale(.985); filter: blur(3px); } - to { opacity: 1; transform: none; filter: none; } -} - .pc-grab { display: none; } .pc-close { position: absolute; top: 12px; right: 12px; } .pc-hidden-input { display: none; } @@ -250,8 +245,10 @@ left: 0; right: 0; bottom: 0; width: auto; max-height: 78dvh; padding: 10px 18px calc(18px + env(safe-area-inset-bottom)); border-radius: 26px 26px 0 0; border-bottom: 0; + /* Ombre pleine sous la feuille, en premier pour passer au-dessus de l'ombre floue : tirée vers le haut (40 px au plus, sheetOffset), elle ne laisse pas voir l'arbre dessous */ + box-shadow: 0 40px 0 var(--pz-surface), var(--pz-shadow-lg); /* Pas de fill-mode « both » : une fois l'entrée finie, le glisser (style inline) reprend la main sur transform */ - animation: pc-sheet 400ms var(--pz-ease-drawer) backwards; + animation: pc-sheet 240ms var(--pz-ease-drawer) backwards; transition: transform 240ms var(--pz-ease-drawer); } /* Pendant le glisser, la feuille suit le doigt sans transition */ @@ -265,11 +262,22 @@ .pc-name { font-size: 24px; } @keyframes pc-sheet { from { transform: translateY(100%); } to { transform: none; } } } +/* Coins continus là où le navigateur sait les dessiner (Chromium) ; ailleurs, les arcs de cercle d'avant. Le rayon grandit + parce qu'un squircle de même rayon paraît plus anguleux. */ +@supports (corner-shape: squircle) { + .person-card { corner-shape: squircle; border-radius: 34px; } + .pc-photo { corner-shape: squircle; border-radius: 38%; } + .pc-tile, .pc-list-item { corner-shape: squircle; border-radius: 18px; } + @media (max-width: 768px) { .person-card { border-radius: 40px 40px 0 0; } } +} @media (prefers-reduced-motion: reduce) { - .person-card { animation: pz-fade 200ms ease both; } .person-card { transition-duration: 1ms; } } +/* Mouvements réduits : la feuille du téléphone ne monte plus, elle s'affiche en fondu court */ +@media (prefers-reduced-motion: reduce) and (max-width: 768px) { + .person-card { animation: pz-fade 200ms ease both; } +} /* Prénom inconnu (issue Anaïs, 18/10) : libellé en italique, case à cocher sous les champs de nom */ .pc-name-unknown { font-style: italic; font-weight: 300; } diff --git a/frontend/src/components/navbar/NavMenu.css b/frontend/src/components/navbar/NavMenu.css index e29f469..0b80e9e 100644 --- a/frontend/src/components/navbar/NavMenu.css +++ b/frontend/src/components/navbar/NavMenu.css @@ -41,7 +41,8 @@ padding: 0 14px calc(16px + env(safe-area-inset-bottom, 0px)); background: var(--pz-surface); border-radius: 24px 24px 0 0; - box-shadow: 0 -12px 40px rgba(42, 38, 34, 0.16); + /* Ombre pleine sous la feuille, en premier pour passer au-dessus de l'ombre floue : tirée vers le haut (40 px au plus, sheetOffset), elle ne laisse pas voir la page dessous */ + box-shadow: 0 40px 0 var(--pz-surface), 0 -12px 40px rgba(42, 38, 34, 0.16); /* Pas de fill-mode : une fois l'entrée finie, le glisser (style inline) reprend la main */ animation: navmSheetIn 320ms var(--pz-ease-drawer); transition: transform 240ms var(--pz-ease-drawer); diff --git a/frontend/src/hooks/useSheetDrag.js b/frontend/src/hooks/useSheetDrag.js index acb5683..a90d60e 100644 --- a/frontend/src/hooks/useSheetDrag.js +++ b/frontend/src/hooks/useSheetDrag.js @@ -5,6 +5,7 @@ import { canStartSheetDrag, shouldCloseSheet, sheetDragIntent, + sheetOffset, velocityBetween, } from '../utils/sheetDrag' @@ -35,7 +36,7 @@ export function useSheetMode() { /** * Glisser pour fermer une feuille du bas. * - Le geste ne part que des éléments de `zone` (sélecteur CSS) et jamais d'un champ de saisie ni d'une sélection de texte. - * - La feuille suit le doigt (transform inline) et ne monte jamais au-delà de sa position de repos. + * - La feuille suit le doigt (transform inline) ; au-dessus de sa position de repos elle résiste (`sheetOffset`) et revient au lâcher. * - Au lâcher : `closes` décide (par défaut, un tiers de la hauteur ou un lâcher rapide). Fermer attend la fin de la sortie, * puis appelle `onClose`. Si `onClose` renvoie `false` (enregistrement refusé), la feuille revient. * - Après un vrai glisser, le clic qui suit est avalé (pas de bouton déclenché au lâcher). @@ -107,7 +108,7 @@ export function useSheetDrag({ } g.prev = g.last g.last = { y: event.clientY, t: event.timeStamp } - setOffset(Math.max(0, dy)) + setOffset(sheetOffset(dy)) } const onPointerUp = (event) => { @@ -142,6 +143,6 @@ export function useSheetDrag({ return { dragging, bind: { onPointerDown, onPointerMove, onPointerUp, onPointerCancel, onClickCapture }, - style: offset > 0 ? { transform: `translateY(${offset}px)` } : undefined, + style: offset !== 0 ? { transform: `translateY(${offset}px)` } : undefined, } } diff --git a/frontend/src/utils/sheetDrag.js b/frontend/src/utils/sheetDrag.js index b54063d..7bb0355 100644 --- a/frontend/src/utils/sheetDrag.js +++ b/frontend/src/utils/sheetDrag.js @@ -29,11 +29,21 @@ export function canStartSheetDrag(target, zoneSelector, selectionText = '') { return !target.closest(NO_DRAG_SELECTOR) } -/** Intention du mouvement depuis l'appui : 'pending' (encore trop petit), 'down' (glisser vers le bas), 'other' (haut ou horizontal) */ +/** Intention du mouvement depuis l'appui : 'pending' (encore trop petit), 'down' ou 'up' (glisser vertical), 'other' (horizontal) */ export function sheetDragIntent(dx, dy) { if (Math.abs(dx) < DIRECTION_LOCK_PX && Math.abs(dy) < DIRECTION_LOCK_PX) return 'pending' - if (dy > 0 && dy >= Math.abs(dx)) return 'down' - return 'other' + if (Math.abs(dy) < Math.abs(dx)) return 'other' + return dy > 0 ? 'down' : 'up' +} + +// Au-dessus de sa position de repos, la feuille cède de moins en moins, sans jamais dépasser ce plafond (px) +export const RUBBER_MAX_PX = 40 +const RUBBER_SOFTNESS_PX = 120 + +/** Décalage de la feuille pour un déplacement du doigt : suit vers le bas, résiste vers le haut (négatif, borné) */ +export function sheetOffset(dy) { + if (dy >= 0) return dy + return -RUBBER_MAX_PX * (1 - 1 / (1 + -dy / RUBBER_SOFTNESS_PX)) } /** Vitesse (px/ms, positive vers le bas) entre deux points {y, t}. Zéro si les points sont trop éloignés dans le temps. */ diff --git a/frontend/src/utils/sheetDrag.test.js b/frontend/src/utils/sheetDrag.test.js index 9ee6dea..c62d1c4 100644 --- a/frontend/src/utils/sheetDrag.test.js +++ b/frontend/src/utils/sheetDrag.test.js @@ -1,9 +1,11 @@ import { describe, expect, it } from 'vitest' import { NO_DRAG_SELECTOR, + RUBBER_MAX_PX, canStartSheetDrag, shouldCloseSheet, sheetDragIntent, + sheetOffset, velocityBetween, } from './sheetDrag' @@ -55,9 +57,32 @@ describe('sheetDragIntent (direction)', () => { expect(sheetDragIntent(10, 20)).toBe('down') }) - it('rejette le haut et l\'horizontal', () => { - expect(sheetDragIntent(0, -20)).toBe('other') + it('reconnaît un glisser vers le haut', () => { + expect(sheetDragIntent(0, -20)).toBe('up') + expect(sheetDragIntent(-10, -20)).toBe('up') + }) + + it('rejette l\'horizontal', () => { expect(sheetDragIntent(30, 20)).toBe('other') + expect(sheetDragIntent(-30, -20)).toBe('other') + }) +}) + +describe('sheetOffset (résistance au-dessus du repos)', () => { + it('suit le doigt vers le bas', () => { + expect(sheetOffset(0)).toBe(0) + expect(sheetOffset(120)).toBe(120) + }) + + it('cède de moins en moins vers le haut, sans dépasser le plafond', () => { + const a = sheetOffset(-60) + const b = sheetOffset(-120) + const c = sheetOffset(-2000) + expect(a).toBeLessThan(0) + expect(b).toBeLessThan(a) + expect(-b).toBeLessThan(120) + expect(b - c).toBeLessThan(0 - b) + expect(c).toBeGreaterThan(-RUBBER_MAX_PX) }) })