Premier jet : gabarit SVG et rendu PNG
This commit is contained in:
commit
81fd9dd717
5 changed files with 761 additions and 0 deletions
197
static/app.js
Normal file
197
static/app.js
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
/* Card Generator — frontend */
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const preview = $("preview-frame");
|
||||
const status = $("status");
|
||||
|
||||
let state = {
|
||||
eyebrow: "RÉALISATION · SYMFONY",
|
||||
title: "pokedwwmm",
|
||||
title_2: "",
|
||||
tagline: "151 Pokémon · Twig Components · JSON",
|
||||
accent: "#ef4444",
|
||||
logo_color:"#ef4444",
|
||||
logo_kind: "preset", // preset | upload | text | none
|
||||
logo_value:"symfony",
|
||||
logo_size: 260,
|
||||
};
|
||||
|
||||
let renderTimer = null;
|
||||
let logoLibrary = [];
|
||||
|
||||
/* ── Wire up form ──────────────────────────────────────────────── */
|
||||
|
||||
const inputBindings = {
|
||||
eyebrow: "eyebrow",
|
||||
title: "title",
|
||||
title_2: "title_2",
|
||||
tagline: "tagline",
|
||||
accent: "accent",
|
||||
logo_color: "logo_color",
|
||||
};
|
||||
|
||||
for (const [stateKey, inputId] of Object.entries(inputBindings)) {
|
||||
$(inputId).addEventListener("input", (e) => {
|
||||
state[stateKey] = e.target.value;
|
||||
schedule();
|
||||
});
|
||||
}
|
||||
|
||||
$("logo_size").addEventListener("input", (e) => {
|
||||
state.logo_size = +e.target.value;
|
||||
$("logo_size_val").value = `${state.logo_size}px`;
|
||||
schedule();
|
||||
});
|
||||
|
||||
/* Swatches */
|
||||
document.querySelectorAll(".swatch").forEach(btn => {
|
||||
const c = btn.dataset.color;
|
||||
btn.style.background = c;
|
||||
btn.addEventListener("click", () => {
|
||||
state.accent = c;
|
||||
state.logo_color = c;
|
||||
$("accent").value = c;
|
||||
$("logo_color").value = c;
|
||||
schedule();
|
||||
});
|
||||
});
|
||||
|
||||
/* Segmented control: logo source */
|
||||
document.querySelectorAll(".seg-btn").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelectorAll(".seg-btn").forEach(b => b.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
state.logo_kind = btn.dataset.kind;
|
||||
document.querySelectorAll(".logo-source").forEach(el => { el.hidden = true; });
|
||||
if (state.logo_kind === "preset") document.querySelector(".preset-source").hidden = false;
|
||||
if (state.logo_kind === "upload") document.querySelector(".upload-source").hidden = false;
|
||||
if (state.logo_kind === "text") document.querySelector(".text-source").hidden = false;
|
||||
schedule();
|
||||
});
|
||||
});
|
||||
|
||||
/* Text logo input */
|
||||
$("logo_text").addEventListener("input", (e) => {
|
||||
if (state.logo_kind === "text") state.logo_value = e.target.value;
|
||||
schedule();
|
||||
});
|
||||
|
||||
/* Upload */
|
||||
$("logo_upload").addEventListener("change", async (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const dataUrl = await new Promise((res, rej) => {
|
||||
const r = new FileReader();
|
||||
r.onload = () => res(r.result);
|
||||
r.onerror = rej;
|
||||
r.readAsDataURL(file);
|
||||
});
|
||||
state.logo_kind = "upload";
|
||||
state.logo_value = dataUrl;
|
||||
const preview = $("upload_preview");
|
||||
preview.hidden = false;
|
||||
preview.innerHTML = `<img src="${dataUrl}" alt="">`;
|
||||
schedule();
|
||||
});
|
||||
|
||||
/* ── Logo library ──────────────────────────────────────────────── */
|
||||
|
||||
async function loadLogoLibrary() {
|
||||
const res = await fetch("/api/logos");
|
||||
logoLibrary = await res.json();
|
||||
const grid = $("logo_grid");
|
||||
grid.innerHTML = "";
|
||||
for (const slug of logoLibrary) {
|
||||
const tile = document.createElement("div");
|
||||
tile.className = "logo-tile";
|
||||
tile.title = slug;
|
||||
if (slug === state.logo_value) tile.classList.add("selected");
|
||||
tile.dataset.slug = slug;
|
||||
const svgRes = await fetch(`/logos/${slug}.svg`);
|
||||
tile.innerHTML = await svgRes.text();
|
||||
tile.addEventListener("click", () => {
|
||||
grid.querySelectorAll(".logo-tile").forEach(t => t.classList.remove("selected"));
|
||||
tile.classList.add("selected");
|
||||
state.logo_kind = "preset";
|
||||
state.logo_value = slug;
|
||||
// Also update segmented control
|
||||
document.querySelectorAll(".seg-btn").forEach(b =>
|
||||
b.classList.toggle("active", b.dataset.kind === "preset"));
|
||||
document.querySelectorAll(".logo-source").forEach(el => { el.hidden = true; });
|
||||
document.querySelector(".preset-source").hidden = false;
|
||||
schedule();
|
||||
});
|
||||
grid.appendChild(tile);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Render pipeline ───────────────────────────────────────────── */
|
||||
|
||||
function schedule() {
|
||||
if (renderTimer) clearTimeout(renderTimer);
|
||||
status.textContent = "rendu…"; status.className = "status working";
|
||||
renderTimer = setTimeout(render, 120);
|
||||
}
|
||||
|
||||
async function render() {
|
||||
try {
|
||||
const res = await fetch("/api/svg", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(state),
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const svgText = await res.text();
|
||||
preview.innerHTML = svgText;
|
||||
status.textContent = "à jour"; status.className = "status ok";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
status.textContent = "erreur"; status.className = "status err";
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Download buttons ──────────────────────────────────────────── */
|
||||
|
||||
$("btn-svg").addEventListener("click", async () => {
|
||||
status.textContent = "génération SVG…"; status.className = "status working";
|
||||
const res = await fetch("/api/svg", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(state),
|
||||
});
|
||||
const blob = await res.blob();
|
||||
downloadBlob(blob, `${$("filename").value || "card"}.svg`);
|
||||
status.textContent = "SVG téléchargé"; status.className = "status ok";
|
||||
});
|
||||
|
||||
$("btn-png").addEventListener("click", async () => {
|
||||
status.textContent = "rendu PNG (rsvg-convert)…"; status.className = "status working";
|
||||
const width = +$("export_width").value;
|
||||
const height = Math.round(width / 1.6);
|
||||
try {
|
||||
const res = await fetch("/api/png", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ ...state, export_width: width, export_height: height,
|
||||
filename: $("filename").value || "card" }),
|
||||
});
|
||||
if (!res.ok) throw new Error(await res.text());
|
||||
const blob = await res.blob();
|
||||
downloadBlob(blob, `${$("filename").value || "card"}.png`);
|
||||
status.textContent = `PNG ${width}×${height} téléchargé`; status.className = "status ok";
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
status.textContent = "erreur PNG"; status.className = "status err";
|
||||
}
|
||||
});
|
||||
|
||||
function downloadBlob(blob, filename) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url; a.download = filename;
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
/* ── Init ──────────────────────────────────────────────────────── */
|
||||
loadLogoLibrary().then(render);
|
||||
4
static/favicon.svg
Normal file
4
static/favicon.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||
<rect width="512" height="512" rx="96" fill="#0a0a0e"/>
|
||||
<path fill="#ef4444" d="M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368-38.1 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z" transform="translate(0, -10)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 667 B |
163
static/index.html
Normal file
163
static/index.html
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
<!doctype html>
|
||||
<html lang="fr" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
<meta name="googlebot" content="noindex, nofollow">
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
|
||||
<title>Card Generator · Yasder</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<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=Fraunces:opsz,wght@9..144,400;9..144,700&family=Syne:wght@400;600&family=DM+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<header class="app-header">
|
||||
<div class="brand">
|
||||
<svg class="brand-logo" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
||||
<path d="M204.3 5C104.9 24.4 24.8 104.3 5.2 203.4c-37 187 131.7 326.4 258.8 306.7 41.2-6.4 61.4-54.6 42.5-91.7-23.1-45.4 9.9-98.4 60.9-98.4h79.7c35.8 0 64.8-29.6 64.9-65.3C511.5 97.1 368 -38.1 204.3 5zM96 320c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm32-128c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128-64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32zm128 64c-17.7 0-32-14.3-32-32s14.3-32 32-32 32 14.3 32 32-14.3 32-32 32z"/>
|
||||
</svg>
|
||||
<span class="brand-mark">YB<span class="dot">.</span></span>
|
||||
<span class="brand-name">Card Generator</span>
|
||||
</div>
|
||||
<div class="brand-tagline">Mêmes cartes que sur yasser.boulayoune.com · Fraunces / Syne / DM Sans</div>
|
||||
</header>
|
||||
|
||||
<main class="app">
|
||||
|
||||
<!-- LEFT: form -->
|
||||
<aside class="panel form-panel">
|
||||
<h2>Contenu</h2>
|
||||
|
||||
<div class="field">
|
||||
<label for="eyebrow">Eyebrow <span class="hint">(petit titre en haut, en majuscules)</span></label>
|
||||
<input id="eyebrow" type="text" value="RÉALISATION · SYMFONY" maxlength="60">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="title">Titre (ligne 1)</label>
|
||||
<input id="title" type="text" value="pokedwwmm" maxlength="40">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="title_2">Titre ligne 2 <span class="hint">(optionnel · pour titre sur 2 lignes)</span></label>
|
||||
<input id="title_2" type="text" value="" maxlength="40">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="tagline">Tagline <span class="hint">(stack, mots-clés)</span></label>
|
||||
<input id="tagline" type="text" value="151 Pokémon · Twig Components · JSON" maxlength="80">
|
||||
</div>
|
||||
|
||||
<h2>Couleurs</h2>
|
||||
|
||||
<div class="field row">
|
||||
<div class="row-col">
|
||||
<label for="accent">Couleur d'accent</label>
|
||||
<input id="accent" type="color" value="#ef4444">
|
||||
</div>
|
||||
<div class="row-col">
|
||||
<label for="logo_color">Couleur du logo <span class="hint">(SVG icônes uniquement)</span></label>
|
||||
<input id="logo_color" type="color" value="#ef4444">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Palettes rapides</label>
|
||||
<div class="swatches">
|
||||
<button type="button" class="swatch" data-color="#ef4444" title="Rouge Symfony"></button>
|
||||
<button type="button" class="swatch" data-color="#5865f2" title="Discord violet"></button>
|
||||
<button type="button" class="swatch" data-color="#21759b" title="WordPress bleu"></button>
|
||||
<button type="button" class="swatch" data-color="#a855f7" title="Violet"></button>
|
||||
<button type="button" class="swatch" data-color="#22c55e" title="Vert Node"></button>
|
||||
<button type="button" class="swatch" data-color="#fb923c" title="Orange Forgejo"></button>
|
||||
<button type="button" class="swatch" data-color="#3b82f6" title="Bleu"></button>
|
||||
<button type="button" class="swatch" data-color="#06b6d4" title="Cyan"></button>
|
||||
<button type="button" class="swatch" data-color="#f5f5f7" title="Blanc"></button>
|
||||
<button type="button" class="swatch" data-color="#a8e063" title="Lime"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Logo</h2>
|
||||
|
||||
<div class="field">
|
||||
<label>Source</label>
|
||||
<div class="seg">
|
||||
<button type="button" class="seg-btn active" data-kind="preset">Bibliothèque</button>
|
||||
<button type="button" class="seg-btn" data-kind="upload">Upload PNG</button>
|
||||
<button type="button" class="seg-btn" data-kind="text">Texte</button>
|
||||
<button type="button" class="seg-btn" data-kind="none">Aucun</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field logo-source preset-source">
|
||||
<label for="logo_preset">Icône (simpleicons + FontAwesome)</label>
|
||||
<div class="logo-grid" id="logo_grid"></div>
|
||||
</div>
|
||||
|
||||
<div class="field logo-source upload-source" hidden>
|
||||
<label for="logo_upload">Upload une image (PNG transparent recommandé, ou SVG)</label>
|
||||
<input id="logo_upload" type="file" accept="image/png,image/svg+xml,image/webp,image/jpeg">
|
||||
<div id="upload_preview" class="upload-preview" hidden></div>
|
||||
</div>
|
||||
|
||||
<div class="field logo-source text-source" hidden>
|
||||
<label for="logo_text">Texte du logo <span class="hint">(monogramme · ex : YB.)</span></label>
|
||||
<input id="logo_text" type="text" value="YB." maxlength="6">
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="logo_size">Taille du logo <output id="logo_size_val">260px</output></label>
|
||||
<input id="logo_size" type="range" min="120" max="380" step="10" value="260">
|
||||
</div>
|
||||
|
||||
<h2>Export</h2>
|
||||
|
||||
<div class="field">
|
||||
<label for="filename">Nom du fichier</label>
|
||||
<input id="filename" type="text" value="card" maxlength="60">
|
||||
</div>
|
||||
|
||||
<div class="field row">
|
||||
<div class="row-col">
|
||||
<label for="export_width">Largeur PNG</label>
|
||||
<select id="export_width">
|
||||
<option value="800">800 × 500 (1×)</option>
|
||||
<option value="1600" selected>1600 × 1000 (2×, retina)</option>
|
||||
<option value="2400">2400 × 1500 (3×)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button id="btn-png" type="button" class="btn btn-primary">⬇ Télécharger PNG</button>
|
||||
<button id="btn-svg" type="button" class="btn">⬇ Télécharger SVG</button>
|
||||
</div>
|
||||
|
||||
</aside>
|
||||
|
||||
<!-- RIGHT: preview -->
|
||||
<section class="panel preview-panel">
|
||||
<header class="preview-header">
|
||||
<h2>Aperçu</h2>
|
||||
<div class="zoom-info">800 × 500 · preview live</div>
|
||||
</header>
|
||||
|
||||
<div class="preview-wrap">
|
||||
<div id="preview-frame">
|
||||
<!-- SVG is injected here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="preview-footer">
|
||||
<span id="status" class="status">prêt</span>
|
||||
</footer>
|
||||
</section>
|
||||
|
||||
</main>
|
||||
|
||||
<script src="/static/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
381
static/style.css
Normal file
381
static/style.css
Normal file
|
|
@ -0,0 +1,381 @@
|
|||
/* ─── Reset + tokens ────────────────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #0a0a0e;
|
||||
--surface: #14141a;
|
||||
--surface-2: #1a1a22;
|
||||
--line: #26262f;
|
||||
--t1: #f5f5f7;
|
||||
--t2: #c8c8d0;
|
||||
--t3: #8a8a92;
|
||||
--accent: #ef4444;
|
||||
--accent-soft: #ef444433;
|
||||
--radius: 12px;
|
||||
--radius-sm: 6px;
|
||||
--serif: 'Fraunces', Georgia, serif;
|
||||
--sans: 'Syne', system-ui, sans-serif;
|
||||
--body: 'DM Sans', system-ui, sans-serif;
|
||||
--mono: 'JetBrains Mono', ui-monospace, monospace;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--body);
|
||||
background: radial-gradient(circle at 10% -10%, #1a1a22, var(--bg) 60%);
|
||||
color: var(--t1);
|
||||
min-height: 100vh;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ─── Header ────────────────────────────────────────────────────── */
|
||||
.app-header {
|
||||
display: flex; align-items: baseline; justify-content: space-between;
|
||||
gap: 24px;
|
||||
padding: 24px 32px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: rgba(10, 10, 14, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.brand {
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
}
|
||||
.brand-logo {
|
||||
width: 36px; height: 36px;
|
||||
fill: var(--accent);
|
||||
filter: drop-shadow(0 4px 12px rgba(239, 68, 68, 0.35));
|
||||
}
|
||||
.brand-mark {
|
||||
font-family: var(--serif); font-weight: 700; font-size: 28px;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
}
|
||||
.brand-mark .dot { color: var(--accent); }
|
||||
.brand-name {
|
||||
font-family: var(--sans); font-weight: 600; font-size: 14px;
|
||||
letter-spacing: 0.18em; text-transform: uppercase; color: var(--t2);
|
||||
}
|
||||
.brand-tagline {
|
||||
font-size: 13px; color: var(--t3); font-family: var(--mono);
|
||||
}
|
||||
|
||||
/* ─── Layout ────────────────────────────────────────────────────── */
|
||||
.app {
|
||||
display: grid;
|
||||
grid-template-columns: 420px 1fr;
|
||||
gap: 24px;
|
||||
padding: 24px 32px 48px;
|
||||
align-items: start;
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.panel {
|
||||
background: var(--surface);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 24px;
|
||||
}
|
||||
.form-panel {
|
||||
position: sticky;
|
||||
top: 24px;
|
||||
max-height: calc(100vh - 48px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.form-panel h2 {
|
||||
font-family: var(--sans);
|
||||
font-weight: 600;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.22em;
|
||||
color: var(--t3);
|
||||
margin: 24px 0 12px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.form-panel h2:first-child { margin-top: 0; }
|
||||
|
||||
/* ─── Form fields ───────────────────────────────────────────────── */
|
||||
.field { margin-bottom: 16px; }
|
||||
.field label {
|
||||
display: block; margin-bottom: 6px;
|
||||
font-size: 13px; font-weight: 500; color: var(--t2);
|
||||
}
|
||||
.field .hint {
|
||||
font-size: 11px; color: var(--t3); font-weight: 400;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.field.row {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
|
||||
}
|
||||
.field.row .row-col { margin: 0; }
|
||||
|
||||
input[type="text"], input[type="file"], select {
|
||||
width: 100%;
|
||||
background: var(--bg);
|
||||
color: var(--t1);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 9px 12px;
|
||||
font-family: var(--body);
|
||||
font-size: 14px;
|
||||
transition: border-color .15s;
|
||||
}
|
||||
input[type="text"]:focus, select:focus, input[type="file"]:focus {
|
||||
outline: none;
|
||||
border-color: var(--accent);
|
||||
}
|
||||
input[type="color"] {
|
||||
width: 100%; height: 42px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
padding: 4px;
|
||||
}
|
||||
input[type="range"] {
|
||||
width: 100%; accent-color: var(--accent);
|
||||
}
|
||||
output {
|
||||
float: right; font-family: var(--mono); font-size: 12px; color: var(--t3);
|
||||
}
|
||||
|
||||
/* Swatches */
|
||||
.swatches {
|
||||
display: flex; flex-wrap: wrap; gap: 8px;
|
||||
}
|
||||
.swatch {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 50%;
|
||||
border: 2px solid var(--line);
|
||||
background: var(--color);
|
||||
cursor: pointer;
|
||||
transition: transform .12s, border-color .12s;
|
||||
}
|
||||
.swatch:hover { transform: scale(1.1); border-color: var(--t2); }
|
||||
|
||||
/* Segmented control */
|
||||
.seg {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 3px;
|
||||
gap: 2px;
|
||||
}
|
||||
.seg-btn {
|
||||
background: transparent;
|
||||
color: var(--t3);
|
||||
border: 0;
|
||||
padding: 8px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: var(--body);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background .15s, color .15s;
|
||||
}
|
||||
.seg-btn:hover { color: var(--t2); }
|
||||
.seg-btn.active {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Logo grid */
|
||||
.logo-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, 1fr);
|
||||
gap: 6px;
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
padding: 6px;
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
.logo-tile {
|
||||
aspect-ratio: 1;
|
||||
background: var(--surface-2);
|
||||
border: 1px solid transparent;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
padding: 6px;
|
||||
transition: border-color .12s, background .12s;
|
||||
}
|
||||
.logo-tile:hover { background: var(--surface); border-color: var(--t3); }
|
||||
.logo-tile.selected { border-color: var(--accent); background: var(--surface); }
|
||||
.logo-tile svg { width: 100%; height: 100%; fill: var(--t1); }
|
||||
|
||||
/* Upload preview */
|
||||
.upload-preview {
|
||||
margin-top: 8px;
|
||||
padding: 12px;
|
||||
background: var(--bg);
|
||||
border: 1px dashed var(--line);
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
min-height: 80px;
|
||||
}
|
||||
.upload-preview img { max-width: 100%; max-height: 80px; }
|
||||
|
||||
/* Actions */
|
||||
.actions {
|
||||
display: flex; gap: 10px; margin-top: 20px;
|
||||
}
|
||||
.btn {
|
||||
flex: 1;
|
||||
background: var(--surface-2);
|
||||
color: var(--t1);
|
||||
border: 1px solid var(--line);
|
||||
padding: 12px 16px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: var(--body);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background .15s, border-color .15s, transform .08s;
|
||||
}
|
||||
.btn:hover { background: var(--line); border-color: var(--t3); }
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
.btn-primary:hover {
|
||||
background: #f87171;
|
||||
border-color: #f87171;
|
||||
}
|
||||
|
||||
/* ─── Preview ───────────────────────────────────────────────────── */
|
||||
.preview-panel { padding: 16px; }
|
||||
.preview-header {
|
||||
display: flex; align-items: baseline; justify-content: space-between;
|
||||
padding: 8px 8px 16px;
|
||||
}
|
||||
.preview-header h2 {
|
||||
font-family: var(--sans); font-weight: 600;
|
||||
font-size: 11px; text-transform: uppercase; letter-spacing: 0.22em;
|
||||
color: var(--t3); margin: 0;
|
||||
}
|
||||
.zoom-info {
|
||||
font-family: var(--mono); font-size: 11px; color: var(--t3);
|
||||
}
|
||||
.preview-wrap {
|
||||
background: repeating-conic-gradient(#0c0c10 0% 25%, #16161e 0% 50%) 50% / 24px 24px;
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 24px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
min-height: 540px;
|
||||
}
|
||||
#preview-frame {
|
||||
width: 800px; max-width: 100%;
|
||||
aspect-ratio: 8/5;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 30px 80px rgba(0,0,0,0.5);
|
||||
}
|
||||
#preview-frame svg {
|
||||
width: 100%; height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.preview-footer {
|
||||
padding: 16px 8px 4px;
|
||||
display: flex; justify-content: flex-end;
|
||||
}
|
||||
.status {
|
||||
font-family: var(--mono); font-size: 11px;
|
||||
color: var(--t3);
|
||||
}
|
||||
.status.working { color: #f59e0b; }
|
||||
.status.ok { color: #22c55e; }
|
||||
.status.err { color: var(--accent); }
|
||||
|
||||
/* ─── Responsive ────────────────────────────────────────────────── */
|
||||
|
||||
/* Tablette large : on empile form + preview, le form reste large */
|
||||
@media (max-width: 1080px) {
|
||||
.app {
|
||||
grid-template-columns: 1fr;
|
||||
padding: 20px 24px 40px;
|
||||
gap: 20px;
|
||||
}
|
||||
.form-panel {
|
||||
position: static;
|
||||
max-height: none;
|
||||
overflow-y: visible;
|
||||
}
|
||||
.preview-wrap { padding: 16px; min-height: 0; }
|
||||
#preview-frame { width: 100%; max-width: 800px; margin: 0 auto; }
|
||||
}
|
||||
|
||||
/* Tablette : header passe sur 2 lignes, paddings réduits */
|
||||
@media (max-width: 720px) {
|
||||
.app-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 18px 20px;
|
||||
}
|
||||
.brand { gap: 10px; }
|
||||
.brand-logo { width: 28px; height: 28px; }
|
||||
.brand-mark { font-size: 22px; }
|
||||
.brand-name { font-size: 12px; letter-spacing: 0.14em; }
|
||||
.brand-tagline { font-size: 12px; }
|
||||
|
||||
.app {
|
||||
padding: 16px 16px 32px;
|
||||
gap: 16px;
|
||||
}
|
||||
.panel { padding: 18px; }
|
||||
.preview-panel { padding: 12px; }
|
||||
.preview-wrap { padding: 10px; }
|
||||
|
||||
/* Grille des logos plus aérée */
|
||||
.logo-grid { grid-template-columns: repeat(5, 1fr); max-height: 240px; }
|
||||
|
||||
/* Le segmented control reste sur une ligne avec un texte plus court */
|
||||
.seg-btn { padding: 8px 4px; font-size: 11px; }
|
||||
}
|
||||
|
||||
/* Mobile : on passe en monocolonne pour la ligne couleurs, output sur sa propre ligne */
|
||||
@media (max-width: 520px) {
|
||||
.app-header { padding: 14px 16px; }
|
||||
.brand-tagline { display: none; }
|
||||
|
||||
.app {
|
||||
padding: 12px 12px 28px;
|
||||
gap: 12px;
|
||||
}
|
||||
.panel { padding: 14px; border-radius: 10px; }
|
||||
.preview-panel { padding: 10px; }
|
||||
.preview-wrap {
|
||||
padding: 8px;
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
.field.row { grid-template-columns: 1fr; gap: 14px; }
|
||||
|
||||
output { float: none; display: block; margin-top: 4px; }
|
||||
|
||||
/* Grille logos : 4 col pour des tuiles plus tappables */
|
||||
.logo-grid { grid-template-columns: repeat(4, 1fr); gap: 8px; }
|
||||
.logo-tile { padding: 10px; }
|
||||
|
||||
/* Boutons d'export empilés pour qu'ils restent lisibles */
|
||||
.actions { flex-direction: column; }
|
||||
.btn { width: 100%; padding: 14px 16px; }
|
||||
|
||||
/* Swatches plus gros, plus faciles au doigt */
|
||||
.swatch { width: 32px; height: 32px; }
|
||||
|
||||
.preview-header { flex-direction: column; align-items: flex-start; gap: 4px; }
|
||||
}
|
||||
|
||||
/* Très petits écrans : on tasse encore un peu */
|
||||
@media (max-width: 360px) {
|
||||
.seg { grid-template-columns: repeat(2, 1fr); }
|
||||
.logo-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue