refactor(ui): simplify scroll area API (#39857)

This commit is contained in:
yyh 2026-07-31 20:43:11 +08:00 committed by GitHub
parent c62ab071eb
commit 7e30aba12a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 1467 additions and 1463 deletions

View File

@ -19,8 +19,8 @@ import { Field, FieldControl, FieldDescription, FieldError, FieldLabel } from '.
import { Form } from '../form'
import { Input } from '../input'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -334,7 +334,7 @@ const OutsideScrollingContentDemo = () => {
<DialogPortal>
<DialogBackdrop className="duration-600 ease-[cubic-bezier(0.22,1,0.36,1)] data-ending-style:duration-350 data-ending-style:ease-[cubic-bezier(0.375,0.015,0.545,0.455)]" />
<DialogViewport className="group/dialog">
<ScrollAreaRoot className="h-full overscroll-contain group-data-ending-style/dialog:pointer-events-none">
<ScrollArea className="h-full group-data-ending-style/dialog:pointer-events-none">
<ScrollAreaViewport
aria-label="Scrollable dialog viewport"
role="region"
@ -359,7 +359,7 @@ const OutsideScrollingContentDemo = () => {
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
</DialogViewport>
</DialogPortal>
</Dialog>
@ -418,11 +418,11 @@ const InsideScrollingContentDemo = () => {
title="Release notes"
description="Highlights from the latest workspace update."
/>
<ScrollAreaRoot className="relative flex min-h-0 flex-auto overflow-hidden">
<ScrollArea className="relative flex min-h-0 flex-auto overflow-hidden">
<ScrollAreaViewport
aria-label="Release note improvements"
role="region"
className="h-full max-h-full max-w-full overflow-y-auto overscroll-contain"
className="h-full max-h-full max-w-full overscroll-contain"
>
<ScrollAreaContent>
<ReleaseNoteSections />
@ -431,7 +431,7 @@ const InsideScrollingContentDemo = () => {
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
<ReleaseNoteFooter onClose={() => setOpen(false)} />
</DialogPopup>
</DialogViewport>

View File

@ -23,8 +23,8 @@ import { Button } from '../button'
import { cn } from '../cn'
import { Input } from '../input'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -723,16 +723,19 @@ export const MobileNavigation: Story = {
<DrawerPortal>
<DrawerBackdrop className="fixed" />
<DrawerViewport className="group">
<ScrollAreaRoot
<ScrollArea
style={{ position: undefined }}
className="h-full overscroll-contain transition-transform duration-600 ease-[cubic-bezier(0.45,1.005,0,1.005)] group-data-ending-style:pointer-events-none group-data-starting-style:translate-y-[100dvh] motion-reduce:transition-none"
className="h-full transition-transform duration-600 ease-[cubic-bezier(0.45,1.005,0,1.005)] group-data-ending-style:pointer-events-none group-data-starting-style:translate-y-[100dvh] motion-reduce:transition-none"
>
<ScrollAreaViewport
className="size-full touch-auto overscroll-contain"
role="region"
aria-label="Mobile drawer viewport"
>
<ScrollAreaContent className="flex min-h-full min-w-0 items-end justify-center pt-8 min-[42rem]:px-16 min-[42rem]:py-16">
<ScrollAreaContent
style={{ minWidth: 0 }}
className="flex min-h-full items-end justify-center pt-8 min-[42rem]:px-16 min-[42rem]:py-16"
>
<DrawerPopup className="relative w-full max-w-2xl touch-auto overflow-visible transition-transform duration-600 ease-[cubic-bezier(0.45,1.005,0,1.005)] data-ending-style:duration-350 data-ending-style:ease-[cubic-bezier(0.375,0.015,0.545,0.455)] data-swiping:select-none data-[swipe-direction=down]:inset-auto data-[swipe-direction=down]:max-h-none data-[swipe-direction=down]:transform-[translateY(var(--drawer-swipe-movement-y,0px))] data-ending-style:data-[swipe-direction=down]:transform-[translateY(calc(max(100dvh,100%)+2px))] motion-reduce:transition-none min-[42rem]:rounded-2xl min-[42rem]:border-[0.5px] min-[42rem]:border-b-[0.5px]">
<nav
aria-label="Mobile navigation"
@ -789,7 +792,7 @@ export const MobileNavigation: Story = {
<ScrollAreaScrollbar orientation="vertical">
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
</DrawerViewport>
</DrawerPortal>
</Drawer>

View File

@ -1,188 +0,0 @@
// Compile-time public API contracts; included by package typecheck and excluded from package exports.
import type {
AutocompleteCollection,
AutocompleteCollectionProps,
AutocompleteGroup,
AutocompleteGroupProps,
AutocompleteItemProps,
AutocompleteList,
AutocompleteListProps,
AutocompleteProps,
} from './autocomplete'
import type {
ComboboxCollection,
ComboboxCollectionProps,
ComboboxGroup,
ComboboxGroupProps,
ComboboxItemProps,
ComboboxList,
ComboboxListProps,
ComboboxProps,
ComboboxValue,
ComboboxValueProps,
} from './combobox'
import type { ContextMenuRadioGroupProps, ContextMenuRadioItemProps } from './context-menu'
import type { DialogHandle, DialogTriggerProps } from './dialog'
import type { DrawerHandle, DrawerTriggerProps } from './drawer'
import type { DropdownMenuRadioGroupProps, DropdownMenuRadioItemProps } from './dropdown-menu'
import type { FormProps } from './form'
import type { PopoverHandle, PopoverTriggerProps } from './popover'
import type { PreviewCardHandle, PreviewCardTriggerProps } from './preview-card'
import type { RadioGroupProps, RadioItemProps } from './radio'
import type { SelectItemProps, SelectProps, SelectValue, SelectValueProps } from './select'
import type { SliderRootProps } from './slider'
type Equal<Left, Right> =
(<Value>() => Value extends Left ? 1 : 2) extends <Value>() => Value extends Right ? 1 : 2
? true
: false
type Expect<Condition extends true> = Condition
type Arguments<Callback> = Callback extends (...args: infer Values) => unknown ? Values : never
type FirstArgument<Callback> = Arguments<Callback>[0]
type SecondArgument<Callback> = Arguments<Callback>[1]
type IsRequired<Props, Key extends keyof Props> =
Record<never, never> extends Pick<Props, Key> ? false : true
type Callable<Member> = Member extends (...args: infer Values) => infer Result
? (...args: Values) => Result
: never
type DomainValue = { id: string }
type FormValues = { enabled: boolean; name: string }
type RangeValue = readonly [number, number]
type OverlayPayload = { sourceId: string }
type SelectSingleValueProps = Parameters<typeof SelectValue<DomainValue>>[0]
type SelectMultipleValueProps = Parameters<typeof SelectValue<DomainValue, true>>[0]
type SelectDynamicValueProps = Parameters<typeof SelectValue<DomainValue, boolean>>[0]
type ComboboxSingleValueProps = Parameters<typeof ComboboxValue<DomainValue>>[0]
type ComboboxMultipleValueProps = Parameters<typeof ComboboxValue<DomainValue, true>>[0]
type ComboboxDynamicValueProps = Parameters<typeof ComboboxValue<DomainValue, boolean>>[0]
type AutocompleteListRuntimeProps = Parameters<typeof AutocompleteList<DomainValue>>[0]
type AutocompleteCollectionRuntimeProps = Parameters<typeof AutocompleteCollection<DomainValue>>[0]
type AutocompleteGroupRuntimeProps = Parameters<typeof AutocompleteGroup<DomainValue>>[0]
type ComboboxListRuntimeProps = Parameters<typeof ComboboxList<DomainValue>>[0]
type ComboboxCollectionRuntimeProps = Parameters<typeof ComboboxCollection<DomainValue>>[0]
type ComboboxGroupRuntimeProps = Parameters<typeof ComboboxGroup<DomainValue>>[0]
type PublicTypeAssertions = [
Expect<Equal<FirstArgument<NonNullable<FormProps<FormValues>['onFormSubmit']>>, FormValues>>,
Expect<
Equal<
FirstArgument<NonNullable<SelectProps<DomainValue, false>['onValueChange']>>,
DomainValue | null
>
>,
Expect<
Equal<
FirstArgument<NonNullable<SelectProps<DomainValue, true>['onValueChange']>>,
DomainValue[]
>
>,
Expect<Equal<IsRequired<SelectProps<DomainValue, true>, 'multiple'>, true>>,
Expect<Equal<IsRequired<SelectProps<DomainValue, boolean>, 'multiple'>, false>>,
Expect<Equal<SelectItemProps<DomainValue>['value'], DomainValue | undefined>>,
Expect<
Equal<
FirstArgument<Callable<SelectValueProps<DomainValue, true>['children']>>,
DomainValue[] | null
>
>,
Expect<Equal<FirstArgument<Callable<SelectSingleValueProps['children']>>, DomainValue | null>>,
Expect<
Equal<FirstArgument<Callable<SelectMultipleValueProps['children']>>, DomainValue[] | null>
>,
Expect<
Equal<
FirstArgument<Callable<SelectDynamicValueProps['children']>>,
DomainValue | DomainValue[] | null
>
>,
Expect<
Equal<
FirstArgument<NonNullable<ComboboxProps<DomainValue, true>['onValueChange']>>,
DomainValue[]
>
>,
Expect<Equal<IsRequired<ComboboxProps<DomainValue, true>, 'multiple'>, true>>,
Expect<Equal<IsRequired<ComboboxProps<DomainValue, boolean>, 'multiple'>, false>>,
Expect<Equal<ComboboxItemProps<DomainValue>['value'], DomainValue | undefined>>,
Expect<
Equal<
FirstArgument<Callable<ComboboxValueProps<DomainValue, true>['children']>>,
DomainValue[] | null
>
>,
Expect<Equal<FirstArgument<Callable<ComboboxSingleValueProps['children']>>, DomainValue | null>>,
Expect<
Equal<FirstArgument<Callable<ComboboxMultipleValueProps['children']>>, DomainValue[] | null>
>,
Expect<
Equal<
FirstArgument<Callable<ComboboxDynamicValueProps['children']>>,
DomainValue | DomainValue[] | null
>
>,
Expect<
Equal<
FirstArgument<NonNullable<AutocompleteProps<DomainValue>['onItemHighlighted']>>,
DomainValue | undefined
>
>,
Expect<Equal<AutocompleteItemProps<DomainValue>['value'], DomainValue | undefined>>,
Expect<
Equal<FirstArgument<Callable<AutocompleteListProps<DomainValue>['children']>>, DomainValue>
>,
Expect<Equal<SecondArgument<Callable<AutocompleteListProps<DomainValue>['children']>>, number>>,
Expect<Equal<FirstArgument<Callable<AutocompleteListRuntimeProps['children']>>, DomainValue>>,
Expect<Equal<FirstArgument<AutocompleteCollectionProps<DomainValue>['children']>, DomainValue>>,
Expect<Equal<SecondArgument<AutocompleteCollectionProps<DomainValue>['children']>, number>>,
Expect<Equal<FirstArgument<AutocompleteCollectionRuntimeProps['children']>, DomainValue>>,
Expect<Equal<AutocompleteGroupProps<DomainValue>['items'], readonly DomainValue[] | undefined>>,
Expect<Equal<AutocompleteGroupRuntimeProps['items'], readonly DomainValue[] | undefined>>,
Expect<Equal<FirstArgument<Callable<ComboboxListProps<DomainValue>['children']>>, DomainValue>>,
Expect<Equal<SecondArgument<Callable<ComboboxListProps<DomainValue>['children']>>, number>>,
Expect<Equal<FirstArgument<Callable<ComboboxListRuntimeProps['children']>>, DomainValue>>,
Expect<Equal<FirstArgument<ComboboxCollectionProps<DomainValue>['children']>, DomainValue>>,
Expect<Equal<SecondArgument<ComboboxCollectionProps<DomainValue>['children']>, number>>,
Expect<Equal<FirstArgument<ComboboxCollectionRuntimeProps['children']>, DomainValue>>,
Expect<Equal<ComboboxGroupProps<DomainValue>['items'], readonly DomainValue[] | undefined>>,
Expect<Equal<ComboboxGroupRuntimeProps['items'], readonly DomainValue[] | undefined>>,
Expect<
Equal<
FirstArgument<NonNullable<ContextMenuRadioGroupProps<DomainValue>['onValueChange']>>,
DomainValue
>
>,
Expect<Equal<ContextMenuRadioItemProps<DomainValue>['value'], DomainValue>>,
Expect<
Equal<
FirstArgument<NonNullable<DropdownMenuRadioGroupProps<DomainValue>['onValueChange']>>,
DomainValue
>
>,
Expect<Equal<DropdownMenuRadioItemProps<DomainValue>['value'], DomainValue>>,
Expect<
Equal<FirstArgument<NonNullable<RadioGroupProps<DomainValue>['onValueChange']>>, DomainValue>
>,
Expect<Equal<RadioItemProps<DomainValue>['value'], DomainValue>>,
Expect<Equal<SliderRootProps<RangeValue>['value'], RangeValue | undefined>>,
Expect<
Equal<FirstArgument<NonNullable<SliderRootProps<RangeValue>['onValueChange']>>, RangeValue>
>,
Expect<
Equal<NonNullable<DialogTriggerProps<OverlayPayload>['handle']>, DialogHandle<OverlayPayload>>
>,
Expect<
Equal<NonNullable<DrawerTriggerProps<OverlayPayload>['handle']>, DrawerHandle<OverlayPayload>>
>,
Expect<
Equal<NonNullable<PopoverTriggerProps<OverlayPayload>['handle']>, PopoverHandle<OverlayPayload>>
>,
Expect<
Equal<
NonNullable<PreviewCardTriggerProps<OverlayPayload>['handle']>,
PreviewCardHandle<OverlayPayload>
>
>,
]
export type { PublicTypeAssertions }

View File

@ -1,10 +1,9 @@
import * as React from 'react'
import type { CSSProperties, UIEvent } from 'react'
import { render } from 'vitest-browser-react'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -35,7 +34,9 @@ const stubElementMetric = (
const renderScrollArea = (
options: {
rootClassName?: string
contentStyle?: CSSProperties
viewportClassName?: string
viewportStyle?: CSSProperties
verticalScrollbarClassName?: string
horizontalScrollbarClassName?: string
verticalThumbClassName?: string
@ -43,9 +44,13 @@ const renderScrollArea = (
} = {},
) => {
return render(
<ScrollAreaRoot className={options.rootClassName ?? 'h-40 w-40'} data-testid="scroll-area-root">
<ScrollAreaViewport data-testid="scroll-area-viewport" className={options.viewportClassName}>
<ScrollAreaContent data-testid="scroll-area-content">
<ScrollArea className={options.rootClassName ?? 'h-40 w-40'} data-testid="scroll-area-root">
<ScrollAreaViewport
data-testid="scroll-area-viewport"
style={options.viewportStyle}
className={options.viewportClassName}
>
<ScrollAreaContent data-testid="scroll-area-content" style={options.contentStyle}>
<div className="h-48 w-48">Scrollable content</div>
</ScrollAreaContent>
</ScrollAreaViewport>
@ -70,11 +75,11 @@ const renderScrollArea = (
className={options.horizontalThumbClassName}
/>
</ScrollAreaScrollbar>
</ScrollAreaRoot>,
</ScrollArea>,
)
}
describe('scroll-area wrapper', () => {
describe('scroll area', () => {
describe('Rendering', () => {
it('should render the compound exports together', async () => {
const screen = await renderScrollArea()
@ -92,34 +97,90 @@ describe('scroll-area wrapper', () => {
await expect.element(screen.getByTestId('scroll-area-horizontal-thumb')).toBeInTheDocument()
})
it('should render the convenience wrapper and apply slot props', async () => {
it('should keep accessible region semantics on the viewport', async () => {
const screen = await render(
<React.Fragment>
<>
<p id="installed-apps-label">Installed apps</p>
<ScrollArea
className="h-40 w-40"
slotClassNames={{
content: 'custom-content-class',
scrollbar: 'custom-scrollbar-class',
viewport: 'custom-viewport-class',
}}
labelledBy="installed-apps-label"
data-testid="scroll-area-wrapper-root"
>
<div className="h-48 w-20">Scrollable content</div>
<ScrollArea className="h-40 w-40" data-testid="scroll-area-root">
<ScrollAreaViewport
aria-labelledby="installed-apps-label"
className="custom-viewport-class"
role="region"
>
<ScrollAreaContent className="custom-content-class">
<div className="h-48 w-20">Scrollable content</div>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar className="custom-scrollbar-class">
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</React.Fragment>,
</>,
)
const viewport = screen.getByRole('region', { name: 'Installed apps' })
const content = screen.getByText('Scrollable content').element().parentElement
await expect.element(screen.getByTestId('scroll-area-wrapper-root')).toBeInTheDocument()
await expect
.element(screen.getByTestId('scroll-area-root'))
.not.toHaveAttribute('role', 'region')
await expect.element(viewport).toHaveClass('custom-viewport-class')
await expect.element(viewport).toHaveAccessibleName('Installed apps')
expect(content).toHaveClass('custom-content-class')
await expect.element(screen.getByText('Scrollable content')).toBeInTheDocument()
})
it('should keep scrolling, focus, events, and refs on the viewport', async () => {
let rootElement: HTMLDivElement | null = null
let viewportElement: HTMLDivElement | null = null
let scrollOwner: HTMLDivElement | null = null
const onScroll = vi.fn((event: UIEvent<HTMLDivElement>) => {
scrollOwner = event.currentTarget
})
await render(
<ScrollArea
ref={(node) => {
rootElement = node
}}
style={{ height: 100, width: 100 }}
>
<ScrollAreaViewport
ref={(node) => {
viewportElement = node
}}
aria-label="Scrollable results"
onScroll={onScroll}
role="region"
style={{ height: '100%', width: '100%' }}
>
<ScrollAreaContent style={{ minWidth: 0 }}>
<div style={{ height: 200, width: 100 }}>Scrollable content</div>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>,
)
await vi.waitFor(() => {
expect(viewportElement).not.toBeNull()
expect(viewportElement!.scrollHeight).toBeGreaterThan(viewportElement!.clientHeight)
expect(viewportElement).toHaveAttribute('tabindex', '0')
})
viewportElement!.focus()
expect(document.activeElement).toBe(viewportElement)
viewportElement!.scrollTop = 40
await vi.waitFor(() => expect(onScroll).toHaveBeenCalled())
expect(viewportElement!.scrollTop).toBe(40)
expect(scrollOwner).toBe(viewportElement)
expect(rootElement).not.toBe(viewportElement)
expect(rootElement).not.toHaveAttribute('tabindex')
})
})
describe('Scrollbar', () => {
@ -178,6 +239,22 @@ describe('scroll-area wrapper', () => {
.element(screen.getByTestId('scroll-area-horizontal-scrollbar'))
.toHaveClass('data-[orientation=horizontal]:mx-2', 'data-[orientation=horizontal]:mb-2')
})
it('should let vertical layouts override the content minimum width without important CSS', async () => {
const screen = await renderScrollArea({ contentStyle: { minWidth: 0 } })
const content = screen.getByTestId('scroll-area-content').element()
expect(getComputedStyle(content).minWidth).toBe('0px')
expect(content.style.getPropertyPriority('min-width')).toBe('')
})
it('should let callers constrain a viewport axis without important CSS', async () => {
const screen = await renderScrollArea({ viewportStyle: { overflowX: 'hidden' } })
const viewport = screen.getByTestId('scroll-area-viewport').element()
expect(getComputedStyle(viewport).overflowX).toBe('hidden')
expect(viewport.style.getPropertyPriority('overflow-x')).toBe('')
})
})
describe('Corner', () => {
@ -186,7 +263,7 @@ describe('scroll-area wrapper', () => {
try {
const screen = await render(
<ScrollAreaRoot className="h-40 w-40" data-testid="scroll-area-root">
<ScrollArea className="h-40 w-40" data-testid="scroll-area-root">
<ScrollAreaViewport
data-testid="scroll-area-viewport"
ref={(node) => {
@ -215,7 +292,7 @@ describe('scroll-area wrapper', () => {
<ScrollAreaThumb data-testid="scroll-area-horizontal-thumb" />
</ScrollAreaScrollbar>
<ScrollAreaCorner data-testid="scroll-area-corner" />
</ScrollAreaRoot>,
</ScrollArea>,
)
await vi.waitFor(() => {

View File

@ -1,9 +1,9 @@
import type { Meta, StoryObj } from '@storybook/react-vite'
import type * as React from 'react'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -12,18 +12,18 @@ import { cn } from '../cn'
const meta = {
title: 'Base/UI/ScrollArea',
component: ScrollAreaRoot,
component: ScrollArea,
parameters: {
layout: 'padded',
docs: {
description: {
component:
'Compound scroll container built on Base UI Scroll Area. The examples mirror the upstream anatomy and focus patterns while applying Dify UI tokens and surface treatments. Base UI ScrollArea.Content defaults to min-width: fit-content, so vertical-only regions that should truncate long content must set min-width: 0 on the content slot.',
'Compound scroll container built on Base UI Scroll Area. ScrollArea is the structural root; ScrollAreaViewport is the actual scroll and focus owner, so place scroll refs, events, role, accessible-name attributes, and overscroll behavior on the viewport. Add role="region" only when the content is important enough to be a landmark, preferring aria-labelledby when a visible heading exists. Base UI sets Viewport overflow and Content min-width inline: use style={{ overflowX: "hidden" }} for an axis override and style={{ minWidth: 0 }} for vertical content that should truncate; regular overflow-x-hidden and min-w-0 utilities cannot override those inline defaults. This is a targeted override contract for those Base UI defaults, not a general preference for inline styles over className.',
},
},
},
tags: ['autodocs'],
} satisfies Meta<typeof ScrollAreaRoot>
} satisfies Meta<typeof ScrollArea>
export default meta
type Story = StoryObj<typeof meta>
@ -54,12 +54,14 @@ const gridCells = Array.from({ length: 100 }, (_, index) => index + 1)
function StorySection({
eyebrow,
title,
titleId,
description,
children,
className,
}: {
eyebrow: string
title: string
titleId?: string
description: string
children: React.ReactNode
className?: string
@ -73,7 +75,9 @@ function StorySection({
>
<div className="space-y-1">
<div className="system-xs-medium-uppercase text-text-tertiary">{eyebrow}</div>
<h3 className="system-md-semibold text-text-primary">{title}</h3>
<h3 id={titleId} className="system-md-semibold text-text-primary">
{title}
</h3>
<p className="max-w-[72ch] system-sm-regular text-pretty text-text-secondary">
{description}
</p>
@ -91,10 +95,7 @@ function VerticalContent({
className?: string
}) {
return (
<ScrollAreaContent
style={verticalContentStyle}
className={cn('w-full max-w-full min-w-0', className)}
>
<ScrollAreaContent style={verticalContentStyle} className={cn('w-full max-w-full', className)}>
{children}
</ScrollAreaContent>
)
@ -105,11 +106,12 @@ export const Anatomy: Story = {
<StorySection
eyebrow="Anatomy"
title="Base UI compound parts"
description="The baseline story mirrors the official Scroll Area anatomy: Root, Viewport, Content, Scrollbar, and Thumb, with keyboard focus drawn by the viewport."
titleId="scroll-area-anatomy-title"
description="The baseline story uses ScrollArea as the structural root, with Viewport, Content, Scrollbar, and Thumb composed explicitly. Keyboard focus and accessible naming belong to the viewport."
>
<ScrollAreaRoot className="relative h-75 w-full max-w-105 min-w-0">
<ScrollArea className="relative h-75 w-full max-w-105 min-w-0">
<ScrollAreaViewport
aria-label="Scrollable anatomy example"
aria-labelledby="scroll-area-anatomy-title"
role="region"
className="h-full max-h-full max-w-full rounded-xl border-[0.5px] border-divider-subtle bg-components-panel-bg"
>
@ -122,7 +124,7 @@ export const Anatomy: Story = {
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
</StorySection>
),
}
@ -134,7 +136,7 @@ export const Vertical: Story = {
title="Long form content"
description="Vertical overflow keeps the official viewport focus pattern while constraining content width so text never leaks outside the frame."
>
<ScrollAreaRoot className="relative h-90 w-full max-w-130 min-w-0">
<ScrollArea className="relative h-90 w-full max-w-130 min-w-0">
<ScrollAreaViewport
aria-label="Long form content"
role="region"
@ -155,7 +157,7 @@ export const Vertical: Story = {
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
</StorySection>
),
}
@ -165,9 +167,9 @@ export const VerticalTruncation: Story = {
<StorySection
eyebrow="Vertical"
title="Constrained content width"
description="Use width constraints plus minWidth: 0 on ScrollArea.Content when a vertical-only list should keep vertical scrolling while truncating long labels instead of creating horizontal scroll."
description="Pass style={{ minWidth: 0 }} to ScrollAreaContent when a vertical-only list should truncate long labels instead of creating horizontal scroll. A regular min-w-0 utility cannot override Base UI's inline fit-content default."
>
<ScrollAreaRoot className="relative h-48 w-full max-w-80 min-w-0">
<ScrollArea className="relative h-48 w-full max-w-80 min-w-0">
<ScrollAreaViewport
aria-label="Vertical file list"
role="region"
@ -190,7 +192,7 @@ export const VerticalTruncation: Story = {
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
</StorySection>
),
}
@ -202,7 +204,7 @@ export const ScrollFade: Story = {
title="Viewport mask with root focus"
description="This mirrors the Base UI scroll-fade example: the viewport owns the mask and the root owns the focus outline so the indicator is never clipped."
>
<ScrollAreaRoot
<ScrollArea
className={cn(
'relative h-90 w-full max-w-130 min-w-0',
'has-[>_:first-child:focus-visible]:outline-2 has-[>_:first-child:focus-visible]:outline-offset-0 has-[>_:first-child:focus-visible]:outline-state-accent-solid',
@ -227,7 +229,7 @@ export const ScrollFade: Story = {
<ScrollAreaScrollbar className="opacity-0 data-hovering:opacity-100 data-scrolling:opacity-100 data-scrolling:duration-0">
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
</StorySection>
),
}
@ -240,7 +242,7 @@ export const Horizontal: Story = {
description="Horizontal overflow keeps Base UI's content sizing behavior and uses the same viewport focus treatment on the scrollable element."
className="mx-auto max-w-190"
>
<ScrollAreaRoot className="relative h-46 w-full max-w-130 min-w-0">
<ScrollArea className="relative h-46 w-full max-w-130 min-w-0">
<ScrollAreaViewport
aria-label="Horizontal numbered row"
role="region"
@ -262,7 +264,7 @@ export const Horizontal: Story = {
<ScrollAreaScrollbar orientation="horizontal">
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
</StorySection>
),
}
@ -274,7 +276,7 @@ export const BothAxes: Story = {
title="Numbered grid"
description="This follows the official two-axis example: both scrollbars are rendered and Corner reserves the intersection."
>
<ScrollAreaRoot className="relative h-85 w-full max-w-140 min-w-0">
<ScrollArea className="relative h-85 w-full max-w-140 min-w-0">
<ScrollAreaViewport
aria-label="Numbered grid"
role="region"
@ -300,7 +302,7 @@ export const BothAxes: Story = {
<ScrollAreaThumb />
</ScrollAreaScrollbar>
<ScrollAreaCorner />
</ScrollAreaRoot>
</ScrollArea>
</StorySection>
),
}

View File

@ -1,30 +1,14 @@
'use client'
import type * as React from 'react'
import { ScrollArea as BaseScrollArea } from '@base-ui/react/scroll-area'
import { cn } from '../cn'
const ScrollAreaRoot = BaseScrollArea.Root
type ScrollAreaRootProps = BaseScrollArea.Root.Props
const ScrollArea = BaseScrollArea.Root
type ScrollAreaProps = BaseScrollArea.Root.Props
const ScrollAreaContent = BaseScrollArea.Content
type ScrollAreaContentProps = BaseScrollArea.Content.Props
type ScrollAreaSlotClassNames = {
viewport?: string
content?: string
scrollbar?: string
}
type ScrollAreaOrientation = NonNullable<BaseScrollArea.Scrollbar.Props['orientation']>
type ScrollAreaProps = Omit<ScrollAreaRootProps, 'children'> & {
children: React.ReactNode
orientation?: ScrollAreaOrientation
slotClassNames?: ScrollAreaSlotClassNames
label?: string
labelledBy?: string
}
const scrollAreaScrollbarClassName = cn(
'group/scrollbar flex touch-none overflow-clip p-1 opacity-100 transition-opacity select-none motion-reduce:transition-none',
'pointer-events-none data-hovering:pointer-events-auto',
@ -89,37 +73,10 @@ function ScrollAreaCorner({ className, ...props }: ScrollAreaCornerProps) {
return <BaseScrollArea.Corner className={cn(scrollAreaCornerClassName, className)} {...props} />
}
function ScrollArea({
children,
className,
orientation = 'vertical',
slotClassNames,
label,
labelledBy,
...props
}: ScrollAreaProps) {
return (
<ScrollAreaRoot className={className} {...props}>
<ScrollAreaViewport
aria-label={label}
aria-labelledby={labelledBy}
className={slotClassNames?.viewport}
role={label || labelledBy ? 'region' : undefined}
>
<ScrollAreaContent className={slotClassNames?.content}>{children}</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar orientation={orientation} className={slotClassNames?.scrollbar}>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
)
}
export {
ScrollArea,
ScrollAreaContent,
ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -129,7 +86,6 @@ export type {
ScrollAreaContentProps,
ScrollAreaCornerProps,
ScrollAreaProps,
ScrollAreaRootProps,
ScrollAreaScrollbarProps,
ScrollAreaThumbProps,
ScrollAreaViewportProps,

View File

@ -1,7 +1,13 @@
'use client'
import type { ResourceOpenScope } from '@/models/access-control'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useCallback, useMemo, useState } from 'react'
@ -89,35 +95,39 @@ const AppAccessConfigContent = ({ appId, maintainerId }: AppAccessConfigContentP
)
return (
<ScrollArea
className="h-full bg-background-default-subtle"
slotClassNames={{ viewport: 'overscroll-contain' }}
>
<header className="flex min-h-15.5 flex-col justify-center px-6 py-3">
<h1 className="system-xl-semibold text-text-primary">
{t(($) => $['settings.resourceAccess'], { ns: 'common' })}
</h1>
<p className="mt-0.5 system-sm-regular text-text-tertiary">
{t(($) => $['accessRule.appDescription'], { ns: 'permission' })}
</p>
</header>
<main className="w-full px-6 pt-8 pb-10">
<AccessRulesEditor
className="w-full max-w-200"
rules={appAccessRules}
userAccessSettings={appUserAccessSettings}
isLoadingRules={isLoadingAppAccessRules}
isLoadingUserAccessSettings={isLoadingAppUserAccessSettings}
openScope={openScope}
isUpdatingOpenScope={isLoadingAppUserAccessSettings || isUpdatingAppOpenScope}
updatingAccountId={updatingAccountId}
maintainerId={maintainerId}
onOpenScopeChange={handleOpenScopeChange}
onUserAccessPoliciesChange={handleUserAccessPoliciesChange}
onRemoveAccessPolicyMemberBinding={handleRemoveAccessPolicyMemberBinding}
onAddAccessSubject={handleUserAccessPoliciesChange}
/>
</main>
<ScrollArea className="h-full bg-background-default-subtle">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent>
<header className="flex min-h-15.5 flex-col justify-center px-6 py-3">
<h1 className="system-xl-semibold text-text-primary">
{t(($) => $['settings.resourceAccess'], { ns: 'common' })}
</h1>
<p className="mt-0.5 system-sm-regular text-text-tertiary">
{t(($) => $['accessRule.appDescription'], { ns: 'permission' })}
</p>
</header>
<main className="w-full px-6 pt-8 pb-10">
<AccessRulesEditor
className="w-full max-w-200"
rules={appAccessRules}
userAccessSettings={appUserAccessSettings}
isLoadingRules={isLoadingAppAccessRules}
isLoadingUserAccessSettings={isLoadingAppUserAccessSettings}
openScope={openScope}
isUpdatingOpenScope={isLoadingAppUserAccessSettings || isUpdatingAppOpenScope}
updatingAccountId={updatingAccountId}
maintainerId={maintainerId}
onOpenScopeChange={handleOpenScopeChange}
onUserAccessPoliciesChange={handleUserAccessPoliciesChange}
onRemoveAccessPolicyMemberBinding={handleRemoveAccessPolicyMemberBinding}
onAddAccessSubject={handleUserAccessPoliciesChange}
/>
</main>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
)
}

View File

@ -8,7 +8,13 @@ import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgeni
import { Field, FieldControl, FieldDescription, FieldLabel } from '@langgenius/dify-ui/field'
import { Form } from '@langgenius/dify-ui/form'
import { Input } from '@langgenius/dify-ui/input'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import {
Select,
SelectContent,
@ -369,355 +375,367 @@ const SettingsModal: FC<ISettingsModalProps> = ({
onFormSubmit={handleFormSubmit}
>
{/* form body */}
<ScrollArea
className="relative min-h-0"
slotClassNames={{
viewport: 'overscroll-contain',
content: 'min-w-0 space-y-5 px-6 py-3',
}}
>
{/* name & icon */}
<div className="flex gap-4">
<Field name="title" className="grow">
<FieldLabel>
{t(($) => $[`${prefixSettings}.webName`], { ns: 'appOverview' })}
</FieldLabel>
<FieldControl
value={inputInfo.title}
onValueChange={(value) => setInputInfo((item) => ({ ...item, title: value }))}
placeholder={t(($) => $.appNamePlaceholder, { ns: 'app' }) || ''}
/>
</Field>
<AppIcon
size="xxl"
onClick={() => {
setShowAppIconPicker(true)
}}
className="mt-2 cursor-pointer"
iconType={appIcon.type === 'link' ? 'image' : appIcon.type}
icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
background={appIcon.type === 'emoji' ? appIcon.background : undefined}
imageUrl={appIcon.type === 'emoji' ? undefined : appIcon.url}
/>
</div>
{/* description */}
<Field name="description">
<FieldLabel>
{t(($) => $[`${prefixSettings}.webDesc`], { ns: 'appOverview' })}
</FieldLabel>
<Textarea
value={inputInfo.desc}
onValueChange={onDesChange}
placeholder={
t(($) => $[`${prefixSettings}.webDescPlaceholder`], {
ns: 'appOverview',
}) as string
}
/>
<FieldDescription>
{t(($) => $[`${prefixSettings}.webDescTip`], { ns: 'appOverview' })}
</FieldDescription>
</Field>
<Divider className="my-0 h-px" />
{/* answer icon */}
{isChat && (
<Field name="use_icon_as_answer_icon" className="w-full">
<div className="flex items-center justify-between gap-3">
<FieldLabel>{t(($) => $['answerIcon.title'], { ns: 'app' })}</FieldLabel>
<Switch
checked={inputInfo.use_icon_as_answer_icon}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })
}
/>
</div>
<FieldDescription>
{t(($) => $['answerIcon.description'], { ns: 'app' })}
</FieldDescription>
</Field>
)}
{/* language */}
<div className="flex items-center">
<div className={cn('grow py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.language`], { ns: 'appOverview' })}
</div>
<Select
value={selectedLanguage?.value ?? null}
onValueChange={handleLanguageChange}
>
<SelectTrigger
aria-label={t(($) => $[`${prefixSettings}.language`], { ns: 'appOverview' })}
size="medium"
className="w-50"
>
{selectedLanguage?.name ?? t(($) => $['placeholder.select'], { ns: 'common' })}
</SelectTrigger>
<SelectContent>
{LANGUAGE_OPTIONS.map((item) => (
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* theme color */}
{isChat && (
<div className="flex items-center">
<div className="grow">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.chatColorTheme`], { ns: 'appOverview' })}
</div>
<div className="pb-0.5 body-xs-regular text-text-tertiary">
{t(($) => $[`${prefixSettings}.chatColorThemeDesc`], { ns: 'appOverview' })}
</div>
</div>
<Field name="chat_color_theme" className="w-50 shrink-0">
<FieldControl
className="mb-1"
value={inputInfo.chatColorTheme ?? ''}
onValueChange={(value) =>
setInputInfo((item) => ({ ...item, chatColorTheme: value }))
}
placeholder="E.g #A020F0"
/>
<div className="flex items-center justify-between gap-2 body-xs-regular text-text-tertiary">
<span>
{t(($) => $[`${prefixSettings}.chatColorThemeInverted`], {
ns: 'appOverview',
})}
</span>
<Switch
checked={inputInfo.chatColorThemeInverted}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, chatColorThemeInverted: v })
}
></Switch>
</div>
</Field>
</div>
)}
{/* workflow detail */}
<Field name="show_workflow_steps" className="w-full">
<div className="flex items-center justify-between gap-3">
<FieldLabel>
{t(($) => $[`${prefixSettings}.workflow.subTitle`], { ns: 'appOverview' })}
</FieldLabel>
<Switch
disabled={
!(
appInfo.mode === AppModeEnum.WORKFLOW ||
appInfo.mode === AppModeEnum.ADVANCED_CHAT
)
}
checked={inputInfo.show_workflow_steps}
onCheckedChange={(v) => setInputInfo({ ...inputInfo, show_workflow_steps: v })}
/>
</div>
<FieldDescription>
{t(($) => $[`${prefixSettings}.workflow.showDesc`], { ns: 'appOverview' })}
</FieldDescription>
</Field>
<Divider className="my-0 h-px" />
<div className="space-y-5">
{INPUT_PLACEHOLDER_SUPPORTED_MODES.includes(appInfo.mode) && (
<div className="w-full">
<div className="flex items-center">
<div className="flex grow items-center">
<div
id={inputPlaceholderLabelId}
className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}
>
{t(($) => $[`${prefixSettings}.more.inputPlaceholder`], {
ns: 'appOverview',
})}
</div>
{isCloudSandboxPlan && (
<div className="h-4.5 select-none">
<PremiumBadgeButton size="s" color="blue" onClick={handlePlanClick}>
<span
aria-hidden="true"
className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-0.75 text-components-premium-badge-indigo-text-stop-0"
/>
<div className="system-xs-medium">
<span className="p-1">
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
</span>
</div>
</PremiumBadgeButton>
</div>
)}
</div>
</div>
<p
id={inputPlaceholderDescriptionId}
className="pb-0.5 body-xs-regular text-text-tertiary"
>
{t(($) => $[`${prefixSettings}.more.inputPlaceholderTip`], {
ns: 'appOverview',
})}
</p>
{isCloudSandboxPlan ? (
<Tooltip>
<TooltipTrigger render={inputPlaceholderField} />
<TooltipContent className="w-45">
{t(($) => $[`${prefixSettings}.more.inputPlaceholderTooltip`], {
ns: 'appOverview',
})}
</TooltipContent>
</Tooltip>
) : (
inputPlaceholderField
)}
{!isCloudSandboxPlan && (
<div className="mt-1 text-right body-xs-regular text-text-tertiary">
{`${inputInfo.inputPlaceholder?.length ?? 0} / ${INPUT_PLACEHOLDER_MAX_LENGTH}`}
</div>
)}
</div>
)}
{/* copyright */}
<div className="w-full">
<div className="flex items-center">
<div className="flex grow items-center">
<div className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.more.copyright`], { ns: 'appOverview' })}
</div>
{/* upgrade button */}
{isCloudSandboxPlan && (
<div className="h-4.5 select-none">
<PremiumBadgeButton size="s" color="blue" onClick={handlePlanClick}>
<span
aria-hidden="true"
className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-0.75 text-components-premium-badge-indigo-text-stop-0"
/>
<div className="system-xs-medium">
<span className="p-1">
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
</span>
</div>
</PremiumBadgeButton>
</div>
)}
</div>
{webappCopyrightEnabled ? (
<Switch
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
ns: 'appOverview',
})}
checked={copyrightSwitchValue}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, copyrightSwitchValue: v })
<ScrollArea className="relative min-h-0">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent style={{ minWidth: 0 }} className="space-y-5 px-6 py-3">
{/* name & icon */}
<div className="flex gap-4">
<Field name="title" className="grow">
<FieldLabel>
{t(($) => $[`${prefixSettings}.webName`], { ns: 'appOverview' })}
</FieldLabel>
<FieldControl
value={inputInfo.title}
onValueChange={(value) =>
setInputInfo((item) => ({ ...item, title: value }))
}
placeholder={t(($) => $.appNamePlaceholder, { ns: 'app' }) || ''}
/>
) : (
<Tooltip>
<TooltipTrigger
render={
<div>
<Switch
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
ns: 'appOverview',
})}
disabled
checked={copyrightSwitchValue}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, copyrightSwitchValue: v })
}
/>
</div>
}
/>
<TooltipContent className="w-45">
{t(($) => $[`${prefixSettings}.more.copyrightTooltip`], {
ns: 'appOverview',
})}
</TooltipContent>
</Tooltip>
)}
</Field>
<AppIcon
size="xxl"
onClick={() => {
setShowAppIconPicker(true)
}}
className="mt-2 cursor-pointer"
iconType={appIcon.type === 'link' ? 'image' : appIcon.type}
icon={appIcon.type === 'image' ? appIcon.fileId : appIcon.icon}
background={appIcon.type === 'emoji' ? appIcon.background : undefined}
imageUrl={appIcon.type === 'emoji' ? undefined : appIcon.url}
/>
</div>
<p className="pb-0.5 body-xs-regular text-text-tertiary">
{t(($) => $[`${prefixSettings}.more.copyrightTip`], { ns: 'appOverview' })}
</p>
{copyrightSwitchValue && (
<Input
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
ns: 'appOverview',
})}
className="mt-2 h-10"
value={inputInfo.copyright}
onChange={onChange('copyright')}
{/* description */}
<Field name="description">
<FieldLabel>
{t(($) => $[`${prefixSettings}.webDesc`], { ns: 'appOverview' })}
</FieldLabel>
<Textarea
value={inputInfo.desc}
onValueChange={onDesChange}
placeholder={
t(($) => $[`${prefixSettings}.more.copyRightPlaceholder`], {
t(($) => $[`${prefixSettings}.webDescPlaceholder`], {
ns: 'appOverview',
}) as string
}
/>
<FieldDescription>
{t(($) => $[`${prefixSettings}.webDescTip`], { ns: 'appOverview' })}
</FieldDescription>
</Field>
<Divider className="my-0 h-px" />
{/* answer icon */}
{isChat && (
<Field name="use_icon_as_answer_icon" className="w-full">
<div className="flex items-center justify-between gap-3">
<FieldLabel>{t(($) => $['answerIcon.title'], { ns: 'app' })}</FieldLabel>
<Switch
checked={inputInfo.use_icon_as_answer_icon}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, use_icon_as_answer_icon: v })
}
/>
</div>
<FieldDescription>
{t(($) => $['answerIcon.description'], { ns: 'app' })}
</FieldDescription>
</Field>
)}
</div>
{/* privacy policy */}
<div className="w-full">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.more.privacyPolicy`], { ns: 'appOverview' })}
{/* language */}
<div className="flex items-center">
<div className={cn('grow py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.language`], { ns: 'appOverview' })}
</div>
<Select
value={selectedLanguage?.value ?? null}
onValueChange={handleLanguageChange}
>
<SelectTrigger
aria-label={t(($) => $[`${prefixSettings}.language`], {
ns: 'appOverview',
})}
size="medium"
className="w-50"
>
{selectedLanguage?.name ??
t(($) => $['placeholder.select'], { ns: 'common' })}
</SelectTrigger>
<SelectContent>
{LANGUAGE_OPTIONS.map((item) => (
<SelectItem key={item.value} value={item.value}>
<SelectItemText>{item.name}</SelectItemText>
<SelectItemIndicator />
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
<Trans
i18nKey={($) => $[`${prefixSettings}.more.privacyPolicyTip`]}
ns="appOverview"
components={{
privacyPolicyLink: (
<Link
href="https://dify.ai/privacy"
target="_blank"
rel="noopener noreferrer"
className="text-text-accent"
{/* theme color */}
{isChat && (
<div className="flex items-center">
<div className="grow">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.chatColorTheme`], { ns: 'appOverview' })}
</div>
<div className="pb-0.5 body-xs-regular text-text-tertiary">
{t(($) => $[`${prefixSettings}.chatColorThemeDesc`], {
ns: 'appOverview',
})}
</div>
</div>
<Field name="chat_color_theme" className="w-50 shrink-0">
<FieldControl
className="mb-1"
value={inputInfo.chatColorTheme ?? ''}
onValueChange={(value) =>
setInputInfo((item) => ({ ...item, chatColorTheme: value }))
}
placeholder="E.g #A020F0"
/>
<div className="flex items-center justify-between gap-2 body-xs-regular text-text-tertiary">
<span>
{t(($) => $[`${prefixSettings}.chatColorThemeInverted`], {
ns: 'appOverview',
})}
</span>
<Switch
checked={inputInfo.chatColorThemeInverted}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, chatColorThemeInverted: v })
}
></Switch>
</div>
</Field>
</div>
)}
{/* workflow detail */}
<Field name="show_workflow_steps" className="w-full">
<div className="flex items-center justify-between gap-3">
<FieldLabel>
{t(($) => $[`${prefixSettings}.workflow.subTitle`], { ns: 'appOverview' })}
</FieldLabel>
<Switch
disabled={
!(
appInfo.mode === AppModeEnum.WORKFLOW ||
appInfo.mode === AppModeEnum.ADVANCED_CHAT
)
}
checked={inputInfo.show_workflow_steps}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, show_workflow_steps: v })
}
/>
</div>
<FieldDescription>
{t(($) => $[`${prefixSettings}.workflow.showDesc`], { ns: 'appOverview' })}
</FieldDescription>
</Field>
<Divider className="my-0 h-px" />
<div className="space-y-5">
{INPUT_PLACEHOLDER_SUPPORTED_MODES.includes(appInfo.mode) && (
<div className="w-full">
<div className="flex items-center">
<div className="flex grow items-center">
<div
id={inputPlaceholderLabelId}
className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}
>
{t(($) => $[`${prefixSettings}.more.inputPlaceholder`], {
ns: 'appOverview',
})}
</div>
{isCloudSandboxPlan && (
<div className="h-4.5 select-none">
<PremiumBadgeButton size="s" color="blue" onClick={handlePlanClick}>
<span
aria-hidden="true"
className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-0.75 text-components-premium-badge-indigo-text-stop-0"
/>
<div className="system-xs-medium">
<span className="p-1">
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
</span>
</div>
</PremiumBadgeButton>
</div>
)}
</div>
</div>
<p
id={inputPlaceholderDescriptionId}
className="pb-0.5 body-xs-regular text-text-tertiary"
>
{t(($) => $[`${prefixSettings}.more.inputPlaceholderTip`], {
ns: 'appOverview',
})}
</p>
{isCloudSandboxPlan ? (
<Tooltip>
<TooltipTrigger render={inputPlaceholderField} />
<TooltipContent className="w-45">
{t(($) => $[`${prefixSettings}.more.inputPlaceholderTooltip`], {
ns: 'appOverview',
})}
</TooltipContent>
</Tooltip>
) : (
inputPlaceholderField
)}
{!isCloudSandboxPlan && (
<div className="mt-1 text-right body-xs-regular text-text-tertiary">
{`${inputInfo.inputPlaceholder?.length ?? 0} / ${INPUT_PLACEHOLDER_MAX_LENGTH}`}
</div>
)}
</div>
)}
{/* copyright */}
<div className="w-full">
<div className="flex items-center">
<div className="flex grow items-center">
<div className={cn('mr-1 py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.more.copyright`], { ns: 'appOverview' })}
</div>
{/* upgrade button */}
{isCloudSandboxPlan && (
<div className="h-4.5 select-none">
<PremiumBadgeButton size="s" color="blue" onClick={handlePlanClick}>
<span
aria-hidden="true"
className="i-custom-public-common-sparkles-soft flex h-3.5 w-3.5 items-center py-px pl-0.75 text-components-premium-badge-indigo-text-stop-0"
/>
<div className="system-xs-medium">
<span className="p-1">
{t(($) => $['upgradeBtn.encourageShort'], { ns: 'billing' })}
</span>
</div>
</PremiumBadgeButton>
</div>
)}
</div>
{webappCopyrightEnabled ? (
<Switch
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
ns: 'appOverview',
})}
checked={copyrightSwitchValue}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, copyrightSwitchValue: v })
}
/>
),
}}
/>
</p>
<Input
aria-label={t(($) => $[`${prefixSettings}.more.privacyPolicy`], {
ns: 'appOverview',
})}
className="mt-1"
value={inputInfo.privacyPolicy}
onChange={onChange('privacyPolicy')}
placeholder={
t(($) => $[`${prefixSettings}.more.privacyPolicyPlaceholder`], {
ns: 'appOverview',
}) as string
}
/>
</div>
{/* custom disclaimer */}
<div className="w-full">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.more.customDisclaimer`], { ns: 'appOverview' })}
) : (
<Tooltip>
<TooltipTrigger
render={
<div>
<Switch
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
ns: 'appOverview',
})}
disabled
checked={copyrightSwitchValue}
onCheckedChange={(v) =>
setInputInfo({ ...inputInfo, copyrightSwitchValue: v })
}
/>
</div>
}
/>
<TooltipContent className="w-45">
{t(($) => $[`${prefixSettings}.more.copyrightTooltip`], {
ns: 'appOverview',
})}
</TooltipContent>
</Tooltip>
)}
</div>
<p className="pb-0.5 body-xs-regular text-text-tertiary">
{t(($) => $[`${prefixSettings}.more.copyrightTip`], { ns: 'appOverview' })}
</p>
{copyrightSwitchValue && (
<Input
aria-label={t(($) => $[`${prefixSettings}.more.copyright`], {
ns: 'appOverview',
})}
className="mt-2 h-10"
value={inputInfo.copyright}
onChange={onChange('copyright')}
placeholder={
t(($) => $[`${prefixSettings}.more.copyRightPlaceholder`], {
ns: 'appOverview',
}) as string
}
/>
)}
</div>
{/* privacy policy */}
<div className="w-full">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.more.privacyPolicy`], { ns: 'appOverview' })}
</div>
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
<Trans
i18nKey={($) => $[`${prefixSettings}.more.privacyPolicyTip`]}
ns="appOverview"
components={{
privacyPolicyLink: (
<Link
href="https://dify.ai/privacy"
target="_blank"
rel="noopener noreferrer"
className="text-text-accent"
/>
),
}}
/>
</p>
<Input
aria-label={t(($) => $[`${prefixSettings}.more.privacyPolicy`], {
ns: 'appOverview',
})}
className="mt-1"
value={inputInfo.privacyPolicy}
onChange={onChange('privacyPolicy')}
placeholder={
t(($) => $[`${prefixSettings}.more.privacyPolicyPlaceholder`], {
ns: 'appOverview',
}) as string
}
/>
</div>
{/* custom disclaimer */}
<div className="w-full">
<div className={cn('py-1 system-sm-semibold text-text-secondary')}>
{t(($) => $[`${prefixSettings}.more.customDisclaimer`], {
ns: 'appOverview',
})}
</div>
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
{t(($) => $[`${prefixSettings}.more.customDisclaimerTip`], {
ns: 'appOverview',
})}
</p>
<Textarea
aria-label={t(($) => $[`${prefixSettings}.more.customDisclaimer`], {
ns: 'appOverview',
})}
className="mt-1"
value={inputInfo.customDisclaimer}
onValueChange={(value) =>
setInputInfo((item) => ({ ...item, customDisclaimer: value }))
}
placeholder={
t(($) => $[`${prefixSettings}.more.customDisclaimerPlaceholder`], {
ns: 'appOverview',
}) as string
}
/>
</div>
</div>
<p className={cn('pb-0.5 body-xs-regular text-text-tertiary')}>
{t(($) => $[`${prefixSettings}.more.customDisclaimerTip`], {
ns: 'appOverview',
})}
</p>
<Textarea
aria-label={t(($) => $[`${prefixSettings}.more.customDisclaimer`], {
ns: 'appOverview',
})}
className="mt-1"
value={inputInfo.customDisclaimer}
onValueChange={(value) =>
setInputInfo((item) => ({ ...item, customDisclaimer: value }))
}
placeholder={
t(($) => $[`${prefixSettings}.more.customDisclaimerPlaceholder`], {
ns: 'appOverview',
}) as string
}
/>
</div>
</div>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
{/* footer */}
<div className="flex shrink-0 justify-end p-6 pt-5">

View File

@ -3,9 +3,9 @@ import type { FC } from 'react'
import type { Category } from './types'
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaCorner,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -51,7 +51,7 @@ const Pricing: FC<PricingProps> = ({ onCancel }) => {
}}
>
<DialogContent className="inset-0 size-full max-h-none max-w-none translate-0 overflow-hidden rounded-none border-none bg-saas-background p-0 shadow-none">
<ScrollAreaRoot className="relative h-full w-full overflow-hidden">
<ScrollArea className="relative h-full w-full overflow-hidden">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent className="min-h-full min-w-300">
<div className="relative grid min-h-full grid-rows-[1fr_auto_auto_1fr] overflow-hidden">
@ -85,7 +85,7 @@ const Pricing: FC<PricingProps> = ({ onCancel }) => {
<ScrollAreaThumb className="rounded-full" />
</ScrollAreaScrollbar>
<ScrollAreaCorner className="bg-saas-background" />
</ScrollAreaRoot>
</ScrollArea>
</DialogContent>
</Dialog>
)

View File

@ -1,7 +1,13 @@
'use client'
import type { ResourceOpenScope } from '@/models/access-control'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useCallback, useMemo, useState } from 'react'
@ -103,35 +109,39 @@ const DatasetAccessConfigPage = ({ datasetId }: DatasetAccessConfigPageProps) =>
if (!canAccessConfig) return <Loading type="app" />
return (
<ScrollArea
className="h-full bg-background-default-subtle"
slotClassNames={{ viewport: 'overscroll-contain' }}
>
<header className="flex min-h-15.5 flex-col justify-center px-6 py-3">
<h1 className="system-sm-semibold text-text-primary">
{t(($) => $['settings.resourceAccess'], { ns: 'common' })}
</h1>
<p className="mt-0.5 system-xs-regular text-text-tertiary">
{t(($) => $['accessRule.datasetDescription'], { ns: 'permission' })}
</p>
</header>
<main className="w-full px-6 pt-8 pb-10">
<AccessRulesEditor
className="w-full max-w-200"
rules={datasetAccessRules}
userAccessSettings={datasetUserAccessSettings}
isLoadingRules={isLoadingDatasetAccessRules}
isLoadingUserAccessSettings={isLoadingDatasetUserAccessSettings}
openScope={openScope}
isUpdatingOpenScope={isLoadingDatasetUserAccessSettings || isUpdatingDatasetOpenScope}
updatingAccountId={updatingAccountId}
maintainerId={dataset?.maintainer}
onOpenScopeChange={handleOpenScopeChange}
onUserAccessPoliciesChange={handleUserAccessPoliciesChange}
onRemoveAccessPolicyMemberBinding={handleRemoveAccessPolicyMemberBinding}
onAddAccessSubject={handleUserAccessPoliciesChange}
/>
</main>
<ScrollArea className="h-full bg-background-default-subtle">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent>
<header className="flex min-h-15.5 flex-col justify-center px-6 py-3">
<h1 className="system-sm-semibold text-text-primary">
{t(($) => $['settings.resourceAccess'], { ns: 'common' })}
</h1>
<p className="mt-0.5 system-xs-regular text-text-tertiary">
{t(($) => $['accessRule.datasetDescription'], { ns: 'permission' })}
</p>
</header>
<main className="w-full px-6 pt-8 pb-10">
<AccessRulesEditor
className="w-full max-w-200"
rules={datasetAccessRules}
userAccessSettings={datasetUserAccessSettings}
isLoadingRules={isLoadingDatasetAccessRules}
isLoadingUserAccessSettings={isLoadingDatasetUserAccessSettings}
openScope={openScope}
isUpdatingOpenScope={isLoadingDatasetUserAccessSettings || isUpdatingDatasetOpenScope}
updatingAccountId={updatingAccountId}
maintainerId={dataset?.maintainer}
onOpenScopeChange={handleOpenScopeChange}
onUserAccessPoliciesChange={handleUserAccessPoliciesChange}
onRemoveAccessPolicyMemberBinding={handleRemoveAccessPolicyMemberBinding}
onAddAccessSubject={handleUserAccessPoliciesChange}
/>
</main>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
)
}

View File

@ -15,8 +15,8 @@ import {
} from '@langgenius/dify-ui/alert-dialog'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -193,7 +193,7 @@ const SideBar = () => {
)}
{!isFold ? (
<div className="min-h-0 flex-1">
<ScrollAreaRoot className="h-full">
<ScrollArea className="h-full">
<ScrollAreaViewport
ref={scrollRef}
aria-busy={installedAppsQuery.isFetchingNextPage}
@ -219,7 +219,7 @@ const SideBar = () => {
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
</div>
) : (
<div

View File

@ -24,8 +24,8 @@ import {
} from '@langgenius/dify-ui/dialog'
import { Kbd, KbdGroup } from '@langgenius/dify-ui/kbd'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -443,11 +443,11 @@ function GotoAnythingDialog() {
<AutocompleteStatus className="sr-only">{autocompleteStatus}</AutocompleteStatus>
<ScrollAreaRoot
aria-busy={isLoading || undefined}
className="relative h-60 min-h-0 overflow-hidden"
>
<ScrollAreaViewport className="scroll-py-1 overscroll-contain">
<ScrollArea className="relative h-60 min-h-0 overflow-hidden">
<ScrollAreaViewport
aria-busy={isLoading || undefined}
className="scroll-py-1 overscroll-contain"
>
<ScrollAreaContent
className="min-h-full w-full max-w-full"
style={{ minWidth: '100%' }}
@ -576,7 +576,7 @@ function GotoAnythingDialog() {
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
<Footer
resultCount={autocompleteResultCount}

View File

@ -2,7 +2,13 @@
import type { AccountSettingTab } from '@/app/components/header/account-setting/constants'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useSuspenseQuery } from '@tanstack/react-query'
import { useAtomValue } from 'jotai'
import { useRef } from 'react'
@ -225,37 +231,44 @@ export default function AccountSetting({
</div>
</div>
<div className="relative flex min-h-0 w-206 min-w-0">
<ScrollArea
ref={scrollContainerRef}
className="h-full min-h-0 min-w-0 flex-1 bg-components-panel-bg"
slotClassNames={{
viewport: 'overscroll-contain overflow-x-hidden',
content: 'min-h-full min-w-0 w-full max-w-full pb-4',
}}
>
<div className="sticky top-0 z-20 mx-8 flex min-h-15 items-end bg-components-panel-bg pt-8 pb-2">
<div className="min-w-0 flex-1 title-2xl-semi-bold text-text-primary">
{activeItem?.title ?? activeItem?.name}
{activeItem?.description && (
<div className="mt-1 system-sm-regular wrap-break-word whitespace-normal text-text-tertiary">
{activeItem?.description}
<ScrollArea className="h-full min-h-0 min-w-0 flex-1 bg-components-panel-bg">
<ScrollAreaViewport
ref={scrollContainerRef}
style={{ overflowX: 'hidden' }}
className="overscroll-contain"
>
<ScrollAreaContent
style={{ minWidth: 0 }}
className="min-h-full w-full max-w-full pb-4"
>
<div className="sticky top-0 z-20 mx-8 flex min-h-15 items-end bg-components-panel-bg pt-8 pb-2">
<div className="min-w-0 flex-1 title-2xl-semi-bold text-text-primary">
{activeItem?.title ?? activeItem?.name}
{activeItem?.description && (
<div className="mt-1 system-sm-regular wrap-break-word whitespace-normal text-text-tertiary">
{activeItem?.description}
</div>
)}
</div>
)}
</div>
</div>
<div className="max-w-full min-w-0 px-4 pt-6 sm:px-8">
{activeMenu === ACCOUNT_SETTING_TAB.MEMBERS && <MembersPage />}
{activeMenu === ACCOUNT_SETTING_TAB.ROLES_AND_PERMISSIONS && (
<PermissionsPage containerRef={scrollContainerRef} />
)}
{activeMenu === ACCOUNT_SETTING_TAB.PERMISSION_SET && <AccessRulesPage />}
{activeMenu === ACCOUNT_SETTING_TAB.BILLING && <BillingPage />}
{activeMenu === ACCOUNT_SETTING_TAB.WORKFLOW_LOG_ARCHIVES && (
<WorkflowLogArchivesPage />
)}
{activeMenu === ACCOUNT_SETTING_TAB.CUSTOM && <CustomPage />}
{activeMenu === ACCOUNT_SETTING_TAB.PREFERENCES && <PreferencePage />}
</div>
</div>
<div className="max-w-full min-w-0 px-4 pt-6 sm:px-8">
{activeMenu === ACCOUNT_SETTING_TAB.MEMBERS && <MembersPage />}
{activeMenu === ACCOUNT_SETTING_TAB.ROLES_AND_PERMISSIONS && (
<PermissionsPage containerRef={scrollContainerRef} />
)}
{activeMenu === ACCOUNT_SETTING_TAB.PERMISSION_SET && <AccessRulesPage />}
{activeMenu === ACCOUNT_SETTING_TAB.BILLING && <BillingPage />}
{activeMenu === ACCOUNT_SETTING_TAB.WORKFLOW_LOG_ARCHIVES && (
<WorkflowLogArchivesPage />
)}
{activeMenu === ACCOUNT_SETTING_TAB.CUSTOM && <CustomPage />}
{activeMenu === ACCOUNT_SETTING_TAB.PREFERENCES && <PreferencePage />}
</div>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</div>
</div>

View File

@ -2,8 +2,8 @@ import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { ComboboxInput, ComboboxInputGroup } from '@langgenius/dify-ui/combobox'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -75,18 +75,19 @@ type ModelSelectorScrollBodyProps = {
export function ModelSelectorScrollBody({ children, label }: ModelSelectorScrollBodyProps) {
return (
<ScrollAreaRoot className="relative min-h-0 overflow-hidden overscroll-contain">
<ScrollArea className="relative min-h-0 overflow-hidden">
<ScrollAreaViewport
aria-label={label}
className="max-h-[calc(min(624px,var(--available-height,624px))-84px)] overflow-x-hidden overscroll-contain"
style={{ overflowX: 'hidden' }}
className="max-h-[calc(min(624px,var(--available-height,624px))-84px)] overscroll-contain"
role="region"
>
<ScrollAreaContent className="min-w-0 overflow-x-hidden">{children}</ScrollAreaContent>
<ScrollAreaContent style={{ minWidth: 0 }}>{children}</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar className="z-2">
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
)
}

View File

@ -3,7 +3,13 @@
import type { PermissionGroup } from '@/models/access-control'
import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
@ -75,113 +81,114 @@ const PermissionGroupList = ({
className,
)}
>
<ScrollArea
className="h-full overflow-hidden rounded-xl"
slotClassNames={{
viewport: 'overscroll-contain',
content: groups.length === 0 ? 'h-full' : undefined,
}}
>
{groups.length === 0 ? (
<div className="flex h-full items-center justify-center px-3 py-6 text-center system-sm-regular text-text-tertiary">
{t(($) => $['permissionList.noPermissionsFound'], { ns: 'permission' })}
</div>
) : (
<div className="flex flex-col">
{groups.map((group) => {
const expanded = expandedGroupKeysMerged.has(group.group_key)
const selectedCount = group.permissions.reduce(
(count, permission) => count + (selectedSet.has(permission.key) ? 1 : 0),
0,
)
const totalCount = group.permissions.length
const allSelected = totalCount > 0 && selectedCount === totalCount
<ScrollArea className="h-full overflow-hidden rounded-xl">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent className={groups.length === 0 ? 'h-full' : undefined}>
{groups.length === 0 ? (
<div className="flex h-full items-center justify-center px-3 py-6 text-center system-sm-regular text-text-tertiary">
{t(($) => $['permissionList.noPermissionsFound'], { ns: 'permission' })}
</div>
) : (
<div className="flex flex-col">
{groups.map((group) => {
const expanded = expandedGroupKeysMerged.has(group.group_key)
const selectedCount = group.permissions.reduce(
(count, permission) => count + (selectedSet.has(permission.key) ? 1 : 0),
0,
)
const totalCount = group.permissions.length
const allSelected = totalCount > 0 && selectedCount === totalCount
return (
<div key={group.group_key} className="border-b border-b-divider-subtle">
<div className="group/permission-category flex h-12 w-full items-center gap-3 px-3 hover:bg-state-base-hover">
<button
type="button"
className="min-w-0 flex-1 truncate text-left system-md-medium text-text-secondary focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
aria-expanded={expanded}
onClick={() => toggleGroupExpanded(group.group_key)}
>
{group.group_name}
</button>
{!readonly && totalCount > 0 && (
<button
type="button"
className="shrink-0 rounded-md px-1.5 py-0.5 system-sm-medium text-text-accent opacity-0 group-hover/permission-category:opacity-100 hover:bg-state-accent-hover focus-visible:opacity-100 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
onClick={() => toggleGroupSelection(group, selectedCount)}
>
{allSelected
? t(($) => $['permissionList.clearAll'], { ns: 'permission' })
: t(($) => $['permissionList.selectAll'], { ns: 'permission' })}
</button>
)}
{selectedCount > 0 && (
<span className="shrink-0 rounded-md bg-util-colors-blue-blue-100 px-2 py-0.5 system-sm-medium text-text-accent">
{selectedCount}/{totalCount}
</span>
)}
<button
type="button"
className="flex h-6 w-6 shrink-0 items-center justify-center focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
aria-label={
expanded
? t(($) => $['permissionList.collapseGroup'], { ns: 'permission' })
: t(($) => $['permissionList.expandGroup'], { ns: 'permission' })
}
onClick={() => toggleGroupExpanded(group.group_key)}
>
<span
aria-hidden
className={cn(
'i-ri-arrow-right-s-fill h-4 w-4 text-text-tertiary transition-transform',
expanded && 'rotate-90',
)}
/>
</button>
</div>
{expanded && (
<div className="flex flex-col">
{group.permissions.map((permission) => {
const checked = selectedSet.has(permission.key)
return (
<div
key={permission.key}
className={cn(
'flex min-h-9 items-center gap-3 px-3 py-1.5',
readonly
? 'cursor-default'
: 'cursor-pointer hover:bg-state-base-hover',
checked && 'bg-state-accent-hover',
checked && !readonly && 'hover:bg-state-accent-hover',
)}
onClick={() => togglePermission(permission.key)}
return (
<div key={group.group_key} className="border-b border-b-divider-subtle">
<div className="group/permission-category flex h-12 w-full items-center gap-3 px-3 hover:bg-state-base-hover">
<button
type="button"
className="min-w-0 flex-1 truncate text-left system-md-medium text-text-secondary focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
aria-expanded={expanded}
onClick={() => toggleGroupExpanded(group.group_key)}
>
{group.group_name}
</button>
{!readonly && totalCount > 0 && (
<button
type="button"
className="shrink-0 rounded-md px-1.5 py-0.5 system-sm-medium text-text-accent opacity-0 group-hover/permission-category:opacity-100 hover:bg-state-accent-hover focus-visible:opacity-100 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
onClick={() => toggleGroupSelection(group, selectedCount)}
>
<Checkbox
checked={checked}
disabled={readonly}
className="shrink-0"
onCheckedChange={() => {
togglePermission(permission.key)
}}
/>
<span className="min-w-0 flex-1 truncate system-md-regular text-text-secondary">
{permission.name}
</span>
</div>
)
})}
{allSelected
? t(($) => $['permissionList.clearAll'], { ns: 'permission' })
: t(($) => $['permissionList.selectAll'], { ns: 'permission' })}
</button>
)}
{selectedCount > 0 && (
<span className="shrink-0 rounded-md bg-util-colors-blue-blue-100 px-2 py-0.5 system-sm-medium text-text-accent">
{selectedCount}/{totalCount}
</span>
)}
<button
type="button"
className="flex h-6 w-6 shrink-0 items-center justify-center focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active"
aria-label={
expanded
? t(($) => $['permissionList.collapseGroup'], { ns: 'permission' })
: t(($) => $['permissionList.expandGroup'], { ns: 'permission' })
}
onClick={() => toggleGroupExpanded(group.group_key)}
>
<span
aria-hidden
className={cn(
'i-ri-arrow-right-s-fill h-4 w-4 text-text-tertiary transition-transform',
expanded && 'rotate-90',
)}
/>
</button>
</div>
{expanded && (
<div className="flex flex-col">
{group.permissions.map((permission) => {
const checked = selectedSet.has(permission.key)
return (
<div
key={permission.key}
className={cn(
'flex min-h-9 items-center gap-3 px-3 py-1.5',
readonly
? 'cursor-default'
: 'cursor-pointer hover:bg-state-base-hover',
checked && 'bg-state-accent-hover',
checked && !readonly && 'hover:bg-state-accent-hover',
)}
onClick={() => togglePermission(permission.key)}
>
<Checkbox
checked={checked}
disabled={readonly}
className="shrink-0"
onCheckedChange={() => {
togglePermission(permission.key)
}}
/>
<span className="min-w-0 flex-1 truncate system-md-regular text-text-secondary">
{permission.name}
</span>
</div>
)
})}
</div>
)}
</div>
)}
</div>
)
})}
</div>
)}
)
})}
</div>
)}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</div>
)

View File

@ -5,7 +5,13 @@ import { Checkbox } from '@langgenius/dify-ui/checkbox'
import { cn } from '@langgenius/dify-ui/cn'
import { Input } from '@langgenius/dify-ui/input'
import { RadioControl, RadioGroup, RadioItem } from '@langgenius/dify-ui/radio'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useLocale } from '@/context/i18n'
@ -227,99 +233,102 @@ const WorkspaceRoleCheckboxList = ({
</div>
</div>
<ScrollArea
ref={containerRef}
className="min-h-0 flex-1"
slotClassNames={{ viewport: 'px-3 overscroll-contain' }}
>
{rolesLoading ? (
<div className="px-3 py-6 text-center system-sm-regular text-text-tertiary">
{t(($) => $['role.loading'], { ns: 'permission' })}
</div>
) : filteredRoles.length === 0 ? (
<div className="px-3 py-6 text-center system-sm-regular text-text-tertiary">
{t(($) => $['role.noMatchingRoles'], { ns: 'permission' })}
</div>
) : (
<>
{allowMultipleRoles ? (
<ul className="flex flex-col gap-0.5 pb-2">
{filteredRoles.map((role) => {
const checked = selectedRoleIdSet.has(role.id)
const disabled = disabledRoleIdSet.has(role.id)
const handleToggle = () => toggleRole(role)
return (
<li key={role.id}>
<div
role="checkbox"
aria-checked={checked}
aria-disabled={disabled}
tabIndex={disabled ? -1 : 0}
className={cn(
'flex cursor-pointer items-start gap-3 rounded-lg px-3 py-2.5 hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
checked && 'bg-state-accent-hover hover:bg-state-accent-hover',
disabled && 'cursor-not-allowed opacity-50 hover:bg-transparent',
)}
onClick={handleToggle}
onKeyDown={(e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault()
handleToggle()
}
}}
>
<Checkbox
checked={checked}
disabled={disabled}
className="pointer-events-none mt-0.5"
/>
{renderRoleText(role)}
</div>
</li>
)
})}
</ul>
<ScrollArea className="min-h-0 flex-1">
<ScrollAreaViewport ref={containerRef} className="overscroll-contain px-3">
<ScrollAreaContent>
{rolesLoading ? (
<div className="px-3 py-6 text-center system-sm-regular text-text-tertiary">
{t(($) => $['role.loading'], { ns: 'permission' })}
</div>
) : filteredRoles.length === 0 ? (
<div className="px-3 py-6 text-center system-sm-regular text-text-tertiary">
{t(($) => $['role.noMatchingRoles'], { ns: 'permission' })}
</div>
) : (
<RadioGroup
aria-label={t(($) => $['role.workspaceRoles.title'], { ns: 'permission' })}
value={selectedRoleIds[0] ?? ''}
onValueChange={handleRadioValueChange}
className="flex-col items-stretch gap-0.5 pb-2"
render={<ul />}
>
{filteredRoles.map((role) => {
const checked = selectedRoleIdSet.has(role.id)
const disabled = disabledRoleIdSet.has(role.id)
<>
{allowMultipleRoles ? (
<ul className="flex flex-col gap-0.5 pb-2">
{filteredRoles.map((role) => {
const checked = selectedRoleIdSet.has(role.id)
const disabled = disabledRoleIdSet.has(role.id)
const handleToggle = () => toggleRole(role)
return (
<li key={role.id}>
<RadioItem
value={role.id}
disabled={disabled}
nativeButton
render={
<button
type="button"
return (
<li key={role.id}>
<div
role="checkbox"
aria-checked={checked}
aria-disabled={disabled}
tabIndex={disabled ? -1 : 0}
className={cn(
'flex w-full cursor-pointer items-start gap-3 rounded-lg border-0 bg-transparent px-3 py-2.5 text-left hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
'flex cursor-pointer items-start gap-3 rounded-lg px-3 py-2.5 hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
checked && 'bg-state-accent-hover hover:bg-state-accent-hover',
disabled && 'cursor-not-allowed opacity-50 hover:bg-transparent',
)}
/>
}
>
<RadioControl className="pointer-events-none mt-0.5" />
{renderRoleText(role)}
</RadioItem>
</li>
)
})}
</RadioGroup>
onClick={handleToggle}
onKeyDown={(e) => {
if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault()
handleToggle()
}
}}
>
<Checkbox
checked={checked}
disabled={disabled}
className="pointer-events-none mt-0.5"
/>
{renderRoleText(role)}
</div>
</li>
)
})}
</ul>
) : (
<RadioGroup
aria-label={t(($) => $['role.workspaceRoles.title'], { ns: 'permission' })}
value={selectedRoleIds[0] ?? ''}
onValueChange={handleRadioValueChange}
className="flex-col items-stretch gap-0.5 pb-2"
render={<ul />}
>
{filteredRoles.map((role) => {
const checked = selectedRoleIdSet.has(role.id)
const disabled = disabledRoleIdSet.has(role.id)
return (
<li key={role.id}>
<RadioItem
value={role.id}
disabled={disabled}
nativeButton
render={
<button
type="button"
className={cn(
'flex w-full cursor-pointer items-start gap-3 rounded-lg border-0 bg-transparent px-3 py-2.5 text-left hover:bg-state-base-hover focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-components-input-border-active',
checked && 'bg-state-accent-hover hover:bg-state-accent-hover',
disabled && 'cursor-not-allowed opacity-50 hover:bg-transparent',
)}
/>
}
>
<RadioControl className="pointer-events-none mt-0.5" />
{renderRoleText(role)}
</RadioItem>
</li>
)
})}
</RadioGroup>
)}
<div ref={anchorRef} className="h-0" />
</>
)}
<div ref={anchorRef} className="h-0" />
</>
)}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</>
)

View File

@ -5,7 +5,13 @@ import type { IntegrationSection } from '@/app/components/integrations/routes'
import type { DocPathWithoutLang } from '@/types/doc-paths'
import { cn } from '@langgenius/dify-ui/cn'
import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from '@langgenius/dify-ui/collapsible'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import UpdateSettingDialog from '@/app/components/header/account-setting/update-setting-dialog'
@ -339,28 +345,32 @@ export default function IntegrationsPage({
/>
</div>
) : (
<ScrollArea
className="min-h-0 flex-1 overflow-hidden"
label={scrollAreaLabel}
slotClassNames={{
viewport: 'overscroll-contain',
content: 'min-h-full',
}}
>
<IntegrationSectionRenderer
key={section}
section={section}
title={integrationHeader?.title ?? activeItem?.label}
description={headerDescriptionWithLink}
providerSearchText={providerSearchText}
onProviderSearchTextChange={setProviderSearchText}
onSwitchToMarketplace={handleSwitchToMarketplace}
canInstallPlugin={canInstallPlugin}
canDeletePlugin={canDeletePlugin}
isInstallPermissionLoading={isReferenceSettingLoading}
canUpdatePlugin={canUpdatePlugin}
pluginCategoryToolbarAction={pluginSettingAction}
/>
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport
aria-label={scrollAreaLabel}
className="overscroll-contain"
role={scrollAreaLabel ? 'region' : undefined}
>
<ScrollAreaContent className="min-h-full">
<IntegrationSectionRenderer
key={section}
section={section}
title={integrationHeader?.title ?? activeItem?.label}
description={headerDescriptionWithLink}
providerSearchText={providerSearchText}
onProviderSearchTextChange={setProviderSearchText}
onSwitchToMarketplace={handleSwitchToMarketplace}
canInstallPlugin={canInstallPlugin}
canDeletePlugin={canDeletePlugin}
isInstallPermissionLoading={isReferenceSettingLoading}
canUpdatePlugin={canUpdatePlugin}
pluginCategoryToolbarAction={pluginSettingAction}
/>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
)}
</section>

View File

@ -1,7 +1,13 @@
'use client'
import type { ReactNode } from 'react'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
type IntegrationSectionLayoutProps = {
bodyClassName?: string
@ -15,15 +21,19 @@ export function IntegrationSectionLayout({
label,
}: IntegrationSectionLayoutProps) {
return (
<ScrollArea
className="min-h-0 flex-1 overflow-hidden"
label={label}
slotClassNames={{
viewport: 'overscroll-contain',
content: 'min-h-full',
}}
>
<div className={bodyClassName}>{children}</div>
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport
aria-label={label}
className="overscroll-contain"
role={label ? 'region' : undefined}
>
<ScrollAreaContent className="min-h-full">
<div className={bodyClassName}>{children}</div>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
)
}

View File

@ -5,8 +5,8 @@ import type { ToolsContentInset } from '@/app/components/tools/content-inset'
import type { Collection } from '@/app/components/tools/types'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -203,7 +203,7 @@ const ProviderList = ({ category, contentInset = 'default', layout }: ProviderLi
const body = (
<>
<div className="relative flex h-0 shrink-0 grow flex-col overflow-hidden bg-components-panel-bg">
<ScrollAreaRoot className="relative min-h-0 grow overflow-hidden bg-components-panel-bg">
<ScrollArea className="relative min-h-0 grow overflow-hidden bg-components-panel-bg">
<ScrollAreaViewport
ref={containerRef}
aria-label={t(($) => $['menus.tools'], { ns: 'common' })}
@ -261,7 +261,7 @@ const ProviderList = ({ category, contentInset = 'default', layout }: ProviderLi
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
</div>
{currentProvider && !currentProvider.plugin_id && (
<ProviderDetail

View File

@ -16,8 +16,8 @@ import {
} from '@langgenius/dify-ui/alert-dialog'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -213,15 +213,16 @@ const WebAppsSectionContent = () => {
</div>
)}
{appsExpanded && (
<ScrollAreaRoot className="relative min-h-0 flex-1 overflow-hidden overscroll-contain">
<ScrollArea className="relative min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport
ref={scrollRef}
aria-busy={installedAppsQuery.isPending || installedAppsQuery.isFetchingNextPage}
aria-label={t(($) => $['sidebar.webApps'], { ns: 'explore' })}
className="overflow-x-hidden"
style={{ overflowX: 'hidden' }}
className="overscroll-contain"
role="region"
>
<ScrollAreaContent className="w-full max-w-full min-w-0! px-2">
<ScrollAreaContent style={{ minWidth: 0 }} className="w-full max-w-full px-2">
{installedAppsQuery.isPending && <WebAppsSkeleton />}
{!installedAppsQuery.isPending && installedAppsQuery.isError && (
<div
@ -277,7 +278,7 @@ const WebAppsSectionContent = () => {
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
)}
<AlertDialog
open={uninstallDialogAppId !== null}

View File

@ -1,7 +1,13 @@
import type { ReactNode } from 'react'
import type { PluginStatus } from '@/app/components/plugins/types'
import type { Locale } from '@/i18n-config'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import PluginItem from './plugin-item'
type PluginSectionProps = {
@ -36,36 +42,36 @@ function PluginSection({
return (
<>
<div className="sticky top-0 flex items-start justify-between pt-1 pr-1 pl-2 system-sm-semibold-uppercase text-text-secondary">
<span className="flex h-6 items-center">
<h3 className="flex h-6 items-center">
{title} ({count})
</span>
</h3>
{headerAction}
</div>
<ScrollArea
className="overflow-hidden"
label={title}
slotClassNames={{
viewport: 'overscroll-contain',
content: 'min-w-0',
}}
>
{plugins.map((plugin) => (
<PluginItem
key={plugin.plugin_unique_identifier}
plugin={plugin}
getIconUrl={getIconUrl}
language={language}
statusIcon={statusIcon}
statusText={plugin.message || defaultStatusText}
statusClassName={statusClassName}
action={renderItemAction?.(plugin)}
onClear={
onClearSingle
? () => onClearSingle(plugin.taskId, plugin.plugin_unique_identifier)
: undefined
}
/>
))}
<ScrollArea className="overflow-hidden">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent style={{ minWidth: 0 }}>
{plugins.map((plugin) => (
<PluginItem
key={plugin.plugin_unique_identifier}
plugin={plugin}
getIconUrl={getIconUrl}
language={language}
statusIcon={statusIcon}
statusText={plugin.message || defaultStatusText}
statusClassName={statusClassName}
action={renderItemAction?.(plugin)}
onClear={
onClearSingle
? () => onClearSingle(plugin.taskId, plugin.plugin_unique_identifier)
: undefined
}
/>
))}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</>
)

View File

@ -1,6 +1,12 @@
import type { PluginStatus } from '@/app/components/plugins/types'
import { Button } from '@langgenius/dify-ui/button'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useTranslation } from 'react-i18next'
import { useGetLanguage } from '@/context/i18n'
import ErrorPluginItem from './error-plugin-item'
@ -36,84 +42,89 @@ function PluginTaskList({
className="w-90 max-w-[calc(100vw-32px)] overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg"
data-testid="plugin-task-list"
>
<ScrollArea
className="max-h-105 overflow-hidden"
label={t(($) => $['task.installing'], { ns: 'plugin' })}
slotClassNames={{
viewport: 'max-h-[420px] overscroll-contain',
content: 'w-full! max-w-full! min-w-0! overflow-x-hidden! pr-3',
}}
>
{runningPlugins.length > 0 && (
<PluginSection
title={runningSectionTitle}
count={runningPlugins.length}
plugins={runningPlugins}
getIconUrl={getIconUrl}
language={language}
statusIcon={
<span className="i-ri-loader-2-line size-3.5 animate-spin text-text-accent" />
}
defaultStatusText={t(($) => $['task.installingHint'], { ns: 'plugin' })}
/>
)}
<ScrollArea className="max-h-105 overflow-hidden">
<ScrollAreaViewport
aria-label={t(($) => $['task.installing'], { ns: 'plugin' })}
style={{ overflowX: 'hidden' }}
className="max-h-[420px] overscroll-contain"
role="region"
>
<ScrollAreaContent style={{ minWidth: 0 }} className="w-full max-w-full pr-3">
{runningPlugins.length > 0 && (
<PluginSection
title={runningSectionTitle}
count={runningPlugins.length}
plugins={runningPlugins}
getIconUrl={getIconUrl}
language={language}
statusIcon={
<span className="i-ri-loader-2-line size-3.5 animate-spin text-text-accent" />
}
defaultStatusText={t(($) => $['task.installingHint'], { ns: 'plugin' })}
/>
)}
{successPlugins.length > 0 && (
<PluginSection
title={successSectionTitle}
count={successPlugins.length}
plugins={successPlugins}
getIconUrl={getIconUrl}
language={language}
statusIcon={<span className="i-ri-checkbox-circle-fill size-3.5 text-text-success" />}
defaultStatusText={t(($) => $['task.installed'], { ns: 'plugin' })}
statusClassName="text-text-success"
headerAction={
<Button
aria-label={`${successSectionTitle} ${t(($) => $['task.clearAll'], { ns: 'plugin' })}`}
className="shrink-0"
size="small"
variant="ghost"
onClick={onClearAll}
>
{t(($) => $['task.clearAll'], { ns: 'plugin' })}
</Button>
}
onClearSingle={onClearSingle}
/>
)}
{successPlugins.length > 0 && (
<PluginSection
title={successSectionTitle}
count={successPlugins.length}
plugins={successPlugins}
getIconUrl={getIconUrl}
language={language}
statusIcon={
<span className="i-ri-checkbox-circle-fill size-3.5 text-text-success" />
}
defaultStatusText={t(($) => $['task.installed'], { ns: 'plugin' })}
statusClassName="text-text-success"
headerAction={
<Button
aria-label={`${successSectionTitle} ${t(($) => $['task.clearAll'], { ns: 'plugin' })}`}
className="shrink-0"
size="small"
variant="ghost"
onClick={onClearAll}
>
{t(($) => $['task.clearAll'], { ns: 'plugin' })}
</Button>
}
onClearSingle={onClearSingle}
/>
)}
{errorPlugins.length > 0 && (
<>
<div className="sticky top-0 flex h-7 items-center justify-between px-2 pt-1 system-sm-semibold-uppercase text-text-secondary">
{errorSectionTitle} ({errorPlugins.length})
<Button
aria-label={`${errorSectionTitle} ${t(($) => $['task.clearAll'], { ns: 'plugin' })}`}
className="shrink-0"
size="small"
variant="ghost"
onClick={onClearErrors}
>
{t(($) => $['task.clearAll'], { ns: 'plugin' })}
</Button>
</div>
<div
aria-label={errorSectionTitle}
className="w-full max-w-full min-w-0 overflow-hidden"
role="region"
>
{errorPlugins.map((plugin) => (
<ErrorPluginItem
key={plugin.plugin_unique_identifier}
plugin={plugin}
getIconUrl={getIconUrl}
language={language}
onClear={() => onClearSingle(plugin.taskId, plugin.plugin_unique_identifier)}
/>
))}
</div>
</>
)}
{errorPlugins.length > 0 && (
<>
<div className="sticky top-0 flex h-7 items-center justify-between px-2 pt-1 system-sm-semibold-uppercase text-text-secondary">
<h3>
{errorSectionTitle} ({errorPlugins.length})
</h3>
<Button
aria-label={`${errorSectionTitle} ${t(($) => $['task.clearAll'], { ns: 'plugin' })}`}
className="shrink-0"
size="small"
variant="ghost"
onClick={onClearErrors}
>
{t(($) => $['task.clearAll'], { ns: 'plugin' })}
</Button>
</div>
<div className="w-full max-w-full min-w-0 overflow-hidden">
{errorPlugins.map((plugin) => (
<ErrorPluginItem
key={plugin.plugin_unique_identifier}
plugin={plugin}
getIconUrl={getIconUrl}
language={language}
onClear={() => onClearSingle(plugin.taskId, plugin.plugin_unique_identifier)}
/>
))}
</div>
</>
)}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</div>
)

View File

@ -5,8 +5,8 @@ import type { Collection } from '@/app/components/tools/types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -103,7 +103,7 @@ const PluginsPanelResults = ({
const { t } = useTranslation()
return (
<ScrollAreaRoot
<ScrollArea
className={cn(
'min-h-0 grow self-stretch overflow-hidden bg-components-panel-bg',
contentFrameClassName,
@ -169,7 +169,7 @@ const PluginsPanelResults = ({
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
)
}

View File

@ -16,7 +16,13 @@ import { Field, FieldItem, FieldLabel } from '@langgenius/dify-ui/field'
import { Fieldset, FieldsetLegend } from '@langgenius/dify-ui/fieldset'
import { Input } from '@langgenius/dify-ui/input'
import { Radio, RadioGroup } from '@langgenius/dify-ui/radio'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { Infotip } from '@/app/components/base/infotip'
@ -112,185 +118,203 @@ export default function ConfigCredential({ positionCenter, credential, onChange,
/>
</div>
</div>
<ScrollArea
className="min-h-0 flex-1 overflow-hidden"
slotClassNames={{
viewport: 'overscroll-contain',
content: 'space-y-4 pt-2 pr-8 pl-6',
}}
>
<Field name="auth_type" className="contents">
<Fieldset
render={
<RadioGroup<AuthType>
className="grid grid-cols-[repeat(auto-fit,minmax(8.5rem,1fr))] gap-2"
value={tempCredential.auth_type}
onValueChange={handleAuthTypeChange}
/>
}
>
<FieldsetLegend className="col-span-full py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.type'], { ns: 'tools' })}
</FieldsetLegend>
<SelectItem<AuthType>
text={t(($) => $['createTool.authMethod.types.none'], { ns: 'tools' })}
value={AuthType.none}
isChecked={tempCredential.auth_type === AuthType.none}
/>
<SelectItem<AuthType>
text={t(($) => $['createTool.authMethod.types.api_key_header'], {
ns: 'tools',
})}
value={AuthType.apiKeyHeader}
isChecked={tempCredential.auth_type === AuthType.apiKeyHeader}
/>
<SelectItem<AuthType>
text={t(($) => $['createTool.authMethod.types.api_key_query'], {
ns: 'tools',
})}
value={AuthType.apiKeyQuery}
isChecked={tempCredential.auth_type === AuthType.apiKeyQuery}
/>
</Fieldset>
</Field>
{tempCredential.auth_type === AuthType.apiKeyHeader && (
<>
<Field name="api_key_header_prefix" className="contents">
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent className="space-y-4 pt-2 pr-8 pl-6">
<Field name="auth_type" className="contents">
<Fieldset
render={
<RadioGroup<AuthHeaderPrefix>
<RadioGroup<AuthType>
className="grid grid-cols-[repeat(auto-fit,minmax(8.5rem,1fr))] gap-2"
value={tempCredential.api_key_header_prefix}
onValueChange={(value) =>
setTempCredential({ ...tempCredential, api_key_header_prefix: value })
}
value={tempCredential.auth_type}
onValueChange={handleAuthTypeChange}
/>
}
>
<FieldsetLegend className="col-span-full py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authHeaderPrefix.title'], { ns: 'tools' })}
{t(($) => $['createTool.authMethod.type'], { ns: 'tools' })}
</FieldsetLegend>
<SelectItem<AuthHeaderPrefix>
text={t(($) => $['createTool.authHeaderPrefix.types.basic'], {
ns: 'tools',
})}
value={AuthHeaderPrefix.basic}
isChecked={
tempCredential.api_key_header_prefix === AuthHeaderPrefix.basic
}
<SelectItem<AuthType>
text={t(($) => $['createTool.authMethod.types.none'], { ns: 'tools' })}
value={AuthType.none}
isChecked={tempCredential.auth_type === AuthType.none}
/>
<SelectItem<AuthHeaderPrefix>
text={t(($) => $['createTool.authHeaderPrefix.types.bearer'], {
<SelectItem<AuthType>
text={t(($) => $['createTool.authMethod.types.api_key_header'], {
ns: 'tools',
})}
value={AuthHeaderPrefix.bearer}
isChecked={
tempCredential.api_key_header_prefix === AuthHeaderPrefix.bearer
}
value={AuthType.apiKeyHeader}
isChecked={tempCredential.auth_type === AuthType.apiKeyHeader}
/>
<SelectItem<AuthHeaderPrefix>
text={t(($) => $['createTool.authHeaderPrefix.types.custom'], {
<SelectItem<AuthType>
text={t(($) => $['createTool.authMethod.types.api_key_query'], {
ns: 'tools',
})}
value={AuthHeaderPrefix.custom}
isChecked={
tempCredential.api_key_header_prefix === AuthHeaderPrefix.custom
}
value={AuthType.apiKeyQuery}
isChecked={tempCredential.auth_type === AuthType.apiKeyQuery}
/>
</Fieldset>
</Field>
<div>
<div className="flex items-center py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.key'], { ns: 'tools' })}
<Infotip
aria-label={t(($) => $['createTool.authMethod.keyTooltip'], {
ns: 'tools',
})}
className="ml-0.5 size-4"
popupClassName="w-[261px] text-text-tertiary"
>
{t(($) => $['createTool.authMethod.keyTooltip'], { ns: 'tools' })}
</Infotip>
</div>
<Input
value={tempCredential.api_key_header}
onChange={(e) =>
setTempCredential({ ...tempCredential, api_key_header: e.target.value })
}
placeholder={t(($) => $['createTool.authMethod.types.apiKeyPlaceholder'], {
ns: 'tools',
})!}
/>
</div>
<div>
<div className="py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.value'], { ns: 'tools' })}
</div>
<Input
value={tempCredential.api_key_value}
onChange={(e) =>
setTempCredential({ ...tempCredential, api_key_value: e.target.value })
}
placeholder={t(
($) => $['createTool.authMethod.types.apiValuePlaceholder'],
{
ns: 'tools',
},
)!}
/>
</div>
</>
)}
{tempCredential.auth_type === AuthType.apiKeyQuery && (
<>
<div>
<div className="flex items-center py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.queryParam'], { ns: 'tools' })}
<Infotip
aria-label={t(($) => $['createTool.authMethod.queryParamTooltip'], {
ns: 'tools',
})}
className="ml-0.5 size-4"
popupClassName="w-[261px] text-text-tertiary"
>
{t(($) => $['createTool.authMethod.queryParamTooltip'], { ns: 'tools' })}
</Infotip>
</div>
<Input
value={tempCredential.api_key_query_param}
onChange={(e) =>
setTempCredential({
...tempCredential,
api_key_query_param: e.target.value,
})
}
placeholder={t(
($) => $['createTool.authMethod.types.queryParamPlaceholder'],
{
ns: 'tools',
},
)!}
/>
</div>
<div>
<div className="py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.value'], { ns: 'tools' })}
</div>
<Input
value={tempCredential.api_key_value}
onChange={(e) =>
setTempCredential({ ...tempCredential, api_key_value: e.target.value })
}
placeholder={t(
($) => $['createTool.authMethod.types.apiValuePlaceholder'],
{
ns: 'tools',
},
)!}
/>
</div>
</>
)}
{tempCredential.auth_type === AuthType.apiKeyHeader && (
<>
<Field name="api_key_header_prefix" className="contents">
<Fieldset
render={
<RadioGroup<AuthHeaderPrefix>
className="grid grid-cols-[repeat(auto-fit,minmax(8.5rem,1fr))] gap-2"
value={tempCredential.api_key_header_prefix}
onValueChange={(value) =>
setTempCredential({
...tempCredential,
api_key_header_prefix: value,
})
}
/>
}
>
<FieldsetLegend className="col-span-full py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authHeaderPrefix.title'], { ns: 'tools' })}
</FieldsetLegend>
<SelectItem<AuthHeaderPrefix>
text={t(($) => $['createTool.authHeaderPrefix.types.basic'], {
ns: 'tools',
})}
value={AuthHeaderPrefix.basic}
isChecked={
tempCredential.api_key_header_prefix === AuthHeaderPrefix.basic
}
/>
<SelectItem<AuthHeaderPrefix>
text={t(($) => $['createTool.authHeaderPrefix.types.bearer'], {
ns: 'tools',
})}
value={AuthHeaderPrefix.bearer}
isChecked={
tempCredential.api_key_header_prefix === AuthHeaderPrefix.bearer
}
/>
<SelectItem<AuthHeaderPrefix>
text={t(($) => $['createTool.authHeaderPrefix.types.custom'], {
ns: 'tools',
})}
value={AuthHeaderPrefix.custom}
isChecked={
tempCredential.api_key_header_prefix === AuthHeaderPrefix.custom
}
/>
</Fieldset>
</Field>
<div>
<div className="flex items-center py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.key'], { ns: 'tools' })}
<Infotip
aria-label={t(($) => $['createTool.authMethod.keyTooltip'], {
ns: 'tools',
})}
className="ml-0.5 size-4"
popupClassName="w-[261px] text-text-tertiary"
>
{t(($) => $['createTool.authMethod.keyTooltip'], { ns: 'tools' })}
</Infotip>
</div>
<Input
value={tempCredential.api_key_header}
onChange={(e) =>
setTempCredential({
...tempCredential,
api_key_header: e.target.value,
})
}
placeholder={t(
($) => $['createTool.authMethod.types.apiKeyPlaceholder'],
{
ns: 'tools',
},
)!}
/>
</div>
<div>
<div className="py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.value'], { ns: 'tools' })}
</div>
<Input
value={tempCredential.api_key_value}
onChange={(e) =>
setTempCredential({
...tempCredential,
api_key_value: e.target.value,
})
}
placeholder={t(
($) => $['createTool.authMethod.types.apiValuePlaceholder'],
{
ns: 'tools',
},
)!}
/>
</div>
</>
)}
{tempCredential.auth_type === AuthType.apiKeyQuery && (
<>
<div>
<div className="flex items-center py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.queryParam'], { ns: 'tools' })}
<Infotip
aria-label={t(($) => $['createTool.authMethod.queryParamTooltip'], {
ns: 'tools',
})}
className="ml-0.5 size-4"
popupClassName="w-[261px] text-text-tertiary"
>
{t(($) => $['createTool.authMethod.queryParamTooltip'], {
ns: 'tools',
})}
</Infotip>
</div>
<Input
value={tempCredential.api_key_query_param}
onChange={(e) =>
setTempCredential({
...tempCredential,
api_key_query_param: e.target.value,
})
}
placeholder={t(
($) => $['createTool.authMethod.types.queryParamPlaceholder'],
{
ns: 'tools',
},
)!}
/>
</div>
<div>
<div className="py-2 system-sm-medium text-text-primary">
{t(($) => $['createTool.authMethod.value'], { ns: 'tools' })}
</div>
<Input
value={tempCredential.api_key_value}
onChange={(e) =>
setTempCredential({
...tempCredential,
api_key_value: e.target.value,
})
}
placeholder={t(
($) => $['createTool.authMethod.types.apiValuePlaceholder'],
{
ns: 'tools',
},
)!}
/>
</div>
</>
)}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
<div className="mt-4 flex shrink-0 justify-end space-x-2 px-6 py-4">
<Button onClick={onHide}>

View File

@ -8,8 +8,8 @@ import {
PreviewCardTrigger,
} from '@langgenius/dify-ui/preview-card'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -154,7 +154,7 @@ const Snippets = ({ searchText, onSearchTextChange, insertPayload, onInserted }:
) : !snippets.length ? (
<SnippetEmptyState />
) : (
<ScrollAreaRoot className="relative max-h-120 max-w-125 overflow-hidden">
<ScrollArea className="relative max-h-120 max-w-125 overflow-hidden">
<ScrollAreaViewport ref={viewportRef}>
<ScrollAreaContent className="p-1">
{snippets.map((item) => {
@ -183,7 +183,7 @@ const Snippets = ({ searchText, onSearchTextChange, insertPayload, onInserted }:
<ScrollAreaScrollbar orientation="vertical">
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
)
return (

View File

@ -7,8 +7,8 @@ import type {
} from '@/app/components/workflow/block-selector/types'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -171,7 +171,7 @@ const TriggerPluginItem: FC<Props> = ({
</BlockSelectorRow>
{!notShowProvider && hasAction && !isFold && (
<ScrollAreaRoot className="relative max-h-60 overflow-hidden overscroll-contain">
<ScrollArea className="relative max-h-60 overflow-hidden">
<ScrollAreaViewport
aria-label={t(($) => $['tabs.allTriggers'], { ns: 'workflow' })}
className="max-h-60 overscroll-contain"
@ -194,7 +194,7 @@ const TriggerPluginItem: FC<Props> = ({
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
)}
</div>
</div>

View File

@ -1,6 +1,12 @@
'use client'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useQuery } from '@tanstack/react-query'
import { useTranslation } from 'react-i18next'
import { useDocLink } from '@/context/i18n'
@ -49,35 +55,40 @@ export function AgentAccessPage({ agentId }: AgentAccessPageProps) {
</div>
</header>
<ScrollArea
className="min-h-0 flex-1 overflow-hidden"
slotClassNames={{
content: 'px-6 pt-2 pb-8',
}}
>
<div className="w-full min-w-0 space-y-6">
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
<WebAppAccessCard
agent={agentQuery.data}
agentId={agentId}
isLoading={agentQuery.isPending}
/>
<ServiceApiAccessCard agentId={agentId} />
</div>
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport>
<ScrollAreaContent className="px-6 pt-2 pb-8">
<div className="w-full min-w-0 space-y-6">
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
<WebAppAccessCard
agent={agentQuery.data}
agentId={agentId}
isLoading={agentQuery.isPending}
/>
<ServiceApiAccessCard agentId={agentId} />
</div>
<section aria-labelledby="agent-workflow-access-title">
<div className="mb-3">
<h3 id="agent-workflow-access-title" className="system-md-semibold text-text-primary">
{t(($) => $['agentDetail.access.workflow.title'])}
</h3>
<p className="mt-0.5 system-xs-regular text-text-tertiary">
{t(($) => $['agentDetail.access.workflow.description'])}
</p>
<section aria-labelledby="agent-workflow-access-title">
<div className="mb-3">
<h3
id="agent-workflow-access-title"
className="system-md-semibold text-text-primary"
>
{t(($) => $['agentDetail.access.workflow.title'])}
</h3>
<p className="mt-0.5 system-xs-regular text-text-tertiary">
{t(($) => $['agentDetail.access.workflow.description'])}
</p>
</div>
<WorkflowReferencesTable agentId={agentId} enabled={agentQuery.isSuccess} />
</section>
</div>
<WorkflowReferencesTable agentId={agentId} enabled={agentQuery.isSuccess} />
</section>
</div>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</AgentDetailSectionSurface>
)

View File

@ -687,7 +687,7 @@ describe('AgentConfigurePage', () => {
)
})
it('should expose a single configure workspace region after loading', () => {
it('should leave the configure landmark to the orchestrate panel after loading', () => {
const queryClient = new QueryClient()
mocks.queryState.composer = {
data: {},
@ -705,11 +705,8 @@ describe('AgentConfigurePage', () => {
)
expect(
screen.getAllByRole('region', { name: 'agentV2.agentDetail.sections.configure' }),
).toHaveLength(1)
expect(
screen.getByRole('region', { name: 'agentV2.agentDetail.sections.configure' }),
).toBeVisible()
screen.queryByRole('region', { name: 'agentV2.agentDetail.sections.configure' }),
).not.toBeInTheDocument()
expect(screen.getByRole('region', { name: 'orchestrate-panel' })).toBeInTheDocument()
})

View File

@ -13,7 +13,13 @@ import {
FileTreeLabel,
FileTreeList,
} from '@langgenius/dify-ui/file-tree'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { Fragment } from 'react'
type AgentFileTreeFolderOpenStrategy = (context: { file: AgentFileNode; depth: number }) => boolean
@ -133,8 +139,7 @@ export function AgentFileTree({
selectedFileId,
id,
treeLabel,
label,
labelledBy,
treeLabelledBy,
header,
className,
scrollAreaClassName,
@ -153,8 +158,7 @@ export function AgentFileTree({
selectedFileId?: string
id?: string
treeLabel?: string
label?: string
labelledBy?: string
treeLabelledBy?: string
header?: ReactNode
className?: string
scrollAreaClassName?: string
@ -174,35 +178,36 @@ export function AgentFileTree({
{header}
<ScrollArea
className={cn('min-h-0 w-full max-w-full flex-1 overflow-hidden', scrollAreaClassName)}
label={label}
labelledBy={labelledBy}
slotClassNames={{
viewport: 'max-h-[inherit]',
content: 'w-full max-w-full min-w-0!',
scrollbar: 'hidden',
}}
>
<FileTree
id={id}
<ScrollAreaViewport
aria-label={treeLabel}
className={cn('w-full max-w-full min-w-0 p-0', rootClassName)}
aria-labelledby={treeLabelledBy}
className="max-h-[inherit]"
role={treeLabel || treeLabelledBy ? 'region' : undefined}
>
<FileTreeList className={cn('w-full max-w-full min-w-0', listClassName)}>
<AgentFileTreeRows
files={files}
selectedFileId={selectedFileId}
depth={1}
folderOpenStrategy={folderOpenStrategy}
folderOpenState={folderOpenState}
onFolderOpenChange={onFolderOpenChange}
onFolderDoubleClick={onFolderDoubleClick}
onFolderOpen={onFolderOpen}
renderFile={renderFile}
renderFolderSuffix={renderFolderSuffix}
renderFolderPanel={renderFolderPanel}
/>
</FileTreeList>
</FileTree>
<ScrollAreaContent style={{ minWidth: 0 }} className="w-full max-w-full">
<FileTree id={id} className={cn('w-full max-w-full min-w-0 p-0', rootClassName)}>
<FileTreeList className={cn('w-full max-w-full min-w-0', listClassName)}>
<AgentFileTreeRows
files={files}
selectedFileId={selectedFileId}
depth={1}
folderOpenStrategy={folderOpenStrategy}
folderOpenState={folderOpenState}
onFolderOpenChange={onFolderOpenChange}
onFolderDoubleClick={onFolderDoubleClick}
onFolderOpen={onFolderOpen}
renderFile={renderFile}
renderFolderSuffix={renderFolderSuffix}
renderFolderPanel={renderFolderPanel}
/>
</FileTreeList>
</FileTree>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar className="hidden">
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</div>
)

View File

@ -9,7 +9,13 @@ import type { AgentBuildDraftChangedKey } from './build-draft-changes-context'
import type { Model } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { AgentComposerModel } from '@/features/agent-v2/agent-composer/form-state'
import { cn } from '@langgenius/dify-ui/cn'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { AgentOrchestrateAddActionsProvider } from './add-actions'
@ -134,36 +140,40 @@ export function AgentOrchestratePanel({
<AgentOrchestrateReadOnlyContext value={readOnly}>
<div aria-readonly={readOnly} className="flex min-h-0 flex-1 flex-col">
<ScrollArea
className="min-h-0 flex-1 overflow-hidden"
label={showHeader ? undefined : orchestrateLabel}
slotClassNames={{
viewport: 'overscroll-contain',
content: cn('min-h-full px-4 py-3', hasBottomAction && 'pb-20'),
scrollbar: hasBottomAction ? 'z-20' : undefined,
}}
>
<AgentConfigApiContextProvider value={configApiContext}>
<AgentOrchestrateAddActionsProvider>
<AgentBuildDraftChangedKeysProvider
changedKeys={
isBuildDraftActive ? buildDraftChangedKeys : EMPTY_BUILD_DRAFT_CHANGED_KEYS
}
>
<AgentModelField
currentModel={currentModel}
textGenerationModelList={textGenerationModelList}
onSelect={onSelectModel}
/>
<AgentPromptEditor />
<AgentSkills />
<AgentFiles />
<AgentTools />
<AgentKnowledgeRetrieval />
<AgentAdvancedSettings />
</AgentBuildDraftChangedKeysProvider>
</AgentOrchestrateAddActionsProvider>
</AgentConfigApiContextProvider>
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport
aria-label={showHeader ? undefined : orchestrateLabel}
aria-labelledby={showHeader ? orchestrateHeadingId : undefined}
className="overscroll-contain"
role="region"
>
<ScrollAreaContent className={cn('min-h-full px-4 py-3', hasBottomAction && 'pb-20')}>
<AgentConfigApiContextProvider value={configApiContext}>
<AgentOrchestrateAddActionsProvider>
<AgentBuildDraftChangedKeysProvider
changedKeys={
isBuildDraftActive ? buildDraftChangedKeys : EMPTY_BUILD_DRAFT_CHANGED_KEYS
}
>
<AgentModelField
currentModel={currentModel}
textGenerationModelList={textGenerationModelList}
onSelect={onSelectModel}
/>
<AgentPromptEditor />
<AgentSkills />
<AgentFiles />
<AgentTools />
<AgentKnowledgeRetrieval />
<AgentAdvancedSettings />
</AgentBuildDraftChangedKeysProvider>
</AgentOrchestrateAddActionsProvider>
</AgentConfigApiContextProvider>
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar className={hasBottomAction ? 'z-20' : undefined}>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</div>
</AgentOrchestrateReadOnlyContext>

View File

@ -10,8 +10,14 @@ import {
DialogTitle,
} from '@langgenius/dify-ui/dialog'
import { FileTreeFile } from '@langgenius/dify-ui/file-tree'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import { useCallback } from 'react'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useCallback, useId } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { AgentFileTree } from '../files/tree'
@ -113,7 +119,7 @@ function AgentSkillFileList({
<AgentFileTree
files={files}
selectedFileId={selectedFileId}
labelledBy="agent-skill-detail-files-heading"
treeLabelledBy="agent-skill-detail-files-heading"
className={cn('h-full bg-background-section p-1', fileListTreeClassName)}
listClassName={fileListTreeListClassName}
scrollAreaClassName="flex-1"
@ -337,8 +343,8 @@ export function AgentSkillDetailDialog({
skillName: string
detail: AgentSkillDetail
}) {
const { t } = useTranslation('agentV2')
const { t: tCommon } = useTranslation('common')
const dialogTitleId = useId()
const previewTitle = detail.filePreview?.fileName
const isHeaderDownloadLoading = detail.filePreview?.downloadActionLoadingTarget === 'header'
@ -355,7 +361,9 @@ export function AgentSkillDetailDialog({
)}
>
<DialogDescription className="sr-only">{detail.description}</DialogDescription>
<DialogTitle className="sr-only">{previewTitle || skillName}</DialogTitle>
<DialogTitle id={dialogTitleId} className="sr-only">
{previewTitle || skillName}
</DialogTitle>
<div className="min-h-0 w-full">
<AgentSkillFileList
fileListHeader={detail.fileListHeader}
@ -408,35 +416,42 @@ export function AgentSkillDetailDialog({
<DialogCloseButton className="static size-7 shrink-0 rounded-md" />
</div>
</div>
<ScrollArea
className="relative min-h-0 flex-1 overflow-hidden has-[>_:first-child:focus-visible]:outline-2 has-[>_:first-child:focus-visible]:outline-offset-0 has-[>_:first-child:focus-visible]:outline-state-accent-solid"
label={t(($) => $['agentDetail.configure.skills.detail.contentRegion'])}
slotClassNames={{
viewport: 'overscroll-contain outline-none focus-visible:outline-none',
content: 'flex min-h-full w-full max-w-full min-w-0 flex-col gap-2',
}}
>
{detail.filePreview && (
<AgentFilePreviewContent
binary={detail.filePreview.binary}
content={detail.filePreview.content}
downloadActionLoadingTarget={detail.filePreview.downloadActionLoadingTarget}
downloadUrl={detail.filePreview.downloadUrl}
fileName={detail.filePreview.fileName}
imageData={detail.filePreview.imageData}
isDownloadError={detail.filePreview.isDownloadError}
isDownloadLoading={detail.filePreview.isDownloadLoading}
isError={detail.filePreview.isError}
isImage={detail.filePreview.isImage}
isLoading={detail.filePreview.isLoading}
onDownloadFile={detail.onDownloadFile}
/>
)}
{detail.sections.map((section) => (
<div key={section.id} className="px-4">
<AgentSkillDetailSectionBlock section={section} />
</div>
))}
<ScrollArea className="relative min-h-0 flex-1 overflow-hidden has-[>_:first-child:focus-visible]:outline-2 has-[>_:first-child:focus-visible]:outline-offset-0 has-[>_:first-child:focus-visible]:outline-state-accent-solid">
<ScrollAreaViewport
aria-labelledby={dialogTitleId}
className="overscroll-contain outline-none focus-visible:outline-none"
role="region"
>
<ScrollAreaContent
style={{ minWidth: 0 }}
className="flex min-h-full w-full max-w-full flex-col gap-2"
>
{detail.filePreview && (
<AgentFilePreviewContent
binary={detail.filePreview.binary}
content={detail.filePreview.content}
downloadActionLoadingTarget={detail.filePreview.downloadActionLoadingTarget}
downloadUrl={detail.filePreview.downloadUrl}
fileName={detail.filePreview.fileName}
imageData={detail.filePreview.imageData}
isDownloadError={detail.filePreview.isDownloadError}
isDownloadLoading={detail.filePreview.isDownloadLoading}
isError={detail.filePreview.isError}
isImage={detail.filePreview.isImage}
isLoading={detail.filePreview.isLoading}
onDownloadFile={detail.onDownloadFile}
/>
)}
{detail.sections.map((section) => (
<div key={section.id} className="px-4">
<AgentSkillDetailSectionBlock section={section} />
</div>
))}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</div>
</DialogContent>

View File

@ -2,7 +2,6 @@
import type { ReactNode } from 'react'
import { cn } from '@langgenius/dify-ui/cn'
import { useTranslation } from 'react-i18next'
type AgentConfigureWorkspaceProps = {
'aria-busy'?: boolean
@ -19,11 +18,8 @@ export function AgentConfigureWorkspace({
rightPanel,
sidePanels,
}: AgentConfigureWorkspaceProps) {
const { t } = useTranslation('agentV2')
return (
<section
aria-label={t(($) => $['agentDetail.sections.configure'])}
aria-busy={ariaBusy}
className={cn(
'flex h-full min-w-0 flex-1 gap-1 overflow-hidden bg-background-body p-1',

View File

@ -3,8 +3,8 @@ import type { ReactNode, TdHTMLAttributes, ThHTMLAttributes } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -52,7 +52,7 @@ export function AgentLogsTable({
</table>
</div>
<ScrollAreaRoot className="relative min-h-0 flex-1 overflow-hidden">
<ScrollArea className="relative min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport
aria-label={t(($) => $['agentDetail.logs.title'])}
role="region"
@ -78,7 +78,7 @@ export function AgentLogsTable({
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
</div>
)
}

View File

@ -3,7 +3,13 @@
import type { AgentLogSourceResponse } from '@dify/contracts/api/console/agent/types.gen'
import type { ReactNode } from 'react'
import { Button } from '@langgenius/dify-ui/button'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import {
Select,
SelectContent,
@ -129,46 +135,48 @@ export function AgentMonitoringPage({ agentId }: AgentMonitoringPageProps) {
</div>
</header>
<ScrollArea
className="min-h-0 flex-1 overflow-hidden"
slotClassNames={{
content: 'px-6 pt-2 pb-3',
}}
>
{shouldShowInitialSkeleton && <AgentMonitoringSkeletonGrid />}
{shouldShowError && (
<AgentMonitoringState>
<div className="flex items-center justify-center gap-2">
<span>{t(($) => $['agentDetail.monitoring.loadFailed'])}</span>
<Button
variant="secondary"
size="small"
onClick={() => {
void statisticsQuery.refetch()
}}
>
{tCommon(($) => $['operation.retry'])}
</Button>
</div>
</AgentMonitoringState>
)}
{!shouldShowInitialSkeleton && !shouldShowError && (
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
{metrics.map((metric) => (
<AgentMonitoringChart
key={metric.id}
titleKey={metric.titleKey}
explanationKey={metric.explanationKey}
summaryValue={metric.summaryValue}
rows={metric.rows}
chartType={metric.chartType}
valueKey={metric.valueKey}
unitKey={metric.unitKey}
yMaxWhenEmpty={metric.yMaxWhenEmpty}
/>
))}
</div>
)}
<ScrollArea className="min-h-0 flex-1 overflow-hidden">
<ScrollAreaViewport>
<ScrollAreaContent className="px-6 pt-2 pb-3">
{shouldShowInitialSkeleton && <AgentMonitoringSkeletonGrid />}
{shouldShowError && (
<AgentMonitoringState>
<div className="flex items-center justify-center gap-2">
<span>{t(($) => $['agentDetail.monitoring.loadFailed'])}</span>
<Button
variant="secondary"
size="small"
onClick={() => {
void statisticsQuery.refetch()
}}
>
{tCommon(($) => $['operation.retry'])}
</Button>
</div>
</AgentMonitoringState>
)}
{!shouldShowInitialSkeleton && !shouldShowError && (
<div className="grid w-full grid-cols-1 gap-3 xl:grid-cols-2">
{metrics.map((metric) => (
<AgentMonitoringChart
key={metric.id}
titleKey={metric.titleKey}
explanationKey={metric.explanationKey}
summaryValue={metric.summaryValue}
rows={metric.rows}
chartType={metric.chartType}
valueKey={metric.valueKey}
unitKey={metric.unitKey}
yMaxWhenEmpty={metric.yMaxWhenEmpty}
/>
))}
</div>
)}
</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
</AgentDetailSectionSurface>
)

View File

@ -3,8 +3,8 @@
import type { AgentAppPartial } from '@dify/contracts/api/console/agent/types.gen'
import type { RosterFilterValue } from './components/roster-filter'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaRoot,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
@ -108,7 +108,7 @@ export default function RosterPage() {
</div>
<div className="min-h-0 flex-1">
<ScrollAreaRoot className="relative h-full min-h-0 min-w-0 overflow-hidden">
<ScrollArea className="relative h-full min-h-0 min-w-0 overflow-hidden">
<ScrollAreaViewport tabIndex={-1} className="overscroll-contain">
<ScrollAreaContent className="min-h-full px-8 pt-2 pb-8">
<AgentRosterList
@ -127,7 +127,7 @@ export default function RosterPage() {
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollAreaRoot>
</ScrollArea>
</div>
</div>
)

View File

@ -3,7 +3,13 @@
import type { ReactNode } from 'react'
import type { GuideStep } from '@/features/deployments/create-guide/state/types'
import { cn } from '@langgenius/dify-ui/cn'
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
import {
ScrollArea,
ScrollAreaContent,
ScrollAreaScrollbar,
ScrollAreaThumb,
ScrollAreaViewport,
} from '@langgenius/dify-ui/scroll-area'
import { useTranslation } from 'react-i18next'
import { TitleTooltip } from '@/features/deployments/shared/components/title-tooltip'
@ -161,14 +167,13 @@ export function GuideCard({
return (
<div className="flex min-h-0 w-full min-w-0 flex-1 flex-col">
{contentScrollable ? (
<ScrollArea
className="min-h-0 flex-1"
slotClassNames={{
viewport: 'overscroll-contain',
content: 'min-h-full pt-0.5 pb-6',
}}
>
{children}
<ScrollArea className="min-h-0 flex-1">
<ScrollAreaViewport className="overscroll-contain">
<ScrollAreaContent className="min-h-full pt-0.5 pb-6">{children}</ScrollAreaContent>
</ScrollAreaViewport>
<ScrollAreaScrollbar>
<ScrollAreaThumb />
</ScrollAreaScrollbar>
</ScrollArea>
) : (
<div className="min-h-0 flex-1 overflow-hidden pt-0.5 pb-6">{children}</div>

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "يشغل Preview الوكيل المكتمل كما سيراه المستخدمون، مع ردود واضحة وميزات الدردشة.",
"agentDetail.configure.rightPanel.previewTipTitle": "عاين وكيلك",
"agentDetail.configure.skills.add": "إضافة مهارة",
"agentDetail.configure.skills.detail.contentRegion": "محتوى تفاصيل المهارة",
"agentDetail.configure.skills.detail.fileCount": "{{count}} ملفات",
"agentDetail.configure.skills.detail.files": "الملفات",
"agentDetail.configure.skills.empty.description": "تمنح المهارات الوكيل خبرة قابلة لإعادة الاستخدام يمكنه استدعاؤها أثناء العمل",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview führt den fertigen Agenten so aus, wie deine Benutzer ihn sehen, mit klaren Antworten und Chat-Funktionen.",
"agentDetail.configure.rightPanel.previewTipTitle": "Agenten vorschauen",
"agentDetail.configure.skills.add": "Skill hinzufügen",
"agentDetail.configure.skills.detail.contentRegion": "Skill-Detailinhalt",
"agentDetail.configure.skills.detail.fileCount": "{{count}} DATEIEN",
"agentDetail.configure.skills.detail.files": "Dateien",
"agentDetail.configure.skills.empty.description": "Skills geben dem Agenten wiederverwendbare Fachkenntnisse, die er bei der Arbeit nutzen kann",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview runs the finished agent the way your users will see it, with clean replies and chat features.",
"agentDetail.configure.rightPanel.previewTipTitle": "Preview your agent",
"agentDetail.configure.skills.add": "Add skill",
"agentDetail.configure.skills.detail.contentRegion": "Skill detail content",
"agentDetail.configure.skills.detail.fileCount": "{{count}} FILES",
"agentDetail.configure.skills.detail.files": "Files",
"agentDetail.configure.skills.empty.description": "Skills give the agent reusable expertise it can call while working",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview ejecuta el agente terminado como lo verán tus usuarios, con respuestas claras y funciones de chat.",
"agentDetail.configure.rightPanel.previewTipTitle": "Previsualiza tu agente",
"agentDetail.configure.skills.add": "Agregar habilidad",
"agentDetail.configure.skills.detail.contentRegion": "Contenido de los detalles de la habilidad",
"agentDetail.configure.skills.detail.fileCount": "{{count}} ARCHIVOS",
"agentDetail.configure.skills.detail.files": "Archivos",
"agentDetail.configure.skills.empty.description": "Las habilidades le dan al agente experiencia reutilizable que puede invocar mientras trabaja",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview عامل تکمیل‌شده را همان‌طور که کاربران می‌بینند اجرا می‌کند، با پاسخ‌های تمیز و قابلیت‌های گفتگو.",
"agentDetail.configure.rightPanel.previewTipTitle": "پیش‌نمایش عامل",
"agentDetail.configure.skills.add": "افزودن مهارت",
"agentDetail.configure.skills.detail.contentRegion": "محتوای جزئیات مهارت",
"agentDetail.configure.skills.detail.fileCount": "{{count}} فایل",
"agentDetail.configure.skills.detail.files": "فایل‌ها",
"agentDetail.configure.skills.empty.description": "مهارت‌ها به عامل تخصص قابل استفاده مجدد می‌دهند که هنگام کار می‌تواند فراخوانی کند",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview exécute lagent final comme vos utilisateurs le verront, avec des réponses claires et les fonctions de chat.",
"agentDetail.configure.rightPanel.previewTipTitle": "Prévisualiser votre agent",
"agentDetail.configure.skills.add": "Ajouter une compétence",
"agentDetail.configure.skills.detail.contentRegion": "Contenu des détails de la compétence",
"agentDetail.configure.skills.detail.fileCount": "{{count}} FICHIERS",
"agentDetail.configure.skills.detail.files": "Fichiers",
"agentDetail.configure.skills.empty.description": "Les compétences offrent à lagent une expertise réutilisable quil peut invoquer pendant son travail",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview तैयार agent को वैसे चलाता है जैसे आपके users उसे देखेंगे, साफ replies और chat features के साथ।",
"agentDetail.configure.rightPanel.previewTipTitle": "अपने agent का preview करें",
"agentDetail.configure.skills.add": "कौशल जोड़ें",
"agentDetail.configure.skills.detail.contentRegion": "कौशल विवरण सामग्री",
"agentDetail.configure.skills.detail.fileCount": "{{count}} फ़ाइलें",
"agentDetail.configure.skills.detail.files": "फ़ाइलें",
"agentDetail.configure.skills.empty.description": "कौशल एजेंट को पुनः उपयोग योग्य विशेषज्ञता देते हैं जिसे वह काम करते समय कॉल कर सकता है",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview menjalankan agent yang sudah selesai seperti yang akan dilihat pengguna, dengan balasan rapi dan fitur chat.",
"agentDetail.configure.rightPanel.previewTipTitle": "Pratinjau agen Anda",
"agentDetail.configure.skills.add": "Tambahkan keterampilan",
"agentDetail.configure.skills.detail.contentRegion": "Konten detail keterampilan",
"agentDetail.configure.skills.detail.fileCount": "{{count}} FILE",
"agentDetail.configure.skills.detail.files": "File",
"agentDetail.configure.skills.empty.description": "Keterampilan memberi agen keahlian yang dapat digunakan kembali yang bisa dipanggil saat bekerja",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview esegue lagente completato come lo vedranno gli utenti, con risposte pulite e funzionalità di chat.",
"agentDetail.configure.rightPanel.previewTipTitle": "Anteprima del tuo agente",
"agentDetail.configure.skills.add": "Aggiungi abilità",
"agentDetail.configure.skills.detail.contentRegion": "Contenuto dei dettagli dellabilità",
"agentDetail.configure.skills.detail.fileCount": "{{count}} FILE",
"agentDetail.configure.skills.detail.files": "File",
"agentDetail.configure.skills.empty.description": "Le abilità forniscono allagente competenze riutilizzabili che può richiamare durante il lavoro",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview は完成したエージェントをユーザーに見える形で実行し、整った返信とチャット機能を確認できます。",
"agentDetail.configure.rightPanel.previewTipTitle": "エージェントをプレビュー",
"agentDetail.configure.skills.add": "スキルを追加",
"agentDetail.configure.skills.detail.contentRegion": "スキル詳細コンテンツ",
"agentDetail.configure.skills.detail.fileCount": "{{count}} 個のファイル",
"agentDetail.configure.skills.detail.files": "ファイル",
"agentDetail.configure.skills.empty.description": "スキルはエージェントが作業中に呼び出せる再利用可能な専門知識を提供します",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview는 사용자가 보게 될 방식으로 완성된 에이전트를 실행하며, 깔끔한 답변과 채팅 기능을 보여줍니다.",
"agentDetail.configure.rightPanel.previewTipTitle": "에이전트 미리보기",
"agentDetail.configure.skills.add": "스킬 추가",
"agentDetail.configure.skills.detail.contentRegion": "스킬 상세 내용",
"agentDetail.configure.skills.detail.fileCount": "파일 {{count}}개",
"agentDetail.configure.skills.detail.files": "파일",
"agentDetail.configure.skills.empty.description": "스킬은 에이전트가 작업하면서 호출할 수 있는 재사용 가능한 전문 능력을 제공합니다",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview voert de voltooide agent uit zoals je gebruikers die zien, met duidelijke antwoorden en chatfuncties.",
"agentDetail.configure.rightPanel.previewTipTitle": "Voorbeeld van je agent",
"agentDetail.configure.skills.add": "Skill toevoegen",
"agentDetail.configure.skills.detail.contentRegion": "Skill-detailinhoud",
"agentDetail.configure.skills.detail.fileCount": "{{count}} BESTANDEN",
"agentDetail.configure.skills.detail.files": "Bestanden",
"agentDetail.configure.skills.empty.description": "Skills geven de agent herbruikbare expertise die hij tijdens het werken kan inzetten",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview uruchamia gotowego agenta tak, jak zobaczą go użytkownicy, z przejrzystymi odpowiedziami i funkcjami czatu.",
"agentDetail.configure.rightPanel.previewTipTitle": "Podgląd agenta",
"agentDetail.configure.skills.add": "Dodaj umiejętność",
"agentDetail.configure.skills.detail.contentRegion": "Zawartość szczegółów umiejętności",
"agentDetail.configure.skills.detail.fileCount": "{{count}} PLIKÓW",
"agentDetail.configure.skills.detail.files": "Pliki",
"agentDetail.configure.skills.empty.description": "Umiejętności dają agentowi reużywalną wiedzę, którą może wywołać podczas pracy",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview executa o agente finalizado como seus usuários o verão, com respostas claras e recursos de chat.",
"agentDetail.configure.rightPanel.previewTipTitle": "Pré-visualize seu agente",
"agentDetail.configure.skills.add": "Adicionar habilidade",
"agentDetail.configure.skills.detail.contentRegion": "Conteúdo dos detalhes da habilidade",
"agentDetail.configure.skills.detail.fileCount": "{{count}} ARQUIVOS",
"agentDetail.configure.skills.detail.files": "Arquivos",
"agentDetail.configure.skills.empty.description": "As habilidades dão ao agente expertise reutilizável que ele pode invocar enquanto trabalha",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview rulează agentul finalizat așa cum îl vor vedea utilizatorii, cu răspunsuri clare și funcții de chat.",
"agentDetail.configure.rightPanel.previewTipTitle": "Previzualizează agentul",
"agentDetail.configure.skills.add": "Adaugă abilitate",
"agentDetail.configure.skills.detail.contentRegion": "Conținutul detaliilor abilității",
"agentDetail.configure.skills.detail.fileCount": "{{count}} FIȘIERE",
"agentDetail.configure.skills.detail.files": "Fișiere",
"agentDetail.configure.skills.empty.description": "Abilitățile oferă agentului expertiză reutilizabilă pe care o poate apela în timpul lucrului",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview запускает готового агента так, как его увидят пользователи, с чистыми ответами и функциями чата.",
"agentDetail.configure.rightPanel.previewTipTitle": "Предпросмотр агента",
"agentDetail.configure.skills.add": "Добавить навык",
"agentDetail.configure.skills.detail.contentRegion": "Содержимое деталей навыка",
"agentDetail.configure.skills.detail.fileCount": "{{count}} ФАЙЛОВ",
"agentDetail.configure.skills.detail.files": "Файлы",
"agentDetail.configure.skills.empty.description": "Навыки дают агенту переиспользуемую экспертизу, которую он может вызывать в работе",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview zažene dokončanega agenta tako, kot ga bodo videli uporabniki, z jasnimi odgovori in funkcijami klepeta.",
"agentDetail.configure.rightPanel.previewTipTitle": "Predogled agenta",
"agentDetail.configure.skills.add": "Dodaj veščino",
"agentDetail.configure.skills.detail.contentRegion": "Vsebina podrobnosti veščine",
"agentDetail.configure.skills.detail.fileCount": "{{count}} DATOTEK",
"agentDetail.configure.skills.detail.files": "Datoteke",
"agentDetail.configure.skills.empty.description": "Veščine agentu dajejo ponovno uporabno strokovnost, ki jo lahko kliče med delom",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview เรียกใช้เอเจนต์ที่เสร็จแล้วในแบบที่ผู้ใช้จะเห็น พร้อมคำตอบที่ชัดเจนและฟีเจอร์แชท",
"agentDetail.configure.rightPanel.previewTipTitle": "ดูตัวอย่างเอเจนต์",
"agentDetail.configure.skills.add": "เพิ่มทักษะ",
"agentDetail.configure.skills.detail.contentRegion": "เนื้อหารายละเอียดทักษะ",
"agentDetail.configure.skills.detail.fileCount": "{{count}} ไฟล์",
"agentDetail.configure.skills.detail.files": "ไฟล์",
"agentDetail.configure.skills.empty.description": "ทักษะให้ตัวแทนมีความเชี่ยวชาญที่นำกลับมาใช้ใหม่ได้ซึ่งสามารถเรียกใช้ได้ขณะทำงาน",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview, tamamlanan ajanı kullanıcılarınızın göreceği şekilde, temiz yanıtlar ve sohbet özellikleriyle çalıştırır.",
"agentDetail.configure.rightPanel.previewTipTitle": "Ajanınızı önizleyin",
"agentDetail.configure.skills.add": "Beceri ekle",
"agentDetail.configure.skills.detail.contentRegion": "Beceri detay içeriği",
"agentDetail.configure.skills.detail.fileCount": "{{count}} DOSYA",
"agentDetail.configure.skills.detail.files": "Dosyalar",
"agentDetail.configure.skills.empty.description": "Beceriler, ajana çalışırken çağırabileceği yeniden kullanılabilir uzmanlık sağlar",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview запускає готового агента так, як його побачать користувачі, з чистими відповідями та функціями чату.",
"agentDetail.configure.rightPanel.previewTipTitle": "Попередній перегляд агента",
"agentDetail.configure.skills.add": "Додати навичку",
"agentDetail.configure.skills.detail.contentRegion": "Вміст деталей навички",
"agentDetail.configure.skills.detail.fileCount": "{{count}} ФАЙЛІВ",
"agentDetail.configure.skills.detail.files": "Файли",
"agentDetail.configure.skills.empty.description": "Навички дають агенту перевикористовну експертизу, яку він може викликати під час роботи",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "Preview chạy agent hoàn chỉnh theo cách người dùng sẽ thấy, với câu trả lời rõ ràng và các tính năng chat.",
"agentDetail.configure.rightPanel.previewTipTitle": "Xem trước agent",
"agentDetail.configure.skills.add": "Thêm kỹ năng",
"agentDetail.configure.skills.detail.contentRegion": "Nội dung chi tiết kỹ năng",
"agentDetail.configure.skills.detail.fileCount": "{{count}} TỆP",
"agentDetail.configure.skills.detail.files": "Tệp",
"agentDetail.configure.skills.empty.description": "Kỹ năng mang đến cho tác nhân chuyên môn có thể tái sử dụng mà nó có thể gọi khi làm việc",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "像用户一样测试它。你的交互不会影响 Agent 后续的行为。",
"agentDetail.configure.rightPanel.previewTipTitle": "预览你的 Agent",
"agentDetail.configure.skills.add": "添加 Skill",
"agentDetail.configure.skills.detail.contentRegion": "Skill 详情内容",
"agentDetail.configure.skills.detail.fileCount": "{{count}} 个文件",
"agentDetail.configure.skills.detail.files": "文件",
"agentDetail.configure.skills.empty.description": "Skill 为 Agent 提供工作时可调用的可复用专业能力",

View File

@ -203,7 +203,6 @@
"agentDetail.configure.rightPanel.previewTipBody": "像使用者一樣測試它。你的互動不會影響 Agent 後續的行為。",
"agentDetail.configure.rightPanel.previewTipTitle": "預覽你的 Agent",
"agentDetail.configure.skills.add": "新增 Skill",
"agentDetail.configure.skills.detail.contentRegion": "Skill 詳情內容",
"agentDetail.configure.skills.detail.fileCount": "{{count}} 個檔案",
"agentDetail.configure.skills.detail.files": "檔案",
"agentDetail.configure.skills.empty.description": "Skill 為 Agent 提供工作時可呼叫的可複用專業能力",