1 Commits
Author SHA1 Message Date
libretechandClaude Opus 4.7 be10c9982d Add React + Vite port of the logo generator
Full React port sharing src/logo.js core (shadow geometry, CSS
output, defaults, font list). App.jsx holds the entire state in a
single useState object; controlled inputs drive the live render and
generated CSS. Keeps the accessibility markup (labels, aria,
<details>/<summary>, focus rings) from the html/vanilla versions;
reset restores defaults via the form's onReset.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 12:19:30 +02:00
10 changed files with 822 additions and 4589 deletions
-1
View File
@@ -1,5 +1,4 @@
node_modules node_modules
dist dist
.astro
*.log *.log
.DS_Store .DS_Store
-5
View File
@@ -1,5 +0,0 @@
import { defineConfig } from 'astro/config';
// Astro builds with Vite under the hood. The page ships as static HTML;
// the interactive logic lives in a bundled client <script> in index.astro.
export default defineConfig({});
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Logo-Generator</title>
<link href="https://fonts.googleapis.com/css2?family=Anton&family=Bowlby+One&family=Bebas+Neue&family=Bungee&family=Passion+One:wght@900&family=Ultra&display=swap" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>
+646 -4376
View File
File diff suppressed because it is too large Load Diff
+11 -7
View File
@@ -1,7 +1,7 @@
{ {
"name": "demo-ecmascript-astro", "name": "demo-ecmascript-react",
"version": "1.0.0", "version": "1.0.0",
"description": "LIBRETECH logo generator — Astro (Vite)", "description": "LIBRETECH logo generator — React + Vite",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "ssh://tengo@git.librete.ch:41240/public/demo-ecmascript.git" "url": "ssh://tengo@git.librete.ch:41240/public/demo-ecmascript.git"
@@ -9,12 +9,16 @@
"license": "WTFPL", "license": "WTFPL",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "astro dev", "dev": "vite",
"build": "astro build", "build": "vite build",
"preview": "astro preview", "preview": "vite preview"
"astro": "astro"
}, },
"dependencies": { "dependencies": {
"astro": "^5.1.1" "react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.3.4",
"vite": "^6.0.0"
} }
} }
+135
View File
@@ -0,0 +1,135 @@
import { useEffect, useState } from 'react';
import { defaults, fonts, logoStyle, cssOutput } from './logo.js';
const FONT_LABELS = { Impact: 'Impact (System)' };
export default function App() {
const [s, setS] = useState(defaults);
const [copied, setCopied] = useState(false);
const set = (key) => (value) => setS((prev) => ({ ...prev, [key]: value }));
const num = (key) => (e) => set(key)(+e.target.value);
const str = (key) => (e) => set(key)(e.target.value);
useEffect(() => {
document.body.style.background = s.bg;
}, [s.bg]);
const css = cssOutput(s);
const copy = async () => {
try {
await navigator.clipboard.writeText(css);
setCopied(true);
setTimeout(() => setCopied(false), 1500);
} catch {
alert('Kopieren fehlgeschlagen — bitte manuell aus dem Code-Feld kopieren.');
}
};
// Controlled inputs are not reset by the browser, so restore defaults here.
const reset = (e) => {
e.preventDefault();
setS(defaults);
};
return (
<>
<div className="logo" style={logoStyle(s)}>{s.text || ' '}</div>
<details className="disclosure" id="panel" open>
<summary className="toggle-panel">Panel ein/aus</summary>
<div className="panel">
<h1>Logo-Generator</h1>
<form id="logoForm" onSubmit={(e) => e.preventDefault()} onReset={reset}>
<h2>Inhalt</h2>
<p className="hint" id="hint-text">Mehrere Zeilen mit Enter trennen.</p>
<textarea
id="text"
rows={2}
aria-label="Logo-Text"
aria-describedby="hint-text"
value={s.text}
onChange={str('text')}
/>
<h2>Schrift</h2>
<select id="font" aria-label="Schriftart" value={s.font} onChange={str('font')}>
{fonts.map((f) => (
<option key={f} value={f}>{FONT_LABELS[f] || f}</option>
))}
</select>
<div className="row-2col">
<div>
<label htmlFor="fs">Größe: <span className="val">{s.fs}</span>vw</label>
<input type="range" id="fs" min="6" max="30" step="0.5" value={s.fs} onChange={num('fs')} />
</div>
<div>
<label htmlFor="lh">Zeilenabstand: <span className="val">{s.lh.toFixed(2)}</span></label>
<input type="range" id="lh" min="0.5" max="2" step="0.05" value={s.lh} onChange={num('lh')} />
</div>
</div>
<label htmlFor="ls">Buchstabenabstand: <span className="val">{s.ls.toFixed(3)}</span>em</label>
<input type="range" id="ls" min="-0.15" max="0.3" step="0.005" value={s.ls} onChange={num('ls')} />
<h2>Farben</h2>
<ColorRow id="bg" label="Hintergrund" value={s.bg} onChange={str('bg')} />
<ColorRow id="face" label="Face" value={s.face} onChange={str('face')} />
<ColorRow id="depth" label="Tiefe" value={s.depth} onChange={str('depth')} />
<ColorRow id="shadow" label="Schatten & Kontur" value={s.shadow} onChange={str('shadow')} />
<h2>Lichtrichtung</h2>
<p className="hint">Steuert alle Versätze gleichzeitig — Schatten geht in Lichtrichtung, Tiefe entgegen.</p>
<label htmlFor="angle">Winkel: <span className="val">{s.angle}</span>° (0=oben, 90=rechts, 180=unten)</label>
<input type="range" id="angle" min="0" max="359" value={s.angle} onChange={num('angle')} />
<h2>Schichten</h2>
<p className="hint">Hinten nach vorn: Schatten · Tiefe · Kontur · Face</p>
<label htmlFor="shadow-len">Schatten-Länge: <span className="val">{s.shadowLen}</span>px</label>
<input type="range" id="shadow-len" min="0" max="80" value={s.shadowLen} onChange={num('shadowLen')} />
<label htmlFor="depth-len">Tiefe (in Gegenrichtung): <span className="val">{s.depthLen}</span>px</label>
<input type="range" id="depth-len" min="0" max="30" value={s.depthLen} onChange={num('depthLen')} />
<label htmlFor="contour-len">Kontur (vor der Tiefe): <span className="val">{s.contourLen}</span>px</label>
<input type="range" id="contour-len" min="0" max="20" value={s.contourLen} onChange={num('contourLen')} />
<h2>Komposition</h2>
<div className="row-2col">
<div>
<label htmlFor="rot">Drehung: <span className="val">{s.rot}</span>°</label>
<input type="range" id="rot" min="-25" max="25" value={s.rot} onChange={num('rot')} />
</div>
<div>
<label htmlFor="skew">Schräge: <span className="val">{s.skew}</span>°</label>
<input type="range" id="skew" min="-20" max="20" value={s.skew} onChange={num('skew')} />
</div>
</div>
<h2>Generiertes CSS</h2>
<code id="cssout">{css}</code>
<button
type="button"
className={'copy-btn' + (copied ? ' copied' : '')}
onClick={copy}
>
{copied ? 'Kopiert!' : 'CSS kopieren'}
</button>
<button type="reset" className="reset-btn">Zurücksetzen</button>
</form>
</div>
</details>
</>
);
}
function ColorRow({ id, label, value, onChange }) {
return (
<div className="color-row">
<input type="color" id={`c-${id}`} value={value} onChange={onChange} aria-labelledby={`lbl-${id}`} />
<span className="swatch-label" id={`lbl-${id}`}>{label}</span>
<span className="hex">{value}</span>
</div>
);
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import App from './App.jsx';
import './style.css';
createRoot(document.getElementById('root')).render(
<StrictMode>
<App />
</StrictMode>,
);
-200
View File
@@ -1,200 +0,0 @@
---
import { defaults, fonts } from '../logo.js';
import '../styles/style.css';
const FONT_LABELS = { Impact: 'Impact (System)' };
---
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Logo-Generator</title>
<link href="https://fonts.googleapis.com/css2?family=Anton&family=Bowlby+One&family=Bebas+Neue&family=Bungee&family=Passion+One:wght@900&family=Ultra&display=swap" rel="stylesheet" />
</head>
<body>
<div class="logo" id="logo">{defaults.text}</div>
<details class="disclosure" id="panel" open>
<summary class="toggle-panel">Panel ein/aus</summary>
<div class="panel">
<h1>Logo-Generator</h1>
<form id="logoForm">
<h2>Inhalt</h2>
<p class="hint" id="hint-text">Mehrere Zeilen mit Enter trennen.</p>
<textarea id="text" rows="2" aria-label="Logo-Text" aria-describedby="hint-text">{defaults.text}</textarea>
<h2>Schrift</h2>
<select id="font" aria-label="Schriftart">
{fonts.map((f) => <option value={f}>{FONT_LABELS[f] ?? f}</option>)}
</select>
<div class="row-2col">
<div>
<label for="fs">Größe: <span class="val" id="v-fs">{defaults.fs}</span>vw</label>
<input type="range" id="fs" min="6" max="30" step="0.5" value={defaults.fs} />
</div>
<div>
<label for="lh">Zeilenabstand: <span class="val" id="v-lh">{defaults.lh.toFixed(2)}</span></label>
<input type="range" id="lh" min="0.5" max="2" step="0.05" value={defaults.lh} />
</div>
</div>
<label for="ls">Buchstabenabstand: <span class="val" id="v-ls">{defaults.ls.toFixed(3)}</span>em</label>
<input type="range" id="ls" min="-0.15" max="0.3" step="0.005" value={defaults.ls} />
<h2>Farben</h2>
<div class="color-row">
<input type="color" id="c-bg" value={defaults.bg} aria-labelledby="lbl-bg" />
<span class="swatch-label" id="lbl-bg">Hintergrund</span>
<span class="hex" id="hex-bg">{defaults.bg}</span>
</div>
<div class="color-row">
<input type="color" id="c-face" value={defaults.face} aria-labelledby="lbl-face" />
<span class="swatch-label" id="lbl-face">Face</span>
<span class="hex" id="hex-face">{defaults.face}</span>
</div>
<div class="color-row">
<input type="color" id="c-depth" value={defaults.depth} aria-labelledby="lbl-depth" />
<span class="swatch-label" id="lbl-depth">Tiefe</span>
<span class="hex" id="hex-depth">{defaults.depth}</span>
</div>
<div class="color-row">
<input type="color" id="c-shadow" value={defaults.shadow} aria-labelledby="lbl-shadow" />
<span class="swatch-label" id="lbl-shadow">Schatten &amp; Kontur</span>
<span class="hex" id="hex-shadow">{defaults.shadow}</span>
</div>
<h2>Lichtrichtung</h2>
<p class="hint">Steuert alle Versätze gleichzeitig — Schatten geht in Lichtrichtung, Tiefe entgegen.</p>
<label for="angle">Winkel: <span class="val" id="v-angle">{defaults.angle}</span>° (0=oben, 90=rechts, 180=unten)</label>
<input type="range" id="angle" min="0" max="359" value={defaults.angle} />
<h2>Schichten</h2>
<p class="hint">Hinten nach vorn: Schatten · Tiefe · Kontur · Face</p>
<label for="shadow-len">Schatten-Länge: <span class="val" id="v-shadow">{defaults.shadowLen}</span>px</label>
<input type="range" id="shadow-len" min="0" max="80" value={defaults.shadowLen} />
<label for="depth-len">Tiefe (in Gegenrichtung): <span class="val" id="v-depth">{defaults.depthLen}</span>px</label>
<input type="range" id="depth-len" min="0" max="30" value={defaults.depthLen} />
<label for="contour-len">Kontur (vor der Tiefe): <span class="val" id="v-contour">{defaults.contourLen}</span>px</label>
<input type="range" id="contour-len" min="0" max="20" value={defaults.contourLen} />
<h2>Komposition</h2>
<div class="row-2col">
<div>
<label for="rot">Drehung: <span class="val" id="v-rot">{defaults.rot}</span>°</label>
<input type="range" id="rot" min="-25" max="25" value={defaults.rot} />
</div>
<div>
<label for="skew">Schräge: <span class="val" id="v-skew">{defaults.skew}</span>°</label>
<input type="range" id="skew" min="-20" max="20" value={defaults.skew} />
</div>
</div>
<h2>Generiertes CSS</h2>
<code id="cssout"></code>
<button type="button" class="copy-btn" id="copyBtn">CSS kopieren</button>
<button type="reset" class="reset-btn" id="resetBtn">Zurücksetzen</button>
</form>
</div>
</details>
<script>
import { inlineTextShadow, cssOutput } from '../logo.js';
const $ = (id) => document.getElementById(id);
const logo = $('logo');
const cssout = $('cssout');
const NUM = {
fs: 'fs',
lh: 'lh',
ls: 'ls',
angle: 'angle',
shadowLen: 'shadow-len',
depthLen: 'depth-len',
contourLen: 'contour-len',
rot: 'rot',
skew: 'skew',
};
function readState() {
return {
text: $('text').value || ' ',
font: $('font').value,
fs: +$('fs').value,
lh: +$('lh').value,
ls: +$('ls').value,
bg: $('c-bg').value,
face: $('c-face').value,
depth: $('c-depth').value,
shadow: $('c-shadow').value,
angle: +$('angle').value,
shadowLen: +$('shadow-len').value,
depthLen: +$('depth-len').value,
contourLen: +$('contour-len').value,
rot: +$('rot').value,
skew: +$('skew').value,
};
}
function render() {
const s = readState();
$('v-fs').textContent = s.fs;
$('v-lh').textContent = s.lh.toFixed(2);
$('v-ls').textContent = s.ls.toFixed(3);
$('v-angle').textContent = s.angle;
$('v-shadow').textContent = s.shadowLen;
$('v-depth').textContent = s.depthLen;
$('v-contour').textContent = s.contourLen;
$('v-rot').textContent = s.rot;
$('v-skew').textContent = s.skew;
$('hex-bg').textContent = s.bg;
$('hex-face').textContent = s.face;
$('hex-depth').textContent = s.depth;
$('hex-shadow').textContent = s.shadow;
logo.textContent = s.text;
logo.style.textShadow = inlineTextShadow(s);
logo.style.transform = `rotate(${s.rot}deg) skewX(${s.skew}deg)`;
logo.style.fontFamily = `'${s.font}', 'Bebas Neue', Impact, sans-serif`;
logo.style.color = s.face;
logo.style.fontSize = s.fs + 'vw';
logo.style.lineHeight = s.lh;
logo.style.letterSpacing = s.ls + 'em';
document.body.style.background = s.bg;
cssout.textContent = cssOutput(s);
}
document
.querySelectorAll('#logoForm input, #logoForm select, #logoForm textarea')
.forEach((el) => el.addEventListener('input', render));
$('copyBtn').addEventListener('click', async () => {
try {
await navigator.clipboard.writeText(cssout.textContent);
const btn = $('copyBtn');
btn.textContent = 'Kopiert!';
btn.classList.add('copied');
setTimeout(() => {
btn.textContent = 'CSS kopieren';
btn.classList.remove('copied');
}, 1500);
} catch {
alert('Kopieren fehlgeschlagen — bitte manuell aus dem Code-Feld kopieren.');
}
});
// Native reset restores each field to its value= attribute; re-render next tick.
const form = $('logoForm');
form.addEventListener('reset', () => setTimeout(render, 0));
form.addEventListener('submit', (e) => e.preventDefault());
render();
</script>
</body>
</html>
+1
View File
@@ -9,6 +9,7 @@ body {
overflow: hidden; overflow: hidden;
transition: background 0.15s; transition: background 0.15s;
} }
#root { display: contents; }
.logo { .logo {
font-size: 18vw; font-size: 18vw;
font-weight: 400; font-weight: 400;
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
});