fix(web): use plugin icon in sidebar, disable text selection on entries

- Replace hardcoded Puzzle/LayoutDashboard icons with actual plugin icon
  image loaded from the plugin icon API endpoint
- Add select-none to all plugin page sidebar entries to prevent
  accidental text selection
- Add pluginIconURL to PluginPageItem data model

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Junyan Qin
2026-04-25 20:10:35 +08:00
parent 5f3cecfbe2
commit 2c28635a39
2 changed files with 29 additions and 8 deletions

View File

@@ -92,7 +92,6 @@ import {
} from '@/components/ui/popover'; } from '@/components/ui/popover';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useSidebarData, SidebarEntityItem } from './SidebarDataContext'; import { useSidebarData, SidebarEntityItem } from './SidebarDataContext';
import { LayoutDashboard, Puzzle } from 'lucide-react';
// Compare two version strings, returns true if v1 > v2 // Compare two version strings, returns true if v1 > v2
function compareVersions(v1: string, v2: string): boolean { function compareVersions(v1: string, v2: string): boolean {
@@ -1057,12 +1056,16 @@ function PluginPagesNav() {
// Group pages by plugin (author/name) // Group pages by plugin (author/name)
const grouped = new Map< const grouped = new Map<
string, string,
{ label: string; pages: typeof pluginPages } { label: string; iconURL: string; pages: typeof pluginPages }
>(); >();
for (const page of pluginPages) { for (const page of pluginPages) {
const key = `${page.pluginAuthor}/${page.pluginName}`; const key = `${page.pluginAuthor}/${page.pluginName}`;
if (!grouped.has(key)) { if (!grouped.has(key)) {
grouped.set(key, { label: page.pluginLabel, pages: [] }); grouped.set(key, {
label: page.pluginLabel,
iconURL: page.pluginIconURL,
pages: [],
});
} }
grouped.get(key)!.pages.push(page); grouped.get(key)!.pages.push(page);
} }
@@ -1075,9 +1078,20 @@ function PluginPagesNav() {
<SidebarGroupContent> <SidebarGroupContent>
<SidebarMenu> <SidebarMenu>
{Array.from(grouped.entries()).map( {Array.from(grouped.entries()).map(
([pluginKey, { label, pages }]) => { ([pluginKey, { label, iconURL, pages }]) => {
const hasActivePage = pages.some((p) => p.id === currentId); const hasActivePage = pages.some((p) => p.id === currentId);
const pluginIcon = (
<img
src={iconURL}
alt=""
className="size-4 rounded-sm object-cover shrink-0"
onError={(e) => {
(e.target as HTMLImageElement).style.display = 'none';
}}
/>
);
// Single page — render directly without nesting // Single page — render directly without nesting
if (pages.length === 1) { if (pages.length === 1) {
const page = pages[0]; const page = pages[0];
@@ -1089,8 +1103,9 @@ function PluginPagesNav() {
isActive={isActive} isActive={isActive}
tooltip={page.name} tooltip={page.name}
onClick={() => navigate(route)} onClick={() => navigate(route)}
className="select-none"
> >
<LayoutDashboard className="size-4 text-blue-500" /> {pluginIcon}
<span>{page.name}</span> <span>{page.name}</span>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
@@ -1106,8 +1121,11 @@ function PluginPagesNav() {
> >
<SidebarMenuItem> <SidebarMenuItem>
<CollapsibleTrigger asChild> <CollapsibleTrigger asChild>
<SidebarMenuButton tooltip={label}> <SidebarMenuButton
<Puzzle className="size-4 text-blue-500" /> tooltip={label}
className="select-none"
>
{pluginIcon}
<span>{label}</span> <span>{label}</span>
<ChevronRight className="ml-auto size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" /> <ChevronRight className="ml-auto size-4 transition-transform duration-200 group-data-[state=open]/collapsible:rotate-90" />
</SidebarMenuButton> </SidebarMenuButton>
@@ -1122,6 +1140,7 @@ function PluginPagesNav() {
<SidebarMenuSubButton <SidebarMenuSubButton
isActive={isActive} isActive={isActive}
onClick={() => navigate(route)} onClick={() => navigate(route)}
className="select-none"
> >
<span>{page.name}</span> <span>{page.name}</span>
</SidebarMenuSubButton> </SidebarMenuSubButton>

View File

@@ -38,9 +38,10 @@ export interface PluginPageItem {
pluginAuthor: string; pluginAuthor: string;
pluginName: string; pluginName: string;
pluginLabel: string; // human-readable plugin display name pluginLabel: string; // human-readable plugin display name
pluginIconURL: string; // plugin icon URL
pageId: string; pageId: string;
path: string; // asset path (HTML file) path: string; // asset path (HTML file)
icon?: string; // optional icon name icon?: string; // optional per-page icon name from page manifest
} }
// Entity lists and refresh functions exposed via context // Entity lists and refresh functions exposed via context
@@ -205,6 +206,7 @@ export function SidebarDataProvider({
pluginAuthor: author, pluginAuthor: author,
pluginName: name, pluginName: name,
pluginLabel: label, pluginLabel: label,
pluginIconURL: httpClient.getPluginIconURL(author, name),
pageId: page.id, pageId: page.id,
path: page.path, path: page.path,
icon: page.icon, icon: page.icon,