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) }) })