typography overhaul, custom scrollbars, import/export, settings UI

This commit is contained in:
2026-02-16 14:56:36 +02:00
parent 8d26af0b0e
commit 98226775ed
9 changed files with 343 additions and 205 deletions

View File

@@ -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} />
</>