feat: apply pro creative-lab art direction and validate astro pipeline
This commit is contained in:
@@ -0,0 +1,55 @@
|
|||||||
|
name: Deploy Astro to GitHub Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pages
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
env:
|
||||||
|
PUBLIC_GTM_CONTAINER_ID: GTM-5SLM67QF
|
||||||
|
|
||||||
|
- name: Setup Pages
|
||||||
|
uses: actions/configure-pages@v5
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-pages-artifact@v3
|
||||||
|
with:
|
||||||
|
path: ./dist
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@v4
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Internal private notes
|
||||||
|
notes-interne/
|
||||||
|
|
||||||
|
# Node / tooling
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
storybook-static/
|
||||||
|
.astro/
|
||||||
|
.DS_Store
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { StorybookConfig } from '@storybook/react-vite';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const config: StorybookConfig = {
|
||||||
|
stories: ['../src/**/*.stories.@(ts|tsx)'],
|
||||||
|
addons: ['@storybook/addon-onboarding', '@storybook/addon-docs', '@storybook/addon-a11y', '@storybook/addon-vitest'],
|
||||||
|
framework: {
|
||||||
|
name: '@storybook/react-vite',
|
||||||
|
options: {}
|
||||||
|
},
|
||||||
|
viteFinal: async (config) => {
|
||||||
|
config.resolve = config.resolve || {};
|
||||||
|
config.resolve.alias = {
|
||||||
|
...(typeof config.resolve.alias === 'object' ? config.resolve.alias : {}),
|
||||||
|
'@': path.resolve(fileURLToPath(new URL('../src', import.meta.url)))
|
||||||
|
};
|
||||||
|
return config;
|
||||||
|
},
|
||||||
|
docs: {
|
||||||
|
autodocs: 'tag'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { Preview } from '@storybook/react-vite';
|
||||||
|
import '../src/styles/global.css';
|
||||||
|
|
||||||
|
const preview: Preview = {
|
||||||
|
parameters: {
|
||||||
|
layout: 'fullscreen',
|
||||||
|
backgrounds: {
|
||||||
|
default: 'studio-dark',
|
||||||
|
values: [
|
||||||
|
{ name: 'studio-dark', value: '#0f0e17' },
|
||||||
|
{ name: 'studio-deep', value: '#151325' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export default preview;
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
# Architecture — Site personnel statique GitHub Pages
|
||||||
|
|
||||||
|
- **Date**: 2026-03-01
|
||||||
|
- **Gate visée**: G1 Arch Freeze
|
||||||
|
- **Responsable (agent)**: Architect
|
||||||
|
|
||||||
|
## Vue d'ensemble
|
||||||
|
Architecture front statique en 2 fichiers:
|
||||||
|
- `index.html`: structure sémantique (header/nav/main/sections/footer)
|
||||||
|
- `assets/styles.css`: thème, layout responsive, composants visuels
|
||||||
|
|
||||||
|
## Interfaces
|
||||||
|
- **Entrée**: navigateur web moderne
|
||||||
|
- **Sortie**: rendu d'un site vitrine personnel
|
||||||
|
- **Hébergement**: GitHub Pages (branch `main`, root)
|
||||||
|
|
||||||
|
## Structure UI
|
||||||
|
- Bloc `hero` avec CTA.
|
||||||
|
- Section `#a-propos` pour la présentation.
|
||||||
|
- Section `#projets` avec grille de cartes.
|
||||||
|
- Section `#contact` avec liens sortants.
|
||||||
|
|
||||||
|
## Contraintes techniques
|
||||||
|
- Aucun JS requis pour la version initiale.
|
||||||
|
- Chemins relatifs uniquement (`assets/styles.css`).
|
||||||
|
- CSS pur (variables, grid, media queries).
|
||||||
|
|
||||||
|
## Budgets
|
||||||
|
- Poids page + CSS < 80KB (source non compressée).
|
||||||
|
- Temps de chargement perçu < 1s sur connexion standard (contenu statique).
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
- Smoke test HTTP local.
|
||||||
|
- Vérification ancres + rendu responsive.
|
||||||
|
- Capture visuelle de preuve.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# G1 Arch Freeze Evidence (v2)
|
||||||
|
|
||||||
|
- Architecture UI détaillée: hero, about, projets, contact.
|
||||||
|
- Contraintes GitHub Pages (chemins relatifs, statique sans build) confirmées.
|
||||||
|
- Budgets et stratégie de validation ajustés.
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# G1 Arch Freeze Evidence
|
||||||
|
|
||||||
|
- Architecture statique validée: `index.html` + `assets/styles.css`
|
||||||
|
- Interfaces et contraintes de chemins relatifs définies
|
||||||
|
- Budgets simplifiés documentés
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Clément Saillant — L'électron rare</title>
|
||||||
|
<meta name="theme-color" content="#f7f4ec" />
|
||||||
|
<meta
|
||||||
|
name="description"
|
||||||
|
content="Site personnel de Clément Saillant (L'électron rare) : projets audiovisuels, performances et contact."
|
||||||
|
/>
|
||||||
|
<link rel="canonical" href="https://electron-rare.github.io/" />
|
||||||
|
<meta name="robots" content="index,follow,max-image-preview:large" />
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:locale" content="fr_FR" />
|
||||||
|
<meta property="og:site_name" content="L'électron rare" />
|
||||||
|
<meta property="og:title" content="Clément Saillant — L'électron rare" />
|
||||||
|
<meta property="og:url" content="https://electron-rare.github.io/" />
|
||||||
|
<meta
|
||||||
|
property="og:description"
|
||||||
|
content="Performances, création sonore expérimentale, collaborations vidéo et projets noise."
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
property="og:image"
|
||||||
|
content="https://electron-rare.github.io/assets/og-cover.jpg"
|
||||||
|
/>
|
||||||
|
<meta property="og:image:width" content="1200" />
|
||||||
|
<meta property="og:image:height" content="630" />
|
||||||
|
<meta property="og:image:alt" content="L'électron rare - performances et créations audiovisuelles" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content="Clément Saillant — L'électron rare" />
|
||||||
|
<meta
|
||||||
|
name="twitter:description"
|
||||||
|
content="Performances et création audiovisuelle expérimentale."
|
||||||
|
/>
|
||||||
|
<meta
|
||||||
|
name="twitter:image"
|
||||||
|
content="https://electron-rare.github.io/assets/og-cover.jpg"
|
||||||
|
/>
|
||||||
|
<link rel="preload" href="assets/styles.css" as="style" />
|
||||||
|
<link rel="stylesheet" href="assets/styles.css" />
|
||||||
|
<script>
|
||||||
|
window.GTM_CONTAINER_ID = "GTM-5SLM67QF";
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
(function (w, d, s, l, i) {
|
||||||
|
if (!i) return;
|
||||||
|
w[l] = w[l] || [];
|
||||||
|
w[l].push({ "gtm.start": new Date().getTime(), event: "gtm.js" });
|
||||||
|
var f = d.getElementsByTagName(s)[0],
|
||||||
|
j = d.createElement(s),
|
||||||
|
dl = l !== "dataLayer" ? "&l=" + l : "";
|
||||||
|
j.async = true;
|
||||||
|
j.src = "https://www.googletagmanager.com/gtm.js?id=" + i + dl;
|
||||||
|
f.parentNode.insertBefore(j, f);
|
||||||
|
})(window, document, "script", "dataLayer", window.GTM_CONTAINER_ID);
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
<iframe
|
||||||
|
src="https://www.googletagmanager.com/ns.html?id=GTM-5SLM67QF"
|
||||||
|
height="0"
|
||||||
|
width="0"
|
||||||
|
style="display:none;visibility:hidden"
|
||||||
|
></iframe>
|
||||||
|
</noscript>
|
||||||
|
<a class="skip-link" href="#top">Aller au contenu</a>
|
||||||
|
<header class="site-header">
|
||||||
|
<div class="container nav-wrap">
|
||||||
|
<a class="brand" href="#top">L'électron rare</a>
|
||||||
|
<nav aria-label="Navigation principale">
|
||||||
|
<ul>
|
||||||
|
<li><a href="#a-propos">À propos</a></li>
|
||||||
|
<li><a href="#projets">Projets</a></li>
|
||||||
|
<li><a href="#contact">Contact</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="top" class="container">
|
||||||
|
<section class="hero" aria-labelledby="hero-title">
|
||||||
|
<div class="hero-grid">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Performance • Noise • Audiovisuel</p>
|
||||||
|
<h1 id="hero-title">Clément Saillant, alias L'électron rare.</h1>
|
||||||
|
<p class="hero-text">
|
||||||
|
Projet artistique orienté expérimentations sonores et visuelles:
|
||||||
|
performances live, textures noise et collaborations entre scène et
|
||||||
|
image.
|
||||||
|
</p>
|
||||||
|
<ul class="hero-metrics" aria-label="Axes artistiques">
|
||||||
|
<li>Live sets</li>
|
||||||
|
<li>Textures bruitistes</li>
|
||||||
|
<li>Collab vidéo</li>
|
||||||
|
</ul>
|
||||||
|
<p class="hero-actions">
|
||||||
|
<a class="btn btn-primary" href="#projets" data-track="cta_hero_projets" data-destination="#projets">Voir mes projets</a>
|
||||||
|
<a class="btn btn-secondary" href="#contact" data-track="cta_hero_contact" data-destination="#contact">Me contacter</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<aside class="hero-card" aria-label="Carte artiste">
|
||||||
|
<p class="hero-card-kicker">Carte rapide</p>
|
||||||
|
<h2>L'électron rare</h2>
|
||||||
|
<p>
|
||||||
|
Performances et création sonore expérimentale, entre énergie live
|
||||||
|
et matière visuelle.
|
||||||
|
</p>
|
||||||
|
<a href="#a-propos" data-track="cta_hero_profile" data-destination="#a-propos">Explorer le profil</a>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="a-propos" class="panel" aria-labelledby="about-title">
|
||||||
|
<h2 id="about-title">À propos</h2>
|
||||||
|
<p>
|
||||||
|
Je suis <strong>Clément Saillant</strong>, artiste et créateur sous le
|
||||||
|
nom <strong>L'électron rare</strong>. J'explore les liens entre matière
|
||||||
|
sonore, récit visuel et énergie live, avec une approche artisanale et
|
||||||
|
directe.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="projets" class="panel" aria-labelledby="projects-title">
|
||||||
|
<h2 id="projects-title">Projets</h2>
|
||||||
|
<div class="cards">
|
||||||
|
<article class="card">
|
||||||
|
<h3>L'électron rare</h3>
|
||||||
|
<p>
|
||||||
|
Univers principal: performances et créations expérimentales entre
|
||||||
|
musique électronique et visuel.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://fr.linkedin.com/in/electron-rare"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
data-track="outbound_linkedin_project"
|
||||||
|
data-destination="linkedin.com"
|
||||||
|
>Profil LinkedIn</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<h3>Collaborations vidéo</h3>
|
||||||
|
<p>
|
||||||
|
Travail autour du VJing et des formats hybrides live, notamment avec
|
||||||
|
des collaborations type VJ Clearscreen.
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
<article class="card">
|
||||||
|
<h3>Électron Fou (noise)</h3>
|
||||||
|
<p>
|
||||||
|
Projet noise affilié à la scène expérimentale, avec sorties publiées
|
||||||
|
et présence sur plateformes audio.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<a
|
||||||
|
href="https://lelectron-fou.bandcamp.com/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
data-track="outbound_bandcamp_project"
|
||||||
|
data-destination="bandcamp.com"
|
||||||
|
>Bandcamp</a
|
||||||
|
>
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="contact" class="panel" aria-labelledby="contact-title">
|
||||||
|
<h2 id="contact-title">Contact</h2>
|
||||||
|
<p class="contact-intro">
|
||||||
|
Pour booking, collaboration artistique ou production audiovisuelle,
|
||||||
|
contactez-moi via les plateformes ci-dessous.
|
||||||
|
</p>
|
||||||
|
<ul class="contact-list">
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://fr.linkedin.com/in/electron-rare"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
data-track="outbound_linkedin_contact"
|
||||||
|
data-destination="linkedin.com"
|
||||||
|
>LinkedIn</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://www.malt.com/profile/clementsaillant"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
data-track="outbound_malt_contact"
|
||||||
|
data-destination="malt.com"
|
||||||
|
>Malt</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://lelectron-fou.bandcamp.com/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
data-track="outbound_bandcamp_contact"
|
||||||
|
data-destination="bandcamp.com"
|
||||||
|
>Bandcamp (Électron Fou)</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<ul class="trust-points" aria-label="Repères de confiance">
|
||||||
|
<li>Profil professionnel vérifiable sur LinkedIn et Malt</li>
|
||||||
|
<li>Projets artistiques publiés sur plateformes publiques</li>
|
||||||
|
<li>Contact direct pour booking et collaborations</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="site-footer">
|
||||||
|
<div class="container">
|
||||||
|
<small>© 2026 Clément Saillant — L'électron rare.</small>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
<script type="application/ld+json">
|
||||||
|
{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@type": "Person",
|
||||||
|
"name": "Clément Saillant",
|
||||||
|
"alternateName": "L'électron rare",
|
||||||
|
"url": "https://electron-rare.github.io/",
|
||||||
|
"jobTitle": "Artiste audiovisuel et créateur sonore expérimental",
|
||||||
|
"description": "Performances live, création noise et collaborations vidéo sous l'identité L'électron rare.",
|
||||||
|
"sameAs": [
|
||||||
|
"https://fr.linkedin.com/in/electron-rare",
|
||||||
|
"https://www.malt.com/profile/clementsaillant",
|
||||||
|
"https://lelectron-fou.bandcamp.com/"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var links = document.querySelectorAll("[data-track]");
|
||||||
|
for (var i = 0; i < links.length; i += 1) {
|
||||||
|
links[i].addEventListener("click", function () {
|
||||||
|
var eventName = this.getAttribute("data-track");
|
||||||
|
var destination =
|
||||||
|
this.getAttribute("data-destination") || this.getAttribute("href") || "unknown";
|
||||||
|
window.dataLayer.push({
|
||||||
|
event: eventName,
|
||||||
|
event_category: "engagement",
|
||||||
|
event_label: eventName,
|
||||||
|
destination: destination
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://electron-rare.github.io/sitemap.xml
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://electron-rare.github.io/</loc>
|
||||||
|
<lastmod>2026-03-01</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
@@ -0,0 +1,424 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #f7f4ec;
|
||||||
|
--surface: #fffdf7;
|
||||||
|
--surface-soft: #f2eee3;
|
||||||
|
--text: #171614;
|
||||||
|
--muted: #625d50;
|
||||||
|
--primary: #d74b2a;
|
||||||
|
--primary-strong: #b83c1f;
|
||||||
|
--border: #d8d1c1;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Avenir Next", "Segoe UI", "Helvetica Neue", sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at 10% 10%, #ffddbf 0%, rgba(255, 221, 191, 0) 40%),
|
||||||
|
radial-gradient(circle at 90% 0%, #f0d8bf 0%, rgba(240, 216, 191, 0) 35%),
|
||||||
|
var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link {
|
||||||
|
position: absolute;
|
||||||
|
left: -9999px;
|
||||||
|
top: auto;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skip-link:focus {
|
||||||
|
left: 0.8rem;
|
||||||
|
top: 0.8rem;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
z-index: 40;
|
||||||
|
background: #111;
|
||||||
|
color: #fff;
|
||||||
|
padding: 0.45rem 0.7rem;
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
width: min(1000px, 92vw);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
section[id] {
|
||||||
|
scroll-margin-top: 6.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-header {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 20;
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
background: rgba(247, 244, 236, 0.86);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-wrap {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0.8rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
color: var(--text);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 2rem;
|
||||||
|
color: var(--muted);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.35rem 0.55rem;
|
||||||
|
border-radius: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a:hover,
|
||||||
|
nav a:focus-visible {
|
||||||
|
color: var(--text);
|
||||||
|
background: rgba(255, 255, 255, 0.66);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
padding: clamp(3.2rem, 7vw, 4.8rem) 0 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.25fr 0.8fr;
|
||||||
|
gap: 1.1rem;
|
||||||
|
align-items: end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eyebrow {
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(2rem, 5vw, 3.2rem);
|
||||||
|
line-height: 1.12;
|
||||||
|
margin: 0.6rem 0 1rem;
|
||||||
|
max-width: 18ch;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-text {
|
||||||
|
max-width: 64ch;
|
||||||
|
margin: 0 0 1.4rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-metrics {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.55rem;
|
||||||
|
margin: 0 0 1.3rem;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-metrics li {
|
||||||
|
background: rgba(255, 255, 255, 0.76);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.3rem 0.65rem;
|
||||||
|
font-size: 0.92rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
min-height: 2.75rem;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.7rem 1.02rem;
|
||||||
|
border-radius: 0.7rem;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: transform 140ms ease, box-shadow 140ms ease, background-color 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: #fff7f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover,
|
||||||
|
.btn-primary:focus-visible {
|
||||||
|
background: var(--primary-strong);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 8px 18px rgba(183, 60, 31, 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
color: var(--text);
|
||||||
|
background: rgba(255, 255, 255, 0.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover,
|
||||||
|
.btn-secondary:focus-visible {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
background: linear-gradient(180deg, #fff6eb 0%, #f3e6d4 100%);
|
||||||
|
border: 1px solid #ddc8ab;
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 1rem 1rem 1.1rem;
|
||||||
|
box-shadow: 0 12px 26px rgba(30, 18, 6, 0.11);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card-kicker {
|
||||||
|
margin: 0;
|
||||||
|
color: #90633d;
|
||||||
|
font-size: 0.78rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card h2 {
|
||||||
|
margin: 0.35rem 0 0.45rem;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card p {
|
||||||
|
margin: 0 0 0.7rem;
|
||||||
|
color: #4f4639;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card a {
|
||||||
|
color: var(--primary-strong);
|
||||||
|
font-weight: 700;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card a:hover,
|
||||||
|
.hero-card a:focus-visible {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: linear-gradient(180deg, var(--surface) 0%, var(--surface-soft) 100%);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 1.1rem 1.2rem 1.25rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
box-shadow: 0 10px 24px rgba(30, 18, 6, 0.07);
|
||||||
|
content-visibility: auto;
|
||||||
|
contain-intrinsic-size: 1px 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-top: 0.2rem;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||||
|
gap: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
grid-column: span 4;
|
||||||
|
background: rgba(255, 252, 246, 0.86);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0.9rem;
|
||||||
|
transition: transform 140ms ease, box-shadow 140ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card h3 {
|
||||||
|
margin-top: 0.1rem;
|
||||||
|
margin-bottom: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card p {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card a {
|
||||||
|
color: var(--primary);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card a:hover,
|
||||||
|
.card a:focus-visible {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 18px rgba(30, 18, 6, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-list a {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 2rem;
|
||||||
|
color: var(--primary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-list a:hover,
|
||||||
|
.contact-list a:focus-visible {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-intro {
|
||||||
|
margin-top: 0;
|
||||||
|
color: var(--muted);
|
||||||
|
max-width: 62ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.trust-points {
|
||||||
|
margin: 0.95rem 0 0;
|
||||||
|
padding-left: 1.1rem;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.trust-points li + li {
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-footer {
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
color: var(--muted);
|
||||||
|
padding: 1.2rem 0 2.2rem;
|
||||||
|
margin-top: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus-visible,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 3px solid var(--primary);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 960px) {
|
||||||
|
.hero-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-card {
|
||||||
|
max-width: 38rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
grid-column: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 760px) {
|
||||||
|
.nav-wrap {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.42rem 0.62rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cards {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
padding-top: 2.4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 390px) {
|
||||||
|
.container {
|
||||||
|
width: min(1000px, 94vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: clamp(1.72rem, 9vw, 2.25rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-actions {
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
* {
|
||||||
|
animation-duration: 1ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 1ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-contrast: more) {
|
||||||
|
:root {
|
||||||
|
--muted: #453f33;
|
||||||
|
--border: #8b816d;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# G0 Spec Freeze Evidence (v2)
|
||||||
|
|
||||||
|
- Spécification orientée « site personnel » finalisée.
|
||||||
|
- AC mis à jour avec navigation ancrée et contenu vitrine.
|
||||||
|
- Non-goals, risques et TODO de personnalisation documentés.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# G0 Spec Freeze Evidence
|
||||||
|
|
||||||
|
- Spec créée: `specs/site-github-pages-spec.md`
|
||||||
|
- AC définis: AC1..AC4
|
||||||
|
- Non-goals et risques documentés
|
||||||
|
- TODO ajoutés pour informations manquantes
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# G2 Implement Evidence (v2)
|
||||||
|
|
||||||
|
Implémentation livrée:
|
||||||
|
- `index.html` transformé en template de site personnel complet.
|
||||||
|
- `assets/styles.css` enrichi (navigation sticky, sections, cartes projets, responsive).
|
||||||
|
- Documentation de déploiement mise à jour.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
# G2 Implement Evidence (v3) - SEO + Acquisition
|
||||||
|
|
||||||
|
Date: 2026-03-01
|
||||||
|
|
||||||
|
## Portée implémentée
|
||||||
|
- SEO technique: canonical, robots, sitemap, OG/Twitter image.
|
||||||
|
- Tracking contract: événements CTA/outbound avec `event_category`, `event_label`, `destination`.
|
||||||
|
- Bootstrap GTM prêt à brancher (`GTM-REPLACE_ME`).
|
||||||
|
- Conversion UX: section contact enrichie + trust points.
|
||||||
|
- Accessibilité/réactivité: skip-link déjà présent + réglage ultra-mobile (`<=390px`).
|
||||||
|
|
||||||
|
## Fichiers modifiés
|
||||||
|
- `index.html`
|
||||||
|
- `assets/styles.css`
|
||||||
|
- `docs/github-pages-deploy.md`
|
||||||
|
- `robots.txt` (nouveau)
|
||||||
|
- `sitemap.xml` (nouveau)
|
||||||
|
- `assets/og-cover.jpg` (nouveau)
|
||||||
|
|
||||||
|
## Mapping tracking (event -> destination)
|
||||||
|
| Event | Type | Destination | Usage |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `cta_hero_projets` | CTA interne | `#projets` | Navigation vers section projets |
|
||||||
|
| `cta_hero_contact` | CTA interne | `#contact` | Navigation vers section contact |
|
||||||
|
| `cta_hero_profile` | CTA interne | `#a-propos` | Navigation vers section profil |
|
||||||
|
| `outbound_linkedin_project` | Sortant | `linkedin.com` | Preuve sociale projet |
|
||||||
|
| `outbound_linkedin_contact` | Sortant | `linkedin.com` | Conversion primaire |
|
||||||
|
| `outbound_malt_contact` | Sortant | `malt.com` | Conversion secondaire |
|
||||||
|
| `outbound_bandcamp_project` | Sortant | `bandcamp.com` | Projet musical |
|
||||||
|
| `outbound_bandcamp_contact` | Sortant | `bandcamp.com` | Contact alternatif |
|
||||||
|
|
||||||
|
Paramètres communs poussés:
|
||||||
|
- `event_category=engagement`
|
||||||
|
- `event_label=<event_name>`
|
||||||
|
- `destination=<domain|anchor>`
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
- Pour activer GTM en production: remplacer `GTM-REPLACE_ME` dans le script head et le bloc noscript.
|
||||||
|
- URL canonique actuelle: `https://electron-rare.github.io/`.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# G2 Implement Evidence (v4) - Astro Studio Migration Foundation
|
||||||
|
|
||||||
|
Date: 2026-03-01
|
||||||
|
|
||||||
|
## Scope implémenté
|
||||||
|
- Mise en place stack primaire:
|
||||||
|
- Astro + TypeScript strict
|
||||||
|
- React integration
|
||||||
|
- Tailwind 4 (via plugin Vite)
|
||||||
|
- Motion
|
||||||
|
- Radix/shadcn-like base (`Button` with Radix Slot + CVA)
|
||||||
|
- Storybook React-Vite
|
||||||
|
- Création architecture `src/` composantisée:
|
||||||
|
- `src/layouts/BaseLayout.astro`
|
||||||
|
- `src/pages/index.astro`
|
||||||
|
- sections React (`Hero`, `About`, `Projects`, `Contact`)
|
||||||
|
- composants UI (`Button`, `Card`)
|
||||||
|
- Conservation contrat SEO/analytics:
|
||||||
|
- metadata canonical/OG/Twitter
|
||||||
|
- JSON-LD Person
|
||||||
|
- contrat `dataLayer` (`event_category`, `event_label`, `destination`)
|
||||||
|
- GTM ID `GTM-5SLM67QF` via `PUBLIC_GTM_CONTAINER_ID` fallback
|
||||||
|
- Préparation déploiement Pages via GitHub Actions:
|
||||||
|
- `.github/workflows/deploy-pages.yml`
|
||||||
|
- Storybook full coverage initiale des composants/sections clés.
|
||||||
|
|
||||||
|
## Fichiers clés ajoutés/modifiés
|
||||||
|
- `package.json`, `package-lock.json`
|
||||||
|
- `astro.config.mjs`, `tsconfig.json`
|
||||||
|
- `src/**` (layouts/components/styles/pages)
|
||||||
|
- `.storybook/main.ts`, `.storybook/preview.ts`
|
||||||
|
- `.github/workflows/deploy-pages.yml`
|
||||||
|
- `public/robots.txt`, `public/sitemap.xml`, `public/assets/og-cover.jpg`
|
||||||
|
- `docs/github-pages-deploy.md`
|
||||||
|
- `specs/site-github-pages-spec.md`
|
||||||
|
|
||||||
|
## Note
|
||||||
|
- L'ancien socle statique (`index.html`, `assets/styles.css`) est conservé temporairement dans le repo pour transition contrôlée.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# G3 Verification Evidence (v2)
|
||||||
|
|
||||||
|
Vérifications exécutées:
|
||||||
|
- `python3 -m http.server 4173`
|
||||||
|
- `curl -I http://127.0.0.1:4173/` -> `HTTP/1.0 200 OK`
|
||||||
|
- `rg 'href="/' index.html` (attendu: aucune occurrence)
|
||||||
|
- Capture Playwright: `artifacts/qa-test/2026-03-01/site-home-v2.png`
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# G3 Verification Evidence (v3) - SEO + Tracking
|
||||||
|
|
||||||
|
Date: 2026-03-01
|
||||||
|
|
||||||
|
## Vérifications exécutées
|
||||||
|
- `rg -n "canonical|og:image|twitter:image|application/ld\+json|GTM_CONTAINER_ID|event_category|event_label|destination" index.html`
|
||||||
|
- `rg -n "data-track=|data-destination=" index.html`
|
||||||
|
- `sips -g pixelWidth -g pixelHeight assets/og-cover.jpg`
|
||||||
|
- `cat robots.txt`
|
||||||
|
- `cat sitemap.xml`
|
||||||
|
- `python3 -m http.server 4173` puis:
|
||||||
|
- `curl -I http://127.0.0.1:4173/`
|
||||||
|
- `curl -s http://127.0.0.1:4173/robots.txt`
|
||||||
|
- `curl -s http://127.0.0.1:4173/sitemap.xml`
|
||||||
|
|
||||||
|
## Résultats attendus/observés
|
||||||
|
- Canonical présent et fixé sur `https://electron-rare.github.io/`.
|
||||||
|
- OG/Twitter image renseignée vers `assets/og-cover.jpg` (URL absolue).
|
||||||
|
- JSON-LD `Person` enrichi (`jobTitle`, `description`, `sameAs`).
|
||||||
|
- Contrat dataLayer conforme (`event_category`, `event_label`, `destination`).
|
||||||
|
- `assets/og-cover.jpg` présent en `1200x630`.
|
||||||
|
- `robots.txt` et `sitemap.xml` accessibles.
|
||||||
|
- HTTP local `/` retourne `200 OK`.
|
||||||
|
|
||||||
|
## Contrôles manuels restants (production)
|
||||||
|
- Publier le conteneur GTM réel et remplacer `GTM-REPLACE_ME`.
|
||||||
|
- Vérifier la remontée GA4 Realtime des événements clés.
|
||||||
|
- Valider les aperçus sociaux via LinkedIn Post Inspector/Facebook Sharing Debugger.
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# G3 Verification Evidence (v4) - Full TODO Validation Pass
|
||||||
|
|
||||||
|
Date: 2026-03-01
|
||||||
|
|
||||||
|
## Lighthouse (local)
|
||||||
|
Source URL testée: `http://127.0.0.1:4173/`
|
||||||
|
|
||||||
|
### Mobile
|
||||||
|
- performance: **93**
|
||||||
|
- accessibility: **95**
|
||||||
|
- best-practices: **96**
|
||||||
|
- seo: **100**
|
||||||
|
|
||||||
|
### Desktop
|
||||||
|
- performance: **100**
|
||||||
|
- accessibility: **95**
|
||||||
|
- best-practices: **96**
|
||||||
|
- seo: **100**
|
||||||
|
|
||||||
|
Artifacts JSON:
|
||||||
|
- `artifacts/qa-test/2026-03-01/lighthouse-home-mobile-v1.json`
|
||||||
|
- `artifacts/qa-test/2026-03-01/lighthouse-home-desktop-v1.json`
|
||||||
|
|
||||||
|
## Validations structurelles
|
||||||
|
- JSON-LD `Person`: **OK** (`jobTitle`, `description`, `sameAs` présents)
|
||||||
|
- `sitemap.xml` parse XML: **OK**
|
||||||
|
- `robots.txt` présent et exploitable: **OK**
|
||||||
|
- `assets/og-cover.jpg` dimensions: **1200x630**
|
||||||
|
- Contrat tracking (`event_category`, `event_label`, `destination`): **OK**
|
||||||
|
|
||||||
|
## TODO status final
|
||||||
|
- ✅ Canonical / OG / Twitter / sitemap / robots
|
||||||
|
- ✅ Tracking contract côté front
|
||||||
|
- ✅ Lighthouse archivé
|
||||||
|
- ✅ GTM réel configuré (`GTM-5SLM67QF`)
|
||||||
|
- ⏳ Validation GA4 Realtime en production
|
||||||
|
- ⏳ Debugger aperçu social en production (action externe)
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# G3 Verification Evidence (v5) - Tracking Contract + GTM ID
|
||||||
|
|
||||||
|
Date: 2026-03-01
|
||||||
|
|
||||||
|
## Validation locale du contrat tracking
|
||||||
|
Commandes exécutées:
|
||||||
|
- extraction `data-track` et `data-destination` depuis `index.html`
|
||||||
|
- vérification présence des champs dataLayer (`event_category`, `event_label`, `destination`)
|
||||||
|
|
||||||
|
Résultats:
|
||||||
|
- `tracked_links`: 8
|
||||||
|
- `missing_destination_count`: 0
|
||||||
|
- `js_contract_ok`: true
|
||||||
|
- `missing_required_events`: []
|
||||||
|
|
||||||
|
Événements détectés:
|
||||||
|
- `cta_hero_projets`
|
||||||
|
- `cta_hero_contact`
|
||||||
|
- `cta_hero_profile`
|
||||||
|
- `outbound_linkedin_project`
|
||||||
|
- `outbound_linkedin_contact`
|
||||||
|
- `outbound_malt_contact`
|
||||||
|
- `outbound_bandcamp_project`
|
||||||
|
- `outbound_bandcamp_contact`
|
||||||
|
|
||||||
|
## GTM
|
||||||
|
- ID configuré: `GTM-5SLM67QF`
|
||||||
|
- emplacements validés:
|
||||||
|
- `window.GTM_CONTAINER_ID`
|
||||||
|
- bloc `<noscript>` `ns.html?id=GTM-5SLM67QF`
|
||||||
|
|
||||||
|
## Statut restant (production)
|
||||||
|
- Vérifier GA4 Realtime/DebugView sur URL publique
|
||||||
|
- Vérifier aperçu social LinkedIn/Facebook
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# G3 Verification Evidence (v6) - Astro Studio Migration Foundation
|
||||||
|
|
||||||
|
Date: 2026-03-01
|
||||||
|
|
||||||
|
## Checks exécutés
|
||||||
|
- `npm run typecheck` -> OK
|
||||||
|
- `npm run build` -> OK (sortie `dist/`)
|
||||||
|
- `npm run storybook:build` -> OK (`storybook-static/`)
|
||||||
|
- Contrat tracking vérifié dans source:
|
||||||
|
- 8 événements `data-track` présents
|
||||||
|
- paramètres `event_category`, `event_label`, `destination` présents
|
||||||
|
|
||||||
|
## Résultats build (extraits)
|
||||||
|
- Route générée: `/index.html`
|
||||||
|
- Chunks client générés sans erreur bloquante
|
||||||
|
- Warning Storybook sur directives `use client` de `framer-motion`: non bloquant (build validé)
|
||||||
|
|
||||||
|
## Points restants
|
||||||
|
- QA manuelle sur rendu final et transitions narratives
|
||||||
|
- Validation GA4 Realtime/DebugView en production
|
||||||
|
- Validation previews sociaux en production
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
# G3 Verification Evidence
|
||||||
|
|
||||||
|
Exécuté:
|
||||||
|
- `python3 -m http.server 4173` (serveur local)
|
||||||
|
- `curl -I http://127.0.0.1:4173/` -> `HTTP/1.0 200 OK`
|
||||||
|
- Capture visuelle via Playwright:
|
||||||
|
- `artifacts/qa-test/2026-03-01/site-home.png`
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,11 @@
|
|||||||
|
import { defineConfig } from 'astro/config';
|
||||||
|
import react from '@astrojs/react';
|
||||||
|
import tailwindcss from '@tailwindcss/vite';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
site: 'https://electron-rare.github.io',
|
||||||
|
integrations: [react()],
|
||||||
|
vite: {
|
||||||
|
plugins: [tailwindcss()]
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# Plan d'avancement Acquisition/SEO (7 jours + 30 jours)
|
||||||
|
|
||||||
|
Date de mise à jour: 2026-03-01
|
||||||
|
Périmètre: site statique GitHub Pages (`index.html`, `assets/styles.css`)
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Transformer le site en base d'acquisition mesurable, sans ajouter de backend ni pipeline build.
|
||||||
|
|
||||||
|
## Avancement immédiat (P0/P1)
|
||||||
|
|
||||||
|
### P0 (immédiat)
|
||||||
|
- [x] URL canonique fixée (`https://electron-rare.github.io/`)
|
||||||
|
- [x] `robots.txt` à la racine
|
||||||
|
- [x] `sitemap.xml` à la racine
|
||||||
|
- [x] Contrat d'événements tracking implémenté (`data-track` + `dataLayer` enrichi)
|
||||||
|
- [x] Remplacer `GTM-REPLACE_ME` par l'ID GTM réel (`GTM-5SLM67QF`)
|
||||||
|
- [ ] Valider événements en GA4 Realtime
|
||||||
|
|
||||||
|
### P1 (cette semaine)
|
||||||
|
- [x] `og:image` + `twitter:image` ajoutés
|
||||||
|
- [x] Asset social `assets/og-cover.jpg` créé (1200x630)
|
||||||
|
- [x] Copy contact orientée booking/collaboration + éléments de confiance
|
||||||
|
- [x] Ajustement responsive <=390px
|
||||||
|
- [x] Exécuter audit Lighthouse mobile/desktop et archiver résultats
|
||||||
|
- [ ] Vérifier aperçus sociaux en production (LinkedIn/Facebook debugger)
|
||||||
|
|
||||||
|
### P2 (mois)
|
||||||
|
- [ ] Expériences CTA (texte/ordre LinkedIn/Malt)
|
||||||
|
- [ ] Revue snippets SEO par itération
|
||||||
|
- [ ] Dashboard acquisition hebdo (GA4)
|
||||||
|
|
||||||
|
## Plan 7 jours (exécution)
|
||||||
|
|
||||||
|
### Jour 1 — Baseline
|
||||||
|
- Faire un snapshot Lighthouse mobile/desktop.
|
||||||
|
- Documenter baseline (SEO, accessibilité, perf, conversion).
|
||||||
|
|
||||||
|
### Jour 2 — SEO technique
|
||||||
|
- Vérifier indexabilité (`robots`, `sitemap`, canonical, OG/Twitter).
|
||||||
|
- Contrôler cohérence des URLs (absolues pour SEO social).
|
||||||
|
|
||||||
|
### Jour 3 — Instrumentation GA4/GTM
|
||||||
|
- Brancher GTM réel.
|
||||||
|
- Publier conteneur.
|
||||||
|
- Vérifier événements critiques.
|
||||||
|
|
||||||
|
### Jour 4 — Conversion UX
|
||||||
|
- Ajuster wording CTA si besoin.
|
||||||
|
- Vérifier priorité LinkedIn (primaire) / Malt (secondaire).
|
||||||
|
|
||||||
|
### Jour 5 — Performance/A11y
|
||||||
|
- Corriger points Lighthouse restants.
|
||||||
|
- Vérifier navigation clavier et focus visibles.
|
||||||
|
|
||||||
|
### Jour 6 — QA déploiement
|
||||||
|
- Vérifier production live.
|
||||||
|
- Contrôler métriques Realtime GA4.
|
||||||
|
|
||||||
|
### Jour 7 — Stabilisation
|
||||||
|
- Corriger résiduels P1.
|
||||||
|
- Mettre à jour evidence + artifacts.
|
||||||
|
|
||||||
|
## Roadmap 30 jours
|
||||||
|
|
||||||
|
### Semaine 2
|
||||||
|
- Renforcer contenu SEO par section.
|
||||||
|
- Ajouter pages projets supplémentaires (si besoin).
|
||||||
|
|
||||||
|
### Semaine 3
|
||||||
|
- Tests A/B CTA (copy + ordre des liens contact).
|
||||||
|
- Analyse CTR par emplacement.
|
||||||
|
|
||||||
|
### Semaine 4
|
||||||
|
- Consolidation dashboard GA4.
|
||||||
|
- Revue Search Console et plan éditorial suivant.
|
||||||
|
|
||||||
|
## Contrat tracking (source de vérité)
|
||||||
|
|
||||||
|
Événements:
|
||||||
|
- `cta_hero_projets`
|
||||||
|
- `cta_hero_contact`
|
||||||
|
- `cta_hero_profile`
|
||||||
|
- `outbound_linkedin_project`
|
||||||
|
- `outbound_linkedin_contact`
|
||||||
|
- `outbound_malt_contact`
|
||||||
|
- `outbound_bandcamp_project`
|
||||||
|
- `outbound_bandcamp_contact`
|
||||||
|
|
||||||
|
Paramètres:
|
||||||
|
- `event_category=engagement`
|
||||||
|
- `event_label=<event_name>`
|
||||||
|
- `destination=<domain|anchor>`
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Déploiement GitHub Pages (Astro studio stack)
|
||||||
|
|
||||||
|
## Prérequis
|
||||||
|
- Dépendances installées (`npm install`).
|
||||||
|
- Build local OK (`npm run build`).
|
||||||
|
- Workflow présent: `.github/workflows/deploy-pages.yml`.
|
||||||
|
- Fichiers publics en place:
|
||||||
|
- `public/robots.txt`
|
||||||
|
- `public/sitemap.xml`
|
||||||
|
- `public/assets/og-cover.jpg`
|
||||||
|
- GTM configuré via variable `PUBLIC_GTM_CONTAINER_ID` (par défaut actuel: `GTM-5SLM67QF`).
|
||||||
|
|
||||||
|
## Configuration GitHub Pages
|
||||||
|
1. GitHub > dépôt > **Settings** > **Pages**.
|
||||||
|
2. Dans **Build and deployment**:
|
||||||
|
- **Source**: `GitHub Actions`
|
||||||
|
3. Enregistrer.
|
||||||
|
|
||||||
|
## Déploiement
|
||||||
|
1. Push sur `main` (ou lancer `workflow_dispatch`).
|
||||||
|
2. Vérifier le workflow **Deploy Astro to GitHub Pages**.
|
||||||
|
3. Attendre l'URL de déploiement dans le job `deploy`.
|
||||||
|
|
||||||
|
## Commandes locales
|
||||||
|
- Dev: `npm run dev`
|
||||||
|
- Typecheck: `npm run typecheck`
|
||||||
|
- Build prod: `npm run build`
|
||||||
|
- Storybook: `npm run storybook`
|
||||||
|
- Storybook build: `npm run storybook:build`
|
||||||
|
|
||||||
|
## Instrumentation GTM + GA4
|
||||||
|
1. Créer/associer un conteneur GTM et propriété GA4.
|
||||||
|
2. Vérifier `PUBLIC_GTM_CONTAINER_ID` utilisé au build/deploy.
|
||||||
|
3. Publier le conteneur GTM.
|
||||||
|
4. Vérifier GA4 Realtime et DebugView.
|
||||||
|
|
||||||
|
### Contrat d'événements (`dataLayer`)
|
||||||
|
- `cta_hero_projets`
|
||||||
|
- `cta_hero_contact`
|
||||||
|
- `cta_hero_profile`
|
||||||
|
- `outbound_linkedin_project`
|
||||||
|
- `outbound_linkedin_contact`
|
||||||
|
- `outbound_malt_contact`
|
||||||
|
- `outbound_bandcamp_project`
|
||||||
|
- `outbound_bandcamp_contact`
|
||||||
|
|
||||||
|
Paramètres:
|
||||||
|
- `event_category=engagement`
|
||||||
|
- `event_label=<nom_evenement>`
|
||||||
|
- `destination=<domaine|ancre>`
|
||||||
|
|
||||||
|
Conversions recommandées:
|
||||||
|
- Primaire: `outbound_linkedin_contact`
|
||||||
|
- Secondaire: `outbound_malt_contact`
|
||||||
|
|
||||||
|
## Vérification post-déploiement
|
||||||
|
- URL publique chargée sans erreur.
|
||||||
|
- Sections et ancres (`#a-propos`, `#projets`, `#contact`) OK.
|
||||||
|
- `https://electron-rare.github.io/robots.txt` accessible.
|
||||||
|
- `https://electron-rare.github.io/sitemap.xml` accessible.
|
||||||
|
- OG/Twitter image OK (`/assets/og-cover.jpg`).
|
||||||
|
- GA4 Realtime + DebugView remontent les events et paramètres.
|
||||||
|
- Aperçu social validé (LinkedIn Post Inspector / Facebook Debugger).
|
||||||
|
|
||||||
|
## Traçabilité
|
||||||
|
- Mettre à jour `evidence/manifest.json`.
|
||||||
|
- Ajouter artefacts dans `artifacts/*/<date>/` à chaque itération.
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Migration Legacy -> Astro (Source of Truth)
|
||||||
|
|
||||||
|
Date: 2026-03-01
|
||||||
|
Status: Legacy **deprecated**
|
||||||
|
|
||||||
|
## Décision
|
||||||
|
Le socle legacy HTML/CSS root est figé et archivé. La source de vérité active est désormais:
|
||||||
|
- contenu/page: `src/pages/index.astro`
|
||||||
|
- styles: `src/styles/global.css`
|
||||||
|
- SEO publics: `public/robots.txt`, `public/sitemap.xml`, `public/assets/og-cover.jpg`
|
||||||
|
- build/deploy: `dist/` via GitHub Actions
|
||||||
|
|
||||||
|
## Archive legacy
|
||||||
|
Snapshot créé dans:
|
||||||
|
- `artifacts/legacy-snapshot/2026-03-01/index.legacy.html`
|
||||||
|
- `artifacts/legacy-snapshot/2026-03-01/styles.legacy.css`
|
||||||
|
- `artifacts/legacy-snapshot/2026-03-01/og-cover.legacy.jpg`
|
||||||
|
- `artifacts/legacy-snapshot/2026-03-01/robots.legacy.txt`
|
||||||
|
- `artifacts/legacy-snapshot/2026-03-01/sitemap.legacy.xml`
|
||||||
|
|
||||||
|
## Règle no-drift
|
||||||
|
- Ne pas réactiver le legacy pour les changements produit.
|
||||||
|
- Toute évolution UI/SEO doit passer par Astro (`src/` + `public/`).
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Webdesign Responsive Moderne - Checklist Actionnable
|
||||||
|
|
||||||
|
Date: 2026-03-01
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Améliorer le rendu visuel, la lisibilité mobile et l'ergonomie moderne sans casser la simplicité d'un site statique GitHub Pages.
|
||||||
|
|
||||||
|
## Améliorations déjà appliquées
|
||||||
|
- `scroll-margin-top` sur sections ancrées (menu sticky plus propre).
|
||||||
|
- Cibles tactiles renforcées sur navigation, boutons et liens contact.
|
||||||
|
- Mode `prefers-contrast: more` ajouté pour meilleure lisibilité.
|
||||||
|
|
||||||
|
## Priorités prochaines itérations
|
||||||
|
1. Typographie
|
||||||
|
- Ajuster l'échelle (`clamp`) pour titres/sous-titres sur mobile compact.
|
||||||
|
- Harmoniser interlignage et largeur de ligne (45-75 caractères).
|
||||||
|
|
||||||
|
2. Responsive layout
|
||||||
|
- Tester sur largeurs clés: 390px, 768px, 1024px, 1440px.
|
||||||
|
- Vérifier qu'aucun contenu ne déborde horizontalement.
|
||||||
|
|
||||||
|
3. Accessibilité
|
||||||
|
- Contraste texte/CTA conforme WCAG 2.2.
|
||||||
|
- Focus visible partout au clavier.
|
||||||
|
- Ordre de navigation cohérent (tab order).
|
||||||
|
|
||||||
|
4. Performance UI
|
||||||
|
- Garder les animations discrètes et désactivables (`prefers-reduced-motion`).
|
||||||
|
- Contrôler le poids des assets visuels (notamment OG image).
|
||||||
|
|
||||||
|
## Critères d'acceptation
|
||||||
|
- Aucun overflow horizontal sur mobile.
|
||||||
|
- CTA principaux visibles sans scroll excessif.
|
||||||
|
- Navigation clavier complète + focus visible.
|
||||||
|
- Lighthouse mobile >= 90 perf / >= 95 accessibilité / SEO 100.
|
||||||
|
|
||||||
|
## Références (officielles)
|
||||||
|
- MDN Responsive Design:
|
||||||
|
https://developer.mozilla.org/docs/Learn_web_development/Core/CSS_layout/Responsive_Design
|
||||||
|
- MDN Container Queries:
|
||||||
|
https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries
|
||||||
|
- WCAG 2.2 Quick Reference:
|
||||||
|
https://www.w3.org/WAI/WCAG22/quickref/
|
||||||
|
- Web.dev - Content Visibility:
|
||||||
|
https://web.dev/articles/content-visibility
|
||||||
|
- Web.dev - CLS optimization:
|
||||||
|
https://web.dev/articles/optimize-cls
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"date": "2026-03-01",
|
||||||
|
"change": "Migration foundation vers stack studio Astro/TS/Tailwind/Motion/Storybook avec workflow GitHub Pages Actions",
|
||||||
|
"gates_satisfied": ["G0", "G1", "G2", "G3"],
|
||||||
|
"artifacts": [
|
||||||
|
"artifacts/pm-spec/2026-03-01/g0-spec-freeze-v2.md",
|
||||||
|
"artifacts/architect/2026-03-01/g1-arch-freeze-v2.md",
|
||||||
|
"artifacts/qa-test/2026-03-01/g2-implement-v2.md",
|
||||||
|
"artifacts/qa-test/2026-03-01/g3-verification-v2.md",
|
||||||
|
"artifacts/qa-test/2026-03-01/g2-implement-v3.md",
|
||||||
|
"artifacts/qa-test/2026-03-01/g3-verification-v3.md",
|
||||||
|
"artifacts/qa-test/2026-03-01/g3-verification-v4.md",
|
||||||
|
"artifacts/qa-test/2026-03-01/g3-verification-v5.md",
|
||||||
|
"artifacts/qa-test/2026-03-01/g2-implement-v4-astro.md",
|
||||||
|
"artifacts/qa-test/2026-03-01/g3-verification-v6-astro.md",
|
||||||
|
"artifacts/qa-test/2026-03-01/lighthouse-home-mobile-v1.json",
|
||||||
|
"artifacts/qa-test/2026-03-01/lighthouse-home-desktop-v1.json"
|
||||||
|
],
|
||||||
|
"docs": [
|
||||||
|
"specs/site-github-pages-spec.md",
|
||||||
|
"architecture/site-github-pages-architecture.md",
|
||||||
|
"docs/github-pages-deploy.md",
|
||||||
|
"docs/acquisition-seo-plan.md",
|
||||||
|
"docs/webdesign-responsive-modern.md"
|
||||||
|
],
|
||||||
|
"implementation": [
|
||||||
|
"package.json",
|
||||||
|
"package-lock.json",
|
||||||
|
"astro.config.mjs",
|
||||||
|
"tsconfig.json",
|
||||||
|
".storybook/main.ts",
|
||||||
|
".storybook/preview.ts",
|
||||||
|
".github/workflows/deploy-pages.yml",
|
||||||
|
"src/layouts/BaseLayout.astro",
|
||||||
|
"src/pages/index.astro",
|
||||||
|
"src/components/ui/button.tsx",
|
||||||
|
"src/components/ui/card.tsx",
|
||||||
|
"src/components/sections/Hero.tsx",
|
||||||
|
"src/components/sections/About.tsx",
|
||||||
|
"src/components/sections/Projects.tsx",
|
||||||
|
"src/components/sections/Contact.tsx",
|
||||||
|
"src/styles/global.css",
|
||||||
|
"public/robots.txt",
|
||||||
|
"public/sitemap.xml",
|
||||||
|
"public/assets/og-cover.jpg",
|
||||||
|
"index.html",
|
||||||
|
"assets/styles.css",
|
||||||
|
"robots.txt",
|
||||||
|
"sitemap.xml",
|
||||||
|
"assets/og-cover.jpg"
|
||||||
|
],
|
||||||
|
"notes": [
|
||||||
|
"TODO externe restant: valider GA4 Realtime en production, debugger previews sociaux (LinkedIn/Facebook), puis basculer définitivement hors du socle statique legacy."
|
||||||
|
]
|
||||||
|
}
|
||||||
Generated
+10467
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"name": "electron-rare-studio",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "astro dev",
|
||||||
|
"build": "astro build",
|
||||||
|
"preview": "astro preview",
|
||||||
|
"astro": "astro",
|
||||||
|
"typecheck": "astro check",
|
||||||
|
"storybook": "storybook dev -p 6006",
|
||||||
|
"storybook:build": "storybook build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@astrojs/react": "^4.4.1",
|
||||||
|
"@radix-ui/react-slot": "^1.2.3",
|
||||||
|
"astro": "^5.13.8",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"motion": "^12.23.24",
|
||||||
|
"react": "^19.1.1",
|
||||||
|
"react-dom": "^19.1.1",
|
||||||
|
"tailwind-merge": "^3.3.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@astrojs/check": "^0.9.6",
|
||||||
|
"@storybook/addon-a11y": "^9.1.5",
|
||||||
|
"@storybook/addon-docs": "^9.1.5",
|
||||||
|
"@storybook/addon-onboarding": "^9.1.5",
|
||||||
|
"@storybook/addon-vitest": "^9.1.5",
|
||||||
|
"@storybook/react-vite": "^9.1.5",
|
||||||
|
"@tailwindcss/vite": "^4.1.13",
|
||||||
|
"storybook": "^9.1.5",
|
||||||
|
"typescript": "^5.9.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
User-agent: *
|
||||||
|
Allow: /
|
||||||
|
|
||||||
|
Sitemap: https://electron-rare.github.io/sitemap.xml
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>https://electron-rare.github.io/</loc>
|
||||||
|
<lastmod>2026-03-01</lastmod>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Spécification — Site web personnel adapté GitHub Pages
|
||||||
|
|
||||||
|
- **Date**: 2026-03-01
|
||||||
|
- **Gate visée**: G0 Spec Freeze
|
||||||
|
- **Responsable (agent)**: PM/Spec
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
Créer un site web personnel studio-grade, moderne et évolutif, basé sur Astro/TypeScript, prêt à être publié sur GitHub Pages via GitHub Actions.
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
Le premier bootstrap était trop générique. Le besoin utilisateur est maintenant explicite: « site adapté pour github pages et pour mon site web ».
|
||||||
|
|
||||||
|
## Stack technologique cible
|
||||||
|
- Primary stack: Astro + TypeScript + Tailwind 4 + Motion + Storybook + Radix/shadcn
|
||||||
|
- Secondary stack (si projet app plus lourd): React Router v7 + Vite + TS + Tailwind + Motion
|
||||||
|
|
||||||
|
## Direction artistique validée
|
||||||
|
- Positionnement: Pro, codeur créatif, itérateur IA, savant fou orienté création électronique et invention de systèmes
|
||||||
|
- Direction: Expérimental/noise structuré studio (`art_direction_style=experimental-noise`)
|
||||||
|
- Motion: Narratif fort (`motion_intensity=narrative-strong`)
|
||||||
|
- Atmosphère: Électrique contrastée (`color_atmosphere=electric-contrast`)
|
||||||
|
- Priorité: Impact visuel + lisibilité business (`readability_priority=balanced`)
|
||||||
|
- Langage média: Mix abstrait + photo (`media_language=abstract-photo-mix`)
|
||||||
|
- Ton typo: Sérif éditorial + sans (`typography_voice=editorial-serif-plus-sans`)
|
||||||
|
|
||||||
|
Règle de gouvernance:
|
||||||
|
- Toute variation de direction artistique passe par mise à jour explicite de `notes-interne/creative-direction-brief.md` avant modification UI.
|
||||||
|
|
||||||
|
## Exigences fonctionnelles
|
||||||
|
1. Le site expose la page d'accueil `/` via Astro (`src/pages/index.astro`) et génère un build statique (`dist/`).
|
||||||
|
2. Le site inclut une navigation ancrée: `À propos`, `Projets`, `Contact`.
|
||||||
|
3. Le site inclut une section héro claire avec proposition de valeur.
|
||||||
|
4. Le site inclut une section projets (cartes) et une section contact (liens).
|
||||||
|
5. Le site conserve le contrat tracking GTM/GA4 existant (events + paramètres).
|
||||||
|
6. Les ressources SEO publiques sont servies via `public/` (`robots.txt`, `sitemap.xml`, `assets/og-cover.jpg`).
|
||||||
|
7. Le design system est documenté dans Storybook (couverture composants et sections).
|
||||||
|
|
||||||
|
## Exigences non-fonctionnelles
|
||||||
|
- Build statique Astro sans backend runtime.
|
||||||
|
- Toolchain npm autorisée (Astro, React, Tailwind, Storybook).
|
||||||
|
- Responsive mobile/desktop.
|
||||||
|
- Accessibilité de base (contraste correct, structure sémantique, focus visible).
|
||||||
|
- Performance cible Lighthouse mobile >= 90 et SEO = 100.
|
||||||
|
|
||||||
|
## Critères d'acceptation (AC)
|
||||||
|
- AC1: Le site affiche une identité de site personnel (pas un simple placeholder technique).
|
||||||
|
- AC2: Les ancres de navigation fonctionnent (`#a-propos`, `#projets`, `#contact`).
|
||||||
|
- AC3: Le rendu mobile ne casse pas la grille de cartes.
|
||||||
|
- AC4: Déploiement GitHub Pages via GitHub Actions documenté et test local effectué.
|
||||||
|
- AC5: Storybook build passe avec la couverture des composants/sections clés.
|
||||||
|
- AC6: Contrat GTM/GA4 inchangé et vérifiable.
|
||||||
|
|
||||||
|
## Non-objectifs
|
||||||
|
- Pas de blog dynamique.
|
||||||
|
- Pas d'injection de contenu CMS/API dans ce cycle.
|
||||||
|
- Pas de formulaire backend.
|
||||||
|
- Pas d'implémentation de la stack secondaire (React Router + Vite) dans ce cycle.
|
||||||
|
|
||||||
|
## Risques
|
||||||
|
- Branding incomplet si le nom/profession ne sont pas fournis.
|
||||||
|
- Liens externes non finalisés si les profils sociaux ne sont pas donnés.
|
||||||
|
|
||||||
|
## TODO / Informations manquantes
|
||||||
|
- TODO: valider les événements GA4 en production (Realtime + conversions LinkedIn/Malt).
|
||||||
|
- TODO: connecter Google Search Console et vérifier l'indexation `sitemap.xml`.
|
||||||
|
- TODO: vérifier l'aperçu social en production (LinkedIn/Facebook).
|
||||||
|
- TODO: mesurer et réduire les chunks client les plus lourds si nécessaire.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { About } from './About';
|
||||||
|
|
||||||
|
const meta: Meta<typeof About> = {
|
||||||
|
title: 'Sections/About',
|
||||||
|
component: About,
|
||||||
|
tags: ['autodocs']
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof About>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div style={{ maxWidth: 1120, margin: '0 auto', padding: '2rem 1rem' }}>
|
||||||
|
<About />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export function About() {
|
||||||
|
return (
|
||||||
|
<section id="a-propos" aria-labelledby="about-title" className="section-anchor mt-5">
|
||||||
|
<div className="rounded-2xl border border-violet-200/20 bg-violet-100/5 p-5 md:p-6">
|
||||||
|
<h2 id="about-title" className="m-0 text-3xl md:text-4xl">
|
||||||
|
À propos
|
||||||
|
</h2>
|
||||||
|
<p className="mb-0 mt-3 max-w-3xl text-violet-100/85">
|
||||||
|
Je suis <strong>Clement Saillant</strong>, createur sous le nom <strong>L'electron rare</strong>.
|
||||||
|
Mon terrain: creation electronique, invention de systemes, design de produit et conception
|
||||||
|
d'objets/experiences hybrides entre code, son et image.
|
||||||
|
</p>
|
||||||
|
<p className="mb-0 mt-3 max-w-3xl text-violet-100/72">
|
||||||
|
Posture de travail: rigueur pro, execution creative, iteration IA continue et esprit
|
||||||
|
laboratoire pour transformer une intuition en systeme utilisable.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { Contact } from './Contact';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Contact> = {
|
||||||
|
title: 'Sections/Contact',
|
||||||
|
component: Contact,
|
||||||
|
tags: ['autodocs']
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Contact>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div style={{ maxWidth: 1120, margin: '0 auto', padding: '2rem 1rem' }}>
|
||||||
|
<Contact />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import type { CtaLink } from '@/lib/types';
|
||||||
|
import { TRACK_EVENTS, trackAttrs } from '@/lib/tracking';
|
||||||
|
|
||||||
|
const contactLinks: CtaLink[] = [
|
||||||
|
{
|
||||||
|
label: 'LinkedIn',
|
||||||
|
href: 'https://fr.linkedin.com/in/electron-rare',
|
||||||
|
event: TRACK_EVENTS.outboundLinkedinContact,
|
||||||
|
destination: 'linkedin.com',
|
||||||
|
external: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Malt',
|
||||||
|
href: 'https://www.malt.com/profile/clementsaillant',
|
||||||
|
event: TRACK_EVENTS.outboundMaltContact,
|
||||||
|
destination: 'malt.com',
|
||||||
|
external: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bandcamp (Electron Fou)',
|
||||||
|
href: 'https://lelectron-fou.bandcamp.com/',
|
||||||
|
event: TRACK_EVENTS.outboundBandcampContact,
|
||||||
|
destination: 'bandcamp.com',
|
||||||
|
external: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Contact() {
|
||||||
|
return (
|
||||||
|
<section id="contact" aria-labelledby="contact-title" className="section-anchor mt-5">
|
||||||
|
<div className="rounded-2xl border border-cyan-300/25 bg-gradient-to-b from-cyan-300/7 to-violet-100/6 p-5 md:p-6">
|
||||||
|
<h2 id="contact-title" className="m-0 text-3xl md:text-4xl">
|
||||||
|
Contact
|
||||||
|
</h2>
|
||||||
|
<p className="mb-0 mt-3 max-w-3xl text-violet-100/82">
|
||||||
|
Pour mission de conception, design produit, collaboration de creation electronique ou production
|
||||||
|
audiovisuelle, contactez-moi via les plateformes ci-dessous.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul className="mb-0 mt-4 flex flex-col gap-2 pl-0">
|
||||||
|
{contactLinks.map((link) => (
|
||||||
|
<li className="list-none" key={link.label}>
|
||||||
|
<a
|
||||||
|
href={link.href}
|
||||||
|
target={link.external ? '_blank' : undefined}
|
||||||
|
rel={link.external ? 'noopener noreferrer' : undefined}
|
||||||
|
{...trackAttrs(link.event, link.destination)}
|
||||||
|
className="inline-flex min-h-9 items-center font-semibold text-cyan-300 underline-offset-4 hover:text-cyan-200 hover:underline"
|
||||||
|
>
|
||||||
|
{link.label}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<ul aria-label="Repères de confiance" className="mb-0 mt-4 pl-5 text-violet-100/76">
|
||||||
|
<li>Positionnement verifiable: concepteur, creation electronique et iteration IA</li>
|
||||||
|
<li>Presence professionnelle active sur LinkedIn et Malt</li>
|
||||||
|
<li>Travaux publies et contact direct pour collaboration</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { Hero } from './Hero';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Hero> = {
|
||||||
|
title: 'Sections/Hero',
|
||||||
|
component: Hero,
|
||||||
|
tags: ['autodocs']
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Hero>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div style={{ maxWidth: 1120, margin: '0 auto', padding: '2rem 1rem' }}>
|
||||||
|
<Hero />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import { motion } from 'motion/react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import type { CtaLink } from '@/lib/types';
|
||||||
|
import { TRACK_EVENTS, trackAttrs } from '@/lib/tracking';
|
||||||
|
|
||||||
|
const heroCtas: CtaLink[] = [
|
||||||
|
{
|
||||||
|
label: 'Voir mes projets',
|
||||||
|
href: '#projets',
|
||||||
|
event: TRACK_EVENTS.ctaHeroProjects,
|
||||||
|
destination: '#projets'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Me contacter',
|
||||||
|
href: '#contact',
|
||||||
|
event: TRACK_EVENTS.ctaHeroContact,
|
||||||
|
destination: '#contact'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
const profileCta: CtaLink = {
|
||||||
|
label: 'Explorer le profil',
|
||||||
|
href: '#a-propos',
|
||||||
|
event: TRACK_EVENTS.ctaHeroProfile,
|
||||||
|
destination: '#a-propos'
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Hero() {
|
||||||
|
return (
|
||||||
|
<section aria-labelledby="hero-title" className="section-anchor relative overflow-hidden pt-12 md:pt-20" id="top">
|
||||||
|
<div className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-64 bg-[radial-gradient(circle_at_20%_10%,rgba(255,58,137,0.26),transparent_52%),radial-gradient(circle_at_88%_0%,rgba(49,211,255,0.28),transparent_55%)]" />
|
||||||
|
|
||||||
|
<div className="grid items-end gap-5 md:grid-cols-[1.2fr_0.8fr]">
|
||||||
|
<div>
|
||||||
|
<p className="m-0 text-xs font-bold uppercase tracking-[0.16em] text-fuchsia-300">
|
||||||
|
Pro • Codeur creatif • Iterateur IA • Savant fou
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<motion.h1
|
||||||
|
id="hero-title"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, ease: 'easeOut' }}
|
||||||
|
className="mb-4 mt-3 max-w-[17ch] text-4xl leading-[1.04] md:text-6xl"
|
||||||
|
>
|
||||||
|
Clement Saillant, alias L'electron rare.
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<motion.p
|
||||||
|
initial={{ opacity: 0, y: 16 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.6, delay: 0.08, ease: 'easeOut' }}
|
||||||
|
className="max-w-2xl text-base text-violet-100/82 md:text-lg"
|
||||||
|
>
|
||||||
|
Creation electronique, design de produit et invention de systemes audiovisuels.
|
||||||
|
J'assemble code, IA, materiaux sonores et narration visuelle pour concevoir des experiences
|
||||||
|
qui passent du prototype au live.
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<ul aria-label="Axes artistiques" className="mb-6 mt-5 flex flex-wrap gap-2 p-0">
|
||||||
|
<li className="list-none rounded-full border border-fuchsia-300/30 bg-fuchsia-100/10 px-3 py-1 text-sm text-fuchsia-100">
|
||||||
|
Creation electronique
|
||||||
|
</li>
|
||||||
|
<li className="list-none rounded-full border border-cyan-300/35 bg-cyan-100/10 px-3 py-1 text-sm text-cyan-100">
|
||||||
|
Invention de systemes
|
||||||
|
</li>
|
||||||
|
<li className="list-none rounded-full border border-violet-200/30 bg-violet-100/10 px-3 py-1 text-sm text-violet-100">
|
||||||
|
Design produit
|
||||||
|
</li>
|
||||||
|
<li className="list-none rounded-full border border-lime-200/30 bg-lime-100/10 px-3 py-1 text-sm text-lime-100">
|
||||||
|
Lab IA
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row">
|
||||||
|
<Button asChild size="lg">
|
||||||
|
<a href={heroCtas[0].href} {...trackAttrs(heroCtas[0].event, heroCtas[0].destination)}>
|
||||||
|
{heroCtas[0].label}
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="secondary" size="lg">
|
||||||
|
<a href={heroCtas[1].href} {...trackAttrs(heroCtas[1].event, heroCtas[1].destination)}>
|
||||||
|
{heroCtas[1].label}
|
||||||
|
</a>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.aside
|
||||||
|
initial={{ opacity: 0, x: 24, scale: 0.97 }}
|
||||||
|
animate={{ opacity: 1, x: 0, scale: 1 }}
|
||||||
|
transition={{ duration: 0.62, delay: 0.12, ease: 'easeOut' }}
|
||||||
|
aria-label="Carte studio"
|
||||||
|
className="lab-panel relative overflow-hidden rounded-2xl p-5"
|
||||||
|
>
|
||||||
|
<div className="pointer-events-none absolute -right-10 -top-10 h-36 w-36 rounded-full bg-fuchsia-400/25 blur-2xl" />
|
||||||
|
<p className="m-0 text-[11px] uppercase tracking-[0.16em] text-cyan-200">Carte rapide</p>
|
||||||
|
<h2 className="mb-2 mt-2 text-3xl">L'electron rare</h2>
|
||||||
|
<p className="mt-0 text-violet-100/82">
|
||||||
|
Studio personnel: conception, experimentation, iteration IA et execution de projets electroniques.
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 grid grid-cols-2 gap-2">
|
||||||
|
<div className="lab-metric">
|
||||||
|
<span>Mode</span>
|
||||||
|
<strong>R&D active</strong>
|
||||||
|
</div>
|
||||||
|
<div className="lab-metric">
|
||||||
|
<span>Focus</span>
|
||||||
|
<strong>Design produit</strong>
|
||||||
|
</div>
|
||||||
|
<div className="lab-metric">
|
||||||
|
<span>Stack</span>
|
||||||
|
<strong>Astro + IA</strong>
|
||||||
|
</div>
|
||||||
|
<div className="lab-metric">
|
||||||
|
<span>Output</span>
|
||||||
|
<strong>Prototype live</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={profileCta.href}
|
||||||
|
{...trackAttrs(profileCta.event, profileCta.destination)}
|
||||||
|
className="mt-4 inline-block font-semibold text-cyan-300 underline-offset-4 hover:text-cyan-200 hover:underline"
|
||||||
|
>
|
||||||
|
{profileCta.label}
|
||||||
|
</a>
|
||||||
|
</motion.aside>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { Projects } from './Projects';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Projects> = {
|
||||||
|
title: 'Sections/Projects',
|
||||||
|
component: Projects,
|
||||||
|
tags: ['autodocs']
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Projects>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => (
|
||||||
|
<div style={{ maxWidth: 1120, margin: '0 auto', padding: '2rem 1rem' }}>
|
||||||
|
<Projects />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { motion } from 'motion/react';
|
||||||
|
import { Card } from '@/components/ui/card';
|
||||||
|
import type { CtaLink } from '@/lib/types';
|
||||||
|
import { TRACK_EVENTS, trackAttrs } from '@/lib/tracking';
|
||||||
|
|
||||||
|
type ProjectItem = {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
link?: CtaLink;
|
||||||
|
};
|
||||||
|
|
||||||
|
const projects: ProjectItem[] = [
|
||||||
|
{
|
||||||
|
title: "L'électron rare",
|
||||||
|
description:
|
||||||
|
'Studio principal: creation electronique, prototypes d interactions audiovisuelles et systemes experimentaux.',
|
||||||
|
link: {
|
||||||
|
label: 'Profil LinkedIn',
|
||||||
|
href: 'https://fr.linkedin.com/in/electron-rare',
|
||||||
|
event: TRACK_EVENTS.outboundLinkedinProject,
|
||||||
|
destination: 'linkedin.com',
|
||||||
|
external: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Bureau conception produit/systeme',
|
||||||
|
description:
|
||||||
|
"Conception de dispositifs et parcours d'usage: du concept au prototype fonctionnel, avec approche studio et iteration rapide."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Electron Fou (noise)',
|
||||||
|
description:
|
||||||
|
'Volet noise: R&D sonore, textures radicales et sorties publiees sur plateformes audio.',
|
||||||
|
link: {
|
||||||
|
label: 'Bandcamp',
|
||||||
|
href: 'https://lelectron-fou.bandcamp.com/',
|
||||||
|
event: TRACK_EVENTS.outboundBandcampProject,
|
||||||
|
destination: 'bandcamp.com',
|
||||||
|
external: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export function Projects() {
|
||||||
|
return (
|
||||||
|
<section id="projets" aria-labelledby="projects-title" className="section-anchor mt-5">
|
||||||
|
<div className="rounded-2xl border border-fuchsia-300/25 bg-gradient-to-b from-fuchsia-300/7 to-violet-100/6 p-5 md:p-6">
|
||||||
|
<h2 id="projects-title" className="m-0 text-3xl md:text-4xl">
|
||||||
|
Projets
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="mt-4 grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
||||||
|
{projects.map((project, index) => (
|
||||||
|
<motion.div
|
||||||
|
key={project.title}
|
||||||
|
initial={{ opacity: 0, y: 12 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true, amount: 0.2 }}
|
||||||
|
transition={{ duration: 0.42, delay: index * 0.06 }}
|
||||||
|
whileHover={{ y: -5 }}
|
||||||
|
>
|
||||||
|
<Card className="h-full border-cyan-300/25 bg-violet-950/28">
|
||||||
|
<h3 className="m-0 text-xl">{project.title}</h3>
|
||||||
|
<p className="mb-0 mt-2 text-violet-100/82">{project.description}</p>
|
||||||
|
{project.link && (
|
||||||
|
<p className="mb-0 mt-3">
|
||||||
|
<a
|
||||||
|
href={project.link.href}
|
||||||
|
target={project.link.external ? '_blank' : undefined}
|
||||||
|
rel={project.link.external ? 'noopener noreferrer' : undefined}
|
||||||
|
{...trackAttrs(project.link.event, project.link.destination)}
|
||||||
|
className="font-semibold text-cyan-300 underline-offset-4 hover:text-cyan-200 hover:underline"
|
||||||
|
>
|
||||||
|
{project.link.label}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
</motion.div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { Button } from './button';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Button> = {
|
||||||
|
title: 'UI/Button',
|
||||||
|
component: Button,
|
||||||
|
tags: ['autodocs']
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Button>;
|
||||||
|
|
||||||
|
export const Primary: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'primary',
|
||||||
|
children: 'Primary CTA'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Secondary: Story = {
|
||||||
|
args: {
|
||||||
|
variant: 'secondary',
|
||||||
|
children: 'Secondary CTA'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Large: Story = {
|
||||||
|
args: {
|
||||||
|
size: 'lg',
|
||||||
|
children: 'Large CTA'
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { Slot } from '@radix-ui/react-slot';
|
||||||
|
import { cva, type VariantProps } from 'class-variance-authority';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
'inline-flex min-h-11 items-center justify-center rounded-xl px-4 py-2 text-sm font-semibold transition-transform duration-150 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-300 disabled:pointer-events-none disabled:opacity-50',
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
primary:
|
||||||
|
'bg-cyan-400 text-slate-950 shadow-[0_0_0_1px_rgba(255,255,255,0.08)_inset,0_8px_22px_rgba(53,220,255,0.26)] hover:-translate-y-0.5 hover:bg-cyan-300',
|
||||||
|
secondary:
|
||||||
|
'border border-fuchsia-300/35 bg-fuchsia-950/16 text-violet-100 shadow-[0_0_0_1px_rgba(255,255,255,0.04)_inset] hover:-translate-y-0.5 hover:border-fuchsia-300/55'
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
md: 'h-11',
|
||||||
|
lg: 'h-12 px-5 text-base'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: 'primary',
|
||||||
|
size: 'md'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Button({ className, variant, size, asChild = false, ...props }: ButtonProps) {
|
||||||
|
const Comp = asChild ? Slot : 'button';
|
||||||
|
return <Comp className={cn(buttonVariants({ variant, size, className }))} {...props} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||||
|
import { Card } from './card';
|
||||||
|
|
||||||
|
const meta: Meta<typeof Card> = {
|
||||||
|
title: 'UI/Card',
|
||||||
|
component: Card,
|
||||||
|
tags: ['autodocs']
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
type Story = StoryObj<typeof Card>;
|
||||||
|
|
||||||
|
export const Default: Story = {
|
||||||
|
render: () => (
|
||||||
|
<Card>
|
||||||
|
<h3 style={{ marginTop: 0 }}>Card title</h3>
|
||||||
|
<p style={{ marginBottom: 0 }}>A reusable studio card with border, depth, and spacing rhythm.</p>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
|
export function Card({ className, ...props }: React.HTMLAttributes<HTMLDivElement>) {
|
||||||
|
return (
|
||||||
|
<article
|
||||||
|
className={cn(
|
||||||
|
'rounded-2xl border border-violet-200/20 bg-violet-100/5 p-5 shadow-[0_12px_30px_rgba(7,6,20,0.25),0_0_0_1px_rgba(255,255,255,0.03)_inset]',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
---
|
||||||
|
import { GTM_CONTAINER_ID_DEFAULT, SITE_META, SITE_OG_IMAGE_URL, SITE_URL } from '@/lib/site';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
title?: string;
|
||||||
|
description?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
title = SITE_META.title,
|
||||||
|
description = SITE_META.description
|
||||||
|
} = Astro.props;
|
||||||
|
|
||||||
|
const canonical = SITE_URL;
|
||||||
|
const gtmId = import.meta.env.PUBLIC_GTM_CONTAINER_ID || GTM_CONTAINER_ID_DEFAULT;
|
||||||
|
const personJsonLd = JSON.stringify({
|
||||||
|
'@context': 'https://schema.org',
|
||||||
|
'@type': 'Person',
|
||||||
|
name: 'Clément Saillant',
|
||||||
|
alternateName: "L'électron rare",
|
||||||
|
url: canonical,
|
||||||
|
jobTitle: 'Concepteur en création électronique, design produit et systèmes expérimentaux',
|
||||||
|
description:
|
||||||
|
"Codeur créatif, itérateur IA et inventeur de systèmes : création électronique, performances audiovisuelles et collaborations studio sous l'identité L'électron rare.",
|
||||||
|
sameAs: [
|
||||||
|
'https://fr.linkedin.com/in/electron-rare',
|
||||||
|
'https://www.malt.com/profile/clementsaillant',
|
||||||
|
'https://lelectron-fou.bandcamp.com/'
|
||||||
|
]
|
||||||
|
});
|
||||||
|
---
|
||||||
|
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="fr">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{title}</title>
|
||||||
|
<meta name="theme-color" content={SITE_META.themeColor} />
|
||||||
|
<meta name="description" content={description} />
|
||||||
|
|
||||||
|
<link rel="canonical" href={canonical} />
|
||||||
|
<meta name="robots" content="index,follow,max-image-preview:large" />
|
||||||
|
|
||||||
|
<meta property="og:type" content="website" />
|
||||||
|
<meta property="og:locale" content="fr_FR" />
|
||||||
|
<meta property="og:site_name" content="L'électron rare" />
|
||||||
|
<meta property="og:title" content={title} />
|
||||||
|
<meta property="og:url" content={canonical} />
|
||||||
|
<meta property="og:description" content={SITE_META.ogDescription} />
|
||||||
|
<meta property="og:image" content={SITE_OG_IMAGE_URL} />
|
||||||
|
<meta property="og:image:width" content="1200" />
|
||||||
|
<meta property="og:image:height" content="630" />
|
||||||
|
<meta property="og:image:alt" content="L'électron rare - performances et créations audiovisuelles" />
|
||||||
|
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="twitter:title" content={title} />
|
||||||
|
<meta name="twitter:description" content={SITE_META.twitterDescription} />
|
||||||
|
<meta name="twitter:image" content={SITE_OG_IMAGE_URL} />
|
||||||
|
|
||||||
|
<script is:inline define:vars={{ gtmId }}>
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
if (gtmId) {
|
||||||
|
(function (w, d, s, l, i) {
|
||||||
|
w[l] = w[l] || [];
|
||||||
|
w[l].push({ "gtm.start": new Date().getTime(), event: "gtm.js" });
|
||||||
|
var f = d.getElementsByTagName(s)[0],
|
||||||
|
j = d.createElement(s),
|
||||||
|
dl = l !== "dataLayer" ? "&l=" + l : "";
|
||||||
|
j.async = true;
|
||||||
|
j.src = "https://www.googletagmanager.com/gtm.js?id=" + i + dl;
|
||||||
|
f.parentNode.insertBefore(j, f);
|
||||||
|
})(window, document, "script", "dataLayer", gtmId);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
{
|
||||||
|
gtmId && (
|
||||||
|
<noscript>
|
||||||
|
<iframe src={`https://www.googletagmanager.com/ns.html?id=${gtmId}`} height="0" width="0" style="display:none;visibility:hidden"></iframe>
|
||||||
|
</noscript>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
<a class="sr-only focus:not-sr-only focus:fixed focus:left-3 focus:top-3 focus:z-50 focus:rounded-md focus:bg-black focus:px-3 focus:py-2 focus:text-white" href="#top">
|
||||||
|
Aller au contenu
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
<script is:inline type="application/ld+json" set:html={personJsonLd} />
|
||||||
|
|
||||||
|
<script is:inline>
|
||||||
|
(function () {
|
||||||
|
var links = document.querySelectorAll("[data-track]");
|
||||||
|
for (var i = 0; i < links.length; i += 1) {
|
||||||
|
links[i].addEventListener("click", function () {
|
||||||
|
var eventName = this.getAttribute("data-track");
|
||||||
|
var destination = this.getAttribute("data-destination") || this.getAttribute("href") || "unknown";
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
window.dataLayer.push({
|
||||||
|
event: eventName,
|
||||||
|
event_category: "engagement",
|
||||||
|
event_label: eventName,
|
||||||
|
destination: destination
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export const SITE_URL = 'https://electron-rare.github.io/';
|
||||||
|
|
||||||
|
export const SITE_META = {
|
||||||
|
title: "Clément Saillant — L'électron rare",
|
||||||
|
description:
|
||||||
|
"Studio de Clément Saillant (L'électron rare) : création électronique, invention de systèmes, design produit et projets audiovisuels.",
|
||||||
|
ogDescription:
|
||||||
|
"Codeur créatif, itérateur IA et concepteur : création électronique, design produit, systèmes expérimentaux et collaborations visuelles.",
|
||||||
|
twitterDescription:
|
||||||
|
"Creation electronique, invention de systemes et design produit sous l'identite L'electron rare.",
|
||||||
|
ogImagePath: 'assets/og-cover.jpg',
|
||||||
|
themeColor: '#0f0e17'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const SITE_OG_IMAGE_URL = `${SITE_URL}${SITE_META.ogImagePath}`;
|
||||||
|
|
||||||
|
export const GTM_CONTAINER_ID_DEFAULT = 'GTM-5SLM67QF';
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
export const TRACK_EVENTS = {
|
||||||
|
ctaHeroProjects: 'cta_hero_projets',
|
||||||
|
ctaHeroContact: 'cta_hero_contact',
|
||||||
|
ctaHeroProfile: 'cta_hero_profile',
|
||||||
|
outboundLinkedinProject: 'outbound_linkedin_project',
|
||||||
|
outboundLinkedinContact: 'outbound_linkedin_contact',
|
||||||
|
outboundMaltContact: 'outbound_malt_contact',
|
||||||
|
outboundBandcampProject: 'outbound_bandcamp_project',
|
||||||
|
outboundBandcampContact: 'outbound_bandcamp_contact'
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type TrackEventName = (typeof TRACK_EVENTS)[keyof typeof TRACK_EVENTS];
|
||||||
|
|
||||||
|
export type TrackEventPayload = {
|
||||||
|
event: TrackEventName;
|
||||||
|
event_category: 'engagement';
|
||||||
|
event_label: TrackEventName;
|
||||||
|
destination: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function trackAttrs(event: TrackEventName, destination: string) {
|
||||||
|
return {
|
||||||
|
'data-track': event,
|
||||||
|
'data-destination': destination
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { TrackEventName } from '@/lib/tracking';
|
||||||
|
|
||||||
|
export type CtaLink = {
|
||||||
|
label: string;
|
||||||
|
href: string;
|
||||||
|
event: TrackEventName;
|
||||||
|
destination: string;
|
||||||
|
external?: boolean;
|
||||||
|
};
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from 'clsx';
|
||||||
|
import { twMerge } from 'tailwind-merge';
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
---
|
||||||
|
import BaseLayout from '@/layouts/BaseLayout.astro';
|
||||||
|
import '@/styles/global.css';
|
||||||
|
import { Hero } from '@/components/sections/Hero';
|
||||||
|
import { About } from '@/components/sections/About';
|
||||||
|
import { Projects } from '@/components/sections/Projects';
|
||||||
|
import { Contact } from '@/components/sections/Contact';
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout>
|
||||||
|
<header class="sticky top-0 z-30 border-b border-violet-200/20 bg-black/35 backdrop-blur-md">
|
||||||
|
<div class="site-shell flex items-center justify-between py-3 max-md:flex-col max-md:gap-3">
|
||||||
|
<a href="#top" class="font-semibold tracking-wide text-violet-100">
|
||||||
|
<span class="lab-kicker">Studio</span>
|
||||||
|
L'electron rare
|
||||||
|
</a>
|
||||||
|
<nav aria-label="Navigation principale">
|
||||||
|
<ul class="m-0 flex list-none gap-2 p-0">
|
||||||
|
<li><a href="#a-propos" class="rounded-md px-3 py-2 text-violet-100/80 hover:bg-violet-100/10 hover:text-violet-100">À propos</a></li>
|
||||||
|
<li><a href="#projets" class="rounded-md px-3 py-2 text-violet-100/80 hover:bg-violet-100/10 hover:text-violet-100">Projets</a></li>
|
||||||
|
<li><a href="#contact" class="rounded-md px-3 py-2 text-violet-100/80 hover:bg-violet-100/10 hover:text-violet-100">Contact</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
<span class="lab-pill lab-glow" aria-label="Etat du studio">
|
||||||
|
Lab mode actif
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="site-shell pb-10 pt-2">
|
||||||
|
<Hero client:load />
|
||||||
|
<About />
|
||||||
|
<Projects client:load />
|
||||||
|
<Contact />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="border-t border-violet-200/20 py-6 text-violet-100/60">
|
||||||
|
<div class="site-shell">
|
||||||
|
© 2026 Clement Saillant - L'electron rare. Studio de creation electronique et design systeme.
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</BaseLayout>
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Source+Sans+3:wght@400;500;600;700&display=swap');
|
||||||
|
@import 'tailwindcss';
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #080812;
|
||||||
|
--surface: #121226;
|
||||||
|
--surface-soft: #1a1833;
|
||||||
|
--text: #f0ecff;
|
||||||
|
--muted: #b8afdb;
|
||||||
|
--primary: #35dcff;
|
||||||
|
--primary-strong: #18bfe5;
|
||||||
|
--accent: #ff3b99;
|
||||||
|
--accent-strong: #ff187f;
|
||||||
|
--signal: #b7ff5d;
|
||||||
|
--border: #3a3360;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Source Sans 3', 'Segoe UI', 'Helvetica Neue', sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
background:
|
||||||
|
radial-gradient(1200px 720px at 88% -18%, color-mix(in oklab, var(--primary) 26%, transparent), transparent 62%),
|
||||||
|
radial-gradient(1000px 650px at -15% -8%, color-mix(in oklab, var(--accent) 24%, transparent), transparent 60%),
|
||||||
|
radial-gradient(540px 360px at 48% -16%, color-mix(in oklab, var(--signal) 18%, transparent), transparent 74%),
|
||||||
|
linear-gradient(180deg, #080812 0%, #090913 55%, #07070f 100%);
|
||||||
|
color: var(--text);
|
||||||
|
text-rendering: optimizeLegibility;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background-image:
|
||||||
|
repeating-linear-gradient(
|
||||||
|
0deg,
|
||||||
|
rgba(255, 255, 255, 0.012) 0px,
|
||||||
|
rgba(255, 255, 255, 0.012) 1px,
|
||||||
|
transparent 1px,
|
||||||
|
transparent 3px
|
||||||
|
);
|
||||||
|
mix-blend-mode: soft-light;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
body::after {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(to right, rgba(94, 103, 145, 0.12) 1px, transparent 1px),
|
||||||
|
linear-gradient(to bottom, rgba(94, 103, 145, 0.1) 1px, transparent 1px);
|
||||||
|
background-size: 32px 32px;
|
||||||
|
mask-image: radial-gradient(circle at 50% 18%, black 12%, transparent 72%);
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
font-family: 'DM Serif Display', 'Times New Roman', serif;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: color-mix(in oklab, var(--accent) 65%, black 10%);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.site-shell {
|
||||||
|
width: min(1120px, 92vw);
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-anchor {
|
||||||
|
scroll-margin-top: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lab-kicker {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 1.6rem;
|
||||||
|
align-items: center;
|
||||||
|
border: 1px solid rgba(183, 255, 93, 0.35);
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.1rem 0.55rem;
|
||||||
|
margin-right: 0.45rem;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: color-mix(in oklab, var(--signal) 80%, #ffffff 20%);
|
||||||
|
background: color-mix(in oklab, var(--signal) 16%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lab-pill {
|
||||||
|
display: inline-flex;
|
||||||
|
min-height: 2rem;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(53, 220, 255, 0.4);
|
||||||
|
padding: 0.25rem 0.75rem;
|
||||||
|
font-size: 0.72rem;
|
||||||
|
letter-spacing: 0.09em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: color-mix(in oklab, var(--primary) 85%, #ffffff 15%);
|
||||||
|
background:
|
||||||
|
linear-gradient(
|
||||||
|
90deg,
|
||||||
|
color-mix(in oklab, var(--primary) 15%, transparent),
|
||||||
|
color-mix(in oklab, var(--accent) 10%, transparent)
|
||||||
|
);
|
||||||
|
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.03) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lab-panel {
|
||||||
|
border: 1px solid rgba(53, 220, 255, 0.32);
|
||||||
|
background:
|
||||||
|
linear-gradient(180deg, rgba(53, 220, 255, 0.08), rgba(255, 59, 153, 0.06)),
|
||||||
|
radial-gradient(180px 120px at 85% -10%, rgba(183, 255, 93, 0.15), transparent 72%);
|
||||||
|
box-shadow:
|
||||||
|
0 16px 40px rgba(7, 6, 20, 0.45),
|
||||||
|
0 0 0 1px rgba(255, 255, 255, 0.03) inset;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lab-metric {
|
||||||
|
border-radius: 0.7rem;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
|
background: rgba(7, 6, 20, 0.45);
|
||||||
|
padding: 0.5rem 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lab-metric span {
|
||||||
|
display: block;
|
||||||
|
color: rgba(184, 175, 219, 0.9);
|
||||||
|
font-size: 0.68rem;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lab-metric strong {
|
||||||
|
display: block;
|
||||||
|
margin-top: 0.12rem;
|
||||||
|
font-size: 0.84rem;
|
||||||
|
color: rgba(240, 236, 255, 0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lab-glow {
|
||||||
|
animation: labPulse 5.5s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes labPulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 rgba(53, 220, 255, 0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 0 10px rgba(53, 220, 255, 0.06);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus-visible,
|
||||||
|
button:focus-visible {
|
||||||
|
outline: 3px solid color-mix(in oklab, var(--primary) 85%, white 15%);
|
||||||
|
outline-offset: 2px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 390px) {
|
||||||
|
.site-shell {
|
||||||
|
width: min(1120px, 94vw);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lab-pill {
|
||||||
|
font-size: 0.66rem;
|
||||||
|
letter-spacing: 0.07em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
* {
|
||||||
|
animation-duration: 1ms !important;
|
||||||
|
animation-iteration-count: 1 !important;
|
||||||
|
transition-duration: 1ms !important;
|
||||||
|
scroll-behavior: auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lab-glow {
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"extends": "astro/tsconfigs/strict",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist", "storybook-static"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user