feat(ui): unified motion language and interaction states
- Shared transition constants (view/modal/toast/tile) with standard ease-out - MotionConfig reducedMotion=user plus CSS reduced-motion neutralizer - Consistent hover/press/focus-visible states across all interactive classes
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
} from "../lib/barcodeScanner";
|
||||
import { listBarcodeCatalog, upsertCloudUserBarcodeMapping } from "../lib/appwriteBarcodes";
|
||||
import { caffeinePerCan, currency, defaultPriceForSize, wholeNumber } from "../lib/metrics";
|
||||
import { MODAL_TRANSITION } from "../lib/motion";
|
||||
import {
|
||||
loadUserBarcodeMappings,
|
||||
upsertUserBarcodeMapping,
|
||||
@@ -311,7 +312,7 @@ export function BarcodeScannerModal({
|
||||
initial={{ opacity: 0, y: 18, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 14, scale: 0.98 }}
|
||||
transition={{ duration: 0.22 }}
|
||||
transition={MODAL_TRANSITION}
|
||||
>
|
||||
<div className="mb-5 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { AlertTriangle, Loader2, Trash2 } from "lucide-react";
|
||||
import { MODAL_TRANSITION } from "../lib/motion";
|
||||
import { Modal } from "./Modal";
|
||||
|
||||
type ConfirmDialogProps = {
|
||||
@@ -31,6 +32,7 @@ export function ConfirmDialog({
|
||||
initial={{ opacity: 0, y: 16, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 12, scale: 0.98 }}
|
||||
transition={MODAL_TRANSITION}
|
||||
>
|
||||
<h2 id="confirm-title" className="text-2xl font-semibold tracking-tight text-white">
|
||||
{title}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEffect, useMemo, useRef, useState, type CSSProperties, type FormEven
|
||||
import { MATERIAL_ACCENTS } from "../data/accents";
|
||||
import { BUILT_IN_FLAVOURS, DEFAULT_FLAVOUR, accentForCustomFlavour, flavourMeta } from "../data/flavours";
|
||||
import { caffeinePerCan, defaultPriceForSize, formatLocalInput, wholeNumber } from "../lib/metrics";
|
||||
import { MODAL_TRANSITION } from "../lib/motion";
|
||||
import { Modal } from "./Modal";
|
||||
import { Field } from "./ui";
|
||||
import type { EntryDraft, Flavour, RedBullEntry } from "../types";
|
||||
@@ -129,7 +130,7 @@ export function EntryModal({
|
||||
initial={{ opacity: 0, y: 18, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 14, scale: 0.98 }}
|
||||
transition={{ duration: 0.22 }}
|
||||
transition={MODAL_TRANSITION}
|
||||
>
|
||||
<div className="mb-6 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { motion } from "framer-motion";
|
||||
import { FileSpreadsheet, Loader2, X } from "lucide-react";
|
||||
import { humanDateTime } from "../lib/metrics";
|
||||
import { MODAL_TRANSITION } from "../lib/motion";
|
||||
import { MiniMetric } from "./MetricTiles";
|
||||
import { Modal } from "./Modal";
|
||||
import type { ImportPreview, ImportPreviewRow } from "../types";
|
||||
@@ -30,6 +31,7 @@ export function ImportPreviewModal({
|
||||
initial={{ opacity: 0, y: 18, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: 14, scale: 0.98 }}
|
||||
transition={MODAL_TRANSITION}
|
||||
>
|
||||
<div className="mb-5 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { motion } from "framer-motion";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { TILE_TRANSITION } from "../lib/motion";
|
||||
import { StatHint } from "./StatHint";
|
||||
import { Card } from "./ui";
|
||||
|
||||
@@ -16,7 +17,7 @@ export function MetricTile({ icon: Icon, label, value, detail, accent }: MetricT
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.22 }}
|
||||
transition={TILE_TRANSITION}
|
||||
>
|
||||
<Card className="metric-tile">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useEffect, useRef, type ReactNode, type RefObject } from "react";
|
||||
import { MODAL_TRANSITION } from "../lib/motion";
|
||||
|
||||
const FOCUSABLE_SELECTOR =
|
||||
'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
|
||||
@@ -116,6 +117,7 @@ export function Modal({
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={MODAL_TRANSITION}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={labelledBy}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { AlertTriangle, CheckCircle2, Info, X } from "lucide-react";
|
||||
import { useCallback, useMemo, useRef, useState, type ReactNode } from "react";
|
||||
import { TOAST_TRANSITION } from "../lib/motion";
|
||||
import { ToastContext, type ToastInput, type ToastTone } from "../lib/toasts";
|
||||
|
||||
const TOAST_DURATION_MS = 4500;
|
||||
@@ -55,7 +56,7 @@ export function ToastProvider({ children }: { children: ReactNode }) {
|
||||
initial={{ opacity: 0, y: -12, scale: 0.96 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
exit={{ opacity: 0, y: -8, scale: 0.96 }}
|
||||
transition={{ duration: 0.18 }}
|
||||
transition={TOAST_TRANSITION}
|
||||
>
|
||||
<ToneIcon className="toast-icon" size={18} aria-hidden="true" />
|
||||
<p className="toast-message">{toast.message}</p>
|
||||
|
||||
@@ -36,7 +36,7 @@ export function TopBar({ activeView, busyAction, onAdd, onScan, className = "" }
|
||||
|
||||
<div className="top-action-row">
|
||||
<button
|
||||
className="secondary-button top-action-button justify-center active:scale-95"
|
||||
className="secondary-button top-action-button justify-center"
|
||||
type="button"
|
||||
onClick={onScan}
|
||||
disabled={Boolean(busyAction)}
|
||||
@@ -46,7 +46,7 @@ export function TopBar({ activeView, busyAction, onAdd, onScan, className = "" }
|
||||
<span className="top-action-label">Scan</span>
|
||||
</button>
|
||||
<button
|
||||
className="primary-button top-action-button justify-center active:scale-95"
|
||||
className="primary-button top-action-button justify-center"
|
||||
type="button"
|
||||
onClick={onAdd}
|
||||
disabled={Boolean(busyAction)}
|
||||
|
||||
Reference in New Issue
Block a user