Files
openchamber/packages/ui/src/components/session/sidebar/sessionFolderDnd.tsx
T
Bohdan Triapitsyn 14357257ae perf(ui): migrate icons to SVG sprite system
Replace @remixicon/react with a shared Icon component that renders
via <use href> references to a single hidden SVG sprite. This reduces
DOM node count by replacing inline SVGs with lightweight references.

- Create Icon component with sprite injection (packages/ui/src/components/icon/)
- Migrate all 164 files from @remixicon/react to Icon component
- Auto-generate sprite data from remixicon bundle (scripts/generate-icon-sprite.mjs)
- Add bun run icons:generate to package.json
- Move @remixicon/react to devDependencies
- Add icon usage instructions to theme-system skill
2026-05-13 13:26:35 +03:00

135 lines
4.5 KiB
TypeScript

import React from 'react';
import {
DndContext,
DragOverlay,
PointerSensor,
closestCenter,
useSensor,
useSensors,
useDraggable,
useDroppable,
type DragEndEvent,
} from '@dnd-kit/core';
import { Icon } from "@/components/icon/Icon";
export const DraggableSessionRow: React.FC<{
sessionId: string;
sessionDirectory: string | null;
sessionTitle: string;
children: React.ReactNode;
}> = ({ sessionId, sessionDirectory, sessionTitle, children }) => {
const { attributes, listeners, setNodeRef, isDragging } = useDraggable({
id: `session-drag:${sessionId}`,
data: { type: 'session', sessionId, sessionDirectory, sessionTitle },
});
const handlePointerDown = React.useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
e.stopPropagation();
if (listeners?.onPointerDown) {
(listeners.onPointerDown as (event: React.PointerEvent) => void)(e);
}
},
[listeners],
);
return (
<div
ref={setNodeRef}
{...attributes}
onPointerDown={handlePointerDown}
className={isDragging ? 'opacity-30' : undefined}
>
{children}
</div>
);
};
export const DroppableFolderWrapper: React.FC<{
folderId: string;
children: (
droppableRef: (node: HTMLElement | null) => void,
isOver: boolean,
) => React.ReactNode;
}> = ({ folderId, children }) => {
const { setNodeRef, isOver } = useDroppable({
id: `folder-drop:${folderId}`,
data: { type: 'folder', folderId },
});
return <>{children(setNodeRef, isOver)}</>;
};
export const SessionFolderDndScope: React.FC<{
scopeKey: string | null;
hasFolders: boolean;
onSessionDroppedOnFolder: (sessionId: string, folderId: string) => void;
children: React.ReactNode;
}> = ({ scopeKey, hasFolders, onSessionDroppedOnFolder, children }) => {
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
);
const [activeDragId, setActiveDragId] = React.useState<string | null>(null);
const [activeDragTitle, setActiveDragTitle] = React.useState<string>('Session');
const [activeDragWidth, setActiveDragWidth] = React.useState<number | null>(null);
const [activeDragHeight, setActiveDragHeight] = React.useState<number | null>(null);
if (!scopeKey) {
return <>{children}</>;
}
const handleDragEnd = (event: DragEndEvent) => {
setActiveDragId(null);
setActiveDragWidth(null);
setActiveDragHeight(null);
const { active, over } = event;
if (!over) return;
const activeData = active.data.current as { type?: string; sessionId?: string } | undefined;
const overData = over.data.current as { type?: string; folderId?: string } | undefined;
if (activeData?.type === 'session' && activeData.sessionId && overData?.type === 'folder' && overData.folderId) {
onSessionDroppedOnFolder(activeData.sessionId, overData.folderId);
}
};
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragStart={(event) => {
const data = event.active.data.current as { type?: string; sessionId?: string; sessionTitle?: string } | undefined;
if (data?.type === 'session' && data.sessionId) {
setActiveDragId(data.sessionId);
setActiveDragTitle(data.sessionTitle ?? 'Session');
const width = event.active.rect.current.initial?.width;
const height = event.active.rect.current.initial?.height;
setActiveDragWidth(typeof width === 'number' ? width : null);
setActiveDragHeight(typeof height === 'number' ? height : null);
}
}}
onDragCancel={() => {
setActiveDragId(null);
setActiveDragWidth(null);
setActiveDragHeight(null);
}}
onDragEnd={handleDragEnd}
>
{children}
<DragOverlay>
{activeDragId && hasFolders ? (
<div
style={{
width: activeDragWidth ? `${activeDragWidth}px` : 'auto',
height: activeDragHeight ? `${activeDragHeight}px` : 'auto',
}}
className="flex items-center rounded-lg border border-[var(--interactive-border)] bg-[var(--surface-elevated)] px-2.5 py-1 shadow-none pointer-events-none"
>
<Icon name="sticky-note" className="h-4 w-4 text-muted-foreground mr-2 flex-shrink-0" />
<div className="min-w-0 flex-1 truncate typography-ui-label font-normal text-foreground">
{activeDragTitle}
</div>
</div>
) : null}
</DragOverlay>
</DndContext>
);
};