mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-07-16 09:26:07 +00:00
feat(extensions): mobile-friendly layout for extensions and add-extension pages
- Stack the extensions page header vertically on small screens, let the filter Tabs scroll horizontally if they overflow, hide the debug button label below sm and let the install/debug controls wrap. - Constrain the debug popover and its inputs to the viewport width so they no longer overflow on phone-sized screens. - Drop the card grid from a fixed 30rem column to a min(100%, 22rem) column at base / 28rem at sm, and reduce the gap, so cards render cleanly at 360px+ widths in both flat and grouped views. - Make the add-extension header actions wrap on lg- viewports and the install dialog responsive instead of a hard 500px box. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -56,7 +56,9 @@ function AddExtensionContent() {
|
|||||||
} = usePluginInstallTasks();
|
} = usePluginInstallTasks();
|
||||||
const [modalOpen, setModalOpen] = useState(false);
|
const [modalOpen, setModalOpen] = useState(false);
|
||||||
const [installInfo, setInstallInfo] = useState<Record<string, string>>({});
|
const [installInfo, setInstallInfo] = useState<Record<string, string>>({});
|
||||||
const [installExtensionType, setInstallExtensionType] = useState<'plugin' | 'mcp' | 'skill'>('plugin');
|
const [installExtensionType, setInstallExtensionType] = useState<
|
||||||
|
'plugin' | 'mcp' | 'skill'
|
||||||
|
>('plugin');
|
||||||
const [pluginInstallStatus, setPluginInstallStatus] =
|
const [pluginInstallStatus, setPluginInstallStatus] =
|
||||||
useState<PluginInstallStatus>(PluginInstallStatus.ASK_CONFIRM);
|
useState<PluginInstallStatus>(PluginInstallStatus.ASK_CONFIRM);
|
||||||
const [installError, setInstallError] = useState<string | null>(null);
|
const [installError, setInstallError] = useState<string | null>(null);
|
||||||
@@ -120,29 +122,38 @@ function AddExtensionContent() {
|
|||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
className="px-4 py-2 cursor-pointer"
|
className="px-3 sm:px-4 py-2 cursor-pointer flex-shrink-0"
|
||||||
onClick={() => navigate('/home/mcp?id=new')}
|
onClick={() => navigate('/home/mcp?id=new')}
|
||||||
>
|
>
|
||||||
<PlusIcon className="w-4 h-4" />
|
<PlusIcon className="w-4 h-4" />
|
||||||
{t('mcp.addMCPServer')}
|
<span className="whitespace-nowrap">{t('mcp.addMCPServer')}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="default" className="px-4 py-2 cursor-pointer">
|
<Button
|
||||||
|
variant="default"
|
||||||
|
className="px-3 sm:px-4 py-2 cursor-pointer flex-shrink-0"
|
||||||
|
>
|
||||||
<PlusIcon className="w-4 h-4" />
|
<PlusIcon className="w-4 h-4" />
|
||||||
{t('skills.addSkill')}
|
<span className="whitespace-nowrap">{t('skills.addSkill')}</span>
|
||||||
<ChevronDownIcon className="ml-2 w-4 h-4" />
|
<ChevronDownIcon className="ml-1 sm:ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => navigate('/home/skills?action=create')}>
|
<DropdownMenuItem
|
||||||
|
onClick={() => navigate('/home/skills?action=create')}
|
||||||
|
>
|
||||||
{t('skills.createManually')}
|
{t('skills.createManually')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => navigate('/home/skills?action=upload')}>
|
<DropdownMenuItem
|
||||||
|
onClick={() => navigate('/home/skills?action=upload')}
|
||||||
|
>
|
||||||
{t('skills.uploadZip')}
|
{t('skills.uploadZip')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => navigate('/home/skills?action=github')}>
|
<DropdownMenuItem
|
||||||
|
onClick={() => navigate('/home/skills?action=github')}
|
||||||
|
>
|
||||||
{t('skills.importFromGithub')}
|
{t('skills.importFromGithub')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@@ -150,17 +161,24 @@ function AddExtensionContent() {
|
|||||||
|
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button variant="default" className="px-4 py-2 cursor-pointer">
|
<Button
|
||||||
|
variant="default"
|
||||||
|
className="px-3 sm:px-4 py-2 cursor-pointer flex-shrink-0"
|
||||||
|
>
|
||||||
<PlusIcon className="w-4 h-4" />
|
<PlusIcon className="w-4 h-4" />
|
||||||
{t('plugins.newPlugin')}
|
<span className="whitespace-nowrap">{t('plugins.newPlugin')}</span>
|
||||||
<ChevronDownIcon className="ml-2 w-4 h-4" />
|
<ChevronDownIcon className="ml-1 sm:ml-2 w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={() => navigate('/home/add-plugin?action=github')}>
|
<DropdownMenuItem
|
||||||
|
onClick={() => navigate('/home/add-plugin?action=github')}
|
||||||
|
>
|
||||||
{t('plugins.installFromGithub')}
|
{t('plugins.installFromGithub')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => navigate('/home/add-plugin?action=upload')}>
|
<DropdownMenuItem
|
||||||
|
onClick={() => navigate('/home/add-plugin?action=upload')}
|
||||||
|
>
|
||||||
{t('plugins.uploadLocal')}
|
{t('plugins.uploadLocal')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@@ -172,7 +190,10 @@ function AddExtensionContent() {
|
|||||||
<>
|
<>
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<div className="flex-1 overflow-y-auto">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<MarketPage installPlugin={handleInstallPlugin} headerActions={extensionActions} />
|
<MarketPage
|
||||||
|
installPlugin={handleInstallPlugin}
|
||||||
|
headerActions={extensionActions}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -185,7 +206,7 @@ function AddExtensionContent() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<DialogContent className="w-[500px] max-h-[80vh] p-6 bg-white dark:bg-[#1a1a1e] overflow-y-auto">
|
<DialogContent className="w-[calc(100vw-2rem)] sm:w-[500px] sm:max-w-[500px] max-h-[80vh] p-4 sm:p-6 overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle className="flex items-center gap-4">
|
<DialogTitle className="flex items-center gap-4">
|
||||||
<Download className="size-6" />
|
<Download className="size-6" />
|
||||||
@@ -238,4 +259,4 @@ function AddExtensionContent() {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ const PluginInstalledComponent = forwardRef<
|
|||||||
({group.items.length})
|
({group.items.length})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-[0.8rem] grid gap-8 [grid-template-columns:repeat(auto-fill,minmax(30rem,1fr))] items-start">
|
<div className="px-[0.8rem] grid gap-5 sm:gap-8 [grid-template-columns:repeat(auto-fill,minmax(min(100%,22rem),1fr))] sm:[grid-template-columns:repeat(auto-fill,minmax(min(100%,28rem),1fr))] items-start">
|
||||||
{group.items.map((vo, index) => (
|
{group.items.map((vo, index) => (
|
||||||
<div key={vo.id || index}>
|
<div key={vo.id || index}>
|
||||||
<ExtensionCardComponent
|
<ExtensionCardComponent
|
||||||
|
|||||||
@@ -14,10 +14,7 @@ import {
|
|||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from '@/components/ui/popover';
|
} from '@/components/ui/popover';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import {
|
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
|
||||||
ToggleGroup,
|
|
||||||
ToggleGroupItem,
|
|
||||||
} from '@/components/ui/toggle-group';
|
|
||||||
import {
|
import {
|
||||||
Search,
|
Search,
|
||||||
Wrench,
|
Wrench,
|
||||||
@@ -153,39 +150,54 @@ function MarketPageContent({
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const transformMCPToVO = useCallback((mcp: any): PluginMarketCardVO => {
|
const transformMCPToVO = useCallback(
|
||||||
return new PluginMarketCardVO({
|
(mcp: any): PluginMarketCardVO => {
|
||||||
pluginId: mcp.author + ' / ' + mcp.name,
|
return new PluginMarketCardVO({
|
||||||
author: mcp.author,
|
pluginId: mcp.author + ' / ' + mcp.name,
|
||||||
pluginName: mcp.name,
|
author: mcp.author,
|
||||||
label: extractI18nObject(mcp.label),
|
pluginName: mcp.name,
|
||||||
description: extractI18nObject(mcp.description) || t('market.noDescription'),
|
label: extractI18nObject(mcp.label),
|
||||||
installCount: mcp.install_count || 0,
|
description:
|
||||||
iconURL: mcp.icon || getCloudServiceClientSync().getPluginIconURL(mcp.author, mcp.name),
|
extractI18nObject(mcp.description) || t('market.noDescription'),
|
||||||
githubURL: mcp.repository,
|
installCount: mcp.install_count || 0,
|
||||||
version: mcp.latest_version,
|
iconURL:
|
||||||
components: mcp.components || {},
|
mcp.icon ||
|
||||||
tags: mcp.tags || [],
|
getCloudServiceClientSync().getPluginIconURL(mcp.author, mcp.name),
|
||||||
type: 'mcp',
|
githubURL: mcp.repository,
|
||||||
});
|
version: mcp.latest_version,
|
||||||
}, [t]);
|
components: mcp.components || {},
|
||||||
|
tags: mcp.tags || [],
|
||||||
|
type: 'mcp',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
const transformSkillToVO = useCallback((skill: any): PluginMarketCardVO => {
|
const transformSkillToVO = useCallback(
|
||||||
return new PluginMarketCardVO({
|
(skill: any): PluginMarketCardVO => {
|
||||||
pluginId: skill.author + ' / ' + skill.name,
|
return new PluginMarketCardVO({
|
||||||
author: skill.author,
|
pluginId: skill.author + ' / ' + skill.name,
|
||||||
pluginName: skill.name,
|
author: skill.author,
|
||||||
label: extractI18nObject(skill.label),
|
pluginName: skill.name,
|
||||||
description: extractI18nObject(skill.description) || t('market.noDescription'),
|
label: extractI18nObject(skill.label),
|
||||||
installCount: skill.install_count || 0,
|
description:
|
||||||
iconURL: skill.icon || getCloudServiceClientSync().getPluginIconURL(skill.author, skill.name),
|
extractI18nObject(skill.description) || t('market.noDescription'),
|
||||||
githubURL: skill.repository,
|
installCount: skill.install_count || 0,
|
||||||
version: skill.latest_version,
|
iconURL:
|
||||||
components: skill.components || {},
|
skill.icon ||
|
||||||
tags: skill.tags || [],
|
getCloudServiceClientSync().getPluginIconURL(
|
||||||
type: 'skill',
|
skill.author,
|
||||||
});
|
skill.name,
|
||||||
}, [t]);
|
),
|
||||||
|
githubURL: skill.repository,
|
||||||
|
version: skill.latest_version,
|
||||||
|
components: skill.components || {},
|
||||||
|
tags: skill.tags || [],
|
||||||
|
type: 'skill',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[t],
|
||||||
|
);
|
||||||
|
|
||||||
// 获取插件列表
|
// 获取插件列表
|
||||||
const fetchPlugins = useCallback(
|
const fetchPlugins = useCallback(
|
||||||
@@ -212,20 +224,24 @@ function MarketPageContent({
|
|||||||
let skillsTotal = 0;
|
let skillsTotal = 0;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const pluginsResponse = await getCloudServiceClientSync().searchMarketplacePlugins(
|
const pluginsResponse =
|
||||||
query,
|
await getCloudServiceClientSync().searchMarketplacePlugins(
|
||||||
page,
|
query,
|
||||||
pageSize,
|
page,
|
||||||
sortBy,
|
pageSize,
|
||||||
sortOrder,
|
sortBy,
|
||||||
undefined,
|
sortOrder,
|
||||||
selectedTags.length > 0 ? selectedTags : undefined,
|
undefined,
|
||||||
'plugin',
|
selectedTags.length > 0 ? selectedTags : undefined,
|
||||||
);
|
'plugin',
|
||||||
|
);
|
||||||
pluginsResult = pluginsResponse.plugins
|
pluginsResult = pluginsResponse.plugins
|
||||||
.filter((plugin) => {
|
.filter((plugin) => {
|
||||||
const keys = Object.keys(plugin.components || {});
|
const keys = Object.keys(plugin.components || {});
|
||||||
return !(keys.length > 0 && keys.every((k) => k === 'KnowledgeRetriever'));
|
return !(
|
||||||
|
keys.length > 0 &&
|
||||||
|
keys.every((k) => k === 'KnowledgeRetriever')
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.map(transformToVO);
|
.map(transformToVO);
|
||||||
pluginsTotal = pluginsResponse.total || 0;
|
pluginsTotal = pluginsResponse.total || 0;
|
||||||
@@ -234,16 +250,17 @@ function MarketPageContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mcpsResponse = await getCloudServiceClientSync().searchMarketplacePlugins(
|
const mcpsResponse =
|
||||||
query,
|
await getCloudServiceClientSync().searchMarketplacePlugins(
|
||||||
page,
|
query,
|
||||||
pageSize,
|
page,
|
||||||
sortBy,
|
pageSize,
|
||||||
sortOrder,
|
sortBy,
|
||||||
undefined,
|
sortOrder,
|
||||||
selectedTags.length > 0 ? selectedTags : undefined,
|
undefined,
|
||||||
'mcp',
|
selectedTags.length > 0 ? selectedTags : undefined,
|
||||||
);
|
'mcp',
|
||||||
|
);
|
||||||
mcpsResult = (mcpsResponse.plugins || []).map(transformMCPToVO);
|
mcpsResult = (mcpsResponse.plugins || []).map(transformMCPToVO);
|
||||||
mcpsTotal = mcpsResponse.total || 0;
|
mcpsTotal = mcpsResponse.total || 0;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -251,17 +268,20 @@ function MarketPageContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const skillsResponse = await getCloudServiceClientSync().searchMarketplacePlugins(
|
const skillsResponse =
|
||||||
query,
|
await getCloudServiceClientSync().searchMarketplacePlugins(
|
||||||
page,
|
query,
|
||||||
pageSize,
|
page,
|
||||||
sortBy,
|
pageSize,
|
||||||
sortOrder,
|
sortBy,
|
||||||
undefined,
|
sortOrder,
|
||||||
selectedTags.length > 0 ? selectedTags : undefined,
|
undefined,
|
||||||
'skill',
|
selectedTags.length > 0 ? selectedTags : undefined,
|
||||||
|
'skill',
|
||||||
|
);
|
||||||
|
skillsResult = (skillsResponse.plugins || []).map(
|
||||||
|
transformSkillToVO,
|
||||||
);
|
);
|
||||||
skillsResult = (skillsResponse.plugins || []).map(transformSkillToVO);
|
|
||||||
skillsTotal = skillsResponse.total || 0;
|
skillsTotal = skillsResponse.total || 0;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Failed to fetch skills:', e);
|
console.warn('Failed to fetch skills:', e);
|
||||||
@@ -270,22 +290,25 @@ function MarketPageContent({
|
|||||||
newPlugins = [...pluginsResult, ...mcpsResult, ...skillsResult];
|
newPlugins = [...pluginsResult, ...mcpsResult, ...skillsResult];
|
||||||
total = pluginsTotal + mcpsTotal + skillsTotal;
|
total = pluginsTotal + mcpsTotal + skillsTotal;
|
||||||
} else {
|
} else {
|
||||||
const response = await getCloudServiceClientSync().searchMarketplacePlugins(
|
const response =
|
||||||
query,
|
await getCloudServiceClientSync().searchMarketplacePlugins(
|
||||||
page,
|
query,
|
||||||
pageSize,
|
page,
|
||||||
sortBy,
|
pageSize,
|
||||||
sortOrder,
|
sortBy,
|
||||||
undefined,
|
sortOrder,
|
||||||
selectedTags.length > 0 ? selectedTags : undefined,
|
undefined,
|
||||||
typeFilter === 'all' ? undefined : typeFilter,
|
selectedTags.length > 0 ? selectedTags : undefined,
|
||||||
);
|
typeFilter === 'all' ? undefined : typeFilter,
|
||||||
|
);
|
||||||
|
|
||||||
const data: ApiRespMarketplacePlugins = response;
|
const data: ApiRespMarketplacePlugins = response;
|
||||||
newPlugins = data.plugins
|
newPlugins = data.plugins
|
||||||
.filter((plugin) => {
|
.filter((plugin) => {
|
||||||
const keys = Object.keys(plugin.components || {});
|
const keys = Object.keys(plugin.components || {});
|
||||||
return !(keys.length > 0 && keys.every((k) => k === 'KnowledgeRetriever'));
|
return !(
|
||||||
|
keys.length > 0 && keys.every((k) => k === 'KnowledgeRetriever')
|
||||||
|
);
|
||||||
})
|
})
|
||||||
.map(transformToVO);
|
.map(transformToVO);
|
||||||
total = data.total;
|
total = data.total;
|
||||||
@@ -300,7 +323,9 @@ function MarketPageContent({
|
|||||||
setTotal(total);
|
setTotal(total);
|
||||||
setHasMore(
|
setHasMore(
|
||||||
newPlugins.length > 0 &&
|
newPlugins.length > 0 &&
|
||||||
(reset || page === 1 ? newPlugins.length : plugins.length + newPlugins.length) < total,
|
(reset || page === 1
|
||||||
|
? newPlugins.length
|
||||||
|
: plugins.length + newPlugins.length) < total,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch plugins:', error);
|
console.error('Failed to fetch plugins:', error);
|
||||||
@@ -455,12 +480,21 @@ function MarketPageContent({
|
|||||||
const pluginV4: PluginV4 = {
|
const pluginV4: PluginV4 = {
|
||||||
id: 0,
|
id: 0,
|
||||||
plugin_id: `${cardVO.author}/${cardVO.pluginName}`,
|
plugin_id: `${cardVO.author}/${cardVO.pluginName}`,
|
||||||
mcp_id: cardVO.type === 'mcp' ? `${cardVO.author}/${cardVO.pluginName}` : undefined,
|
mcp_id:
|
||||||
skill_id: cardVO.type === 'skill' ? `${cardVO.author}/${cardVO.pluginName}` : undefined,
|
cardVO.type === 'mcp'
|
||||||
|
? `${cardVO.author}/${cardVO.pluginName}`
|
||||||
|
: undefined,
|
||||||
|
skill_id:
|
||||||
|
cardVO.type === 'skill'
|
||||||
|
? `${cardVO.author}/${cardVO.pluginName}`
|
||||||
|
: undefined,
|
||||||
author: cardVO.author,
|
author: cardVO.author,
|
||||||
name: cardVO.pluginName,
|
name: cardVO.pluginName,
|
||||||
label: { en_US: cardVO.label, zh_Hans: cardVO.label },
|
label: { en_US: cardVO.label, zh_Hans: cardVO.label },
|
||||||
description: { en_US: cardVO.description, zh_Hans: cardVO.description },
|
description: {
|
||||||
|
en_US: cardVO.description,
|
||||||
|
zh_Hans: cardVO.description,
|
||||||
|
},
|
||||||
icon: cardVO.iconURL,
|
icon: cardVO.iconURL,
|
||||||
repository: cardVO.githubURL,
|
repository: cardVO.githubURL,
|
||||||
tags: cardVO.tags || [],
|
tags: cardVO.tags || [],
|
||||||
@@ -482,7 +516,10 @@ function MarketPageContent({
|
|||||||
cardVO.pluginName,
|
cardVO.pluginName,
|
||||||
);
|
);
|
||||||
if (!response?.plugin) {
|
if (!response?.plugin) {
|
||||||
console.error('Failed to install plugin: plugin not found', { author: cardVO.author, pluginName: cardVO.pluginName });
|
console.error('Failed to install plugin: plugin not found', {
|
||||||
|
author: cardVO.author,
|
||||||
|
pluginName: cardVO.pluginName,
|
||||||
|
});
|
||||||
toast.error(t('market.installFailed'));
|
toast.error(t('market.installFailed'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -601,7 +638,7 @@ function MarketPageContent({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{headerActions && (
|
{headerActions && (
|
||||||
<div className="flex items-center gap-2 flex-shrink-0">
|
<div className="flex items-center gap-2 flex-wrap lg:flex-nowrap lg:flex-shrink-0">
|
||||||
{headerActions}
|
{headerActions}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -631,7 +668,9 @@ function MarketPageContent({
|
|||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="outline" className="relative flex-shrink-0">
|
<Button variant="outline" className="relative flex-shrink-0">
|
||||||
<SlidersHorizontal className="h-4 w-4" />
|
<SlidersHorizontal className="h-4 w-4" />
|
||||||
<span className="hidden sm:inline">{t('market.filters.more')}</span>
|
<span className="hidden sm:inline">
|
||||||
|
{t('market.filters.more')}
|
||||||
|
</span>
|
||||||
{activeAdvancedFilters > 0 && (
|
{activeAdvancedFilters > 0 && (
|
||||||
<span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] leading-none text-primary-foreground">
|
<span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full bg-primary px-1 text-[10px] leading-none text-primary-foreground">
|
||||||
{activeAdvancedFilters}
|
{activeAdvancedFilters}
|
||||||
@@ -641,7 +680,9 @@ function MarketPageContent({
|
|||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent align="end" className="w-[320px] space-y-4">
|
<PopoverContent align="end" className="w-[320px] space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium">{t('market.filters.advancedTitle')}</div>
|
<div className="text-sm font-medium">
|
||||||
|
{t('market.filters.advancedTitle')}
|
||||||
|
</div>
|
||||||
<div className="mt-1 text-xs text-muted-foreground">
|
<div className="mt-1 text-xs text-muted-foreground">
|
||||||
{t('market.filters.advancedDescription')}
|
{t('market.filters.advancedDescription')}
|
||||||
</div>
|
</div>
|
||||||
@@ -735,16 +776,15 @@ function MarketPageContent({
|
|||||||
className="flex-1 overflow-y-auto px-3 sm:px-4"
|
className="flex-1 overflow-y-auto px-3 sm:px-4"
|
||||||
>
|
>
|
||||||
{/* Recommendation Lists */}
|
{/* Recommendation Lists */}
|
||||||
{!searchQuery &&
|
{!searchQuery && selectedTags.length === 0 && (
|
||||||
selectedTags.length === 0 && (
|
<div className="pt-4">
|
||||||
<div className="pt-4">
|
<RecommendationLists
|
||||||
<RecommendationLists
|
lists={recommendationLists}
|
||||||
lists={recommendationLists}
|
tagNames={tagNames}
|
||||||
tagNames={tagNames}
|
onInstall={handleInstallPlugin}
|
||||||
onInstall={handleInstallPlugin}
|
/>
|
||||||
/>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="flex items-center justify-center py-12">
|
<div className="flex items-center justify-center py-12">
|
||||||
@@ -823,7 +863,10 @@ export default function MarketPage({
|
|||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MarketPageContent installPlugin={installPlugin} headerActions={headerActions} />
|
<MarketPageContent
|
||||||
|
installPlugin={installPlugin}
|
||||||
|
headerActions={headerActions}
|
||||||
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -537,22 +537,24 @@ function PluginListView() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Header bar with filter tabs, debug info, and task queue */}
|
{/* Header bar with filter tabs, debug info, and task queue */}
|
||||||
<div className="flex flex-row justify-between items-center px-[0.8rem] pb-4 flex-shrink-0 gap-2">
|
<div className="flex flex-col md:flex-row md:justify-between md:items-center px-[0.8rem] pb-4 flex-shrink-0 gap-2">
|
||||||
<Tabs
|
<div className="overflow-x-auto -mx-1 px-1">
|
||||||
value={filterType}
|
<Tabs
|
||||||
onValueChange={(value) => setFilterType(value as FilterType)}
|
value={filterType}
|
||||||
>
|
onValueChange={(value) => setFilterType(value as FilterType)}
|
||||||
<TabsList>
|
>
|
||||||
{FilterOptions.map((option) => (
|
<TabsList>
|
||||||
<TabsTrigger key={option.value} value={option.value}>
|
{FilterOptions.map((option) => (
|
||||||
{option.icon && <option.icon className="size-4" />}
|
<TabsTrigger key={option.value} value={option.value}>
|
||||||
{t(option.labelKey)}
|
{option.icon && <option.icon className="size-4" />}
|
||||||
</TabsTrigger>
|
{t(option.labelKey)}
|
||||||
))}
|
</TabsTrigger>
|
||||||
</TabsList>
|
))}
|
||||||
</Tabs>
|
</TabsList>
|
||||||
<div className="flex flex-row items-center gap-2">
|
</Tabs>
|
||||||
<div className="flex items-center gap-2 px-2">
|
</div>
|
||||||
|
<div className="flex flex-row items-center gap-2 flex-wrap">
|
||||||
|
<div className="flex items-center gap-2 px-1 sm:px-2">
|
||||||
<Switch
|
<Switch
|
||||||
id="group-by-type"
|
id="group-by-type"
|
||||||
checked={groupByType}
|
checked={groupByType}
|
||||||
@@ -572,14 +574,19 @@ function PluginListView() {
|
|||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
className="px-4 py-4 cursor-pointer"
|
className="px-3 sm:px-4 py-4 cursor-pointer"
|
||||||
onClick={handleShowDebugInfo}
|
onClick={handleShowDebugInfo}
|
||||||
>
|
>
|
||||||
<Code className="w-4 h-4 mr-2" />
|
<Code className="w-4 h-4 sm:mr-2" />
|
||||||
{t('plugins.debugInfo')}
|
<span className="hidden sm:inline">
|
||||||
|
{t('plugins.debugInfo')}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-[380px]" align="end">
|
<PopoverContent
|
||||||
|
className="w-[calc(100vw-2rem)] max-w-[380px]"
|
||||||
|
align="end"
|
||||||
|
>
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{/* Header with icon and title */}
|
{/* Header with icon and title */}
|
||||||
<div className="flex items-center gap-2 pb-2 border-b">
|
<div className="flex items-center gap-2 pb-2 border-b">
|
||||||
@@ -597,7 +604,7 @@ function PluginListView() {
|
|||||||
<Input
|
<Input
|
||||||
value={debugInfo?.debug_url || ''}
|
value={debugInfo?.debug_url || ''}
|
||||||
readOnly
|
readOnly
|
||||||
className="w-[220px] font-mono text-xs h-8"
|
className="flex-1 min-w-0 font-mono text-xs h-8"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|||||||
@@ -14,8 +14,15 @@
|
|||||||
padding-top: 2rem;
|
padding-top: 2rem;
|
||||||
padding-bottom: 2rem;
|
padding-bottom: 2rem;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(30rem, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(min(100%, 22rem), 1fr));
|
||||||
gap: 2rem;
|
gap: 1.25rem;
|
||||||
justify-items: stretch;
|
justify-items: stretch;
|
||||||
align-items: start;
|
align-items: start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.pluginListContainer {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(min(100%, 28rem), 1fr));
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user