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. 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 sitemap from "@astrojs/sitemap";
import tailwind from "@astrojs/tailwind"; 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({ export default defineConfig({
site: "https://ia-qua.fr", // À changer si autre domaine site: "https://ia-qua.fr", // À changer si autre domaine
integrations: [mdx(), sitemap(), tailwind()], integrations: [mdx(), sitemap(), tailwind()],
markdown: { markdown: {
shikiConfig: { shikiConfig: {
theme: "github-dark", theme: iaQuaSyntax,
wrap: false,
}, },
}, },
}); });

View File

@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
# ─── deploy.sh — Blog IA qu'à... ─── # ─── deploy.sh — Blog IA qu'à ───
# Usage : sudo bash deploy.sh # Usage : sudo bash deploy.sh
# Depuis le répertoire du repo sur Cloudbreak # 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"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect width="32" height="32" rx="6" fill="#1B2A4A"/> <defs>
<text x="16" y="22" text-anchor="middle" font-family="Georgia, serif" font-weight="bold" font-size="16" fill="#6B8F71">IA</text> <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> </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"> <a href={`/blog/${slug}`} class="no-underline block">
{heroImage ? ( {
<img src={heroImage} alt={title} class="w-full h-48 object-cover" /> 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 class="w-full h-48 bg-brand-gradient flex items-center justify-center">
</div> <span class="text-white/20 text-6xl">&#9673;</span>
)} </div>
)
}
<div class="p-5"> <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> series && (
<h3 class="font-heading text-blue-night text-lg font-semibold leading-snug mt-1 mb-2">{title}</h3> <div class="mb-2">
<p class="text-sm text-text-light line-clamp-2 mb-3">{description}</p> <SeriesBadge
<span class="text-xs text-text-light">{formattedDate}</span> 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> </div>
</a> </a>
</article> </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(); 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> <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>
<p class="mt-2"> <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 &middot; Self-hosted
</p> </p>
</footer> </footer>

View File

@ -1,4 +1,6 @@
--- ---
import BrandLogo from "./BrandLogo.astro";
const currentPath = Astro.url.pathname; const currentPath = Astro.url.pathname;
const links = [ 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"> <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"> <a href="/" class="no-underline shrink-0" aria-label="Accueil — IA qu'à">
IA qu'à<span class="text-sage">...</span> <BrandLogo class="h-7 w-auto" />
</a> </a>
<!-- Desktop --> <div class="flex items-center gap-6">
<ul class="hidden md:flex gap-8 list-none"> <!-- Desktop -->
{links.map((link) => ( <ul class="hidden md:flex gap-7 list-none">
<li> {
<a links.map((link) => (
href={link.href} <li>
class:list={[ <a
"text-sm font-medium no-underline transition-colors", href={link.href}
currentPath === link.href class:list={[
? "text-blue-night border-b-2 border-sage pb-0.5" "text-sm font-medium no-underline transition-colors",
: "text-text-light hover:text-blue-night", 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> {link.label}
))} </a>
</ul> </li>
))
}
</ul>
<!-- Mobile toggle --> <!-- Theme toggle -->
<button id="menu-toggle" class="md:hidden text-blue-night" aria-label="Menu"> <button
<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"> id="theme-toggle"
<line x1="3" y1="12" x2="21" y2="12"></line> class="text-slate-500 dark:text-slate-400 hover:text-brand-cyan dark:hover:text-brand-cyan transition-colors"
<line x1="3" y1="6" x2="21" y2="6"></line> aria-label="Basculer le thème clair / sombre"
<line x1="3" y1="18" x2="21" y2="18"></line> >
</svg> <!-- soleil (visible en mode sombre) -->
</button> <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> </div>
<!-- Mobile menu --> <!-- 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"> <ul class="flex flex-col gap-4 list-none">
{links.map((link) => ( {
<li> links.map((link) => (
<a href={link.href} class="text-sm font-medium text-text-light no-underline hover:text-blue-night"> <li>
{link.label} <a
</a> href={link.href}
</li> 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> </ul>
</div> </div>
</nav> </nav>
<script> <script>
// Menu mobile
document.getElementById("menu-toggle")?.addEventListener("click", () => { document.getElementById("menu-toggle")?.addEventListener("click", () => {
document.getElementById("mobile-menu")?.classList.toggle("hidden"); 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> </script>

View File

@ -9,15 +9,30 @@ const { name, part, total } = Astro.props;
const dots = Array.from({ length: total }, (_, i) => i < part); 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"> <div
<svg class="w-3.5 h-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> 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"
<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" /> <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> </svg>
<span>Série : {name}</span> <span>Série : {name}</span>
<div class="flex gap-0.5 ml-1"> <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>
</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"> seriesPosts.length > 1 && (
<h3 class="font-heading text-blue-night text-lg font-semibold mb-1"> <aside class="my-8 p-6 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl">
Série : {seriesName} <h3 class="font-heading text-slate-900 dark:text-slate-50 text-lg font-semibold mb-1">
</h3> Série : {seriesName}
<p class="text-text-light text-sm mb-4"> </h3>
Article {currentPart} sur {seriesPosts.length} <p class="text-slate-500 dark:text-slate-400 text-sm mb-4">
</p> Article {currentPart} sur {seriesPosts.length}
</p>
<div class="w-full h-1 bg-border rounded-full mb-4 overflow-hidden"> <div class="w-full h-1 bg-slate-200 dark:bg-slate-800 rounded-full mb-4 overflow-hidden">
<div <div
class="h-full bg-sage rounded-full transition-all" class="h-full bg-brand-gradient rounded-full transition-all"
style={`width: ${(currentPart / seriesPosts.length) * 100}%`} style={`width: ${(currentPart / seriesPosts.length) * 100}%`}
/> />
</div> </div>
<ol class="list-none space-y-2"> <ol class="list-none space-y-2">
{seriesPosts.map((post) => { {seriesPosts.map((post) => {
const isCurrent = post.data.series?.part === currentPart; const isCurrent = post.data.series?.part === currentPart;
return ( return (
<li> <li>
<a <a
href={`/blog/${post.id}`} 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
class:list={[ 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 isCurrent
? "bg-sage text-white" ? "bg-brand-gradient-soft text-brand-deep dark:text-brand-cyan font-semibold"
: "bg-border text-text-light", : "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> class:list={[
<span>{post.data.title}</span> "flex-shrink-0 w-6 h-6 rounded-full flex items-center justify-center text-xs font-bold",
</a> isCurrent
</li> ? "bg-brand-gradient text-white"
); : "bg-slate-200 dark:bg-slate-700 text-slate-500 dark:text-slate-300",
})} ]}
</ol> >
</aside> {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> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="sitemap" href="/sitemap-index.xml" /> <link rel="sitemap" href="/sitemap-index.xml" />
<!-- SEO --> <!-- SEO -->
<title>{title} | IA qu'à...</title> <title>{title} | IA qu'à</title>
<meta name="description" content={description} /> <meta name="description" content={description} />
<link rel="canonical" href={canonicalURL} /> <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} /> <SeriesBadge name={series.name} part={series.part} total={series.total} />
</div> </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} {category}
</span> </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} {title}
</h1> </h1>
<p class="text-text-light">{description}</p> <p class="text-slate-500 dark:text-slate-400">{description}</p>
<div class="flex items-center gap-2 mt-4 text-sm text-text-light"> <div class="flex items-center gap-2 mt-4 text-sm text-slate-400 dark:text-slate-500">
<time datetime={pubDate.toISOString()}>{formattedDate}</time> <time datetime={pubDate.toISOString()}>{formattedDate}</time>
{updatedDate && ( {updatedDate && (
<> <>
@ -62,7 +62,7 @@ const formattedDate = pubDate.toLocaleDateString("fr-FR", {
<img <img
src={heroImage} src={heroImage}
alt={title} 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"> <BaseLayout title="À propos">
<article class="max-w-3xl mx-auto px-6 py-16"> <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 À propos
</h1> </h1>

View File

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

View File

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

View File

@ -2,60 +2,86 @@
@tailwind components; @tailwind components;
@tailwind utilities; @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 { @layer base {
html {
@apply scroll-smooth;
}
body { 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, h1,
h2, h2,
h3, h3,
h4 { 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 { .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 { .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 { .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 { .prose p {
@apply mb-5; @apply mb-5;
} }
.prose strong {
@apply text-slate-900 dark:text-slate-100 font-semibold;
}
.prose a { .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 { .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 { /* Inline code (not inside a fenced block) */
@apply bg-sage-light text-blue-night px-1.5 py-0.5 rounded text-sm; .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 { .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 { .prose pre code {
@apply bg-transparent text-white p-0; @apply bg-transparent p-0;
} }
.prose img { .prose img {
@apply rounded-lg my-6; @apply rounded-xl my-6 ring-1 ring-slate-200 dark:ring-slate-800;
} }
.prose ul, .prose ul,
@ -63,6 +89,22 @@
@apply my-4 pl-6; @apply my-4 pl-6;
} }
.prose ul {
@apply list-disc;
}
.prose ol {
@apply list-decimal;
}
.prose li { .prose li {
@apply mb-2; @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} */ /** @type {import('tailwindcss').Config} */
export default { export default {
content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"], content: ["./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}"],
darkMode: "class",
theme: { theme: {
extend: { extend: {
colors: { colors: {
"blue-night": "#1B2A4A", // ─── Brand "Electric Teal" ───
"blue-mid": "#2d4a7a", brand: {
sage: "#6B8F71", cyan: "#06B6D4", // accent primary
"sage-light": "#E8F0E9", blue: "#3B82F6", // accent secondary
"bg-warm": "#FAFAF7", teal: "#22D3EE", // deep marine highlight
"text-dark": "#2D2D2D", deep: "#0891B2", // deep teal (hover / prestige)
"text-light": "#6B6B6B", indigo: "#4F46E5",
border: "#E5E5E0", },
// ─── 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: { fontFamily: {
heading: ["Fraunces", "Georgia", "serif"], heading: ["Fraunces", "Georgia", "serif"],
body: ["Inter", "-apple-system", "BlinkMacSystemFont", "sans-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: [], plugins: [],