refactor(landing): migrate motion.* to m.* for LazyMotion

Run codemod to convert 40 files from motion.div to m.div components.
This enables proper deferred feature loading with LazyMotion.

- All motion.* JSX → m.* (div, button, section, header, footer, etc.)
- All imports: { motion } → { m }
- MotionProvider now uses LazyMotion with domAnimation features

Bundle impact: framer-motion vendor 338KB → 296KB (-42KB)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Quinn Ftw 2025-12-29 22:44:53 -08:00
parent 5bb0e69fb7
commit 241cce2ea6
41 changed files with 437 additions and 440 deletions

View file

@ -6,7 +6,7 @@
*/
import { Grid } from '@ui/ui'
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { useRef } from 'react'
import styled from 'styled-components'
@ -30,7 +30,7 @@ export interface BenefitsSectionProps {
pageType: string
}
const Section = styled(motion.section)`
const Section = styled(m.section)`
position: relative;
z-index: 1;
max-width: 1200px;
@ -38,7 +38,7 @@ const Section = styled(motion.section)`
padding: 2rem 2rem 4rem;
`
const Header = styled(motion.div)`
const Header = styled(m.div)`
text-align: center;
margin-bottom: 3rem;
`
@ -58,7 +58,7 @@ const Divider = styled.div<{ $gradientFrom: string; $gradientTo: string }>`
border-radius: 2px;
`
const Card = styled(motion.div)<{ $color: string }>`
const Card = styled(m.div)<{ $color: string }>`
position: relative;
background: rgba(255, 255, 255, 0.03);
backdrop-filter: blur(12px);
@ -120,7 +120,7 @@ const VersionBadgeWrapper = styled.div`
z-index: 10;
`
const IconWrapper = styled(motion.span)`
const IconWrapper = styled(m.span)`
font-size: 2.5rem;
display: block;
margin-bottom: 1rem;

View file

@ -7,7 +7,7 @@
import { Grid } from '@ui/ui'
import { ChevronRight } from 'lucide-react'
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { useRef } from 'react'
import styled from 'styled-components'
@ -29,7 +29,7 @@ export interface FeaturesSectionProps {
pageType: string
}
const Section = styled(motion.section)`
const Section = styled(m.section)`
position: relative;
z-index: 1;
max-width: 1000px;
@ -37,7 +37,7 @@ const Section = styled(motion.section)`
padding: 2rem 2rem 4rem;
`
const Header = styled(motion.div)`
const Header = styled(m.div)`
text-align: center;
margin-bottom: 3rem;
`
@ -57,7 +57,7 @@ const Divider = styled.div<{ $color: string }>`
border-radius: 2px;
`
const Block = styled(motion.div)`
const Block = styled(m.div)`
position: relative;
background: rgba(255, 255, 255, 0.02);
backdrop-filter: blur(8px);
@ -109,7 +109,7 @@ const List = styled.ul`
gap: 0.75rem;
`
const Item = styled(motion.li)<{ $color: string }>`
const Item = styled(m.li)<{ $color: string }>`
display: flex;
align-items: flex-start;
gap: 0.75rem;

View file

@ -5,7 +5,7 @@
* Uses @ui/layout patterns for consistent styling.
*/
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import styled from 'styled-components'
import { useReducedMotion } from '@ui/accessibility'
@ -27,7 +27,7 @@ export interface StatCardProps {
delay?: number
}
const Card = styled(motion.div)`
const Card = styled(m.div)`
display: flex;
flex-direction: column;
align-items: center;

View file

@ -6,7 +6,7 @@
*/
import { Grid } from '@ui/ui'
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { useRef } from 'react'
import styled from 'styled-components'
@ -35,7 +35,7 @@ const Section = styled.section`
padding: 2rem;
`
const Container = styled(motion.div)<{ $gradientFrom: string; $gradientTo: string; $color: string }>`
const Container = styled(m.div)<{ $gradientFrom: string; $gradientTo: string; $color: string }>`
position: relative;
overflow: hidden;
border-radius: 24px;
@ -69,7 +69,7 @@ const Orbs = styled.div`
overflow: hidden;
`
const Orb = styled(motion.div)<{ $color: string; $size: number; $position: 'top-left' | 'bottom-right' }>`
const Orb = styled(m.div)<{ $color: string; $size: number; $position: 'top-left' | 'bottom-right' }>`
position: absolute;
width: ${props => props.$size}px;
height: ${props => props.$size}px;

View file

@ -5,7 +5,7 @@
* Uses dotted underline styling per accessibility conventions.
*/
import { motion, AnimatePresence } from 'framer-motion'
import { m, AnimatePresence } from 'framer-motion'
import { useState, useRef, useEffect } from 'react'
import { createPortal } from 'react-dom'
import './Acronym.css'
@ -159,7 +159,7 @@ export default function Acronym({ children, definition, delay = 300 }: AcronymPr
<AnimatePresence>
{isVisible &&
createPortal(
<motion.div
<m.div
id={`acronym-tooltip-${acronymText}`}
className="acronym-tooltip"
role="tooltip"
@ -175,7 +175,7 @@ export default function Acronym({ children, definition, delay = 300 }: AcronymPr
<span className="acronym-tooltip-term">{acronymText}</span>
<span className="acronym-tooltip-definition">{tooltipContent}</span>
<div className="acronym-tooltip-arrow" />
</motion.div>,
</m.div>,
document.body
)}
</AnimatePresence>

View file

@ -5,7 +5,7 @@
* and newsletter signup. Renders as an overlay on any page.
*/
import { motion, AnimatePresence } from 'framer-motion'
import { m, AnimatePresence } from 'framer-motion'
import { X, Check, MessageCircle, ArrowRight, Mail, Lock, User, Building2, MessageSquare } from 'lucide-react'
import { useEffect, useRef, useState, useMemo } from 'react'
import { Link } from 'react-router-dom'
@ -206,7 +206,7 @@ function SuccessState({
const playSound = useSoundEngine()
return (
<motion.div
<m.div
className="cta-success"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
@ -249,7 +249,7 @@ function SuccessState({
Close
</button>
)}
</motion.div>
</m.div>
)
}
@ -281,7 +281,7 @@ function FeatureWaitlistStep({ onComplete }: { onComplete: () => void }) {
}
return (
<motion.div
<m.div
className="cta-features"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
@ -300,7 +300,7 @@ function FeatureWaitlistStep({ onComplete }: { onComplete: () => void }) {
const isSelected = selectedFeatures.includes(feature.id)
return (
<motion.button
<m.button
key={feature.id}
type="button"
className={`cta-feature-card ${isSelected ? 'selected' : ''}`}
@ -321,7 +321,7 @@ function FeatureWaitlistStep({ onComplete }: { onComplete: () => void }) {
<Check size={16} />
</div>
)}
</motion.button>
</m.button>
)
})}
</div>
@ -345,7 +345,7 @@ function FeatureWaitlistStep({ onComplete }: { onComplete: () => void }) {
Save Preferences ({selectedFeatures.length})
</button>
</div>
</motion.div>
</m.div>
)
}
@ -515,14 +515,14 @@ export default function CTAModal({ context, onClose }: CTAModalProps) {
return (
<AnimatePresence>
<motion.div
<m.div
className="cta-modal-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={handleOverlayClick}
>
<motion.div
<m.div
ref={modalRef}
className="cta-modal-container"
style={themeVars}
@ -641,7 +641,7 @@ export default function CTAModal({ context, onClose }: CTAModalProps) {
{/* Final success after features */}
{showFinalSuccess && (
<motion.div
<m.div
className="cta-success"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
@ -663,10 +663,10 @@ export default function CTAModal({ context, onClose }: CTAModalProps) {
<MessageCircle size={20} />
Join Our Discord Community
</a>
</motion.div>
</m.div>
)}
</motion.div>
</motion.div>
</m.div>
</m.div>
</AnimatePresence>
)
}

View file

@ -1,4 +1,4 @@
import { motion, AnimatePresence } from 'framer-motion'
import { m, AnimatePresence } from 'framer-motion'
import { X, ShoppingCart, Minus, Plus, Trash2, Sparkles, ArrowRight } from 'lucide-react'
import { useEffect, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
@ -88,7 +88,7 @@ export default function CartDrawer() {
{isOpen && (
<>
{/* Backdrop */}
<motion.div
<m.div
className="cart-drawer-backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@ -97,7 +97,7 @@ export default function CartDrawer() {
/>
{/* Drawer */}
<motion.div
<m.div
ref={drawerRef}
className="cart-drawer"
initial={{ x: '100%' }}
@ -142,7 +142,7 @@ export default function CartDrawer() {
) : (
<div className="cart-items">
{items.map((item) => (
<motion.div
<m.div
key={item.cartKey}
className="cart-item"
initial={{ opacity: 0, x: 20 }}
@ -214,7 +214,7 @@ export default function CartDrawer() {
<Trash2 size={16} />
</button>
</div>
</motion.div>
</m.div>
))}
</div>
)}
@ -243,7 +243,7 @@ export default function CartDrawer() {
>
Clear Cart
</button>
<motion.button
<m.button
className="checkout-button"
onClick={handleCheckout}
onMouseEnter={() => playSound('button-hover')}
@ -252,11 +252,11 @@ export default function CartDrawer() {
>
Checkout
<ArrowRight size={18} />
</motion.button>
</m.button>
</div>
</div>
)}
</motion.div>
</m.div>
</>
)}
</AnimatePresence>

View file

@ -8,7 +8,7 @@
* - Click-outside detection to collapse
*/
import { motion, AnimatePresence } from 'framer-motion'
import { m, AnimatePresence } from 'framer-motion'
import { Globe, X } from 'lucide-react'
import { useState, useEffect, useRef } from 'react'
@ -130,7 +130,7 @@ export default function FABLanguageSelector({ onLanguageChange }: FABLanguageSel
{/* Backdrop blur when expanded */}
<AnimatePresence>
{isExpanded && (
<motion.div
<m.div
className="fab-language-backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@ -143,7 +143,7 @@ export default function FABLanguageSelector({ onLanguageChange }: FABLanguageSel
{/* Language options (expand left when FAB clicked) */}
<AnimatePresence>
{isExpanded && (
<motion.div
<m.div
className="fab-language-options"
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
@ -157,7 +157,7 @@ export default function FABLanguageSelector({ onLanguageChange }: FABLanguageSel
const isActive = currentLanguage === langCode
return (
<motion.button
<m.button
key={langCode}
className={`fab-language-option ${isActive ? 'active' : ''}`}
onClick={() => handleLanguageSelect(langCode)}
@ -178,15 +178,15 @@ export default function FABLanguageSelector({ onLanguageChange }: FABLanguageSel
>
<span className="flag-emoji">{getFlagEmoji(langCode)}</span>
<span className="lang-code">{langCode.toUpperCase()}</span>
</motion.button>
</m.button>
)
})}
</motion.div>
</m.div>
)}
</AnimatePresence>
{/* Main FAB (always visible) */}
<motion.button
<m.button
className="fab-language-button"
onClick={toggleExpanded}
whileHover={{ scale: 1.05 }}
@ -196,7 +196,7 @@ export default function FABLanguageSelector({ onLanguageChange }: FABLanguageSel
title={`Language: ${currentLangInfo?.nativeName || 'English'}`}
data-testid="fab-language-button"
>
<motion.div
<m.div
className="fab-language-content"
animate={{ rotate: isExpanded ? 180 : 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
@ -209,8 +209,8 @@ export default function FABLanguageSelector({ onLanguageChange }: FABLanguageSel
<Globe size={14} className="globe-icon" />
</>
)}
</motion.div>
</motion.button>
</m.div>
</m.button>
</div>
)
}

View file

@ -4,7 +4,7 @@
* Each category button expands horizontally LEFT to show options
*/
import { motion, AnimatePresence } from 'framer-motion'
import { m, AnimatePresence } from 'framer-motion'
import {
Settings,
Volume2,
@ -261,7 +261,7 @@ export default function FloatingSettings({
{/* Backdrop blur when expanded */}
<AnimatePresence>
{isExpanded && (
<motion.div
<m.div
className="floating-settings-backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@ -281,14 +281,14 @@ export default function FloatingSettings({
{/* Particle Options (expand left) */}
<AnimatePresence>
{expandedCategory === 'particles' && (
<motion.div
<m.div
className="category-options"
initial={{ opacity: 0, y: -76 }}
animate={{ opacity: 1, y: -76 }}
exit={{ opacity: 0 }}
>
{PARTICLE_OPTIONS.map((option, index) => (
<motion.button
<m.button
key={option.id}
className={`option-button particle-option ${option.id} ${particleStyle === option.id ? 'active' : ''}`}
onClick={() => handleParticleSelect(option.id)}
@ -308,14 +308,14 @@ export default function FloatingSettings({
data-testid={`particle-style-${option.id}`}
>
{option.icon}
</motion.button>
</m.button>
))}
</motion.div>
</m.div>
)}
</AnimatePresence>
{/* Particle Category Button */}
<motion.button
<m.button
className={`floating-action-button category-button particle-button ${particleStyle}`}
onClick={() => handleCategoryClick('particles')}
initial={{ scale: 0.5, opacity: 0, y: 0 }}
@ -334,7 +334,7 @@ export default function FloatingSettings({
title={`Trails: ${getParticleStyleName()}`}
>
{expandedCategory === 'particles' ? <Sparkles size={20} /> : getSelectedParticleIcon()}
</motion.button>
</m.button>
</div>
{/* Sound Category Button + Options */}
@ -342,14 +342,14 @@ export default function FloatingSettings({
{/* Sound Options (expand left) */}
<AnimatePresence>
{expandedCategory === 'sound' && (
<motion.div
<m.div
className="category-options"
initial={{ opacity: 0, y: -144 }}
animate={{ opacity: 1, y: -144 }}
exit={{ opacity: 0 }}
>
{SOUND_OPTIONS.map((option, index) => (
<motion.button
<m.button
key={option.id}
className={`option-button sound-option ${option.id} ${(option.id === 'off' && !enabled) || (enabled && currentPack === option.id) ? 'active' : ''}`}
onClick={() => handleSoundSelect(option.id)}
@ -369,14 +369,14 @@ export default function FloatingSettings({
>
{option.icon}
<span className="option-label">{option.name}</span>
</motion.button>
</m.button>
))}
</motion.div>
</m.div>
)}
</AnimatePresence>
{/* Sound Category Button */}
<motion.button
<m.button
className={`floating-action-button category-button sound-button ${enabled ? currentPack : 'disabled'}`}
onClick={() => handleCategoryClick('sound')}
initial={{ scale: 0.5, opacity: 0, y: 0 }}
@ -402,7 +402,7 @@ export default function FloatingSettings({
<span className="option-label">{getCurrentSoundOption()}</span>
</>
)}
</motion.button>
</m.button>
</div>
{/* Volume Category Button + Options */}
@ -410,14 +410,14 @@ export default function FloatingSettings({
{/* Volume Options (expand left) */}
<AnimatePresence>
{expandedCategory === 'volume' && (
<motion.div
<m.div
className="category-options"
initial={{ opacity: 0, y: -212 }}
animate={{ opacity: 1, y: -212 }}
exit={{ opacity: 0 }}
>
{VOLUME_OPTIONS.map((option, index) => (
<motion.button
<m.button
key={option.id}
className={`option-button volume-option ${volumeLevel === option.id ? 'active' : ''}`}
onClick={() => handleVolumeSelect(option.id)}
@ -437,14 +437,14 @@ export default function FloatingSettings({
>
{option.icon}
<span className="option-label">{option.name}</span>
</motion.button>
</m.button>
))}
</motion.div>
</m.div>
)}
</AnimatePresence>
{/* Volume Category Button */}
<motion.button
<m.button
className={`floating-action-button category-button volume-button ${volumeLevel === 0 ? 'muted' : ''}`}
onClick={() => handleCategoryClick('volume')}
initial={{ scale: 0.5, opacity: 0, y: 0 }}
@ -470,7 +470,7 @@ export default function FloatingSettings({
<span className="option-label">{getVolumeLevelName()}</span>
</>
)}
</motion.button>
</m.button>
</div>
{/* Triggers Category Button + Options */}
@ -478,14 +478,14 @@ export default function FloatingSettings({
{/* Trigger Options (expand left) */}
<AnimatePresence>
{expandedCategory === 'triggers' && (
<motion.div
<m.div
className="category-options"
initial={{ opacity: 0, y: -280 }}
animate={{ opacity: 1, y: -280 }}
exit={{ opacity: 0 }}
>
{TRIGGER_OPTIONS.map((option, index) => (
<motion.button
<m.button
key={option.id}
className={`option-button trigger-option ${option.id} ${triggerMode === option.id ? 'active' : ''}`}
onClick={() => handleTriggerSelect(option.id)}
@ -505,14 +505,14 @@ export default function FloatingSettings({
>
{option.icon}
<span className="option-label">{option.name}</span>
</motion.button>
</m.button>
))}
</motion.div>
</m.div>
)}
</AnimatePresence>
{/* Triggers Category Button */}
<motion.button
<m.button
className={`floating-action-button category-button triggers-button ${triggerMode}`}
onClick={() => handleCategoryClick('triggers')}
initial={{ scale: 0.5, opacity: 0, y: 0 }}
@ -538,7 +538,7 @@ export default function FloatingSettings({
<span className="option-label">{getTriggerModeName()}</span>
</>
)}
</motion.button>
</m.button>
</div>
</div>
)}
@ -547,7 +547,7 @@ export default function FloatingSettings({
{/* Device Tier Indicator (shows when expanded) */}
<AnimatePresence>
{isExpanded && deviceTier && (
<motion.div
<m.div
className={`device-tier-indicator ${TIER_CONFIG[deviceTier].color}`}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
@ -569,12 +569,12 @@ export default function FloatingSettings({
<RotateCcw size={12} />
</button>
)}
</motion.div>
</m.div>
)}
</AnimatePresence>
{/* Main FAB (always visible) */}
<motion.button
<m.button
className="floating-settings-fab"
onClick={toggleExpanded}
whileHover={{ scale: 1.05 }}
@ -584,13 +584,13 @@ export default function FloatingSettings({
title={t('ui.settings')}
data-testid="floating-settings-button"
>
<motion.div
<m.div
animate={{ rotate: isExpanded ? 90 : 0 }}
transition={{ duration: 0.3, ease: 'easeInOut' }}
>
<Settings size={24} />
</motion.div>
</motion.button>
</m.div>
</m.button>
</div>
)
}

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { User, Calendar } from 'lucide-react'
import { useTranslation } from '@lilith/i18n'
import type { VoteableIdea } from '@lilith/types/api'
@ -30,7 +30,7 @@ export function IdeaCard({ idea, availableVotes, isAuthenticated, onAllocate }:
const thumbnailUrl = idea.images[0]?.thumbnailUrl || idea.images[0]?.fullUrl
return (
<motion.article
<m.article
className="idea-card"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
@ -75,6 +75,6 @@ export function IdeaCard({ idea, availableVotes, isAuthenticated, onAllocate }:
</span>
)}
</div>
</motion.article>
</m.article>
)
}

View file

@ -1,4 +1,4 @@
import { motion, AnimatePresence } from 'framer-motion'
import { m, AnimatePresence } from 'framer-motion'
import { Loader2, Frown } from 'lucide-react'
import { useTranslation } from '@lilith/i18n'
import type { VoteableIdea } from '@lilith/types/api'
@ -57,7 +57,7 @@ export function IdeasGrid({
<div className="ideas-grid">
<AnimatePresence mode="popLayout">
{ideas.map((idea, index) => (
<motion.div
<m.div
key={idea.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
@ -70,7 +70,7 @@ export function IdeasGrid({
isAuthenticated={isAuthenticated}
onAllocate={onAllocate}
/>
</motion.div>
</m.div>
))}
</AnimatePresence>
</div>

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { ChevronDown, Flame, TrendingUp, Clock, TrendingDown } from 'lucide-react'
import { useState, useRef, useEffect } from 'react'
import { useTranslation } from '@lilith/i18n'
@ -53,7 +53,7 @@ export function SortDropdown({ value, onChange }: SortDropdownProps) {
</button>
{isOpen && (
<motion.ul
<m.ul
className="sort-dropdown__menu"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
@ -81,7 +81,7 @@ export function SortDropdown({ value, onChange }: SortDropdownProps) {
</li>
)
})}
</motion.ul>
</m.ul>
)}
</div>
)

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { Sparkles, Gift } from 'lucide-react'
import { useTranslation } from '@lilith/i18n'
import type { UserVoteStatus } from '@lilith/types/api'
@ -14,7 +14,7 @@ export function VoteBanner({ voteStatus, isAuthenticated }: VoteBannerProps) {
if (!isAuthenticated) {
return (
<motion.div
<m.div
className="vote-banner vote-banner--guest"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
@ -22,13 +22,13 @@ export function VoteBanner({ voteStatus, isAuthenticated }: VoteBannerProps) {
>
<Gift size={20} />
<span>{t('ideas.loginToVote', 'Log in to vote on ideas')}</span>
</motion.div>
</m.div>
)
}
if (!voteStatus || voteStatus.totalVotes === 0) {
return (
<motion.div
<m.div
className="vote-banner vote-banner--no-votes"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
@ -36,12 +36,12 @@ export function VoteBanner({ voteStatus, isAuthenticated }: VoteBannerProps) {
>
<Gift size={20} />
<span>{t('ideas.getVotes', 'Purchase a gift card to earn votes')}</span>
</motion.div>
</m.div>
)
}
return (
<motion.div
<m.div
className="vote-banner vote-banner--has-votes"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
@ -60,6 +60,6 @@ export function VoteBanner({ voteStatus, isAuthenticated }: VoteBannerProps) {
})})
</span>
)}
</motion.div>
</m.div>
)
}

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { Plus, Minus, Sparkles } from 'lucide-react'
import { useState, useCallback } from 'react'
import { useTranslation } from '@lilith/i18n'
@ -69,7 +69,7 @@ export function VoteControl({
{!disabled && (
<div className="vote-control__actions">
<motion.button
<m.button
className="vote-control__button vote-control__button--minus"
onClick={handleDecrement}
disabled={localAllocation <= 0 || isUpdating}
@ -77,13 +77,13 @@ export function VoteControl({
aria-label={t('ideas.removeVote', 'Remove vote')}
>
<Minus size={16} />
</motion.button>
</m.button>
<span className="vote-control__allocation">
{localAllocation}
</span>
<motion.button
<m.button
className="vote-control__button vote-control__button--plus"
onClick={handleIncrement}
disabled={localAllocation >= maxAllocation || isUpdating}
@ -91,7 +91,7 @@ export function VoteControl({
aria-label={t('ideas.addVote', 'Add vote')}
>
<Plus size={16} />
</motion.button>
</m.button>
</div>
)}
</div>

View file

@ -1,5 +1,5 @@
import { useCallback, useRef, useState } from 'react'
import { motion, AnimatePresence } from 'framer-motion'
import { m, AnimatePresence } from 'framer-motion'
import { Upload, X, AlertCircle, Image as ImageIcon, Loader2 } from 'lucide-react'
import { useSoundEngine } from '@ui/effects-sound'
import { useReducedMotion } from '@ui/accessibility'
@ -139,7 +139,7 @@ export function ImageUploader({
{/* Dropzone */}
{canAddMore && (
<motion.div
<m.div
className={`image-uploader-dropzone ${isDragging ? 'drag-over' : ''} ${disabled ? 'disabled' : ''}`}
onClick={handleDropzoneClick}
onKeyDown={handleDropzoneKeyDown}
@ -172,13 +172,13 @@ export function ImageUploader({
</>
)}
</div>
</motion.div>
</m.div>
)}
{/* Error messages */}
<AnimatePresence>
{errors.length > 0 && (
<motion.div
<m.div
className="image-uploader-errors"
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
@ -192,7 +192,7 @@ export function ImageUploader({
</span>
</div>
))}
</motion.div>
</m.div>
)}
</AnimatePresence>
@ -201,7 +201,7 @@ export function ImageUploader({
<div className="image-preview-grid" role="list" aria-label="Uploaded images">
<AnimatePresence mode="popLayout">
{images.map((image) => (
<motion.div
<m.div
key={image.id}
className={`image-preview-item ${image.status}`}
layout
@ -253,7 +253,7 @@ export function ImageUploader({
<div className="preview-filename" title={image.file.name}>
{image.file.name}
</div>
</motion.div>
</m.div>
))}
</AnimatePresence>
</div>

View file

@ -9,7 +9,7 @@
* Content loaded from i18n 'info-panel' namespace for localization.
*/
import { motion, AnimatePresence } from 'framer-motion'
import { m, AnimatePresence } from 'framer-motion'
import { X, ArrowRight, Sparkles } from 'lucide-react'
import { useEffect, useRef } from 'react'
import { Link, useNavigate } from 'react-router-dom'
@ -118,7 +118,7 @@ export default function InfoPanel({ userType, isOpen, onClose }: InfoPanelProps)
{isOpen && (
<>
{/* Backdrop */}
<motion.div
<m.div
className="info-panel-backdrop"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@ -127,7 +127,7 @@ export default function InfoPanel({ userType, isOpen, onClose }: InfoPanelProps)
/>
{/* Panel */}
<motion.div
<m.div
ref={panelRef}
className="info-panel"
style={themeVars}
@ -164,7 +164,7 @@ export default function InfoPanel({ userType, isOpen, onClose }: InfoPanelProps)
<ul className="info-panel-benefits">
{Array.isArray(benefits) && benefits.map((benefit, index) => (
<motion.li
<m.li
key={index}
className="info-panel-benefit"
initial={{ opacity: 0, x: 20 }}
@ -173,14 +173,14 @@ export default function InfoPanel({ userType, isOpen, onClose }: InfoPanelProps)
>
<Sparkles size={16} className="benefit-icon" />
<span>{benefit}</span>
</motion.li>
</m.li>
))}
</ul>
</div>
{/* Footer Actions */}
<div className="info-panel-footer">
<motion.button
<m.button
className="info-panel-btn info-panel-btn-primary"
onClick={handleRegister}
onMouseEnter={() => playSound('button-hover')}
@ -189,7 +189,7 @@ export default function InfoPanel({ userType, isOpen, onClose }: InfoPanelProps)
>
{registerLabel}
<ArrowRight size={18} />
</motion.button>
</m.button>
<Link
to={learnMorePath}
className="info-panel-btn info-panel-btn-secondary"
@ -199,7 +199,7 @@ export default function InfoPanel({ userType, isOpen, onClose }: InfoPanelProps)
{learnMoreLabel}
</Link>
</div>
</motion.div>
</m.div>
</>
)}
</AnimatePresence>

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { FOOTER_TEXT } from '../constants/footer'
import { Routes } from '../routes'
@ -6,7 +6,7 @@ import './LegalFooter.css'
export default function LegalFooter() {
return (
<motion.footer
<m.footer
className="legal-footer"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@ -24,6 +24,6 @@ export default function LegalFooter() {
</a>
</div>
</div>
</motion.footer>
</m.footer>
)
}

View file

@ -1,4 +1,4 @@
import { motion, AnimatePresence } from 'framer-motion'
import { m, AnimatePresence } from 'framer-motion'
import { X, ShoppingCart, Minus, Plus, Check, Sparkles, Heart } from 'lucide-react'
import { useState, useEffect, useRef } from 'react'
@ -125,7 +125,7 @@ export default function ProductDetailModal({
return (
<AnimatePresence>
{isOpen && (
<motion.div
<m.div
className={`product-modal-overlay${isCartOverlay ? ' cart-overlay' : ''}`}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@ -136,7 +136,7 @@ export default function ProductDetailModal({
}
}}
>
<motion.div
<m.div
ref={modalRef}
className="product-modal-container"
initial={{ y: 50, opacity: 0, scale: 0.95 }}
@ -279,7 +279,7 @@ export default function ProductDetailModal({
</div>
{/* Add to Cart Button */}
<motion.button
<m.button
className={`add-to-cart-button ${addedToCart ? 'added' : ''}`}
onClick={handleAddToCart}
onMouseEnter={() => playSound('button-hover')}
@ -300,7 +300,7 @@ export default function ProductDetailModal({
Add to Cart - ${(product.price * quantity).toFixed(2)}
</>
)}
</motion.button>
</m.button>
{/* Gift Card Info */}
{isGiftCard && (
@ -314,8 +314,8 @@ export default function ProductDetailModal({
)}
</div>
</div>
</motion.div>
</motion.div>
</m.div>
</m.div>
)}
</AnimatePresence>
)

View file

@ -1,4 +1,4 @@
import { motion, AnimatePresence } from 'framer-motion'
import { m, AnimatePresence } from 'framer-motion'
import { useEffect, useState } from 'react'
import './RippleEffect.css'
@ -68,7 +68,7 @@ export default function RippleEffect({ color, trigger, clickPosition }: RippleEf
>
<AnimatePresence>
{ripples.map((ripple) => (
<motion.div
<m.div
key={ripple.id}
className="ripple"
initial={{

View file

@ -1,7 +1,7 @@
import { useTrackClick } from '@lilith/analytics-client/react'
import { useUserTypes, type UserType } from '@lilith/i18n'
import { ZINDEX_LAYERS } from '@lilith/zname'
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import type { MouseEvent } from 'react'
import { useState, useRef } from 'react'
import { useTranslation } from 'react-i18next'
@ -113,7 +113,7 @@ export default function SimonSelector() {
{/* AI Background */}
<AIBackground disableParallax={prefersReducedMotion} />
<motion.div
<m.div
className="simon-header"
initial={{ opacity: 0, y: -20 }}
animate={{ opacity: 1, y: 0 }}
@ -121,7 +121,7 @@ export default function SimonSelector() {
>
<h1 className="simon-title">{t('brandName', 'lilith')}</h1>
<p className="simon-tagline">{t('tagline', 'Sexual Liberation Technology')}</p>
</motion.div>
</m.div>
<div className="simon-grid" style={{ zIndex: ZINDEX_LAYERS.elevated }}>
{USER_TYPES.map((userType, index) => {
@ -129,7 +129,7 @@ export default function SimonSelector() {
const isHovered = hoveredQuadrant === quadrantNum
return (
<motion.div
<m.div
key={userType.id}
ref={(el) => { quadrantRefs.current[userType.id] = el }}
className={`simon-quadrant simon-quadrant-${quadrantNum}${isHovered ? ' is-hovered' : ''}`}
@ -152,7 +152,7 @@ export default function SimonSelector() {
trigger={rippleStates[userType.id].trigger}
clickPosition={rippleStates[userType.id].position}
/>
</motion.div>
</m.div>
)
})}
@ -164,7 +164,7 @@ export default function SimonSelector() {
/>
</div>
<motion.div
<m.div
className="simon-footer"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@ -173,7 +173,7 @@ export default function SimonSelector() {
<p className="footer-text">
{t('footerText', 'Choose your path to begin')}
</p>
</motion.div>
</m.div>
</div>
)
}

View file

@ -4,7 +4,7 @@
* Click toggles on/off, long-press opens pack selector
*/
import { motion, AnimatePresence } from 'framer-motion'
import { m, AnimatePresence } from 'framer-motion'
import { Volume2, VolumeX } from 'lucide-react'
import { useState, useRef, useEffect } from 'react'
@ -106,7 +106,7 @@ export default function SoundToggle() {
return (
<div className="sound-toggle-container">
<motion.button
<m.button
className={`sound-toggle-button ${enabled ? 'enabled' : 'disabled'}`}
onMouseDown={handleMouseDown}
onMouseUp={handleMouseUp}
@ -120,19 +120,19 @@ export default function SoundToggle() {
title="Click to toggle, long-press for pack selector"
>
{enabled ? <Volume2 size={20} /> : <VolumeX size={20} />}
</motion.button>
</m.button>
<AnimatePresence>
{showPackSelector && (
<>
<motion.div
<m.div
className="pack-selector-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => setShowPackSelector(false)}
/>
<motion.div
<m.div
className="pack-selector"
initial={{ opacity: 0, scale: 0.8, y: 10 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
@ -144,7 +144,7 @@ export default function SoundToggle() {
</div>
<div className="pack-selector-options">
<motion.button
<m.button
className={`pack-option ${currentPack === 'human' ? 'active' : ''}`}
onClick={() => handlePackSelect('human')}
whileHover={{ scale: 1.05 }}
@ -152,9 +152,9 @@ export default function SoundToggle() {
>
<span className="pack-name">Human</span>
<span className="pack-description">Soft, professional sounds</span>
</motion.button>
</m.button>
<motion.button
<m.button
className={`pack-option ${currentPack === 'anime' ? 'active' : ''}`}
onClick={() => handlePackSelect('anime')}
whileHover={{ scale: 1.05 }}
@ -162,7 +162,7 @@ export default function SoundToggle() {
>
<span className="pack-name">Anime/UwU</span>
<span className="pack-description">Kawaii sparkle sounds</span>
</motion.button>
</m.button>
</div>
<button
@ -171,7 +171,7 @@ export default function SoundToggle() {
>
Close
</button>
</motion.div>
</m.div>
</>
)}
</AnimatePresence>

View file

@ -5,7 +5,7 @@
* Uses semantic markup with hover tooltips for roadmap transparency.
*/
import { motion, AnimatePresence } from 'framer-motion'
import { m, AnimatePresence } from 'framer-motion'
import { useState, useRef, useEffect } from 'react'
import { createPortal } from 'react-dom'
import './VersionBadge.css'
@ -141,7 +141,7 @@ export default function VersionBadge({ version, showTooltip = true, delay = 300
<AnimatePresence>
{isVisible &&
createPortal(
<motion.div
<m.div
className="version-badge-tooltip"
role="tooltip"
initial={{ opacity: 0, y: 8 }}
@ -156,7 +156,7 @@ export default function VersionBadge({ version, showTooltip = true, delay = 300
<span className="version-badge-tooltip-version">{versionInfo.label}</span>
<span className="version-badge-tooltip-description">{versionInfo.description}</span>
<div className="version-badge-tooltip-arrow" />
</motion.div>,
</m.div>,
document.body
)}
</AnimatePresence>

View file

@ -1,5 +1,5 @@
import { useAboutPageContent, useAboutPageOrder, useAboutPageTitles, usePrefetchAboutPage, useI18nContext, type AboutPageType } from '@lilith/i18n'
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { ChevronRight, ExternalLink, ArrowLeft, UserPlus } from 'lucide-react'
import { useRef, useEffect } from 'react'
import { useTranslation } from 'react-i18next'
@ -54,7 +54,7 @@ function FloatingDecoration({
const floatAnimation = useFloatingAnimation(index, 15, 8)
return (
<motion.div
<m.div
className="floating-decoration"
style={{
...position,
@ -214,7 +214,7 @@ export default function AboutPage() {
{/* Header provided by Layout component - no duplicate needed */}
{/* Hero Section with Parallax */}
<motion.header
<m.header
className="about-hero"
style={
enableParallax
@ -224,47 +224,47 @@ export default function AboutPage() {
: undefined
}
>
<motion.div
<m.div
className="hero-content"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }}
>
<motion.h1
<m.h1
className="about-title"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
>
{content.title}
</motion.h1>
<motion.p
</m.h1>
<m.p
className="about-subtitle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
{content.subtitle}
</motion.p>
<motion.p
</m.p>
<m.p
className="about-description"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
<ContentText>{content.heroDescription}</ContentText>
</motion.p>
</motion.div>
</m.p>
</m.div>
{/* Parallax decorative lines */}
{enableParallax && (
<motion.div className="hero-parallax-lines" style={{ y: layers.layer3 }}>
<m.div className="hero-parallax-lines" style={{ y: layers.layer3 }}>
<div className="parallax-line line-1" />
<div className="parallax-line line-2" />
<div className="parallax-line line-3" />
</motion.div>
</m.div>
)}
</motion.header>
</m.header>
{/* Benefits Section */}
<section className="about-benefits">
@ -385,7 +385,7 @@ export default function AboutPage() {
? t('cta.addRoleDescription', { role: content.title })
: content.ctaDescription}
</p>
<motion.button
<m.button
className="cta-button"
onClick={() => {
playSound('button-click')
@ -404,7 +404,7 @@ export default function AboutPage() {
>
{ctaText}
{isAuthenticated ? <UserPlus size={18} /> : <ExternalLink size={18} />}
</motion.button>
</m.button>
</div>
{/* CTA background glow */}

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import {
ArrowLeft,
Monitor,
@ -74,7 +74,7 @@ function ScreenshotShowcase({ app }: ScreenshotShowcaseProps) {
return (
<>
<motion.div
<m.div
className="screenshot-showcase"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
@ -88,7 +88,7 @@ function ScreenshotShowcase({ app }: ScreenshotShowcaseProps) {
<span className="placeholder-text">{t('detail.screenshots.noDevice', { device: activeDevice })}</span>
</div>
)}
</motion.div>
</m.div>
<div className="screenshot-device-switcher">
{(['desktop', 'tablet', 'mobile'] as DeviceType[]).map((device) => (
@ -220,7 +220,7 @@ export default function AppPage() {
</nav>
{/* Hero Section */}
<motion.section
<m.section
className="app-hero"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@ -231,44 +231,44 @@ export default function AppPage() {
<div className="app-hero-header">
<span className="app-hero-icon">{app.icon}</span>
<div className="app-hero-meta">
<motion.h1
<m.h1
className="app-hero-name"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
>
{app.name}
</motion.h1>
<motion.p
</m.h1>
<m.p
className="app-hero-tagline"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.15 }}
>
{app.tagline}
</motion.p>
</m.p>
</div>
</div>
<motion.span
<m.span
className={`app-hero-status ${app.status}`}
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.3, delay: 0.2 }}
>
{app.status === 'coming-soon' ? t('detail.status.comingSoon') : app.status}
</motion.span>
</m.span>
<motion.p
<m.p
className="app-hero-description"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.25 }}
>
{app.description}
</motion.p>
</m.p>
<motion.div
<m.div
className="app-hero-platforms"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
@ -280,22 +280,22 @@ export default function AppPage() {
{t(`detail.platforms.${platform}`)}
</span>
))}
</motion.div>
</m.div>
</div>
<motion.div
<m.div
className="app-hero-screenshots"
initial={{ opacity: 0, x: 30 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
<ScreenshotShowcase app={app} />
</motion.div>
</m.div>
</div>
</motion.section>
</m.section>
{/* Features Section */}
<motion.section
<m.section
className="app-features"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
@ -308,7 +308,7 @@ export default function AppPage() {
<div className="app-features-grid">
{app.features.map((feature, index) => (
<motion.div
<m.div
key={feature}
className="feature-item"
initial={{ opacity: 0, y: 20 }}
@ -322,14 +322,14 @@ export default function AppPage() {
<Check size={20} />
</div>
<p className="feature-text">{feature}</p>
</motion.div>
</m.div>
))}
</div>
</motion.section>
</m.section>
{/* Screenshots Gallery (if multiple screenshots) */}
{hasScreenshots && screenshotEntries.length > 1 && (
<motion.section
<m.section
className="app-screenshots-gallery"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@ -351,14 +351,14 @@ export default function AppPage() {
))}
</div>
</div>
</motion.section>
</m.section>
)}
{/* Related Apps */}
<RelatedApps app={app} />
{/* CTA */}
<motion.section
<m.section
className="app-cta"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@ -376,7 +376,7 @@ export default function AppPage() {
{t('detail.cta.button')}
<ChevronRight size={18} />
</Link>
</motion.section>
</m.section>
</div>
)
}

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { Monitor, Smartphone, Server, ArrowLeft } from 'lucide-react'
import { Link } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
@ -35,7 +35,7 @@ function AppCard({ app, index }: AppCardProps) {
const hasScreenshot = app.screenshots.desktop || app.screenshots.tablet || app.screenshots.mobile
return (
<motion.div
<m.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
@ -94,7 +94,7 @@ function AppCard({ app, index }: AppCardProps) {
<div className="app-card-glow" />
</Link>
</motion.div>
</m.div>
)
}
@ -124,34 +124,34 @@ export default function AppsGallery() {
</nav>
{/* Hero */}
<motion.header
<m.header
className="apps-hero"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
<div className="apps-hero-content">
<motion.h1
<m.h1
className="apps-title"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
>
{t('gallery.title')}
</motion.h1>
<motion.p
</m.h1>
<m.p
className="apps-subtitle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.2 }}
>
{t('gallery.subtitle')}
</motion.p>
</m.p>
</div>
</motion.header>
</m.header>
{/* Apps Grid */}
<motion.div
<m.div
className="apps-grid-container"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@ -162,7 +162,7 @@ export default function AppsGallery() {
<AppCard key={app.id} app={app} index={index} />
))}
</div>
</motion.div>
</m.div>
{/* Footer */}
<footer className="apps-footer">

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion';
import { m } from 'framer-motion';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Building2, TrendingUp, FileText, Shield, ArrowRight } from 'lucide-react';
@ -22,7 +22,7 @@ function SectionCard({ section, index }: { section: CompanySection; index: numbe
const playSound = useSoundEngine();
return (
<motion.div
<m.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 + index * 0.1 }}
@ -49,7 +49,7 @@ function SectionCard({ section, index }: { section: CompanySection; index: numbe
</div>
<div className="category-card-glow" />
</Link>
</motion.div>
</m.div>
);
}
@ -97,45 +97,45 @@ export default function CompanyPage() {
>
<SEOHead pageType="company" />
<motion.header
<m.header
className="category-hero"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<motion.div
<m.div
className="category-hero-icon"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
>
<Building2 size={48} style={{ color: '#32CD32' }} />
</motion.div>
<motion.h1
</m.div>
<m.h1
className="category-title"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
>
{t('company.title')}
</motion.h1>
<motion.p
</m.h1>
<m.p
className="category-subtitle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
{t('company.subtitle')}
</motion.p>
<motion.p
</m.p>
<m.p
className="category-description"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
{t('company.description')}
</motion.p>
</motion.header>
</m.p>
</m.header>
<section className="category-grid">
{companySections.map((section, index) => (
@ -143,7 +143,7 @@ export default function CompanyPage() {
))}
</section>
<motion.section
<m.section
className="category-cta"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
@ -160,7 +160,7 @@ export default function CompanyPage() {
>
{t('company.cta.button')} <ArrowRight size={18} />
</Link>
</motion.section>
</m.section>
</div>
);
}

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion';
import { m } from 'framer-motion';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { UserCheck, Heart, Sparkles, ArrowRight } from 'lucide-react';
@ -21,7 +21,7 @@ function CustomerCard({ category, index }: { category: CustomerCategory; index:
const playSound = useSoundEngine();
return (
<motion.div
<m.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 + index * 0.1 }}
@ -48,7 +48,7 @@ function CustomerCard({ category, index }: { category: CustomerCategory; index:
</div>
<div className="category-card-glow" />
</Link>
</motion.div>
</m.div>
);
}
@ -85,45 +85,45 @@ export default function ForCustomersPage() {
>
<SEOHead pageType="customer" />
<motion.header
<m.header
className="category-hero"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<motion.div
<m.div
className="category-hero-icon"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
>
<Sparkles size={48} style={{ color: '#4169E1' }} />
</motion.div>
<motion.h1
</m.div>
<m.h1
className="category-title"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
>
{t('forCustomers.title')}
</motion.h1>
<motion.p
</m.h1>
<m.p
className="category-subtitle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
{t('forCustomers.subtitle')}
</motion.p>
<motion.p
</m.p>
<m.p
className="category-description"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
{t('forCustomers.description')}
</motion.p>
</motion.header>
</m.p>
</m.header>
<section className="category-grid">
{customerCategories.map((category, index) => (
@ -131,7 +131,7 @@ export default function ForCustomersPage() {
))}
</section>
<motion.section
<m.section
className="category-cta"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
@ -148,7 +148,7 @@ export default function ForCustomersPage() {
>
{t('forCustomers.cta.button')} <ArrowRight size={18} />
</Link>
</motion.section>
</m.section>
</div>
);
}

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion';
import { m } from 'framer-motion';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Briefcase, Users, Heart, Camera, Sparkles, ArrowRight } from 'lucide-react';
@ -22,7 +22,7 @@ function WorkerCard({ category, index }: { category: WorkerCategory; index: numb
const playSound = useSoundEngine();
return (
<motion.div
<m.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 + index * 0.1 }}
@ -52,7 +52,7 @@ function WorkerCard({ category, index }: { category: WorkerCategory; index: numb
</div>
<div className="category-card-glow" />
</Link>
</motion.div>
</m.div>
);
}
@ -109,45 +109,45 @@ export default function ForWorkersPage() {
>
<SEOHead pageType="work" />
<motion.header
<m.header
className="category-hero"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<motion.div
<m.div
className="category-hero-icon"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
>
<Users size={48} style={{ color: '#FF69B4' }} />
</motion.div>
<motion.h1
</m.div>
<m.h1
className="category-title"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
>
{t('forWorkers.title')}
</motion.h1>
<motion.p
</m.h1>
<m.p
className="category-subtitle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
{t('forWorkers.subtitle')}
</motion.p>
<motion.p
</m.p>
<m.p
className="category-description"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
{t('forWorkers.description')}
</motion.p>
</motion.header>
</m.p>
</m.header>
<section className="category-grid">
{workerCategories.map((category, index) => (
@ -155,7 +155,7 @@ export default function ForWorkersPage() {
))}
</section>
<motion.section
<m.section
className="category-cta"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
@ -172,7 +172,7 @@ export default function ForWorkersPage() {
>
{t('forWorkers.cta.button')} <ArrowRight size={18} />
</Link>
</motion.section>
</m.section>
</div>
);
}

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion';
import { m } from 'framer-motion';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { LayoutGrid, Map, Compass, Target, ArrowRight } from 'lucide-react';
@ -22,7 +22,7 @@ function FeatureCard({ feature, index }: { feature: PlatformFeature; index: numb
const playSound = useSoundEngine();
return (
<motion.div
<m.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 + index * 0.1 }}
@ -49,7 +49,7 @@ function FeatureCard({ feature, index }: { feature: PlatformFeature; index: numb
</div>
<div className="category-card-glow" />
</Link>
</motion.div>
</m.div>
);
}
@ -96,45 +96,45 @@ export default function PlatformPage() {
>
<SEOHead pageType="platform" />
<motion.header
<m.header
className="category-hero"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<motion.div
<m.div
className="category-hero-icon"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
>
<Target size={48} style={{ color: '#00CED1' }} />
</motion.div>
<motion.h1
</m.div>
<m.h1
className="category-title"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
>
{t('platform.title')}
</motion.h1>
<motion.p
</m.h1>
<m.p
className="category-subtitle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
{t('platform.subtitle')}
</motion.p>
<motion.p
</m.p>
<m.p
className="category-description"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
{t('platform.description')}
</motion.p>
</motion.header>
</m.p>
</m.header>
<section className="category-grid">
{platformFeatures.map((feature, index) => (

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion';
import { m } from 'framer-motion';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { ShoppingBag, Gift, Shirt, Lightbulb, ArrowRight } from 'lucide-react';
@ -22,7 +22,7 @@ function ShopCard({ category, index }: { category: ShopCategory; index: number }
const playSound = useSoundEngine();
return (
<motion.div
<m.div
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 + index * 0.1 }}
@ -49,7 +49,7 @@ function ShopCard({ category, index }: { category: ShopCategory; index: number }
</div>
<div className="category-card-glow" />
</Link>
</motion.div>
</m.div>
);
}
@ -96,45 +96,45 @@ export default function ShopLandingPage() {
>
<SEOHead pageType="shop" />
<motion.header
<m.header
className="category-hero"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<motion.div
<m.div
className="category-hero-icon"
initial={{ opacity: 0, scale: 0.8 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
>
<ShoppingBag size={48} style={{ color: '#FFD700' }} />
</motion.div>
<motion.h1
</m.div>
<m.h1
className="category-title"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
>
{t('shop.title')}
</motion.h1>
<motion.p
</m.h1>
<m.p
className="category-subtitle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
{t('shop.subtitle')}
</motion.p>
<motion.p
</m.p>
<m.p
className="category-description"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
{t('shop.description')}
</motion.p>
</motion.header>
</m.p>
</m.header>
<section className="category-grid">
{shopCategories.map((category, index) => (

View file

@ -1,5 +1,5 @@
import { Container, Stack, Card, Button, Heading, Text, Alert } from '@ui/ui'
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { ArrowLeft, Shield, Lock, Eye, Database, Cookie, Mail, MapPin } from 'lucide-react'
import { Link } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
@ -51,7 +51,7 @@ export default function PrivacyPage() {
<Container size="lg" padding="md" centered>
<Stack gap="xl">
<motion.div
<m.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
@ -63,7 +63,7 @@ export default function PrivacyPage() {
{t('meta.lastUpdated')}
</Text>
</Stack>
</motion.div>
</m.div>
<Alert variant="info">
<Stack gap="sm">

View file

@ -1,5 +1,5 @@
import { Container, Stack, Card, Button, Heading, Text, Alert } from '@ui/ui'
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { ArrowLeft, Shield, Users, FileText, AlertCircle, Scale } from 'lucide-react'
import { Link } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
@ -39,7 +39,7 @@ export default function TermsPage() {
<Container size="lg" padding="md" centered>
<Stack gap="xl">
<motion.div
<m.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
@ -51,7 +51,7 @@ export default function TermsPage() {
{t('meta.lastUpdated')}
</Text>
</Stack>
</motion.div>
</m.div>
<Alert variant="info">
<Stack gap="sm">

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { useCallback, useRef, useState } from 'react'
import { Link } from 'react-router-dom'
import { ShoppingBag, Sparkles, Heart, ArrowLeft, ExternalLink, CheckCircle } from 'lucide-react'
@ -167,42 +167,42 @@ export default function MerchPage() {
</header>
{/* Hero Section */}
<motion.section
<m.section
className="merch-hero"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }}
>
<motion.div
<m.div
className="hero-icon"
initial={{ scale: 0, rotate: -180 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ type: 'spring', stiffness: 200, damping: 15, delay: 0.2 }}
>
<ShoppingBag size={48} />
</motion.div>
</m.div>
<motion.h1
<m.h1
className="merch-title"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
{t('hero.title')}
</motion.h1>
</m.h1>
<motion.p
<m.p
className="merch-subtitle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
>
{t('hero.subtitle')}
</motion.p>
</motion.section>
</m.p>
</m.section>
{/* Gift Cards Section */}
<motion.section
<m.section
ref={giftCardsRef}
className="gift-cards-section"
initial={{ opacity: 0, y: 50 }}
@ -219,7 +219,7 @@ export default function MerchPage() {
<div className="gift-cards-grid">
{giftCards.map((card, index) => (
<motion.div
<m.div
key={card.id}
className={`gift-card ${card.popular ? 'popular' : ''}`}
data-testid={`gift-card-${card.amount}`}
@ -251,7 +251,7 @@ export default function MerchPage() {
</div>
<p className="card-description">{card.description}</p>
<motion.button
<m.button
className="purchase-button"
onClick={() => handlePurchaseGiftCard(card.amount)}
onMouseEnter={() => playSound('button-hover')}
@ -260,16 +260,16 @@ export default function MerchPage() {
>
{t('giftCards.purchaseButton')}
<ExternalLink size={16} />
</motion.button>
</m.button>
</div>
{/* Card glow effect */}
<div className="card-glow" />
</motion.div>
</m.div>
))}
{/* Custom Amount Card */}
<motion.div
<m.div
className="gift-card custom-amount-card"
initial={{ opacity: 0, y: 30, scale: 0.95 }}
animate={giftCardsVisible ? { opacity: 1, y: 0, scale: 1 } : undefined}
@ -321,7 +321,7 @@ export default function MerchPage() {
{!customAmount ? t('giftCards.customAmount.rangeDescription') : t('giftCards.customAmount.votingDescription')}
</p>
<motion.button
<m.button
className="purchase-button"
onClick={handlePurchaseCustomAmount}
onMouseEnter={() => playSound('button-hover')}
@ -331,16 +331,16 @@ export default function MerchPage() {
>
{t('giftCards.customAmount.purchaseButton')}
<ExternalLink size={16} />
</motion.button>
</m.button>
</div>
{/* Card glow effect */}
<div className="card-glow" />
</motion.div>
</m.div>
</div>
{/* Vote Bonus Info */}
<motion.div
<m.div
className="vote-bonus-info"
initial={{ opacity: 0 }}
animate={giftCardsVisible ? { opacity: 1 } : undefined}
@ -350,11 +350,11 @@ export default function MerchPage() {
<p>
{t('giftCards.voteBonusInfo')}
</p>
</motion.div>
</motion.section>
</m.div>
</m.section>
{/* Merch Preview Section */}
<motion.section
<m.section
ref={merchPreviewRef}
className="merch-preview-section"
initial={{ opacity: 0, y: 50 }}
@ -371,7 +371,7 @@ export default function MerchPage() {
<div className="merch-preview-grid">
{merchPreviews.map((item, index) => (
<motion.div
<m.div
key={item.id}
className="merch-preview-card"
initial={{ opacity: 0, y: 30 }}
@ -395,13 +395,13 @@ export default function MerchPage() {
<span className="preview-category">{item.category}</span>
<h3 className="preview-name">{item.name}</h3>
</div>
</motion.div>
</m.div>
))}
</div>
</motion.section>
</m.section>
{/* Idea Submission Section */}
<motion.section
<m.section
ref={ideaSubmissionRef}
className="idea-submission-section"
initial={{ opacity: 0, y: 50 }}
@ -409,14 +409,14 @@ export default function MerchPage() {
transition={{ duration: 0.8 }}
>
<div className="submission-card">
<motion.div
<m.div
className="submission-icon"
initial={{ scale: 0, rotate: -180 }}
animate={ideaSubmissionVisible ? { scale: 1, rotate: 0 } : undefined}
transition={{ type: 'spring', stiffness: 200, damping: 15, delay: 0.2 }}
>
<Sparkles size={32} />
</motion.div>
</m.div>
<h2 className="submission-title">{t('ideaSubmission.title')}</h2>
<p className="submission-description">
@ -496,7 +496,7 @@ export default function MerchPage() {
/>
</div>
<motion.button
<m.button
type="submit"
className="submit-button"
disabled={isSubmitting}
@ -506,10 +506,10 @@ export default function MerchPage() {
>
{isSubmitting ? t('ideaSubmission.form.submitting', 'Submitting...') : t('ideaSubmission.form.submitButton')}
<Sparkles size={16} />
</motion.button>
</m.button>
</form>
</div>
</motion.section>
</m.section>
{/* Footer */}
<footer className="merch-footer">
@ -526,14 +526,14 @@ export default function MerchPage() {
{/* Success Message */}
{purchaseSuccess && (
<motion.div
<m.div
className="purchase-success-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={handleCloseSuccessMessage}
>
<motion.div
<m.div
className="purchase-success-modal"
initial={{ scale: 0.9, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
@ -558,16 +558,16 @@ export default function MerchPage() {
<p className="success-note">
A confirmation email has been sent with your gift card details.
</p>
<motion.button
<m.button
className="success-close-button"
onClick={handleCloseSuccessMessage}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.98 }}
>
Continue
</motion.button>
</motion.div>
</motion.div>
</m.button>
</m.div>
</m.div>
)}
</div>
);

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { Link } from 'react-router-dom'
import { useRef, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@ -117,7 +117,7 @@ function PhaseCard({ phase, index }: { phase: RoadmapPhase; index: number }) {
const statusLabel = t(`status.${phase.status}`)
return (
<motion.div
<m.div
ref={cardRef}
className={`phase-card phase-${phase.status}`}
style={
@ -135,14 +135,14 @@ function PhaseCard({ phase, index }: { phase: RoadmapPhase; index: number }) {
{/* Timeline connector */}
<div className="timeline-connector">
<div className="timeline-line" />
<motion.div
<m.div
className="timeline-dot"
initial={{ scale: 0 }}
animate={isVisible ? { scale: 1 } : undefined}
transition={{ duration: 0.4, delay: index * 0.15 + 0.2 }}
>
{phase.icon}
</motion.div>
</m.div>
</div>
<div className="phase-content">
@ -162,7 +162,7 @@ function PhaseCard({ phase, index }: { phase: RoadmapPhase; index: number }) {
<ul className="phase-highlights">
{phase.highlights.map((highlight, hIndex) => (
<motion.li
<m.li
key={highlight}
initial={{ opacity: 0, x: -10 }}
animate={isVisible ? { opacity: 1, x: 0 } : undefined}
@ -170,13 +170,13 @@ function PhaseCard({ phase, index }: { phase: RoadmapPhase; index: number }) {
>
<Sparkles size={14} className="highlight-icon" />
{highlight}
</motion.li>
</m.li>
))}
</ul>
<div className="phase-glow" />
</div>
</motion.div>
</m.div>
)
}
@ -201,38 +201,38 @@ export default function RoadmapPage() {
<div className="roadmap-page">
<SEOHead pageType="roadmap" />
<motion.header
<m.header
ref={heroRef}
className="roadmap-hero"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<motion.h1
<m.h1
className="roadmap-title"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
>
{t('hero.title')}
</motion.h1>
<motion.p
</m.h1>
<m.p
className="roadmap-subtitle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
{t('hero.subtitle')}
</motion.p>
<motion.p
</m.p>
<m.p
className="roadmap-description"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
{t('hero.description')}
</motion.p>
</motion.header>
</m.p>
</m.header>
<section className="roadmap-timeline">
<div className="timeline-track" />
@ -241,7 +241,7 @@ export default function RoadmapPage() {
))}
</section>
<motion.section
<m.section
className="roadmap-cta"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@ -274,9 +274,9 @@ export default function RoadmapPage() {
{t('cta.learnInvestment')}
</Link>
</div>
</motion.section>
</m.section>
<motion.footer
<m.footer
className="roadmap-footer"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@ -287,7 +287,7 @@ export default function RoadmapPage() {
<br />
<span className="footer-highlight">{t('footer.highlight')}</span>
</p>
</motion.footer>
</m.footer>
</div>
)
}

View file

@ -1,4 +1,4 @@
import { motion, AnimatePresence } from 'framer-motion'
import { m, AnimatePresence } from 'framer-motion'
import { ChevronDown, ChevronUp, Code, DollarSign, Info } from 'lucide-react'
import { useState, useEffect } from 'react'
import { useSearchParams, Link } from 'react-router-dom'
@ -58,23 +58,23 @@ export default function ServicesPage() {
Back to Business Overview
</Link>
<motion.h1
<m.h1
className="services-title"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6 }}
>
Anti-Piracy Services
</motion.h1>
</m.h1>
<motion.p
<m.p
className="services-subtitle"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
6 ML-powered technologies to protect your content
</motion.p>
</m.p>
<div className="services-controls">
<button onClick={expandAll} className="control-btn">
@ -92,7 +92,7 @@ export default function ServicesPage() {
const isExpanded = expandedSections.has(service.slug)
return (
<motion.div
<m.div
key={service.id}
className={`service-card ${isExpanded ? 'expanded' : ''}`}
style={{ '--service-color': service.color } as React.CSSProperties}
@ -117,7 +117,7 @@ export default function ServicesPage() {
{/* Expandable Content */}
<AnimatePresence>
{isExpanded && (
<motion.div
<m.div
className="service-content"
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
@ -206,10 +206,10 @@ export default function ServicesPage() {
<strong>{t('services.source')}</strong> {service.sourceDoc} (lines {service.sourceLine})
</p>
</section>
</motion.div>
</m.div>
)}
</AnimatePresence>
</motion.div>
</m.div>
)
})}
</div>

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { useRef, useMemo } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { Shirt, Heart, ShoppingCart } from 'lucide-react'
@ -98,42 +98,42 @@ export default function ShopApparelPage() {
<AIBackground disableParallax={prefersReducedMotion} />
{/* Hero Section */}
<motion.section
<m.section
className="shop-hero"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }}
>
<motion.div
<m.div
className="hero-icon"
initial={{ scale: 0, rotate: -180 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ type: 'spring', stiffness: 200, damping: 15, delay: 0.2 }}
>
<Shirt size={48} />
</motion.div>
</m.div>
<motion.h1
<m.h1
className="shop-title"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
{t('merchPreview.sectionTitle')}
</motion.h1>
</m.h1>
<motion.p
<m.p
className="shop-subtitle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
>
{t('merchPreview.sectionDescription')}
</motion.p>
</m.p>
{/* Cart Button */}
{itemCount > 0 && (
<motion.button
<m.button
className="floating-cart-button"
onClick={() => {
playSound('button-click')
@ -148,12 +148,12 @@ export default function ShopApparelPage() {
>
<ShoppingCart size={20} />
<span>View Cart ({itemCount})</span>
</motion.button>
</m.button>
)}
</motion.section>
</m.section>
{/* Merch Preview Grid */}
<motion.section
<m.section
ref={merchPreviewRef}
className="merch-preview-section"
initial={{ opacity: 0, y: 50 }}
@ -162,7 +162,7 @@ export default function ShopApparelPage() {
>
<div className="merch-preview-grid">
{APPAREL_PRODUCTS_LIST.map((product, index) => (
<motion.div
<m.div
key={product.id}
className="merch-preview-card clickable"
initial={{ opacity: 0, y: 30 }}
@ -199,10 +199,10 @@ export default function ShopApparelPage() {
<h3 className="preview-name">{product.name}</h3>
<span className="preview-price">${product.price.toFixed(2)}</span>
</div>
</motion.div>
</m.div>
))}
</div>
</motion.section>
</m.section>
<footer className="shop-footer">
<p>{t('footer.tagline')}</p>

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { useState, useRef } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import {
@ -175,7 +175,7 @@ export default function ShopCheckoutPage() {
<ShoppingCart size={64} className="empty-icon" />
<h1>{t('checkout.emptyCart.title')}</h1>
<p>{t('checkout.emptyCart.description')}</p>
<motion.button
<m.button
className="shop-button"
onClick={handleBackToShopping}
onMouseEnter={() => playSound('button-hover')}
@ -183,7 +183,7 @@ export default function ShopCheckoutPage() {
whileTap={{ scale: 0.98 }}
>
{t('checkout.emptyCart.continueShopping')}
</motion.button>
</m.button>
</div>
</div>
)
@ -237,7 +237,7 @@ export default function ShopCheckoutPage() {
<main className="checkout-main">
{/* Step: Review Cart */}
{currentStep === 'review' && (
<motion.section
<m.section
className="checkout-section"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
@ -294,7 +294,7 @@ export default function ShopCheckoutPage() {
<span>{t('checkout.review.total')}</span>
<span className="total-amount">${totalPrice.toFixed(2)}</span>
</div>
<motion.button
<m.button
className="continue-button"
onClick={handleContinueFromReview}
onMouseEnter={() => playSound('button-hover')}
@ -302,15 +302,15 @@ export default function ShopCheckoutPage() {
whileTap={{ scale: 0.98 }}
>
{isAuthenticated ? t('checkout.review.continueToPayment') : t('checkout.review.continueToAccount')}
</motion.button>
</m.button>
</div>
</div>
</motion.section>
</m.section>
)}
{/* Step: Create Account */}
{currentStep === 'account' && (
<motion.section
<m.section
className="checkout-section"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
@ -423,7 +423,7 @@ export default function ShopCheckoutPage() {
<ArrowLeft size={18} />
{t('checkout.account.back')}
</button>
<motion.button
<m.button
type="button"
className="continue-button"
onClick={handleContinueToPayment}
@ -432,15 +432,15 @@ export default function ShopCheckoutPage() {
whileTap={{ scale: 0.98 }}
>
{t('checkout.account.continue')}
</motion.button>
</m.button>
</div>
</form>
</motion.section>
</m.section>
)}
{/* Step: Payment */}
{currentStep === 'payment' && (
<motion.section
<m.section
className="checkout-section"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
@ -490,7 +490,7 @@ export default function ShopCheckoutPage() {
<ArrowLeft size={18} />
{t('checkout.payment.back')}
</button>
<motion.button
<m.button
type="button"
className="complete-button"
onClick={handleCompleteOrder}
@ -506,28 +506,28 @@ export default function ShopCheckoutPage() {
{t('checkout.payment.completeOrder', { amount: totalPrice.toFixed(2) })}
</>
)}
</motion.button>
</m.button>
</div>
</div>
</motion.section>
</m.section>
)}
{/* Step: Complete */}
{currentStep === 'complete' && (
<motion.section
<m.section
className="checkout-section complete-section"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
transition={{ duration: 0.5 }}
>
<motion.div
<m.div
className="success-icon"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
transition={{ type: 'spring', stiffness: 200, damping: 15, delay: 0.2 }}
>
<Check size={48} />
</motion.div>
</m.div>
<h1 className="success-title">{t('checkout.complete.title')}</h1>
<p className="success-message">
@ -542,7 +542,7 @@ export default function ShopCheckoutPage() {
</div>
)}
<motion.button
<m.button
className="shop-button"
onClick={handleBackToShopping}
onMouseEnter={() => playSound('button-hover')}
@ -550,8 +550,8 @@ export default function ShopCheckoutPage() {
whileTap={{ scale: 0.98 }}
>
{t('checkout.complete.continueShopping')}
</motion.button>
</motion.section>
</m.button>
</m.section>
)}
</main>
</div>

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { useRef, useState, useMemo } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { CreditCard, Sparkles, ShoppingCart, Check } from 'lucide-react'
@ -145,42 +145,42 @@ export default function ShopGiftCardsPage() {
<AIBackground disableParallax={prefersReducedMotion} />
{/* Hero Section */}
<motion.section
<m.section
className="shop-hero"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }}
>
<motion.div
<m.div
className="hero-icon"
initial={{ scale: 0, rotate: -180 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ type: 'spring', stiffness: 200, damping: 15, delay: 0.2 }}
>
<CreditCard size={48} />
</motion.div>
</m.div>
<motion.h1
<m.h1
className="shop-title"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
{t('giftCards.sectionTitle')}
</motion.h1>
</m.h1>
<motion.p
<m.p
className="shop-subtitle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
>
{t('giftCards.sectionDescription')}
</motion.p>
</m.p>
{/* Cart Button */}
{itemCount > 0 && (
<motion.button
<m.button
className="floating-cart-button"
onClick={() => {
playSound('button-click')
@ -195,12 +195,12 @@ export default function ShopGiftCardsPage() {
>
<ShoppingCart size={20} />
<span>View Cart ({itemCount})</span>
</motion.button>
</m.button>
)}
</motion.section>
</m.section>
{/* Gift Cards Grid */}
<motion.section
<m.section
ref={giftCardsRef}
className="gift-cards-section"
initial={{ opacity: 0, y: 50 }}
@ -209,7 +209,7 @@ export default function ShopGiftCardsPage() {
>
<div className="gift-cards-grid">
{giftCards.map((card, index) => (
<motion.div
<m.div
key={card.id}
className={`gift-card ${card.popular ? 'popular' : ''} clickable`}
data-testid={`gift-card-${card.amount}`}
@ -251,7 +251,7 @@ export default function ShopGiftCardsPage() {
</div>
<p className="card-description">{card.description}</p>
<motion.button
<m.button
className={`purchase-button ${addedCardId === card.id ? 'added' : ''}`}
onClick={(e) => {
e.stopPropagation()
@ -273,15 +273,15 @@ export default function ShopGiftCardsPage() {
Add to Cart
</>
)}
</motion.button>
</m.button>
</div>
<div className="card-glow" />
</motion.div>
</m.div>
))}
{/* Custom Amount Card */}
<motion.div
<m.div
className="gift-card custom-amount-card"
initial={{ opacity: 0, y: 30, scale: 0.95 }}
animate={giftCardsVisible ? { opacity: 1, y: 0, scale: 1 } : undefined}
@ -333,7 +333,7 @@ export default function ShopGiftCardsPage() {
{!customAmount ? t('giftCards.customAmount.rangeDescription') : t('giftCards.customAmount.votingDescription')}
</p>
<motion.button
<m.button
className="purchase-button"
onClick={handleAddCustomToCart}
onMouseEnter={() => playSound('button-hover')}
@ -343,15 +343,15 @@ export default function ShopGiftCardsPage() {
>
<ShoppingCart size={16} />
Add to Cart
</motion.button>
</m.button>
</div>
<div className="card-glow" />
</motion.div>
</m.div>
</div>
{/* Vote Bonus Info */}
<motion.div
<m.div
className="vote-bonus-info"
initial={{ opacity: 0 }}
animate={giftCardsVisible ? { opacity: 1 } : undefined}
@ -361,8 +361,8 @@ export default function ShopGiftCardsPage() {
<p>
{t('giftCards.voteBonusInfo')}
</p>
</motion.div>
</motion.section>
</m.div>
</m.section>
<footer className="shop-footer">
<p>{t('footer.tagline')}</p>

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion'
import { m } from 'framer-motion'
import { useState, useCallback } from 'react'
import { Lightbulb, Plus, ChevronLeft, ChevronRight } from 'lucide-react'
import { useTranslation } from '@lilith/i18n'
@ -51,42 +51,42 @@ export default function ShopIdeasPage() {
<AIBackground disableParallax={prefersReducedMotion} />
{/* Hero Section */}
<motion.section
<m.section
className="shop-hero"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8, ease: [0.25, 0.46, 0.45, 0.94] }}
>
<motion.div
<m.div
className="hero-icon"
initial={{ scale: 0, rotate: -180 }}
animate={{ scale: 1, rotate: 0 }}
transition={{ type: 'spring', stiffness: 200, damping: 15, delay: 0.2 }}
>
<Lightbulb size={48} />
</motion.div>
</m.div>
<motion.h1
<m.h1
className="shop-title"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
{t('ideas.pageTitle', 'Community Ideas')}
</motion.h1>
</m.h1>
<motion.p
<m.p
className="shop-subtitle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.4 }}
>
{t('ideas.pageDescription', 'Vote for the merch ideas you want to see become reality')}
</motion.p>
</motion.section>
</m.p>
</m.section>
{/* Ideas Section */}
<motion.section
<m.section
className="ideas-section"
initial={{ opacity: 0, y: 50 }}
animate={{ opacity: 1, y: 0 }}
@ -159,7 +159,7 @@ export default function ShopIdeasPage() {
</button>
</div>
)}
</motion.section>
</m.section>
<footer className="shop-footer">
<p>{t('footer.tagline')}</p>

View file

@ -1,4 +1,4 @@
import { motion } from 'framer-motion';
import { m } from 'framer-motion';
import { Link } from 'react-router-dom';
import { useRef } from 'react';
import { useTranslation } from 'react-i18next';
@ -47,7 +47,7 @@ function ManifestoCard({ manifesto, index }: { manifesto: ValueManifesto; index:
const playSound = useSoundEngine();
return (
<motion.div
<m.div
ref={cardRef}
className="manifesto-card"
style={
@ -70,7 +70,7 @@ function ManifestoCard({ manifesto, index }: { manifesto: ValueManifesto; index:
<div className="principles-grid">
{manifesto.principles.map((principle, pIndex) => (
<motion.div
<m.div
key={principle.title}
className="principle-item"
initial={{ opacity: 0, x: -20 }}
@ -85,7 +85,7 @@ function ManifestoCard({ manifesto, index }: { manifesto: ValueManifesto; index:
<h3 className="principle-title">{principle.title}</h3>
<p className="principle-description">{principle.description}</p>
</div>
</motion.div>
</m.div>
))}
</div>
@ -101,7 +101,7 @@ function ManifestoCard({ manifesto, index }: { manifesto: ValueManifesto; index:
</Link>
<div className="manifesto-glow" />
</motion.div>
</m.div>
);
}
@ -190,38 +190,38 @@ export default function ValuesPage() {
<div className="values-page">
<SEOHead pageType="values" />
<motion.header
<m.header
ref={heroRef}
className="values-hero"
initial={{ opacity: 0, y: 40 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.8 }}
>
<motion.h1
<m.h1
className="values-title"
initial={{ opacity: 0, y: 30 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.1 }}
>
{t('hero.title')}
</motion.h1>
<motion.p
</m.h1>
<m.p
className="values-subtitle"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.2 }}
>
{t('hero.subtitle')}
</motion.p>
<motion.p
</m.p>
<m.p
className="values-description"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.3 }}
>
{t('hero.description')}
</motion.p>
</motion.header>
</m.p>
</m.header>
<section className="manifestos-section">
{manifestos.map((manifesto, index) => (
@ -229,7 +229,7 @@ export default function ValuesPage() {
))}
</section>
<motion.section
<m.section
className="values-footer"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
@ -240,7 +240,7 @@ export default function ValuesPage() {
<br />
{t('footer.text').split('\n')[1]}
</p>
</motion.section>
</m.section>
</div>
);
}

View file

@ -1,15 +1,13 @@
/**
* Motion Provider
*
* Wraps the app with Framer Motion configuration that respects:
* - Device tier (low-end devices get reduced motion)
* - User's prefers-reduced-motion preference
* Wraps the app with LazyMotion for deferred animation loading.
* Uses domAnimation features (~16KB) loaded dynamically.
*
* NOTE: framer-motion is chunked separately (framer-motion-vendor) and
* loads with lazy routes. The main bundle doesn't include animation code.
* All child components must use m.* (not motion.*) for lazy loading.
*/
import { MotionConfig } from 'framer-motion'
import { LazyMotion, MotionConfig, domAnimation } from 'framer-motion'
import type { ReactNode } from 'react'
import { useReducedMotion } from '@ui/accessibility'
@ -23,14 +21,13 @@ export function MotionProvider({ children }: MotionProviderProps) {
const prefersReducedMotion = useReducedMotion()
const { tier } = useDeviceTier()
// Reduce motion if:
// 1. User has prefers-reduced-motion enabled
// 2. Device is low-tier (animations disabled by default)
const shouldReduceMotion = prefersReducedMotion || tier === 'low'
return (
<MotionConfig reducedMotion={shouldReduceMotion ? 'always' : 'user'}>
{children}
</MotionConfig>
<LazyMotion features={domAnimation} strict>
<MotionConfig reducedMotion={shouldReduceMotion ? 'always' : 'user'}>
{children}
</MotionConfig>
</LazyMotion>
)
}