feat(theme): color package Electric Teal + bascule nom IA qu'à

This commit is contained in:
Paul Atlan 2026-05-30 20:13:55 +02:00
parent 229f7e879e
commit cd6a641d72
18 changed files with 630 additions and 241 deletions

View File

@ -1,4 +1,4 @@
# IA qu'à... — Blog
# IA qu'à — Blog
Blog statique personnel sur l'IA en PME.

View File

@ -3,12 +3,104 @@ import mdx from "@astrojs/mdx";
import sitemap from "@astrojs/sitemap";
import tailwind from "@astrojs/tailwind";
/**
* "IA qu'à" Atom Syntax theme Electric Teal color package §3/§4.
* Dark Slate canvas with the 7-color diagnostic spectrum mapped onto
* standard code tokens. Used for all fenced code blocks (light & dark site).
*/
const iaQuaSyntax = {
name: "ia-qua-atom-syntax",
type: "dark",
colors: {
"editor.background": "#0F172A",
"editor.foreground": "#F1F5F9",
},
settings: [
{ settings: { background: "#0F172A", foreground: "#F1F5F9" } },
// comment / metadata → Orchid
{
scope: ["comment", "punctuation.definition.comment", "string.comment"],
settings: { foreground: "#8B5CF6", fontStyle: "italic" },
},
// keyword / control → Ember
{
scope: [
"keyword",
"keyword.control",
"storage",
"storage.type",
"storage.modifier",
"keyword.operator.new",
"entity.name.tag",
],
settings: { foreground: "#FF6B00" },
},
// string / regex → Gold
{
scope: [
"string",
"string.quoted",
"string.template",
"constant.other.symbol",
"string.regexp",
],
settings: { foreground: "#F59E0B" },
},
// support / type / class → Emerald
{
scope: [
"entity.name.class",
"entity.name.type",
"support.type",
"support.class",
"storage.type.class",
"entity.other.attribute-name",
],
settings: { foreground: "#10B981" },
},
// entity / function → Indigo
{
scope: [
"entity.name.function",
"support.function",
"meta.function-call",
"variable.function",
],
settings: { foreground: "#6366F1" },
},
// variable / parameter & numerics → Sky
{
scope: [
"variable",
"variable.other",
"variable.parameter",
"support.variable",
"constant.numeric",
"constant.language",
"constant",
],
settings: { foreground: "#0EA5E9" },
},
// exception / invalid → Rose
{
scope: ["invalid", "invalid.illegal", "invalid.deprecated"],
settings: { foreground: "#F43F5E" },
},
// punctuation kept muted
{
scope: ["punctuation", "meta.brace", "punctuation.separator"],
settings: { foreground: "#94A3B8" },
},
],
};
export default defineConfig({
site: "https://ia-qua.fr", // À changer si autre domaine
integrations: [mdx(), sitemap(), tailwind()],
markdown: {
shikiConfig: {
theme: "github-dark",
theme: iaQuaSyntax,
wrap: false,
},
},
});

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash
set -euo pipefail
# ─── deploy.sh — Blog IA qu'à... ───
# ─── deploy.sh — Blog IA qu'à ───
# Usage : sudo bash deploy.sh
# Depuis le répertoire du repo sur Cloudbreak

View File

@ -1,4 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#1B2A4A"/>
<text x="16" y="22" text-anchor="middle" font-family="Georgia, serif" font-weight="bold" font-size="16" fill="#6B8F71">IA</text>
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#06B6D4"/>
<stop offset="100%" stop-color="#3B82F6"/>
</linearGradient>
</defs>
<rect width="32" height="32" rx="7" fill="url(#g)"/>
<text x="16" y="22" text-anchor="middle" font-family="Inter, system-ui, sans-serif" font-weight="800" font-size="17" fill="#FFFFFF">IA</text>
</svg>

Before

Width:  |  Height:  |  Size: 254 B

After

Width:  |  Height:  |  Size: 460 B

14
public/logo.svg Normal file
View File

@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 196 48" role="img" aria-label="IA qu'à">
<defs>
<linearGradient id="iaGrad" x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#06B6D4"/>
<stop offset="100%" stop-color="#3B82F6"/>
</linearGradient>
</defs>
<text x="0" y="37" font-family="Inter, system-ui, -apple-system, sans-serif" font-size="38" letter-spacing="-0.5">
<tspan fill="url(#iaGrad)" font-weight="800">IA</tspan>
<tspan fill="#334155" font-weight="700" dx="6">qu</tspan>
<tspan fill="url(#iaGrad)" font-weight="600">&#8217;</tspan>
<tspan fill="#334155" font-weight="700">à</tspan>
</text>
</svg>

After

Width:  |  Height:  |  Size: 669 B

View File

@ -20,21 +20,44 @@ const formattedDate = pubDate.toLocaleDateString("fr-FR", {
});
---
<article class="bg-white rounded-xl border border-border overflow-hidden transition-all hover:-translate-y-1 hover:shadow-lg hover:shadow-blue-night/5">
<article
class="bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-800 overflow-hidden transition-all hover:-translate-y-1 hover:border-brand-cyan/50 hover:shadow-lg hover:shadow-brand-cyan/10"
>
<a href={`/blog/${slug}`} class="no-underline block">
{heroImage ? (
<img src={heroImage} alt={title} class="w-full h-48 object-cover" />
) : (
<div class="w-full h-48 bg-gradient-to-br from-blue-night to-blue-mid flex items-center justify-center">
<span class="text-white/10 text-6xl">&#9673;</span>
</div>
)}
{
heroImage ? (
<img src={heroImage} alt={title} class="w-full h-48 object-cover" />
) : (
<div class="w-full h-48 bg-brand-gradient flex items-center justify-center">
<span class="text-white/20 text-6xl">&#9673;</span>
</div>
)
}
<div class="p-5">
{series && <div class="mb-2"><SeriesBadge name={series.name} part={series.part} total={series.total} /></div>}
<span class="text-xs font-semibold text-sage uppercase tracking-wide">{category}</span>
<h3 class="font-heading text-blue-night text-lg font-semibold leading-snug mt-1 mb-2">{title}</h3>
<p class="text-sm text-text-light line-clamp-2 mb-3">{description}</p>
<span class="text-xs text-text-light">{formattedDate}</span>
{
series && (
<div class="mb-2">
<SeriesBadge
name={series.name}
part={series.part}
total={series.total}
/>
</div>
)
}
<span
class="text-xs font-semibold text-brand-deep dark:text-brand-cyan uppercase tracking-wide"
>{category}</span
>
<h3
class="font-heading text-slate-900 dark:text-slate-50 text-lg font-semibold leading-snug mt-1 mb-2"
>
{title}
</h3>
<p class="text-sm text-slate-500 dark:text-slate-400 line-clamp-2 mb-3">
{description}
</p>
<span class="text-xs text-slate-400 dark:text-slate-500">{formattedDate}</span>
</div>
</a>
</article>

View File

@ -0,0 +1,44 @@
---
/**
* BrandLogo — wordmark "IA qu'à" (color package §1, palette Classic Electric Teal).
* "IA" + apostrophe en dégradé cyan→bleu, "qu'à" en slate adaptatif (clair/sombre).
* Double sens assumé : "IA qu'à" ↔ "Y'a qu'à". Rendu en Inter (police du site).
*/
interface Props {
class?: string;
}
const { class: className = "h-7 w-auto" } = Astro.props;
// id unique pour éviter les collisions de gradient si plusieurs instances
const gid = "iaGrad-" + Math.random().toString(36).slice(2, 8);
---
<svg
viewBox="0 0 196 48"
role="img"
aria-label="IA qu'à"
class={className}
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<linearGradient id={gid} x1="0" y1="0" x2="1" y2="1">
<stop offset="0%" stop-color="#06B6D4"></stop>
<stop offset="100%" stop-color="#3B82F6"></stop>
</linearGradient>
</defs>
<text
x="0"
y="37"
font-family="Inter, system-ui, -apple-system, sans-serif"
font-size="38"
letter-spacing="-0.5"
>
<tspan fill={`url(#${gid})`} font-weight="800">IA</tspan>
<tspan
class="fill-slate-700 dark:fill-slate-100"
font-weight="700"
dx="6">qu</tspan
>
<tspan fill={`url(#${gid})`} font-weight="600">&#8217;</tspan>
<tspan class="fill-slate-700 dark:fill-slate-100" font-weight="700">à</tspan>
</text>
</svg>

View File

@ -2,12 +2,20 @@
const year = new Date().getFullYear();
---
<footer class="max-w-6xl mx-auto mt-16 px-6 py-8 border-t border-border text-center text-sm text-text-light">
<footer
class="max-w-6xl mx-auto mt-16 px-6 py-8 border-t border-slate-200 dark:border-slate-800 text-center text-sm text-slate-500 dark:text-slate-400"
>
<p>
<strong class="font-heading text-blue-night">IA qu'à...</strong> — Journal de bord d'un DSI en PME face à l'IA
<strong class="font-heading text-slate-900 dark:text-slate-100"
>IA qu'à</strong
> — Journal d'un DSI de PME
</p>
<p class="mt-2">
&copy; {year} — Fait avec <a href="https://astro.build" class="text-sage no-underline hover:underline">Astro</a>
&copy; {year} — Fait avec <a
href="https://astro.build"
class="text-brand-deep dark:text-brand-cyan no-underline hover:underline"
>Astro</a
>
&middot; Self-hosted
</p>
</footer>

View File

@ -1,4 +1,6 @@
---
import BrandLogo from "./BrandLogo.astro";
const currentPath = Astro.url.pathname;
const links = [
@ -11,57 +13,132 @@ const links = [
];
---
<nav class="bg-white border-b border-border sticky top-0 z-50">
<nav
class="sticky top-0 z-50 border-b border-slate-200 dark:border-slate-800 bg-canvas-light/80 dark:bg-canvas-dark/80 backdrop-blur-md"
>
<div class="max-w-6xl mx-auto px-6 py-4 flex items-center justify-between">
<a href="/" class="font-heading font-extrabold text-xl text-blue-night no-underline">
IA qu'à<span class="text-sage">...</span>
<a href="/" class="no-underline shrink-0" aria-label="Accueil — IA qu'à">
<BrandLogo class="h-7 w-auto" />
</a>
<!-- Desktop -->
<ul class="hidden md:flex gap-8 list-none">
{links.map((link) => (
<li>
<a
href={link.href}
class:list={[
"text-sm font-medium no-underline transition-colors",
currentPath === link.href
? "text-blue-night border-b-2 border-sage pb-0.5"
: "text-text-light hover:text-blue-night",
]}
>
{link.label}
</a>
</li>
))}
</ul>
<div class="flex items-center gap-6">
<!-- Desktop -->
<ul class="hidden md:flex gap-7 list-none">
{
links.map((link) => (
<li>
<a
href={link.href}
class:list={[
"text-sm font-medium no-underline transition-colors",
currentPath === link.href
? "text-slate-900 dark:text-white border-b-2 border-brand-cyan pb-0.5"
: "text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white",
]}
>
{link.label}
</a>
</li>
))
}
</ul>
<!-- Mobile toggle -->
<button id="menu-toggle" class="md:hidden text-blue-night" aria-label="Menu">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
<!-- Theme toggle -->
<button
id="theme-toggle"
class="text-slate-500 dark:text-slate-400 hover:text-brand-cyan dark:hover:text-brand-cyan transition-colors"
aria-label="Basculer le thème clair / sombre"
>
<!-- soleil (visible en mode sombre) -->
<svg
class="hidden dark:block"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="4"></circle>
<path
d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"
></path>
</svg>
<!-- lune (visible en mode clair) -->
<svg
class="block dark:hidden"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path>
</svg>
</button>
<!-- Mobile toggle -->
<button
id="menu-toggle"
class="md:hidden text-slate-700 dark:text-slate-200"
aria-label="Menu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</svg>
</button>
</div>
</div>
<!-- Mobile menu -->
<div id="mobile-menu" class="hidden md:hidden border-t border-border px-6 py-4">
<div
id="mobile-menu"
class="hidden md:hidden border-t border-slate-200 dark:border-slate-800 px-6 py-4"
>
<ul class="flex flex-col gap-4 list-none">
{links.map((link) => (
<li>
<a href={link.href} class="text-sm font-medium text-text-light no-underline hover:text-blue-night">
{link.label}
</a>
</li>
))}
{
links.map((link) => (
<li>
<a
href={link.href}
class="text-sm font-medium text-slate-500 dark:text-slate-400 no-underline hover:text-slate-900 dark:hover:text-white"
>
{link.label}
</a>
</li>
))
}
</ul>
</div>
</nav>
<script>
// Menu mobile
document.getElementById("menu-toggle")?.addEventListener("click", () => {
document.getElementById("mobile-menu")?.classList.toggle("hidden");
});
// Bascule de thème clair / sombre (persistée)
document.getElementById("theme-toggle")?.addEventListener("click", () => {
const root = document.documentElement;
const isDark = root.classList.toggle("dark");
localStorage.setItem("theme", isDark ? "dark" : "light");
});
</script>

View File

@ -9,15 +9,30 @@ const { name, part, total } = Astro.props;
const dots = Array.from({ length: total }, (_, i) => i < part);
---
<div class="inline-flex items-center gap-2 bg-sage-light text-sage text-xs font-semibold px-3 py-1 rounded-full">
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
<div
class="inline-flex items-center gap-2 bg-brand-gradient-soft text-brand-deep dark:text-brand-cyan ring-1 ring-brand-cyan/20 text-xs font-semibold px-3 py-1 rounded-full"
>
<svg
class="w-3.5 h-3.5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"></path>
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"></path>
</svg>
<span>Série : {name}</span>
<div class="flex gap-0.5 ml-1">
{dots.map((filled) => (
<div class:list={["w-1.5 h-1.5 rounded-full bg-sage", filled ? "opacity-100" : "opacity-30"]} />
))}
{
dots.map((filled) => (
<div
class:list={[
"w-1.5 h-1.5 rounded-full bg-brand-cyan",
filled ? "opacity-100" : "opacity-30",
]}
/>
))
}
</div>
</div>

View File

@ -23,51 +23,53 @@ const seriesPosts = allPosts.sort(
);
---
{seriesPosts.length > 1 && (
<aside class="my-8 p-6 bg-white border border-border rounded-xl">
<h3 class="font-heading text-blue-night text-lg font-semibold mb-1">
Série : {seriesName}
</h3>
<p class="text-text-light text-sm mb-4">
Article {currentPart} sur {seriesPosts.length}
</p>
{
seriesPosts.length > 1 && (
<aside class="my-8 p-6 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl">
<h3 class="font-heading text-slate-900 dark:text-slate-50 text-lg font-semibold mb-1">
Série : {seriesName}
</h3>
<p class="text-slate-500 dark:text-slate-400 text-sm mb-4">
Article {currentPart} sur {seriesPosts.length}
</p>
<div class="w-full h-1 bg-border rounded-full mb-4 overflow-hidden">
<div
class="h-full bg-sage rounded-full transition-all"
style={`width: ${(currentPart / seriesPosts.length) * 100}%`}
/>
</div>
<div class="w-full h-1 bg-slate-200 dark:bg-slate-800 rounded-full mb-4 overflow-hidden">
<div
class="h-full bg-brand-gradient rounded-full transition-all"
style={`width: ${(currentPart / seriesPosts.length) * 100}%`}
/>
</div>
<ol class="list-none space-y-2">
{seriesPosts.map((post) => {
const isCurrent = post.data.series?.part === currentPart;
return (
<li>
<a
href={`/blog/${post.id}`}
class:list={[
"flex items-center gap-3 py-2 px-3 rounded-lg text-sm no-underline transition-colors",
isCurrent
? "bg-sage-light text-sage font-semibold"
: "text-text-light hover:bg-sage-light/50 hover:text-blue-night",
]}
>
<span
<ol class="list-none space-y-2">
{seriesPosts.map((post) => {
const isCurrent = post.data.series?.part === currentPart;
return (
<li>
<a
href={`/blog/${post.id}`}
class:list={[
"flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold",
"flex items-center gap-3 py-2 px-3 rounded-lg text-sm no-underline transition-colors",
isCurrent
? "bg-sage text-white"
: "bg-border text-text-light",
? "bg-brand-gradient-soft text-brand-deep dark:text-brand-cyan font-semibold"
: "text-slate-500 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-900 dark:hover:text-white",
]}
>
{post.data.series?.part}
</span>
<span>{post.data.title}</span>
</a>
</li>
);
})}
</ol>
</aside>
)}
<span
class:list={[
"flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold",
isCurrent
? "bg-brand-gradient text-white"
: "bg-slate-200 dark:bg-slate-700 text-slate-500 dark:text-slate-300",
]}
>
{post.data.series?.part}
</span>
<span>{post.data.title}</span>
</a>
</li>
);
})}
</ol>
</aside>
)
}

View File

@ -23,11 +23,26 @@ const canonicalURL = new URL(Astro.url.pathname, Astro.site);
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<!-- Thème clair/sombre : appliqué avant le paint pour éviter le flash -->
<script is:inline>
(() => {
const stored = localStorage.getItem("theme");
const prefersDark = window.matchMedia(
"(prefers-color-scheme: dark)"
).matches;
if (stored === "dark" || (!stored && prefersDark)) {
document.documentElement.classList.add("dark");
}
})();
</script>
<meta name="theme-color" content="#06B6D4" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="sitemap" href="/sitemap-index.xml" />
<!-- SEO -->
<title>{title} | IA qu'à...</title>
<title>{title} | IA qu'à</title>
<meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} />

View File

@ -32,14 +32,14 @@ const formattedDate = pubDate.toLocaleDateString("fr-FR", {
<SeriesBadge name={series.name} part={series.part} total={series.total} />
</div>
)}
<span class="text-xs font-semibold text-sage uppercase tracking-wide">
<span class="text-xs font-semibold text-brand-deep dark:text-brand-cyan uppercase tracking-wide">
{category}
</span>
<h1 class="font-heading text-blue-night text-4xl font-extrabold leading-tight mt-2 mb-3">
<h1 class="font-heading text-slate-900 dark:text-slate-50 text-4xl font-extrabold leading-tight mt-2 mb-3">
{title}
</h1>
<p class="text-text-light">{description}</p>
<div class="flex items-center gap-2 mt-4 text-sm text-text-light">
<p class="text-slate-500 dark:text-slate-400">{description}</p>
<div class="flex items-center gap-2 mt-4 text-sm text-slate-400 dark:text-slate-500">
<time datetime={pubDate.toISOString()}>{formattedDate}</time>
{updatedDate && (
<>
@ -62,7 +62,7 @@ const formattedDate = pubDate.toLocaleDateString("fr-FR", {
<img
src={heroImage}
alt={title}
class="w-full rounded-xl mb-8"
class="w-full rounded-xl mb-8 ring-1 ring-slate-200 dark:ring-slate-800"
/>
)}

View File

@ -4,7 +4,7 @@ import BaseLayout from "../layouts/BaseLayout.astro";
<BaseLayout title="À propos">
<article class="max-w-3xl mx-auto px-6 py-16">
<h1 class="font-heading text-blue-night text-4xl font-extrabold leading-tight mb-6">
<h1 class="font-heading text-slate-900 dark:text-slate-50 text-4xl font-extrabold leading-tight mb-6">
À propos
</h1>

View File

@ -34,79 +34,96 @@ const seriesList = Array.from(seriesMap.values());
<BaseLayout title="Accueil">
<!-- Hero -->
<section class="max-w-3xl mx-auto text-center px-6 pt-16 pb-10">
<h1 class="font-heading text-blue-night text-4xl md:text-5xl font-extrabold leading-tight mb-4">
L'IA en PME,<br />sans bullshit.
<h1
class="font-heading text-slate-900 dark:text-slate-50 text-4xl md:text-5xl font-extrabold leading-tight mb-4"
>
L'IA en PME,<br /><span class="text-gradient">sans bullshit.</span>
</h1>
<p class="text-lg text-text-light max-w-xl mx-auto">
Journal de bord d'un DSI qui teste, bidouille, plante et recommence.
Ce qui marche vraiment, ce qui ne marche pas, et pourquoi.
<p class="text-lg text-slate-500 dark:text-slate-400 max-w-xl mx-auto">
Journal de bord d'un DSI qui teste, bidouille, plante et recommence. Ce
qui marche vraiment, ce qui ne marche pas, et pourquoi.
</p>
</section>
<!-- Catégories -->
<div class="flex justify-center gap-3 flex-wrap px-6 mb-10">
{categories.map((cat) => (
<a
href={`/categorie/${cat.toLowerCase().replace(/ & /g, "-").replace(/ /g, "-")}`}
class="px-4 py-1.5 rounded-full text-xs font-medium border border-border bg-white text-text-light no-underline hover:bg-sage-light hover:border-sage hover:text-sage transition-colors"
>
{cat}
</a>
))}
{
categories.map((cat) => (
<a
href={`/categorie/${cat.toLowerCase().replace(/ & /g, "-").replace(/ /g, "-")}`}
class="px-4 py-1.5 rounded-full text-xs font-medium border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 text-slate-500 dark:text-slate-400 no-underline hover:border-brand-cyan hover:text-brand-deep dark:hover:text-brand-cyan transition-colors"
>
{cat}
</a>
))
}
</div>
<!-- Articles -->
<section class="max-w-6xl mx-auto px-6">
<div class="flex items-center justify-between mb-6">
<h2 class="font-heading text-xl text-blue-night">Articles récents</h2>
<h2 class="font-heading text-xl text-slate-900 dark:text-slate-50">
Articles récents
</h2>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{posts.map((post) => (
<ArticleCard
title={post.data.title}
description={post.data.description}
pubDate={post.data.pubDate}
category={post.data.category}
heroImage={post.data.heroImage}
slug={post.id}
series={post.data.series}
/>
))}
{
posts.map((post) => (
<ArticleCard
title={post.data.title}
description={post.data.description}
pubDate={post.data.pubDate}
category={post.data.category}
heroImage={post.data.heroImage}
slug={post.id}
series={post.data.series}
/>
))
}
</div>
</section>
<!-- Séries en cours -->
{seriesList.length > 0 && (
<section class="max-w-6xl mx-auto px-6 mt-16">
<div class="flex items-center justify-between mb-6">
<h2 class="font-heading text-xl text-blue-night">Séries en cours</h2>
<a href="/series" class="text-sm text-sage no-underline font-medium hover:underline">
Toutes les séries &rarr;
</a>
</div>
{seriesList.map((s) => (
<a
href={`/series/${s.name.toLowerCase().replace(/ /g, "-")}`}
class="block bg-white border border-border rounded-xl p-5 mb-3 no-underline hover:translate-x-1 transition-transform"
>
<div class="flex items-center justify-between">
<div>
<h3 class="font-heading text-blue-night text-base font-semibold">{s.name}</h3>
</div>
<div class="flex items-center gap-3">
<div class="w-28 h-1 bg-border rounded-full overflow-hidden">
<div
class="h-full bg-sage rounded-full"
style={`width: ${(s.count / s.total) * 100}%`}
/>
{
seriesList.length > 0 && (
<section class="max-w-6xl mx-auto px-6 mt-16">
<div class="flex items-center justify-between mb-6">
<h2 class="font-heading text-xl text-slate-900 dark:text-slate-50">
Séries en cours
</h2>
<a
href="/series"
class="text-sm text-brand-deep dark:text-brand-cyan no-underline font-medium hover:underline"
>
Toutes les séries &rarr;
</a>
</div>
{seriesList.map((s) => (
<a
href={`/series/${s.name.toLowerCase().replace(/ /g, "-")}`}
class="block bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl p-5 mb-3 no-underline hover:border-brand-cyan/50 hover:translate-x-1 transition-all"
>
<div class="flex items-center justify-between">
<div>
<h3 class="font-heading text-slate-900 dark:text-slate-50 text-base font-semibold">
{s.name}
</h3>
</div>
<div class="flex items-center gap-3">
<div class="w-28 h-1 bg-slate-200 dark:bg-slate-800 rounded-full overflow-hidden">
<div
class="h-full bg-brand-gradient rounded-full"
style={`width: ${(s.count / s.total) * 100}%`}
/>
</div>
<span class="text-xs text-slate-400 dark:text-slate-500 whitespace-nowrap">
{s.count} / {s.total} articles
</span>
</div>
<span class="text-xs text-text-light whitespace-nowrap">
{s.count} / {s.total} articles
</span>
</div>
</div>
</a>
))}
</section>
)}
</a>
))}
</section>
)
}
</BaseLayout>

View File

@ -32,63 +32,71 @@ const seriesList = Array.from(seriesMap.values()).map((s) => ({
<BaseLayout title="Séries">
<section class="max-w-3xl mx-auto px-6 py-16">
<h1 class="font-heading text-blue-night text-4xl font-extrabold leading-tight mb-3">
<h1
class="font-heading text-slate-900 dark:text-slate-50 text-4xl font-extrabold leading-tight mb-3"
>
Séries
</h1>
<p class="text-text-light mb-10">
<p class="text-slate-500 dark:text-slate-400 mb-10">
Des articles qui se suivent pour explorer un sujet en profondeur.
</p>
{seriesList.length === 0 && (
<p class="text-text-light italic">Aucune série pour le moment.</p>
)}
{
seriesList.length === 0 && (
<p class="text-slate-500 dark:text-slate-400 italic">
Aucune série pour le moment.
</p>
)
}
{seriesList.map((series) => (
<div class="bg-white border border-border rounded-xl p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="font-heading text-blue-night text-xl font-semibold">
{series.name}
</h2>
<span class="text-xs text-text-light">
{series.posts.length} / {series.total} articles
</span>
</div>
{
seriesList.map((series) => (
<div class="bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl p-6 mb-6">
<div class="flex items-center justify-between mb-4">
<h2 class="font-heading text-slate-900 dark:text-slate-50 text-xl font-semibold">
{series.name}
</h2>
<span class="text-xs text-slate-400 dark:text-slate-500">
{series.posts.length} / {series.total} articles
</span>
</div>
<div class="w-full h-1 bg-border rounded-full mb-5 overflow-hidden">
<div
class="h-full bg-sage rounded-full"
style={`width: ${(series.posts.length / series.total) * 100}%`}
/>
</div>
<div class="w-full h-1 bg-slate-200 dark:bg-slate-800 rounded-full mb-5 overflow-hidden">
<div
class="h-full bg-brand-gradient rounded-full"
style={`width: ${(series.posts.length / series.total) * 100}%`}
/>
</div>
<ol class="list-none space-y-2">
{series.posts.map((post) => (
<li>
<a
href={`/blog/${post.id}`}
class="flex items-center gap-3 py-2 px-3 rounded-lg text-sm text-text-light no-underline hover:bg-sage-light/50 hover:text-blue-night transition-colors"
>
<span class="flex-shrink-0 w-6 h-6 rounded-full bg-sage text-white flex items-center justify-center text-xs font-bold">
{post.data.series?.part}
</span>
<span>{post.data.title}</span>
</a>
</li>
))}
{/* Articles pas encore publiés */}
{Array.from(
{ length: series.total - series.posts.length },
(_, i) => (
<li class="flex items-center gap-3 py-2 px-3 text-sm text-text-light/50">
<span class="flex-shrink-0 w-6 h-6 rounded-full bg-border text-text-light/50 flex items-center justify-center text-xs font-bold">
{series.posts.length + i + 1}
</span>
<span class="italic">À venir...</span>
<ol class="list-none space-y-2">
{series.posts.map((post) => (
<li>
<a
href={`/blog/${post.id}`}
class="flex items-center gap-3 py-2 px-3 rounded-lg text-sm text-slate-500 dark:text-slate-400 no-underline hover:bg-slate-100 dark:hover:bg-slate-800 hover:text-slate-900 dark:hover:text-white transition-colors"
>
<span class="flex-shrink-0 w-6 h-6 rounded-full bg-brand-gradient text-white flex items-center justify-center text-xs font-bold">
{post.data.series?.part}
</span>
<span>{post.data.title}</span>
</a>
</li>
)
)}
</ol>
</div>
))}
))}
{/* Articles pas encore publiés */}
{Array.from(
{ length: series.total - series.posts.length },
(_, i) => (
<li class="flex items-center gap-3 py-2 px-3 text-sm text-slate-400/60 dark:text-slate-500/60">
<span class="flex-shrink-0 w-6 h-6 rounded-full bg-slate-200 dark:bg-slate-800 text-slate-400 dark:text-slate-500 flex items-center justify-center text-xs font-bold">
{series.posts.length + i + 1}
</span>
<span class="italic">À venir...</span>
</li>
)
)}
</ol>
</div>
))
}
</section>
</BaseLayout>

View File

@ -2,60 +2,86 @@
@tailwind components;
@tailwind utilities;
@import url("https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,600;0,9..144,800;1,9..144,400&family=Inter:wght@400;500;600&display=swap");
/* Polices Fraunces + Inter : chargées via <link> dans BaseLayout.astro */
@layer base {
html {
@apply scroll-smooth;
}
body {
@apply bg-bg-warm text-text-dark font-body leading-relaxed;
@apply bg-canvas-light text-slate-700 font-body leading-relaxed antialiased;
@apply dark:bg-canvas-dark dark:text-slate-300;
}
h1,
h2,
h3,
h4 {
@apply font-heading text-blue-night;
@apply font-heading text-slate-900 dark:text-slate-50;
}
/* Selection picks up the brand teal */
::selection {
@apply bg-brand-cyan/25 text-slate-900 dark:text-white;
}
}
/* ─── Prose (articles MDX) ─── */
@layer components {
/* Reusable gradient text helper for the wordmark / accents */
.text-gradient {
@apply bg-brand-gradient bg-clip-text text-transparent;
}
}
/*
Prose (articles MDX) light + dark
*/
.prose {
@apply max-w-none text-text-dark leading-relaxed;
@apply max-w-none text-slate-700 dark:text-slate-300 leading-relaxed;
}
.prose h2 {
@apply font-heading text-blue-night text-2xl font-semibold mt-10 mb-4;
@apply font-heading text-slate-900 dark:text-slate-50 text-2xl font-semibold mt-10 mb-4;
}
.prose h3 {
@apply font-heading text-blue-night text-xl font-semibold mt-8 mb-3;
@apply font-heading text-slate-900 dark:text-slate-50 text-xl font-semibold mt-8 mb-3;
}
.prose p {
@apply mb-5;
}
.prose strong {
@apply text-slate-900 dark:text-slate-100 font-semibold;
}
.prose a {
@apply text-sage underline underline-offset-2 hover:text-blue-night transition-colors;
@apply text-brand-deep dark:text-brand-cyan underline underline-offset-2 decoration-brand-cyan/40 hover:decoration-brand-cyan transition-colors;
}
.prose blockquote {
@apply border-l-4 border-sage pl-4 italic text-text-light my-6;
@apply border-l-4 border-brand-cyan pl-4 italic text-slate-500 dark:text-slate-400 my-6;
}
.prose code {
@apply bg-sage-light text-blue-night px-1.5 py-0.5 rounded text-sm;
/* Inline code (not inside a fenced block) */
.prose :not(pre) > code {
@apply bg-slate-100 dark:bg-slate-800 text-brand-deep dark:text-brand-teal px-1.5 py-0.5 rounded text-[0.9em] font-medium;
}
/* Fenced code blocks: Shiki sets the Slate background via the theme.
We only handle spacing, rounding and horizontal scroll. */
.prose pre {
@apply bg-blue-night text-white rounded-lg p-4 overflow-x-auto my-6;
@apply rounded-xl p-4 overflow-x-auto my-6 text-sm leading-relaxed ring-1 ring-slate-800/60;
}
.prose pre code {
@apply bg-transparent text-white p-0;
@apply bg-transparent p-0;
}
.prose img {
@apply rounded-lg my-6;
@apply rounded-xl my-6 ring-1 ring-slate-200 dark:ring-slate-800;
}
.prose ul,
@ -63,6 +89,22 @@
@apply my-4 pl-6;
}
.prose ul {
@apply list-disc;
}
.prose ol {
@apply list-decimal;
}
.prose li {
@apply mb-2;
}
.prose li::marker {
@apply text-brand-cyan;
}
.prose hr {
@apply my-10 border-slate-200 dark:border-slate-800;
}

View File

@ -1,22 +1,48 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
darkMode: "class",
theme: {
extend: {
colors: {
"blue-night": "#1B2A4A",
"blue-mid": "#2d4a7a",
sage: "#6B8F71",
"sage-light": "#E8F0E9",
"bg-warm": "#FAFAF7",
"text-dark": "#2D2D2D",
"text-light": "#6B6B6B",
border: "#E5E5E0",
// ─── Brand "Electric Teal" ───
brand: {
cyan: "#06B6D4", // accent primary
blue: "#3B82F6", // accent secondary
teal: "#22D3EE", // deep marine highlight
deep: "#0891B2", // deep teal (hover / prestige)
indigo: "#4F46E5",
},
// ─── Canvas (Slate) ───
canvas: {
dark: "#0F172A", // dark body / terminal backdrop
deep: "#090D16", // dark footer / deepest sheet
light: "#F8FAFC", // light body (alpine snow)
},
// ─── 7-color diagnostic & syntax spectrum ───
diag: {
rose: "#F43F5E", // exception / invalid
orange: "#FF6B00", // keyword / control
amber: "#F59E0B", // string / regex
emerald: "#10B981", // support / type / class
sky: "#0EA5E9", // variable / parameter
indigo: "#6366F1", // entity / function
violet: "#8B5CF6", // comment / metadata
},
},
fontFamily: {
heading: ["Fraunces", "Georgia", "serif"],
body: ["Inter", "-apple-system", "BlinkMacSystemFont", "sans-serif"],
},
backgroundImage: {
// 135° brand gradient (logo ligatures, CTAs, highlights)
"brand-gradient": "linear-gradient(135deg, #06B6D4 0%, #3B82F6 100%)",
"brand-gradient-soft":
"linear-gradient(135deg, rgba(6,182,212,0.12) 0%, rgba(59,130,246,0.12) 100%)",
},
boxShadow: {
glow: "0 8px 30px -8px rgba(6,182,212,0.35)",
},
},
},
plugins: [],