feat: enable Immich ML, dark mode fixes, sidebar improvements #6
@@ -44,27 +44,44 @@
|
|||||||
{ label: "Speedtest", remoteHref: "https://speed.jimmygan.com:8443", icon: "speedtest", external: true },
|
{ label: "Speedtest", remoteHref: "https://speed.jimmygan.com:8443", icon: "speedtest", external: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
// Item key: use id if present, otherwise label
|
// All items indexed by key for cross-category lookup
|
||||||
function itemKey(item) { return item.id || item.label; }
|
const allItems = new Map([...defaultLinks, ...defaultMedia, ...defaultTools].map(i => [itemKey(i), i]));
|
||||||
|
|
||||||
// Apply saved order: saved items first (in order), then new items appended
|
// Build category lists from saved order, supporting cross-category moves
|
||||||
function applyOrder(items, savedKeys) {
|
function buildLists(savedOrder) {
|
||||||
if (!savedKeys || !savedKeys.length) return [...items];
|
if (!savedOrder) return { main: [...defaultLinks], media: [...defaultMedia], tools: [...defaultTools] };
|
||||||
const byKey = new Map(items.map(i => [itemKey(i), i]));
|
|
||||||
const ordered = [];
|
const used = new Set();
|
||||||
for (const k of savedKeys) {
|
const result = { main: [], media: [], tools: [] };
|
||||||
if (byKey.has(k)) { ordered.push(byKey.get(k)); byKey.delete(k); }
|
|
||||||
|
for (const cat of ["main", "media", "tools"]) {
|
||||||
|
const keys = savedOrder[cat] || [];
|
||||||
|
for (const k of keys) {
|
||||||
|
if (allItems.has(k) && !used.has(k)) {
|
||||||
|
result[cat].push(allItems.get(k));
|
||||||
|
used.add(k);
|
||||||
}
|
}
|
||||||
// Append items not in saved order
|
|
||||||
for (const i of items) {
|
|
||||||
if (byKey.has(itemKey(i))) ordered.push(i);
|
|
||||||
}
|
}
|
||||||
return ordered;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let links = $derived(applyOrder(defaultLinks, sidebarOrder?.main));
|
// Append any new items not in saved order to their default category
|
||||||
let media = $derived(applyOrder(defaultMedia, sidebarOrder?.media));
|
const defaults = { main: defaultLinks, media: defaultMedia, tools: defaultTools };
|
||||||
let tools = $derived(applyOrder(defaultTools, sidebarOrder?.tools));
|
for (const cat of ["main", "media", "tools"]) {
|
||||||
|
for (const item of defaults[cat]) {
|
||||||
|
if (!used.has(itemKey(item))) {
|
||||||
|
result[cat].push(item);
|
||||||
|
used.add(itemKey(item));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
let categoryLists = $derived(buildLists(sidebarOrder));
|
||||||
|
let links = $derived(categoryLists.main);
|
||||||
|
let media = $derived(categoryLists.media);
|
||||||
|
let tools = $derived(categoryLists.tools);
|
||||||
|
|
||||||
// Drag-and-drop state
|
// Drag-and-drop state
|
||||||
let dragCategory = $state(null);
|
let dragCategory = $state(null);
|
||||||
@@ -154,166 +171,118 @@
|
|||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#snippet iconSvg(icon)}
|
||||||
|
<span class="w-5 h-5 flex items-center justify-center">
|
||||||
|
{#if icon === "grid"}
|
||||||
|
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
|
||||||
|
{:else if icon === "box"}
|
||||||
|
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
|
||||||
|
{:else if icon === "folder"}
|
||||||
|
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
|
||||||
|
{:else if icon === "terminal"}
|
||||||
|
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||||
|
{:else if icon === "shield"}
|
||||||
|
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
|
||||||
|
{:else if icon === "music"}
|
||||||
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2z" /></svg>
|
||||||
|
{:else if icon === "play"}
|
||||||
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
||||||
|
{:else if icon === "headphones"}
|
||||||
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M3 18v-6a9 9 0 0118 0v6M3 18a3 3 0 003 3h0a3 3 0 003-3v-2a3 3 0 00-3-3h0a3 3 0 00-3 3v2zm18 0a3 3 0 01-3 3h0a3 3 0 01-3-3v-2a3 3 0 013-3h0a3 3 0 013 3v2z" /></svg>
|
||||||
|
{:else if icon === "image"}
|
||||||
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
||||||
|
{:else if icon === "openclaw"}
|
||||||
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" /></svg>
|
||||||
|
{:else if icon === "chat"}
|
||||||
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a2 2 0 01-2-2v-1m0-3V6a2 2 0 012-2h8a2 2 0 012 2v3a2 2 0 01-2 2H9l-4 4V9z" /></svg>
|
||||||
|
{:else if icon === "git"}
|
||||||
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>
|
||||||
|
{:else if icon === "pdf"}
|
||||||
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
|
||||||
|
{:else if icon === "lock"}
|
||||||
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
|
||||||
|
{:else if icon === "speedtest"}
|
||||||
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
||||||
|
{:else if icon === "users"}
|
||||||
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
|
||||||
|
{:else if icon === "n8n"}
|
||||||
|
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" /><circle cx="8" cy="6" r="1.5" fill="currentColor"/><circle cx="16" cy="12" r="1.5" fill="currentColor"/><circle cx="10" cy="18" r="1.5" fill="currentColor"/></svg>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<!-- Mobile overlay -->
|
<!-- Mobile overlay -->
|
||||||
{#if sidebarOpen}
|
{#if sidebarOpen}
|
||||||
<button class="fixed inset-0 bg-black/40 z-40 md:hidden" onclick={() => sidebarOpen = false} aria-label="Close sidebar"></button>
|
<button class="fixed inset-0 bg-black/40 z-40 md:hidden" onclick={() => sidebarOpen = false} aria-label="Close sidebar"></button>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<aside class="fixed inset-y-0 left-0 z-50 w-[240px] h-screen flex flex-col shrink-0 border-r border-surface-200 bg-white transition-transform duration-200 md:static md:translate-x-0 {dark ? 'bg-surface-800 border-surface-700' : ''} {sidebarOpen ? 'translate-x-0' : '-translate-x-full'}">
|
<aside class="fixed inset-y-0 left-0 z-50 w-[240px] h-screen flex flex-col shrink-0 border-r border-surface-200 dark:border-surface-700 bg-white dark:bg-surface-800 transition-transform duration-200 md:static md:translate-x-0 {sidebarOpen ? 'translate-x-0' : '-translate-x-full'}">
|
||||||
<!-- Logo -->
|
<!-- Logo -->
|
||||||
<div class="px-5 py-5 flex items-center gap-3">
|
<div class="px-5 py-5 flex items-center gap-3">
|
||||||
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center shadow-md">
|
<div class="w-8 h-8 rounded-lg bg-gradient-to-br from-primary-500 to-primary-700 flex items-center justify-center shadow-md">
|
||||||
<svg class="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" /></svg>
|
<svg class="w-4 h-4 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2.5"><path stroke-linecap="round" stroke-linejoin="round" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2" /></svg>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-sm font-bold tracking-tight {dark ? 'text-white' : 'text-surface-900'}">NAS Dashboard</h1>
|
<h1 class="text-sm font-bold tracking-tight text-surface-900 dark:text-white">NAS Dashboard</h1>
|
||||||
<p class="text-[10px] text-surface-400 font-medium">DS224+ · Synology</p>
|
<p class="text-[10px] text-surface-400 font-medium">DS224+ · Synology</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 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">
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-wider text-surface-400 px-3 mb-2" ondragover={(e) => handleCategoryDragOver("main", e)} ondrop={(e) => handleCategoryDrop("main", e)}>Main</p>
|
{#each [["main", "Main", links], ["media", "Media", media], ["tools", "Tools", tools]] as [cat, label, items], ci}
|
||||||
{#each links as link, i}
|
{#if ci > 0}
|
||||||
{#if canAccess(link.id)}
|
<div class="my-4 border-t border-surface-200 dark:border-surface-700"></div>
|
||||||
<button
|
|
||||||
draggable="true"
|
|
||||||
ondragstart={(e) => handleDragStart("main", i, e)}
|
|
||||||
ondragover={(e) => handleDragOver("main", i, e)}
|
|
||||||
ondrop={(e) => handleDrop("main", i, e)}
|
|
||||||
ondragend={handleDragEnd}
|
|
||||||
class="w-full text-left px-3 py-2.5 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 transition-all duration-150 group
|
|
||||||
{page === link.id
|
|
||||||
? 'bg-primary-50 text-primary-700 shadow-sm ' + (dark ? 'bg-primary-900/30 text-primary-300' : '')
|
|
||||||
: 'text-surface-500 hover:bg-surface-100 hover:text-surface-700 ' + (dark ? 'hover:bg-surface-700 hover:text-surface-200 text-surface-400' : '')
|
|
||||||
} {dragClasses('main', i)}"
|
|
||||||
onclick={() => navigate(link.id)}
|
|
||||||
>
|
|
||||||
<svg class="w-3 h-3 opacity-0 group-hover:opacity-30 -ml-1 mr-0.5 shrink-0 cursor-grab" fill="currentColor" viewBox="0 0 24 24"><circle cx="9" cy="6" r="2"/><circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="9" cy="18" r="2"/><circle cx="15" cy="18" r="2"/></svg>
|
|
||||||
<span class="w-5 h-5 flex items-center justify-center">
|
|
||||||
{#if link.icon === "grid"}
|
|
||||||
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zm10 0a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
|
|
||||||
{:else if link.icon === "box"}
|
|
||||||
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
|
|
||||||
{:else if link.icon === "folder"}
|
|
||||||
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" /></svg>
|
|
||||||
{:else if link.icon === "terminal"}
|
|
||||||
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
|
||||||
{:else if link.icon === "shield"}
|
|
||||||
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg>
|
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
<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>
|
||||||
{link.label}
|
{#each items as item, i}
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<div class="my-4 border-t border-surface-200 {dark ? 'border-surface-700' : ''}"></div>
|
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-wider text-surface-400 px-3 mb-2" ondragover={(e) => handleCategoryDragOver("media", e)} ondrop={(e) => handleCategoryDrop("media", e)}>Media</p>
|
|
||||||
{#each media as svc, i}
|
|
||||||
<a
|
|
||||||
href={extHref(svc)}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
draggable="true"
|
|
||||||
ondragstart={(e) => handleDragStart("media", i, e)}
|
|
||||||
ondragover={(e) => handleDragOver("media", i, e)}
|
|
||||||
ondrop={(e) => handleDrop("media", i, e)}
|
|
||||||
ondragend={handleDragEnd}
|
|
||||||
class="w-full text-left px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 text-surface-500 hover:bg-surface-100 hover:text-surface-700 transition-all duration-150 group {dark ? 'hover:bg-surface-700 hover:text-surface-200 text-surface-400' : ''} {dragClasses('media', i)}"
|
|
||||||
>
|
|
||||||
<svg class="w-3 h-3 opacity-0 group-hover:opacity-30 -ml-1 mr-0.5 shrink-0 cursor-grab" fill="currentColor" viewBox="0 0 24 24"><circle cx="9" cy="6" r="2"/><circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="9" cy="18" r="2"/><circle cx="15" cy="18" r="2"/></svg>
|
|
||||||
<span class="w-5 h-5 flex items-center justify-center">
|
|
||||||
{#if svc.icon === "music"}
|
|
||||||
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M9 19V6l12-3v13M9 19c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2zm12-3c0 1.105-1.343 2-3 2s-3-.895-3-2 1.343-2 3-2 3 .895 3 2z" /></svg>
|
|
||||||
{:else if svc.icon === "play"}
|
|
||||||
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" /><path stroke-linecap="round" stroke-linejoin="round" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
|
|
||||||
{:else if svc.icon === "headphones"}
|
|
||||||
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M3 18v-6a9 9 0 0118 0v6M3 18a3 3 0 003 3h0a3 3 0 003-3v-2a3 3 0 00-3-3h0a3 3 0 00-3 3v2zm18 0a3 3 0 01-3 3h0a3 3 0 01-3-3v-2a3 3 0 013-3h0a3 3 0 013 3v2z" /></svg>
|
|
||||||
{:else if svc.icon === "image"}
|
|
||||||
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
{svc.label}
|
|
||||||
<svg class="w-3 h-3 ml-auto opacity-40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
|
|
||||||
</a>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<div class="my-4 border-t border-surface-200 {dark ? 'border-surface-700' : ''}"></div>
|
|
||||||
<p class="text-[10px] font-semibold uppercase tracking-wider text-surface-400 px-3 mb-2" ondragover={(e) => handleCategoryDragOver("tools", e)} ondrop={(e) => handleCategoryDrop("tools", e)}>Tools</p>
|
|
||||||
{#each tools as item, i}
|
|
||||||
{#if canAccess(item.id)}
|
{#if canAccess(item.id)}
|
||||||
{#if item.external || item.port}
|
{#if item.external || item.remoteHref || item.port}
|
||||||
<a
|
<a
|
||||||
href={extHref(item)}
|
href={extHref(item)}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
draggable="true"
|
draggable="true"
|
||||||
ondragstart={(e) => handleDragStart("tools", i, e)}
|
ondragstart={(e) => handleDragStart(cat, i, e)}
|
||||||
ondragover={(e) => handleDragOver("tools", i, e)}
|
ondragover={(e) => handleDragOver(cat, i, e)}
|
||||||
ondrop={(e) => handleDrop("tools", i, e)}
|
ondrop={(e) => handleDrop(cat, i, e)}
|
||||||
ondragend={handleDragEnd}
|
ondragend={handleDragEnd}
|
||||||
class="w-full text-left px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 text-surface-500 hover:bg-surface-100 hover:text-surface-700 transition-all duration-150 group {dark ? 'hover:bg-surface-700 hover:text-surface-200 text-surface-400' : ''} {dragClasses('tools', i)}"
|
class="w-full text-left px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 text-surface-500 hover:bg-surface-100 dark:hover:bg-surface-700 hover:text-surface-700 dark:hover:text-surface-200 dark:text-surface-400 transition-all duration-150 group {dragClasses(cat, i)}"
|
||||||
>
|
>
|
||||||
<svg class="w-3 h-3 opacity-0 group-hover:opacity-30 -ml-1 mr-0.5 shrink-0 cursor-grab" fill="currentColor" viewBox="0 0 24 24"><circle cx="9" cy="6" r="2"/><circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="9" cy="18" r="2"/><circle cx="15" cy="18" r="2"/></svg>
|
<svg class="w-3 h-3 opacity-0 group-hover:opacity-30 -ml-1 mr-0.5 shrink-0 cursor-grab" fill="currentColor" viewBox="0 0 24 24"><circle cx="9" cy="6" r="2"/><circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="9" cy="18" r="2"/><circle cx="15" cy="18" r="2"/></svg>
|
||||||
<span class="w-5 h-5 flex items-center justify-center">
|
{@render iconSvg(item.icon)}
|
||||||
{#if item.icon === "openclaw"}
|
|
||||||
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" /></svg>
|
|
||||||
{:else if item.icon === "chat"}
|
|
||||||
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a2 2 0 01-2-2v-1m0-3V6a2 2 0 012-2h8a2 2 0 012 2v3a2 2 0 01-2 2H9l-4 4V9z" /></svg>
|
|
||||||
{:else if item.icon === "git"}
|
|
||||||
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" /></svg>
|
|
||||||
{:else if item.icon === "pdf"}
|
|
||||||
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg>
|
|
||||||
{:else if item.icon === "lock"}
|
|
||||||
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
|
|
||||||
{:else if item.icon === "speedtest"}
|
|
||||||
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
|
||||||
{:else if item.icon === "users"}
|
|
||||||
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
|
|
||||||
{:else if item.icon === "n8n"}
|
|
||||||
<svg class="w-[16px] h-[16px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16" /><circle cx="8" cy="6" r="1.5" fill="currentColor"/><circle cx="16" cy="12" r="1.5" fill="currentColor"/><circle cx="10" cy="18" r="1.5" fill="currentColor"/></svg>
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
{item.label}
|
{item.label}
|
||||||
<svg class="w-3 h-3 ml-auto opacity-40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
|
<svg class="w-3 h-3 ml-auto opacity-40" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" /></svg>
|
||||||
</a>
|
</a>
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
<button
|
||||||
draggable="true"
|
draggable="true"
|
||||||
ondragstart={(e) => handleDragStart("tools", i, e)}
|
ondragstart={(e) => handleDragStart(cat, i, e)}
|
||||||
ondragover={(e) => handleDragOver("tools", i, e)}
|
ondragover={(e) => handleDragOver(cat, i, e)}
|
||||||
ondrop={(e) => handleDrop("tools", i, e)}
|
ondrop={(e) => handleDrop(cat, i, e)}
|
||||||
ondragend={handleDragEnd}
|
ondragend={handleDragEnd}
|
||||||
class="w-full text-left px-3 py-2.5 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 transition-all duration-150 group
|
class="w-full text-left px-3 py-2.5 rounded-lg text-[13px] font-medium flex items-center gap-2.5 mb-0.5 transition-all duration-150 group
|
||||||
{page === item.id
|
{page === item.id
|
||||||
? 'bg-primary-50 text-primary-700 shadow-sm ' + (dark ? 'bg-primary-900/30 text-primary-300' : '')
|
? 'bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 shadow-sm'
|
||||||
: 'text-surface-500 hover:bg-surface-100 hover:text-surface-700 ' + (dark ? 'hover:bg-surface-700 hover:text-surface-200 text-surface-400' : '')
|
: 'text-surface-500 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 hover:text-surface-700 dark:hover:text-surface-200'
|
||||||
} {dragClasses('tools', i)}"
|
} {dragClasses(cat, i)}"
|
||||||
onclick={() => navigate(item.id)}
|
onclick={() => navigate(item.id)}
|
||||||
>
|
>
|
||||||
<svg class="w-3 h-3 opacity-0 group-hover:opacity-30 -ml-1 mr-0.5 shrink-0 cursor-grab" fill="currentColor" viewBox="0 0 24 24"><circle cx="9" cy="6" r="2"/><circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="9" cy="18" r="2"/><circle cx="15" cy="18" r="2"/></svg>
|
<svg class="w-3 h-3 opacity-0 group-hover:opacity-30 -ml-1 mr-0.5 shrink-0 cursor-grab" fill="currentColor" viewBox="0 0 24 24"><circle cx="9" cy="6" r="2"/><circle cx="15" cy="6" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="9" cy="18" r="2"/><circle cx="15" cy="18" r="2"/></svg>
|
||||||
<span class="w-5 h-5 flex items-center justify-center">
|
{@render iconSvg(item.icon)}
|
||||||
{#if item.icon === "openclaw"}
|
|
||||||
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 10h.01M12 10h.01M16 10h.01M9 16H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-5l-5 5v-5z" /></svg>
|
|
||||||
{:else if item.icon === "chat"}
|
|
||||||
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a2 2 0 01-2-2v-1m0-3V6a2 2 0 012-2h8a2 2 0 012 2v3a2 2 0 01-2 2H9l-4 4V9z" /></svg>
|
|
||||||
{:else if item.icon === "git"}
|
|
||||||
<svg class="w-[18px] h-[18px]" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M8 9l3 3-3 3m5 0h3M5 20h14a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
|
|
||||||
{/if}
|
|
||||||
</span>
|
|
||||||
{item.label}
|
{item.label}
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
{/each}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Bottom actions -->
|
<!-- Bottom actions -->
|
||||||
<div class="px-4 pb-4 pt-2 border-t border-surface-200 {dark ? 'border-surface-700' : ''}">
|
<div class="px-4 pb-4 pt-2 border-t border-surface-200 dark:border-surface-700">>
|
||||||
{#if userRole === "admin"}
|
{#if userRole === "admin"}
|
||||||
<button
|
<button
|
||||||
class="w-full px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 text-surface-500 hover:bg-surface-100 transition-all duration-150 mb-1 {dark ? 'hover:bg-surface-700 text-surface-400' : ''} {page === 'settings' ? 'bg-primary-50 text-primary-700 shadow-sm ' + (dark ? 'bg-primary-900/30 text-primary-300' : '') : ''}"
|
class="w-full px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 text-surface-500 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 transition-all duration-150 mb-1 {page === 'settings' ? 'bg-primary-50 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 shadow-sm' : ''}"
|
||||||
onclick={() => navigate('settings')}
|
onclick={() => navigate('settings')}
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.066 2.573c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.573 1.066c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.066-2.573c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
|
||||||
@@ -322,7 +291,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
onclick={toggleTheme}
|
onclick={toggleTheme}
|
||||||
class="w-full px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 text-surface-500 hover:bg-surface-100 transition-all duration-150 mb-1 {dark ? 'hover:bg-surface-700 text-surface-400' : ''}"
|
class="w-full px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 text-surface-500 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 transition-all duration-150 mb-1"
|
||||||
>
|
>
|
||||||
{#if dark}
|
{#if dark}
|
||||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
|
||||||
@@ -334,7 +303,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={logout}
|
onclick={logout}
|
||||||
class="w-full px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 text-surface-500 hover:bg-surface-100 transition-all duration-150 hover:text-red-600 {dark ? 'hover:bg-surface-700 text-surface-400 hover:text-red-400' : ''}"
|
class="w-full px-3 py-2 rounded-lg text-[13px] font-medium flex items-center gap-2.5 text-surface-500 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-700 transition-all duration-150 hover:text-red-600 dark:hover:text-red-400"
|
||||||
>
|
>
|
||||||
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
|
<svg class="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8"><path stroke-linecap="round" stroke-linejoin="round" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
|
||||||
Sign out
|
Sign out
|
||||||
|
|||||||
Reference in New Issue
Block a user