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

Includes changes from prior sessions: Epilogue + Space Mono fonts,
OverlayScrollbars integration, markdown editor fixes, settings dialog,
import/export buttons, and various UI refinements.
This commit is contained in:
Your Name
2026-02-16 14:56:36 +02:00
parent 8dedbf6032
commit 414c1f7d68
9 changed files with 343 additions and 205 deletions

View File

@@ -1,6 +1,6 @@
import { useState } from "react";
import { useState, useRef, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { springs, scaleIn, microInteraction } from "@/lib/motion";
import { springs, microInteraction } from "@/lib/motion";
import {
Sun, Moon, Monitor, RotateCcw,
} from "lucide-react";
@@ -94,45 +94,68 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
const setDensity = useAppStore((s) => s.setDensity);
const setDefaultColumnWidth = useAppStore((s) => s.setDefaultColumnWidth);
const roRef = useRef<ResizeObserver | null>(null);
const [height, setHeight] = useState<number | "auto">("auto");
// Callback ref: sets up ResizeObserver when dialog content mounts in portal
const contentRef = useCallback((node: HTMLDivElement | null) => {
if (roRef.current) {
roRef.current.disconnect();
roRef.current = null;
}
if (node) {
const measure = () => setHeight(node.getBoundingClientRect().height);
measure();
roRef.current = new ResizeObserver(measure);
roRef.current.observe(node);
}
}, []);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="bg-pylon-surface sm:max-w-lg">
<DialogHeader>
<DialogTitle className="font-heading text-pylon-text">
Settings
</DialogTitle>
<DialogDescription className="text-pylon-text-secondary">
Configure your OpenPylon preferences.
</DialogDescription>
</DialogHeader>
<DialogContent className="bg-pylon-surface sm:max-w-lg overflow-hidden p-0">
<motion.div
animate={{ height: typeof height === "number" && height > 0 ? height : "auto" }}
initial={false}
transition={{ type: "spring", stiffness: 500, damping: 30 }}
className="overflow-hidden"
>
<div ref={contentRef} className="flex flex-col gap-4 p-6">
<DialogHeader>
<DialogTitle className="font-heading text-pylon-text">
Settings
</DialogTitle>
<DialogDescription className="text-pylon-text-secondary">
Configure your OpenPylon preferences.
</DialogDescription>
</DialogHeader>
{/* Tab bar */}
<div className="flex gap-1 border-b border-border pb-2">
{TABS.map((t) => (
<Button
key={t.value}
variant={tab === t.value ? "secondary" : "ghost"}
size="sm"
onClick={() => setTab(t.value)}
className="font-mono text-xs"
>
{t.label}
</Button>
))}
</div>
{/* Tab bar */}
<div className="flex gap-1 border-b border-border pb-2">
{TABS.map((t) => (
<Button
key={t.value}
variant={tab === t.value ? "secondary" : "ghost"}
size="sm"
onClick={() => setTab(t.value)}
className="font-mono text-xs"
>
{t.label}
</Button>
))}
</div>
{/* Tab content */}
<AnimatePresence mode="wait">
<motion.div
key={tab}
className="flex flex-col gap-5 pt-1"
variants={scaleIn}
initial="hidden"
animate="visible"
exit="exit"
transition={springs.snappy}
>
{tab === "appearance" && (
{/* Tab content — entire dialog height animates between tabs */}
<AnimatePresence mode="popLayout" initial={false}>
<motion.div
key={tab}
className="flex flex-col gap-5"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
>
{tab === "appearance" && (
<>
{/* Theme */}
<div>
@@ -292,8 +315,10 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
</p>
</div>
)}
</motion.div>
</AnimatePresence>
</motion.div>
</AnimatePresence>
</div>
</motion.div>
</DialogContent>
</Dialog>
);