# 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" ```