Skip to content

Commit 17aca2a

Browse files
committed
Merge branch 'develop' into dw/direct-ip
2 parents 842d72a + e4a783a commit 17aca2a

15 files changed

Lines changed: 508 additions & 85 deletions

File tree

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "hyperdrive_lib"
33
authors = ["Sybil Technologies AG"]
4-
version = "1.9.2"
4+
version = "1.9.3"
55
edition = "2021"
66
description = "A general-purpose sovereign cloud computing platform"
77
homepage = "https://hyperware.ai"

hyperdrive/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "hyperdrive"
33
authors = ["Sybil Technologies AG"]
4-
version = "1.9.2"
4+
version = "1.9.3"
55
edition = "2021"
66
description = "A general-purpose sovereign cloud computing platform"
77
homepage = "https://hyperware.ai"

hyperdrive/packages/homepage/homepage/src/lib.rs

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,65 @@ enum NotificationsResponse {
6363
Err(String),
6464
}
6565

66+
/// WebSocket message types sent to frontend
67+
#[derive(Serialize, Deserialize, Debug, Clone)]
68+
#[serde(tag = "kind", content = "data")]
69+
#[serde(rename_all = "snake_case")]
70+
enum WsMessage {
71+
AppsUpdate(Vec<WsApp>),
72+
}
73+
74+
/// App data sent over WebSocket (mirrors HomepageApp in TypeScript)
75+
#[derive(Serialize, Deserialize, Debug, Clone)]
76+
struct WsApp {
77+
id: String,
78+
process: String,
79+
package_name: String,
80+
publisher: String,
81+
#[serde(skip_serializing_if = "Option::is_none")]
82+
path: Option<String>,
83+
label: String,
84+
#[serde(skip_serializing_if = "Option::is_none")]
85+
base64_icon: Option<String>,
86+
#[serde(skip_serializing_if = "Option::is_none")]
87+
widget: Option<String>,
88+
order: u32,
89+
favorite: bool,
90+
}
91+
92+
impl From<&homepage::App> for WsApp {
93+
fn from(app: &homepage::App) -> Self {
94+
WsApp {
95+
id: app.id.clone(),
96+
process: app.process.clone(),
97+
package_name: app.package_name.clone(),
98+
publisher: app.publisher.clone(),
99+
path: app.path.clone(),
100+
label: app.label.clone(),
101+
base64_icon: app.base64_icon.clone(),
102+
widget: app.widget.clone(),
103+
order: app.order,
104+
favorite: app.favorite,
105+
}
106+
}
107+
}
108+
109+
/// Helper function to broadcast app updates to all WebSocket clients
110+
fn broadcast_apps_update(
111+
http_server: &server::HttpServer,
112+
app_data: &BTreeMap<String, homepage::App>,
113+
) {
114+
let apps: Vec<WsApp> = app_data.values().map(WsApp::from).collect();
115+
let message = WsMessage::AppsUpdate(apps);
116+
let json = serde_json::to_string(&message).unwrap();
117+
118+
http_server.ws_push_all_channels(
119+
"/",
120+
http::server::WsMessageType::Text,
121+
LazyLoadBlob::new(Some("application/json"), json.into_bytes()),
122+
);
123+
}
124+
66125
wit_bindgen::generate!({
67126
path: "../target/wit",
68127
world: "homepage-sys-v1",
@@ -295,6 +354,10 @@ fn init(our: Address) {
295354
.bind_http_path("/api/notifications/test-vapid", http_config)
296355
.expect("failed to bind /api/notifications/test-vapid");
297356

357+
http_server
358+
.bind_ws_path("/", server::WsBindingConfig::default())
359+
.expect("failed to bind ws path");
360+
298361
hyperware_process_lib::homepage::add_to_homepage(
299362
"Clock",
300363
None,
@@ -319,6 +382,23 @@ fn init(our: Address) {
319382
let Ok(request) = http_server.parse_request(message.body()) else {
320383
continue;
321384
};
385+
// Handle WebSocket events
386+
match &request {
387+
http::server::HttpServerRequest::WebSocketOpen { path, channel_id } => {
388+
http_server.handle_websocket_open(path, *channel_id);
389+
continue;
390+
}
391+
http::server::HttpServerRequest::WebSocketClose(channel_id) => {
392+
http_server.handle_websocket_close(*channel_id);
393+
continue;
394+
}
395+
http::server::HttpServerRequest::WebSocketPush { .. } => {
396+
continue;
397+
}
398+
http::server::HttpServerRequest::Http(_) => {
399+
// Fall through to handle_request for HTTP
400+
}
401+
}
322402
http_server.handle_request(
323403
request,
324404
|incoming| {
@@ -955,11 +1035,13 @@ fn init(our: Address) {
9551035
.contains(&message.source().process.to_string().as_str()),
9561036
},
9571037
);
1038+
broadcast_apps_update(&http_server, &app_data);
9581039
}
9591040
homepage::Request::Remove => {
9601041
let id = message.source().process.to_string();
9611042
app_data.remove(&id);
9621043
persisted_app_order.remove(&id);
1044+
broadcast_apps_update(&http_server, &app_data);
9631045
}
9641046
homepage::Request::RemoveOther(id) => {
9651047
// caps check
@@ -973,6 +1055,7 @@ fn init(our: Address) {
9731055
// end caps check
9741056
app_data.remove(&id);
9751057
persisted_app_order.remove(&id);
1058+
broadcast_apps_update(&http_server, &app_data);
9761059
}
9771060
homepage::Request::GetApps => {
9781061
let apps = app_data.values().cloned().collect::<Vec<homepage::App>>();

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+
}

0 commit comments

Comments
 (0)