typography overhaul, custom scrollbars, import/export, settings UI
This commit is contained in:
@@ -1,16 +1,63 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Plus, ArrowUpDown } from "lucide-react";
|
||||
import { motion } from "framer-motion";
|
||||
import { OverlayScrollbarsComponent } from "overlayscrollbars-react";
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
closestCenter,
|
||||
PointerSensor,
|
||||
KeyboardSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
type DragStartEvent,
|
||||
type DragEndEvent,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
SortableContext,
|
||||
rectSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { staggerContainer, scaleIn, springs } from "@/lib/motion";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useAppStore } from "@/stores/app-store";
|
||||
import { BoardCard } from "@/components/boards/BoardCard";
|
||||
import { BoardCardOverlay } from "@/components/boards/BoardCardOverlay";
|
||||
import { NewBoardDialog } from "@/components/boards/NewBoardDialog";
|
||||
import { ImportExportButtons } from "@/components/import-export/ImportExportButtons";
|
||||
import { ImportButton } from "@/components/import-export/ImportExportButtons";
|
||||
import type { BoardSortOrder } from "@/types/settings";
|
||||
|
||||
const SORT_LABELS: Record<BoardSortOrder, string> = {
|
||||
manual: "Manual",
|
||||
title: "Name",
|
||||
updated: "Last modified",
|
||||
created: "Date created",
|
||||
};
|
||||
|
||||
export function BoardList() {
|
||||
const boards = useAppStore((s) => s.boards);
|
||||
const sortOrder = useAppStore((s) => s.settings.boardSortOrder);
|
||||
const setSortOrder = useAppStore((s) => s.setBoardSortOrder);
|
||||
const setBoardManualOrder = useAppStore((s) => s.setBoardManualOrder);
|
||||
const getSortedBoards = useAppStore((s) => s.getSortedBoards);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [activeBoardId, setActiveBoardId] = useState<string | null>(null);
|
||||
|
||||
const sortedBoards = getSortedBoards();
|
||||
const isManual = sortOrder === "manual";
|
||||
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor, {
|
||||
activationConstraint: { distance: 5 },
|
||||
}),
|
||||
useSensor(KeyboardSensor)
|
||||
);
|
||||
|
||||
// Listen for custom event to open new board dialog from command palette
|
||||
useEffect(() => {
|
||||
@@ -23,6 +70,33 @@ export function BoardList() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const handleDragStart = useCallback((event: DragStartEvent) => {
|
||||
setActiveBoardId(event.active.id as string);
|
||||
}, []);
|
||||
|
||||
const handleDragEnd = useCallback(
|
||||
(event: DragEndEvent) => {
|
||||
setActiveBoardId(null);
|
||||
const { active, over } = event;
|
||||
if (!over || active.id === over.id) return;
|
||||
|
||||
const currentOrder = useAppStore.getState().getSortedBoards().map((b) => b.id);
|
||||
const oldIndex = currentOrder.indexOf(active.id as string);
|
||||
const newIndex = currentOrder.indexOf(over.id as string);
|
||||
if (oldIndex === -1 || newIndex === -1) return;
|
||||
|
||||
const newOrder = [...currentOrder];
|
||||
newOrder.splice(oldIndex, 1);
|
||||
newOrder.splice(newIndex, 0, active.id as string);
|
||||
setBoardManualOrder(newOrder);
|
||||
},
|
||||
[setBoardManualOrder]
|
||||
);
|
||||
|
||||
const handleDragCancel = useCallback(() => {
|
||||
setActiveBoardId(null);
|
||||
}, []);
|
||||
|
||||
if (boards.length === 0) {
|
||||
return (
|
||||
<>
|
||||
@@ -47,7 +121,7 @@ export function BoardList() {
|
||||
<Plus className="size-4" />
|
||||
Create Board
|
||||
</Button>
|
||||
<ImportExportButtons />
|
||||
<ImportButton />
|
||||
</div>
|
||||
</motion.div>
|
||||
<NewBoardDialog open={dialogOpen} onOpenChange={setDialogOpen} />
|
||||
@@ -55,16 +129,51 @@ export function BoardList() {
|
||||
);
|
||||
}
|
||||
|
||||
const activeBoard = activeBoardId
|
||||
? sortedBoards.find((b) => b.id === activeBoardId)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-full overflow-y-auto p-6">
|
||||
<OverlayScrollbarsComponent
|
||||
className="h-full"
|
||||
options={{ scrollbars: { theme: "os-theme-pylon", autoHide: "scroll", autoHideDelay: 600, clickScroll: true } }}
|
||||
defer
|
||||
>
|
||||
<div className="p-6">
|
||||
{/* Heading row */}
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<h2 className="font-mono text-xs font-semibold uppercase tracking-widest text-pylon-text-secondary">
|
||||
Your Boards
|
||||
</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<ImportExportButtons />
|
||||
{/* Sort dropdown */}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-pylon-text-secondary hover:text-pylon-text"
|
||||
>
|
||||
<ArrowUpDown className="size-3.5" />
|
||||
<span className="font-mono text-xs">{SORT_LABELS[sortOrder]}</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuRadioGroup
|
||||
value={sortOrder}
|
||||
onValueChange={(v) => setSortOrder(v as BoardSortOrder)}
|
||||
>
|
||||
{(Object.keys(SORT_LABELS) as BoardSortOrder[]).map((key) => (
|
||||
<DropdownMenuRadioItem key={key} value={key}>
|
||||
{SORT_LABELS[key]}
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<ImportButton />
|
||||
<Button size="sm" onClick={() => setDialogOpen(true)}>
|
||||
<Plus className="size-4" />
|
||||
New
|
||||
@@ -73,17 +182,35 @@ export function BoardList() {
|
||||
</div>
|
||||
|
||||
{/* Board grid */}
|
||||
<motion.div
|
||||
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||
variants={staggerContainer(0.05)}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={closestCenter}
|
||||
onDragStart={handleDragStart}
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragCancel={handleDragCancel}
|
||||
>
|
||||
{boards.map((board) => (
|
||||
<BoardCard key={board.id} board={board} />
|
||||
))}
|
||||
</motion.div>
|
||||
<SortableContext
|
||||
items={sortedBoards.map((b) => b.id)}
|
||||
strategy={rectSortingStrategy}
|
||||
>
|
||||
<motion.div
|
||||
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"
|
||||
variants={staggerContainer(0.05)}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
{sortedBoards.map((board) => (
|
||||
<BoardCard key={board.id} board={board} sortable={isManual} />
|
||||
))}
|
||||
</motion.div>
|
||||
</SortableContext>
|
||||
|
||||
<DragOverlay dropAnimation={null}>
|
||||
{activeBoard ? <BoardCardOverlay board={activeBoard} /> : null}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</div>
|
||||
</OverlayScrollbarsComponent>
|
||||
|
||||
<NewBoardDialog open={dialogOpen} onOpenChange={setDialogOpen} />
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user