From b5982927de939f05966303f0e9dcd8a1983696b6 Mon Sep 17 00:00:00 2001 From: yyh <92089059+lyzno1@users.noreply.github.com> Date: Mon, 24 Aug 2026 09:04:30 +0000 Subject: [PATCH] fix: align main navigation card grids (#41167) --- .../components/apps/__tests__/list.spec.tsx | 23 ------ web/app/components/apps/app-list-catalog.tsx | 3 +- web/app/components/apps/constants.ts | 6 +- .../apps/first-empty-state/index.tsx | 9 ++- .../components/explore/learn-dify/index.tsx | 3 +- .../__tests__/card-grid.browser.spec.tsx | 72 +++++++++++++++++++ web/app/components/main-nav/app-card-grid.ts | 2 + .../home/continue-work/continue-work.tsx | 3 +- .../__tests__/home-content.spec.tsx | 2 +- .../home/home-content/home-content.tsx | 8 +-- .../home/home-content/style.module.css | 23 ------ web/features/home/home-skeleton.tsx | 6 +- 12 files changed, 101 insertions(+), 59 deletions(-) create mode 100644 web/app/components/main-nav/__tests__/card-grid.browser.spec.tsx create mode 100644 web/app/components/main-nav/app-card-grid.ts delete mode 100644 web/features/home/home-content/style.module.css diff --git a/web/app/components/apps/__tests__/list.spec.tsx b/web/app/components/apps/__tests__/list.spec.tsx index 8ade51a73a3..79816ab08a8 100644 --- a/web/app/components/apps/__tests__/list.spec.tsx +++ b/web/app/components/apps/__tests__/list.spec.tsx @@ -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 }] } diff --git a/web/app/components/apps/app-list-catalog.tsx b/web/app/components/apps/app-list-catalog.tsx index 1b7dc2df745..3d3ba81e0b3 100644 --- a/web/app/components/apps/app-list-catalog.tsx +++ b/web/app/components/apps/app-list-catalog.tsx @@ -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} /> -
+
{isFetchNextPageError && (
-
+
{EMPTY_PLACEHOLDER_CARD_IDS.map((id) => (
))} diff --git a/web/app/components/explore/learn-dify/index.tsx b/web/app/components/explore/learn-dify/index.tsx index ce920be506f..ef1fb730ea3 100644 --- a/web/app/components/explore/learn-dify/index.tsx +++ b/web/app/components/explore/learn-dify/index.tsx @@ -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 = ({ )}
-
+
{visibleItems.map((item) => ( + {cardIds.map((id) => ( +
+ ))} + + ) +} + +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( + <> + + + + , + ) + + 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) + }) +}) diff --git a/web/app/components/main-nav/app-card-grid.ts b/web/app/components/main-nav/app-card-grid.ts new file mode 100644 index 00000000000..8bd2eae7a08 --- /dev/null +++ b/web/app/components/main-nav/app-card-grid.ts @@ -0,0 +1,2 @@ +export const MAIN_NAV_APP_CARD_GRID_CLASS_NAME = + 'grid grid-cols-[repeat(auto-fill,minmax(min(100%,288px),1fr))]' diff --git a/web/features/home/continue-work/continue-work.tsx b/web/features/home/continue-work/continue-work.tsx index 632028fe7f2..486065bcd75 100644 --- a/web/features/home/continue-work/continue-work.tsx +++ b/web/features/home/continue-work/continue-work.tsx @@ -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) {
-
+
{apps.map((app) => ( ))} diff --git a/web/features/home/home-content/__tests__/home-content.spec.tsx b/web/features/home/home-content/__tests__/home-content.spec.tsx index 5c418b0d692..947c3c916c2 100644 --- a/web/features/home/home-content/__tests__/home-content.spec.tsx +++ b/web/features/home/home-content/__tests__/home-content.spec.tsx @@ -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', () => { diff --git a/web/features/home/home-content/home-content.tsx b/web/features/home/home-content/home-content.tsx index 239622e040d..076086b2346 100644 --- a/web/features/home/home-content/home-content.tsx +++ b/web/features/home/home-content/home-content.tsx @@ -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() { />
- +
{isShowCreateModal && ( diff --git a/web/features/home/home-content/style.module.css b/web/features/home/home-content/style.module.css deleted file mode 100644 index cf0adac17c5..00000000000 --- a/web/features/home/home-content/style.module.css +++ /dev/null @@ -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)); - } -} diff --git a/web/features/home/home-skeleton.tsx b/web/features/home/home-skeleton.tsx index 271d3bb5759..ea83ed4d568 100644 --- a/web/features/home/home-skeleton.tsx +++ b/web/features/home/home-skeleton.tsx @@ -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() {
-
+
{Array.from({ length: 4 }, (_, index) => (
+
{Array.from({ length: 8 }, (_, index) => ( ))}