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
+
+
+
+
+
+
+
+ Harmonien kommen 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"
+```