Skip to content

Commit 014bb53

Browse files
committed
homepage: add animations
1 parent b88c126 commit 014bb53

7 files changed

Lines changed: 198 additions & 12 deletions

File tree

hyperdrive/packages/homepage/ui/src/components/Home/components/AppContainer.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,14 @@ export const AppContainer: React.FC<AppContainerProps> = ({ app, isVisible }) =>
1010
const iframeRef = useRef<HTMLIFrameElement>(null);
1111
const [hasError, setHasError] = useState(false);
1212
const [isLoading, setIsLoading] = useState(true);
13+
const [hasAnimatedIn, setHasAnimatedIn] = useState(false);
14+
15+
// Track when the app first becomes visible to trigger entrance animation
16+
useEffect(() => {
17+
if (isVisible && !hasAnimatedIn) {
18+
setHasAnimatedIn(true);
19+
}
20+
}, [isVisible, hasAnimatedIn]);
1321

1422
// Ensure we have a valid path
1523
const appUrl = useMemo(() => {
@@ -29,8 +37,13 @@ export const AppContainer: React.FC<AppContainerProps> = ({ app, isVisible }) =>
2937

3038
return (
3139
<div
32-
className={`app-container fixed inset-0 dark:bg-black bg-white z-30 transition-transform duration-300
33-
${isVisible ? 'translate-x-0' : 'translate-x-full'}`}
40+
className={`app-container fixed inset-0 dark:bg-black bg-white z-30
41+
${isVisible
42+
? hasAnimatedIn ? 'animate-app-launch' : 'opacity-0'
43+
: 'pointer-events-none opacity-0 scale-95'}`}
44+
style={{
45+
transition: isVisible ? 'none' : 'opacity 0.2s ease-in, transform 0.2s ease-in',
46+
}}
3447
>
3548
{hasError ? (
3649
<div className="w-full h-full flex flex-col items-center justify-center bg-gradient-to-b from-gray-100 to-gray-200 dark:from-gray-800 dark:to-gray-900">

hyperdrive/packages/homepage/ui/src/components/Home/components/AppDrawer.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ export const AppDrawer: React.FC = () => {
3333

3434
return (
3535
<div
36-
className="app-drawer fixed inset-0 bg-gradient-to-b from-gray-100/20 to-white/20 dark:from-gray-900/20 dark:to-black/20 backdrop-blur-xl z-50 flex flex-col"
36+
className="app-drawer fixed inset-0 bg-gradient-to-b from-gray-100/20 to-white/20 dark:from-gray-900/20 dark:to-black/20 backdrop-blur-xl z-50 flex flex-col animate-modal-backdrop"
3737
onClick={toggleAppDrawer}
3838
>
3939
<div className="px-2 py-1 self-stretch flex items-center gap-2">
@@ -59,10 +59,11 @@ export const AppDrawer: React.FC = () => {
5959
'grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6': filteredApps.length > 0,
6060
'grid-cols-2': filteredApps.length === 0,
6161
})}>
62-
{filteredApps.map(app => (
62+
{filteredApps.map((app, index) => (
6363
<div
6464
key={app.id}
65-
className="relative group"
65+
className="relative group animate-grid-enter"
66+
style={{ '--item-index': index } as React.CSSProperties}
6667
data-app-id={app.id}
6768
>
6869
<div onClick={(e) => {

hyperdrive/packages/homepage/ui/src/components/Home/components/AppIcon.tsx

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,26 +18,38 @@ export const AppIcon: React.FC<AppIconProps> = ({
1818
}) => {
1919
const { openApp } = useNavigationStore();
2020
const [isPressed, setIsPressed] = useState(false);
21+
const [isHovered, setIsHovered] = useState(false);
2122

2223
const handlePress = () => {
2324
if (!isEditMode && app.path && app.path !== null) {
2425
openApp(app);
2526
}
2627
};
2728

29+
// Calculate scale based on state priority: pressed > hovered > default
30+
const getScale = () => {
31+
if (isPressed) return 'scale(0.94)';
32+
if (isHovered && !isEditMode && isFloating) return 'scale(1.08)';
33+
return 'scale(1)';
34+
};
35+
2836
return (
2937
<div
30-
className={classNames('app-icon relative flex gap-1 flex-col items-center justify-center rounded-2xl cursor-pointer select-none transition-all', {
31-
'scale-95': isPressed,
32-
'scale-100': !isPressed,
38+
className={classNames('app-icon relative flex gap-1 flex-col items-center justify-center rounded-2xl cursor-pointer select-none', {
3339
'animate-wiggle': isEditMode && isFloating,
34-
'hover:scale-110': !isEditMode && isFloating,
3540
'opacity-50': !app.path && !(app.process && app.publisher) && !app.base64_icon,
3641
'p-2': isUndocked,
3742
})}
43+
style={{
44+
transform: getScale(),
45+
transition: 'transform var(--duration-fast, 150ms) var(--ease-spring, cubic-bezier(0.34, 1.56, 0.64, 1))',
46+
}}
3847
onMouseDown={() => setIsPressed(true)}
3948
onMouseUp={() => setIsPressed(false)}
40-
onMouseLeave={() => setIsPressed(false)}
49+
onMouseEnter={() => setIsHovered(true)}
50+
onMouseLeave={() => { setIsPressed(false); setIsHovered(false); }}
51+
onTouchStart={() => setIsPressed(true)}
52+
onTouchEnd={() => setIsPressed(false)}
4153
onClick={handlePress}
4254
data-app-id={app.id}
4355
data-app-path={app.path}

hyperdrive/packages/homepage/ui/src/components/Home/components/HomeScreen.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,7 @@ export const HomeScreen: React.FC = () => {
430430
>
431431
{app ? (
432432
<div
433+
className="dock-icon"
433434
draggable
434435
onDragStart={(e) => {
435436
e.dataTransfer.setData('appId', app.id);

hyperdrive/packages/homepage/ui/src/components/Home/components/Modal.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ export const Modal: React.FC<ModalProps> = ({
1818
title
1919
}) => {
2020
return (
21-
<div className={classNames("fixed inset-0 backdrop-blur-sm bg-black/10 dark:bg-black/50 flex items-center justify-center z-50", backdropClassName)}>
22-
<div className={classNames("bg-white dark:bg-black shadow-lg dark:shadow-white/10 p-4 rounded-lg relative w-full max-w-screen md:max-w-md min-h-0 max-h-screen overflow-y-auto flex flex-col items-stretch gap-4", modalClassName)} >
21+
<div className={classNames("fixed inset-0 backdrop-blur-sm bg-black/10 dark:bg-black/50 flex items-center justify-center z-50 animate-modal-backdrop", backdropClassName)}>
22+
<div className={classNames("bg-white dark:bg-black shadow-lg dark:shadow-white/10 p-4 rounded-lg relative w-full max-w-screen md:max-w-md min-h-0 max-h-screen overflow-y-auto flex flex-col items-stretch gap-4 animate-modal-content", modalClassName)}>
2323
<div className="flex items-center justify-between">
2424
{title && <h2 className="text-lg font-bold prose">{title}</h2>}
2525
<button

hyperdrive/packages/homepage/ui/src/components/Home/styles/animations.css

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,91 @@
2121
.animate-slide-down {
2222
animation: slide-down 0.3s ease-out;
2323
}
24+
25+
/* App launch - scale up with subtle blur clear */
26+
@keyframes app-launch {
27+
0% {
28+
transform: scale(0.92);
29+
opacity: 0;
30+
filter: blur(4px);
31+
}
32+
100% {
33+
transform: scale(1);
34+
opacity: 1;
35+
filter: blur(0);
36+
}
37+
}
38+
39+
/* App close - scale down and fade */
40+
@keyframes app-close {
41+
0% {
42+
transform: scale(1);
43+
opacity: 1;
44+
}
45+
100% {
46+
transform: scale(0.96);
47+
opacity: 0;
48+
}
49+
}
50+
51+
/* Staggered grid item entrance */
52+
@keyframes grid-item-enter {
53+
0% {
54+
opacity: 0;
55+
transform: scale(0.85) translateY(12px);
56+
}
57+
100% {
58+
opacity: 1;
59+
transform: scale(1) translateY(0);
60+
}
61+
}
62+
63+
/* Modal entrance */
64+
@keyframes modal-enter {
65+
0% {
66+
opacity: 0;
67+
transform: scale(0.95) translateY(-8px);
68+
}
69+
100% {
70+
opacity: 1;
71+
transform: scale(1) translateY(0);
72+
}
73+
}
74+
75+
/* Modal backdrop fade with blur */
76+
@keyframes backdrop-enter {
77+
0% {
78+
opacity: 0;
79+
backdrop-filter: blur(0);
80+
}
81+
100% {
82+
opacity: 1;
83+
backdrop-filter: blur(8px);
84+
}
85+
}
86+
87+
/* OmniButton idle pulse with neon glow */
88+
@keyframes omni-pulse {
89+
0%, 100% {
90+
box-shadow: 0 0 0 0 var(--neon-green-light, rgba(220, 255, 113, 0.4));
91+
}
92+
50% {
93+
box-shadow: 0 0 0 10px transparent;
94+
}
95+
}
96+
97+
/* Card entrance for RecentApps */
98+
@keyframes card-enter {
99+
0% {
100+
opacity: 0;
101+
transform: translateY(20px) rotate(-2deg) scale(0.95);
102+
}
103+
100% {
104+
opacity: 1;
105+
transform: translateY(0) rotate(0) scale(1);
106+
}
107+
}
108+
109+
.animate-card-enter {
110+
animation: card-enter 0.35s var(--ease-out, cubic-bezier(0.0, 0.0, 0.2, 1)) both;
111+
}

hyperdrive/packages/homepage/ui/src/index.css

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,21 @@
4646
--transition-fast: 150ms ease;
4747
--transition-normal: 250ms ease;
4848
--button-border-width: 2px;
49+
50+
/* Motion design system - durations */
51+
--duration-instant: 100ms;
52+
--duration-fast: 150ms;
53+
--duration-normal: 200ms;
54+
--duration-medium: 300ms;
55+
--duration-slow: 400ms;
56+
--duration-emphasis: 500ms;
57+
58+
/* Motion design system - easing curves */
59+
--ease-out: cubic-bezier(0.0, 0.0, 0.2, 1);
60+
--ease-in: cubic-bezier(0.4, 0.0, 1, 1);
61+
--ease-in-out: cubic-bezier(0.4, 0.0, 0.2, 1);
62+
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
63+
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
4964
}
5065

5166

@@ -520,4 +535,60 @@ footer {
520535
.empty-state p {
521536
text-align: center;
522537
font-size: 14px;
538+
}
539+
540+
/* Dock hover effects */
541+
.dock-icon {
542+
transition: transform var(--duration-fast) var(--ease-spring),
543+
box-shadow var(--duration-fast) var(--ease-out);
544+
}
545+
546+
.dock-icon:hover {
547+
transform: translateY(-6px) scale(1.08);
548+
box-shadow: 0 10px 20px -6px rgba(0, 0, 0, 0.25);
549+
}
550+
551+
.dock-icon:active {
552+
transform: translateY(-2px) scale(0.98);
553+
}
554+
555+
/* Staggered grid entrance animation - fast for snappy feel */
556+
.animate-grid-enter {
557+
animation: grid-item-enter var(--duration-fast) var(--ease-out) both;
558+
animation-delay: calc(var(--item-index, 0) * 12ms);
559+
}
560+
561+
/* Modal animations */
562+
.animate-modal-backdrop {
563+
animation: backdrop-enter var(--duration-fast) var(--ease-out) both;
564+
}
565+
566+
.animate-modal-content {
567+
animation: modal-enter var(--duration-medium) var(--ease-out) both;
568+
animation-delay: 50ms;
569+
}
570+
571+
/* OmniButton animations */
572+
.animate-omni-pulse {
573+
animation: omni-pulse 2.5s ease-in-out infinite;
574+
}
575+
576+
/* App container animations */
577+
.animate-app-launch {
578+
animation: app-launch var(--duration-slow) var(--ease-out) both;
579+
}
580+
581+
.animate-app-close {
582+
animation: app-close var(--duration-normal) var(--ease-in) both;
583+
}
584+
585+
/* Reduced motion support */
586+
@media (prefers-reduced-motion: reduce) {
587+
*,
588+
*::before,
589+
*::after {
590+
animation-duration: 0.01ms !important;
591+
animation-iteration-count: 1 !important;
592+
transition-duration: 0.01ms !important;
593+
}
523594
}

0 commit comments

Comments
 (0)