import {
closestCenter, DndContext, KeyboardSensor, PointerSensor, useSensor, useSensors,
type DragEndEvent,
} from '@dnd-kit/core';
import {
SortableContext, sortableKeyboardCoordinates, useSortable, verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import {CSS} from '@dnd-kit/utilities';
import type {SectionItem} from '@o2o/shared';
import {GripVertical, Layers, Lock, Shapes} from 'lucide-react';
import {Switch} from '@/components/ui/switch';
import {cn} from '@/lib/utils';
import {useBuilderStore} from '@/stores/builder';
import {resolveVariant} from './canvas/registry';
interface SectionListPanelProps {
onAfterSelect?: () => void;
}
interface SortableSectionProps extends SectionListPanelProps {
section: SectionItem;
index: number;
}
function SortableSection({section, index, onAfterSelect}: SortableSectionProps) {
const industry = useBuilderStore((s) => s.industry);
const setRightTab = useBuilderStore((s) => s.setRightTab);
const isSelected = useBuilderStore((s) => s.selectedSectionId === section.id);
const selectSection = useBuilderStore((s) => s.selectSection);
const toggleSection = useBuilderStore((s) => s.toggleSection);
const variant = resolveVariant(section, industry);
const {attributes, listeners, setNodeRef, transform, transition, isDragging} = useSortable({
id: section.id,
});
const select = () => {
selectSection(section.id);
setRightTab('content');
onAfterSelect?.();
if (!section.isEnabled) return;
// 좁은 화면에서는 onAfterSelect가 캔버스 탭을 먼저 연다. 다음 페인트가 끝난 뒤
// 실제 섹션으로 이동해야 display:none 상태의 위치를 잘못 읽지 않는다.
requestAnimationFrame(() => {
requestAnimationFrame(() => {
document
.getElementById(`canvas-section-${section.id}`)
?.scrollIntoView({behavior: 'smooth', block: 'start'});
});
});
};
return (
{
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
select();
}
}}
className={cn(
'group flex cursor-pointer items-center justify-between rounded-md border p-2 text-xs transition-[background-color,border-color,opacity,box-shadow]',
isDragging && 'cursor-grabbing opacity-70 shadow-lg',
isSelected
? 'border-primary bg-primary/8 font-semibold text-foreground'
: section.isEnabled
? 'border-border bg-card hover:bg-muted/60'
: 'border-border bg-muted/40 text-muted-foreground',
)}
>
{index + 1}
{section.name}
{variant && (
{
event.stopPropagation();
selectSection(section.id);
setRightTab('design');
onAfterSelect?.();
}}
onKeyDown={(event) => {
if (event.key !== 'Enter' && event.key !== ' ') return;
event.preventDefault();
event.stopPropagation();
selectSection(section.id);
setRightTab('design');
onAfterSelect?.();
}}
className="flex cursor-pointer items-center gap-0.5 truncate text-[9px] font-normal text-muted-foreground hover:text-foreground"
>
{variant.name}
)}
{section.isLocked && (
필수 고정
)}
event.stopPropagation()} role="presentation">
{section.isLocked ? (
) : (
toggleSection(section.id)} aria-label={`${section.name} 섹션 표시 여부`} />
)}
);
}
export function SectionListPanel({onAfterSelect}: SectionListPanelProps) {
const sections = useBuilderStore((s) => s.sections);
const reorderSection = useBuilderStore((s) => s.reorderSection);
const sensors = useSensors(
useSensor(PointerSensor, {activationConstraint: {distance: 6}}),
useSensor(KeyboardSensor, {coordinateGetter: sortableKeyboardCoordinates}),
);
const activeCount = sections.filter((section) => section.isEnabled).length;
const handleDragEnd = ({active, over}: DragEndEvent) => {
if (!over || active.id === over.id) return;
const fromIndex = sections.findIndex((section) => section.id === active.id);
const toIndex = sections.findIndex((section) => section.id === over.id);
reorderSection(fromIndex, toIndex);
};
return (
섹션 관리
{activeCount} / {sections.length} 활성
핸들을 드래그해 순서를 바꾸고, 레이아웃 이름을 누르면 모양을 바꿉니다.
section.id)} strategy={verticalListSortingStrategy}>
{sections.map((section, index) => (
))}
* 자물쇠 항목은 검색·AI 노출에 필요한 마크업이 달려 있어 고정입니다.
);
}