Front : mineures et correctifs groupés (Dependabot #31), refs hors du rendu

React et react-dom 19.3.0, @types/react et @types/react-dom 19.3.0, posthog-js 1.434.15, autoprefixer 10.6.1, eslint-plugin-react-hooks 7.1.1, eslint-plugin-react-refresh 0.5.7. La règle react-hooks/refs de la 7.1 voit maintenant les refs lus ou écrits pendant le rendu, corrigés plutôt que désactivés : le nom de l'arbre partagé affiché par l'écran compte passe par un état (un ref ne redessine rien), la personne de départ de l'arbre est recopiée dans un effet déclaré avant celui du cadrage, l'API de zoom est exposée par useImperativeHandle, l'horodatage des portraits est posé au premier chargement au lieu de Date.now() au rendu (react-hooks/purity). react-refresh 0.5 garde l'export par défaut et la config vite utilisés ici.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
IliesAllaliandClaude Opus 5.5 committed 2026-10-01 11:12:32 +02:00
1 parent ea49f77651
commit 5fcaa85d8f
5 files changed
+125 -113

No files matched your search

+65 -57
View File
@@ -9,19 +9,19 @@
"version": "0.0.0",
"dependencies": {
"lucide-react": "^0.564.0",
"posthog-js": "^1.353.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
"posthog-js": "^1.435.6",
"react": "^19.3.0",
"react-dom": "^19.3.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.24",
"autoprefixer": "^10.6.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.7",
"globals": "^16.5.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
@@ -1070,28 +1070,28 @@
}
},
"node_modules/@posthog/browser-common": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/@posthog/browser-common/-/browser-common-0.9.0.tgz",
"integrity": "sha512-EWoVzs1vVY7ihpn8yOjj3jyjriHUEEMVSBNL/TW3hk09fTriWRe76vJXrRg0G0yjTYxHmtlSkNn4mOV24ZEzzA==",
"version": "0.9.1",
"resolved": "https://registry.npmjs.org/@posthog/browser-common/-/browser-common-0.9.1.tgz",
"integrity": "sha512-Dl8uV2OZAxfHfWyC7K0Y3HtT7ld+WGLbXIwgCgAGziOMGDIa/bepCRrCxy7IxsEzqY1HOrq/VDdcoNN7hBTSQQ==",
"license": "MIT",
"dependencies": {
"@posthog/core": "^1.54.2",
"@posthog/types": "^1.412.1"
"@posthog/core": "^1.55.3",
"@posthog/types": "^1.413.0"
}
},
"node_modules/@posthog/core": {
"version": "1.55.2",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.55.2.tgz",
"integrity": "sha512-2mYxGbDmTLUs096VbcZqSWo0lrI48/BrX65r54NynxxFq409BOx9w73HxOAtOKZ1vblkdV6/GxQTe7KDcIQidg==",
"version": "1.55.3",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.55.3.tgz",
"integrity": "sha512-XnmG9jo7WlxTeN4oCNWH4ESLQHR6ZPZ+mcq+8JxU62k5i1BAe3BSrybnmXHjqKp7e3rUvLK2bOzGZGSlfHvlNA==",
"license": "MIT",
"dependencies": {
"@posthog/types": "^1.412.4"
"@posthog/types": "^1.413.0"
}
},
"node_modules/@posthog/types": {
"version": "1.412.4",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.412.4.tgz",
"integrity": "sha512-Q7lV9O9TbLngjOYw1ucm3bS3tn48xb1B4ZQdDy8gv7yfKe99hCTeWYG4xZ36nJM80VxpTF9TriiJt5hngDOkBg==",
"version": "1.413.0",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.413.0.tgz",
"integrity": "sha512-tYAVzNojWckuG/wxBB0fwE70sFruQQq4xbhkci7eH2ARU3mMfUTkvpN1HwBMpm0AtbxidLiH0IFvagmdJ8opPw==",
"license": "MIT"
},
"node_modules/@rolldown/pluginutils": {
@@ -1530,21 +1530,23 @@
"dev": true
},
"node_modules/@types/react": {
"version": "19.2.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.13.tgz",
"integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
"version": "19.3.0",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.3.0.tgz",
"integrity": "sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"version": "19.3.0",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.3.0.tgz",
"integrity": "sha512-ZI7bU42mZXXKHn/qNLEw2IrbiINU7X5+vfgdixBHkCNpYWXjKgfQ/P+uyGb5CjOLB9UcnTeg3rylQtV2hym44Q==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
"@types/react": "^19.3.0"
}
},
"node_modules/@types/trusted-types": {
@@ -1795,9 +1797,9 @@
}
},
"node_modules/autoprefixer": {
"version": "10.4.24",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.24.tgz",
"integrity": "sha512-uHZg7N9ULTVbutaIsDRoUkoS8/h3bdsmVJYZ5l3wv8Cp/6UIIoRDm90hZ+BwxUj/hGBEzLxdHNSKuFpn8WOyZw==",
"version": "10.6.1",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.6.1.tgz",
"integrity": "sha512-cL1Qz6ADZhcEbny/8HPfe99J6HhNoYtpX2LFLIbhgGE7Q1hlQVkYFdetDN7Id3KiQxhDrHwzlHr/YQCnZ8+xSA==",
"dev": true,
"funding": [
{
@@ -1813,9 +1815,10 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.28.1",
"caniuse-lite": "^1.0.30001766",
"browserslist": "^4.28.9",
"caniuse-lite": "^1.0.30001810",
"fraction.js": "^5.3.4",
"picocolors": "^1.1.1",
"postcss-value-parser": "^4.2.0"
@@ -2286,10 +2289,11 @@
}
},
"node_modules/eslint-plugin-react-hooks": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
"integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
"integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/parser": "^7.24.4",
@@ -2301,16 +2305,17 @@
"node": ">=18"
},
"peerDependencies": {
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
}
},
"node_modules/eslint-plugin-react-refresh": {
"version": "0.4.26",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz",
"integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==",
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.7.tgz",
"integrity": "sha512-XhJSzLljuYD4UjNuFGJu2v7aD3sqNVK12w5/DCUfrNfHWegZo2+TsavNx8MuxMwEquuFAzNbNfXKU0WoZ+YUIg==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"eslint": ">=8.40"
"eslint": "^9 || ^10"
}
},
"node_modules/eslint-scope": {
@@ -3356,14 +3361,14 @@
"dev": true
},
"node_modules/posthog-js": {
"version": "1.434.12",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.434.12.tgz",
"integrity": "sha512-gD7Xv8/iuSGiYYQHB9fxnfTsiPvi+WNJiEW9OXnZ/UFS9ZVkOzMVmEq29/NFLeYebVGQpVCBh0H4xbK1sJ7j5g==",
"version": "1.435.6",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.435.6.tgz",
"integrity": "sha512-PdBZw1Np4Ch2ZzvQ/Ij0CTBrJSkSeEmDJsyJfbaipTYH8XJKJk45BYLtNgL4z9DlevRcYJxhwb15VrDzbw7shA==",
"license": "(Apache-2.0 AND MIT)",
"dependencies": {
"@posthog/browser-common": "^0.9.0",
"@posthog/core": "^1.55.2",
"@posthog/types": "^1.412.4",
"@posthog/browser-common": "^0.9.1",
"@posthog/core": "^1.55.3",
"@posthog/types": "^1.413.0",
"core-js": "^3.49.0",
"dompurify": "^3.4.13",
"fflate": "^0.4.8",
@@ -3448,22 +3453,24 @@
]
},
"node_modules/react": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"version": "19.3.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.3.0.tgz",
"integrity": "sha512-E8LUcbtBWt20bbl2YoHfx4ZDBdxVTfOKtCZn9cDSJ4l6/nuoApcpIBcj47t2wZoVX8g2ZHuMHbiShgCR1T5Sog==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.4",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"version": "19.3.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.3.0.tgz",
"integrity": "sha512-JDk8dgif51OjFoDE70+OT9ICyYr+69HlmihNwp1+Nsfbna3t5sIiCa9ZJktDmQ4/1b/rn26hIAR2uYXDMr5r0Q==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
"scheduler": "^0.28.0"
},
"peerDependencies": {
"react": "^19.2.4"
"react": "^19.3.0"
}
},
"node_modules/react-refresh": {
@@ -3618,9 +3625,10 @@
}
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="
"version": "0.28.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.28.0.tgz",
"integrity": "sha512-juorfCmIkIw8tT+p5BXSm6PJjQF/ycEYmKyzURCIt/RaZIhL+PulbQ9Yu2z1HdOJDdqDTlxA1+xKBmHXJsczAw==",
"license": "MIT"
},
"node_modules/semver": {
"version": "6.3.1",
+8 -8
View File
@@ -13,19 +13,19 @@
},
"dependencies": {
"lucide-react": "^0.564.0",
"posthog-js": "^1.353.0",
"react": "^19.2.0",
"react-dom": "^19.2.0"
"posthog-js": "^1.435.6",
"react": "^19.3.0",
"react-dom": "^19.3.0"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@types/react": "^19.3.0",
"@types/react-dom": "^19.3.0",
"@vitejs/plugin-react": "^5.1.1",
"autoprefixer": "^10.4.24",
"autoprefixer": "^10.6.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.7",
"globals": "^16.5.0",
"postcss": "^8.5.6",
"tailwindcss": "^3.4.19",
+5 -1
View File
@@ -436,6 +436,8 @@ function App() {
useEffect(() => { setBooting(tree.bootState === 'loading') }, [tree.bootState])
// Arbre partagé d'où l'on vient quand on ouvre l'écran compte : on y revient après connexion/création
const pendingSharedTreeRef = useRef(null)
// Son nom, affiché par l'écran compte : un ref lu au rendu ne redessine rien quand il change
const [pendingSharedTreeName, setPendingSharedTreeName] = useState('')
// Écran de compte ouvert depuis un arbre affiché : la croix y ramène
const [accountFromTree, setAccountFromTree] = useState(false)
const refreshTreeAndKeepSelection = useCallback(async (personId, options = {}) => {
@@ -2064,6 +2066,7 @@ function App() {
treeOwnerEmail: ctx.treeOwnerEmail,
}
: null
setPendingSharedTreeName(pendingSharedTreeRef.current?.treeName || '')
setTreeNotFound(false)
tree.setGateError('')
auth.setAccountError('')
@@ -2130,6 +2133,7 @@ function App() {
return true
} finally {
pendingSharedTreeRef.current = null
setPendingSharedTreeName('')
}
}
@@ -2456,7 +2460,7 @@ function App() {
trees={auth.accountTrees}
activeTreeId={tree.treeContext.treeId}
defaultMode={accountEntryMode}
pendingTreeName={pendingSharedTreeRef.current?.treeName || ''}
pendingTreeName={pendingSharedTreeName}
onLogin={handleLogin}
onRegister={handleRegister}
onOpenTree={handleOpenTreeFromAccount}
+43 -44
View File
@@ -1,4 +1,4 @@
import { memo, useEffect, useRef, useState, useCallback } from 'react'
import { memo, useEffect, useImperativeHandle, useRef, useState, useCallback } from 'react'
import { getPersonMedias, getParents, getChildren, getPersonUnions } from '../data/mockData'
import { PERSON_R, FLOAT_SPEED, FLOAT_AMPLITUDE, REPULSION_RADIUS, REPULSION_STRENGTH, MAX_ORBIT_MEDIAS, ENTRANCE_GEN_DELAY, ENTRANCE_NODE_DURATION, ENTRANCE_NODE_STAGGER, ENTRANCE_MAX_GEN_SPAN, ENTRANCE_MAX_NODE_SPAN, ENTRANCE_LINE_DELAY, ENTRANCE_LINE_DURATION, ENTRANCE_ORBIT_DELAY, ENTRANCE_ORBIT_DURATION, ENTRANCE_ORBIT_STAGGER } from './galaxy/constants'
import { easeOutCubic } from './galaxy/utils'
@@ -33,7 +33,8 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
const canvasRef = useRef(null)
// Lu au calcul du cadrage seulement : changer de personne de départ ne recadre pas une vue en cours
const focusPersonIdRef = useRef(focusPersonId)
focusPersonIdRef.current = focusPersonId
// Mis à jour hors du rendu, avant l'effet de cadrage déclaré plus bas (même phase, ordre de déclaration)
useEffect(() => { focusPersonIdRef.current = focusPersonId }, [focusPersonId])
const layoutDataRef = useRef(null)
const [layoutReady, setLayoutReady] = useState(false)
const transformRef = useRef({ x: 0, y: 0, scale: 1 })
@@ -118,48 +119,46 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
maxScale: MAX_SCALE,
})
// Expose zoom API to parent (ZoomControl component)
if (zoomApiRef) {
zoomApiRef.current = {
zoomTo: (newScale) => {
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const cx = rect.width / 2
const cy = rect.height / 2
const base = targetTransformRef.current
const clamped = Math.min(MAX_SCALE, Math.max(MIN_SCALE, newScale))
const ratio = clamped / base.scale
targetTransformRef.current = {
scale: clamped,
x: cx - ratio * (cx - base.x),
y: cy - ratio * (cy - base.y),
}
},
resetZoom: () => {
if (initialTransformRef.current) {
targetTransformRef.current = { ...initialTransformRef.current }
}
},
// Recherche : glisse jusqu'à la personne, dans la partie de l'écran que la fiche ne couvre pas
// (fiche à gauche sur ordinateur, 408 px ; par le bas sur téléphone)
focusPerson: (personId) => {
const canvas = canvasRef.current
const node = layoutDataRef.current?.children.find(n => n.id === `p-${personId}`)
if (!canvas || !node) return
const rect = canvas.getBoundingClientRect()
const pos = getAnimatedPos(node, performance.now())
const scale = Math.min(MAX_SCALE, Math.max(targetTransformRef.current.scale, 1))
const phone = rect.width <= 768
const screenX = phone ? rect.width / 2 : (408 + rect.width) / 2
const screenY = phone ? rect.height * 0.22 : rect.height / 2
targetTransformRef.current = { scale, x: screenX - pos.cx * scale, y: screenY - pos.cy * scale }
},
getScale: () => transformRef.current.scale,
MIN_SCALE,
MAX_SCALE,
}
}
// Expose zoom API to parent (ZoomControl component), posée au commit et non pendant le rendu
useImperativeHandle(zoomApiRef, () => ({
zoomTo: (newScale) => {
const canvas = canvasRef.current
if (!canvas) return
const rect = canvas.getBoundingClientRect()
const cx = rect.width / 2
const cy = rect.height / 2
const base = targetTransformRef.current
const clamped = Math.min(MAX_SCALE, Math.max(MIN_SCALE, newScale))
const ratio = clamped / base.scale
targetTransformRef.current = {
scale: clamped,
x: cx - ratio * (cx - base.x),
y: cy - ratio * (cy - base.y),
}
},
resetZoom: () => {
if (initialTransformRef.current) {
targetTransformRef.current = { ...initialTransformRef.current }
}
},
// Recherche : glisse jusqu'à la personne, dans la partie de l'écran que la fiche ne couvre pas
// (fiche à gauche sur ordinateur, 408 px ; par le bas sur téléphone)
focusPerson: (personId) => {
const canvas = canvasRef.current
const node = layoutDataRef.current?.children.find(n => n.id === `p-${personId}`)
if (!canvas || !node) return
const rect = canvas.getBoundingClientRect()
const pos = getAnimatedPos(node, performance.now())
const scale = Math.min(MAX_SCALE, Math.max(targetTransformRef.current.scale, 1))
const phone = rect.width <= 768
const screenX = phone ? rect.width / 2 : (408 + rect.width) / 2
const screenY = phone ? rect.height * 0.22 : rect.height / 2
targetTransformRef.current = { scale, x: screenX - pos.cx * scale, y: screenY - pos.cy * scale }
},
getScale: () => transformRef.current.scale,
MIN_SCALE,
MAX_SCALE,
}), [getAnimatedPos])
// Zoom util (used by pinch)
const zoomAt = useCallback((factor, center) => {
+4 -3
View File
@@ -52,8 +52,9 @@ export function useTreeAccess() {
// Change à chaque chargement des données, même sans nouveau placement : l'arbre y charge les images manquantes
const [assetRevision, setAssetRevision] = useState(0)
// Horodatage des portraits stable pendant la session : un rechargement ne force pas à tout retélécharger.
// Il ne change que lorsqu'un portrait change (envoi d'une nouvelle photo).
const avatarCacheBustRef = useRef(Date.now())
// Il ne change que lorsqu'un portrait change (envoi d'une nouvelle photo). Posé au premier chargement
// d'un arbre, pas au rendu (Date.now() rendrait le rendu impur).
const avatarCacheBustRef = useRef(null)
const canEditCurrentTree = useMemo(() => {
const role = normalizeRole(treeContext.role)
@@ -70,7 +71,7 @@ export function useTreeAccess() {
loadGraphData(graphPayload.graph, {
mediaToken: graphPayload.mediaToken,
apiBaseUrl: API_BASE_URL,
avatarCacheBust: options.avatarCacheBust ?? avatarCacheBustRef.current,
avatarCacheBust: options.avatarCacheBust ?? (avatarCacheBustRef.current ??= Date.now()),
})
if (options.avatarCacheBust) avatarCacheBustRef.current = options.avatarCacheBust
setAssetRevision((current) => current + 1)