feat: add micro-animations to TopBar, toasts, settings, and shortcut help
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
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 { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -199,11 +201,20 @@ export function TopBar() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
)}
|
||||
{savingStatus && (
|
||||
<span className="mr-2 font-mono text-xs text-pylon-text-secondary">
|
||||
{savingStatus}
|
||||
</span>
|
||||
)}
|
||||
<AnimatePresence mode="wait">
|
||||
{savingStatus && (
|
||||
<motion.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>
|
||||
<TooltipTrigger asChild>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { springs, scaleIn, microInteraction } from "@/lib/motion";
|
||||
import {
|
||||
Sun, Moon, Monitor, RotateCcw,
|
||||
} from "lucide-react";
|
||||
@@ -120,7 +122,16 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
||||
</div>
|
||||
|
||||
{/* 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" && (
|
||||
<>
|
||||
{/* Theme */}
|
||||
@@ -193,16 +204,19 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
||||
? "oklch(50% 0 0)"
|
||||
: `oklch(55% 0.12 ${hue})`;
|
||||
return (
|
||||
<button
|
||||
<motion.button
|
||||
key={hue}
|
||||
type="button"
|
||||
onClick={() => setAccentColor(hue)}
|
||||
className="size-7 rounded-full transition-transform hover:scale-110"
|
||||
className="size-7 rounded-full"
|
||||
style={{
|
||||
backgroundColor: bg,
|
||||
outline: settings.accentColor === hue ? "2px solid currentColor" : "none",
|
||||
outlineOffset: "2px",
|
||||
}}
|
||||
whileHover={microInteraction.hover}
|
||||
whileTap={microInteraction.tap}
|
||||
transition={springs.snappy}
|
||||
aria-label={label}
|
||||
title={label}
|
||||
/>
|
||||
@@ -278,7 +292,8 @@ export function SettingsDialog({ open, onOpenChange }: SettingsDialogProps) {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { springs, staggerContainer, fadeSlideUp } from "@/lib/motion";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -42,9 +44,14 @@ export function ShortcutHelpModal({ open, onOpenChange }: ShortcutHelpModalProps
|
||||
</DialogDescription>
|
||||
</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) => (
|
||||
<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">
|
||||
{group.category}
|
||||
</h4>
|
||||
@@ -58,9 +65,9 @@ export function ShortcutHelpModal({ open, onOpenChange }: ShortcutHelpModalProps
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
))}
|
||||
</div>
|
||||
</motion.div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { springs } from "@/lib/motion";
|
||||
import { useToastStore } from "@/stores/toast-store";
|
||||
|
||||
const TYPE_STYLES = {
|
||||
@@ -18,8 +19,8 @@ export function ToastContainer() {
|
||||
key={toast.id}
|
||||
initial={{ opacity: 0, y: 20, scale: 0.95 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 10, scale: 0.95 }}
|
||||
transition={{ type: "spring", stiffness: 400, damping: 25 }}
|
||||
exit={{ opacity: 0, y: 20, scale: 0.9 }}
|
||||
transition={springs.wobbly}
|
||||
className={`pointer-events-auto rounded-lg border px-4 py-2 text-sm shadow-md ${TYPE_STYLES[toast.type]}`}
|
||||
>
|
||||
{toast.message}
|
||||
|
||||
Reference in New Issue
Block a user