new file: client/src/lib/embed-utils.ts

This commit is contained in:
MrPiglr 2026-02-03 10:25:47 -07:00
parent ad5f15271e
commit 293d3c0d02
24 changed files with 782 additions and 421 deletions

View file

@ -1,14 +1,5 @@
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
// Copy web assets to the native project
task copyWebApp(type: Copy) {
from '../../dist'
into 'src/main/assets/public'
}
// Before building the app, run the copyWebApp task
preBuild.dependsOn copyWebApp
android {
compileOptions {
sourceCompatibility JavaVersion.VERSION_21
@ -36,6 +27,7 @@ dependencies {
implementation project(':capacitor-splash-screen')
implementation project(':capacitor-status-bar')
implementation project(':capacitor-toast')
implementation project(':capacitor-native-biometric')
}

View file

@ -37,6 +37,22 @@
</application>
<!-- Permissions -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<!-- Hardware features -->
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
<uses-feature android:name="android.hardware.fingerprint" android:required="false" />
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
</manifest>

View file

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- AeThex Brand Colors -->
<color name="colorPrimary">#DC2626</color>
<color name="colorPrimaryDark">#0a0a0a</color>
<color name="colorAccent">#D4AF37</color>
<!-- Splash Screen -->
<color name="splash_background">#0a0a0a</color>
<!-- Status Bar -->
<color name="status_bar">#0a0a0a</color>
<!-- Navigation Bar -->
<color name="navigation_bar">#0a0a0a</color>
<!-- Foundation Theme -->
<color name="foundation_primary">#DC2626</color>
<color name="foundation_gold">#D4AF37</color>
<color name="foundation_dark">#1a0505</color>
<!-- Corp Theme -->
<color name="corp_primary">#3B82F6</color>
<color name="corp_silver">#C0C0C0</color>
<color name="corp_dark">#0f172a</color>
</resources>

View file

@ -2,21 +2,27 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowBackground">@color/splash_background</item>
<item name="android:statusBarColor">@color/status_bar</item>
<item name="android:navigationBarColor">@color/navigation_bar</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:background">@null</item>
<item name="android:statusBarColor">@color/status_bar</item>
<item name="android:navigationBarColor">@color/navigation_bar</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
<item name="android:background">@drawable/splash</item>
<item name="android:statusBarColor">@color/splash_background</item>
<item name="android:navigationBarColor">@color/splash_background</item>
</style>
</resources>

View file

@ -55,3 +55,6 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacit
include ':capacitor-toast'
project(':capacitor-toast').projectDir = new File('../node_modules/@capacitor/toast/android')
include ':capacitor-native-biometric'
project(':capacitor-native-biometric').projectDir = new File('../node_modules/capacitor-native-biometric/android')

View file

@ -20,22 +20,23 @@ const config: CapacitorConfig = {
},
plugins: {
SplashScreen: {
launchShowDuration: 0,
launchShowDuration: 2000,
launchAutoHide: true,
backgroundColor: '#000000',
launchFadeOutDuration: 500,
backgroundColor: '#0a0a0a',
androidSplashResourceName: 'splash',
androidScaleType: 'CENTER_CROP',
showSpinner: false,
androidSpinnerStyle: 'large',
showSpinner: true,
androidSpinnerStyle: 'small',
iosSpinnerStyle: 'small',
spinnerColor: '#999999',
spinnerColor: '#DC2626',
splashFullScreen: true,
splashImmersive: true
},
StatusBar: {
style: 'DARK',
backgroundColor: '#000000',
overlaysWebView: true
backgroundColor: '#0a0a0a',
overlaysWebView: false
},
App: {
backButtonEnabled: true
@ -45,8 +46,18 @@ const config: CapacitorConfig = {
},
LocalNotifications: {
smallIcon: 'ic_stat_icon_config_sample',
iconColor: '#488AFF',
iconColor: '#DC2626',
sound: 'beep.wav'
},
Keyboard: {
resize: 'body',
resizeOnFullScreen: true,
style: 'dark'
},
Haptics: {
selectionStart: true,
selectionChanged: true,
selectionEnd: true
}
},
android: {

View file

@ -5,6 +5,7 @@ import { Toaster } from "@/components/ui/toaster";
import { AuthProvider } from "@/lib/auth";
import { TutorialProvider } from "@/components/Tutorial";
import { ProtectedRoute } from "@/components/ProtectedRoute";
import { HapticProvider } from "@/components/mobile/HapticFeedback";
import NotFound from "@/pages/not-found";
import Home from "@/pages/home";
import Passport from "@/pages/passport";
@ -114,12 +115,14 @@ function App() {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<LabTerminalProvider>
<TutorialProvider>
<Toaster />
<Router />
</TutorialProvider>
</LabTerminalProvider>
<HapticProvider enableGlobalTouchFeedback={true}>
<LabTerminalProvider>
<TutorialProvider>
<Toaster />
<Router />
</TutorialProvider>
</LabTerminalProvider>
</HapticProvider>
</AuthProvider>
</QueryClientProvider>
);

View file

@ -0,0 +1,37 @@
import * as React from "react";
import { Button, ButtonProps } from "@/components/ui/button";
import { useHaptics } from "@/hooks/use-haptics";
export interface HapticButtonProps extends ButtonProps {
hapticStyle?: 'light' | 'medium' | 'heavy';
hapticOnPress?: boolean;
}
/**
* Button wrapper that adds haptic feedback on mobile devices.
* Falls back gracefully to normal button on web.
*/
export const HapticButton = React.forwardRef<HTMLButtonElement, HapticButtonProps>(
({ hapticStyle = 'light', hapticOnPress = true, onClick, ...props }, ref) => {
const haptics = useHaptics();
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
if (hapticOnPress) {
haptics.impact(hapticStyle);
}
onClick?.(e);
};
return (
<Button
ref={ref}
onClick={handleClick}
{...props}
/>
);
}
);
HapticButton.displayName = "HapticButton";
export default HapticButton;

View file

@ -0,0 +1,90 @@
import React, { createContext, useContext, useEffect } from 'react';
import { useHaptics } from '@/hooks/use-haptics';
import { isMobile } from '@/lib/platform';
interface HapticContextValue {
triggerImpact: (style?: 'light' | 'medium' | 'heavy') => void;
triggerNotification: (type?: 'success' | 'warning' | 'error') => void;
triggerSelection: () => void;
}
const HapticContext = createContext<HapticContextValue>({
triggerImpact: () => {},
triggerNotification: () => {},
triggerSelection: () => {},
});
export function useHapticFeedback() {
return useContext(HapticContext);
}
interface HapticProviderProps {
children: React.ReactNode;
enableGlobalTouchFeedback?: boolean;
}
/**
* Provider that enables haptic feedback throughout the app.
* Wrap your app with this to enable automatic haptics on touch events.
*/
export function HapticProvider({ children, enableGlobalTouchFeedback = true }: HapticProviderProps) {
const haptics = useHaptics();
// Add global touch feedback for interactive elements
useEffect(() => {
if (!enableGlobalTouchFeedback || !isMobile()) return;
const handleTouchStart = (e: TouchEvent) => {
const target = e.target as HTMLElement;
// Check if the touched element is interactive
const isInteractive =
target.tagName === 'BUTTON' ||
target.tagName === 'A' ||
target.closest('button') ||
target.closest('a') ||
target.closest('[role="button"]') ||
target.closest('[data-haptic]');
if (isInteractive) {
haptics.impact('light');
}
};
document.addEventListener('touchstart', handleTouchStart, { passive: true });
return () => document.removeEventListener('touchstart', handleTouchStart);
}, [enableGlobalTouchFeedback, haptics]);
const value: HapticContextValue = {
triggerImpact: (style = 'medium') => haptics.impact(style),
triggerNotification: (type = 'success') => haptics.notification(type),
triggerSelection: () => haptics.selectionChanged(),
};
return (
<HapticContext.Provider value={value}>
{children}
</HapticContext.Provider>
);
}
/**
* HOC to add haptic feedback to any component
*/
export function withHapticFeedback<P extends object>(
WrappedComponent: React.ComponentType<P>,
hapticStyle: 'light' | 'medium' | 'heavy' = 'light'
) {
return function HapticWrapper(props: P & { onClick?: (e: React.MouseEvent) => void }) {
const haptics = useHaptics();
const handleClick = (e: React.MouseEvent) => {
haptics.impact(hapticStyle);
props.onClick?.(e);
};
return <WrappedComponent {...props} onClick={handleClick} />;
};
}
export default HapticProvider;

View file

@ -8,6 +8,15 @@ interface MobileHeaderProps {
backPath?: string;
}
// Check if we're inside an iframe (embedded in the OS)
const isEmbedded = () => {
try {
return window.self !== window.top;
} catch (e) {
return true; // If we can't access parent, assume embedded
}
};
export function MobileHeader({
title = 'AeThex OS',
onMenuClick,
@ -16,6 +25,11 @@ export function MobileHeader({
}: MobileHeaderProps) {
const [, navigate] = useLocation();
// Don't render the header if we're embedded inside the OS iframe
if (isEmbedded()) {
return null;
}
return (
<div className="fixed top-0 left-0 right-0 z-50 bg-black/95 backdrop-blur-xl border-b border-emerald-500/30">
<div className="flex items-center justify-between px-4 py-3 safe-area-inset-top">

View file

@ -1,14 +1,15 @@
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { Capacitor } from '@capacitor/core';
// Note: Biometric auth requires native-auth plugin or similar
// For now we'll create the interface and you can install the plugin later
import { NativeBiometric, BiometryType } from 'capacitor-native-biometric';
interface BiometricAuthResult {
isAvailable: boolean;
biometricType: 'fingerprint' | 'face' | 'iris' | 'none';
authenticate: () => Promise<boolean>;
authenticate: (reason?: string) => Promise<boolean>;
isAuthenticated: boolean;
checkCredentials: () => Promise<boolean>;
setCredentials: (username: string, password: string) => Promise<void>;
deleteCredentials: () => Promise<void>;
}
export function useBiometricAuth(): BiometricAuthResult {
@ -23,31 +24,46 @@ export function useBiometricAuth(): BiometricAuthResult {
return;
}
// Check if biometrics are available
// This would use @capacitor-community/native-biometric or similar
// For now, we'll assume it's available on mobile
setIsAvailable(true);
setBiometricType('fingerprint'); // Default assumption
try {
const result = await NativeBiometric.isAvailable();
setIsAvailable(result.isAvailable);
// Map biometry type
switch (result.biometryType) {
case BiometryType.FINGERPRINT:
case BiometryType.TOUCH_ID:
setBiometricType('fingerprint');
break;
case BiometryType.FACE_ID:
case BiometryType.FACE_AUTHENTICATION:
setBiometricType('face');
break;
case BiometryType.IRIS_AUTHENTICATION:
setBiometricType('iris');
break;
default:
setBiometricType('none');
}
} catch (error) {
console.error('Biometric availability check failed:', error);
setIsAvailable(false);
}
};
checkAvailability();
}, []);
const authenticate = async (): Promise<boolean> => {
const authenticate = useCallback(async (reason?: string): Promise<boolean> => {
if (!isAvailable) return false;
try {
// This is where you'd call the actual biometric auth
// For example with @capacitor-community/native-biometric:
// const result = await NativeBiometric.verifyIdentity({
// reason: "Authenticate to access AeThex OS",
// title: "Biometric Authentication",
// subtitle: "Use your fingerprint or face",
// description: "Please authenticate"
// });
await NativeBiometric.verifyIdentity({
reason: reason || 'Authenticate to access AeThex OS',
title: 'Biometric Authentication',
subtitle: 'Use your fingerprint or face',
description: 'Verify your identity to continue',
});
// For now, simulate success
console.log('Biometric auth would trigger here');
setIsAuthenticated(true);
return true;
} catch (error) {
@ -55,12 +71,43 @@ export function useBiometricAuth(): BiometricAuthResult {
setIsAuthenticated(false);
return false;
}
};
}, [isAvailable]);
const checkCredentials = useCallback(async (): Promise<boolean> => {
if (!Capacitor.isNativePlatform()) return false;
try {
await NativeBiometric.getCredentials({ server: 'com.aethex.os' });
return true;
} catch {
return false;
}
}, []);
const setCredentials = useCallback(async (username: string, password: string): Promise<void> => {
if (!Capacitor.isNativePlatform()) return;
await NativeBiometric.setCredentials({
server: 'com.aethex.os',
username,
password,
});
}, []);
const deleteCredentials = useCallback(async (): Promise<void> => {
if (!Capacitor.isNativePlatform()) return;
try {
await NativeBiometric.deleteCredentials({ server: 'com.aethex.os' });
} catch {
// Credentials may not exist
}
}, []);
return {
isAvailable,
biometricType,
authenticate,
isAuthenticated
isAuthenticated,
checkCredentials,
setCredentials,
deleteCredentials,
};
}

View file

@ -0,0 +1,12 @@
/**
* Utility to detect if the current page is embedded in an iframe
* Used by hub pages to hide their own navigation when loaded inside the OS window system
*/
export const isEmbedded = (): boolean => {
try {
return window.self !== window.top;
} catch (e) {
// If cross-origin, we can't access parent - assume embedded
return true;
}
};

View file

@ -3,6 +3,7 @@ import { Link } from "wouter";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { ArrowLeft, TrendingUp, Code, Star, Eye, Heart, Share2, Loader2 } from "lucide-react";
import { isEmbedded } from "@/lib/embed-utils";
import { supabase } from "@/lib/supabase";
import { useAuth } from "@/lib/auth";
@ -53,21 +54,25 @@ export default function CodeGallery() {
}
};
const embedded = isEmbedded();
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
{/* Header */}
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
<div className="flex items-center gap-4">
<Link href="/">
<button className="text-slate-400 hover:text-white">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<Code className="w-6 h-6 text-cyan-400" />
<h1 className="text-2xl font-bold text-white">Code Gallery</h1>
{/* Header - hidden when embedded in OS iframe */}
{!embedded && (
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
<div className="flex items-center gap-4">
<Link href="/">
<button className="text-slate-400 hover:text-white">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<Code className="w-6 h-6 text-cyan-400" />
<h1 className="text-2xl font-bold text-white">Code Gallery</h1>
</div>
<Button className="bg-cyan-600 hover:bg-cyan-700">Share Snippet</Button>
</div>
<Button className="bg-cyan-600 hover:bg-cyan-700">Share Snippet</Button>
</div>
)}
<div className="p-6 max-w-7xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">

View file

@ -3,6 +3,7 @@ import { Link } from "wouter";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { ArrowLeft, FileText, Folder, Plus, Trash2, Download, Copy, Loader2 } from "lucide-react";
import { isEmbedded } from "@/lib/embed-utils";
import { supabase } from "@/lib/supabase";
import { useAuth } from "@/lib/auth";
import { nanoid } from "nanoid";
@ -70,23 +71,27 @@ export default function FileManager() {
}
};
const embedded = isEmbedded();
return (
<div className="h-screen flex flex-col bg-slate-900">
{/* Header */}
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
<div className="flex items-center gap-4">
<Link href="/">
<button className="text-slate-400 hover:text-white">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<h1 className="text-2xl font-bold text-white">File Manager</h1>
{/* Header - hidden when embedded in OS iframe */}
{!embedded && (
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
<div className="flex items-center gap-4">
<Link href="/">
<button className="text-slate-400 hover:text-white">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<h1 className="text-2xl font-bold text-white">File Manager</h1>
</div>
<Button className="bg-cyan-600 hover:bg-cyan-700 gap-2">
<Plus className="w-4 h-4" />
New File
</Button>
</div>
<Button className="bg-cyan-600 hover:bg-cyan-700 gap-2">
<Plus className="w-4 h-4" />
New File
</Button>
</div>
)}
<div className="flex flex-1 overflow-hidden">
{/* File List */}

View file

@ -8,6 +8,7 @@ import {
ArrowLeft, ShoppingCart, Star, Plus, Loader2, Gamepad2,
Zap, Trophy, Users, DollarSign, TrendingUp, Filter, Search
} from "lucide-react";
import { isEmbedded } from "@/lib/embed-utils";
import { supabase } from "@/lib/supabase";
import { useAuth } from "@/lib/auth";
@ -196,55 +197,59 @@ export default function GameMarketplace() {
item.name.toLowerCase().includes(searchQuery.toLowerCase())
);
const embedded = isEmbedded();
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-950 text-white">
{/* Header */}
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-4 gap-2">
<div className="flex items-center gap-3">
<Link href="/hub">
<button className="text-slate-400 hover:text-white transition-colors">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<div className="flex items-center gap-2">
<Gamepad2 className="w-6 h-6 text-cyan-400" />
<h1 className="text-2xl font-bold">Game Marketplace</h1>
{/* Header - hidden when embedded in OS iframe */}
{!embedded && (
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-4 gap-2">
<div className="flex items-center gap-3">
<Link href="/hub">
<button className="text-slate-400 hover:text-white transition-colors">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<div className="flex items-center gap-2">
<Gamepad2 className="w-6 h-6 text-cyan-400" />
<h1 className="text-2xl font-bold">Game Marketplace</h1>
</div>
</div>
{/* Wallet Balance */}
<div className="bg-slate-800 px-4 py-2 rounded-lg border border-slate-700 flex items-center gap-2">
<DollarSign className="w-4 h-4 text-yellow-400" />
<span className="font-mono font-bold text-lg text-cyan-400">{wallet.balance} {wallet.currency}</span>
</div>
</div>
{/* Wallet Balance */}
<div className="bg-slate-800 px-4 py-2 rounded-lg border border-slate-700 flex items-center gap-2">
<DollarSign className="w-4 h-4 text-yellow-400" />
<span className="font-mono font-bold text-lg text-cyan-400">{wallet.balance} {wallet.currency}</span>
{/* Search & Filter */}
<div className="flex gap-2">
<div className="flex-1 relative">
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
<Input
placeholder="Search games, assets, creators..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-slate-800 border-slate-700 text-white"
/>
</div>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"
>
<option value="newest">Newest</option>
<option value="popular">Popular</option>
<option value="price-low">Price: LowHigh</option>
<option value="price-high">Price: HighLow</option>
</select>
</div>
</div>
{/* Search & Filter */}
<div className="flex gap-2">
<div className="flex-1 relative">
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
<Input
placeholder="Search games, assets, creators..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-10 bg-slate-800 border-slate-700 text-white"
/>
</div>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"
>
<option value="newest">Newest</option>
<option value="popular">Popular</option>
<option value="price-low">Price: LowHigh</option>
<option value="price-high">Price: HighLow</option>
</select>
</div>
</div>
</div>
)}
{/* Main Content */}
<div className="max-w-7xl mx-auto p-4 md:p-6">

View file

@ -7,6 +7,7 @@ import {
ArrowLeft, Radio, Eye, Heart, MessageCircle, Share2,
Twitch, Youtube, Play, Clock, Users, TrendingUp, Filter, Search
} from "lucide-react";
import { isEmbedded } from "@/lib/embed-utils";
interface Stream {
id: string;
@ -180,43 +181,47 @@ export default function GameStreaming() {
const liveStreams = filteredStreams.filter(s => s.isLive);
const recordedStreams = filteredStreams.filter(s => !s.isLive);
const embedded = isEmbedded();
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-950 text-white">
{/* Header */}
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
<div className="max-w-7xl mx-auto">
<div className="flex items-center gap-3 mb-4">
<Link href="/hub">
<button className="text-slate-400 hover:text-white transition-colors">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<h1 className="text-2xl font-bold">Game Streaming Hub</h1>
</div>
{/* Search & Filter */}
<div className="flex gap-2 flex-col sm:flex-row">
<div className="flex-1 relative">
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
<input
placeholder="Search streams, channels..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm placeholder-slate-400"
/>
{/* Header - hidden when embedded in OS iframe */}
{!embedded && (
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
<div className="max-w-7xl mx-auto">
<div className="flex items-center gap-3 mb-4">
<Link href="/hub">
<button className="text-slate-400 hover:text-white transition-colors">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<h1 className="text-2xl font-bold">Game Streaming Hub</h1>
</div>
{/* Search & Filter */}
<div className="flex gap-2 flex-col sm:flex-row">
<div className="flex-1 relative">
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
<input
placeholder="Search streams, channels..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm placeholder-slate-400"
/>
</div>
<select
value={selectedPlatform}
onChange={(e) => setSelectedPlatform(e.target.value as any)}
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"
>
<option value="all">All Platforms</option>
<option value="twitch">Twitch</option>
<option value="youtube">YouTube</option>
</select>
</div>
<select
value={selectedPlatform}
onChange={(e) => setSelectedPlatform(e.target.value as any)}
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"
>
<option value="all">All Platforms</option>
<option value="twitch">Twitch</option>
<option value="youtube">YouTube</option>
</select>
</div>
</div>
</div>
)}
{/* Stats */}
<div className="max-w-7xl mx-auto p-4 md:p-6">

View file

@ -7,6 +7,7 @@ import {
Trash2, Award, User, Calendar, Search, Filter, Plus, Loader2,
Package, AlertCircle, CheckCircle
} from "lucide-react";
import { isEmbedded } from "@/lib/embed-utils";
interface Mod {
id: string;
@ -199,69 +200,72 @@ export default function ModWorkshop() {
const games = ["all", "Minecraft", "Roblox", "Steam Games", "All Games"];
const embedded = isEmbedded();
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-950 text-white">
{/* Header */}
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-4 gap-2">
<div className="flex items-center gap-3">
<Link href="/hub">
<button className="text-slate-400 hover:text-white transition-colors">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<div className="flex items-center gap-2">
<Package className="w-6 h-6 text-cyan-400" />
<h1 className="text-2xl font-bold">Mod Workshop</h1>
{/* Header - hidden when embedded in OS iframe */}
{!embedded && (
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
<div className="max-w-7xl mx-auto">
<div className="flex items-center justify-between mb-4 gap-2">
<div className="flex items-center gap-3">
<Link href="/hub">
<button className="text-slate-400 hover:text-white transition-colors">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<div className="flex items-center gap-2">
<Package className="w-6 h-6 text-cyan-400" />
<h1 className="text-2xl font-bold">Mod Workshop</h1>
</div>
</div>
</div>
<Button
onClick={() => setShowUploadModal(true)}
className="bg-cyan-600 hover:bg-cyan-700 gap-2"
>
<Upload className="w-4 h-4" />
<span className="hidden sm:inline">Upload Mod</span>
<span className="sm:hidden">Upload</span>
</Button>
</div>
{/* Search & Filters */}
<div className="space-y-2">
<div className="flex gap-2">
<div className="flex-1 relative">
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
<input
placeholder="Search mods, authors..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm placeholder-slate-400"
/>
</div>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"
<Button
onClick={() => setShowUploadModal(true)}
className="bg-cyan-600 hover:bg-cyan-700 gap-2"
>
<option value="trending">Trending</option>
<option value="newest">Newest</option>
<option value="popular">Most Downloaded</option>
<option value="rating">Highest Rated</option>
</select>
<Upload className="w-4 h-4" />
<span className="hidden sm:inline">Upload Mod</span>
<span className="sm:hidden">Upload</span>
</Button>
</div>
{/* Category & Game Filters */}
<div className="flex gap-2 flex-wrap">
{/* Search & Filters */}
<div className="space-y-2">
<div className="flex gap-2">
{(["all", "gameplay", "cosmetic", "utility", "enhancement"] as const).map(cat => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-3 py-1 rounded-lg text-xs font-medium capitalize transition-colors ${
selectedCategory === cat
? "bg-cyan-600 text-white"
: "bg-slate-800 text-slate-300 hover:bg-slate-700"
<div className="flex-1 relative">
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
<input
placeholder="Search mods, authors..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm placeholder-slate-400"
/>
</div>
<select
value={sortBy}
onChange={(e) => setSortBy(e.target.value as any)}
className="px-3 py-2 bg-slate-800 border border-slate-700 rounded-lg text-white text-sm"
>
<option value="trending">Trending</option>
<option value="newest">Newest</option>
<option value="popular">Most Downloaded</option>
<option value="rating">Highest Rated</option>
</select>
</div>
{/* Category & Game Filters */}
<div className="flex gap-2 flex-wrap">
<div className="flex gap-2">
{(["all", "gameplay", "cosmetic", "utility", "enhancement"] as const).map(cat => (
<button
key={cat}
onClick={() => setSelectedCategory(cat)}
className={`px-3 py-1 rounded-lg text-xs font-medium capitalize transition-colors ${
selectedCategory === cat
? "bg-cyan-600 text-white"
: "bg-slate-800 text-slate-300 hover:bg-slate-700"
}`}
>
{cat === "all" ? "All Categories" : cat}

View file

@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input";
import { Card } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ArrowLeft, ShoppingCart, Star, Plus, Loader2 } from "lucide-react";
import { isEmbedded } from "@/lib/embed-utils";
import { supabase } from "@/lib/supabase";
import { useAuth } from "@/lib/auth";
@ -75,32 +76,36 @@ export default function Marketplace() {
}
};
const embedded = isEmbedded();
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
{/* Header */}
<div className="bg-slate-950 border-b border-slate-700 px-3 md:px-6 py-3 md:py-4 sticky top-0 z-10">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 md:gap-4 min-w-0 flex-1">
<Link href="/">
<button className="text-slate-400 hover:text-white shrink-0">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<h1 className="text-lg md:text-2xl font-bold text-white truncate">Marketplace</h1>
</div>
<div className="flex items-center gap-2 md:gap-4 shrink-0">
<div className="bg-slate-800 px-2 md:px-4 py-1.5 md:py-2 rounded-lg border border-slate-700">
<p className="text-xs text-slate-400 hidden sm:block">Balance</p>
<p className="text-sm md:text-xl font-bold text-cyan-400">{balance} LP</p>
{/* Header - hidden when embedded in OS iframe */}
{!embedded && (
<div className="bg-slate-950 border-b border-slate-700 px-3 md:px-6 py-3 md:py-4 sticky top-0 z-10">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 md:gap-4 min-w-0 flex-1">
<Link href="/">
<button className="text-slate-400 hover:text-white shrink-0">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<h1 className="text-lg md:text-2xl font-bold text-white truncate">Marketplace</h1>
</div>
<div className="flex items-center gap-2 md:gap-4 shrink-0">
<div className="bg-slate-800 px-2 md:px-4 py-1.5 md:py-2 rounded-lg border border-slate-700">
<p className="text-xs text-slate-400 hidden sm:block">Balance</p>
<p className="text-sm md:text-xl font-bold text-cyan-400">{balance} LP</p>
</div>
<Button className="bg-cyan-600 hover:bg-cyan-700 gap-1 md:gap-2 text-xs md:text-sm px-2 md:px-4 h-8 md:h-10">
<Plus className="w-3 h-3 md:w-4 md:h-4" />
<span className="hidden sm:inline">Sell Item</span>
<span className="sm:hidden">Sell</span>
</Button>
</div>
<Button className="bg-cyan-600 hover:bg-cyan-700 gap-1 md:gap-2 text-xs md:text-sm px-2 md:px-4 h-8 md:h-10">
<Plus className="w-3 h-3 md:w-4 md:h-4" />
<span className="hidden sm:inline">Sell Item</span>
<span className="sm:hidden">Sell</span>
</Button>
</div>
</div>
</div>
)}
<div className="p-3 md:p-6 max-w-7xl mx-auto">
{/* Category Tabs */}

View file

@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input";
import { Card } from "@/components/ui/card";
import { ArrowLeft, Send, Search, Loader2 } from "lucide-react";
import { MobileHeader } from "@/components/mobile/MobileHeader";
import { isEmbedded } from "@/lib/embed-utils";
import { supabase } from "@/lib/supabase";
import { useAuth } from "@/lib/auth";
import { nanoid } from "nanoid";
@ -96,22 +97,29 @@ export default function Messaging() {
c.username.toLowerCase().includes(searchQuery.toLowerCase())
);
const embedded = isEmbedded();
return (
<div className="h-screen flex flex-col bg-slate-900">
{/* Mobile Header */}
<div className="md:hidden">
<MobileHeader title="Messages" />
</div>
{/* Desktop Header */}
<div className="hidden md:flex bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center gap-4">
<Link href="/">
<button className="text-slate-400 hover:text-white">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<h1 className="text-2xl font-bold text-white">Messages</h1>
</div>
{/* Headers - hidden when embedded in OS iframe */}
{!embedded && (
<>
{/* Mobile Header */}
<div className="md:hidden">
<MobileHeader title="Messages" />
</div>
{/* Desktop Header */}
<div className="hidden md:flex bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center gap-4">
<Link href="/">
<button className="text-slate-400 hover:text-white">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<h1 className="text-2xl font-bold text-white">Messages</h1>
</div>
</>
)}
<div className="flex flex-1 overflow-hidden">
{/* Chat List */}

View file

@ -6,6 +6,7 @@ import { Card } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ArrowLeft, Plus, Trash2, ExternalLink, Github, Globe, Loader2 } from "lucide-react";
import { MobileHeader } from "@/components/mobile/MobileHeader";
import { isEmbedded } from "@/lib/embed-utils";
import { supabase } from "@/lib/supabase";
import { useAuth } from "@/lib/auth";
import { nanoid } from "nanoid";
@ -102,31 +103,38 @@ export default function Projects() {
}
};
const embedded = isEmbedded();
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
{/* Mobile Header */}
<div className="md:hidden">
<MobileHeader title="Projects" />
</div>
{/* Desktop Header */}
<div className="hidden md:block bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
<div className="flex items-center gap-4">
<Link href="/">
<button className="text-slate-400 hover:text-white transition-colors">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<h1 className="text-2xl font-bold text-white">Projects & Portfolio</h1>
</div>
<Button
onClick={() => setShowForm(!showForm)}
className="bg-cyan-600 hover:bg-cyan-700 gap-2"
>
<Plus className="w-4 h-4" />
New Project
</Button>
</div>
{/* Headers - hidden when embedded in OS iframe */}
{!embedded && (
<>
{/* Mobile Header */}
<div className="md:hidden">
<MobileHeader title="Projects" />
</div>
{/* Desktop Header */}
<div className="hidden md:block bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
<div className="flex items-center gap-4">
<Link href="/">
<button className="text-slate-400 hover:text-white transition-colors">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<h1 className="text-2xl font-bold text-white">Projects & Portfolio</h1>
</div>
<Button
onClick={() => setShowForm(!showForm)}
className="bg-cyan-600 hover:bg-cyan-700 gap-2"
>
<Plus className="w-4 h-4" />
New Project
</Button>
</div>
</>
)}
<div className="p-6 max-w-7xl mx-auto">
{/* Add Project Form */}

View file

@ -3,6 +3,7 @@ import { Link } from "wouter";
import { Button } from "@/components/ui/button";
import { Card } from "@/components/ui/card";
import { ArrowLeft, Settings, Bell, Lock, Palette, HardDrive, User, Loader2 } from "lucide-react";
import { isEmbedded } from "@/lib/embed-utils";
import { supabase } from "@/lib/supabase";
import { useAuth } from "@/lib/auth";
import { nanoid } from "nanoid";
@ -88,18 +89,22 @@ export default function SettingsWorkspace() {
saveSettings(newSettings);
};
const embedded = isEmbedded();
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
{/* Header */}
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center gap-4 sticky top-0 z-10">
<Link href="/">
<button className="text-slate-400 hover:text-white">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<Settings className="w-6 h-6 text-cyan-400" />
<h1 className="text-2xl font-bold text-white">Workspace Settings</h1>
</div>
{/* Header - hidden when embedded in OS iframe */}
{!embedded && (
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center gap-4 sticky top-0 z-10">
<Link href="/">
<button className="text-slate-400 hover:text-white">
<ArrowLeft className="w-5 h-5" />
</button>
</Link>
<Settings className="w-6 h-6 text-cyan-400" />
<h1 className="text-2xl font-bold text-white">Workspace Settings</h1>
</div>
)}
<div className="p-6 max-w-4xl mx-auto">
{/* Appearance Settings */}

View file

@ -11,6 +11,8 @@ import { useMobileNative } from "@/hooks/use-mobile-native";
import { useNativeFeatures } from "@/hooks/use-native-features";
import { useBiometricAuth } from "@/hooks/use-biometric-auth";
import { StatusBar, Style } from '@capacitor/status-bar';
import { App as CapacitorApp } from '@capacitor/app';
import { isMobile } from '@/lib/platform';
import { MobileQuickActions } from "@/components/MobileQuickActions";
import { Minesweeper } from "@/components/games/Minesweeper";
import { CookieClicker } from "@/components/games/CookieClicker";
@ -1215,131 +1217,162 @@ export default function AeThexOS() {
};
}, [layout.isMobile]);
// Handle Android hardware back button
useEffect(() => {
if (!layout.isMobile || !isMobile()) return;
const backHandler = CapacitorApp.addListener('backButton', () => {
// Get current active windows (non-minimized)
const activeWindows = windows.filter(w => !w.minimized);
if (activeWindows.length > 0) {
// Close the topmost window
const topWindow = activeWindows[activeWindows.length - 1];
closeWindow(topWindow.id);
impact('light');
} else {
// No windows open - minimize app (don't exit)
CapacitorApp.minimizeApp();
}
});
return () => {
backHandler.remove();
};
}, [layout.isMobile, windows, closeWindow, impact]);
// Native Android App Layout
if (layout.isMobile) {
const activeWindows = windows.filter(w => !w.minimized);
const currentWindow = activeWindows[activeWindows.length - 1];
// Dynamic theme colors based on clearance mode
const isFoundation = clearanceMode === 'foundation';
const mobileTheme = {
primary: isFoundation ? 'rgb(220, 38, 38)' : 'rgb(59, 130, 246)', // red-600 or blue-500
secondary: isFoundation ? 'rgb(212, 175, 55)' : 'rgb(148, 163, 184)', // gold or slate-400
primaryClass: isFoundation ? 'text-red-500' : 'text-blue-500',
secondaryClass: isFoundation ? 'text-amber-400' : 'text-slate-300',
borderClass: isFoundation ? 'border-red-900/50' : 'border-blue-900/50',
bgAccent: isFoundation ? 'bg-red-900/20' : 'bg-blue-900/20',
iconClass: isFoundation ? 'text-red-400' : 'text-blue-400',
gradientBg: isFoundation
? 'linear-gradient(135deg, #0a0a0a 0%, #1a0505 50%, #0a0a0a 100%)'
: 'linear-gradient(135deg, #0a0a0a 0%, #050a14 50%, #0a0a0a 100%)',
};
return (
<div className="h-screen w-screen bg-black overflow-hidden flex flex-col">
<style>{`
@keyframes scan {
0% { transform: translateY(-100%); }
100% { transform: translateY(100%); }
}
@keyframes pulse-border {
0%, 100% { opacity: 0.3; }
50% { opacity: 0.8; }
}
`}</style>
{/* Ingress Status Bar - Minimal */}
<div className="relative h-8 bg-black/90 border-b border-emerald-500/50 shrink-0">
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-emerald-500/5 to-transparent"></div>
<div className="h-screen w-screen overflow-hidden flex flex-col" style={{ background: mobileTheme.gradientBg }}>
{/* AeThex Mobile Status Bar */}
<div className={`relative h-10 bg-black/90 ${mobileTheme.borderClass} border-b shrink-0`} style={{ paddingTop: 'env(safe-area-inset-top)' }}>
<div className="relative flex items-center justify-between px-4 h-full">
<div className="flex items-center gap-3">
<Activity className="w-3.5 h-3.5 text-emerald-400" />
<Wifi className="w-3.5 h-3.5 text-cyan-400" />
<div className="flex items-center gap-0.5">
{[...Array(4)].map((_, i) => (
<div key={i} className="w-0.5 h-1.5 bg-emerald-400 rounded-full" style={{ height: `${(i + 1) * 2}px` }} />
))}
</div>
<span className={`${mobileTheme.primaryClass} font-bold text-sm font-mono`}>AeThex</span>
<span className={`${mobileTheme.secondaryClass} text-xs font-mono opacity-60`}>
{isFoundation ? 'FOUNDATION' : 'CORP'}
</span>
<div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse"></div>
</div>
<div className="flex items-center gap-4 text-cyan-400 text-xs font-mono font-bold tracking-wider">
<div className={`flex items-center gap-3 ${mobileTheme.secondaryClass} opacity-80 text-xs font-mono`}>
<Wifi className="w-3.5 h-3.5" />
<span>{batteryInfo?.level || 100}%</span>
<Battery className="w-4 h-4 text-green-400" />
<span className="font-mono text-cyan-400">{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
<Battery className="w-4 h-4" />
<span>{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
</div>
</div>
</div>
{/* Main Content */}
<div className="flex-1 overflow-hidden relative bg-black">
<AnimatePresence mode="wait">
<div className="flex-1 overflow-hidden relative">
<AnimatePresence mode="wait" initial={false}>
{currentWindow ? (
// Fullscreen App View with 3D Card Flip
<motion.div
key={currentWindow.id}
initial={{ rotateY: 90, opacity: 0 }}
animate={{ rotateY: 0, opacity: 1 }}
exit={{ rotateY: -90, opacity: 0 }}
transition={{ duration: 0.4, type: "spring" }}
style={{ transformStyle: "preserve-3d" }}
className="h-full w-full flex flex-col relative"
key={`window-${currentWindow.id}`}
initial={{ opacity: 0, x: 20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="absolute inset-0 flex flex-col"
>
{/* Ingress Style - Minimal App Bar */}
<div className="relative h-12 bg-black/95 border-b-2 border-emerald-500/50 shrink-0">
<div className="absolute inset-0" style={{ animation: 'pulse-border 2s ease-in-out infinite' }}>
<div className="absolute inset-x-0 bottom-0 h-[2px] bg-gradient-to-r from-transparent via-cyan-500 to-transparent"></div>
</div>
<div className="relative flex items-center px-3 h-full">
<button
onClick={() => {
impact('light');
closeWindow(currentWindow.id);
}}
className="w-10 h-10 flex items-center justify-center border border-emerald-500/50 active:bg-emerald-500/20"
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
>
<ChevronLeft className="w-5 h-5 text-emerald-400" />
</button>
<div className="flex-1 px-4">
<h1 className="text-cyan-400 font-mono font-bold text-lg uppercase tracking-widest">
{currentWindow.title}
</h1>
</div>
<button
className="w-10 h-10 flex items-center justify-center border border-cyan-500/50 active:bg-cyan-500/20"
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
>
<MoreVertical className="w-5 h-5 text-cyan-400" />
</button>
{/* App Header */}
<div className={`h-14 bg-black/95 ${mobileTheme.borderClass} border-b shrink-0 flex items-center px-4 gap-3`}>
<button
onClick={() => {
impact('light');
closeWindow(currentWindow.id);
}}
className={`w-10 h-10 rounded-lg ${mobileTheme.bgAccent} border ${mobileTheme.borderClass} flex items-center justify-center active:opacity-70`}
>
<ChevronLeft className={`w-5 h-5 ${mobileTheme.iconClass}`} />
</button>
<div className="flex-1">
<h1 className={`${mobileTheme.secondaryClass} font-bold text-lg`}>{currentWindow.title}</h1>
</div>
<button className="w-10 h-10 rounded-lg bg-zinc-900/50 border border-zinc-800/40 flex items-center justify-center">
<MoreVertical className="w-5 h-5 text-zinc-400" />
</button>
</div>
{/* App Content */}
<div className="flex-1 overflow-auto relative bg-black">
<div className="flex-1 overflow-auto bg-black/80">
{renderAppContent(currentWindow.component)}
</div>
</motion.div>
) : (
// ULTRA FUTURISTIC LAUNCHER
// Home Launcher
<motion.div
key="launcher"
initial={{ opacity: 0, scale: 0.9 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.9 }}
className="h-full flex flex-col relative"
key="launcher-home"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15 }}
className="absolute inset-0 flex flex-col"
>
{/* Ingress Style Search Bar */}
<div className="px-4 pt-6 pb-4">
<div className="relative bg-black/80 border border-emerald-500/50 p-3">
<div className="absolute inset-0 border border-cyan-500/30" style={{ clipPath: 'polygon(0 0, calc(100% - 12px) 0, 100% 12px, 100% 100%, 12px 100%, 0 calc(100% - 12px))' }}></div>
<div className="relative flex items-center gap-3">
<Search className="w-5 h-5 text-emerald-400" />
{/* Header with Theme Toggle */}
<div className="px-4 pt-4 pb-2 flex items-center justify-between">
<div>
<h1 className={`${mobileTheme.primaryClass} font-bold text-xl`}>
{isFoundation ? 'The Foundation' : 'The Corporation'}
</h1>
<p className="text-zinc-500 text-xs">Welcome back, Architect</p>
</div>
<button
onClick={() => {
impact('medium');
setClearanceMode(isFoundation ? 'corp' : 'foundation');
}}
className={`px-3 py-1.5 rounded-lg ${mobileTheme.bgAccent} border ${mobileTheme.borderClass}`}
>
<span className={`${mobileTheme.secondaryClass} text-xs font-mono`}>
{isFoundation ? '→ CORP' : '→ FND'}
</span>
</button>
</div>
{/* Search Bar */}
<div className="px-4 pt-2 pb-3">
<div className={`relative bg-zinc-900/80 border ${mobileTheme.borderClass} rounded-xl p-3`}>
<div className="flex items-center gap-3">
<Search className={`w-5 h-5 ${mobileTheme.iconClass}`} />
<input
type="text"
placeholder="SCANNER SEARCH..."
className="flex-1 bg-transparent text-emerald-400 placeholder:text-emerald-400/40 outline-none text-sm font-mono uppercase tracking-wide"
placeholder="Search apps..."
className="flex-1 bg-transparent text-white placeholder:text-zinc-500 outline-none text-sm"
onFocus={() => impact('light')}
/>
<Mic className="w-5 h-5 text-cyan-400" />
</div>
</div>
</div>
{/* App Grid - Hexagonal */}
<div className="flex-1 overflow-auto px-4 pb-24">
{/* App Grid */}
<div className="flex-1 overflow-auto px-4 pb-32">
<div className="mb-6">
<div className="flex items-center gap-2 mb-3 px-2">
<div className="w-2 h-2 bg-emerald-400"></div>
<h2 className="text-emerald-400 text-xs uppercase tracking-widest font-mono font-bold">
Quick Access
</h2>
<div className="flex-1 h-[1px] bg-emerald-500/30"></div>
<div className="flex items-center gap-2 mb-4">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: mobileTheme.primary }}></div>
<h2 className={`${mobileTheme.secondaryClass} opacity-90 text-xs uppercase tracking-widest font-bold`}>Quick Access</h2>
<div className={`flex-1 h-px ${mobileTheme.borderClass} bg-current opacity-30`}></div>
</div>
<div className="grid grid-cols-4 gap-3">
<div className="grid grid-cols-4 gap-4">
{apps.slice(0, 8).map((app) => (
<button
key={app.id}
@ -1347,16 +1380,12 @@ export default function AeThexOS() {
impact('medium');
openApp(app);
}}
className="flex flex-col items-center gap-2 p-2 active:bg-emerald-500/10"
className={`flex flex-col items-center gap-2 p-2 rounded-xl active:${mobileTheme.bgAccent}`}
>
<div
className="relative w-16 h-16 bg-black border-2 border-emerald-500/50 flex items-center justify-center active:border-cyan-400"
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
>
<div className="text-emerald-400 scale-75">{app.icon}</div>
<div className="absolute inset-0 border border-cyan-500/20" style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}></div>
<div className={`w-14 h-14 rounded-2xl bg-gradient-to-br from-zinc-900 to-zinc-950 border ${mobileTheme.borderClass} flex items-center justify-center shadow-lg`}>
<div className={mobileTheme.iconClass}>{app.icon}</div>
</div>
<span className="text-cyan-400 text-[9px] font-mono text-center line-clamp-2 leading-tight uppercase">
<span className="text-zinc-300 text-[10px] font-medium text-center line-clamp-2 leading-tight">
{app.title}
</span>
</button>
@ -1364,14 +1393,12 @@ export default function AeThexOS() {
</div>
</div>
{/* All Apps - Minimal List */}
{/* All Apps List */}
<div>
<div className="flex items-center gap-2 mb-3 px-2">
<div className="w-2 h-2 bg-cyan-400"></div>
<h2 className="text-cyan-400 text-xs uppercase tracking-widest font-mono font-bold">
All Systems
</h2>
<div className="flex-1 h-[1px] bg-cyan-500/30"></div>
<div className="flex items-center gap-2 mb-4">
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: mobileTheme.secondary }}></div>
<h2 className={`${mobileTheme.secondaryClass} opacity-90 text-xs uppercase tracking-widest font-bold`}>All Apps</h2>
<div className={`flex-1 h-px opacity-30`} style={{ backgroundColor: mobileTheme.secondary }}></div>
</div>
<div className="space-y-2">
{apps.slice(8).map((app) => (
@ -1381,13 +1408,13 @@ export default function AeThexOS() {
impact('medium');
openApp(app);
}}
className="relative w-full flex items-center gap-3 p-3 border border-emerald-500/30 active:bg-emerald-500/10 active:border-cyan-500"
className={`w-full flex items-center gap-4 p-3 rounded-xl bg-zinc-900/40 border border-zinc-800/40 active:${mobileTheme.bgAccent} active:${mobileTheme.borderClass}`}
>
<div className="w-10 h-10 bg-black border border-emerald-500/50 flex items-center justify-center shrink-0">
<div className="text-emerald-400 scale-75">{app.icon}</div>
<div className={`w-11 h-11 rounded-xl bg-gradient-to-br from-zinc-800 to-zinc-900 border ${mobileTheme.borderClass} flex items-center justify-center shrink-0`}>
<div className={`${mobileTheme.iconClass} scale-90`}>{app.icon}</div>
</div>
<span className="text-cyan-400 font-mono text-sm text-left flex-1 uppercase tracking-wide">{app.title}</span>
<ChevronRight className="w-4 h-4 text-emerald-400" />
<span className="text-zinc-200 font-medium text-sm text-left flex-1">{app.title}</span>
<ChevronRight className="w-4 h-4 text-zinc-600" />
</button>
))}
</div>
@ -1398,30 +1425,27 @@ export default function AeThexOS() {
</AnimatePresence>
</div>
{/* INGRESS STYLE NAVIGATION BAR - Lightweight */}
{/* Bottom Navigation */}
<div
className="relative bg-black/95 border-t-2 border-emerald-500/50 shrink-0 z-50"
style={{
paddingTop: '0.75rem',
paddingBottom: 'calc(0.75rem + env(safe-area-inset-bottom))',
}}
className={`bg-black/95 border-t ${mobileTheme.borderClass} shrink-0`}
style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}
>
<div className="absolute inset-x-0 top-0 h-[2px] bg-gradient-to-r from-transparent via-cyan-500 to-transparent" style={{ animation: 'pulse-border 2s ease-in-out infinite' }}></div>
<div className="relative flex items-center justify-around px-6">
<div className="flex items-center justify-around py-2 px-4">
<button
onClick={() => {
impact('medium');
windows.forEach(w => closeWindow(w.id));
}}
className="relative w-14 h-14 bg-black border-2 border-emerald-500/70 flex items-center justify-center active:bg-emerald-500/20 active:border-cyan-400"
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
className={`flex flex-col items-center gap-1 p-3 rounded-2xl ${!currentWindow ? mobileTheme.bgAccent : ''}`}
>
<Home className="w-6 h-6 text-emerald-400" />
<Home className={`w-6 h-6 ${!currentWindow ? mobileTheme.iconClass : 'text-zinc-500'}`} />
<span className={`text-[10px] font-medium ${!currentWindow ? mobileTheme.secondaryClass : 'text-zinc-600'}`}>Home</span>
</button>
<button
onClick={() => {
impact('medium');
// Open apps drawer / show minimized apps
const minimized = windows.filter(w => w.minimized);
if (minimized.length > 0) {
setWindows(prev => prev.map(w =>
@ -1429,12 +1453,12 @@ export default function AeThexOS() {
));
}
}}
className="relative w-14 h-14 bg-black border-2 border-cyan-500/70 flex items-center justify-center active:bg-cyan-500/20 active:border-emerald-400"
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
className="flex flex-col items-center gap-1 p-3 rounded-2xl relative"
>
<Square className="w-6 h-6 text-cyan-400" />
<Layers className={`w-6 h-6 ${windows.length > 0 ? mobileTheme.iconClass : 'text-zinc-500'}`} />
<span className="text-[10px] text-zinc-600 font-medium">Recents</span>
{windows.filter(w => w.minimized).length > 0 && (
<span className="absolute -top-1 -right-1 w-5 h-5 bg-emerald-500 text-black text-xs flex items-center justify-center font-bold">
<span className="absolute top-1 right-1 w-5 h-5 text-white text-[10px] flex items-center justify-center rounded-full font-bold" style={{ backgroundColor: mobileTheme.primary }}>
{windows.filter(w => w.minimized).length}
</span>
)}
@ -1443,19 +1467,19 @@ export default function AeThexOS() {
<button
onClick={() => {
impact('medium');
if (currentWindow) {
closeWindow(currentWindow.id);
}
// Find and open settings app
const settingsApp = apps.find(a => a.id === 'settings');
if (settingsApp) openApp(settingsApp);
}}
className="relative w-14 h-14 bg-black border-2 border-emerald-500/70 flex items-center justify-center active:bg-emerald-500/20 active:border-cyan-400"
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
className={`flex flex-col items-center gap-1 p-3 rounded-2xl ${currentWindow?.component === 'settings' ? mobileTheme.bgAccent : ''}`}
>
<ArrowLeft className="w-6 h-6 text-emerald-400" />
<Settings className={`w-6 h-6 ${currentWindow?.component === 'settings' ? mobileTheme.iconClass : 'text-zinc-500'}`} />
<span className={`text-[10px] font-medium ${currentWindow?.component === 'settings' ? mobileTheme.secondaryClass : 'text-zinc-600'}`}>Settings</span>
</button>
</div>
</div>
{/* Floating Action Button with Orbital Menu */}
{/* Floating Quick Actions */}
<MobileQuickActions />
</div>
);

61
package-lock.json generated
View file

@ -65,6 +65,7 @@
"@tanstack/react-query": "^5.60.5",
"@types/bcrypt": "^6.0.0",
"bcrypt": "^6.0.0",
"capacitor-native-biometric": "^4.2.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@ -186,6 +187,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@ -566,6 +568,7 @@
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.0.2.tgz",
"integrity": "sha512-EXZfxkL6GFJS2cb7TIBR7RiHA5iz6ufDcl1VmUpI2pga3lJ5Ck2+iqbx7N+osL3XYem9ad4XCidJEMm64DX6UQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
@ -4832,6 +4835,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@ -4875,6 +4879,7 @@
"integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
@ -4907,6 +4912,7 @@
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@ -4917,6 +4923,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@ -5088,6 +5095,7 @@
"integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/utils": "4.0.18",
"fflate": "^0.8.2",
@ -5419,6 +5427,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@ -5449,20 +5458,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/bufferutil": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.1.0.tgz",
"integrity": "sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/bytes": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -5522,6 +5517,24 @@
],
"license": "CC-BY-4.0"
},
"node_modules/capacitor-native-biometric": {
"version": "4.2.2",
"resolved": "https://registry.npmjs.org/capacitor-native-biometric/-/capacitor-native-biometric-4.2.2.tgz",
"integrity": "sha512-stg0h48UxgkNuNcCAgCXLp2DUspRQs79bCBPntpCBhsDxk2bhDRUu+J/QpFtDQHG4M4DioSUcYaAsVw2N6N7wA==",
"license": "MIT",
"dependencies": {
"@capacitor/core": "^3.4.3"
}
},
"node_modules/capacitor-native-biometric/node_modules/@capacitor/core": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-3.9.0.tgz",
"integrity": "sha512-j1lL0+/7stY8YhIq1Lm6xixvUqIn89vtyH5ZpJNNmcZ0kwz6K9eLkcG6fvq1UWMDgSVZg9JrRGSFhb4LLoYOsw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/chai": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
@ -6047,6 +6060,7 @@
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.39.3.tgz",
"integrity": "sha512-EZ8ZpYvDIvKU9C56JYLOmUskazhad+uXZCTCRN4OnRMsL+xAJ05dv1eCpAG5xzhsm1hqiuC5kAZUCS924u2DTw==",
"license": "Apache-2.0",
"peer": true,
"peerDependencies": {
"@aws-sdk/client-rds-data": ">=3",
"@cloudflare/workers-types": ">=4",
@ -6212,7 +6226,8 @@
"version": "8.6.0",
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/embla-carousel-react": {
"version": "8.6.0",
@ -6402,6 +6417,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@ -7608,6 +7624,7 @@
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
"license": "MIT",
"peer": true,
"dependencies": {
"dompurify": "3.2.7",
"marked": "14.0.0"
@ -7940,6 +7957,7 @@
"resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
"integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"pg-connection-string": "^2.11.0",
"pg-pool": "^3.11.0",
@ -8037,6 +8055,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -8108,6 +8127,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -8274,6 +8294,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -8314,6 +8335,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@ -8326,6 +8348,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@ -9100,7 +9123,8 @@
"version": "4.1.18",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@ -9826,6 +9850,7 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -9968,6 +9993,7 @@
"integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
@ -10055,6 +10081,7 @@
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@ -10614,6 +10641,7 @@
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@vitest/expect": "4.0.18",
"@vitest/mocker": "4.0.18",
@ -10888,6 +10916,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View file

@ -20,11 +20,11 @@
"start": "NODE_ENV=production node dist/index.js",
"check": "tsc",
"db:push": "drizzle-kit push",
"tauri": "cd shell/aethex-shell && npm run tauri",
"tauri:dev": "cd shell/aethex-shell && npm run tauri dev",
"tauri:build": "cd shell/aethex-shell && npm run tauri build",
"audit:org-scope": "tsx script/org-scope-audit.ts",
"test:org-scope": "tsx --test server/org-scoping.test.ts"
"tauri": "cd shell/aethex-shell && npm run tauri",
"tauri:dev": "cd shell/aethex-shell && npm run tauri dev",
"tauri:build": "cd shell/aethex-shell && npm run tauri build",
"audit:org-scope": "tsx script/org-scope-audit.ts",
"test:org-scope": "tsx --test server/org-scoping.test.ts"
},
"dependencies": {
"@capacitor-community/privacy-screen": "^6.0.0",
@ -83,6 +83,7 @@
"@tanstack/react-query": "^5.60.5",
"@types/bcrypt": "^6.0.0",
"bcrypt": "^6.0.0",
"capacitor-native-biometric": "^4.2.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",