feat: add Tools-2 category + collapsible sidebar categories
Deploy Dashboard (Dev) / deploy-dev (push) Successful
Deploy Dashboard (Dev) / deploy-dev (push) Successful
This commit is contained in:
@@ -44,19 +44,24 @@
|
|||||||
{ label: "Speedtest", remoteHref: "https://speed.jimmygan.com:8443", icon: "speedtest", external: true },
|
{ label: "Speedtest", remoteHref: "https://speed.jimmygan.com:8443", icon: "speedtest", external: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const defaultTools2 = [];
|
||||||
|
|
||||||
function itemKey(item) { return item.id || item.label; }
|
function itemKey(item) { return item.id || item.label; }
|
||||||
|
|
||||||
// All items indexed by key for cross-category lookup
|
// All items indexed by key for cross-category lookup
|
||||||
const allItems = new Map([...defaultLinks, ...defaultMedia, ...defaultTools].map(i => [itemKey(i), i]));
|
const allItems = new Map([...defaultLinks, ...defaultMedia, ...defaultTools, ...defaultTools2].map(i => [itemKey(i), i]));
|
||||||
|
|
||||||
// Build category lists from saved order, supporting cross-category moves
|
// Build category lists from saved order, supporting cross-category moves
|
||||||
|
const categories = ["main", "media", "tools", "tools2"];
|
||||||
|
const categoryDefaults = { main: defaultLinks, media: defaultMedia, tools: defaultTools, tools2: defaultTools2 };
|
||||||
|
|
||||||
function buildLists(savedOrder) {
|
function buildLists(savedOrder) {
|
||||||
if (!savedOrder) return { main: [...defaultLinks], media: [...defaultMedia], tools: [...defaultTools] };
|
if (!savedOrder) return { main: [...defaultLinks], media: [...defaultMedia], tools: [...defaultTools], tools2: [...defaultTools2] };
|
||||||
|
|
||||||
const used = new Set();
|
const used = new Set();
|
||||||
const result = { main: [], media: [], tools: [] };
|
const result = { main: [], media: [], tools: [], tools2: [] };
|
||||||
|
|
||||||
for (const cat of ["main", "media", "tools"]) {
|
for (const cat of categories) {
|
||||||
const keys = savedOrder[cat] || [];
|
const keys = savedOrder[cat] || [];
|
||||||
for (const k of keys) {
|
for (const k of keys) {
|
||||||
if (allItems.has(k) && !used.has(k)) {
|
if (allItems.has(k) && !used.has(k)) {
|
||||||
@@ -67,9 +72,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Append any new items not in saved order to their default category
|
// Append any new items not in saved order to their default category
|
||||||
const defaults = { main: defaultLinks, media: defaultMedia, tools: defaultTools };
|
for (const cat of categories) {
|
||||||
for (const cat of ["main", "media", "tools"]) {
|
for (const item of categoryDefaults[cat]) {
|
||||||
for (const item of defaults[cat]) {
|
|
||||||
if (!used.has(itemKey(item))) {
|
if (!used.has(itemKey(item))) {
|
||||||
result[cat].push(item);
|
result[cat].push(item);
|
||||||
used.add(itemKey(item));
|
used.add(itemKey(item));
|
||||||
@@ -84,6 +88,10 @@
|
|||||||
let links = $derived(categoryLists.main);
|
let links = $derived(categoryLists.main);
|
||||||
let media = $derived(categoryLists.media);
|
let media = $derived(categoryLists.media);
|
||||||
let tools = $derived(categoryLists.tools);
|
let tools = $derived(categoryLists.tools);
|
||||||
|
let tools2 = $derived(categoryLists.tools2);
|
||||||
|
|
||||||
|
// Collapsed state for categories (persisted in sidebarOrder)
|
||||||
|
let collapsed = $state(sidebarOrder?.collapsed ? { ...sidebarOrder.collapsed } : {});
|
||||||
|
|
||||||
// Drag-and-drop state
|
// Drag-and-drop state
|
||||||
let dragCategory = $state(null);
|
let dragCategory = $state(null);
|
||||||
@@ -116,13 +124,15 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (dragCategory === null) { handleDragEnd(); return; }
|
if (dragCategory === null) { handleDragEnd(); return; }
|
||||||
if (dragCategory === category && dragIndex === index) { handleDragEnd(); return; }
|
if (dragCategory === category && dragIndex === index) { handleDragEnd(); return; }
|
||||||
const lists = { main: [...links], media: [...media], tools: [...tools] };
|
const lists = { main: [...links], media: [...media], tools: [...tools], tools2: [...tools2] };
|
||||||
const [moved] = lists[dragCategory].splice(dragIndex, 1);
|
const [moved] = lists[dragCategory].splice(dragIndex, 1);
|
||||||
lists[category].splice(index, 0, moved);
|
lists[category].splice(index, 0, moved);
|
||||||
const newOrder = {
|
const newOrder = {
|
||||||
main: lists.main.map(itemKey),
|
main: lists.main.map(itemKey),
|
||||||
media: lists.media.map(itemKey),
|
media: lists.media.map(itemKey),
|
||||||
tools: lists.tools.map(itemKey),
|
tools: lists.tools.map(itemKey),
|
||||||
|
tools2: lists.tools2.map(itemKey),
|
||||||
|
collapsed,
|
||||||
};
|
};
|
||||||
onOrderChange(newOrder);
|
onOrderChange(newOrder);
|
||||||
handleDragEnd();
|
handleDragEnd();
|
||||||
@@ -139,13 +149,15 @@
|
|||||||
function handleCategoryDrop(category, e) {
|
function handleCategoryDrop(category, e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (dragCategory === null) { handleDragEnd(); return; }
|
if (dragCategory === null) { handleDragEnd(); return; }
|
||||||
const lists = { main: [...links], media: [...media], tools: [...tools] };
|
const lists = { main: [...links], media: [...media], tools: [...tools], tools2: [...tools2] };
|
||||||
const [moved] = lists[dragCategory].splice(dragIndex, 1);
|
const [moved] = lists[dragCategory].splice(dragIndex, 1);
|
||||||
lists[category].push(moved);
|
lists[category].push(moved);
|
||||||
const newOrder = {
|
const newOrder = {
|
||||||
main: lists.main.map(itemKey),
|
main: lists.main.map(itemKey),
|
||||||
media: lists.media.map(itemKey),
|
media: lists.media.map(itemKey),
|
||||||
tools: lists.tools.map(itemKey),
|
tools: lists.tools.map(itemKey),
|
||||||
|
tools2: lists.tools2.map(itemKey),
|
||||||
|
collapsed,
|
||||||
};
|
};
|
||||||
onOrderChange(newOrder);
|
onOrderChange(newOrder);
|
||||||
handleDragEnd();
|
handleDragEnd();
|
||||||
@@ -171,6 +183,18 @@
|
|||||||
if (dropCategory === category && index === dropIndex) return "border-t-2 border-primary-500";
|
if (dropCategory === category && index === dropIndex) return "border-t-2 border-primary-500";
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleCollapse(cat) {
|
||||||
|
collapsed[cat] = !collapsed[cat];
|
||||||
|
const newOrder = {
|
||||||
|
main: links.map(itemKey),
|
||||||
|
media: media.map(itemKey),
|
||||||
|
tools: tools.map(itemKey),
|
||||||
|
tools2: tools2.map(itemKey),
|
||||||
|
collapsed,
|
||||||
|
};
|
||||||
|
onOrderChange(newOrder);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet iconSvg(icon)}
|
{#snippet iconSvg(icon)}
|
||||||
@@ -232,11 +256,23 @@
|
|||||||
|
|
||||||
<!-- Nav Links -->
|
<!-- Nav Links -->
|
||||||
<nav class="flex-1 px-3 mt-2 overflow-y-auto">
|
<nav class="flex-1 px-3 mt-2 overflow-y-auto">
|
||||||
{#each [["main", "Main", links], ["media", "Media", media], ["tools", "Tools", tools]] as [cat, label, items], ci}
|
{#each [["main", "Main", links], ["media", "Media", media], ["tools", "Tools", tools], ["tools2", "Tools 2", tools2]] as [cat, label, items], ci}
|
||||||
{#if ci > 0}
|
{#if ci > 0}
|
||||||
<div class="my-4 border-t border-surface-200 dark:border-surface-700"></div>
|
<div class="my-4 border-t border-surface-200 dark:border-surface-700"></div>
|
||||||
{/if}
|
{/if}
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-wider text-surface-400 px-3 mb-2" ondragover={(e) => handleCategoryDragOver(cat, e)} ondrop={(e) => handleCategoryDrop(cat, e)}>{label}</p>
|
<button
|
||||||
|
class="w-full text-left text-[10px] font-semibold uppercase tracking-wider text-surface-400 px-3 mb-2 flex items-center justify-between {cat !== 'main' ? 'cursor-pointer' : ''}"
|
||||||
|
ondragover={(e) => handleCategoryDragOver(cat, e)}
|
||||||
|
ondrop={(e) => handleCategoryDrop(cat, e)}
|
||||||
|
onclick={() => cat !== 'main' && toggleCollapse(cat)}
|
||||||
|
disabled={cat === 'main'}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
{#if cat !== "main"}
|
||||||
|
<svg class="w-3 h-3 opacity-30 hover:opacity-50 transition-all duration-150 {collapsed[cat] ? '-rotate-90' : ''}" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" /></svg>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
{#if cat === "main" || !collapsed[cat]}
|
||||||
{#each items as item, i}
|
{#each items as item, i}
|
||||||
{#if canAccess(item.id)}
|
{#if canAccess(item.id)}
|
||||||
{#if item.external || item.remoteHref || item.port}
|
{#if item.external || item.remoteHref || item.port}
|
||||||
@@ -277,6 +313,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user