feat: new dnd list (#9311)

* feat: add Sortable

* refactor: update SortableItem style, fix grid layout

* refactor: dragOverlay

* refactor: use Sortable grid in mcp server list

* refactor: improve style

* refactor: support custom dropAnimation for drag overlay

* fix: cursor grabbing

* fix: unexpected drag

* fix: z-index

* revert: assistants tab

* refactor: improve button layout

* docs: update comments

* fix: interaction between Sortable and portal elements

* refactor: improve McpServerCard dnd experience

* refactor: prevent pointer events on drag overlay

* refactor: rename and extraction

* refactor: simplify usage

* refactor: add showGhost
This commit is contained in:
one 2025-08-25 14:19:56 +08:00 committed by GitHub
parent cce88745c2
commit 070614cd3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 641 additions and 67 deletions

View File

@ -103,6 +103,10 @@
"@cherrystudio/embedjs-loader-xml": "^0.1.31",
"@cherrystudio/embedjs-ollama": "^0.1.31",
"@cherrystudio/embedjs-openai": "^0.1.31",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
"@electron-toolkit/eslint-config-ts": "^3.0.0",
"@electron-toolkit/preload": "^3.0.0",

View File

@ -0,0 +1,110 @@
import { DraggableSyntheticListeners } from '@dnd-kit/core'
import { Transform } from '@dnd-kit/utilities'
import { classNames } from '@renderer/utils'
import React, { useEffect } from 'react'
import styled from 'styled-components'
interface ItemRendererProps<T> {
ref?: React.Ref<HTMLDivElement>
item: T
renderItem: (item: T, props: { dragging: boolean }) => React.ReactNode
dragging?: boolean
dragOverlay?: boolean
ghost?: boolean
transform?: Transform | null
transition?: string | null
listeners?: DraggableSyntheticListeners
}
export function ItemRenderer<T>({
ref,
item,
renderItem,
dragging,
dragOverlay,
ghost,
transform,
transition,
listeners,
...props
}: ItemRendererProps<T>) {
useEffect(() => {
if (!dragOverlay) {
return
}
document.body.style.cursor = 'grabbing'
return () => {
document.body.style.cursor = ''
}
}, [dragOverlay])
const wrapperStyle = {
transition,
'--translate-x': transform ? `${Math.round(transform.x)}px` : undefined,
'--translate-y': transform ? `${Math.round(transform.y)}px` : undefined,
'--scale-x': transform?.scaleX ? `${transform.scaleX}` : undefined,
'--scale-y': transform?.scaleY ? `${transform.scaleY}` : undefined
} as React.CSSProperties
return (
<ItemWrapper ref={ref} className={classNames({ dragOverlay: dragOverlay })} style={{ ...wrapperStyle }}>
<DraggableItem
className={classNames({ dragging: dragging, dragOverlay: dragOverlay, ghost: ghost })}
{...listeners}
{...props}>
{renderItem(item, { dragging: !!dragging })}
</DraggableItem>
</ItemWrapper>
)
}
const ItemWrapper = styled.div`
box-sizing: border-box;
transform: translate3d(var(--translate-x, 0), var(--translate-y, 0), 0) scaleX(var(--scale-x, 1))
scaleY(var(--scale-y, 1));
transform-origin: 0 0;
touch-action: manipulation;
&.dragOverlay {
--scale: 1.02;
z-index: 999;
position: relative;
}
`
const DraggableItem = styled.div`
position: relative;
box-sizing: border-box;
cursor: pointer; /* default cursor for items */
touch-action: manipulation;
transform-origin: 50% 50%;
transform: scale(var(--scale, 1));
&.dragging:not(.dragOverlay) {
z-index: 0;
opacity: 0.25;
&:not(.ghost) {
opacity: 0;
}
}
&.dragOverlay {
cursor: inherit;
animation: pop 200ms cubic-bezier(0.18, 0.67, 0.6, 1.22);
transform: scale(var(--scale));
opacity: 1;
pointer-events: none; /* prevent pointer events on drag overlay */
}
@keyframes pop {
0% {
transform: scale(1);
}
100% {
transform: scale(var(--scale));
}
}
`

View File

@ -0,0 +1,192 @@
import {
Active,
defaultDropAnimationSideEffects,
DndContext,
DragOverlay,
DropAnimation,
KeyboardSensor,
Over,
TouchSensor,
UniqueIdentifier,
useSensor,
useSensors
} from '@dnd-kit/core'
import { restrictToHorizontalAxis, restrictToVerticalAxis } from '@dnd-kit/modifiers'
import {
horizontalListSortingStrategy,
rectSortingStrategy,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy
} from '@dnd-kit/sortable'
import React, { useCallback, useMemo, useState } from 'react'
import { createPortal } from 'react-dom'
import styled from 'styled-components'
import { ItemRenderer } from './ItemRenderer'
import { SortableItem } from './SortableItem'
import { PortalSafePointerSensor } from './utils'
interface SortableProps<T> {
/** Array of sortable items */
items: T[]
/** Function or key to get unique identifier for each item */
itemKey: keyof T | ((item: T) => string | number)
/** Callback when sorting is complete, receives old and new indices */
onSortEnd: (event: { oldIndex: number; newIndex: number }) => void
/** Callback when drag starts, will be passed to dnd-kit's onDragStart */
onDragStart?: (event: { active: Active }) => void
/** Callback when drag ends, will be passed to dnd-kit's onDragEnd */
onDragEnd?: (event: { over: Over }) => void
/** Function to render individual item, receives item data and drag state */
renderItem: (item: T, props: { dragging: boolean }) => React.ReactNode
/** Layout type - 'list' for vertical/horizontal list, 'grid' for grid layout */
layout?: 'list' | 'grid'
/** Whether sorting is horizontal */
horizontal?: boolean
/** Whether to use drag overlay
* If you want to hide ghost item, set showGhost to false rather than useDragOverlay.
*/
useDragOverlay?: boolean
/** Whether to show ghost item, only works when useDragOverlay is true */
showGhost?: boolean
/** Item list class name */
className?: string
/** Item list style */
listStyle?: React.CSSProperties
/** Ghost item style */
ghostItemStyle?: React.CSSProperties
}
function Sortable<T>({
items,
itemKey,
onSortEnd,
onDragStart: customOnDragStart,
onDragEnd: customOnDragEnd,
renderItem,
layout = 'list',
horizontal = false,
useDragOverlay = true,
showGhost = false,
className,
listStyle
}: SortableProps<T>) {
const sensors = useSensors(
useSensor(PortalSafePointerSensor, {
activationConstraint: {
distance: 8
}
}),
useSensor(TouchSensor, {
activationConstraint: {
delay: 100,
tolerance: 5
}
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates
})
)
const getId = useCallback(
(item: T) => (typeof itemKey === 'function' ? itemKey(item) : (item[itemKey] as string | number)),
[itemKey]
)
const itemIds = useMemo(() => items.map(getId), [items, getId])
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null)
const activeItem = activeId ? items.find((item) => getId(item) === activeId) : null
const getIndex = (id: UniqueIdentifier) => itemIds.indexOf(id)
const activeIndex = activeId ? getIndex(activeId) : -1
const handleDragStart = ({ active }) => {
customOnDragStart?.({ active })
if (active) {
setActiveId(active.id)
}
}
const handleDragEnd = ({ over }) => {
setActiveId(null)
customOnDragEnd?.({ over })
if (over) {
const overIndex = getIndex(over.id)
if (activeIndex !== overIndex) {
onSortEnd({ oldIndex: activeIndex, newIndex: overIndex })
}
}
}
const handleDragCancel = () => {
setActiveId(null)
}
const strategy =
layout === 'list' ? (horizontal ? horizontalListSortingStrategy : verticalListSortingStrategy) : rectSortingStrategy
const modifiers = layout === 'list' ? (horizontal ? [restrictToHorizontalAxis] : [restrictToVerticalAxis]) : []
const dropAnimation: DropAnimation = useMemo(
() => ({
sideEffects: defaultDropAnimationSideEffects({
styles: {
active: { opacity: showGhost ? '0.25' : '0' }
}
})
}),
[showGhost]
)
return (
<DndContext
sensors={sensors}
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
onDragCancel={handleDragCancel}
modifiers={modifiers}>
<SortableContext items={itemIds} strategy={strategy}>
<ListWrapper className={className} data-layout={layout} style={listStyle}>
{items.map((item, index) => (
<SortableItem
key={itemIds[index]}
item={item}
getId={getId}
renderItem={renderItem}
useDragOverlay={useDragOverlay}
showGhost={showGhost}
/>
))}
</ListWrapper>
</SortableContext>
{useDragOverlay
? createPortal(
<DragOverlay adjustScale dropAnimation={dropAnimation}>
{activeItem ? <ItemRenderer item={activeItem} renderItem={renderItem} dragOverlay /> : null}
</DragOverlay>,
document.body
)
: null}
</DndContext>
)
}
const ListWrapper = styled.div`
&[data-layout='grid'] {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
width: 100%;
gap: 12px;
@media (max-width: 768px) {
grid-template-columns: 1fr;
}
}
`
export default Sortable

View File

@ -0,0 +1,41 @@
import { useSortable } from '@dnd-kit/sortable'
import React from 'react'
import { ItemRenderer } from './ItemRenderer'
interface SortableItemProps<T> {
item: T
getId: (item: T) => string | number
renderItem: (item: T, props: { dragging: boolean }) => React.ReactNode
useDragOverlay?: boolean
showGhost?: boolean
}
export function SortableItem<T>({
item,
getId,
renderItem,
useDragOverlay = true,
showGhost = true
}: SortableItemProps<T>) {
const id = getId(item)
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id
})
return (
<ItemRenderer
ref={setNodeRef}
item={item}
renderItem={renderItem}
dragging={isDragging}
dragOverlay={!useDragOverlay && isDragging}
ghost={showGhost && useDragOverlay && isDragging}
transform={transform}
transition={transition}
listeners={listeners}
{...attributes}
/>
)
}

View File

@ -0,0 +1,3 @@
export { default as Sortable } from './Sortable'
export * from './useDndReorder'
export * from './useDndState'

View File

@ -0,0 +1,74 @@
import { Key, useCallback, useMemo } from 'react'
interface UseDndReorderParams<T> {
/** 原始的、完整的数据列表 */
originalList: T[]
/** 当前在界面上渲染的、可能被过滤的列表 */
filteredList: T[]
/** 用于更新原始列表状态的函数 */
onUpdate: (newList: T[]) => void
/** 用于从列表项中获取唯一ID的属性名或函数 */
idKey: keyof T | ((item: T) => Key)
}
/**
*
*
* @template T
* @param params - { originalList, filteredList, onUpdate, idKey }
* @returns Sortable onSortEnd
*/
export function useDndReorder<T>({ originalList, filteredList, onUpdate, idKey }: UseDndReorderParams<T>) {
const getId = useCallback((item: T) => (typeof idKey === 'function' ? idKey(item) : (item[idKey] as Key)), [idKey])
// 创建从 item ID 到其在 *原始列表* 中索引的映射
const itemIndexMap = useMemo(() => {
const map = new Map<Key, number>()
originalList.forEach((item, index) => {
map.set(getId(item), index)
})
return map
}, [originalList, getId])
// 创建一个函数,将 *过滤后列表* 的视图索引转换为 *原始列表* 的数据索引
const getItemKey = useCallback(
(index: number): Key => {
const item = filteredList[index]
// 如果找不到item返回视图索引兜底
if (!item) return index
const originalIndex = itemIndexMap.get(getId(item))
return originalIndex ?? index
},
[filteredList, itemIndexMap, getId]
)
// 创建 onSortEnd 回调,封装了所有重排逻辑
const onSortEnd = useCallback(
({ oldIndex, newIndex }: { oldIndex: number; newIndex: number }) => {
// 使用 getItemKey 将视图索引转换为数据索引
const sourceOriginalIndex = getItemKey(oldIndex) as number
const destOriginalIndex = getItemKey(newIndex) as number
// 如果索引转换失败,不进行任何操作
if (sourceOriginalIndex === undefined || destOriginalIndex === undefined) {
return
}
if (sourceOriginalIndex === destOriginalIndex) {
return
}
// 操作原始列表的副本
const newList = [...originalList]
const [movedItem] = newList.splice(sourceOriginalIndex, 1)
newList.splice(destOriginalIndex, 0, movedItem)
// 调用外部更新函数
onUpdate(newList)
},
[getItemKey, originalList, onUpdate]
)
return { onSortEnd, itemKey: getItemKey }
}

View File

@ -0,0 +1,28 @@
import { useDndContext } from '@dnd-kit/core'
interface DndState {
/** 是否有元素正在拖拽 */
isDragging: boolean
/** 当前拖拽元素的ID */
draggedId: string | number | null
/** 当前悬停位置的ID */
overId: string | number | null
/** 是否正在悬停在某个可放置区域 */
isOver: boolean
}
/**
* dnd-kit
*
* @returns
*/
export function useDndState(): DndState {
const { active, over } = useDndContext()
return {
isDragging: active !== null,
draggedId: active?.id ?? null,
overId: over?.id ?? null,
isOver: over !== null
}
}

View File

@ -0,0 +1,45 @@
import { defaultDropAnimationSideEffects, type DropAnimation, PointerSensor } from '@dnd-kit/core'
export const PORTAL_NO_DND_SELECTORS = [
'.ant-dropdown',
'.ant-select-dropdown',
'.ant-popover',
'.ant-tooltip',
'.ant-modal'
].join(',')
/**
* Default drop animation config.
* The opacity is set so to match the drag overlay case.
*/
export const dropAnimationConfig: DropAnimation = {
sideEffects: defaultDropAnimationSideEffects({
styles: {
active: {
opacity: '0.25'
}
}
})
}
/**
* Prevent drag on elements with specific classes or data-no-dnd attribute
*/
export class PortalSafePointerSensor extends PointerSensor {
static activators = [
{
eventName: 'onPointerDown',
handler: ({ nativeEvent: event }) => {
let target = event.target as HTMLElement
while (target) {
if (target.closest(PORTAL_NO_DND_SELECTORS) || target.dataset?.noDnd) {
return false
}
target = target.parentElement as HTMLElement
}
return true
}
}
] as (typeof PointerSensor)['activators']
}

View File

@ -137,7 +137,10 @@ const AssistantItem: FC<AssistantItemProps> = ({
)
return (
<Dropdown menu={{ items: menuItems }} trigger={['contextMenu']}>
<Dropdown
menu={{ items: menuItems }}
trigger={['contextMenu']}
popupRender={(menu) => <div onPointerDown={(e) => e.stopPropagation()}>{menu}</div>}>
<Container onClick={handleSwitch} className={isActive ? 'active' : ''}>
<AssistantNameRow className="name" title={fullAssistantName}>
{assistantIconType === 'model' ? (
@ -386,7 +389,6 @@ const Container = styled.div`
border-radius: var(--list-item-border-radius);
border: 0.5px solid transparent;
width: calc(var(--assistants-width) - 20px);
cursor: pointer;
&:hover {
background-color: var(--color-list-item-hover);
}

View File

@ -1,7 +1,7 @@
import { DeleteIcon } from '@renderer/components/Icons'
import { getMcpTypeLabel } from '@renderer/i18n/label'
import { MCPServer } from '@renderer/types'
import { Badge, Button, Switch, Tag } from 'antd'
import { Button, Switch, Tag, Typography } from 'antd'
import { Settings2, SquareArrowOutUpRight } from 'lucide-react'
import { FC } from 'react'
import styled from 'styled-components'
@ -33,38 +33,49 @@ const McpServerCard: FC<McpServerCardProps> = ({
}
return (
<CardContainer $isActive={server.isActive}>
<CardContainer $isActive={server.isActive} onClick={onEdit}>
<ServerHeader>
<ServerName>
<ServerNameWrapper>
{server.logoUrl && <ServerLogo src={server.logoUrl} alt={`${server.name} logo`} />}
<ServerNameText>{server.name}</ServerNameText>
{version && <VersionBadge count={version} color="blue" />}
<ServerNameText ellipsis={{ tooltip: true }}>{server.name}</ServerNameText>
{server.providerUrl && (
<Button
type="text"
size="small"
shape="circle"
icon={<SquareArrowOutUpRight size={14} />}
className="nodrag"
onClick={handleOpenUrl}
data-no-dnd
/>
)}
</ServerName>
</ServerNameWrapper>
<ToolbarWrapper onClick={(e) => e.stopPropagation()}>
<Switch value={server.isActive} key={server.id} loading={isLoading} onChange={onToggle} size="small" />
<Switch
value={server.isActive}
key={server.id}
loading={isLoading}
onChange={onToggle}
size="small"
data-no-dnd
/>
<Button
type="text"
shape="circle"
icon={<DeleteIcon size={16} className="lucide-custom" />}
className="nodrag"
icon={<DeleteIcon size={14} className="lucide-custom" />}
danger
onClick={onDelete}
data-no-dnd
/>
<Button type="text" shape="circle" icon={<Settings2 size={16} />} className="nodrag" onClick={onEdit} />
<Button type="text" shape="circle" icon={<Settings2 size={14} />} onClick={onEdit} data-no-dnd />
</ToolbarWrapper>
</ServerHeader>
<ServerDescription>{server.description}</ServerDescription>
<ServerFooter>
{version && (
<VersionBadge color="#108ee9">
<VersionText ellipsis={{ tooltip: true }}>{version}</VersionText>
</VersionBadge>
)}
<ServerTag color="processing">{getMcpTypeLabel(server.type ?? 'stdio')}</ServerTag>
{server.provider && <ServerTag color="success">{server.provider}</ServerTag>}
{server.tags
@ -85,17 +96,17 @@ const CardContainer = styled.div<{ $isActive: boolean }>`
flex-direction: column;
border: 0.5px solid var(--color-border);
border-radius: var(--list-item-border-radius);
padding: 10px 16px;
padding: 10px 10px 10px 16px;
transition: all 0.2s ease;
background-color: var(--color-background);
margin-bottom: 5px;
height: 125px;
cursor: pointer;
opacity: ${(props) => (props.$isActive ? 1 : 0.6)};
&:hover {
border-color: var(--color-primary);
opacity: 1;
border-color: var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
`
@ -105,17 +116,16 @@ const ServerHeader = styled.div`
margin-bottom: 5px;
`
const ServerName = styled.div`
const ServerNameWrapper = styled.div`
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
gap: 4px;
`
const ServerNameText = styled.span`
const ServerNameText = styled(Typography.Text)`
font-size: 15px;
font-weight: 500;
`
@ -131,7 +141,7 @@ const ServerLogo = styled.img`
const ToolbarWrapper = styled.div`
display: flex;
align-items: center;
gap: 4px;
margin-left: 8px;
> :first-child {
margin-right: 4px;
@ -163,19 +173,14 @@ const ServerTag = styled(Tag)`
margin: 0;
`
const VersionBadge = styled(Badge)`
.ant-badge-count {
background-color: var(--color-primary);
color: white;
font-size: 10px;
font-weight: 500;
padding: 0 5px;
height: 16px;
line-height: 16px;
border-radius: 8px;
min-width: 16px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
const VersionBadge = styled(ServerTag)`
font-weight: 500;
max-width: 6rem !important;
`
const VersionText = styled(Typography.Text)`
font-size: inherit;
color: white;
`
export default McpServerCard

View File

@ -1,5 +1,5 @@
import { nanoid } from '@reduxjs/toolkit'
import { DraggableList } from '@renderer/components/DraggableList'
import { Sortable } from '@renderer/components/dnd'
import { EditIcon, RefreshIcon } from '@renderer/components/Icons'
import Scrollbar from '@renderer/components/Scrollbar'
import { useMCPServers } from '@renderer/hooks/useMCPServers'
@ -192,53 +192,57 @@ const McpServersList: FC = () => {
<ListHeader>
<SettingTitle style={{ gap: 3 }}>
<span>{t('settings.mcp.newServer')}</span>
<Button icon={<EditIcon size={14} />} type="text" onClick={() => EditMcpJsonPopup.show()} shape="circle" />
</SettingTitle>
<ButtonGroup>
<InstallNpxUv mini />
<Button icon={<EditIcon size={14} />} type="default" shape="round" onClick={() => EditMcpJsonPopup.show()}>
{t('common.edit')}
</Button>
<Dropdown
menu={{
items: menuItems
}}
trigger={['click']}>
<Button icon={<Plus size={16} />} type="default" shape="round">
{t('settings.mcp.addServer.label')}
{t('common.add')}
</Button>
</Dropdown>
<Button icon={<RefreshIcon size={16} />} type="default" onClick={onSyncServers} shape="round">
{t('settings.mcp.sync.title', 'Sync Servers')}
<Button icon={<RefreshIcon size={14} />} type="default" onClick={onSyncServers} shape="round">
{t('settings.mcp.sync.button')}
</Button>
</ButtonGroup>
</ListHeader>
<DraggableList
style={{ width: '100%' }}
list={mcpServers}
onUpdate={updateMcpServers}
listProps={{
locale: {
emptyText: (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={t('settings.mcp.noServers')}
style={{ marginTop: 20 }}
/>
)
}
}}>
{(server: MCPServer) => (
<div onClick={() => navigate(`/settings/mcp/settings/${encodeURIComponent(server.id)}`)}>
<McpServerCard
server={server}
version={serverVersions[server.id]}
isLoading={loadingServerIds.has(server.id)}
onToggle={(active) => handleToggleActive(server, active)}
onDelete={() => onDeleteMcpServer(server)}
onEdit={() => navigate(`/settings/mcp/settings/${encodeURIComponent(server.id)}`)}
onOpenUrl={(url) => window.open(url, '_blank')}
/>
</div>
<Sortable
items={mcpServers}
itemKey="id"
onSortEnd={({ oldIndex, newIndex }) => {
const newList = [...mcpServers]
const [removed] = newList.splice(oldIndex, 1)
newList.splice(newIndex, 0, removed)
updateMcpServers(newList)
}}
layout="grid"
useDragOverlay
showGhost
renderItem={(server) => (
<McpServerCard
server={server}
version={serverVersions[server.id]}
isLoading={loadingServerIds.has(server.id)}
onToggle={(active) => handleToggleActive(server, active)}
onDelete={() => onDeleteMcpServer(server)}
onEdit={() => navigate(`/settings/mcp/settings/${encodeURIComponent(server.id)}`)}
onOpenUrl={(url) => window.open(url, '_blank')}
/>
)}
</DraggableList>
/>
{mcpServers.length === 0 && (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={t('settings.mcp.noServers')}
style={{ marginTop: 20 }}
/>
)}
<McpMarketList />
<BuiltinMCPServerList />

View File

@ -2691,6 +2691,68 @@ __metadata:
languageName: node
linkType: hard
"@dnd-kit/accessibility@npm:^3.1.1":
version: 3.1.1
resolution: "@dnd-kit/accessibility@npm:3.1.1"
dependencies:
tslib: "npm:^2.0.0"
peerDependencies:
react: ">=16.8.0"
checksum: 10c0/be0bf41716dc58f9386bc36906ec1ce72b7b42b6d1d0e631d347afe9bd8714a829bd6f58a346dd089b1519e93918ae2f94497411a61a4f5e4d9247c6cfd1fef8
languageName: node
linkType: hard
"@dnd-kit/core@npm:^6.3.1":
version: 6.3.1
resolution: "@dnd-kit/core@npm:6.3.1"
dependencies:
"@dnd-kit/accessibility": "npm:^3.1.1"
"@dnd-kit/utilities": "npm:^3.2.2"
tslib: "npm:^2.0.0"
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: 10c0/196db95d81096d9dc248983533eab91ba83591770fa5c894b1ac776f42af0d99522b3fd5bb3923411470e4733fcfa103e6ee17adc17b9b7eb54c7fbec5ff7c52
languageName: node
linkType: hard
"@dnd-kit/modifiers@npm:^9.0.0":
version: 9.0.0
resolution: "@dnd-kit/modifiers@npm:9.0.0"
dependencies:
"@dnd-kit/utilities": "npm:^3.2.2"
tslib: "npm:^2.0.0"
peerDependencies:
"@dnd-kit/core": ^6.3.0
react: ">=16.8.0"
checksum: 10c0/ca8cc9da8296df10774d779c1611074dc327ccc3c49041c102111c98c7f2b2b73b6af5209c0eef6b2fe978ac63dc2a985efa87c85a8d786577304bd2e64cee1d
languageName: node
linkType: hard
"@dnd-kit/sortable@npm:^10.0.0":
version: 10.0.0
resolution: "@dnd-kit/sortable@npm:10.0.0"
dependencies:
"@dnd-kit/utilities": "npm:^3.2.2"
tslib: "npm:^2.0.0"
peerDependencies:
"@dnd-kit/core": ^6.3.0
react: ">=16.8.0"
checksum: 10c0/37ee48bc6789fb512dc0e4c374a96d19abe5b2b76dc34856a5883aaa96c3297891b94cc77bbc409e074dcce70967ebcb9feb40cd9abadb8716fc280b4c7f99af
languageName: node
linkType: hard
"@dnd-kit/utilities@npm:^3.2.2":
version: 3.2.2
resolution: "@dnd-kit/utilities@npm:3.2.2"
dependencies:
tslib: "npm:^2.0.0"
peerDependencies:
react: ">=16.8.0"
checksum: 10c0/9aa90526f3e3fd567b5acc1b625a63177b9e8d00e7e50b2bd0e08fa2bf4dba7e19529777e001fdb8f89a7ce69f30b190c8364d390212634e0afdfa8c395e85a0
languageName: node
linkType: hard
"@electron-toolkit/eslint-config-prettier@npm:^3.0.0":
version: 3.0.0
resolution: "@electron-toolkit/eslint-config-prettier@npm:3.0.0"
@ -8406,6 +8468,10 @@ __metadata:
"@cherrystudio/embedjs-loader-xml": "npm:^0.1.31"
"@cherrystudio/embedjs-ollama": "npm:^0.1.31"
"@cherrystudio/embedjs-openai": "npm:^0.1.31"
"@dnd-kit/core": "npm:^6.3.1"
"@dnd-kit/modifiers": "npm:^9.0.0"
"@dnd-kit/sortable": "npm:^10.0.0"
"@dnd-kit/utilities": "npm:^3.2.2"
"@electron-toolkit/eslint-config-prettier": "npm:^3.0.0"
"@electron-toolkit/eslint-config-ts": "npm:^3.0.0"
"@electron-toolkit/preload": "npm:^3.0.0"
@ -21309,7 +21375,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.8.1":
"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.1.0, tslib@npm:^2.4.0, tslib@npm:^2.6.2, tslib@npm:^2.8.1":
version: 2.8.1
resolution: "tslib@npm:2.8.1"
checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62