Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ export const AppContainer: React.FC<AppContainerProps> = ({ app, isVisible }) =>
const iframeRef = useRef<HTMLIFrameElement>(null);
const [hasError, setHasError] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [hasAnimatedIn, setHasAnimatedIn] = useState(false);

// Track when the app first becomes visible to trigger entrance animation
useEffect(() => {
if (isVisible && !hasAnimatedIn) {
setHasAnimatedIn(true);
}
}, [isVisible, hasAnimatedIn]);

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

return (
<div
className={`app-container fixed inset-0 dark:bg-black bg-white z-30 transition-transform duration-300
${isVisible ? 'translate-x-0' : 'translate-x-full'}`}
className={`app-container fixed inset-0 dark:bg-black bg-white z-30
${isVisible
? hasAnimatedIn ? 'animate-app-launch' : 'opacity-0'
: 'pointer-events-none opacity-0 scale-95'}`}
style={{
transition: isVisible ? 'none' : 'opacity 0.2s ease-in, transform 0.2s ease-in',
}}
>
{hasError ? (
<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">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const AppDrawer: React.FC = () => {

return (
<div
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"
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"
onClick={toggleAppDrawer}
>
<div className="px-2 py-1 self-stretch flex items-center gap-2">
Expand All @@ -59,10 +59,11 @@ export const AppDrawer: React.FC = () => {
'grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6': filteredApps.length > 0,
'grid-cols-2': filteredApps.length === 0,
})}>
{filteredApps.map(app => (
{filteredApps.map((app, index) => (
<div
key={app.id}
className="relative group"
className="relative group animate-grid-enter"
style={{ '--item-index': index } as React.CSSProperties}
data-app-id={app.id}
>
<div onClick={(e) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,38 @@ export const AppIcon: React.FC<AppIconProps> = ({
}) => {
const { openApp } = useNavigationStore();
const [isPressed, setIsPressed] = useState(false);
const [isHovered, setIsHovered] = useState(false);

const handlePress = () => {
if (!isEditMode && app.path && app.path !== null) {
openApp(app);
}
};

// Calculate scale based on state priority: pressed > hovered > default
const getScale = () => {
if (isPressed) return 'scale(0.94)';
if (isHovered && !isEditMode && isFloating) return 'scale(1.08)';
return 'scale(1)';
};

return (
<div
className={classNames('app-icon relative flex gap-1 flex-col items-center justify-center rounded-2xl cursor-pointer select-none transition-all', {
'scale-95': isPressed,
'scale-100': !isPressed,
className={classNames('app-icon relative flex gap-1 flex-col items-center justify-center rounded-2xl cursor-pointer select-none', {
'animate-wiggle': isEditMode && isFloating,
'hover:scale-110': !isEditMode && isFloating,
'opacity-50': !app.path && !(app.process && app.publisher) && !app.base64_icon,
'p-2': isUndocked,
})}
style={{
transform: getScale(),
transition: 'transform var(--duration-fast, 150ms) var(--ease-spring, cubic-bezier(0.34, 1.56, 0.64, 1))',
}}
onMouseDown={() => setIsPressed(true)}
onMouseUp={() => setIsPressed(false)}
onMouseLeave={() => setIsPressed(false)}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => { setIsPressed(false); setIsHovered(false); }}
onTouchStart={() => setIsPressed(true)}
onTouchEnd={() => setIsPressed(false)}
onClick={handlePress}
data-app-id={app.id}
data-app-path={app.path}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ export const HomeScreen: React.FC = () => {
>
{app ? (
<div
className="dock-icon"
draggable
onDragStart={(e) => {
e.dataTransfer.setData('appId', app.id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ export const Modal: React.FC<ModalProps> = ({
title
}) => {
return (
<div className={classNames("fixed inset-0 backdrop-blur-sm bg-black/10 dark:bg-black/50 flex items-center justify-center z-50", backdropClassName)}>
<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)} >
<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)}>
<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)}>
<div className="flex items-center justify-between">
{title && <h2 className="text-lg font-bold prose">{title}</h2>}
<button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,91 @@
.animate-slide-down {
animation: slide-down 0.3s ease-out;
}

/* App launch - scale up with subtle blur clear */
@keyframes app-launch {
0% {
transform: scale(0.92);
opacity: 0;
filter: blur(4px);
}
100% {
transform: scale(1);
opacity: 1;
filter: blur(0);
}
}

/* App close - scale down and fade */
@keyframes app-close {
0% {
transform: scale(1);
opacity: 1;
}
100% {
transform: scale(0.96);
opacity: 0;
}
}

/* Staggered grid item entrance */
@keyframes grid-item-enter {
0% {
opacity: 0;
transform: scale(0.85) translateY(12px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}

/* Modal entrance */
@keyframes modal-enter {
0% {
opacity: 0;
transform: scale(0.95) translateY(-8px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}

/* Modal backdrop fade with blur */
@keyframes backdrop-enter {
0% {
opacity: 0;
backdrop-filter: blur(0);
}
100% {
opacity: 1;
backdrop-filter: blur(8px);
}
}

/* OmniButton idle pulse with neon glow */
@keyframes omni-pulse {
0%, 100% {
box-shadow: 0 0 0 0 var(--neon-green-light, rgba(220, 255, 113, 0.4));
}
50% {
box-shadow: 0 0 0 10px transparent;
}
}

/* Card entrance for RecentApps */
@keyframes card-enter {
0% {
opacity: 0;
transform: translateY(20px) rotate(-2deg) scale(0.95);
}
100% {
opacity: 1;
transform: translateY(0) rotate(0) scale(1);
}
}

.animate-card-enter {
animation: card-enter 0.35s var(--ease-out, cubic-bezier(0.0, 0.0, 0.2, 1)) both;
}
71 changes: 71 additions & 0 deletions hyperdrive/packages/homepage/ui/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@
--transition-fast: 150ms ease;
--transition-normal: 250ms ease;
--button-border-width: 2px;

/* Motion design system - durations */
--duration-instant: 100ms;
--duration-fast: 150ms;
--duration-normal: 200ms;
--duration-medium: 300ms;
--duration-slow: 400ms;
--duration-emphasis: 500ms;

/* Motion design system - easing curves */
--ease-out: cubic-bezier(0.0, 0.0, 0.2, 1);
--ease-in: cubic-bezier(0.4, 0.0, 1, 1);
--ease-in-out: cubic-bezier(0.4, 0.0, 0.2, 1);
--ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1);
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}


Expand Down Expand Up @@ -520,4 +535,60 @@ footer {
.empty-state p {
text-align: center;
font-size: 14px;
}

/* Dock hover effects */
.dock-icon {
transition: transform var(--duration-fast) var(--ease-spring),
box-shadow var(--duration-fast) var(--ease-out);
}

.dock-icon:hover {
transform: translateY(-6px) scale(1.08);
box-shadow: 0 10px 20px -6px rgba(0, 0, 0, 0.25);
}

.dock-icon:active {
transform: translateY(-2px) scale(0.98);
}

/* Staggered grid entrance animation - fast for snappy feel */
.animate-grid-enter {
animation: grid-item-enter var(--duration-fast) var(--ease-out) both;
animation-delay: calc(var(--item-index, 0) * 12ms);
}

/* Modal animations */
.animate-modal-backdrop {
animation: backdrop-enter var(--duration-fast) var(--ease-out) both;
}

.animate-modal-content {
animation: modal-enter var(--duration-medium) var(--ease-out) both;
animation-delay: 50ms;
}

/* OmniButton animations */
.animate-omni-pulse {
animation: omni-pulse 2.5s ease-in-out infinite;
}

/* App container animations */
.animate-app-launch {
animation: app-launch var(--duration-slow) var(--ease-out) both;
}

.animate-app-close {
animation: app-close var(--duration-normal) var(--ease-in) both;
}

/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}