feat(legal): polished UX for 5 legal pages + AGPL external link (sticky TOC, prev/next, breadcrumb)
Refonte visuelle et accessibilité (WCAG 2.2 AA) de la section /legal/ sans toucher au contenu juridique signé (dc4ac97). Templates : - templates/legal/index.html : grille 6 cartes (5 internes + AGPL externe) avec icônes SVG sémantiques, hero gradient, bloc info sous-processeurs, carte AGPL ↗ (target=_blank, rel=noopener noreferrer). - templates/legal/_layout.html : breadcrumb sticky, TOC sticky desktop + collapsible mobile (Alpine.js + IntersectionObserver), prev/next nav entre les 6 docs, skip link, landmarks (main / aside / nav), typographie améliorée (h2 avec accent gradient, tables zebrées, blockquotes), print stylesheet (cache header/breadcrumb/TOC/prev-next). Routes (src/legal/routes.py) : - DISPLAY_ORDER + EXTERNAL_LINKS + PAGE_ICONS exposés. - legal_page() calcule prev/next via _neighbour() helper. - legal_index() concatène pages internes + EXTERNAL_LINKS dans `pages`. Footer : lien AGPL déjà présent depuisdc4ac97(col 4 Compte, ligne 49). Tests (tests/test_legal_pages.py) : 9 anciens + 9 nouveaux = 18/18 PASS - AGPL external link (target+rel) - 5 internes + 1 externe sur l'index - Skip link présent partout - Prev/next existe sur chaque page - Conditions (1ère) sans prev / Mentions (dernière) sans next - Landmarks aside aria-label="Table des matières" - Landmark main role + id="main-content" - Breadcrumb avec aria-current="page" Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ from src.legal import LEGAL_VERSION, legal_bp
|
||||
|
||||
CONTENT_DIR = Path(__file__).parent / 'content'
|
||||
|
||||
# All slugs that have a markdown file rendered by this blueprint.
|
||||
VALID_PAGES = (
|
||||
'conditions',
|
||||
'confidentialite',
|
||||
@@ -21,6 +22,17 @@ VALID_PAGES = (
|
||||
'mentions',
|
||||
)
|
||||
|
||||
# Display order on the index page (mentions kept in VALID_PAGES but
|
||||
# rendered last on /legal/ — purely a presentation choice).
|
||||
DISPLAY_ORDER = (
|
||||
'conditions',
|
||||
'confidentialite',
|
||||
'cookies',
|
||||
'remboursement',
|
||||
'accessibilite',
|
||||
'mentions',
|
||||
)
|
||||
|
||||
PAGE_TITLES = {
|
||||
'conditions': "Conditions d'utilisation",
|
||||
'confidentialite': "Politique de confidentialité (Loi 25)",
|
||||
@@ -39,6 +51,90 @@ PAGE_DESCRIPTIONS = {
|
||||
'mentions': "Mentions légales — DictIA Inc. (filiale d'InnovA AI S.E.N.C.).",
|
||||
}
|
||||
|
||||
# Inline SVG icons (semantic, brand-colored). Each opens with a 24x24 viewBox.
|
||||
PAGE_ICONS = {
|
||||
'conditions': (
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" '
|
||||
'fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
|
||||
'stroke-linejoin="round" aria-hidden="true">'
|
||||
'<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>'
|
||||
'<polyline points="14 2 14 8 20 8"/>'
|
||||
'<line x1="9" y1="13" x2="15" y2="13"/>'
|
||||
'<line x1="9" y1="17" x2="15" y2="17"/>'
|
||||
'</svg>'
|
||||
),
|
||||
'confidentialite': (
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" '
|
||||
'fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
|
||||
'stroke-linejoin="round" aria-hidden="true">'
|
||||
'<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>'
|
||||
'<polyline points="9 12 11 14 15 10"/>'
|
||||
'</svg>'
|
||||
),
|
||||
'cookies': (
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" '
|
||||
'fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
|
||||
'stroke-linejoin="round" aria-hidden="true">'
|
||||
'<path d="M12 2a10 10 0 1 0 10 10 4 4 0 0 1-5-5 4 4 0 0 1-5-5"/>'
|
||||
'<circle cx="8.5" cy="10.5" r="0.6" fill="currentColor"/>'
|
||||
'<circle cx="13" cy="14" r="0.6" fill="currentColor"/>'
|
||||
'<circle cx="16" cy="9" r="0.6" fill="currentColor"/>'
|
||||
'<circle cx="9" cy="15.5" r="0.6" fill="currentColor"/>'
|
||||
'</svg>'
|
||||
),
|
||||
'remboursement': (
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" '
|
||||
'fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
|
||||
'stroke-linejoin="round" aria-hidden="true">'
|
||||
'<path d="M21 12V8a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-1"/>'
|
||||
'<path d="M16 12h5"/>'
|
||||
'<circle cx="17" cy="12" r="2"/>'
|
||||
'<path d="M3 9l4-4 2 2"/>'
|
||||
'</svg>'
|
||||
),
|
||||
'accessibilite': (
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" '
|
||||
'fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
|
||||
'stroke-linejoin="round" aria-hidden="true">'
|
||||
'<circle cx="12" cy="12" r="10"/>'
|
||||
'<circle cx="12" cy="6.5" r="1.2" fill="currentColor"/>'
|
||||
'<path d="M5 9h14"/>'
|
||||
'<path d="M12 9v4l-3 6"/>'
|
||||
'<path d="M12 13l3 6"/>'
|
||||
'</svg>'
|
||||
),
|
||||
'mentions': (
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" '
|
||||
'fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
|
||||
'stroke-linejoin="round" aria-hidden="true">'
|
||||
'<rect x="3" y="4" width="18" height="16" rx="2"/>'
|
||||
'<line x1="7" y1="9" x2="17" y2="9"/>'
|
||||
'<line x1="7" y1="13" x2="17" y2="13"/>'
|
||||
'<line x1="7" y1="17" x2="13" y2="17"/>'
|
||||
'</svg>'
|
||||
),
|
||||
'agpl': (
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" width="28" height="28" viewBox="0 0 24 24" '
|
||||
'fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" '
|
||||
'stroke-linejoin="round" aria-hidden="true">'
|
||||
'<polyline points="16 18 22 12 16 6"/>'
|
||||
'<polyline points="8 6 2 12 8 18"/>'
|
||||
'<line x1="14" y1="4" x2="10" y2="20"/>'
|
||||
'</svg>'
|
||||
),
|
||||
}
|
||||
|
||||
# External links surfaced on the legal index alongside the markdown pages.
|
||||
EXTERNAL_LINKS = (
|
||||
{
|
||||
'slug': 'agpl',
|
||||
'title': 'Code source AGPL',
|
||||
'description': "Code source de DictIA publié sous licence AGPL-3.0 — conformité art. 13 de la licence.",
|
||||
'url': 'https://gitea.dictia.ca',
|
||||
'external': True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _render_markdown(page: str) -> str:
|
||||
"""Read the markdown file for `page` and return rendered HTML."""
|
||||
@@ -53,11 +149,24 @@ def _render_markdown(page: str) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _neighbour(slug: str, offset: int):
|
||||
"""Return the (slug, title) of the previous/next page in DISPLAY_ORDER."""
|
||||
if slug not in DISPLAY_ORDER:
|
||||
return None, None
|
||||
idx = DISPLAY_ORDER.index(slug) + offset
|
||||
if idx < 0 or idx >= len(DISPLAY_ORDER):
|
||||
return None, None
|
||||
neighbour_slug = DISPLAY_ORDER[idx]
|
||||
return neighbour_slug, PAGE_TITLES[neighbour_slug]
|
||||
|
||||
|
||||
@legal_bp.route('/<page>')
|
||||
def legal_page(page):
|
||||
"""Render one of the 6 legal pages by slug."""
|
||||
if page not in VALID_PAGES:
|
||||
abort(404)
|
||||
prev_slug, prev_title = _neighbour(page, -1)
|
||||
next_slug, next_title = _neighbour(page, +1)
|
||||
return render_template(
|
||||
'legal/_layout.html',
|
||||
title=PAGE_TITLES[page],
|
||||
@@ -65,20 +174,40 @@ def legal_page(page):
|
||||
content=_render_markdown(page),
|
||||
page=page,
|
||||
legal_version=LEGAL_VERSION,
|
||||
prev_page=prev_slug,
|
||||
prev_title=prev_title,
|
||||
next_page=next_slug,
|
||||
next_title=next_title,
|
||||
)
|
||||
|
||||
|
||||
@legal_bp.route('/')
|
||||
def legal_index():
|
||||
"""Index page listing all 6 legal pages."""
|
||||
"""Index page listing all internal legal pages plus external links."""
|
||||
pages = [
|
||||
{'slug': slug, 'title': PAGE_TITLES[slug], 'description': PAGE_DESCRIPTIONS[slug]}
|
||||
for slug in VALID_PAGES
|
||||
{
|
||||
'slug': slug,
|
||||
'title': PAGE_TITLES[slug],
|
||||
'description': PAGE_DESCRIPTIONS[slug],
|
||||
'icon': PAGE_ICONS.get(slug, ''),
|
||||
'external': False,
|
||||
}
|
||||
for slug in DISPLAY_ORDER
|
||||
] + [
|
||||
{
|
||||
'slug': link['slug'],
|
||||
'title': link['title'],
|
||||
'description': link['description'],
|
||||
'icon': PAGE_ICONS.get(link['slug'], ''),
|
||||
'url': link['url'],
|
||||
'external': True,
|
||||
}
|
||||
for link in EXTERNAL_LINKS
|
||||
]
|
||||
return render_template(
|
||||
'legal/index.html',
|
||||
title="Documents légaux DictIA",
|
||||
description="Index des documents légaux DictIA — conditions, confidentialité, cookies, remboursement, accessibilité, mentions.",
|
||||
description="Index des documents légaux DictIA — conditions, confidentialité, cookies, remboursement, accessibilité, mentions, code source AGPL.",
|
||||
pages=pages,
|
||||
legal_version=LEGAL_VERSION,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user