feat: Hierarchien für Farbschemata via Drag & Drop

Schematas können per Drag & Drop als Unterkategorien anderer Schematas
organisiert werden. Kinder werden eingerückt mit linkem Rand angezeigt.
Entnesten via Button oder Root-Dropzone. Löschen orphaned Kinder.
Umbenennen aktualisiert Kind-Referenzen automatisch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ferdinand
2026-04-02 15:13:46 +02:00
parent 8a31d5336e
commit 20abe09590
2 changed files with 229 additions and 54 deletions

View File

@@ -80,21 +80,38 @@ export function addColorToSchema(hsl) {
export function saveSchema(name, farben, bild, originalName) { export function saveSchema(name, farben, bild, originalName) {
const data = load(); const data = load();
if (originalName && originalName !== name) { if (originalName && originalName !== name) {
const old = data.schemata.find(s => s.name === originalName);
const preservedParent = old?.parent;
// Kind-Referenzen auf neuen Namen umbiegen
data.schemata.forEach(s => { if (s.parent === originalName) s.parent = name; });
data.schemata = data.schemata.filter(s => s.name !== originalName); data.schemata = data.schemata.filter(s => s.name !== originalName);
} const entry = { name, farben, bild: bild || null };
if (preservedParent) entry.parent = preservedParent;
data.schemata.push(entry);
} else {
const existing = data.schemata.find(s => s.name === name); const existing = data.schemata.find(s => s.name === name);
if (existing) { if (existing) {
existing.farben = farben; existing.farben = farben;
if (bild !== undefined) existing.bild = bild || null; if (bild !== undefined) existing.bild = bild || null;
// parent bleibt erhalten
} else { } else {
data.schemata.push({ name, farben, bild: bild || null }); data.schemata.push({ name, farben, bild: bild || null });
} }
}
save(data); save(data);
renderSammlung(); renderSammlung();
} }
export function deleteSchema(name) { export function deleteSchema(name) {
const data = load(); const data = load();
const schema = data.schemata.find(s => s.name === name);
const parentOfDeleted = schema?.parent || null;
// Kinder auf die Eltern-Ebene des gelöschten Schemas verschieben
data.schemata.forEach(s => {
if (s.parent === name) {
if (parentOfDeleted) { s.parent = parentOfDeleted; } else { delete s.parent; }
}
});
data.schemata = data.schemata.filter(s => s.name !== name); data.schemata = data.schemata.filter(s => s.name !== name);
save(data); save(data);
renderSammlung(); renderSammlung();
@@ -153,6 +170,147 @@ export function importCollection() {
input.click(); input.click();
} }
// --- Schema-Hierarchie ---
function buildTree(schemata) {
const nodes = {};
schemata.forEach(s => { nodes[s.name] = { ...s, children: [] }; });
const roots = [];
schemata.forEach(s => {
if (s.parent && nodes[s.parent]) {
nodes[s.parent].children.push(nodes[s.name]);
} else {
roots.push(nodes[s.name]);
}
});
return roots;
}
function isDescendant(schemata, ancestorName, checkName) {
for (const s of schemata) {
if (s.parent === ancestorName) {
if (s.name === checkName || isDescendant(schemata, s.name, checkName)) return true;
}
}
return false;
}
function setSchemaParent(childName, parentName) {
const data = load();
const schema = data.schemata.find(s => s.name === childName);
if (!schema) return;
if (parentName) { schema.parent = parentName; } else { delete schema.parent; }
save(data);
renderSammlung();
}
let draggedSchemaName = null;
function renderSchemaCard(node, container, allSchemata, depth) {
const card = document.createElement('div');
card.className = depth === 0 ? 'schema-card' : 'schema-card schema-card-nested';
card.dataset.schemaName = node.name;
card.draggable = true;
card.addEventListener('dragstart', (e) => {
draggedSchemaName = node.name;
e.dataTransfer.effectAllowed = 'move';
setTimeout(() => card.classList.add('schema-dragging'), 0);
});
card.addEventListener('dragend', () => {
card.classList.remove('schema-dragging');
draggedSchemaName = null;
document.querySelectorAll('.schema-drag-over').forEach(el => el.classList.remove('schema-drag-over'));
});
card.addEventListener('dragover', (e) => {
if (!draggedSchemaName || draggedSchemaName === node.name) return;
if (isDescendant(allSchemata, draggedSchemaName, node.name)) return;
e.preventDefault();
e.stopPropagation();
document.querySelectorAll('.schema-drag-over').forEach(el => el.classList.remove('schema-drag-over'));
card.classList.add('schema-drag-over');
});
card.addEventListener('dragleave', (e) => {
if (!card.contains(e.relatedTarget)) card.classList.remove('schema-drag-over');
});
card.addEventListener('drop', (e) => {
e.preventDefault();
e.stopPropagation();
card.classList.remove('schema-drag-over');
if (!draggedSchemaName || draggedSchemaName === node.name) return;
if (isDescendant(allSchemata, draggedSchemaName, node.name)) return;
setSchemaParent(draggedSchemaName, node.name);
});
const header = document.createElement('div');
header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem;gap:0.5rem';
const nameRow = document.createElement('div');
nameRow.style.cssText = 'display:flex;align-items:center;gap:0.5rem;min-width:0';
const handle = document.createElement('span');
handle.textContent = '⠿';
handle.title = 'Ziehen um Untergruppe zu setzen';
handle.style.cssText = 'color:#ccc;cursor:grab;font-size:1.1rem;user-select:none;flex-shrink:0';
nameRow.appendChild(handle);
if (node.bild) {
const thumb = document.createElement('div');
thumb.className = 'schema-thumb';
thumb.style.backgroundImage = 'url(' + node.bild + ')';
nameRow.appendChild(thumb);
}
const nameEl = document.createElement('strong');
nameEl.textContent = node.name;
nameRow.appendChild(nameEl);
const btnGroup = document.createElement('div');
btnGroup.style.cssText = 'display:flex;gap:0.4rem;flex-shrink:0';
if (depth > 0) {
const unestBtn = document.createElement('button');
unestBtn.className = 'action-btn';
unestBtn.textContent = '↑ Entnesten';
unestBtn.style.fontSize = '0.75rem';
unestBtn.addEventListener('click', () => setSchemaParent(node.name, null));
btnGroup.appendChild(unestBtn);
}
const editBtn = document.createElement('button');
editBtn.className = 'action-btn';
editBtn.textContent = 'Bearbeiten';
editBtn.style.fontSize = '0.75rem';
editBtn.addEventListener('click', () => editSchemaHandler?.(node));
const delBtn = document.createElement('button');
delBtn.className = 'action-btn';
delBtn.textContent = 'Löschen';
delBtn.style.fontSize = '0.75rem';
delBtn.addEventListener('click', () => deleteSchema(node.name));
btnGroup.appendChild(editBtn);
btnGroup.appendChild(delBtn);
header.appendChild(nameRow);
header.appendChild(btnGroup);
const swatchesDiv = document.createElement('div');
swatchesDiv.style.cssText = 'display:flex;gap:0.5rem;flex-wrap:wrap';
node.farben.forEach(hsl => swatchesDiv.appendChild(makeSwatch(hsl)));
card.appendChild(header);
card.appendChild(swatchesDiv);
if (node.children.length > 0) {
const childrenDiv = document.createElement('div');
childrenDiv.className = 'schema-children';
node.children.forEach(child => renderSchemaCard(child, childrenDiv, allSchemata, depth + 1));
card.appendChild(childrenDiv);
}
container.appendChild(card);
}
function makeSwatch(hsl) { function makeSwatch(hsl) {
const hex = hslToHex(hsl); const hex = hslToHex(hsl);
@@ -227,56 +385,24 @@ export function renderSammlung() {
msg.textContent = 'Noch keine Schemata.'; msg.textContent = 'Noch keine Schemata.';
schemataContainer.appendChild(msg); schemataContainer.appendChild(msg);
} else { } else {
data.schemata.forEach(schema => { const tree = buildTree(data.schemata);
const card = document.createElement('div'); tree.forEach(node => renderSchemaCard(node, schemataContainer, data.schemata, 0));
card.style.cssText = 'border:1px solid #ddd;border-radius:8px;padding:1rem;margin-bottom:0.75rem;background:#fff';
const header = document.createElement('div'); const rootZone = document.createElement('div');
header.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem'; rootZone.className = 'schema-root-dropzone';
rootZone.textContent = 'Hierher ziehen um auf oberste Ebene zu verschieben';
// Thumbnail + Name nebeneinander rootZone.addEventListener('dragover', (e) => {
const nameRow = document.createElement('div'); if (!draggedSchemaName) return;
nameRow.style.cssText = 'display:flex;align-items:center;gap:0.6rem'; e.preventDefault();
rootZone.classList.add('active');
if (schema.bild) {
const thumb = document.createElement('div');
thumb.className = 'schema-thumb';
thumb.style.backgroundImage = 'url(' + schema.bild + ')';
nameRow.appendChild(thumb);
}
const nameEl = document.createElement('strong');
nameEl.textContent = schema.name;
nameRow.appendChild(nameEl);
const editBtn = document.createElement('button');
editBtn.className = 'action-btn';
editBtn.textContent = 'Schema bearbeiten';
editBtn.style.fontSize = '0.75rem';
editBtn.addEventListener('click', () => editSchemaHandler?.(schema));
const delBtn = document.createElement('button');
delBtn.className = 'action-btn';
delBtn.textContent = 'Schema löschen';
delBtn.style.fontSize = '0.75rem';
delBtn.addEventListener('click', () => deleteSchema(schema.name));
const btnGroup = document.createElement('div');
btnGroup.style.cssText = 'display:flex;gap:0.4rem';
btnGroup.appendChild(editBtn);
btnGroup.appendChild(delBtn);
header.appendChild(nameRow);
header.appendChild(btnGroup);
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);
}); });
rootZone.addEventListener('dragleave', () => rootZone.classList.remove('active'));
rootZone.addEventListener('drop', (e) => {
e.preventDefault();
rootZone.classList.remove('active');
if (draggedSchemaName) setSchemaParent(draggedSchemaName, null);
});
schemataContainer.appendChild(rootZone);
} }
} }
} }

View File

@@ -238,6 +238,55 @@ button.action-btn:hover { background: #f0f0f0; }
} }
.schema-bild-preview.hat-bild { border-style: solid; border-color: #ccc; } .schema-bild-preview.hat-bild { border-style: solid; border-color: #ccc; }
/* --- Schema-Hierarchie --- */
.schema-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
margin-bottom: 0.75rem;
background: #fff;
transition: border-color 0.15s, box-shadow 0.15s;
}
.schema-card-nested {
background: #fafafa;
}
.schema-card.schema-drag-over {
border-color: #4a90d9;
box-shadow: 0 0 0 3px rgba(74,144,217,0.2);
}
.schema-card.schema-dragging { opacity: 0.45; }
.schema-children {
margin-top: 0.75rem;
padding-left: 1rem;
border-left: 3px solid #f0f0f0;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.schema-children .schema-card { margin-bottom: 0; }
.schema-root-dropzone {
border: 2px dashed #ddd;
border-radius: 8px;
padding: 0.75rem;
text-align: center;
font-size: 0.8rem;
color: #bbb;
margin-top: 0.5rem;
transition: border-color 0.15s, background 0.15s, color 0.15s;
}
.schema-root-dropzone.active {
border-color: #4a90d9;
background: #f0f7ff;
color: #4a90d9;
}
/* Thumbnail in der Sammlung */ /* Thumbnail in der Sammlung */
.schema-thumb { .schema-thumb {
width: 40px; width: 40px;