mirror of
https://github.com/langgenius/dify.git
synced 2026-04-15 09:57:03 +08:00
Merge branch 'feat/collaboration2' of github.com:langgenius/dify into feat/collaboration2
This commit is contained in:
commit
30cc5ac417
@ -11,26 +11,22 @@ vi.mock('../../../base/app-icon', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/ui/dialog', () => ({
|
||||
Dialog: ({ open, onOpenChange, children }: {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
vi.mock('@/app/components/base/content-dialog', () => ({
|
||||
default: ({ show, onClose, children, className }: {
|
||||
show: boolean
|
||||
onClose: () => void
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
}) => (
|
||||
open
|
||||
show
|
||||
? (
|
||||
<div data-testid="detail-drawer">
|
||||
<button type="button" data-testid="dialog-close" onClick={() => onOpenChange(false)}>Close</button>
|
||||
<div data-testid="content-dialog" className={className}>
|
||||
<button type="button" data-testid="dialog-close" onClick={onClose}>Close</button>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
: null
|
||||
),
|
||||
DialogPortal: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
DialogBackdrop: () => <div data-testid="dialog-backdrop" />,
|
||||
DialogPopup: ({ children, className }: { children: React.ReactNode, className?: string }) => (
|
||||
<div data-testid="dialog-popup" className={className}>{children}</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view', () => ({
|
||||
@ -100,12 +96,12 @@ describe('AppInfoDetailPanel', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should not render when show is false', () => {
|
||||
render(<AppInfoDetailPanel {...defaultProps} show={false} />)
|
||||
expect(screen.queryByTestId('detail-drawer')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('content-dialog')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render dialog when show is true', () => {
|
||||
render(<AppInfoDetailPanel {...defaultProps} />)
|
||||
expect(screen.getByTestId('detail-drawer')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('content-dialog')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display app name', () => {
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
import type { Operation } from './app-operations'
|
||||
import type { AppInfoModalType } from './use-app-info-actions'
|
||||
import type { App, AppSSO } from '@/types/app'
|
||||
import {
|
||||
RiDeleteBinLine,
|
||||
RiEditLine,
|
||||
RiExchange2Line,
|
||||
RiFileCopy2Line,
|
||||
RiFileDownloadLine,
|
||||
RiFileUploadLine,
|
||||
} from '@remixicon/react'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import CardView from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/card-view'
|
||||
import Button from '@/app/components/base/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogBackdrop,
|
||||
DialogPopup,
|
||||
DialogPortal,
|
||||
} from '@/app/components/base/ui/dialog'
|
||||
import ContentDialog from '@/app/components/base/content-dialog'
|
||||
import { AppModeEnum } from '@/types/app'
|
||||
import AppIcon from '../../base/app-icon'
|
||||
import { getAppModeLabel } from './app-mode-labels'
|
||||
@ -34,28 +37,23 @@ const AppInfoDetailPanel = ({
|
||||
}: AppInfoDetailPanelProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const handleOpenChange = useCallback((open: boolean) => {
|
||||
if (!open)
|
||||
onClose()
|
||||
}, [onClose])
|
||||
|
||||
const primaryOperations = useMemo<Operation[]>(() => [
|
||||
{
|
||||
id: 'edit',
|
||||
title: t('editApp', { ns: 'app' }),
|
||||
icon: <span className="i-ri-edit-line size-4" />,
|
||||
icon: <RiEditLine />,
|
||||
onClick: () => openModal('edit'),
|
||||
},
|
||||
{
|
||||
id: 'duplicate',
|
||||
title: t('duplicate', { ns: 'app' }),
|
||||
icon: <span className="i-ri-file-copy-2-line size-4" />,
|
||||
icon: <RiFileCopy2Line />,
|
||||
onClick: () => openModal('duplicate'),
|
||||
},
|
||||
{
|
||||
id: 'export',
|
||||
title: t('export', { ns: 'app' }),
|
||||
icon: <span className="i-ri-file-download-line size-4" />,
|
||||
icon: <RiFileDownloadLine />,
|
||||
onClick: exportCheck,
|
||||
},
|
||||
], [t, openModal, exportCheck])
|
||||
@ -65,7 +63,7 @@ const AppInfoDetailPanel = ({
|
||||
? [{
|
||||
id: 'import',
|
||||
title: t('common.importDSL', { ns: 'workflow' }),
|
||||
icon: <span className="i-ri-file-upload-line size-4" />,
|
||||
icon: <RiFileUploadLine />,
|
||||
onClick: () => openModal('importDSL'),
|
||||
}]
|
||||
: [],
|
||||
@ -79,7 +77,7 @@ const AppInfoDetailPanel = ({
|
||||
{
|
||||
id: 'delete',
|
||||
title: t('operation.delete', { ns: 'common' }),
|
||||
icon: <span className="i-ri-delete-bin-line size-4" />,
|
||||
icon: <RiDeleteBinLine />,
|
||||
onClick: () => openModal('delete'),
|
||||
},
|
||||
], [appDetail.mode, t, openModal])
|
||||
@ -90,64 +88,63 @@ const AppInfoDetailPanel = ({
|
||||
return {
|
||||
id: 'switch',
|
||||
title: t('switch', { ns: 'app' }),
|
||||
icon: <span className="i-ri-exchange-2-line size-4" />,
|
||||
icon: <RiExchange2Line />,
|
||||
onClick: () => openModal('switch'),
|
||||
}
|
||||
}, [appDetail.mode, t, openModal])
|
||||
|
||||
return (
|
||||
<Dialog open={show} onOpenChange={handleOpenChange}>
|
||||
<DialogPortal>
|
||||
<DialogBackdrop className="duration-300" />
|
||||
<DialogPopup className="inset-y-0 left-0 m-2 flex w-[420px] flex-col rounded-2xl border-r border-divider-burn bg-app-detail-bg transition-transform duration-300 ease-out data-ending-style:-translate-x-[calc(100%+theme(spacing.2))] data-starting-style:-translate-x-[calc(100%+theme(spacing.2))] motion-reduce:transition-none">
|
||||
<div className="flex shrink-0 flex-col items-start justify-center gap-3 self-stretch p-4">
|
||||
<div className="flex items-center gap-3 self-stretch">
|
||||
<AppIcon
|
||||
size="large"
|
||||
iconType={appDetail.icon_type}
|
||||
icon={appDetail.icon}
|
||||
background={appDetail.icon_background}
|
||||
imageUrl={appDetail.icon_url}
|
||||
/>
|
||||
<div className="flex flex-1 flex-col items-start justify-center overflow-hidden">
|
||||
<div className="w-full truncate system-md-semibold text-text-secondary">{appDetail.name}</div>
|
||||
<div className="system-2xs-medium-uppercase text-text-tertiary">
|
||||
{getAppModeLabel(appDetail.mode, t)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{appDetail.description && (
|
||||
<div className="max-h-[105px] w-full max-w-full overflow-y-auto system-xs-regular wrap-anywhere text-text-tertiary">
|
||||
{appDetail.description}
|
||||
</div>
|
||||
)}
|
||||
<AppOperations
|
||||
gap={4}
|
||||
primaryOperations={primaryOperations}
|
||||
secondaryOperations={secondaryOperations}
|
||||
/>
|
||||
</div>
|
||||
<CardView
|
||||
appId={appDetail.id}
|
||||
isInPanel={true}
|
||||
className="flex flex-1 flex-col gap-2 overflow-auto px-2 py-1"
|
||||
<ContentDialog
|
||||
show={show}
|
||||
onClose={onClose}
|
||||
className="absolute bottom-2 left-2 top-2 flex w-[420px] flex-col rounded-2xl p-0!"
|
||||
>
|
||||
<div className="flex shrink-0 flex-col items-start justify-center gap-3 self-stretch p-4">
|
||||
<div className="flex items-center gap-3 self-stretch">
|
||||
<AppIcon
|
||||
size="large"
|
||||
iconType={appDetail.icon_type}
|
||||
icon={appDetail.icon}
|
||||
background={appDetail.icon_background}
|
||||
imageUrl={appDetail.icon_url}
|
||||
/>
|
||||
{switchOperation && (
|
||||
<div className="flex min-h-fit shrink-0 flex-col items-start justify-center gap-3 self-stretch pb-2">
|
||||
<Button
|
||||
size="medium"
|
||||
variant="ghost"
|
||||
className="gap-0.5"
|
||||
onClick={switchOperation.onClick}
|
||||
>
|
||||
{switchOperation.icon}
|
||||
<span className="system-sm-medium text-text-tertiary">{switchOperation.title}</span>
|
||||
</Button>
|
||||
<div className="flex flex-1 flex-col items-start justify-center overflow-hidden">
|
||||
<div className="w-full truncate text-text-secondary system-md-semibold">{appDetail.name}</div>
|
||||
<div className="text-text-tertiary system-2xs-medium-uppercase">
|
||||
{getAppModeLabel(appDetail.mode, t)}
|
||||
</div>
|
||||
)}
|
||||
</DialogPopup>
|
||||
</DialogPortal>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
{appDetail.description && (
|
||||
<div className="overflow-wrap-anywhere max-h-[105px] w-full max-w-full overflow-y-auto whitespace-normal wrap-break-word text-text-tertiary system-xs-regular">
|
||||
{appDetail.description}
|
||||
</div>
|
||||
)}
|
||||
<AppOperations
|
||||
gap={4}
|
||||
primaryOperations={primaryOperations}
|
||||
secondaryOperations={secondaryOperations}
|
||||
/>
|
||||
</div>
|
||||
<CardView
|
||||
appId={appDetail.id}
|
||||
isInPanel={true}
|
||||
className="flex flex-1 flex-col gap-2 overflow-auto px-2 py-1"
|
||||
/>
|
||||
{switchOperation && (
|
||||
<div className="flex min-h-fit shrink-0 flex-col items-start justify-center gap-3 self-stretch pb-2">
|
||||
<Button
|
||||
size="medium"
|
||||
variant="ghost"
|
||||
className="gap-0.5"
|
||||
onClick={switchOperation.onClick}
|
||||
>
|
||||
{switchOperation.icon}
|
||||
<span className="text-text-tertiary system-sm-medium">{switchOperation.title}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</ContentDialog>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { JSX } from 'react'
|
||||
import { RiMoreLine } from '@remixicon/react'
|
||||
import { cloneElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Button from '@/app/components/base/button'
|
||||
import { cn } from '@/utils/classnames'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '../../base/portal-to-follow-elem'
|
||||
|
||||
export type Operation = {
|
||||
@ -133,8 +133,8 @@ const AppOperations = ({
|
||||
className="gap-px"
|
||||
tabIndex={-1}
|
||||
>
|
||||
{cloneElement(operation.icon, { className: cn(operation.icon.props.className, 'h-3.5 w-3.5 text-components-button-secondary-text') })}
|
||||
<span className="system-xs-medium text-components-button-secondary-text">
|
||||
{cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })}
|
||||
<span className="text-components-button-secondary-text system-xs-medium">
|
||||
{operation.title}
|
||||
</span>
|
||||
</Button>
|
||||
@ -146,8 +146,8 @@ const AppOperations = ({
|
||||
className="gap-px"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<span className="i-ri-more-line h-3.5 w-3.5 text-components-button-secondary-text" />
|
||||
<span className="system-xs-medium text-components-button-secondary-text">
|
||||
<RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
|
||||
<span className="text-components-button-secondary-text system-xs-medium">
|
||||
{t('operation.more', { ns: 'common' })}
|
||||
</span>
|
||||
</Button>
|
||||
@ -162,8 +162,8 @@ const AppOperations = ({
|
||||
className="gap-px"
|
||||
onClick={operation.onClick}
|
||||
>
|
||||
{cloneElement(operation.icon, { className: cn(operation.icon.props.className, 'h-3.5 w-3.5 text-components-button-secondary-text') })}
|
||||
<span className="system-xs-medium text-components-button-secondary-text">
|
||||
{cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })}
|
||||
<span className="text-components-button-secondary-text system-xs-medium">
|
||||
{operation.title}
|
||||
</span>
|
||||
</Button>
|
||||
@ -181,8 +181,8 @@ const AppOperations = ({
|
||||
variant="secondary"
|
||||
className="gap-px"
|
||||
>
|
||||
<span className="i-ri-more-line h-3.5 w-3.5 text-components-button-secondary-text" />
|
||||
<span className="system-xs-medium text-components-button-secondary-text">
|
||||
<RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
|
||||
<span className="text-components-button-secondary-text system-xs-medium">
|
||||
{t('operation.more', { ns: 'common' })}
|
||||
</span>
|
||||
</Button>
|
||||
@ -199,8 +199,8 @@ const AppOperations = ({
|
||||
className="flex h-8 cursor-pointer items-center gap-x-1 rounded-lg p-1.5 hover:bg-state-base-hover"
|
||||
onClick={item.onClick}
|
||||
>
|
||||
{cloneElement(item.icon, { className: cn(item.icon.props.className, 'h-4 w-4 text-text-tertiary') })}
|
||||
<span className="system-md-regular text-text-secondary">{item.title}</span>
|
||||
{cloneElement(item.icon, { className: 'h-4 w-4 text-text-tertiary' })}
|
||||
<span className="text-text-secondary system-md-regular">{item.title}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -0,0 +1,59 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import ContentDialog from '../index'
|
||||
|
||||
describe('ContentDialog', () => {
|
||||
it('renders children when show is true', async () => {
|
||||
render(
|
||||
<ContentDialog show={true}>
|
||||
<div>Dialog body</div>
|
||||
</ContentDialog>,
|
||||
)
|
||||
|
||||
await screen.findByText('Dialog body')
|
||||
expect(screen.getByText('Dialog body')).toBeInTheDocument()
|
||||
|
||||
const backdrop = document.querySelector('.bg-app-detail-overlay-bg')
|
||||
expect(backdrop).toBeTruthy()
|
||||
})
|
||||
|
||||
it('does not render children when show is false', () => {
|
||||
render(
|
||||
<ContentDialog show={false}>
|
||||
<div>Hidden content</div>
|
||||
</ContentDialog>,
|
||||
)
|
||||
|
||||
expect(screen.queryByText('Hidden content')).toBeNull()
|
||||
expect(document.querySelector('.bg-app-detail-overlay-bg')).toBeNull()
|
||||
})
|
||||
|
||||
it('calls onClose when backdrop is clicked', async () => {
|
||||
const onClose = vi.fn()
|
||||
render(
|
||||
<ContentDialog show={true} onClose={onClose}>
|
||||
<div>Body</div>
|
||||
</ContentDialog>,
|
||||
)
|
||||
|
||||
const user = userEvent.setup()
|
||||
const backdrop = document.querySelector('.bg-app-detail-overlay-bg') as HTMLElement | null
|
||||
expect(backdrop).toBeTruthy()
|
||||
|
||||
await user.click(backdrop!)
|
||||
expect(onClose).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('applies provided className to the content panel', () => {
|
||||
render(
|
||||
<ContentDialog show={true} className="my-panel-class">
|
||||
<div>Panel content</div>
|
||||
</ContentDialog>,
|
||||
)
|
||||
|
||||
const contentPanel = document.querySelector('.bg-app-detail-bg') as HTMLElement | null
|
||||
expect(contentPanel).toBeTruthy()
|
||||
expect(contentPanel?.className).toContain('my-panel-class')
|
||||
expect(screen.getByText('Panel content')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
119
web/app/components/base/content-dialog/index.stories.tsx
Normal file
119
web/app/components/base/content-dialog/index.stories.tsx
Normal file
@ -0,0 +1,119 @@
|
||||
import type { Meta, StoryObj } from '@storybook/nextjs-vite'
|
||||
import { useEffect, useState } from 'react'
|
||||
import ContentDialog from '.'
|
||||
|
||||
type Props = React.ComponentProps<typeof ContentDialog>
|
||||
|
||||
const meta = {
|
||||
title: 'Base/Feedback/ContentDialog',
|
||||
component: ContentDialog,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
docs: {
|
||||
description: {
|
||||
component: 'Sliding panel overlay used in the app detail view. Includes dimmed backdrop and animated entrance/exit transitions.',
|
||||
},
|
||||
},
|
||||
},
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
className: {
|
||||
control: 'text',
|
||||
description: 'Additional classes applied to the sliding panel container.',
|
||||
},
|
||||
show: {
|
||||
control: 'boolean',
|
||||
description: 'Controls visibility of the dialog.',
|
||||
},
|
||||
onClose: {
|
||||
control: false,
|
||||
description: 'Invoked when the overlay/backdrop is clicked.',
|
||||
},
|
||||
children: {
|
||||
control: false,
|
||||
table: { disable: true },
|
||||
},
|
||||
},
|
||||
args: {
|
||||
show: false,
|
||||
children: null,
|
||||
},
|
||||
} satisfies Meta<typeof ContentDialog>
|
||||
|
||||
export default meta
|
||||
type Story = StoryObj<typeof meta>
|
||||
|
||||
const DemoWrapper = (props: Props) => {
|
||||
const [open, setOpen] = useState(props.show)
|
||||
|
||||
useEffect(() => {
|
||||
setOpen(props.show)
|
||||
}, [props.show])
|
||||
|
||||
return (
|
||||
<div className="relative h-[480px] w-full overflow-hidden bg-gray-100">
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<button
|
||||
className="rounded-md bg-primary-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-primary-700"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
Open dialog
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<ContentDialog
|
||||
{...props}
|
||||
show={open}
|
||||
onClose={() => {
|
||||
props.onClose?.()
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<div className="flex h-full flex-col space-y-4 bg-white p-6">
|
||||
<h2 className="text-lg font-semibold text-gray-900">Plan summary</h2>
|
||||
<p className="text-sm text-gray-600">
|
||||
Use this area to present rich content for the selected run, configuration details, or
|
||||
any supporting context.
|
||||
</p>
|
||||
<div className="flex-1 overflow-y-auto rounded-md border border-dashed border-gray-200 bg-gray-50 p-4 text-xs text-gray-500">
|
||||
Scrollable placeholder content. Add domain-specific information, activity logs, or
|
||||
editors in the real application.
|
||||
</div>
|
||||
<div className="flex justify-end gap-2 pt-4">
|
||||
<button
|
||||
className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-600 hover:bg-gray-50"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button className="rounded-md bg-primary-600 px-3 py-1.5 text-sm text-white hover:bg-primary-700">
|
||||
Apply changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ContentDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
children: null,
|
||||
},
|
||||
render: args => <DemoWrapper {...args} />,
|
||||
}
|
||||
|
||||
export const NarrowPanel: Story = {
|
||||
render: args => <DemoWrapper {...args} />,
|
||||
args: {
|
||||
className: 'max-w-[420px]',
|
||||
children: null,
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: 'Applies a custom width class to show the dialog as a narrower information panel.',
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
40
web/app/components/base/content-dialog/index.tsx
Normal file
40
web/app/components/base/content-dialog/index.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { Transition, TransitionChild } from '@headlessui/react'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
||||
type ContentDialogProps = {
|
||||
className?: string
|
||||
show: boolean
|
||||
onClose?: () => void
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
const ContentDialog = ({
|
||||
className,
|
||||
show,
|
||||
onClose,
|
||||
children,
|
||||
}: ContentDialogProps) => {
|
||||
return (
|
||||
<Transition
|
||||
show={show}
|
||||
as="div"
|
||||
className="absolute left-0 top-0 z-[70] box-border h-full w-full p-2"
|
||||
>
|
||||
<TransitionChild>
|
||||
<div
|
||||
className={cn('absolute inset-0 left-0 w-full bg-app-detail-overlay-bg', 'duration-300 ease-in data-closed:opacity-0', 'data-enter:opacity-100', 'data-leave:opacity-0')}
|
||||
onClick={onClose}
|
||||
/>
|
||||
</TransitionChild>
|
||||
|
||||
<TransitionChild>
|
||||
<div className={cn('absolute left-0 w-full border-r border-divider-burn bg-app-detail-bg', 'duration-100 ease-in data-closed:-translate-x-full', 'data-enter:translate-x-0 data-enter:duration-300 data-enter:ease-out', 'data-leave:-translate-x-full data-leave:duration-200 data-leave:ease-in', className)}>
|
||||
{children}
|
||||
</div>
|
||||
</TransitionChild>
|
||||
</Transition>
|
||||
)
|
||||
}
|
||||
|
||||
export default ContentDialog
|
||||
@ -39,37 +39,6 @@ export function DialogCloseButton({
|
||||
)
|
||||
}
|
||||
|
||||
export function DialogBackdrop({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof BaseDialog.Backdrop>) {
|
||||
return (
|
||||
<BaseDialog.Backdrop
|
||||
{...props}
|
||||
className={cn(
|
||||
'fixed inset-0 z-1002 bg-background-overlay',
|
||||
'transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 motion-reduce:transition-none',
|
||||
className,
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function DialogPopup({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentPropsWithoutRef<typeof BaseDialog.Popup>) {
|
||||
return (
|
||||
<BaseDialog.Popup
|
||||
{...props}
|
||||
className={cn('fixed z-1002', className)}
|
||||
>
|
||||
{children}
|
||||
</BaseDialog.Popup>
|
||||
)
|
||||
}
|
||||
|
||||
type DialogContentProps = {
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
@ -83,25 +52,26 @@ export function DialogContent({
|
||||
overlayClassName,
|
||||
backdropProps,
|
||||
}: DialogContentProps) {
|
||||
const backdropContentProps = backdropProps
|
||||
? (({ className: _className, ...rest }) => rest)(backdropProps)
|
||||
: {}
|
||||
|
||||
return (
|
||||
<DialogPortal>
|
||||
<DialogBackdrop
|
||||
{...backdropContentProps}
|
||||
className={cn(overlayClassName, backdropProps?.className)}
|
||||
/>
|
||||
<DialogPopup
|
||||
<BaseDialog.Backdrop
|
||||
{...backdropProps}
|
||||
className={cn(
|
||||
'top-1/2 left-1/2 max-h-[80dvh] w-[480px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-6 shadow-xl',
|
||||
'fixed inset-0 z-1002 bg-background-overlay',
|
||||
'transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0 motion-reduce:transition-none',
|
||||
overlayClassName,
|
||||
backdropProps?.className,
|
||||
)}
|
||||
/>
|
||||
<BaseDialog.Popup
|
||||
className={cn(
|
||||
'fixed top-1/2 left-1/2 z-1002 max-h-[80dvh] w-[480px] max-w-[calc(100vw-2rem)] -translate-x-1/2 -translate-y-1/2 overflow-y-auto overscroll-contain rounded-2xl border-[0.5px] border-components-panel-border bg-components-panel-bg p-6 shadow-xl',
|
||||
'transition-[transform,scale,opacity] duration-150 data-ending-style:scale-95 data-ending-style:opacity-0 data-starting-style:scale-95 data-starting-style:opacity-0 motion-reduce:transition-none',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</DialogPopup>
|
||||
</BaseDialog.Popup>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
@ -314,6 +314,11 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/app-sidebar/app-info/app-info-detail-panel.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"app/components/app-sidebar/app-info/app-info-trigger.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 2
|
||||
@ -325,6 +330,9 @@
|
||||
},
|
||||
"react/set-state-in-effect": {
|
||||
"count": 4
|
||||
},
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"app/components/app-sidebar/app-sidebar-dropdown.tsx": {
|
||||
@ -1977,6 +1985,16 @@
|
||||
"count": 5
|
||||
}
|
||||
},
|
||||
"app/components/base/content-dialog/index.stories.tsx": {
|
||||
"react/set-state-in-effect": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/base/content-dialog/index.tsx": {
|
||||
"tailwindcss/enforce-consistent-class-order": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"app/components/base/copy-feedback/index.tsx": {
|
||||
"no-restricted-imports": {
|
||||
"count": 1
|
||||
|
||||
Loading…
Reference in New Issue
Block a user