fix: align main navigation card grids (#41167)

This commit is contained in:
yyh
2026-08-24 09:04:30 +00:00
committed by GitHub
parent 28a4859218
commit b5982927de
12 changed files with 101 additions and 59 deletions
@@ -899,29 +899,6 @@ describe('List', () => {
)
})
it('should lay out first empty state placeholder cards with auto-fill grid columns', () => {
mockAppData = { pages: [{ data: [], total: 0 }] }
const { container } = renderList()
const placeholderGrid = Array.from(container.querySelectorAll('.pointer-events-none')).find(
(element) => element.className.includes('grid-rows-4'),
)
if (!placeholderGrid) throw new Error('Expected first empty state placeholder grid to render')
expect(placeholderGrid).toHaveClass(
'grid',
'grid-cols-[repeat(auto-fill,minmax(296px,1fr))]',
'grid-rows-4',
)
expect(placeholderGrid).not.toHaveClass(
'grid-cols-1',
'sm:grid-cols-2',
'lg:grid-cols-3',
'xl:grid-cols-4',
)
})
it('should hide learn dify in first empty state when learn app is disabled', () => {
mockAppData = { pages: [{ data: [], total: 0 }] }
+2 -1
View File
@@ -15,6 +15,7 @@ import { keepPreviousData, useInfiniteQuery, useQuery } from '@tanstack/react-qu
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { InfiniteScrollSentinel } from '@/app/components/base/infinite-scroll-sentinel'
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid'
import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { consoleQuery } from '@/service/client'
@@ -217,7 +218,7 @@ function AppListCatalogContent({
preloadDistance={getPreloadDistance}
scrollContainerRef={scrollViewportRef}
/>
<div className="relative grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5">
<div className={cn('relative gap-2.5', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)}>
<AppCardSkeleton count={3} />
{isFetchNextPageError && (
<div
+4 -2
View File
@@ -1,3 +1,5 @@
import { cn } from '@langgenius/dify-ui/cn'
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid'
export const APP_LIST_SEARCH_DEBOUNCE_MS = 500
export const APP_LIST_GRID_CLASS_NAME =
'grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5 px-8 pt-2'
export const APP_LIST_GRID_CLASS_NAME = cn('gap-2.5 px-8 pt-2', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)
@@ -3,8 +3,10 @@
import type { ReactNode } from 'react'
import type { App } from '@/models/explore'
import type { TryAppSelection } from '@/types/try-app'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from 'react-i18next'
import LearnDify from '@/app/components/explore/learn-dify'
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid'
import { STEP_BY_STEP_TOUR_TARGETS } from '@/app/components/step-by-step-tour/target-registry'
import FirstEmptyActionCard from './action-card'
@@ -71,7 +73,12 @@ function FirstEmptyState({
return (
<div className="flex grow flex-col overflow-hidden">
<div className="relative min-h-107.5 flex-1 overflow-hidden">
<div className="pointer-events-none absolute inset-x-8 inset-y-2 grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] grid-rows-4 gap-3">
<div
className={cn(
'pointer-events-none absolute inset-x-8 inset-y-2 grid-rows-4 gap-3',
MAIN_NAV_APP_CARD_GRID_CLASS_NAME,
)}
>
{EMPTY_PLACEHOLDER_CARD_IDS.map((id) => (
<div key={id} className="rounded-xl bg-background-default-lighter opacity-75" />
))}
@@ -7,6 +7,7 @@ import { useSuspenseQuery } from '@tanstack/react-query'
import * as React from 'react'
import { useEffect, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid'
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
import { useLearnDifyAppList } from '@/service/use-explore'
import LearnDifyItem from './item'
@@ -127,7 +128,7 @@ const LearnDifyContent = ({
</button>
)}
</div>
<div className="grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5">
<div className={cn('gap-2.5', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)}>
{visibleItems.map((item) => (
<LearnDifyItem
key={item.app_id}
@@ -0,0 +1,72 @@
import { cn } from '@langgenius/dify-ui/cn'
import { render } from 'vitest-browser-react'
import { APP_LIST_GRID_CLASS_NAME } from '@/app/components/apps/constants'
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '../app-card-grid'
const cardIds = ['first', 'second', 'third', 'fourth']
function CardGrid({
className,
label,
width,
}: {
className: string
label: string
width: number
}) {
return (
<section aria-label={label} className={className} style={{ width }}>
{cardIds.map((id) => (
<article key={id} aria-label={id} className="h-10" />
))}
</section>
)
}
describe('Main navigation card grid', () => {
it('aligns Studio starred and app cards and remains overflow-free in a narrow container', async () => {
// happy-dom does not resolve CSS Grid tracks or browser layout geometry.
const screen = await render(
<>
<CardGrid label="Starred apps" className={APP_LIST_GRID_CLASS_NAME} width={1264} />
<CardGrid label="All apps" className={APP_LIST_GRID_CLASS_NAME} width={1264} />
<CardGrid
label="Narrow card grid"
className={cn('gap-2.5', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)}
width={280}
/>
</>,
)
const starredGrid = screen.getByRole('region', { name: 'Starred apps' })
const starredGridRect = starredGrid.element().getBoundingClientRect()
const starredCardRects = starredGrid
.getByRole('article')
.elements()
.map((element) => element.getBoundingClientRect())
const appCardRects = screen
.getByRole('region', { name: 'All apps' })
.getByRole('article')
.elements()
.map((element) => element.getBoundingClientRect())
expect(new Set(starredCardRects.map((rect) => rect.top)).size).toBe(1)
expect(starredCardRects[0]!.left - starredGridRect.left).toBeCloseTo(32)
expect(starredCardRects[0]!.width).toBeCloseTo(292.5)
expect(starredGridRect.right - starredCardRects.at(-1)!.right).toBeCloseTo(32)
expect(appCardRects.map(({ left, width }) => ({ left, width }))).toEqual(
starredCardRects.map(({ left, width }) => ({ left, width })),
)
const narrowGrid = screen.getByRole('region', { name: 'Narrow card grid' })
const narrowGridElement = narrowGrid.element()
const narrowCardRects = narrowGrid
.getByRole('article')
.elements()
.map((element) => element.getBoundingClientRect())
expect(new Set(narrowCardRects.map((rect) => rect.top)).size).toBe(4)
expect(narrowCardRects.every((rect) => Math.abs(rect.width - 280) < 0.1)).toBe(true)
expect(narrowGridElement.scrollWidth).toBe(narrowGridElement.clientWidth)
})
})
@@ -0,0 +1,2 @@
export const MAIN_NAV_APP_CARD_GRID_CLASS_NAME =
'grid grid-cols-[repeat(auto-fill,minmax(min(100%,288px),1fr))]'
@@ -3,6 +3,7 @@
import type { RecentAppResponse } from '@dify/contracts/api/console/apps/types.gen'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from 'react-i18next'
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid'
import Link from '@/next/link'
import { ContinueWorkItem } from './item'
@@ -33,7 +34,7 @@ export function ContinueWork({ apps, className }: ContinueWorkProps) {
<span className="i-ri-arrow-right-line size-3 shrink-0" aria-hidden="true" />
</Link>
</div>
<div className="grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5 pt-2">
<div className={cn('gap-2.5 pt-2', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)}>
{apps.map((app) => (
<ContinueWorkItem key={app.id} app={app} />
))}
@@ -698,7 +698,7 @@ describe('HomeContent', () => {
expect(screen.getByText('Alpha')).toBeInTheDocument()
expect(screen.getByText('Beta')).toBeInTheDocument()
expect(screen.getByText('explore.apps.title')).toBeInTheDocument()
expect(screen.getByRole('region', { name: 'explore.apps.title' })).toBeInTheDocument()
})
it('should render continue work with the first eight workspace apps', () => {
@@ -11,6 +11,7 @@ import { useAtomValue, useSetAtom } from 'jotai'
import { useQueryState } from 'nuqs'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid'
import {
getStepByStepTourPermissionVariant,
trackStepByStepTourEvent,
@@ -40,7 +41,6 @@ import { HomeIntro } from '../home-intro'
import { HomeShell } from '../home-shell'
import { TemplateCard } from '../template-card'
import { HomeRecommendations } from './recommendations'
import s from './style.module.css'
import { HomeTemplatesHeader } from './templates-header'
const TryApp = dynamic(() => import('@/app/components/explore/try-app'), { ssr: false })
@@ -431,9 +431,9 @@ export function HomeContent() {
/>
<div className={cn('relative flex flex-1 shrink-0 grow flex-col pb-6')}>
<nav
<section
aria-labelledby="home-templates-title"
className={cn(s.templateGrid, 'grid shrink-0 content-start gap-3 px-8')}
className={cn('shrink-0 content-start gap-2.5 px-8', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)}
>
{searchFilteredList.map((app) => (
<TemplateCard
@@ -444,7 +444,7 @@ export function HomeContent() {
onTry={handleTryApp}
/>
))}
</nav>
</section>
</div>
</div>
{isShowCreateModal && (
@@ -1,23 +0,0 @@
.textGradient {
background: linear-gradient(to right, rgba(16, 74, 225, 1) 0, rgba(0, 152, 238, 1) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.templateGrid {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
@media (min-width: 1280px) {
.templateGrid {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
}
@media (min-width: 640px) and (max-width: 1279px) {
.templateGrid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
+4 -2
View File
@@ -1,7 +1,9 @@
'use client'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from 'react-i18next'
import { SkeletonContainer, SkeletonRectangle, SkeletonRow } from '@/app/components/base/skeleton'
import { MAIN_NAV_APP_CARD_GRID_CLASS_NAME } from '@/app/components/main-nav/app-card-grid'
import { HomeIntroSkeleton } from './home-intro'
function HomeTemplateCardSkeleton() {
@@ -39,7 +41,7 @@ function HomeRecommendationsSkeleton() {
<SkeletonRectangle className="my-0 h-6 w-48 animate-pulse" />
</div>
</div>
<div className="grid grid-cols-[repeat(auto-fill,minmax(296px,1fr))] gap-2.5 pt-2">
<div className={cn('gap-2.5 pt-2', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)}>
{Array.from({ length: 4 }, (_, index) => (
<div
key={index}
@@ -82,7 +84,7 @@ function HomeTemplatesHeaderSkeletonBody() {
function HomeTemplatesSkeletonBody() {
return (
<div className="grid shrink-0 grid-cols-[repeat(auto-fill,minmax(296px,1fr))] content-start gap-3 px-8">
<div className={cn('shrink-0 content-start gap-2.5 px-8', MAIN_NAV_APP_CARD_GRID_CLASS_NAME)}>
{Array.from({ length: 8 }, (_, index) => (
<HomeTemplateCardSkeleton key={index} />
))}