Refonte de la direction artistique : accent terracotta, landing, app et démo
- Landing refaite : nouvelle palette claire, Newsreader et DM Sans, logo validé, sections (garanties, trois gestes, démo, pour qui, engagements, guides, bloc final). Illustration du hero conservée et recolorée, portraits alignés sur les âges. - App : couleurs et typographie passées à la nouvelle DA, structure et fonctionnalités inchangées. Composants communs dans src/styles/pz.css. - Refaits de zéro : fiche d'une personne (souvenirs, infos, liens, panneau mobile), connexion, création de compte, liste des arbres, accès par mot de passe, recherche, barre d'actions, menu du compte, zoom, et toutes les fenêtres (compte, création d'arbre, contributions, envoi, sortie d'édition, brouillon, ajout de personne, souvenirs, visionneuse, paramètres de l'arbre). - Arbre : chaque personne garde sa forme de cadre tirée au hasard, cadre vertical pour une photo carrée. - Démo : photos d'époque libres de droits hébergées dans public/demo (sources et licences dans sources.json et CREDITS.md), famille Dupont crédible, Isabelle née à Fort-de-France. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
No files matched your search
@@ -3,14 +3,14 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#FDF8EC" />
|
||||
<meta name="theme-color" content="#FBFAF7" />
|
||||
<title>Parenthèse</title>
|
||||
<meta name="description" content="Parenthèse, l'application. Les arbres partagés sont privés : la vitrine publique est sur parenthese.io." />
|
||||
<!-- Arbres familiaux privés (noms, dates) : l'app ne doit pas être indexée, la vitrine est parenthese.io -->
|
||||
<meta name="robots" content="noindex, nofollow" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Red+Hat+Mono:ital,wght@0,300..700;1,300..700&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300..600;1,6..72,300..500&family=DM+Sans:opsz,wght@9..40,400..600&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# Photos de l'arbre de démonstration
|
||||
|
||||
Les personnes et les souvenirs de la famille Dupont sont fictifs. Les photos sont réelles et libres d'usage.
|
||||
|
||||
- Photos anciennes : Wikimedia Commons, domaine public ou CC0 (Gallica / BnF, Agence Rol, Nationaal Archief / Anefo, State Library of Queensland, NARA, PikiWiki Israel).
|
||||
- « Portrait de famille » (souvenir-portrait-famille) : Australian National Maritime Museum, sans restriction de droits connue.
|
||||
- Portraits actuels, diplôme et plan : Pexels (licence Pexels).
|
||||
|
||||
Source exacte, licence et auteur de chaque fichier : `sources.json`.
|
||||
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,263 @@
|
||||
{
|
||||
"jean.webp": {
|
||||
"candidate": "jean-07",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:Album_de_photographies_de_la_Premi%C3%A8re_Guerre_mondiale_compos%C3%A9_par_le_soldat_Luthier_-_btv1b104633375_(07_of_10).jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/c/c9/Album_de_photographies_de_la_Premi%C3%A8re_Guerre_mondiale_compos%C3%A9_par_le_soldat_Luthier_-_btv1b104633375_%2807_of_10%29.jpg",
|
||||
"license": "Public domain",
|
||||
"author": "Luthier, Émile (1885-19..). Concepteur",
|
||||
"title": "Album de photographies de la Première Guerre mondiale composé par le soldat Luthier - btv1b104633375 (07 of 10).jpg",
|
||||
"date": "1915"
|
||||
},
|
||||
"marie.webp": {
|
||||
"candidate": "marie-16",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:Portrait_de_femme_-_btv1b531623363.jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/8/8a/Portrait_de_femme_-_btv1b531623363.jpg",
|
||||
"license": "Public domain",
|
||||
"author": "Agence Rol",
|
||||
"title": "Portrait de femme - btv1b531623363.jpg",
|
||||
"date": "1926"
|
||||
},
|
||||
"robert.webp": {
|
||||
"candidate": "m40-23",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:Portret_van_een_spiv,_Bestanddeelnr_191-1039.jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/e/e0/Portret_van_een_spiv%2C_Bestanddeelnr_191-1039.jpg",
|
||||
"license": "CC0",
|
||||
"author": "Willem van de Poll",
|
||||
"title": "Portret van een spiv, Bestanddeelnr 191-1039.jpg",
|
||||
"date": "1947"
|
||||
},
|
||||
"andre.webp": {
|
||||
"candidate": "m40-08",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:Portret_van_een_korporaal,_Bestanddeelnr_6341.jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/6/66/Portret_van_een_korporaal%2C_Bestanddeelnr_6341.jpg",
|
||||
"license": "CC0",
|
||||
"author": "Nationaal Archief",
|
||||
"title": "Portret van een korporaal, Bestanddeelnr 6341.jpg",
|
||||
"date": "1/01-7/-19"
|
||||
},
|
||||
"simone.webp": {
|
||||
"candidate": "simone-01",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:Portret_van_een_verpleegster,_Bestanddeelnr_6786.jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/5/55/Portret_van_een_verpleegster%2C_Bestanddeelnr_6786.jpg",
|
||||
"license": "CC0",
|
||||
"author": "Nationaal Archief",
|
||||
"title": "Portret van een verpleegster, Bestanddeelnr 6786.jpg",
|
||||
"date": "1947-01-01"
|
||||
},
|
||||
"isabelle.webp": {
|
||||
"candidate": "isabelle-09",
|
||||
"source_page": "https://www.pexels.com/photo/an-older-woman-in-a-pink-shirt-smiles-for-the-camera-28278106/",
|
||||
"file_url": "https://images.pexels.com/photos/28278106/pexels-photo-28278106.jpeg",
|
||||
"license": "Pexels License",
|
||||
"author": null,
|
||||
"title": "an-older-woman-in-a-pink-shirt-smiles-for-the-camera-28278106",
|
||||
"date": null
|
||||
},
|
||||
"jacques.webp": {
|
||||
"candidate": "m40-04",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:Portret_van_een_onbekende,_Bestanddeelnr_927-0170.jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/1/12/Portret_van_een_onbekende%2C_Bestanddeelnr_927-0170.jpg",
|
||||
"license": "CC0",
|
||||
"author": "Hans Peters for Anefo",
|
||||
"title": "Portret van een onbekende, Bestanddeelnr 927-0170.jpg",
|
||||
"date": "1974-02-20"
|
||||
},
|
||||
"philippe.webp": {
|
||||
"candidate": "philippe-03",
|
||||
"source_page": "https://www.pexels.com/photo/close-up-shot-of-an-elderly-man-smiling-8185930/",
|
||||
"file_url": "https://images.pexels.com/photos/8185930/pexels-photo-8185930.jpeg",
|
||||
"license": "Pexels License",
|
||||
"author": null,
|
||||
"title": "close-up-shot-of-an-elderly-man-smiling-8185930",
|
||||
"date": null
|
||||
},
|
||||
"claire.webp": {
|
||||
"candidate": "claire-03",
|
||||
"source_page": "https://www.pexels.com/photo/portrait-of-elderly-woman-on-grey-background-12645006/",
|
||||
"file_url": "https://images.pexels.com/photos/12645006/pexels-photo-12645006.jpeg",
|
||||
"license": "Pexels License",
|
||||
"author": null,
|
||||
"title": "portrait-of-elderly-woman-on-grey-background-12645006",
|
||||
"date": null
|
||||
},
|
||||
"sophie.webp": {
|
||||
"candidate": "sophie-06",
|
||||
"source_page": "https://www.pexels.com/photo/joyful-portrait-of-senior-woman-in-green-dress-36792488/",
|
||||
"file_url": "https://images.pexels.com/photos/36792488/pexels-photo-36792488.jpeg",
|
||||
"license": "Pexels License",
|
||||
"author": null,
|
||||
"title": "joyful-portrait-of-senior-woman-in-green-dress-36792488",
|
||||
"date": null
|
||||
},
|
||||
"thomas.webp": {
|
||||
"candidate": "thomas-06",
|
||||
"source_page": "https://www.pexels.com/photo/portrait-of-elderly-man-in-shirt-16779595/",
|
||||
"file_url": "https://images.pexels.com/photos/16779595/pexels-photo-16779595.jpeg",
|
||||
"license": "Pexels License",
|
||||
"author": null,
|
||||
"title": "portrait-of-elderly-man-in-shirt-16779595",
|
||||
"date": null
|
||||
},
|
||||
"souvenir-mariage-1918.webp": {
|
||||
"candidate": "mariage-09",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:StateLibQld_2_163403_Wedding_of_Fraz_and_Emilie_Muller,_1918.jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/2/25/StateLibQld_2_163403_Wedding_of_Fraz_and_Emilie_Muller%2C_1918.jpg",
|
||||
"license": "Public domain",
|
||||
"author": null,
|
||||
"title": "StateLibQld 2 163403 Wedding of Fraz and Emilie Muller, 1918.jpg",
|
||||
"date": "1918"
|
||||
},
|
||||
"souvenir-mariage-1918-thumb.webp": {
|
||||
"candidate": "mariage-09",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:StateLibQld_2_163403_Wedding_of_Fraz_and_Emilie_Muller,_1918.jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/2/25/StateLibQld_2_163403_Wedding_of_Fraz_and_Emilie_Muller%2C_1918.jpg",
|
||||
"license": "Public domain",
|
||||
"author": null,
|
||||
"title": "StateLibQld 2 163403 Wedding of Fraz and Emilie Muller, 1918.jpg",
|
||||
"date": "1918"
|
||||
},
|
||||
"souvenir-atelier.webp": {
|
||||
"candidate": "atelier-05",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:PikiWiki_Israel_13458_Carpentry_workshop_of_the_Alliance_School_in_Jerus.jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/d/df/PikiWiki_Israel_13458_Carpentry_workshop_of_the_Alliance_School_in_Jerus.jpg",
|
||||
"license": "Public domain",
|
||||
"author": "Unknown author",
|
||||
"title": "PikiWiki Israel 13458 Carpentry workshop of the Alliance School in Jerus.jpg",
|
||||
"date": "between 1910 and 1914"
|
||||
},
|
||||
"souvenir-atelier-thumb.webp": {
|
||||
"candidate": "atelier-05",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:PikiWiki_Israel_13458_Carpentry_workshop_of_the_Alliance_School_in_Jerus.jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/d/df/PikiWiki_Israel_13458_Carpentry_workshop_of_the_Alliance_School_in_Jerus.jpg",
|
||||
"license": "Public domain",
|
||||
"author": "Unknown author",
|
||||
"title": "PikiWiki Israel 13458 Carpentry workshop of the Alliance School in Jerus.jpg",
|
||||
"date": "between 1910 and 1914"
|
||||
},
|
||||
"souvenir-jeunesse.webp": {
|
||||
"candidate": "jeunesse-13",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:Twenty-one_photographs_of_the_same_unknown_family_members,_individually_and_in_groups,_various_dates_-_DPLA_-_f644083f547ba09779917864fd96c396_(page_11).jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/9/93/Twenty-one_photographs_of_the_same_unknown_family_members%2C_individually_and_in_groups%2C_various_dates_-_DPLA_-_f644083f547ba09779917864fd96c396_%28page_11%29.jpg",
|
||||
"license": "Public domain",
|
||||
"author": "Warner, Marion G., 1861-1922",
|
||||
"title": "Twenty-one photographs of the same unknown family members, individually and in groups, various dates - DPLA - f644083f547ba09779917864fd96c396 (page 11).jpg",
|
||||
"date": "1910-09-27; 1912-06; 1912-08-04; 1912-10-27; 1913-08-03; 191"
|
||||
},
|
||||
"souvenir-jeunesse-thumb.webp": {
|
||||
"candidate": "jeunesse-13",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:Twenty-one_photographs_of_the_same_unknown_family_members,_individually_and_in_groups,_various_dates_-_DPLA_-_f644083f547ba09779917864fd96c396_(page_11).jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/9/93/Twenty-one_photographs_of_the_same_unknown_family_members%2C_individually_and_in_groups%2C_various_dates_-_DPLA_-_f644083f547ba09779917864fd96c396_%28page_11%29.jpg",
|
||||
"license": "Public domain",
|
||||
"author": "Warner, Marion G., 1861-1922",
|
||||
"title": "Twenty-one photographs of the same unknown family members, individually and in groups, various dates - DPLA - f644083f547ba09779917864fd96c396 (page 11).jpg",
|
||||
"date": "1910-09-27; 1912-06; 1912-08-04; 1912-10-27; 1913-08-03; 191"
|
||||
},
|
||||
"souvenir-portrait-famille.webp": {
|
||||
"candidate": "famille-10",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:Group_portrait_of_an_unknown_family_(8434152558).jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/7/77/Group_portrait_of_an_unknown_family_%288434152558%29.jpg",
|
||||
"license": "No restrictions",
|
||||
"author": "Australian National Maritime Museum on The Commons",
|
||||
"title": "Group portrait of an unknown family (8434152558).jpg",
|
||||
"date": "1925"
|
||||
},
|
||||
"souvenir-portrait-famille-thumb.webp": {
|
||||
"candidate": "famille-10",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:Group_portrait_of_an_unknown_family_(8434152558).jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/7/77/Group_portrait_of_an_unknown_family_%288434152558%29.jpg",
|
||||
"license": "No restrictions",
|
||||
"author": "Australian National Maritime Museum on The Commons",
|
||||
"title": "Group portrait of an unknown family (8434152558).jpg",
|
||||
"date": "1925"
|
||||
},
|
||||
"souvenir-service-militaire.webp": {
|
||||
"candidate": "m40-20",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:Portret_van_een_soldaat,_Bestanddeelnr_255-9071.jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/c/c0/Portret_van_een_soldaat%2C_Bestanddeelnr_255-9071.jpg",
|
||||
"license": "CC0",
|
||||
"author": "Willem van de Poll",
|
||||
"title": "Portret van een soldaat, Bestanddeelnr 255-9071.jpg",
|
||||
"date": "Unknown date"
|
||||
},
|
||||
"souvenir-service-militaire-thumb.webp": {
|
||||
"candidate": "m40-20",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:Portret_van_een_soldaat,_Bestanddeelnr_255-9071.jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/c/c0/Portret_van_een_soldaat%2C_Bestanddeelnr_255-9071.jpg",
|
||||
"license": "CC0",
|
||||
"author": "Willem van de Poll",
|
||||
"title": "Portret van een soldaat, Bestanddeelnr 255-9071.jpg",
|
||||
"date": "Unknown date"
|
||||
},
|
||||
"souvenir-annees-70.webp": {
|
||||
"candidate": "annees70-16",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:FATHER_AND_DAUGHTER_ENJOYING_THE_FOURTH_OF_JULY_HOLIDAY_WHILE_ON_A_PICNIC_IN_A_PARK_AT_SLEEPY_EYE,_MINNESOTA._WHICH..._-_NARA_-_558186.jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/8/89/FATHER_AND_DAUGHTER_ENJOYING_THE_FOURTH_OF_JULY_HOLIDAY_WHILE_ON_A_PICNIC_IN_A_PARK_AT_SLEEPY_EYE%2C_MINNESOTA._WHICH..._-_NARA_-_558186.jpg",
|
||||
"license": "Public domain",
|
||||
"author": "Flip Schulke",
|
||||
"title": "FATHER AND DAUGHTER ENJOYING THE FOURTH OF JULY HOLIDAY WHILE ON A PICNIC IN A PARK AT SLEEPY EYE, MINNESOTA. WHICH... - NARA - 558186.jpg",
|
||||
"date": "Taken in July 1974"
|
||||
},
|
||||
"souvenir-annees-70-thumb.webp": {
|
||||
"candidate": "annees70-16",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:FATHER_AND_DAUGHTER_ENJOYING_THE_FOURTH_OF_JULY_HOLIDAY_WHILE_ON_A_PICNIC_IN_A_PARK_AT_SLEEPY_EYE,_MINNESOTA._WHICH..._-_NARA_-_558186.jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/8/89/FATHER_AND_DAUGHTER_ENJOYING_THE_FOURTH_OF_JULY_HOLIDAY_WHILE_ON_A_PICNIC_IN_A_PARK_AT_SLEEPY_EYE%2C_MINNESOTA._WHICH..._-_NARA_-_558186.jpg",
|
||||
"license": "Public domain",
|
||||
"author": "Flip Schulke",
|
||||
"title": "FATHER AND DAUGHTER ENJOYING THE FOURTH OF JULY HOLIDAY WHILE ON A PICNIC IN A PARK AT SLEEPY EYE, MINNESOTA. WHICH... - NARA - 558186.jpg",
|
||||
"date": "Taken in July 1974"
|
||||
},
|
||||
"souvenir-diplome.webp": {
|
||||
"candidate": "diplome-07",
|
||||
"source_page": "https://www.pexels.com/photo/person-holding-diploma-2293027/",
|
||||
"file_url": "https://images.pexels.com/photos/2293027/pexels-photo-2293027.jpeg",
|
||||
"license": "Pexels License",
|
||||
"author": null,
|
||||
"title": "person-holding-diploma-2293027",
|
||||
"date": null
|
||||
},
|
||||
"souvenir-diplome-thumb.webp": {
|
||||
"candidate": "diplome-07",
|
||||
"source_page": "https://www.pexels.com/photo/person-holding-diploma-2293027/",
|
||||
"file_url": "https://images.pexels.com/photos/2293027/pexels-photo-2293027.jpeg",
|
||||
"license": "Pexels License",
|
||||
"author": null,
|
||||
"title": "person-holding-diploma-2293027",
|
||||
"date": null
|
||||
},
|
||||
"souvenir-plan-cabinet.webp": {
|
||||
"candidate": "plan-01",
|
||||
"source_page": "https://www.pexels.com/photo/house-floor-plan-271667/",
|
||||
"file_url": "https://images.pexels.com/photos/271667/pexels-photo-271667.jpeg",
|
||||
"license": "Pexels License",
|
||||
"author": null,
|
||||
"title": "house-floor-plan-271667",
|
||||
"date": null
|
||||
},
|
||||
"souvenir-plan-cabinet-thumb.webp": {
|
||||
"candidate": "plan-01",
|
||||
"source_page": "https://www.pexels.com/photo/house-floor-plan-271667/",
|
||||
"file_url": "https://images.pexels.com/photos/271667/pexels-photo-271667.jpeg",
|
||||
"license": "Pexels License",
|
||||
"author": null,
|
||||
"title": "house-floor-plan-271667",
|
||||
"date": null
|
||||
},
|
||||
"souvenir-redaction.webp": {
|
||||
"candidate": "redaction-01",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:Redactie_bij_krant_(_Het_Parool_),_Bestanddeelnr_926-3740.jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/d/dc/Redactie_bij_krant_%28_Het_Parool_%29%2C_Bestanddeelnr_926-3740.jpg",
|
||||
"license": "CC0",
|
||||
"author": "Rob Croes / Anefo",
|
||||
"title": "Redactie bij krant ( Het Parool ), Bestanddeelnr 926-3740.jpg",
|
||||
"date": "1973-04-27"
|
||||
},
|
||||
"souvenir-redaction-thumb.webp": {
|
||||
"candidate": "redaction-01",
|
||||
"source_page": "https://commons.wikimedia.org/wiki/File:Redactie_bij_krant_(_Het_Parool_),_Bestanddeelnr_926-3740.jpg",
|
||||
"file_url": "https://upload.wikimedia.org/wikipedia/commons/d/dc/Redactie_bij_krant_%28_Het_Parool_%29%2C_Bestanddeelnr_926-3740.jpg",
|
||||
"license": "CC0",
|
||||
"author": "Rob Croes / Anefo",
|
||||
"title": "Redactie bij krant ( Het Parool ), Bestanddeelnr 926-3740.jpg",
|
||||
"date": "1973-04-27"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 48 KiB |
|
After Width: | Height: | Size: 7.3 KiB |
|
After Width: | Height: | Size: 62 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 32 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 3.0 KiB |
|
After Width: | Height: | Size: 43 KiB |
|
After Width: | Height: | Size: 26 KiB |
@@ -59,6 +59,7 @@ import {
|
||||
trackAppEvent,
|
||||
} from './utils/analytics'
|
||||
import './App.css'
|
||||
import './styles/pz.css'
|
||||
|
||||
function yearToIsoDate(value) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
|
||||
@@ -1,4 +1 @@
|
||||
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="400" height="400" rx="59" fill="#F7F1E6"/>
|
||||
<path d="M174.887 159.122C169.851 167.606 163.424 170.911 155.806 170.911C151.516 170.911 146.062 170.294 139.361 168.956L139.214 168.926L139.066 168.904L136.494 168.511C130.481 167.577 124.376 166.512 118.179 165.315C111.028 163.933 103.992 162.551 97.0728 161.169L96.9257 161.139L96.7773 161.117C89.662 160.051 83.4359 159.49 78.1617 159.49C69.1859 159.49 61.0961 161.278 54.0787 164.947C47.0981 168.596 41.4507 173.962 37.0058 180.803L36.9938 180.822L36.9817 180.84C28.1973 194.726 24 213.38 24 236.509V272H51.2117V237.548C51.2117 222.972 53.9416 212.085 58.96 204.553C61.3391 201.15 64.3726 198.736 68.0205 197.145C71.7017 195.54 76.2888 194.653 81.9743 194.653C86.8306 194.653 92.1966 195.123 98.0908 196.096C104.072 197.473 110.278 198.85 116.708 200.225L116.728 200.23L116.75 200.234C123.508 201.63 130.268 202.852 137.031 203.902C144.293 205.343 151.478 206.074 158.579 206.074C164.902 206.074 170.875 204.335 176.414 201.175C181.975 198.003 186.682 193.552 190.634 188.003C194.375 182.751 197.378 176.456 199.706 169.182C202.04 176.425 205.273 182.755 209.461 188.131C218.173 199.364 228.964 206.074 242.115 206.074C248.993 206.074 255.953 205.342 262.986 203.9C269.977 202.849 276.746 201.626 283.292 200.225C289.729 198.848 295.941 197.47 301.929 196.091C307.578 195.123 312.823 194.653 317.68 194.653C323.365 194.653 327.952 195.54 331.633 197.145C335.285 198.738 338.321 201.156 340.701 204.565C345.714 212.097 348.441 222.98 348.441 237.548V272H376V236.509C376 214.043 371.791 195.592 363.04 181.394L363.018 181.359L362.599 180.706C358.225 174.002 352.689 168.723 345.867 165.094C338.785 161.328 330.596 159.49 321.492 159.49C316.218 159.49 309.992 160.051 302.877 161.117L302.708 161.142L302.541 161.177C295.867 162.555 288.96 163.935 281.821 165.315C274.739 166.683 267.776 167.88 260.934 168.904L260.786 168.926L260.639 168.956C253.938 170.294 248.485 170.911 244.194 170.911C236.556 170.911 230.009 167.586 224.748 159.09C219.607 150.411 216.658 138.373 216.205 122.739L216.096 119H183.904L183.796 122.739C183.341 138.378 180.276 150.405 174.92 159.068L174.903 159.095L174.887 159.122Z" fill="#9A734C"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-920.7 -1664.7 1841.3 1841.3"><g transform="scale(1,-1)"><path d="M-373.06825730486446 -21.7549454949945 -404.3987845573672 -69.67222246941051Q-415.45661770530944 -84.41600000000005 -426.51445085325156 -84.41600000000005Q-433.88633961854634 -84.41600000000005 -443.10120057516474 -78.88708342602897Q-590.5389758810602 -1.4822513904338734 -699.2743351691581 129.36877419354835Q-808.009694457256 260.21979977753057 -867.9062906752761 420.5583804226919Q-927.8028868932961 580.8969610678532 -927.8028868932961 743.0785139043383Q-927.8028868932961 905.2600667408232 -868.8277767709379 1065.5986473859843Q-809.8526666485798 1225.9372280311457 -701.1173073604818 1358.6312258064515Q-592.3819480723839 1491.3252235817574 -444.94417276648846 1568.7300556173527Q-437.5722840011937 1572.416 -430.2003952358989 1572.416Q-417.29958989663305 1572.416 -408.08472894001466 1557.6722224694104L-373.06825730486446 1507.9119733036707Q-367.5393407308934 1500.540084538376 -367.5393407308934 1491.3252235817574Q-367.5393407308934 1480.2673904338153 -380.44014607015924 1469.2095572858732Q-441.25822838384113 1421.2922803114573 -496.5473941235519 1341.1229899888767Q-551.8365598632627 1260.953699666296 -596.0678924550314 1161.4332013348167Q-640.2992250468 1061.9127030033371 -666.1008357253318 954.0988298109011Q-691.9024464038635 846.284956618465 -691.9024464038635 743.0785139043383Q-691.9024464038635 638.0290989988877 -666.1008357253318 530.2152258064516Q-640.2992250468 422.40135261401554 -596.0678924550314 322.8808542825361Q-551.8365598632627 223.3603559510567 -496.5473941235519 143.19106562847605Q-441.25822838384113 63.0217753058954 -380.44014607015924 15.104498331479377Q-367.5393407308934 4.046665183537215 -367.5393407308934 -7.011167964404933Q-367.5393407308934 -12.540084538376021 -373.06825730486446 -21.7549454949945Z" fill="#2a2622" stroke="#2a2622" stroke-width="48.1" stroke-linejoin="round"/><path d="M373.06825730486446 -21.7549454949945Q367.5393407308934 -12.540084538376021 367.5393407308934 -7.011167964404933Q367.5393407308934 4.046665183537215 380.44014607015924 15.104498331479377Q441.25822838384113 63.0217753058954 496.5473941235519 143.19106562847605Q551.8365598632627 223.3603559510567 596.0678924550314 322.8808542825361Q640.2992250468 422.40135261401554 666.1008357253318 530.2152258064516Q691.9024464038635 638.0290989988877 691.9024464038635 743.0785139043383Q691.9024464038635 846.284956618465 666.1008357253318 954.0988298109011Q640.2992250468 1061.9127030033371 596.0678924550314 1161.4332013348167Q551.8365598632627 1260.953699666296 496.5473941235519 1341.1229899888767Q441.25822838384113 1421.2922803114573 380.44014607015924 1469.2095572858732Q367.5393407308934 1480.2673904338153 367.5393407308934 1491.3252235817574Q367.5393407308934 1500.540084538376 373.06825730486446 1507.9119733036707L408.08472894001466 1557.6722224694104Q417.2995898966331 1572.416 430.20039523589895 1572.416Q437.57228400119374 1572.416 444.9441727664885 1568.7300556173527Q594.2249202637076 1491.3252235817574 702.0387934561437 1358.6312258064515Q809.8526666485798 1225.9372280311457 868.8277767709379 1065.5986473859843Q927.8028868932961 905.2600667408232 927.8028868932961 743.0785139043383Q927.8028868932961 580.8969610678532 867.9062906752761 420.5583804226919Q808.009694457256 260.21979977753057 699.2743351691581 129.36877419354835Q590.5389758810603 -1.4822513904338734 443.10120057516485 -78.88708342602897Q435.72931180987007 -84.41600000000005 426.51445085325156 -84.41600000000005Q415.45661770530944 -84.41600000000005 404.39878455736726 -69.67222246941051Z" fill="#2a2622" stroke="#2a2622" stroke-width="48.1" stroke-linejoin="round"/><circle cx="0.0" cy="744.0" r="424.3" fill="#93402a"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 3.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-951.9 -1596.5 12857.7 1704.9"><g transform="scale(1,-1)"><path d="M-373.06825730486446 -21.7549454949945 -404.3987845573672 -69.67222246941051Q-415.45661770530944 -84.41600000000005 -426.51445085325156 -84.41600000000005Q-433.88633961854634 -84.41600000000005 -443.10120057516474 -78.88708342602897Q-590.5389758810602 -1.4822513904338734 -699.2743351691581 129.36877419354835Q-808.009694457256 260.21979977753057 -867.9062906752761 420.5583804226919Q-927.8028868932961 580.8969610678532 -927.8028868932961 743.0785139043383Q-927.8028868932961 905.2600667408232 -868.8277767709379 1065.5986473859843Q-809.8526666485798 1225.9372280311457 -701.1173073604818 1358.6312258064515Q-592.3819480723839 1491.3252235817574 -444.94417276648846 1568.7300556173527Q-437.5722840011937 1572.416 -430.2003952358989 1572.416Q-417.29958989663305 1572.416 -408.08472894001466 1557.6722224694104L-373.06825730486446 1507.9119733036707Q-367.5393407308934 1500.540084538376 -367.5393407308934 1491.3252235817574Q-367.5393407308934 1480.2673904338153 -380.44014607015924 1469.2095572858732Q-441.25822838384113 1421.2922803114573 -496.5473941235519 1341.1229899888767Q-551.8365598632627 1260.953699666296 -596.0678924550314 1161.4332013348167Q-640.2992250468 1061.9127030033371 -666.1008357253318 954.0988298109011Q-691.9024464038635 846.284956618465 -691.9024464038635 743.0785139043383Q-691.9024464038635 638.0290989988877 -666.1008357253318 530.2152258064516Q-640.2992250468 422.40135261401554 -596.0678924550314 322.8808542825361Q-551.8365598632627 223.3603559510567 -496.5473941235519 143.19106562847605Q-441.25822838384113 63.0217753058954 -380.44014607015924 15.104498331479377Q-367.5393407308934 4.046665183537215 -367.5393407308934 -7.011167964404933Q-367.5393407308934 -12.540084538376021 -373.06825730486446 -21.7549454949945Z" fill="#2a2622" stroke="#2a2622" stroke-width="48.1" stroke-linejoin="round"/><path d="M373.06825730486446 -21.7549454949945Q367.5393407308934 -12.540084538376021 367.5393407308934 -7.011167964404933Q367.5393407308934 4.046665183537215 380.44014607015924 15.104498331479377Q441.25822838384113 63.0217753058954 496.5473941235519 143.19106562847605Q551.8365598632627 223.3603559510567 596.0678924550314 322.8808542825361Q640.2992250468 422.40135261401554 666.1008357253318 530.2152258064516Q691.9024464038635 638.0290989988877 691.9024464038635 743.0785139043383Q691.9024464038635 846.284956618465 666.1008357253318 954.0988298109011Q640.2992250468 1061.9127030033371 596.0678924550314 1161.4332013348167Q551.8365598632627 1260.953699666296 496.5473941235519 1341.1229899888767Q441.25822838384113 1421.2922803114573 380.44014607015924 1469.2095572858732Q367.5393407308934 1480.2673904338153 367.5393407308934 1491.3252235817574Q367.5393407308934 1500.540084538376 373.06825730486446 1507.9119733036707L408.08472894001466 1557.6722224694104Q417.2995898966331 1572.416 430.20039523589895 1572.416Q437.57228400119374 1572.416 444.9441727664885 1568.7300556173527Q594.2249202637076 1491.3252235817574 702.0387934561437 1358.6312258064515Q809.8526666485798 1225.9372280311457 868.8277767709379 1065.5986473859843Q927.8028868932961 905.2600667408232 927.8028868932961 743.0785139043383Q927.8028868932961 580.8969610678532 867.9062906752761 420.5583804226919Q808.009694457256 260.21979977753057 699.2743351691581 129.36877419354835Q590.5389758810603 -1.4822513904338734 443.10120057516485 -78.88708342602897Q435.72931180987007 -84.41600000000005 426.51445085325156 -84.41600000000005Q415.45661770530944 -84.41600000000005 404.39878455736726 -69.67222246941051Z" fill="#2a2622" stroke="#2a2622" stroke-width="48.1" stroke-linejoin="round"/><circle cx="0.0" cy="744.0" r="424.3" fill="#d2694a"/><path d="M2452.827153953267 1028.7071533203125Q2452.827153953267 1132.94287109375 2412.1582696759233 1205.6658325195312Q2371.4893853985795 1278.3887939453125 2280.298040183736 1316.2964477539062Q2189.106694968892 1354.2041015625 2036.5393121563923 1354.2041015625H1808.8454645001423L1817.5332086407673 1430.0H2242.5770318829545Q2431.920903953267 1430.0 2548.3826349102983 1377.3280639648438Q2664.8443658673295 1324.6561279296875 2718.529058250142 1235.4969482421875Q2772.2137506329545 1146.3377685546875 2772.2137506329545 1036.9683837890625Q2772.2137506329545 925.78369140625 2716.8316095196733 832.493896484375Q2661.449468406392 739.2041015625 2544.487737449361 683.8092041015625Q2427.5260064923295 628.414306640625 2242.5770318829545 628.414306640625H1817.5332086407673L1808.8454645001423 704.210205078125H2122.310796531392Q2218.572027000142 704.210205078125 2293.3460138165483 741.1178588867188Q2368.1200006329545 778.0255126953125 2410.473577293111 850.4459228515625Q2452.827153953267 922.8663330078125 2452.827153953267 1028.7071533203125ZM1990.6025445782673 1430.0V76.9234619140625L2155.386235984517 32.5540771484375V0.0H1500.5598199688923V32.5540771484375L1665.3435113751423 76.9234619140625V1354.0765380859375L1500.5598199688923 1397.05102539Line truncated
|
||||
|
After Width: | Height: | Size: 22 KiB |
@@ -1,4 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import logoUrl from '../assets/parenthese-logo.svg?url'
|
||||
import './AccountScreens.css'
|
||||
|
||||
const AccessGate = ({
|
||||
initialTreeId,
|
||||
@@ -31,84 +33,100 @@ const AccessGate = ({
|
||||
const ownerLabel = treeOwnerName || ''
|
||||
|
||||
return (
|
||||
<div className="access-gate">
|
||||
<div className="access-gate-mock-blur" aria-hidden="true">
|
||||
<div className="access-gate-mock-card access-gate-mock-card-main" />
|
||||
<div className="access-gate-mock-card access-gate-mock-card-side" />
|
||||
<div className="access-gate-mock-dot" />
|
||||
</div>
|
||||
<div className="access-gate pz-screen">
|
||||
<div className="pz-screen-inner">
|
||||
<img src={logoUrl} alt="Parenthèse" className="pz-screen-logo" />
|
||||
|
||||
<form className="access-gate-card" onSubmit={handleSubmit}>
|
||||
<h1>Accès à votre arbre</h1>
|
||||
{treeNotFound ? (
|
||||
<p>Cet arbre n'existe pas ou n'est plus disponible.</p>
|
||||
) : (
|
||||
<p>
|
||||
{treeName
|
||||
? `Entrez le mot de passe partagé pour ouvrir "${treeName}".`
|
||||
: "Entrez les informations reçues avec votre lien de partage pour ouvrir l'arbre familial."}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!treeNotFound && (treeName || treeDescription || ownerLabel) && (
|
||||
<div className="access-gate-tree-info">
|
||||
{treeName && <div className="access-gate-tree-name">{treeName}</div>}
|
||||
{ownerLabel && <div className="access-gate-tree-meta">Partagé par : {ownerLabel}</div>}
|
||||
{treeDescription && <div className="access-gate-tree-desc">{treeDescription}</div>}
|
||||
<form className="pz-card pz-screen-card" onSubmit={handleSubmit}>
|
||||
<div className="pz-auth-head">
|
||||
<p className="pz-eyebrow">Arbre partagé</p>
|
||||
{treeNotFound ? (
|
||||
<h1 className="pz-title">Cet arbre est <em>introuvable</em></h1>
|
||||
) : (
|
||||
<h1 className="pz-title">
|
||||
{treeName ? <>Ouvrir <em>{treeName}</em></> : <>Ouvrir un arbre <em>partagé</em></>}
|
||||
</h1>
|
||||
)}
|
||||
<p className="pz-sub">
|
||||
{treeNotFound
|
||||
? "Le lien est peut-être incomplet, ou l'arbre a été supprimé. Demandez un nouveau lien à la personne qui vous l'a envoyé."
|
||||
: treeName
|
||||
? 'Entrez le mot de passe reçu avec le lien. Pas besoin de compte pour regarder.'
|
||||
: "Entrez le code et le mot de passe reçus avec votre lien de partage."}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!treeNotFound && (
|
||||
<>
|
||||
<label htmlFor="treeId">Code de l'arbre partagé</label>
|
||||
<input
|
||||
id="treeId"
|
||||
type="text"
|
||||
value={treeId}
|
||||
onChange={(event) => setTreeId(event.target.value)}
|
||||
disabled={Boolean(initialTreeId) || loading}
|
||||
placeholder="Ex: cm..."
|
||||
required
|
||||
/>
|
||||
{!treeNotFound && (ownerLabel || treeDescription) && (
|
||||
<div className="pz-gate-owner">
|
||||
{ownerLabel && (
|
||||
<span className="pz-gate-mono" aria-hidden="true">{ownerLabel.trim().charAt(0).toUpperCase()}</span>
|
||||
)}
|
||||
<div className="pz-gate-owner-text">
|
||||
{ownerLabel && <p className="access-gate-tree-meta pz-small">Partagé par <strong>{ownerLabel}</strong></p>}
|
||||
{treeDescription && <p className="pz-small">{treeDescription}</p>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label htmlFor="sharePassword">Mot de passe de partage</label>
|
||||
<input
|
||||
id="sharePassword"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
disabled={loading}
|
||||
placeholder="********"
|
||||
required
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!treeNotFound && (
|
||||
<div className="pz-auth-form">
|
||||
{!initialTreeId && (
|
||||
<div className="pz-field">
|
||||
<label htmlFor="treeId">Code de l'arbre</label>
|
||||
<input
|
||||
id="treeId"
|
||||
type="text"
|
||||
value={treeId}
|
||||
onChange={(event) => setTreeId(event.target.value)}
|
||||
disabled={loading}
|
||||
placeholder="Il commence souvent par cm"
|
||||
autoComplete="off"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{errorMessage && <div className="access-gate-error">{errorMessage}</div>}
|
||||
<div className="pz-field">
|
||||
<label htmlFor="sharePassword">Mot de passe de partage</label>
|
||||
<input
|
||||
id="sharePassword"
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
disabled={loading}
|
||||
autoFocus={Boolean(initialTreeId)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!treeNotFound && (
|
||||
<button type="submit" disabled={loading}>
|
||||
{loading ? 'Connexion...' : "Ouvrir l'arbre"}
|
||||
</button>
|
||||
)}
|
||||
{errorMessage && <div className="access-gate-error pz-error" role="alert">{errorMessage}</div>}
|
||||
|
||||
{treeNotFound && onUseAccount && (
|
||||
<button type="button" className="ghost" onClick={() => onUseAccount('register')} disabled={loading}>
|
||||
Créer mon arbre
|
||||
</button>
|
||||
)}
|
||||
{!treeNotFound && (
|
||||
<button type="submit" className="pz-btn pz-btn--primary pz-btn--block" disabled={loading}>
|
||||
{loading ? 'Ouverture…' : "Ouvrir l'arbre"}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{treeNotFound && onUseAccount && (
|
||||
<button type="button" className="pz-btn pz-btn--primary pz-btn--block" onClick={() => onUseAccount('register')} disabled={loading}>
|
||||
Créer mon arbre
|
||||
</button>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{onUseAccount && !treeNotFound && (
|
||||
<>
|
||||
<button type="button" className="ghost" onClick={() => onUseAccount('login')} disabled={loading}>
|
||||
<div className="pz-account-foot">
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--sm" onClick={() => onUseAccount('login')} disabled={loading}>
|
||||
Se connecter avec un compte
|
||||
</button>
|
||||
<button type="button" className="ghost" onClick={() => onUseAccount('register')} disabled={loading}>
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--sm" onClick={() => onUseAccount('register')} disabled={loading}>
|
||||
Créer un compte
|
||||
</button>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { ChevronRight, KeyRound, LogOut, Plus, Sparkles, X } from 'lucide-react'
|
||||
import logoUrl from '../assets/parenthese-logo.svg?url'
|
||||
import './AccountScreens.css'
|
||||
|
||||
const AccountDashboard = ({
|
||||
authenticated,
|
||||
@@ -87,161 +90,205 @@ const AccountDashboard = ({
|
||||
|
||||
if (!authenticated) {
|
||||
return (
|
||||
<div className="account-dashboard">
|
||||
<div className="account-dashboard-card auth-only">
|
||||
<h1>Parenthèse</h1>
|
||||
<p className="account-subtitle">
|
||||
{isRegister ? 'Créez votre compte.' : 'Votre histoire familiale, vivante et partagée.'}
|
||||
</p>
|
||||
{pendingTreeName && (
|
||||
<p className="account-pending-tree">
|
||||
Vous reviendrez ensuite sur l'arbre « {pendingTreeName} », rattaché à votre compte.
|
||||
</p>
|
||||
)}
|
||||
<div className="account-dashboard pz-screen">
|
||||
<div className="pz-screen-inner">
|
||||
<img src={logoUrl} alt="Parenthèse" className="pz-screen-logo" />
|
||||
|
||||
<form className="account-form" onSubmit={handleAuthSubmit}>
|
||||
{isRegister && (
|
||||
<>
|
||||
<label htmlFor="accountFirstName">Prénom</label>
|
||||
<input
|
||||
id="accountFirstName"
|
||||
type="text"
|
||||
autoComplete="given-name"
|
||||
value={firstName}
|
||||
maxLength={100}
|
||||
onChange={(event) => setFirstName(event.target.value)}
|
||||
required
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="pz-card pz-screen-card pz-auth">
|
||||
<div className="pz-auth-head">
|
||||
<h1 className="pz-title">{isRegister ? <>Créez votre <em>compte</em></> : <>Retrouvez votre <em>famille</em></>}</h1>
|
||||
<p className="pz-sub">
|
||||
{isRegister
|
||||
? 'Gratuit, et ça le restera. Un prénom, un email, un mot de passe, et vous pouvez commencer.'
|
||||
: 'Connectez-vous pour ouvrir vos arbres et ajouter des souvenirs.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label htmlFor="accountEmail">Email</label>
|
||||
<input
|
||||
id="accountEmail"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
required
|
||||
/>
|
||||
|
||||
<label htmlFor="accountPassword">Mot de passe</label>
|
||||
<input
|
||||
id="accountPassword"
|
||||
type="password"
|
||||
autoComplete={isRegister ? 'new-password' : 'current-password'}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
minLength={8}
|
||||
required
|
||||
/>
|
||||
{isRegister && <p className="account-hint">8 caractères minimum.</p>}
|
||||
|
||||
{errorMessage && <div className="account-error">{errorMessage}</div>}
|
||||
|
||||
<button type="submit" disabled={!canSubmitAuth}>
|
||||
{loading ? 'Traitement...' : isRegister ? 'Créer mon compte' : 'Se connecter'}
|
||||
</button>
|
||||
{isRegister && (
|
||||
<p className="account-hint">
|
||||
En créant un compte, vous acceptez{' '}
|
||||
<a href="https://parenthese.io/donnees-et-vie-privee/" target="_blank" rel="noopener noreferrer">
|
||||
la façon dont Parenthèse traite vos données
|
||||
</a>
|
||||
.
|
||||
{pendingTreeName && (
|
||||
<p className="account-pending-tree pz-auth-note">
|
||||
Vous reviendrez ensuite sur l'arbre « {pendingTreeName} », rattaché à votre compte.
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<div className="account-divider">ou</div>
|
||||
<form className="pz-auth-form" onSubmit={handleAuthSubmit}>
|
||||
{isRegister && (
|
||||
<div className="pz-field">
|
||||
<label htmlFor="accountFirstName">Prénom</label>
|
||||
<input
|
||||
id="accountFirstName"
|
||||
type="text"
|
||||
autoComplete="given-name"
|
||||
placeholder="Camille"
|
||||
value={firstName}
|
||||
maxLength={100}
|
||||
onChange={(event) => setFirstName(event.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="pz-field">
|
||||
<label htmlFor="accountEmail">Email</label>
|
||||
<input
|
||||
id="accountEmail"
|
||||
type="email"
|
||||
autoComplete="email"
|
||||
placeholder="camille@exemple.fr"
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="pz-field">
|
||||
<label htmlFor="accountPassword">Mot de passe</label>
|
||||
<input
|
||||
id="accountPassword"
|
||||
type="password"
|
||||
autoComplete={isRegister ? 'new-password' : 'current-password'}
|
||||
value={password}
|
||||
onChange={(event) => setPassword(event.target.value)}
|
||||
minLength={8}
|
||||
required
|
||||
/>
|
||||
{isRegister && <p className="pz-hint">8 caractères minimum.</p>}
|
||||
</div>
|
||||
|
||||
{errorMessage && <div className="account-error pz-error" role="alert">{errorMessage}</div>}
|
||||
|
||||
<button type="submit" className="pz-btn pz-btn--primary pz-btn--block" disabled={!canSubmitAuth}>
|
||||
{loading ? 'Un instant…' : isRegister ? 'Créer mon compte' : 'Se connecter'}
|
||||
</button>
|
||||
|
||||
{isRegister && (
|
||||
<p className="pz-small pz-auth-legal">
|
||||
En créant un compte, vous acceptez{' '}
|
||||
<a href="https://parenthese.io/donnees-et-vie-privee/" target="_blank" rel="noopener noreferrer">
|
||||
la façon dont Parenthèse traite vos données
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
</form>
|
||||
|
||||
<div className="pz-divider">ou</div>
|
||||
|
||||
<div className="account-secondary-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="account-secondary-btn account-signup-toggle"
|
||||
onClick={() => setMode(isRegister ? 'login' : 'register')}
|
||||
disabled={loading}
|
||||
>
|
||||
{isRegister ? "J'ai déjà un compte" : 'Créer un compte'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="account-secondary-btn account-shared-link"
|
||||
className="pz-btn pz-btn--secondary pz-btn--block account-shared-link"
|
||||
onClick={onBackToAccess}
|
||||
disabled={loading}
|
||||
>
|
||||
Accéder avec un lien partagé
|
||||
<KeyRound size={16} aria-hidden="true" />
|
||||
Ouvrir un arbre partagé avec moi
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="pz-auth-switch pz-small">
|
||||
{isRegister ? 'Vous avez déjà un compte ?' : 'Pas encore de compte ?'}{' '}
|
||||
<button
|
||||
type="button"
|
||||
className="pz-link account-signup-toggle"
|
||||
onClick={() => setMode(isRegister ? 'login' : 'register')}
|
||||
disabled={loading}
|
||||
>
|
||||
{isRegister ? 'Me connecter' : 'Créer un compte'}
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="account-dashboard">
|
||||
<div className="account-dashboard-card">
|
||||
<h1>Votre compte</h1>
|
||||
<p>Choisissez un arbre ou créez-en un nouveau.</p>
|
||||
<div className="account-dashboard pz-screen">
|
||||
<div className="pz-screen-inner">
|
||||
<img src={logoUrl} alt="Parenthèse" className="pz-screen-logo" />
|
||||
|
||||
{errorMessage && <div className="account-error">{errorMessage}</div>}
|
||||
<div className="pz-card pz-screen-card">
|
||||
<div className="pz-auth-head">
|
||||
<h1 className="pz-title">Vos <em>arbres</em></h1>
|
||||
<p className="pz-sub">Ouvrez un arbre, ou commencez celui d'une autre branche de la famille.</p>
|
||||
</div>
|
||||
|
||||
<div className="tree-list">
|
||||
{trees.length === 0 ? (
|
||||
<div className="tree-empty">Aucun arbre pour le moment.</div>
|
||||
) : (
|
||||
trees.map((tree) => (
|
||||
<button
|
||||
key={tree.id}
|
||||
type="button"
|
||||
className={`tree-item ${activeTreeId === tree.id ? 'active' : ''}`}
|
||||
onClick={() => onOpenTree(tree.id)}
|
||||
>
|
||||
<span className="tree-name">{tree.name}</span>
|
||||
{errorMessage && <div className="account-error pz-error" role="alert">{errorMessage}</div>}
|
||||
|
||||
<div className="tree-list pz-tree-list">
|
||||
{trees.length === 0 ? (
|
||||
<div className="tree-empty pz-tree-empty">
|
||||
<p className="pz-sub">Aucun arbre pour le moment.</p>
|
||||
<p className="pz-small">Commencez par le vôtre, il suffit d'un prénom.</p>
|
||||
</div>
|
||||
) : (
|
||||
trees.map((tree) => {
|
||||
const isActive = activeTreeId === tree.id
|
||||
return (
|
||||
<button
|
||||
key={tree.id}
|
||||
type="button"
|
||||
className={`tree-item pz-tree-item ${isActive ? 'active' : ''}`}
|
||||
onClick={() => onOpenTree(tree.id)}
|
||||
>
|
||||
<span className="pz-tree-mono" aria-hidden="true">{(tree.name || '?').trim().charAt(0).toUpperCase()}</span>
|
||||
<span className="tree-name pz-tree-name">{tree.name}</span>
|
||||
{isActive && <span className="pz-tag pz-tag--accent">Ouvert</span>}
|
||||
<ChevronRight size={18} aria-hidden="true" className="pz-tree-chevron" />
|
||||
</button>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button type="button" className="pz-btn pz-btn--primary pz-btn--block" onClick={onStartTreeWizard} disabled={loading}>
|
||||
<Plus size={17} aria-hidden="true" />
|
||||
Créer un nouvel arbre
|
||||
</button>
|
||||
|
||||
<div className="account-actions pz-account-actions">
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--sm" onClick={onBackToAccess} disabled={loading}>
|
||||
<KeyRound size={15} aria-hidden="true" />
|
||||
Accès par mot de passe partagé
|
||||
</button>
|
||||
{onUseDemo && (
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--sm" onClick={onUseDemo} disabled={loading}>
|
||||
<Sparkles size={15} aria-hidden="true" />
|
||||
Essayer la démo
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" onClick={onStartTreeWizard} disabled={loading}>
|
||||
Créer un nouvel arbre
|
||||
</button>
|
||||
|
||||
<div className="account-actions">
|
||||
<button type="button" className="ghost" onClick={onLogout} disabled={loading}>
|
||||
<div className="pz-account-foot">
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--sm" onClick={onLogout} disabled={loading}>
|
||||
<LogOut size={15} aria-hidden="true" />
|
||||
Déconnexion
|
||||
</button>
|
||||
<button type="button" className="ghost" onClick={onBackToAccess} disabled={loading}>
|
||||
Accès par mot de passe partagé
|
||||
</button>
|
||||
{onUseDemo && (
|
||||
<button type="button" className="ghost" onClick={onUseDemo} disabled={loading}>
|
||||
Essayer la démo
|
||||
{onDeleteAccount && (
|
||||
<button type="button" className="pz-btn pz-btn--danger-ghost pz-btn--sm account-delete-link" onClick={openDeleteModal} disabled={loading}>
|
||||
Supprimer mon compte
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{onDeleteAccount && (
|
||||
<div className="account-delete-zone">
|
||||
<button type="button" className="account-delete-link" onClick={openDeleteModal} disabled={loading}>
|
||||
Supprimer mon compte
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{deleteModalOpen && (
|
||||
<div className="settings-delete-modal-overlay" onClick={closeDeleteModal}>
|
||||
<div className="pz-overlay" onClick={closeDeleteModal}>
|
||||
<div
|
||||
className="settings-delete-modal account-delete-modal"
|
||||
className="pz-modal account-delete-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="accountDeleteTitle"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<h3 id="accountDeleteTitle">Supprimer mon compte</h3>
|
||||
<p>Cette suppression est définitive, elle ne pourra pas être annulée.</p>
|
||||
<ul className="account-delete-list">
|
||||
<div className="pz-modal-head">
|
||||
<p className="pz-eyebrow">Compte</p>
|
||||
<h3 id="accountDeleteTitle" className="pz-title">Supprimer mon compte</h3>
|
||||
<p className="pz-sub">Cette suppression est définitive, elle ne pourra pas être annulée.</p>
|
||||
</div>
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--icon pz-modal-close" onClick={closeDeleteModal} aria-label="Fermer" disabled={deleting}>
|
||||
<X size={18} aria-hidden="true" />
|
||||
</button>
|
||||
<ul className="pz-delete-list">
|
||||
<li>Votre compte et votre adresse email sont effacés.</li>
|
||||
<li>
|
||||
Les arbres que vous avez créés sont supprimés avec toutes leurs personnes et tous leurs médias,
|
||||
@@ -250,27 +297,29 @@ const AccountDashboard = ({
|
||||
<li>Les arbres partagés avec vous restent à leurs propriétaires. Vous n'y aurez simplement plus accès.</li>
|
||||
</ul>
|
||||
|
||||
<form className="account-delete-form" onSubmit={handleDeleteSubmit}>
|
||||
<label htmlFor="accountDeletePassword">Mot de passe actuel</label>
|
||||
<input
|
||||
id="accountDeletePassword"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
value={deletePassword}
|
||||
onChange={(event) => setDeletePassword(event.target.value)}
|
||||
disabled={deleting}
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
<form className="pz-auth-form" onSubmit={handleDeleteSubmit}>
|
||||
<div className="pz-field">
|
||||
<label htmlFor="accountDeletePassword">Mot de passe actuel</label>
|
||||
<input
|
||||
id="accountDeletePassword"
|
||||
type="password"
|
||||
autoComplete="current-password"
|
||||
value={deletePassword}
|
||||
onChange={(event) => setDeletePassword(event.target.value)}
|
||||
disabled={deleting}
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
{deleteError && <div className="account-error">{deleteError}</div>}
|
||||
{deleteError && <div className="account-error pz-error" role="alert">{deleteError}</div>}
|
||||
|
||||
<div className="settings-delete-actions">
|
||||
<button type="button" className="ghost" onClick={closeDeleteModal} disabled={deleting}>
|
||||
<div className="pz-modal-actions">
|
||||
<button type="button" className="pz-btn pz-btn--ghost" onClick={closeDeleteModal} disabled={deleting}>
|
||||
Annuler
|
||||
</button>
|
||||
<button type="submit" className="danger-action" disabled={deleting || !deletePassword}>
|
||||
{deleting ? 'Suppression...' : 'Supprimer définitivement'}
|
||||
<button type="submit" className="pz-btn pz-btn--danger" disabled={deleting || !deletePassword}>
|
||||
{deleting ? 'Suppression…' : 'Supprimer définitivement'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { X } from 'lucide-react'
|
||||
import './AccountScreens.css'
|
||||
|
||||
const AccountPanel = ({
|
||||
visible,
|
||||
@@ -28,52 +30,49 @@ const AccountPanel = ({
|
||||
return null
|
||||
}
|
||||
|
||||
const displayName = linkedPerson
|
||||
? `${linkedPerson.firstName || ''} ${linkedPerson.lastName || ''}`.trim()
|
||||
: userDisplayName || 'Mon compte'
|
||||
|
||||
return (
|
||||
<div className="account-overlay account-overlay--open">
|
||||
<div className="account-card">
|
||||
<div className="account-header">
|
||||
<div className="account-header-text">
|
||||
<h1>Compte</h1>
|
||||
{userEmail && <p>{userEmail}</p>}
|
||||
</div>
|
||||
<button type="button" className="ghost" onClick={onClose}>Fermer</button>
|
||||
<div className="pz-overlay account-overlay account-overlay--open" onClick={onClose}>
|
||||
<div className="pz-modal account-card" role="dialog" aria-modal="true" aria-labelledby="accountPanelTitle" onClick={(event) => event.stopPropagation()}>
|
||||
<div className="pz-modal-head">
|
||||
<p className="pz-eyebrow">Mon compte</p>
|
||||
<h1 id="accountPanelTitle" className="pz-title">{displayName}</h1>
|
||||
{userEmail && <p className="pz-sub">{userEmail}</p>}
|
||||
</div>
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--icon pz-modal-close" onClick={onClose} aria-label="Fermer">
|
||||
<X size={18} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<div className="ap-linked">
|
||||
<span className="ap-avatar" aria-hidden="true">
|
||||
{linkedPerson?.photo ? <img src={linkedPerson.photo} alt="" /> : (linkedPerson?.firstName || userDisplayName || 'C').charAt(0).toUpperCase()}
|
||||
</span>
|
||||
<p className="pz-small">
|
||||
{linkedPerson
|
||||
? 'Vous apparaissez dans cet arbre sous ce nom et avec cette photo.'
|
||||
: "Dites-nous qui vous êtes dans cet arbre, votre nom et votre photo viendront de votre fiche."}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="account-linked">
|
||||
<h2>Profil lié dans cet arbre</h2>
|
||||
<div className="account-linked-person">
|
||||
<div className="account-linked-avatar">
|
||||
{linkedPerson?.photo ? (
|
||||
<img src={linkedPerson.photo} alt="" />
|
||||
) : (
|
||||
<span>{(linkedPerson?.firstName || userDisplayName || 'C').charAt(0).toUpperCase()}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="account-linked-meta">
|
||||
<strong>
|
||||
{linkedPerson ? `${linkedPerson.firstName || ''} ${linkedPerson.lastName || ''}`.trim() : userDisplayName || 'Compte'}
|
||||
</strong>
|
||||
<span>
|
||||
{linkedPerson
|
||||
? 'Cette personne est utilisée pour votre nom et photo de profil.'
|
||||
: 'Aucune personne liée pour le moment.'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ap-pick">
|
||||
<div className="pz-field">
|
||||
<label htmlFor="accountPanelSearch">Je suis cette personne</label>
|
||||
<input
|
||||
id="accountPanelSearch"
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Chercher votre prénom"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="account-pick">
|
||||
<h2>Je suis cette personne</h2>
|
||||
<input
|
||||
type="text"
|
||||
value={query}
|
||||
onChange={(event) => setQuery(event.target.value)}
|
||||
placeholder="Rechercher une personne..."
|
||||
/>
|
||||
|
||||
<div className="account-person-list">
|
||||
<div className="ap-list">
|
||||
{filteredPeople.length === 0 ? (
|
||||
<div className="account-empty">Aucun résultat.</div>
|
||||
<p className="pz-small ap-empty">Personne ne correspond à cette recherche.</p>
|
||||
) : (
|
||||
filteredPeople.map((person) => {
|
||||
const isSelected = String(selectedPersonId) === String(person.id)
|
||||
@@ -81,30 +80,29 @@ const AccountPanel = ({
|
||||
<button
|
||||
key={person.id}
|
||||
type="button"
|
||||
className={`account-person-item ${isSelected ? 'active' : ''}`}
|
||||
className={`ap-item ${isSelected ? 'is-active' : ''}`}
|
||||
onClick={() => onSelectPerson?.(person.id)}
|
||||
>
|
||||
<div className="account-person-avatar">
|
||||
{person.photo ? <img src={person.photo} alt="" /> : <span>{(person.firstName || '?').charAt(0)}</span>}
|
||||
</div>
|
||||
<div className="account-person-name">
|
||||
{person.firstName} {person.lastName || ''}
|
||||
</div>
|
||||
{isSelected && <span className="account-person-tag">Lié</span>}
|
||||
<span className="ap-item-avatar" aria-hidden="true">
|
||||
{person.photo ? <img src={person.photo} alt="" /> : (person.firstName || '?').charAt(0)}
|
||||
</span>
|
||||
<span className="ap-item-name">{person.firstName} <em>{person.lastName || ''}</em></span>
|
||||
{isSelected && <span className="pz-tag pz-tag--accent">C'est moi</span>}
|
||||
</button>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedPersonId && (
|
||||
<button type="button" className="ghost" onClick={() => onSelectPerson?.(null)}>
|
||||
Retirer la liaison
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedPersonId && (
|
||||
<div className="pz-modal-actions">
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--sm" onClick={() => onSelectPerson?.(null)}>
|
||||
Ce n'est plus moi
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button type="button" className="account-backdrop" aria-label="Fermer" onClick={onClose} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/* Accueil, connexion, création de compte, liste des arbres (DA terracotta) */
|
||||
|
||||
.pz-auth-head { display: grid; gap: 8px; }
|
||||
.pz-auth-form { display: grid; gap: 16px; }
|
||||
.pz-auth-legal { text-align: center; }
|
||||
.pz-auth-switch { text-align: center; }
|
||||
.pz-auth-switch .pz-link { font-size: 13.5px; }
|
||||
|
||||
.pz-auth-note {
|
||||
margin: 0; padding: 12px 14px; border-radius: var(--pz-r-s);
|
||||
background: var(--pz-a50); border: 1px solid var(--pz-a100);
|
||||
font: 400 13.5px/1.5 var(--pz-sans); color: var(--pz-ink-2);
|
||||
}
|
||||
|
||||
/* Liste des arbres */
|
||||
.pz-tree-list { display: grid; gap: 8px; }
|
||||
.pz-tree-item {
|
||||
display: flex; align-items: center; gap: 12px; width: 100%; padding: 10px 12px 10px 10px;
|
||||
border: 1px solid var(--pz-line); border-radius: var(--pz-r-m); background: var(--pz-surface);
|
||||
cursor: pointer; text-align: left; transition: border-color 160ms ease, background-color 160ms ease, transform 140ms var(--pz-ease);
|
||||
}
|
||||
.pz-tree-item:active { transform: scale(.99); }
|
||||
.pz-tree-item.active { border-color: var(--pz-a300); background: var(--pz-a50); }
|
||||
@media (hover: hover) and (pointer: fine) { .pz-tree-item:hover { border-color: var(--pz-a300); } }
|
||||
.pz-tree-mono {
|
||||
width: 40px; height: 40px; flex: none; display: grid; place-items: center; border-radius: 12px;
|
||||
background: var(--pz-a100); color: var(--pz-a700); font: 400 20px/1 var(--pz-serif);
|
||||
}
|
||||
.pz-tree-item .pz-tree-name { flex: 1; min-width: 0; font: 400 19px/1.2 var(--pz-serif); color: var(--pz-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.pz-tree-chevron { color: var(--pz-faint); flex: none; }
|
||||
.pz-tree-empty { display: grid; gap: 4px; padding: 20px; text-align: center; border: 1px dashed var(--pz-a300); border-radius: var(--pz-r-m); background: var(--pz-a50); }
|
||||
|
||||
.pz-account-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 4px; }
|
||||
.pz-account-foot { display: flex; justify-content: center; flex-wrap: wrap; gap: 4px; }
|
||||
|
||||
.pz-delete-list { margin: 0; padding-left: 18px; display: grid; gap: 8px; font: 400 14px/1.5 var(--pz-sans); color: var(--pz-ink-2); }
|
||||
|
||||
/* Arbre partagé (mot de passe) */
|
||||
.pz-gate-owner { display: flex; gap: 12px; align-items: center; padding: 12px 14px; border-radius: var(--pz-r-m); background: var(--pz-a50); border: 1px solid var(--pz-a100); }
|
||||
.pz-gate-mono { width: 36px; height: 36px; flex: none; display: grid; place-items: center; border-radius: 50%; background: var(--pz-a100); color: var(--pz-a700); font: 400 18px/1 var(--pz-serif); }
|
||||
.pz-gate-owner-text { display: grid; gap: 2px; min-width: 0; }
|
||||
.pz-gate-owner-text strong { color: var(--pz-ink); font-weight: 500; }
|
||||
|
||||
/* Fenêtre « Mon compte » (personne liée) */
|
||||
.ap-linked { display: flex; gap: 12px; align-items: center; padding: 12px 14px; border-radius: var(--pz-r-m); background: var(--pz-a50); border: 1px solid var(--pz-a100); }
|
||||
.ap-avatar { width: 44px; height: 44px; flex: none; display: grid; place-items: center; overflow: hidden; border-radius: 50%; background: var(--pz-a100); color: var(--pz-a700); font: 400 20px/1 var(--pz-serif); }
|
||||
.ap-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.ap-pick { display: grid; gap: 10px; }
|
||||
.ap-list { display: grid; gap: 2px; max-height: 280px; overflow-y: auto; padding: 4px; border: 1px solid var(--pz-line); border-radius: var(--pz-r-m); scrollbar-width: thin; scrollbar-color: var(--pz-line) transparent; }
|
||||
.ap-empty { padding: 14px; text-align: center; }
|
||||
.ap-item { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 44px; padding: 6px 8px; border: 0; border-radius: 10px; background: none; cursor: pointer; text-align: left; }
|
||||
.ap-item.is-active { background: var(--pz-a50); }
|
||||
@media (hover: hover) and (pointer: fine) { .ap-item:hover { background: var(--pz-line-soft); } .ap-item.is-active:hover { background: var(--pz-a100); } }
|
||||
.ap-item-avatar { width: 32px; height: 32px; flex: none; display: grid; place-items: center; overflow: hidden; border-radius: 50%; background: var(--pz-a100); color: var(--pz-a700); font: 400 14px/1 var(--pz-serif); }
|
||||
.ap-item-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.ap-item-name { flex: 1; min-width: 0; font: 400 16px/1.2 var(--pz-serif); color: var(--pz-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.ap-item-name em { font-style: italic; font-weight: 300; }
|
||||
@@ -8,14 +8,14 @@
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
|
||||
background: rgba(247, 236, 217, 0.95);
|
||||
background: rgba(242, 239, 234, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 2px solid rgba(166, 124, 82, 0.3);
|
||||
border: 2px solid rgba(147, 64, 42, 0.3);
|
||||
|
||||
box-shadow:
|
||||
0 8px 32px rgba(93, 82, 75, 0.25),
|
||||
0 2px 8px rgba(93, 82, 75, 0.12);
|
||||
0 8px 32px rgba(42, 38, 34, 0.25),
|
||||
0 2px 8px rgba(42, 38, 34, 0.12);
|
||||
|
||||
cursor: grab;
|
||||
user-select: none;
|
||||
@@ -35,18 +35,18 @@
|
||||
.add-person-button:hover {
|
||||
transform: scale(1.12);
|
||||
box-shadow:
|
||||
0 12px 40px rgba(166, 124, 82, 0.35),
|
||||
0 4px 12px rgba(93, 82, 75, 0.18);
|
||||
border-color: rgba(166, 124, 82, 0.5);
|
||||
0 12px 40px rgba(147, 64, 42, 0.35),
|
||||
0 4px 12px rgba(42, 38, 34, 0.18);
|
||||
border-color: rgba(147, 64, 42, 0.5);
|
||||
}
|
||||
|
||||
.add-person-button.dragging {
|
||||
cursor: grabbing;
|
||||
transform: scale(1.2) rotate(5deg);
|
||||
box-shadow:
|
||||
0 16px 48px rgba(166, 124, 82, 0.45),
|
||||
0 6px 16px rgba(93, 82, 75, 0.25);
|
||||
border-color: rgba(166, 124, 82, 0.7);
|
||||
0 16px 48px rgba(147, 64, 42, 0.45),
|
||||
0 6px 16px rgba(42, 38, 34, 0.25);
|
||||
border-color: rgba(147, 64, 42, 0.7);
|
||||
z-index: 1100;
|
||||
}
|
||||
|
||||
@@ -54,9 +54,9 @@
|
||||
.add-person-button-icon {
|
||||
font-size: 32px;
|
||||
font-weight: 300;
|
||||
color: #A67C52;
|
||||
color: #93402A;
|
||||
line-height: 1;
|
||||
filter: drop-shadow(0 1px 3px rgba(93, 82, 75, 0.15));
|
||||
filter: drop-shadow(0 1px 3px rgba(42, 38, 34, 0.15));
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -95,8 +95,8 @@
|
||||
transform: translateY(-50%);
|
||||
|
||||
padding: 8px 14px;
|
||||
background: rgba(93, 82, 75, 0.95);
|
||||
color: #FEF9ED;
|
||||
background: rgba(42, 38, 34, 0.95);
|
||||
color: #FFFFFF;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
border-radius: 8px;
|
||||
@@ -106,7 +106,7 @@
|
||||
pointer-events: none;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0 4px 12px rgba(42, 38, 34, 0.2);
|
||||
}
|
||||
|
||||
.add-person-button:hover .add-person-button-tooltip {
|
||||
@@ -126,18 +126,18 @@
|
||||
z-index: 1050;
|
||||
|
||||
padding: 10px 18px;
|
||||
background: rgba(247, 236, 217, 0.98);
|
||||
background: rgba(242, 239, 234, 0.98);
|
||||
backdrop-filter: blur(24px);
|
||||
border-radius: 50px;
|
||||
border: 1px solid rgba(166, 124, 82, 0.2);
|
||||
border: 1px solid rgba(147, 64, 42, 0.2);
|
||||
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #5D524B;
|
||||
color: #2A2622;
|
||||
|
||||
box-shadow:
|
||||
0 6px 24px rgba(93, 82, 75, 0.18),
|
||||
0 2px 8px rgba(93, 82, 75, 0.08);
|
||||
0 6px 24px rgba(42, 38, 34, 0.18),
|
||||
0 2px 8px rgba(42, 38, 34, 0.08);
|
||||
|
||||
animation: hintSlideDown 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
@@ -1,413 +1,106 @@
|
||||
/* ============================================================
|
||||
Add Person Panel - Slide-in gauche ajout de personne
|
||||
400px desktop, 90% mobile
|
||||
Ajouter une personne (DA terracotta)
|
||||
Panneau flottant à gauche, de la même famille que la fiche.
|
||||
Sur téléphone, panneau qui monte par le bas.
|
||||
============================================================ */
|
||||
|
||||
.add-person-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 400px;
|
||||
top: 24px;
|
||||
left: 24px;
|
||||
bottom: 24px;
|
||||
z-index: 1050;
|
||||
|
||||
background: rgba(254, 249, 237, 0.98);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border-right: 1px solid rgba(166, 124, 82, 0.15);
|
||||
box-shadow: 6px 0 32px rgba(93, 82, 75, 0.15);
|
||||
|
||||
transform: translateX(-100%);
|
||||
transition: transform 0.35s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
|
||||
width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
gap: 18px;
|
||||
padding: 22px 20px 18px;
|
||||
background: var(--pz-surface);
|
||||
border: 1px solid var(--pz-line);
|
||||
border-radius: var(--pz-r-l);
|
||||
box-shadow: var(--pz-shadow-lg);
|
||||
transform: translateX(calc(-100% - 40px));
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: transform 320ms var(--pz-ease), opacity 220ms ease, visibility 0s linear 320ms;
|
||||
}
|
||||
|
||||
.add-person-panel--open {
|
||||
transform: translateX(0);
|
||||
transform: none;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: transform 320ms var(--pz-ease), opacity 220ms ease, visibility 0s;
|
||||
}
|
||||
|
||||
/* --- Header --- */
|
||||
.apn-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
|
||||
.apn-close { flex: none; margin: -6px -6px 0 0; }
|
||||
|
||||
.add-person-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px 16px;
|
||||
border-bottom: 1px solid rgba(166, 124, 82, 0.1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.apn-form { flex: 1; min-height: 0; display: flex; flex-direction: column; gap: 14px; }
|
||||
.apn-scroll { flex: 1; min-height: 0; overflow-y: auto; display: grid; gap: 16px; align-content: start; margin: 0 -6px; padding: 2px 6px 4px; scrollbar-width: thin; scrollbar-color: var(--pz-line) transparent; }
|
||||
|
||||
.add-person-header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: #5D524B;
|
||||
}
|
||||
.apn-optional { font-weight: 400; color: var(--pz-faint); margin-left: 4px; }
|
||||
.apn-input--error { border-color: var(--pz-danger) !important; box-shadow: 0 0 0 3px var(--pz-danger-bg) !important; }
|
||||
.apn-error-text { margin: -8px 0 0; font: 500 12.5px var(--pz-sans); color: var(--pz-danger); }
|
||||
|
||||
.add-person-header-title h2 {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
color: #5D524B;
|
||||
}
|
||||
.apn-section { display: grid; gap: 12px; padding-top: 16px; border-top: 1px solid var(--pz-line-soft); }
|
||||
.apn-types .pz-tab { font-size: 13px; padding: 0 6px; }
|
||||
|
||||
.add-person-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #8B7355;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
.apn-relation { position: relative; }
|
||||
.apn-search { position: relative; display: flex; align-items: center; }
|
||||
.apn-search input { padding-left: 38px !important; padding-right: 36px !important; }
|
||||
.apn-search-icon { position: absolute; left: 13px; color: var(--pz-muted); pointer-events: none; }
|
||||
.apn-search-chevron { position: absolute; right: 13px; color: var(--pz-faint); pointer-events: none; }
|
||||
|
||||
.add-person-close:hover {
|
||||
background: rgba(93, 82, 75, 0.1);
|
||||
color: #5D524B;
|
||||
}
|
||||
|
||||
/* --- Form --- */
|
||||
|
||||
.add-person-form {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px 24px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.add-person-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.add-person-label {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #8B7355;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.add-person-required {
|
||||
color: #C0392B;
|
||||
}
|
||||
|
||||
.add-person-input {
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1.5px solid rgba(166, 124, 82, 0.2);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
color: #5D524B;
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.add-person-input::placeholder {
|
||||
color: #B8A898;
|
||||
}
|
||||
|
||||
.add-person-input:focus {
|
||||
border-color: #A67C52;
|
||||
box-shadow: 0 0 0 3px rgba(166, 124, 82, 0.12);
|
||||
}
|
||||
|
||||
.add-person-input--error {
|
||||
border-color: rgba(192, 57, 43, 0.5);
|
||||
}
|
||||
|
||||
.add-person-input--error:focus {
|
||||
border-color: #C0392B;
|
||||
box-shadow: 0 0 0 3px rgba(192, 57, 43, 0.1);
|
||||
}
|
||||
|
||||
.add-person-error-text {
|
||||
font-size: 14px;
|
||||
color: #C0392B;
|
||||
}
|
||||
|
||||
.add-person-row {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.add-person-row .add-person-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* --- Section Relations --- */
|
||||
|
||||
.add-person-section {
|
||||
margin-top: 8px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(166, 124, 82, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.add-person-section-title {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #5D524B;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* Relation type pills */
|
||||
.relation-type-pills {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.relation-type-pill {
|
||||
padding: 5px 12px;
|
||||
border-radius: 20px;
|
||||
border: 1.5px solid rgba(166, 124, 82, 0.2);
|
||||
background: transparent;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #8B7355;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.relation-type-pill:hover {
|
||||
border-color: rgba(166, 124, 82, 0.4);
|
||||
background: rgba(166, 124, 82, 0.05);
|
||||
}
|
||||
|
||||
.relation-type-pill--active {
|
||||
border-color: #A67C52;
|
||||
background: rgba(166, 124, 82, 0.1);
|
||||
color: #A67C52;
|
||||
}
|
||||
|
||||
/* Relation search */
|
||||
.relation-search-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.relation-search-icon {
|
||||
.apn-dropdown {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #B8A898;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.relation-search-input {
|
||||
padding-left: 32px !important;
|
||||
padding-right: 28px !important;
|
||||
}
|
||||
|
||||
.relation-search-chevron {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: #B8A898;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Relation dropdown */
|
||||
.relation-dropdown-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.relation-dropdown-list {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
top: calc(100% + 6px);
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
margin: 4px 0 0;
|
||||
padding: 4px;
|
||||
z-index: 5;
|
||||
list-style: none;
|
||||
background: rgba(254, 249, 237, 0.99);
|
||||
backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(166, 124, 82, 0.2);
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(93, 82, 75, 0.15);
|
||||
max-height: 200px;
|
||||
margin: 0;
|
||||
padding: 5px;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
background: var(--pz-surface);
|
||||
border: 1px solid var(--pz-line);
|
||||
border-radius: var(--pz-r-m);
|
||||
box-shadow: var(--pz-shadow-lg);
|
||||
animation: pz-rise 160ms var(--pz-ease) both;
|
||||
}
|
||||
.apn-dropdown-item { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 42px; padding: 5px 8px; border: 0; border-radius: 10px; background: none; cursor: pointer; text-align: left; }
|
||||
@media (hover: hover) and (pointer: fine) { .apn-dropdown-item:hover { background: var(--pz-a50); } }
|
||||
.apn-avatar { width: 30px; height: 30px; flex: none; display: grid; place-items: center; overflow: hidden; border-radius: 50%; background: var(--pz-a100); color: var(--pz-a700); font: 400 14px/1 var(--pz-serif); }
|
||||
.apn-avatar img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.apn-dropdown-name { flex: 1; min-width: 0; font: 400 16px/1.2 var(--pz-serif); color: var(--pz-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.apn-dropdown-name em { font-style: italic; font-weight: 300; }
|
||||
.apn-dropdown-date { flex: none; font: 400 12px var(--pz-sans); color: var(--pz-muted); font-variant-numeric: tabular-nums; }
|
||||
|
||||
.relation-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
text-align: left;
|
||||
}
|
||||
.apn-links { display: grid; gap: 6px; }
|
||||
.apn-link { display: flex; align-items: center; gap: 10px; padding: 6px 6px 6px 12px; border-radius: 999px; background: var(--pz-a50); border: 1px solid var(--pz-a100); }
|
||||
.apn-link-type { flex: none; font: 500 12px var(--pz-sans); color: var(--pz-a700); }
|
||||
.apn-link-name { flex: 1; min-width: 0; font: 400 15px/1.2 var(--pz-serif); color: var(--pz-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.apn-link-remove { width: 30px; min-height: 30px; color: var(--pz-muted); }
|
||||
|
||||
.relation-dropdown-item:hover {
|
||||
background: rgba(166, 124, 82, 0.08);
|
||||
}
|
||||
|
||||
.relation-dropdown-name {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #5D524B;
|
||||
}
|
||||
|
||||
.relation-dropdown-date {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
color: #B8A898;
|
||||
}
|
||||
|
||||
/* Relation tags */
|
||||
.relation-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.relation-tag {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 6px 4px 10px;
|
||||
border-radius: 20px;
|
||||
background: rgba(166, 124, 82, 0.08);
|
||||
border: 1px solid rgba(166, 124, 82, 0.15);
|
||||
}
|
||||
|
||||
.relation-tag-type {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #A67C52;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.relation-tag-name {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #5D524B;
|
||||
}
|
||||
|
||||
.relation-tag-remove {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #8B7355;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.relation-tag-remove:hover {
|
||||
background: rgba(192, 57, 43, 0.1);
|
||||
color: #C0392B;
|
||||
}
|
||||
|
||||
/* --- Error --- */
|
||||
|
||||
.add-person-error {
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
background: rgba(192, 57, 43, 0.08);
|
||||
border: 1px solid rgba(192, 57, 43, 0.2);
|
||||
color: #C0392B;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* --- Actions --- */
|
||||
|
||||
.add-person-actions {
|
||||
margin-top: auto;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid rgba(166, 124, 82, 0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.add-person-submit {
|
||||
padding: 12px 20px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
background: #A67C52;
|
||||
color: #FEF9ED;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.add-person-submit:hover:not(:disabled) {
|
||||
background: #96703E;
|
||||
box-shadow: 0 4px 16px rgba(166, 124, 82, 0.3);
|
||||
}
|
||||
|
||||
.add-person-submit:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.add-person-cancel {
|
||||
padding: 10px 20px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #8B7355;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.add-person-cancel:hover {
|
||||
background: rgba(93, 82, 75, 0.08);
|
||||
color: #5D524B;
|
||||
}
|
||||
|
||||
/* --- Responsive Mobile --- */
|
||||
.apn-actions { flex: none; display: flex; justify-content: flex-end; gap: 8px; padding-top: 14px; border-top: 1px solid var(--pz-line); }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.add-person-panel {
|
||||
width: 90vw;
|
||||
}
|
||||
|
||||
.add-person-header {
|
||||
padding: 16px 18px 12px;
|
||||
}
|
||||
|
||||
.add-person-form {
|
||||
padding: 16px 18px 20px;
|
||||
top: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: auto;
|
||||
max-height: 90dvh;
|
||||
border-radius: 26px 26px 0 0;
|
||||
border-bottom: 0;
|
||||
padding: 18px 18px calc(16px + env(safe-area-inset-bottom));
|
||||
transform: translateY(100%);
|
||||
transition: transform 400ms var(--pz-ease-drawer), opacity 220ms ease, visibility 0s linear 400ms;
|
||||
}
|
||||
.add-person-panel--open { transform: none; transition: transform 400ms var(--pz-ease-drawer), opacity 220ms ease, visibility 0s; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.add-person-panel, .add-person-panel--open { transform: none; transition: opacity 180ms ease, visibility 0s linear 180ms; }
|
||||
.add-person-panel--open { transition: opacity 180ms ease, visibility 0s; }
|
||||
}
|
||||
@@ -140,203 +140,167 @@ function AddPersonPanel({ visible, persons = [], loading = false, error = '', on
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`add-person-panel ${visible ? 'add-person-panel--open' : ''}`} ref={panelRef}>
|
||||
<div className="add-person-header">
|
||||
<div className="add-person-header-title">
|
||||
<UserPlus size={20} strokeWidth={2} />
|
||||
<h2>Ajouter une personne</h2>
|
||||
<div
|
||||
className={`add-person-panel ${visible ? 'add-person-panel--open' : ''}`}
|
||||
ref={panelRef}
|
||||
role="dialog"
|
||||
aria-labelledby="addPersonTitle"
|
||||
aria-hidden={!visible}
|
||||
>
|
||||
<div className="apn-head">
|
||||
<div className="pz-modal-head">
|
||||
<p className="pz-eyebrow">Nouvelle fiche</p>
|
||||
<h2 id="addPersonTitle" className="pz-title">Ajouter <em>quelqu'un</em></h2>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="add-person-close"
|
||||
onClick={onClose}
|
||||
aria-label="Fermer"
|
||||
>
|
||||
<X size={20} strokeWidth={2} />
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--icon apn-close" onClick={onClose} aria-label="Fermer">
|
||||
<X size={18} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form className="add-person-form" onSubmit={handleSubmit}>
|
||||
{/* Prénom (requis) */}
|
||||
<div className="add-person-field">
|
||||
<label htmlFor="ap-firstName" className="add-person-label">
|
||||
Prénom <span className="add-person-required">*</span>
|
||||
</label>
|
||||
<input
|
||||
ref={firstNameRef}
|
||||
id="ap-firstName"
|
||||
type="text"
|
||||
className={`add-person-input ${touched.firstName && !firstName.trim() ? 'add-person-input--error' : ''}`}
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
onBlur={() => setTouched((t) => ({ ...t, firstName: true }))}
|
||||
placeholder="Ex : Marie"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<form className="apn-form" onSubmit={handleSubmit}>
|
||||
<div className="apn-scroll">
|
||||
<div className="pz-row2">
|
||||
<div className="pz-field">
|
||||
<label htmlFor="ap-firstName">Prénom</label>
|
||||
<input
|
||||
ref={firstNameRef}
|
||||
id="ap-firstName"
|
||||
type="text"
|
||||
className={touched.firstName && !firstName.trim() ? 'apn-input--error' : ''}
|
||||
value={firstName}
|
||||
onChange={(e) => setFirstName(e.target.value)}
|
||||
onBlur={() => setTouched((t) => ({ ...t, firstName: true }))}
|
||||
placeholder="Marie"
|
||||
autoComplete="off"
|
||||
aria-invalid={touched.firstName && !firstName.trim()}
|
||||
/>
|
||||
</div>
|
||||
<div className="pz-field">
|
||||
<label htmlFor="ap-lastName">Nom</label>
|
||||
<input
|
||||
id="ap-lastName"
|
||||
type="text"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
placeholder="Dupont"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{touched.firstName && !firstName.trim() && (
|
||||
<span className="add-person-error-text">Le prénom est requis</span>
|
||||
<p className="apn-error-text">Il faut au moins un prénom.</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Nom */}
|
||||
<div className="add-person-field">
|
||||
<label htmlFor="ap-lastName" className="add-person-label">Nom</label>
|
||||
<input
|
||||
id="ap-lastName"
|
||||
type="text"
|
||||
className="add-person-input"
|
||||
value={lastName}
|
||||
onChange={(e) => setLastName(e.target.value)}
|
||||
placeholder="Ex : Dupont"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Nom de naissance */}
|
||||
<div className="add-person-field">
|
||||
<label htmlFor="ap-birthName" className="add-person-label">Nom de naissance</label>
|
||||
<input
|
||||
id="ap-birthName"
|
||||
type="text"
|
||||
className="add-person-input"
|
||||
value={birthName}
|
||||
onChange={(e) => setBirthName(e.target.value)}
|
||||
placeholder="Ex : Martin"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Dates en ligne */}
|
||||
<div className="add-person-row">
|
||||
<div className="add-person-field">
|
||||
<label htmlFor="ap-birthDate" className="add-person-label">Date de naissance</label>
|
||||
<div className="pz-field">
|
||||
<label htmlFor="ap-birthName">Nom de naissance <span className="apn-optional">si différent</span></label>
|
||||
<input
|
||||
id="ap-birthDate"
|
||||
type="date"
|
||||
className="add-person-input"
|
||||
value={birthDate}
|
||||
onChange={(e) => setBirthDate(e.target.value)}
|
||||
id="ap-birthName"
|
||||
type="text"
|
||||
value={birthName}
|
||||
onChange={(e) => setBirthName(e.target.value)}
|
||||
placeholder="Martin"
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div className="add-person-field">
|
||||
<label htmlFor="ap-deathDate" className="add-person-label">Date de décès</label>
|
||||
<input
|
||||
id="ap-deathDate"
|
||||
type="date"
|
||||
className="add-person-input"
|
||||
value={deathDate}
|
||||
onChange={(e) => setDeathDate(e.target.value)}
|
||||
/>
|
||||
|
||||
<div className="pz-row2">
|
||||
<div className="pz-field">
|
||||
<label htmlFor="ap-birthDate">Naissance</label>
|
||||
<input id="ap-birthDate" type="date" value={birthDate} onChange={(e) => setBirthDate(e.target.value)} />
|
||||
</div>
|
||||
<div className="pz-field">
|
||||
<label htmlFor="ap-deathDate">Décès</label>
|
||||
<input id="ap-deathDate" type="date" value={deathDate} onChange={(e) => setDeathDate(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section Relations */}
|
||||
<div className="add-person-section">
|
||||
<h3 className="add-person-section-title">Relations</h3>
|
||||
<section className="apn-section">
|
||||
<h3 className="pz-eyebrow">Place dans la famille</h3>
|
||||
|
||||
{/* Type de relation */}
|
||||
<div className="add-person-field">
|
||||
<label htmlFor="ap-relationType" className="add-person-label">Type de lien</label>
|
||||
<div className="relation-type-pills">
|
||||
<div className="pz-tabs apn-types" role="radiogroup" aria-label="Type de lien">
|
||||
{Object.entries(relationTypeLabels).map(([type, label]) => (
|
||||
<button
|
||||
key={type}
|
||||
type="button"
|
||||
className={`relation-type-pill ${selectedRelationType === type ? 'relation-type-pill--active' : ''}`}
|
||||
role="radio"
|
||||
aria-checked={selectedRelationType === type}
|
||||
className={`pz-tab ${selectedRelationType === type ? 'is-active' : ''}`}
|
||||
onClick={() => setSelectedRelationType(type)}
|
||||
>
|
||||
{label.split(' ')[0]}
|
||||
{label.replace(/ de$/, '')}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Recherche personne existante */}
|
||||
<div className="add-person-field relation-dropdown-wrapper">
|
||||
<label className="add-person-label">
|
||||
{relationTypeLabels[selectedRelationType]}
|
||||
</label>
|
||||
<div className="relation-search-wrapper">
|
||||
<Search size={14} strokeWidth={2} className="relation-search-icon" />
|
||||
<input
|
||||
type="text"
|
||||
className="add-person-input relation-search-input"
|
||||
value={relationSearch}
|
||||
onChange={(e) => {
|
||||
setRelationSearch(e.target.value)
|
||||
setRelationDropdownOpen(true)
|
||||
}}
|
||||
onFocus={() => setRelationDropdownOpen(true)}
|
||||
placeholder="Rechercher une personne..."
|
||||
autoComplete="off"
|
||||
/>
|
||||
<ChevronDown size={14} strokeWidth={2} className="relation-search-chevron" />
|
||||
<div className="pz-field relation-dropdown-wrapper apn-relation">
|
||||
<label htmlFor="ap-relationSearch">{relationTypeLabels[selectedRelationType]}</label>
|
||||
<div className="apn-search">
|
||||
<Search size={15} strokeWidth={2} className="apn-search-icon" />
|
||||
<input
|
||||
id="ap-relationSearch"
|
||||
type="text"
|
||||
value={relationSearch}
|
||||
onChange={(e) => {
|
||||
setRelationSearch(e.target.value)
|
||||
setRelationDropdownOpen(true)
|
||||
}}
|
||||
onFocus={() => setRelationDropdownOpen(true)}
|
||||
placeholder="Chercher dans l'arbre"
|
||||
autoComplete="off"
|
||||
/>
|
||||
<ChevronDown size={15} strokeWidth={2} className="apn-search-chevron" />
|
||||
</div>
|
||||
|
||||
{relationDropdownOpen && filteredPersons.length > 0 && (
|
||||
<ul className="apn-dropdown">
|
||||
{filteredPersons.slice(0, 8).map((p) => (
|
||||
<li key={p.id}>
|
||||
<button type="button" className="apn-dropdown-item" onClick={() => handleAddRelation(p)}>
|
||||
<span className="apn-avatar" aria-hidden="true">
|
||||
{p.photo ? <img src={p.photo} alt="" /> : (p.firstName || '?').charAt(0)}
|
||||
</span>
|
||||
<span className="apn-dropdown-name">{p.firstName} <em>{p.lastName || ''}</em></span>
|
||||
{(p.birthYear || p.birthDate) && (
|
||||
<span className="apn-dropdown-date">{p.birthYear || String(p.birthDate).slice(0, 4)}</span>
|
||||
)}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{relationDropdownOpen && filteredPersons.length > 0 && (
|
||||
<ul className="relation-dropdown-list">
|
||||
{filteredPersons.slice(0, 8).map((p) => (
|
||||
<li key={p.id}>
|
||||
{relations.length > 0 && (
|
||||
<div className="apn-links">
|
||||
{relations.map((rel) => (
|
||||
<div key={rel.personId} className="apn-link">
|
||||
<span className="apn-link-type">{relationTypeLabels[rel.type]}</span>
|
||||
<span className="apn-link-name">{rel.personName}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="relation-dropdown-item"
|
||||
onClick={() => handleAddRelation(p)}
|
||||
className="pz-btn pz-btn--ghost pz-btn--icon apn-link-remove"
|
||||
onClick={() => handleRemoveRelation(rel.personId)}
|
||||
aria-label={`Retirer ${rel.personName}`}
|
||||
>
|
||||
<span className="relation-dropdown-name">
|
||||
{p.firstName} {p.lastName || ''}
|
||||
</span>
|
||||
{p.birthDate && (
|
||||
<span className="relation-dropdown-date">{p.birthDate}</span>
|
||||
)}
|
||||
<X size={14} strokeWidth={2.2} />
|
||||
</button>
|
||||
</li>
|
||||
</div>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Relations ajoutées */}
|
||||
{relations.length > 0 && (
|
||||
<div className="relation-tags">
|
||||
{relations.map((rel) => (
|
||||
<div key={rel.personId} className="relation-tag">
|
||||
<span className="relation-tag-type">{relationTypeLabels[rel.type].split(' ')[0]}</span>
|
||||
<span className="relation-tag-name">{rel.personName}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="relation-tag-remove"
|
||||
onClick={() => handleRemoveRelation(rel.personId)}
|
||||
aria-label={`Retirer ${rel.personName}`}
|
||||
>
|
||||
<X size={12} strokeWidth={2.5} />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="pz-error" role="alert">{error}</div>}
|
||||
</div>
|
||||
|
||||
{/* Erreur */}
|
||||
{error && (
|
||||
<div className="add-person-error" role="alert">{error}</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="add-person-actions">
|
||||
<button
|
||||
type="submit"
|
||||
className="add-person-submit"
|
||||
disabled={!isValid || loading}
|
||||
>
|
||||
{loading ? 'Ajout en cours...' : 'Ajouter cette personne'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="add-person-cancel"
|
||||
onClick={onClose}
|
||||
>
|
||||
<div className="apn-actions">
|
||||
<button type="button" className="pz-btn pz-btn--ghost" onClick={onClose}>
|
||||
Annuler
|
||||
</button>
|
||||
<button type="submit" className="pz-btn pz-btn--primary" disabled={!isValid || loading}>
|
||||
<UserPlus size={16} strokeWidth={2} />
|
||||
{loading ? 'Ajout…' : 'Ajouter cette personne'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { Download, Trash2, X } from 'lucide-react'
|
||||
import './SettingsPanels.css'
|
||||
import {
|
||||
deleteTree,
|
||||
exportGedcom,
|
||||
@@ -243,79 +245,80 @@ const AdminPanel = ({
|
||||
}
|
||||
}
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`contrib-overlay tree-settings-overlay ${visible ? 'contrib-overlay--open tree-settings-overlay--open' : ''}`}
|
||||
className="pz-overlay tree-settings-overlay"
|
||||
onClick={() => {
|
||||
if (visible && !showDeleteModal) onClose?.()
|
||||
if (!showDeleteModal) onClose?.()
|
||||
}}
|
||||
aria-hidden={!visible}
|
||||
>
|
||||
<div
|
||||
className="contrib-card tree-settings-card"
|
||||
className="pz-modal pz-modal--wide tree-settings-card"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Paramètres de l'arbre"
|
||||
aria-labelledby="treeSettingsTitle"
|
||||
>
|
||||
<div className="contrib-header tree-settings-header">
|
||||
<div className="contrib-header-text">
|
||||
<h1>Paramètres de l'arbre</h1>
|
||||
<p>{tree?.name || 'Réglages de cet arbre familial'}</p>
|
||||
</div>
|
||||
<button type="button" className="ghost" onClick={onClose}>Fermer</button>
|
||||
<div className="pz-modal-head">
|
||||
<p className="pz-eyebrow">Paramètres de l'arbre</p>
|
||||
<h1 id="treeSettingsTitle" className="pz-title">{tree?.name || 'Votre arbre'}</h1>
|
||||
</div>
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--icon pz-modal-close" onClick={onClose} aria-label="Fermer">
|
||||
<X size={18} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<div className="pz-tabs" role="tablist">
|
||||
<button type="button" role="tab" aria-selected={section === 'tree'} className="pz-tab" onClick={() => setSection('tree')}>Votre arbre</button>
|
||||
<button type="button" role="tab" aria-selected={section === 'data'} className="pz-tab" onClick={() => setSection('data')}>Données</button>
|
||||
<button type="button" role="tab" aria-selected={section === 'danger'} className="pz-tab" onClick={() => setSection('danger')}>Avancé</button>
|
||||
</div>
|
||||
|
||||
<div className="tree-settings-tabs">
|
||||
<button type="button" className={`tree-settings-tab ${section === 'tree' ? 'active' : ''}`} onClick={() => setSection('tree')}>Votre arbre</button>
|
||||
<button type="button" className={`tree-settings-tab ${section === 'data' ? 'active' : ''}`} onClick={() => setSection('data')}>Données</button>
|
||||
<button type="button" className={`tree-settings-tab ${section === 'danger' ? 'active' : ''}`} onClick={() => setSection('danger')}>Paramètres avancés</button>
|
||||
</div>
|
||||
|
||||
<div className="tree-settings-content">
|
||||
<div className="ts-content">
|
||||
{loading ? (
|
||||
<div className="contrib-empty">Chargement...</div>
|
||||
<p className="pz-small ts-loading">Chargement…</p>
|
||||
) : (
|
||||
<>
|
||||
{errorMessage && <div className="contrib-error">{errorMessage}</div>}
|
||||
{successMessage && <div className="contrib-ok">{successMessage}</div>}
|
||||
{errorMessage && <div className="pz-error" role="alert">{errorMessage}</div>}
|
||||
{successMessage && <div className="pz-success" role="status">{successMessage}</div>}
|
||||
|
||||
{section === 'tree' && (
|
||||
<form className="tree-settings-panel" onSubmit={handleSaveTree}>
|
||||
<h2>Votre arbre</h2>
|
||||
<p className="tree-settings-help">Modifiez le nom et la description affichés à votre famille.</p>
|
||||
<form className="ts-panel" onSubmit={handleSaveTree}>
|
||||
<div className="pz-field">
|
||||
<label htmlFor="settingsTreeName">Nom de l'arbre</label>
|
||||
<input
|
||||
id="settingsTreeName"
|
||||
type="text"
|
||||
value={treeName}
|
||||
maxLength={120}
|
||||
onChange={(event) => setTreeName(event.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label htmlFor="settingsTreeName">Nom de l'arbre</label>
|
||||
<input
|
||||
id="settingsTreeName"
|
||||
type="text"
|
||||
value={treeName}
|
||||
maxLength={120}
|
||||
onChange={(event) => setTreeName(event.target.value)}
|
||||
required
|
||||
/>
|
||||
<div className="pz-field">
|
||||
<label htmlFor="settingsTreeDescription">Description <span className="ts-optional">facultatif</span></label>
|
||||
<input
|
||||
id="settingsTreeDescription"
|
||||
type="text"
|
||||
value={treeDescription}
|
||||
maxLength={500}
|
||||
onChange={(event) => setTreeDescription(event.target.value)}
|
||||
placeholder="Quelques mots pour accueillir la famille"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label htmlFor="settingsTreeDescription">Description (optionnelle)</label>
|
||||
<input
|
||||
id="settingsTreeDescription"
|
||||
type="text"
|
||||
value={treeDescription}
|
||||
maxLength={500}
|
||||
onChange={(event) => setTreeDescription(event.target.value)}
|
||||
/>
|
||||
|
||||
<div className="tree-settings-actions">
|
||||
<button type="submit" className="tree-settings-primary" disabled={saving}>
|
||||
{saving ? 'Enregistrement...' : 'Enregistrer les modifications'}
|
||||
<div className="ts-actions">
|
||||
<button type="submit" className="pz-btn pz-btn--primary" disabled={saving}>
|
||||
{saving ? 'Enregistrement…' : 'Enregistrer'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<fieldset className="tree-settings-policy" disabled={savingPolicy}>
|
||||
<legend>Contributions de la famille</legend>
|
||||
<p className="tree-settings-help">
|
||||
Ce que deviennent les ajouts et corrections faits avec le mot de passe de contribution.
|
||||
</p>
|
||||
<label className="tree-settings-radio">
|
||||
<fieldset className="ts-policy" disabled={savingPolicy}>
|
||||
<legend className="pz-eyebrow">Contributions de la famille</legend>
|
||||
<p className="pz-small">Ce que deviennent les ajouts et corrections faits avec le mot de passe de contribution.</p>
|
||||
<label className={`ts-choice ${contributionPolicy === 'pending' ? 'is-active' : ''}`}>
|
||||
<input
|
||||
type="radio"
|
||||
name="contributionPolicy"
|
||||
@@ -323,12 +326,12 @@ const AdminPanel = ({
|
||||
checked={contributionPolicy === 'pending'}
|
||||
onChange={() => handlePolicyChange('pending')}
|
||||
/>
|
||||
<span>
|
||||
<span className="ts-choice-text">
|
||||
<strong>Je valide chaque proposition</strong>
|
||||
<small>Rien n'apparaît dans l'arbre avant votre accord.</small>
|
||||
</span>
|
||||
</label>
|
||||
<label className="tree-settings-radio">
|
||||
<label className={`ts-choice ${contributionPolicy === 'direct' ? 'is-active' : ''}`}>
|
||||
<input
|
||||
type="radio"
|
||||
name="contributionPolicy"
|
||||
@@ -336,8 +339,8 @@ const AdminPanel = ({
|
||||
checked={contributionPolicy === 'direct'}
|
||||
onChange={() => handlePolicyChange('direct')}
|
||||
/>
|
||||
<span>
|
||||
<strong>Les modifications sont appliquées directement</strong>
|
||||
<span className="ts-choice-text">
|
||||
<strong>Les modifications s'appliquent tout de suite</strong>
|
||||
<small>La famille modifie l'arbre sans attendre. Vous gardez l'historique.</small>
|
||||
</span>
|
||||
</label>
|
||||
@@ -346,62 +349,50 @@ const AdminPanel = ({
|
||||
)}
|
||||
|
||||
{section === 'data' && (
|
||||
<div className="tree-settings-panel">
|
||||
<h2>Données de l'arbre</h2>
|
||||
<p className="tree-settings-help">Récupérez vos informations à tout moment.</p>
|
||||
|
||||
<div className="tree-settings-row">
|
||||
<div className="tree-settings-row-text">
|
||||
<div className="ts-panel">
|
||||
<div className="ts-row">
|
||||
<div className="ts-row-text">
|
||||
<strong>Importer un fichier GEDCOM</strong>
|
||||
<p>Ajoute les personnes et relations du fichier dans cet arbre.</p>
|
||||
<p className="pz-small">Ajoute les personnes et les liens du fichier à cet arbre.</p>
|
||||
{gedcomFile && <span className="pz-tag pz-tag--accent ts-file">{gedcomFile.name}</span>}
|
||||
</div>
|
||||
<div className="tree-settings-row-actions">
|
||||
<div className="ts-row-actions">
|
||||
<input
|
||||
ref={gedcomInputRef}
|
||||
className="tree-settings-file-input"
|
||||
className="ts-file-input"
|
||||
type="file"
|
||||
accept=".ged,text/x-gedcom,text/plain"
|
||||
onChange={(event) => setGedcomFile(event.target.files?.[0] || null)}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="tree-settings-secondary"
|
||||
onClick={() => gedcomInputRef.current?.click()}
|
||||
disabled={saving}
|
||||
>
|
||||
<button type="button" className="pz-btn pz-btn--secondary pz-btn--sm" onClick={() => gedcomInputRef.current?.click()} disabled={saving}>
|
||||
Choisir un fichier
|
||||
</button>
|
||||
{gedcomFile && <span className="tree-settings-file-name">{gedcomFile.name}</span>}
|
||||
<button
|
||||
type="button"
|
||||
className="tree-settings-primary"
|
||||
onClick={handleImportGedcom}
|
||||
disabled={saving || !gedcomFile}
|
||||
>
|
||||
{importingGedcom ? 'Import...' : 'Importer .ged'}
|
||||
<button type="button" className="pz-btn pz-btn--primary pz-btn--sm" onClick={handleImportGedcom} disabled={saving || !gedcomFile}>
|
||||
{importingGedcom ? 'Import…' : 'Importer'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tree-settings-row">
|
||||
<div className="tree-settings-row-text">
|
||||
<strong>Fichier GEDCOM</strong>
|
||||
<p>Format standard généalogique, compatible avec les principaux outils.</p>
|
||||
<div className="ts-row">
|
||||
<div className="ts-row-text">
|
||||
<strong>Télécharger en GEDCOM</strong>
|
||||
<p className="pz-small">Le format standard, lisible par les autres logiciels d'arbre.</p>
|
||||
</div>
|
||||
<div className="tree-settings-row-actions tree-settings-row-actions--inline">
|
||||
<button type="button" className="tree-settings-primary" onClick={handleExportGedcom} disabled={saving}>
|
||||
<div className="ts-row-actions">
|
||||
<button type="button" className="pz-btn pz-btn--secondary pz-btn--sm" onClick={handleExportGedcom} disabled={saving}>
|
||||
<Download size={15} aria-hidden="true" />
|
||||
Télécharger .ged
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="tree-settings-row tree-settings-row--muted">
|
||||
<div className="tree-settings-row-text">
|
||||
<div className="ts-row ts-row--muted">
|
||||
<div className="ts-row-text">
|
||||
<strong>Sauvegarde complète</strong>
|
||||
<p>Toutes les données et médias (phase 2).</p>
|
||||
<p className="pz-small">Toutes les données et les médias. Bientôt disponible.</p>
|
||||
</div>
|
||||
<div className="tree-settings-row-actions tree-settings-row-actions--inline">
|
||||
<button type="button" className="tree-settings-secondary" disabled>
|
||||
<div className="ts-row-actions">
|
||||
<button type="button" className="pz-btn pz-btn--secondary pz-btn--sm" disabled>
|
||||
Télécharger .zip
|
||||
</button>
|
||||
</div>
|
||||
@@ -410,15 +401,16 @@ const AdminPanel = ({
|
||||
)}
|
||||
|
||||
{section === 'danger' && (
|
||||
<div className="tree-settings-panel tree-settings-panel--danger">
|
||||
<h2>Paramètres avancés</h2>
|
||||
<p className="tree-settings-help">Ces actions ont un impact définitif.</p>
|
||||
|
||||
<div className="tree-settings-danger-box">
|
||||
<button type="button" className="danger-action" onClick={() => setShowDeleteModal(true)}>
|
||||
<div className="ts-panel">
|
||||
<div className="ts-danger">
|
||||
<div className="ts-row-text">
|
||||
<strong>Supprimer cet arbre</strong>
|
||||
<p className="pz-small">Toutes les personnes, les souvenirs et les médias sont effacés, pour toute la famille. C'est définitif.</p>
|
||||
</div>
|
||||
<button type="button" className="pz-btn pz-btn--danger pz-btn--sm danger-action" onClick={() => setShowDeleteModal(true)}>
|
||||
<Trash2 size={15} aria-hidden="true" />
|
||||
Supprimer cet arbre
|
||||
</button>
|
||||
<p>Cette action est irréversible. Toutes les données et médias seront supprimés.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -428,22 +420,30 @@ const AdminPanel = ({
|
||||
</div>
|
||||
|
||||
{showDeleteModal && (
|
||||
<div className="settings-delete-modal-overlay" onClick={() => !saving && setShowDeleteModal(false)}>
|
||||
<div className="settings-delete-modal" onClick={(event) => event.stopPropagation()}>
|
||||
<h3>Confirmer la suppression</h3>
|
||||
<p>Saisissez le nom de l'arbre pour confirmer : <strong>{tree?.name}</strong></p>
|
||||
<input
|
||||
type="text"
|
||||
value={deleteConfirmName}
|
||||
onChange={(event) => setDeleteConfirmName(event.target.value)}
|
||||
placeholder="Nom exact de l'arbre"
|
||||
/>
|
||||
<div className="settings-delete-actions">
|
||||
<button type="button" className="ghost" onClick={() => setShowDeleteModal(false)} disabled={saving}>
|
||||
<div className="pz-overlay ts-delete-overlay" onClick={(event) => { event.stopPropagation(); if (!saving) setShowDeleteModal(false) }}>
|
||||
<div className="pz-modal settings-delete-modal" role="dialog" aria-modal="true" aria-labelledby="treeDeleteTitle" onClick={(event) => event.stopPropagation()}>
|
||||
<div className="pz-modal-head">
|
||||
<p className="pz-eyebrow">Dernière vérification</p>
|
||||
<h3 id="treeDeleteTitle" className="pz-title">Supprimer « {tree?.name} »</h3>
|
||||
<p className="pz-sub">Tapez le nom exact de l'arbre pour confirmer.</p>
|
||||
</div>
|
||||
<div className="pz-field">
|
||||
<label htmlFor="treeDeleteConfirm">Nom de l'arbre</label>
|
||||
<input
|
||||
id="treeDeleteConfirm"
|
||||
type="text"
|
||||
value={deleteConfirmName}
|
||||
onChange={(event) => setDeleteConfirmName(event.target.value)}
|
||||
placeholder={tree?.name || ''}
|
||||
autoComplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div className="pz-modal-actions">
|
||||
<button type="button" className="pz-btn pz-btn--ghost" onClick={() => setShowDeleteModal(false)} disabled={saving}>
|
||||
Annuler
|
||||
</button>
|
||||
<button type="button" className="danger-action" onClick={handleDeleteTree} disabled={saving}>
|
||||
{saving ? 'Suppression...' : 'Supprimer définitivement'}
|
||||
<button type="button" className="pz-btn pz-btn--danger danger-action" onClick={handleDeleteTree} disabled={saving}>
|
||||
{saving ? 'Suppression…' : 'Supprimer définitivement'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Check, X, ChevronDown } from 'lucide-react'
|
||||
import { Check, ChevronDown, Copy, RefreshCw, X } from 'lucide-react'
|
||||
import './SettingsPanels.css'
|
||||
|
||||
// ─── helpers ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -84,7 +85,7 @@ function getAnnotationPreview(change) {
|
||||
if (!type) return null
|
||||
if (type === 'sticker') return { type, emoji: data.content || '?' }
|
||||
if (type === 'text') return { type, text: data.content || '' }
|
||||
if (type === 'drawing') return { type, color: data.style?.color || '#5D524B' }
|
||||
if (type === 'drawing') return { type, color: data.style?.color || '#2A2622' }
|
||||
if (type === 'photo') {
|
||||
try {
|
||||
const parsed = typeof data.content === 'string' ? JSON.parse(data.content) : data.content
|
||||
@@ -254,26 +255,24 @@ const ContributionPanel = ({
|
||||
if (result?.ok) { setEditingContributorPassword(false); setNewContributorPassword('') }
|
||||
}
|
||||
|
||||
if (!visible) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`contrib-overlay ${visible ? 'contrib-overlay--open' : ''}`}
|
||||
onClick={() => visible && onClose?.()}
|
||||
aria-hidden={!visible}
|
||||
>
|
||||
<div className="pz-overlay contrib-overlay" onClick={() => onClose?.()}>
|
||||
<div
|
||||
className="contrib-card"
|
||||
className="pz-modal pz-modal--wide contrib-card"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label="Panel contributions"
|
||||
aria-labelledby="contribPanelTitle"
|
||||
>
|
||||
<div className="contrib-header">
|
||||
<div className="contrib-header-text">
|
||||
<h1>Contributions</h1>
|
||||
<p>Validation des propositions de modification</p>
|
||||
</div>
|
||||
<button type="button" className="ghost" onClick={onClose}>Fermer</button>
|
||||
<div className="pz-modal-head">
|
||||
<p className="pz-eyebrow">Partager et relire</p>
|
||||
<h1 id="contribPanelTitle" className="pz-title">La famille <em>participe</em></h1>
|
||||
</div>
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--icon pz-modal-close" onClick={onClose} aria-label="Fermer">
|
||||
<X size={18} strokeWidth={2} />
|
||||
</button>
|
||||
|
||||
{canModerate ? (
|
||||
<div className="contrib-list-section">
|
||||
@@ -281,17 +280,17 @@ const ContributionPanel = ({
|
||||
{/* ── Section invitation ── */}
|
||||
<div className="contrib-invite">
|
||||
<div className="contrib-invite-top">
|
||||
<strong className="contrib-invite-title">Invitez vos proches à alimenter l'arbre</strong>
|
||||
<strong className="contrib-invite-title">Le lien à envoyer à la famille</strong>
|
||||
{copyFeedback && <span className="contrib-copy-feedback">{copyFeedback}</span>}
|
||||
</div>
|
||||
<div className="contrib-invite-link">
|
||||
<code className="contrib-link-url">{inviteShareUrl || 'Lien indisponible'}</code>
|
||||
<button type="button" className="contrib-pill-btn contrib-pill-btn--ghost" disabled={!inviteShareUrl} onClick={() => handleCopy(inviteShareUrl, 'Lien copié')}>Copier</button>
|
||||
<button type="button" className="pz-btn pz-btn--primary pz-btn--sm" disabled={!inviteShareUrl} onClick={() => handleCopy(inviteShareUrl, 'Lien copié')}><Copy size={14} strokeWidth={2} />Copier</button>
|
||||
</div>
|
||||
<div className="contrib-invite-passwords">
|
||||
{/* Visiteur */}
|
||||
<div className="contrib-role-row">
|
||||
<span className="contrib-role-badge">Visiteur</span>
|
||||
<span className="contrib-role-badge"><strong>Pour regarder</strong><small>mot de passe visiteur</small></span>
|
||||
{editingVisitorPassword ? (
|
||||
<form className="contrib-pass-edit-inline" onSubmit={handleSaveVisitorPassword}>
|
||||
<input className="contrib-pass-edit-input" type="password" value={newVisitorPassword} placeholder="Nouveau MDP (8+ car.)" autoFocus onChange={(e) => setNewVisitorPassword(e.target.value)} />
|
||||
@@ -307,7 +306,7 @@ const ContributionPanel = ({
|
||||
</div>
|
||||
{/* Contributeur */}
|
||||
<div className="contrib-role-row">
|
||||
<span className="contrib-role-badge contrib-role-badge--contrib">Contributeur</span>
|
||||
<span className="contrib-role-badge contrib-role-badge--contrib"><strong>Pour ajouter</strong><small>mot de passe contributeur</small></span>
|
||||
{editingContributorPassword ? (
|
||||
<form className="contrib-pass-edit-inline" onSubmit={handleSaveContributorPassword}>
|
||||
<input className="contrib-pass-edit-input" type="password" value={newContributorPassword} placeholder="Nouveau MDP (8+ car.)" autoFocus onChange={(e) => setNewContributorPassword(e.target.value)} />
|
||||
@@ -329,21 +328,21 @@ const ContributionPanel = ({
|
||||
{/* ── En-tête sessions ── */}
|
||||
<div className="contrib-list-header">
|
||||
<div className="contrib-list-heading">
|
||||
<h2>Propositions en attente</h2>
|
||||
<p>Approuvez ou rejetez chaque modification individuellement</p>
|
||||
<h2>Propositions à relire</h2>
|
||||
<p>Acceptez ou refusez chaque modification, puis appliquez vos choix.</p>
|
||||
</div>
|
||||
<div className="contrib-list-header-actions">
|
||||
<span className="contrib-count">{sessions.length || 0}</span>
|
||||
<button type="button" className="ghost" onClick={onRefresh} disabled={loading}>Actualiser</button>
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--sm" onClick={onRefresh} disabled={loading}><RefreshCw size={14} strokeWidth={2} />Actualiser</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{errorMessage && <div className="contrib-error">{errorMessage}</div>}
|
||||
|
||||
{loading ? (
|
||||
<div className="contrib-empty">Chargement...</div>
|
||||
<div className="contrib-empty">Chargement…</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<div className="contrib-empty">Aucune proposition en attente.</div>
|
||||
<div className="contrib-empty">Rien à relire pour l'instant. Les propositions de la famille apparaîtront ici.</div>
|
||||
) : (
|
||||
<div className="contrib-list">
|
||||
{sessions.map((session) => {
|
||||
@@ -364,7 +363,7 @@ const ContributionPanel = ({
|
||||
onKeyDown={(e) => e.key === 'Enter' && toggleCollapse(session.id)}
|
||||
>
|
||||
<div className="contrib-item-top">
|
||||
<strong className="contrib-session-title">{session.title || 'Session sans titre'}</strong>
|
||||
<strong className="contrib-session-title">{session.title || 'Proposition sans titre'}</strong>
|
||||
<div className="contrib-session-top-right">
|
||||
{formatSessionDate(session.createdAt) && (
|
||||
<span className="contrib-session-date">{formatSessionDate(session.createdAt)}</span>
|
||||
@@ -383,13 +382,13 @@ const ContributionPanel = ({
|
||||
{hasDecisions && (
|
||||
<>
|
||||
<span className="contrib-meta-sep">·</span>
|
||||
{summary.approved > 0 && <span className="contrib-decision-summary contrib-decision-summary--ok">{summary.approved} ✓</span>}
|
||||
{summary.rejected > 0 && <span className="contrib-decision-summary contrib-decision-summary--ko">{summary.rejected} ✗</span>}
|
||||
{summary.approved > 0 && <span className="contrib-decision-summary contrib-decision-summary--ok">{summary.approved} acceptée{summary.approved > 1 ? 's' : ''}</span>}
|
||||
{summary.rejected > 0 && <span className="contrib-decision-summary contrib-decision-summary--ko">{summary.rejected} refusée{summary.rejected > 1 ? 's' : ''}</span>}
|
||||
{summary.pending > 0 && <span className="contrib-decision-summary contrib-decision-summary--pending">{summary.pending} en attente</span>}
|
||||
</>
|
||||
)}
|
||||
{summary.pending === 0 && summary.total > 0 && (
|
||||
<span className="contrib-all-decided-badge">✓ Complet</span>
|
||||
<span className="contrib-all-decided-badge">Tout est relu</span>
|
||||
)}
|
||||
</div>
|
||||
{session.comment && (
|
||||
@@ -435,7 +434,7 @@ const ContributionPanel = ({
|
||||
{personName || formatEntityType(change.entityType)}
|
||||
</span>
|
||||
{change.conflictState === 'needs_review' && (
|
||||
<span className="contrib-conflict-badge">⚠ Conflit</span>
|
||||
<span className="contrib-conflict-badge">À vérifier</span>
|
||||
)}
|
||||
<div className="contrib-card-decisions">
|
||||
<button
|
||||
@@ -443,7 +442,7 @@ const ContributionPanel = ({
|
||||
className={`contrib-decision-icon contrib-decision-icon--approve${isApproved ? ' active' : ''}`}
|
||||
onClick={(e) => { e.stopPropagation(); toggleChangeDecision(session.id, change.id, 'approved') }}
|
||||
disabled={isReviewing}
|
||||
title="Approuver"
|
||||
title="Accepter" aria-label="Accepter"
|
||||
>
|
||||
<Check size={13} strokeWidth={2.5} />
|
||||
</button>
|
||||
@@ -452,7 +451,7 @@ const ContributionPanel = ({
|
||||
className={`contrib-decision-icon contrib-decision-icon--reject${isRejected ? ' active' : ''}`}
|
||||
onClick={(e) => { e.stopPropagation(); toggleChangeDecision(session.id, change.id, 'rejected') }}
|
||||
disabled={isReviewing}
|
||||
title="Rejeter"
|
||||
title="Refuser" aria-label="Refuser"
|
||||
>
|
||||
<X size={13} strokeWidth={2.5} />
|
||||
</button>
|
||||
@@ -482,12 +481,12 @@ const ContributionPanel = ({
|
||||
<span className="contrib-field-label">{label}</span>
|
||||
{oldVal !== undefined ? (
|
||||
<span className="contrib-field-diff">
|
||||
<span className="contrib-field-old">{displayVal(oldVal) ?? '—'}</span>
|
||||
<span className="contrib-field-old">{displayVal(oldVal) ?? 'vide'}</span>
|
||||
<span className="contrib-field-arrow">→</span>
|
||||
<span className="contrib-field-new">{displayVal(newVal) ?? '—'}</span>
|
||||
<span className="contrib-field-new">{displayVal(newVal) ?? 'vide'}</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="contrib-field-new">{displayVal(newVal) ?? '—'}</span>
|
||||
<span className="contrib-field-new">{displayVal(newVal) ?? 'vide'}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
@@ -520,27 +519,27 @@ const ContributionPanel = ({
|
||||
onClick={() => applyDecisions(session.id, session.changes)}
|
||||
disabled={isReviewing}
|
||||
>
|
||||
{isReviewing ? 'Traitement...' : `Appliquer les décisions${summary.pending > 0 ? ` (${summary.pending} non décidée${summary.pending > 1 ? 's' : ''} → rejetée${summary.pending > 1 ? 's' : ''})` : ''}`}
|
||||
{isReviewing ? 'Un instant…' : `Appliquer mes choix${summary.pending > 0 ? `, ${summary.pending} sans réponse ser${summary.pending > 1 ? 'ont refusées' : 'a refusée'}` : ''}`}
|
||||
</button>
|
||||
<div className="contrib-session-quick">
|
||||
{confirmApproveAll === session.id ? (
|
||||
<div className="contrib-confirm-approve">
|
||||
<span>Tout approuver ?</span>
|
||||
<span>Tout accepter ?</span>
|
||||
<button type="button" className="contrib-confirm-yes" onClick={() => { setConfirmApproveAll(null); onReviewSession(session.id, { decision: 'approved' }) }} disabled={isReviewing}>Oui</button>
|
||||
<button type="button" className="contrib-confirm-no" onClick={() => setConfirmApproveAll(null)}>Non</button>
|
||||
</div>
|
||||
) : (
|
||||
<button type="button" className="contrib-quick-btn" onClick={() => setConfirmApproveAll(session.id)} disabled={isReviewing}>Tout approuver</button>
|
||||
<button type="button" className="contrib-quick-btn" onClick={() => setConfirmApproveAll(session.id)} disabled={isReviewing}>Tout accepter</button>
|
||||
)}
|
||||
<button type="button" className="contrib-quick-btn" onClick={() => onReviewSession(session.id, { decision: 'rejected' })} disabled={isReviewing}>Tout rejeter</button>
|
||||
<button type="button" className="contrib-quick-btn" onClick={() => onReviewSession(session.id, { decision: 'rejected' })} disabled={isReviewing}>Tout refuser</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="contrib-session-quick contrib-session-quick--full">
|
||||
{confirmApproveAll === session.id ? (
|
||||
<div className="contrib-confirm-approve">
|
||||
<span>Confirmer l'approbation de toutes les modifications ?</span>
|
||||
<button type="button" className="contrib-confirm-yes" onClick={() => { setConfirmApproveAll(null); onReviewSession(session.id, { decision: 'approved' }) }} disabled={isReviewing}>Oui, tout approuver</button>
|
||||
<span>Accepter toutes les modifications de cette proposition ?</span>
|
||||
<button type="button" className="contrib-confirm-yes" onClick={() => { setConfirmApproveAll(null); onReviewSession(session.id, { decision: 'approved' }) }} disabled={isReviewing}>Oui, tout accepter</button>
|
||||
<button type="button" className="contrib-confirm-no" onClick={() => setConfirmApproveAll(null)}>Annuler</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -550,10 +549,10 @@ const ContributionPanel = ({
|
||||
onClick={() => setConfirmApproveAll(session.id)}
|
||||
disabled={isReviewing}
|
||||
>
|
||||
{isReviewing ? 'Traitement...' : 'Tout approuver'}
|
||||
{isReviewing ? 'Un instant…' : 'Tout accepter'}
|
||||
</button>
|
||||
)}
|
||||
<button type="button" className="contrib-quick-btn" onClick={() => onReviewSession(session.id, { decision: 'rejected' })} disabled={isReviewing}>Tout rejeter</button>
|
||||
<button type="button" className="contrib-quick-btn" onClick={() => onReviewSession(session.id, { decision: 'rejected' })} disabled={isReviewing}>Tout refuser</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,337 +1,17 @@
|
||||
/* ============================================================
|
||||
Contribution Session Modal - Finalisation contribution
|
||||
============================================================ */
|
||||
/* Envoi d'une contribution (DA terracotta) : la fenêtre vient de pz.css */
|
||||
.cs-recap { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.cs-tag--added { background: var(--pz-success-bg); color: var(--pz-success); border-color: transparent; }
|
||||
.cs-tag--modified { background: var(--pz-a100); color: var(--pz-a700); border-color: transparent; }
|
||||
.cs-tag--deleted { background: var(--pz-danger-bg); color: var(--pz-danger); border-color: transparent; }
|
||||
|
||||
.contrib-session-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
background: rgba(30, 25, 20, 0.55);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: contribFadeIn 0.2s ease;
|
||||
}
|
||||
.cs-changes { list-style: none; margin: 0; padding: 4px 0; display: grid; max-height: 200px; overflow-y: auto; border-block: 1px solid var(--pz-line-soft); scrollbar-width: thin; scrollbar-color: var(--pz-line) transparent; }
|
||||
.cs-change { display: flex; align-items: center; gap: 10px; min-height: 36px; font: 400 14px/1.35 var(--pz-sans); color: var(--pz-ink); }
|
||||
.cs-dot { width: 8px; height: 8px; flex: none; border-radius: 50%; background: var(--pz-a500); }
|
||||
.cs-dot--create { background: var(--pz-success); }
|
||||
.cs-dot--delete { background: var(--pz-danger); }
|
||||
.cs-change-action { flex: none; width: 96px; font: 500 12.5px var(--pz-sans); color: var(--pz-muted); }
|
||||
.cs-change-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
@keyframes contribFadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.contrib-session-modal {
|
||||
position: relative;
|
||||
width: 440px;
|
||||
max-width: calc(100vw - 32px);
|
||||
max-height: calc(100vh - 64px);
|
||||
overflow-y: auto;
|
||||
padding: 32px 28px 24px;
|
||||
background: rgba(254, 249, 237, 0.97);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(166, 124, 82, 0.2);
|
||||
box-shadow:
|
||||
0 20px 60px rgba(93, 82, 75, 0.25),
|
||||
0 4px 16px rgba(93, 82, 75, 0.1);
|
||||
animation: contribSlideUp 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
@keyframes contribSlideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px) scale(0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.contrib-session-close {
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #8B7355;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.contrib-session-close:hover {
|
||||
background: rgba(93, 82, 75, 0.1);
|
||||
color: #5D524B;
|
||||
}
|
||||
|
||||
.contrib-session-header {
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.contrib-session-icon {
|
||||
color: #FF9500;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.contrib-session-title {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #5D524B;
|
||||
margin: 0 0 6px;
|
||||
}
|
||||
|
||||
.contrib-session-subtitle {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
color: #8B7355;
|
||||
margin: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Récap modifications */
|
||||
.contrib-session-recap {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.contrib-recap-tag {
|
||||
padding: 4px 10px;
|
||||
border-radius: 12px;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
.contrib-recap-tag--added {
|
||||
background: rgba(82, 178, 120, 0.12);
|
||||
color: #3D9A5E;
|
||||
}
|
||||
|
||||
.contrib-recap-tag--modified {
|
||||
background: rgba(255, 149, 0, 0.12);
|
||||
color: #E08600;
|
||||
}
|
||||
|
||||
.contrib-recap-tag--deleted {
|
||||
background: rgba(192, 57, 43, 0.1);
|
||||
color: #C0392B;
|
||||
}
|
||||
|
||||
/* Liste des changements */
|
||||
.contrib-session-changes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 20px;
|
||||
max-height: 180px;
|
||||
overflow-y: auto;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.5);
|
||||
border: 1px solid rgba(166, 124, 82, 0.15);
|
||||
}
|
||||
|
||||
.contrib-session-change-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.contrib-session-change-badge {
|
||||
padding: 2px 7px;
|
||||
border-radius: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.contrib-session-change-badge--create {
|
||||
background: rgba(82, 178, 120, 0.15);
|
||||
color: #3D9A5E;
|
||||
}
|
||||
|
||||
.contrib-session-change-badge--update {
|
||||
background: rgba(255, 149, 0, 0.12);
|
||||
color: #E08600;
|
||||
}
|
||||
|
||||
.contrib-session-change-badge--delete {
|
||||
background: rgba(192, 57, 43, 0.1);
|
||||
color: #C0392B;
|
||||
}
|
||||
|
||||
.contrib-session-change-label {
|
||||
color: #5D524B;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.contrib-session-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.contrib-session-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.contrib-session-label {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #8B7355;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.contrib-session-optional {
|
||||
font-weight: 400;
|
||||
text-transform: none;
|
||||
letter-spacing: 0;
|
||||
color: #B8A898;
|
||||
}
|
||||
|
||||
.contrib-session-input {
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1.5px solid rgba(166, 124, 82, 0.2);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
color: #5D524B;
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.contrib-session-input:focus {
|
||||
border-color: #A67C52;
|
||||
box-shadow: 0 0 0 3px rgba(166, 124, 82, 0.12);
|
||||
}
|
||||
|
||||
.contrib-session-input::placeholder {
|
||||
color: #B8A898;
|
||||
}
|
||||
|
||||
.contrib-session-textarea {
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1.5px solid rgba(166, 124, 82, 0.2);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
color: #5D524B;
|
||||
transition: all 0.2s ease;
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.contrib-session-textarea:focus {
|
||||
border-color: #A67C52;
|
||||
box-shadow: 0 0 0 3px rgba(166, 124, 82, 0.12);
|
||||
}
|
||||
|
||||
.contrib-session-textarea::placeholder {
|
||||
color: #B8A898;
|
||||
}
|
||||
|
||||
.contrib-session-count {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
right: 12px;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 12px;
|
||||
color: #B8A898;
|
||||
}
|
||||
|
||||
.contrib-session-error {
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
background: rgba(192, 57, 43, 0.08);
|
||||
border: 1px solid rgba(192, 57, 43, 0.2);
|
||||
color: #C0392B;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* Actions */
|
||||
.contrib-session-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.contrib-session-submit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
background: #FF9500;
|
||||
color: white;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.contrib-session-submit:hover:not(:disabled) {
|
||||
background: #E08600;
|
||||
box-shadow: 0 4px 16px rgba(255, 149, 0, 0.3);
|
||||
}
|
||||
|
||||
.contrib-session-submit:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.contrib-session-cancel {
|
||||
padding: 10px 20px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #8B7355;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.contrib-session-cancel:hover {
|
||||
background: rgba(93, 82, 75, 0.08);
|
||||
color: #5D524B;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.contrib-session-modal {
|
||||
margin: 16px;
|
||||
padding: 24px 20px 20px;
|
||||
}
|
||||
}
|
||||
.cs-form { display: grid; gap: 16px; }
|
||||
.cs-optional { font-weight: 400; color: var(--pz-faint); margin-left: 4px; }
|
||||
.cs-count { justify-self: end; font-variant-numeric: tabular-nums; }
|
||||
@@ -54,106 +54,70 @@ function ContributionSessionModal({ visible, changes = [], recap = {}, loading =
|
||||
if (!visible) return null
|
||||
|
||||
return (
|
||||
<div className="contrib-session-backdrop" onClick={onCancel}>
|
||||
<div className="pz-overlay" onClick={onCancel}>
|
||||
<div
|
||||
className="contrib-session-modal"
|
||||
className="pz-modal contrib-session-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="contrib-session-title"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="contrib-session-close"
|
||||
onClick={onCancel}
|
||||
aria-label="Fermer"
|
||||
>
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--icon pz-modal-close" onClick={onCancel} aria-label="Fermer">
|
||||
<X size={18} strokeWidth={2} />
|
||||
</button>
|
||||
|
||||
<div className="contrib-session-header">
|
||||
<Send size={24} strokeWidth={1.8} className="contrib-session-icon" />
|
||||
<h2 id="contrib-session-title" className="contrib-session-title">
|
||||
Finaliser votre contribution
|
||||
</h2>
|
||||
<p className="contrib-session-subtitle">
|
||||
Vos modifications seront soumises à validation par le propriétaire de l'arbre.
|
||||
</p>
|
||||
<div className="pz-modal-head">
|
||||
<p className="pz-eyebrow">Votre contribution</p>
|
||||
<h2 id="contrib-session-title" className="pz-title">Envoyer vos <em>modifications</em></h2>
|
||||
<p className="pz-sub">La personne qui gère l'arbre les relit, puis elles apparaissent pour toute la famille.</p>
|
||||
</div>
|
||||
|
||||
{/* Récap modifications */}
|
||||
{totalChanges > 0 && (
|
||||
<div className="contrib-session-recap">
|
||||
{recap.added > 0 && (
|
||||
<span className="contrib-recap-tag contrib-recap-tag--added">
|
||||
{recap.added} ajouté{recap.added > 1 ? 's' : ''}
|
||||
</span>
|
||||
)}
|
||||
{recap.modified > 0 && (
|
||||
<span className="contrib-recap-tag contrib-recap-tag--modified">
|
||||
{recap.modified} modifié{recap.modified > 1 ? 's' : ''}
|
||||
</span>
|
||||
)}
|
||||
{recap.deleted > 0 && (
|
||||
<span className="contrib-recap-tag contrib-recap-tag--deleted">
|
||||
{recap.deleted} supprimé{recap.deleted > 1 ? 's' : ''}
|
||||
</span>
|
||||
)}
|
||||
<div className="cs-recap">
|
||||
{recap.added > 0 && <span className="pz-tag cs-tag cs-tag--added">{recap.added} ajout{recap.added > 1 ? 's' : ''}</span>}
|
||||
{recap.modified > 0 && <span className="pz-tag cs-tag cs-tag--modified">{recap.modified} modification{recap.modified > 1 ? 's' : ''}</span>}
|
||||
{recap.deleted > 0 && <span className="pz-tag cs-tag cs-tag--deleted">{recap.deleted} suppression{recap.deleted > 1 ? 's' : ''}</span>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Liste détaillée des changements */}
|
||||
{changes.length > 0 && (
|
||||
<div className="contrib-session-changes">
|
||||
<ul className="cs-changes">
|
||||
{changes.map((ch, i) => (
|
||||
<div key={i} className="contrib-session-change-row">
|
||||
<span className={`contrib-session-change-badge contrib-session-change-badge--${ch.action}`}>
|
||||
{formatAction(ch.action)}
|
||||
</span>
|
||||
<span className="contrib-session-change-label">{ch.label}</span>
|
||||
</div>
|
||||
<li key={i} className="cs-change">
|
||||
<span className={`cs-dot cs-dot--${ch.action}`} aria-hidden="true" />
|
||||
<span className="cs-change-action">{formatAction(ch.action)}</span>
|
||||
<span className="cs-change-label">{ch.label}</span>
|
||||
</li>
|
||||
))}
|
||||
</div>
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<form className="contrib-session-form" onSubmit={handleSubmit}>
|
||||
<div className="contrib-session-field">
|
||||
<label htmlFor="cs-comment" className="contrib-session-label">
|
||||
Message <span className="contrib-session-optional">(optionnel)</span>
|
||||
<form className="cs-form" onSubmit={handleSubmit}>
|
||||
<div className="pz-field">
|
||||
<label htmlFor="cs-comment">
|
||||
Un mot pour accompagner <span className="cs-optional">facultatif</span>
|
||||
</label>
|
||||
<textarea
|
||||
id="cs-comment"
|
||||
className="contrib-session-textarea"
|
||||
value={comment}
|
||||
onChange={(e) => setComment(e.target.value.slice(0, 500))}
|
||||
placeholder="Ajoutez un mot pour le propriétaire de l'arbre..."
|
||||
placeholder="Par exemple, j'ai ajouté les photos du mariage de mes grands-parents"
|
||||
rows={3}
|
||||
maxLength={500}
|
||||
/>
|
||||
<span className="contrib-session-count">{comment.length}/500</span>
|
||||
<span className="pz-hint cs-count">{comment.length} / 500</span>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="contrib-session-error" role="alert">{error}</div>
|
||||
)}
|
||||
{error && <div className="pz-error" role="alert">{error}</div>}
|
||||
|
||||
<div className="contrib-session-actions">
|
||||
<button
|
||||
type="submit"
|
||||
className="contrib-session-submit"
|
||||
disabled={loading}
|
||||
>
|
||||
<Send size={16} strokeWidth={2} />
|
||||
{loading ? 'Envoi en cours...' : 'Envoyer mes contributions'}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="contrib-session-cancel"
|
||||
onClick={onCancel}
|
||||
disabled={loading}
|
||||
>
|
||||
<div className="pz-modal-actions">
|
||||
<button type="button" className="pz-btn pz-btn--ghost" onClick={onCancel} disabled={loading}>
|
||||
Annuler
|
||||
</button>
|
||||
<button type="submit" className="pz-btn pz-btn--primary" disabled={loading}>
|
||||
<Send size={16} strokeWidth={2} />
|
||||
{loading ? 'Envoi…' : 'Envoyer mes contributions'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -15,19 +15,19 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 13px 22px;
|
||||
border: 1px solid rgba(93, 82, 75, 0.1);
|
||||
border: 1px solid rgba(42, 38, 34, 0.1);
|
||||
border-radius: 50px;
|
||||
background: rgba(247, 236, 217, 0.97);
|
||||
background: rgba(242, 239, 234, 0.97);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow:
|
||||
0 4px 40px rgba(93, 82, 75, 0.22),
|
||||
0 1px 6px rgba(93, 82, 75, 0.1);
|
||||
0 4px 40px rgba(42, 38, 34, 0.22),
|
||||
0 1px 6px rgba(42, 38, 34, 0.1);
|
||||
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #5D524B;
|
||||
color: #2A2622;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -47,10 +47,10 @@
|
||||
}
|
||||
|
||||
.cmt-pill:hover {
|
||||
background: rgba(247, 236, 217, 1);
|
||||
background: rgba(242, 239, 234, 1);
|
||||
box-shadow:
|
||||
0 8px 48px rgba(93, 82, 75, 0.28),
|
||||
0 2px 8px rgba(93, 82, 75, 0.12);
|
||||
0 8px 48px rgba(42, 38, 34, 0.28),
|
||||
0 2px 8px rgba(42, 38, 34, 0.12);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@@ -68,14 +68,14 @@
|
||||
z-index: 1001;
|
||||
|
||||
width: 300px;
|
||||
background: rgba(254, 249, 237, 0.97);
|
||||
background: rgba(255, 255, 255, 0.97);
|
||||
backdrop-filter: blur(28px);
|
||||
-webkit-backdrop-filter: blur(28px);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(166, 124, 82, 0.18);
|
||||
border: 1px solid rgba(147, 64, 42, 0.18);
|
||||
box-shadow:
|
||||
0 12px 48px rgba(93, 82, 75, 0.22),
|
||||
0 2px 10px rgba(93, 82, 75, 0.1);
|
||||
0 12px 48px rgba(42, 38, 34, 0.22),
|
||||
0 2px 10px rgba(42, 38, 34, 0.1);
|
||||
|
||||
transform-origin: bottom right;
|
||||
animation: cmtMorphIn 0.38s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
@@ -104,14 +104,14 @@
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px 18px 12px;
|
||||
border-bottom: 1px solid rgba(166, 124, 82, 0.1);
|
||||
border-bottom: 1px solid rgba(147, 64, 42, 0.1);
|
||||
}
|
||||
|
||||
.cmt-panel-title {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #5D524B;
|
||||
color: #2A2622;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
@@ -123,8 +123,8 @@
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: rgba(93, 82, 75, 0.07);
|
||||
color: #8B7355;
|
||||
background: rgba(42, 38, 34, 0.07);
|
||||
color: #6B645D;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
@@ -132,8 +132,8 @@
|
||||
}
|
||||
|
||||
.cmt-panel-close:hover {
|
||||
background: rgba(93, 82, 75, 0.13);
|
||||
color: #5D524B;
|
||||
background: rgba(42, 38, 34, 0.13);
|
||||
color: #2A2622;
|
||||
}
|
||||
|
||||
/* Body */
|
||||
@@ -143,9 +143,9 @@
|
||||
|
||||
.cmt-label {
|
||||
display: block;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #8B7355;
|
||||
color: #6B645D;
|
||||
margin-bottom: 7px;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
@@ -154,33 +154,33 @@
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(166, 124, 82, 0.25);
|
||||
background: rgba(255, 253, 248, 0.9);
|
||||
color: #453d37;
|
||||
border: 1px solid rgba(147, 64, 42, 0.25);
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #2a2622;
|
||||
padding: 10px 13px;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
font-size: 14px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.cmt-input:focus {
|
||||
border-color: #b99762;
|
||||
box-shadow: 0 0 0 3px rgba(185, 151, 98, 0.12);
|
||||
border-color: #d2694a;
|
||||
box-shadow: 0 0 0 3px rgba(210, 105, 74, 0.12);
|
||||
}
|
||||
|
||||
.cmt-slug-preview {
|
||||
margin-top: 8px;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
font-size: 11px;
|
||||
color: #A08060;
|
||||
color: #93402A;
|
||||
}
|
||||
|
||||
.cmt-slug-preview code {
|
||||
background: rgba(166, 124, 82, 0.1);
|
||||
background: rgba(147, 64, 42, 0.1);
|
||||
border-radius: 4px;
|
||||
padding: 1px 5px;
|
||||
color: #6a4f32;
|
||||
color: #2a2622;
|
||||
}
|
||||
|
||||
.cmt-error {
|
||||
@@ -190,7 +190,7 @@
|
||||
color: #862f2f;
|
||||
border-radius: 10px;
|
||||
padding: 9px 12px;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@@ -200,9 +200,9 @@
|
||||
padding: 11px 16px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
background: linear-gradient(135deg, #8b6a43, #6a4f32);
|
||||
color: #fef9ed;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
background: #2a2622;
|
||||
color: #ffffff;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
@@ -210,8 +210,8 @@
|
||||
}
|
||||
|
||||
.cmt-btn-create:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #9c7a52, #7a5b3a);
|
||||
box-shadow: 0 4px 14px rgba(166, 124, 82, 0.32);
|
||||
background: linear-gradient(135deg, #93402a, #2a2622);
|
||||
box-shadow: 0 4px 14px rgba(147, 64, 42, 0.32);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,127 +1,54 @@
|
||||
/* ============================================================
|
||||
Draft Restore Banner - Restauration brouillon au reload
|
||||
============================================================ */
|
||||
|
||||
/* Brouillon retrouvé au rechargement (DA terracotta) : bandeau sombre en haut au centre */
|
||||
.draft-restore-banner {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
top: 18px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1002;
|
||||
animation: draftBannerSlideDown 0.5s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
max-width: calc(100vw - 140px);
|
||||
transform: translateX(-50%);
|
||||
animation: draft-in 280ms var(--pz-ease) both;
|
||||
}
|
||||
|
||||
@keyframes draftBannerSlideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-24px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
@keyframes draft-in {
|
||||
from { opacity: 0; transform: translate(-50%, -8px); }
|
||||
to { opacity: 1; transform: translate(-50%, 0); }
|
||||
}
|
||||
|
||||
.draft-restore-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 12px 18px;
|
||||
background: rgba(254, 249, 237, 0.97);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border-radius: 50px;
|
||||
border: 1px solid rgba(166, 124, 82, 0.25);
|
||||
box-shadow:
|
||||
0 8px 28px rgba(93, 82, 75, 0.18),
|
||||
0 2px 8px rgba(93, 82, 75, 0.08);
|
||||
padding: 6px 6px 6px 18px;
|
||||
background: var(--pz-ink);
|
||||
color: var(--pz-bg);
|
||||
border-radius: 999px;
|
||||
box-shadow: var(--pz-shadow-lg);
|
||||
}
|
||||
|
||||
.draft-restore-text {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #5D524B;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.draft-restore-text { display: grid; gap: 1px; font: 500 14px/1.3 var(--pz-sans); }
|
||||
.draft-restore-time { font: 400 12px var(--pz-sans); color: rgba(251, 250, 247, .65); }
|
||||
|
||||
.draft-restore-time {
|
||||
font-weight: 400;
|
||||
color: #8B7355;
|
||||
}
|
||||
|
||||
.draft-restore-actions {
|
||||
display: flex;
|
||||
.draft-restore-actions { display: flex; gap: 4px; flex: none; }
|
||||
.draft-restore-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.draft-restore-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
border: none;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
min-height: 36px;
|
||||
padding: 0 14px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
font: 500 13.5px var(--pz-sans);
|
||||
transition: background-color 160ms ease, transform 140ms var(--pz-ease);
|
||||
}
|
||||
|
||||
.draft-restore-btn:focus-visible {
|
||||
outline: 2px solid #A67C52;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Restaurer - primary */
|
||||
.draft-restore-btn--restore {
|
||||
background: #A67C52;
|
||||
color: #FEF9ED;
|
||||
}
|
||||
|
||||
.draft-restore-btn--restore:hover {
|
||||
background: #96703E;
|
||||
box-shadow: 0 2px 8px rgba(166, 124, 82, 0.3);
|
||||
}
|
||||
|
||||
/* Ignorer - ghost */
|
||||
.draft-restore-btn--discard {
|
||||
background: rgba(93, 82, 75, 0.08);
|
||||
color: #8B7355;
|
||||
}
|
||||
|
||||
.draft-restore-btn--discard:hover {
|
||||
background: rgba(93, 82, 75, 0.15);
|
||||
color: #5D524B;
|
||||
.draft-restore-btn:active { transform: scale(.97); }
|
||||
.draft-restore-btn--restore { background: var(--pz-a100); color: var(--pz-a800); }
|
||||
.draft-restore-btn--discard { background: transparent; color: rgba(251, 250, 247, .8); }
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.draft-restore-btn--restore:hover { background: var(--pz-a200); }
|
||||
.draft-restore-btn--discard:hover { background: rgba(251, 250, 247, .1); color: var(--pz-bg); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.draft-restore-banner {
|
||||
top: 12px;
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
@keyframes draftBannerSlideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-24px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.draft-restore-content {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
border-radius: 16px;
|
||||
padding: 10px 14px;
|
||||
gap: 8px;
|
||||
}
|
||||
.draft-restore-banner { top: 68px; max-width: calc(100vw - 24px); width: calc(100vw - 24px); }
|
||||
.draft-restore-content { border-radius: 20px; flex-wrap: wrap; padding: 12px 12px 10px 16px; }
|
||||
}
|
||||
@@ -40,8 +40,8 @@ function DraftRestoreBanner({ visible, draftTimestamp, onRestore, onDiscard }) {
|
||||
<div className="draft-restore-banner" role="alert" aria-live="polite">
|
||||
<div className="draft-restore-content">
|
||||
<span className="draft-restore-text">
|
||||
Un brouillon a été trouvé
|
||||
{timeAgo && <span className="draft-restore-time"> ({timeAgo})</span>}
|
||||
Des modifications non enregistrées vous attendent
|
||||
{timeAgo && <span className="draft-restore-time">{timeAgo}</span>}
|
||||
</span>
|
||||
|
||||
<div className="draft-restore-actions">
|
||||
|
||||
@@ -1,142 +1,15 @@
|
||||
/* ============================================================
|
||||
Edit Confirm Modal - Confirmation désactivation mode édition
|
||||
============================================================ */
|
||||
|
||||
.edit-confirm-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
background: rgba(30, 25, 20, 0.55);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
/* Confirmation à la sortie du mode édition (DA terracotta) : la fenêtre vient de pz.css */
|
||||
.edit-confirm-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: confirmFadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
@keyframes confirmFadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.edit-confirm-modal {
|
||||
width: 380px;
|
||||
max-width: calc(100vw - 32px);
|
||||
padding: 32px 28px 24px;
|
||||
background: rgba(254, 249, 237, 0.97);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(166, 124, 82, 0.2);
|
||||
box-shadow:
|
||||
0 20px 60px rgba(93, 82, 75, 0.25),
|
||||
0 4px 16px rgba(93, 82, 75, 0.1);
|
||||
text-align: center;
|
||||
animation: confirmSlideUp 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
@keyframes confirmSlideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px) scale(0.97);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.edit-confirm-icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
background: rgba(255, 149, 0, 0.1);
|
||||
color: #E08600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.edit-confirm-title {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #5D524B;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
.edit-confirm-message {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
color: #8B7355;
|
||||
margin: 0 0 24px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.edit-confirm-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.edit-confirm-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
@media (max-width: 560px) {
|
||||
.edit-confirm-modal .pz-modal-actions--split { flex-direction: column-reverse; align-items: stretch; }
|
||||
.edit-confirm-right { flex-direction: column-reverse; }
|
||||
.edit-confirm-modal .pz-btn { width: 100%; }
|
||||
}
|
||||
|
||||
.edit-confirm-btn:focus-visible {
|
||||
outline: 2px solid #A67C52;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Continuer l'édition - primary outline */
|
||||
.edit-confirm-btn--continue {
|
||||
background: rgba(166, 124, 82, 0.08);
|
||||
color: #A67C52;
|
||||
border: 1.5px solid rgba(166, 124, 82, 0.25);
|
||||
}
|
||||
|
||||
.edit-confirm-btn--continue:hover {
|
||||
background: rgba(166, 124, 82, 0.15);
|
||||
border-color: rgba(166, 124, 82, 0.4);
|
||||
}
|
||||
|
||||
/* Enregistrer et quitter - primary solid */
|
||||
.edit-confirm-btn--save {
|
||||
background: #A67C52;
|
||||
color: #FEF9ED;
|
||||
}
|
||||
|
||||
.edit-confirm-btn--save:hover {
|
||||
background: #96703E;
|
||||
box-shadow: 0 4px 12px rgba(166, 124, 82, 0.3);
|
||||
}
|
||||
|
||||
/* Abandonner - danger text */
|
||||
.edit-confirm-btn--discard {
|
||||
background: transparent;
|
||||
color: #C0392B;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.edit-confirm-btn--discard:hover {
|
||||
background: rgba(192, 57, 43, 0.08);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.edit-confirm-modal {
|
||||
padding: 24px 20px 20px;
|
||||
}
|
||||
}
|
||||
.edit-confirm-modal { width: min(100%, 580px); }
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { AlertTriangle, Save, X, ArrowLeft } from 'lucide-react'
|
||||
import { Save, X, ArrowLeft } from 'lucide-react'
|
||||
import './EditConfirmModal.css'
|
||||
|
||||
/**
|
||||
@@ -33,54 +33,35 @@ function EditConfirmModal({ visible, onContinue, onSaveAndQuit, onDiscard }) {
|
||||
if (!visible) return null
|
||||
|
||||
return (
|
||||
<div className="edit-confirm-backdrop" onClick={onContinue}>
|
||||
<div className="pz-overlay" onClick={onContinue}>
|
||||
<div
|
||||
className="edit-confirm-modal"
|
||||
className="pz-modal edit-confirm-modal"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="edit-confirm-title"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="edit-confirm-icon">
|
||||
<AlertTriangle size={28} strokeWidth={1.8} />
|
||||
<div className="pz-modal-head">
|
||||
<p className="pz-eyebrow">Modifications en cours</p>
|
||||
<h2 id="edit-confirm-title" className="pz-title">Quitter <em>l'édition</em> ?</h2>
|
||||
<p className="pz-sub">Vos dernières modifications ne sont pas encore enregistrées. Elles restent sur cet appareil tant que vous ne les abandonnez pas.</p>
|
||||
</div>
|
||||
|
||||
<h2 id="edit-confirm-title" className="edit-confirm-title">
|
||||
Quitter le mode édition ?
|
||||
</h2>
|
||||
|
||||
<p className="edit-confirm-message">
|
||||
Vous avez des modifications non enregistrées sur le serveur.
|
||||
</p>
|
||||
|
||||
<div className="edit-confirm-actions">
|
||||
<button
|
||||
ref={continueRef}
|
||||
type="button"
|
||||
className="edit-confirm-btn edit-confirm-btn--continue"
|
||||
onClick={onContinue}
|
||||
>
|
||||
<ArrowLeft size={16} strokeWidth={2} />
|
||||
Continuer l'édition
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="edit-confirm-btn edit-confirm-btn--save"
|
||||
onClick={onSaveAndQuit}
|
||||
>
|
||||
<Save size={16} strokeWidth={2} />
|
||||
Enregistrer et quitter
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="edit-confirm-btn edit-confirm-btn--discard"
|
||||
onClick={onDiscard}
|
||||
>
|
||||
<div className="pz-modal-actions pz-modal-actions--split">
|
||||
<button type="button" className="pz-btn pz-btn--danger-ghost" onClick={onDiscard}>
|
||||
<X size={16} strokeWidth={2} />
|
||||
Abandonner
|
||||
</button>
|
||||
<div className="edit-confirm-right">
|
||||
<button ref={continueRef} type="button" className="pz-btn pz-btn--secondary" onClick={onContinue}>
|
||||
<ArrowLeft size={16} strokeWidth={2} />
|
||||
Continuer
|
||||
</button>
|
||||
<button type="button" className="pz-btn pz-btn--primary" onClick={onSaveAndQuit}>
|
||||
<Save size={16} strokeWidth={2} />
|
||||
Enregistrer et quitter
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,137 +1,58 @@
|
||||
/* Mode édition (DA terracotta) : un liseré autour de l'écran et une pastille en haut */
|
||||
.edit-mode-border {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 999;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.edit-mode-border--contributor {
|
||||
border-color: rgba(255, 149, 0, 0.58);
|
||||
box-shadow:
|
||||
inset 0 0 32px rgba(255, 149, 0, 0.10),
|
||||
0 0 32px rgba(255, 149, 0, 0.14);
|
||||
animation: editPulseContributor 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.edit-mode-border--admin {
|
||||
border-color: rgba(82, 178, 120, 0.52);
|
||||
box-shadow:
|
||||
inset 0 0 32px rgba(82, 178, 120, 0.10),
|
||||
0 0 32px rgba(82, 178, 120, 0.13);
|
||||
animation: editPulseAdmin 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes editPulseContributor {
|
||||
0%, 100% {
|
||||
border-color: rgba(255, 149, 0, 0.58);
|
||||
}
|
||||
50% {
|
||||
border-color: rgba(255, 149, 0, 0.72);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes editPulseAdmin {
|
||||
0%, 100% {
|
||||
border-color: rgba(82, 178, 120, 0.52);
|
||||
}
|
||||
50% {
|
||||
border-color: rgba(82, 178, 120, 0.68);
|
||||
}
|
||||
pointer-events: none;
|
||||
box-shadow: inset 0 0 0 2px var(--pz-a500);
|
||||
animation: pz-fade 240ms ease both;
|
||||
}
|
||||
|
||||
.edit-mode-toast {
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
top: 18px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1001;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
padding: 9px 12px;
|
||||
max-width: calc(100vw - 140px);
|
||||
padding: 5px 5px 5px 16px;
|
||||
background: var(--pz-surface);
|
||||
border: 1px solid var(--pz-line);
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 149, 0, 0.28);
|
||||
background: rgba(247, 236, 217, 0.96);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
box-shadow: 0 6px 20px rgba(93, 82, 75, 0.12);
|
||||
box-shadow: var(--pz-shadow-lg);
|
||||
transform: translateX(-50%);
|
||||
animation: edit-toast-in 260ms var(--pz-ease) both;
|
||||
}
|
||||
@keyframes edit-toast-in {
|
||||
from { opacity: 0; transform: translate(-50%, -8px); }
|
||||
to { opacity: 1; transform: translate(-50%, 0); }
|
||||
}
|
||||
|
||||
.edit-mode-toast--admin {
|
||||
border-color: rgba(82, 178, 120, 0.3);
|
||||
.edit-mode-toast-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
flex: none;
|
||||
border-radius: 50%;
|
||||
background: var(--pz-a500);
|
||||
box-shadow: 0 0 0 4px var(--pz-a100);
|
||||
}
|
||||
|
||||
.edit-mode-toast-text {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #5D524B;
|
||||
font: 500 14px var(--pz-sans);
|
||||
color: var(--pz-ink);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.edit-mode-submit {
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
padding: 6px 12px;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: #FEF9ED;
|
||||
background: #A67C52;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.edit-mode-submit:hover:not(:disabled) {
|
||||
background: #8E6840;
|
||||
}
|
||||
|
||||
.edit-mode-submit:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.edit-mode-submit--admin {
|
||||
background: #52B278;
|
||||
}
|
||||
|
||||
.edit-mode-submit--admin:hover:not(:disabled) {
|
||||
background: #439A66;
|
||||
}
|
||||
|
||||
.edit-mode-toast-close {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
color: #7C6B5C;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.edit-mode-toast-close:hover {
|
||||
background: rgba(93, 82, 75, 0.12);
|
||||
color: #5D524B;
|
||||
}
|
||||
.edit-mode-submit { flex: none; }
|
||||
.edit-mode-toast-close { width: 36px; min-height: 36px; flex: none; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.edit-mode-toast {
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
transform: none;
|
||||
border-radius: 12px;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.edit-mode-toast { top: 68px; max-width: calc(100vw - 24px); }
|
||||
.edit-mode-toast-text { font-size: 13px; }
|
||||
}
|
||||
|
||||
.edit-mode-toast-text {
|
||||
min-width: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.edit-mode-toast { animation: pz-fade 160ms ease both; }
|
||||
}
|
||||
@@ -9,7 +9,7 @@ function EditSessionBar({ active, hasDraft, role = 'contributor', onDeactivate,
|
||||
`edit-mode-border ${isAdmin ? 'edit-mode-border--admin' : 'edit-mode-border--contributor'}`
|
||||
), [isAdmin])
|
||||
|
||||
const toastLabel = isAdmin ? 'Mode édition actif' : 'Mode contribution actif'
|
||||
const toastLabel = isAdmin ? "Vous modifiez l'arbre" : 'Vous proposez des modifications'
|
||||
const submitLabel = isAdmin ? "Mettre à jour l'arbre" : 'Envoyer les modifications'
|
||||
|
||||
if (!active) return null
|
||||
@@ -19,15 +19,16 @@ function EditSessionBar({ active, hasDraft, role = 'contributor', onDeactivate,
|
||||
<div className={borderClass} aria-hidden="true" />
|
||||
|
||||
<div className={`edit-mode-toast ${isAdmin ? 'edit-mode-toast--admin' : ''}`} role="status" aria-live="polite">
|
||||
<span className="edit-mode-toast-dot" aria-hidden="true" />
|
||||
<span className="edit-mode-toast-text">{toastLabel}</span>
|
||||
|
||||
{onSubmit && (
|
||||
<button
|
||||
type="button"
|
||||
className={`edit-mode-submit ${isAdmin ? 'edit-mode-submit--admin' : ''}`}
|
||||
className="pz-btn pz-btn--primary pz-btn--sm edit-mode-submit"
|
||||
onClick={onSubmit}
|
||||
disabled={!hasDraft}
|
||||
title={hasDraft ? submitLabel : 'Aucune modification à soumettre'}
|
||||
title={hasDraft ? submitLabel : "Aucune modification à envoyer pour l'instant"}
|
||||
>
|
||||
{submitLabel}
|
||||
</button>
|
||||
@@ -35,11 +36,11 @@ function EditSessionBar({ active, hasDraft, role = 'contributor', onDeactivate,
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="edit-mode-toast-close"
|
||||
className="pz-btn pz-btn--ghost pz-btn--icon edit-mode-toast-close"
|
||||
onClick={onDeactivate}
|
||||
aria-label="Quitter le mode édition"
|
||||
>
|
||||
<X size={16} strokeWidth={2} />
|
||||
<X size={17} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -28,15 +28,15 @@ class ErrorBoundary extends Component {
|
||||
justifyContent: 'center',
|
||||
height: '100vh',
|
||||
fontFamily: 'system-ui, sans-serif',
|
||||
color: '#4a3728',
|
||||
background: '#faf6f1',
|
||||
color: '#2a2622',
|
||||
background: '#f7f5f4',
|
||||
padding: '2rem',
|
||||
textAlign: 'center',
|
||||
}}>
|
||||
<h1 style={{ fontSize: '1.5rem', marginBottom: '0.5rem' }}>
|
||||
Une erreur inattendue est survenue
|
||||
</h1>
|
||||
<p style={{ color: '#8a7568', marginBottom: '1.5rem', maxWidth: '400px' }}>
|
||||
<p style={{ color: '#6b645d', marginBottom: '1.5rem', maxWidth: '400px' }}>
|
||||
L application a rencontre un probleme. Vous pouvez essayer de recharger ou revenir a l ecran precedent.
|
||||
</p>
|
||||
<div style={{ display: 'flex', gap: '0.75rem' }}>
|
||||
@@ -46,9 +46,9 @@ class ErrorBoundary extends Component {
|
||||
style={{
|
||||
padding: '0.5rem 1.25rem',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid #c4b5a8',
|
||||
border: '1px solid #bab6b2',
|
||||
background: 'white',
|
||||
color: '#4a3728',
|
||||
color: '#2a2622',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.9rem',
|
||||
}}
|
||||
@@ -62,7 +62,7 @@ class ErrorBoundary extends Component {
|
||||
padding: '0.5rem 1.25rem',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
background: '#4a3728',
|
||||
background: '#2a2622',
|
||||
color: 'white',
|
||||
cursor: 'pointer',
|
||||
fontSize: '0.9rem',
|
||||
@@ -75,10 +75,10 @@ class ErrorBoundary extends Component {
|
||||
<pre style={{
|
||||
marginTop: '2rem',
|
||||
padding: '1rem',
|
||||
background: '#f0ebe4',
|
||||
background: '#f8e4db',
|
||||
borderRadius: '6px',
|
||||
fontSize: '0.75rem',
|
||||
color: '#8a7568',
|
||||
color: '#6b645d',
|
||||
maxWidth: '600px',
|
||||
overflow: 'auto',
|
||||
textAlign: 'left',
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
transform: translateX(-50%);
|
||||
z-index: 500;
|
||||
|
||||
background: rgba(93, 82, 75, 0.88);
|
||||
background: rgba(42, 38, 34, 0.88);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
color: #FEF9ED;
|
||||
color: #FFFFFF;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-family: "Red Hat Mono", ui-monospace, monospace;
|
||||
font-family: "DM Sans", system-ui, sans-serif;
|
||||
padding: 8px 20px;
|
||||
border-radius: 20px;
|
||||
white-space: nowrap;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
display: grid;
|
||||
place-items: center;
|
||||
position: relative;
|
||||
background: #efe8db;
|
||||
background: #f8e4db;
|
||||
}
|
||||
|
||||
.geo-media-thumb__svg {
|
||||
@@ -16,14 +16,14 @@
|
||||
}
|
||||
|
||||
.geo-media-thumb__status {
|
||||
color: rgba(93, 82, 75, 0.55);
|
||||
color: rgba(42, 38, 34, 0.55);
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.geo-media-thumb__fallback {
|
||||
color: rgba(93, 82, 75, 0.75);
|
||||
color: rgba(42, 38, 34, 0.75);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.8px;
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(93, 82, 75, 0.4);
|
||||
background: rgba(42, 38, 34, 0.4);
|
||||
backdrop-filter: blur(4px);
|
||||
z-index: 1200;
|
||||
animation: overlayFadeIn 0.2s ease;
|
||||
@@ -26,15 +26,15 @@
|
||||
.inline-person-form {
|
||||
position: fixed;
|
||||
padding: 24px;
|
||||
background: rgba(247, 236, 217, 0.98);
|
||||
background: rgba(242, 239, 234, 0.98);
|
||||
backdrop-filter: blur(32px);
|
||||
-webkit-backdrop-filter: blur(32px);
|
||||
border-radius: 16px;
|
||||
border: 1px solid rgba(93, 82, 75, 0.12);
|
||||
border: 1px solid rgba(42, 38, 34, 0.12);
|
||||
|
||||
box-shadow:
|
||||
0 12px 48px rgba(93, 82, 75, 0.3),
|
||||
0 4px 16px rgba(93, 82, 75, 0.15);
|
||||
0 12px 48px rgba(42, 38, 34, 0.3),
|
||||
0 4px 16px rgba(42, 38, 34, 0.15);
|
||||
|
||||
min-width: 320px;
|
||||
max-width: 400px;
|
||||
@@ -62,14 +62,14 @@
|
||||
justify-content: space-between;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid rgba(93, 82, 75, 0.1);
|
||||
border-bottom: 1px solid rgba(42, 38, 34, 0.1);
|
||||
}
|
||||
|
||||
.inline-person-form-title {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #5D524B;
|
||||
color: #2A2622;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
@@ -85,7 +85,7 @@
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #8B7355;
|
||||
color: #6B645D;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -95,8 +95,8 @@
|
||||
}
|
||||
|
||||
.inline-person-form-close:hover {
|
||||
background: rgba(93, 82, 75, 0.1);
|
||||
color: #5D524B;
|
||||
background: rgba(42, 38, 34, 0.1);
|
||||
color: #2A2622;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
@@ -123,8 +123,8 @@
|
||||
}
|
||||
|
||||
.inline-person-form-badge.sibling {
|
||||
background: rgba(255, 149, 0, 0.15);
|
||||
color: #FF9500;
|
||||
background: rgba(210, 105, 74, 0.15);
|
||||
color: #93402A;
|
||||
}
|
||||
|
||||
/* Fields */
|
||||
@@ -144,7 +144,7 @@
|
||||
.inline-person-form-label {
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #5D524B;
|
||||
color: #2A2622;
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
@@ -156,23 +156,23 @@
|
||||
.inline-person-form-input {
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
border: 1.5px solid rgba(93, 82, 75, 0.15);
|
||||
border: 1.5px solid rgba(42, 38, 34, 0.15);
|
||||
background: rgba(255, 255, 255, 0.6);
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
font-size: 16px; /* ≥16px prevents iOS Safari from auto-zooming on focus */
|
||||
color: #5D524B;
|
||||
color: #2A2622;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.inline-person-form-input:focus {
|
||||
outline: none;
|
||||
border-color: #A67C52;
|
||||
border-color: #93402A;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
box-shadow: 0 0 0 3px rgba(166, 124, 82, 0.1);
|
||||
box-shadow: 0 0 0 3px rgba(147, 64, 42, 0.1);
|
||||
}
|
||||
|
||||
.inline-person-form-input::placeholder {
|
||||
color: #8B7355;
|
||||
color: #6B645D;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
@@ -194,7 +194,7 @@
|
||||
padding: 10px 20px;
|
||||
border-radius: 10px;
|
||||
border: none;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
cursor: pointer;
|
||||
@@ -203,23 +203,23 @@
|
||||
|
||||
.inline-person-form-button.cancel {
|
||||
background: transparent;
|
||||
color: #8B7355;
|
||||
color: #6B645D;
|
||||
}
|
||||
|
||||
.inline-person-form-button.cancel:hover {
|
||||
background: rgba(93, 82, 75, 0.08);
|
||||
color: #5D524B;
|
||||
background: rgba(42, 38, 34, 0.08);
|
||||
color: #2A2622;
|
||||
}
|
||||
|
||||
.inline-person-form-button.submit {
|
||||
background: linear-gradient(135deg, #A67C52, #8B6843);
|
||||
color: #FEF9ED;
|
||||
box-shadow: 0 4px 12px rgba(166, 124, 82, 0.3);
|
||||
background: #2a2622;
|
||||
color: #FFFFFF;
|
||||
box-shadow: 0 4px 12px rgba(147, 64, 42, 0.3);
|
||||
}
|
||||
|
||||
.inline-person-form-button.submit:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(166, 124, 82, 0.4);
|
||||
box-shadow: 0 6px 16px rgba(147, 64, 42, 0.4);
|
||||
}
|
||||
|
||||
.inline-person-form-button.submit:active {
|
||||
|
||||
@@ -1,262 +1,68 @@
|
||||
.media-manager-modal-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2200;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 20px;
|
||||
background: rgba(35, 27, 21, 0.52);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
/* Souvenirs d'une personne : ajouter, ranger, supprimer (DA terracotta). La fenêtre vient de pz.css */
|
||||
.media-manager-modal-overlay { z-index: 2200; }
|
||||
|
||||
.media-manager-modal-card {
|
||||
width: min(820px, 100%);
|
||||
max-height: calc(100vh - 40px);
|
||||
overflow: auto;
|
||||
.mm-form { display: grid; gap: 16px; }
|
||||
.mm-optional { font-weight: 400; color: var(--pz-faint); margin-left: 4px; }
|
||||
|
||||
.mm-types { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 6px; }
|
||||
@media (max-width: 640px) { .mm-types { grid-template-columns: repeat(auto-fill, minmax(92px, 1fr)); } }
|
||||
.mm-type {
|
||||
display: grid;
|
||||
justify-items: center;
|
||||
gap: 6px;
|
||||
padding: 12px 6px 10px;
|
||||
border: 1px solid var(--pz-line);
|
||||
border-radius: var(--pz-r-m);
|
||||
background: var(--pz-surface);
|
||||
color: var(--pz-ink-2);
|
||||
font: 500 12.5px var(--pz-sans);
|
||||
cursor: pointer;
|
||||
transition: border-color 160ms ease, background-color 160ms ease, color 160ms ease, transform 140ms var(--pz-ease);
|
||||
}
|
||||
.mm-type:active { transform: scale(.97); }
|
||||
.mm-type.is-active { border-color: var(--pz-a500); background: var(--pz-a50); color: var(--pz-a700); box-shadow: 0 0 0 3px var(--pz-a100); }
|
||||
@media (hover: hover) and (pointer: fine) { .mm-type:hover:not(.is-active) { border-color: var(--pz-a300); } }
|
||||
|
||||
.mm-drop-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
|
||||
.mm-drop {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(166, 124, 82, 0.25);
|
||||
background:
|
||||
radial-gradient(circle at top right, rgba(255, 244, 227, 0.65), transparent 60%),
|
||||
#fffdf8;
|
||||
padding: 18px;
|
||||
box-shadow:
|
||||
0 24px 50px rgba(32, 22, 15, 0.42),
|
||||
0 2px 10px rgba(32, 22, 15, 0.18);
|
||||
}
|
||||
|
||||
.media-manager-modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.media-manager-modal-title-wrap h1 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
color: #453d37;
|
||||
}
|
||||
|
||||
.media-manager-modal-title-wrap p {
|
||||
margin: 4px 0 0;
|
||||
color: #7a6d62;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.media-manager-close {
|
||||
border: 1px solid #ccb79a;
|
||||
border-radius: 10px;
|
||||
padding: 9px 12px;
|
||||
background: transparent;
|
||||
color: #6a4f32;
|
||||
font-weight: 700;
|
||||
padding: 14px;
|
||||
border: 1.5px dashed var(--pz-a300);
|
||||
border-radius: var(--pz-r-m);
|
||||
background: var(--pz-a50);
|
||||
cursor: pointer;
|
||||
transition: background-color 160ms ease;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) { .mm-drop:hover { background: var(--pz-a100); } }
|
||||
.mm-drop-input:focus-visible + .mm-drop { outline: 2px solid var(--pz-a500); outline-offset: 2px; }
|
||||
.mm-drop-icon { width: 40px; height: 40px; flex: none; display: grid; place-items: center; border-radius: 12px; background: var(--pz-surface); color: var(--pz-a700); }
|
||||
.mm-drop-text { display: grid; gap: 2px; min-width: 0; }
|
||||
.mm-drop-text strong { font: 500 14px var(--pz-sans); color: var(--pz-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.mm-drop-text span { font: 400 12.5px var(--pz-sans); color: var(--pz-muted); }
|
||||
|
||||
.media-manager-close:hover {
|
||||
background: rgba(166, 124, 82, 0.1);
|
||||
}
|
||||
|
||||
.media-manager-modal-form {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
border: 1px solid rgba(166, 124, 82, 0.2);
|
||||
border-radius: 12px;
|
||||
padding: 12px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
}
|
||||
|
||||
.media-manager-modal-form label {
|
||||
font-size: 14px;
|
||||
color: #6e6258;
|
||||
}
|
||||
|
||||
.media-manager-modal-form input,
|
||||
.media-manager-modal-form select {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #d9cdb8;
|
||||
background: #fffdf8;
|
||||
color: #453d37;
|
||||
padding: 10px 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.media-manager-modal-textarea {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #d9cdb8;
|
||||
background: #fffdf8;
|
||||
color: #453d37;
|
||||
padding: 10px 12px;
|
||||
outline: none;
|
||||
resize: vertical;
|
||||
min-height: 96px;
|
||||
}
|
||||
|
||||
.media-manager-modal-form input:focus,
|
||||
.media-manager-modal-form select:focus,
|
||||
.media-manager-modal-textarea:focus {
|
||||
border-color: #b99762;
|
||||
box-shadow: 0 0 0 2px rgba(185, 151, 98, 0.15);
|
||||
}
|
||||
|
||||
.media-manager-video-mode {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.media-manager-video-mode label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.media-manager-video-mode input[type="radio"] {
|
||||
width: auto;
|
||||
margin: 0;
|
||||
accent-color: #8b6a43;
|
||||
}
|
||||
|
||||
.media-manager-modal-form button {
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
font-weight: 700;
|
||||
color: #fef9ed;
|
||||
background: linear-gradient(135deg, #8b6a43, #6a4f32);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.media-manager-modal-form button:disabled,
|
||||
.media-manager-modal-actions button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.media-manager-modal-error {
|
||||
background: rgba(171, 49, 49, 0.1);
|
||||
border: 1px solid rgba(171, 49, 49, 0.32);
|
||||
color: #862f2f;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.media-manager-modal-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.media-manager-modal-item {
|
||||
display: grid;
|
||||
grid-template-columns: 74px 1fr auto;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border: 1px solid #d8c7ac;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.media-manager-modal-preview {
|
||||
width: 74px;
|
||||
height: 74px;
|
||||
border-radius: 8px;
|
||||
background: #f3ecdf;
|
||||
overflow: hidden;
|
||||
.mm-list-wrap { display: grid; gap: 10px; padding-top: 18px; border-top: 1px solid var(--pz-line); }
|
||||
.mm-empty { padding: 4px 0; }
|
||||
.mm-list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
|
||||
.mm-item { display: flex; align-items: center; gap: 12px; padding: 6px 6px 6px 6px; border: 1px solid var(--pz-line); border-radius: var(--pz-r-m); }
|
||||
.mm-preview {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
flex: none;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #6e6258;
|
||||
font-size: 14px;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.media-manager-modal-preview img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.media-manager-modal-preview-map {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.media-manager-modal-meta {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
color: #5a4d44;
|
||||
}
|
||||
|
||||
.media-manager-modal-meta span {
|
||||
color: #7a6f65;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.media-manager-modal-meta a {
|
||||
color: #6a4f32;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.media-manager-modal-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.media-manager-action-ghost {
|
||||
border: 1px solid #ccb79a;
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
background: transparent;
|
||||
color: #6a4f32;
|
||||
font-weight: 450;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.media-manager-action-ghost:hover:not(:disabled) {
|
||||
background: rgba(166, 124, 82, 0.1);
|
||||
}
|
||||
|
||||
.media-manager-action-danger {
|
||||
border: 1px solid rgba(192, 57, 43, 0.22);
|
||||
border-radius: 8px;
|
||||
padding: 8px 10px;
|
||||
background: rgba(192, 57, 43, 0.1);
|
||||
color: #b13a2f;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.media-manager-action-danger:hover:not(:disabled) {
|
||||
background: rgba(192, 57, 43, 0.18);
|
||||
}
|
||||
|
||||
.media-manager-modal-empty {
|
||||
color: #6e6258;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px dashed #d8c7ac;
|
||||
overflow: hidden;
|
||||
border-radius: 10px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.media-manager-modal-item {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.media-manager-modal-actions {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.media-manager-modal-actions button {
|
||||
flex: 1;
|
||||
}
|
||||
background: var(--pz-a50);
|
||||
color: var(--pz-a700);
|
||||
font: 500 12px var(--pz-sans);
|
||||
}
|
||||
.mm-preview img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.media-manager-modal-preview-map { width: 100%; height: 100%; }
|
||||
.mm-meta { flex: 1; display: grid; gap: 2px; min-width: 0; }
|
||||
.mm-meta strong { font: 500 14px/1.3 var(--pz-sans); color: var(--pz-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.mm-meta span { font: 400 12.5px var(--pz-sans); color: var(--pz-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.mm-meta a { color: var(--pz-a700); }
|
||||
.mm-actions { display: flex; gap: 0; flex: none; }
|
||||
.mm-actions .pz-btn--icon { width: 36px; min-height: 36px; }
|
||||
@@ -1,8 +1,21 @@
|
||||
import { useMemo, useState } from 'react'
|
||||
import { ArrowDown, ArrowUp, FileText, Image, MapPin, Mic, Plus, Quote, Route, Trash2, Upload, Video, X } from 'lucide-react'
|
||||
import { getAcceptForMediaType } from '../utils/mediaUpload'
|
||||
import GeoMediaThumbnail from './GeoMediaThumbnail'
|
||||
import './MediaManagerPanel.css'
|
||||
|
||||
const MEDIA_TYPES = [
|
||||
{ value: 'photo', label: 'Photo', Icon: Image },
|
||||
{ value: 'video', label: 'Vidéo', Icon: Video },
|
||||
{ value: 'audio', label: 'Voix', Icon: Mic },
|
||||
{ value: 'citation', label: 'Citation', Icon: Quote },
|
||||
{ value: 'document', label: 'Document', Icon: FileText },
|
||||
{ value: 'geojson', label: 'Carte', Icon: MapPin },
|
||||
{ value: 'gpx', label: 'Trace GPX', Icon: Route },
|
||||
]
|
||||
const TYPE_NAMES = { photo: 'Photo', video: 'Vidéo', audio: 'Voix', citation: 'Citation', document: 'Document', geojson: 'Carte', gpx: 'Trace GPX' }
|
||||
const TYPE_SHORT = { video: 'Vidéo', audio: 'Voix', citation: '«', document: 'Doc' }
|
||||
|
||||
const YOUTUBE_RE = /^https:\/\/(www\.)?(youtube\.com|youtu\.be)\//
|
||||
const HTTP_URL_RE = /^https?:\/\/\S+$/i
|
||||
|
||||
@@ -94,168 +107,174 @@ const MediaManagerPanel = ({
|
||||
return normalized
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="media-manager-modal-overlay" onClick={handleOverlayClick}>
|
||||
<div className="media-manager-modal-card">
|
||||
<div className="media-manager-modal-header">
|
||||
<div className="media-manager-modal-title-wrap">
|
||||
<h1>Médias</h1>
|
||||
<p>{person.firstName} {person.lastName}</p>
|
||||
</div>
|
||||
<button type="button" className="media-manager-close" onClick={onClose}>Fermer</button>
|
||||
</div>
|
||||
const changeType = (value) => {
|
||||
setMediaType(value)
|
||||
setFile(null)
|
||||
setCitationText('')
|
||||
setVideoMode('file')
|
||||
setYoutubeUrl('')
|
||||
}
|
||||
|
||||
<form className="media-manager-modal-form" onSubmit={handleSubmit}>
|
||||
<label htmlFor="mediaUploadType">Type de media</label>
|
||||
<select
|
||||
id="mediaUploadType"
|
||||
value={mediaType}
|
||||
onChange={(event) => {
|
||||
setMediaType(event.target.value)
|
||||
setFile(null)
|
||||
setCitationText('')
|
||||
setVideoMode('file')
|
||||
setYoutubeUrl('')
|
||||
}}
|
||||
>
|
||||
<option value="photo">Photo (max 5 Mo)</option>
|
||||
<option value="video">Video (max 20 Mo)</option>
|
||||
<option value="audio">Audio (max 20 Mo)</option>
|
||||
<option value="document">Document (max 20 Mo)</option>
|
||||
<option value="geojson">Carte GPS GeoJSON (max 20 Mo)</option>
|
||||
<option value="gpx">Trace GPX (max 20 Mo)</option>
|
||||
<option value="citation">Citation (texte)</option>
|
||||
</select>
|
||||
return (
|
||||
<div className="pz-overlay media-manager-modal-overlay" onClick={handleOverlayClick}>
|
||||
<div className="pz-modal pz-modal--wide media-manager-modal-card" role="dialog" aria-modal="true" aria-labelledby="mediaManagerTitle">
|
||||
<div className="pz-modal-head">
|
||||
<p className="pz-eyebrow">Souvenirs</p>
|
||||
<h1 id="mediaManagerTitle" className="pz-title">{person.firstName} <em>{person.lastName}</em></h1>
|
||||
</div>
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--icon pz-modal-close" onClick={onClose} aria-label="Fermer">
|
||||
<X size={18} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<form className="mm-form" onSubmit={handleSubmit}>
|
||||
<div className="pz-field">
|
||||
<span className="pz-label" id="mediaUploadTypeLabel">Ajouter</span>
|
||||
<div className="mm-types" role="radiogroup" aria-labelledby="mediaUploadTypeLabel">
|
||||
{MEDIA_TYPES.map((type) => (
|
||||
<button
|
||||
key={type.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={mediaType === type.value}
|
||||
className={`mm-type ${mediaType === type.value ? 'is-active' : ''}`}
|
||||
onClick={() => changeType(type.value)}
|
||||
>
|
||||
<type.Icon size={18} strokeWidth={1.8} aria-hidden="true" />
|
||||
<span>{type.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{mediaType === 'video' && (
|
||||
<div className="media-manager-video-mode">
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="videoMode"
|
||||
value="file"
|
||||
checked={videoMode === 'file'}
|
||||
onChange={() => { setVideoMode('file'); setYoutubeUrl('') }}
|
||||
/>
|
||||
{' '}Fichier video
|
||||
</label>
|
||||
<label>
|
||||
<input
|
||||
type="radio"
|
||||
name="videoMode"
|
||||
value="youtube"
|
||||
checked={videoMode === 'youtube'}
|
||||
onChange={() => { setVideoMode('youtube'); setFile(null) }}
|
||||
/>
|
||||
{' '}Lien YouTube
|
||||
</label>
|
||||
<div className="pz-tabs mm-video-mode" role="radiogroup" aria-label="Source de la vidéo">
|
||||
<button type="button" role="radio" aria-checked={videoMode === 'file'} className={`pz-tab ${videoMode === 'file' ? 'is-active' : ''}`} onClick={() => { setVideoMode('file'); setYoutubeUrl('') }}>
|
||||
Un fichier
|
||||
</button>
|
||||
<button type="button" role="radio" aria-checked={videoMode === 'youtube'} className={`pz-tab ${videoMode === 'youtube' ? 'is-active' : ''}`} onClick={() => { setVideoMode('youtube'); setFile(null) }}>
|
||||
Un lien YouTube
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isCitation && !isYoutubeVideo && (
|
||||
<>
|
||||
<label htmlFor="mediaUploadFile">
|
||||
Fichier {mediaType === 'photo' ? '(max 5 Mo)' : '(max 20 Mo)'}
|
||||
<div className="pz-field">
|
||||
<label htmlFor="mediaUploadFile" className="mm-drop">
|
||||
<span className="mm-drop-icon" aria-hidden="true"><Upload size={20} strokeWidth={1.8} /></span>
|
||||
<span className="mm-drop-text">
|
||||
<strong>{file ? file.name : 'Choisir un fichier'}</strong>
|
||||
<span>{file ? 'Cliquez pour en choisir un autre' : `${mediaType === 'photo' ? '5 Mo' : '20 Mo'} au maximum`}</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="mediaUploadFile"
|
||||
className="mm-drop-input"
|
||||
type="file"
|
||||
accept={fileAccept}
|
||||
onChange={(event) => setFile(event.target.files?.[0] || null)}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isYoutubeVideo && (
|
||||
<>
|
||||
<label htmlFor="mediaUploadYoutube">URL YouTube</label>
|
||||
<div className="pz-field">
|
||||
<label htmlFor="mediaUploadYoutube">Lien de la vidéo</label>
|
||||
<input
|
||||
id="mediaUploadYoutube"
|
||||
type="url"
|
||||
value={youtubeUrl}
|
||||
onChange={(event) => setYoutubeUrl(event.target.value)}
|
||||
placeholder="https://www.youtube.com/watch?v=..."
|
||||
placeholder="https://www.youtube.com/watch?v=…"
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isCitation && (
|
||||
<>
|
||||
<label htmlFor="mediaUploadCitation">Texte de la citation</label>
|
||||
<div className="pz-field">
|
||||
<label htmlFor="mediaUploadCitation">Ce qu'elle ou il disait</label>
|
||||
<textarea
|
||||
id="mediaUploadCitation"
|
||||
className="media-manager-modal-textarea"
|
||||
value={citationText}
|
||||
onChange={(event) => setCitationText(event.target.value)}
|
||||
placeholder="Saisissez la citation..."
|
||||
rows={4}
|
||||
placeholder="On partait à six dans la 4L, le coffre attaché avec une ficelle."
|
||||
rows={3}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<label htmlFor="mediaUploadCaption">Legende (optionnel)</label>
|
||||
<input
|
||||
id="mediaUploadCaption"
|
||||
type="text"
|
||||
value={caption}
|
||||
onChange={(event) => setCaption(event.target.value)}
|
||||
/>
|
||||
<div className="pz-row2">
|
||||
<div className="pz-field">
|
||||
<label htmlFor="mediaUploadCaption">Titre <span className="mm-optional">facultatif</span></label>
|
||||
<input
|
||||
id="mediaUploadCaption"
|
||||
type="text"
|
||||
value={caption}
|
||||
onChange={(event) => setCaption(event.target.value)}
|
||||
placeholder="Été à Quiberon, 1978"
|
||||
/>
|
||||
</div>
|
||||
<div className="pz-field">
|
||||
<label htmlFor="mediaUploadSource">Source <span className="mm-optional">facultatif</span></label>
|
||||
<input
|
||||
id="mediaUploadSource"
|
||||
type="text"
|
||||
value={source}
|
||||
onChange={(event) => setSource(event.target.value)}
|
||||
placeholder="Album de famille, ou un lien"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label htmlFor="mediaUploadSource">Source (optionnel: texte ou lien)</label>
|
||||
<input
|
||||
id="mediaUploadSource"
|
||||
type="text"
|
||||
value={source}
|
||||
onChange={(event) => setSource(event.target.value)}
|
||||
placeholder="https://... ou description de la source"
|
||||
/>
|
||||
{errorMessage && <div className="pz-error" role="alert">{errorMessage}</div>}
|
||||
|
||||
<button type="submit" disabled={!canUpload}>
|
||||
{loading ? 'Upload...' : 'Uploader'}
|
||||
</button>
|
||||
<div className="pz-modal-actions">
|
||||
<button type="submit" className="pz-btn pz-btn--primary" disabled={!canUpload}>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
{loading ? 'Envoi…' : 'Ajouter ce souvenir'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{errorMessage && <div className="media-manager-modal-error">{errorMessage}</div>}
|
||||
|
||||
<div className="media-manager-modal-list">
|
||||
<section className="mm-list-wrap">
|
||||
<h2 className="pz-eyebrow">{medias.length > 0 ? `Déjà là · ${medias.length}` : 'Déjà là'}</h2>
|
||||
{medias.length === 0 ? (
|
||||
<div className="media-manager-modal-empty">Aucun media pour cette personne.</div>
|
||||
<p className="pz-small mm-empty">Pas encore de souvenir. Le premier que vous ajoutez apparaît en tête de sa fiche.</p>
|
||||
) : (
|
||||
medias.map((media, index) => (
|
||||
<div key={media.id} className="media-manager-modal-item">
|
||||
<div className="media-manager-modal-preview">
|
||||
{media.type === 'photo' && media.url ? (
|
||||
<img src={media.url} alt="" />
|
||||
) : media.type === 'geojson' || media.type === 'gpx' ? (
|
||||
<GeoMediaThumbnail media={media} className="media-manager-modal-preview-map" />
|
||||
) : (
|
||||
<span>{media.type === 'citation' ? 'Citation' : media.type.toUpperCase()}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="media-manager-modal-meta">
|
||||
<strong>{media.label || `Media ${index + 1}`}</strong>
|
||||
<span>Type: {media.type}</span>
|
||||
{media.source && <span>Source: {renderSource(media.source)}</span>}
|
||||
<span>Ordre: {media.displayOrder || index + 1}</span>
|
||||
</div>
|
||||
<div className="media-manager-modal-actions">
|
||||
<button type="button" className="media-manager-action-ghost" onClick={() => onMove(media.id, -1)} disabled={loading || index === 0}>
|
||||
Monter
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="media-manager-action-ghost"
|
||||
onClick={() => onMove(media.id, 1)}
|
||||
disabled={loading || index === medias.length - 1}
|
||||
>
|
||||
Descendre
|
||||
</button>
|
||||
<button type="button" className="media-manager-action-danger" onClick={() => onDelete(media.id)} disabled={loading}>Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
<ul className="mm-list">
|
||||
{medias.map((media, index) => (
|
||||
<li key={media.id} className="mm-item">
|
||||
<span className="mm-preview" aria-hidden="true">
|
||||
{media.type === 'photo' && media.url ? (
|
||||
<img src={media.url} alt="" />
|
||||
) : media.type === 'geojson' || media.type === 'gpx' ? (
|
||||
<GeoMediaThumbnail media={media} className="media-manager-modal-preview-map" />
|
||||
) : (
|
||||
<span>{TYPE_SHORT[media.type] || media.type}</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="mm-meta">
|
||||
<strong>{media.label || `Souvenir ${index + 1}`}</strong>
|
||||
<span>
|
||||
{TYPE_NAMES[media.type] || media.type}
|
||||
{media.source && <> · {renderSource(media.source)}</>}
|
||||
</span>
|
||||
</span>
|
||||
<span className="mm-actions">
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--icon" onClick={() => onMove(media.id, -1)} disabled={loading || index === 0} aria-label="Monter">
|
||||
<ArrowUp size={16} aria-hidden="true" />
|
||||
</button>
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--icon" onClick={() => onMove(media.id, 1)} disabled={loading || index === medias.length - 1} aria-label="Descendre">
|
||||
<ArrowDown size={16} aria-hidden="true" />
|
||||
</button>
|
||||
<button type="button" className="pz-btn pz-btn--danger-ghost pz-btn--icon" onClick={() => onDelete(media.id)} disabled={loading} aria-label="Supprimer">
|
||||
<Trash2 size={16} aria-hidden="true" />
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,230 +1,74 @@
|
||||
/* ============================================================
|
||||
Visionneuse d'un souvenir (DA terracotta)
|
||||
L'image d'abord, la légende dessous. Fond sombre discret.
|
||||
============================================================ */
|
||||
.media-viewer-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(45, 40, 36, 0.65);
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
animation: overlayFadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes overlayFadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
background: rgba(42, 38, 34, .62);
|
||||
backdrop-filter: blur(4px);
|
||||
-webkit-backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.media-viewer-card {
|
||||
position: relative;
|
||||
max-width: 480px;
|
||||
width: 90vw;
|
||||
max-height: 85vh;
|
||||
background: rgba(247, 236, 217, 0.95);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(93, 82, 75, 0.08);
|
||||
box-shadow:
|
||||
0 8px 100px rgba(93, 82, 75, 0.25),
|
||||
0 2px 8px rgba(93, 82, 75, 0.08);
|
||||
width: min(100%, 720px);
|
||||
max-height: calc(100dvh - 32px);
|
||||
overflow-y: auto;
|
||||
animation: cardSlideIn 0.4s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
transform: rotate(-0.5deg);
|
||||
}
|
||||
|
||||
.media-viewer-card::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.media-viewer-card::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.media-viewer-card::-webkit-scrollbar-thumb {
|
||||
background: rgba(166, 124, 82, 0.2);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
@keyframes cardSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.95) rotate(-0.5deg);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1) rotate(-0.5deg);
|
||||
}
|
||||
}
|
||||
|
||||
.media-viewer-card::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) rotate(2deg);
|
||||
width: 56px;
|
||||
height: 14px;
|
||||
background: rgba(166, 124, 82, 0.16);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 1px 3px rgba(93, 82, 75, 0.06);
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
gap: 0;
|
||||
background: var(--pz-surface);
|
||||
border-radius: var(--pz-r-l);
|
||||
box-shadow: var(--pz-shadow-lg);
|
||||
animation: pz-rise 240ms var(--pz-ease) both;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--pz-line) transparent;
|
||||
}
|
||||
.media-viewer-card--citation,
|
||||
.media-viewer-card--audio,
|
||||
.media-viewer-card--document { width: min(100%, 560px); }
|
||||
|
||||
.media-viewer-close {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 0.75rem;
|
||||
background: rgba(93, 82, 75, 0.06);
|
||||
border: none;
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
color: #8B7355;
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
transition: background 0.25s ease, color 0.25s ease;
|
||||
z-index: 10;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 2;
|
||||
background: rgba(255, 255, 255, .92);
|
||||
box-shadow: var(--pz-shadow);
|
||||
}
|
||||
|
||||
.media-viewer-close:hover {
|
||||
background: rgba(93, 82, 75, 0.12);
|
||||
color: #5D524B;
|
||||
}
|
||||
|
||||
/* --- Contenu média --- */
|
||||
.media-viewer-content {
|
||||
padding: 1.75rem 1.5rem 1.5rem;
|
||||
}
|
||||
|
||||
/* Header avec info personne */
|
||||
.media-viewer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.media-viewer-person-avatar {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(247, 236, 217, 1),
|
||||
0 2px 10px rgba(166, 124, 82, 0.15);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.media-viewer-person-initials {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(145deg, #A67C52, #8B6843);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #FEF9ED;
|
||||
font-family: 'Bradford LL', 'Iowan Old Style', 'Palatino Linotype', 'Times New Roman', serif;
|
||||
font-size: 16px;
|
||||
font-weight: 450;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 2px 10px rgba(166, 124, 82, 0.15);
|
||||
}
|
||||
|
||||
.media-viewer-person-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
.media-viewer-person-name {
|
||||
font-family: 'Bradford LL', 'Iowan Old Style', 'Palatino Linotype', 'Times New Roman', serif;
|
||||
font-size: 16px;
|
||||
color: #5D524B;
|
||||
font-weight: 450;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.media-viewer-person-years {
|
||||
font-size: 14px;
|
||||
color: #8B7355;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.media-viewer-label {
|
||||
font-family: 'Bradford LL', 'Iowan Old Style', 'Palatino Linotype', 'Times New Roman', serif;
|
||||
font-size: 20px;
|
||||
font-weight: 450;
|
||||
color: #5D524B;
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.media-viewer-source {
|
||||
margin: -0.4rem 0 1rem;
|
||||
color: #6f6256;
|
||||
font-size: 14px;
|
||||
line-height: 1.35;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.media-viewer-source a {
|
||||
color: #6a4f32;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
|
||||
.media-viewer-source a:hover {
|
||||
color: #4f3b26;
|
||||
}
|
||||
|
||||
.media-viewer-type-badge {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.6rem;
|
||||
border-radius: 50px;
|
||||
font-size: 12px;
|
||||
font-weight: 450;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.media-viewer-type-badge.photo { background: rgba(166, 124, 82, 0.12); color: #A67C52; }
|
||||
.media-viewer-type-badge.video { background: rgba(58, 53, 48, 0.1); color: #3A3530; }
|
||||
.media-viewer-type-badge.audio { background: rgba(44, 40, 37, 0.1); color: #2C2825; }
|
||||
.media-viewer-type-badge.document { background: rgba(200, 185, 160, 0.2); color: #8B7355; }
|
||||
.media-viewer-type-badge.citation { background: rgba(245, 237, 207, 0.5); color: #A67C52; }
|
||||
.media-viewer-type-badge.geojson { background: rgba(96, 140, 112, 0.18); color: #4f7660; }
|
||||
.media-viewer-type-badge.gpx { background: rgba(64, 118, 170, 0.2); color: #37668f; }
|
||||
.mv-stage { padding: 12px 12px 0; }
|
||||
.mv-caption { display: grid; gap: 10px; padding: 18px 22px 22px; }
|
||||
.mv-title { margin: 0; font: 400 26px/1.15 var(--pz-serif); color: var(--pz-ink); text-wrap: balance; padding-right: 8px; }
|
||||
.mv-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }
|
||||
.mv-person { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
|
||||
.mv-person-avatar { width: 28px; height: 28px; flex: none; border-radius: 50%; object-fit: cover; }
|
||||
.mv-person-initial { display: grid; place-items: center; background: var(--pz-a100); color: var(--pz-a700); font: 400 13px/1 var(--pz-serif); }
|
||||
.mv-person-name { font: 400 16px/1.2 var(--pz-serif); color: var(--pz-ink); }
|
||||
.mv-person-name em { font-style: italic; font-weight: 300; }
|
||||
.mv-person-years { font: 400 12.5px var(--pz-sans); color: var(--pz-muted); font-variant-numeric: tabular-nums; }
|
||||
.mv-source { margin: 0; font: 400 13px/1.5 var(--pz-sans); color: var(--pz-muted); overflow-wrap: anywhere; }
|
||||
.mv-source a { color: var(--pz-a700); text-underline-offset: 3px; }
|
||||
|
||||
/* --- Skins par type --- */
|
||||
|
||||
/* Photo */
|
||||
.media-photo-frame {
|
||||
padding: 8px;
|
||||
background: #FEF9ED;
|
||||
border-radius: 4px;
|
||||
box-shadow:
|
||||
0 2px 20px rgba(93, 82, 75, 0.1),
|
||||
0 0 0 1px rgba(93, 82, 75, 0.05);
|
||||
transform: rotate(0.8deg);
|
||||
overflow: hidden;
|
||||
border-radius: 14px;
|
||||
background: var(--pz-bg);
|
||||
}
|
||||
|
||||
.media-photo-frame img {
|
||||
width: 100%;
|
||||
border-radius: 2px;
|
||||
max-height: 68dvh;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
background: var(--pz-bg);
|
||||
}
|
||||
|
||||
/* Video */
|
||||
.media-video-container {
|
||||
background: #2C2825;
|
||||
background: #2A2622;
|
||||
border-radius: 8px;
|
||||
padding: 2.5rem 3rem;
|
||||
display: flex;
|
||||
@@ -258,14 +102,14 @@
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 1px;
|
||||
background: rgba(254, 249, 237, 0.06);
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.media-video-play {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 50%;
|
||||
background: rgba(254, 249, 237, 0.1);
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -274,21 +118,21 @@
|
||||
}
|
||||
|
||||
.media-video-play:hover {
|
||||
background: rgba(254, 249, 237, 0.18);
|
||||
background: rgba(255, 255, 255, 0.18);
|
||||
transform: scale(1.05);
|
||||
}
|
||||
|
||||
.media-video-play-triangle {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 20px solid rgba(254, 249, 237, 0.7);
|
||||
border-left: 20px solid rgba(255, 255, 255, 0.7);
|
||||
border-top: 12px solid transparent;
|
||||
border-bottom: 12px solid transparent;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.media-video-text {
|
||||
color: rgba(254, 249, 237, 0.35);
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
@@ -298,8 +142,8 @@
|
||||
.media-video-player {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
background: #1f1b18;
|
||||
box-shadow: 0 3px 18px rgba(44, 40, 37, 0.24);
|
||||
background: #191817;
|
||||
box-shadow: 0 3px 18px rgba(42, 38, 34, 0.24);
|
||||
}
|
||||
|
||||
.media-video-container--youtube {
|
||||
@@ -312,12 +156,12 @@
|
||||
aspect-ratio: 16 / 9;
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 3px 18px rgba(44, 40, 37, 0.24);
|
||||
box-shadow: 0 3px 18px rgba(42, 38, 34, 0.24);
|
||||
}
|
||||
|
||||
/* Audio */
|
||||
.media-audio-container {
|
||||
background: #2C2825;
|
||||
background: #2A2622;
|
||||
border-radius: 50%;
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
@@ -327,8 +171,8 @@
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
box-shadow:
|
||||
0 4px 30px rgba(44, 40, 37, 0.3),
|
||||
inset 0 0 40px rgba(0, 0, 0, 0.2);
|
||||
0 4px 30px rgba(42, 38, 34, 0.3),
|
||||
inset 0 0 40px rgba(42, 38, 34, 0.2);
|
||||
}
|
||||
|
||||
.media-audio-grooves {
|
||||
@@ -339,8 +183,8 @@
|
||||
circle at center,
|
||||
transparent 0px,
|
||||
transparent 4px,
|
||||
rgba(254, 249, 237, 0.04) 4px,
|
||||
rgba(254, 249, 237, 0.04) 5px
|
||||
rgba(255, 255, 255, 0.04) 4px,
|
||||
rgba(255, 255, 255, 0.04) 5px
|
||||
);
|
||||
}
|
||||
|
||||
@@ -348,7 +192,7 @@
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(145deg, #A67C52, #8B6843);
|
||||
background: #2a2622;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -359,22 +203,22 @@
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #2C2825;
|
||||
background: #2A2622;
|
||||
}
|
||||
|
||||
.media-audio-text {
|
||||
text-align: center;
|
||||
color: #8B7355;
|
||||
color: #6B645D;
|
||||
font-size: 14px;
|
||||
margin-top: 1rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.media-audio-player-wrap {
|
||||
padding: 14px 12px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.56);
|
||||
box-shadow: inset 0 0 0 1px rgba(93, 82, 75, 0.08);
|
||||
margin-top: 36px;
|
||||
padding: 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--pz-a100);
|
||||
}
|
||||
|
||||
.media-audio-player {
|
||||
@@ -383,14 +227,14 @@
|
||||
|
||||
/* Document */
|
||||
.media-document-container {
|
||||
background: #F0E8D8;
|
||||
background: #FCF3EF;
|
||||
border-radius: 2px;
|
||||
padding: 2rem 1.5rem;
|
||||
min-height: 160px;
|
||||
position: relative;
|
||||
box-shadow:
|
||||
0 2px 12px rgba(93, 82, 75, 0.08),
|
||||
0 0 0 1px rgba(93, 82, 75, 0.06);
|
||||
0 2px 12px rgba(42, 38, 34, 0.08),
|
||||
0 0 0 1px rgba(42, 38, 34, 0.06);
|
||||
}
|
||||
|
||||
.media-document-corner {
|
||||
@@ -399,7 +243,7 @@
|
||||
right: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: linear-gradient(135deg, transparent 50%, #DDD3C2 50%);
|
||||
background: linear-gradient(135deg, transparent 50%, #E6E1DA 50%);
|
||||
}
|
||||
|
||||
.media-document-corner::before {
|
||||
@@ -409,7 +253,7 @@
|
||||
right: 0;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background: linear-gradient(135deg, #F7ECD9 50%, transparent 50%);
|
||||
background: linear-gradient(135deg, #F2EFEA 50%, transparent 50%);
|
||||
}
|
||||
|
||||
.media-document-lines {
|
||||
@@ -421,7 +265,7 @@
|
||||
|
||||
.media-document-line {
|
||||
height: 3px;
|
||||
background: rgba(93, 82, 75, 0.08);
|
||||
background: rgba(42, 38, 34, 0.08);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
@@ -431,7 +275,7 @@
|
||||
|
||||
.media-document-text {
|
||||
text-align: center;
|
||||
color: #8B7355;
|
||||
color: #6B645D;
|
||||
font-size: 14px;
|
||||
margin-top: 1.25rem;
|
||||
font-style: italic;
|
||||
@@ -445,7 +289,7 @@
|
||||
.media-document-pdf {
|
||||
width: 100%;
|
||||
min-height: 430px;
|
||||
border: 1px solid rgba(93, 82, 75, 0.18);
|
||||
border: 1px solid rgba(42, 38, 34, 0.18);
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
}
|
||||
@@ -457,16 +301,16 @@
|
||||
}
|
||||
|
||||
.media-document-download {
|
||||
color: #6a4f32;
|
||||
color: #2a2622;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dashed rgba(106, 79, 50, 0.45);
|
||||
border-bottom: 1px dashed rgba(42, 38, 34, 0.45);
|
||||
}
|
||||
|
||||
.media-document-download:hover {
|
||||
color: #4f3b26;
|
||||
border-bottom-color: rgba(79, 59, 38, 0.6);
|
||||
color: #2a2622;
|
||||
border-bottom-color: rgba(42, 38, 34, 0.6);
|
||||
}
|
||||
|
||||
/* Geo map */
|
||||
@@ -480,8 +324,8 @@
|
||||
height: 280px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(93, 82, 75, 0.14);
|
||||
background: #f1ecdf;
|
||||
border: 1px solid rgba(42, 38, 34, 0.14);
|
||||
background: #f8e4db;
|
||||
}
|
||||
|
||||
.media-map-embed {
|
||||
@@ -508,7 +352,7 @@
|
||||
|
||||
.media-map-grid {
|
||||
fill: none;
|
||||
stroke: rgba(93, 82, 75, 0.12);
|
||||
stroke: rgba(42, 38, 34, 0.12);
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
@@ -517,7 +361,7 @@
|
||||
}
|
||||
|
||||
.media-map-bg-edge {
|
||||
stop-color: rgba(232, 226, 210, 0.84);
|
||||
stop-color: rgba(248, 228, 219, 0.84);
|
||||
}
|
||||
|
||||
.media-map-trace {
|
||||
@@ -546,20 +390,20 @@
|
||||
|
||||
.media-map-meta span {
|
||||
background: rgba(255, 255, 255, 0.68);
|
||||
border: 1px solid rgba(93, 82, 75, 0.14);
|
||||
border: 1px solid rgba(42, 38, 34, 0.14);
|
||||
border-radius: 999px;
|
||||
padding: 4px 10px;
|
||||
color: #5d524b;
|
||||
color: #2a2622;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.media-map-status {
|
||||
color: #7a6f65;
|
||||
color: #6b645d;
|
||||
font-size: 14px;
|
||||
text-align: center;
|
||||
padding: 16px;
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
border: 1px dashed rgba(93, 82, 75, 0.22);
|
||||
border: 1px dashed rgba(42, 38, 34, 0.22);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@@ -585,57 +429,24 @@
|
||||
|
||||
/* Citation */
|
||||
.media-citation-container {
|
||||
background: #F5EDCF;
|
||||
border-radius: 4px;
|
||||
padding: 2.5rem 2rem 2rem;
|
||||
position: relative;
|
||||
min-height: 100px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow:
|
||||
0 2px 12px rgba(93, 82, 75, 0.06);
|
||||
}
|
||||
|
||||
.media-citation-tape {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) rotate(1.5deg);
|
||||
width: 48px;
|
||||
height: 10px;
|
||||
background: rgba(166, 124, 82, 0.15);
|
||||
border-radius: 1px;
|
||||
}
|
||||
|
||||
.media-citation-guillemet {
|
||||
font-family: Georgia, 'Times New Roman', serif;
|
||||
font-size: 44px;
|
||||
color: rgba(166, 124, 82, 0.2);
|
||||
line-height: 0.6;
|
||||
margin-bottom: 0.5rem;
|
||||
margin: 44px 0 0;
|
||||
padding: 8px 12px 8px 22px;
|
||||
border-left: 2px solid var(--pz-a300);
|
||||
}
|
||||
|
||||
.media-citation-text {
|
||||
font-family: 'Bradford LL', 'Iowan Old Style', 'Palatino Linotype', 'Times New Roman', serif;
|
||||
font-size: 18px;
|
||||
font-style: italic;
|
||||
color: #5D524B;
|
||||
text-align: center;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
font: 300 italic 26px/1.4 var(--pz-serif);
|
||||
color: var(--pz-ink);
|
||||
text-wrap: pretty;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.media-viewer-card {
|
||||
width: 95vw;
|
||||
max-width: none;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
.media-audio-container {
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
}
|
||||
.mv-title { font-size: 22px; }
|
||||
.media-citation-text { font-size: 22px; }
|
||||
}
|
||||
@@ -97,8 +97,8 @@ const MediaViewer = ({ media, person, onClose }) => {
|
||||
|
||||
const typeLabels = useMemo(() => ({
|
||||
photo: 'Photo',
|
||||
video: 'Video',
|
||||
audio: 'Audio',
|
||||
video: 'Vidéo',
|
||||
audio: 'Voix',
|
||||
document: 'Document',
|
||||
citation: 'Citation',
|
||||
geojson: 'Carte GPS',
|
||||
@@ -349,11 +349,9 @@ const MediaViewer = ({ media, person, onClose }) => {
|
||||
return renderGeoMap()
|
||||
case 'citation':
|
||||
return (
|
||||
<div className="media-citation-container">
|
||||
<div className="media-citation-tape" />
|
||||
<div className="media-citation-guillemet">"</div>
|
||||
<div className="media-citation-text">{media?.label || 'Citation sans texte'}</div>
|
||||
</div>
|
||||
<blockquote className="media-citation-container">
|
||||
<p className="media-citation-text">« {media?.label || 'Citation sans texte'} »</p>
|
||||
</blockquote>
|
||||
)
|
||||
default:
|
||||
return null
|
||||
@@ -363,44 +361,45 @@ const MediaViewer = ({ media, person, onClose }) => {
|
||||
if (!media) return null
|
||||
|
||||
return (
|
||||
<div className="media-viewer-overlay" onClick={handleOverlayClick}>
|
||||
<div className="media-viewer-card">
|
||||
<div className="pz-overlay media-viewer-overlay" onClick={handleOverlayClick}>
|
||||
<div
|
||||
className={`media-viewer-card media-viewer-card--${mediaType}`}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={media?.label || typeLabels[mediaType] || 'Souvenir'}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="media-viewer-close"
|
||||
className="pz-btn pz-btn--ghost pz-btn--icon media-viewer-close"
|
||||
onClick={onClose}
|
||||
aria-label="Fermer le media"
|
||||
aria-label="Fermer le souvenir"
|
||||
>
|
||||
<X size={18} strokeWidth={2.25} />
|
||||
</button>
|
||||
|
||||
<div className="media-viewer-content">
|
||||
<div className="media-viewer-header">
|
||||
{person?.photo ? (
|
||||
<img className="media-viewer-person-avatar" src={person.photo} alt="" />
|
||||
) : (
|
||||
<div className="media-viewer-person-initials">
|
||||
{(person?.firstName?.[0] || '')}{(person?.lastName?.[0] || '')}
|
||||
</div>
|
||||
)}
|
||||
<div className="media-viewer-person-info">
|
||||
<span className="media-viewer-person-name">
|
||||
{person?.firstName} {person?.lastName}
|
||||
</span>
|
||||
<span className="media-viewer-person-years">{personYears}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mv-stage">
|
||||
{renderContent()}
|
||||
</div>
|
||||
|
||||
<div className={`media-viewer-type-badge ${mediaType}`}>
|
||||
{typeLabels[mediaType] || mediaType}
|
||||
</div>
|
||||
|
||||
{mediaType !== 'citation' && (
|
||||
<h2 className="media-viewer-label">{media?.label || ''}</h2>
|
||||
<div className="mv-caption">
|
||||
{mediaType !== 'citation' && media?.label && (
|
||||
<h2 className="mv-title">{media.label}</h2>
|
||||
)}
|
||||
<div className="mv-meta">
|
||||
<span className="mv-person">
|
||||
{person?.photo ? (
|
||||
<img className="mv-person-avatar" src={person.photo} alt="" />
|
||||
) : (
|
||||
<span className="mv-person-avatar mv-person-initial">{person?.firstName?.[0] || '?'}</span>
|
||||
)}
|
||||
<span className="mv-person-name">{person?.firstName} <em>{person?.lastName}</em></span>
|
||||
{personYears && <span className="mv-person-years">{personYears}</span>}
|
||||
</span>
|
||||
<span className="pz-tag">{typeLabels[mediaType] || mediaType}</span>
|
||||
</div>
|
||||
{mediaSource && (
|
||||
<p className="media-viewer-source">
|
||||
Source:{' '}
|
||||
<p className="mv-source">
|
||||
Source{' '}
|
||||
{isMediaSourceUrl ? (
|
||||
<a href={mediaSource} target="_blank" rel="noreferrer">{mediaSource}</a>
|
||||
) : (
|
||||
@@ -408,8 +407,6 @@ const MediaViewer = ({ media, person, onClose }) => {
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{renderContent()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useRef, forwardRef } from 'react'
|
||||
import { Save, Trash2, Plus, X, Search, Check } from 'lucide-react'
|
||||
import { Save, Trash2, Plus, X, Search, Check, Play } from 'lucide-react'
|
||||
import { getParents, getChildren, getPersonUnions, getPersonMedias, filiations, persons, unions } from '../data/mockData'
|
||||
import GeoMediaThumbnail from './GeoMediaThumbnail'
|
||||
import './PersonCard.css'
|
||||
@@ -14,6 +14,9 @@ const TYPE_LABELS = {
|
||||
gpx: 'Trace GPX',
|
||||
}
|
||||
|
||||
// Hauteurs fixes de l'onde du lecteur vocal (pas d'aléatoire au rendu)
|
||||
const VOICE_WAVE = [40, 64, 48, 82, 58, 92, 70, 46, 68, 86, 52, 74, 42, 62, 88, 56, 46, 72, 84, 50, 64, 42]
|
||||
|
||||
const PersonCard = ({
|
||||
person,
|
||||
onClose,
|
||||
@@ -34,7 +37,7 @@ const PersonCard = ({
|
||||
relationActionSuccess = '',
|
||||
relationActionError = '',
|
||||
}) => {
|
||||
const [tab, setTab] = useState('infos')
|
||||
const [tab, setTab] = useState(() => (getPersonMedias(person.id).length > 0 ? 'medias' : 'infos'))
|
||||
|
||||
// État local pour les modifications en mode édition
|
||||
const [edits, setEdits] = useState({})
|
||||
@@ -243,7 +246,7 @@ const PersonCard = ({
|
||||
return (
|
||||
<input
|
||||
type={type}
|
||||
className="value value--editable"
|
||||
className="pc-input value--editable"
|
||||
value={getEditValue(field, value)}
|
||||
onChange={(e) => handleFieldChange(field, e.target.value)}
|
||||
placeholder={placeholder || '...'}
|
||||
@@ -251,510 +254,419 @@ const PersonCard = ({
|
||||
)
|
||||
}
|
||||
|
||||
// Souvenirs regroupés par nature : citation, voix, images, autres
|
||||
const quoteMedia = personMedias.find((m) => m.type === 'citation' && m.label)
|
||||
const voiceMedias = personMedias.filter((m) => m.type === 'audio')
|
||||
const visualMedias = personMedias.filter((m) => m.type === 'photo' || m.type === 'video')
|
||||
const otherMedias = personMedias.filter((m) => !['citation', 'audio', 'photo', 'video'].includes(m.type) || (m.type === 'citation' && m !== quoteMedia))
|
||||
|
||||
const initials = `${person.firstName?.[0] || ''}${person.lastName?.[0] || ''}` || '?'
|
||||
const lifeLine = (() => {
|
||||
const born = person.birthYear || (person.birthDate ? String(person.birthDate).slice(0, 4) : '')
|
||||
const died = person.deathYear || (person.deathDate ? String(person.deathDate).slice(0, 4) : '')
|
||||
const parts = []
|
||||
if (born) parts.push(`${born}${person.birthPlace ? `, ${person.birthPlace}` : ''}`)
|
||||
if (!person.isAlive && died) parts.push(died)
|
||||
return parts.join(' – ')
|
||||
})()
|
||||
const ageLabel = age === null ? null : person.isAlive ? `${age} ans` : `${age} ans au décès`
|
||||
|
||||
const renderRelationRow = (target, relationType, meta) => (
|
||||
<div key={`${relationType}-${target?.id}`} className="pc-rel-row">
|
||||
<span className="pc-rel-avatar" aria-hidden="true">
|
||||
{target?.photo ? <img src={target.photo} alt="" /> : <span>{target?.firstName?.[0] || '?'}</span>}
|
||||
</span>
|
||||
<span className="pc-rel-name">{target?.firstName} {target?.lastName || ''}</span>
|
||||
{meta && <span className="pc-rel-meta">{meta}</span>}
|
||||
<button
|
||||
type="button"
|
||||
className="pz-btn pz-btn--ghost pz-btn--icon pc-rel-remove"
|
||||
aria-label={`Retirer ${target?.firstName}`}
|
||||
onClick={() => handleRelationRemove(target.id, relationType)}
|
||||
disabled={relationActionLoading}
|
||||
>
|
||||
<X size={15} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
|
||||
const renderRelationAdd = (relationType, label, placeholder) => (
|
||||
activeRelationSearch === relationType ? (
|
||||
<RelationSearchInline
|
||||
ref={relationSearchRef}
|
||||
query={relationSearchQuery}
|
||||
onQueryChange={setRelationSearchQuery}
|
||||
results={getFilteredPersonsForRelation(relationType)}
|
||||
onSelect={(p) => handleRelationAdd(p, relationType)}
|
||||
onClose={() => { setActiveRelationSearch(null); setRelationSearchQuery('') }}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
) : (
|
||||
<button type="button" className="pc-rel-add" onClick={() => setActiveRelationSearch(relationType)} disabled={relationActionLoading}>
|
||||
<Plus size={15} strokeWidth={2} />
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<div className={`person-card ${editMode ? 'person-card--edit-mode' : ''}`}>
|
||||
<button className="close-btn" onClick={onClose}>
|
||||
<div className={`person-card ${editMode ? 'person-card--edit-mode' : ''}`} role="dialog" aria-label={`Fiche de ${person.firstName} ${person.lastName || ''}`}>
|
||||
<span className="pc-grab" aria-hidden="true" />
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--icon pc-close" onClick={onClose} aria-label="Fermer la fiche">
|
||||
<X size={18} strokeWidth={2} />
|
||||
</button>
|
||||
|
||||
{/* Indicateur mode édition */}
|
||||
{editMode && (
|
||||
<div className="person-card-edit-indicator">
|
||||
<span className="person-card-edit-dot" />
|
||||
Édition
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="card-content">
|
||||
<div className="avatar-section">
|
||||
<div className="avatar-frame">
|
||||
{person.photo ? (
|
||||
<img src={person.photo} alt={`${person.firstName} ${person.lastName}`} />
|
||||
) : (
|
||||
<div className="avatar-placeholder">
|
||||
{person.firstName[0]}{person.lastName[0]}
|
||||
</div>
|
||||
)}
|
||||
{/* ── En-tête : visage, nom, repères ── */}
|
||||
<header className="pc-head">
|
||||
<div className="pc-photo-wrap">
|
||||
<div className="pc-photo">
|
||||
{person.photo
|
||||
? <img src={person.photo} alt={`${person.firstName} ${person.lastName || ''}`} />
|
||||
: <span className="pc-photo-initials">{initials}</span>}
|
||||
</div>
|
||||
{editMode && (
|
||||
<div className="avatar-edit-actions">
|
||||
<div className="pc-photo-actions">
|
||||
<input
|
||||
ref={avatarInputRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
className="avatar-edit-input"
|
||||
className="pc-hidden-input"
|
||||
onChange={handleAvatarFileChange}
|
||||
disabled={avatarActionLoading}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="avatar-edit-btn"
|
||||
onClick={() => avatarInputRef.current?.click()}
|
||||
disabled={avatarActionLoading}
|
||||
>
|
||||
{person.photo ? 'Modifier avatar' : 'Ajouter avatar'}
|
||||
<button type="button" className="pc-photo-btn" onClick={() => avatarInputRef.current?.click()} disabled={avatarActionLoading}>
|
||||
{person.photo ? 'Changer' : 'Ajouter une photo'}
|
||||
</button>
|
||||
{person.photo && (
|
||||
<button
|
||||
type="button"
|
||||
className="avatar-edit-btn avatar-edit-btn--danger"
|
||||
onClick={() => onAvatarDelete?.(person.id)}
|
||||
disabled={avatarActionLoading}
|
||||
>
|
||||
Retirer avatar
|
||||
<button type="button" className="pc-photo-btn pc-photo-btn--danger" onClick={() => onAvatarDelete?.(person.id)} disabled={avatarActionLoading}>
|
||||
Retirer
|
||||
</button>
|
||||
)}
|
||||
{avatarActionError && (
|
||||
<div className="avatar-edit-error">{avatarActionError}</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="info-header">
|
||||
<div className="pc-id">
|
||||
{editMode ? (
|
||||
<div className="info-header-edit">
|
||||
<div className="pc-name-edit">
|
||||
<input
|
||||
type="text"
|
||||
className="person-name-input"
|
||||
className="pc-input pc-input--name"
|
||||
value={getEditValue('firstName', person.firstName)}
|
||||
onChange={(e) => handleFieldChange('firstName', e.target.value)}
|
||||
placeholder="Prénom"
|
||||
aria-label="Prénom"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
className="person-name-input person-name-input--last"
|
||||
className="pc-input pc-input--name pc-input--last"
|
||||
value={getEditValue('lastName', person.lastName)}
|
||||
onChange={(e) => handleFieldChange('lastName', e.target.value)}
|
||||
placeholder="Nom"
|
||||
aria-label="Nom"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<h2 className="person-name">
|
||||
{person.firstName} <span className="last-name">{person.lastName}</span>
|
||||
<h2 className="pc-name">
|
||||
{person.firstName} <em>{person.lastName}</em>
|
||||
</h2>
|
||||
)}
|
||||
{person.isAlive && (
|
||||
<div className="status-badge alive">Vivant(e)</div>
|
||||
)}
|
||||
{!editMode && lifeLine && <p className="pc-life">{lifeLine}</p>}
|
||||
<div className="pc-chips">
|
||||
{editMode && <span className="pz-tag pz-tag--accent pz-tag--dot">Édition</span>}
|
||||
{!editMode && ageLabel && <span className="pz-tag">{ageLabel}</span>}
|
||||
{!editMode && person.profession && <span className="pz-tag">{person.profession}</span>}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Onglets */}
|
||||
<div className="card-tabs">
|
||||
<button
|
||||
className={`card-tab ${tab === 'infos' ? 'active' : ''}`}
|
||||
onClick={() => setTab('infos')}
|
||||
>
|
||||
Infos
|
||||
{avatarActionError && <div className="pz-error pc-inline-msg">{avatarActionError}</div>}
|
||||
|
||||
{/* ── Onglets ── */}
|
||||
<div className="pz-tabs pc-tabs" role="tablist">
|
||||
<button type="button" role="tab" aria-selected={tab === 'medias'} className="pz-tab" onClick={() => setTab('medias')}>
|
||||
Souvenirs{personMedias.length > 0 ? ` · ${personMedias.length}` : ''}
|
||||
</button>
|
||||
<button type="button" role="tab" aria-selected={tab === 'infos'} className="pz-tab" onClick={() => setTab('infos')}>
|
||||
Infos
|
||||
</button>
|
||||
{editMode && (
|
||||
<button type="button" role="tab" aria-selected={tab === 'relations'} className="pz-tab" onClick={() => setTab('relations')}>
|
||||
Liens
|
||||
</button>
|
||||
{editMode && (
|
||||
<button
|
||||
className={`card-tab ${tab === 'relations' ? 'active' : ''}`}
|
||||
onClick={() => setTab('relations')}
|
||||
>
|
||||
Relations
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={`card-tab ${tab === 'medias' ? 'active' : ''}`}
|
||||
onClick={() => setTab('medias')}
|
||||
>
|
||||
Médias{personMedias.length > 0 ? ` (${personMedias.length})` : ''}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="card-tab-content">
|
||||
{tab === 'infos' && (
|
||||
<div className="details">
|
||||
{/* --- État civil --- */}
|
||||
<div className="detail-section-title">État civil</div>
|
||||
|
||||
<div className="detail-row">
|
||||
<span className="label">Né(e)</span>
|
||||
{renderEditableValue({ field: 'birthYear', value: person.birthDate || person.birthYear, placeholder: 'Date de naissance', type: 'date' })}
|
||||
<div className="pc-body">
|
||||
{/* ── Souvenirs ── */}
|
||||
{tab === 'medias' && (
|
||||
<div className="pc-souvenirs">
|
||||
{personMedias.length === 0 && (
|
||||
<div className="pc-empty">
|
||||
<p className="pz-sub">Pas encore de souvenir pour {person.firstName}.</p>
|
||||
<p className="pz-small">Une photo, une anecdote, sa voix. Tout le monde peut en ajouter.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(person.birthPlace || editMode) && (
|
||||
<div className="detail-row">
|
||||
<span className="label">Lieu</span>
|
||||
{renderEditableValue({ field: 'birthPlace', value: person.birthPlace || '', placeholder: 'Lieu de naissance' })}
|
||||
</div>
|
||||
)}
|
||||
{quoteMedia && (
|
||||
<button type="button" className="pc-quote" onClick={() => onMediaSelect?.(quoteMedia, person)}>
|
||||
« {quoteMedia.label} »
|
||||
{quoteMedia.source && <small>{quoteMedia.source}</small>}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{(person.birthName || editMode) && (
|
||||
<div className="detail-row">
|
||||
<span className="label">Nom de naissance</span>
|
||||
{renderEditableValue({ field: 'birthName', value: person.birthName || '', placeholder: 'Nom de naissance' })}
|
||||
</div>
|
||||
)}
|
||||
{voiceMedias.map((media) => (
|
||||
<button key={media.id} type="button" className="pc-voice" onClick={() => onMediaSelect?.(media, person)}>
|
||||
<span className="pc-voice-play" aria-hidden="true"><Play size={13} fill="currentColor" strokeWidth={0} /></span>
|
||||
<span className="pc-voice-wave" aria-hidden="true">{VOICE_WAVE.map((h, i) => <i key={i} style={{ height: `${h}%` }} />)}</span>
|
||||
<span className="pc-voice-label">{media.label || 'Enregistrement'}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{editMode && (
|
||||
<div className="detail-row">
|
||||
<span className="label">Statut</span>
|
||||
{visualMedias.length > 0 && (
|
||||
<div className="pc-grid">
|
||||
{visualMedias.map((media) => (
|
||||
<button
|
||||
key={media.id}
|
||||
type="button"
|
||||
className={`alive-toggle ${getEditValue('isAlive', person.isAlive) ? 'alive-toggle--alive' : 'alive-toggle--deceased'}`}
|
||||
onClick={() => handleFieldChange('isAlive', !getEditValue('isAlive', person.isAlive))}
|
||||
className={`pc-tile pc-tile--${media.type}`}
|
||||
onClick={() => onMediaSelect?.(media, person)}
|
||||
title={media.label || TYPE_LABELS[media.type]}
|
||||
>
|
||||
<span className="alive-toggle-dot" />
|
||||
{getEditValue('isAlive', person.isAlive) ? 'Vivant(e)' : 'Décédé(e)'}
|
||||
{media.type === 'photo' && media.url
|
||||
? <img src={media.url} alt={media.label || ''} loading="lazy" />
|
||||
: <span className="pc-tile-video"><Play size={18} fill="currentColor" strokeWidth={0} /></span>}
|
||||
{media.label && <span className="pc-tile-label">{media.label}</span>}
|
||||
</button>
|
||||
))}
|
||||
{editMode && canManageMedia && (
|
||||
<button type="button" className="pc-tile pc-tile--add" onClick={() => onManageMedia?.()} aria-label="Ajouter des souvenirs">
|
||||
<Plus size={20} strokeWidth={1.8} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{otherMedias.length > 0 && (
|
||||
<div className="pc-list">
|
||||
{otherMedias.map((media) => (
|
||||
<button key={media.id} type="button" className="pc-list-item" onClick={() => onMediaSelect?.(media, person)}>
|
||||
<span className={`pc-list-icon pc-list-icon--${media.type}`}>{renderMediaIcon(media)}</span>
|
||||
<span className="pc-list-text">
|
||||
<span className="pc-list-label">{media.label || TYPE_LABELS[media.type]}</span>
|
||||
<span className="pc-list-type">{TYPE_LABELS[media.type] || media.type}</span>
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{editMode && canManageMedia && (
|
||||
<button type="button" className="pz-btn pz-btn--soft pz-btn--block" onClick={() => onManageMedia?.()}>
|
||||
<Plus size={16} strokeWidth={2} />
|
||||
Ajouter ou ranger des souvenirs
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Infos ── */}
|
||||
{tab === 'infos' && (
|
||||
<div className="pc-infos">
|
||||
<section className="pc-section">
|
||||
<h3 className="pz-eyebrow">État civil</h3>
|
||||
<div className="pc-rows">
|
||||
<div className="pc-row">
|
||||
<span className="pc-label">Naissance</span>
|
||||
{renderEditableValue({ field: 'birthYear', value: person.birthDate || person.birthYear, placeholder: 'Date de naissance', type: 'date' })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(!getEditValue('isAlive', person.isAlive) || (!editMode && !person.isAlive)) && (
|
||||
<div className="detail-row">
|
||||
<span className="label">Décédé(e)</span>
|
||||
{renderEditableValue({ field: 'deathYear', value: person.deathDate || person.deathYear || '', placeholder: 'Date de décès', type: 'date' })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!editMode && (
|
||||
<div className="detail-row">
|
||||
<span className="label">Âge</span>
|
||||
<span className="value">
|
||||
{age === null
|
||||
? '-'
|
||||
: person.isAlive
|
||||
? `${age} ans`
|
||||
: `${age} ans (au décès)`}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(person.region || editMode) && (
|
||||
<div className="detail-row">
|
||||
<span className="label">Région</span>
|
||||
{renderEditableValue({ field: 'region', value: person.region || '', placeholder: 'Région' })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(person.nationality || editMode) && (
|
||||
<div className="detail-row">
|
||||
<span className="label">Nationalité</span>
|
||||
{renderEditableValue({ field: 'nationality', value: person.nationality || '', placeholder: 'Nationalité' })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(person.profession || editMode) && (
|
||||
<div className="detail-row">
|
||||
<span className="label">Métier</span>
|
||||
{renderEditableValue({ field: 'profession', value: person.profession || '', placeholder: 'Métier' })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* --- Famille (lecture seule) --- */}
|
||||
{!editMode && (parentsList.length > 0 || personUnions.length > 0 || childrenList.length > 0) && (
|
||||
<>
|
||||
<div className="detail-section-title">Famille</div>
|
||||
{(person.birthPlace || editMode) && (
|
||||
<div className="pc-row">
|
||||
<span className="pc-label">Lieu</span>
|
||||
{renderEditableValue({ field: 'birthPlace', value: person.birthPlace || '', placeholder: 'Lieu de naissance' })}
|
||||
</div>
|
||||
)}
|
||||
{(person.birthName || editMode) && (
|
||||
<div className="pc-row">
|
||||
<span className="pc-label">Nom de naissance</span>
|
||||
{renderEditableValue({ field: 'birthName', value: person.birthName || '', placeholder: 'Nom de naissance' })}
|
||||
</div>
|
||||
)}
|
||||
{editMode && (
|
||||
<div className="pc-row">
|
||||
<span className="pc-label">Statut</span>
|
||||
<button
|
||||
type="button"
|
||||
className={`pc-alive ${getEditValue('isAlive', person.isAlive) ? 'is-alive' : 'is-deceased'}`}
|
||||
onClick={() => handleFieldChange('isAlive', !getEditValue('isAlive', person.isAlive))}
|
||||
>
|
||||
<span className="pc-alive-dot" />
|
||||
{getEditValue('isAlive', person.isAlive) ? 'En vie' : 'Décédé(e)'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{(!getEditValue('isAlive', person.isAlive) || (!editMode && !person.isAlive)) && (
|
||||
<div className="pc-row">
|
||||
<span className="pc-label">Décès</span>
|
||||
{renderEditableValue({ field: 'deathYear', value: person.deathDate || person.deathYear || '', placeholder: 'Date de décès', type: 'date' })}
|
||||
</div>
|
||||
)}
|
||||
{!editMode && (
|
||||
<div className="pc-row">
|
||||
<span className="pc-label">Âge</span>
|
||||
<span className="value">{ageLabel || '-'}</span>
|
||||
</div>
|
||||
)}
|
||||
{(person.region || editMode) && (
|
||||
<div className="pc-row">
|
||||
<span className="pc-label">Région</span>
|
||||
{renderEditableValue({ field: 'region', value: person.region || '', placeholder: 'Région' })}
|
||||
</div>
|
||||
)}
|
||||
{(person.nationality || editMode) && (
|
||||
<div className="pc-row">
|
||||
<span className="pc-label">Nationalité</span>
|
||||
{renderEditableValue({ field: 'nationality', value: person.nationality || '', placeholder: 'Nationalité' })}
|
||||
</div>
|
||||
)}
|
||||
{(person.profession || editMode) && (
|
||||
<div className="pc-row">
|
||||
<span className="pc-label">Métier</span>
|
||||
{renderEditableValue({ field: 'profession', value: person.profession || '', placeholder: 'Métier' })}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{!editMode && (parentsList.length > 0 || personUnions.length > 0 || childrenList.length > 0) && (
|
||||
<section className="pc-section">
|
||||
<h3 className="pz-eyebrow">Famille</h3>
|
||||
<div className="pc-rows">
|
||||
{parentsList.length > 0 && (
|
||||
<div className="detail-row">
|
||||
<span className="label">Parents</span>
|
||||
<span className="value">
|
||||
{getParentageLabel() || parentsList.map(p => `${p.firstName} ${p.lastName}`).join(' & ')}
|
||||
</span>
|
||||
<div className="pc-row">
|
||||
<span className="pc-label">Parents</span>
|
||||
<span className="value">{getParentageLabel() || parentsList.map(p => `${p.firstName} ${p.lastName}`).join(' et ')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{preferredFiliation && preferredFiliation.parentageType !== 'biologique' && parentsList.length <= 1 && (
|
||||
<div className="detail-row">
|
||||
<span className="label">Type</span>
|
||||
<span className="value parentage-type">
|
||||
{preferredFiliation.parentageType === 'adoption' ? 'Adoption' : 'Parent inconnu'}
|
||||
</span>
|
||||
<div className="pc-row">
|
||||
<span className="pc-label">Filiation</span>
|
||||
<span className="value">{preferredFiliation.parentageType === 'adoption' ? 'Adoption' : 'Parent inconnu'}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{personUnions.length > 0 && (
|
||||
<div className="detail-row">
|
||||
<span className="label">{personUnions.length > 1 ? 'Unions' : 'Union'}</span>
|
||||
<div className="pc-row">
|
||||
<span className="pc-label">{personUnions.length > 1 ? 'Unions' : 'Union'}</span>
|
||||
<span className="value">
|
||||
{personUnions.map(u => {
|
||||
const partnerId = u.partner1Id === person.id ? u.partner2Id : u.partner1Id
|
||||
const partner = persons.find(p => p.id === partnerId)
|
||||
const yearRange = u.startYear
|
||||
? u.endYear ? `${u.startYear}-${u.endYear}` : `depuis ${u.startYear}`
|
||||
: ''
|
||||
return `${partner?.firstName || '?'} ${partner?.lastName || ''} ${yearRange ? `(${yearRange})` : ''}`
|
||||
}).join(' / ')}
|
||||
const yearRange = u.startYear ? (u.endYear ? `${u.startYear}–${u.endYear}` : `depuis ${u.startYear}`) : ''
|
||||
return `${partner?.firstName || '?'} ${partner?.lastName || ''}${yearRange ? ` (${yearRange})` : ''}`
|
||||
}).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{childrenList.length > 0 && (
|
||||
<div className="detail-row">
|
||||
<span className="label">Enfants</span>
|
||||
<span className="value">
|
||||
{childrenList.map(c => c.firstName).join(', ')}
|
||||
</span>
|
||||
<div className="pc-row">
|
||||
<span className="pc-label">Enfants</span>
|
||||
<span className="value">{childrenList.map(c => c.firstName).join(', ')}</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* --- Notes --- */}
|
||||
{(person.note || editMode) && (
|
||||
<>
|
||||
<div className="detail-section-title">Notes</div>
|
||||
{editMode ? (
|
||||
<textarea
|
||||
className="detail-note detail-note--editable"
|
||||
value={getEditValue('note', person.note || '')}
|
||||
onChange={(e) => handleFieldChange('note', e.target.value)}
|
||||
placeholder="Ajouter une note..."
|
||||
rows={3}
|
||||
/>
|
||||
) : (
|
||||
<div className="detail-note">{person.note}</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Onglet Relations (mode édition uniquement) */}
|
||||
{tab === 'relations' && editMode && (
|
||||
<div className="details">
|
||||
{(relationActionLoading || relationActionSuccess || relationActionError) && (
|
||||
<div
|
||||
className={`relation-feedback ${
|
||||
relationActionError
|
||||
? 'relation-feedback--error'
|
||||
: relationActionSuccess
|
||||
? 'relation-feedback--success'
|
||||
: 'relation-feedback--loading'
|
||||
}`}
|
||||
>
|
||||
{relationActionLoading
|
||||
? 'Mise à jour des liens...'
|
||||
: relationActionError || relationActionSuccess}
|
||||
</div>
|
||||
)}
|
||||
<div className="detail-section-title">Parents</div>
|
||||
{parentsList.length > 0 ? (
|
||||
parentsList.map(p => (
|
||||
<div key={p.id} className="relation-row">
|
||||
<div className="relation-avatar-mini">
|
||||
{p.photo
|
||||
? <img src={p.photo} alt="" />
|
||||
: <span>{p.firstName[0]}</span>
|
||||
}
|
||||
</div>
|
||||
<span className="relation-name">{p.firstName} {p.lastName}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="relation-remove"
|
||||
aria-label={`Retirer ${p.firstName}`}
|
||||
onClick={() => handleRelationRemove(p.id, 'parent')}
|
||||
disabled={relationActionLoading}
|
||||
>
|
||||
<X size={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="relation-empty">Aucun parent défini</div>
|
||||
)}
|
||||
{activeRelationSearch === 'parent' ? (
|
||||
<RelationSearchInline
|
||||
ref={relationSearchRef}
|
||||
query={relationSearchQuery}
|
||||
onQueryChange={setRelationSearchQuery}
|
||||
results={getFilteredPersonsForRelation('parent')}
|
||||
onSelect={(p) => handleRelationAdd(p, 'parent')}
|
||||
onClose={() => { setActiveRelationSearch(null); setRelationSearchQuery('') }}
|
||||
placeholder="Rechercher un parent..."
|
||||
/>
|
||||
) : (
|
||||
<button type="button" className="relation-add-btn" onClick={() => setActiveRelationSearch('parent')} disabled={relationActionLoading}>
|
||||
<Plus size={14} strokeWidth={2} />
|
||||
Ajouter un parent
|
||||
</button>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
<div className="detail-section-title">Conjoint·e·s</div>
|
||||
{personUnions.length > 0 ? (
|
||||
personUnions.map(u => {
|
||||
const partnerId = u.partner1Id === person.id ? u.partner2Id : u.partner1Id
|
||||
const partner = persons.find(p => p.id === partnerId)
|
||||
return (
|
||||
<div key={u.id} className="relation-row">
|
||||
<div className="relation-avatar-mini">
|
||||
{partner?.photo
|
||||
? <img src={partner.photo} alt="" />
|
||||
: <span>{partner?.firstName?.[0] || '?'}</span>
|
||||
}
|
||||
</div>
|
||||
<span className="relation-name">{partner?.firstName} {partner?.lastName || ''}</span>
|
||||
<span className="relation-meta">{u.unionType}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="relation-remove"
|
||||
aria-label={`Retirer ${partner?.firstName}`}
|
||||
onClick={() => handleRelationRemove(partnerId, 'spouse')}
|
||||
disabled={relationActionLoading}
|
||||
>
|
||||
<X size={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="relation-empty">Aucun·e conjoint·e</div>
|
||||
)}
|
||||
{activeRelationSearch === 'spouse' ? (
|
||||
<RelationSearchInline
|
||||
ref={relationSearchRef}
|
||||
query={relationSearchQuery}
|
||||
onQueryChange={setRelationSearchQuery}
|
||||
results={getFilteredPersonsForRelation('spouse')}
|
||||
onSelect={(p) => handleRelationAdd(p, 'spouse')}
|
||||
onClose={() => { setActiveRelationSearch(null); setRelationSearchQuery('') }}
|
||||
placeholder="Rechercher un·e conjoint·e..."
|
||||
/>
|
||||
) : (
|
||||
<button type="button" className="relation-add-btn" onClick={() => setActiveRelationSearch('spouse')} disabled={relationActionLoading}>
|
||||
<Plus size={14} strokeWidth={2} />
|
||||
Ajouter un·e conjoint·e
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="detail-section-title">Enfants</div>
|
||||
{childrenList.length > 0 ? (
|
||||
childrenList.map(c => (
|
||||
<div key={c.id} className="relation-row">
|
||||
<div className="relation-avatar-mini">
|
||||
{c.photo
|
||||
? <img src={c.photo} alt="" />
|
||||
: <span>{c.firstName[0]}</span>
|
||||
}
|
||||
</div>
|
||||
<span className="relation-name">{c.firstName} {c.lastName}</span>
|
||||
<button
|
||||
type="button"
|
||||
className="relation-remove"
|
||||
aria-label={`Retirer ${c.firstName}`}
|
||||
onClick={() => handleRelationRemove(c.id, 'child')}
|
||||
disabled={relationActionLoading}
|
||||
>
|
||||
<X size={14} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<div className="relation-empty">Aucun enfant</div>
|
||||
)}
|
||||
{activeRelationSearch === 'child' ? (
|
||||
<RelationSearchInline
|
||||
ref={relationSearchRef}
|
||||
query={relationSearchQuery}
|
||||
onQueryChange={setRelationSearchQuery}
|
||||
results={getFilteredPersonsForRelation('child')}
|
||||
onSelect={(p) => handleRelationAdd(p, 'child')}
|
||||
onClose={() => { setActiveRelationSearch(null); setRelationSearchQuery('') }}
|
||||
placeholder="Rechercher un enfant..."
|
||||
/>
|
||||
) : (
|
||||
<button type="button" className="relation-add-btn" onClick={() => setActiveRelationSearch('child')} disabled={relationActionLoading}>
|
||||
<Plus size={14} strokeWidth={2} />
|
||||
Ajouter un enfant
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tab === 'medias' && (
|
||||
<div>
|
||||
<div className="media-list">
|
||||
{personMedias.length === 0 ? (
|
||||
<div className="media-empty">Aucun média associé</div>
|
||||
{(person.note || editMode) && (
|
||||
<section className="pc-section">
|
||||
<h3 className="pz-eyebrow">Notes</h3>
|
||||
{editMode ? (
|
||||
<textarea
|
||||
className="pc-input pc-note-input"
|
||||
value={getEditValue('note', person.note || '')}
|
||||
onChange={(e) => handleFieldChange('note', e.target.value)}
|
||||
placeholder="Une anecdote, un trait de caractère, ce qu'on se raconte à son sujet"
|
||||
rows={3}
|
||||
/>
|
||||
) : (
|
||||
personMedias.map(media => (
|
||||
<button
|
||||
key={media.id}
|
||||
className="media-item"
|
||||
onClick={() => onMediaSelect?.(media, person)}
|
||||
>
|
||||
<div className={`media-item-icon ${media.type}`}>
|
||||
{renderMediaIcon(media)}
|
||||
|
||||
</div>
|
||||
<div className="media-item-info">
|
||||
<span className="media-item-label">{media.label}</span>
|
||||
<span className="media-item-type">{TYPE_LABELS[media.type] || media.type}</span>
|
||||
</div>
|
||||
</button>
|
||||
))
|
||||
<p className="pc-note">{person.note}</p>
|
||||
)}
|
||||
</div>
|
||||
{editMode && canManageMedia && (
|
||||
<div className="media-manage-footer">
|
||||
<button type="button" className="media-manage-btn media-manage-btn--footer" onClick={() => onManageMedia?.()}>
|
||||
Ajouter / Gérer des médias
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Actions mode édition */}
|
||||
{editMode && (
|
||||
<div className="person-card-actions">
|
||||
<button
|
||||
type="button"
|
||||
className={`person-card-save ${saveSuccess ? 'person-card-save--success' : ''}`}
|
||||
disabled={(!hasChanges && !saveSuccess) || saving}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{saveSuccess ? (
|
||||
<>
|
||||
<Check size={16} strokeWidth={2.5} />
|
||||
Enregistré
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save size={16} strokeWidth={2} />
|
||||
{saving ? 'Enregistrement...' : 'Sauvegarder'}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{isAdmin && (
|
||||
<>
|
||||
{showDeleteConfirm ? (
|
||||
<div className="person-card-delete-confirm">
|
||||
<span className="person-card-delete-text">Supprimer cette personne ?</span>
|
||||
<button type="button" className="person-card-delete-yes" onClick={handleDelete}>
|
||||
Confirmer
|
||||
</button>
|
||||
<button type="button" className="person-card-delete-no" onClick={() => setShowDeleteConfirm(false)}>
|
||||
Annuler
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="person-card-delete"
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
>
|
||||
<Trash2 size={14} strokeWidth={2} />
|
||||
Supprimer
|
||||
</button>
|
||||
)}
|
||||
</>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Liens (édition) ── */}
|
||||
{tab === 'relations' && editMode && (
|
||||
<div className="pc-infos">
|
||||
{(relationActionLoading || relationActionSuccess || relationActionError) && (
|
||||
<div className={relationActionError ? 'pz-error pc-inline-msg' : relationActionSuccess ? 'pz-success pc-inline-msg' : 'pc-inline-msg pc-inline-msg--loading'}>
|
||||
{relationActionLoading ? 'Mise à jour des liens…' : relationActionError || relationActionSuccess}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className="pc-section">
|
||||
<h3 className="pz-eyebrow">Parents</h3>
|
||||
{parentsList.length > 0 ? parentsList.map(p => renderRelationRow(p, 'parent')) : <p className="pc-rel-empty">Aucun parent pour l'instant</p>}
|
||||
{renderRelationAdd('parent', 'Ajouter un parent', 'Rechercher un parent')}
|
||||
</section>
|
||||
|
||||
<section className="pc-section">
|
||||
<h3 className="pz-eyebrow">Conjoint·e·s</h3>
|
||||
{personUnions.length > 0 ? personUnions.map(u => {
|
||||
const partnerId = u.partner1Id === person.id ? u.partner2Id : u.partner1Id
|
||||
const partner = persons.find(p => p.id === partnerId)
|
||||
return renderRelationRow(partner || { id: partnerId, firstName: '?' }, 'spouse', u.unionType)
|
||||
}) : <p className="pc-rel-empty">Aucun·e conjoint·e</p>}
|
||||
{renderRelationAdd('spouse', 'Ajouter un·e conjoint·e', 'Rechercher un·e conjoint·e')}
|
||||
</section>
|
||||
|
||||
<section className="pc-section">
|
||||
<h3 className="pz-eyebrow">Enfants</h3>
|
||||
{childrenList.length > 0 ? childrenList.map(c => renderRelationRow(c, 'child')) : <p className="pc-rel-empty">Aucun enfant</p>}
|
||||
{renderRelationAdd('child', 'Ajouter un enfant', 'Rechercher un enfant')}
|
||||
</section>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── Actions d'édition ── */}
|
||||
{editMode && (
|
||||
<footer className="pc-foot">
|
||||
{showDeleteConfirm ? (
|
||||
<div className="pc-delete-confirm">
|
||||
<span>Supprimer {person.firstName} de l'arbre ?</span>
|
||||
<div className="pc-delete-actions">
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--sm" onClick={() => setShowDeleteConfirm(false)}>Annuler</button>
|
||||
<button type="button" className="pz-btn pz-btn--danger pz-btn--sm" onClick={handleDelete}>Supprimer</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{isAdmin && (
|
||||
<button type="button" className="pz-btn pz-btn--danger-ghost pz-btn--sm" onClick={() => setShowDeleteConfirm(true)}>
|
||||
<Trash2 size={15} strokeWidth={2} />
|
||||
Supprimer
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={`pz-btn pz-btn--primary pc-save ${saveSuccess ? 'is-saved' : ''}`}
|
||||
disabled={(!hasChanges && !saveSuccess) || saving}
|
||||
onClick={handleSave}
|
||||
>
|
||||
{saveSuccess ? (<><Check size={16} strokeWidth={2.5} />Enregistré</>) : (<><Save size={16} strokeWidth={2} />{saving ? 'Enregistrement…' : 'Enregistrer'}</>)}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</footer>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Inline searchable dropdown for adding relations
|
||||
* Appears in-place when clicking "+ Ajouter un parent/conjoint/enfant"
|
||||
* Recherche en ligne pour ajouter un lien (parent, conjoint·e, enfant)
|
||||
*/
|
||||
const RelationSearchInline = forwardRef(function RelationSearchInline(
|
||||
{ query, onQueryChange, results, onSelect, onClose, placeholder },
|
||||
@@ -763,13 +675,13 @@ const RelationSearchInline = forwardRef(function RelationSearchInline(
|
||||
const filtered = results.slice(0, 6)
|
||||
|
||||
return (
|
||||
<div className="relation-search-inline">
|
||||
<div className="relation-search-inline-input-wrap">
|
||||
<Search size={13} strokeWidth={2} className="relation-search-inline-icon" />
|
||||
<div className="relation-search-inline pc-search">
|
||||
<div className="pc-search-field">
|
||||
<Search size={15} strokeWidth={2} className="pc-search-icon" />
|
||||
<input
|
||||
ref={ref}
|
||||
type="text"
|
||||
className="relation-search-inline-input"
|
||||
className="pc-search-input"
|
||||
value={query}
|
||||
onChange={(e) => onQueryChange(e.target.value)}
|
||||
placeholder={placeholder}
|
||||
@@ -781,44 +693,29 @@ const RelationSearchInline = forwardRef(function RelationSearchInline(
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button type="button" className="relation-search-inline-close" onClick={onClose}>
|
||||
<X size={13} strokeWidth={2} />
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--icon pc-search-close" onClick={onClose} aria-label="Fermer la recherche">
|
||||
<X size={15} strokeWidth={2} />
|
||||
</button>
|
||||
</div>
|
||||
{filtered.length > 0 ? (
|
||||
<ul className="relation-search-inline-list">
|
||||
<ul className="pc-search-list">
|
||||
{filtered.map((p) => (
|
||||
<li key={p.id}>
|
||||
<button
|
||||
type="button"
|
||||
className="relation-search-inline-item"
|
||||
onClick={() => onSelect(p)}
|
||||
>
|
||||
<div className="relation-search-inline-avatar">
|
||||
{p.photo
|
||||
? <img src={p.photo} alt="" />
|
||||
: <span>{p.firstName[0]}{(p.lastName || '')[0] || ''}</span>
|
||||
}
|
||||
</div>
|
||||
<div className="relation-search-inline-info">
|
||||
<span className="relation-search-inline-name">
|
||||
{p.firstName} {p.lastName || ''}
|
||||
</span>
|
||||
{p.birthYear && (
|
||||
<span className="relation-search-inline-year">{p.birthYear}</span>
|
||||
)}
|
||||
</div>
|
||||
<button type="button" className="pc-search-item" onClick={() => onSelect(p)}>
|
||||
<span className="pc-rel-avatar" aria-hidden="true">
|
||||
{p.photo ? <img src={p.photo} alt="" /> : <span>{p.firstName[0]}</span>}
|
||||
</span>
|
||||
<span className="pc-rel-name">{p.firstName} <em>{p.lastName || ''}</em></span>
|
||||
{p.birthYear && <span className="pc-rel-meta">{p.birthYear}</span>}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : query ? (
|
||||
<div className="relation-search-inline-empty">Aucun résultat</div>
|
||||
<p className="pc-rel-empty">Aucune personne ne correspond</p>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default PersonCard
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
.proximity-line-path.sibling {
|
||||
stroke: rgba(255, 149, 0, 0.7);
|
||||
stroke: rgba(210, 105, 74, 0.7);
|
||||
}
|
||||
|
||||
.proximity-line-path.none {
|
||||
@@ -61,7 +61,7 @@
|
||||
pointer-events: none;
|
||||
z-index: 1001;
|
||||
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0 4px 12px rgba(42, 38, 34, 0.15);
|
||||
|
||||
animation: badgePulse 0.5s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
@@ -88,7 +88,7 @@
|
||||
}
|
||||
|
||||
.proximity-badge.sibling {
|
||||
background: rgba(255, 149, 0, 0.95);
|
||||
background: rgba(210, 105, 74, 0.95);
|
||||
color: white;
|
||||
}
|
||||
|
||||
@@ -128,8 +128,8 @@
|
||||
.proximity-highlight-glow.sibling {
|
||||
background: radial-gradient(
|
||||
circle,
|
||||
rgba(255, 149, 0, 0.35) 0%,
|
||||
rgba(255, 149, 0, 0.15) 50%,
|
||||
rgba(210, 105, 74, 0.35) 0%,
|
||||
rgba(210, 105, 74, 0.15) 50%,
|
||||
transparent 100%
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
/* ============================================================
|
||||
Paramètres de l'arbre et contributions de la famille (DA terracotta)
|
||||
Les fenêtres, onglets, boutons et champs viennent de src/styles/pz.css.
|
||||
============================================================ */
|
||||
|
||||
/* ── Paramètres de l'arbre ── */
|
||||
.ts-content { display: grid; gap: 14px; min-height: 220px; align-content: start; }
|
||||
.ts-loading { padding: 24px 0; text-align: center; }
|
||||
.ts-panel { display: grid; gap: 16px; }
|
||||
.ts-optional { font-weight: 400; color: var(--pz-faint); margin-left: 4px; }
|
||||
.ts-actions { display: flex; justify-content: flex-end; }
|
||||
|
||||
.ts-policy { display: grid; gap: 10px; margin: 6px 0 0; padding: 18px 0 0; border: 0; border-top: 1px solid var(--pz-line); min-width: 0; }
|
||||
.ts-policy legend { padding: 0; margin-bottom: 4px; float: left; width: 100%; }
|
||||
.ts-policy[disabled] { opacity: .6; }
|
||||
.ts-choice {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--pz-line);
|
||||
border-radius: var(--pz-r-m);
|
||||
cursor: pointer;
|
||||
transition: border-color 160ms ease, background-color 160ms ease;
|
||||
}
|
||||
.ts-choice.is-active { border-color: var(--pz-a500); background: var(--pz-a50); box-shadow: 0 0 0 3px var(--pz-a100); }
|
||||
.ts-choice input { margin-top: 3px; accent-color: var(--pz-a700); width: 16px; height: 16px; flex: none; }
|
||||
.ts-choice-text { display: grid; gap: 3px; }
|
||||
.ts-choice-text strong { font: 500 14.5px var(--pz-sans); color: var(--pz-ink); }
|
||||
.ts-choice-text small { font: 400 13px/1.45 var(--pz-sans); color: var(--pz-muted); }
|
||||
|
||||
.ts-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px; border: 1px solid var(--pz-line); border-radius: var(--pz-r-m); }
|
||||
.ts-row--muted { opacity: .6; }
|
||||
.ts-row-text { display: grid; gap: 4px; min-width: 0; }
|
||||
.ts-row-text strong { font: 500 14.5px var(--pz-sans); color: var(--pz-ink); }
|
||||
.ts-row-actions { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; flex: none; }
|
||||
.ts-file { justify-self: start; margin-top: 4px; }
|
||||
.ts-file-input { display: none; }
|
||||
.ts-danger { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 16px; border: 1px solid #f1c9c4; border-radius: var(--pz-r-m); background: var(--pz-danger-bg); }
|
||||
.ts-delete-overlay { z-index: 2100; }
|
||||
@media (max-width: 560px) {
|
||||
.ts-row, .ts-danger { flex-direction: column; align-items: stretch; }
|
||||
.ts-row-actions { justify-content: flex-start; }
|
||||
}
|
||||
|
||||
/* ── Contributions : invitation ── */
|
||||
.contrib-list-section { display: grid; gap: 18px; }
|
||||
.contrib-invite { display: grid; gap: 12px; padding: 16px; border-radius: var(--pz-r-m); background: var(--pz-a50); border: 1px solid var(--pz-a100); }
|
||||
.contrib-invite-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.contrib-invite-title { font: 500 14.5px var(--pz-sans); color: var(--pz-ink); }
|
||||
.contrib-copy-feedback { font: 500 12.5px var(--pz-sans); color: var(--pz-success); }
|
||||
.contrib-invite-link { display: flex; align-items: center; gap: 8px; min-width: 0; padding: 5px 5px 5px 14px; background: var(--pz-surface); border: 1px solid var(--pz-line); border-radius: 999px; }
|
||||
.contrib-link-url { flex: 1; min-width: 0; font: 400 13px var(--pz-sans); color: var(--pz-ink-2); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; background: none; padding: 0; }
|
||||
.contrib-invite-passwords { display: grid; gap: 6px; }
|
||||
.contrib-role-row { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; padding: 8px 8px 8px 12px; background: var(--pz-surface); border: 1px solid var(--pz-line); border-radius: var(--pz-r-m); }
|
||||
.contrib-role-badge { display: grid; gap: 1px; min-width: 130px; flex: 1; }
|
||||
.contrib-role-badge strong { font: 500 14px var(--pz-sans); color: var(--pz-ink); }
|
||||
.contrib-role-badge small { font: 400 12px var(--pz-sans); color: var(--pz-muted); }
|
||||
.contrib-pass-reveal {
|
||||
min-height: 34px;
|
||||
padding: 0 12px;
|
||||
border: 1px dashed var(--pz-a300);
|
||||
border-radius: 999px;
|
||||
background: transparent;
|
||||
font: 500 13.5px var(--pz-sans);
|
||||
letter-spacing: .02em;
|
||||
color: var(--pz-ink);
|
||||
cursor: pointer;
|
||||
transition: filter 200ms ease;
|
||||
}
|
||||
.contrib-pass-reveal--blurred { filter: blur(4px); }
|
||||
.contrib-pass-reveal:disabled { cursor: default; color: var(--pz-faint); filter: none; }
|
||||
.contrib-pass-edit-inline { display: flex; gap: 6px; flex-wrap: wrap; flex: 1; justify-content: flex-end; }
|
||||
.contrib-pass-edit-input { min-height: 34px; min-width: 180px; padding: 0 12px; border: 1px solid var(--pz-a500); border-radius: 999px; box-shadow: 0 0 0 3px var(--pz-a100); font: 400 13.5px var(--pz-sans); outline: none; }
|
||||
|
||||
/* Petits boutons en pilule de cette fenêtre */
|
||||
.contrib-pill-btn,
|
||||
.contrib-quick-btn,
|
||||
.contrib-confirm-yes,
|
||||
.contrib-confirm-no {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 34px;
|
||||
padding: 0 14px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font: 500 13px var(--pz-sans);
|
||||
transition: background-color 160ms ease, transform 140ms var(--pz-ease);
|
||||
}
|
||||
.contrib-pill-btn:active, .contrib-quick-btn:active, .contrib-confirm-yes:active, .contrib-confirm-no:active { transform: scale(.97); }
|
||||
.contrib-pill-btn:disabled, .contrib-quick-btn:disabled, .contrib-confirm-yes:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.contrib-pill-btn--edit, .contrib-pill-btn--ghost, .contrib-quick-btn, .contrib-confirm-no { background: transparent; color: var(--pz-ink-2); box-shadow: inset 0 0 0 1px var(--pz-line); }
|
||||
.contrib-pill-btn--save, .contrib-confirm-yes { background: var(--pz-ink); color: #fff; }
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.contrib-pill-btn--edit:hover:not(:disabled), .contrib-pill-btn--ghost:hover:not(:disabled), .contrib-quick-btn:hover:not(:disabled), .contrib-confirm-no:hover { background: var(--pz-line-soft); }
|
||||
.contrib-pill-btn--save:hover:not(:disabled), .contrib-confirm-yes:hover:not(:disabled) { background: #3d3731; }
|
||||
}
|
||||
|
||||
.contrib-error { padding: 11px 14px; border-radius: var(--pz-r-s); background: var(--pz-danger-bg); color: var(--pz-danger); font: 500 13.5px/1.45 var(--pz-sans); }
|
||||
.contrib-ok { padding: 11px 14px; border-radius: var(--pz-r-s); background: var(--pz-success-bg); color: var(--pz-success); font: 500 13.5px/1.45 var(--pz-sans); }
|
||||
|
||||
/* ── Contributions : propositions ── */
|
||||
.contrib-list-header { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 12px; padding-top: 8px; border-top: 1px solid var(--pz-line); }
|
||||
.contrib-list-heading h2 { margin: 12px 0 2px; font: 400 22px/1.2 var(--pz-serif); color: var(--pz-ink); }
|
||||
.contrib-list-heading p { margin: 0; font: 400 13px var(--pz-sans); color: var(--pz-muted); }
|
||||
.contrib-list-header-actions { display: flex; align-items: center; gap: 6px; flex: none; }
|
||||
.contrib-count { min-width: 26px; height: 26px; padding: 0 8px; display: grid; place-items: center; border-radius: 999px; background: var(--pz-a100); color: var(--pz-a700); font: 600 12.5px var(--pz-sans); }
|
||||
|
||||
.contrib-empty, .contrib-readonly { padding: 22px 16px; text-align: center; border: 1px dashed var(--pz-a300); border-radius: var(--pz-r-m); background: var(--pz-a50); font: 400 14px/1.5 var(--pz-sans); color: var(--pz-ink-2); }
|
||||
|
||||
.contrib-list { display: grid; gap: 12px; }
|
||||
.contrib-list-section, .contrib-invite, .contrib-list, .contrib-item, .contrib-change-card { min-width: 0; }
|
||||
.contrib-item { border: 1px solid var(--pz-line); border-radius: var(--pz-r-m); background: var(--pz-surface); overflow: hidden; }
|
||||
.contrib-item-main { display: grid; gap: 6px; padding: 14px 16px; }
|
||||
.contrib-item-main--clickable { cursor: pointer; }
|
||||
@media (hover: hover) and (pointer: fine) { .contrib-item-main--clickable:hover { background: var(--pz-bg); } }
|
||||
.contrib-item-top { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
|
||||
.contrib-session-title { font: 400 18px/1.25 var(--pz-serif); color: var(--pz-ink); }
|
||||
.contrib-session-top-right { display: flex; align-items: center; gap: 8px; flex: none; color: var(--pz-muted); }
|
||||
.contrib-session-date { font: 400 12.5px var(--pz-sans); color: var(--pz-muted); }
|
||||
.contrib-collapse-icon { transition: transform 200ms var(--pz-ease); }
|
||||
.contrib-collapse-icon--up { transform: rotate(180deg); }
|
||||
.contrib-item-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font: 400 12.5px var(--pz-sans); color: var(--pz-muted); }
|
||||
.contrib-meta-sep { color: var(--pz-faint); }
|
||||
.contrib-decision-summary { padding: 2px 8px; border-radius: 999px; font: 500 12px var(--pz-sans); }
|
||||
.contrib-decision-summary--ok { background: var(--pz-success-bg); color: var(--pz-success); }
|
||||
.contrib-decision-summary--ko { background: var(--pz-danger-bg); color: var(--pz-danger); }
|
||||
.contrib-decision-summary--pending { background: var(--pz-line-soft); color: var(--pz-ink-2); }
|
||||
.contrib-all-decided-badge { padding: 2px 8px; border-radius: 999px; background: var(--pz-a100); color: var(--pz-a700); font: 500 12px var(--pz-sans); }
|
||||
.contrib-session-comment { margin: 4px 0 0; padding-left: 12px; border-left: 2px solid var(--pz-a300); font: 300 italic 16px/1.45 var(--pz-serif); color: var(--pz-ink); }
|
||||
|
||||
.contrib-change-list { display: grid; gap: 10px; padding: 0 16px 14px; }
|
||||
.contrib-change-group { display: grid; gap: 6px; }
|
||||
.contrib-group-header { font: 500 11.5px var(--pz-sans); letter-spacing: .08em; text-transform: uppercase; color: var(--pz-faint); padding-top: 4px; }
|
||||
|
||||
.contrib-change-card { display: grid; gap: 8px; padding: 10px 12px; border: 1px solid var(--pz-line); border-radius: 12px; background: var(--pz-surface); transition: border-color 160ms ease, background-color 160ms ease; }
|
||||
.contrib-change-card--approved { border-color: #b9d8b2; background: var(--pz-success-bg); }
|
||||
.contrib-change-card--rejected { border-color: #f1c9c4; background: var(--pz-danger-bg); }
|
||||
.contrib-change-card--rejected .contrib-change-fields { opacity: .6; }
|
||||
.contrib-change-card-head { display: flex; align-items: center; gap: 8px; }
|
||||
.contrib-action-badge { flex: none; padding: 2px 8px; border-radius: 999px; font: 500 11.5px var(--pz-sans); background: var(--pz-line-soft); color: var(--pz-ink-2); }
|
||||
.contrib-action-badge--create { background: var(--pz-success-bg); color: var(--pz-success); }
|
||||
.contrib-action-badge--update { background: var(--pz-a100); color: var(--pz-a700); }
|
||||
.contrib-action-badge--delete { background: var(--pz-danger-bg); color: var(--pz-danger); }
|
||||
.contrib-change-entity-name { flex: 1; min-width: 0; font: 400 16px/1.2 var(--pz-serif); color: var(--pz-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.contrib-conflict-badge { flex: none; padding: 2px 8px; border-radius: 999px; background: #fff1d6; color: #7a5600; font: 500 11.5px var(--pz-sans); }
|
||||
.contrib-card-decisions { display: flex; gap: 4px; flex: none; }
|
||||
.contrib-decision-icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid var(--pz-line);
|
||||
border-radius: 50%;
|
||||
background: var(--pz-surface);
|
||||
color: var(--pz-muted);
|
||||
cursor: pointer;
|
||||
transition: background-color 160ms ease, color 160ms ease, border-color 160ms ease, transform 140ms var(--pz-ease);
|
||||
}
|
||||
.contrib-decision-icon:active { transform: scale(.94); }
|
||||
.contrib-decision-icon--approve.active { background: var(--pz-success); border-color: var(--pz-success); color: #fff; }
|
||||
.contrib-decision-icon--reject.active { background: var(--pz-danger); border-color: var(--pz-danger); color: #fff; }
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.contrib-decision-icon--approve:hover:not(.active):not(:disabled) { border-color: var(--pz-success); color: var(--pz-success); }
|
||||
.contrib-decision-icon--reject:hover:not(.active):not(:disabled) { border-color: var(--pz-danger); color: var(--pz-danger); }
|
||||
}
|
||||
|
||||
.contrib-change-fields { display: grid; gap: 4px; }
|
||||
.contrib-field-row { display: flex; gap: 10px; font: 400 13px/1.4 var(--pz-sans); }
|
||||
.contrib-field-label { flex: none; width: 110px; color: var(--pz-muted); }
|
||||
.contrib-field-diff { display: inline-flex; flex-wrap: wrap; gap: 6px; align-items: baseline; min-width: 0; }
|
||||
.contrib-field-old { color: var(--pz-faint); text-decoration: line-through; }
|
||||
.contrib-field-arrow { color: var(--pz-faint); }
|
||||
.contrib-field-new { color: var(--pz-ink); font-weight: 500; }
|
||||
|
||||
.contrib-annotation-preview { display: flex; align-items: center; gap: 8px; }
|
||||
.contrib-ann-emoji { font-size: 22px; }
|
||||
.contrib-ann-text { font: 300 italic 15px var(--pz-serif); color: var(--pz-ink-2); }
|
||||
.contrib-ann-drawing { padding-left: 8px; border-left: 3px solid; font: 400 13px var(--pz-sans); color: var(--pz-muted); }
|
||||
.contrib-ann-photo { max-width: 120px; border-radius: 8px; }
|
||||
|
||||
.contrib-session-footer { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px; padding: 12px 16px; border-top: 1px solid var(--pz-line-soft); background: var(--pz-bg); }
|
||||
.contrib-session-quick { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
||||
.contrib-session-quick--full { width: 100%; justify-content: flex-end; }
|
||||
.contrib-apply-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 38px;
|
||||
padding: 0 16px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: var(--pz-ink);
|
||||
color: #fff;
|
||||
font: 500 13.5px var(--pz-sans);
|
||||
cursor: pointer;
|
||||
transition: background-color 160ms ease, transform 140ms var(--pz-ease);
|
||||
}
|
||||
.contrib-apply-btn:active { transform: scale(.97); }
|
||||
.contrib-apply-btn:disabled { opacity: .45; cursor: not-allowed; }
|
||||
@media (hover: hover) and (pointer: fine) { .contrib-apply-btn:hover:not(:disabled) { background: #3d3731; } }
|
||||
.contrib-confirm-approve { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font: 500 13px var(--pz-sans); color: var(--pz-ink); }
|
||||
|
||||
/* ── Notification après envoi d'une contribution (montée dans App.jsx) ── */
|
||||
.contrib-success-toast {
|
||||
position: fixed;
|
||||
bottom: 92px;
|
||||
left: 50%;
|
||||
z-index: 3000;
|
||||
padding: 11px 20px;
|
||||
border-radius: 999px;
|
||||
background: var(--pz-ink);
|
||||
color: var(--pz-bg);
|
||||
font: 500 14px var(--pz-sans);
|
||||
box-shadow: var(--pz-shadow-lg);
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
animation: contrib-toast-in 260ms var(--pz-ease) both;
|
||||
}
|
||||
@keyframes contrib-toast-in {
|
||||
from { opacity: 0; transform: translate(-50%, 8px); }
|
||||
to { opacity: 1; transform: translate(-50%, 0); }
|
||||
}
|
||||
@media (max-width: 768px) { .contrib-success-toast { bottom: 78px; font-size: 13px; } }
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.contrib-invite-link { flex-wrap: wrap; border-radius: var(--pz-r-m); padding: 10px; }
|
||||
.contrib-link-url { flex-basis: 100%; }
|
||||
.contrib-session-footer { flex-direction: column; align-items: stretch; }
|
||||
.contrib-session-quick { justify-content: flex-end; }
|
||||
.contrib-field-label { width: 88px; }
|
||||
}
|
||||
@@ -1,92 +1,27 @@
|
||||
.tree-wizard-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 40;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 20px;
|
||||
background: rgba(44, 36, 30, 0.36);
|
||||
}
|
||||
/* Création d'un arbre en deux étapes (DA terracotta) : la fenêtre vient de pz.css */
|
||||
.tw-steps { display: flex; align-items: center; gap: 6px; }
|
||||
.tw-step { width: 26px; height: 4px; border-radius: 4px; background: var(--pz-line); transition: background-color 240ms ease; }
|
||||
.tw-step.is-on { background: var(--pz-a500); }
|
||||
.tw-steps .pz-small { margin-left: 6px; }
|
||||
|
||||
.tree-wizard-card {
|
||||
width: min(480px, 100%);
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #d8c7ac;
|
||||
background: #fffdf8;
|
||||
padding: 20px;
|
||||
box-shadow: 0 24px 45px rgba(50, 38, 25, 0.34);
|
||||
}
|
||||
.tw-form { display: grid; gap: 18px; }
|
||||
.tw-optional { font-weight: 400; color: var(--pz-faint); margin-left: 4px; }
|
||||
|
||||
.tree-wizard-card h2 {
|
||||
color: #453d37;
|
||||
font-size: 26px;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.tree-wizard-card p {
|
||||
color: #6e6258;
|
||||
}
|
||||
|
||||
.tree-wizard-card label {
|
||||
font-size: 14px;
|
||||
color: #6e6258;
|
||||
}
|
||||
|
||||
.tree-wizard-card input {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #d9cdb8;
|
||||
background: #fffdf8;
|
||||
color: #453d37;
|
||||
padding: 10px 12px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tree-wizard-card input:focus {
|
||||
border-color: #b99762;
|
||||
box-shadow: 0 0 0 2px rgba(185, 151, 98, 0.15);
|
||||
}
|
||||
|
||||
.tree-wizard-form {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.tree-wizard-actions {
|
||||
.tw-photo-input { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
|
||||
.tw-photo {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.tree-wizard-actions button {
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
padding: 11px 14px;
|
||||
font-weight: 450;
|
||||
color: #fef9ed;
|
||||
background: linear-gradient(135deg, #8b6a43, #6a4f32);
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px 14px;
|
||||
border: 1.5px dashed var(--pz-a300);
|
||||
border-radius: var(--pz-r-m);
|
||||
background: var(--pz-a50);
|
||||
cursor: pointer;
|
||||
transition: background-color 160ms ease, border-color 160ms ease;
|
||||
}
|
||||
|
||||
.tree-wizard-actions button:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.tree-wizard-actions .ghost {
|
||||
background: transparent;
|
||||
color: #6a4f32;
|
||||
border: 1px solid #cdb89a;
|
||||
}
|
||||
|
||||
.tree-wizard-error {
|
||||
background: rgba(171, 49, 49, 0.1);
|
||||
border: 1px solid rgba(171, 49, 49, 0.32);
|
||||
color: #862f2f;
|
||||
border-radius: 10px;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) { .tw-photo:hover { background: var(--pz-a100); } }
|
||||
.tw-photo-input:focus-visible + .tw-photo, .tw-photo:focus-within { outline: 2px solid var(--pz-a500); outline-offset: 2px; }
|
||||
.tw-photo-icon { width: 40px; height: 40px; flex: none; display: grid; place-items: center; border-radius: 12px; background: var(--pz-surface); color: var(--pz-a700); }
|
||||
.tw-photo-text { display: grid; gap: 2px; min-width: 0; }
|
||||
.tw-photo-text strong { font: 500 14px var(--pz-sans); color: var(--pz-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.tw-photo-text span { font: 400 12.5px var(--pz-sans); color: var(--pz-muted); }
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { ImagePlus } from 'lucide-react'
|
||||
import './TreeCreationWizard.css'
|
||||
|
||||
const INITIAL_STATE = {
|
||||
@@ -73,86 +74,119 @@ function TreeCreationWizard({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="tree-wizard-overlay" role="dialog" aria-modal="true">
|
||||
<div className="tree-wizard-card">
|
||||
<div className="pz-overlay tree-wizard-overlay" role="dialog" aria-modal="true" aria-labelledby="treeWizardTitle">
|
||||
<div className="pz-modal tree-wizard-card">
|
||||
<div className="tw-steps" aria-label={`Étape ${step} sur 2`}>
|
||||
<span className={`tw-step ${step >= 1 ? 'is-on' : ''}`} />
|
||||
<span className={`tw-step ${step >= 2 ? 'is-on' : ''}`} />
|
||||
<span className="pz-small">Étape {step} sur 2</span>
|
||||
</div>
|
||||
|
||||
{step === 1 ? (
|
||||
<>
|
||||
<h2>Comment s appelle votre famille ?</h2>
|
||||
<p>Ce nom apparaitra sur votre arbre et dans le lien de partage.</p>
|
||||
<div className="pz-modal-head">
|
||||
<h2 id="treeWizardTitle" className="pz-title">Comment s'appelle <em>votre famille</em> ?</h2>
|
||||
<p className="pz-sub">Ce nom apparaîtra en haut de l'arbre et dans le lien que vous partagerez.</p>
|
||||
</div>
|
||||
|
||||
<label htmlFor="wizardTreeName">Nom de la famille</label>
|
||||
<input
|
||||
id="wizardTreeName"
|
||||
type="text"
|
||||
autoFocus
|
||||
maxLength={120}
|
||||
value={form.treeName}
|
||||
onChange={(event) => updateField('treeName', event.target.value)}
|
||||
placeholder="Famille Martin"
|
||||
required
|
||||
/>
|
||||
<div className="pz-field">
|
||||
<label htmlFor="wizardTreeName">Nom de la famille</label>
|
||||
<input
|
||||
id="wizardTreeName"
|
||||
type="text"
|
||||
autoFocus
|
||||
maxLength={120}
|
||||
value={form.treeName}
|
||||
onChange={(event) => updateField('treeName', event.target.value)}
|
||||
onKeyDown={(event) => { if (event.key === 'Enter') { event.preventDefault(); handleContinue() } }}
|
||||
placeholder="Famille Martin"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="tree-wizard-actions">
|
||||
<button type="button" className="ghost" onClick={onClose} disabled={loading}>
|
||||
<div className="pz-modal-actions">
|
||||
<button type="button" className="pz-btn pz-btn--ghost" onClick={onClose} disabled={loading}>
|
||||
Fermer
|
||||
</button>
|
||||
<button type="button" onClick={handleContinue} disabled={!canContinue}>
|
||||
<button type="button" className="pz-btn pz-btn--primary" onClick={handleContinue} disabled={!canContinue}>
|
||||
Continuer
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<form onSubmit={handleCreate} className="tree-wizard-form">
|
||||
<h2>Commencez par vous.</h2>
|
||||
<p>Votre arbre sera cree avec votre premiere fiche.</p>
|
||||
<form onSubmit={handleCreate} className="tw-form">
|
||||
<div className="pz-modal-head">
|
||||
<h2 id="treeWizardTitle" className="pz-title">Commencez <em>par vous</em></h2>
|
||||
<p className="pz-sub">L'arbre « {form.treeName.trim()} » démarre avec votre fiche. Vous ajouterez les autres ensuite.</p>
|
||||
</div>
|
||||
|
||||
<label htmlFor="wizardSelfFirstName">Votre prenom</label>
|
||||
<input
|
||||
id="wizardSelfFirstName"
|
||||
type="text"
|
||||
maxLength={120}
|
||||
value={form.selfFirstName}
|
||||
onChange={(event) => updateField('selfFirstName', event.target.value)}
|
||||
required
|
||||
/>
|
||||
<div className="pz-row2">
|
||||
<div className="pz-field">
|
||||
<label htmlFor="wizardSelfFirstName">Votre prénom</label>
|
||||
<input
|
||||
id="wizardSelfFirstName"
|
||||
type="text"
|
||||
maxLength={120}
|
||||
autoComplete="given-name"
|
||||
value={form.selfFirstName}
|
||||
onChange={(event) => updateField('selfFirstName', event.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="pz-field">
|
||||
<label htmlFor="wizardSelfLastName">Votre nom</label>
|
||||
<input
|
||||
id="wizardSelfLastName"
|
||||
type="text"
|
||||
maxLength={120}
|
||||
autoComplete="family-name"
|
||||
value={form.selfLastName}
|
||||
onChange={(event) => updateField('selfLastName', event.target.value)}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label htmlFor="wizardSelfLastName">Votre nom</label>
|
||||
<input
|
||||
id="wizardSelfLastName"
|
||||
type="text"
|
||||
maxLength={120}
|
||||
value={form.selfLastName}
|
||||
onChange={(event) => updateField('selfLastName', event.target.value)}
|
||||
required
|
||||
/>
|
||||
<div className="pz-field">
|
||||
<label htmlFor="wizardSelfBirthYear">Année de naissance <span className="tw-optional">facultatif</span></label>
|
||||
<input
|
||||
id="wizardSelfBirthYear"
|
||||
type="number"
|
||||
inputMode="numeric"
|
||||
min="1000"
|
||||
max="2999"
|
||||
value={form.selfBirthYear}
|
||||
onChange={(event) => updateField('selfBirthYear', event.target.value)}
|
||||
placeholder="1990"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label htmlFor="wizardSelfBirthYear">Annee de naissance (optionnel)</label>
|
||||
<input
|
||||
id="wizardSelfBirthYear"
|
||||
type="number"
|
||||
min="1000"
|
||||
max="2999"
|
||||
value={form.selfBirthYear}
|
||||
onChange={(event) => updateField('selfBirthYear', event.target.value)}
|
||||
placeholder="1990"
|
||||
/>
|
||||
<div className="pz-field">
|
||||
<span className="pz-label">Votre photo <span className="tw-optional">facultatif</span></span>
|
||||
<label htmlFor="wizardSelfPhoto" className="tw-photo">
|
||||
<span className="tw-photo-icon" aria-hidden="true"><ImagePlus size={20} strokeWidth={1.8} /></span>
|
||||
<span className="tw-photo-text">
|
||||
<strong>{form.photoFile ? form.photoFile.name : 'Choisir une photo'}</strong>
|
||||
<span>{form.photoFile ? 'Cliquez pour en choisir une autre' : 'Un portrait où l\'on voit bien votre visage'}</span>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
id="wizardSelfPhoto"
|
||||
className="tw-photo-input"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(event) => updateField('photoFile', event.target.files?.[0] || null)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<label htmlFor="wizardSelfPhoto">Ajouter une photo (optionnel)</label>
|
||||
<input
|
||||
id="wizardSelfPhoto"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={(event) => updateField('photoFile', event.target.files?.[0] || null)}
|
||||
/>
|
||||
{error && <div className="pz-error tree-wizard-error" role="alert">{error}</div>}
|
||||
|
||||
{error && <div className="tree-wizard-error">{error}</div>}
|
||||
|
||||
<div className="tree-wizard-actions">
|
||||
<button type="button" className="ghost" onClick={() => setStep(1)} disabled={loading}>
|
||||
<div className="pz-modal-actions">
|
||||
<button type="button" className="pz-btn pz-btn--ghost" onClick={() => setStep(1)} disabled={loading}>
|
||||
Retour
|
||||
</button>
|
||||
<button type="submit" disabled={!canCreate}>
|
||||
{loading ? 'Creation en cours...' : 'Creer mon arbre'}
|
||||
<button type="submit" className="pz-btn pz-btn--primary" disabled={!canCreate}>
|
||||
{loading ? 'Création en cours…' : 'Créer mon arbre'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -28,18 +28,14 @@
|
||||
|
||||
.zoom-button {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 50%;
|
||||
|
||||
/* Même pill que navbar-content */
|
||||
background: rgba(247, 236, 217, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(93, 82, 75, 0.08);
|
||||
box-shadow:
|
||||
0 4px 80px rgba(93, 82, 75, 0.20),
|
||||
0 1px 6px rgba(93, 82, 75, 0.08);
|
||||
/* Même pastille que la barre d'actions */
|
||||
background: var(--pz-surface);
|
||||
border: 1px solid var(--pz-line);
|
||||
box-shadow: var(--pz-shadow-lg);
|
||||
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -47,16 +43,12 @@
|
||||
justify-content: center;
|
||||
|
||||
/* Même comportement que nav-button */
|
||||
color: #5D524B;
|
||||
transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
color: #2A2622;
|
||||
transition: border-color 160ms ease, transform 140ms var(--pz-ease);
|
||||
}
|
||||
|
||||
.zoom-button:hover {
|
||||
background: rgba(247, 236, 217, 0.98);
|
||||
transform: scale(1.08);
|
||||
box-shadow:
|
||||
0 6px 80px rgba(93, 82, 75, 0.28),
|
||||
0 2px 8px rgba(93, 82, 75, 0.12);
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.zoom-button:hover { border-color: var(--pz-a300); }
|
||||
}
|
||||
|
||||
.zoom-button:active {
|
||||
@@ -64,14 +56,13 @@
|
||||
}
|
||||
|
||||
.zoom-btn-icon {
|
||||
color: #5D524B;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.05));
|
||||
color: #2A2622;
|
||||
filter: drop-shadow(0 1px 2px rgba(42, 38, 34, 0.05));
|
||||
transition: color 0.2s ease, transform 0.2s ease;
|
||||
}
|
||||
|
||||
.zoom-button:hover .zoom-btn-icon {
|
||||
color: #A67C52;
|
||||
transform: scale(1.05);
|
||||
color: #93402A;
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -88,14 +79,10 @@
|
||||
padding: 14px 0 12px;
|
||||
|
||||
/* Même panel que navbar-annot-panel */
|
||||
background: rgba(254, 249, 237, 0.96);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border: 1px solid rgba(166, 124, 82, 0.15);
|
||||
border-radius: 16px;
|
||||
box-shadow:
|
||||
0 8px 40px rgba(93, 82, 75, 0.18),
|
||||
0 2px 8px rgba(93, 82, 75, 0.06);
|
||||
background: var(--pz-surface);
|
||||
border: 1px solid var(--pz-line);
|
||||
border-radius: 999px;
|
||||
box-shadow: var(--pz-shadow-lg);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -123,7 +110,7 @@
|
||||
}
|
||||
|
||||
.zoom-panel-icon {
|
||||
color: #5D524B;
|
||||
color: #2A2622;
|
||||
opacity: 0.6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -133,9 +120,9 @@
|
||||
============================================================ */
|
||||
|
||||
.zoom-slider-track {
|
||||
width: 6px;
|
||||
width: 4px;
|
||||
height: 120px;
|
||||
background: rgba(166, 124, 82, 0.15);
|
||||
background: var(--pz-line);
|
||||
border-radius: 3px;
|
||||
position: relative;
|
||||
cursor: ns-resize;
|
||||
@@ -155,11 +142,7 @@
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
background: linear-gradient(
|
||||
to top,
|
||||
rgba(166, 124, 82, 0.65),
|
||||
rgba(166, 124, 82, 0.2)
|
||||
);
|
||||
background: var(--pz-a500);
|
||||
border-radius: 3px;
|
||||
pointer-events: none;
|
||||
}
|
||||
@@ -168,19 +151,18 @@
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
background: #FEF9ED;
|
||||
border: 2px solid rgba(166, 124, 82, 0.7);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: var(--pz-ink);
|
||||
border: 3px solid var(--pz-surface);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 2px 8px rgba(93, 82, 75, 0.25);
|
||||
box-shadow: 0 1px 4px rgba(42, 38, 34, 0.3);
|
||||
pointer-events: none;
|
||||
transition: transform 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.zoom-slider-track:hover .zoom-slider-thumb {
|
||||
transform: translateX(-50%) scale(1.25);
|
||||
border-color: #A67C52;
|
||||
transform: translateX(-50%) scale(1.15);
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
|
||||
@@ -22,11 +22,11 @@
|
||||
gap: 4px;
|
||||
padding: 8px 6px;
|
||||
border-radius: 16px;
|
||||
background: rgba(254, 249, 237, 0.95);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border: 1px solid rgba(166, 124, 82, 0.18);
|
||||
box-shadow: 0 8px 32px rgba(93, 82, 75, 0.15);
|
||||
border: 1px solid rgba(147, 64, 42, 0.18);
|
||||
box-shadow: 0 8px 32px rgba(42, 38, 34, 0.15);
|
||||
}
|
||||
|
||||
.annotation-toolbar__btn {
|
||||
@@ -40,21 +40,21 @@
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
color: #5D524B;
|
||||
color: #2A2622;
|
||||
}
|
||||
|
||||
.annotation-toolbar__btn:hover:not(:disabled) {
|
||||
background: rgba(166, 124, 82, 0.1);
|
||||
background: rgba(147, 64, 42, 0.1);
|
||||
}
|
||||
|
||||
.annotation-toolbar__btn--active {
|
||||
background: linear-gradient(135deg, #8b6a43, #6a4f32);
|
||||
color: #fef9ed;
|
||||
box-shadow: 0 2px 8px rgba(166, 124, 82, 0.3);
|
||||
background: #2a2622;
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 8px rgba(147, 64, 42, 0.3);
|
||||
}
|
||||
|
||||
.annotation-toolbar__btn--active:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #96703e, #7a5a3a);
|
||||
background: linear-gradient(135deg, #93402a, #3d3731);
|
||||
}
|
||||
|
||||
.annotation-toolbar__btn--delete:disabled {
|
||||
@@ -95,18 +95,18 @@
|
||||
width: 28px;
|
||||
height: 1px;
|
||||
margin: 4px auto;
|
||||
background: rgba(166, 124, 82, 0.2);
|
||||
background: rgba(147, 64, 42, 0.2);
|
||||
}
|
||||
|
||||
/* Sub-panel (options) */
|
||||
.annotation-toolbar__panel {
|
||||
padding: 12px;
|
||||
border-radius: 14px;
|
||||
background: rgba(254, 249, 237, 0.96);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border: 1px solid rgba(166, 124, 82, 0.18);
|
||||
box-shadow: 0 8px 32px rgba(93, 82, 75, 0.15);
|
||||
border: 1px solid rgba(147, 64, 42, 0.18);
|
||||
box-shadow: 0 8px 32px rgba(42, 38, 34, 0.15);
|
||||
min-width: 180px;
|
||||
animation: annotation-panel-in 0.18s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
.drawing-options__label {
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #8B7355;
|
||||
color: #6B645D;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
@@ -38,8 +38,8 @@
|
||||
}
|
||||
|
||||
.drawing-options__color-btn--active {
|
||||
border-color: #5D524B;
|
||||
box-shadow: 0 0 0 2px rgba(93, 82, 75, 0.2);
|
||||
border-color: #2A2622;
|
||||
box-shadow: 0 0 0 2px rgba(42, 38, 34, 0.2);
|
||||
}
|
||||
|
||||
.drawing-options__widths {
|
||||
@@ -54,19 +54,19 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 8px;
|
||||
border: 1.5px solid rgba(166, 124, 82, 0.2);
|
||||
border: 1.5px solid rgba(147, 64, 42, 0.2);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.drawing-options__width-btn:hover {
|
||||
background: rgba(166, 124, 82, 0.08);
|
||||
background: rgba(147, 64, 42, 0.08);
|
||||
}
|
||||
|
||||
.drawing-options__width-btn--active {
|
||||
border-color: #A67C52;
|
||||
background: rgba(166, 124, 82, 0.1);
|
||||
border-color: #93402A;
|
||||
background: rgba(147, 64, 42, 0.1);
|
||||
}
|
||||
|
||||
.drawing-options__width-preview {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import './DrawingOptions.css'
|
||||
|
||||
const COLORS = [
|
||||
'#A67C52', '#5D524B', '#C0392B', '#2980B9',
|
||||
'#93402A', '#2A2622', '#C0392B', '#2980B9',
|
||||
'#27AE60', '#F39C12', '#8E44AD', '#2C3E50',
|
||||
]
|
||||
|
||||
|
||||
@@ -10,13 +10,13 @@
|
||||
gap: 10px;
|
||||
padding: 16px 32px 14px;
|
||||
border-radius: 20px;
|
||||
background: rgba(254, 249, 237, 0.95);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border: 2px dashed rgba(166, 124, 82, 0.3);
|
||||
border: 2px dashed rgba(147, 64, 42, 0.3);
|
||||
box-shadow:
|
||||
0 8px 40px rgba(93, 82, 75, 0.2),
|
||||
0 2px 8px rgba(93, 82, 75, 0.08);
|
||||
0 8px 40px rgba(42, 38, 34, 0.2),
|
||||
0 2px 8px rgba(42, 38, 34, 0.08);
|
||||
animation: trashSlideDown 0.35s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
@@ -36,12 +36,12 @@
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
background: rgba(166, 124, 82, 0.1);
|
||||
border: 2px solid rgba(166, 124, 82, 0.2);
|
||||
background: rgba(147, 64, 42, 0.1);
|
||||
border: 2px solid rgba(147, 64, 42, 0.2);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #8B7355;
|
||||
color: #6B645D;
|
||||
transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
.drop-trash-zone__label {
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #5D524B;
|
||||
color: #2A2622;
|
||||
transition: all 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
.size-options__label {
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #8B7355;
|
||||
color: #6B645D;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
.size-options__value {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #A67C52;
|
||||
color: #93402A;
|
||||
min-width: 28px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -34,10 +34,10 @@
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
#A67C52 0%,
|
||||
#A67C52 var(--slider-progress, 50%),
|
||||
rgba(166, 124, 82, 0.2) var(--slider-progress, 50%),
|
||||
rgba(166, 124, 82, 0.2) 100%
|
||||
#93402A 0%,
|
||||
#93402A var(--slider-progress, 50%),
|
||||
rgba(147, 64, 42, 0.2) var(--slider-progress, 50%),
|
||||
rgba(147, 64, 42, 0.2) 100%
|
||||
);
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
@@ -50,9 +50,9 @@
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #A67C52;
|
||||
background: #93402A;
|
||||
cursor: pointer;
|
||||
box-shadow: 0 1px 4px rgba(166, 124, 82, 0.4);
|
||||
box-shadow: 0 1px 4px rgba(147, 64, 42, 0.4);
|
||||
transition: transform 0.1s ease;
|
||||
}
|
||||
|
||||
@@ -64,8 +64,8 @@
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
background: #A67C52;
|
||||
background: #93402A;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
box-shadow: 0 1px 4px rgba(166, 124, 82, 0.4);
|
||||
box-shadow: 0 1px 4px rgba(147, 64, 42, 0.4);
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
.sticker-picker__label {
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #8B7355;
|
||||
color: #6B645D;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
@@ -33,37 +33,37 @@
|
||||
}
|
||||
|
||||
.sticker-picker__btn:hover {
|
||||
background: rgba(166, 124, 82, 0.06);
|
||||
background: rgba(147, 64, 42, 0.06);
|
||||
transform: scale(1.1) translateY(-1px);
|
||||
box-shadow: 0 3px 8px rgba(93, 82, 75, 0.12);
|
||||
box-shadow: 0 3px 8px rgba(42, 38, 34, 0.12);
|
||||
}
|
||||
|
||||
.sticker-picker__btn--active {
|
||||
border-color: rgba(166, 124, 82, 0.3);
|
||||
background: rgba(254, 249, 237, 0.95);
|
||||
border-color: rgba(147, 64, 42, 0.3);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
transform: scale(1.2) translateY(-6px);
|
||||
box-shadow:
|
||||
0 8px 20px rgba(93, 82, 75, 0.2),
|
||||
0 2px 6px rgba(93, 82, 75, 0.1);
|
||||
0 8px 20px rgba(42, 38, 34, 0.2),
|
||||
0 2px 6px rgba(42, 38, 34, 0.1);
|
||||
animation: stickerGrab 0.35s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
@keyframes stickerGrab {
|
||||
0% {
|
||||
transform: scale(1) translateY(0);
|
||||
box-shadow: 0 1px 2px rgba(93, 82, 75, 0.08);
|
||||
box-shadow: 0 1px 2px rgba(42, 38, 34, 0.08);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.28) translateY(-10px);
|
||||
box-shadow:
|
||||
0 14px 28px rgba(93, 82, 75, 0.22),
|
||||
0 4px 8px rgba(93, 82, 75, 0.1);
|
||||
0 14px 28px rgba(42, 38, 34, 0.22),
|
||||
0 4px 8px rgba(42, 38, 34, 0.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1.2) translateY(-6px);
|
||||
box-shadow:
|
||||
0 8px 20px rgba(93, 82, 75, 0.2),
|
||||
0 2px 6px rgba(93, 82, 75, 0.1);
|
||||
0 8px 20px rgba(42, 38, 34, 0.2),
|
||||
0 2px 6px rgba(42, 38, 34, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,8 +73,8 @@
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
border-radius: 10px;
|
||||
background: rgba(166, 124, 82, 0.08);
|
||||
border: 1px solid rgba(166, 124, 82, 0.15);
|
||||
background: rgba(147, 64, 42, 0.08);
|
||||
border: 1px solid rgba(147, 64, 42, 0.15);
|
||||
animation: previewSlideIn 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
@@ -103,6 +103,6 @@
|
||||
|
||||
.sticker-picker__preview-hint {
|
||||
font-size: 14px;
|
||||
color: #8B7355;
|
||||
color: #6B645D;
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -8,21 +8,21 @@
|
||||
min-width: 120px;
|
||||
max-width: 400px;
|
||||
padding: 6px 10px;
|
||||
border: 1.5px solid rgba(166, 124, 82, 0.3);
|
||||
border: 1.5px solid rgba(147, 64, 42, 0.3);
|
||||
border-radius: 8px;
|
||||
outline: none;
|
||||
font-family: inherit;
|
||||
font-size: 16px; /* ≥16px prevents iOS Safari from auto-zooming on focus */
|
||||
box-shadow: 0 4px 16px rgba(93, 82, 75, 0.2);
|
||||
box-shadow: 0 4px 16px rgba(42, 38, 34, 0.2);
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.text-input-overlay__input:focus {
|
||||
border-color: #A67C52;
|
||||
box-shadow: 0 4px 16px rgba(93, 82, 75, 0.2), 0 0 0 3px rgba(166, 124, 82, 0.12);
|
||||
border-color: #93402A;
|
||||
box-shadow: 0 4px 16px rgba(42, 38, 34, 0.2), 0 0 0 3px rgba(147, 64, 42, 0.12);
|
||||
}
|
||||
|
||||
.text-input-overlay__input::placeholder {
|
||||
color: rgba(93, 82, 75, 0.4);
|
||||
color: rgba(42, 38, 34, 0.4);
|
||||
font-style: italic;
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export default function TextInputOverlay({
|
||||
color: currentStyle.color,
|
||||
fontSize: currentStyle.fontSize,
|
||||
background: currentStyle.stabilo
|
||||
? 'rgba(255, 243, 128, 0.5)'
|
||||
? 'rgba(237, 182, 164, 0.5)'
|
||||
: 'rgba(255, 255, 255, 0.85)',
|
||||
}}
|
||||
onKeyDown={handleKeyDown}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
.text-options__label {
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #8B7355;
|
||||
color: #6B645D;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
@@ -38,8 +38,8 @@
|
||||
}
|
||||
|
||||
.text-options__color-btn--active {
|
||||
border-color: #5D524B;
|
||||
box-shadow: 0 0 0 2px rgba(93, 82, 75, 0.2);
|
||||
border-color: #2A2622;
|
||||
box-shadow: 0 0 0 2px rgba(42, 38, 34, 0.2);
|
||||
}
|
||||
|
||||
.text-options__sizes {
|
||||
@@ -51,9 +51,9 @@
|
||||
flex: 1;
|
||||
padding: 6px 4px;
|
||||
border-radius: 8px;
|
||||
border: 1.5px solid rgba(166, 124, 82, 0.2);
|
||||
border: 1.5px solid rgba(147, 64, 42, 0.2);
|
||||
background: transparent;
|
||||
color: #5D524B;
|
||||
color: #2A2622;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
cursor: pointer;
|
||||
@@ -61,13 +61,13 @@
|
||||
}
|
||||
|
||||
.text-options__size-btn:hover {
|
||||
background: rgba(166, 124, 82, 0.08);
|
||||
background: rgba(147, 64, 42, 0.08);
|
||||
}
|
||||
|
||||
.text-options__size-btn--active {
|
||||
border-color: #A67C52;
|
||||
background: rgba(166, 124, 82, 0.1);
|
||||
color: #A67C52;
|
||||
border-color: #93402A;
|
||||
background: rgba(147, 64, 42, 0.1);
|
||||
color: #93402A;
|
||||
}
|
||||
|
||||
/* Toggle switch */
|
||||
@@ -90,12 +90,12 @@
|
||||
width: 36px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
background: rgba(166, 124, 82, 0.2);
|
||||
background: rgba(147, 64, 42, 0.2);
|
||||
transition: background 0.2s ease;
|
||||
}
|
||||
|
||||
.text-options__toggle input:checked + .text-options__toggle-track {
|
||||
background: #A67C52;
|
||||
background: #93402A;
|
||||
}
|
||||
|
||||
.text-options__toggle-thumb {
|
||||
@@ -106,7 +106,7 @@
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
background: white;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.15);
|
||||
box-shadow: 0 1px 3px rgba(42, 38, 34, 0.15);
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
@@ -117,5 +117,5 @@
|
||||
.text-options__toggle-label {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #5D524B;
|
||||
color: #2A2622;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import './TextOptions.css'
|
||||
|
||||
const COLORS = [
|
||||
'#A67C52', '#5D524B', '#C0392B', '#2980B9',
|
||||
'#93402A', '#2A2622', '#C0392B', '#2980B9',
|
||||
'#27AE60', '#F39C12', '#8E44AD', '#2C3E50',
|
||||
]
|
||||
|
||||
|
||||
@@ -48,15 +48,15 @@ export const ENTRANCE_ORBIT_DURATION = 400
|
||||
export const ENTRANCE_ORBIT_STAGGER = 100
|
||||
|
||||
export const COLORS = {
|
||||
nodeAlive: '#F7ECD9',
|
||||
nodeDead: '#E8DCC8',
|
||||
borderAlive: '#A67C52',
|
||||
borderDead: '#8B7355',
|
||||
text: '#5D524B',
|
||||
textLight: '#8B7355',
|
||||
link: '#000000',
|
||||
linkUnknown: '#8B7355',
|
||||
placeholder: '#C5BDB6',
|
||||
placeholderBorder: '#A89F97',
|
||||
placeholderFill: '#E8E2DC',
|
||||
nodeAlive: '#F2EFEA',
|
||||
nodeDead: '#EBE6DF',
|
||||
borderAlive: '#93402A',
|
||||
borderDead: '#6B645D',
|
||||
text: '#2A2622',
|
||||
textLight: '#6B645D',
|
||||
link: '#2A2622',
|
||||
linkUnknown: '#6B645D',
|
||||
placeholder: '#C0BEBB',
|
||||
placeholderBorder: '#A2A09D',
|
||||
placeholderFill: '#EBE6DF',
|
||||
}
|
||||
@@ -31,9 +31,9 @@ function pointInView(view, cx, cy) {
|
||||
export function drawBackground(ctx, canvas) {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
const baseGradient = ctx.createLinearGradient(0, 0, 0, canvas.height)
|
||||
baseGradient.addColorStop(0, '#FDF8EC')
|
||||
baseGradient.addColorStop(0.6, '#FDF8EC')
|
||||
baseGradient.addColorStop(1, '#FDF8EC')
|
||||
baseGradient.addColorStop(0, '#FBFAF7')
|
||||
baseGradient.addColorStop(0.6, '#FBFAF7')
|
||||
baseGradient.addColorStop(1, '#FBFAF7')
|
||||
ctx.fillStyle = baseGradient
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
@@ -41,8 +41,8 @@ export function drawBackground(ctx, canvas) {
|
||||
canvas.width * 0.14, canvas.height * 0.08, 50,
|
||||
canvas.width * 0.14, canvas.height * 0.08, canvas.width * 0.65
|
||||
)
|
||||
glowGradient.addColorStop(0, 'rgba(253, 248, 236, 0.2)')
|
||||
glowGradient.addColorStop(1, 'rgba(253, 248, 236, 0)')
|
||||
glowGradient.addColorStop(0, 'rgba(251, 250, 247, 0.2)')
|
||||
glowGradient.addColorStop(1, 'rgba(251, 250, 247, 0)')
|
||||
ctx.fillStyle = glowGradient
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
}
|
||||
@@ -242,7 +242,7 @@ export function drawPathHighlight(ctx, {
|
||||
p1PosKeyed, p2PosKeyed, unionPos, p1R, p2R, nodeRandomData
|
||||
)
|
||||
|
||||
ctx.strokeStyle = `rgba(180, 120, 50, ${0.55 + 0.25 * hp})`
|
||||
ctx.strokeStyle = `rgba(147, 64, 42, ${0.55 + 0.25 * hp})`
|
||||
ctx.lineWidth = 1.5 + 1.5 * hp
|
||||
ctx.setLineDash([])
|
||||
ctx.lineCap = 'round'
|
||||
@@ -275,7 +275,7 @@ export function drawPathHighlight(ctx, {
|
||||
rdChild?.anchorAngleOffset || 0,
|
||||
)
|
||||
|
||||
ctx.strokeStyle = `rgba(180, 120, 50, ${0.55 + 0.25 * hp})`
|
||||
ctx.strokeStyle = `rgba(147, 64, 42, ${0.55 + 0.25 * hp})`
|
||||
ctx.lineWidth = 1.5 + 1.5 * hp
|
||||
ctx.setLineDash([])
|
||||
ctx.lineCap = 'round'
|
||||
@@ -295,8 +295,8 @@ export function drawPathHighlight(ctx, {
|
||||
if (!pos) continue
|
||||
const pathGlowR = PERSON_R + 10
|
||||
const pathGlow = ctx.createRadialGradient(pos.cx, pos.cy, PERSON_R * 0.7, pos.cx, pos.cy, pathGlowR)
|
||||
pathGlow.addColorStop(0, `rgba(180, 120, 50, ${0.12 * hp})`)
|
||||
pathGlow.addColorStop(1, 'rgba(180, 120, 50, 0)')
|
||||
pathGlow.addColorStop(0, `rgba(147, 64, 42, ${0.12 * hp})`)
|
||||
pathGlow.addColorStop(1, 'rgba(147, 64, 42, 0)')
|
||||
ctx.fillStyle = pathGlow
|
||||
ctx.beginPath()
|
||||
ctx.arc(pos.cx, pos.cy, pathGlowR, 0, Math.PI * 2)
|
||||
@@ -386,13 +386,13 @@ function drawPersonNode(ctx, node, pos, nodeProgress, {
|
||||
ctx.rotate(slot.rot)
|
||||
|
||||
if (withShadow) {
|
||||
ctx.shadowColor = 'rgba(93, 82, 75, 0.18)'
|
||||
ctx.shadowColor = 'rgba(42, 38, 34, 0.18)'
|
||||
ctx.shadowBlur = 4
|
||||
ctx.shadowOffsetY = 1
|
||||
}
|
||||
|
||||
const pad = 2
|
||||
ctx.fillStyle = '#FEF9ED'
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(-ms - pad, -ms - pad, (ms + pad) * 2, (ms + pad) * 2 + 4, 2)
|
||||
ctx.fill()
|
||||
@@ -416,9 +416,9 @@ function drawPersonNode(ctx, node, pos, nodeProgress, {
|
||||
const selGlowR = PERSON_R + 24 + sg * 14
|
||||
const selAlpha = sg * 0.38 * nodeProgress
|
||||
const selGlow = ctx.createRadialGradient(cx, cy, PERSON_R * 0.3, cx, cy, selGlowR)
|
||||
selGlow.addColorStop(0, `rgba(210, 160, 80, ${selAlpha})`)
|
||||
selGlow.addColorStop(0.5, `rgba(210, 160, 80, ${selAlpha * 0.45})`)
|
||||
selGlow.addColorStop(1, 'rgba(210, 160, 80, 0)')
|
||||
selGlow.addColorStop(0, `rgba(210, 105, 74, ${selAlpha})`)
|
||||
selGlow.addColorStop(0.5, `rgba(210, 105, 74, ${selAlpha * 0.45})`)
|
||||
selGlow.addColorStop(1, 'rgba(210, 105, 74, 0)')
|
||||
ctx.fillStyle = selGlow
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, selGlowR, 0, Math.PI * 2)
|
||||
@@ -431,9 +431,9 @@ function drawPersonNode(ctx, node, pos, nodeProgress, {
|
||||
const searchGlowR = PERSON_R + 28 + srch * 18
|
||||
const searchAlpha = srch * 0.55 * nodeProgress
|
||||
const searchGlow = ctx.createRadialGradient(cx, cy, PERSON_R * 0.25, cx, cy, searchGlowR)
|
||||
searchGlow.addColorStop(0, `rgba(255, 225, 92, ${searchAlpha})`)
|
||||
searchGlow.addColorStop(0.5, `rgba(255, 225, 92, ${searchAlpha * 0.55})`)
|
||||
searchGlow.addColorStop(1, 'rgba(255, 225, 92, 0)')
|
||||
searchGlow.addColorStop(0, `rgba(210, 105, 74, ${searchAlpha})`)
|
||||
searchGlow.addColorStop(0.5, `rgba(210, 105, 74, ${searchAlpha * 0.55})`)
|
||||
searchGlow.addColorStop(1, 'rgba(210, 105, 74, 0)')
|
||||
ctx.fillStyle = searchGlow
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, searchGlowR, 0, Math.PI * 2)
|
||||
@@ -446,8 +446,8 @@ function drawPersonNode(ctx, node, pos, nodeProgress, {
|
||||
const glowR = PERSON_R + 12 + hs * 6
|
||||
const glowAlpha = (person.isAlive ? 0.15 + hs * 0.1 : 0.08 + hs * 0.06) * nodeProgress
|
||||
const glow = ctx.createRadialGradient(cx, cy, PERSON_R * 0.6, cx, cy, glowR)
|
||||
glow.addColorStop(0, `rgba(166, 124, 82, ${glowAlpha})`)
|
||||
glow.addColorStop(1, 'rgba(166, 124, 82, 0)')
|
||||
glow.addColorStop(0, `rgba(147, 64, 42, ${glowAlpha})`)
|
||||
glow.addColorStop(1, 'rgba(147, 64, 42, 0)')
|
||||
ctx.fillStyle = glow
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, glowR, 0, Math.PI * 2)
|
||||
@@ -465,7 +465,7 @@ function drawPersonNode(ctx, node, pos, nodeProgress, {
|
||||
const shadowBlur = 10 + hs * 14
|
||||
const shadowY = 3 + hs * 4
|
||||
const shadowAlpha = 0.15 + hs * 0.1
|
||||
ctx.shadowColor = `rgba(93, 82, 75, ${shadowAlpha})`
|
||||
ctx.shadowColor = `rgba(42, 38, 34, ${shadowAlpha})`
|
||||
ctx.shadowBlur = shadowBlur
|
||||
ctx.shadowOffsetX = 0
|
||||
ctx.shadowOffsetY = shadowY
|
||||
@@ -483,7 +483,7 @@ function drawPersonNode(ctx, node, pos, nodeProgress, {
|
||||
ctx.translate(cx, cy + labelOffsetY)
|
||||
ctx.rotate(labelRot)
|
||||
|
||||
ctx.font = '450 13px "Bradford LL", "Iowan Old Style", "Palatino Linotype", serif'
|
||||
ctx.font = '400 13px "Newsreader", Georgia, serif'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
const nameLabel = person.firstName || ''
|
||||
@@ -498,10 +498,10 @@ function drawPersonNode(ctx, node, pos, nodeProgress, {
|
||||
const labelGlowRx = Math.max(26, Math.round(nameMetrics.width * 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(253, 248, 236, 0.98)')
|
||||
labelGlow.addColorStop(0.38, 'rgba(253, 248, 236, 0.82)')
|
||||
labelGlow.addColorStop(0.72, 'rgba(253, 248, 236, 0.34)')
|
||||
labelGlow.addColorStop(1, 'rgba(253, 248, 236, 0)')
|
||||
labelGlow.addColorStop(0, 'rgba(251, 250, 247, 0.98)')
|
||||
labelGlow.addColorStop(0.38, 'rgba(251, 250, 247, 0.82)')
|
||||
labelGlow.addColorStop(0.72, 'rgba(251, 250, 247, 0.34)')
|
||||
labelGlow.addColorStop(1, 'rgba(251, 250, 247, 0)')
|
||||
|
||||
ctx.save()
|
||||
ctx.fillStyle = labelGlow
|
||||
@@ -517,7 +517,7 @@ function drawPersonNode(ctx, node, pos, nodeProgress, {
|
||||
}
|
||||
|
||||
if (withDetail) {
|
||||
ctx.font = '400 11px "Red Hat Mono", ui-monospace, monospace'
|
||||
ctx.font = '400 11px "DM Sans", system-ui, sans-serif'
|
||||
ctx.fillStyle = COLORS.textLight
|
||||
const birthYearLabel = person.birthYear ?? '-'
|
||||
const years = person.deathYear ? `${birthYearLabel}–${person.deathYear}` : `${birthYearLabel}`
|
||||
@@ -595,7 +595,7 @@ function drawMapThumbnailBase(ctx, ms, palette) {
|
||||
|
||||
const vignette = ctx.createRadialGradient(0, 0, ms * 0.2, 0, 0, ms * 1.2)
|
||||
vignette.addColorStop(0, 'rgba(255, 255, 255, 0)')
|
||||
vignette.addColorStop(1, palette.vignette || 'rgba(0, 0, 0, 0.06)')
|
||||
vignette.addColorStop(1, palette.vignette || 'rgba(42, 38, 34, 0.06)')
|
||||
ctx.fillStyle = vignette
|
||||
ctx.fillRect(-ms, -ms, size, size)
|
||||
|
||||
@@ -675,20 +675,20 @@ function drawMediaContent(ctx, media, ms, imageCache) {
|
||||
ctx.drawImage(mImg, -ms, -ms, ms * 2, ms * 2)
|
||||
ctx.restore()
|
||||
} else if (media.type === 'video') {
|
||||
ctx.fillStyle = '#3A3530'
|
||||
ctx.fillStyle = '#3D3731'
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(-ms, -ms, ms * 2, ms * 2, 1)
|
||||
ctx.fill()
|
||||
ctx.fillStyle = '#FEF9ED'
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
for (let p = 0; p < 4; p++) {
|
||||
const py = -ms + 4 + p * (ms * 2 - 6) / 3
|
||||
ctx.fillRect(-ms + 1.5, py, 3, 3)
|
||||
ctx.fillRect(ms - 4.5, py, 3, 3)
|
||||
}
|
||||
const innerPad = 5
|
||||
ctx.fillStyle = '#5D524B'
|
||||
ctx.fillStyle = '#2A2622'
|
||||
ctx.fillRect(-ms + innerPad, -ms + 2, (ms - innerPad) * 2, ms * 2 - 4)
|
||||
ctx.fillStyle = 'rgba(254, 249, 237, 0.7)'
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(-4, -5)
|
||||
ctx.lineTo(-4, 5)
|
||||
@@ -696,27 +696,27 @@ function drawMediaContent(ctx, media, ms, imageCache) {
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
} else if (media.type === 'audio') {
|
||||
ctx.fillStyle = '#2C2825'
|
||||
ctx.fillStyle = '#2A2622'
|
||||
ctx.beginPath()
|
||||
ctx.arc(0, 0, ms, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.strokeStyle = 'rgba(254, 249, 237, 0.08)'
|
||||
ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)'
|
||||
ctx.lineWidth = 0.5
|
||||
for (let r = 5; r < ms; r += 3) {
|
||||
ctx.beginPath()
|
||||
ctx.arc(0, 0, r, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
}
|
||||
ctx.fillStyle = '#A67C52'
|
||||
ctx.fillStyle = '#93402A'
|
||||
ctx.beginPath()
|
||||
ctx.arc(0, 0, 6, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
ctx.fillStyle = '#2C2825'
|
||||
ctx.fillStyle = '#2A2622'
|
||||
ctx.beginPath()
|
||||
ctx.arc(0, 0, 1.5, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
} else if (media.type === 'document') {
|
||||
ctx.fillStyle = '#F0E8D8'
|
||||
ctx.fillStyle = '#FCF3EF'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(-ms, -ms)
|
||||
ctx.lineTo(ms - 5, -ms)
|
||||
@@ -725,14 +725,14 @@ function drawMediaContent(ctx, media, ms, imageCache) {
|
||||
ctx.lineTo(-ms, ms)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
ctx.fillStyle = '#DDD3C2'
|
||||
ctx.fillStyle = '#E6E1DA'
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(ms - 5, -ms)
|
||||
ctx.lineTo(ms - 5, -ms + 5)
|
||||
ctx.lineTo(ms, -ms + 5)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
ctx.fillStyle = 'rgba(93, 82, 75, 0.15)'
|
||||
ctx.fillStyle = 'rgba(42, 38, 34, 0.15)'
|
||||
for (let l = 0; l < 5; l++) {
|
||||
const ly = -ms + 8 + l * 6
|
||||
const lw = l === 4 ? ms * 0.6 : ms * 1.4
|
||||
@@ -743,19 +743,19 @@ function drawMediaContent(ctx, media, ms, imageCache) {
|
||||
} else if (media.type === 'gpx') {
|
||||
drawGpxThumbnail(ctx, ms)
|
||||
} else if (media.type === 'citation') {
|
||||
ctx.fillStyle = '#F5EDCF'
|
||||
ctx.fillStyle = '#F8E4DB'
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(-ms, -ms, ms * 2, ms * 2, 1)
|
||||
ctx.fill()
|
||||
ctx.fillStyle = 'rgba(166, 124, 82, 0.12)'
|
||||
ctx.fillStyle = 'rgba(147, 64, 42, 0.12)'
|
||||
ctx.fillRect(-6, -ms, 12, 3)
|
||||
ctx.fillStyle = '#A67C52'
|
||||
ctx.font = `450 ${ms * 1.1}px "Bradford LL", "Iowan Old Style", "Palatino Linotype", serif`
|
||||
ctx.fillStyle = '#93402A'
|
||||
ctx.font = `400 ${ms * 1.1}px "Newsreader", Georgia, serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText('„', 0, -ms * 0.4)
|
||||
} else {
|
||||
ctx.fillStyle = '#E8E2DC'
|
||||
ctx.fillStyle = '#EBE6DF'
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(-ms, -ms, ms * 2, ms * 2, 1)
|
||||
ctx.fill()
|
||||
@@ -782,11 +782,9 @@ function resolveFrameType(person, img) {
|
||||
return 'polaroid'
|
||||
}
|
||||
|
||||
if (ratio >= 0.95 && ratio <= 1.05) {
|
||||
return 'square'
|
||||
}
|
||||
|
||||
return 'round'
|
||||
// Photo carrée ou presque : chaque personne garde sa forme tirée au hasard
|
||||
// (person.frameType, voir pickFrameType), pour que l'arbre ne soit pas uniforme
|
||||
return fallback
|
||||
}
|
||||
|
||||
function drawFrame(ctx, frameType, person, img) {
|
||||
@@ -795,13 +793,13 @@ function drawFrame(ctx, frameType, person, img) {
|
||||
const ph = PERSON_R + 4
|
||||
const padSide = 4
|
||||
const padBottom = 10
|
||||
ctx.fillStyle = '#FEF9ED'
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(-pw - padSide, -ph - padSide, (pw + padSide) * 2, (ph + padSide) + ph + padBottom, 3)
|
||||
ctx.fill()
|
||||
ctx.shadowColor = 'transparent'
|
||||
ctx.shadowBlur = 0
|
||||
ctx.strokeStyle = 'rgba(93, 82, 75, 0.08)'
|
||||
ctx.strokeStyle = 'rgba(42, 38, 34, 0.08)'
|
||||
ctx.lineWidth = 0.5
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
@@ -811,13 +809,13 @@ function drawFrame(ctx, frameType, person, img) {
|
||||
} else if (frameType === 'square') {
|
||||
const s = PERSON_R + 4
|
||||
const pad = 3
|
||||
ctx.fillStyle = '#FEF9ED'
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(-s - pad, -s - pad, (s + pad) * 2, (s + pad) * 2, 4)
|
||||
ctx.fill()
|
||||
ctx.shadowColor = 'transparent'
|
||||
ctx.shadowBlur = 0
|
||||
ctx.strokeStyle = 'rgba(93, 82, 75, 0.08)'
|
||||
ctx.strokeStyle = 'rgba(42, 38, 34, 0.08)'
|
||||
ctx.lineWidth = 0.5
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
@@ -828,17 +826,18 @@ function drawFrame(ctx, frameType, person, img) {
|
||||
const ratio = img?.naturalWidth && img?.naturalHeight
|
||||
? img.naturalWidth / img.naturalHeight
|
||||
: 1
|
||||
const isLandscape = ratio >= 1
|
||||
// Photo carrée : cadre vertical, qui garde le visage entier
|
||||
const isLandscape = ratio > 1.05
|
||||
const rw = isLandscape ? PERSON_R + 12 : PERSON_R + 2
|
||||
const rh = isLandscape ? PERSON_R - 8 : PERSON_R + 6
|
||||
const pad = 3
|
||||
ctx.fillStyle = '#FEF9ED'
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(-rw - pad, -rh - pad, (rw + pad) * 2, (rh + pad) * 2, 5)
|
||||
ctx.fill()
|
||||
ctx.shadowColor = 'transparent'
|
||||
ctx.shadowBlur = 0
|
||||
ctx.strokeStyle = 'rgba(93, 82, 75, 0.08)'
|
||||
ctx.strokeStyle = 'rgba(42, 38, 34, 0.08)'
|
||||
ctx.lineWidth = 0.5
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
@@ -846,7 +845,7 @@ function drawFrame(ctx, frameType, person, img) {
|
||||
ctx.clip()
|
||||
drawPersonImage(ctx, img, person, -rw, -rh, rw * 2, rh * 2)
|
||||
} else {
|
||||
ctx.fillStyle = '#FEF9ED'
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.beginPath()
|
||||
ctx.arc(0, 0, PERSON_R + 3, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
@@ -870,14 +869,14 @@ function drawPersonImage(ctx, img, person, x, y, w, h) {
|
||||
if (img) {
|
||||
drawImageCover(ctx, img, x, y, w, h)
|
||||
if (!person.isAlive) {
|
||||
ctx.fillStyle = 'rgba(232, 220, 200, 0.25)'
|
||||
ctx.fillStyle = 'rgba(235, 230, 223, 0.25)'
|
||||
ctx.fillRect(x, y, w, h)
|
||||
}
|
||||
} else {
|
||||
ctx.fillStyle = person.isAlive ? COLORS.nodeAlive : COLORS.nodeDead
|
||||
ctx.fillRect(x, y, w, h)
|
||||
ctx.fillStyle = COLORS.text
|
||||
ctx.font = '450 16px "Bradford LL", "Iowan Old Style", "Palatino Linotype", serif'
|
||||
ctx.font = '400 16px "Newsreader", Georgia, serif'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText((person.firstName[0] || '') + (person.lastName[0] || ''), 0, 0)
|
||||
@@ -959,12 +958,12 @@ export function drawNodes(ctx, layoutData, posMap, entrance, entranceActive, ela
|
||||
ctx.setLineDash([])
|
||||
|
||||
ctx.fillStyle = COLORS.placeholder
|
||||
ctx.font = '400 16px "Red Hat Mono", ui-monospace, monospace'
|
||||
ctx.font = '400 16px "DM Sans", system-ui, sans-serif'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText('?', 0, 0)
|
||||
|
||||
ctx.font = '450 italic 10px "Bradford LL", "Iowan Old Style", "Palatino Linotype", serif'
|
||||
ctx.font = '400 italic 10px "Newsreader", Georgia, serif'
|
||||
ctx.textBaseline = 'top'
|
||||
ctx.fillText('Inconnu', 0, UNKNOWN_R + 4)
|
||||
ctx.restore()
|
||||
@@ -988,7 +987,7 @@ function drawDrawingAnnotation(ctx, ann) {
|
||||
for (const path of paths) {
|
||||
if (!path.points || path.points.length < 2) continue
|
||||
ctx.save()
|
||||
ctx.strokeStyle = path.color || ann.style?.color || '#A67C52'
|
||||
ctx.strokeStyle = path.color || ann.style?.color || '#93402A'
|
||||
ctx.lineWidth = path.width || ann.style?.brushWidth || 3
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
@@ -1017,12 +1016,12 @@ function drawStickerAnnotation(ctx, ann) {
|
||||
|
||||
function drawTextAnnotation(ctx, ann) {
|
||||
const fontSize = ann.style?.fontSize || 18
|
||||
const color = ann.style?.color || '#5D524B'
|
||||
const color = ann.style?.color || '#2A2622'
|
||||
const stabilo = ann.style?.stabilo || false
|
||||
|
||||
ctx.save()
|
||||
ctx.globalAlpha = ann.style?.opacity ?? 1
|
||||
ctx.font = `450 ${fontSize}px "Bradford LL", "Iowan Old Style", "Palatino Linotype", serif`
|
||||
ctx.font = `400 ${fontSize}px "Newsreader", Georgia, serif`
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
|
||||
@@ -1030,7 +1029,7 @@ function drawTextAnnotation(ctx, ann) {
|
||||
const metrics = ctx.measureText(ann.content)
|
||||
const padX = 6
|
||||
const padY = 4
|
||||
ctx.fillStyle = 'rgba(246, 229, 184, 0.7)'
|
||||
ctx.fillStyle = 'rgba(248, 228, 219, 0.7)'
|
||||
ctx.fillRect(
|
||||
-metrics.width / 2 - padX,
|
||||
-fontSize / 2 - padY,
|
||||
@@ -1078,8 +1077,8 @@ function drawPhotoAnnotation(ctx, ann, imageCache) {
|
||||
}
|
||||
// Placeholder while loading
|
||||
ctx.save()
|
||||
ctx.fillStyle = 'rgba(200, 180, 150, 0.25)'
|
||||
ctx.strokeStyle = 'rgba(166, 124, 82, 0.4)'
|
||||
ctx.fillStyle = 'rgba(210, 105, 74, 0.25)'
|
||||
ctx.strokeStyle = 'rgba(147, 64, 42, 0.4)'
|
||||
ctx.lineWidth = 1.5
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(-drawW / 2, -drawH / 2, drawW, drawH, 6)
|
||||
@@ -1104,7 +1103,7 @@ function drawSelectionBorder(ctx, ann) {
|
||||
|
||||
if (ann.type === 'text') {
|
||||
const fontSize = ann.style?.fontSize || 18
|
||||
ctx.font = `450 ${fontSize}px "Bradford LL", "Iowan Old Style", "Palatino Linotype", serif`
|
||||
ctx.font = `400 ${fontSize}px "Newsreader", Georgia, serif`
|
||||
const metrics = ctx.measureText(ann.content)
|
||||
w = metrics.width + 16
|
||||
h = fontSize + 12
|
||||
@@ -1127,7 +1126,7 @@ function drawSelectionBorder(ctx, ann) {
|
||||
}
|
||||
|
||||
ctx.save()
|
||||
ctx.strokeStyle = '#A67C52'
|
||||
ctx.strokeStyle = '#93402A'
|
||||
ctx.lineWidth = 1.5
|
||||
ctx.setLineDash([5, 3])
|
||||
ctx.strokeRect(-w / 2, -h / 2, w, h)
|
||||
@@ -1173,7 +1172,7 @@ export function drawAnnotations(
|
||||
ctx.translate(0, -4 * lift)
|
||||
const s = 1 + 0.15 * lift
|
||||
ctx.scale(s, s)
|
||||
ctx.shadowColor = `rgba(93, 82, 75, ${0.4 * lift})`
|
||||
ctx.shadowColor = `rgba(42, 38, 34, ${0.4 * lift})`
|
||||
ctx.shadowBlur = 28 * lift
|
||||
ctx.shadowOffsetX = 0
|
||||
ctx.shadowOffsetY = 12 * lift
|
||||
@@ -1244,7 +1243,7 @@ function drawStickerPreview(ctx, preview) {
|
||||
function drawActiveDrawingPath(ctx, pathData) {
|
||||
if (!pathData.points || pathData.points.length < 2) return
|
||||
ctx.save()
|
||||
ctx.strokeStyle = pathData.color || '#A67C52'
|
||||
ctx.strokeStyle = pathData.color || '#93402A'
|
||||
ctx.lineWidth = pathData.width || 3
|
||||
ctx.lineCap = 'round'
|
||||
ctx.lineJoin = 'round'
|
||||
@@ -1278,17 +1277,17 @@ export function drawPathLabel(ctx, pathLabelRef, pathLabelAlpha, pathNodesRef, p
|
||||
ctx.save()
|
||||
ctx.globalAlpha = pathLabelAlpha * 0.9
|
||||
|
||||
ctx.font = '500 11px "Red Hat Mono", ui-monospace, monospace'
|
||||
ctx.font = '500 11px "DM Sans", system-ui, sans-serif'
|
||||
const metrics = ctx.measureText(pathLabelRef)
|
||||
const pillW = metrics.width + 16
|
||||
const pillH = 22
|
||||
|
||||
ctx.fillStyle = 'rgba(93, 82, 75, 0.85)'
|
||||
ctx.fillStyle = 'rgba(42, 38, 34, 0.85)'
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(labelX - pillW / 2, labelY - pillH / 2, pillW, pillH, pillH / 2)
|
||||
ctx.fill()
|
||||
|
||||
ctx.fillStyle = '#FEF9ED'
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.textAlign = 'center'
|
||||
ctx.textBaseline = 'middle'
|
||||
ctx.fillText(pathLabelRef, labelX, labelY)
|
||||
|
||||
@@ -1,156 +1,57 @@
|
||||
/* Menu du compte (DA terracotta), s'ouvre au-dessus de l'avatar en bas à droite */
|
||||
.avatar-menu.account-menu {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 8px);
|
||||
bottom: calc(100% + 10px);
|
||||
right: 0;
|
||||
width: min(340px, 86vw);
|
||||
max-height: min(70vh, 520px);
|
||||
width: min(320px, 88vw);
|
||||
max-height: min(72vh, 540px);
|
||||
overflow-y: auto;
|
||||
background: rgba(247, 236, 217, 0.98);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(93, 82, 75, 0.08);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(93, 82, 75, 0.25),
|
||||
0 2px 8px rgba(93, 82, 75, 0.08);
|
||||
padding: 8px;
|
||||
animation: dropdownSlideUp 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
transform-origin: bottom right;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--pz-line) transparent;
|
||||
}
|
||||
|
||||
@keyframes dropdownSlideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(8px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.account-menu-header {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
padding: 8px 10px 10px;
|
||||
}
|
||||
|
||||
.account-menu-header--action {
|
||||
width: 100%;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.account-menu-header--action:hover {
|
||||
background: rgba(166, 124, 82, 0.1);
|
||||
}
|
||||
|
||||
.account-menu-header strong {
|
||||
color: #42382f;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.account-menu-header span {
|
||||
color: #7d6e60;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.account-menu-trees {
|
||||
border-top: 1px solid rgba(93, 82, 75, 0.08);
|
||||
border-bottom: 1px solid rgba(93, 82, 75, 0.08);
|
||||
padding: 8px 0;
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.account-menu-single-tree {
|
||||
.am-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 8px;
|
||||
padding: 10px 12px;
|
||||
color: #5d524b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.avatar-menu-item {
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
padding: 10px;
|
||||
border: 0;
|
||||
border-radius: 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #5d524b;
|
||||
text-align: left;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.avatar-menu-item:hover {
|
||||
background: rgba(166, 124, 82, 0.1);
|
||||
}
|
||||
|
||||
.avatar-menu-item.danger {
|
||||
color: #d32f2f;
|
||||
}
|
||||
|
||||
.avatar-menu-item.danger:hover {
|
||||
background: rgba(211, 47, 47, 0.08);
|
||||
}
|
||||
|
||||
.tree-choice {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tree-choice-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
@media (hover: hover) and (pointer: fine) { .am-head:hover { background: var(--pz-line-soft); } }
|
||||
.am-head-mono {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex: none;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: #baa17f;
|
||||
flex-shrink: 0;
|
||||
background: var(--pz-a100);
|
||||
color: var(--pz-a700);
|
||||
font: 400 19px/1 var(--pz-serif);
|
||||
}
|
||||
.am-head-text { display: grid; gap: 2px; min-width: 0; flex: 1; }
|
||||
.am-head-text strong { font: 400 18px/1.2 var(--pz-serif); color: var(--pz-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.am-head-text span { font: 400 12.5px var(--pz-sans); color: var(--pz-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.am-head-chevron { color: var(--pz-faint); flex: none; }
|
||||
|
||||
.tree-choice.active {
|
||||
background: rgba(166, 124, 82, 0.16);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.tree-choice.active .tree-choice-dot {
|
||||
background: #8b6a43;
|
||||
}
|
||||
|
||||
.tree-choice-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tree-choice-arrow {
|
||||
color: #9a8570;
|
||||
}
|
||||
|
||||
.tree-create {
|
||||
color: #7a5d3a;
|
||||
}
|
||||
|
||||
.avatar-menu-icon {
|
||||
opacity: 0.85;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.avatar-menu-separator {
|
||||
height: 1px;
|
||||
background: rgba(93, 82, 75, 0.08);
|
||||
margin: 6px 0;
|
||||
.am-tree-mono {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
flex: none;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
background: var(--pz-a100);
|
||||
color: var(--pz-a700);
|
||||
font: 400 14px/1 var(--pz-serif);
|
||||
}
|
||||
.am-tree-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.am-check { color: var(--pz-a700) !important; }
|
||||
.am-single { cursor: default; }
|
||||
@media (hover: hover) and (pointer: fine) { .am-single:hover { background: transparent; } }
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LogOut, Plus, Settings } from 'lucide-react'
|
||||
import { Check, ChevronRight, LogOut, Plus, Settings } from 'lucide-react'
|
||||
import './AvatarMenu.css'
|
||||
|
||||
function AvatarMenu({
|
||||
@@ -22,53 +22,60 @@ function AvatarMenu({
|
||||
|| 'Aucun arbre'
|
||||
)
|
||||
|
||||
const displayName = linkedPersonName || userDisplayName || 'Mon compte'
|
||||
|
||||
return (
|
||||
<div className="avatar-menu account-menu">
|
||||
<button type="button" className="account-menu-header account-menu-header--action" onClick={onAccountClick}>
|
||||
<strong>{linkedPersonName || userDisplayName || 'Compte'}</strong>
|
||||
<span>{userEmail || 'Ouvrir les options du compte'}</span>
|
||||
<div className="avatar-menu account-menu pz-menu" role="menu">
|
||||
<button type="button" className="am-head" onClick={onAccountClick} role="menuitem">
|
||||
<span className="am-head-mono" aria-hidden="true">{displayName.trim().charAt(0).toUpperCase()}</span>
|
||||
<span className="am-head-text">
|
||||
<strong>{displayName}</strong>
|
||||
<span>{userEmail || 'Mon compte'}</span>
|
||||
</span>
|
||||
<ChevronRight size={16} aria-hidden="true" className="am-head-chevron" />
|
||||
</button>
|
||||
|
||||
<div className="account-menu-trees">
|
||||
{hasSeveralTrees ? (
|
||||
trees.map((tree) => (
|
||||
<div className="pz-menu-sep" />
|
||||
<p className="pz-menu-label">{hasSeveralTrees ? 'Vos arbres' : 'Arbre ouvert'}</p>
|
||||
|
||||
{hasSeveralTrees ? (
|
||||
trees.map((tree) => {
|
||||
const isActive = String(tree.id) === String(activeTreeId)
|
||||
return (
|
||||
<button
|
||||
key={tree.id}
|
||||
type="button"
|
||||
className={`avatar-menu-item tree-choice ${String(tree.id) === String(activeTreeId) ? 'active' : ''}`}
|
||||
role="menuitem"
|
||||
className={`pz-menu-item ${isActive ? 'is-active' : ''}`}
|
||||
onClick={() => onSelectTree?.(tree.id)}
|
||||
>
|
||||
<span className="tree-choice-dot" aria-hidden="true" />
|
||||
<span className="tree-choice-name">{tree.name}</span>
|
||||
<span className="tree-choice-arrow" aria-hidden="true">{'>'}</span>
|
||||
<span className="am-tree-mono" aria-hidden="true">{(tree.name || '?').trim().charAt(0).toUpperCase()}</span>
|
||||
<span className="am-tree-name">{tree.name}</span>
|
||||
{isActive && <Check size={16} aria-hidden="true" className="am-check" />}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="account-menu-single-tree">
|
||||
<span className="tree-choice-dot" aria-hidden="true" />
|
||||
<span>{currentTreeName}</span>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div className="pz-menu-item am-single" aria-current="true">
|
||||
<span className="am-tree-mono" aria-hidden="true">{currentTreeName.trim().charAt(0).toUpperCase()}</span>
|
||||
<span className="am-tree-name">{currentTreeName}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
className="avatar-menu-item tree-create"
|
||||
onClick={onCreateTree}
|
||||
>
|
||||
<span className="avatar-menu-icon"><Plus size={16} /></span>
|
||||
<span>Créer un nouvel arbre</span>
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" role="menuitem" className="pz-menu-item" onClick={onCreateTree}>
|
||||
<Plus size={16} aria-hidden="true" />
|
||||
<span>Créer un nouvel arbre</span>
|
||||
</button>
|
||||
|
||||
<div className="avatar-menu-separator" />
|
||||
<div className="pz-menu-sep" />
|
||||
|
||||
<button type="button" className="avatar-menu-item" onClick={onSettings}>
|
||||
<span className="avatar-menu-icon"><Settings size={16} /></span>
|
||||
<button type="button" role="menuitem" className="pz-menu-item" onClick={onSettings}>
|
||||
<Settings size={16} aria-hidden="true" />
|
||||
<span>Paramètres de l'arbre</span>
|
||||
</button>
|
||||
|
||||
<button type="button" className="avatar-menu-item danger" onClick={onLogout}>
|
||||
<span className="avatar-menu-icon"><LogOut size={16} /></span>
|
||||
<button type="button" role="menuitem" className="pz-menu-item pz-menu-item--danger" onClick={onLogout}>
|
||||
<LogOut size={16} aria-hidden="true" />
|
||||
<span>Déconnexion</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,114 +1,72 @@
|
||||
/* Barre d'actions flottante en bas à droite (DA terracotta) */
|
||||
.contextual-navbar {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
animation: navbarSlideIn 0.6s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
animation: navbarSlideIn 420ms var(--pz-ease) both;
|
||||
}
|
||||
|
||||
/* Let touch/mouse events pass through to canvas during annotation drag */
|
||||
/* Laisse passer les gestes vers le canvas pendant le glisser d'une annotation */
|
||||
.contextual-navbar--dragging {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@keyframes navbarSlideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px) scale(0.95);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
background: rgba(247, 236, 217, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border-radius: 50px;
|
||||
border: 1px solid rgba(93, 82, 75, 0.08);
|
||||
box-shadow:
|
||||
0 4px 80px rgba(93, 82, 75, 0.20),
|
||||
0 1px 6px rgba(93, 82, 75, 0.08);
|
||||
gap: 2px;
|
||||
padding: 5px;
|
||||
background: var(--pz-surface);
|
||||
border: 1px solid var(--pz-line);
|
||||
border-radius: 999px;
|
||||
box-shadow: var(--pz-shadow-lg);
|
||||
}
|
||||
|
||||
/* Wrapper pour les dropdowns */
|
||||
.nav-dropdown-wrapper {
|
||||
position: relative;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
/* Avatar */
|
||||
/* Avatar du compte */
|
||||
.nav-avatar {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(145deg, #A67C52, #8B6843);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: var(--pz-a100);
|
||||
cursor: pointer;
|
||||
transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.nav-avatar:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 4px 16px rgba(166, 124, 82, 0.3);
|
||||
}
|
||||
|
||||
.nav-avatar.active {
|
||||
border-color: #A67C52;
|
||||
}
|
||||
|
||||
.nav-avatar.admin {
|
||||
box-shadow:
|
||||
0 0 0 2px rgba(247, 236, 217, 1),
|
||||
0 0 0 3px #A67C52,
|
||||
0 2px 8px rgba(0, 0, 0, 0.12);
|
||||
transition: box-shadow 160ms ease, transform 140ms var(--pz-ease);
|
||||
box-shadow: 0 0 0 2px var(--pz-surface), 0 0 0 3px var(--pz-line);
|
||||
}
|
||||
.nav-avatar:active { transform: scale(.95); }
|
||||
@media (hover: hover) and (pointer: fine) { .nav-avatar:hover { box-shadow: 0 0 0 2px var(--pz-surface), 0 0 0 3px var(--pz-a300); } }
|
||||
.nav-avatar.active,
|
||||
.nav-avatar.admin.active { box-shadow: 0 0 0 2px var(--pz-surface), 0 0 0 4px var(--pz-a500); }
|
||||
.nav-avatar:focus-visible { outline: 2px solid var(--pz-a500); outline-offset: 3px; }
|
||||
|
||||
.nav-avatar-initial {
|
||||
font-family: 'Bradford LL', 'Iowan Old Style', 'Palatino Linotype', 'Times New Roman', serif;
|
||||
font-size: 20px;
|
||||
font-weight: 450;
|
||||
color: #FEF9ED;
|
||||
font: 400 19px/1 var(--pz-serif);
|
||||
color: var(--pz-a700);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.nav-avatar-photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
/* Responsive mobile */
|
||||
@media (max-width: 768px) {
|
||||
.contextual-navbar {
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
}
|
||||
|
||||
.navbar-content {
|
||||
padding: 4px;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.nav-avatar,
|
||||
.nav-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.nav-button-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
.contextual-navbar { bottom: 12px; right: 12px; }
|
||||
.navbar-content { padding: 4px; }
|
||||
}
|
||||
|
||||
/* ============================================================
|
||||
@@ -120,7 +78,7 @@
|
||||
.navbar-content--annot {
|
||||
gap: 3px;
|
||||
padding: 5px;
|
||||
border: 1px solid rgba(166, 124, 82, 0.15);
|
||||
border: 1px solid rgba(147, 64, 42, 0.15);
|
||||
animation: annotNavMorphIn 0.45s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
@@ -177,16 +135,16 @@
|
||||
justify-content: center;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
background: rgba(93, 82, 75, 0.08);
|
||||
color: #8B7355;
|
||||
background: rgba(42, 38, 34, 0.08);
|
||||
color: #6B645D;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-annot-exit:hover {
|
||||
background: rgba(93, 82, 75, 0.16);
|
||||
color: #5D524B;
|
||||
background: rgba(42, 38, 34, 0.16);
|
||||
color: #2A2622;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
@@ -198,7 +156,7 @@
|
||||
.nav-annot-sep {
|
||||
width: 1px;
|
||||
height: 24px;
|
||||
background: linear-gradient(180deg, transparent, rgba(166, 124, 82, 0.18), transparent);
|
||||
background: linear-gradient(180deg, transparent, rgba(147, 64, 42, 0.18), transparent);
|
||||
margin: 0 2px;
|
||||
border-radius: 1px;
|
||||
flex-shrink: 0;
|
||||
@@ -215,14 +173,14 @@
|
||||
border: none;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: #5D524B;
|
||||
color: #2A2622;
|
||||
cursor: pointer;
|
||||
transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-annot-btn:hover:not(:disabled) {
|
||||
background: rgba(166, 124, 82, 0.1);
|
||||
background: rgba(147, 64, 42, 0.1);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -232,13 +190,13 @@
|
||||
|
||||
/* Active tool — gradient fill */
|
||||
.nav-annot-btn--active {
|
||||
background: linear-gradient(140deg, #8b6a43, #6a4f32);
|
||||
color: #fef9ed;
|
||||
box-shadow: 0 2px 10px rgba(166, 124, 82, 0.35);
|
||||
background: linear-gradient(140deg, #93402a, #2a2622);
|
||||
color: #ffffff;
|
||||
box-shadow: 0 2px 10px rgba(147, 64, 42, 0.35);
|
||||
}
|
||||
|
||||
.nav-annot-btn--active:hover:not(:disabled) {
|
||||
background: linear-gradient(140deg, #96703e, #7a5a3a);
|
||||
background: linear-gradient(140deg, #93402a, #3d3731);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -252,7 +210,7 @@
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
border-radius: 50%;
|
||||
background: #fef9ed;
|
||||
background: #ffffff;
|
||||
opacity: 0.7;
|
||||
animation: annotDotFade 1.8s ease-in-out infinite;
|
||||
}
|
||||
@@ -288,11 +246,11 @@
|
||||
|
||||
/* --- Add person button --- */
|
||||
.nav-annot-btn--add {
|
||||
color: #A67C52;
|
||||
color: #93402A;
|
||||
}
|
||||
|
||||
.nav-annot-btn--add:hover:not(:disabled) {
|
||||
color: #8B6843;
|
||||
color: #3D3731;
|
||||
}
|
||||
|
||||
/* --- Sub-panel (options) above navbar --- */
|
||||
@@ -302,13 +260,13 @@
|
||||
right: 0;
|
||||
padding: 14px;
|
||||
border-radius: 16px;
|
||||
background: rgba(254, 249, 237, 0.96);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border: 1px solid rgba(166, 124, 82, 0.15);
|
||||
border: 1px solid rgba(147, 64, 42, 0.15);
|
||||
box-shadow:
|
||||
0 8px 40px rgba(93, 82, 75, 0.18),
|
||||
0 2px 8px rgba(93, 82, 75, 0.06);
|
||||
0 8px 40px rgba(42, 38, 34, 0.18),
|
||||
0 2px 8px rgba(42, 38, 34, 0.06);
|
||||
min-width: 190px;
|
||||
animation: annotPanelSlideUp 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
@@ -1,76 +1,41 @@
|
||||
/* Bouton rond de la barre d'actions (DA terracotta) */
|
||||
.nav-button {
|
||||
position: relative;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
color: #5D524B;
|
||||
background: transparent;
|
||||
color: var(--pz-ink);
|
||||
cursor: pointer;
|
||||
transition: background-color 160ms ease, color 160ms ease, transform 140ms var(--pz-ease);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) { .nav-button:hover:not(.disabled) { background: var(--pz-line-soft); } }
|
||||
.nav-button:active:not(.disabled) { transform: scale(.94); }
|
||||
.nav-button:focus-visible { outline: 2px solid var(--pz-a500); outline-offset: 2px; }
|
||||
.nav-button.active { background: var(--pz-a100); color: var(--pz-a700); }
|
||||
.nav-button.disabled { opacity: .35; cursor: not-allowed; }
|
||||
|
||||
.nav-button:hover:not(.disabled) {
|
||||
background: rgba(166, 124, 82, 0.12);
|
||||
transform: scale(1.08);
|
||||
}
|
||||
.nav-button-icon { display: grid; place-items: center; line-height: 1; }
|
||||
.nav-button-icon svg { width: 20px; height: 20px; }
|
||||
|
||||
.nav-button:active:not(.disabled) {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.nav-button.active {
|
||||
background: rgba(166, 124, 82, 0.18);
|
||||
color: #A67C52;
|
||||
}
|
||||
|
||||
.nav-button.disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.nav-button-icon {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.05));
|
||||
}
|
||||
|
||||
/* Badge notifications */
|
||||
.nav-button-badge {
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 4px;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
|
||||
animation: badgePulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.nav-button-badge.orange {
|
||||
background: #FF9500;
|
||||
}
|
||||
|
||||
.nav-button-badge.gray {
|
||||
background: #8E8E93;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes badgePulse {
|
||||
0%, 100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 999px;
|
||||
border: 2px solid var(--pz-surface);
|
||||
font: 600 10.5px/1 var(--pz-sans);
|
||||
color: #fff;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
.nav-button-badge.orange { background: var(--pz-a500); }
|
||||
.nav-button-badge.gray { background: var(--pz-faint); }
|
||||
@@ -5,14 +5,14 @@
|
||||
min-width: 320px;
|
||||
max-width: 400px;
|
||||
max-height: 480px;
|
||||
background: rgba(247, 236, 217, 0.98);
|
||||
background: rgba(242, 239, 234, 0.98);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(93, 82, 75, 0.08);
|
||||
border: 1px solid rgba(42, 38, 34, 0.08);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(93, 82, 75, 0.25),
|
||||
0 2px 8px rgba(93, 82, 75, 0.08);
|
||||
0 8px 32px rgba(42, 38, 34, 0.25),
|
||||
0 2px 8px rgba(42, 38, 34, 0.08);
|
||||
padding: 8px;
|
||||
overflow-y: auto;
|
||||
animation: dropdownSlideUp 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
@@ -28,7 +28,7 @@
|
||||
}
|
||||
|
||||
.tree-dropdown::-webkit-scrollbar-thumb {
|
||||
background: rgba(166, 124, 82, 0.2);
|
||||
background: rgba(147, 64, 42, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
@@ -41,10 +41,10 @@
|
||||
}
|
||||
|
||||
.tree-dropdown-section-title {
|
||||
font-family: 'Bradford LL', 'Iowan Old Style', 'Palatino Linotype', 'Times New Roman', serif;
|
||||
font-family: 'Newsreader', Georgia, 'Times New Roman', serif;
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #A67C52;
|
||||
color: #93402A;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.8px;
|
||||
padding: 8px 12px 6px;
|
||||
@@ -65,11 +65,11 @@
|
||||
}
|
||||
|
||||
.tree-dropdown-item:hover {
|
||||
background: rgba(166, 124, 82, 0.08);
|
||||
background: rgba(147, 64, 42, 0.08);
|
||||
}
|
||||
|
||||
.tree-dropdown-item.active {
|
||||
background: rgba(166, 124, 82, 0.15);
|
||||
background: rgba(147, 64, 42, 0.15);
|
||||
}
|
||||
|
||||
.tree-dropdown-item-thumb {
|
||||
@@ -77,7 +77,7 @@
|
||||
height: 40px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(145deg, #A67C52, #8B6843);
|
||||
background: #2a2622;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -102,7 +102,7 @@
|
||||
.tree-dropdown-item-name {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #5D524B;
|
||||
color: #2A2622;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -110,7 +110,7 @@
|
||||
|
||||
.tree-dropdown-item-role {
|
||||
font-size: 14px;
|
||||
color: #8B7355;
|
||||
color: #6B645D;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
margin-top: 2px;
|
||||
@@ -118,7 +118,7 @@
|
||||
|
||||
/* Actions */
|
||||
.tree-dropdown-actions {
|
||||
border-top: 1px solid rgba(93, 82, 75, 0.08);
|
||||
border-top: 1px solid rgba(42, 38, 34, 0.08);
|
||||
padding-top: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -129,20 +129,20 @@
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
background: rgba(166, 124, 82, 0.08);
|
||||
border: 1px dashed rgba(166, 124, 82, 0.3);
|
||||
background: rgba(147, 64, 42, 0.08);
|
||||
border: 1px dashed rgba(147, 64, 42, 0.3);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: #A67C52;
|
||||
color: #93402A;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.tree-dropdown-create:hover {
|
||||
background: rgba(166, 124, 82, 0.15);
|
||||
border-color: rgba(166, 124, 82, 0.5);
|
||||
background: rgba(147, 64, 42, 0.15);
|
||||
border-color: rgba(147, 64, 42, 0.5);
|
||||
}
|
||||
|
||||
.tree-dropdown-create-icon {
|
||||
|
||||
@@ -1,146 +1,75 @@
|
||||
/* Recherche (DA terracotta) : bouton rond en haut à droite, champ en pilule une fois ouvert */
|
||||
.expandable-search {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
transition: all 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
/* Bouton icône collapsed */
|
||||
.search-icon-button {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 1px solid var(--pz-line);
|
||||
border-radius: 50%;
|
||||
background: rgba(247, 236, 217, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
border: 1px solid rgba(93, 82, 75, 0.08);
|
||||
box-shadow:
|
||||
0 4px 20px rgba(93, 82, 75, 0.18),
|
||||
0 1px 4px rgba(93, 82, 75, 0.08);
|
||||
background: var(--pz-surface);
|
||||
color: var(--pz-ink);
|
||||
box-shadow: var(--pz-shadow);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
animation: searchFadeIn 0.4s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
transition: transform 140ms var(--pz-ease), border-color 160ms ease;
|
||||
}
|
||||
.search-icon-button:active { transform: scale(.95); }
|
||||
@media (hover: hover) and (pointer: fine) { .search-icon-button:hover { border-color: var(--pz-a300); } }
|
||||
.search-icon { display: grid; place-items: center; }
|
||||
.search-icon svg { width: 20px; height: 20px; }
|
||||
|
||||
@keyframes searchFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.search-icon-button:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow:
|
||||
0 6px 24px rgba(166, 124, 82, 0.25),
|
||||
0 2px 6px rgba(93, 82, 75, 0.12);
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
font-size: 20px;
|
||||
filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.05));
|
||||
}
|
||||
|
||||
/* Contenu expanded */
|
||||
.search-expanded-content {
|
||||
animation: searchExpand 0.3s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
position: relative;
|
||||
width: 380px;
|
||||
animation: pz-rise 180ms var(--pz-ease) both;
|
||||
}
|
||||
|
||||
@keyframes searchExpand {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Input wrapper */
|
||||
.search-input-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
width: 340px;
|
||||
height: 48px;
|
||||
padding: 0 12px 0 16px;
|
||||
background: rgba(247, 236, 217, 0.98);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border-radius: 50px;
|
||||
border: 1px solid rgba(93, 82, 75, 0.08);
|
||||
box-shadow:
|
||||
0 4px 20px rgba(93, 82, 75, 0.18),
|
||||
0 1px 4px rgba(93, 82, 75, 0.08);
|
||||
background: var(--pz-surface);
|
||||
border: 1px solid var(--pz-a300);
|
||||
border-radius: 999px;
|
||||
box-shadow: 0 0 0 4px var(--pz-a100), var(--pz-shadow);
|
||||
}
|
||||
|
||||
.search-input-icon {
|
||||
font-size: 18px;
|
||||
opacity: 0.6;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search-input-icon { position: absolute; left: 16px; display: grid; place-items: center; color: var(--pz-muted); pointer-events: none; }
|
||||
.search-input-icon svg { width: 18px; height: 18px; }
|
||||
.search-input {
|
||||
flex: 1;
|
||||
border: none;
|
||||
height: 100%;
|
||||
padding: 0 48px 0 46px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
font: 400 15px var(--pz-sans);
|
||||
color: var(--pz-ink);
|
||||
outline: none;
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-size: 16px;
|
||||
color: #5D524B;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: #8B7355;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.search-input::placeholder { color: var(--pz-faint); }
|
||||
.search-close-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: #8B7355;
|
||||
font-size: 16px;
|
||||
color: var(--pz-muted);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) { .search-close-button:hover { background: var(--pz-line-soft); color: var(--pz-ink); } }
|
||||
|
||||
.search-close-button:hover {
|
||||
background: rgba(93, 82, 75, 0.1);
|
||||
color: #5D524B;
|
||||
}
|
||||
|
||||
/* Responsive mobile */
|
||||
@media (max-width: 768px) {
|
||||
.expandable-search {
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
.search-icon-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.search-input-wrapper {
|
||||
width: 100%;
|
||||
}
|
||||
.expandable-search { top: 12px; right: 12px; }
|
||||
.expandable-search.expanded { left: 12px; }
|
||||
.search-expanded-content { width: 100%; }
|
||||
}
|
||||
@@ -111,7 +111,7 @@ function ExpandableSearch({
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
className="search-input"
|
||||
placeholder="Rechercher une personne..."
|
||||
placeholder="Un prénom, un nom, une ville"
|
||||
value={query}
|
||||
onChange={handleInputChange}
|
||||
aria-label="Rechercher une personne"
|
||||
|
||||
@@ -1,175 +1,64 @@
|
||||
/* Résultats de recherche (DA terracotta) */
|
||||
.search-results-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
top: calc(100% + 10px);
|
||||
right: 0;
|
||||
width: 380px;
|
||||
max-height: 480px;
|
||||
background: rgba(247, 236, 217, 0.98);
|
||||
backdrop-filter: blur(24px);
|
||||
-webkit-backdrop-filter: blur(24px);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(93, 82, 75, 0.08);
|
||||
box-shadow:
|
||||
0 8px 32px rgba(93, 82, 75, 0.25),
|
||||
0 2px 8px rgba(93, 82, 75, 0.08);
|
||||
overflow: hidden;
|
||||
animation: dropdownSlideDown 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
@keyframes dropdownSlideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.search-results-list {
|
||||
max-height: 440px;
|
||||
left: 0;
|
||||
max-height: min(60vh, 460px);
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
padding: 6px;
|
||||
background: var(--pz-surface);
|
||||
border: 1px solid var(--pz-line);
|
||||
border-radius: var(--pz-r-m);
|
||||
box-shadow: var(--pz-shadow-lg);
|
||||
animation: pz-rise 160ms var(--pz-ease) both;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--pz-line) transparent;
|
||||
}
|
||||
|
||||
.search-results-list::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
.search-results-list { display: grid; gap: 2px; }
|
||||
|
||||
.search-results-list::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.search-results-list::-webkit-scrollbar-thumb {
|
||||
background: rgba(166, 124, 82, 0.2);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
/* Résultat item */
|
||||
.search-result-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.search-result-item:hover {
|
||||
background: rgba(166, 124, 82, 0.12);
|
||||
transform: translateX(2px);
|
||||
cursor: pointer;
|
||||
transition: background-color 140ms ease;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) { .search-result-item:hover { background: var(--pz-a50); } }
|
||||
.search-result-item:focus-visible { outline: 2px solid var(--pz-a500); outline-offset: -2px; }
|
||||
|
||||
.search-result-thumb {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
flex: none;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(145deg, #A67C52, #8B6843);
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.search-result-thumb img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.search-result-initials {
|
||||
font-family: 'Bradford LL', 'Iowan Old Style', 'Palatino Linotype', 'Times New Roman', serif;
|
||||
font-size: 16px;
|
||||
font-weight: 450;
|
||||
color: #FEF9ED;
|
||||
}
|
||||
|
||||
/* Info */
|
||||
.search-result-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search-result-name {
|
||||
font-size: 14px;
|
||||
font-weight: 450;
|
||||
color: #5D524B;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.search-result-lastname {
|
||||
font-style: italic;
|
||||
color: #8B7355;
|
||||
}
|
||||
|
||||
.search-result-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 3px;
|
||||
min-width: 0;
|
||||
border-radius: 24%;
|
||||
background: var(--pz-a100);
|
||||
box-shadow: 0 0 0 1px rgba(42, 38, 34, .06);
|
||||
}
|
||||
.search-result-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
||||
.search-result-initials { width: 100%; height: 100%; display: grid; place-items: center; font: 400 18px/1 var(--pz-serif); color: var(--pz-a700); }
|
||||
|
||||
.search-result-info { display: grid; gap: 2px; min-width: 0; }
|
||||
.search-result-name { font: 400 17px/1.2 var(--pz-serif); color: var(--pz-ink); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.search-result-lastname { font-style: italic; font-weight: 300; }
|
||||
.search-result-meta { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; font: 400 12.5px var(--pz-sans); color: var(--pz-muted); }
|
||||
.search-result-year { font-variant-numeric: tabular-nums; }
|
||||
.search-result-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
background: rgba(166, 124, 82, 0.15);
|
||||
color: #A67C52;
|
||||
font-size: 12px;
|
||||
font-weight: 450;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
border-radius: 999px;
|
||||
background: var(--pz-a100);
|
||||
color: var(--pz-a700);
|
||||
font: 500 11.5px var(--pz-sans);
|
||||
}
|
||||
.search-result-match-detail { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
|
||||
.search-result-year {
|
||||
font-size: 14px;
|
||||
color: #8B7355;
|
||||
}
|
||||
|
||||
.search-result-match-detail {
|
||||
font-size: 14px;
|
||||
color: #8B7355;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.search-result-empty {
|
||||
text-align: center;
|
||||
color: #8B7355;
|
||||
font-size: 14px;
|
||||
font-style: italic;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.search-results-footer {
|
||||
padding: 10px 12px;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
color: #8B7355;
|
||||
background: rgba(166, 124, 82, 0.05);
|
||||
border-top: 1px solid rgba(93, 82, 75, 0.08);
|
||||
}
|
||||
|
||||
/* Responsive mobile */
|
||||
@media (max-width: 768px) {
|
||||
.search-results-dropdown {
|
||||
width: 100%;
|
||||
max-width: calc(100vw - 24px);
|
||||
}
|
||||
}
|
||||
.search-result-empty { padding: 18px 14px; text-align: center; font: 400 14px/1.5 var(--pz-sans); color: var(--pz-muted); }
|
||||
.search-results-footer { padding: 10px 12px 6px; border-top: 1px solid var(--pz-line-soft); margin-top: 4px; font: 400 12.5px var(--pz-sans); color: var(--pz-faint); text-align: center; }
|
||||
@@ -25,7 +25,7 @@ function SearchResultsDropdown({
|
||||
return (
|
||||
<div className="search-results-dropdown">
|
||||
<div className="search-result-empty">
|
||||
Aucun résultat pour "{query}"
|
||||
Personne ne s'appelle « {query} » dans cet arbre
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -49,7 +49,7 @@ function SearchResultsDropdown({
|
||||
<img src={person.photo} alt="" />
|
||||
) : (
|
||||
<div className="search-result-initials">
|
||||
{person.firstName[0]}{person.lastName[0]}
|
||||
{person.firstName?.[0] || '?'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -60,8 +60,9 @@ function SearchResultsDropdown({
|
||||
{person.firstName} <span className="search-result-lastname">{person.lastName}</span>
|
||||
</div>
|
||||
<div className="search-result-meta">
|
||||
<span className="search-result-badge">{reason}</span>
|
||||
{matchDetail && (
|
||||
{person.birthYear && <span className="search-result-year">{person.birthYear}{person.deathYear ? `–${person.deathYear}` : ''}</span>}
|
||||
{reason && reason.toLowerCase() !== 'nom' && <span className="search-result-badge">{reason}</span>}
|
||||
{matchDetail && matchDetail !== `${person.firstName} ${person.lastName}` && (
|
||||
<span className="search-result-match-detail">{matchDetail}</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -72,7 +73,7 @@ function SearchResultsDropdown({
|
||||
|
||||
{results.length > 10 && (
|
||||
<div className="search-results-footer">
|
||||
+{results.length - 10} résultats supplémentaires
|
||||
{results.length - 10} autres résultats, précisez la recherche
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -4,32 +4,32 @@
|
||||
// ============================================================
|
||||
|
||||
export let persons = [
|
||||
{ id: 1, firstName: 'Jean', lastName: 'Dupont', birthYear: 1890, deathYear: 1965, isAlive: false, photo: 'https://randomuser.me/api/portraits/men/81.jpg', frameType: 'polaroid', birthPlace: 'Limoges', region: 'Haute-Vienne', profession: 'Menuisier', nationality: 'Française', note: 'Patriarche de la famille' },
|
||||
{ id: 2, firstName: 'Marie', lastName: 'Martin', birthYear: 1895, deathYear: 1970, isAlive: false, photo: 'https://randomuser.me/api/portraits/women/71.jpg', frameType: 'round', birthPlace: 'Brive-la-Gaillarde', region: 'Corrèze', profession: 'Couturière', note: 'Atelier de couture rue du Commerce' },
|
||||
{ id: 3, firstName: 'Robert', lastName: 'Dupont', birthYear: 1920, deathYear: 1995, isAlive: false, photo: 'https://randomuser.me/api/portraits/men/67.jpg', frameType: 'rect', birthPlace: 'Limoges', region: 'Haute-Vienne', profession: 'Instituteur', note: 'A enseigné 35 ans à l\'école communale' },
|
||||
{ id: 4, firstName: 'André', lastName: 'Dupont', birthYear: 1923, deathYear: 2000, isAlive: false, photo: 'https://randomuser.me/api/portraits/men/58.jpg', frameType: 'polaroid', birthPlace: 'Limoges', region: 'Haute-Vienne', profession: 'Charpentier' },
|
||||
{ id: 5, firstName: 'Simone', lastName: 'Petit', birthYear: 1925, deathYear: 2010, isAlive: false, photo: 'https://randomuser.me/api/portraits/women/54.jpg', frameType: 'round', birthPlace: 'Guéret', region: 'Creuse', profession: 'Infirmière' },
|
||||
{ id: 9, firstName: 'Isabelle', lastName: 'Bernard', birthYear: 1928, isAlive: true, photo: 'https://randomuser.me/api/portraits/women/66.jpg', frameType: 'rect', birthPlace: 'Tulle', region: 'Corrèze', profession: 'Sage-femme', note: 'Doyenne de la famille, vit à Brive' },
|
||||
{ id: 6, firstName: 'Jacques', lastName: 'Dupont', birthYear: 1945, deathYear: 2015, isAlive: false, photo: 'https://randomuser.me/api/portraits/men/45.jpg', frameType: 'polaroid', birthPlace: 'Limoges', region: 'Haute-Vienne', profession: 'Journaliste', note: 'Reporter au Populaire du Centre' },
|
||||
{ id: 7, firstName: 'Philippe', lastName: 'Dupont', birthYear: 1948, isAlive: true, photo: 'https://randomuser.me/api/portraits/men/32.jpg', frameType: 'round', birthPlace: 'Limoges', region: 'Haute-Vienne', profession: 'Pharmacien' },
|
||||
{ id: 8, firstName: 'Sophie', lastName: 'Dupont', birthYear: 1960, isAlive: true, photo: 'https://randomuser.me/api/portraits/women/65.jpg', frameType: 'rect', birthPlace: 'Brive-la-Gaillarde', region: 'Corrèze', profession: 'Architecte', note: 'Cabinet d’architecture à Bordeaux' },
|
||||
{ id: 10, firstName: 'Thomas', lastName: 'Dupont', birthYear: 1962, isAlive: true, photo: 'https://randomuser.me/api/portraits/men/29.jpg', frameType: 'polaroid', birthPlace: 'Brive-la-Gaillarde', region: 'Corrèze', profession: 'Vétérinaire' },
|
||||
{ id: 11, firstName: 'Claire', lastName: 'Dupont', birthYear: 1955, isAlive: true, photo: 'https://randomuser.me/api/portraits/women/58.jpg', birthPlace: 'Paris', region: 'Ile-de-France', profession: 'Enseignante' },
|
||||
{ id: 1, firstName: 'Jean', lastName: 'Dupont', birthYear: 1890, deathYear: 1965, isAlive: false, photo: '/demo/jean.webp', frameType: 'polaroid', birthPlace: 'Limoges', region: 'Haute-Vienne', profession: 'Menuisier', nationality: 'Française', note: 'Patriarche de la famille' },
|
||||
{ id: 2, firstName: 'Marie', lastName: 'Martin', birthYear: 1895, deathYear: 1970, isAlive: false, photo: '/demo/marie.webp', frameType: 'round', birthPlace: 'Brive-la-Gaillarde', region: 'Corrèze', profession: 'Couturière', note: 'Atelier de couture rue du Commerce' },
|
||||
{ id: 3, firstName: 'Robert', lastName: 'Dupont', birthYear: 1920, deathYear: 1995, isAlive: false, photo: '/demo/robert.webp', frameType: 'rect', birthPlace: 'Limoges', region: 'Haute-Vienne', profession: 'Instituteur', note: 'A enseigné 35 ans à l\'école communale' },
|
||||
{ id: 4, firstName: 'André', lastName: 'Dupont', birthYear: 1923, deathYear: 2000, isAlive: false, photo: '/demo/andre.webp', frameType: 'polaroid', birthPlace: 'Limoges', region: 'Haute-Vienne', profession: 'Charpentier' },
|
||||
{ id: 5, firstName: 'Simone', lastName: 'Petit', birthYear: 1925, deathYear: 2010, isAlive: false, photo: '/demo/simone.webp', frameType: 'round', birthPlace: 'Guéret', region: 'Creuse', profession: 'Infirmière' },
|
||||
{ id: 9, firstName: 'Isabelle', lastName: 'Bernard', birthYear: 1928, isAlive: true, photo: '/demo/isabelle.webp', frameType: 'rect', birthPlace: 'Fort-de-France', region: 'Martinique', profession: 'Sage-femme', note: 'Doyenne de la famille, vit à Brive' },
|
||||
{ id: 6, firstName: 'Jacques', lastName: 'Dupont', birthYear: 1945, deathYear: 2015, isAlive: false, photo: '/demo/jacques.webp', frameType: 'polaroid', birthPlace: 'Limoges', region: 'Haute-Vienne', profession: 'Journaliste', note: 'Reporter au Populaire du Centre' },
|
||||
{ id: 7, firstName: 'Philippe', lastName: 'Dupont', birthYear: 1948, isAlive: true, photo: '/demo/philippe.webp', frameType: 'round', birthPlace: 'Limoges', region: 'Haute-Vienne', profession: 'Pharmacien' },
|
||||
{ id: 8, firstName: 'Sophie', lastName: 'Dupont', birthYear: 1960, isAlive: true, photo: '/demo/sophie.webp', frameType: 'rect', birthPlace: 'Brive-la-Gaillarde', region: 'Corrèze', profession: 'Architecte', note: 'Cabinet d’architecture à Bordeaux' },
|
||||
{ id: 10, firstName: 'Thomas', lastName: 'Dupont', birthYear: 1962, isAlive: true, photo: '/demo/thomas.webp', frameType: 'polaroid', birthPlace: 'Brive-la-Gaillarde', region: 'Corrèze', profession: 'Vétérinaire' },
|
||||
{ id: 11, firstName: 'Claire', lastName: 'Dupont', birthYear: 1955, isAlive: true, photo: '/demo/claire.webp', birthPlace: 'Paris', region: 'Ile-de-France', profession: 'Enseignante' },
|
||||
]
|
||||
|
||||
export let medias = [
|
||||
{ id: 'm1', personId: 1, type: 'photo', url: 'https://images.pexels.com/photos/415829/pexels-photo-415829.jpeg?auto=compress&cs=tinysrgb&w=200&h=200&dpr=1', urlHd: 'https://images.pexels.com/photos/415829/pexels-photo-415829.jpeg?auto=compress&cs=tinysrgb&w=1200&dpr=1', label: 'Mariage 1918' },
|
||||
{ id: 'm2', personId: 1, type: 'photo', url: 'https://images.pexels.com/photos/712513/pexels-photo-712513.jpeg?auto=compress&cs=tinysrgb&w=200&h=200&dpr=1', urlHd: 'https://images.pexels.com/photos/712513/pexels-photo-712513.jpeg?auto=compress&cs=tinysrgb&w=1200&dpr=1', label: 'Atelier de menuiserie' },
|
||||
{ id: 'm1', personId: 1, type: 'photo', url: '/demo/souvenir-mariage-1918-thumb.webp', urlHd: '/demo/souvenir-mariage-1918.webp', label: 'Mariage 1918' },
|
||||
{ id: 'm2', personId: 1, type: 'photo', url: '/demo/souvenir-atelier-thumb.webp', urlHd: '/demo/souvenir-atelier.webp', label: 'Atelier de menuiserie' },
|
||||
{ id: 'm3', personId: 1, type: 'video', url: 'https://www.youtube.com/watch?v=jNQXAC9IVRw', mimeType: 'video/youtube', label: 'Témoignage filmé 1960' },
|
||||
{ id: 'm4', personId: 2, type: 'photo', url: 'https://images.pexels.com/photos/1462630/pexels-photo-1462630.jpeg?auto=compress&cs=tinysrgb&w=200&h=200&dpr=1', urlHd: 'https://images.pexels.com/photos/1462630/pexels-photo-1462630.jpeg?auto=compress&cs=tinysrgb&w=1200&dpr=1', label: 'Jeunesse' },
|
||||
{ id: 'm5', personId: 2, type: 'photo', url: 'https://images.pexels.com/photos/1542085/pexels-photo-1542085.jpeg?auto=compress&cs=tinysrgb&w=200&h=200&dpr=1', urlHd: 'https://images.pexels.com/photos/1542085/pexels-photo-1542085.jpeg?auto=compress&cs=tinysrgb&w=1200&dpr=1', label: 'Portrait de famille' },
|
||||
{ id: 'm6', personId: 3, type: 'photo', url: 'https://images.pexels.com/photos/1043474/pexels-photo-1043474.jpeg?auto=compress&cs=tinysrgb&w=200&h=200&dpr=1', urlHd: 'https://images.pexels.com/photos/1043474/pexels-photo-1043474.jpeg?auto=compress&cs=tinysrgb&w=1200&dpr=1', label: 'Service militaire' },
|
||||
{ id: 'm4', personId: 2, type: 'photo', url: '/demo/souvenir-jeunesse-thumb.webp', urlHd: '/demo/souvenir-jeunesse.webp', label: 'Jeunesse' },
|
||||
{ id: 'm5', personId: 2, type: 'photo', url: '/demo/souvenir-portrait-famille-thumb.webp', urlHd: '/demo/souvenir-portrait-famille.webp', label: 'Portrait de famille' },
|
||||
{ id: 'm6', personId: 3, type: 'photo', url: '/demo/souvenir-service-militaire-thumb.webp', urlHd: '/demo/souvenir-service-militaire.webp', label: 'Service militaire' },
|
||||
{ id: 'm7', personId: 3, type: 'video', url: 'https://www.youtube.com/watch?v=aqz-KE-bpKQ', mimeType: 'video/youtube', label: 'Mariage Simone 1943' },
|
||||
{ id: 'm8', personId: 3, type: 'photo', url: 'https://images.pexels.com/photos/1516680/pexels-photo-1516680.jpeg?auto=compress&cs=tinysrgb&w=200&h=200&dpr=1', urlHd: 'https://images.pexels.com/photos/1516680/pexels-photo-1516680.jpeg?auto=compress&cs=tinysrgb&w=1200&dpr=1', label: 'Années 70' },
|
||||
{ id: 'm9', personId: 8, type: 'photo', url: 'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg?auto=compress&cs=tinysrgb&w=200&h=200&dpr=1', urlHd: 'https://images.pexels.com/photos/774909/pexels-photo-774909.jpeg?auto=compress&cs=tinysrgb&w=1200&dpr=1', label: 'Diplôme' },
|
||||
{ id: 'm10', personId: 8, type: 'photo', url: 'https://images.pexels.com/photos/1300402/pexels-photo-1300402.jpeg?auto=compress&cs=tinysrgb&w=200&h=200&dpr=1', urlHd: 'https://images.pexels.com/photos/1300402/pexels-photo-1300402.jpeg?auto=compress&cs=tinysrgb&w=1200&dpr=1', label: 'Plan du cabinet 1985' },
|
||||
{ id: 'm8', personId: 3, type: 'photo', url: '/demo/souvenir-annees-70-thumb.webp', urlHd: '/demo/souvenir-annees-70.webp', label: 'Années 70' },
|
||||
{ id: 'm9', personId: 8, type: 'photo', url: '/demo/souvenir-diplome-thumb.webp', urlHd: '/demo/souvenir-diplome.webp', label: 'Diplôme' },
|
||||
{ id: 'm10', personId: 8, type: 'photo', url: '/demo/souvenir-plan-cabinet-thumb.webp', urlHd: '/demo/souvenir-plan-cabinet.webp', label: 'Plan du cabinet 1985' },
|
||||
{ id: 'm11', personId: 10, type: 'video', url: 'https://www.youtube.com/watch?v=M7lc1UVf-VE', mimeType: 'video/youtube', label: 'Anniversaire 2020' },
|
||||
{ id: 'm12', personId: 6, type: 'photo', url: 'https://images.pexels.com/photos/3777931/pexels-photo-3777931.jpeg?auto=compress&cs=tinysrgb&w=200&h=200&dpr=1', urlHd: 'https://images.pexels.com/photos/3777931/pexels-photo-3777931.jpeg?auto=compress&cs=tinysrgb&w=1200&dpr=1', label: 'En rédaction' },
|
||||
{ id: 'm12', personId: 6, type: 'photo', url: '/demo/souvenir-redaction-thumb.webp', urlHd: '/demo/souvenir-redaction.webp', label: 'En rédaction' },
|
||||
{ id: 'm13', personId: 6, type: 'video', url: 'https://www.youtube.com/watch?v=ScMzIvxBSi4', mimeType: 'video/youtube', label: 'Interview archive radio' },
|
||||
]
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ export function useAnnotations({ editModeActive, updateDraft }) {
|
||||
const [revision, setRevision] = useState(0)
|
||||
|
||||
const [currentStyle, setCurrentStyle] = useState({
|
||||
color: '#A67C52',
|
||||
color: '#93402A',
|
||||
brushWidth: 3,
|
||||
fontSize: 18,
|
||||
stabilo: false,
|
||||
|
||||
@@ -2,42 +2,6 @@
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@font-face {
|
||||
font-family: 'Bradford LL';
|
||||
src: url('./assets/Bradford/BradfordLLWeb-Regular.woff') format('woff'),
|
||||
url('./assets/Bradford/BradfordLL-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Bradford LL';
|
||||
src: url('./assets/Bradford/BradfordLLWeb-Bold.woff') format('woff'),
|
||||
url('./assets/Bradford/BradfordLL-Bold.ttf') format('truetype');
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Bradford LL';
|
||||
src: url('./assets/Bradford/BradfordLLWeb-Italic.woff') format('woff'),
|
||||
url('./assets/Bradford/BradfordLL-Italic.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Bradford Mono LL';
|
||||
src: url('./assets/Bradford/BradfordMonoLLWeb-Regular.woff') format('woff'),
|
||||
url('./assets/Bradford/BradfordMonoLL-Regular.ttf') format('truetype');
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
margin: 0;
|
||||
@@ -46,21 +10,21 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Bradford LL', 'Iowan Old Style', 'Palatino Linotype', 'Times New Roman', serif;
|
||||
font-weight: 450;
|
||||
background-color: #FDF8EC;
|
||||
color: #5D524B;
|
||||
font-family: 'Newsreader', Georgia, 'Times New Roman', serif;
|
||||
font-weight: 400;
|
||||
background-color: #FBFAF7;
|
||||
color: #2A2622;
|
||||
overflow: hidden;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
font-family: 'Bradford LL', 'Iowan Old Style', 'Palatino Linotype', 'Times New Roman', serif;
|
||||
font-weight: 450;
|
||||
font-family: 'Newsreader', Georgia, 'Times New Roman', serif;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
/* Red Hat Mono pour les petits éléments UI : labels, meta, nav, badges */
|
||||
/* DM Sans pour les petits éléments UI : labels, meta, nav, badges */
|
||||
button, input, select, textarea,
|
||||
label,
|
||||
.tree-meta, .admin-meta, .admin-stat-card span,
|
||||
@@ -71,16 +35,16 @@
|
||||
.admin-audit-main span, .admin-audit-meta,
|
||||
.admin-person-item span,
|
||||
.account-help {
|
||||
font-family: 'Red Hat Mono', ui-monospace, monospace;
|
||||
font-family: 'DM Sans', system-ui, sans-serif;
|
||||
}
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
.text-cosmos-cream {
|
||||
color: #FEF9ED;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.bg-cosmos-black {
|
||||
background-color: #000000;
|
||||
background-color: #2A2622;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/* ============================================================
|
||||
Parenthèse · composants de la DA (accent terracotta, validé le 24/09/2026)
|
||||
Base commune des écrans refaits : accueil, compte, fenêtres, menus, fiche.
|
||||
============================================================ */
|
||||
|
||||
:root {
|
||||
--pz-bg: #fbfaf7;
|
||||
--pz-surface: #ffffff;
|
||||
--pz-ink: #2a2622;
|
||||
--pz-ink-2: #4f4841;
|
||||
--pz-muted: #6b645d;
|
||||
--pz-faint: #9a948d;
|
||||
--pz-line: #ebe6df;
|
||||
--pz-line-soft: #f2eee8;
|
||||
--pz-a50: #fcf3ef;
|
||||
--pz-a100: #f8e4db;
|
||||
--pz-a200: #f4d6c9;
|
||||
--pz-a300: #edb6a4;
|
||||
--pz-a500: #d2694a;
|
||||
--pz-a700: #93402a;
|
||||
--pz-a800: #7a3421;
|
||||
--pz-danger: #b3261e;
|
||||
--pz-danger-bg: #fbeae8;
|
||||
--pz-success: #2f6e3a;
|
||||
--pz-success-bg: #e6f2e4;
|
||||
--pz-shadow: 0 1px 2px rgba(42, 38, 34, .05), 0 8px 24px rgba(42, 38, 34, .07);
|
||||
--pz-shadow-lg: 0 30px 80px rgba(42, 38, 34, .22), 0 2px 6px rgba(42, 38, 34, .06);
|
||||
--pz-serif: 'Newsreader', Georgia, 'Times New Roman', serif;
|
||||
--pz-sans: 'DM Sans', system-ui, -apple-system, sans-serif;
|
||||
--pz-ease: cubic-bezier(.23, 1, .32, 1);
|
||||
--pz-ease-drawer: cubic-bezier(.32, .72, 0, 1);
|
||||
--pz-r-s: 10px;
|
||||
--pz-r-m: 14px;
|
||||
--pz-r-l: 22px;
|
||||
}
|
||||
|
||||
/* ── Typo ─────────────────────────────────────────────────────── */
|
||||
.pz-eyebrow { font: 500 12px/1.3 var(--pz-sans); letter-spacing: .1em; text-transform: uppercase; color: var(--pz-muted); margin: 0; }
|
||||
.pz-title { font: 400 30px/1.1 var(--pz-serif); letter-spacing: -.01em; color: var(--pz-ink); margin: 0; text-wrap: balance; }
|
||||
.pz-title em { font-style: italic; font-weight: 300; }
|
||||
.pz-title-lg { font: 300 clamp(36px, 5vw, 48px)/1.04 var(--pz-serif); letter-spacing: -.02em; color: var(--pz-ink); margin: 0; text-wrap: balance; }
|
||||
.pz-title-lg em { font-style: italic; }
|
||||
.pz-sub { font: 400 15px/1.55 var(--pz-sans); color: var(--pz-ink-2); margin: 0; }
|
||||
.pz-small { font: 400 13px/1.5 var(--pz-sans); color: var(--pz-muted); margin: 0; }
|
||||
.pz-small a, .pz-sub a { color: var(--pz-a700); text-underline-offset: 3px; }
|
||||
|
||||
/* ── Boutons ──────────────────────────────────────────────────── */
|
||||
.pz-btn {
|
||||
display: inline-flex; align-items: center; justify-content: center; gap: 8px;
|
||||
min-height: 46px; padding: 0 22px; border: 0; border-radius: 999px; cursor: pointer;
|
||||
font: 500 15px/1 var(--pz-sans); text-decoration: none; white-space: nowrap;
|
||||
transition: transform 140ms var(--pz-ease), background-color 180ms ease, color 180ms ease, box-shadow 180ms ease, opacity 180ms ease;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
.pz-btn:active:not(:disabled) { transform: scale(.97); }
|
||||
.pz-btn:disabled { opacity: .45; cursor: not-allowed; }
|
||||
.pz-btn:focus-visible { outline: 2px solid var(--pz-a500); outline-offset: 3px; }
|
||||
.pz-btn--primary { background: var(--pz-ink); color: #fff; }
|
||||
.pz-btn--accent { background: var(--pz-a700); color: #fff; }
|
||||
.pz-btn--secondary { background: var(--pz-surface); color: var(--pz-ink); box-shadow: inset 0 0 0 1px var(--pz-line); }
|
||||
.pz-btn--soft { background: var(--pz-a100); color: var(--pz-a800); }
|
||||
.pz-btn--ghost { background: transparent; color: var(--pz-ink-2); padding: 0 14px; }
|
||||
.pz-btn--danger { background: var(--pz-danger); color: #fff; }
|
||||
.pz-btn--danger-ghost { background: transparent; color: var(--pz-danger); padding: 0 14px; }
|
||||
.pz-btn--sm { min-height: 36px; padding: 0 14px; font-size: 13.5px; }
|
||||
.pz-btn--block { width: 100%; }
|
||||
.pz-btn--icon { width: 40px; min-height: 40px; padding: 0; }
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
.pz-btn--primary:hover:not(:disabled) { background: #3d3731; }
|
||||
.pz-btn--accent:hover:not(:disabled) { background: var(--pz-a800); }
|
||||
.pz-btn--secondary:hover:not(:disabled) { box-shadow: inset 0 0 0 1px var(--pz-a300); background: var(--pz-a50); }
|
||||
.pz-btn--soft:hover:not(:disabled) { background: var(--pz-a200); }
|
||||
.pz-btn--ghost:hover:not(:disabled) { background: var(--pz-line-soft); color: var(--pz-ink); }
|
||||
.pz-btn--danger:hover:not(:disabled) { background: #93201a; }
|
||||
.pz-btn--danger-ghost:hover:not(:disabled) { background: var(--pz-danger-bg); }
|
||||
}
|
||||
.pz-link { background: none; border: 0; padding: 0; cursor: pointer; font: 500 14px var(--pz-sans); color: var(--pz-a700); text-decoration: underline; text-underline-offset: 3px; }
|
||||
|
||||
/* ── Champs ───────────────────────────────────────────────────── */
|
||||
.pz-field { display: grid; gap: 7px; }
|
||||
.pz-field > label, .pz-label { font: 500 13px/1.3 var(--pz-sans); color: var(--pz-ink-2); }
|
||||
.pz-input, .pz-field input:not([type=checkbox]):not([type=radio]), .pz-field textarea, .pz-field select {
|
||||
width: 100%; min-height: 46px; padding: 11px 14px; box-sizing: border-box;
|
||||
font: 400 15px/1.4 var(--pz-sans); color: var(--pz-ink);
|
||||
background: var(--pz-surface); border: 1px solid var(--pz-line); border-radius: var(--pz-r-s);
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease; appearance: none;
|
||||
}
|
||||
.pz-field textarea { min-height: 96px; resize: vertical; }
|
||||
.pz-input::placeholder, .pz-field input::placeholder, .pz-field textarea::placeholder { color: var(--pz-faint); }
|
||||
.pz-input:focus, .pz-field input:focus, .pz-field textarea:focus, .pz-field select:focus {
|
||||
outline: none; border-color: var(--pz-a500); box-shadow: 0 0 0 3px var(--pz-a100);
|
||||
}
|
||||
.pz-hint { font: 400 12.5px/1.45 var(--pz-sans); color: var(--pz-muted); margin: 0; }
|
||||
.pz-error {
|
||||
display: flex; gap: 8px; align-items: flex-start; padding: 11px 14px; border-radius: var(--pz-r-s);
|
||||
background: var(--pz-danger-bg); color: var(--pz-danger); font: 500 13.5px/1.45 var(--pz-sans);
|
||||
}
|
||||
.pz-success {
|
||||
display: flex; gap: 8px; align-items: flex-start; padding: 11px 14px; border-radius: var(--pz-r-s);
|
||||
background: var(--pz-success-bg); color: var(--pz-success); font: 500 13.5px/1.45 var(--pz-sans);
|
||||
}
|
||||
.pz-row2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
||||
@media (max-width: 480px) { .pz-row2 { grid-template-columns: 1fr; } }
|
||||
|
||||
/* ── Cartes, tags, séparateurs ────────────────────────────────── */
|
||||
.pz-card { background: var(--pz-surface); border: 1px solid var(--pz-line); border-radius: var(--pz-r-l); box-shadow: var(--pz-shadow); }
|
||||
.pz-tag {
|
||||
display: inline-flex; align-items: center; gap: 6px; padding: 5px 10px; border-radius: 999px;
|
||||
font: 500 12px/1 var(--pz-sans); color: var(--pz-ink-2); background: var(--pz-bg); border: 1px solid var(--pz-line);
|
||||
}
|
||||
.pz-tag--accent { background: var(--pz-a100); color: var(--pz-a700); border-color: transparent; }
|
||||
.pz-tag--dot::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--pz-a500); }
|
||||
.pz-divider { display: flex; align-items: center; gap: 12px; font: 400 12.5px var(--pz-sans); color: var(--pz-faint); }
|
||||
.pz-divider::before, .pz-divider::after { content: ""; flex: 1; height: 1px; background: var(--pz-line); }
|
||||
|
||||
/* ── Onglets ──────────────────────────────────────────────────── */
|
||||
.pz-tabs { display: flex; gap: 4px; padding: 4px; background: var(--pz-bg); border: 1px solid var(--pz-line); border-radius: 999px; }
|
||||
.pz-tab {
|
||||
flex: 1; min-height: 34px; padding: 0 12px; border: 0; border-radius: 999px; background: transparent; cursor: pointer;
|
||||
font: 500 13.5px var(--pz-sans); color: var(--pz-muted); transition: background-color 160ms ease, color 160ms ease;
|
||||
}
|
||||
.pz-tab[aria-selected="true"], .pz-tab.is-active { background: var(--pz-surface); color: var(--pz-ink); box-shadow: 0 1px 2px rgba(42, 38, 34, .08); }
|
||||
|
||||
/* ── Fenêtres ─────────────────────────────────────────────────── */
|
||||
.pz-overlay {
|
||||
position: fixed; inset: 0; z-index: 2000; display: grid; place-items: center; padding: 16px;
|
||||
background: rgba(42, 38, 34, .32); backdrop-filter: blur(3px); -webkit-backdrop-filter: blur(3px);
|
||||
animation: pz-fade 180ms ease both;
|
||||
}
|
||||
.pz-modal {
|
||||
position: relative; width: min(100%, 520px); max-height: calc(100dvh - 32px); overflow: auto;
|
||||
background: var(--pz-surface); border-radius: var(--pz-r-l); box-shadow: var(--pz-shadow-lg);
|
||||
padding: 28px; display: grid; gap: 20px; align-content: start;
|
||||
animation: pz-rise 220ms var(--pz-ease) both;
|
||||
}
|
||||
.pz-modal--wide { width: min(100%, 680px); }
|
||||
/* Les enfants d'une grille ne débordent pas (textes longs, liens) */
|
||||
.pz-modal > *, .pz-screen-card > * { min-width: 0; }
|
||||
.pz-modal-head { display: grid; gap: 6px; padding-right: 36px; }
|
||||
.pz-modal-close { position: absolute; top: 16px; right: 16px; }
|
||||
.pz-modal-actions { display: flex; justify-content: flex-end; gap: 8px; flex-wrap: wrap; }
|
||||
.pz-modal-actions--split { justify-content: space-between; align-items: center; }
|
||||
@keyframes pz-fade { from { opacity: 0; } to { opacity: 1; } }
|
||||
@keyframes pz-rise { from { opacity: 0; transform: translateY(10px) scale(.985); } to { opacity: 1; transform: none; } }
|
||||
@media (max-width: 560px) {
|
||||
.pz-overlay { place-items: end center; padding: 0; }
|
||||
.pz-modal { width: 100%; max-height: 92dvh; border-radius: 26px 26px 0 0; padding: 24px 20px calc(20px + env(safe-area-inset-bottom)); animation: pz-sheet 380ms var(--pz-ease-drawer) both; }
|
||||
@keyframes pz-sheet { from { transform: translateY(100%); } to { transform: none; } }
|
||||
}
|
||||
|
||||
/* ── Écran plein (accueil, compte, accès) ─────────────────────── */
|
||||
.pz-screen {
|
||||
position: fixed; inset: 0; overflow: auto; background: var(--pz-bg);
|
||||
display: grid; place-items: center; padding: 32px 16px;
|
||||
background-image: radial-gradient(circle at 50% 0%, var(--pz-a50), transparent 60%);
|
||||
}
|
||||
.pz-screen-inner { width: min(100%, 440px); display: grid; gap: 22px; }
|
||||
.pz-screen-logo { justify-self: center; height: 26px; width: auto; display: block; }
|
||||
.pz-screen-card { padding: 30px; display: grid; gap: 20px; }
|
||||
@media (max-width: 480px) { .pz-screen-card { padding: 24px 20px; } }
|
||||
|
||||
/* ── Menus ────────────────────────────────────────────────────── */
|
||||
.pz-menu {
|
||||
min-width: 240px; padding: 6px; background: var(--pz-surface); border: 1px solid var(--pz-line);
|
||||
border-radius: var(--pz-r-m); box-shadow: var(--pz-shadow-lg); display: grid; gap: 2px;
|
||||
animation: pz-rise 160ms var(--pz-ease) both;
|
||||
}
|
||||
.pz-menu-item {
|
||||
display: flex; align-items: center; gap: 10px; width: 100%; min-height: 40px; padding: 8px 12px;
|
||||
border: 0; border-radius: 10px; background: transparent; cursor: pointer; text-align: left;
|
||||
font: 500 14px/1.3 var(--pz-sans); color: var(--pz-ink);
|
||||
}
|
||||
.pz-menu-item svg { color: var(--pz-muted); flex: none; }
|
||||
.pz-menu-item.is-active { background: var(--pz-a50); color: var(--pz-a700); }
|
||||
.pz-menu-item--danger { color: var(--pz-danger); }
|
||||
.pz-menu-item--danger svg { color: var(--pz-danger); }
|
||||
@media (hover: hover) and (pointer: fine) { .pz-menu-item:hover { background: var(--pz-line-soft); } .pz-menu-item.is-active:hover { background: var(--pz-a100); } }
|
||||
.pz-menu-sep { height: 1px; margin: 4px 6px; background: var(--pz-line); }
|
||||
.pz-menu-label { padding: 8px 12px 4px; font: 500 11.5px var(--pz-sans); letter-spacing: .08em; text-transform: uppercase; color: var(--pz-faint); }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.pz-overlay, .pz-modal, .pz-menu { animation: pz-fade 160ms ease both !important; }
|
||||
.pz-btn:active:not(:disabled) { transform: none; }
|
||||
}
|
||||
@@ -8,17 +8,17 @@ export default {
|
||||
extend: {
|
||||
colors: {
|
||||
cosmos: {
|
||||
black: '#000000',
|
||||
cream: '#FEF9ED',
|
||||
brown: '#5D524B',
|
||||
gold: '#A67C52',
|
||||
surface: '#F7ECD9',
|
||||
yellow: '#F6E5B8',
|
||||
black: '#2A2622',
|
||||
cream: '#FFFFFF',
|
||||
brown: '#2A2622',
|
||||
gold: '#93402A',
|
||||
surface: '#F2EFEA',
|
||||
yellow: '#F8E4DB',
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
'bradford': ['Bradford LL', 'Iowan Old Style', 'Palatino Linotype', 'Times New Roman', 'serif'],
|
||||
'mono': ['Red Hat Mono', 'ui-monospace', 'monospace'],
|
||||
'bradford': ['Newsreader', 'Georgia', 'Times New Roman', 'serif'],
|
||||
'mono': ['DM Sans', 'system-ui', 'sans-serif'],
|
||||
'segoe': ['Segoe UI', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
spacing: {
|
||||
@@ -37,9 +37,9 @@ export default {
|
||||
'pill': '50px',
|
||||
},
|
||||
boxShadow: {
|
||||
'diffuse': '0 2px 24px rgba(93,82,75,0.12)',
|
||||
'soft': '0 3px 12px rgba(0,0,0,0.10)',
|
||||
'glow': '0 0 12px rgba(166,124,82,0.20)',
|
||||
'diffuse': '0 2px 24px rgba(42,38,34,0.12)',
|
||||
'soft': '0 3px 12px rgba(42,38,34,0.10)',
|
||||
'glow': '0 0 12px rgba(147,64,42,0.20)',
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="theme-color" content="#FDF8EC" />
|
||||
<meta name="theme-color" content="#FBFAF7" />
|
||||
|
||||
<title>Parenthèse : l'arbre généalogique vivant de votre famille</title>
|
||||
<meta name="description" content="Créez l'arbre généalogique de votre famille en ligne, avec photos, vidéos et voix. Partagez-le en un lien : la famille consulte sans compte et contribue avec un mot de passe." />
|
||||
@@ -30,10 +30,10 @@
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||
|
||||
<!-- Preconnect for Google Fonts (Red Hat Mono) -->
|
||||
<!-- Google Fonts : Newsreader et DM Sans (DA Lavande) -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Red+Hat+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,300;0,6..72,400;1,6..72,300&family=DM+Sans:opsz,wght@9..40,400;9..40,500&display=swap" rel="stylesheet" />
|
||||
|
||||
<script type="application/ld+json">
|
||||
{
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 6.2 KiB |
@@ -1,4 +1 @@
|
||||
<svg width="400" height="400" viewBox="0 0 400 400" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="400" height="400" rx="59" fill="#F7F1E6"/>
|
||||
<path d="M174.887 159.122C169.851 167.606 163.424 170.911 155.806 170.911C151.516 170.911 146.062 170.294 139.361 168.956L139.214 168.926L139.066 168.904L136.494 168.511C130.481 167.577 124.376 166.512 118.179 165.315C111.028 163.933 103.992 162.551 97.0728 161.169L96.9257 161.139L96.7773 161.117C89.662 160.051 83.4359 159.49 78.1617 159.49C69.1859 159.49 61.0961 161.278 54.0787 164.947C47.0981 168.596 41.4507 173.962 37.0058 180.803L36.9938 180.822L36.9817 180.84C28.1973 194.726 24 213.38 24 236.509V272H51.2117V237.548C51.2117 222.972 53.9416 212.085 58.96 204.553C61.3391 201.15 64.3726 198.736 68.0205 197.145C71.7017 195.54 76.2888 194.653 81.9743 194.653C86.8306 194.653 92.1966 195.123 98.0908 196.096C104.072 197.473 110.278 198.85 116.708 200.225L116.728 200.23L116.75 200.234C123.508 201.63 130.268 202.852 137.031 203.902C144.293 205.343 151.478 206.074 158.579 206.074C164.902 206.074 170.875 204.335 176.414 201.175C181.975 198.003 186.682 193.552 190.634 188.003C194.375 182.751 197.378 176.456 199.706 169.182C202.04 176.425 205.273 182.755 209.461 188.131C218.173 199.364 228.964 206.074 242.115 206.074C248.993 206.074 255.953 205.342 262.986 203.9C269.977 202.849 276.746 201.626 283.292 200.225C289.729 198.848 295.941 197.47 301.929 196.091C307.578 195.123 312.823 194.653 317.68 194.653C323.365 194.653 327.952 195.54 331.633 197.145C335.285 198.738 338.321 201.156 340.701 204.565C345.714 212.097 348.441 222.98 348.441 237.548V272H376V236.509C376 214.043 371.791 195.592 363.04 181.394L363.018 181.359L362.599 180.706C358.225 174.002 352.689 168.723 345.867 165.094C338.785 161.328 330.596 159.49 321.492 159.49C316.218 159.49 309.992 160.051 302.877 161.117L302.708 161.142L302.541 161.177C295.867 162.555 288.96 163.935 281.821 165.315C274.739 166.683 267.776 167.88 260.934 168.904L260.786 168.926L260.639 168.956C253.938 170.294 248.485 170.911 244.194 170.911C236.556 170.911 230.009 167.586 224.748 159.09C219.607 150.411 216.658 138.373 216.205 122.739L216.096 119H183.904L183.796 122.739C183.341 138.378 180.276 150.405 174.92 159.068L174.903 159.095L174.887 159.122Z" fill="#9A734C"/>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-920.7 -1664.7 1841.3 1841.3"><g transform="scale(1,-1)"><path d="M-373.06825730486446 -21.7549454949945 -404.3987845573672 -69.67222246941051Q-415.45661770530944 -84.41600000000005 -426.51445085325156 -84.41600000000005Q-433.88633961854634 -84.41600000000005 -443.10120057516474 -78.88708342602897Q-590.5389758810602 -1.4822513904338734 -699.2743351691581 129.36877419354835Q-808.009694457256 260.21979977753057 -867.9062906752761 420.5583804226919Q-927.8028868932961 580.8969610678532 -927.8028868932961 743.0785139043383Q-927.8028868932961 905.2600667408232 -868.8277767709379 1065.5986473859843Q-809.8526666485798 1225.9372280311457 -701.1173073604818 1358.6312258064515Q-592.3819480723839 1491.3252235817574 -444.94417276648846 1568.7300556173527Q-437.5722840011937 1572.416 -430.2003952358989 1572.416Q-417.29958989663305 1572.416 -408.08472894001466 1557.6722224694104L-373.06825730486446 1507.9119733036707Q-367.5393407308934 1500.540084538376 -367.5393407308934 1491.3252235817574Q-367.5393407308934 1480.2673904338153 -380.44014607015924 1469.2095572858732Q-441.25822838384113 1421.2922803114573 -496.5473941235519 1341.1229899888767Q-551.8365598632627 1260.953699666296 -596.0678924550314 1161.4332013348167Q-640.2992250468 1061.9127030033371 -666.1008357253318 954.0988298109011Q-691.9024464038635 846.284956618465 -691.9024464038635 743.0785139043383Q-691.9024464038635 638.0290989988877 -666.1008357253318 530.2152258064516Q-640.2992250468 422.40135261401554 -596.0678924550314 322.8808542825361Q-551.8365598632627 223.3603559510567 -496.5473941235519 143.19106562847605Q-441.25822838384113 63.0217753058954 -380.44014607015924 15.104498331479377Q-367.5393407308934 4.046665183537215 -367.5393407308934 -7.011167964404933Q-367.5393407308934 -12.540084538376021 -373.06825730486446 -21.7549454949945Z" fill="#2a2622" stroke="#2a2622" stroke-width="48.1" stroke-linejoin="round"/><path d="M373.06825730486446 -21.7549454949945Q367.5393407308934 -12.540084538376021 367.5393407308934 -7.011167964404933Q367.5393407308934 4.046665183537215 380.44014607015924 15.104498331479377Q441.25822838384113 63.0217753058954 496.5473941235519 143.19106562847605Q551.8365598632627 223.3603559510567 596.0678924550314 322.8808542825361Q640.2992250468 422.40135261401554 666.1008357253318 530.2152258064516Q691.9024464038635 638.0290989988877 691.9024464038635 743.0785139043383Q691.9024464038635 846.284956618465 666.1008357253318 954.0988298109011Q640.2992250468 1061.9127030033371 596.0678924550314 1161.4332013348167Q551.8365598632627 1260.953699666296 496.5473941235519 1341.1229899888767Q441.25822838384113 1421.2922803114573 380.44014607015924 1469.2095572858732Q367.5393407308934 1480.2673904338153 367.5393407308934 1491.3252235817574Q367.5393407308934 1500.540084538376 373.06825730486446 1507.9119733036707L408.08472894001466 1557.6722224694104Q417.2995898966331 1572.416 430.20039523589895 1572.416Q437.57228400119374 1572.416 444.9441727664885 1568.7300556173527Q594.2249202637076 1491.3252235817574 702.0387934561437 1358.6312258064515Q809.8526666485798 1225.9372280311457 868.8277767709379 1065.5986473859843Q927.8028868932961 905.2600667408232 927.8028868932961 743.0785139043383Q927.8028868932961 580.8969610678532 867.9062906752761 420.5583804226919Q808.009694457256 260.21979977753057 699.2743351691581 129.36877419354835Q590.5389758810603 -1.4822513904338734 443.10120057516485 -78.88708342602897Q435.72931180987007 -84.41600000000005 426.51445085325156 -84.41600000000005Q415.45661770530944 -84.41600000000005 404.39878455736726 -69.67222246941051Z" fill="#2a2622" stroke="#2a2622" stroke-width="48.1" stroke-linejoin="round"/><circle cx="0.0" cy="744.0" r="424.3" fill="#93402a"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 18 KiB |