mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-06 23:32:08 +00:00
Lien d'un onglet de famille : ?famille=<charnière> ouvre l'arbre sur cette famille, et le lien de « La famille participe » suit l'onglet ouvert (#167)
- Galaxy : l'onglet du lien s'ouvre dès que l'arbre et ses onglets sont connus, et de nouveau si la personne de départ arrive après (visiteur), tant qu'on n'a pas changé d'onglet soi-même ; onglets coupés dans ce navigateur, l'arbre entier s'ouvre sur la charnière - App : l'adresse de la page porte l'onglet ouvert, le lien à envoyer aussi, avec le nom de la famille dans le titre - Même mot de passe, même accès : le lien choisit l'onglet d'arrivée, il ne restreint rien (Patch, 26/10) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b9f301619b
commit
42e4ed63bb
4 files changed
+55
-8
No files matched your search
+15
-1
@@ -544,6 +544,17 @@ function App() {
|
||||
})
|
||||
|
||||
const [adminPanelVisible, setAdminPanelVisible] = useState(false)
|
||||
// Onglet de famille ouvert : l'adresse de la page le porte (?famille=<charnière>), et le lien à envoyer à la
|
||||
// famille aussi, pour qu'il ouvre l'arbre sur cette famille
|
||||
const [initialFamilyKey] = useState(() => new URLSearchParams(window.location.search).get('famille'))
|
||||
const [openFamily, setOpenFamily] = useState(null)
|
||||
const handleFamilyChange = useCallback((family) => {
|
||||
setOpenFamily(family)
|
||||
const url = new URL(window.location.href)
|
||||
if (family) url.searchParams.set('famille', family.key)
|
||||
else url.searchParams.delete('famille')
|
||||
window.history.replaceState(window.history.state, '', `${url.pathname}${url.search}${url.hash}`)
|
||||
}, [])
|
||||
// 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 */ } }, [])
|
||||
@@ -3404,6 +3415,8 @@ function App() {
|
||||
onAnnotationsAnchored={persistAnnotationAnchors}
|
||||
selfPersonId={linkedSelfPersonId || null}
|
||||
ghosts={ghosts}
|
||||
initialFamilyKey={initialFamilyKey}
|
||||
onFamilyChange={handleFamilyChange}
|
||||
/>
|
||||
<ZoomControl zoomApiRef={galaxyZoomApiRef} />
|
||||
{selectedGhost && !tree.selectedPerson && (
|
||||
@@ -3510,7 +3523,8 @@ function App() {
|
||||
canModerate={tree.canEditCurrentTree}
|
||||
loading={contrib.contribState.loading}
|
||||
errorMessage={contrib.contribState.error}
|
||||
inviteShareUrl={contribInviteState.shareUrl}
|
||||
inviteShareUrl={contribInviteState.shareUrl && openFamily ? `${contribInviteState.shareUrl}?famille=${encodeURIComponent(openFamily.key)}` : contribInviteState.shareUrl}
|
||||
inviteFamilyName={openFamily?.label || ''}
|
||||
inviteKnownPasswords={contribInviteState.knownPasswords}
|
||||
inviteSaving={contribInviteState.loading}
|
||||
inviteError={contribInviteState.error}
|
||||
|
||||
@@ -240,6 +240,8 @@ const ContributionPanel = ({
|
||||
loading,
|
||||
errorMessage,
|
||||
inviteShareUrl,
|
||||
// Onglet de famille ouvert : le lien ouvre l'arbre sur cette famille
|
||||
inviteFamilyName = '',
|
||||
inviteKnownPasswords,
|
||||
inviteSaving,
|
||||
inviteError,
|
||||
@@ -403,7 +405,7 @@ const ContributionPanel = ({
|
||||
{/* ── Section invitation ── */}
|
||||
<div className="contrib-invite">
|
||||
<div className="contrib-invite-top">
|
||||
<strong className="contrib-invite-title">{t('Le lien à envoyer à la famille')}</strong>
|
||||
<strong className="contrib-invite-title">{inviteFamilyName ? t('Le lien à envoyer à la famille {name}', { name: inviteFamilyName }) : t('Le lien à envoyer à la famille')}</strong>
|
||||
{copyFeedback && <span className="contrib-copy-feedback">{copyFeedback}</span>}
|
||||
</div>
|
||||
<div className="contrib-invite-link">
|
||||
|
||||
@@ -44,7 +44,7 @@ const DROP_SPRING = { stiffness: 480, dampingRatio: 0.72 }
|
||||
const centerX = (node) => node.x + node.width / 2
|
||||
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 }) => {
|
||||
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 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
|
||||
@@ -54,6 +54,14 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
const [activeKey, setActiveKey] = useState(focusPersonId != null ? String(focusPersonId) : null)
|
||||
const tabsCacheRef = useRef({ rev: -1, start: undefined, tabs: [] })
|
||||
const pendingFocusRef = useRef(null) // personne cherchée dans un autre onglet, visée après le changement
|
||||
// Lien d'un onglet (?famille=<charnière>) : ouvert dès que l'arbre et ses onglets sont connus, et de nouveau si
|
||||
// la personne de départ arrive après (visiteur), tant qu'on n'a pas soi-même changé d'onglet
|
||||
const linkFamilyRef = useRef(initialFamilyKey != null ? String(initialFamilyKey) : null)
|
||||
const pendingFamilyRef = useRef(initialFamilyKey != null ? String(initialFamilyKey) : null)
|
||||
// Famille du lien sans onglet dans ce navigateur (onglets coupés) : l'arbre entier s'ouvre sur sa charnière
|
||||
const frameOnRef = useRef(null)
|
||||
const onFamilyChangeRef = useRef(onFamilyChange)
|
||||
useEffect(() => { onFamilyChangeRef.current = onFamilyChange }, [onFamilyChange])
|
||||
const activeTabRef = useRef(null)
|
||||
const familyTabsRef = useRef(familyTabs)
|
||||
useEffect(() => { familyTabsRef.current = familyTabs }, [familyTabs])
|
||||
@@ -63,7 +71,13 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
const onAnnotationsAnchoredRef = useRef(onAnnotationsAnchored)
|
||||
useEffect(() => { onAnnotationsAnchoredRef.current = onAnnotationsAnchored }, [onAnnotationsAnchored])
|
||||
const startRefRef = useRef(focusPersonId)
|
||||
useEffect(() => { startRefRef.current = focusPersonId; setActiveKey(focusPersonId != null ? String(focusPersonId) : null) }, [focusPersonId])
|
||||
useEffect(() => {
|
||||
startRefRef.current = focusPersonId
|
||||
if (linkFamilyRef.current != null) pendingFamilyRef.current = linkFamilyRef.current
|
||||
setActiveKey(focusPersonId != null ? String(focusPersonId) : null)
|
||||
}, [focusPersonId])
|
||||
// Onglet choisi par la personne (barre, badge, recherche) : le lien d'arrivée ne s'impose plus
|
||||
const chooseTab = useCallback((key) => { linkFamilyRef.current = null; pendingFamilyRef.current = null; frameOnRef.current = null; setActiveKey(key) }, [])
|
||||
// Lu au calcul du cadrage et par le réordonnancement : la référence de l'arbre affiché
|
||||
const focusPersonIdRef = useRef(focusPersonId)
|
||||
// La barre défile quand les onglets débordent : l'onglet ouvert se met en vue (sans animation) et les bords
|
||||
@@ -83,7 +97,7 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
return () => window.removeEventListener('resize', updateTabsEdges)
|
||||
}, [activeKey, tabs, updateTabsEdges])
|
||||
// Clic sur un badge : l'onglet de cette famille (ou, depuis un onglet, celui d'où l'on vient)
|
||||
const openFamily = useCallback((tabKey) => { if (tabKey != null) setActiveKey(String(tabKey)) }, [])
|
||||
const openFamily = useCallback((tabKey) => { if (tabKey != null) chooseTab(String(tabKey)) }, [chooseTab])
|
||||
// Propriétaire : un portrait vide survolé montre « + photo »
|
||||
const [layoutReady, setLayoutReady] = useState(false)
|
||||
const transformRef = useRef({ x: 0, y: 0, scale: 1 })
|
||||
@@ -418,7 +432,7 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
const id = String(personId)
|
||||
const all = tabsCacheRef.current.tabs
|
||||
const home = all.find((tb) => tb.personIds?.has(id) && !(tb.hinges || []).map(String).includes(id)) || all.find((tb) => tb.personIds?.has(id)) || all[0]
|
||||
if (home && home.key !== activeTabRef.current?.key) { pendingFocusRef.current = id; setActiveKey(home.key) }
|
||||
if (home && home.key !== activeTabRef.current?.key) { pendingFocusRef.current = id; chooseTab(home.key) }
|
||||
return
|
||||
}
|
||||
if (!canvas || !node) return
|
||||
@@ -490,6 +504,15 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
setTabs(tabsCacheRef.current.tabs)
|
||||
}
|
||||
if (!familyTabs && tabsCacheRef.current.tabs.length) { tabsCacheRef.current = { rev: -1, start: undefined, tabs: [] }; setTabs([]) }
|
||||
// La famille du lien : son onglet s'il existe dans ce navigateur, sinon l'arbre entier ouvert sur sa charnière
|
||||
if (pendingFamilyRef.current != null && graphPersons.length) {
|
||||
const key = pendingFamilyRef.current
|
||||
pendingFamilyRef.current = null
|
||||
if (familyTabs && tabsCacheRef.current.tabs.some((tb) => tb.key === key)) {
|
||||
frameOnRef.current = null
|
||||
if (key !== activeKey) { setActiveKey(key); return }
|
||||
} else frameOnRef.current = key
|
||||
}
|
||||
const tab = familyTabs ? (tabsCacheRef.current.tabs.find((tb) => tb.key === activeKey) || tabsCacheRef.current.tabs[0] || null) : null
|
||||
activeTabRef.current = tab
|
||||
// La personne de départ (ou la charnière de l'onglet) sert de référence aux rangées
|
||||
@@ -554,7 +577,7 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
|
||||
// Grand arbre : vu en entier, les portraits deviennent des pastilles illisibles.
|
||||
// On ouvre sur la personne de départ, à une taille où l'on reconnaît les visages.
|
||||
const focusId = focusPersonIdRef.current
|
||||
const focusId = frameOnRef.current ?? focusPersonIdRef.current
|
||||
const personNodes = result.children.filter((n) => n._type === 'person')
|
||||
let focusNode = focusId != null
|
||||
? personNodes.find((n) => String(n._data?.id) === String(focusId))
|
||||
@@ -663,6 +686,13 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
}
|
||||
}, [graphRevision, activeKey, familyTabs, selfPersonId, ghosts])
|
||||
|
||||
// Onglet ouvert, remonté pour l'adresse de la page et le lien de partage ; null pour la famille de départ
|
||||
useEffect(() => {
|
||||
if (pendingFamilyRef.current != null) return
|
||||
const open = familyTabs && tabs.length > 1 ? tabs.find((tb) => tb.key === activeKey) : null
|
||||
onFamilyChangeRef.current?.(open && open !== tabs[0] ? { key: open.key, label: open.label } : null)
|
||||
}, [activeKey, tabs, familyTabs])
|
||||
|
||||
// Sync selectedPersonId
|
||||
useEffect(() => {
|
||||
selectedPersonIdRef.current = selectedPersonId
|
||||
@@ -1114,7 +1144,7 @@ const Galaxy = ({ onPersonSelect, onMediaSelect, selectedPersonId, searchHighlig
|
||||
type="button"
|
||||
className={`family-tab${active ? ' is-active' : ''}`}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
onClick={() => setActiveKey(tb.key)}
|
||||
onClick={() => chooseTab(tb.key)}
|
||||
>
|
||||
<span className="family-tab-name">{tb.label ? tr('Famille {name}', { name: tb.label }) : tr('Famille')}</span>
|
||||
<span className="family-tab-count">{tb.count}</span>
|
||||
|
||||
@@ -56,6 +56,7 @@ export default {
|
||||
'participe': 'joins in',
|
||||
'Fermer': 'Close',
|
||||
'Le lien à envoyer à la famille': 'The link to send to your family',
|
||||
'Le lien à envoyer à la famille {name}': 'The link to send to the {name} family',
|
||||
'Lien indisponible': 'Link unavailable',
|
||||
'Lien copié': 'Link copied',
|
||||
'Copier': 'Copy',
|
||||
|
||||
Reference in new issue
Block a user