feat: add micro-animations to TopBar, toasts, settings, and shortcut help

This commit is contained in:
Your Name
2026-02-15 21:01:10 +02:00
parent 24219bb212
commit 85c54a3768
4 changed files with 49 additions and 15 deletions

View File

@@ -1,4 +1,6 @@
import { useState, useRef, useEffect, useCallback } from "react"; import { useState, useRef, useEffect, useCallback } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { springs } from "@/lib/motion";
import { ArrowLeft, Settings, Search, Undo2, Redo2, SlidersHorizontal } from "lucide-react"; import { ArrowLeft, Settings, Search, Undo2, Redo2, SlidersHorizontal } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@@ -199,11 +201,20 @@ export function TopBar() {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}
{savingStatus && ( <AnimatePresence mode="wait">
<span className="mr-2 font-mono text-xs text-pylon-text-secondary"> {savingStatus && (
{savingStatus} <motion.span
</span> key={savingStatus}
)} className="mr-2 font-mono text-xs text-pylon-text-secondary"
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 4 }}
transition={springs.snappy}
>
{savingStatus}
</motion.span>
)}
</AnimatePresence>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>

View File

@@ -1,4 +1,6 @@
import { useState } from "react"; import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { springs, scaleIn, microInteraction } from "@/lib/motion";
import { import {
Sun, Moon, Monitor, RotateCcw, Sun, Moon, Monitor, RotateCcw,
} from "lucide-react"; } from "lucide-react";
@@ -120,7 +122,16 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
</div> </div>
{/* Tab content */} {/* Tab content */}
<div className="flex flex-col gap-5 pt-1"> <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 === "appearance" && (
<> <>
{/* Theme */} {/* Theme */}
@@ -193,16 +204,19 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
? "oklch(50% 0 0)" ? "oklch(50% 0 0)"
: `oklch(55% 0.12 ${hue})`; : `oklch(55% 0.12 ${hue})`;
return ( return (
<button <motion.button
key={hue} key={hue}
type="button" type="button"
onClick={() => setAccentColor(hue)} onClick={() => setAccentColor(hue)}
className="size-7 rounded-full transition-transform hover:scale-110" className="size-7 rounded-full"
style={{ style={{
backgroundColor: bg, backgroundColor: bg,
outline: settings.accentColor === hue ? "2px solid currentColor" : "none", outline: settings.accentColor === hue ? "2px solid currentColor" : "none",
outlineOffset: "2px", outlineOffset: "2px",
}} }}
whileHover={microInteraction.hover}
whileTap={microInteraction.tap}
transition={springs.snappy}
aria-label={label} aria-label={label}
title={label} title={label}
/> />
@@ -278,7 +292,8 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
</p> </p>
</div> </div>
)} )}
</div> </motion.div>
</AnimatePresence>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@@ -1,3 +1,5 @@
import { motion } from "framer-motion";
import { springs, staggerContainer, fadeSlideUp } from "@/lib/motion";
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@@ -42,9 +44,14 @@ export function ShortcutHelpModal({ open, onOpenChange }: ShortcutHelpModalProps
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex flex-col gap-4"> <motion.div
className="flex flex-col gap-4"
variants={staggerContainer(0.06)}
initial="hidden"
animate="visible"
>
{SHORTCUT_GROUPS.map((group) => ( {SHORTCUT_GROUPS.map((group) => (
<div key={group.category}> <motion.div key={group.category} variants={fadeSlideUp} transition={springs.bouncy}>
<h4 className="mb-2 font-mono text-xs font-semibold uppercase tracking-widest text-pylon-text-secondary"> <h4 className="mb-2 font-mono text-xs font-semibold uppercase tracking-widest text-pylon-text-secondary">
{group.category} {group.category}
</h4> </h4>
@@ -58,9 +65,9 @@ export function ShortcutHelpModal({ open, onOpenChange }: ShortcutHelpModalProps
</div> </div>
))} ))}
</div> </div>
</div> </motion.div>
))} ))}
</div> </motion.div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@@ -1,4 +1,5 @@
import { AnimatePresence, motion } from "framer-motion"; import { AnimatePresence, motion } from "framer-motion";
import { springs } from "@/lib/motion";
import { useToastStore } from "@/stores/toast-store"; import { useToastStore } from "@/stores/toast-store";
const TYPE_STYLES = { const TYPE_STYLES = {
@@ -18,8 +19,8 @@ export function ToastContainer() {
key={toast.id} key={toast.id}
initial={{ opacity: 0, y: 20, scale: 0.95 }} initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }} animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 10, scale: 0.95 }} exit={{ opacity: 0, y: 20, scale: 0.9 }}
transition={{ type: "spring", stiffness: 400, damping: 25 }} transition={springs.wobbly}
className={`pointer-events-auto rounded-lg border px-4 py-2 text-sm shadow-md ${TYPE_STYLES[toast.type]}`} className={`pointer-events-auto rounded-lg border px-4 py-2 text-sm shadow-md ${TYPE_STYLES[toast.type]}`}
> >
{toast.message} {toast.message}