fix(site-builder): bounce stays visible + springier; image/video crop fills (cover) + resize shrinks footprint
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -491,3 +491,55 @@ describe('Preview body-replacement keeps exactly one reveal script (animation fi
|
||||
expect(buildAnimationScript(exportBodyHtml(plainState).html)).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* FIX: bounce entrance-animation disappears after finishing + reads like a
|
||||
* fade (see .superpowers/sdd/fix-anim-image-contract.md, section A). Root
|
||||
* cause: the old `@keyframes bounce` set opacity at 0% and 60% but NOT at
|
||||
* 100% -- with `animation-fill-mode: both`, on finish the element reverted
|
||||
* to the base `[data-animation]{opacity:0}` rule and vanished. The fix is a
|
||||
* springier keyframe that ends at `opacity:1`.
|
||||
*/
|
||||
describe('bounce keyframe ends at opacity:1 (fix-anim-image A)', () => {
|
||||
const animatedState = (animation: string) => JSON.stringify({
|
||||
ROOT: {
|
||||
type: { resolvedName: 'Container' },
|
||||
isCanvas: true,
|
||||
props: { tag: 'div', style: {}, animation },
|
||||
displayName: 'Container',
|
||||
custom: {},
|
||||
hidden: false,
|
||||
nodes: [],
|
||||
linkedNodes: {},
|
||||
},
|
||||
});
|
||||
|
||||
const NEW_BOUNCE_MINIFIED = '@keyframes bounce{0%{opacity:0;transform:translateY(40px)}40%{opacity:1;transform:translateY(-12px)}60%{transform:translateY(6px)}80%{transform:translateY(-3px)}100%{opacity:1;transform:translateY(0)}}';
|
||||
const OLD_BOUNCE_TAIL = '100%{transform:translateY(0)}}';
|
||||
|
||||
test('minified export contains the new springier bounce substring, byte-identical to the shared contract', () => {
|
||||
const { html } = exportToHtml(animatedState('bounce'), { title: 'Page' });
|
||||
expect(html).toContain(NEW_BOUNCE_MINIFIED);
|
||||
});
|
||||
|
||||
test('minified export does NOT contain the old bounce tail (100% with no opacity)', () => {
|
||||
const { html } = exportToHtml(animatedState('bounce'), { title: 'Page' });
|
||||
expect(html).not.toContain(OLD_BOUNCE_TAIL);
|
||||
// Every keyframe's 100% frame in this doc must carry opacity:1 now.
|
||||
expect(html).toContain('100%{opacity:1;transform:translateY(0)}}');
|
||||
});
|
||||
|
||||
test('pretty (non-minified) export ends the bounce keyframe at 100% { opacity: 1; transform: translateY(0); }', () => {
|
||||
const { html } = exportToHtml(animatedState('bounce'), { title: 'Page', minifyCss: false });
|
||||
const NEW_BOUNCE_PRETTY = '@keyframes bounce { 0% { opacity: 0; transform: translateY(40px); } 40% { opacity: 1; transform: translateY(-12px); } 60% { transform: translateY(6px); } 80% { transform: translateY(-3px); } 100% { opacity: 1; transform: translateY(0); } }';
|
||||
expect(html).toContain(NEW_BOUNCE_PRETTY);
|
||||
expect(html).not.toContain('100% { transform: translateY(0); } }');
|
||||
});
|
||||
|
||||
test('other keyframes (fadeIn/slideUp/zoomIn) are unchanged', () => {
|
||||
const { html } = exportToHtml(animatedState('bounce'), { title: 'Page' });
|
||||
expect(html).toContain('@keyframes fadeIn{from{opacity:0}to{opacity:1}}');
|
||||
expect(html).toContain('@keyframes slideUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}');
|
||||
expect(html).toContain('@keyframes zoomIn{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -338,7 +338,7 @@ const ANIMATION_CSS = `
|
||||
@keyframes slideLeft { from { opacity: 0; transform: translateX(-30px); } to { opacity: 1; transform: translateX(0); } }
|
||||
@keyframes slideRight { from { opacity: 0; transform: translateX(30px); } to { opacity: 1; transform: translateX(0); } }
|
||||
@keyframes zoomIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
|
||||
@keyframes bounce { 0% { opacity: 0; transform: translateY(30px); } 60% { opacity: 1; transform: translateY(-5px); } 100% { transform: translateY(0); } }
|
||||
@keyframes bounce { 0% { opacity: 0; transform: translateY(40px); } 40% { opacity: 1; transform: translateY(-12px); } 60% { transform: translateY(6px); } 80% { transform: translateY(-3px); } 100% { opacity: 1; transform: translateY(0); } }
|
||||
|
||||
[data-animation] { opacity: 0; }
|
||||
[data-animation].animated { animation-duration: 0.6s; animation-fill-mode: both; }
|
||||
@@ -349,7 +349,7 @@ const ANIMATION_CSS = `
|
||||
[data-animation="zoom-in"].animated { animation-name: zoomIn; }
|
||||
[data-animation="bounce"].animated { animation-name: bounce; }`;
|
||||
|
||||
const ANIMATION_CSS_MINIFIED = `@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes slideUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@keyframes slideLeft{from{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:translateX(0)}}@keyframes slideRight{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@keyframes zoomIn{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}@keyframes bounce{0%{opacity:0;transform:translateY(30px)}60%{opacity:1;transform:translateY(-5px)}100%{transform:translateY(0)}}[data-animation]{opacity:0}[data-animation].animated{animation-duration:.6s;animation-fill-mode:both}[data-animation="fade-in"].animated{animation-name:fadeIn}[data-animation="slide-up"].animated{animation-name:slideUp}[data-animation="slide-left"].animated{animation-name:slideLeft}[data-animation="slide-right"].animated{animation-name:slideRight}[data-animation="zoom-in"].animated{animation-name:zoomIn}[data-animation="bounce"].animated{animation-name:bounce}`;
|
||||
const ANIMATION_CSS_MINIFIED = `@keyframes fadeIn{from{opacity:0}to{opacity:1}}@keyframes slideUp{from{opacity:0;transform:translateY(30px)}to{opacity:1;transform:translateY(0)}}@keyframes slideLeft{from{opacity:0;transform:translateX(-30px)}to{opacity:1;transform:translateX(0)}}@keyframes slideRight{from{opacity:0;transform:translateX(30px)}to{opacity:1;transform:translateX(0)}}@keyframes zoomIn{from{opacity:0;transform:scale(.9)}to{opacity:1;transform:scale(1)}}@keyframes bounce{0%{opacity:0;transform:translateY(40px)}40%{opacity:1;transform:translateY(-12px)}60%{transform:translateY(6px)}80%{transform:translateY(-3px)}100%{opacity:1;transform:translateY(0)}}[data-animation]{opacity:0}[data-animation].animated{animation-duration:.6s;animation-fill-mode:both}[data-animation="fade-in"].animated{animation-name:fadeIn}[data-animation="slide-up"].animated{animation-name:slideUp}[data-animation="slide-left"].animated{animation-name:slideLeft}[data-animation="slide-right"].animated{animation-name:slideRight}[data-animation="zoom-in"].animated{animation-name:zoomIn}[data-animation="bounce"].animated{animation-name:bounce}`;
|
||||
|
||||
const ANIMATION_SCRIPT = `<script>
|
||||
document.querySelectorAll('[data-animation]').forEach(function(el) {
|
||||
|
||||
Reference in New Issue
Block a user