diff --git a/docs/superpowers/plans/2026-04-01-farbhelfer.md b/docs/superpowers/plans/2026-04-01-farbhelfer.md new file mode 100644 index 0000000..18e18af --- /dev/null +++ b/docs/superpowers/plans/2026-04-01-farbhelfer.md @@ -0,0 +1,1120 @@ +# Farbhelfer Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a browser-based color tool that extracts hex codes from images, converts between color formats, shows color harmonies, and manages a personal color collection. + +**Architecture:** Single `index.html` with tab-based navigation, three focused JS modules (`converter.js`, `picker.js`, `collection.js`), and one `style.css`. A global `state` object holds the active color and is passed between modules. No build step — open `index.html` directly in the browser. + +**Tech Stack:** Vanilla HTML5, CSS3, JavaScript (ES6 modules), Canvas API, localStorage, no external dependencies. + +--- + +## File Map + +| File | Verantwortung | +|---|---| +| `index.html` | Shell, Tab-Navigation, alle Tab-Inhalte als Sections | +| `style.css` | Layout, Tab-UI, Farbfelder, Buttons, responsive Basis | +| `js/converter.js` | Hex-RGB-HSL Umrechnung, Harmonieberechnungen | +| `js/picker.js` | Bild laden (Upload/Paste/Drop), Canvas, Pixelauswahl | +| `js/collection.js` | localStorage lesen/schreiben, Favoriten, Historie, Schemata, Export/Import | +| `js/eingabe.js` | Eingabe-Tab Logik, bidirektionale Umrechnung | +| `js/harmonien.js` | Harmonien-Tab Logik, Rendering | +| `js/app.js` | Globaler State, Tab-Wechsel, Modul-Koordination | + +--- + +### Task 1: Projekt-Grundstruktur + +**Files:** +- Create: `index.html` +- Create: `style.css` +- Create: `js/app.js` + +- [ ] **Schritt 1: `index.html` erstellen** + +```html + + + + + + Farbhelfer + + + +
+

Farbhelfer

+ +
+ +
+
+

Picker kommt hier

+
+
+

Eingabe kommt hier

+
+
+

Harmonien kommen hier

+
+
+

Sammlung kommt hier

+
+
+ + + + +``` + +- [ ] **Schritt 2: `style.css` Basis erstellen** + +```css +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: system-ui, sans-serif; + background: #f5f5f5; + color: #222; + min-height: 100vh; +} + +header { + background: #fff; + border-bottom: 1px solid #ddd; + padding: 1rem 1.5rem; + position: sticky; + top: 0; + z-index: 10; +} + +header h1 { font-size: 1.2rem; margin-bottom: 0.75rem; } + +nav { display: flex; gap: 0.5rem; } + +.tab-btn { + padding: 0.4rem 1rem; + border: 1px solid #ccc; + border-radius: 6px; + background: #fff; + cursor: pointer; + font-size: 0.9rem; +} + +.tab-btn.active { + background: #222; + color: #fff; + border-color: #222; +} + +main { padding: 1.5rem; max-width: 900px; margin: 0 auto; } + +.tab-content { display: none; } +.tab-content.active { display: block; } + +.color-preview { + width: 100%; + height: 80px; + border-radius: 8px; + border: 1px solid #ddd; + margin-bottom: 0.75rem; +} + +.color-codes { + display: flex; + gap: 1rem; + flex-wrap: wrap; + margin-bottom: 1rem; +} + +.color-code-group { display: flex; flex-direction: column; gap: 0.25rem; } +.color-code-group label { font-size: 0.75rem; color: #666; text-transform: uppercase; } +.color-code-group input { + padding: 0.4rem 0.6rem; + border: 1px solid #ccc; + border-radius: 6px; + font-family: monospace; + font-size: 0.9rem; + width: 140px; +} + +button.action-btn { + padding: 0.4rem 0.9rem; + border: 1px solid #ccc; + border-radius: 6px; + background: #fff; + cursor: pointer; + font-size: 0.85rem; +} + +button.action-btn:hover { background: #f0f0f0; } + +.swatch { + display: inline-block; + width: 40px; + height: 40px; + border-radius: 6px; + border: 1px solid #ddd; + cursor: pointer; + vertical-align: middle; +} +``` + +- [ ] **Schritt 3: `js/app.js` mit Tab-Logik und globalem State erstellen** + +```js +// Globaler State — aktive Farbe als { h, s, l } (HSL, 0-360, 0-100, 0-100) +export const state = { + color: { h: 200, s: 60, l: 50 }, + setColor(hsl) { + this.color = hsl; + document.dispatchEvent(new CustomEvent('colorChanged', { detail: hsl })); + } +}; + +// Tab-Navigation +document.querySelectorAll('.tab-btn').forEach(btn => { + btn.addEventListener('click', () => { + const tab = btn.dataset.tab; + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + document.querySelectorAll('.tab-content').forEach(s => s.classList.remove('active')); + btn.classList.add('active'); + document.getElementById('tab-' + tab).classList.add('active'); + }); +}); +``` + +- [ ] **Schritt 4: Im Browser öffnen — Tab-Wechsel muss funktionieren** + +`index.html` direkt im Browser öffnen (Doppelklick oder `open index.html`). Alle 4 Tabs müssen klickbar sein und den Inhalt wechseln. + +- [ ] **Schritt 5: Committen** + +```bash +git add index.html style.css js/app.js +git commit -m "feat: Grundstruktur mit Tab-Navigation" +``` + +--- + +### Task 2: `converter.js` — Farbumrechnung und Harmonien + +**Files:** +- Create: `js/converter.js` + +- [ ] **Schritt 1: `js/converter.js` erstellen** + +```js +// Hex → RGB +export function hexToRgb(hex) { + const h = hex.replace('#', ''); + const n = parseInt(h.length === 3 + ? h.split('').map(c => c + c).join('') + : h, 16); + return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 }; +} + +// RGB → Hex +export function rgbToHex({ r, g, b }) { + return '#' + [r, g, b].map(v => v.toString(16).padStart(2, '0')).join(''); +} + +// RGB → HSL (h: 0-360, s: 0-100, l: 0-100) +export function rgbToHsl({ r, g, b }) { + r /= 255; g /= 255; b /= 255; + const max = Math.max(r, g, b), min = Math.min(r, g, b); + let h, s; + const l = (max + min) / 2; + if (max === min) { + h = s = 0; + } else { + const d = max - min; + s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + switch (max) { + case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break; + case g: h = ((b - r) / d + 2) / 6; break; + case b: h = ((r - g) / d + 4) / 6; break; + } + } + return { h: Math.round(h * 360), s: Math.round(s * 100), l: Math.round(l * 100) }; +} + +// HSL → RGB +export function hslToRgb({ h, s, l }) { + s /= 100; l /= 100; + const k = n => (n + h / 30) % 12; + const a = s * Math.min(l, 1 - l); + const f = n => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))); + return { r: Math.round(f(0) * 255), g: Math.round(f(8) * 255), b: Math.round(f(4) * 255) }; +} + +// HSL → Hex +export function hslToHex(hsl) { + return rgbToHex(hslToRgb(hsl)); +} + +// Hex → HSL +export function hexToHsl(hex) { + return rgbToHsl(hexToRgb(hex)); +} + +// Harmonien — Input: HSL, Output: Objekt mit Arrays von HSL-Objekten +export function getHarmonies(hsl) { + const rotate = deg => ({ ...hsl, h: (hsl.h + deg + 360) % 360 }); + return { + komplementaer: [rotate(180)], + analog: [rotate(-30), rotate(30)], + triade: [rotate(120), rotate(240)], + splitKomplementaer: [rotate(150), rotate(210)], + }; +} +``` + +- [ ] **Schritt 2: Schnelltest in der Browser-Konsole** + +`index.html` öffnen, in der Browserkonsole eingeben: +```js +import('./js/converter.js').then(m => { + console.log(m.hexToRgb('#3a8fc1')); // { r: 58, g: 143, b: 193 } + console.log(m.rgbToHex({r:58,g:143,b:193})); // #3a8fc1 + console.log(m.rgbToHsl({r:58,g:143,b:193})); // { h: 204, s: 54, l: 49 } + console.log(m.hslToHex({h:204,s:54,l:49})); // nahe #3a8fc1 +}); +``` +Alle Werte müssen stimmen. + +- [ ] **Schritt 3: Committen** + +```bash +git add js/converter.js +git commit -m "feat: Farbumrechnung Hex/RGB/HSL und Harmonieberechnungen" +``` + +--- + +### Task 3: Eingabe-Tab + +**Files:** +- Modify: `index.html` — Tab-Inhalt für Eingabe befüllen +- Create: `js/eingabe.js` +- Modify: `js/app.js` — Eingabe-Modul initialisieren + +- [ ] **Schritt 1: HTML für Eingabe-Tab in `index.html` ersetzen** + +Den Platzhalter `
` ersetzen mit: + +```html +
+

Farbe eingeben

+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+``` + +- [ ] **Schritt 2: `js/eingabe.js` erstellen** + +```js +import { hexToRgb, rgbToHex, hexToHsl, hslToHex, hslToRgb, rgbToHsl } from './converter.js'; +import { state } from './app.js'; + +function hslToDisplay({ h, s, l }) { return h + ', ' + s + '%, ' + l + '%'; } +function rgbToDisplay({ r, g, b }) { return r + ', ' + g + ', ' + b; } + +function parseRgb(str) { + const parts = str.replace(/[^\d,]/g, '').split(',').map(Number); + if (parts.length !== 3 || parts.some(isNaN)) return null; + const [r, g, b] = parts; + if ([r, g, b].some(v => v < 0 || v > 255)) return null; + return { r, g, b }; +} + +function parseHsl(str) { + const parts = str.replace(/[^\d,]/g, '').split(',').map(Number); + if (parts.length !== 3 || parts.some(isNaN)) return null; + const [h, s, l] = parts; + if (h < 0 || h > 360 || s < 0 || s > 100 || l < 0 || l > 100) return null; + return { h, s, l }; +} + +function updateUI(hsl) { + const hex = hslToHex(hsl); + const rgb = hslToRgb(hsl); + document.getElementById('eingabe-preview').style.background = hex; + document.getElementById('eingabe-hex').value = hex; + document.getElementById('eingabe-rgb').value = rgbToDisplay(rgb); + document.getElementById('eingabe-hsl').value = hslToDisplay(hsl); +} + +export function initEingabe(onSaveFavorit, onSaveSchema) { + const hexInput = document.getElementById('eingabe-hex'); + const rgbInput = document.getElementById('eingabe-rgb'); + const hslInput = document.getElementById('eingabe-hsl'); + + hexInput.addEventListener('input', () => { + const val = hexInput.value.trim(); + if (/^#[0-9a-fA-F]{6}$/.test(val)) { + const hsl = hexToHsl(val); + state.setColor(hsl); + updateUI(hsl); + } + }); + + rgbInput.addEventListener('input', () => { + const rgb = parseRgb(rgbInput.value); + if (rgb) { + const hsl = rgbToHsl(rgb); + state.setColor(hsl); + updateUI(hsl); + } + }); + + hslInput.addEventListener('input', () => { + const hsl = parseHsl(hslInput.value); + if (hsl) { + state.setColor(hsl); + updateUI(hsl); + } + }); + + document.addEventListener('colorChanged', () => updateUI(state.color)); + + document.getElementById('eingabe-fav-btn').addEventListener('click', () => { + onSaveFavorit(state.color); + }); + + document.getElementById('eingabe-schema-btn').addEventListener('click', () => { + onSaveSchema(state.color); + }); + + updateUI(state.color); +} +``` + +- [ ] **Schritt 3: `initEingabe` in `app.js` einbinden** + +Am Ende von `js/app.js` ergänzen (Platzhalter-Funktionen, bis `collection.js` in Task 6 fertig ist): + +```js +import { initEingabe } from './eingabe.js'; + +function addFavorit(hsl) { console.log('addFavorit', hsl); } +function addColorToSchema(hsl) { console.log('addColorToSchema', hsl); } + +initEingabe(addFavorit, addColorToSchema); +``` + +- [ ] **Schritt 4: Im Browser testen** + +Eingabe-Tab öffnen: +- Hex `#ff6600` eingeben → RGB `255, 102, 0` und HSL `24, 100%, 50%` erscheinen, Preview wird orange +- RGB `0, 128, 0` eingeben → Hex `#008000`, HSL `120, 100%, 25%` +- HSL `300, 100%, 50%` eingeben → Hex `#ff00ff`, RGB `255, 0, 255` + +- [ ] **Schritt 5: Committen** + +```bash +git add index.html js/eingabe.js js/app.js +git commit -m "feat: Eingabe-Tab mit bidirektionaler Farbumrechnung" +``` + +--- + +### Task 4: Picker-Tab + +**Files:** +- Modify: `index.html` — Picker-Tab-Inhalt +- Create: `js/picker.js` +- Modify: `js/app.js` — Picker initialisieren + +- [ ] **Schritt 1: HTML für Picker-Tab in `index.html` ersetzen** + +Den Platzhalter `
` ersetzen mit: + +```html +
+

Farbe aus Bild

+
+

Bild hierher ziehen, einfügen (Strg+V) oder

+ + +
+ + +
+``` + +CSS für Dropzone in `style.css` ergänzen: + +```css +#picker-dropzone { + border: 2px dashed #ccc; + border-radius: 8px; + padding: 2rem; + text-align: center; + margin-bottom: 1rem; + background: #fafafa; +} + +#picker-dropzone.drag-over { + border-color: #222; + background: #f0f0f0; +} + +#picker-dropzone p { margin-bottom: 0.75rem; color: #666; font-size: 0.9rem; } +``` + +- [ ] **Schritt 2: `js/picker.js` erstellen** + +```js +import { rgbToHex, rgbToHsl } from './converter.js'; +import { state } from './app.js'; + +function rgbToDisplay({ r, g, b }) { return r + ', ' + g + ', ' + b; } +function hslToDisplay({ h, s, l }) { return h + ', ' + s + '%, ' + l + '%'; } + +function showColor(r, g, b) { + const hex = rgbToHex({ r, g, b }); + const hsl = rgbToHsl({ r, g, b }); + state.setColor(hsl); + document.getElementById('picker-preview').style.background = hex; + document.getElementById('picker-hex').value = hex; + document.getElementById('picker-rgb').value = rgbToDisplay({ r, g, b }); + document.getElementById('picker-hsl').value = hslToDisplay(hsl); + document.getElementById('picker-result').style.display = 'block'; +} + +function loadImage(file) { + const reader = new FileReader(); + reader.onload = (e) => { + const img = new Image(); + img.onload = () => { + const canvas = document.getElementById('picker-canvas'); + canvas.width = img.width; + canvas.height = img.height; + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + canvas.style.display = 'block'; + + // Mittlerer Pixel als Vorschlag + const mx = Math.floor(img.width / 2); + const my = Math.floor(img.height / 2); + const px = ctx.getImageData(mx, my, 1, 1).data; + showColor(px[0], px[1], px[2]); + + // Eyedropper: Klick wählt Pixel frei + canvas.onclick = (ev) => { + const rect = canvas.getBoundingClientRect(); + const scaleX = canvas.width / rect.width; + const scaleY = canvas.height / rect.height; + const x = Math.floor((ev.clientX - rect.left) * scaleX); + const y = Math.floor((ev.clientY - rect.top) * scaleY); + const d = ctx.getImageData(x, y, 1, 1).data; + showColor(d[0], d[1], d[2]); + }; + }; + img.src = e.target.result; + }; + reader.readAsDataURL(file); +} + +export function initPicker(onSaveFavorit, onSaveSchema) { + const dropzone = document.getElementById('picker-dropzone'); + + document.getElementById('picker-file-trigger').addEventListener('click', () => { + document.getElementById('picker-file-input').click(); + }); + + document.getElementById('picker-file-input').addEventListener('change', (e) => { + if (e.target.files[0]) loadImage(e.target.files[0]); + }); + + dropzone.addEventListener('dragover', (e) => { + e.preventDefault(); + dropzone.classList.add('drag-over'); + }); + dropzone.addEventListener('dragleave', () => dropzone.classList.remove('drag-over')); + dropzone.addEventListener('drop', (e) => { + e.preventDefault(); + dropzone.classList.remove('drag-over'); + const file = e.dataTransfer.files[0]; + if (file && file.type.startsWith('image/')) loadImage(file); + }); + + // Paste (Strg+V / Cmd+V) + document.addEventListener('paste', (e) => { + const item = Array.from(e.clipboardData.items).find(i => i.type.startsWith('image/')); + if (item) loadImage(item.getAsFile()); + }); + + document.getElementById('picker-fav-btn').addEventListener('click', () => onSaveFavorit(state.color)); + document.getElementById('picker-schema-btn').addEventListener('click', () => onSaveSchema(state.color)); +} +``` + +- [ ] **Schritt 3: `initPicker` in `app.js` einbinden** + +Ergänzen (nach den bestehenden Imports): + +```js +import { initPicker } from './picker.js'; + +initPicker(addFavorit, addColorToSchema); +``` + +- [ ] **Schritt 4: Im Browser testen** + +- Screenshot mit `Cmd+Shift+4` (Mac) erstellen → mit `Cmd+V` einfügen → Bild erscheint, mittlerer Pixel wird vorgeschlagen +- Auf beliebigen Punkt im Bild klicken → Farbe wechselt +- Datei hochladen über "Datei wählen" → funktioniert gleich +- Bild per Drag & Drop ins Dropzone-Feld ziehen → Bild lädt + +- [ ] **Schritt 5: Committen** + +```bash +git add index.html style.css js/picker.js js/app.js +git commit -m "feat: Picker-Tab mit Upload, Paste und Eyedropper" +``` + +--- + +### Task 5: Harmonien-Tab + +**Files:** +- Modify: `index.html` — Harmonien-Tab-Inhalt +- Create: `js/harmonien.js` +- Modify: `js/app.js` — Harmonien initialisieren + +- [ ] **Schritt 1: HTML für Harmonien-Tab in `index.html` ersetzen** + +```html +
+

Farbharmonien

+

Ausgangsfarbe: #3a8fc1

+
+
+
+``` + +CSS in `style.css` ergänzen: + +```css +.subtitle { font-size: 0.85rem; color: #666; margin-bottom: 0.5rem; } + +.harmony-row h3 { font-size: 0.9rem; margin-bottom: 0.5rem; } +.harmony-swatches { display: flex; gap: 0.75rem; flex-wrap: wrap; align-items: flex-start; } +.harmony-swatch { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; +} +.harmony-swatch .swatch { width: 56px; height: 56px; } +.harmony-swatch span { font-size: 0.75rem; font-family: monospace; } +``` + +- [ ] **Schritt 2: `js/harmonien.js` erstellen** + +```js +import { getHarmonies, hslToHex } from './converter.js'; +import { state } from './app.js'; + +function renderHarmony(label, colors, onSaveFavorit, onSaveSchema) { + const row = document.createElement('div'); + row.className = 'harmony-row'; + + const heading = document.createElement('h3'); + heading.textContent = label; + row.appendChild(heading); + + const swatchesDiv = document.createElement('div'); + swatchesDiv.className = 'harmony-swatches'; + + colors.forEach(hsl => { + const hex = hslToHex(hsl); + + const div = document.createElement('div'); + div.className = 'harmony-swatch'; + + const swatch = document.createElement('div'); + swatch.className = 'swatch'; + swatch.style.background = hex; + swatch.title = hex; + + const label = document.createElement('span'); + label.textContent = hex; + + const btnRow = document.createElement('div'); + btnRow.style.cssText = 'display:flex;gap:0.25rem'; + + const favBtn = document.createElement('button'); + favBtn.className = 'action-btn'; + favBtn.textContent = 'Fav'; + favBtn.style.fontSize = '0.7rem'; + favBtn.addEventListener('click', () => onSaveFavorit(hsl)); + + const schemaBtn = document.createElement('button'); + schemaBtn.className = 'action-btn'; + schemaBtn.textContent = '+Schema'; + schemaBtn.style.fontSize = '0.7rem'; + schemaBtn.addEventListener('click', () => onSaveSchema(hsl)); + + btnRow.appendChild(favBtn); + btnRow.appendChild(schemaBtn); + + div.appendChild(swatch); + div.appendChild(label); + div.appendChild(btnRow); + swatchesDiv.appendChild(div); + }); + + row.appendChild(swatchesDiv); + return row; +} + +function render(onSaveFavorit, onSaveSchema) { + const hsl = state.color; + const hex = hslToHex(hsl); + + document.getElementById('harmonien-base-hex').textContent = hex; + document.getElementById('harmonien-base-preview').style.background = hex; + + const grid = document.getElementById('harmonien-grid'); + grid.textContent = ''; + + const h = getHarmonies(hsl); + grid.appendChild(renderHarmony('Komplementar', h.komplementaer, onSaveFavorit, onSaveSchema)); + grid.appendChild(renderHarmony('Analog', h.analog, onSaveFavorit, onSaveSchema)); + grid.appendChild(renderHarmony('Triade', h.triade, onSaveFavorit, onSaveSchema)); + grid.appendChild(renderHarmony('Split-Komplementar', h.splitKomplementaer, onSaveFavorit, onSaveSchema)); +} + +export function initHarmonien(onSaveFavorit, onSaveSchema) { + document.addEventListener('colorChanged', () => render(onSaveFavorit, onSaveSchema)); + render(onSaveFavorit, onSaveSchema); +} +``` + +- [ ] **Schritt 3: `initHarmonien` in `app.js` einbinden** + +```js +import { initHarmonien } from './harmonien.js'; + +initHarmonien(addFavorit, addColorToSchema); +``` + +- [ ] **Schritt 4: Im Browser testen** + +Harmonien-Tab öffnen → 4 Sektionen (Komplementar, Analog, Triade, Split-Komplementar) mit Farbfeldern und Hex-Codes. Im Eingabe-Tab Farbe ändern → zurück zu Harmonien → Farben haben sich aktualisiert. + +- [ ] **Schritt 5: Committen** + +```bash +git add index.html style.css js/harmonien.js js/app.js +git commit -m "feat: Harmonien-Tab mit Komplementar, Analog, Triade, Split-Komplementar" +``` + +--- + +### Task 6: `collection.js` — Sammlung, localStorage, Export/Import + +**Files:** +- Create: `js/collection.js` +- Modify: `index.html` — Sammlung-Tab-Inhalt +- Modify: `js/app.js` — Platzhalter-Funktionen durch echte Imports ersetzen + +- [ ] **Schritt 1: `js/collection.js` erstellen** + +```js +import { hslToHex } from './converter.js'; + +const STORAGE_KEY = 'farbhelfer'; +const HISTORY_MAX = 20; + +function load() { + try { + return JSON.parse(localStorage.getItem(STORAGE_KEY)) || { favoriten: [], history: [], schemata: [] }; + } catch { + return { favoriten: [], history: [], schemata: [] }; + } +} + +function save(data) { + localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); +} + +export function addFavorit(hsl) { + const data = load(); + const hex = hslToHex(hsl); + if (!data.favoriten.some(f => hslToHex(f) === hex)) { + data.favoriten.push(hsl); + save(data); + renderSammlung(); + } +} + +export function removeFavorit(hex) { + const data = load(); + data.favoriten = data.favoriten.filter(f => hslToHex(f) !== hex); + save(data); + renderSammlung(); +} + +export function addToHistory(hsl) { + const data = load(); + const hex = hslToHex(hsl); + data.history = data.history.filter(h => hslToHex(h) !== hex); + data.history.unshift(hsl); + if (data.history.length > HISTORY_MAX) data.history = data.history.slice(0, HISTORY_MAX); + save(data); +} + +export function addColorToSchema(hsl) { + const name = prompt('Schema-Name (leer = letztes Schema):'); + const data = load(); + if (name) { + const existing = data.schemata.find(s => s.name === name); + if (existing) { + if (existing.farben.length < 4) { + existing.farben.push(hsl); + save(data); + renderSammlung(); + } else { + alert('Schema hat bereits 4 Farben.'); + } + } else { + data.schemata.push({ name, farben: [hsl] }); + save(data); + renderSammlung(); + } + } else if (data.schemata.length > 0) { + const last = data.schemata[data.schemata.length - 1]; + if (last.farben.length < 4) { + last.farben.push(hsl); + save(data); + renderSammlung(); + } else { + alert('Letztes Schema hat bereits 4 Farben.'); + } + } else { + alert('Kein Schema vorhanden. Bitte Name eingeben.'); + } +} + +export function deleteSchema(name) { + const data = load(); + data.schemata = data.schemata.filter(s => s.name !== name); + save(data); + renderSammlung(); +} + +export function exportCollection() { + const data = load(); + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const a = document.createElement('a'); + a.href = URL.createObjectURL(blob); + a.download = 'farbhelfer-sammlung.json'; + a.click(); +} + +export function importCollection() { + const input = document.createElement('input'); + input.type = 'file'; + input.accept = '.json'; + input.onchange = (e) => { + const file = e.target.files[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => { + try { + const imported = JSON.parse(ev.target.result); + const data = load(); + const existingFavHexes = new Set(data.favoriten.map(hslToHex)); + (imported.favoriten || []).forEach(hsl => { + if (!existingFavHexes.has(hslToHex(hsl))) data.favoriten.push(hsl); + }); + const existingSchemaNames = new Set(data.schemata.map(s => s.name)); + (imported.schemata || []).forEach(s => { + if (!existingSchemaNames.has(s.name)) data.schemata.push(s); + }); + save(data); + renderSammlung(); + alert('Import erfolgreich.'); + } catch { + alert('Ungultige Datei.'); + } + }; + reader.readAsText(file); + }; + input.click(); +} + +function makeSwatch(hsl) { + const hex = hslToHex(hsl); + + const wrapper = document.createElement('div'); + wrapper.style.cssText = 'display:inline-flex;flex-direction:column;align-items:center;gap:0.25rem'; + + const swatch = document.createElement('div'); + swatch.className = 'swatch'; + swatch.style.background = hex; + swatch.title = hex; + + const label = document.createElement('span'); + label.style.cssText = 'font-size:0.75rem;font-family:monospace'; + label.textContent = hex; + + wrapper.appendChild(swatch); + wrapper.appendChild(label); + return wrapper; +} + +export function renderSammlung() { + const data = load(); + + // Favoriten + const favContainer = document.getElementById('sammlung-favoriten'); + if (favContainer) { + favContainer.textContent = ''; + if (data.favoriten.length === 0) { + const msg = document.createElement('p'); + msg.style.cssText = 'color:#999;font-size:0.85rem'; + msg.textContent = 'Noch keine Favoriten.'; + favContainer.appendChild(msg); + } else { + data.favoriten.forEach(hsl => { + const hex = hslToHex(hsl); + const cell = document.createElement('div'); + cell.style.cssText = 'display:inline-flex;flex-direction:column;align-items:center;gap:0.25rem;margin:0.25rem'; + cell.appendChild(makeSwatch(hsl)); + + const del = document.createElement('button'); + del.className = 'action-btn'; + del.textContent = 'Loschen'; + del.style.fontSize = '0.7rem'; + del.addEventListener('click', () => removeFavorit(hex)); + cell.appendChild(del); + favContainer.appendChild(cell); + }); + } + } + + // Historie + const histContainer = document.getElementById('sammlung-history'); + if (histContainer) { + histContainer.textContent = ''; + if (data.history.length === 0) { + const msg = document.createElement('p'); + msg.style.cssText = 'color:#999;font-size:0.85rem'; + msg.textContent = 'Noch kein Verlauf.'; + histContainer.appendChild(msg); + } else { + data.history.forEach(hsl => { + const cell = document.createElement('div'); + cell.style.cssText = 'display:inline-block;margin:0.25rem'; + cell.appendChild(makeSwatch(hsl)); + histContainer.appendChild(cell); + }); + } + } + + // Schemata + const schemataContainer = document.getElementById('sammlung-schemata'); + if (schemataContainer) { + schemataContainer.textContent = ''; + if (data.schemata.length === 0) { + const msg = document.createElement('p'); + msg.style.cssText = 'color:#999;font-size:0.85rem'; + msg.textContent = 'Noch keine Schemata.'; + schemataContainer.appendChild(msg); + } else { + data.schemata.forEach(schema => { + const card = document.createElement('div'); + card.style.cssText = 'border:1px solid #ddd;border-radius:8px;padding:1rem;margin-bottom:0.75rem;background:#fff'; + + const header = document.createElement('div'); + header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem'; + + const nameEl = document.createElement('strong'); + nameEl.textContent = schema.name; + + const delBtn = document.createElement('button'); + delBtn.className = 'action-btn'; + delBtn.textContent = 'Schema loschen'; + delBtn.style.fontSize = '0.75rem'; + delBtn.addEventListener('click', () => deleteSchema(schema.name)); + + header.appendChild(nameEl); + header.appendChild(delBtn); + + const swatchesDiv = document.createElement('div'); + swatchesDiv.style.cssText = 'display:flex;gap:0.5rem;flex-wrap:wrap'; + schema.farben.forEach(hsl => swatchesDiv.appendChild(makeSwatch(hsl))); + + card.appendChild(header); + card.appendChild(swatchesDiv); + schemataContainer.appendChild(card); + }); + } + } +} +``` + +- [ ] **Schritt 2: HTML für Sammlung-Tab in `index.html` ersetzen** + +```html +
+

Sammlung

+ +
+ + +
+ +

Favoriten

+
+ +

Verlauf (letzte 20)

+
+ +

Farbschemata

+
+
+``` + +- [ ] **Schritt 3: `app.js` vollständig aktualisieren** + +`js/app.js` komplett ersetzen mit der finalen Version (Platzhalter weg, alle echten Imports): + +```js +import { addFavorit, addColorToSchema, addToHistory, renderSammlung, exportCollection, importCollection } from './collection.js'; +import { initEingabe } from './eingabe.js'; +import { initPicker } from './picker.js'; +import { initHarmonien } from './harmonien.js'; + +export const state = { + color: { h: 200, s: 60, l: 50 }, + setColor(hsl) { + this.color = hsl; + document.dispatchEvent(new CustomEvent('colorChanged', { detail: hsl })); + } +}; + +// Tab-Navigation +document.querySelectorAll('.tab-btn').forEach(btn => { + btn.addEventListener('click', () => { + const tab = btn.dataset.tab; + document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active')); + document.querySelectorAll('.tab-content').forEach(s => s.classList.remove('active')); + btn.classList.add('active'); + document.getElementById('tab-' + tab).classList.add('active'); + }); +}); + +// Jede Farbänderung in die Historie +document.addEventListener('colorChanged', (e) => addToHistory(e.detail)); + +initEingabe(addFavorit, addColorToSchema); +initPicker(addFavorit, addColorToSchema); +initHarmonien(addFavorit, addColorToSchema); + +document.getElementById('sammlung-export-btn').addEventListener('click', exportCollection); +document.getElementById('sammlung-import-btn').addEventListener('click', importCollection); + +renderSammlung(); +``` + +- [ ] **Schritt 4: Im Browser testen** + +- Im Eingabe-Tab Farbe eingeben → "Zu Favoriten" → Sammlung-Tab zeigt sie +- Im Picker Farbe wählen → Verlauf in Sammlung aktualisiert sich +- "Zu Schema hinzufügen" → Prompt erscheint → Schema-Name eingeben → Sammlung zeigt Schema +- Exportieren → JSON-Download startet +- Importierte JSON wieder importieren → keine Duplikate + +- [ ] **Schritt 5: Committen** + +```bash +git add js/collection.js index.html js/app.js +git commit -m "feat: Sammlung mit Favoriten, Verlauf, Schemata, Export und Import" +``` + +--- + +### Task 7: Finales CSS-Finishing + +**Files:** +- Modify: `style.css` + +- [ ] **Schritt 1: Finale Styles an `style.css` anhängen** + +```css +h2 { font-size: 1.1rem; margin-bottom: 1rem; } +h3 { font-size: 0.95rem; } + +.tab-content { padding-top: 0.5rem; } + +.color-preview { transition: background 0.15s; } + +.action-btn:active { transform: scale(0.97); } + +@media (max-width: 600px) { + nav { flex-wrap: wrap; } + .color-codes { flex-direction: column; } + .color-code-group input { width: 100%; } +} +``` + +- [ ] **Schritt 2: Visuell prüfen** + +Alle 4 Tabs durchklicken — aufgeräumtes Layout? DevTools → Toggle device toolbar → unter 600px: Navigation und Inputs stapeln sich vertikal. + +- [ ] **Schritt 3: Committen** + +```bash +git add style.css +git commit -m "style: finales CSS-Finishing und mobile Anpassungen" +```