mirror of https://github.com/langgenius/dify.git
style(scroll-bar): align design (#33751)
This commit is contained in:
parent
7019395a28
commit
8bbaa862f2
|
|
@ -8,6 +8,7 @@ import {
|
|||
ScrollAreaThumb,
|
||||
ScrollAreaViewport,
|
||||
} from '../index'
|
||||
import styles from '../index.module.css'
|
||||
|
||||
const renderScrollArea = (options: {
|
||||
rootClassName?: string
|
||||
|
|
@ -72,20 +73,19 @@ describe('scroll-area wrapper', () => {
|
|||
const thumb = screen.getByTestId('scroll-area-vertical-thumb')
|
||||
|
||||
expect(scrollbar).toHaveAttribute('data-orientation', 'vertical')
|
||||
expect(scrollbar).toHaveClass(styles.scrollbar)
|
||||
expect(scrollbar).toHaveClass(
|
||||
'flex',
|
||||
'overflow-clip',
|
||||
'p-1',
|
||||
'touch-none',
|
||||
'select-none',
|
||||
'opacity-0',
|
||||
'opacity-100',
|
||||
'transition-opacity',
|
||||
'motion-reduce:transition-none',
|
||||
'pointer-events-none',
|
||||
'data-[hovering]:pointer-events-auto',
|
||||
'data-[hovering]:opacity-100',
|
||||
'data-[scrolling]:pointer-events-auto',
|
||||
'data-[scrolling]:opacity-100',
|
||||
'hover:pointer-events-auto',
|
||||
'hover:opacity-100',
|
||||
'data-[orientation=vertical]:absolute',
|
||||
'data-[orientation=vertical]:inset-y-0',
|
||||
'data-[orientation=vertical]:w-3',
|
||||
|
|
@ -97,7 +97,6 @@ describe('scroll-area wrapper', () => {
|
|||
'rounded-[4px]',
|
||||
'bg-state-base-handle',
|
||||
'transition-[background-color]',
|
||||
'hover:bg-state-base-handle-hover',
|
||||
'motion-reduce:transition-none',
|
||||
'data-[orientation=vertical]:w-1',
|
||||
)
|
||||
|
|
@ -112,20 +111,19 @@ describe('scroll-area wrapper', () => {
|
|||
const thumb = screen.getByTestId('scroll-area-horizontal-thumb')
|
||||
|
||||
expect(scrollbar).toHaveAttribute('data-orientation', 'horizontal')
|
||||
expect(scrollbar).toHaveClass(styles.scrollbar)
|
||||
expect(scrollbar).toHaveClass(
|
||||
'flex',
|
||||
'overflow-clip',
|
||||
'p-1',
|
||||
'touch-none',
|
||||
'select-none',
|
||||
'opacity-0',
|
||||
'opacity-100',
|
||||
'transition-opacity',
|
||||
'motion-reduce:transition-none',
|
||||
'pointer-events-none',
|
||||
'data-[hovering]:pointer-events-auto',
|
||||
'data-[hovering]:opacity-100',
|
||||
'data-[scrolling]:pointer-events-auto',
|
||||
'data-[scrolling]:opacity-100',
|
||||
'hover:pointer-events-auto',
|
||||
'hover:opacity-100',
|
||||
'data-[orientation=horizontal]:absolute',
|
||||
'data-[orientation=horizontal]:inset-x-0',
|
||||
'data-[orientation=horizontal]:h-3',
|
||||
|
|
@ -137,7 +135,6 @@ describe('scroll-area wrapper', () => {
|
|||
'rounded-[4px]',
|
||||
'bg-state-base-handle',
|
||||
'transition-[background-color]',
|
||||
'hover:bg-state-base-handle-hover',
|
||||
'motion-reduce:transition-none',
|
||||
'data-[orientation=horizontal]:h-1',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,75 @@
|
|||
.scrollbar::before,
|
||||
.scrollbar::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
border-radius: 9999px;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 150ms ease;
|
||||
}
|
||||
|
||||
.scrollbar[data-orientation='vertical']::before {
|
||||
left: 50%;
|
||||
top: 4px;
|
||||
width: 4px;
|
||||
height: 12px;
|
||||
transform: translateX(-50%);
|
||||
background: linear-gradient(to bottom, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
|
||||
}
|
||||
|
||||
.scrollbar[data-orientation='vertical']::after {
|
||||
left: 50%;
|
||||
bottom: 4px;
|
||||
width: 4px;
|
||||
height: 12px;
|
||||
transform: translateX(-50%);
|
||||
background: linear-gradient(to top, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
|
||||
}
|
||||
|
||||
.scrollbar[data-orientation='horizontal']::before {
|
||||
top: 50%;
|
||||
left: 4px;
|
||||
width: 12px;
|
||||
height: 4px;
|
||||
transform: translateY(-50%);
|
||||
background: linear-gradient(to right, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
|
||||
}
|
||||
|
||||
.scrollbar[data-orientation='horizontal']::after {
|
||||
top: 50%;
|
||||
right: 4px;
|
||||
width: 12px;
|
||||
height: 4px;
|
||||
transform: translateY(-50%);
|
||||
background: linear-gradient(to left, var(--scroll-area-edge-hint-bg, var(--color-components-panel-bg)), transparent);
|
||||
}
|
||||
|
||||
.scrollbar[data-orientation='vertical']:not([data-overflow-y-start])::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.scrollbar[data-orientation='vertical']:not([data-overflow-y-end])::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.scrollbar[data-orientation='horizontal']:not([data-overflow-x-start])::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.scrollbar[data-orientation='horizontal']:not([data-overflow-x-end])::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.scrollbar[data-hovering] > [data-orientation],
|
||||
.scrollbar[data-scrolling] > [data-orientation],
|
||||
.scrollbar > [data-orientation]:active {
|
||||
background-color: var(--scroll-area-thumb-bg-active, var(--color-state-base-handle-hover));
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.scrollbar::before,
|
||||
.scrollbar::after {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import type { ReactNode } from 'react'
|
||||
import * as React from 'react'
|
||||
import AppIcon from '@/app/components/base/app-icon'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import {
|
||||
|
|
@ -78,6 +79,16 @@ const activityRows = Array.from({ length: 14 }, (_, index) => ({
|
|||
body: 'A short line of copy to mimic dense operational feeds in settings and debug panels.',
|
||||
}))
|
||||
|
||||
const scrollbarShowcaseRows = Array.from({ length: 18 }, (_, index) => ({
|
||||
title: `Scroll checkpoint ${index + 1}`,
|
||||
body: 'Dedicated story content so the scrollbar can be inspected without sticky headers, masks, or clipped shells.',
|
||||
}))
|
||||
|
||||
const horizontalShowcaseCards = Array.from({ length: 8 }, (_, index) => ({
|
||||
title: `Lane ${index + 1}`,
|
||||
body: 'Horizontal scrollbar reference without edge hints.',
|
||||
}))
|
||||
|
||||
const webAppsRows = [
|
||||
{ id: 'invoice-copilot', name: 'Invoice Copilot', meta: 'Pinned', icon: '🧾', iconBackground: '#FFEAD5', selected: true, pinned: true },
|
||||
{ id: 'rag-ops', name: 'RAG Ops Console', meta: 'Ops', icon: '🛰️', iconBackground: '#E0F2FE', selected: false, pinned: true },
|
||||
|
|
@ -255,6 +266,112 @@ const HorizontalRailPane = () => (
|
|||
</div>
|
||||
)
|
||||
|
||||
const ScrollbarStatePane = ({
|
||||
eyebrow,
|
||||
title,
|
||||
description,
|
||||
initialPosition,
|
||||
}: {
|
||||
eyebrow: string
|
||||
title: string
|
||||
description: string
|
||||
initialPosition: 'top' | 'middle' | 'bottom'
|
||||
}) => {
|
||||
const viewportId = React.useId()
|
||||
|
||||
React.useEffect(() => {
|
||||
let frameA = 0
|
||||
let frameB = 0
|
||||
|
||||
const syncScrollPosition = () => {
|
||||
const viewport = document.getElementById(viewportId)
|
||||
|
||||
if (!(viewport instanceof HTMLDivElement))
|
||||
return
|
||||
|
||||
const maxScrollTop = Math.max(0, viewport.scrollHeight - viewport.clientHeight)
|
||||
|
||||
if (initialPosition === 'top')
|
||||
viewport.scrollTop = 0
|
||||
|
||||
if (initialPosition === 'middle')
|
||||
viewport.scrollTop = maxScrollTop / 2
|
||||
|
||||
if (initialPosition === 'bottom')
|
||||
viewport.scrollTop = maxScrollTop
|
||||
}
|
||||
|
||||
frameA = requestAnimationFrame(() => {
|
||||
frameB = requestAnimationFrame(syncScrollPosition)
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(frameA)
|
||||
cancelAnimationFrame(frameB)
|
||||
}
|
||||
}, [initialPosition, viewportId])
|
||||
|
||||
return (
|
||||
<div className="min-w-0 rounded-[28px] border border-divider-subtle bg-background-body p-5">
|
||||
<div className="space-y-1">
|
||||
<div className={labelClassName}>{eyebrow}</div>
|
||||
<div className="text-text-primary system-md-semibold">{title}</div>
|
||||
<p className="text-text-secondary system-sm-regular">{description}</p>
|
||||
</div>
|
||||
<div className="mt-4 min-w-0 rounded-[24px] border border-divider-subtle bg-components-panel-bg p-3">
|
||||
<ScrollArea className="h-[320px] p-1">
|
||||
<ScrollAreaViewport id={viewportId} className="rounded-[20px] bg-components-panel-bg">
|
||||
<ScrollAreaContent className="min-w-0 space-y-2 p-4 pr-6">
|
||||
{scrollbarShowcaseRows.map(item => (
|
||||
<article key={item.title} className="min-w-0 rounded-xl border border-divider-subtle bg-components-panel-bg-alt p-3">
|
||||
<div className="truncate text-text-primary system-sm-semibold">{item.title}</div>
|
||||
<div className="mt-1 break-words text-text-secondary system-sm-regular">{item.body}</div>
|
||||
</article>
|
||||
))}
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar className={insetScrollbarClassName}>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const HorizontalScrollbarShowcasePane = () => (
|
||||
<div className="min-w-0 rounded-[28px] border border-divider-subtle bg-background-body p-5">
|
||||
<div className="space-y-1">
|
||||
<div className={labelClassName}>Horizontal</div>
|
||||
<div className="text-text-primary system-md-semibold">Horizontal track reference</div>
|
||||
<p className="text-text-secondary system-sm-regular">Current design delivery defines the horizontal scrollbar body, but not a horizontal edge hint.</p>
|
||||
</div>
|
||||
<div className="mt-4 min-w-0 rounded-[24px] border border-divider-subtle bg-components-panel-bg p-3">
|
||||
<ScrollArea className="h-[240px] p-1">
|
||||
<ScrollAreaViewport className="rounded-[20px] bg-components-panel-bg">
|
||||
<ScrollAreaContent className="min-h-full min-w-max space-y-4 p-4 pb-6">
|
||||
<div className="space-y-1">
|
||||
<div className="text-text-primary system-sm-semibold">Horizontal scrollbar</div>
|
||||
<div className="text-text-secondary system-sm-regular">A clean horizontal pane to inspect thickness, padding, and thumb behavior without extra masks.</div>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
{horizontalShowcaseCards.map(card => (
|
||||
<article key={card.title} className="flex h-[120px] w-[220px] shrink-0 flex-col justify-between rounded-2xl border border-divider-subtle bg-components-panel-bg-alt p-4">
|
||||
<div className="text-text-primary system-sm-semibold">{card.title}</div>
|
||||
<div className="text-text-secondary system-sm-regular">{card.body}</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</ScrollAreaContent>
|
||||
</ScrollAreaViewport>
|
||||
<ScrollAreaScrollbar orientation="horizontal" className={insetScrollbarClassName}>
|
||||
<ScrollAreaThumb />
|
||||
</ScrollAreaScrollbar>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
const OverlayPane = () => (
|
||||
<div className="flex h-[420px] min-w-0 items-center justify-center rounded-[28px] bg-[radial-gradient(circle_at_top,_rgba(21,90,239,0.12),_transparent_45%),linear-gradient(180deg,rgba(16,24,40,0.03),transparent)] p-6">
|
||||
<div className={cn(blurPanelClassName, 'w-full max-w-[360px]')}>
|
||||
|
|
@ -561,3 +678,35 @@ export const PrimitiveComposition: Story = {
|
|||
</StoryCard>
|
||||
),
|
||||
}
|
||||
|
||||
export const ScrollbarDelivery: Story = {
|
||||
render: () => (
|
||||
<StoryCard
|
||||
eyebrow="Scrollbar"
|
||||
title="Dedicated scrollbar delivery review"
|
||||
description="Three vertical panes pin the viewport to top, middle, and bottom so the edge hint can be inspected without sticky headers, viewport masks, or clipped shells. A separate horizontal pane shows the current non-edge-hint track."
|
||||
>
|
||||
<div className="grid gap-5 xl:grid-cols-2">
|
||||
<ScrollbarStatePane
|
||||
eyebrow="Top"
|
||||
title="At top edge"
|
||||
description="Top edge hint should sit exactly on the handle area edge."
|
||||
initialPosition="top"
|
||||
/>
|
||||
<ScrollbarStatePane
|
||||
eyebrow="Middle"
|
||||
title="Away from edges"
|
||||
description="No edge hint should be visible when the viewport is not pinned to either end."
|
||||
initialPosition="middle"
|
||||
/>
|
||||
<ScrollbarStatePane
|
||||
eyebrow="Bottom"
|
||||
title="At bottom edge"
|
||||
description="Bottom edge hint should sit exactly on the handle area edge."
|
||||
initialPosition="bottom"
|
||||
/>
|
||||
<HorizontalScrollbarShowcasePane />
|
||||
</div>
|
||||
</StoryCard>
|
||||
),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { ScrollArea as BaseScrollArea } from '@base-ui/react/scroll-area'
|
||||
import * as React from 'react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import styles from './index.module.css'
|
||||
|
||||
export const ScrollArea = BaseScrollArea.Root
|
||||
export type ScrollAreaRootProps = React.ComponentPropsWithRef<typeof BaseScrollArea.Root>
|
||||
|
|
@ -11,16 +12,16 @@ export const ScrollAreaContent = BaseScrollArea.Content
|
|||
export type ScrollAreaContentProps = React.ComponentPropsWithRef<typeof BaseScrollArea.Content>
|
||||
|
||||
export const scrollAreaScrollbarClassName = cn(
|
||||
'flex touch-none select-none opacity-0 transition-opacity motion-reduce:transition-none',
|
||||
'pointer-events-none data-[hovering]:pointer-events-auto data-[hovering]:opacity-100',
|
||||
'data-[scrolling]:pointer-events-auto data-[scrolling]:opacity-100',
|
||||
'hover:pointer-events-auto hover:opacity-100',
|
||||
styles.scrollbar,
|
||||
'flex touch-none select-none overflow-clip p-1 opacity-100 transition-opacity motion-reduce:transition-none',
|
||||
'pointer-events-none data-[hovering]:pointer-events-auto',
|
||||
'data-[scrolling]:pointer-events-auto',
|
||||
'data-[orientation=vertical]:absolute data-[orientation=vertical]:inset-y-0 data-[orientation=vertical]:w-3 data-[orientation=vertical]:justify-center',
|
||||
'data-[orientation=horizontal]:absolute data-[orientation=horizontal]:inset-x-0 data-[orientation=horizontal]:h-3 data-[orientation=horizontal]:items-center',
|
||||
)
|
||||
|
||||
export const scrollAreaThumbClassName = cn(
|
||||
'shrink-0 rounded-[4px] bg-state-base-handle transition-[background-color] hover:bg-state-base-handle-hover motion-reduce:transition-none',
|
||||
'shrink-0 rounded-[4px] bg-state-base-handle transition-[background-color] motion-reduce:transition-none',
|
||||
'data-[orientation=vertical]:w-1',
|
||||
'data-[orientation=horizontal]:h-1',
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue