A real desktop for React — the kind people already know how to use.
Icon grid, marquee multi-select, drag to move or drop into a folder, right-click and long-press menus, click-a-selected-name to rename in place, sort / align / tidy, and external file drop.
It knows about exactly two things: folders and files. What an icon looks like, what "open" means, whether a given folder may be dropped into — all of that is yours.
npm install @liuman/react-desktopPeer deps: react, react-dom, antd, styled-components.
import { Desktop, type DesktopItem } from '@liuman/react-desktop';
const items: DesktopItem[] = [
{ id: 'twin', name: '数字孪生', kind: 'folder', icon: <TwinIcon />, color: '#1890ff', droppable: false },
{ id: 'mirror', name: '数物镜像', kind: 'folder', icon: <MirrorIcon />, color: '#13c2c2' },
{ id: 'v3', name: '2026-08 版本', kind: 'file', icon: <VersionIcon />, caption: 'v3' },
];
<Desktop
items={items}
onOpen={(item, mode) =>
mode === 'newWindow' ? window.open(`/projects/${item.id}`, '_blank') : openInlineWindow(item)
}
onRename={(item, name) => save(item.id, name)}
onPositionsChange={(pos) => localStorage.setItem('desktop', JSON.stringify(pos))}
canDropInto={(folder) => folder.id !== 'twin'} // 数字孪生只读,不接收拖入
onDropInto={(folder, dragged) => copyInto(folder, dragged)}
onDropFiles={(files, folder) => importFiles(files, folder)}
acceptFileExtensions={['.json', '.dxf', '.dwg', '.xlsx']}
buildMenu={({ item }) => (item?.kind === 'file' ? [{ key: 'download', label: '下载' }] : [])}
onMenuAction={(key, ctx) => key === 'download' && exportItem(ctx.item!)}
onCreateFolder={createFolder}
onCreateFile={createFile}
/>- Positions are grid cells, not pixels.
{col, row}survives a resize, a zoom, and a different screen; pixels don't. - Long-press (500ms) opens the same menu as right-click, so touch and mouse are equal. Moving more than 4px cancels it and starts a drag instead.
- Rename is the desktop convention: click a selected item's name once. The stem is preselected, so an extension survives a careless keystroke.
- Folders sort before files, always.
canDropIntois checked while dragging, so a rejecting folder never even highlights.- One focused surface at a time. With several
<Desktop>s on screen (a root desktop plus folder windows), pressing on one gives it the focus and every other surface clears its selection, closes its menu and leaves rename mode — pressing empty space counts too. Without this you get two highlights at once, and worse:Delete/⌘Cbound onwindowact on whichever surface still happened to be lit, not the one you just clicked. Give each surface adragSource(it doubles as the focus id; one is generated otherwise) and gate global shortcuts withhasDesktopFocus(id).
items, cellWidth, cellHeight, sortKey, onOpen, onRename, onPositionsChange,
onDropInto, canDropInto, onDropFiles, acceptFileExtensions, onSelectionChange,
buildMenu, onMenuAction, onCreateFolder, onCreateFile, onSortKeyChange,
dragSource (source tag for cross-surface drags; doubles as the focus id).
Focus bus (claimDesktopFocus, releaseDesktopFocus, getDesktopFocus, hasDesktopFocus,
subscribeDesktopFocus) is exported so callers can route global keyboard shortcuts to
whichever surface the user last pressed.
Layout helpers (resolvePositions, alignToGrid, tidyUp, sortItems, cellToPixel,
pixelToCell, findFreeCell) are exported for callers that persist their own layout.
MIT
这个包来自昱弘的产品线 —— 做在断网、掉电、现场网络多变的环境里也能持续运转的系统。