1 Commits
Author SHA1 Message Date
libretechandClaude Opus 4.7 86650a1396 Add single-file logo generator with accessibility fixes
Single-file HTML/CSS/JS logo generator. Accessibility and
native-HTML improvements over the original draft:

- viewport meta for mobile zoom/reflow
- programmatic label association (label[for], aria-label,
  aria-labelledby) for all form controls
- visible keyboard focus indicators (restored outline)
- proper heading hierarchy (h1/h2)
- native <details>/<summary> for panel show/hide (replaces JS toggle)
- native <form> reset via <button type="reset"> (replaces manual
  field-by-field reset and the duplicated DEFAULTS object)
- AA-safe text contrast for hints/hex labels

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-29 12:02:35 +02:00
8 changed files with 323 additions and 1511 deletions
-4
View File
@@ -1,4 +0,0 @@
node_modules
dist
*.log
.DS_Store
+319 -1
View File
@@ -5,6 +5,176 @@
<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">
<style>
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
background: #362582;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Anton', 'Bebas Neue', Impact, sans-serif;
overflow: hidden;
transition: background 0.15s;
}
.logo {
font-size: 18vw;
font-weight: 400;
color: #f6d939;
line-height: 1.05;
letter-spacing: 0.01em;
transform: rotate(-10deg) skewX(-8deg);
transform-origin: center;
text-align: center;
user-select: none;
transition: color 0.15s;
white-space: pre-line;
}
.panel {
position: fixed;
top: 12px;
right: 12px;
background: rgba(0,0,0,0.72);
color: #eee;
padding: 14px 18px;
border-radius: 10px;
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
font-size: 12px;
width: 320px;
backdrop-filter: blur(10px);
max-height: 96vh;
overflow-y: auto;
box-shadow: 0 6px 30px rgba(0,0,0,0.4);
}
.panel h1 { margin: 0 0 10px; font-size: 14px; color: #fff; }
.panel h2 {
margin: 14px 0 6px;
font-size: 11px;
color: #f6d939;
text-transform: uppercase;
letter-spacing: 0.08em;
border-bottom: 1px solid rgba(255,255,255,0.1);
padding-bottom: 4px;
}
.panel .hint {
font-size: 10px;
color: #aaa;
margin: 0 0 8px;
font-style: italic;
}
.panel label {
display: block;
margin: 6px 0 3px;
font-size: 11px;
color: #ccc;
}
.panel input[type=range] { width: 100%; margin: 0; }
.panel input[type=text],
.panel textarea,
.panel select {
width: 100%;
padding: 5px 8px;
background: rgba(255,255,255,0.08);
color: white;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px;
font-family: inherit;
font-size: 12px;
}
.panel textarea {
resize: vertical;
min-height: 60px;
}
.panel input:focus, .panel textarea:focus, .panel select:focus {
outline: 2px solid #f6d939;
outline-offset: 1px;
border-color: rgba(246,217,57,0.6);
}
.toggle-panel:focus-visible,
.copy-btn:focus-visible,
.reset-btn:focus-visible {
outline: 2px solid #f6d939;
outline-offset: 2px;
}
.color-row {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 8px;
align-items: center;
margin: 4px 0;
}
.color-row input[type=color] {
width: 36px;
height: 28px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px;
background: transparent;
cursor: pointer;
padding: 2px;
}
.color-row .swatch-label { font-size: 11px; color: #ccc; }
.color-row .hex { font-size: 10px; color: #aaa; text-align: right; width: 60px; }
.val { color: #f6d939; font-weight: bold; }
.row-2col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.panel code {
display: block;
background: rgba(0,0,0,0.5);
padding: 8px;
margin-top: 10px;
border-radius: 4px;
font-size: 10px;
max-height: 220px;
overflow-y: auto;
white-space: pre;
color: #aaa;
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
}
.copy-btn, .reset-btn {
width: 100%;
padding: 6px;
margin-top: 6px;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-size: 11px;
}
.copy-btn {
background: rgba(246,217,57,0.15);
color: #f6d939;
border: 1px solid rgba(246,217,57,0.4);
}
.copy-btn:hover { background: rgba(246,217,57,0.25); }
.copy-btn.copied { background: rgba(120,200,120,0.25); color: #cfc; border-color: rgba(120,200,120,0.5); }
.reset-btn {
background: rgba(255,255,255,0.05);
color: #ccc;
border: 1px solid rgba(255,255,255,0.15);
}
.reset-btn:hover { background: rgba(255,255,255,0.1); }
.toggle-panel {
position: fixed;
top: 12px;
left: 12px;
background: rgba(0,0,0,0.6);
color: white;
border: 1px solid rgba(255,255,255,0.2);
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-size: 11px;
z-index: 10;
}
summary.toggle-panel { list-style: none; }
summary.toggle-panel::-webkit-details-marker { display: none; }
.disclosure { position: static; }
</style>
</head>
<body>
<div class="logo" id="logo">LIBRE
@@ -101,6 +271,154 @@ TECH</textarea>
</div>
</details>
<script type="module" src="/src/main.js"></script>
<script>
const $ = id => document.getElementById(id);
const logo = $('logo');
const cssout = $('cssout');
// Kompakte text-shadow-Liste mit CSS-Variablen
function buildShadows(angle, shadowLen, depthLen, contourLen) {
const rad = angle * Math.PI / 180;
const dx = Math.sin(rad);
const dy = -Math.cos(rad);
const round = n => Math.round(n * 100) / 100;
const lines = [];
// Layer "Kontur" (vorn): Stufen in Lichtrichtung, Schatten-Farbe
for (let i = 1; i <= contourLen; i++) {
lines.push(` ${round(dx * i)}px ${round(dy * i)}px 0 var(--shadow)`);
}
// Layer "Tiefe" (mitte): in Gegenrichtung
for (let i = 1; i <= depthLen; i++) {
lines.push(` ${round(-dx * i)}px ${round(-dy * i)}px 0 var(--depth)`);
}
// Layer "Schatten" (hinten): lang in Lichtrichtung
// Kontur und Schatten gehen in dieselbe Richtung; ab contourLen+1 weitermachen,
// damit keine doppelten Stufen entstehen
for (let i = contourLen + 1; i <= contourLen + shadowLen; i++) {
lines.push(` ${round(dx * i)}px ${round(dy * i)}px 0 var(--shadow)`);
}
return lines;
}
function update() {
// Inputs lesen
const text = $('text').value || ' ';
const font = $('font').value;
const fs = +$('fs').value;
const lh = +$('lh').value;
const ls = +$('ls').value;
const cBg = $('c-bg').value;
const cFace = $('c-face').value;
const cDepth = $('c-depth').value;
const cShadow = $('c-shadow').value;
const angle = +$('angle').value;
const shadowLen = +$('shadow').value;
const depthLen = +$('depth').value;
const contourLen = +$('contour').value;
const rot = +$('rot').value;
const skew = +$('skew').value;
// Anzeigewerte
$('v-fs').textContent = fs;
$('v-lh').textContent = lh.toFixed(2);
$('v-ls').textContent = ls.toFixed(3);
$('v-angle').textContent = angle;
$('v-shadow').textContent = shadowLen;
$('v-depth').textContent = depthLen;
$('v-contour').textContent = contourLen;
$('v-rot').textContent = rot;
$('v-skew').textContent = skew;
// Hex-Anzeige spiegelt die Color-Inputs (auch nach Reset)
$('hex-bg').textContent = cBg;
$('hex-face').textContent = cFace;
$('hex-depth').textContent = cDepth;
$('hex-shadow').textContent = cShadow;
// Logo: Text als Multi-Line
logo.textContent = text;
// text-shadow direkt mit Hex-Werten anwenden (Browser kennt die var() nicht
// automatisch in inline-Styles, daher hier direkte Werte)
const rad = angle * Math.PI / 180;
const dx = Math.sin(rad);
const dy = -Math.cos(rad);
const round = n => Math.round(n * 100) / 100;
const inlineShadows = [];
for (let i = 1; i <= contourLen; i++) {
inlineShadows.push(`${round(dx * i)}px ${round(dy * i)}px 0 ${cShadow}`);
}
for (let i = 1; i <= depthLen; i++) {
inlineShadows.push(`${round(-dx * i)}px ${round(-dy * i)}px 0 ${cDepth}`);
}
for (let i = contourLen + 1; i <= contourLen + shadowLen; i++) {
inlineShadows.push(`${round(dx * i)}px ${round(dy * i)}px 0 ${cShadow}`);
}
logo.style.textShadow = inlineShadows.length ? inlineShadows.join(', ') : 'none';
logo.style.transform = `rotate(${rot}deg) skewX(${skew}deg)`;
logo.style.fontFamily = `'${font}', 'Bebas Neue', Impact, sans-serif`;
logo.style.color = cFace;
logo.style.fontSize = fs + 'vw';
logo.style.lineHeight = lh;
logo.style.letterSpacing = ls + 'em';
document.body.style.background = cBg;
// CSS-Output mit Variablen — sauber, idiomatisch, modern
const shadowLines = buildShadows(angle, shadowLen, depthLen, contourLen);
const css = `:root {
--bg: ${cBg};
--face: ${cFace};
--depth: ${cDepth};
--shadow: ${cShadow};
}
body {
background: var(--bg);
}
.logo {
font-family: '${font}', sans-serif;
font-size: ${fs}vw;
line-height: ${lh};
letter-spacing: ${ls}em;
color: var(--face);
transform: rotate(${rot}deg) skewX(${skew}deg);
white-space: pre-line;
text-shadow:
${shadowLines.join(',\n')};
}`;
cssout.textContent = css;
}
// Inputs binden
document.querySelectorAll('.panel input, .panel select, .panel textarea').forEach(el => {
el.addEventListener('input', update);
});
$('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 (err) {
alert('Kopieren fehlgeschlagen — bitte manuell aus dem Code-Feld kopieren.');
}
});
// Native <button type="reset"> setzt alle Felder auf ihre value=-Attribute zurück;
// danach (nächster Tick) neu rendern.
const form = $('logoForm');
form.addEventListener('reset', () => setTimeout(update, 0));
form.addEventListener('submit', e => e.preventDefault());
update();
</script>
</body>
</html>
-1144
View File
File diff suppressed because it is too large Load Diff
+4 -7
View File
@@ -1,19 +1,16 @@
{
"name": "demo-ecmascript",
"version": "1.0.0",
"description": "LIBRETECH logo generator — vanilla JS + Vite",
"description": "",
"repository": {
"type": "git",
"url": "ssh://tengo@git.librete.ch:41240/public/demo-ecmascript.git"
},
"license": "WTFPL",
"author": "",
"type": "module",
"main": "index.html",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"devDependencies": {
"vite": "^6.0.0"
"test": "echo \"Error: no test specified\" && exit 1"
}
}
-99
View File
@@ -1,99 +0,0 @@
// Framework-agnostic core for the logo generator.
// State shape (see `defaults`) is shared by every port.
export const defaults = {
text: 'LIBRE\nTECH',
font: 'Anton',
fs: 18,
lh: 1.05,
ls: 0.01,
bg: '#362582',
face: '#f6d939',
depth: '#ea741e',
shadow: '#221953',
angle: 133,
shadowLen: 30,
depthLen: 15,
contourLen: 11,
rot: -10,
skew: -8,
};
export const fonts = [
'Anton',
'Bowlby One',
'Bebas Neue',
'Bungee',
'Passion One',
'Ultra',
'Impact',
];
const round = (n) => Math.round(n * 100) / 100;
// Layered text-shadow offsets. `depthColor`/`shadowColor` are substituted
// so the same geometry serves both the live render (literal hex) and the
// generated snippet (CSS var()).
export function shadowLayers(s, depthColor, shadowColor) {
const rad = (s.angle * Math.PI) / 180;
const dx = Math.sin(rad);
const dy = -Math.cos(rad);
const parts = [];
// Kontur (front): steps along the light direction, shadow colour.
for (let i = 1; i <= s.contourLen; i++) {
parts.push(`${round(dx * i)}px ${round(dy * i)}px 0 ${shadowColor}`);
}
// Tiefe (middle): opposite direction.
for (let i = 1; i <= s.depthLen; i++) {
parts.push(`${round(-dx * i)}px ${round(-dy * i)}px 0 ${depthColor}`);
}
// Schatten (back): long, same direction as Kontur; continue past
// contourLen so steps do not overlap.
for (let i = s.contourLen + 1; i <= s.contourLen + s.shadowLen; i++) {
parts.push(`${round(dx * i)}px ${round(dy * i)}px 0 ${shadowColor}`);
}
return parts;
}
export function inlineTextShadow(s) {
const parts = shadowLayers(s, s.depth, s.shadow);
return parts.length ? parts.join(', ') : 'none';
}
export function logoStyle(s) {
return {
fontFamily: `'${s.font}', 'Bebas Neue', Impact, sans-serif`,
fontSize: `${s.fs}vw`,
lineHeight: String(s.lh),
letterSpacing: `${s.ls}em`,
color: s.face,
transform: `rotate(${s.rot}deg) skewX(${s.skew}deg)`,
textShadow: inlineTextShadow(s),
};
}
export function cssOutput(s) {
const lines = shadowLayers(s, 'var(--depth)', 'var(--shadow)').map((l) => ' ' + l);
return `:root {
--bg: ${s.bg};
--face: ${s.face};
--depth: ${s.depth};
--shadow: ${s.shadow};
}
body {
background: var(--bg);
}
.logo {
font-family: '${s.font}', sans-serif;
font-size: ${s.fs}vw;
line-height: ${s.lh};
letter-spacing: ${s.ls}em;
color: var(--face);
transform: rotate(${s.rot}deg) skewX(${s.skew}deg);
white-space: pre-line;
text-shadow:
${lines.join(',\n')};
}`;
}
-83
View File
@@ -1,83 +0,0 @@
import './style.css';
import { inlineTextShadow, cssOutput } from './logo.js';
const $ = (id) => document.getElementById(id);
const logo = $('logo');
const cssout = $('cssout');
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').value,
depthLen: +$('depth').value,
contourLen: +$('contour').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 <button type="reset"> restores every field to its value= attribute;
// re-render on the next tick once the reset has applied.
const form = $('logoForm');
form.addEventListener('reset', () => setTimeout(render, 0));
form.addEventListener('submit', (e) => e.preventDefault());
render();
-168
View File
@@ -1,168 +0,0 @@
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
background: #362582;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Anton', 'Bebas Neue', Impact, sans-serif;
overflow: hidden;
transition: background 0.15s;
}
.logo {
font-size: 18vw;
font-weight: 400;
color: #f6d939;
line-height: 1.05;
letter-spacing: 0.01em;
transform: rotate(-10deg) skewX(-8deg);
transform-origin: center;
text-align: center;
user-select: none;
transition: color 0.15s;
white-space: pre-line;
}
.panel {
position: fixed;
top: 12px;
right: 12px;
background: rgba(0,0,0,0.72);
color: #eee;
padding: 14px 18px;
border-radius: 10px;
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
font-size: 12px;
width: 320px;
backdrop-filter: blur(10px);
max-height: 96vh;
overflow-y: auto;
box-shadow: 0 6px 30px rgba(0,0,0,0.4);
}
.panel h1 { margin: 0 0 10px; font-size: 14px; color: #fff; }
.panel h2 {
margin: 14px 0 6px;
font-size: 11px;
color: #f6d939;
text-transform: uppercase;
letter-spacing: 0.08em;
border-bottom: 1px solid rgba(255,255,255,0.1);
padding-bottom: 4px;
}
.panel .hint {
font-size: 10px;
color: #aaa;
margin: 0 0 8px;
font-style: italic;
}
.panel label {
display: block;
margin: 6px 0 3px;
font-size: 11px;
color: #ccc;
}
.panel input[type=range] { width: 100%; margin: 0; }
.panel input[type=text],
.panel textarea,
.panel select {
width: 100%;
padding: 5px 8px;
background: rgba(255,255,255,0.08);
color: white;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px;
font-family: inherit;
font-size: 12px;
}
.panel textarea {
resize: vertical;
min-height: 60px;
}
.panel input:focus, .panel textarea:focus, .panel select:focus {
outline: 2px solid #f6d939;
outline-offset: 1px;
border-color: rgba(246,217,57,0.6);
}
.toggle-panel:focus-visible,
.copy-btn:focus-visible,
.reset-btn:focus-visible {
outline: 2px solid #f6d939;
outline-offset: 2px;
}
.color-row {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 8px;
align-items: center;
margin: 4px 0;
}
.color-row input[type=color] {
width: 36px;
height: 28px;
border: 1px solid rgba(255,255,255,0.2);
border-radius: 4px;
background: transparent;
cursor: pointer;
padding: 2px;
}
.color-row .swatch-label { font-size: 11px; color: #ccc; }
.color-row .hex { font-size: 10px; color: #aaa; text-align: right; width: 60px; }
.val { color: #f6d939; font-weight: bold; }
.row-2col {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.panel code {
display: block;
background: rgba(0,0,0,0.5);
padding: 8px;
margin-top: 10px;
border-radius: 4px;
font-size: 10px;
max-height: 220px;
overflow-y: auto;
white-space: pre;
color: #aaa;
font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
}
.copy-btn, .reset-btn {
width: 100%;
padding: 6px;
margin-top: 6px;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-size: 11px;
}
.copy-btn {
background: rgba(246,217,57,0.15);
color: #f6d939;
border: 1px solid rgba(246,217,57,0.4);
}
.copy-btn:hover { background: rgba(246,217,57,0.25); }
.copy-btn.copied { background: rgba(120,200,120,0.25); color: #cfc; border-color: rgba(120,200,120,0.5); }
.reset-btn {
background: rgba(255,255,255,0.05);
color: #ccc;
border: 1px solid rgba(255,255,255,0.15);
}
.reset-btn:hover { background: rgba(255,255,255,0.1); }
.toggle-panel {
position: fixed;
top: 12px;
left: 12px;
background: rgba(0,0,0,0.6);
color: white;
border: 1px solid rgba(255,255,255,0.2);
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
font-size: 11px;
z-index: 10;
}
summary.toggle-panel { list-style: none; }
summary.toggle-panel::-webkit-details-marker { display: none; }
.disclosure { position: static; }
-5
View File
@@ -1,5 +0,0 @@
import { defineConfig } from 'vite';
export default defineConfig({
// index.html at project root is the single entry; defaults are fine.
});