diff --git a/package.json b/package.json index 871468a..0acaa84 100644 --- a/package.json +++ b/package.json @@ -1,20 +1,22 @@ { "name": "@switchev/web-shared", - "version": "0.5.0", - "description": "Shared browser helpers (formatters, markdown, image polling, combobox) for the SwitchEV frontends", + "version": "0.6.0", + "description": "Shared browser helpers (formatters, markdown, image polling, combobox, animations) for the SwitchEV frontends", "type": "module", "exports": { ".": "./src/index.js", "./format": "./src/format.js", "./markdown": "./src/markdown.js", "./wait-image": "./src/wait-image.js", - "./combobox": "./src/combobox.js" + "./combobox": "./src/combobox.js", + "./animations.css": "./src/animations.css" }, "files": [ "src" ], "sideEffects": [ - "./src/markdown.js" + "./src/markdown.js", + "./src/animations.css" ], "dependencies": { "fuzzysort": "^3.1.0" diff --git a/src/animations.css b/src/animations.css new file mode 100644 index 0000000..47b2a18 --- /dev/null +++ b/src/animations.css @@ -0,0 +1,70 @@ +/* @switchev/web-shared — shared animation primitives (SEV-337). + * + * Plain CSS (no Tailwind utilities), so it can be imported directly by each + * SwitchEV frontend instead of copy-pasting the same keyframes + helper classes + * into every app's style.css. Import once per app (e.g. in main.js): + * import '@switchev/web-shared/animations.css'; + * App-specific, non-animation styles (brand theme, star ratings, gradient text) + * stay in each app's own stylesheet. + */ + +/* Skeleton loading shimmer */ +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +.skeleton { + background: linear-gradient(90deg, #e5e7eb 25%, #f3f4f6 50%, #e5e7eb 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; + border-radius: 0.375rem; +} + +/* SEV-238: AI-working shimmer — applied to a field while an AI draft is in + flight so the disabled state reads as "AI is writing here", not broken. + Reuses the skeleton shimmer keyframes with a brand-green tint. */ +.ai-shimmer { + background-image: linear-gradient(90deg, + rgba(22, 163, 74, 0.06) 25%, + rgba(22, 163, 74, 0.18) 50%, + rgba(22, 163, 74, 0.06) 75%); + background-size: 200% 100%; + animation: shimmer 1.2s ease-in-out infinite; +} + +/* Fade-in */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.fade-in { + animation: fadeIn 0.5s ease-out both; +} + +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +.fade-in-up { + animation: fadeInUp 0.6s ease-out both; +} + +/* Staggered animation delays */ +.delay-100 { animation-delay: 0.1s; } +.delay-200 { animation-delay: 0.2s; } +.delay-300 { animation-delay: 0.3s; } +.delay-400 { animation-delay: 0.4s; } + +/* Shake — invalid input (e.g. a wrong verification code) */ +@keyframes shake { + 0%, 100% { transform: translateX(0); } + 10%, 30%, 50%, 70%, 90% { transform: translateX(-4px); } + 20%, 40%, 60%, 80% { transform: translateX(4px); } +} + +.shake { + animation: shake 0.5s ease-in-out; +}