feat: group icons by category in edit modal, reorder categories, mobile padding
All checks were successful
CI / update (push) Successful in 3m43s
All checks were successful
CI / update (push) Successful in 3m43s
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user