feat: group icons by category in edit modal, reorder categories, mobile padding
All checks were successful
CI / update (push) Successful in 3m43s

This commit is contained in:
2026-04-08 09:13:50 +02:00
parent ca5a2f67c5
commit 565b35154f

View File

@@ -8,6 +8,7 @@
import { slide } from 'svelte/transition'; import { slide } from 'svelte/transition';
import { SvelteSet } from 'svelte/reactivity'; import { SvelteSet } from 'svelte/reactivity';
import catalogData from '$lib/data/shoppingCatalog.json'; import catalogData from '$lib/data/shoppingCatalog.json';
import iconCategoriesData from '$lib/data/shoppingIconCategories.json';
import { Share2, X, Copy, Check } from '@lucide/svelte'; import { Share2, X, Copy, Check } from '@lucide/svelte';
@@ -167,11 +168,27 @@
let editSaving = $state(false); let editSaving = $state(false);
const allIcons = Object.entries(/** @type {Record<string, string>} */ (catalogData)); const allIcons = Object.entries(/** @type {Record<string, string>} */ (catalogData));
const iconCategories = /** @type {Record<string, string>} */ (iconCategoriesData);
let filteredIcons = $derived( /** Icons grouped by category, ordered by SHOPPING_CATEGORIES */
const iconsByCategory = (() => {
/** @type {Map<string, [string, string][]>} */
const groups = new Map();
for (const cat of SHOPPING_CATEGORIES) groups.set(cat, []);
for (const [name, file] of allIcons) {
const cat = iconCategories[name] || 'Sonstiges';
if (!groups.has(cat)) groups.set(cat, []);
groups.get(cat)?.push([name, file]);
}
return [...groups.entries()].filter(([, icons]) => icons.length > 0);
})();
let filteredIconGroups = $derived(
iconSearch.trim() iconSearch.trim()
? allIcons.filter(([name]) => name.includes(iconSearch.toLowerCase())) ? iconsByCategory
: allIcons .map(([cat, icons]) => /** @type {[string, [string,string][]]} */ ([cat, icons.filter(([name]) => name.includes(iconSearch.toLowerCase()))]))
.filter(([, icons]) => icons.length > 0)
: iconsByCategory
); );
/** @param {import('$lib/js/shoppingSync.svelte').ShoppingItem} item */ /** @param {import('$lib/js/shoppingSync.svelte').ShoppingItem} item */
@@ -446,15 +463,23 @@
<input bind:value={iconSearch} type="text" placeholder="Icon suchen..." /> <input bind:value={iconSearch} type="text" placeholder="Icon suchen..." />
</div> </div>
<div class="icon-picker"> <div class="icon-picker">
{#each filteredIcons as [name, file]} {#each filteredIconGroups as [cat, icons]}
<button {@const meta = categoryMeta[cat] || categoryMeta['Sonstiges']}
class="icon-option" <div class="icon-group">
class:selected={editIcon === file} <span class="icon-group-label" style="color: {meta.color}">{cat}</span>
onclick={() => { editIcon = file; }} <div class="icon-group-grid">
title={name} {#each icons as [name, file]}
> <button
<img src="https://bocken.org/static/shopping-icons/{file}.png" alt={name} /> class="icon-option"
</button> class:selected={editIcon === file}
onclick={() => { editIcon = file; }}
title={name}
>
<img src="https://bocken.org/static/shopping-icons/{file}.png" alt={name} />
</button>
{/each}
</div>
</div>
{/each} {/each}
</div> </div>
@@ -891,14 +916,25 @@
} }
.icon-picker { .icon-picker {
display: grid; display: flex;
grid-template-columns: repeat(auto-fill, minmax(48px, 1fr)); flex-direction: column;
gap: 0.35rem; gap: 0.5rem;
max-height: 200px; max-height: 250px;
overflow-y: auto; overflow-y: auto;
margin-bottom: 1.25rem; margin-bottom: 1.25rem;
padding: 0.25rem; padding: 0.25rem;
} }
.icon-group-label {
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.03em;
}
.icon-group-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(42px, 1fr));
gap: 0.3rem;
}
.icon-option { .icon-option {
aspect-ratio: 1; aspect-ratio: 1;
display: flex; display: flex;
@@ -1117,4 +1153,9 @@
z-index: 200; z-index: 200;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
} }
@media (max-width: 500px) {
.edit-backdrop { padding: 0.5rem; }
.edit-modal { padding: 1rem 0.75rem; }
}
</style> </style>