From dd809d36f88a397b85b83bdb29167ced4d3b2f3c Mon Sep 17 00:00:00 2001 From: Junyan Qin Date: Sun, 10 May 2026 00:15:55 +0800 Subject: [PATCH] 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) --- web/src/app/home/add-extension/page.tsx | 55 ++-- .../PluginInstalledComponent.tsx | 2 +- .../plugin-market/PluginMarketComponent.tsx | 237 +++++++++++------- web/src/app/home/plugins/page.tsx | 49 ++-- web/src/app/home/plugins/plugins.module.css | 11 +- 5 files changed, 216 insertions(+), 138 deletions(-) diff --git a/web/src/app/home/add-extension/page.tsx b/web/src/app/home/add-extension/page.tsx index cda161e5..b8922a3a 100644 --- a/web/src/app/home/add-extension/page.tsx +++ b/web/src/app/home/add-extension/page.tsx @@ -56,7 +56,9 @@ function AddExtensionContent() { } = usePluginInstallTasks(); const [modalOpen, setModalOpen] = useState(false); const [installInfo, setInstallInfo] = useState>({}); - const [installExtensionType, setInstallExtensionType] = useState<'plugin' | 'mcp' | 'skill'>('plugin'); + const [installExtensionType, setInstallExtensionType] = useState< + 'plugin' | 'mcp' | 'skill' + >('plugin'); const [pluginInstallStatus, setPluginInstallStatus] = useState(PluginInstallStatus.ASK_CONFIRM); const [installError, setInstallError] = useState(null); @@ -120,29 +122,38 @@ function AddExtensionContent() { <> - - navigate('/home/skills?action=create')}> + navigate('/home/skills?action=create')} + > {t('skills.createManually')} - navigate('/home/skills?action=upload')}> + navigate('/home/skills?action=upload')} + > {t('skills.uploadZip')} - navigate('/home/skills?action=github')}> + navigate('/home/skills?action=github')} + > {t('skills.importFromGithub')} @@ -150,17 +161,24 @@ function AddExtensionContent() { - - navigate('/home/add-plugin?action=github')}> + navigate('/home/add-plugin?action=github')} + > {t('plugins.installFromGithub')} - navigate('/home/add-plugin?action=upload')}> + navigate('/home/add-plugin?action=upload')} + > {t('plugins.uploadLocal')} @@ -172,7 +190,10 @@ function AddExtensionContent() { <>
- +
@@ -185,7 +206,7 @@ function AddExtensionContent() { } }} > - + @@ -238,4 +259,4 @@ function AddExtensionContent() { ); -} \ No newline at end of file +} diff --git a/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx b/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx index b530e7d4..3f455b60 100644 --- a/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-installed/PluginInstalledComponent.tsx @@ -462,7 +462,7 @@ const PluginInstalledComponent = forwardRef< ({group.items.length}) -
+
{group.items.map((vo, index) => (
{ - return new PluginMarketCardVO({ - pluginId: mcp.author + ' / ' + mcp.name, - author: mcp.author, - pluginName: mcp.name, - label: extractI18nObject(mcp.label), - description: extractI18nObject(mcp.description) || t('market.noDescription'), - installCount: mcp.install_count || 0, - iconURL: mcp.icon || getCloudServiceClientSync().getPluginIconURL(mcp.author, mcp.name), - githubURL: mcp.repository, - version: mcp.latest_version, - components: mcp.components || {}, - tags: mcp.tags || [], - type: 'mcp', - }); - }, [t]); + const transformMCPToVO = useCallback( + (mcp: any): PluginMarketCardVO => { + return new PluginMarketCardVO({ + pluginId: mcp.author + ' / ' + mcp.name, + author: mcp.author, + pluginName: mcp.name, + label: extractI18nObject(mcp.label), + description: + extractI18nObject(mcp.description) || t('market.noDescription'), + installCount: mcp.install_count || 0, + iconURL: + mcp.icon || + getCloudServiceClientSync().getPluginIconURL(mcp.author, mcp.name), + githubURL: mcp.repository, + version: mcp.latest_version, + components: mcp.components || {}, + tags: mcp.tags || [], + type: 'mcp', + }); + }, + [t], + ); - const transformSkillToVO = useCallback((skill: any): PluginMarketCardVO => { - return new PluginMarketCardVO({ - pluginId: skill.author + ' / ' + skill.name, - author: skill.author, - pluginName: skill.name, - label: extractI18nObject(skill.label), - description: extractI18nObject(skill.description) || t('market.noDescription'), - installCount: skill.install_count || 0, - iconURL: skill.icon || getCloudServiceClientSync().getPluginIconURL(skill.author, skill.name), - githubURL: skill.repository, - version: skill.latest_version, - components: skill.components || {}, - tags: skill.tags || [], - type: 'skill', - }); - }, [t]); + const transformSkillToVO = useCallback( + (skill: any): PluginMarketCardVO => { + return new PluginMarketCardVO({ + pluginId: skill.author + ' / ' + skill.name, + author: skill.author, + pluginName: skill.name, + label: extractI18nObject(skill.label), + description: + extractI18nObject(skill.description) || t('market.noDescription'), + installCount: skill.install_count || 0, + iconURL: + skill.icon || + getCloudServiceClientSync().getPluginIconURL( + skill.author, + skill.name, + ), + githubURL: skill.repository, + version: skill.latest_version, + components: skill.components || {}, + tags: skill.tags || [], + type: 'skill', + }); + }, + [t], + ); // 获取插件列表 const fetchPlugins = useCallback( @@ -212,20 +224,24 @@ function MarketPageContent({ let skillsTotal = 0; try { - const pluginsResponse = await getCloudServiceClientSync().searchMarketplacePlugins( - query, - page, - pageSize, - sortBy, - sortOrder, - undefined, - selectedTags.length > 0 ? selectedTags : undefined, - 'plugin', - ); + const pluginsResponse = + await getCloudServiceClientSync().searchMarketplacePlugins( + query, + page, + pageSize, + sortBy, + sortOrder, + undefined, + selectedTags.length > 0 ? selectedTags : undefined, + 'plugin', + ); pluginsResult = pluginsResponse.plugins .filter((plugin) => { 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); pluginsTotal = pluginsResponse.total || 0; @@ -234,16 +250,17 @@ function MarketPageContent({ } try { - const mcpsResponse = await getCloudServiceClientSync().searchMarketplacePlugins( - query, - page, - pageSize, - sortBy, - sortOrder, - undefined, - selectedTags.length > 0 ? selectedTags : undefined, - 'mcp', - ); + const mcpsResponse = + await getCloudServiceClientSync().searchMarketplacePlugins( + query, + page, + pageSize, + sortBy, + sortOrder, + undefined, + selectedTags.length > 0 ? selectedTags : undefined, + 'mcp', + ); mcpsResult = (mcpsResponse.plugins || []).map(transformMCPToVO); mcpsTotal = mcpsResponse.total || 0; } catch (e) { @@ -251,17 +268,20 @@ function MarketPageContent({ } try { - const skillsResponse = await getCloudServiceClientSync().searchMarketplacePlugins( - query, - page, - pageSize, - sortBy, - sortOrder, - undefined, - selectedTags.length > 0 ? selectedTags : undefined, - 'skill', + const skillsResponse = + await getCloudServiceClientSync().searchMarketplacePlugins( + query, + page, + pageSize, + sortBy, + sortOrder, + undefined, + selectedTags.length > 0 ? selectedTags : undefined, + 'skill', + ); + skillsResult = (skillsResponse.plugins || []).map( + transformSkillToVO, ); - skillsResult = (skillsResponse.plugins || []).map(transformSkillToVO); skillsTotal = skillsResponse.total || 0; } catch (e) { console.warn('Failed to fetch skills:', e); @@ -270,22 +290,25 @@ function MarketPageContent({ newPlugins = [...pluginsResult, ...mcpsResult, ...skillsResult]; total = pluginsTotal + mcpsTotal + skillsTotal; } else { - const response = await getCloudServiceClientSync().searchMarketplacePlugins( - query, - page, - pageSize, - sortBy, - sortOrder, - undefined, - selectedTags.length > 0 ? selectedTags : undefined, - typeFilter === 'all' ? undefined : typeFilter, - ); + const response = + await getCloudServiceClientSync().searchMarketplacePlugins( + query, + page, + pageSize, + sortBy, + sortOrder, + undefined, + selectedTags.length > 0 ? selectedTags : undefined, + typeFilter === 'all' ? undefined : typeFilter, + ); const data: ApiRespMarketplacePlugins = response; newPlugins = data.plugins .filter((plugin) => { 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); total = data.total; @@ -300,7 +323,9 @@ function MarketPageContent({ setTotal(total); setHasMore( 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) { console.error('Failed to fetch plugins:', error); @@ -455,12 +480,21 @@ function MarketPageContent({ const pluginV4: PluginV4 = { id: 0, plugin_id: `${cardVO.author}/${cardVO.pluginName}`, - mcp_id: cardVO.type === 'mcp' ? `${cardVO.author}/${cardVO.pluginName}` : undefined, - skill_id: cardVO.type === 'skill' ? `${cardVO.author}/${cardVO.pluginName}` : undefined, + mcp_id: + cardVO.type === 'mcp' + ? `${cardVO.author}/${cardVO.pluginName}` + : undefined, + skill_id: + cardVO.type === 'skill' + ? `${cardVO.author}/${cardVO.pluginName}` + : undefined, author: cardVO.author, name: cardVO.pluginName, 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, repository: cardVO.githubURL, tags: cardVO.tags || [], @@ -482,7 +516,10 @@ function MarketPageContent({ cardVO.pluginName, ); 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')); return; } @@ -601,7 +638,7 @@ function MarketPageContent({ />
{headerActions && ( -
+
{headerActions}
)} @@ -631,7 +668,9 @@ function MarketPageContent({ - +
{/* Header with icon and title */}
@@ -597,7 +604,7 @@ function PluginListView() {