@liuman/react-desktop
v0.9.9
Published
A real desktop for React: multi-page icon grid with auto-paging, marquee select, drag to move / into folders / across pages, right-click and long-press menus, inline rename, sort/align/tidy, and external file drop
Maintainers
Readme
@liuman/react-desktop
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.
Install
npm install @liuman/react-desktopPeer deps: react, react-dom, antd, styled-components.
Usage
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}
/>Behavior notes
- 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).
API
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.
License
MIT
