mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-02 03:55:55 +00:00
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:
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user