diff --git a/__tests__/schema.test.ts b/__tests__/schema.test.ts index 618ab5b..e96eb20 100644 --- a/__tests__/schema.test.ts +++ b/__tests__/schema.test.ts @@ -64,3 +64,50 @@ describe("schema rejects invalid data", () => { expect(workspaceSchema.safeParse({ icon: "" }).success).toBe(false); }); }); + +describe("candidate.archived の取り扱い", () => { + const baseCandidate = { + id: "c-archived-test", + profile: { + name: "テスト 太郎", + birthday: "", + source: "", + email: "", + phone: "", + address: "", + recruiter: "", + desiredSalaryMin: "", + desiredSalaryMax: "", + availableStartDate: "", + careerText: "", + motivationFull: "", + }, + scorecards: [], + stage: "screening" as const, + }; + + it("archived 未指定なら false がデフォルトで埋まる", () => { + const result = candidatesSchema.safeParse([baseCandidate]); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data[0].archived).toBe(false); + } + }); + + it("archived: true を許容する", () => { + const result = candidatesSchema.safeParse([ + { ...baseCandidate, archived: true }, + ]); + expect(result.success).toBe(true); + if (result.success) { + expect(result.data[0].archived).toBe(true); + } + }); + + it("archived が boolean でなければ不可", () => { + const result = candidatesSchema.safeParse([ + { ...baseCandidate, archived: "yes" }, + ]); + expect(result.success).toBe(false); + }); +}); diff --git a/__tests__/scorecards-derived.test.ts b/__tests__/scorecards-derived.test.ts index 59bcfa2..55db9d8 100644 --- a/__tests__/scorecards-derived.test.ts +++ b/__tests__/scorecards-derived.test.ts @@ -104,6 +104,7 @@ describe("getCandidateAverageScore / getScorecardsAverageScore", () => { }, scorecards: [], stage: "screening", + archived: false, }; expect(getCandidateAverageScore(candidate)).toBeNull(); expect(getScorecardsAverageScore([])).toBeNull(); diff --git a/components/workspace/CandidateListPane.tsx b/components/workspace/CandidateListPane.tsx index 94a4765..71390b6 100644 --- a/components/workspace/CandidateListPane.tsx +++ b/components/workspace/CandidateListPane.tsx @@ -1,14 +1,50 @@ "use client"; import { useState } from "react"; -import { MoreHorizontal, Plus, Star, Trash2 } from "lucide-react"; +import { + Archive, + ArchiveRestore, + ChevronDown, + MoreHorizontal, + Plus, + Star, +} from "lucide-react"; +import { + DndContext, + DragOverlay, + KeyboardSensor, + PointerSensor, + closestCenter, + useDroppable, + useSensor, + useSensors, + type Announcements, + type DragEndEvent, + type DragStartEvent, + type ScreenReaderInstructions, +} from "@dnd-kit/core"; +import { + SortableContext, + sortableKeyboardCoordinates, + verticalListSortingStrategy, +} from "@dnd-kit/sortable"; import { cn } from "@/lib/utils"; -import { type Group, type StageKey } from "@/lib/schema"; +import { + type CandidateRow, + type Group, + type StageKey, +} from "@/lib/schema"; import { DeleteConfirmDialog } from "@/components/workspace/DeleteConfirmDialog"; +import { SortableCandidateRow } from "@/components/workspace/SortableCandidateRow"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible"; import { DropdownMenu, DropdownMenuContent, @@ -18,13 +54,22 @@ import { } from "@/components/ui/dropdown-menu"; import { ScrollArea } from "@/components/ui/scroll-area"; import { AddItemDialog } from "@/components/workspace/AddItemDialog"; +import { STAGE_LABELS } from "@/lib/labels"; + +// dnd-kit のスクリーンリーダー向け日本語化。 +const screenReaderInstructions: ScreenReaderInstructions = { + draggable: + "Space または Enter で候補者を持ち上げ、矢印キーで移動、Space で確定、Esc でキャンセルします。", +}; type CandidateListPaneProps = { groups: Group[]; selectedCandidateId: string; onSelectCandidate: (id: string) => void; onAddCandidate: (stage: StageKey, name: string) => void; - onDeleteCandidate: (id: string) => void; + onArchiveCandidate: (id: string) => void; + onRestoreCandidate: (id: string) => void; + onMoveCandidate: (id: string, toStage: StageKey, toIndex: number) => void; }; export function CandidateListPane({ @@ -32,16 +77,124 @@ export function CandidateListPane({ selectedCandidateId, onSelectCandidate, onAddCandidate, - onDeleteCandidate, + onArchiveCandidate, + onRestoreCandidate, + onMoveCandidate, }: CandidateListPaneProps) { const [addDialogStage, setAddDialogStage] = useState<{ stage: StageKey; label: string; } | null>(null); - const [deleteTarget, setDeleteTarget] = useState<{ + const [archiveTarget, setArchiveTarget] = useState<{ id: string; name: string; } | null>(null); + const [archivedOpen, setArchivedOpen] = useState(false); + const [activeDragId, setActiveDragId] = useState(null); + + // PointerSensor の distance 制約で「クリック」と「ドラッグ」を区別する + // (6px 以上動かさないとドラッグ起動しない → 行クリック / メニュー操作と衝突しない)。 + // KeyboardSensor は Tab 移動 → Space で持ち上げ → 矢印で移動 → Enter で確定。 + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 6 } }), + useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }), + ); + + const stageGroups = groups.filter( + (g): g is Extract => g.kind === "stage", + ); + const archivedGroup = groups.find( + (g): g is Extract => g.kind === "archived", + ); + + // ドラッグ中の浮遊行表示用に元データを引く + const activeDragRow: { row: CandidateRow; stage: StageKey } | null = (() => { + if (!activeDragId) return null; + for (const g of stageGroups) { + const row = g.items.find((r) => r.id === activeDragId); + if (row) return { row, stage: g.stage }; + } + return null; + })(); + + const announcements: Announcements = { + onDragStart: ({ active }) => { + const name = (active.data.current?.name as string | undefined) ?? "候補者"; + return `${name}を持ち上げました。`; + }, + onDragOver: ({ active, over }) => { + const name = (active.data.current?.name as string | undefined) ?? "候補者"; + if (!over) return `${name}を移動中です。`; + const overContainer = over.data.current?.containerId as + | StageKey + | undefined; + if (overContainer) + return `${name}を「${STAGE_LABELS[overContainer]}」の上に移動しました。`; + return `${name}を移動中です。`; + }, + onDragEnd: ({ active, over }) => { + const name = (active.data.current?.name as string | undefined) ?? "候補者"; + if (!over) return `${name}の移動をキャンセルしました。`; + const overContainer = + (over.data.current?.containerId as StageKey | undefined) ?? + (typeof over.id === "string" && + stageGroups.some((g) => g.stage === over.id) + ? (over.id as StageKey) + : undefined); + if (!overContainer) return `${name}を確定しました。`; + return `${name}を「${STAGE_LABELS[overContainer]}」に移動しました。`; + }, + onDragCancel: ({ active }) => { + const name = (active.data.current?.name as string | undefined) ?? "候補者"; + return `${name}の移動をキャンセルしました。`; + }, + }; + + const handleDragStart = (event: DragStartEvent) => { + setActiveDragId(String(event.active.id)); + }; + + const handleDragEnd = (event: DragEndEvent) => { + const { active, over } = event; + setActiveDragId(null); + if (!over) return; + + const activeContainer = active.data.current?.containerId as + | StageKey + | undefined; + // over の所属コンテナは、要素ドロップなら data.current.containerId、 + // SortableContext そのものへのドロップなら over.id(= "stage" のキー)。 + const overContainer = + (over.data.current?.containerId as StageKey | undefined) ?? + (typeof over.id === "string" && + stageGroups.some((g) => g.stage === over.id) + ? (over.id as StageKey) + : undefined); + + if (!activeContainer || !overContainer) return; + + const targetGroup = stageGroups.find((g) => g.stage === overContainer); + if (!targetGroup) return; + + let toIndex: number; + if (active.id === over.id) return; + + const overIndexInTarget = targetGroup.items.findIndex( + (r) => r.id === over.id, + ); + if (overIndexInTarget >= 0) { + // 同コンテナ内: active を一旦取り除いた後の index を保つため、 + // active が同コンテナ内で over より前にあれば overIndexInTarget はそのまま、 + // 後ろなら -1 ずれる、という自然な算出になる。Workspace.moveCandidate 側で + // active を除いた配列上で挿入するため、ここでは単純に over の現 index を渡す。 + toIndex = overIndexInTarget; + } else { + // 空コンテナ・末尾領域へのドロップ: 末尾に追加 + toIndex = targetGroup.items.length; + } + + onMoveCandidate(String(active.id), overContainer, toIndex); + }; return (
@@ -51,108 +204,64 @@ export function CandidateListPane({ -
- {groups.map((group) => ( -
-
-
-

- {group.label} -

- - {group.items.length} - + setActiveDragId(null)} + > +
+ {stageGroups.map((group) => ( + + setAddDialogStage({ + stage: group.stage, + label: group.label, + }) + } + onArchiveRequest={(id, name) => + setArchiveTarget({ id, name }) + } + /> + ))} + {archivedGroup && ( + + )} +
+ + {activeDragRow && ( +
+ + + {activeDragRow.row.name[0] ?? "?"} + + +
+

{activeDragRow.row.name}

-
-
    - {group.items.map((cand) => { - const selected = cand.id === selectedCandidateId; - return ( -
  • - - - - - - } - /> - - - - setDeleteTarget({ - id: cand.id, - name: cand.name, - }) - } - > - - 削除 - - - - -
  • - ); - })} -
-
- ))} -
+ )} + + {addDialogStage && ( @@ -171,16 +280,18 @@ export function CandidateListPane({ )} { - if (!open) setDeleteTarget(null); + if (!open) setArchiveTarget(null); }} - title="候補者を削除しますか?" - itemName={deleteTarget?.name ?? ""} + title="候補者をアーカイブしますか?" + itemName={archiveTarget?.name ?? ""} + description={`「${archiveTarget?.name ?? ""}」をアーカイブします。後で「アーカイブ済み」から復元できます。`} + actionLabel="アーカイブ" onConfirm={() => { - if (deleteTarget) { - onDeleteCandidate(deleteTarget.id); - setDeleteTarget(null); + if (archiveTarget) { + onArchiveCandidate(archiveTarget.id); + setArchiveTarget(null); } }} /> @@ -188,6 +299,233 @@ export function CandidateListPane({ ); } +function StageGroup({ + stage, + label, + items, + selectedCandidateId, + onSelectCandidate, + onAddRequest, + onArchiveRequest, +}: { + stage: StageKey; + label: string; + items: CandidateRow[]; + selectedCandidateId: string; + onSelectCandidate: (id: string) => void; + onAddRequest: () => void; + onArchiveRequest: (id: string, name: string) => void; +}) { + // 空ステージでもドロップを受け取れるようにする(最後の 1 名を別ステージへ + // 動かした後の戻し先を保つため、ステージは常時表示)。 + // SortableContext id (= stage) は要素の並び替え用、useDroppable id は + // 「コンテナ自体」の drop ターゲット用。両者を分けることで衝突を避ける。 + const { setNodeRef, isOver } = useDroppable({ + id: `dropzone:${stage}`, + data: { containerId: stage }, + }); + + return ( +
+
+
+

+ {label} +

+ + {items.length} + +
+ +
+ i.id)} + strategy={verticalListSortingStrategy} + > +
    + {items.length === 0 ? ( + + ) : ( + items.map((cand) => ( + onArchiveRequest(cand.id, cand.name)} + > + + アーカイブ + + } + /> + )) + )} +
+
+
+ ); +} + +function ArchivedGroup({ + label, + items, + open, + onOpenChange, + selectedCandidateId, + onSelectCandidate, + onRestore, +}: { + label: string; + items: CandidateRow[]; + open: boolean; + onOpenChange: (open: boolean) => void; + selectedCandidateId: string; + onSelectCandidate: (id: string) => void; + onRestore: (id: string) => void; +}) { + return ( + + + } + > +
+

+ {label} +

+ + {items.length} + +
+
+ +
    + {items.map((cand) => ( + + ))} +
+
+
+ ); +} + +function ArchivedRowItem({ + cand, + selected, + onSelect, + onRestore, +}: { + cand: CandidateRow; + selected: boolean; + onSelect: (id: string) => void; + onRestore: (id: string) => void; +}) { + return ( +
  • + + + + + + } + /> + + + onRestore(cand.id)}> + + 復元 + + + + +
  • + ); +} + function ScoreBadge({ avg, selected, diff --git a/components/workspace/DeleteConfirmDialog.tsx b/components/workspace/DeleteConfirmDialog.tsx index aa2b8ff..073323b 100644 --- a/components/workspace/DeleteConfirmDialog.tsx +++ b/components/workspace/DeleteConfirmDialog.tsx @@ -17,6 +17,11 @@ type DeleteConfirmDialogProps = { title: string; itemName: string; onConfirm: () => void; + /** 既定は「『{itemName}』を削除します。この操作は取り消せません。」。 + * 論理削除(アーカイブ)など別文言が必要な呼び出し元のために上書きできる。 */ + description?: string; + /** 既定は「削除」。アーカイブ等で別ラベルにしたい場合に上書きする。 */ + actionLabel?: string; }; export function DeleteConfirmDialog({ @@ -25,6 +30,8 @@ export function DeleteConfirmDialog({ title, itemName, onConfirm, + description, + actionLabel = "削除", }: DeleteConfirmDialogProps) { return ( @@ -32,12 +39,12 @@ export function DeleteConfirmDialog({ {title} - 「{itemName}」を削除します。この操作は取り消せません。 + {description ?? `「${itemName}」を削除します。この操作は取り消せません。`} キャンセル - 削除 + {actionLabel} diff --git a/components/workspace/SortableCandidateRow.tsx b/components/workspace/SortableCandidateRow.tsx new file mode 100644 index 0000000..505a21f --- /dev/null +++ b/components/workspace/SortableCandidateRow.tsx @@ -0,0 +1,166 @@ +"use client"; + +import { type CSSProperties, type ReactNode } from "react"; +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; +import { GripVertical, MoreHorizontal, Star } from "lucide-react"; + +import { cn } from "@/lib/utils"; +import { type CandidateRow, type StageKey } from "@/lib/schema"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Button } from "@/components/ui/button"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; + +/** + * Pane 2 のステージグループ用、ドラッグ可能な候補者行。 + * + * - 行全体クリック = 候補者を選択(onSelect 経由) + * - 左端のグリップだけが drag listener を持つ。`MoreHorizontal` メニューや + * 行クリックとは衝突しない(`activationConstraint: distance` でも吸収済み) + * - DragOverlay 描画中は `isDragging` で半透明 + pointer-events 抑止 + * + * archived グループの行はドラッグさせないため、本コンポーネントは stage グループ + * 専用。archived は CandidateListPane 側の通常 `
  • ` で描画する。 + */ +export function SortableCandidateRow({ + cand, + stage, + selected, + onSelect, + actions, +}: { + cand: CandidateRow; + stage: StageKey; + selected: boolean; + onSelect: (id: string) => void; + actions: ReactNode; +}) { + const { + attributes, + listeners, + setNodeRef, + transform, + transition, + isDragging, + } = useSortable({ + id: cand.id, + data: { containerId: stage, name: cand.name }, + }); + + const style: CSSProperties = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
  • + + + + + + } + /> + + {actions} + + +
  • + ); +} + +function ScoreBadge({ + avg, + selected, +}: { + avg: number | null; + selected: boolean; +}) { + if (avg === null) { + return ( + + — + + ); + } + return ( + + + {avg.toFixed(1)} + + ); +} diff --git a/components/workspace/Workspace.tsx b/components/workspace/Workspace.tsx index 38e768e..d4929fe 100644 --- a/components/workspace/Workspace.tsx +++ b/components/workspace/Workspace.tsx @@ -57,7 +57,7 @@ import { createMinimalScorecard, } from "@/lib/data/factories"; import { getCandidateAverageScore } from "@/lib/computed/scorecards"; -import { STAGE_LABELS } from "@/lib/labels"; +import { ARCHIVED_GROUP_LABEL, STAGE_LABELS } from "@/lib/labels"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { GlobalHeader } from "@/components/workspace/GlobalHeader"; import { PositionPane } from "@/components/workspace/PositionPane"; @@ -186,6 +186,7 @@ export function Workspace({ profile: createMinimalProfile(name), scorecards: [], stage, + archived: false, }; setCandidates((prev) => [...prev, newCandidate]); setSelectedCandidateId(newId); @@ -194,17 +195,75 @@ export function Workspace({ setPane4ManuallyClosed(false); }, []); - const deleteCandidate = useCallback((id: string) => { + // 候補者をアーカイブ(論理削除)する。データは残し `archived: true` を立てる。 + // 復元は `restoreCandidate` から、もしくは Pane 2「アーカイブ済み」グループの + // 「復元」ボタン経由。アクティブ候補者をアーカイブした場合は、非 archived の + // 先頭候補者にフォールバックし、ステージ詳細(Pane 4)はクリアする。 + const archiveCandidate = useCallback((id: string) => { setCandidates((prev) => { - const next = prev.filter((c) => c.id !== id); + const next = prev.map((c) => + c.id === id ? { ...c, archived: true } : c, + ); setSelectedCandidateId((prevId) => { if (prevId !== id) return prevId; - return next.length > 0 ? next[0].id : ""; + const fallback = next.find((c) => !c.archived); + return fallback ? fallback.id : ""; }); return next; }); + setSelectedDetail(null); + setPane4ManuallyClosed(false); }, []); + // アーカイブ済み候補者を元のステージに復元する。`stage` は archived 中も保持 + // しているので、そのステージへ戻すだけでよい。 + const restoreCandidate = useCallback((id: string) => { + setCandidates((prev) => + prev.map((c) => (c.id === id ? { ...c, archived: false } : c)), + ); + }, []); + + // 候補者を別ステージへ移動 / 同ステージ内で並び替え。 + // + // `toStage` は移動先のステージキー。`toIndex` はそのステージグループ内での + // 0-origin の挿入位置。配列順を SSoT としているため、candidates 配列上の + // 絶対インデックスに変換して `splice` 相当の挿入を行う。 + // + // 同ステージ内ドラッグ・別ステージへのドラッグの両方をこの 1 関数で扱う。 + // archived 候補者は対象外(DnD はアクティブな候補者のみ可能)。 + const moveCandidate = useCallback( + (id: string, toStage: StageKey, toIndex: number) => { + setCandidates((prev) => { + const subjectIndex = prev.findIndex((c) => c.id === id); + if (subjectIndex < 0) return prev; + const subject = prev[subjectIndex]; + if (subject.archived) return prev; + + const without = prev.filter((_, i) => i !== subjectIndex); + const updated: Candidate = { ...subject, stage: toStage }; + + let count = 0; + let absInsertAt = without.length; + for (let i = 0; i < without.length; i++) { + const c = without[i]; + if (!c.archived && c.stage === toStage) { + if (count === toIndex) { + absInsertAt = i; + break; + } + count++; + } + } + return [ + ...without.slice(0, absInsertAt), + updated, + ...without.slice(absInsertAt), + ]; + }); + }, + [], + ); + const addDepartment = useCallback((name: string) => { setDepartments((prev) => [ ...prev, @@ -301,21 +360,37 @@ export function Workspace({ const positionTitle = "フロントエンドエンジニア"; const departmentTitle = "プロダクト開発"; - const candidateGroups: Group[] = useMemo( - () => - STAGE_ORDER.map((stage) => ({ - stage, - label: STAGE_LABELS[stage], - items: candidates - .filter((c) => c.stage === stage) - .map((c) => ({ - id: c.id, - name: c.profile.name, - averageScore: getCandidateAverageScore(c), - })), - })).filter((g) => g.items.length > 0), - [candidates], - ); + const candidateGroups: Group[] = useMemo(() => { + // ステージグループは常に 4 段階すべて表示する。空ステージも残すことで、 + // 「最後の 1 名を別ステージへ動かしたら戻し先が消える」事故を防ぐ + // (ADR-006 §2-2 の補足)。 + const stageGroups: Group[] = STAGE_ORDER.map((stage) => ({ + kind: "stage" as const, + stage, + label: STAGE_LABELS[stage], + items: candidates + .filter((c) => !c.archived && c.stage === stage) + .map((c) => ({ + id: c.id, + name: c.profile.name, + averageScore: getCandidateAverageScore(c), + })), + })); + + const archivedItems = candidates + .filter((c) => c.archived) + .map((c) => ({ + id: c.id, + name: c.profile.name, + averageScore: getCandidateAverageScore(c), + })); + + if (archivedItems.length === 0) return stageGroups; + return [ + ...stageGroups, + { kind: "archived" as const, label: ARCHIVED_GROUP_LABEL, items: archivedItems }, + ]; + }, [candidates]); return ( // shadcn/ui の SidebarProvider が外側を取り、Pane 1 (``) を全高で固定 @@ -352,7 +427,9 @@ export function Workspace({ selectedCandidateId={selectedCandidateId} onSelectCandidate={selectCandidate} onAddCandidate={addCandidate} - onDeleteCandidate={deleteCandidate} + onArchiveCandidate={archiveCandidate} + onRestoreCandidate={restoreCandidate} + onMoveCandidate={moveCandidate} /> = { final: "最終面接", }; +// Pane 2 末尾の「アーカイブ済み」グループの見出しラベル。 +// archived === true の候補者を束ねる仮想グループで、ステージとは直交した概念。 +export const ARCHIVED_GROUP_LABEL = "アーカイブ済み"; + // ===== Pane 3 ダッシュボードのセクション見出し(ADR-0014) ===== export const PANE3_SECTION = { diff --git a/lib/schema.ts b/lib/schema.ts index 933719f..3e00d52 100644 --- a/lib/schema.ts +++ b/lib/schema.ts @@ -164,6 +164,11 @@ export const candidateSchema = z.object({ profile: profileSchema, scorecards: z.array(scorecardSchema), stage: stageKeySchema, + // 論理削除フラグ。`stage` とは直交する。アーカイブされた候補者は通常のステージ + // グループから外れ、Pane 2 末尾の「アーカイブ済み」グループに表示される。 + // 復元時は `stage` をそのまま使って元のステージへ戻る。JSON シードでは省略可 + // (`.default(false)` で読み込み時に補完)。 + archived: z.boolean().default(false), }); export type Candidate = z.infer; @@ -203,14 +208,15 @@ export type CandidateRow = { averageScore: number | null; }; -// Pane 2 のグループ表示単位(ステージごとに候補者行を束ねる)。 +// Pane 2 のグループ表示単位(ステージ or アーカイブ済み)。 // 候補者データは `INITIAL_CANDIDATES` を SSoT とし、`candidateGroups` で // 派生計算して CandidateListPane に props で渡す(Workspace 内で計算)。 // -// stage: StageKey は「+ ボタン」から `addCandidate(stage)` を呼ぶときの -// 引数に使う(型安全に StageKey を渡すため)。日本語ラベルは label に分離。 -export type Group = { - stage: StageKey; - label: string; - items: CandidateRow[]; -}; +// `kind: "stage"` は通常の選考ステージグループ。`stage: StageKey` は +// 「+ ボタン」から `addCandidate(stage)` を呼ぶときの引数に使う。 +// `kind: "archived"` は archived === true の候補者を集めた末尾の仮想グループで、 +// 「+ 追加」操作は持たず、復元のみ可能。並び順は `kind: "stage"` を STAGE_ORDER で +// 並べた後、最後に `kind: "archived"` を 1 つだけ置く(要素があるときのみ表示)。 +export type Group = + | { kind: "stage"; stage: StageKey; label: string; items: CandidateRow[] } + | { kind: "archived"; label: string; items: CandidateRow[] }; diff --git a/package-lock.json b/package-lock.json index 099b8d1..bbe9822 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,9 @@ "version": "0.1.0", "dependencies": { "@base-ui/react": "^1.4.1", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -735,6 +738,59 @@ "integrity": "sha512-P5LUNhtbj6YfI3iJjw5EL9eUAG6OitD0W3fWQcpQjDRc/QIsL0tRNuO1PcDvPccWL1fSTXXdE1ds+l95DV/OFA==", "license": "MIT" }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@dotenvx/dotenvx": { "version": "1.65.0", "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.65.0.tgz", diff --git a/package.json b/package.json index 7b604c9..7cd8862 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,9 @@ }, "dependencies": { "@base-ui/react": "^1.4.1", + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1",