Files
veye-lalwa/web/src/routes/textes/[slug]/+page.svelte
T

572 lines
13 KiB
Svelte
Raw Normal View History

<script lang="ts">
import BadgeGuadeloupe from '$composants/BadgeGuadeloupe.svelte';
import BadgeStatut from '$composants/BadgeStatut.svelte';
import { dateLongue } from '$lib/format';
import {
LIBELLES_CONFIANCE,
LIBELLES_ETAPE,
LIBELLES_IMPACT,
LIBELLES_RESULTAT_CC,
LIBELLES_THEME,
LIBELLES_TYPE
} from '$lib/libelles';
import type { PageData } from './$types';
const { data }: { data: PageData } = $props();
const t = $derived(data.texte);
const PUBLICS = ['individus', 'associations', 'entreprises'] as const;
let copie = $state(false);
function citation(): string {
const nom = t.numero_officiel ? `Loi n° ${t.numero_officiel}` : t.titre_court;
const date = t.date_promulgation
? `, ${dateLongue(t.date_promulgation)}`
: t.date_adoption
? `, adoptée le ${dateLongue(t.date_adoption)}`
: '';
const url =
typeof window === 'undefined' ? `/textes/${t.id}` : window.location.href;
return `${nom}${t.titre_court}${date}. Veille législative Gwadloup, consulté le ${dateLongue(
new Date().toISOString()
)}. ${url}`;
}
async function copierCitation() {
try {
await navigator.clipboard.writeText(citation());
copie = true;
setTimeout(() => (copie = false), 2500);
} catch {
/* presse-papiers refusé : la citation reste sélectionnable à la main */
}
}
</script>
<svelte:head>
<title>{t.titre_court} · Veille législative Gwadloup</title>
<meta name="description" content={(t.resume ?? t.titre_court).slice(0, 180)} />
</svelte:head>
<nav class="fil" aria-label="Fil d'Ariane">
<a href="/">Tableau de bord</a>
<a href="/recherche">Recherche</a>
<span aria-current="page">{t.numero_officiel ? `Loi n° ${t.numero_officiel}` : t.titre_court}</span>
</nav>
<article class="fiche">
<header>
{#if t.numero_officiel}<p class="numero">Loi n° {t.numero_officiel}</p>{/if}
<h1>{t.titre_court}</h1>
{#if t.titre_officiel && t.titre_officiel !== t.titre_court}
<p class="titre-officiel">{t.titre_officiel}</p>
{/if}
<div class="ligne badges">
<BadgeStatut statut={t.statut} devantCc={t.devant_cc === 1} />
<BadgeGuadeloupe
pertinence={t.guadeloupe_pertinence}
note={t.guadeloupe_note}
aVerifier={t.a_verifier === 1}
/>
<span class="tag">{LIBELLES_TYPE[t.type] ?? t.type}</span>
<span class="tag confiance" data-niveau={t.confiance}>
Confiance : {LIBELLES_CONFIANCE[t.confiance] ?? t.confiance}
</span>
</div>
{#if t.a_verifier === 1 && t.motif_verification}
<aside class="avertissement" role="note">
<strong>À vérifier —</strong>
{t.motif_verification}
</aside>
{/if}
</header>
<div class="corps">
<div class="principal">
{#if t.resume}
<section>
<h2>Résumé</h2>
<p class="resume">{t.resume}</p>
</section>
{/if}
{#if t.points_cles.length}
<section>
<h2>Points clés</h2>
<ul class="points">
{#each t.points_cles as point (point)}
<li>{point}</li>
{/each}
</ul>
</section>
{/if}
{#if Object.keys(t.impacts).length}
<section>
<h2>Effets sur les libertés</h2>
<div class="impacts">
{#each PUBLICS as pub (pub)}
{@const impact = t.impacts[pub]}
{#if impact}
<div class="impact" data-sens={impact.sens}>
<h3>{pub}</h3>
<p class="sens">{LIBELLES_IMPACT[`${pub}:${impact.sens}`] ?? impact.sens}</p>
{#if impact.note}<p class="note">{impact.note}</p>{/if}
</div>
{/if}
{/each}
</div>
</section>
{/if}
{#if t.evenements.length}
<section>
<h2>Parcours du texte</h2>
<ol class="timeline">
{#each t.evenements as evenement (evenement.date_evenement + evenement.type_etape)}
<li class:previsionnel={evenement.previsionnel === 1}>
<time datetime={evenement.date_evenement}>
{dateLongue(evenement.date_evenement)}
</time>
<div>
<strong>{LIBELLES_ETAPE[evenement.type_etape] ?? evenement.type_etape}</strong>
{#if evenement.previsionnel === 1}
<span class="prevu">prévisionnel</span>
{/if}
<p>{evenement.description}</p>
</div>
</li>
{/each}
</ol>
</section>
{/if}
{#if t.decisions_cc.length}
<section>
<h2>Conseil constitutionnel</h2>
{#each t.decisions_cc as decision (decision.numero_affaire)}
<div class="carte decision">
<h3>Affaire n° {decision.numero_affaire}</h3>
<dl>
{#if decision.date_saisine}
<dt>Saisine</dt>
<dd>{dateLongue(decision.date_saisine)}</dd>
{/if}
{#if decision.date_decision}
<dt>Décision</dt>
<dd>{dateLongue(decision.date_decision)}</dd>
{:else if decision.date_decision_attendue}
<dt>Décision attendue</dt>
<dd>{dateLongue(decision.date_decision_attendue)}</dd>
{/if}
{#if decision.resultat}
<dt>Résultat</dt>
<dd>{LIBELLES_RESULTAT_CC[decision.resultat] ?? decision.resultat}</dd>
{/if}
{#if decision.saisissants}
<dt>Saisissants</dt>
<dd>{decision.saisissants}</dd>
{/if}
</dl>
{#if decision.resume}<p>{decision.resume}</p>{/if}
</div>
{/each}
</section>
{/if}
<section>
<h2>Sources ({t.sources.length})</h2>
<p class="avant-sources">
Chaque source est citée telle qu'elle a été consultée. Les sources primaires
(Journal officiel, assemblées, Conseil constitutionnel, Conseil d'État) sont
marquées T1, la presse et les analyses T2.
</p>
<ol class="sources">
{#each t.sources as source (source.url + (source.marqueur ?? ''))}
<li class="carte source" data-tier={source.tier}>
<div class="ligne entete-source">
<span class="tier">{source.tier}</span>
{#if source.editeur}<span class="editeur">{source.editeur}</span>{/if}
{#if source.date_publication}
<time datetime={source.date_publication}>
{dateLongue(source.date_publication)}
</time>
{/if}
</div>
{#if source.titre}<p class="titre-source">{source.titre}</p>{/if}
{#if source.extrait_verbatim}
<blockquote>{source.extrait_verbatim}</blockquote>
{/if}
{#if source.contexte}<p class="contexte">{source.contexte}</p>{/if}
<a class="url" href={source.url} rel="noopener nofollow" target="_blank">
{source.url}
</a>
</li>
{/each}
</ol>
</section>
</div>
<aside class="lateral">
<div class="carte encart">
<h2>Repères</h2>
<dl>
{#if t.date_depot}
<dt>Dépôt</dt>
<dd>{dateLongue(t.date_depot)}</dd>
{/if}
{#if t.date_adoption}
<dt>Adoption</dt>
<dd>{dateLongue(t.date_adoption)}</dd>
{/if}
{#if t.date_promulgation}
<dt>Promulgation</dt>
<dd>{dateLongue(t.date_promulgation)}</dd>
{/if}
{#if t.date_entree_vigueur}
<dt>Entrée en vigueur</dt>
<dd>{dateLongue(t.date_entree_vigueur)}</dd>
{/if}
{#if t.prochaine_echeance_label}
<dt>Prochaine étape</dt>
<dd>{t.prochaine_echeance_label}</dd>
{/if}
</dl>
</div>
{#if t.guadeloupe_note}
<div class="carte encart gwada">
<h2>Guadeloupe</h2>
<p>{t.guadeloupe_note}</p>
</div>
{/if}
{#if t.themes.length}
<div class="carte encart">
<h2>Thèmes</h2>
<div class="ligne">
{#each t.themes as theme (theme)}
<a class="tag" href="/recherche?theme={theme}">
{LIBELLES_THEME[theme] ?? theme}
</a>
{/each}
</div>
</div>
{/if}
<div class="carte encart">
<h2>Citer cette fiche</h2>
<button type="button" class="bouton" onclick={copierCitation}>
<span>{copie ? 'Copié' : 'Copier la citation'}</span>
</button>
{#if t.source_seed}
<p class="provenance">Origine : <code>{t.source_seed}</code></p>
{/if}
{#if t.derniere_verif}
<p class="provenance">Dernière vérification : {t.derniere_verif}</p>
{/if}
</div>
</aside>
</div>
</article>
<style>
.fil {
font-size: 0.78rem;
color: var(--muted-fort);
margin-bottom: var(--space-2);
}
.fil a {
color: var(--muted);
}
.numero {
font-family: var(--font-mono);
color: var(--or-oki);
margin: 0 0 0.2rem;
font-size: 0.9rem;
}
.titre-officiel {
color: var(--muted);
font-size: 0.92rem;
max-width: 72ch;
}
.badges {
margin-bottom: var(--space-2);
}
.confiance[data-niveau='low'] {
border-color: color-mix(in srgb, var(--rouge-oki) 55%, transparent);
background: color-mix(in srgb, var(--rouge-oki) 10%, transparent);
}
.avertissement {
border: 1px solid color-mix(in srgb, var(--or-oki) 55%, transparent);
background: color-mix(in srgb, var(--or-oki) 9%, transparent);
border-radius: var(--radius-md);
padding: 0.7rem 0.9rem;
font-size: 0.86rem;
margin-bottom: var(--space-2);
}
.corps {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(240px, 300px);
gap: var(--space-3);
align-items: start;
}
@media (max-width: 900px) {
.corps {
grid-template-columns: 1fr;
}
}
section {
margin-bottom: var(--space-4);
}
section h2 {
font-size: 0.85rem;
letter-spacing: 0.05em;
color: var(--or-oki);
border-bottom: 1px solid var(--line);
padding-bottom: 0.35rem;
margin-bottom: var(--space-2);
}
.resume {
font-size: 1rem;
max-width: 70ch;
}
.points {
margin: 0;
padding-left: 1.1rem;
display: flex;
flex-direction: column;
gap: 0.45rem;
font-size: 0.92rem;
max-width: 74ch;
}
.points li::marker {
color: var(--or-oki);
}
.impacts {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 0.7rem;
}
.impact {
border: var(--border-card);
border-left: 4px solid var(--impact-neutre);
border-radius: var(--radius-md);
padding: 0.7rem 0.85rem;
background: var(--card-bg);
}
.impact[data-sens='positif'] {
border-left-color: var(--impact-positif);
}
.impact[data-sens='negatif'] {
border-left-color: var(--impact-negatif);
}
.impact[data-sens='mixte'] {
border-left-color: var(--impact-mixte);
}
.impact h3 {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted-fort);
margin-bottom: 0.2rem;
}
.impact .sens {
font-weight: 600;
font-size: 0.88rem;
margin-bottom: 0.3rem;
}
.impact .note {
font-size: 0.82rem;
color: var(--muted);
margin: 0;
}
.timeline {
list-style: none;
margin: 0;
padding: 0 0 0 1rem;
border-left: 2px solid var(--line);
display: flex;
flex-direction: column;
gap: var(--space-2);
}
.timeline li {
position: relative;
display: grid;
grid-template-columns: minmax(120px, auto) 1fr;
gap: 0.8rem;
font-size: 0.88rem;
}
.timeline li::before {
content: '';
position: absolute;
left: -1.35rem;
top: 0.4rem;
width: 8px;
height: 8px;
background: var(--or-oki);
border-radius: 1px;
}
.timeline li.previsionnel::before {
background: transparent;
border: 2px solid var(--or-oki);
}
.timeline time {
color: var(--or-oki);
font-weight: 600;
font-size: 0.82rem;
}
.timeline p {
margin: 0.15rem 0 0;
color: var(--muted);
font-size: 0.85rem;
}
.prevu {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--muted-fort);
border: 1px solid var(--line);
border-radius: var(--radius-sm);
padding: 0 0.35em;
margin-left: 0.35em;
}
.decision {
border-left-color: var(--orange-flamme);
margin-bottom: 0.6rem;
}
.decision h3 {
font-family: var(--font-mono);
font-size: 0.9rem;
text-transform: none;
}
dl {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.25rem 0.8rem;
margin: 0 0 0.5rem;
font-size: 0.85rem;
}
dt {
color: var(--muted-fort);
}
dd {
margin: 0;
}
.avant-sources {
font-size: 0.82rem;
color: var(--muted-fort);
max-width: 68ch;
}
.sources {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.source {
border-left-color: var(--muted-fort);
font-size: 0.86rem;
}
.source[data-tier='T1'] {
border-left-color: var(--vert-oki);
}
.entete-source {
gap: 0.5rem;
font-size: 0.76rem;
color: var(--muted-fort);
}
.tier {
font-family: var(--font-mono);
font-weight: 700;
color: var(--vert-oki);
}
.source[data-tier='T2'] .tier {
color: var(--muted-fort);
}
.editeur {
font-weight: 600;
color: var(--blanc-creme);
}
.titre-source {
margin: 0.3rem 0;
}
blockquote {
margin: 0.4rem 0;
padding-left: 0.8rem;
border-left: 2px solid var(--or-oki);
color: var(--muted);
font-style: italic;
}
.contexte {
font-size: 0.82rem;
color: var(--muted-fort);
margin: 0.3rem 0;
}
.url {
font-size: 0.75rem;
word-break: break-all;
font-family: var(--font-mono);
}
.lateral {
display: flex;
flex-direction: column;
gap: 0.7rem;
position: sticky;
top: 5.5rem;
}
.encart h2 {
font-size: 0.75rem;
border: 0;
padding: 0;
margin-bottom: 0.5rem;
}
.encart.gwada {
border-left-color: var(--turquoise-caraibes);
}
.encart p {
font-size: 0.85rem;
color: var(--muted);
margin: 0;
}
.provenance {
font-size: 0.74rem !important;
color: var(--muted-fort) !important;
margin-top: 0.5rem !important;
}
code {
font-family: var(--font-mono);
font-size: 0.9em;
word-break: break-all;
}
@media (max-width: 900px) {
.lateral {
position: static;
}
}
@media (max-width: 560px) {
.timeline li {
grid-template-columns: 1fr;
gap: 0.15rem;
}
}
</style>