mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
fix: stabilize marketplace home sticky navigation
This commit is contained in:
parent
ecf72a0860
commit
8e670491a3
@ -68,6 +68,8 @@ describe('PluginTypeSwitch', () => {
|
||||
await user.click(screen.getByRole('button', { name: 'category.models' }))
|
||||
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('category')).toBe('model')
|
||||
const update = onUrlUpdate.mock.calls.at(-1)?.[0]
|
||||
expect(update?.searchParams.get('category')).toBe('model')
|
||||
expect(update?.options.scroll).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@ -48,8 +48,9 @@ describe('HomeCatalogNavigation', () => {
|
||||
expect(navigationSection).toHaveClass(styles.catalogNavigation)
|
||||
expect(navigationSection.firstElementChild).toHaveClass('w-full')
|
||||
expect(navigationSection.firstElementChild).not.toHaveClass('mx-auto', 'max-w-[1200px]')
|
||||
const activeTab = screen.getByText('plugin.marketplace.home.plugins')
|
||||
const activeTab = screen.getByRole('link', { name: 'plugin.marketplace.home.plugins' })
|
||||
expect(activeTab).toHaveAttribute('aria-current', 'page')
|
||||
expect(activeTab).toHaveAttribute('href', '/plugins')
|
||||
expect(activeTab.querySelector('[aria-hidden="true"]')).toHaveClass(
|
||||
'absolute',
|
||||
'h-0.5',
|
||||
@ -62,9 +63,26 @@ describe('HomeCatalogNavigation', () => {
|
||||
expect(screen.getByTestId('plugin-type-switch')).toHaveAttribute('data-variant', 'home')
|
||||
})
|
||||
|
||||
it('keeps compact tabs clickable and hides the active indicator', () => {
|
||||
render(<HomeCatalogTabs compact isMarketplacePlatform />)
|
||||
|
||||
const pluginsTab = screen.getByRole('link', { name: 'plugin.marketplace.home.plugins' })
|
||||
const templatesTab = screen.getByRole('link', { name: 'plugin.marketplace.home.templates' })
|
||||
|
||||
expect(pluginsTab).toHaveAttribute('href', '/plugins')
|
||||
expect(pluginsTab).toHaveClass('cursor-pointer')
|
||||
expect(pluginsTab.querySelector('[aria-hidden="true"]')).not.toBeInTheDocument()
|
||||
expect(templatesTab).toHaveAttribute('href', '/templates')
|
||||
expect(templatesTab).toHaveClass('cursor-pointer')
|
||||
})
|
||||
|
||||
it('links Dify users to the hosted Marketplace templates page', () => {
|
||||
renderNavigation(false)
|
||||
|
||||
expect(screen.getByRole('link', { name: 'plugin.marketplace.home.plugins' })).toHaveAttribute(
|
||||
'href',
|
||||
'https://marketplace.dify.ai/plugins?source=console',
|
||||
)
|
||||
expect(
|
||||
screen.getByRole('link', { name: /plugin\.marketplace\.home\.templates/ }),
|
||||
).toHaveAttribute('href', 'https://marketplace.dify.ai/templates?source=console')
|
||||
@ -128,4 +146,19 @@ describe('HomeCatalogNavigation', () => {
|
||||
|
||||
scrollContainer.remove()
|
||||
})
|
||||
|
||||
it('prevents scroll anchoring from reversing the sticky threshold', () => {
|
||||
const scrollContainer = document.createElement('div')
|
||||
scrollContainer.id = 'marketplace-container'
|
||||
document.body.appendChild(scrollContainer)
|
||||
|
||||
const { unmount } = renderNavigation(true)
|
||||
|
||||
expect(scrollContainer.style.overflowAnchor).toBe('none')
|
||||
|
||||
unmount()
|
||||
expect(scrollContainer.style.overflowAnchor).toBe('')
|
||||
|
||||
scrollContainer.remove()
|
||||
})
|
||||
})
|
||||
|
||||
@ -25,6 +25,11 @@ function HomeCatalogNavigation({ catalogTabs }: HomeCatalogNavigationProps) {
|
||||
const scrollContainer = document.getElementById('marketplace-container')
|
||||
if (!scrollContainer) return
|
||||
|
||||
const previousOverflowAnchor = scrollContainer.style.overflowAnchor
|
||||
// The sticky section becomes shorter when its tabs move into the header.
|
||||
// Prevent browser scroll anchoring from moving it back across the pin threshold.
|
||||
scrollContainer.style.overflowAnchor = 'none'
|
||||
|
||||
const updatePinnedState = () => {
|
||||
const pinTrigger = pinTriggerRef.current
|
||||
if (!pinTrigger) return
|
||||
@ -41,6 +46,7 @@ function HomeCatalogNavigation({ catalogTabs }: HomeCatalogNavigationProps) {
|
||||
return () => {
|
||||
scrollContainer.removeEventListener('scroll', updatePinnedState)
|
||||
window.removeEventListener('resize', updatePinnedState)
|
||||
scrollContainer.style.overflowAnchor = previousOverflowAnchor
|
||||
}
|
||||
}, [setIsPinned])
|
||||
|
||||
|
||||
@ -5,11 +5,17 @@ import { getMarketplaceUrl } from '@/utils/var'
|
||||
|
||||
type HomeCatalogTabsProps = {
|
||||
className?: string
|
||||
compact?: boolean
|
||||
isMarketplacePlatform: boolean
|
||||
}
|
||||
|
||||
const HomeCatalogTabs = ({ className, isMarketplacePlatform }: HomeCatalogTabsProps) => {
|
||||
const HomeCatalogTabs = ({
|
||||
className,
|
||||
compact = false,
|
||||
isMarketplacePlatform,
|
||||
}: HomeCatalogTabsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const pluginsHref = isMarketplacePlatform ? '/plugins' : getMarketplaceUrl('/plugins')
|
||||
const templatesHref = isMarketplacePlatform ? '/templates' : getMarketplaceUrl('/templates')
|
||||
|
||||
return (
|
||||
@ -17,24 +23,35 @@ const HomeCatalogTabs = ({ className, isMarketplacePlatform }: HomeCatalogTabsPr
|
||||
aria-label={t(($) => $['mainNav.marketplace'], { ns: 'common' })}
|
||||
className={cn('flex h-8 items-center gap-1', className)}
|
||||
>
|
||||
<span
|
||||
<Link
|
||||
href={pluginsHref}
|
||||
aria-current="page"
|
||||
className="relative flex h-8 items-start px-[9px] pt-2 body-sm-medium text-text-accent"
|
||||
className={cn(
|
||||
'relative flex h-8 cursor-pointer items-start rounded-lg px-[9px] pt-2 body-sm-medium outline-hidden focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
compact ? 'bg-state-base-active text-text-primary' : 'text-text-accent',
|
||||
)}
|
||||
>
|
||||
{t(($) => $['marketplace.home.plugins'], { ns: 'plugin' })}
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute bottom-0 left-1/2 h-0.5 w-[21px] -translate-x-1/2 rounded-full bg-text-accent"
|
||||
/>
|
||||
</span>
|
||||
{!compact && (
|
||||
<span
|
||||
aria-hidden
|
||||
className="absolute bottom-0 left-1/2 h-0.5 w-[21px] -translate-x-1/2 rounded-full bg-text-accent"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
<Link
|
||||
href={templatesHref}
|
||||
className="flex h-8 items-center gap-2 rounded-[10px] p-2 body-sm-regular text-text-primary outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
|
||||
className={cn(
|
||||
'flex h-8 cursor-pointer items-center gap-2 rounded-[10px] p-2 body-sm-regular outline-hidden hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid',
|
||||
compact ? 'text-text-tertiary' : 'text-text-primary',
|
||||
)}
|
||||
>
|
||||
<span>{t(($) => $['marketplace.home.templates'], { ns: 'plugin' })}</span>
|
||||
<span className="flex items-center rounded-full bg-saas-dify-blue-accessible px-[5px] py-0.5 system-2xs-regular text-text-primary-on-surface uppercase">
|
||||
{t(($) => $['marketplace.home.new'], { ns: 'plugin' })}
|
||||
</span>
|
||||
{!compact && (
|
||||
<span className="flex items-center rounded-full bg-saas-dify-blue-accessible px-[5px] py-0.5 system-2xs-regular text-text-primary-on-surface uppercase">
|
||||
{t(($) => $['marketplace.home.new'], { ns: 'plugin' })}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</nav>
|
||||
)
|
||||
|
||||
@ -18,7 +18,7 @@ function Guide() {
|
||||
const docLink = useDocLink()
|
||||
|
||||
return (
|
||||
<Link href={docLink()} target="_blank" rel="noopener noreferrer">
|
||||
<Link href={docLink()} target="_blank" rel="noopener noreferrer" className={styles.guide}>
|
||||
<Button variant="ghost" size="large" className="w-[94px] gap-0.5 px-3 text-text-primary">
|
||||
<span aria-hidden className="i-ri-map-2-line size-5" />
|
||||
<span className="px-1 system-md-medium">Guide</span>
|
||||
@ -33,15 +33,18 @@ const HomeHeader = ({ actions, brandName, isMarketplacePlatform }: HomeHeaderPro
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
'sticky top-0 z-50 flex w-full shrink-0 items-center gap-4 bg-background-default px-4 py-1.5 backdrop-blur-sm md:px-9',
|
||||
'sticky top-0 z-50 flex w-full shrink-0 items-center gap-4 border-b border-divider-regular bg-background-default px-4 py-1.5 md:px-9',
|
||||
styles.header,
|
||||
)}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 items-center gap-4">
|
||||
<Link href="/" className="flex h-full w-[142px] shrink-0 items-center">
|
||||
<Link href="/" className={cn('flex h-full w-[142px] shrink-0 items-center', styles.brand)}>
|
||||
<DifyLogo size="small" className="h-[18px] w-[39px] shrink-0" />
|
||||
<span
|
||||
className="ml-1 text-[17.684px] leading-[20.21px] font-medium whitespace-nowrap text-dify-logo-black not-italic [text-box-edge:cap] [text-box-trim:trim-both]"
|
||||
className={cn(
|
||||
'ml-1 text-[12.94px] leading-[14.786px] font-medium whitespace-nowrap text-dify-logo-black not-italic [text-box-edge:cap] [text-box-trim:trim-both]',
|
||||
styles.brandName,
|
||||
)}
|
||||
style={{ fontFamily: "var(--font-family-brand, 'Söhne', var(--font-sans))" }}
|
||||
>
|
||||
{brandName ?? t(($) => $['mainNav.marketplace'])}
|
||||
@ -50,6 +53,7 @@ const HomeHeader = ({ actions, brandName, isMarketplacePlatform }: HomeHeaderPro
|
||||
<HomeStickyCatalogTabs>
|
||||
<HomeCatalogTabs
|
||||
className={styles.headerCatalogTabs}
|
||||
compact
|
||||
isMarketplacePlatform={isMarketplacePlatform}
|
||||
/>
|
||||
</HomeStickyCatalogTabs>
|
||||
|
||||
@ -23,8 +23,13 @@ const HomeSearch = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className={cn('sticky z-[60] -mt-9 flex h-9 shrink-0 justify-center px-4', styles.search)}>
|
||||
<div ref={searchRef} className="relative w-full max-w-[420px]">
|
||||
<div
|
||||
className={cn(
|
||||
'pointer-events-none sticky z-[60] -mt-9 flex h-9 shrink-0 justify-center px-4',
|
||||
styles.search,
|
||||
)}
|
||||
>
|
||||
<div ref={searchRef} className="pointer-events-auto relative w-full max-w-[420px]">
|
||||
<SearchBoxWrapper
|
||||
wrapperClassName="w-full max-w-none"
|
||||
inputClassName="h-9 w-full rounded-[10px] bg-components-input-bg-normal [&>div]:px-2.5"
|
||||
|
||||
@ -3,12 +3,7 @@
|
||||
}
|
||||
|
||||
.headerCatalogTabs {
|
||||
display: none;
|
||||
height: 34px;
|
||||
}
|
||||
|
||||
.headerCatalogTabs > [aria-current='page'] {
|
||||
height: 34px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search {
|
||||
@ -31,20 +26,42 @@
|
||||
}
|
||||
|
||||
.catalogNavigationPinned {
|
||||
padding-top: 12px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--color-divider-regular);
|
||||
padding-top: 16px;
|
||||
padding-bottom: 15px;
|
||||
background-color: var(--color-background-default);
|
||||
}
|
||||
|
||||
@media (min-width: 1100px) {
|
||||
.headerCatalogTabs {
|
||||
display: flex;
|
||||
}
|
||||
.catalogTabsPinned {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.catalogTabsPinned {
|
||||
.categoriesPinned {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.catalogContent {
|
||||
min-height: calc(100vh - 112px);
|
||||
min-height: calc(100dvh - 112px);
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.guide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.categoriesPinned {
|
||||
margin-top: 0;
|
||||
.search {
|
||||
position: relative;
|
||||
top: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 559px) {
|
||||
.brand {
|
||||
width: 39px;
|
||||
}
|
||||
|
||||
.brandName {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import HomeHeader from './home-header'
|
||||
import HomeHero from './home-hero'
|
||||
import HomeSearch from './home-search'
|
||||
import { HomeStickyStateProvider } from './home-sticky-state-provider'
|
||||
import styles from './home-sticky.module.css'
|
||||
import HomeTrending from './home-trending'
|
||||
|
||||
type MarketplaceHomeProps = {
|
||||
@ -47,6 +48,7 @@ const MarketplaceHome = ({
|
||||
/>
|
||||
<div className="contents [&>div]:bg-background-default!">
|
||||
<ListWrapper
|
||||
className={styles.catalogContent}
|
||||
showInstallButton={showInstallButton}
|
||||
linkToMarketplaceDetail={linkToMarketplaceDetail}
|
||||
/>
|
||||
|
||||
@ -101,4 +101,14 @@ describe('ListWrapper', () => {
|
||||
|
||||
expect(screen.getAllByTestId('loading')).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('keeps the supplied layout constraint while category results are loading', () => {
|
||||
mockMarketplaceData.isLoading = true
|
||||
mockMarketplaceData.page = 1
|
||||
|
||||
const { container } = render(<ListWrapper className="catalog-content-min-height" />)
|
||||
|
||||
expect(container.firstElementChild).toHaveClass('catalog-content-min-height')
|
||||
expect(screen.getByTestId('loading')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
'use client'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useTranslation } from '#i18n'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import SortDropdown from '../sort-dropdown'
|
||||
@ -6,10 +7,15 @@ import { useMarketplaceData } from '../state'
|
||||
import List from './index'
|
||||
|
||||
type ListWrapperProps = {
|
||||
className?: string
|
||||
showInstallButton?: boolean
|
||||
linkToMarketplaceDetail?: boolean
|
||||
}
|
||||
const ListWrapper = ({ showInstallButton, linkToMarketplaceDetail }: ListWrapperProps) => {
|
||||
const ListWrapper = ({
|
||||
className,
|
||||
showInstallButton,
|
||||
linkToMarketplaceDetail,
|
||||
}: ListWrapperProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const {
|
||||
@ -28,7 +34,10 @@ const ListWrapper = ({ showInstallButton, linkToMarketplaceDetail }: ListWrapper
|
||||
scrollbarGutter: 'stable',
|
||||
paddingBottom: 'calc(0.5rem + var(--marketplace-header-collapse-offset, 0px))',
|
||||
}}
|
||||
className="relative flex grow flex-col bg-background-default-subtle px-8 py-2"
|
||||
className={cn(
|
||||
'relative flex grow flex-col bg-background-default-subtle px-8 py-2',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="flex w-full grow flex-col">
|
||||
{plugins && (
|
||||
|
||||
@ -86,7 +86,7 @@ function PluginTypeSwitch({ className, variant = 'default' }: PluginTypeSwitchPr
|
||||
isHero
|
||||
? 'flex shrink-0 items-center gap-1 overflow-x-auto'
|
||||
: isHome
|
||||
? 'flex w-full shrink-0 items-center justify-start gap-1 overflow-x-auto'
|
||||
? 'flex w-full shrink-0 scrollbar-none items-center justify-start gap-1 overflow-x-auto'
|
||||
: 'flex shrink-0 items-center justify-center space-x-2 bg-background-body py-3',
|
||||
className,
|
||||
)}
|
||||
@ -106,7 +106,7 @@ function PluginTypeSwitch({ className, variant = 'default' }: PluginTypeSwitchPr
|
||||
isHero
|
||||
? 'text-text-primary-on-surface'
|
||||
: isHome
|
||||
? cn('min-w-12 justify-center text-text-tertiary', styles.homeItem)
|
||||
? cn('min-w-12 shrink-0 justify-center text-text-tertiary', styles.homeItem)
|
||||
: 'text-text-tertiary',
|
||||
!isActive &&
|
||||
(isHero
|
||||
|
||||
@ -8,7 +8,7 @@ export const marketplaceSearchParamsParsers = {
|
||||
Object.values(PLUGIN_TYPE_SEARCH_MAP) as ActivePluginType[],
|
||||
)
|
||||
.withDefault('all')
|
||||
.withOptions({ history: 'replace', clearOnDefault: false }),
|
||||
.withOptions({ history: 'replace', clearOnDefault: false, scroll: false }),
|
||||
q: parseAsString.withDefault('').withOptions({ history: 'replace' }),
|
||||
tags: parseAsArrayOf(parseAsString).withDefault([]).withOptions({ history: 'replace' }),
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user