mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-06 23:32:08 +00:00
Merge pull request #147 from IliesAllali/fix/fiche-modifier-ipad
Fiche : « Modifier » d'un clic, « ? » entier sur les cadres ronds, arbre jusqu'en bas sur iPad
This commit is contained in:
9 files changed
+76
-15
No files matched your search
@@ -1,6 +1,8 @@
|
||||
.app {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
/* La hauteur visible réelle (barres de Safari comprises) là où le navigateur la connaît */
|
||||
height: 100dvh;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: #FBFAF7;
|
||||
|
||||
@@ -3259,6 +3259,7 @@ function App() {
|
||||
onMoveSibling={(delta) => galaxyZoomApiRef.current?.moveSibling?.(tree.selectedPerson.id, delta)}
|
||||
onUploadMomentFile={tree.canEditCurrentTree || tree.canSubmitContribution ? media.uploadMomentFile : undefined}
|
||||
onMomentSaved={media.refreshMomentMedia}
|
||||
onQuickEdit={tree.canEditCurrentTree && userRole.isAdmin && !isDemoMode ? handleEditToggle : undefined}
|
||||
/>
|
||||
)}
|
||||
{/* Crayon de modification masqué en démo : rien n'y est enregistré */}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
.galaxy-canvas {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: default;
|
||||
display: block;
|
||||
touch-action: none;
|
||||
|
||||
@@ -447,24 +447,31 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
}
|
||||
}, [isZooming, targetTransformRef, zoomTimeoutRef])
|
||||
|
||||
// Resize canvas to device pixel ratio
|
||||
// Resize canvas to device pixel ratio. Le CSS donne au canvas toute la page (.galaxy-canvas) ; on lit sa taille
|
||||
// réelle. Avec window.innerHeight, Safari sur iPad gardait une hauteur prise clavier ou barre ouverte, sans
|
||||
// événement resize ensuite : l'arbre s'arrêtait net au-dessus du bas de l'écran.
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return undefined
|
||||
const resize = () => {
|
||||
const canvas = canvasRef.current
|
||||
if (!canvas) return
|
||||
const cssW = window.innerWidth
|
||||
const cssH = window.innerHeight
|
||||
const cssW = canvas.clientWidth || window.innerWidth
|
||||
const cssH = canvas.clientHeight || window.innerHeight
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 2)
|
||||
const w = Math.floor(cssW * dpr), h = Math.floor(cssH * dpr)
|
||||
if (canvas.width === w && canvas.height === h && dprRef.current === dpr) return
|
||||
dprRef.current = dpr
|
||||
canvas.width = Math.floor(cssW * dpr)
|
||||
canvas.height = Math.floor(cssH * dpr)
|
||||
canvas.style.width = `${cssW}px`
|
||||
canvas.style.height = `${cssH}px`
|
||||
canvas.width = w
|
||||
canvas.height = h
|
||||
needsRedrawRef.current = true
|
||||
}
|
||||
resize()
|
||||
const observer = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(resize)
|
||||
observer?.observe(canvas)
|
||||
window.addEventListener('resize', resize)
|
||||
return () => window.removeEventListener('resize', resize)
|
||||
return () => {
|
||||
observer?.disconnect()
|
||||
window.removeEventListener('resize', resize)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Layout
|
||||
|
||||
@@ -296,3 +296,11 @@
|
||||
|
||||
/* Titres de section de l'onglet Infos et Liens : casse normale, Newsreader 15 px, comme la maquette (.sec) */
|
||||
.pc-infos .pz-eyebrow { text-transform: none; letter-spacing: 0; font: 400 15px/1.3 var(--pz-serif); color: var(--pz-ink); }
|
||||
|
||||
/* « Modifier » de l'administrateur : une pastille de la ligne des repères, qui fait passer la fiche en modification */
|
||||
.pc-quick-edit {
|
||||
display: inline-flex; align-items: center; gap: 6px; min-height: 30px; padding: 0 12px; border: 0; border-radius: 999px; cursor: pointer;
|
||||
background: var(--pz-surface); color: var(--pz-ink); box-shadow: inset 0 0 0 1px var(--pz-line); font: 500 12.5px var(--pz-sans);
|
||||
}
|
||||
.pc-quick-edit:focus-visible { outline: 2px solid var(--pz-a500); outline-offset: 2px; }
|
||||
@media (hover: hover) and (pointer: fine) { .pc-quick-edit:hover { background: var(--pz-line-soft); } }
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef, forwardRef } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Save, Trash2, Plus, X, Search, Check, Play, ImagePlus, Camera, Crop, Sparkles, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { Save, Trash2, Plus, X, Search, Check, Play, ImagePlus, Camera, Crop, Sparkles, ChevronLeft, ChevronRight, Pencil } from 'lucide-react'
|
||||
import { getParents, getChildren, getPersonUnions, getPersonMedias, getPersonTaggedMedias, getDemoLife, getPersonById, filiations, persons, unions } from '../data/mockData'
|
||||
import { fetchPersonLife } from '../api/treeApi.js'
|
||||
import LifeTimeline from './LifeTimeline.jsx'
|
||||
@@ -74,6 +74,8 @@ const PersonCard = ({
|
||||
onUploadMomentFile,
|
||||
onMomentSaved,
|
||||
saveOnLeaveRef = null,
|
||||
// Administrateur, hors démo : la fiche passe en modification d'un clic, sans chercher le crayon de l'arbre
|
||||
onQuickEdit,
|
||||
}) => {
|
||||
// Souvenirs d'abord quand il y en a, ou quand on peut en ajouter : c'est là qu'est le bouton
|
||||
const [tab, setTab] = useState(() => (getPersonMedias(person.id).length > 0 || getPersonTaggedMedias(person.id).length > 0 || canManageMedia ? 'medias' : 'infos'))
|
||||
@@ -754,6 +756,12 @@ const PersonCard = ({
|
||||
)}
|
||||
{!editMode && person.toVerify && <span className="pz-tag pz-tag--accent pz-tag--dot">{t('À vérifier')}</span>}
|
||||
{!editMode && person.profession && <span className="pz-tag">{person.profession}</span>}
|
||||
{!editMode && onQuickEdit && (
|
||||
<button type="button" className="pc-quick-edit" onClick={() => { setTab('infos'); onQuickEdit() }}>
|
||||
<Pencil size={13} strokeWidth={2} aria-hidden="true" />
|
||||
{t('Modifier')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{editMode && ((unknownFirstName || !String(getEditValue('firstName', person.firstName) ?? '').trim())) && (
|
||||
|
||||
@@ -602,9 +602,7 @@ function drawPersonNode(ctx, node, pos, nodeProgress, {
|
||||
ctx.shadowOffsetY = shadowY
|
||||
}
|
||||
|
||||
drawFrame(ctx, frameType, person, img)
|
||||
// « À vérifier » : petit rond terracotta avec un « ? » en haut à droite du cadre, absent au dézoom
|
||||
if (person.toVerify && withDetail) drawToVerifyMark(ctx)
|
||||
drawPortrait(ctx, frameType, person, img, person.toVerify && withDetail)
|
||||
ctx.restore()
|
||||
|
||||
// --- Labels (estompés puis absents en dézoom) ---
|
||||
@@ -931,6 +929,16 @@ function resolveFrameType(person, img) {
|
||||
return resolveFrameKind(person.frameType || 'round', ratio, cropShape(person.avatarCrop))
|
||||
}
|
||||
|
||||
// Cadre et portrait, puis la marque « À vérifier » (petit rond terracotta avec un « ? » en haut à droite du cadre,
|
||||
// absente au dézoom). Le cadre découpe le canvas à sa forme : save/restore autour, sinon la marque est rognée
|
||||
// (sur un cadre rond, elle tombe presque entière hors du cercle)
|
||||
export function drawPortrait(ctx, frameType, person, img, withMark) {
|
||||
ctx.save()
|
||||
drawFrame(ctx, frameType, person, img)
|
||||
ctx.restore()
|
||||
if (withMark) drawToVerifyMark(ctx)
|
||||
}
|
||||
|
||||
function drawToVerifyMark(ctx) {
|
||||
const r = 9
|
||||
const x = PERSON_R * 0.74, y = -PERSON_R * 0.74
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { traceSibship } from './renderers'
|
||||
import { drawPortrait, traceSibship } from './renderers'
|
||||
|
||||
// Contexte canvas minimal qui enregistre les appels de tracé
|
||||
const recorder = () => {
|
||||
@@ -49,3 +49,25 @@ describe('traceSibship (accolade franche)', () => {
|
||||
expect(curve.slice(5)).toEqual([515, 480])
|
||||
})
|
||||
})
|
||||
|
||||
describe('drawPortrait, marque « À vérifier »', () => {
|
||||
const person = { firstName: 'Marie', lastName: 'Martin', isAlive: true }
|
||||
for (const frame of ['round', 'square', 'polaroid', 'rect']) {
|
||||
it(`cadre ${frame} : la marque est dessinée hors de la découpe du cadre`, () => {
|
||||
const { ctx, calls } = recorder()
|
||||
drawPortrait(ctx, frame, person, null, true)
|
||||
const clip = calls.findIndex(c => c[0] === 'clip')
|
||||
const restore = calls.findIndex((c, i) => i > clip && c[0] === 'restore')
|
||||
const markText = calls.findIndex(c => c[0] === 'fillText' && c[1] === '?')
|
||||
expect(clip).toBeGreaterThan(-1)
|
||||
expect(restore).toBeGreaterThan(clip)
|
||||
expect(markText).toBeGreaterThan(restore)
|
||||
})
|
||||
}
|
||||
|
||||
it('sans « À vérifier », pas de marque', () => {
|
||||
const { ctx, calls } = recorder()
|
||||
drawPortrait(ctx, 'round', person, null, false)
|
||||
expect(calls.some(c => c[0] === 'fillText' && c[1] === '?')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,7 @@
|
||||
// Traductions anglaises, clé = texte français exact passé à t().
|
||||
export default {
|
||||
// Fiche d'une personne (PersonCard)
|
||||
'Modifier': 'Edit',
|
||||
'Photo': 'Photo',
|
||||
'Vidéo': 'Video',
|
||||
'Audio': 'Audio',
|
||||
|
||||
Reference in new issue
Block a user