diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 6d16842..ddea931 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -582,6 +582,9 @@ function App() { // Grandes belles-familles dans des onglets (réglage de ce navigateur, voir Paramètres de l'arbre) const [familyTabs, setFamilyTabsState] = useState(() => { try { return localStorage.getItem('parenthese.familyTabs') !== 'off' } catch { return true } }) const setFamilyTabs = useCallback((on) => { setFamilyTabsState(on); try { localStorage.setItem('parenthese.familyTabs', on ? 'on' : 'off') } catch { /* navigateur sans stockage */ } }, []) + // Nom de famille sous le prénom dans l'arbre, celui de naissance s'il est connu (#168). Réglage de ce navigateur. + const [fullNames, setFullNamesState] = useState(() => { try { return localStorage.getItem('parenthese.fullNames') === 'on' } catch { return false } }) + const setFullNames = useCallback((on) => { setFullNamesState(on); try { localStorage.setItem('parenthese.fullNames', on ? 'on' : 'off') } catch { /* navigateur sans stockage */ } }, []) // Premiers visages : fenêtre ouverte après un import GEDCOM, ou depuis les paramètres de l'arbre const [photoStart, setPhotoStart] = useState({ visible: false, personsImported: null, refs: [], mediaRefs: [], faces: [], bundledFiles: [] }) const photoStartDirtyRef = useRef(false) @@ -3485,6 +3488,7 @@ function App() { reorderEnabled={canReorderSiblings} onReorderSiblings={handleReorderSiblings} familyTabs={familyTabs} + fullNames={fullNames} annotationViewRef={annotationViewRef} onAnnotationsAnchored={persistAnnotationAnchors} selfPersonId={linkedSelfPersonId || null} @@ -3659,6 +3663,8 @@ function App() { onClose={() => setAdminPanelVisible(false)} familyTabs={familyTabs} onFamilyTabsChange={setFamilyTabs} + fullNames={fullNames} + onFullNamesChange={setFullNames} /> {t("Une belle-famille plus large qu'un écran s'ouvre dans son propre onglet au lieu d'être dessinée dans l'arbre. Réglage de ce navigateur.")} + )} diff --git a/frontend/src/components/Galaxy.jsx b/frontend/src/components/Galaxy.jsx index b9a1825..86e9ab8 100644 --- a/frontend/src/components/Galaxy.jsx +++ b/frontend/src/components/Galaxy.jsx @@ -47,7 +47,7 @@ const centerX = (node) => node.x + node.width / 2 const easeOutQuint = (t) => 1 - (1 - t) ** 5 const prefersReducedMotion = () => typeof window !== 'undefined' && window.matchMedia?.('(prefers-reduced-motion: reduce)').matches -const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlightIds = [], graphRevision = 0, annotationHandlers = null, transformReadRef = null, zoomApiRef = null, focusPersonId = null, assetRevision = 0, reorderEnabled = false, onReorderSiblings = null, familyTabs = true, annotationViewRef = null, onAnnotationsAnchored = null, selfPersonId = null, ghosts = null, initialFamilyKey = null, onFamilyChange = null }) => { +const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlightIds = [], graphRevision = 0, annotationHandlers = null, transformReadRef = null, zoomApiRef = null, focusPersonId = null, assetRevision = 0, reorderEnabled = false, onReorderSiblings = null, familyTabs = true, fullNames = false, annotationViewRef = null, onAnnotationsAnchored = null, selfPersonId = null, ghosts = null, initialFamilyKey = null, onFamilyChange = null }) => { const canvasRef = useRef(null) const layoutDataRef = useRef(null) // Onglets de familles (réglage `familyTabs`) : la liste des familles de l'arbre, calculée une fois par @@ -79,6 +79,8 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig useEffect(() => { familyTabsRef.current = familyTabs }, [familyTabs]) // Propositions pas encore relues (utils/ghosts.js) : placées et dessinées en transparence, jamais dans les données const ghostsRef = useRef(ghosts) + const fullNamesRef = useRef(fullNames) + useEffect(() => { fullNamesRef.current = fullNames; needsRedrawRef.current = true }, [fullNames]) useEffect(() => { ghostsRef.current = ghosts; needsRedrawRef.current = true }, [ghosts]) const onAnnotationsAnchoredRef = useRef(onAnnotationsAnchored) useEffect(() => { onAnnotationsAnchoredRef.current = onAnnotationsAnchored }, [onAnnotationsAnchored]) @@ -1084,6 +1086,7 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig view, liftScales: liftScalesRef.current, ghostPatches: ghostsRef.current?.patches || null, + fullNames: fullNamesRef.current, }) drawRenvoiPills(ctx) // Annotations dessinées APRES les noeuds pour que stickers/texte soient au-dessus. diff --git a/frontend/src/components/galaxy/renderers.js b/frontend/src/components/galaxy/renderers.js index 4273ece..c1f838c 100644 --- a/frontend/src/components/galaxy/renderers.js +++ b/frontend/src/components/galaxy/renderers.js @@ -472,7 +472,7 @@ function measureTextCached(ctx, text, fontKey) { // ============================================================ function drawPersonNode(ctx, node, pos, nodeProgress, { nodeRandomData, imageCache, hoverScales, selectedGlowScales, searchGlowScales, - mediaHoverState, entrance, entranceActive, elapsed, view = null, liftScales = null, + mediaHoverState, entrance, entranceActive, elapsed, view = null, liftScales = null, fullNames = false, }) { const { cx, cy } = pos const person = getPersonById(node._data.id) || node._data @@ -633,15 +633,20 @@ function drawPersonNode(ctx, node, pos, nodeProgress, { ctx.font = `${nameUnknown ? 'italic ' : ''}400 13px "Newsreader", Georgia, serif` ctx.textAlign = 'center' ctx.textBaseline = 'middle' + // Réglage « nom de famille sous le prénom » (#168) : celui de naissance s'il est connu, coupé comme le surnom + const familyName = fullNames && withDetail ? String(person.birthName || person.lastName || '').trim() : '' + const familyLabel = familyName ? fitNickname(ctx, familyName, NICKNAME_MAX_W) : '' + const lineShift = familyLabel ? 15 : 0 if (nameLabel) { if (withDetail) { const nameMetrics = measureTextCached(ctx, nameLabel, nameUnknown ? 'name-unknown' : 'name') const nameBlockH = Math.max( 18, (nameMetrics.actualBoundingBoxAscent || 7) + (nameMetrics.actualBoundingBoxDescent || 4) + 8, - ) - const labelGlowY = 6 - const labelGlowRx = Math.max(26, Math.round(nameMetrics.width * 0.62)) + ) + lineShift + const labelGlowY = 6 + lineShift / 2 + const labelW = familyLabel ? Math.max(nameMetrics.width, ctx.measureText(familyLabel).width) : nameMetrics.width + const labelGlowRx = Math.max(26, Math.round(labelW * 0.62)) const labelGlowRy = Math.max(16, Math.round(nameBlockH * 1.15)) const labelGlow = ctx.createRadialGradient(0, labelGlowY, 0, 0, labelGlowY, labelGlowRx) labelGlow.addColorStop(0, 'rgba(251, 250, 247, 0.98)') @@ -661,13 +666,18 @@ function drawPersonNode(ctx, node, pos, nodeProgress, { ctx.fillStyle = COLORS.text ctx.fillText(nameLabel, 0, 0) } + if (familyLabel) { + ctx.font = '400 13px "Newsreader", Georgia, serif' + ctx.fillStyle = COLORS.text + ctx.fillText(familyLabel, 0, 15) + } // Surnom sous le nom, en italique : les dates descendent pour lui faire place const hasNickname = withDetail && Boolean(person.nickname) if (hasNickname) { ctx.font = 'italic 300 13px "Newsreader", Georgia, serif' ctx.fillStyle = COLORS.textLight - ctx.fillText(fitNickname(ctx, person.nickname, NICKNAME_MAX_W), 0, 15) + ctx.fillText(fitNickname(ctx, person.nickname, NICKNAME_MAX_W), 0, 15 + lineShift) } if (withDetail) { @@ -677,7 +687,7 @@ function drawPersonNode(ctx, node, pos, nodeProgress, { const years = person.deathYear ? `${birthYearLabel}–${person.deathYear}` : person.isAlive === false ? `${birthYearLabel}–?` : `${birthYearLabel}` - ctx.fillText(years, 0, hasNickname ? 31 : 15) + ctx.fillText(years, 0, (hasNickname ? 31 : 15) + lineShift) } ctx.restore() diff --git a/frontend/src/i18n/en/arbre.js b/frontend/src/i18n/en/arbre.js index 1afee5d..824fe8b 100644 --- a/frontend/src/i18n/en/arbre.js +++ b/frontend/src/i18n/en/arbre.js @@ -6,6 +6,8 @@ export default { 'Affichage': 'Display', 'Les grandes familles par alliance dans des onglets': 'Large in-law families in tabs', "Une belle-famille plus large qu'un écran s'ouvre dans son propre onglet au lieu d'être dessinée dans l'arbre. Réglage de ce navigateur.": 'An in-law family wider than a screen opens in its own tab instead of being drawn in the tree. Saved in this browser.', + 'Le nom de famille sous le prénom': 'Last name under the first name', + "Le nom de naissance quand il est connu, sinon le nom d'usage. Réglage de ce navigateur.": 'The maiden name when it is known, otherwise the current last name. Saved in this browser.', // Arbre sur canvas (components/galaxy/renderers.js) 'Voir les parents': 'See parents', 'Voir {firstName}': 'See {firstName}',