mirror of
https://github.com/AeThex-Corporation/AeThex-OS.git
synced 2026-04-25 17:37:19 +00:00
new file: client/src/lib/embed-utils.ts
This commit is contained in:
parent
ad5f15271e
commit
293d3c0d02
24 changed files with 782 additions and 421 deletions
|
|
@ -1,14 +1,5 @@
|
||||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
// 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 {
|
android {
|
||||||
compileOptions {
|
compileOptions {
|
||||||
sourceCompatibility JavaVersion.VERSION_21
|
sourceCompatibility JavaVersion.VERSION_21
|
||||||
|
|
@ -36,6 +27,7 @@ dependencies {
|
||||||
implementation project(':capacitor-splash-screen')
|
implementation project(':capacitor-splash-screen')
|
||||||
implementation project(':capacitor-status-bar')
|
implementation project(':capacitor-status-bar')
|
||||||
implementation project(':capacitor-toast')
|
implementation project(':capacitor-toast')
|
||||||
|
implementation project(':capacitor-native-biometric')
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,22 @@
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
<!-- Permissions -->
|
<!-- Permissions -->
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<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>
|
</manifest>
|
||||||
|
|
|
||||||
26
android/app/src/main/res/values/colors.xml
Normal file
26
android/app/src/main/res/values/colors.xml
Normal 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>
|
||||||
|
|
@ -2,21 +2,27 @@
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<!-- Base application theme. -->
|
<!-- Base application theme. -->
|
||||||
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
|
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
<!-- Customize your theme here. -->
|
<!-- Customize your theme here. -->
|
||||||
<item name="colorPrimary">@color/colorPrimary</item>
|
<item name="colorPrimary">@color/colorPrimary</item>
|
||||||
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
|
||||||
<item name="colorAccent">@color/colorAccent</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>
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
|
||||||
<item name="windowActionBar">false</item>
|
<item name="windowActionBar">false</item>
|
||||||
<item name="windowNoTitle">true</item>
|
<item name="windowNoTitle">true</item>
|
||||||
<item name="android:background">@null</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>
|
||||||
|
|
||||||
|
|
||||||
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
<style name="AppTheme.NoActionBarLaunch" parent="Theme.SplashScreen">
|
||||||
<item name="android:background">@drawable/splash</item>
|
<item name="android:background">@drawable/splash</item>
|
||||||
|
<item name="android:statusBarColor">@color/splash_background</item>
|
||||||
|
<item name="android:navigationBarColor">@color/splash_background</item>
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
@ -55,3 +55,6 @@ project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacit
|
||||||
|
|
||||||
include ':capacitor-toast'
|
include ':capacitor-toast'
|
||||||
project(':capacitor-toast').projectDir = new File('../node_modules/@capacitor/toast/android')
|
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')
|
||||||
|
|
|
||||||
|
|
@ -20,22 +20,23 @@ const config: CapacitorConfig = {
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
SplashScreen: {
|
SplashScreen: {
|
||||||
launchShowDuration: 0,
|
launchShowDuration: 2000,
|
||||||
launchAutoHide: true,
|
launchAutoHide: true,
|
||||||
backgroundColor: '#000000',
|
launchFadeOutDuration: 500,
|
||||||
|
backgroundColor: '#0a0a0a',
|
||||||
androidSplashResourceName: 'splash',
|
androidSplashResourceName: 'splash',
|
||||||
androidScaleType: 'CENTER_CROP',
|
androidScaleType: 'CENTER_CROP',
|
||||||
showSpinner: false,
|
showSpinner: true,
|
||||||
androidSpinnerStyle: 'large',
|
androidSpinnerStyle: 'small',
|
||||||
iosSpinnerStyle: 'small',
|
iosSpinnerStyle: 'small',
|
||||||
spinnerColor: '#999999',
|
spinnerColor: '#DC2626',
|
||||||
splashFullScreen: true,
|
splashFullScreen: true,
|
||||||
splashImmersive: true
|
splashImmersive: true
|
||||||
},
|
},
|
||||||
StatusBar: {
|
StatusBar: {
|
||||||
style: 'DARK',
|
style: 'DARK',
|
||||||
backgroundColor: '#000000',
|
backgroundColor: '#0a0a0a',
|
||||||
overlaysWebView: true
|
overlaysWebView: false
|
||||||
},
|
},
|
||||||
App: {
|
App: {
|
||||||
backButtonEnabled: true
|
backButtonEnabled: true
|
||||||
|
|
@ -45,8 +46,18 @@ const config: CapacitorConfig = {
|
||||||
},
|
},
|
||||||
LocalNotifications: {
|
LocalNotifications: {
|
||||||
smallIcon: 'ic_stat_icon_config_sample',
|
smallIcon: 'ic_stat_icon_config_sample',
|
||||||
iconColor: '#488AFF',
|
iconColor: '#DC2626',
|
||||||
sound: 'beep.wav'
|
sound: 'beep.wav'
|
||||||
|
},
|
||||||
|
Keyboard: {
|
||||||
|
resize: 'body',
|
||||||
|
resizeOnFullScreen: true,
|
||||||
|
style: 'dark'
|
||||||
|
},
|
||||||
|
Haptics: {
|
||||||
|
selectionStart: true,
|
||||||
|
selectionChanged: true,
|
||||||
|
selectionEnd: true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
android: {
|
android: {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { Toaster } from "@/components/ui/toaster";
|
||||||
import { AuthProvider } from "@/lib/auth";
|
import { AuthProvider } from "@/lib/auth";
|
||||||
import { TutorialProvider } from "@/components/Tutorial";
|
import { TutorialProvider } from "@/components/Tutorial";
|
||||||
import { ProtectedRoute } from "@/components/ProtectedRoute";
|
import { ProtectedRoute } from "@/components/ProtectedRoute";
|
||||||
|
import { HapticProvider } from "@/components/mobile/HapticFeedback";
|
||||||
import NotFound from "@/pages/not-found";
|
import NotFound from "@/pages/not-found";
|
||||||
import Home from "@/pages/home";
|
import Home from "@/pages/home";
|
||||||
import Passport from "@/pages/passport";
|
import Passport from "@/pages/passport";
|
||||||
|
|
@ -114,12 +115,14 @@ function App() {
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<LabTerminalProvider>
|
<HapticProvider enableGlobalTouchFeedback={true}>
|
||||||
<TutorialProvider>
|
<LabTerminalProvider>
|
||||||
<Toaster />
|
<TutorialProvider>
|
||||||
<Router />
|
<Toaster />
|
||||||
</TutorialProvider>
|
<Router />
|
||||||
</LabTerminalProvider>
|
</TutorialProvider>
|
||||||
|
</LabTerminalProvider>
|
||||||
|
</HapticProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
37
client/src/components/mobile/HapticButton.tsx
Normal file
37
client/src/components/mobile/HapticButton.tsx
Normal 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;
|
||||||
90
client/src/components/mobile/HapticFeedback.tsx
Normal file
90
client/src/components/mobile/HapticFeedback.tsx
Normal 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;
|
||||||
|
|
@ -8,6 +8,15 @@ interface MobileHeaderProps {
|
||||||
backPath?: string;
|
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({
|
export function MobileHeader({
|
||||||
title = 'AeThex OS',
|
title = 'AeThex OS',
|
||||||
onMenuClick,
|
onMenuClick,
|
||||||
|
|
@ -16,6 +25,11 @@ export function MobileHeader({
|
||||||
}: MobileHeaderProps) {
|
}: MobileHeaderProps) {
|
||||||
const [, navigate] = useLocation();
|
const [, navigate] = useLocation();
|
||||||
|
|
||||||
|
// Don't render the header if we're embedded inside the OS iframe
|
||||||
|
if (isEmbedded()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
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="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">
|
<div className="flex items-center justify-between px-4 py-3 safe-area-inset-top">
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,15 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { Capacitor } from '@capacitor/core';
|
import { Capacitor } from '@capacitor/core';
|
||||||
|
import { NativeBiometric, BiometryType } from 'capacitor-native-biometric';
|
||||||
// Note: Biometric auth requires native-auth plugin or similar
|
|
||||||
// For now we'll create the interface and you can install the plugin later
|
|
||||||
|
|
||||||
interface BiometricAuthResult {
|
interface BiometricAuthResult {
|
||||||
isAvailable: boolean;
|
isAvailable: boolean;
|
||||||
biometricType: 'fingerprint' | 'face' | 'iris' | 'none';
|
biometricType: 'fingerprint' | 'face' | 'iris' | 'none';
|
||||||
authenticate: () => Promise<boolean>;
|
authenticate: (reason?: string) => Promise<boolean>;
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean;
|
||||||
|
checkCredentials: () => Promise<boolean>;
|
||||||
|
setCredentials: (username: string, password: string) => Promise<void>;
|
||||||
|
deleteCredentials: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useBiometricAuth(): BiometricAuthResult {
|
export function useBiometricAuth(): BiometricAuthResult {
|
||||||
|
|
@ -23,31 +24,46 @@ export function useBiometricAuth(): BiometricAuthResult {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if biometrics are available
|
try {
|
||||||
// This would use @capacitor-community/native-biometric or similar
|
const result = await NativeBiometric.isAvailable();
|
||||||
// For now, we'll assume it's available on mobile
|
setIsAvailable(result.isAvailable);
|
||||||
setIsAvailable(true);
|
|
||||||
setBiometricType('fingerprint'); // Default assumption
|
// 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();
|
checkAvailability();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const authenticate = async (): Promise<boolean> => {
|
const authenticate = useCallback(async (reason?: string): Promise<boolean> => {
|
||||||
if (!isAvailable) return false;
|
if (!isAvailable) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This is where you'd call the actual biometric auth
|
await NativeBiometric.verifyIdentity({
|
||||||
// For example with @capacitor-community/native-biometric:
|
reason: reason || 'Authenticate to access AeThex OS',
|
||||||
// const result = await NativeBiometric.verifyIdentity({
|
title: 'Biometric Authentication',
|
||||||
// reason: "Authenticate to access AeThex OS",
|
subtitle: 'Use your fingerprint or face',
|
||||||
// title: "Biometric Authentication",
|
description: 'Verify your identity to continue',
|
||||||
// subtitle: "Use your fingerprint or face",
|
});
|
||||||
// description: "Please authenticate"
|
|
||||||
// });
|
|
||||||
|
|
||||||
// For now, simulate success
|
|
||||||
console.log('Biometric auth would trigger here');
|
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -55,12 +71,43 @@ export function useBiometricAuth(): BiometricAuthResult {
|
||||||
setIsAuthenticated(false);
|
setIsAuthenticated(false);
|
||||||
return 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 {
|
return {
|
||||||
isAvailable,
|
isAvailable,
|
||||||
biometricType,
|
biometricType,
|
||||||
authenticate,
|
authenticate,
|
||||||
isAuthenticated
|
isAuthenticated,
|
||||||
|
checkCredentials,
|
||||||
|
setCredentials,
|
||||||
|
deleteCredentials,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
12
client/src/lib/embed-utils.ts
Normal file
12
client/src/lib/embed-utils.ts
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -3,6 +3,7 @@ import { Link } from "wouter";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { ArrowLeft, TrendingUp, Code, Star, Eye, Heart, Share2, Loader2 } from "lucide-react";
|
import { ArrowLeft, TrendingUp, Code, Star, Eye, Heart, Share2, Loader2 } from "lucide-react";
|
||||||
|
import { isEmbedded } from "@/lib/embed-utils";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
import { useAuth } from "@/lib/auth";
|
import { useAuth } from "@/lib/auth";
|
||||||
|
|
||||||
|
|
@ -53,21 +54,25 @@ export default function CodeGallery() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const embedded = isEmbedded();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
||||||
{/* Header */}
|
{/* Header - hidden when embedded in OS iframe */}
|
||||||
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
|
{!embedded && (
|
||||||
<div className="flex items-center gap-4">
|
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
|
||||||
<Link href="/">
|
<div className="flex items-center gap-4">
|
||||||
<button className="text-slate-400 hover:text-white">
|
<Link href="/">
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<button className="text-slate-400 hover:text-white">
|
||||||
</button>
|
<ArrowLeft className="w-5 h-5" />
|
||||||
</Link>
|
</button>
|
||||||
<Code className="w-6 h-6 text-cyan-400" />
|
</Link>
|
||||||
<h1 className="text-2xl font-bold text-white">Code Gallery</h1>
|
<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>
|
</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="p-6 max-w-7xl mx-auto">
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Link } from "wouter";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { ArrowLeft, FileText, Folder, Plus, Trash2, Download, Copy, Loader2 } from "lucide-react";
|
import { ArrowLeft, FileText, Folder, Plus, Trash2, Download, Copy, Loader2 } from "lucide-react";
|
||||||
|
import { isEmbedded } from "@/lib/embed-utils";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
import { useAuth } from "@/lib/auth";
|
import { useAuth } from "@/lib/auth";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
@ -70,23 +71,27 @@ export default function FileManager() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const embedded = isEmbedded();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col bg-slate-900">
|
<div className="h-screen flex flex-col bg-slate-900">
|
||||||
{/* Header */}
|
{/* Header - hidden when embedded in OS iframe */}
|
||||||
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
|
{!embedded && (
|
||||||
<div className="flex items-center gap-4">
|
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center justify-between sticky top-0 z-10">
|
||||||
<Link href="/">
|
<div className="flex items-center gap-4">
|
||||||
<button className="text-slate-400 hover:text-white">
|
<Link href="/">
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<button className="text-slate-400 hover:text-white">
|
||||||
</button>
|
<ArrowLeft className="w-5 h-5" />
|
||||||
</Link>
|
</button>
|
||||||
<h1 className="text-2xl font-bold text-white">File Manager</h1>
|
</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>
|
</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">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* File List */}
|
{/* File List */}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
ArrowLeft, ShoppingCart, Star, Plus, Loader2, Gamepad2,
|
ArrowLeft, ShoppingCart, Star, Plus, Loader2, Gamepad2,
|
||||||
Zap, Trophy, Users, DollarSign, TrendingUp, Filter, Search
|
Zap, Trophy, Users, DollarSign, TrendingUp, Filter, Search
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { isEmbedded } from "@/lib/embed-utils";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
import { useAuth } from "@/lib/auth";
|
import { useAuth } from "@/lib/auth";
|
||||||
|
|
||||||
|
|
@ -196,55 +197,59 @@ export default function GameMarketplace() {
|
||||||
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
item.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const embedded = isEmbedded();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-950 text-white">
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-950 text-white">
|
||||||
{/* Header */}
|
{/* Header - hidden when embedded in OS iframe */}
|
||||||
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
|
{!embedded && (
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
|
||||||
<div className="flex items-center justify-between mb-4 gap-2">
|
<div className="max-w-7xl mx-auto">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center justify-between mb-4 gap-2">
|
||||||
<Link href="/hub">
|
<div className="flex items-center gap-3">
|
||||||
<button className="text-slate-400 hover:text-white transition-colors">
|
<Link href="/hub">
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<button className="text-slate-400 hover:text-white transition-colors">
|
||||||
</button>
|
<ArrowLeft className="w-5 h-5" />
|
||||||
</Link>
|
</button>
|
||||||
<div className="flex items-center gap-2">
|
</Link>
|
||||||
<Gamepad2 className="w-6 h-6 text-cyan-400" />
|
<div className="flex items-center gap-2">
|
||||||
<h1 className="text-2xl font-bold">Game Marketplace</h1>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Wallet Balance */}
|
{/* Search & Filter */}
|
||||||
<div className="bg-slate-800 px-4 py-2 rounded-lg border border-slate-700 flex items-center gap-2">
|
<div className="flex gap-2">
|
||||||
<DollarSign className="w-4 h-4 text-yellow-400" />
|
<div className="flex-1 relative">
|
||||||
<span className="font-mono font-bold text-lg text-cyan-400">{wallet.balance} {wallet.currency}</span>
|
<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: Low→High</option>
|
||||||
|
<option value="price-high">Price: High→Low</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</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: Low→High</option>
|
|
||||||
<option value="price-high">Price: High→Low</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="max-w-7xl mx-auto p-4 md:p-6">
|
<div className="max-w-7xl mx-auto p-4 md:p-6">
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
ArrowLeft, Radio, Eye, Heart, MessageCircle, Share2,
|
ArrowLeft, Radio, Eye, Heart, MessageCircle, Share2,
|
||||||
Twitch, Youtube, Play, Clock, Users, TrendingUp, Filter, Search
|
Twitch, Youtube, Play, Clock, Users, TrendingUp, Filter, Search
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { isEmbedded } from "@/lib/embed-utils";
|
||||||
|
|
||||||
interface Stream {
|
interface Stream {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -180,43 +181,47 @@ export default function GameStreaming() {
|
||||||
const liveStreams = filteredStreams.filter(s => s.isLive);
|
const liveStreams = filteredStreams.filter(s => s.isLive);
|
||||||
const recordedStreams = filteredStreams.filter(s => !s.isLive);
|
const recordedStreams = filteredStreams.filter(s => !s.isLive);
|
||||||
|
|
||||||
|
const embedded = isEmbedded();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-950 text-white">
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-950 text-white">
|
||||||
{/* Header */}
|
{/* Header - hidden when embedded in OS iframe */}
|
||||||
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
|
{!embedded && (
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
|
||||||
<div className="flex items-center gap-3 mb-4">
|
<div className="max-w-7xl mx-auto">
|
||||||
<Link href="/hub">
|
<div className="flex items-center gap-3 mb-4">
|
||||||
<button className="text-slate-400 hover:text-white transition-colors">
|
<Link href="/hub">
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<button className="text-slate-400 hover:text-white transition-colors">
|
||||||
</button>
|
<ArrowLeft className="w-5 h-5" />
|
||||||
</Link>
|
</button>
|
||||||
<h1 className="text-2xl font-bold">Game Streaming Hub</h1>
|
</Link>
|
||||||
</div>
|
<h1 className="text-2xl font-bold">Game Streaming Hub</h1>
|
||||||
|
</div>
|
||||||
{/* Search & Filter */}
|
|
||||||
<div className="flex gap-2 flex-col sm:flex-row">
|
{/* Search & Filter */}
|
||||||
<div className="flex-1 relative">
|
<div className="flex gap-2 flex-col sm:flex-row">
|
||||||
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
|
<div className="flex-1 relative">
|
||||||
<input
|
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
|
||||||
placeholder="Search streams, channels..."
|
<input
|
||||||
value={searchQuery}
|
placeholder="Search streams, channels..."
|
||||||
onChange={(e) => setSearchQuery(e.target.value)}
|
value={searchQuery}
|
||||||
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"
|
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>
|
</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>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="max-w-7xl mx-auto p-4 md:p-6">
|
<div className="max-w-7xl mx-auto p-4 md:p-6">
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
Trash2, Award, User, Calendar, Search, Filter, Plus, Loader2,
|
Trash2, Award, User, Calendar, Search, Filter, Plus, Loader2,
|
||||||
Package, AlertCircle, CheckCircle
|
Package, AlertCircle, CheckCircle
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { isEmbedded } from "@/lib/embed-utils";
|
||||||
|
|
||||||
interface Mod {
|
interface Mod {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -199,69 +200,72 @@ export default function ModWorkshop() {
|
||||||
|
|
||||||
const games = ["all", "Minecraft", "Roblox", "Steam Games", "All Games"];
|
const games = ["all", "Minecraft", "Roblox", "Steam Games", "All Games"];
|
||||||
|
|
||||||
|
const embedded = isEmbedded();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-950 text-white">
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-950 text-white">
|
||||||
{/* Header */}
|
{/* Header - hidden when embedded in OS iframe */}
|
||||||
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
|
{!embedded && (
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="bg-slate-950 border-b border-slate-700 sticky top-0 z-10 py-4 px-4 md:px-6">
|
||||||
<div className="flex items-center justify-between mb-4 gap-2">
|
<div className="max-w-7xl mx-auto">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center justify-between mb-4 gap-2">
|
||||||
<Link href="/hub">
|
<div className="flex items-center gap-3">
|
||||||
<button className="text-slate-400 hover:text-white transition-colors">
|
<Link href="/hub">
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<button className="text-slate-400 hover:text-white transition-colors">
|
||||||
</button>
|
<ArrowLeft className="w-5 h-5" />
|
||||||
</Link>
|
</button>
|
||||||
<div className="flex items-center gap-2">
|
</Link>
|
||||||
<Package className="w-6 h-6 text-cyan-400" />
|
<div className="flex items-center gap-2">
|
||||||
<h1 className="text-2xl font-bold">Mod Workshop</h1>
|
<Package className="w-6 h-6 text-cyan-400" />
|
||||||
|
<h1 className="text-2xl font-bold">Mod Workshop</h1>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowUploadModal(true)}
|
onClick={() => setShowUploadModal(true)}
|
||||||
className="bg-cyan-600 hover:bg-cyan-700 gap-2"
|
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"
|
|
||||||
>
|
>
|
||||||
<option value="trending">Trending</option>
|
<Upload className="w-4 h-4" />
|
||||||
<option value="newest">Newest</option>
|
<span className="hidden sm:inline">Upload Mod</span>
|
||||||
<option value="popular">Most Downloaded</option>
|
<span className="sm:hidden">Upload</span>
|
||||||
<option value="rating">Highest Rated</option>
|
</Button>
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Category & Game Filters */}
|
{/* Search & Filters */}
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="space-y-2">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{(["all", "gameplay", "cosmetic", "utility", "enhancement"] as const).map(cat => (
|
<div className="flex-1 relative">
|
||||||
<button
|
<Search className="absolute left-3 top-3 w-4 h-4 text-slate-400" />
|
||||||
key={cat}
|
<input
|
||||||
onClick={() => setSelectedCategory(cat)}
|
placeholder="Search mods, authors..."
|
||||||
className={`px-3 py-1 rounded-lg text-xs font-medium capitalize transition-colors ${
|
value={searchQuery}
|
||||||
selectedCategory === cat
|
onChange={(e) => setSearchQuery(e.target.value)}
|
||||||
? "bg-cyan-600 text-white"
|
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"
|
||||||
: "bg-slate-800 text-slate-300 hover:bg-slate-700"
|
/>
|
||||||
|
</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}
|
{cat === "all" ? "All Categories" : cat}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { ArrowLeft, ShoppingCart, Star, Plus, Loader2 } from "lucide-react";
|
import { ArrowLeft, ShoppingCart, Star, Plus, Loader2 } from "lucide-react";
|
||||||
|
import { isEmbedded } from "@/lib/embed-utils";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
import { useAuth } from "@/lib/auth";
|
import { useAuth } from "@/lib/auth";
|
||||||
|
|
||||||
|
|
@ -75,32 +76,36 @@ export default function Marketplace() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const embedded = isEmbedded();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
||||||
{/* Header */}
|
{/* Header - hidden when embedded in OS iframe */}
|
||||||
<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">
|
{!embedded && (
|
||||||
<div className="flex items-center justify-between gap-2">
|
<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 gap-2 md:gap-4 min-w-0 flex-1">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<Link href="/">
|
<div className="flex items-center gap-2 md:gap-4 min-w-0 flex-1">
|
||||||
<button className="text-slate-400 hover:text-white shrink-0">
|
<Link href="/">
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<button className="text-slate-400 hover:text-white shrink-0">
|
||||||
</button>
|
<ArrowLeft className="w-5 h-5" />
|
||||||
</Link>
|
</button>
|
||||||
<h1 className="text-lg md:text-2xl font-bold text-white truncate">Marketplace</h1>
|
</Link>
|
||||||
</div>
|
<h1 className="text-lg md:text-2xl font-bold text-white truncate">Marketplace</h1>
|
||||||
<div className="flex items-center gap-2 md:gap-4 shrink-0">
|
</div>
|
||||||
<div className="bg-slate-800 px-2 md:px-4 py-1.5 md:py-2 rounded-lg border border-slate-700">
|
<div className="flex items-center gap-2 md:gap-4 shrink-0">
|
||||||
<p className="text-xs text-slate-400 hidden sm:block">Balance</p>
|
<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-sm md:text-xl font-bold text-cyan-400">{balance} LP</p>
|
<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>
|
</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>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
<div className="p-3 md:p-6 max-w-7xl mx-auto">
|
<div className="p-3 md:p-6 max-w-7xl mx-auto">
|
||||||
{/* Category Tabs */}
|
{/* Category Tabs */}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { Input } from "@/components/ui/input";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { ArrowLeft, Send, Search, Loader2 } from "lucide-react";
|
import { ArrowLeft, Send, Search, Loader2 } from "lucide-react";
|
||||||
import { MobileHeader } from "@/components/mobile/MobileHeader";
|
import { MobileHeader } from "@/components/mobile/MobileHeader";
|
||||||
|
import { isEmbedded } from "@/lib/embed-utils";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
import { useAuth } from "@/lib/auth";
|
import { useAuth } from "@/lib/auth";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
@ -96,22 +97,29 @@ export default function Messaging() {
|
||||||
c.username.toLowerCase().includes(searchQuery.toLowerCase())
|
c.username.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const embedded = isEmbedded();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-screen flex flex-col bg-slate-900">
|
<div className="h-screen flex flex-col bg-slate-900">
|
||||||
{/* Mobile Header */}
|
{/* Headers - hidden when embedded in OS iframe */}
|
||||||
<div className="md:hidden">
|
{!embedded && (
|
||||||
<MobileHeader title="Messages" />
|
<>
|
||||||
</div>
|
{/* Mobile Header */}
|
||||||
|
<div className="md:hidden">
|
||||||
|
<MobileHeader title="Messages" />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Desktop Header */}
|
{/* Desktop Header */}
|
||||||
<div className="hidden md:flex bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center gap-4">
|
<div className="hidden md:flex bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center gap-4">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<button className="text-slate-400 hover:text-white">
|
<button className="text-slate-400 hover:text-white">
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
<h1 className="text-2xl font-bold text-white">Messages</h1>
|
<h1 className="text-2xl font-bold text-white">Messages</h1>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-1 overflow-hidden">
|
<div className="flex flex-1 overflow-hidden">
|
||||||
{/* Chat List */}
|
{/* Chat List */}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import { Card } from "@/components/ui/card";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { ArrowLeft, Plus, Trash2, ExternalLink, Github, Globe, Loader2 } from "lucide-react";
|
import { ArrowLeft, Plus, Trash2, ExternalLink, Github, Globe, Loader2 } from "lucide-react";
|
||||||
import { MobileHeader } from "@/components/mobile/MobileHeader";
|
import { MobileHeader } from "@/components/mobile/MobileHeader";
|
||||||
|
import { isEmbedded } from "@/lib/embed-utils";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
import { useAuth } from "@/lib/auth";
|
import { useAuth } from "@/lib/auth";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
@ -102,31 +103,38 @@ export default function Projects() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const embedded = isEmbedded();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
||||||
{/* Mobile Header */}
|
{/* Headers - hidden when embedded in OS iframe */}
|
||||||
<div className="md:hidden">
|
{!embedded && (
|
||||||
<MobileHeader title="Projects" />
|
<>
|
||||||
</div>
|
{/* Mobile Header */}
|
||||||
|
<div className="md:hidden">
|
||||||
|
<MobileHeader title="Projects" />
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Desktop Header */}
|
{/* 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="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">
|
<div className="flex items-center gap-4">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<button className="text-slate-400 hover:text-white transition-colors">
|
<button className="text-slate-400 hover:text-white transition-colors">
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<ArrowLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
<h1 className="text-2xl font-bold text-white">Projects & Portfolio</h1>
|
<h1 className="text-2xl font-bold text-white">Projects & Portfolio</h1>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => setShowForm(!showForm)}
|
onClick={() => setShowForm(!showForm)}
|
||||||
className="bg-cyan-600 hover:bg-cyan-700 gap-2"
|
className="bg-cyan-600 hover:bg-cyan-700 gap-2"
|
||||||
>
|
>
|
||||||
<Plus className="w-4 h-4" />
|
<Plus className="w-4 h-4" />
|
||||||
New Project
|
New Project
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="p-6 max-w-7xl mx-auto">
|
<div className="p-6 max-w-7xl mx-auto">
|
||||||
{/* Add Project Form */}
|
{/* Add Project Form */}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Link } from "wouter";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card } from "@/components/ui/card";
|
import { Card } from "@/components/ui/card";
|
||||||
import { ArrowLeft, Settings, Bell, Lock, Palette, HardDrive, User, Loader2 } from "lucide-react";
|
import { ArrowLeft, Settings, Bell, Lock, Palette, HardDrive, User, Loader2 } from "lucide-react";
|
||||||
|
import { isEmbedded } from "@/lib/embed-utils";
|
||||||
import { supabase } from "@/lib/supabase";
|
import { supabase } from "@/lib/supabase";
|
||||||
import { useAuth } from "@/lib/auth";
|
import { useAuth } from "@/lib/auth";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
@ -88,18 +89,22 @@ export default function SettingsWorkspace() {
|
||||||
saveSettings(newSettings);
|
saveSettings(newSettings);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const embedded = isEmbedded();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
<div className="min-h-screen bg-gradient-to-br from-slate-900 to-slate-800">
|
||||||
{/* Header */}
|
{/* Header - hidden when embedded in OS iframe */}
|
||||||
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center gap-4 sticky top-0 z-10">
|
{!embedded && (
|
||||||
<Link href="/">
|
<div className="bg-slate-950 border-b border-slate-700 px-6 py-4 flex items-center gap-4 sticky top-0 z-10">
|
||||||
<button className="text-slate-400 hover:text-white">
|
<Link href="/">
|
||||||
<ArrowLeft className="w-5 h-5" />
|
<button className="text-slate-400 hover:text-white">
|
||||||
</button>
|
<ArrowLeft className="w-5 h-5" />
|
||||||
</Link>
|
</button>
|
||||||
<Settings className="w-6 h-6 text-cyan-400" />
|
</Link>
|
||||||
<h1 className="text-2xl font-bold text-white">Workspace Settings</h1>
|
<Settings className="w-6 h-6 text-cyan-400" />
|
||||||
</div>
|
<h1 className="text-2xl font-bold text-white">Workspace Settings</h1>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="p-6 max-w-4xl mx-auto">
|
<div className="p-6 max-w-4xl mx-auto">
|
||||||
{/* Appearance Settings */}
|
{/* Appearance Settings */}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import { useMobileNative } from "@/hooks/use-mobile-native";
|
||||||
import { useNativeFeatures } from "@/hooks/use-native-features";
|
import { useNativeFeatures } from "@/hooks/use-native-features";
|
||||||
import { useBiometricAuth } from "@/hooks/use-biometric-auth";
|
import { useBiometricAuth } from "@/hooks/use-biometric-auth";
|
||||||
import { StatusBar, Style } from '@capacitor/status-bar';
|
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 { MobileQuickActions } from "@/components/MobileQuickActions";
|
||||||
import { Minesweeper } from "@/components/games/Minesweeper";
|
import { Minesweeper } from "@/components/games/Minesweeper";
|
||||||
import { CookieClicker } from "@/components/games/CookieClicker";
|
import { CookieClicker } from "@/components/games/CookieClicker";
|
||||||
|
|
@ -1215,131 +1217,162 @@ export default function AeThexOS() {
|
||||||
};
|
};
|
||||||
}, [layout.isMobile]);
|
}, [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
|
// Native Android App Layout
|
||||||
if (layout.isMobile) {
|
if (layout.isMobile) {
|
||||||
const activeWindows = windows.filter(w => !w.minimized);
|
const activeWindows = windows.filter(w => !w.minimized);
|
||||||
const currentWindow = activeWindows[activeWindows.length - 1];
|
const currentWindow = activeWindows[activeWindows.length - 1];
|
||||||
|
|
||||||
return (
|
// Dynamic theme colors based on clearance mode
|
||||||
<div className="h-screen w-screen bg-black overflow-hidden flex flex-col">
|
const isFoundation = clearanceMode === 'foundation';
|
||||||
<style>{`
|
const mobileTheme = {
|
||||||
@keyframes scan {
|
primary: isFoundation ? 'rgb(220, 38, 38)' : 'rgb(59, 130, 246)', // red-600 or blue-500
|
||||||
0% { transform: translateY(-100%); }
|
secondary: isFoundation ? 'rgb(212, 175, 55)' : 'rgb(148, 163, 184)', // gold or slate-400
|
||||||
100% { transform: translateY(100%); }
|
primaryClass: isFoundation ? 'text-red-500' : 'text-blue-500',
|
||||||
}
|
secondaryClass: isFoundation ? 'text-amber-400' : 'text-slate-300',
|
||||||
@keyframes pulse-border {
|
borderClass: isFoundation ? 'border-red-900/50' : 'border-blue-900/50',
|
||||||
0%, 100% { opacity: 0.3; }
|
bgAccent: isFoundation ? 'bg-red-900/20' : 'bg-blue-900/20',
|
||||||
50% { opacity: 0.8; }
|
iconClass: isFoundation ? 'text-red-400' : 'text-blue-400',
|
||||||
}
|
gradientBg: isFoundation
|
||||||
`}</style>
|
? 'linear-gradient(135deg, #0a0a0a 0%, #1a0505 50%, #0a0a0a 100%)'
|
||||||
|
: 'linear-gradient(135deg, #0a0a0a 0%, #050a14 50%, #0a0a0a 100%)',
|
||||||
|
};
|
||||||
|
|
||||||
{/* Ingress Status Bar - Minimal */}
|
return (
|
||||||
<div className="relative h-8 bg-black/90 border-b border-emerald-500/50 shrink-0">
|
<div className="h-screen w-screen overflow-hidden flex flex-col" style={{ background: mobileTheme.gradientBg }}>
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-emerald-500/5 to-transparent"></div>
|
{/* 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="relative flex items-center justify-between px-4 h-full">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Activity className="w-3.5 h-3.5 text-emerald-400" />
|
<span className={`${mobileTheme.primaryClass} font-bold text-sm font-mono`}>AeThex</span>
|
||||||
<Wifi className="w-3.5 h-3.5 text-cyan-400" />
|
<span className={`${mobileTheme.secondaryClass} text-xs font-mono opacity-60`}>
|
||||||
<div className="flex items-center gap-0.5">
|
{isFoundation ? 'FOUNDATION' : 'CORP'}
|
||||||
{[...Array(4)].map((_, i) => (
|
</span>
|
||||||
<div key={i} className="w-0.5 h-1.5 bg-emerald-400 rounded-full" style={{ height: `${(i + 1) * 2}px` }} />
|
<div className="w-1.5 h-1.5 rounded-full bg-green-500 animate-pulse"></div>
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</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>
|
<span>{batteryInfo?.level || 100}%</span>
|
||||||
<Battery className="w-4 h-4 text-green-400" />
|
<Battery className="w-4 h-4" />
|
||||||
<span className="font-mono text-cyan-400">{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
<span>{time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="flex-1 overflow-hidden relative bg-black">
|
<div className="flex-1 overflow-hidden relative">
|
||||||
<AnimatePresence mode="wait">
|
<AnimatePresence mode="wait" initial={false}>
|
||||||
{currentWindow ? (
|
{currentWindow ? (
|
||||||
// Fullscreen App View with 3D Card Flip
|
|
||||||
<motion.div
|
<motion.div
|
||||||
key={currentWindow.id}
|
key={`window-${currentWindow.id}`}
|
||||||
initial={{ rotateY: 90, opacity: 0 }}
|
initial={{ opacity: 0, x: 20 }}
|
||||||
animate={{ rotateY: 0, opacity: 1 }}
|
animate={{ opacity: 1, x: 0 }}
|
||||||
exit={{ rotateY: -90, opacity: 0 }}
|
exit={{ opacity: 0, x: -20 }}
|
||||||
transition={{ duration: 0.4, type: "spring" }}
|
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||||
style={{ transformStyle: "preserve-3d" }}
|
className="absolute inset-0 flex flex-col"
|
||||||
className="h-full w-full flex flex-col relative"
|
|
||||||
>
|
>
|
||||||
{/* Ingress Style - Minimal App Bar */}
|
{/* App Header */}
|
||||||
<div className="relative h-12 bg-black/95 border-b-2 border-emerald-500/50 shrink-0">
|
<div className={`h-14 bg-black/95 ${mobileTheme.borderClass} border-b shrink-0 flex items-center px-4 gap-3`}>
|
||||||
<div className="absolute inset-0" style={{ animation: 'pulse-border 2s ease-in-out infinite' }}>
|
<button
|
||||||
<div className="absolute inset-x-0 bottom-0 h-[2px] bg-gradient-to-r from-transparent via-cyan-500 to-transparent"></div>
|
onClick={() => {
|
||||||
</div>
|
impact('light');
|
||||||
<div className="relative flex items-center px-3 h-full">
|
closeWindow(currentWindow.id);
|
||||||
<button
|
}}
|
||||||
onClick={() => {
|
className={`w-10 h-10 rounded-lg ${mobileTheme.bgAccent} border ${mobileTheme.borderClass} flex items-center justify-center active:opacity-70`}
|
||||||
impact('light');
|
>
|
||||||
closeWindow(currentWindow.id);
|
<ChevronLeft className={`w-5 h-5 ${mobileTheme.iconClass}`} />
|
||||||
}}
|
</button>
|
||||||
className="w-10 h-10 flex items-center justify-center border border-emerald-500/50 active:bg-emerald-500/20"
|
<div className="flex-1">
|
||||||
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
|
<h1 className={`${mobileTheme.secondaryClass} font-bold text-lg`}>{currentWindow.title}</h1>
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* App Content */}
|
{/* App Content */}
|
||||||
<div className="flex-1 overflow-auto relative bg-black">
|
<div className="flex-1 overflow-auto bg-black/80">
|
||||||
{renderAppContent(currentWindow.component)}
|
{renderAppContent(currentWindow.component)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
) : (
|
) : (
|
||||||
// ULTRA FUTURISTIC LAUNCHER
|
// Home Launcher
|
||||||
<motion.div
|
<motion.div
|
||||||
key="launcher"
|
key="launcher-home"
|
||||||
initial={{ opacity: 0, scale: 0.9 }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0, scale: 0.9 }}
|
exit={{ opacity: 0 }}
|
||||||
className="h-full flex flex-col relative"
|
transition={{ duration: 0.15 }}
|
||||||
|
className="absolute inset-0 flex flex-col"
|
||||||
>
|
>
|
||||||
{/* Ingress Style Search Bar */}
|
{/* Header with Theme Toggle */}
|
||||||
<div className="px-4 pt-6 pb-4">
|
<div className="px-4 pt-4 pb-2 flex items-center justify-between">
|
||||||
<div className="relative bg-black/80 border border-emerald-500/50 p-3">
|
<div>
|
||||||
<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>
|
<h1 className={`${mobileTheme.primaryClass} font-bold text-xl`}>
|
||||||
<div className="relative flex items-center gap-3">
|
{isFoundation ? 'The Foundation' : 'The Corporation'}
|
||||||
<Search className="w-5 h-5 text-emerald-400" />
|
</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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="SCANNER SEARCH..."
|
placeholder="Search apps..."
|
||||||
className="flex-1 bg-transparent text-emerald-400 placeholder:text-emerald-400/40 outline-none text-sm font-mono uppercase tracking-wide"
|
className="flex-1 bg-transparent text-white placeholder:text-zinc-500 outline-none text-sm"
|
||||||
onFocus={() => impact('light')}
|
onFocus={() => impact('light')}
|
||||||
/>
|
/>
|
||||||
<Mic className="w-5 h-5 text-cyan-400" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* App Grid - Hexagonal */}
|
{/* App Grid */}
|
||||||
<div className="flex-1 overflow-auto px-4 pb-24">
|
<div className="flex-1 overflow-auto px-4 pb-32">
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
<div className="flex items-center gap-2 mb-3 px-2">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<div className="w-2 h-2 bg-emerald-400"></div>
|
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: mobileTheme.primary }}></div>
|
||||||
<h2 className="text-emerald-400 text-xs uppercase tracking-widest font-mono font-bold">
|
<h2 className={`${mobileTheme.secondaryClass} opacity-90 text-xs uppercase tracking-widest font-bold`}>Quick Access</h2>
|
||||||
Quick Access
|
<div className={`flex-1 h-px ${mobileTheme.borderClass} bg-current opacity-30`}></div>
|
||||||
</h2>
|
|
||||||
<div className="flex-1 h-[1px] bg-emerald-500/30"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-4 gap-3">
|
<div className="grid grid-cols-4 gap-4">
|
||||||
{apps.slice(0, 8).map((app) => (
|
{apps.slice(0, 8).map((app) => (
|
||||||
<button
|
<button
|
||||||
key={app.id}
|
key={app.id}
|
||||||
|
|
@ -1347,16 +1380,12 @@ export default function AeThexOS() {
|
||||||
impact('medium');
|
impact('medium');
|
||||||
openApp(app);
|
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
|
<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`}>
|
||||||
className="relative w-16 h-16 bg-black border-2 border-emerald-500/50 flex items-center justify-center active:border-cyan-400"
|
<div className={mobileTheme.iconClass}>{app.icon}</div>
|
||||||
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>
|
</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}
|
{app.title}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -1364,14 +1393,12 @@ export default function AeThexOS() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* All Apps - Minimal List */}
|
{/* All Apps List */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2 mb-3 px-2">
|
<div className="flex items-center gap-2 mb-4">
|
||||||
<div className="w-2 h-2 bg-cyan-400"></div>
|
<div className="w-2 h-2 rounded-full" style={{ backgroundColor: mobileTheme.secondary }}></div>
|
||||||
<h2 className="text-cyan-400 text-xs uppercase tracking-widest font-mono font-bold">
|
<h2 className={`${mobileTheme.secondaryClass} opacity-90 text-xs uppercase tracking-widest font-bold`}>All Apps</h2>
|
||||||
All Systems
|
<div className={`flex-1 h-px opacity-30`} style={{ backgroundColor: mobileTheme.secondary }}></div>
|
||||||
</h2>
|
|
||||||
<div className="flex-1 h-[1px] bg-cyan-500/30"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{apps.slice(8).map((app) => (
|
{apps.slice(8).map((app) => (
|
||||||
|
|
@ -1381,13 +1408,13 @@ export default function AeThexOS() {
|
||||||
impact('medium');
|
impact('medium');
|
||||||
openApp(app);
|
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={`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="text-emerald-400 scale-75">{app.icon}</div>
|
<div className={`${mobileTheme.iconClass} scale-90`}>{app.icon}</div>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-cyan-400 font-mono text-sm text-left flex-1 uppercase tracking-wide">{app.title}</span>
|
<span className="text-zinc-200 font-medium text-sm text-left flex-1">{app.title}</span>
|
||||||
<ChevronRight className="w-4 h-4 text-emerald-400" />
|
<ChevronRight className="w-4 h-4 text-zinc-600" />
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1398,30 +1425,27 @@ export default function AeThexOS() {
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* INGRESS STYLE NAVIGATION BAR - Lightweight */}
|
{/* Bottom Navigation */}
|
||||||
<div
|
<div
|
||||||
className="relative bg-black/95 border-t-2 border-emerald-500/50 shrink-0 z-50"
|
className={`bg-black/95 border-t ${mobileTheme.borderClass} shrink-0`}
|
||||||
style={{
|
style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}
|
||||||
paddingTop: '0.75rem',
|
|
||||||
paddingBottom: 'calc(0.75rem + 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="flex items-center justify-around py-2 px-4">
|
||||||
<div className="relative flex items-center justify-around px-6">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
impact('medium');
|
impact('medium');
|
||||||
windows.forEach(w => closeWindow(w.id));
|
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"
|
className={`flex flex-col items-center gap-1 p-3 rounded-2xl ${!currentWindow ? mobileTheme.bgAccent : ''}`}
|
||||||
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
|
|
||||||
>
|
>
|
||||||
<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>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
impact('medium');
|
impact('medium');
|
||||||
|
// Open apps drawer / show minimized apps
|
||||||
const minimized = windows.filter(w => w.minimized);
|
const minimized = windows.filter(w => w.minimized);
|
||||||
if (minimized.length > 0) {
|
if (minimized.length > 0) {
|
||||||
setWindows(prev => prev.map(w =>
|
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"
|
className="flex flex-col items-center gap-1 p-3 rounded-2xl relative"
|
||||||
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
|
|
||||||
>
|
>
|
||||||
<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 && (
|
{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}
|
{windows.filter(w => w.minimized).length}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1443,19 +1467,19 @@ export default function AeThexOS() {
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
impact('medium');
|
impact('medium');
|
||||||
if (currentWindow) {
|
// Find and open settings app
|
||||||
closeWindow(currentWindow.id);
|
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"
|
className={`flex flex-col items-center gap-1 p-3 rounded-2xl ${currentWindow?.component === 'settings' ? mobileTheme.bgAccent : ''}`}
|
||||||
style={{ clipPath: 'polygon(30% 0%, 70% 0%, 100% 30%, 100% 70%, 70% 100%, 30% 100%, 0% 70%, 0% 30%)' }}
|
|
||||||
>
|
>
|
||||||
<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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Floating Action Button with Orbital Menu */}
|
{/* Floating Quick Actions */}
|
||||||
<MobileQuickActions />
|
<MobileQuickActions />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
61
package-lock.json
generated
61
package-lock.json
generated
|
|
@ -65,6 +65,7 @@
|
||||||
"@tanstack/react-query": "^5.60.5",
|
"@tanstack/react-query": "^5.60.5",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
|
"capacitor-native-biometric": "^4.2.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
|
@ -186,6 +187,7 @@
|
||||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
|
|
@ -566,6 +568,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@capacitor/core/-/core-8.0.2.tgz",
|
||||||
"integrity": "sha512-EXZfxkL6GFJS2cb7TIBR7RiHA5iz6ufDcl1VmUpI2pga3lJ5Ck2+iqbx7N+osL3XYem9ad4XCidJEMm64DX6UQ==",
|
"integrity": "sha512-EXZfxkL6GFJS2cb7TIBR7RiHA5iz6ufDcl1VmUpI2pga3lJ5Ck2+iqbx7N+osL3XYem9ad4XCidJEMm64DX6UQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"tslib": "^2.1.0"
|
"tslib": "^2.1.0"
|
||||||
}
|
}
|
||||||
|
|
@ -4832,6 +4835,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz",
|
||||||
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
|
"integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
|
|
@ -4875,6 +4879,7 @@
|
||||||
"integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==",
|
"integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"pg-protocol": "*",
|
"pg-protocol": "*",
|
||||||
|
|
@ -4907,6 +4912,7 @@
|
||||||
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
|
"integrity": "sha512-WPigyYuGhgZ/cTPRXB2EwUw+XvsRA3GqHlsP4qteqrnnjDrApbS7MxcGr/hke5iUoeB7E/gQtrs9I37zAJ0Vjw==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
}
|
}
|
||||||
|
|
@ -4917,6 +4923,7 @@
|
||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
|
|
@ -5088,6 +5095,7 @@
|
||||||
"integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==",
|
"integrity": "sha512-CGJ25bc8fRi8Lod/3GHSvXRKi7nBo3kxh0ApW4yCjmrWmRmlT53B5E08XRSZRliygG0aVNxLrBEqPYdz/KcCtQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "4.0.18",
|
"@vitest/utils": "4.0.18",
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
|
|
@ -5419,6 +5427,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
|
|
@ -5449,20 +5458,6 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/bytes": {
|
||||||
"version": "3.1.2",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||||
|
|
@ -5522,6 +5517,24 @@
|
||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"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": {
|
"node_modules/chai": {
|
||||||
"version": "6.2.2",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.39.3.tgz",
|
||||||
"integrity": "sha512-EZ8ZpYvDIvKU9C56JYLOmUskazhad+uXZCTCRN4OnRMsL+xAJ05dv1eCpAG5xzhsm1hqiuC5kAZUCS924u2DTw==",
|
"integrity": "sha512-EZ8ZpYvDIvKU9C56JYLOmUskazhad+uXZCTCRN4OnRMsL+xAJ05dv1eCpAG5xzhsm1hqiuC5kAZUCS924u2DTw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@aws-sdk/client-rds-data": ">=3",
|
"@aws-sdk/client-rds-data": ">=3",
|
||||||
"@cloudflare/workers-types": ">=4",
|
"@cloudflare/workers-types": ">=4",
|
||||||
|
|
@ -6212,7 +6226,8 @@
|
||||||
"version": "8.6.0",
|
"version": "8.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/embla-carousel/-/embla-carousel-8.6.0.tgz",
|
||||||
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
"integrity": "sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/embla-carousel-react": {
|
"node_modules/embla-carousel-react": {
|
||||||
"version": "8.6.0",
|
"version": "8.6.0",
|
||||||
|
|
@ -6402,6 +6417,7 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"esbuild": "bin/esbuild"
|
"esbuild": "bin/esbuild"
|
||||||
},
|
},
|
||||||
|
|
@ -7608,6 +7624,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
|
||||||
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
"integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dompurify": "3.2.7",
|
"dompurify": "3.2.7",
|
||||||
"marked": "14.0.0"
|
"marked": "14.0.0"
|
||||||
|
|
@ -7940,6 +7957,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz",
|
||||||
"integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
|
"integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"pg-connection-string": "^2.11.0",
|
"pg-connection-string": "^2.11.0",
|
||||||
"pg-pool": "^3.11.0",
|
"pg-pool": "^3.11.0",
|
||||||
|
|
@ -8037,6 +8055,7 @@
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
|
|
@ -8108,6 +8127,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
|
|
@ -8274,6 +8294,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
|
|
@ -8314,6 +8335,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"scheduler": "^0.27.0"
|
"scheduler": "^0.27.0"
|
||||||
},
|
},
|
||||||
|
|
@ -8326,6 +8348,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz",
|
||||||
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
|
"integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
|
|
@ -9100,7 +9123,8 @@
|
||||||
"version": "4.1.18",
|
"version": "4.1.18",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz",
|
||||||
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
"integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==",
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/tailwindcss-animate": {
|
"node_modules/tailwindcss-animate": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
|
|
@ -9826,6 +9850,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
||||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
|
|
@ -9968,6 +9993,7 @@
|
||||||
"integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==",
|
"integrity": "sha512-q3l3P9UtEEiAHcsgsqTgf9PPjctrDWoIXW3NpOHFdRDbLvu4DLIcxHangJ4RLrWkBcKjmcs/6NkerI8T/rE4LA==",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"node-gyp-build": "^4.3.0"
|
"node-gyp-build": "^4.3.0"
|
||||||
},
|
},
|
||||||
|
|
@ -10055,6 +10081,7 @@
|
||||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.27.0",
|
"esbuild": "^0.27.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|
@ -10614,6 +10641,7 @@
|
||||||
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
"integrity": "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "4.0.18",
|
"@vitest/expect": "4.0.18",
|
||||||
"@vitest/mocker": "4.0.18",
|
"@vitest/mocker": "4.0.18",
|
||||||
|
|
@ -10888,6 +10916,7 @@
|
||||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
11
package.json
11
package.json
|
|
@ -20,11 +20,11 @@
|
||||||
"start": "NODE_ENV=production node dist/index.js",
|
"start": "NODE_ENV=production node dist/index.js",
|
||||||
"check": "tsc",
|
"check": "tsc",
|
||||||
"db:push": "drizzle-kit push",
|
"db:push": "drizzle-kit push",
|
||||||
"tauri": "cd shell/aethex-shell && npm run tauri",
|
"tauri": "cd shell/aethex-shell && npm run tauri",
|
||||||
"tauri:dev": "cd shell/aethex-shell && npm run tauri dev",
|
"tauri:dev": "cd shell/aethex-shell && npm run tauri dev",
|
||||||
"tauri:build": "cd shell/aethex-shell && npm run tauri build",
|
"tauri:build": "cd shell/aethex-shell && npm run tauri build",
|
||||||
"audit:org-scope": "tsx script/org-scope-audit.ts",
|
"audit:org-scope": "tsx script/org-scope-audit.ts",
|
||||||
"test:org-scope": "tsx --test server/org-scoping.test.ts"
|
"test:org-scope": "tsx --test server/org-scoping.test.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor-community/privacy-screen": "^6.0.0",
|
"@capacitor-community/privacy-screen": "^6.0.0",
|
||||||
|
|
@ -83,6 +83,7 @@
|
||||||
"@tanstack/react-query": "^5.60.5",
|
"@tanstack/react-query": "^5.60.5",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
"bcrypt": "^6.0.0",
|
"bcrypt": "^6.0.0",
|
||||||
|
"capacitor-native-biometric": "^4.2.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cmdk": "^1.1.1",
|
"cmdk": "^1.1.1",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue