feat: Sammlung mit Favoriten, Verlauf, Schemata, Export und Import
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
16
index.html
16
index.html
@@ -77,7 +77,21 @@
|
|||||||
<div id="harmonien-grid" style="display:flex;flex-direction:column;gap:1.5rem;margin-top:1rem"></div>
|
<div id="harmonien-grid" style="display:flex;flex-direction:column;gap:1.5rem;margin-top:1rem"></div>
|
||||||
</section>
|
</section>
|
||||||
<section id="tab-sammlung" class="tab-content">
|
<section id="tab-sammlung" class="tab-content">
|
||||||
<p>Sammlung kommt hier</p>
|
<h2>Sammlung</h2>
|
||||||
|
|
||||||
|
<div style="display:flex;gap:0.5rem;margin-bottom:1.5rem">
|
||||||
|
<button class="action-btn" id="sammlung-export-btn">Exportieren</button>
|
||||||
|
<button class="action-btn" id="sammlung-import-btn">Importieren</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 style="margin-bottom:0.5rem">Favoriten</h3>
|
||||||
|
<div id="sammlung-favoriten" style="display:flex;flex-wrap:wrap;gap:0.5rem;margin-bottom:1.5rem"></div>
|
||||||
|
|
||||||
|
<h3 style="margin-bottom:0.5rem">Verlauf (letzte 20)</h3>
|
||||||
|
<div id="sammlung-history" style="display:flex;flex-wrap:wrap;gap:0.5rem;margin-bottom:1.5rem"></div>
|
||||||
|
|
||||||
|
<h3 style="margin-bottom:0.5rem">Farbschemata</h3>
|
||||||
|
<div id="sammlung-schemata"></div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|||||||
11
js/app.js
11
js/app.js
@@ -1,8 +1,8 @@
|
|||||||
import { initEingabe } from './eingabe.js';
|
import { initEingabe } from './eingabe.js';
|
||||||
import { initPicker } from './picker.js';
|
import { initPicker } from './picker.js';
|
||||||
import { initHarmonien } from './harmonien.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,
|
// state.color is read-only from outside — always use setColor() to update,
|
||||||
// so that the colorChanged event is dispatched to all listening modules.
|
// so that the colorChanged event is dispatched to all listening modules.
|
||||||
export const state = {
|
export const state = {
|
||||||
@@ -26,9 +26,14 @@ document.querySelectorAll('.tab-btn').forEach(btn => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function addFavorit(hsl) { console.log('addFavorit', hsl); }
|
// Every color change goes into history
|
||||||
function addColorToSchema(hsl) { console.log('addColorToSchema', hsl); }
|
document.addEventListener('colorChanged', (e) => addToHistory(e.detail));
|
||||||
|
|
||||||
initEingabe(addFavorit, addColorToSchema);
|
initEingabe(addFavorit, addColorToSchema);
|
||||||
initPicker(addFavorit, addColorToSchema);
|
initPicker(addFavorit, addColorToSchema);
|
||||||
initHarmonien(addFavorit, addColorToSchema);
|
initHarmonien(addFavorit, addColorToSchema);
|
||||||
|
|
||||||
|
document.getElementById('sammlung-export-btn').addEventListener('click', exportCollection);
|
||||||
|
document.getElementById('sammlung-import-btn').addEventListener('click', importCollection);
|
||||||
|
|
||||||
|
renderSammlung();
|
||||||
|
|||||||
232
js/collection.js
Normal file
232
js/collection.js
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user