mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-06 23:32:08 +00:00
Merge pull request #92 from IliesAllali/fix/fiche-entete-partage
Fiche, partage, landing et bannière de création
This commit is contained in:
11 files changed
+167
-95
No files matched your search
+15
-18
@@ -544,31 +544,28 @@
|
||||
left: 20px;
|
||||
z-index: 1200;
|
||||
max-width: 340px;
|
||||
padding: 12px 14px;
|
||||
padding: 18px 20px 20px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid rgba(42, 38, 34, 0.14);
|
||||
background: rgba(242, 239, 234, 0.95);
|
||||
box-shadow: 0 6px 24px rgba(52, 48, 44, 0.18);
|
||||
color: #2a2622;
|
||||
font-size: 13px;
|
||||
line-height: 1.45;
|
||||
gap: 10px;
|
||||
border-radius: var(--pz-r-l);
|
||||
border: 1px solid var(--pz-line);
|
||||
background: var(--pz-surface);
|
||||
box-shadow: var(--pz-shadow);
|
||||
color: var(--pz-ink-2);
|
||||
font: 400 14px/1.5 var(--pz-sans);
|
||||
}
|
||||
|
||||
.tree-created-prompt strong {
|
||||
color: #2A2622;
|
||||
font-size: 14px;
|
||||
color: var(--pz-ink);
|
||||
font: 400 21px/1.15 var(--pz-serif);
|
||||
letter-spacing: -.01em;
|
||||
}
|
||||
|
||||
.tree-created-prompt button {
|
||||
.tree-created-prompt .pz-btn {
|
||||
justify-self: start;
|
||||
border: 1px solid #efc7b8;
|
||||
border-radius: 8px;
|
||||
background: #ffffff;
|
||||
color: #2a2622;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
min-height: 38px;
|
||||
padding: 0 18px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.settings-card {
|
||||
|
||||
@@ -2898,7 +2898,7 @@ function App() {
|
||||
<div className="tree-created-prompt" role="status">
|
||||
<strong>{t('Votre arbre est créé.')}</strong>
|
||||
{t("Ajoutez un proche pour commencer, comme votre mère, votre père ou un frère. Il suffit d'un prénom.")}
|
||||
<button type="button" onClick={() => setTreeWelcomeVisible(false)} aria-label={t('Fermer')}>
|
||||
<button type="button" className="pz-btn pz-btn--secondary" onClick={() => setTreeWelcomeVisible(false)} aria-label={t('Fermer')}>
|
||||
{t('Fermer')}
|
||||
</button>
|
||||
</div>
|
||||
@@ -2908,7 +2908,7 @@ function App() {
|
||||
<strong>{t("Votre arbre est créé, mais l'import n'a pas abouti.")}</strong>
|
||||
{` ${treeImportError} `}
|
||||
{t("Vous pouvez réessayer depuis les paramètres de l'arbre.")}
|
||||
<button type="button" onClick={() => setTreeImportError('')} aria-label={t('Fermer')}>
|
||||
<button type="button" className="pz-btn pz-btn--secondary" onClick={() => setTreeImportError('')} aria-label={t('Fermer')}>
|
||||
{t('Fermer')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -238,6 +238,8 @@ const ContributionPanel = ({
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
const [inviteLocalError, setInviteLocalError] = useState('')
|
||||
const [copyFeedback, setCopyFeedback] = useState('')
|
||||
// Le message d'invitation reste replié : une ligne, le texte ne s'ouvre que sur demande
|
||||
const [showInviteMessage, setShowInviteMessage] = useState(false)
|
||||
|
||||
// Load persisted decisions from localStorage
|
||||
useEffect(() => {
|
||||
@@ -366,7 +368,6 @@ const ContributionPanel = ({
|
||||
aria-labelledby="contribPanelTitle"
|
||||
>
|
||||
<div className="pz-modal-head">
|
||||
<p className="pz-eyebrow">{t('Partager et relire')}</p>
|
||||
<h1 id="contribPanelTitle" className="pz-title">{familyTitleBefore}<em>{t('participe')}</em>{familyTitleAfter}</h1>
|
||||
</div>
|
||||
<button type="button" className="pz-btn pz-btn--ghost pz-btn--icon pz-modal-close" onClick={onClose} aria-label={t('Fermer')}>
|
||||
@@ -412,11 +413,18 @@ const ContributionPanel = ({
|
||||
</div>
|
||||
{defaultInviteText && (
|
||||
<div className="contrib-invite-message">
|
||||
<label className="contrib-invite-title" htmlFor="contrib-invite-text">{t('Le message à envoyer')}</label>
|
||||
<textarea id="contrib-invite-text" className="contrib-invite-textarea" rows={9} value={inviteText} onChange={(e) => setInviteText(e.target.value)} />
|
||||
<div className="contrib-invite-message-actions">
|
||||
<button type="button" className="pz-btn pz-btn--primary pz-btn--sm" onClick={() => handleCopy(inviteText, t('Message copié'))}><Copy size={14} strokeWidth={2} />{t('Copier le message')}</button>
|
||||
<div className="contrib-invite-message-row">
|
||||
<button type="button" className="contrib-pill-btn contrib-pill-btn--edit" onClick={() => handleCopy(inviteText, t('Message copié'))}><Copy size={14} strokeWidth={2} />{t("Copier le message d'invitation")}</button>
|
||||
<button type="button" className="contrib-link-btn" aria-expanded={showInviteMessage} aria-controls="contrib-invite-text" onClick={() => setShowInviteMessage((v) => !v)}>
|
||||
{showInviteMessage ? t('Masquer le message') : t('Voir ou modifier le message')}
|
||||
</button>
|
||||
</div>
|
||||
{showInviteMessage && (
|
||||
<>
|
||||
<label className="contrib-invite-message-label" htmlFor="contrib-invite-text">{t('Le message à envoyer')}</label>
|
||||
<textarea id="contrib-invite-text" className="contrib-invite-textarea" rows={6} value={inviteText} onChange={(e) => setInviteText(e.target.value)} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{(inviteLocalError || inviteError) && <div className="contrib-error">{inviteLocalError || inviteError}</div>}
|
||||
|
||||
@@ -35,8 +35,8 @@
|
||||
.pc-hidden-input { display: none; }
|
||||
|
||||
/* ── En-tête ── */
|
||||
.pc-head { display: flex; gap: 16px; align-items: flex-start; padding-right: 36px; }
|
||||
.pc-photo-wrap { display: grid; gap: 8px; justify-items: center; flex: none; }
|
||||
.pc-head { --pc-name-h: 42px; --pc-name-gap: 6px; display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 12px 16px; align-items: start; padding-right: 36px; }
|
||||
.pc-photo-wrap { position: relative; display: grid; gap: 8px; justify-items: center; flex: none; }
|
||||
.pc-photo {
|
||||
width: 84px; height: 84px; border-radius: 24%; overflow: hidden;
|
||||
background: var(--pz-a100); box-shadow: 0 0 0 1px rgba(42, 38, 34, .06), var(--pz-shadow);
|
||||
@@ -54,22 +54,42 @@
|
||||
.pc-photo--add:active:not(:disabled) { transform: scale(.97); }
|
||||
.pc-photo--add:focus-visible { outline: 2px solid var(--pz-a500); outline-offset: 3px; }
|
||||
.pc-photo--add:disabled { opacity: .6; cursor: default; }
|
||||
.pc-photo-actions { display: grid; gap: 4px; justify-items: center; }
|
||||
/* Menu du choix de source du portrait : au-dessus de la fiche (z-index 1000), sous la modale Immich (2200) */
|
||||
.pc-source-backdrop { position: fixed; inset: 0; z-index: 1100; }
|
||||
.pc-source-menu { position: fixed; z-index: 1150; min-width: 220px; }
|
||||
/* Mode modification : le portrait prend la hauteur des deux champs Prénom et Nom (écart compris) et reste carré */
|
||||
.person-card--edit-mode .pc-id { padding-top: 0; }
|
||||
.person-card--edit-mode .pc-photo-wrap { display: block; }
|
||||
/* Taille fixée par les champs, pas par le contenu : une vraie photo a une taille propre qui faisait grandir le portrait */
|
||||
.person-card--edit-mode .pc-photo-wrap > .pc-photo { --pc-photo-edit: calc(var(--pc-name-h) * 2 + var(--pc-name-gap)); width: var(--pc-photo-edit); height: var(--pc-photo-edit); }
|
||||
|
||||
.pc-photo-badge {
|
||||
position: absolute; right: -6px; bottom: -6px; z-index: 1;
|
||||
width: 30px; height: 30px; display: grid; place-items: center; padding: 0;
|
||||
border: 0; border-radius: 50%; background: #fff; color: var(--pz-ink-2); cursor: pointer;
|
||||
box-shadow: 0 0 0 1px rgba(42, 38, 34, .06), var(--pz-shadow);
|
||||
transition: transform 140ms var(--pz-ease), background-color 160ms ease;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
@media (hover: hover) and (pointer: fine) { .pc-photo-badge:hover:not(:disabled) { background: var(--pz-a50); color: var(--pz-a700); } }
|
||||
.pc-photo-badge:active:not(:disabled) { transform: scale(.94); }
|
||||
.pc-photo-badge:focus-visible { outline: 2px solid var(--pz-a500); outline-offset: 2px; }
|
||||
.pc-photo-badge:disabled { opacity: .5; cursor: default; }
|
||||
.pc-photo-btn {
|
||||
border: 0; background: none; padding: 2px 4px; cursor: pointer;
|
||||
font: 500 12px var(--pz-sans); color: var(--pz-a700); text-decoration: underline; text-underline-offset: 2px;
|
||||
}
|
||||
.pc-photo-btn--danger { color: var(--pz-danger); }
|
||||
.pc-photo-btn--danger { color: var(--pz-danger); justify-self: center; }
|
||||
.pc-photo-btn:disabled { opacity: .5; cursor: default; }
|
||||
|
||||
.pc-id { display: grid; gap: 6px; min-width: 0; padding-top: 4px; }
|
||||
.pc-name { margin: 0; font: 400 28px/1.08 var(--pz-serif); letter-spacing: -.01em; color: var(--pz-ink); text-wrap: balance; }
|
||||
.pc-name em { font-style: italic; font-weight: 300; }
|
||||
.pc-nick { margin: 0; font: italic 300 17px/1.3 var(--pz-serif); color: var(--pz-muted); }
|
||||
.pc-life { margin: 0; font: 400 13.5px/1.4 var(--pz-sans); color: var(--pz-muted); }
|
||||
.pc-meta { margin: 0; font: 400 13.5px/1.5 var(--pz-sans); color: var(--pz-muted); text-wrap: pretty; }
|
||||
.pc-meta-item { display: inline; }
|
||||
.pc-meta-item--nowrap { white-space: nowrap; }
|
||||
.pc-nick { font: italic 300 15.5px/1 var(--pz-serif); color: var(--pz-ink-2); }
|
||||
.pc-meta-sep { margin: 0 .45em; color: var(--pz-faint); }
|
||||
.pc-chips { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
|
||||
.pc-chips:empty { display: none; }
|
||||
/* Place dans la fratrie (mode édition) : ‹ 2 sur 3 › dans une pastille */
|
||||
@@ -88,7 +108,7 @@
|
||||
.pc-order-btn:hover:not(:disabled) { background: var(--pz-line-soft); color: var(--pz-ink); }
|
||||
}
|
||||
@media (pointer: coarse) { .pc-order-btn { width: 32px; height: 32px; } }
|
||||
.pc-name-edit { display: grid; gap: 6px; }
|
||||
.pc-name-edit { display: grid; gap: var(--pc-name-gap); }
|
||||
|
||||
/* ── Champs d'édition ── */
|
||||
.pc-input {
|
||||
@@ -99,7 +119,7 @@
|
||||
}
|
||||
.pc-input:focus { outline: none; border-color: var(--pz-a500); box-shadow: 0 0 0 3px var(--pz-a100); }
|
||||
.pc-input::placeholder { color: var(--pz-faint); }
|
||||
.pc-input--name { font: 400 20px/1.2 var(--pz-serif); min-height: 42px; }
|
||||
.pc-input--name { font: 400 20px/1.2 var(--pz-serif); min-height: var(--pc-name-h); }
|
||||
.pc-input--last { font-style: italic; }
|
||||
.pc-note-input { min-height: 88px; resize: vertical; }
|
||||
|
||||
@@ -253,7 +273,7 @@
|
||||
|
||||
/* Prénom inconnu (issue Anaïs, 18/10) : libellé en italique, case à cocher sous les champs de nom */
|
||||
.pc-name-unknown { font-style: italic; font-weight: 300; }
|
||||
.pc-unknown-first { display: inline-flex; align-items: center; gap: 7px; margin-top: 6px; font: 400 13px var(--pz-sans); color: var(--pz-ink-2); cursor: pointer; }
|
||||
.pc-unknown-first { grid-column: 1 / -1; display: flex; align-items: center; gap: 8px; margin: -4px 0 0; font: 400 12.5px var(--pz-sans); color: var(--pz-ink-2); cursor: pointer; white-space: nowrap; }
|
||||
.pc-unknown-first input { width: 15px; height: 15px; accent-color: var(--pz-a500); }
|
||||
.pc-name-error { margin: 6px 0 0; font: 500 12.5px var(--pz-sans); color: var(--pz-danger); }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect, useRef, forwardRef } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { Save, Trash2, Plus, X, Search, Check, Play, ImagePlus, Sparkles, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { Save, Trash2, Plus, X, Search, Check, Play, ImagePlus, Camera, Sparkles, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { getParents, getChildren, getPersonUnions, getPersonMedias, getDemoLife, filiations, persons, unions } from '../data/mockData'
|
||||
import { fetchPersonLife } from '../api/treeApi.js'
|
||||
import LifeTimeline from './LifeTimeline.jsx'
|
||||
@@ -508,6 +508,12 @@ const PersonCard = ({
|
||||
return parts.join(' – ')
|
||||
})()
|
||||
const ageLabel = age === null ? null : person.isAlive ? t('{age} ans', { age }) : t('{age} ans au décès', { age })
|
||||
// Une seule ligne sous le nom : surnom, repères de vie, âge, séparés par un point médian
|
||||
const metaItems = [
|
||||
person.nickname && { key: 'nick', node: <em className="pc-nick">{person.nickname}</em>, nowrap: true },
|
||||
lifeLine && { key: 'life', node: lifeLine },
|
||||
ageLabel && { key: 'age', node: ageLabel, nowrap: true },
|
||||
].filter(Boolean)
|
||||
|
||||
const renderRelationRow = (target, relationType, meta) => (
|
||||
<div key={`${relationType}-${target?.id}`} className="pc-rel-row">
|
||||
@@ -574,9 +580,10 @@ const PersonCard = ({
|
||||
className="pc-photo pc-photo--add"
|
||||
onClick={() => avatarInputRef.current?.click()}
|
||||
disabled={avatarActionLoading}
|
||||
aria-label={t('Ajouter une photo')}
|
||||
title={t('Ajouter une photo')}
|
||||
>
|
||||
<ImagePlus size={22} strokeWidth={1.8} aria-hidden="true" />
|
||||
<span>{avatarActionLoading ? t('Envoi') : t('Ajouter une photo')}</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="pc-photo">
|
||||
@@ -586,29 +593,29 @@ const PersonCard = ({
|
||||
</div>
|
||||
)}
|
||||
{editMode && onAvatarUpload && (
|
||||
<div className="pc-photo-actions">
|
||||
<button
|
||||
ref={sourceBtnRef}
|
||||
type="button"
|
||||
className="pc-photo-btn"
|
||||
onClick={openAvatarPicker}
|
||||
disabled={avatarActionLoading}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={showSourceMenu}
|
||||
>
|
||||
{person.photo ? t('Changer') : t('Ajouter une photo')}
|
||||
</button>
|
||||
{person.photo && (
|
||||
<button
|
||||
type="button"
|
||||
className="pc-photo-btn pc-photo-btn--danger"
|
||||
onClick={() => onAvatarDelete?.(person.id, person.avatarFromImmich)}
|
||||
disabled={avatarActionLoading}
|
||||
>
|
||||
{t('Retirer')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
ref={sourceBtnRef}
|
||||
type="button"
|
||||
className="pc-photo-badge"
|
||||
onClick={openAvatarPicker}
|
||||
disabled={avatarActionLoading}
|
||||
aria-label={t('Changer la photo')}
|
||||
title={t('Changer la photo')}
|
||||
aria-haspopup="menu"
|
||||
aria-expanded={showSourceMenu}
|
||||
>
|
||||
<Camera size={14} strokeWidth={2} aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
{editMode && onAvatarUpload && person.photo && (
|
||||
<button
|
||||
type="button"
|
||||
className="pc-photo-btn pc-photo-btn--danger"
|
||||
onClick={() => onAvatarDelete?.(person.id, person.avatarFromImmich)}
|
||||
disabled={avatarActionLoading}
|
||||
>
|
||||
{t('Retirer')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -639,22 +646,6 @@ const PersonCard = ({
|
||||
placeholder={t('Nom')}
|
||||
aria-label={t('Nom')}
|
||||
/>
|
||||
{/* Visible tant que le prénom est vide, et tant qu'elle est cochée (pour pouvoir la décocher) */}
|
||||
{(unknownFirstName || !String(getEditValue('firstName', person.firstName) ?? '').trim()) && (
|
||||
<label className="pc-unknown-first">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={unknownFirstName}
|
||||
onChange={(e) => {
|
||||
setUnknownFirstName(e.target.checked)
|
||||
setFirstNameMissing(false)
|
||||
setLastNameMissing(false)
|
||||
handleFieldChange('firstName', '')
|
||||
}}
|
||||
/>
|
||||
{t('Je ne connais pas son prénom')}
|
||||
</label>
|
||||
)}
|
||||
{firstNameMissing && <p className="pc-name-error">{t('Il faut au moins un prénom.')}</p>}
|
||||
{lastNameMissing && <p className="pc-name-error">{t('Il faut au moins un nom si le prénom est inconnu.')}</p>}
|
||||
</div>
|
||||
@@ -664,10 +655,17 @@ const PersonCard = ({
|
||||
{person.lastName && <>{' '}<em>{person.lastName}</em></>}
|
||||
</h2>
|
||||
)}
|
||||
{!editMode && person.nickname && <p className="pc-nick">{person.nickname}</p>}
|
||||
{!editMode && lifeLine && <p className="pc-life">{lifeLine}</p>}
|
||||
{!editMode && metaItems.length > 0 && (
|
||||
<p className="pc-meta">
|
||||
{metaItems.map((item, i) => (
|
||||
<span key={item.key} className={`pc-meta-item${item.nowrap ? ' pc-meta-item--nowrap' : ''}`}>
|
||||
{item.node}
|
||||
{i < metaItems.length - 1 && <span className="pc-meta-sep" aria-hidden="true">·</span>}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
)}
|
||||
<div className="pc-chips">
|
||||
{editMode && <span className="pz-tag pz-tag--accent pz-tag--dot">{t('Édition')}</span>}
|
||||
{editMode && siblingPlace && (
|
||||
<span className="pz-tag pc-order" role="group" aria-label={t('Place {n} sur {total} parmi les frères et sœurs', { n: siblingPlace.index + 1, total: siblingPlace.count })}>
|
||||
<button type="button" className="pc-order-btn" onClick={() => onMoveSibling?.(-1)} disabled={siblingPlace.index === 0} aria-label={t('Déplacer à gauche')} title={t('Déplacer à gauche')}>
|
||||
@@ -679,10 +677,24 @@ const PersonCard = ({
|
||||
</button>
|
||||
</span>
|
||||
)}
|
||||
{!editMode && ageLabel && <span className="pz-tag">{ageLabel}</span>}
|
||||
{!editMode && person.profession && <span className="pz-tag">{person.profession}</span>}
|
||||
</div>
|
||||
</div>
|
||||
{editMode && ((unknownFirstName || !String(getEditValue('firstName', person.firstName) ?? '').trim())) && (
|
||||
<label className="pc-unknown-first">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={unknownFirstName}
|
||||
onChange={(e) => {
|
||||
setUnknownFirstName(e.target.checked)
|
||||
setFirstNameMissing(false)
|
||||
setLastNameMissing(false)
|
||||
handleFieldChange('firstName', '')
|
||||
}}
|
||||
/>
|
||||
{t('Je ne connais pas son prénom')}
|
||||
</label>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{avatarActionError && <div className="pz-error pc-inline-msg">{avatarActionError}</div>}
|
||||
|
||||
@@ -67,9 +67,12 @@
|
||||
.contrib-invite-passwords { display: grid; gap: 6px; }
|
||||
.contrib-pass-hint { margin: 2px 4px 0; font: 400 12.5px/1.45 var(--pz-sans); color: var(--pz-muted); }
|
||||
.contrib-invite-message { display: grid; gap: 8px; }
|
||||
.contrib-invite-message-row { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 6px 12px; }
|
||||
.contrib-invite-message-label { font: 500 12.5px var(--pz-sans); color: var(--pz-muted); }
|
||||
.contrib-link-btn { border: 0; padding: 4px 2px; background: none; cursor: pointer; font: 500 12.5px var(--pz-sans); color: var(--pz-a700); text-decoration: underline; text-underline-offset: 2px; }
|
||||
.contrib-link-btn:focus-visible { outline: 2px solid var(--pz-a500); outline-offset: 2px; border-radius: 4px; }
|
||||
.contrib-invite-textarea { width: 100%; box-sizing: border-box; padding: 12px 14px; background: var(--pz-surface); border: 1px solid var(--pz-line); border-radius: var(--pz-r-m); font: 400 13.5px/1.5 var(--pz-sans); color: var(--pz-ink-2); resize: vertical; outline: none; field-sizing: content; min-height: 9.5lh; }
|
||||
.contrib-invite-textarea:focus { border-color: var(--pz-a500); box-shadow: 0 0 0 3px var(--pz-a100); }
|
||||
.contrib-invite-message-actions { display: flex; justify-content: flex-end; }
|
||||
.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); }
|
||||
|
||||
@@ -52,7 +52,6 @@ export default {
|
||||
// ─── Panneau de relecture ───
|
||||
'Copie indisponible': 'Copy unavailable',
|
||||
'Minimum 8 caractères.': 'At least 8 characters.',
|
||||
'Partager et relire': 'Share and review',
|
||||
'La famille {em}': 'The family {em}',
|
||||
'participe': 'joins in',
|
||||
'Fermer': 'Close',
|
||||
@@ -82,7 +81,9 @@ export default {
|
||||
'Changing the password means everyone who has already opened the tree will need to enter the new one.',
|
||||
'Le message à envoyer': 'The message to send',
|
||||
'Message copié': 'Message copied',
|
||||
'Copier le message': 'Copy message',
|
||||
"Copier le message d'invitation": 'Copy invitation message',
|
||||
'Voir ou modifier le message': 'View or edit the message',
|
||||
'Masquer le message': 'Hide the message',
|
||||
'Propositions à relire': 'Suggestions to review',
|
||||
'Acceptez ou refusez chaque modification, puis appliquez vos choix.': 'Accept or decline each change, then apply your choices.',
|
||||
'Actualiser': 'Refresh',
|
||||
|
||||
@@ -19,6 +19,7 @@ export default {
|
||||
'Fermer la fiche': 'Close profile',
|
||||
'Changer': 'Change',
|
||||
'Ajouter une photo': 'Add a photo',
|
||||
'Changer la photo': 'Change photo',
|
||||
'Retirer': 'Remove',
|
||||
'Prénom': 'First name',
|
||||
'Nom': 'Last name',
|
||||
|
||||
+16
-3
@@ -773,14 +773,27 @@ function Header({ t, onCta, loginHref, elevated }) {
|
||||
return (
|
||||
<header className={`site-nav${elevated ? ' is-elevated' : ''}`}>
|
||||
<div className="wrap nav-in">
|
||||
<a href={t.home} aria-label={t.logoHome}><LogoLockup height={22} /></a>
|
||||
<a href={t.home} aria-label={t.logoHome} className="site-logo">
|
||||
{/* Sur téléphone, le symbole seul : le nom « Parenthèse » reste dans le nom accessible du lien */}
|
||||
<LogoLockup height={22} className="lg lk lk-full" decorative />
|
||||
<LogoLockup height={24} mark className="lg lk lk-mark" decorative />
|
||||
</a>
|
||||
<nav aria-label="Sections">
|
||||
<a href="#comment">{t.nav.how}</a>
|
||||
<a href="#demo">{t.nav.demo}</a>
|
||||
{t.nav.privacy && <a href={t.privacyHref}>{t.nav.privacy}</a>}
|
||||
<a href={loginHref}>{t.nav.login}</a>
|
||||
</nav>
|
||||
<button type="button" onClick={onCta} className="btn p sm">{t.cta}</button>
|
||||
<div className="nav-cta">
|
||||
<a href={loginHref} className="btn s sm nav-login" aria-label={t.nav.login}>
|
||||
{/* Silhouette de profil, icône « user » de Lucide (ISC), tracée à la main : la landing n'embarque pas la bibliothèque */}
|
||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
||||
<circle cx="12" cy="7" r="4" />
|
||||
<path d="M4 21v-1a6 6 0 0 1 6-6h4a6 6 0 0 1 6 6v1" />
|
||||
</svg>
|
||||
<span className="nav-login-text">{t.nav.login}</span>
|
||||
</a>
|
||||
<button type="button" onClick={onCta} className="btn p sm">{t.cta}</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
||||
+14
-10
@@ -4,25 +4,29 @@ import { LOGO_NOM } from './logoNomPaths.js'
|
||||
// Logo de l'en-tête en SVG en ligne, pour animer son arrivée : les parenthèses s'ouvrent,
|
||||
// le rond se pose, le nom suit. Une fois par visite, décidé dans index.html (classe
|
||||
// logo-intro sur <html>), animation dans index.css. Même dessin que /logo.svg, rapport 7,541.
|
||||
export default function LogoLockup({ height = 22 }) {
|
||||
// Symbole seul (parenthèses et rond, sans le nom) : emprise mesurée sur le dessin, pour le téléphone
|
||||
export default function LogoLockup({ height = 22, mark = false, decorative = false, className = 'lg lk' }) {
|
||||
const viewBox = mark ? '-940 -1585 1880 1680' : '-951.9 -1596.5 12857.7 1704.9'
|
||||
const width = mark ? Math.round(height * 1880 / 1680) : Math.round(height * 7.541)
|
||||
return (
|
||||
<svg
|
||||
className="lg lk"
|
||||
viewBox="-951.9 -1596.5 12857.7 1704.9"
|
||||
width={Math.round(height * 7.541)}
|
||||
className={className}
|
||||
viewBox={viewBox}
|
||||
width={width}
|
||||
height={height}
|
||||
style={{ height }}
|
||||
role="img"
|
||||
aria-label="Parenthèse"
|
||||
{...(decorative ? { 'aria-hidden': 'true', focusable: 'false' } : { role: 'img', 'aria-label': 'Parenthèse' })}
|
||||
>
|
||||
<g className="lk-pl"><path transform="scale(1,-1)" d={PAREN_LEFT} /></g>
|
||||
<g className="lk-pr"><path transform="scale(1,-1)" d={PAREN_RIGHT} /></g>
|
||||
<circle className="lk-dot" cx="0" cy="-744" r="424.3" />
|
||||
<g className="lk-nom">
|
||||
<g transform="scale(1,-1)">
|
||||
{LOGO_NOM.map((d, i) => <path key={i} d={d} />)}
|
||||
{!mark && (
|
||||
<g className="lk-nom">
|
||||
<g transform="scale(1,-1)">
|
||||
{LOGO_NOM.map((d, i) => <path key={i} d={d} />)}
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
)}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -78,12 +78,18 @@ em.s { font-style: italic; font-weight: 300; }
|
||||
.site-nav.is-elevated { border-bottom-color: var(--trait); }
|
||||
.nav-in { display: flex; align-items: center; justify-content: space-between; gap: 16px; height: 68px; }
|
||||
.nav-in nav { display: flex; gap: 4px; }
|
||||
.nav-cta { display: flex; align-items: center; gap: 8px; flex: none; }
|
||||
.nav-login svg { flex: none; }
|
||||
/* Connexion : fond blanc, sans contour, même hauteur que « Créer mon arbre » (btn sm) */
|
||||
.btn.s.nav-login { background: #fff; color: var(--encre); box-shadow: none; }
|
||||
@media (hover: hover) and (pointer: fine) { .btn.s.nav-login:hover { background: var(--a50); } }
|
||||
.nav-in nav a {
|
||||
font: 500 14px var(--sans); color: var(--encre2); text-decoration: none;
|
||||
padding: 8px 12px; border-radius: 999px; transition: background-color 160ms ease;
|
||||
}
|
||||
@media (hover: hover) { .nav-in nav a:hover { background: var(--a100); } }
|
||||
.lg { display: block; width: auto; }
|
||||
.site-logo .lk-mark { display: none; }
|
||||
|
||||
/* logo de l'en-tête (SVG en ligne) */
|
||||
.lk-pl path, .lk-pr path { fill: var(--encre); stroke: var(--encre); stroke-width: 48.1; stroke-linejoin: round; }
|
||||
@@ -237,6 +243,13 @@ html.logo-intro .lk-nom { animation: lk-nom 700ms var(--ease-out) 380ms both; }
|
||||
@media (max-width: 820px) {
|
||||
.steps, .promise, .guides { grid-template-columns: 1fr; }
|
||||
.nav-in nav { display: none; }
|
||||
/* Téléphone : symbole du logo seul, icône de profil ronde à gauche de « Créer mon arbre », le texte passe en aria-label */
|
||||
.site-logo .lk-full { display: none; }
|
||||
.site-logo .lk-mark { display: block; }
|
||||
.btn.s.nav-login { width: 34px; height: 34px; padding: 0; border-radius: 50%; }
|
||||
.nav-in { gap: 10px; }
|
||||
.nav-cta { gap: 6px; }
|
||||
.nav-login-text { display: none; }
|
||||
}
|
||||
@media (max-width: 560px) {
|
||||
.hero { padding-top: 8px; }
|
||||
|
||||
Reference in new issue
Block a user