diff --git a/index.html b/index.html
index 579a34d..46da92d 100644
--- a/index.html
+++ b/index.html
@@ -77,8 +77,22 @@
+ Sammlung
+
+
+
+
+
+
+ Favoriten
+
+
+ Verlauf (letzte 20)
+
+
+ Farbschemata
+
+
diff --git a/js/app.js b/js/app.js
index db257ae..10ac70a 100644
--- a/js/app.js
+++ b/js/app.js
@@ -1,8 +1,8 @@
import { initEingabe } from './eingabe.js';
import { initPicker } from './picker.js';
import { initHarmonien } from './harmonien.js';
+import { addFavorit, addColorToSchema, addToHistory, renderSammlung, exportCollection, importCollection } from './collection.js';
-// Globaler State — aktive Farbe als { h, s, l } (HSL, 0-360, 0-100, 0-100)
// state.color is read-only from outside — always use setColor() to update,
// so that the colorChanged event is dispatched to all listening modules.
export const state = {
@@ -26,9 +26,14 @@ document.querySelectorAll('.tab-btn').forEach(btn => {
});
});
-function addFavorit(hsl) { console.log('addFavorit', hsl); }
-function addColorToSchema(hsl) { console.log('addColorToSchema', hsl); }
+// Every color change goes into history
+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();
diff --git a/js/collection.js b/js/collection.js
new file mode 100644
index 0000000..f620fb0
--- /dev/null
+++ b/js/collection.js
@@ -0,0 +1,232 @@
+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);
+ });
+ }
+ }
+}