learn and share notes on atproto (wip) 🦉 malfestio.stormlightlabs.org/
readability solid axum atproto srs
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: expand animation utilities

+338 -31
+62 -11
web/src/index.css
··· 25 25 --spacing-3xl: 64px; 26 26 --spacing-4xl: 96px; 27 27 28 - /* Elevation Layers - Carbon-inspired */ 28 + /* Elevation Layers */ 29 29 --layer-00: #161616; 30 - --layer-01: #1E1E1E; 30 + --layer-01: #1e1e1e; 31 31 --layer-02: #262626; 32 - --layer-03: #2C2C2C; 32 + --layer-03: #2c2c2c; 33 33 --layer-04: #323232; 34 34 35 35 /* Elevation Shadows - Subtle depth */ ··· 45 45 --duration-slow: 350ms; 46 46 --duration-slower: 500ms; 47 47 48 - --easing-standard: cubic-bezier(0.4, 0.0, 0.2, 1); 49 - --easing-accelerate: cubic-bezier(0.4, 0.0, 1, 1); 50 - --easing-decelerate: cubic-bezier(0.0, 0.0, 0.2, 1); 51 - --easing-sharp: cubic-bezier(0.4, 0.0, 0.6, 1); 48 + --easing-standard: cubic-bezier(0.4, 0, 0.2, 1); 49 + --easing-accelerate: cubic-bezier(0.4, 0, 1, 1); 50 + --easing-decelerate: cubic-bezier(0, 0, 0.2, 1); 51 + --easing-sharp: cubic-bezier(0.4, 0, 0.6, 1); 52 52 53 - /* Density Multiplier - Set by context provider */ 54 - --density-multiplier: 1.0; 53 + --density-multiplier: 1; 55 54 } 56 55 57 56 * { ··· 108 107 } 109 108 110 109 .density-comfortable { 111 - --density-multiplier: 1.0; 110 + --density-multiplier: 1; 112 111 } 113 112 114 113 .density-spacious { 115 114 --density-multiplier: 1.25; 116 115 } 117 116 118 - /* Transition Utilities - Consistent motion */ 119 117 .transition-standard { 120 118 transition-duration: var(--duration-normal); 121 119 transition-timing-function: var(--easing-standard); ··· 130 128 transition-duration: var(--duration-slow); 131 129 transition-timing-function: var(--easing-decelerate); 132 130 } 131 + 132 + @keyframes pulse { 133 + 0%, 134 + 100% { 135 + opacity: 1; 136 + } 137 + 50% { 138 + opacity: 0.5; 139 + } 140 + } 141 + 142 + @keyframes shimmer { 143 + 0% { 144 + background-position: -200% 0; 145 + } 146 + 100% { 147 + background-position: 200% 0; 148 + } 149 + } 150 + 151 + @keyframes breathe { 152 + 0%, 153 + 100% { 154 + transform: scale(1); 155 + } 156 + 50% { 157 + transform: scale(1.02); 158 + } 159 + } 160 + 161 + .animate-pulse { 162 + animation: pulse 2s ease-in-out infinite; 163 + } 164 + 165 + .animate-shimmer { 166 + background: linear-gradient( 167 + 90deg, 168 + var(--layer-01) 0%, 169 + var(--layer-02) 50%, 170 + var(--layer-01) 100% 171 + ); 172 + background-size: 200% 100%; 173 + animation: shimmer 1.5s ease-in-out infinite; 174 + } 175 + 176 + .animate-breathe { 177 + animation: breathe 3s ease-in-out infinite; 178 + } 179 + 180 + .press-effect:active { 181 + transform: scale(0.97); 182 + transition: transform var(--duration-instant) var(--easing-sharp); 183 + }
+113 -20
web/src/lib/animations.ts
··· 1 1 import type { Options as MotionOptions } from "solid-motionone"; 2 + import { motion } from "./design-tokens"; 2 3 3 4 /** Spring animation config for natural bounce */ 4 5 export const springConfig = { stiffness: 300, damping: 24 }; ··· 6 7 /** Standard easing for UI animations */ 7 8 export const easeOut = [0.22, 1, 0.36, 1] as const; 8 9 10 + /** Bounce easing for playful animations */ 11 + export const easeBounce = [0.34, 1.56, 0.64, 1] as const; 12 + 9 13 /** Fade in animation */ 10 14 export const fadeIn: MotionOptions = { 11 15 initial: { opacity: 0 }, 12 16 animate: { opacity: 1 }, 13 - transition: { duration: 0.2, easing: easeOut }, 17 + transition: { duration: motion.duration.fast / 1000, easing: easeOut }, 14 18 }; 15 19 16 - /** Fade out animation */ 17 - export const fadeOut: MotionOptions = { 18 - initial: { opacity: 1 }, 19 - animate: { opacity: 0 }, 20 - transition: { duration: 0.15 }, 20 + /** Fade in while sliding up 20px */ 21 + export const fadeInUp: MotionOptions = { 22 + initial: { opacity: 0, y: 20 }, 23 + animate: { opacity: 1, y: 0 }, 24 + transition: { duration: motion.duration.normal / 1000, easing: easeOut }, 21 25 }; 22 26 23 27 /** Slide in from right */ 24 28 export const slideInRight: MotionOptions = { 25 29 initial: { opacity: 0, x: 20 }, 26 30 animate: { opacity: 1, x: 0 }, 27 - transition: { duration: 0.25, easing: easeOut }, 31 + transition: { duration: motion.duration.normal / 1000, easing: easeOut }, 32 + }; 33 + 34 + /** Slide in from left */ 35 + export const slideInLeft: MotionOptions = { 36 + initial: { opacity: 0, x: -20 }, 37 + animate: { opacity: 1, x: 0 }, 38 + transition: { duration: motion.duration.slow / 1000, easing: easeOut }, 28 39 }; 29 40 30 41 /** Slide in from bottom */ 31 42 export const slideInUp: MotionOptions = { 32 43 initial: { opacity: 0, y: 10 }, 33 44 animate: { opacity: 1, y: 0 }, 34 - transition: { duration: 0.2, easing: easeOut }, 45 + transition: { duration: motion.duration.fast / 1000, easing: easeOut }, 35 46 }; 36 47 37 48 /** Scale in (pop) */ 38 49 export const scaleIn: MotionOptions = { 39 50 initial: { opacity: 0, scale: 0.95 }, 40 51 animate: { opacity: 1, scale: 1 }, 41 - transition: { duration: 0.2, easing: easeOut }, 52 + transition: { duration: motion.duration.fast / 1000, easing: easeOut }, 53 + }; 54 + 55 + /** Scale in with bounce effect */ 56 + export const scaleInBounce: MotionOptions = { 57 + initial: { opacity: 0, scale: 0.8 }, 58 + animate: { opacity: 1, scale: 1 }, 59 + transition: { duration: motion.duration.slow / 1000, easing: easeBounce }, 60 + }; 61 + 62 + /** Fade out animation */ 63 + export const fadeOut: MotionOptions = { 64 + initial: { opacity: 1 }, 65 + animate: { opacity: 0 }, 66 + transition: { duration: motion.duration.fast / 1000 }, 67 + }; 68 + 69 + /** Fade out while sliding down */ 70 + export const fadeOutDown: MotionOptions = { 71 + initial: { opacity: 1, y: 0 }, 72 + animate: { opacity: 0, y: 20 }, 73 + transition: { duration: motion.duration.fast / 1000 }, 42 74 }; 43 75 44 76 /** Scale out */ 45 77 export const scaleOut: MotionOptions = { 46 78 initial: { opacity: 1, scale: 1 }, 47 79 animate: { opacity: 0, scale: 0.95 }, 48 - transition: { duration: 0.15 }, 80 + transition: { duration: motion.duration.fast / 1000 }, 81 + }; 82 + 83 + /** Quick scale out to zero */ 84 + export const scaleOutFast: MotionOptions = { 85 + initial: { opacity: 1, scale: 1 }, 86 + animate: { opacity: 0, scale: 0 }, 87 + transition: { duration: motion.duration.instant / 1000 }, 49 88 }; 50 89 51 - /** Stagger delay for list items */ 52 - export const staggerDelay = (index: number, baseDelay = 0.05) => index * baseDelay; 90 + /** Slide out left (for card dismissal) */ 91 + export const slideOutLeft: MotionOptions = { 92 + initial: { opacity: 1, x: 0 }, 93 + animate: { opacity: 0, x: -100 }, 94 + transition: { duration: motion.duration.normal / 1000, easing: easeOut }, 95 + }; 96 + 97 + /** Slide out right */ 98 + export const slideOutRight: MotionOptions = { 99 + initial: { opacity: 1, x: 0 }, 100 + animate: { opacity: 0, x: 100 }, 101 + transition: { duration: motion.duration.normal / 1000, easing: easeOut }, 102 + }; 53 103 54 104 /** Card flip animation (3D) */ 55 105 export const cardFlip: MotionOptions = { ··· 58 108 transition: { duration: 0.4, easing: easeOut }, 59 109 }; 60 110 61 - /** Slide out left (for card dismissal) */ 62 - export const slideOutLeft: MotionOptions = { 63 - initial: { opacity: 1, x: 0 }, 64 - animate: { opacity: 0, x: -100 }, 65 - transition: { duration: 0.25, easing: easeOut }, 66 - }; 67 - 68 111 /** Bounce in (for success feedback) */ 69 112 export const bounceIn: MotionOptions = { 70 113 initial: { opacity: 0, scale: 0.8 }, 71 114 animate: { opacity: 1, scale: 1 }, 72 - transition: { duration: 0.3, easing: [0.34, 1.56, 0.64, 1] }, 115 + transition: { duration: motion.duration.slow / 1000, easing: easeBounce }, 116 + }; 117 + 118 + /** Button press feedback - scale down slightly */ 119 + export const pressDown: MotionOptions = { 120 + initial: { scale: 1 }, 121 + animate: { scale: 0.97 }, 122 + transition: { duration: motion.duration.instant / 1000 }, 73 123 }; 124 + 125 + /** Modal backdrop fade animation */ 126 + export const modalBackdrop: MotionOptions = { 127 + initial: { opacity: 0 }, 128 + animate: { opacity: 1 }, 129 + exit: { opacity: 0 }, 130 + transition: { duration: motion.duration.normal / 1000 }, 131 + }; 132 + 133 + /** Modal content scale + fade animation */ 134 + export const modalContent: MotionOptions = { 135 + initial: { opacity: 0, scale: 0.9 }, 136 + animate: { opacity: 1, scale: 1 }, 137 + exit: { opacity: 0, scale: 0.9 }, 138 + transition: { duration: motion.duration.slow / 1000, easing: easeOut }, 139 + }; 140 + 141 + /** Stagger delay for list items */ 142 + export const staggerDelay = (index: number, baseDelay = 0.05) => index * baseDelay; 143 + 144 + /** 145 + * Create staggered animation options for a list of items 146 + */ 147 + export const createStaggeredList = ( 148 + count: number, 149 + baseAnimation: MotionOptions = fadeInUp, 150 + staggerMs = 50, 151 + ): MotionOptions[] => { 152 + return Array.from( 153 + { length: count }, 154 + (_, index) => ({ 155 + ...baseAnimation, 156 + transition: { ...baseAnimation.transition, delay: (index * staggerMs) / 1000 }, 157 + }), 158 + ); 159 + }; 160 + 161 + /** CSS class names for keyframe animations */ 162 + export const cssAnimations = { 163 + pulse: "animate-pulse", 164 + shimmer: "animate-shimmer", 165 + breathe: "animate-breathe", 166 + } as const;
+163
web/src/lib/tests/animations.test.ts
··· 1 + import { describe, expect, it } from "vitest"; 2 + import { 3 + bounceIn, 4 + cardFlip, 5 + createStaggeredList, 6 + cssAnimations, 7 + easeBounce, 8 + easeOut, 9 + fadeIn, 10 + fadeInUp, 11 + fadeOut, 12 + fadeOutDown, 13 + modalBackdrop, 14 + modalContent, 15 + pressDown, 16 + scaleIn, 17 + scaleInBounce, 18 + scaleOut, 19 + scaleOutFast, 20 + slideInLeft, 21 + slideInRight, 22 + slideInUp, 23 + slideOutLeft, 24 + slideOutRight, 25 + springConfig, 26 + staggerDelay, 27 + } from "../animations"; 28 + 29 + describe("animations", () => { 30 + describe("config", () => { 31 + it("exports spring config with stiffness and damping", () => { 32 + expect(springConfig.stiffness).toBe(300); 33 + expect(springConfig.damping).toBe(24); 34 + }); 35 + 36 + it("exports easing curves as tuples", () => { 37 + expect(easeOut).toHaveLength(4); 38 + expect(easeBounce).toHaveLength(4); 39 + }); 40 + }); 41 + 42 + describe("entrance animations", () => { 43 + it.each([ 44 + ["fadeIn", fadeIn], 45 + ["fadeInUp", fadeInUp], 46 + ["slideInRight", slideInRight], 47 + ["slideInLeft", slideInLeft], 48 + ["slideInUp", slideInUp], 49 + ["scaleIn", scaleIn], 50 + ["scaleInBounce", scaleInBounce], 51 + ])("%s has required properties", (_, preset) => { 52 + expect(preset).toHaveProperty("initial"); 53 + expect(preset).toHaveProperty("animate"); 54 + expect(preset).toHaveProperty("transition"); 55 + }); 56 + 57 + it("fadeInUp starts from y: 20", () => { 58 + expect(fadeInUp.initial).toEqual({ opacity: 0, y: 20 }); 59 + expect(fadeInUp.animate).toEqual({ opacity: 1, y: 0 }); 60 + }); 61 + 62 + it("slideInLeft starts from x: -20", () => { 63 + expect(slideInLeft.initial).toEqual({ opacity: 0, x: -20 }); 64 + }); 65 + }); 66 + 67 + describe("exit animations", () => { 68 + it.each([ 69 + ["fadeOut", fadeOut], 70 + ["fadeOutDown", fadeOutDown], 71 + ["scaleOut", scaleOut], 72 + ["scaleOutFast", scaleOutFast], 73 + ["slideOutLeft", slideOutLeft], 74 + ["slideOutRight", slideOutRight], 75 + ])("%s has required properties", (_, preset) => { 76 + expect(preset).toHaveProperty("initial"); 77 + expect(preset).toHaveProperty("animate"); 78 + expect(preset).toHaveProperty("transition"); 79 + }); 80 + 81 + it("fadeOutDown animates to y: 20", () => { 82 + expect(fadeOutDown.animate).toEqual({ opacity: 0, y: 20 }); 83 + }); 84 + 85 + it("slideOutRight animates to x: 100", () => { 86 + expect(slideOutRight.animate).toEqual({ opacity: 0, x: 100 }); 87 + }); 88 + }); 89 + 90 + describe("interactive animations", () => { 91 + it("cardFlip rotates 180 degrees", () => { 92 + expect(cardFlip.animate).toEqual({ rotateY: 180 }); 93 + }); 94 + 95 + it("bounceIn uses bounce easing", () => { 96 + expect(bounceIn.transition?.easing).toEqual(easeBounce); 97 + }); 98 + 99 + it("pressDown scales to 0.97", () => { 100 + expect(pressDown.animate).toEqual({ scale: 0.97 }); 101 + }); 102 + }); 103 + 104 + describe("modal animations", () => { 105 + it("modalBackdrop has exit property", () => { 106 + expect(modalBackdrop).toHaveProperty("exit"); 107 + expect(modalBackdrop.exit).toEqual({ opacity: 0 }); 108 + }); 109 + 110 + it("modalContent has scale and fade", () => { 111 + expect(modalContent.initial).toEqual({ opacity: 0, scale: 0.9 }); 112 + expect(modalContent.animate).toEqual({ opacity: 1, scale: 1 }); 113 + expect(modalContent.exit).toEqual({ opacity: 0, scale: 0.9 }); 114 + }); 115 + }); 116 + 117 + describe("staggerDelay", () => { 118 + it("calculates delay based on index", () => { 119 + expect(staggerDelay(0)).toBe(0); 120 + expect(staggerDelay(1)).toBe(0.05); 121 + expect(staggerDelay(5)).toBe(0.25); 122 + }); 123 + 124 + it("accepts custom base delay", () => { 125 + expect(staggerDelay(2, 0.1)).toBe(0.2); 126 + expect(staggerDelay(3, 0.02)).toBe(0.06); 127 + }); 128 + }); 129 + 130 + describe("createStaggeredList", () => { 131 + it("creates array of motion options with staggered delays", () => { 132 + const list = createStaggeredList(3); 133 + 134 + expect(list).toHaveLength(3); 135 + expect(list[0].transition?.delay).toBe(0); 136 + expect(list[1].transition?.delay).toBe(0.05); 137 + expect(list[2].transition?.delay).toBe(0.1); 138 + }); 139 + 140 + it("uses provided base animation", () => { 141 + const list = createStaggeredList(2, fadeIn); 142 + 143 + expect(list[0].initial).toEqual(fadeIn.initial); 144 + expect(list[0].animate).toEqual(fadeIn.animate); 145 + }); 146 + 147 + it("accepts custom stagger duration", () => { 148 + const list = createStaggeredList(3, fadeInUp, 100); 149 + 150 + expect(list[0].transition?.delay).toBe(0); 151 + expect(list[1].transition?.delay).toBe(0.1); 152 + expect(list[2].transition?.delay).toBe(0.2); 153 + }); 154 + }); 155 + 156 + describe("cssAnimations", () => { 157 + it("exports CSS class names for keyframe animations", () => { 158 + expect(cssAnimations.pulse).toBe("animate-pulse"); 159 + expect(cssAnimations.shimmer).toBe("animate-shimmer"); 160 + expect(cssAnimations.breathe).toBe("animate-breathe"); 161 + }); 162 + }); 163 + });