From 26fe8c1cc50b9db79946e75147b2c3508bfce7be Mon Sep 17 00:00:00 2001 From: yessenia Date: Wed, 11 Feb 2026 23:49:15 +0800 Subject: [PATCH] feat: enhance marketplace components with new template card features, improved navigation options, and updated translations for better user experience --- .../plugins/marketplace/description/index.tsx | 33 ++++-------- .../marketplace/list/template-card.tsx | 5 +- .../plugins/plugin-page/nav-operations.tsx | 52 +++++++++++++------ web/i18n/en-US/plugin.json | 2 + web/i18n/zh-Hans/plugin.json | 2 + web/i18n/zh-Hant/plugin.json | 1 + 6 files changed, 57 insertions(+), 38 deletions(-) diff --git a/web/app/components/plugins/marketplace/description/index.tsx b/web/app/components/plugins/marketplace/description/index.tsx index f4f16eb73b..9d5a07cd48 100644 --- a/web/app/components/plugins/marketplace/description/index.tsx +++ b/web/app/components/plugins/marketplace/description/index.tsx @@ -2,7 +2,7 @@ import type { MotionValue } from 'motion/react' import { useTranslation } from '#i18n' -import { motion, useMotionValue, useTransform } from 'motion/react' +import { motion, useMotionValue, useSpring, useTransform } from 'motion/react' import { useEffect, useLayoutEffect, useRef } from 'react' import marketPlaceBg from '@/public/marketplace/hero-bg.jpg' import marketplaceGradientNoise from '@/public/marketplace/hero-gradient-noise.svg' @@ -36,10 +36,10 @@ export const Description = ({ const heroSubtitleKey = isTemplatesView ? 'marketplace.templatesHeroSubtitle' : 'marketplace.pluginsHeroSubtitle' const rafRef = useRef(null) const lastProgressRef = useRef(0) - const maxScrollableRef = useRef(1) const titleRef = useRef(null) const progress = useMotionValue(0) const titleHeight = useMotionValue(0) + const smoothProgress = useSpring(progress, { stiffness: 260, damping: 34 }) useLayoutEffect(() => { const node = titleRef.current @@ -74,17 +74,7 @@ export const Description = ({ rafRef.current = requestAnimationFrame(() => { const scrollTop = Math.round(container.scrollTop) const heightDelta = container.scrollHeight - container.clientHeight - // Keep collapse threshold stable during the same scroll session. - // If we recompute with shrinking scrollHeight on every frame, progress can stay stuck at 1 - // until scrollTop reaches 0, which feels like "cannot scroll to top". - const observedScrollable = Math.max(1, heightDelta) - const shouldResetThreshold = scrollTop === 0 - if (shouldResetThreshold) - maxScrollableRef.current = observedScrollable - else - maxScrollableRef.current = Math.max(maxScrollableRef.current, observedScrollable) - - const effectiveMaxScroll = Math.max(1, Math.min(MAX_SCROLL, maxScrollableRef.current)) + const effectiveMaxScroll = Math.max(1, Math.min(MAX_SCROLL, heightDelta)) const rawProgress = Math.min(Math.max(scrollTop / effectiveMaxScroll, 0), 1) const snappedProgress = rawProgress >= 0.95 ? 1 @@ -109,19 +99,19 @@ export const Description = ({ if (rafRef.current) cancelAnimationFrame(rafRef.current) } - }, [progress, scrollContainerId]) + }, [smoothProgress, scrollContainerId]) // Calculate interpolated values - const contentOpacity = useTransform(progress, [0, 1], [1, 0]) - const contentScale = useTransform(progress, [0, 1], [1, 0.9]) + const contentOpacity = useTransform(smoothProgress, [0, 1], [1, 0]) + const contentScale = useTransform(smoothProgress, [0, 1], [1, 0.9]) const titleMaxHeight: MotionValue = useTransform( - [progress, titleHeight], + [smoothProgress, titleHeight], (values: number[]) => values[1] * (1 - values[0]), ) - const tabsMarginTop = useTransform(progress, [0, 1], [48, marketplaceNav ? 16 : 0]) - const titleMarginTop = useTransform(progress, [0, 1], [marketplaceNav ? 80 : 0, 0]) - const paddingTop = useTransform(progress, [0, 1], [marketplaceNav ? COLLAPSED_PADDING_TOP : EXPANDED_PADDING_TOP, COLLAPSED_PADDING_TOP]) - const paddingBottom = useTransform(progress, [0, 1], [EXPANDED_PADDING_BOTTOM, COLLAPSED_PADDING_BOTTOM]) + const tabsMarginTop = useTransform(smoothProgress, [0, 1], [48, marketplaceNav ? 16 : 0]) + const titleMarginTop = useTransform(smoothProgress, [0, 1], [marketplaceNav ? 80 : 0, 0]) + const paddingTop = useTransform(smoothProgress, [0, 1], [marketplaceNav ? COLLAPSED_PADDING_TOP : EXPANDED_PADDING_TOP, COLLAPSED_PADDING_TOP]) + const paddingBottom = useTransform(smoothProgress, [0, 1], [EXPANDED_PADDING_BOTTOM, COLLAPSED_PADDING_BOTTOM]) return ( {/* Blue base background */} diff --git a/web/app/components/plugins/marketplace/list/template-card.tsx b/web/app/components/plugins/marketplace/list/template-card.tsx index eac248ae78..c82c47aa92 100644 --- a/web/app/components/plugins/marketplace/list/template-card.tsx +++ b/web/app/components/plugins/marketplace/list/template-card.tsx @@ -6,6 +6,7 @@ import Image from 'next/image' import Link from 'next/link' import * as React from 'react' import { useCallback, useMemo } from 'react' +import CornerMark from '@/app/components/plugins/card/base/corner-mark' import useTheme from '@/hooks/use-theme' import { cn } from '@/utils/classnames' import { getIconFromMarketPlace } from '@/utils/get-icon' @@ -52,7 +53,8 @@ const TemplateCardComponent = ({ const locale = useLocale() const { t } = useTranslation() const { theme } = useTheme() - const { id, template_name, overview, icon, publisher_handle, usage_count, icon_background, deps_plugins } = template + const { id, template_name, overview, icon, publisher_handle, usage_count, icon_background, deps_plugins, kind } = template + const isSandbox = kind === 'sandboxed' const isIconUrl = !!icon && /^(?:https?:)?\/\//.test(icon) const avatarBgStyle = useMemo(() => { @@ -92,6 +94,7 @@ const TemplateCardComponent = ({ )} onClick={handleClick} > + {isSandbox && } {/* Header */}
{/* Avatar */} diff --git a/web/app/components/plugins/plugin-page/nav-operations.tsx b/web/app/components/plugins/plugin-page/nav-operations.tsx index 6a819cc94b..c739922636 100644 --- a/web/app/components/plugins/plugin-page/nav-operations.tsx +++ b/web/app/components/plugins/plugin-page/nav-operations.tsx @@ -1,5 +1,6 @@ 'use client' -import { RiAddLine, RiBookOpenLine, RiGithubLine } from '@remixicon/react' +import type { DocPathWithoutLang } from '@/types/doc-paths' +import { RiAddLine, RiArrowRightUpLine, RiBookOpenLine } from '@remixicon/react' import Link from 'next/link' import { useState } from 'react' import { useTranslation } from 'react-i18next' @@ -12,6 +13,7 @@ import { PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' import { CREATION_TYPE } from '@/app/components/plugins/marketplace/search-params' +import { MARKETPLACE_URL_PREFIX } from '@/config' import { useDocLink } from '@/context/i18n' import { cn } from '@/utils/classnames' import { useCreationType } from '../marketplace/atoms' @@ -31,10 +33,33 @@ const DropdownItem = ({ href, icon, text, onClick }: DropdownItemProps) => ( onClick={onClick} > {icon} - {text} + {text} + ) +type OptionLabelKey = 'requestAPlugin' | 'publishPlugins' | 'createPublishTemplates' + +const getOptions = (docLink: (path: DocPathWithoutLang) => string): { href: string, icon: React.ReactNode, labelKey: OptionLabelKey }[] => { + return [ + { + href: 'https://github.com/langgenius/dify-plugins/issues/new?template=plugin_request.yaml', + icon: , + labelKey: 'requestAPlugin', + }, + { + href: docLink('/develop-plugin/publishing/marketplace-listing/release-to-dify-marketplace'), + icon: , + labelKey: 'publishPlugins', + }, + { + href: MARKETPLACE_URL_PREFIX.replace('marketplace', 'creators'), + icon: , + labelKey: 'createPublishTemplates', + }, + ] +} + export const SubmitRequestDropdown = () => { const { t } = useTranslation() const [open, setOpen] = useState(false) @@ -63,18 +88,15 @@ export const SubmitRequestDropdown = () => {
- } - text={t('requestAPlugin', { ns: 'plugin' })} - onClick={() => setOpen(false)} - /> - } - text={t('publishPlugins', { ns: 'plugin' })} - onClick={() => setOpen(false)} - /> + {getOptions(docLink).map(option => ( + setOpen(false)} + /> + ))}
@@ -113,7 +135,7 @@ export const CreationTypeTabs = () => { {t('templates', { ns: 'plugin' })} - NEW + {t('badge.new', { ns: 'plugin' })}
diff --git a/web/i18n/en-US/plugin.json b/web/i18n/en-US/plugin.json index 27035ff2af..c837f85aa6 100644 --- a/web/i18n/en-US/plugin.json +++ b/web/i18n/en-US/plugin.json @@ -63,6 +63,7 @@ "autoUpdate.upgradeMode.partial": "Only selected", "autoUpdate.upgradeModePlaceholder.exclude": "Selected plugins will not auto-update", "autoUpdate.upgradeModePlaceholder.partial": "Only selected plugins will auto-update. No plugins are currently selected, so no plugins will auto-update.", + "badge.new": "NEW", "category.agents": "Agent Strategies", "category.all": "All", "category.allTypes": "All types", @@ -79,6 +80,7 @@ "categorySingle.model": "Model", "categorySingle.tool": "Tool", "categorySingle.trigger": "Trigger", + "createPublishTemplates": "Create / Publish templates", "debugInfo.title": "Debugging", "debugInfo.viewDocs": "View Docs", "deprecated": "Deprecated", diff --git a/web/i18n/zh-Hans/plugin.json b/web/i18n/zh-Hans/plugin.json index 730efeafb5..0d702819f8 100644 --- a/web/i18n/zh-Hans/plugin.json +++ b/web/i18n/zh-Hans/plugin.json @@ -63,6 +63,7 @@ "autoUpdate.upgradeMode.partial": "仅选定", "autoUpdate.upgradeModePlaceholder.exclude": "选定的插件将不会自动更新", "autoUpdate.upgradeModePlaceholder.partial": "仅选定的插件将自动更新。目前未选择任何插件,因此不会自动更新任何插件。", + "badge.new": "NEW", "category.agents": "Agent 策略", "category.all": "全部", "category.allTypes": "所有类型", @@ -79,6 +80,7 @@ "categorySingle.model": "模型", "categorySingle.tool": "工具", "categorySingle.trigger": "触发器", + "createPublishTemplates": "创建 / 发布模板", "debugInfo.title": "调试", "debugInfo.viewDocs": "查看文档", "deprecated": "已弃用", diff --git a/web/i18n/zh-Hant/plugin.json b/web/i18n/zh-Hant/plugin.json index ca2d39ae0d..cb75d0c5c4 100644 --- a/web/i18n/zh-Hant/plugin.json +++ b/web/i18n/zh-Hant/plugin.json @@ -78,6 +78,7 @@ "categorySingle.model": "型", "categorySingle.tool": "工具", "categorySingle.trigger": "觸發器", + "createPublishTemplates": "建立 / 發佈範本", "debugInfo.title": "調試", "debugInfo.viewDocs": "查看文件", "deprecated": "不推薦使用的",