# 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
```
- [ ] **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
```
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"
```