Compare commits
2 Commits
c3c9dbb408
...
642747d762
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
642747d762 | ||
|
|
3052a7e909 |
@ -1,13 +1,19 @@
|
||||
export function Footnotes() {
|
||||
return (
|
||||
<aside className="border-t border-line bg-cream">
|
||||
<div className="max-w-[820px] mx-auto px-5 sm:px-8 py-8">
|
||||
<div className="max-w-[820px] mx-auto px-5 sm:px-8 py-8 space-y-3">
|
||||
<p className="text-[13.5px] text-ink-3 leading-relaxed">
|
||||
<span className="text-rubis font-semibold mr-1.5">*</span>
|
||||
<b className="text-ink-2">OCR</b> — pour <i>Optical Character Recognition</i>. La
|
||||
reconnaissance automatique du texte sur un PDF ou une photo. La machine lit votre
|
||||
facture par-dessus votre épaule, en somme.
|
||||
</p>
|
||||
<p className="text-[13.5px] text-ink-3 leading-relaxed">
|
||||
<span className="text-rubis font-semibold mr-1.5">*</span>
|
||||
<b className="text-ink-2">DSO</b> — pour <i>Days Sales Outstanding</i>. Le délai
|
||||
moyen, en jours, entre l'émission d'une facture et son encaissement. Plus il est
|
||||
bas, plus votre trésorerie respire.
|
||||
</p>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
|
||||
@ -16,7 +16,7 @@ export function Gamification() {
|
||||
<p className="mt-5 max-w-[680px] mx-auto text-[17px] text-cream/85 leading-relaxed">
|
||||
À chaque relance que Rubis envoie à votre place, vous gagnez un rubis. À la fin du
|
||||
mois, vous voyez exactement combien d'heures vous avez récupérées. Pas un graphique
|
||||
de DSO. Pas un PDF abscons. Du temps. Concret.
|
||||
de DSO*. Du temps. Concret.
|
||||
</p>
|
||||
|
||||
<div className="mt-10 flex flex-col items-center gap-3">
|
||||
@ -35,7 +35,7 @@ export function Gamification() {
|
||||
</div>
|
||||
|
||||
<p className="mt-10 max-w-[460px] mx-auto text-[14px] text-cream/70">
|
||||
Et oui, on garde un classement amical. Les meilleurs utilisateurs libèrent{" "}
|
||||
Les meilleurs utilisateurs libèrent{" "}
|
||||
<b className="text-cream">30 heures par mois</b>. Plus de quoi prendre un long
|
||||
week-end. Toutes les 4 semaines.
|
||||
</p>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Button, Eyebrow, Gem, cn } from "@rubis/ui";
|
||||
import { Brand, Button, Eyebrow, Gem, cn } from "@rubis/ui";
|
||||
import { Check } from "lucide-react";
|
||||
|
||||
const APP_URL = "https://app.rubis.pro";
|
||||
@ -52,10 +52,14 @@ export function Hero() {
|
||||
<Check size={14} className="text-rubis" aria-hidden />
|
||||
30 jours gratuits puis Free 5 factures
|
||||
</span>
|
||||
<span aria-hidden className="size-[3px] rounded-full bg-ink-3" />
|
||||
<span>Hébergement souverain</span>
|
||||
<span aria-hidden className="size-[3px] rounded-full bg-ink-3" />
|
||||
<span>Made in France 🇫🇷</span>
|
||||
<span className="inline-flex items-center gap-3">
|
||||
<span aria-hidden className="size-[3px] rounded-full bg-ink-3" />
|
||||
Hébergement souverain
|
||||
</span>
|
||||
<span className="inline-flex items-center gap-3">
|
||||
<span aria-hidden className="size-[3px] rounded-full bg-ink-3" />
|
||||
Made in France 🇫🇷
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -68,22 +72,31 @@ export function Hero() {
|
||||
<div className="relative w-full max-w-[480px] mx-auto lg:ml-auto lg:mr-0">
|
||||
<div
|
||||
className={cn(
|
||||
"bg-white border border-line rounded-card shadow-card",
|
||||
"p-6 sm:p-7 lg:p-8",
|
||||
"bg-white border border-line rounded-card shadow-card overflow-hidden",
|
||||
)}
|
||||
>
|
||||
{/* Hero rubis */}
|
||||
<div className="flex items-center gap-4 pb-5 border-b border-line">
|
||||
<Gem size={56} glow />
|
||||
<div>
|
||||
<div className="font-display font-bold text-[32px] tracking-[-0.022em] leading-none text-ink">
|
||||
124 rubis
|
||||
</div>
|
||||
<div className="mt-1.5 text-[14px] text-ink-2">
|
||||
≈ <b className="text-ink">24 h 48</b> que vous n'avez pas passées à relancer.
|
||||
{/* Topbar mock — identifie la carte comme un dashboard Rubis.pro,
|
||||
pas juste un widget de chiffres flottants. */}
|
||||
<div className="flex items-center justify-between px-6 sm:px-7 lg:px-8 py-3.5 border-b border-line bg-cream/40">
|
||||
<Brand withSuffix gemSize={18} />
|
||||
<span className="text-[11px] uppercase tracking-[0.08em] font-semibold text-ink-3">
|
||||
Tableau de bord
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="p-6 sm:p-7 lg:p-8">
|
||||
{/* Hero rubis */}
|
||||
<div className="flex items-center gap-4 pb-5 border-b border-line">
|
||||
<Gem size={56} glow />
|
||||
<div>
|
||||
<div className="font-display font-bold text-[32px] tracking-[-0.022em] leading-none text-ink">
|
||||
124 rubis
|
||||
</div>
|
||||
<div className="mt-1.5 text-[14px] text-ink-2">
|
||||
≈ <b className="text-ink">24 h 48</b> que vous n'avez pas passées à relancer.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KPIs */}
|
||||
<div className="grid grid-cols-2 gap-5 mt-5">
|
||||
@ -100,7 +113,7 @@ export function Hero() {
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[10.5px] uppercase tracking-[0.06em] font-semibold text-ink-3">
|
||||
DSO
|
||||
DSO*
|
||||
</div>
|
||||
<div className="mt-1.5 font-display font-bold text-[22px] tracking-[-0.015em] text-ink tabular-nums">
|
||||
38 j
|
||||
@ -135,6 +148,7 @@ export function Hero() {
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Badge flottant — relatif au wrapper carte (max-w 480) */}
|
||||
|
||||
@ -17,7 +17,7 @@ export function HowItWorks() {
|
||||
<Step
|
||||
num="01"
|
||||
title="Vous importez vos factures."
|
||||
body="PDF, photo prise depuis votre téléphone à la caisse, scan reçu par mail — peu importe. L'OCR* lit, extrait montant, client, échéance, RIB. Vous vérifiez. Vingt secondes par facture, montre en main."
|
||||
body="PDF, photo prise depuis votre téléphone, scan reçu par mail — peu importe. L'OCR* lit, extrait montant, client, échéance. Vous vérifiez. Vingt secondes par facture, montre en main."
|
||||
>
|
||||
<DropzoneWidget />
|
||||
</Step>
|
||||
@ -47,7 +47,7 @@ export function HowItWorks() {
|
||||
Pendant que vous travaillez, Rubis envoie les emails au moment prévu, suit qui a
|
||||
ouvert, qui n'a pas répondu, et avant chaque relance vous demande discrètement par
|
||||
email : « Cette facture a-t-elle été réglée ? ». Vous répondez en deux secondes.
|
||||
La machine fait le reste.
|
||||
L'algorithme fait le reste.
|
||||
</p>
|
||||
}
|
||||
>
|
||||
|
||||
@ -7,14 +7,14 @@ export function Promise() {
|
||||
<div className="text-center max-w-[820px] mx-auto">
|
||||
<Eyebrow>Notre conviction</Eyebrow>
|
||||
<blockquote className="mt-6 font-display font-bold text-ink leading-[1.05] tracking-[-0.03em] text-[40px] sm:text-[56px] lg:text-[64px]">
|
||||
Votre temps vaut <em>plus que ça</em>.
|
||||
Votre temps est <em>plus précieux</em>.
|
||||
</blockquote>
|
||||
</div>
|
||||
|
||||
<div className="mt-14 grid lg:grid-cols-[1.4fr_1fr] gap-10 lg:gap-16 items-start max-w-[1080px] mx-auto">
|
||||
<div className="space-y-5 md:text-justify hyphens-auto">
|
||||
<p className="text-[17.5px] leading-relaxed text-ink-2">
|
||||
Vous n'avez pas créé votre boîte pour passer vos lundis soirs à rédiger des
|
||||
Vous n'avez pas créé votre entreprise pour passer vos journées à rédiger des
|
||||
relances polies. Pendant que vous écrivez "je me permets un petit rappel
|
||||
concernant…", vous ne facturez pas, vous ne vendez pas, vous ne créez pas.
|
||||
</p>
|
||||
@ -24,6 +24,9 @@ export function Promise() {
|
||||
<b className="text-ink">moins de 3</b>. Soit 5 heures de votre vie récupérées.
|
||||
Toutes les semaines. Pour toujours.
|
||||
</p>
|
||||
<p className="text-[15px] leading-relaxed text-ink-3 italic">
|
||||
Parfois moins, si votre plan par défaut est bien réglé.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-cream-2 border border-line rounded-card p-7">
|
||||
|
||||
@ -25,11 +25,10 @@ export function Stats() {
|
||||
<div className="text-center max-w-[640px] mx-auto mb-12">
|
||||
<Eyebrow>L'état des paiements en France</Eyebrow>
|
||||
<h2 className="mt-4 font-display font-bold text-ink leading-[1.1] tracking-[-0.025em] text-[34px] sm:text-[44px]">
|
||||
Trois chiffres qui devraient vous fâcher.
|
||||
Trois chiffres exorbitants.
|
||||
</h2>
|
||||
<p className="mt-4 text-[17px] text-ink-2 leading-relaxed">
|
||||
Si vous lisez ça, vous avez probablement une facture impayée à l'heure où on parle.
|
||||
Vous n'êtes pas un cas isolé.
|
||||
Et vous faites sûrement partie intégrante de ces enquêtes.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
305
docs/linkedin-launch-source.html
Normal file
305
docs/linkedin-launch-source.html
Normal file
@ -0,0 +1,305 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>LinkedIn launch image — Rubis.pro</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,600;12..96,700;12..96,800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--rubis: #9F1239;
|
||||
--rubis-deep: #771328;
|
||||
--rubis-light: #C9415C;
|
||||
--rubis-glow: #FBE4EA;
|
||||
--cream: #FAF7F2;
|
||||
--cream-2: #F4EFE7;
|
||||
--ink: #1A1410;
|
||||
--ink-2: #4A3F36;
|
||||
--ink-3: #8B7E73;
|
||||
--line: #E8E0D6;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
background: #2a2520;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40px 20px;
|
||||
gap: 20px;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
}
|
||||
.instructions {
|
||||
max-width: 900px;
|
||||
color: #ddd;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
background: #1a1410;
|
||||
padding: 16px 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.instructions code {
|
||||
background: #3a3530;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, Menlo, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
/*
|
||||
* #li : carré 1200×1200 — format optimal pour le feed LinkedIn mobile
|
||||
* (LinkedIn affiche les images carrées sans crop, ce qui maximise la
|
||||
* vertical real estate quand le user scrolle). Pour exporter : Chrome
|
||||
* DevTools → Elements → sélectionner #li → clic droit → "Capture node
|
||||
* screenshot".
|
||||
*/
|
||||
#li {
|
||||
width: 1200px;
|
||||
height: 1200px;
|
||||
background: var(--cream);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--ink);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 90px 90px 80px;
|
||||
}
|
||||
|
||||
/* Halo rubis en haut-droite — brand color signal sans dominer */
|
||||
.halo {
|
||||
position: absolute;
|
||||
top: -300px;
|
||||
right: -300px;
|
||||
width: 800px;
|
||||
height: 800px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, var(--rubis-glow) 0%, transparent 65%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Gem géant en filigrane derrière le headline — signature visuelle */
|
||||
.gem-bg {
|
||||
position: absolute;
|
||||
bottom: -140px;
|
||||
left: -140px;
|
||||
width: 720px;
|
||||
height: 720px;
|
||||
opacity: 0.08;
|
||||
transform: rotate(0deg);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ============ Header (brand lockup) ============ */
|
||||
.top {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 36px;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--ink);
|
||||
line-height: 1;
|
||||
}
|
||||
.brand .gem {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
}
|
||||
.brand .suffix {
|
||||
color: var(--ink-3);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 16px;
|
||||
background: white;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
.badge .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: #2d8a4e;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 3px rgba(45, 138, 78, 0.18);
|
||||
}
|
||||
|
||||
/* ============ Centre — headline + sub ============ */
|
||||
.center {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
max-width: 920px;
|
||||
}
|
||||
h1 {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 110px;
|
||||
line-height: 0.98;
|
||||
letter-spacing: -0.04em;
|
||||
color: var(--ink);
|
||||
}
|
||||
h1 em {
|
||||
font-style: italic;
|
||||
color: var(--rubis);
|
||||
font-weight: 800;
|
||||
}
|
||||
.sub {
|
||||
margin-top: 32px;
|
||||
font-size: 28px;
|
||||
line-height: 1.4;
|
||||
color: var(--ink-2);
|
||||
font-weight: 400;
|
||||
max-width: 760px;
|
||||
}
|
||||
.sub b { color: var(--ink); font-weight: 700; }
|
||||
|
||||
/* ============ Stat chip ============ */
|
||||
.stat-row {
|
||||
margin-top: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.stat {
|
||||
display: inline-flex;
|
||||
align-items: baseline;
|
||||
gap: 10px;
|
||||
padding: 14px 22px;
|
||||
background: var(--rubis);
|
||||
color: var(--cream);
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 8px 24px rgba(159, 18, 57, 0.25);
|
||||
}
|
||||
.stat .num {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 36px;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
}
|
||||
.stat .label {
|
||||
font-size: 17px;
|
||||
font-weight: 500;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.stat-sep {
|
||||
font-size: 22px;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
.stat-soft {
|
||||
font-size: 20px;
|
||||
color: var(--ink-2);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ============ Footer ============ */
|
||||
.bottom {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
}
|
||||
.domain {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--ink);
|
||||
}
|
||||
.domain .arrow {
|
||||
color: var(--rubis);
|
||||
margin-right: 8px;
|
||||
}
|
||||
.tag {
|
||||
font-size: 17px;
|
||||
color: var(--ink-3);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p class="instructions">
|
||||
<strong>Comment exporter en PNG 1200×1200</strong> — ouvrir Chrome DevTools (<code>⌘⌥I</code>),
|
||||
sélectionner la div <code>#li</code> dans l'arbre Elements, clic droit →
|
||||
<em>Capture node screenshot</em>. Format optimal pour un post LinkedIn dans le feed mobile.
|
||||
</p>
|
||||
|
||||
<div id="li">
|
||||
<div class="halo"></div>
|
||||
|
||||
<!-- Gem fantôme en arrière-plan -->
|
||||
<svg class="gem-bg" viewBox="0 0 100 100" fill="none">
|
||||
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1"/>
|
||||
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8"/>
|
||||
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65"/>
|
||||
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48"/>
|
||||
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
||||
<!-- Top : brand + statut -->
|
||||
<div class="top">
|
||||
<div class="brand">
|
||||
<svg class="gem" viewBox="0 0 100 100" fill="none">
|
||||
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1"/>
|
||||
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8"/>
|
||||
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65"/>
|
||||
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48"/>
|
||||
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>Rubis<span class="suffix">.pro</span></span>
|
||||
</div>
|
||||
<span class="badge">
|
||||
<span class="dot"></span>
|
||||
En ligne
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Centre : headline -->
|
||||
<div class="center">
|
||||
<h1>
|
||||
Vos factures<br />
|
||||
relancées<br />
|
||||
<em>toutes seules</em>.
|
||||
</h1>
|
||||
<p class="sub">
|
||||
Le SaaS de relance pour TPE-PME françaises. Drag-and-drop, OCR,
|
||||
plans de relance automatiques.
|
||||
</p>
|
||||
<div class="stat-row">
|
||||
<span class="stat">
|
||||
<span class="num">5 h</span>
|
||||
<span class="label">par semaine récupérées</span>
|
||||
</span>
|
||||
<span class="stat-sep">·</span>
|
||||
<span class="stat-soft">8 h → moins de 3</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom : domaine + tag -->
|
||||
<div class="bottom">
|
||||
<span class="domain">
|
||||
<span class="arrow">→</span>rubis.pro
|
||||
</span>
|
||||
<span class="tag">30 jours gratuits · sans carte bancaire</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
585
docs/logo-export.html
Normal file
585
docs/logo-export.html
Normal file
@ -0,0 +1,585 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Logo export — Rubis.pro</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,500;12..96,600;12..96,700;12..96,800&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--rubis: #9F1239;
|
||||
--rubis-deep: #771328;
|
||||
--rubis-light: #C9415C;
|
||||
--rubis-glow: #FBE4EA;
|
||||
--cream: #FAF7F2;
|
||||
--ink: #1A1410;
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
background: var(--cream);
|
||||
color: var(--ink);
|
||||
min-height: 100vh;
|
||||
padding: 32px;
|
||||
}
|
||||
.app {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
gap: 32px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 28px;
|
||||
margin: 0 0 4px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.sub {
|
||||
font-size: 13px;
|
||||
color: #6b5f57;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.panel {
|
||||
background: white;
|
||||
border: 1px solid #e8e0d6;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
.controls h2 {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: #6b5f57;
|
||||
margin: 0 0 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.group { margin-bottom: 20px; }
|
||||
.group:last-child { margin-bottom: 0; }
|
||||
.seg {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 6px;
|
||||
}
|
||||
.seg.three { grid-template-columns: repeat(3, 1fr); }
|
||||
.seg button {
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 13px;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #e8e0d6;
|
||||
background: white;
|
||||
color: var(--ink);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s;
|
||||
}
|
||||
.seg button:hover { border-color: var(--rubis-light); }
|
||||
.seg button.active {
|
||||
background: var(--rubis);
|
||||
color: white;
|
||||
border-color: var(--rubis);
|
||||
}
|
||||
.swatches {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 6px;
|
||||
}
|
||||
.swatches button {
|
||||
aspect-ratio: 1;
|
||||
border: 2px solid #e8e0d6;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s;
|
||||
position: relative;
|
||||
}
|
||||
.swatches button.active {
|
||||
border-color: var(--rubis);
|
||||
box-shadow: 0 0 0 2px var(--rubis-glow);
|
||||
}
|
||||
.swatches button[data-bg="transparent"] {
|
||||
background-image:
|
||||
linear-gradient(45deg, #ddd 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #ddd 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #ddd 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #ddd 75%);
|
||||
background-size: 8px 8px;
|
||||
background-position: 0 0, 0 4px, 4px -4px, -4px 0;
|
||||
}
|
||||
label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
color: #6b5f57;
|
||||
}
|
||||
input[type="range"] {
|
||||
width: 100%;
|
||||
accent-color: var(--rubis);
|
||||
}
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
padding: 8px 10px;
|
||||
border: 1px solid #e8e0d6;
|
||||
border-radius: 8px;
|
||||
font-family: "Inter", sans-serif;
|
||||
font-size: 13px;
|
||||
}
|
||||
input[type="text"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--rubis);
|
||||
box-shadow: 0 0 0 3px var(--rubis-glow);
|
||||
}
|
||||
.toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 10px;
|
||||
background: var(--cream);
|
||||
border-radius: 8px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.toggle input { margin: 0; accent-color: var(--rubis); }
|
||||
.export {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: var(--rubis);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.export:hover { background: var(--rubis-deep); }
|
||||
|
||||
.preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.preview-stage {
|
||||
flex: 1;
|
||||
min-height: 480px;
|
||||
border: 1px dashed #d4cabe;
|
||||
border-radius: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
background-image:
|
||||
linear-gradient(45deg, #f0e9de 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #f0e9de 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #f0e9de 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #f0e9de 75%);
|
||||
background-size: 16px 16px;
|
||||
background-position: 0 0, 0 8px, 8px -8px, -8px 0;
|
||||
}
|
||||
#stage canvas {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
box-shadow: 0 8px 30px rgba(26, 20, 16, 0.12);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.info {
|
||||
font-size: 12px;
|
||||
color: #6b5f57;
|
||||
text-align: center;
|
||||
}
|
||||
.font-status {
|
||||
font-size: 12px;
|
||||
color: #6b5f57;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.font-status.loaded { color: #2d8a4e; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<aside class="controls">
|
||||
<h1>Logo export</h1>
|
||||
<p class="sub">Wordmark Rubis.pro · Bricolage Grotesque · PNG haute résolution</p>
|
||||
|
||||
<div class="panel">
|
||||
<div class="group">
|
||||
<h2>Texte</h2>
|
||||
<input type="text" id="text" value="Rubis.pro" />
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<h2>Layout</h2>
|
||||
<div class="seg three" id="layout">
|
||||
<button data-layout="horizontal" class="active">Horizontal</button>
|
||||
<button data-layout="vertical">Vertical</button>
|
||||
<button data-layout="wordmark">Texte seul</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<h2>Couleur du logo</h2>
|
||||
<div class="seg three" id="fg">
|
||||
<button data-fg="rubis" class="active">Rubis</button>
|
||||
<button data-fg="ink">Encre</button>
|
||||
<button data-fg="cream">Crème</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<h2>Fond</h2>
|
||||
<div class="swatches" id="bg">
|
||||
<button data-bg="cream" style="background:#FAF7F2" class="active" title="Crème"></button>
|
||||
<button data-bg="white" style="background:#ffffff" title="Blanc"></button>
|
||||
<button data-bg="ink" style="background:#1A1410" title="Encre"></button>
|
||||
<button data-bg="rubis" style="background:#9F1239" title="Rubis"></button>
|
||||
<button data-bg="transparent" title="Transparent"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<h2>Graisse</h2>
|
||||
<div class="seg three" id="weight">
|
||||
<button data-weight="500">500</button>
|
||||
<button data-weight="700" class="active">700</button>
|
||||
<button data-weight="800">800</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<h2>Taille export</h2>
|
||||
<div class="seg" id="size-presets" style="grid-template-columns: repeat(5, 1fr); margin-bottom: 8px;">
|
||||
<button data-size="120" title="Google OAuth consent (120×120)">120</button>
|
||||
<button data-size="512">512</button>
|
||||
<button data-size="1024">1024</button>
|
||||
<button data-size="2048" class="active">2048</button>
|
||||
<button data-size="4096">4096</button>
|
||||
</div>
|
||||
<label><span>Côté</span><span id="size-val">2048 × 2048 px</span></label>
|
||||
<input type="range" id="size" min="120" max="4096" step="8" value="2048" />
|
||||
</div>
|
||||
|
||||
<div class="group">
|
||||
<label class="toggle">
|
||||
<span>Padding interne</span>
|
||||
<input type="checkbox" id="padding" checked />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="export" id="export-btn">Télécharger en PNG</button>
|
||||
<p class="font-status" id="font-status">Chargement de la police…</p>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main class="preview">
|
||||
<div class="preview-stage">
|
||||
<div id="stage"></div>
|
||||
</div>
|
||||
<p class="info" id="info"></p>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// --- State ---
|
||||
const state = {
|
||||
text: "Rubis.pro",
|
||||
layout: "horizontal",
|
||||
fg: "rubis",
|
||||
bg: "cream",
|
||||
weight: 700,
|
||||
size: 2048,
|
||||
padding: true,
|
||||
};
|
||||
|
||||
const COLORS = {
|
||||
rubis: "#9F1239",
|
||||
"rubis-deep": "#771328",
|
||||
"rubis-light": "#C9415C",
|
||||
ink: "#1A1410",
|
||||
cream: "#FAF7F2",
|
||||
white: "#ffffff",
|
||||
};
|
||||
|
||||
// --- Drawing ---
|
||||
/**
|
||||
* Dessine le gem facetté (4 facettes + contour) à la position (x, y) avec
|
||||
* la taille `size` et la couleur `color`. Reproduit exactement <Gem/> du
|
||||
* SPA / favicon.svg, mais en canvas — garantit l'identité visuelle 1:1.
|
||||
*/
|
||||
function drawGem(ctx, x, y, size, color) {
|
||||
const s = size / 88; // viewBox interne 88x88 (de 6 à 94)
|
||||
const cx = x + size / 2;
|
||||
const cy = y + size / 2;
|
||||
const tx = (px) => cx + (px - 50) * s;
|
||||
const ty = (py) => cy + (py - 50) * s;
|
||||
|
||||
const facets = [
|
||||
{ pts: [[50, 6], [6, 50], [50, 50]], alpha: 1.0 },
|
||||
{ pts: [[50, 6], [94, 50], [50, 50]], alpha: 0.8 },
|
||||
{ pts: [[6, 50], [50, 50], [50, 94]], alpha: 0.65 },
|
||||
{ pts: [[50, 50], [94, 50], [50, 94]], alpha: 0.48 },
|
||||
];
|
||||
for (const { pts, alpha } of facets) {
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.fillStyle = color;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(tx(pts[0][0]), ty(pts[0][1]));
|
||||
ctx.lineTo(tx(pts[1][0]), ty(pts[1][1]));
|
||||
ctx.lineTo(tx(pts[2][0]), ty(pts[2][1]));
|
||||
ctx.closePath();
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.strokeStyle = color;
|
||||
ctx.lineWidth = Math.max(1.6 * s, 1);
|
||||
ctx.lineJoin = "round";
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(tx(50), ty(6));
|
||||
ctx.lineTo(tx(94), ty(50));
|
||||
ctx.lineTo(tx(50), ty(94));
|
||||
ctx.lineTo(tx(6), ty(50));
|
||||
ctx.closePath();
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/**
|
||||
* Construit un canvas avec le logo selon le state courant.
|
||||
* Renvoie { canvas, width, height } — le canvas est dimensionné pour matcher
|
||||
* le contenu (avec padding optionnel), pas forcé carré.
|
||||
*/
|
||||
function renderLogo(opts = {}) {
|
||||
const exportSize = opts.size ?? state.size;
|
||||
const dpr = opts.dpr ?? 1;
|
||||
const fgColor = COLORS[state.fg];
|
||||
const bgColor = state.bg === "transparent" ? null : COLORS[state.bg];
|
||||
|
||||
// Le wordmark est dimensionné en fonction de la largeur cible.
|
||||
// Bricolage Grotesque a une cap-height ~0.72 et un descender ~0.22.
|
||||
// Pour un rendu propre, on calcule d'abord la métrique du texte puis
|
||||
// on positionne tout autour.
|
||||
|
||||
// Off-screen mesure
|
||||
const measure = document.createElement("canvas").getContext("2d");
|
||||
|
||||
// En layout "wordmark" pur, le texte fait ~80% de la largeur.
|
||||
// En "horizontal", gem prend ~0.18 de la largeur, gap ~0.05, texte le reste.
|
||||
// En "vertical", gem au-dessus, texte en-dessous.
|
||||
|
||||
const padding = state.padding ? exportSize * 0.08 : 0;
|
||||
|
||||
let textHeightRatio; // hauteur du texte / hauteur totale du contenu
|
||||
let gemSize, gap, contentW, contentH;
|
||||
let fontSize;
|
||||
|
||||
if (state.layout === "wordmark") {
|
||||
// Texte seul : on calibre la fontSize pour que le texte rendu fasse
|
||||
// ~exportSize - 2*padding de large.
|
||||
const targetW = exportSize - 2 * padding;
|
||||
fontSize = 100;
|
||||
measure.font = `${state.weight} ${fontSize}px "Bricolage Grotesque", sans-serif`;
|
||||
const w0 = measure.measureText(state.text).width;
|
||||
fontSize = (targetW / w0) * fontSize;
|
||||
measure.font = `${state.weight} ${fontSize}px "Bricolage Grotesque", sans-serif`;
|
||||
const m = measure.measureText(state.text);
|
||||
const ascent = m.actualBoundingBoxAscent || fontSize * 0.75;
|
||||
const descent = m.actualBoundingBoxDescent || fontSize * 0.22;
|
||||
contentW = m.width;
|
||||
contentH = ascent + descent;
|
||||
gemSize = 0;
|
||||
gap = 0;
|
||||
} else if (state.layout === "horizontal") {
|
||||
// Gem + texte côte à côte. Hauteur du gem = hauteur du texte (cap-height-ish).
|
||||
// On vise une largeur totale = exportSize - 2*padding.
|
||||
const targetW = exportSize - 2 * padding;
|
||||
// Premier passage : on suppose gem = hauteur du texte ≈ 0.78 * fontSize.
|
||||
// gap = 0.18 * gemSize, contentW = gemSize + gap + textW.
|
||||
// On calibre fontSize pour que tout ça fasse targetW.
|
||||
fontSize = 100;
|
||||
measure.font = `${state.weight} ${fontSize}px "Bricolage Grotesque", sans-serif`;
|
||||
const w0 = measure.measureText(state.text).width;
|
||||
// gem ≈ 0.78 * fontSize (proche de la cap-height)
|
||||
// gap ≈ 0.22 * gem
|
||||
const gemRatio = 0.85;
|
||||
const gapRatio = 0.18;
|
||||
const totalAtFs100 = gemRatio * fontSize + gapRatio * gemRatio * fontSize + w0;
|
||||
fontSize = (targetW / totalAtFs100) * fontSize;
|
||||
measure.font = `${state.weight} ${fontSize}px "Bricolage Grotesque", sans-serif`;
|
||||
const m = measure.measureText(state.text);
|
||||
const ascent = m.actualBoundingBoxAscent || fontSize * 0.75;
|
||||
const descent = m.actualBoundingBoxDescent || fontSize * 0.22;
|
||||
gemSize = gemRatio * fontSize;
|
||||
gap = gapRatio * gemSize;
|
||||
contentW = gemSize + gap + m.width;
|
||||
contentH = Math.max(gemSize, ascent + descent);
|
||||
} else {
|
||||
// vertical : gem au-dessus, texte en-dessous, centrés horizontalement
|
||||
const targetW = exportSize - 2 * padding;
|
||||
fontSize = 100;
|
||||
measure.font = `${state.weight} ${fontSize}px "Bricolage Grotesque", sans-serif`;
|
||||
const w0 = measure.measureText(state.text).width;
|
||||
fontSize = (targetW / w0) * fontSize;
|
||||
measure.font = `${state.weight} ${fontSize}px "Bricolage Grotesque", sans-serif`;
|
||||
const m = measure.measureText(state.text);
|
||||
const ascent = m.actualBoundingBoxAscent || fontSize * 0.75;
|
||||
const descent = m.actualBoundingBoxDescent || fontSize * 0.22;
|
||||
gemSize = m.width * 0.42; // gem proportionné au texte
|
||||
gap = gemSize * 0.25;
|
||||
contentW = m.width;
|
||||
contentH = gemSize + gap + ascent + descent;
|
||||
}
|
||||
|
||||
// Canvas final : carré exportSize × exportSize, contenu centré.
|
||||
const totalW = exportSize;
|
||||
const totalH = exportSize;
|
||||
|
||||
const canvas = document.createElement("canvas");
|
||||
canvas.width = Math.round(totalW * dpr);
|
||||
canvas.height = Math.round(totalH * dpr);
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
// Fond
|
||||
if (bgColor) {
|
||||
ctx.fillStyle = bgColor;
|
||||
ctx.fillRect(0, 0, totalW, totalH);
|
||||
}
|
||||
|
||||
// Dessin selon layout
|
||||
ctx.fillStyle = fgColor;
|
||||
ctx.textBaseline = "alphabetic";
|
||||
ctx.font = `${state.weight} ${fontSize}px "Bricolage Grotesque", sans-serif`;
|
||||
|
||||
if (state.layout === "wordmark") {
|
||||
const m = ctx.measureText(state.text);
|
||||
const ascent = m.actualBoundingBoxAscent || fontSize * 0.75;
|
||||
const descent = m.actualBoundingBoxDescent || fontSize * 0.22;
|
||||
const x = (totalW - contentW) / 2;
|
||||
const y = totalH / 2 - (ascent + descent) / 2 + ascent;
|
||||
ctx.fillText(state.text, x, y);
|
||||
} else if (state.layout === "horizontal") {
|
||||
const m = ctx.measureText(state.text);
|
||||
const ascent = m.actualBoundingBoxAscent || fontSize * 0.75;
|
||||
const descent = m.actualBoundingBoxDescent || fontSize * 0.22;
|
||||
const textH = ascent + descent;
|
||||
const startX = (totalW - contentW) / 2;
|
||||
const centerY = totalH / 2;
|
||||
// Gem centré verticalement
|
||||
const gemY = centerY - gemSize / 2;
|
||||
drawGem(ctx, startX, gemY, gemSize, fgColor);
|
||||
// Texte aligné sur la baseline → on positionne pour que le centre vertical du texte = centerY
|
||||
const textY = centerY - (ascent + descent) / 2 + ascent;
|
||||
ctx.fillStyle = fgColor;
|
||||
ctx.fillText(state.text, startX + gemSize + gap, textY);
|
||||
} else {
|
||||
// vertical
|
||||
const m = ctx.measureText(state.text);
|
||||
const ascent = m.actualBoundingBoxAscent || fontSize * 0.75;
|
||||
const gemX = (totalW - gemSize) / 2;
|
||||
const gemY = (totalH - contentH) / 2;
|
||||
drawGem(ctx, gemX, gemY, gemSize, fgColor);
|
||||
ctx.fillStyle = fgColor;
|
||||
const textX = (totalW - m.width) / 2;
|
||||
const textY = gemY + gemSize + gap + ascent;
|
||||
ctx.fillText(state.text, textX, textY);
|
||||
}
|
||||
|
||||
return { canvas, width: totalW, height: totalH };
|
||||
}
|
||||
|
||||
// --- Preview ---
|
||||
function updatePreview() {
|
||||
const stage = document.getElementById("stage");
|
||||
stage.innerHTML = "";
|
||||
// Preview = mêmes proportions, taille bornée pour l'affichage
|
||||
const previewSize = Math.min(state.size, 1024);
|
||||
const { canvas, width, height } = renderLogo({ size: previewSize, dpr: 2 });
|
||||
// Cadre transparent visible si bg = transparent
|
||||
stage.appendChild(canvas);
|
||||
document.getElementById("info").textContent =
|
||||
`Export : ${state.size} × ${Math.round((height / width) * state.size)} px`;
|
||||
}
|
||||
|
||||
// --- Export ---
|
||||
function exportPng() {
|
||||
const { canvas } = renderLogo({ size: state.size, dpr: 1 });
|
||||
const variant = [state.layout, state.fg, state.bg, `${state.size}px`].join("-");
|
||||
const filename = `rubis-logo-${variant}.png`;
|
||||
canvas.toBlob((blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
}, "image/png");
|
||||
}
|
||||
|
||||
// --- Wiring ---
|
||||
function wireSegmented(containerId, key, type = "string") {
|
||||
const container = document.getElementById(containerId);
|
||||
container.addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("button");
|
||||
if (!btn) return;
|
||||
container.querySelectorAll("button").forEach((b) => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
const val = btn.dataset[key];
|
||||
state[key] = type === "number" ? Number(val) : val;
|
||||
updatePreview();
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("text").addEventListener("input", (e) => {
|
||||
state.text = e.target.value || "Rubis.pro";
|
||||
updatePreview();
|
||||
});
|
||||
wireSegmented("layout", "layout");
|
||||
wireSegmented("fg", "fg");
|
||||
wireSegmented("bg", "bg");
|
||||
wireSegmented("weight", "weight", "number");
|
||||
|
||||
function setSize(value) {
|
||||
state.size = Number(value);
|
||||
document.getElementById("size").value = state.size;
|
||||
document.getElementById("size-val").textContent = `${state.size} × ${state.size} px`;
|
||||
const presets = document.getElementById("size-presets");
|
||||
presets.querySelectorAll("button").forEach((b) => {
|
||||
b.classList.toggle("active", Number(b.dataset.size) === state.size);
|
||||
});
|
||||
updatePreview();
|
||||
}
|
||||
document.getElementById("size").addEventListener("input", (e) => setSize(e.target.value));
|
||||
document.getElementById("size-presets").addEventListener("click", (e) => {
|
||||
const btn = e.target.closest("button");
|
||||
if (btn) setSize(btn.dataset.size);
|
||||
});
|
||||
document.getElementById("padding").addEventListener("change", (e) => {
|
||||
state.padding = e.target.checked;
|
||||
updatePreview();
|
||||
});
|
||||
document.getElementById("export-btn").addEventListener("click", exportPng);
|
||||
|
||||
// --- Font loading ---
|
||||
// On attend que Bricolage Grotesque soit chargée avant de rendre le canvas,
|
||||
// sinon le navigateur fallback en sans-serif et le rendu est faux.
|
||||
const status = document.getElementById("font-status");
|
||||
Promise.all([
|
||||
document.fonts.load('700 100px "Bricolage Grotesque"'),
|
||||
document.fonts.load('800 100px "Bricolage Grotesque"'),
|
||||
document.fonts.load('500 100px "Bricolage Grotesque"'),
|
||||
]).then(() => {
|
||||
status.textContent = "Police chargée — prêt à exporter.";
|
||||
status.classList.add("loaded");
|
||||
updatePreview();
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
355
docs/og-default-source.html
Normal file
355
docs/og-default-source.html
Normal file
@ -0,0 +1,355 @@
|
||||
<!doctype html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>OG default source — Rubis.pro</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,600;12..96,700;12..96,800&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<style>
|
||||
:root {
|
||||
--rubis: #9F1239;
|
||||
--rubis-deep: #771328;
|
||||
--rubis-light: #C9415C;
|
||||
--rubis-glow: #FBE4EA;
|
||||
--cream: #FAF7F2;
|
||||
--cream-2: #F4EFE7;
|
||||
--ink: #1A1410;
|
||||
--ink-2: #4A3F36;
|
||||
--ink-3: #8B7E73;
|
||||
--line: #E8E0D6;
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body {
|
||||
background: #2a2520;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40px 20px;
|
||||
gap: 20px;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
}
|
||||
.instructions {
|
||||
max-width: 900px;
|
||||
color: #ddd;
|
||||
font-size: 14px;
|
||||
line-height: 1.6;
|
||||
background: #1a1410;
|
||||
padding: 16px 20px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.instructions code {
|
||||
background: #3a3530;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: ui-monospace, Menlo, monospace;
|
||||
font-size: 13px;
|
||||
}
|
||||
.instructions a { color: #FBE4EA; }
|
||||
/*
|
||||
* #og : EXACTEMENT 1200×630 (specs Open Graph).
|
||||
* Pour exporter : Chrome DevTools → Inspecter cette div → clic droit
|
||||
* → "Capture node screenshot". Le PNG sortira pile à 1200×630.
|
||||
*/
|
||||
#og {
|
||||
width: 1200px;
|
||||
height: 630px;
|
||||
background: var(--cream);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
font-family: "Inter", system-ui, sans-serif;
|
||||
color: var(--ink);
|
||||
}
|
||||
/* Halo rubis discret en haut-droite */
|
||||
.halo {
|
||||
position: absolute;
|
||||
top: -200px;
|
||||
right: -200px;
|
||||
width: 600px;
|
||||
height: 600px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, var(--rubis-glow) 0%, transparent 65%);
|
||||
pointer-events: none;
|
||||
}
|
||||
/* Triangle fantôme en bas (clin d'œil au gem) */
|
||||
.ghost-tri {
|
||||
position: absolute;
|
||||
bottom: -100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%) rotate(45deg);
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
background: var(--rubis);
|
||||
opacity: 0.06;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
padding: 70px 80px;
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 1fr;
|
||||
gap: 60px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* ============ Brand lockup ============ */
|
||||
.brand {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 28px;
|
||||
letter-spacing: -0.02em;
|
||||
color: var(--ink);
|
||||
line-height: 1;
|
||||
}
|
||||
.brand .gem {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
.brand .suffix {
|
||||
color: var(--ink-3);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
/* ============ Headline ============ */
|
||||
h1 {
|
||||
margin-top: 32px;
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 70px;
|
||||
line-height: 1.02;
|
||||
letter-spacing: -0.035em;
|
||||
color: var(--ink);
|
||||
}
|
||||
h1 em {
|
||||
font-style: italic;
|
||||
color: var(--rubis);
|
||||
font-weight: 800;
|
||||
}
|
||||
.sub {
|
||||
margin-top: 24px;
|
||||
font-size: 22px;
|
||||
line-height: 1.45;
|
||||
color: var(--ink-2);
|
||||
max-width: 520px;
|
||||
}
|
||||
.sub b { color: var(--ink); font-weight: 700; }
|
||||
|
||||
.cta {
|
||||
margin-top: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 18px;
|
||||
background: white;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 999px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
box-shadow: 0 4px 12px rgba(26, 20, 16, 0.06);
|
||||
}
|
||||
.cta .gem {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
/* ============ Mock card ============ */
|
||||
.mock {
|
||||
background: white;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 12px 40px rgba(26, 20, 16, 0.10);
|
||||
overflow: hidden;
|
||||
}
|
||||
.mock-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 22px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: rgba(244, 239, 231, 0.4);
|
||||
}
|
||||
.mock-topbar .brand { font-size: 16px; }
|
||||
.mock-topbar .brand .gem { width: 18px; height: 18px; }
|
||||
.mock-topbar .label {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
font-weight: 600;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
.mock-body { padding: 24px 26px; }
|
||||
.mock-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.mock-hero .gem-glow {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
filter: drop-shadow(0 4px 16px rgba(159, 18, 57, 0.35));
|
||||
}
|
||||
.mock-count {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 32px;
|
||||
letter-spacing: -0.022em;
|
||||
line-height: 1;
|
||||
color: var(--ink);
|
||||
}
|
||||
.mock-count-sub {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
.mock-count-sub b { color: var(--ink); font-weight: 700; }
|
||||
.mock-kpis {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 18px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.kpi-label {
|
||||
font-size: 9.5px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
font-weight: 600;
|
||||
color: var(--ink-3);
|
||||
}
|
||||
.kpi-value {
|
||||
margin-top: 4px;
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 22px;
|
||||
letter-spacing: -0.015em;
|
||||
color: var(--ink);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.kpi-delta {
|
||||
margin-top: 3px;
|
||||
font-size: 10.5px;
|
||||
color: var(--rubis);
|
||||
font-weight: 500;
|
||||
}
|
||||
.mock-activity {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: 14px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px dashed var(--line);
|
||||
font-size: 12px;
|
||||
}
|
||||
.mock-activity .text { color: var(--ink-2); }
|
||||
.mock-activity b { color: var(--ink); font-weight: 600; }
|
||||
.mock-activity time {
|
||||
color: var(--ink-3);
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 10.5px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p class="instructions">
|
||||
<strong>Comment exporter en PNG 1200×630</strong> — ouvrir Chrome DevTools (<code>⌘⌥I</code>),
|
||||
sélectionner la div <code>#og</code>, clic droit dans l'arbre → <em>Capture node screenshot</em>.
|
||||
Le PNG sort pile à 1200×630. Remplacer ensuite
|
||||
<code>apps/landing/public/og-default.png</code>.
|
||||
</p>
|
||||
|
||||
<div id="og">
|
||||
<div class="halo"></div>
|
||||
<div class="ghost-tri"></div>
|
||||
|
||||
<div class="content">
|
||||
<div>
|
||||
<div class="brand">
|
||||
<svg class="gem" viewBox="0 0 100 100" fill="none">
|
||||
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1"/>
|
||||
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8"/>
|
||||
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65"/>
|
||||
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48"/>
|
||||
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>Rubis<span class="suffix">.pro</span></span>
|
||||
</div>
|
||||
|
||||
<h1>
|
||||
Vos factures relancées <em>toutes seules</em>.
|
||||
</h1>
|
||||
|
||||
<p class="sub">
|
||||
Le SaaS de relance pour TPE-PME françaises. <b>5 heures par semaine</b> récupérées, automatiquement.
|
||||
</p>
|
||||
|
||||
<span class="cta">
|
||||
<svg class="gem" viewBox="0 0 100 100" fill="none">
|
||||
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1"/>
|
||||
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8"/>
|
||||
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65"/>
|
||||
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48"/>
|
||||
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
rubis.pro
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="mock">
|
||||
<div class="mock-topbar">
|
||||
<div class="brand">
|
||||
<svg class="gem" viewBox="0 0 100 100" fill="none">
|
||||
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1"/>
|
||||
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8"/>
|
||||
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65"/>
|
||||
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48"/>
|
||||
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<span>Rubis<span class="suffix">.pro</span></span>
|
||||
</div>
|
||||
<span class="label">Tableau de bord</span>
|
||||
</div>
|
||||
<div class="mock-body">
|
||||
<div class="mock-hero">
|
||||
<svg class="gem-glow" viewBox="0 0 100 100" fill="none">
|
||||
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1"/>
|
||||
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8"/>
|
||||
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65"/>
|
||||
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48"/>
|
||||
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
<div>
|
||||
<div class="mock-count">124 rubis</div>
|
||||
<div class="mock-count-sub">≈ <b>24 h 48</b> libérées ce mois</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mock-kpis">
|
||||
<div>
|
||||
<div class="kpi-label">Encaissé</div>
|
||||
<div class="kpi-value">14 320 €</div>
|
||||
<div class="kpi-delta">+ 2 800 € vs avril</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="kpi-label">DSO*</div>
|
||||
<div class="kpi-value">38 j</div>
|
||||
<div class="kpi-delta">↘ −6 j depuis Rubis</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mock-activity">
|
||||
<span class="text">✓ Facture <b>F-2024-035</b> encaissée</span>
|
||||
<time>10:02</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -2,8 +2,11 @@ import { cn } from "../lib/cn.js";
|
||||
import { Gem } from "./Gem.js";
|
||||
|
||||
/**
|
||||
* Lockup horizontal : ◆ + "Rubis" (+ optionnel "sur l'ongle" en suffixe italique muted).
|
||||
* À utiliser dans les headers, le sidebar, les emails.
|
||||
* Lockup horizontal : ◆ + "Rubis" (+ optionnel suffixe ".pro" muted, attaché
|
||||
* au wordmark sans espace). À utiliser dans les headers, le sidebar, les
|
||||
* emails. Le suffixe rend le mark identifiant (cf. exigence Google OAuth :
|
||||
* un logo doit "identifier la marque de manière unique" — la gem seule
|
||||
* est trop générique).
|
||||
*
|
||||
* Architecture : on s'appuie sur le composant <Gem/> SVG — pas de PNG, pas
|
||||
* de border externe. La pierre EST le logo. Plus rien ne casse à l'export
|
||||
@ -13,7 +16,7 @@ import { Gem } from "./Gem.js";
|
||||
* Cf. /docs/marque.md §2 et le pattern de la landing.
|
||||
*/
|
||||
type BrandProps = {
|
||||
/** Affiche le suffixe "sur l'ongle" en italique muted. */
|
||||
/** Affiche le suffixe ".pro" attaché au wordmark, en couleur muted. */
|
||||
withSuffix?: boolean;
|
||||
/**
|
||||
* Taille de la gem en pixels. Default 22 (lockup) ou 32 (onlyImage).
|
||||
@ -54,16 +57,7 @@ export function Brand({
|
||||
<Gem size={resolvedSize} />
|
||||
<span className="leading-none">
|
||||
Rubis
|
||||
{withSuffix && (
|
||||
<span
|
||||
className={cn(
|
||||
"ml-1 font-display italic font-medium text-ink-3",
|
||||
"text-[12.5px] tracking-[-0.005em]",
|
||||
)}
|
||||
>
|
||||
sur l'ongle
|
||||
</span>
|
||||
)}
|
||||
{withSuffix && <span className="text-ink-3">.pro</span>}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user