fix(web): align Tailwind v4 CSS migration (#35829)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-05-06 14:20:28 +08:00 committed by GitHub
parent 506e1a8bc7
commit 03e227f8f1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 310 additions and 253 deletions

View File

@ -438,11 +438,6 @@
"count": 1
}
},
"web/app/components/app/configuration/dataset-config/select-dataset/index.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/app/configuration/dataset-config/settings-modal/index.tsx": {
"react/set-state-in-effect": {
"count": 2
@ -3567,11 +3562,6 @@
"count": 1
}
},
"web/app/components/workflow/dsl-export-confirm-modal.tsx": {
"no-restricted-imports": {
"count": 1
}
},
"web/app/components/workflow/header/run-mode.tsx": {
"no-console": {
"count": 1

View File

@ -46,6 +46,12 @@ vi.mock('@/app/components/workflow/update-dsl-modal', () => ({
}))
vi.mock('@/app/components/workflow/dsl-export-confirm-modal', () => ({
DSLExportConfirmContent: ({ onConfirm, onClose }: { onConfirm: (include?: boolean) => void, onClose: () => void }) => (
<div data-testid="dsl-export-confirm-modal">
<button type="button" onClick={() => onConfirm(true)}>Export Include</button>
<button type="button" onClick={onClose}>Close Export</button>
</div>
),
default: ({ onConfirm, onClose }: { onConfirm: (include?: boolean) => void, onClose: () => void }) => (
<div data-testid="dsl-export-confirm-modal">
<button type="button" onClick={() => onConfirm(true)}>Export Include</button>

View File

@ -228,6 +228,21 @@ describe('AppOperations', () => {
})
describe('Visible operations click', () => {
it('should keep focus ring inside visible operation buttons', () => {
const cleanup = setupDomMeasurements(500, 60, [80])
const editOp = createOperation('edit', 'Edit')
render(<AppOperations gap={4} operations={[editOp]} />)
const visibleButton = screen.getAllByText('Edit')
.map(label => label.closest('button'))
.find(button => button?.tabIndex !== -1)
expect(visibleButton).toHaveClass('focus-visible:ring-inset')
cleanup()
})
it('should call onClick when a visible operation is clicked', async () => {
const cleanup = setupDomMeasurements(500, 60, [80, 80])
const user = userEvent.setup()

View File

@ -16,13 +16,13 @@ import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Input from '@/app/components/base/input'
import { DSLExportConfirmContent } from '@/app/components/workflow/dsl-export-confirm-modal'
import dynamic from '@/next/dynamic'
const SwitchAppModal = dynamic(() => import('@/app/components/app/switch-app-modal'), { ssr: false })
const CreateAppModal = dynamic(() => import('@/app/components/explore/create-app-modal'), { ssr: false })
const DuplicateAppModal = dynamic(() => import('@/app/components/app/duplicate-modal'), { ssr: false })
const UpdateDSLModal = dynamic(() => import('@/app/components/workflow/update-dsl-modal'), { ssr: false })
const DSLExportConfirmModal = dynamic(() => import('@/app/components/workflow/dsl-export-confirm-modal'), { ssr: false })
type AppInfoModalsProps = {
appDetail: App & Partial<AppSSO>
@ -54,7 +54,14 @@ const AppInfoModals = ({
const { t } = useTranslation()
const [confirmDeleteInput, setConfirmDeleteInput] = useState('')
const [isConfirmingExport, setIsConfirmingExport] = useState(false)
const [isSecretExporting, setIsSecretExporting] = useState(false)
const isDeleteConfirmDisabled = confirmDeleteInput !== appDetail.name
const exportDialogMode = secretEnvList.length > 0
? 'secret'
: activeModal === 'exportWarning'
? 'warning'
: null
const isExportDialogOpen = exportDialogMode !== null
const handleDeleteDialogClose = () => {
setConfirmDeleteInput('')
@ -74,6 +81,22 @@ const AppInfoModals = ({
}
}, [handleConfirmExport, isConfirmingExport])
const handleExportDialogClose = useCallback(() => {
if (exportDialogMode === 'secret') {
setSecretEnvList([])
return
}
closeModal()
}, [closeModal, exportDialogMode, setSecretEnvList])
const handleExportDialogOpenChange = useCallback((open: boolean) => {
if (open || isConfirmingExport || isSecretExporting)
return
handleExportDialogClose()
}, [handleExportDialogClose, isConfirmingExport, isSecretExporting])
return (
<>
{activeModal === 'switch' && (
@ -163,38 +186,42 @@ const AppInfoModals = ({
onBackup={exportCheck}
/>
)}
<AlertDialog open={activeModal === 'exportWarning'} onOpenChange={open => !open && closeModal()}>
<AlertDialogContent>
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
{t('sidebar.exportWarning', { ns: 'workflow' })}
</AlertDialogTitle>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{t('sidebar.exportWarningDesc', { ns: 'workflow' })}
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton>{t('operation.cancel', { ns: 'common' })}</AlertDialogCancelButton>
<AlertDialogConfirmButton
tone="default"
loading={isConfirmingExport}
disabled={isConfirmingExport}
onClick={handleExportWarningConfirm}
>
{isConfirmingExport
? t('operation.exporting', { ns: 'common' })
: t('operation.confirm', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
<AlertDialog open={isExportDialogOpen} onOpenChange={handleExportDialogOpenChange}>
{exportDialogMode === 'secret'
? (
<DSLExportConfirmContent
envList={secretEnvList}
onConfirm={onExport}
onClose={() => setSecretEnvList([])}
onExportingChange={setIsSecretExporting}
/>
)
: exportDialogMode === 'warning' && (
<AlertDialogContent>
<div className="flex flex-col gap-2 px-6 pt-6 pb-4">
<AlertDialogTitle className="w-full truncate title-2xl-semi-bold text-text-primary">
{t('sidebar.exportWarning', { ns: 'workflow' })}
</AlertDialogTitle>
<AlertDialogDescription className="w-full system-md-regular wrap-break-word whitespace-pre-wrap text-text-tertiary">
{t('sidebar.exportWarningDesc', { ns: 'workflow' })}
</AlertDialogDescription>
</div>
<AlertDialogActions>
<AlertDialogCancelButton>{t('operation.cancel', { ns: 'common' })}</AlertDialogCancelButton>
<AlertDialogConfirmButton
tone="default"
loading={isConfirmingExport}
disabled={isConfirmingExport}
onClick={handleExportWarningConfirm}
>
{isConfirmingExport
? t('operation.exporting', { ns: 'common' })
: t('operation.confirm', { ns: 'common' })}
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
)}
</AlertDialog>
{secretEnvList.length > 0 && (
<DSLExportConfirmModal
envList={secretEnvList}
onConfirm={onExport}
onClose={() => setSecretEnvList([])}
/>
)}
</>
)
}

View File

@ -133,7 +133,7 @@ const AppOperations = ({
data-targetid={operation.id}
size="small"
variant="secondary"
className="gap-px"
className="gap-px focus-visible:ring-inset"
tabIndex={-1}
>
{cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })}
@ -146,7 +146,7 @@ const AppOperations = ({
id="more-measure"
size="small"
variant="secondary"
className="gap-px"
className="gap-px focus-visible:ring-inset"
tabIndex={-1}
>
<RiMoreLine className="h-3.5 w-3.5 text-components-button-secondary-text" />
@ -162,7 +162,7 @@ const AppOperations = ({
data-targetid={operation.id}
size="small"
variant="secondary"
className="gap-px"
className="gap-px focus-visible:ring-inset"
onClick={operation.onClick}
>
{cloneElement(operation.icon, { className: 'h-3.5 w-3.5 text-components-button-secondary-text' })}
@ -178,7 +178,7 @@ const AppOperations = ({
<Button
size="small"
variant="secondary"
className="gap-px"
className="gap-px focus-visible:ring-inset"
/>
)}
>

View File

@ -121,7 +121,7 @@ type BaseItemProps = {
}
function BaseItem({ icon, onRemove, children }: BaseItemProps) {
return (
<div className="group flex flex-row items-center gap-x-1 rounded-full border-[0.5px] bg-components-badge-white-to-dark p-1 pr-1.5 shadow-xs">
<div className="group flex flex-row items-center gap-x-1 rounded-full border-[0.5px] border-components-panel-border-subtle bg-components-badge-white-to-dark p-1 pr-1.5 shadow-xs">
<div className="h-5 w-5 overflow-hidden rounded-full bg-components-icon-bg-blue-solid">
<div className="bg-access-app-icon-mask-bg flex h-full w-full items-center justify-center">
{icon}

View File

@ -74,7 +74,7 @@ const AgentSetting: FC<Props> = ({
</div>
{/* Body */}
<div
className="grow overflow-y-auto border-b p-6 pt-5 pb-[68px]"
className="grow overflow-y-auto border-b border-divider-regular p-6 pt-5 pb-[68px]"
style={{
borderBottom: 'rgba(0, 0, 0, 0.05)',
}}

View File

@ -174,14 +174,12 @@ const ConfigurationView: FC<ConfigurationViewModel> = ({
</AlertDialogContent>
</AlertDialog>
{isShowSelectDataSet && (
<SelectDataSet
isShow={isShowSelectDataSet}
onClose={onCloseSelectDataSet}
selectedIds={selectedIds}
onSelect={onSelectDataSets}
/>
)}
<SelectDataSet
isShow={isShowSelectDataSet}
onClose={onCloseSelectDataSet}
selectedIds={selectedIds}
onSelect={onSelectDataSets}
/>
{isShowHistoryModal && (
<EditHistoryModal

View File

@ -3,14 +3,14 @@ import type { FC } from 'react'
import type { DataSet } from '@/models/datasets'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
import { useInfiniteScroll } from 'ahooks'
import * as React from 'react'
import { useMemo, useRef, useState } from 'react'
import { useCallback, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import AppIcon from '@/app/components/base/app-icon'
import Badge from '@/app/components/base/badge'
import Loading from '@/app/components/base/loading'
import Modal from '@/app/components/base/modal'
import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import FeatureIcon from '@/app/components/header/account-setting/model-provider-page/model-selector/feature-icon'
import { useKnowledge } from '@/hooks/use-knowledge'
@ -79,100 +79,103 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
onSelect(selected)
}
const handleClose = useCallback(() => {
setSelectedIdsInModal(selectedIds)
onClose()
}, [onClose, selectedIds])
const handleOpenChange = useCallback((open: boolean) => {
if (!open)
handleClose()
}, [handleClose])
return (
<Modal
isShow={isShow}
onClose={onClose}
className="w-[400px]"
title={t('feature.dataSet.selectTitle', { ns: 'appDebug' })}
>
{(isLoading && datasets.length === 0) && (
<div className="flex h-[200px]">
<Loading type="area" />
</div>
)}
<Dialog open={isShow} onOpenChange={handleOpenChange}>
<DialogContent className="w-100 overflow-hidden">
<DialogTitle className="title-2xl-semi-bold text-text-primary">
{t('feature.dataSet.selectTitle', { ns: 'appDebug' })}
</DialogTitle>
<DialogCloseButton aria-label={t('operation.close', { ns: 'common' })} />
{(isLoading && datasets.length === 0) && (
<div className="flex h-50">
<Loading type="area" />
</div>
)}
{hasNoData && (
<div
className="mt-6 flex h-[128px] items-center justify-center space-x-1 rounded-lg border text-[13px]"
style={{
background: 'rgba(0, 0, 0, 0.02)',
borderColor: 'rgba(0, 0, 0, 0.02',
}}
>
<span className="text-text-tertiary">{t('feature.dataSet.noDataSet', { ns: 'appDebug' })}</span>
<Link href="/datasets/create" className="font-normal text-text-accent">{t('feature.dataSet.toCreate', { ns: 'appDebug' })}</Link>
</div>
)}
{hasNoData && (
<div className="mt-6 flex h-32 items-center justify-center space-x-1 rounded-lg border border-divider-subtle bg-components-panel-on-panel-item-bg text-[13px]">
<span className="text-text-tertiary">{t('feature.dataSet.noDataSet', { ns: 'appDebug' })}</span>
<Link href="/datasets/create" className="font-normal text-text-accent">{t('feature.dataSet.toCreate', { ns: 'appDebug' })}</Link>
</div>
)}
{datasets.length > 0 && (
<>
<div ref={listRef} className="mt-7 max-h-[286px] space-y-1 overflow-y-auto">
{datasets.map(item => (
<div
key={item.id}
className={cn(
'flex h-10 cursor-pointer items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2 shadow-xs hover:border-components-panel-border hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm',
selectedIdsInModal.includes(item.id) && 'border-[1.5px] border-components-option-card-option-selected-border bg-state-accent-hover shadow-xs hover:border-components-option-card-option-selected-border hover:bg-state-accent-hover hover:shadow-xs',
!item.embedding_available && 'hover:border-components-panel-border-subtle hover:bg-components-panel-on-panel-item-bg hover:shadow-xs',
)}
onClick={() => {
if (!item.embedding_available)
return
toggleSelect(item)
}}
>
<div className="mr-1 flex grow items-center overflow-hidden">
<div className={cn('mr-2', !item.embedding_available && 'opacity-30')}>
<AppIcon
size="tiny"
iconType={item.icon_info.icon_type}
icon={item.icon_info.icon}
background={item.icon_info.icon_type === 'image' ? undefined : item.icon_info.icon_background}
imageUrl={item.icon_info.icon_type === 'image' ? item.icon_info.icon_url : undefined}
/>
</div>
<div className={cn('max-w-[200px] truncate text-[13px] font-medium text-text-secondary', !item.embedding_available && 'max-w-[120px]! opacity-30')}>{item.name}</div>
{!item.embedding_available && (
<span className="ml-1 shrink-0 rounded-md border border-divider-deep px-1 text-xs leading-[18px] font-normal text-text-tertiary">{t('unavailable', { ns: 'dataset' })}</span>
{datasets.length > 0 && (
<>
<div ref={listRef} className="mt-7 max-h-71.5 space-y-1 overflow-y-auto">
{datasets.map(item => (
<button
key={item.id}
type="button"
disabled={!item.embedding_available}
className={cn(
'flex h-10 w-full cursor-pointer items-center rounded-lg border-[0.5px] border-components-panel-border-subtle bg-components-panel-on-panel-item-bg px-2 text-left shadow-xs outline-hidden hover:border-components-panel-border hover:bg-components-panel-on-panel-item-bg-hover hover:shadow-sm focus-visible:ring-2 focus-visible:ring-state-accent-solid',
selectedIdsInModal.includes(item.id) && 'border-[1.5px] border-components-option-card-option-selected-border bg-state-accent-hover shadow-xs hover:border-components-option-card-option-selected-border hover:bg-state-accent-hover hover:shadow-xs',
!item.embedding_available && 'cursor-default hover:border-components-panel-border-subtle hover:bg-components-panel-on-panel-item-bg hover:shadow-xs',
)}
</div>
{item.is_multimodal && (
<div className="mr-1 shrink-0">
<FeatureIcon feature={ModelFeatureEnum.vision} />
onClick={() => toggleSelect(item)}
>
<div className="mr-1 flex grow items-center overflow-hidden">
<div className={cn('mr-2', !item.embedding_available && 'opacity-30')}>
<AppIcon
size="tiny"
iconType={item.icon_info.icon_type}
icon={item.icon_info.icon}
background={item.icon_info.icon_type === 'image' ? undefined : item.icon_info.icon_background}
imageUrl={item.icon_info.icon_type === 'image' ? item.icon_info.icon_url : undefined}
/>
</div>
<div className={cn('max-w-50 truncate text-[13px] font-medium text-text-secondary', !item.embedding_available && 'max-w-30! opacity-30')}>{item.name}</div>
{!item.embedding_available && (
<span className="ml-1 shrink-0 rounded-md border border-divider-deep px-1 text-xs leading-[18px] font-normal text-text-tertiary">{t('unavailable', { ns: 'dataset' })}</span>
)}
</div>
)}
{
!!item.indexing_technique && (
<Badge
className="shrink-0"
text={formatIndexingTechniqueAndMethod(item.indexing_technique, item.retrieval_model_dict?.search_method)}
/>
)
}
{
item.provider === 'external' && (
<Badge className="shrink-0" text={t('externalTag', { ns: 'dataset' })} />
)
}
</div>
))}
{isFetchingNextPage && <Loading />}
{item.is_multimodal && (
<div className="mr-1 shrink-0">
<FeatureIcon feature={ModelFeatureEnum.vision} />
</div>
)}
{
!!item.indexing_technique && (
<Badge
className="shrink-0"
text={formatIndexingTechniqueAndMethod(item.indexing_technique, item.retrieval_model_dict?.search_method)}
/>
)
}
{
item.provider === 'external' && (
<Badge className="shrink-0" text={t('externalTag', { ns: 'dataset' })} />
)
}
</button>
))}
{isFetchingNextPage && <Loading />}
</div>
</>
)}
{!isLoading && (
<div className="mt-8 flex items-center justify-between">
<div className="text-sm font-medium text-text-secondary">
{selected.length > 0 && `${selected.length} ${t('feature.dataSet.selected', { ns: 'appDebug' })}`}
</div>
<div className="flex space-x-2">
<Button onClick={handleClose}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button variant="primary" onClick={handleSelect} disabled={hasNoData}>{t('operation.add', { ns: 'common' })}</Button>
</div>
</div>
</>
)}
{!isLoading && (
<div className="mt-8 flex items-center justify-between">
<div className="text-sm font-medium text-text-secondary">
{selected.length > 0 && `${selected.length} ${t('feature.dataSet.selected', { ns: 'appDebug' })}`}
</div>
<div className="flex space-x-2">
<Button onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
<Button variant="primary" onClick={handleSelect} disabled={hasNoData}>{t('operation.add', { ns: 'common' })}</Button>
</div>
</div>
)}
</Modal>
)}
</DialogContent>
</Dialog>
)
}
export default React.memo(SelectDataSet)

View File

@ -95,7 +95,7 @@ const ImageInput: FC<UploaderProps> = ({
return (
<div className={cn(className, 'w-full px-3 py-1.5')}>
<div
className={cn(isDragActive && 'border-primary-600', 'relative flex aspect-square flex-col items-center justify-center rounded-lg border-[1.5px] border-dashed text-gray-500')}
className={cn('relative flex aspect-square flex-col items-center justify-center rounded-lg border-[1.5px] border-dashed border-components-input-border-hover text-gray-500', isDragActive && 'border-primary-600')}
onDragEnter={handleDragEnter}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}

View File

@ -146,7 +146,7 @@ const ChatInputArea = ({ readonly, botName, showFeatureBar, showFileUpload, feat
<div ref={textValueRef} className="pointer-events-none invisible absolute h-auto w-auto p-1 body-lg-regular leading-6 whitespace-pre">
{query}
</div>
<Textarea ref={ref => textareaRef.current = ref as any} className={cn('w-full resize-none bg-transparent p-1 body-lg-regular leading-6 text-text-primary outline-none')} placeholder={decode(t(readonly ? 'chat.inputDisabledPlaceholder' : 'chat.inputPlaceholder', { ns: 'common', botName }) || '')} autoFocus minRows={1} value={query} onChange={e => handleQueryChange(e.target.value)} onKeyDown={handleKeyDown} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} onPaste={handleClipboardPasteFile} onDragEnter={handleDragFileEnter} onDragLeave={handleDragFileLeave} onDragOver={handleDragFileOver} onDrop={handleDropFile} readOnly={readonly} />
<Textarea ref={ref => textareaRef.current = ref as any} className={cn('w-full resize-none bg-transparent p-1 body-lg-regular leading-6 text-text-primary outline-hidden')} placeholder={decode(t(readonly ? 'chat.inputDisabledPlaceholder' : 'chat.inputPlaceholder', { ns: 'common', botName }) || '')} autoFocus minRows={1} value={query} onChange={e => handleQueryChange(e.target.value)} onKeyDown={handleKeyDown} onCompositionStart={handleCompositionStart} onCompositionEnd={handleCompositionEnd} onPaste={handleClipboardPasteFile} onDragEnter={handleDragFileEnter} onDragLeave={handleDragFileLeave} onDragOver={handleDragFileOver} onDrop={handleDropFile} readOnly={readonly} />
</div>
{!isMultipleLine && operation}
</div>

View File

@ -80,7 +80,7 @@ const FileImageItem = ({
}
{
showDownloadAction && (
<div className="bg-opacity-[0.3] absolute inset-0.5 z-10 hidden bg-background-overlay-alt group-hover/file-image:block">
<div className="absolute inset-0.5 z-10 hidden bg-background-overlay-alt group-hover/file-image:block">
<div
className="absolute right-0.5 bottom-0.5 flex h-6 w-6 items-center justify-center rounded-lg bg-components-actionbar-bg shadow-md"
onClick={(e) => {

View File

@ -77,7 +77,7 @@ const TagInput: FC<TagInputProps> = ({ items, onChange, disableAdd, disableRemov
<div className={cn('group/tag-add mt-1 flex items-center gap-x-0.5', !isSpecialMode ? 'rounded-md border border-dashed border-divider-deep px-1.5' : '')}>
{!isSpecialMode && !focused && <span className="i-ri-add-line h-3.5 w-3.5 text-text-placeholder group-hover/tag-add:text-text-secondary" />}
<AutosizeInput
inputClassName={cn('appearance-none text-text-primary caret-[#295EFF] outline-none placeholder:text-text-placeholder group-hover/tag-add:placeholder:text-text-secondary', isSpecialMode ? 'bg-transparent' : '', inputClassName)}
inputClassName={cn('appearance-none text-text-primary caret-[#295EFF] outline-hidden placeholder:text-text-placeholder group-hover/tag-add:placeholder:text-text-secondary', isSpecialMode ? 'bg-transparent' : '', inputClassName)}
className={cn(!isInWorkflow && 'max-w-[300px]', isInWorkflow && 'max-w-[146px]', 'overflow-hidden rounded-md py-1 system-xs-regular', isSpecialMode && 'border border-transparent px-1.5', focused && isSpecialMode && 'border-dashed border-divider-deep')}
onFocus={() => setFocused(true)}
onBlur={handleBlur}

View File

@ -53,7 +53,7 @@ const TagManagementModal = ({ show, type }: TagManagementModalProps) => {
<span className="i-ri-close-line h-4 w-4 text-text-tertiary" data-testid="tag-management-modal-close-button" />
</div>
<div className="mt-3 flex flex-wrap gap-2">
<input className="w-[100px] shrink-0 appearance-none rounded-lg border border-dashed border-divider-regular bg-transparent px-2 py-1 text-sm leading-5 text-text-secondary caret-primary-600 outline-none placeholder:text-text-quaternary focus:border-solid" placeholder={t('tag.addNew', { ns: 'common' }) || ''} autoFocus value={name} onChange={e => setName(e.target.value)} onKeyDown={e => e.key === 'Enter' && !e.nativeEvent.isComposing && createNewTag()} onBlur={createNewTag} />
<input className="w-[100px] shrink-0 appearance-none rounded-lg border border-dashed border-divider-regular bg-transparent px-2 py-1 text-sm leading-5 text-text-secondary caret-primary-600 outline-hidden placeholder:text-text-quaternary focus:border-solid" placeholder={t('tag.addNew', { ns: 'common' }) || ''} autoFocus value={name} onChange={e => setName(e.target.value)} onKeyDown={e => e.key === 'Enter' && !e.nativeEvent.isComposing && createNewTag()} onBlur={createNewTag} />
{tagList.map(tag => (<TagItemEditor key={tag.id} tag={tag} />))}
</div>
</Modal>

View File

@ -124,7 +124,7 @@ const TagItemEditor: FC<TagItemEditorProps> = ({ tag }) => {
</div>
</>
)}
{isEditing && (<input className="shrink-0 appearance-none caret-primary-600 outline-none placeholder:text-text-quaternary" autoFocus value={name} onChange={e => setName(e.target.value)} onKeyDown={e => e.key === 'Enter' && editTag(tag.id, name)} onBlur={() => editTag(tag.id, name)} />)}
{isEditing && (<input className="shrink-0 appearance-none caret-primary-600 outline-hidden placeholder:text-text-quaternary" autoFocus value={name} onChange={e => setName(e.target.value)} onKeyDown={e => e.key === 'Enter' && editTag(tag.id, name)} onBlur={() => editTag(tag.id, name)} />)}
</div>
<AlertDialog open={showRemoveModal} onOpenChange={open => !open && setShowRemoveModal(false)}>
<AlertDialogContent>

View File

@ -31,7 +31,7 @@ const COLOR_MAP = {
export default function Tag({ children, color = 'green', className = '', bordered = false, hideBg = false }: ITagProps) {
return (
<div className={
cn('inline-flex shrink-0 items-center rounded-md px-2.5 py-px text-xs leading-5', COLOR_MAP[color] ? `${COLOR_MAP[color].text} ${COLOR_MAP[color].bg}` : '', bordered ? 'border' : '', hideBg ? 'bg-transparent' : '', className)
cn('inline-flex shrink-0 items-center rounded-md px-2.5 py-px text-xs leading-5', COLOR_MAP[color] ? `${COLOR_MAP[color].text} ${COLOR_MAP[color].bg}` : '', bordered ? 'border border-divider-regular' : '', hideBg ? 'bg-transparent' : '', className)
}
>
{children}

View File

@ -48,8 +48,8 @@ const HeaderBillingBtn: FC<Props> = ({
<div
onClick={handleClick}
className={cn(
'flex h-[22px] items-center rounded-md border border-divider-regular px-2 text-xs font-semibold uppercase',
classNames,
'flex h-[22px] items-center rounded-md border px-2 text-xs font-semibold uppercase',
isDisplayOnly ? 'cursor-default' : 'cursor-pointer',
)}
>

View File

@ -26,7 +26,7 @@
@apply text-text-secondary text-sm;
}
.addFileBtn {
@apply mt-4 w-fit !text-[13px] font-medium border-[0.5px];
@apply mt-4 w-fit !text-[13px] font-medium border-[0.5px] border-components-button-secondary-border;
}
.plusIcon {
@apply w-4 h-4 mr-2 stroke-current stroke-[1.5px];

View File

@ -79,7 +79,7 @@ const ExternalApiSelect: React.FC<ExternalApiSelectProps> = ({ items, value, onS
<RiArrowDownSLine className={`h-4 w-4 text-text-quaternary transition-transform ${isOpen ? 'text-text-secondary' : ''}`} />
</div>
{isOpen && (
<div className="absolute z-10 mt-1 w-full rounded-xl border bg-components-panel-bg-blur shadow-lg">
<div className="absolute z-10 mt-1 w-full rounded-xl border border-components-panel-border bg-components-panel-bg-blur shadow-lg">
{items.map(item => (
<div
key={item.value}

View File

@ -47,7 +47,7 @@ const InvitationLink = ({
</TooltipContent>
</Tooltip>
</div>
<div className="h-4 shrink-0 border bg-divider-regular" />
<div className="h-4 shrink-0 border border-divider-regular bg-divider-regular" />
<Tooltip>
<TooltipTrigger
render={(

View File

@ -141,7 +141,7 @@ const NavSelector = ({ curNav, navigationItems, createText, isApp, onCreate, onL
className="
absolute right-0 -left-11 mt-1.5 w-60 max-w-80
origin-top-right divide-y divide-divider-regular rounded-lg bg-components-panel-bg-blur
shadow-lg outline-none
shadow-lg outline-hidden
"
>
<div className="overflow-auto px-1 py-1" style={{ maxHeight: '50vh' }} onScroll={handleScroll}>

View File

@ -535,7 +535,7 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
<Textarea
ref={textareaRef}
className={cn(
'relative z-10 w-full resize-none bg-transparent p-1 body-lg-regular leading-6 text-transparent caret-primary-500 outline-none',
'relative z-10 w-full resize-none bg-transparent p-1 body-lg-regular leading-6 text-transparent caret-primary-500 outline-hidden',
'placeholder:text-text-tertiary',
)}
style={{ paddingRight, paddingBottom }}

View File

@ -1,15 +1,18 @@
'use client'
import type { EnvironmentVariable } from '@/app/components/workflow/types'
import { Button } from '@langgenius/dify-ui/button'
import {
AlertDialog,
AlertDialogActions,
AlertDialogCancelButton,
AlertDialogConfirmButton,
AlertDialogContent,
AlertDialogTitle,
} from '@langgenius/dify-ui/alert-dialog'
import { cn } from '@langgenius/dify-ui/cn'
import { RiCloseLine, RiLock2Line } from '@remixicon/react'
import { noop } from 'es-toolkit/function'
import * as React from 'react'
import { useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Checkbox from '@/app/components/base/checkbox'
import { Env } from '@/app/components/base/icons/src/vender/line/others'
import Modal from '@/app/components/base/modal'
export type DSLExportConfirmModalProps = {
envList: EnvironmentVariable[]
@ -17,11 +20,16 @@ export type DSLExportConfirmModalProps = {
onClose: () => void
}
const DSLExportConfirmModal = ({
type DSLExportConfirmContentProps = DSLExportConfirmModalProps & {
onExportingChange?: (isExporting: boolean) => void
}
export const DSLExportConfirmContent = ({
envList = [],
onConfirm,
onClose,
}: DSLExportConfirmModalProps) => {
onExportingChange,
}: DSLExportConfirmContentProps) => {
const { t } = useTranslation()
const [exportSecrets, setExportSecrets] = useState<boolean>(false)
@ -32,73 +40,75 @@ const DSLExportConfirmModal = ({
return
setIsExporting(true)
onExportingChange?.(true)
try {
await onConfirm(exportSecrets)
onClose()
}
finally {
setIsExporting(false)
onExportingChange?.(false)
}
}, [exportSecrets, isExporting, onClose, onConfirm])
}, [exportSecrets, isExporting, onClose, onConfirm, onExportingChange])
return (
<Modal
isShow={true}
onClose={noop}
className={cn('w-[480px] max-w-[480px]')}
>
<div className="relative pb-6 title-2xl-semi-bold text-text-primary">{t('env.export.title', { ns: 'workflow' })}</div>
<div
className={cn('absolute top-4 right-4 p-2', !isExporting && 'cursor-pointer')}
onClick={() => !isExporting && onClose()}
>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</div>
<div className="relative">
<table className="w-full border-separate border-spacing-0 rounded-lg border border-divider-regular shadow-xs">
<thead className="system-xs-medium-uppercase text-text-tertiary">
<tr>
<td width={220} className="h-7 border-r border-b border-divider-regular pl-3">{t('env.export.name', { ns: 'workflow' })}</td>
<td className="h-7 border-b border-divider-regular pl-3">{t('env.export.value', { ns: 'workflow' })}</td>
</tr>
</thead>
<tbody>
{envList.map((env, index) => (
<tr key={env.name}>
<td className={cn('h-7 border-r pl-3 system-xs-medium', index + 1 !== envList.length && 'border-b')}>
<div className="flex w-[200px] items-center gap-1">
<Env className="h-4 w-4 shrink-0 text-util-colors-violet-violet-600" />
<div className="truncate text-text-primary">{env.name}</div>
<div className="shrink-0 text-text-tertiary">{t('env.export.secret', { ns: 'workflow' })}</div>
<RiLock2Line className="h-3 w-3 shrink-0 text-text-tertiary" />
</div>
</td>
<td className={cn('h-7 pl-3', index + 1 !== envList.length && 'border-b')}>
<div className="truncate system-xs-regular text-text-secondary">{env.value}</div>
</td>
<AlertDialogContent className="w-120 max-w-120">
<div className="px-6 pt-6">
<AlertDialogTitle className="pb-6 title-2xl-semi-bold text-text-primary">
{t('env.export.title', { ns: 'workflow' })}
</AlertDialogTitle>
<div className="relative">
<table className="w-full border-separate border-spacing-0 rounded-lg border border-divider-regular shadow-xs">
<thead className="system-xs-medium-uppercase text-text-tertiary">
<tr>
<td width={220} className="h-7 border-r border-b border-divider-regular pl-3">{t('env.export.name', { ns: 'workflow' })}</td>
<td className="h-7 border-b border-divider-regular pl-3">{t('env.export.value', { ns: 'workflow' })}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-4 flex gap-2">
<Checkbox
className="shrink-0"
checked={exportSecrets}
disabled={isExporting}
onCheck={() => setExportSecrets(!exportSecrets)}
/>
<div
className={cn('system-sm-medium text-text-primary', !isExporting && 'cursor-pointer')}
onClick={() => !isExporting && setExportSecrets(!exportSecrets)}
>
{t('env.export.checkbox', { ns: 'workflow' })}
</thead>
<tbody>
{envList.map((env, index) => (
<tr key={env.name}>
<td className={cn('h-7 border-r border-divider-regular pl-3 system-xs-medium', index + 1 !== envList.length && 'border-b border-divider-regular')}>
<div className="flex w-50 items-center gap-1">
<span aria-hidden="true" className="i-custom-vender-line-others-env h-4 w-4 shrink-0 text-util-colors-violet-violet-600" />
<div className="truncate text-text-primary">{env.name}</div>
<div className="shrink-0 text-text-tertiary">{t('env.export.secret', { ns: 'workflow' })}</div>
<span aria-hidden="true" className="i-ri-lock-2-line h-3 w-3 shrink-0 text-text-tertiary" />
</div>
</td>
<td className={cn('h-7 pl-3', index + 1 !== envList.length && 'border-b border-divider-regular')}>
<div className="truncate system-xs-regular text-text-secondary">{env.value}</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-4 flex gap-2">
<Checkbox
className="shrink-0"
checked={exportSecrets}
disabled={isExporting}
onCheck={() => setExportSecrets(!exportSecrets)}
ariaLabelledBy="dsl-export-secrets-checkbox-label"
/>
<button
id="dsl-export-secrets-checkbox-label"
type="button"
disabled={isExporting}
className="cursor-pointer rounded-sm text-left system-sm-medium text-text-primary outline-hidden focus-visible:ring-1 focus-visible:ring-components-input-border-hover disabled:cursor-not-allowed disabled:opacity-50"
onClick={() => setExportSecrets(!exportSecrets)}
>
{t('env.export.checkbox', { ns: 'workflow' })}
</button>
</div>
</div>
<div className="flex flex-row-reverse pt-6">
<Button
className="ml-2"
variant="primary"
<AlertDialogActions>
<AlertDialogCancelButton disabled={isExporting}>
{t('operation.cancel', { ns: 'common' })}
</AlertDialogCancelButton>
<AlertDialogConfirmButton
tone="default"
loading={isExporting}
disabled={isExporting}
onClick={submit}
@ -108,10 +118,28 @@ const DSLExportConfirmModal = ({
: exportSecrets
? t('env.export.export', { ns: 'workflow' })
: t('env.export.ignore', { ns: 'workflow' })}
</Button>
<Button disabled={isExporting} onClick={onClose}>{t('operation.cancel', { ns: 'common' })}</Button>
</div>
</Modal>
</AlertDialogConfirmButton>
</AlertDialogActions>
</AlertDialogContent>
)
}
const DSLExportConfirmModal = (props: DSLExportConfirmModalProps) => {
const { envList, onClose } = props
const [isExporting, setIsExporting] = useState(false)
const isDialogOpen = envList.length > 0
const handleOpenChange = useCallback((open: boolean) => {
if (open || isExporting)
return
onClose()
}, [isExporting, onClose])
return (
<AlertDialog open={isDialogOpen} onOpenChange={handleOpenChange}>
<DSLExportConfirmContent {...props} onExportingChange={setIsExporting} />
</AlertDialog>
)
}

View File

@ -43,7 +43,7 @@ const Field: FC<Props> = ({
disabled={depth !== MAX_DEPTH + 1}
render={(
<div
className={cn('flex items-center justify-between rounded-md pr-2 outline-none focus:outline-none focus-visible:outline-none', !readonly && 'hover:bg-state-base-hover', depth !== MAX_DEPTH + 1 && 'cursor-pointer')}
className={cn('flex items-center justify-between rounded-md pr-2 outline-hidden focus:outline-hidden focus-visible:outline-hidden', !readonly && 'hover:bg-state-base-hover', depth !== MAX_DEPTH + 1 && 'cursor-pointer')}
onMouseDown={() => !readonly && onSelect?.([...valueSelector, name])}
>
<div className="flex grow items-stretch">

View File

@ -211,7 +211,7 @@ const Item: FC<ItemProps> = ({
className={cn(
(isObj || isStructureOutput) ? 'pr-1' : 'pr-[18px]',
(isHovering || isSelected) && ((isObj || isStructureOutput) ? 'bg-components-panel-on-panel-item-bg-hover' : 'bg-state-base-hover'),
'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3 outline-none focus:outline-none focus-visible:outline-none',
'relative flex h-6 w-full cursor-pointer items-center rounded-md pl-3 outline-hidden focus:outline-hidden focus-visible:outline-hidden',
className,
)}
data-selected={isSelected ? 'true' : 'false'}

View File

@ -26,17 +26,20 @@ const AddDataset: FC<Props> = ({
}, [onChange, hideModal])
return (
<div>
<div className="cursor-pointer rounded-md p-1 select-none hover:bg-state-base-hover" onClick={showModal} data-testid="add-button">
<span className="i-ri-add-line h-4 w-4 text-text-tertiary" />
</div>
{isShowModal && (
<SelectDataset
isShow={isShowModal}
onClose={hideModal}
selectedIds={selectedIds}
onSelect={handleSelect}
/>
)}
<button
type="button"
className="cursor-pointer rounded-md p-1 outline-hidden select-none hover:bg-state-base-hover focus-visible:ring-2 focus-visible:ring-state-accent-solid"
onClick={showModal}
data-testid="add-button"
>
<span aria-hidden="true" className="i-ri-add-line h-4 w-4 text-text-tertiary" />
</button>
<SelectDataset
isShow={isShowModal}
onClose={hideModal}
selectedIds={selectedIds}
onSelect={handleSelect}
/>
</div>
)
}

View File

@ -18,7 +18,7 @@ const StatusContainer: FC<Props> = ({
return (
<div
className={cn(
'relative rounded-lg border px-3 py-2.5 system-xs-regular break-all',
'relative rounded-lg border border-workflow-display-disabled-border-1 px-3 py-2.5 system-xs-regular break-all',
status === 'succeeded' && 'border-[rgba(23,178,106,0.8)] bg-workflow-display-success-bg bg-[url(~@/app/components/workflow/run/assets/bg-line-success.svg)] text-text-success',
status === 'succeeded' && theme === Theme.light && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.5),inset_0_1px_3px_0_rgba(0,0,0,0.12),inset_0_2px_24px_0_rgba(23,178,106,0.2),0_1px_2px_0_rgba(9,9,11,0.05),0_0_0_1px_rgba(0,0,0,0.05)]',
status === 'succeeded' && theme === Theme.dark && 'shadow-[inset_2px_2px_0_0_rgba(255,255,255,0.12),inset_0_1px_3px_0_rgba(0,0,0,0.4),inset_0_2px_24px_0_rgba(23,178,106,0.25),0_1px_2px_0_rgba(0,0,0,0.1),0_0_0_1px_rgba(24,24,27,0.95)]',

View File

@ -12,7 +12,7 @@
@import 'tailwindcss/theme.css' layer(theme);
@import 'tailwindcss/utilities.css' layer(utilities);
/* Local preflight (replaces v3 `corePlugins.preflight: false`). */
/* Local preflight keeps the browser baseline controlled across app entries. */
@import './preflight.css' layer(base);
/* Design system: palette overrides, semantic tokens, light/dark themes,
@ -98,19 +98,6 @@
--background-image-chat-answer-human-input-form-divider-bg: var(--color-chat-answer-human-input-form-divider-bg);
}
/* ---------- Backwards-compat: gray-200 default border color ----------- *
* v4 changed the default border color to `currentColor`. Preserve the v3
* baseline used throughout the codebase. */
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentcolor);
}
}
/* ---------- App-level component CSS ----------------------------------- */
@layer components {
[class*='code-'] {