Skip to content

Commit ef1e604

Browse files
authored
fix(landing): stabilize mobile feature graphic sizing (#7649)
* fix(landing): stabilize mobile feature graphic sizing * test(landing): cover uncapped graphic scaling
1 parent e7faad3 commit ef1e604

4 files changed

Lines changed: 113 additions & 57 deletions

File tree

apps/sim/app/(landing)/components/shared/responsive-design-stage/responsive-design-stage.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,43 @@ describe('calculateFitScale', () => {
114114
})
115115

116116
describe('ResponsiveDesignStage', () => {
117+
it.each([
118+
{ width: 280, height: 400, supportsZoom: true },
119+
{ width: 280, height: 400, supportsZoom: false },
120+
{ width: 350, height: 340, supportsZoom: true },
121+
{ width: 350, height: 340, supportsZoom: false },
122+
])(
123+
'fits an uncapped $width × $height stage with zoom support: $supportsZoom',
124+
({ width, height, supportsZoom }) => {
125+
vi.stubGlobal('CSS', { supports: vi.fn(() => supportsZoom) })
126+
act(() => {
127+
root.render(
128+
createElement(
129+
ResponsiveDesignStage,
130+
{ width, height, maxScale: Number.POSITIVE_INFINITY },
131+
createElement('span', null, 'Preview')
132+
)
133+
)
134+
})
135+
136+
const surface = container.firstElementChild?.firstElementChild
137+
if (!(surface instanceof HTMLElement) || !resizeObserver) {
138+
throw new Error('responsive stage did not mount')
139+
}
140+
const observer = resizeObserver
141+
142+
act(() => observer.deliver(width * 2, height * 1.5))
143+
expect(surface.style.zoom).toBe(supportsZoom ? '1.5' : '1')
144+
expect(surface.style.transform).toBe(supportsZoom ? '' : 'scale(1.5)')
145+
expect(surface.style.opacity).toBe('1')
146+
147+
act(() => observer.deliver(width / 2, height * 2))
148+
expect(surface.style.zoom).toBe(supportsZoom ? '0.5' : '1')
149+
expect(surface.style.transform).toBe(supportsZoom ? '' : 'scale(0.5)')
150+
expect(surface.style.opacity).toBe('1')
151+
}
152+
)
153+
117154
it('hides an already visible surface until a measurable size returns', () => {
118155
act(() => {
119156
root.render(

apps/sim/app/(landing)/files/components/feature-graphics/file-library-graphic.module.css

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,9 @@
4444

4545
.folderButton {
4646
position: relative;
47-
min-width: 0;
48-
min-height: 0;
49-
container-type: size;
47+
display: block;
48+
width: 100%;
49+
height: 100%;
5050
cursor: pointer;
5151
touch-action: manipulation;
5252
border-radius: 12px;
@@ -60,11 +60,10 @@
6060
width: 321px;
6161
height: 270px;
6262
left: 50%;
63-
bottom: 12px;
63+
bottom: 0;
6464
margin-left: -160.5px;
6565
perspective: 800px;
6666
transform-origin: bottom center;
67-
scale: min(tan(atan2(100cqw, 350px)), tan(atan2(100cqh, 340px)));
6867
}
6968
.folderBack {
7069
position: absolute;

apps/sim/app/(landing)/files/components/feature-graphics/interactive-library-folder.tsx

Lines changed: 62 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { useState } from 'react'
44
import { cn } from '@sim/emcn'
55
import { motion, useReducedMotion } from 'framer-motion'
6+
import { ResponsiveDesignStage } from '@/app/(landing)/components/shared/responsive-design-stage'
67
import styles from '@/app/(landing)/files/components/feature-graphics/file-library-graphic.module.css'
78

89
/**
@@ -72,53 +73,68 @@ export function InteractiveLibraryFolder({
7273
: { type: 'spring' as const, stiffness: 120, damping: 14 }
7374

7475
return (
75-
<button
76-
type='button'
77-
className={cn(styles.folderButton, className)}
78-
aria-label={`${name}, ${count}`}
79-
aria-pressed={open}
80-
onPointerEnter={(event) => {
81-
if (event.pointerType !== 'touch') setHovered(true)
82-
}}
83-
onPointerLeave={() => setHovered(false)}
84-
onFocus={() => setHovered(true)}
85-
onBlur={() => setHovered(false)}
86-
onClick={() => setOpen((value) => !value)}
87-
onKeyDown={(event) => {
88-
if (event.key === 'Escape') setOpen(false)
89-
}}
90-
>
91-
<span aria-hidden='true' className={styles.folderScene}>
92-
<span className={styles.folderBack} />
93-
{CARDS.map((card, index) => (
94-
<motion.span
95-
key={card.x}
96-
className={styles.folderPaper}
97-
animate={{
98-
x: open ? card.openX : card.x,
99-
y: open ? card.openY : hovered ? card.hoverY : card.y,
100-
rotate: open ? card.openRotate : hovered ? card.hoverRotate : card.rotate,
101-
}}
102-
transition={{ ...transition, delay: reducedMotion ? 0 : (2 - index) * 0.05 }}
103-
>
104-
<span className={styles.paperTitle} />
105-
<span className={styles.paperLines} />
106-
</motion.span>
107-
))}
108-
<motion.span
109-
className={styles.folderFlap}
110-
animate={{ rotateX: open ? -55 : hovered ? -45 : -15 }}
111-
transition={transition}
76+
<div className={cn('relative min-h-0 min-w-0', className)}>
77+
<ResponsiveDesignStage
78+
width={350}
79+
height={340}
80+
maxScale={Number.POSITIVE_INFINITY}
81+
className='-translate-y-3 absolute inset-0 items-end overflow-visible [contain:none]'
82+
contentClassName='origin-bottom'
83+
>
84+
<button
85+
type='button'
86+
className={styles.folderButton}
87+
aria-label={`${name}, ${count}`}
88+
aria-pressed={open}
89+
onPointerEnter={(event) => {
90+
if (event.pointerType !== 'touch') setHovered(true)
91+
}}
92+
onPointerLeave={() => setHovered(false)}
93+
onFocus={() => setHovered(true)}
94+
onBlur={() => setHovered(false)}
95+
onClick={() => setOpen((value) => !value)}
96+
onKeyDown={(event) => {
97+
if (event.key === 'Escape') setOpen(false)
98+
}}
11299
>
113-
<svg className='absolute inset-0 size-full' viewBox='0 0 321 241' fill='none'>
114-
<path d={FLAP_PATH} fill='var(--surface-3)' stroke='var(--border)' strokeWidth='2' />
115-
</svg>
116-
<span className={styles.folderLabel}>
117-
<span>{name}</span>
118-
<span>{count}</span>
100+
<span aria-hidden='true' className={styles.folderScene}>
101+
<span className={styles.folderBack} />
102+
{CARDS.map((card, index) => (
103+
<motion.span
104+
key={card.x}
105+
className={styles.folderPaper}
106+
animate={{
107+
x: open ? card.openX : card.x,
108+
y: open ? card.openY : hovered ? card.hoverY : card.y,
109+
rotate: open ? card.openRotate : hovered ? card.hoverRotate : card.rotate,
110+
}}
111+
transition={{ ...transition, delay: reducedMotion ? 0 : (2 - index) * 0.05 }}
112+
>
113+
<span className={styles.paperTitle} />
114+
<span className={styles.paperLines} />
115+
</motion.span>
116+
))}
117+
<motion.span
118+
className={styles.folderFlap}
119+
animate={{ rotateX: open ? -55 : hovered ? -45 : -15 }}
120+
transition={transition}
121+
>
122+
<svg className='absolute inset-0 size-full' viewBox='0 0 321 241' fill='none'>
123+
<path
124+
d={FLAP_PATH}
125+
fill='var(--surface-3)'
126+
stroke='var(--border)'
127+
strokeWidth='2'
128+
/>
129+
</svg>
130+
<span className={styles.folderLabel}>
131+
<span>{name}</span>
132+
<span>{count}</span>
133+
</span>
134+
</motion.span>
119135
</span>
120-
</motion.span>
121-
</span>
122-
</button>
136+
</button>
137+
</ResponsiveDesignStage>
138+
</div>
123139
)
124140
}

apps/sim/app/(landing)/workflows/components/feature-graphics/workflow-canvas-graphic.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { ChipTag, cn } from '@sim/emcn'
22
import colorMixFallbacks from '@/app/(landing)/components/shared/color-mix-fallbacks/color-mix-fallbacks.module.css'
3+
import { ResponsiveDesignStage } from '@/app/(landing)/components/shared/responsive-design-stage'
34
import { FeatureGraphicShell } from '@/app/(landing)/enterprise/components/feature-graphics'
45
import styles from '@/app/(landing)/workflows/components/feature-graphics/workflow-canvas-graphic.module.css'
56

@@ -65,11 +66,14 @@ const EDGE_DRAW_CLASSES = [styles.edgeDraw0, styles.edgeDraw1, styles.edgeDraw2]
6566
export function WorkflowCanvasGraphic() {
6667
return (
6768
<FeatureGraphicShell variant='portrait'>
68-
<div
69-
aria-hidden='true'
70-
className='absolute inset-0 flex items-center justify-center p-3 [container-type:size]'
71-
>
72-
<div className='relative h-[400px] w-[280px] shrink-0 [scale:min(tan(atan2(100cqw,280px)),tan(atan2(100cqh,400px)))]'>
69+
<div aria-hidden='true' className='absolute inset-3'>
70+
<ResponsiveDesignStage
71+
width={LAYOUT.width}
72+
height={LAYOUT.height}
73+
maxScale={Number.POSITIVE_INFINITY}
74+
className='h-full w-full'
75+
contentClassName='relative'
76+
>
7377
<svg
7478
className='absolute inset-0'
7579
fill='none'
@@ -139,7 +143,7 @@ export function WorkflowCanvasGraphic() {
139143
</span>
140144
</div>
141145
))}
142-
</div>
146+
</ResponsiveDesignStage>
143147
</div>
144148
</FeatureGraphicShell>
145149
)

0 commit comments

Comments
 (0)