build: 3d326e8d-e3da-4f61-941d-6fd836a5b391
This commit is contained in:
+39
@@ -0,0 +1,39 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Redirect, Route } from 'react-router-dom';
|
||||
import { IonApp, IonRouterOutlet } from '@ionic/react';
|
||||
import { IonReactRouter } from '@ionic/react-router';
|
||||
import { setStatusBarStyle, Style } from './utils/statusBar';
|
||||
|
||||
import '@ionic/react/css/core.css';
|
||||
import '@ionic/react/css/normalize.css';
|
||||
import '@ionic/react/css/structure.css';
|
||||
import '@ionic/react/css/typography.css';
|
||||
import '@ionic/react/css/padding.css';
|
||||
import '@ionic/react/css/float-elements.css';
|
||||
import '@ionic/react/css/text-alignment.css';
|
||||
import '@ionic/react/css/text-transformation.css';
|
||||
import '@ionic/react/css/flex-utils.css';
|
||||
import '@ionic/react/css/display.css';
|
||||
|
||||
import './theme/variables.css';
|
||||
|
||||
import Home from './pages/Home';
|
||||
|
||||
const App: React.FC = () => {
|
||||
useEffect(() => {
|
||||
setStatusBarStyle(Style.Light);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<IonApp>
|
||||
<IonReactRouter>
|
||||
<IonRouterOutlet>
|
||||
<Route path="/home" component={Home} exact />
|
||||
<Route exact path="/" render={() => <Redirect to="/home" />} />
|
||||
</IonRouterOutlet>
|
||||
</IonReactRouter>
|
||||
</IonApp>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { setupIonicReact } from '@ionic/react';
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import App from './App';
|
||||
|
||||
// ── Ionic initialisation ──────────────────────────────────────────────────────
|
||||
// ?mode=md|ios overrides (used by AppSuite preview); otherwise follow the platform.
|
||||
const _urlMode = new URLSearchParams(window.location.search).get('mode');
|
||||
const _platform = Capacitor.getPlatform(); // 'ios' | 'android' | 'web'
|
||||
setupIonicReact({
|
||||
mode: (_urlMode === 'md' ? 'md' : _urlMode === 'ios' ? 'ios' : _platform === 'android' ? 'md' : 'ios'),
|
||||
});
|
||||
|
||||
// Derive the studio parent origin dynamically so this works in both dev
|
||||
// (parent at localhost:3000) and production (parent at appcakes.qqura.com).
|
||||
const studioOrigin = (() => {
|
||||
try {
|
||||
if (document.referrer) return new URL(document.referrer).origin;
|
||||
} catch {}
|
||||
return 'http://localhost:3000';
|
||||
})();
|
||||
|
||||
// ── Safe area bridge ──────────────────────────────────────────────────────────
|
||||
// Priority: URL params (first load) → sessionStorage (HMR full-reloads) → postMessage.
|
||||
// Capacitor sets env() natively in compiled apps; these paths cover the browser preview.
|
||||
function _applyInsets(sat: string | null, sab: string | null) {
|
||||
if (sat) { document.documentElement.style.setProperty('--ion-safe-area-top', `${sat}px`); sessionStorage.setItem('__apsuite_sat', sat); }
|
||||
if (sab) { document.documentElement.style.setProperty('--ion-safe-area-bottom', `${sab}px`); sessionStorage.setItem('__apsuite_sab', sab); }
|
||||
}
|
||||
|
||||
const _sp = new URLSearchParams(window.location.search);
|
||||
_applyInsets(
|
||||
_sp.get('sat') ?? sessionStorage.getItem('__apsuite_sat'),
|
||||
_sp.get('sab') ?? sessionStorage.getItem('__apsuite_sab'),
|
||||
);
|
||||
|
||||
window.addEventListener('message', (e) => {
|
||||
if (e.data?.type !== '__apsuite_insets') return;
|
||||
const { sat, sab } = e.data as { sat?: number; sab?: number };
|
||||
_applyInsets(sat != null ? String(sat) : null, sab != null ? String(sab) : null);
|
||||
});
|
||||
|
||||
// Re-apply after React Fast Refresh so insets survive soft HMR cycles.
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.on('vite:afterUpdate', () => {
|
||||
_applyInsets(sessionStorage.getItem('__apsuite_sat'), sessionStorage.getItem('__apsuite_sab'));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Session bridge ────────────────────────────────────────────────────────────
|
||||
// Post auth session to the AppSuite parent frame so the AI can test auth-protected
|
||||
// Edge Functions. Uses import.meta.glob so Vite never errors if supabase.ts is absent.
|
||||
(async () => {
|
||||
const mods = import.meta.glob('./supabase.ts', { eager: false });
|
||||
if ('./supabase.ts' in mods) {
|
||||
try {
|
||||
const { supabase } = await mods['./supabase.ts']() as { supabase: any };
|
||||
supabase.auth.onAuthStateChange((_event: unknown, session: any) => {
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: '__apsuite_session',
|
||||
access_token: session?.access_token ?? null,
|
||||
email: session?.user?.email ?? null,
|
||||
},
|
||||
studioOrigin,
|
||||
);
|
||||
});
|
||||
} catch {
|
||||
// supabase client failed to initialise — session bridge unavailable
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
// ── Runtime error reporting ───────────────────────────────────────────────────
|
||||
// Forward uncaught errors to the AppSuite dev server so the AI can read them.
|
||||
function reportError(message: string, stack?: string) {
|
||||
fetch(`${studioOrigin}/api/agent/runtime-error`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message, stack }),
|
||||
}).catch(() => {});
|
||||
}
|
||||
|
||||
window.onerror = (_msg, _src, _line, _col, err) => {
|
||||
reportError(String(_msg), err?.stack);
|
||||
return false;
|
||||
};
|
||||
|
||||
window.addEventListener('unhandledrejection', (e) => {
|
||||
const err = e.reason as Error | undefined;
|
||||
reportError(err?.message ?? String(e.reason), err?.stack);
|
||||
});
|
||||
|
||||
// ── PWA Elements (Action Sheet, Camera, Toast etc. in browser preview) ────────
|
||||
import { defineCustomElements } from '@ionic/pwa-elements/loader';
|
||||
defineCustomElements(window);
|
||||
|
||||
// ── App bootstrap ─────────────────────────────────────────────────────────────
|
||||
const container = document.getElementById('root');
|
||||
const root = createRoot(container!);
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
IonContent,
|
||||
IonHeader,
|
||||
IonPage,
|
||||
IonTitle,
|
||||
IonToolbar,
|
||||
} from '@ionic/react';
|
||||
|
||||
const Home: React.FC = () => (
|
||||
<IonPage>
|
||||
<IonHeader translucent className="home-header">
|
||||
<IonToolbar className="home-toolbar">
|
||||
<IonTitle>Home</IonTitle>
|
||||
</IonToolbar>
|
||||
</IonHeader>
|
||||
<IonContent fullscreen className="home-content">
|
||||
<div className="home-shell minimal-home-shell">
|
||||
<section className="minimal-welcome-card subtle-welcome-card">
|
||||
<h1>Welcome to your new app</h1>
|
||||
<p className="minimal-subtitle">
|
||||
Chat with the assistant to start adding features and pages
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</IonContent>
|
||||
</IonPage>
|
||||
);
|
||||
|
||||
export default Home;
|
||||
@@ -0,0 +1,152 @@
|
||||
/* Ionic CSS Variables — customise to match your app's brand */
|
||||
:root {
|
||||
--ion-color-primary: #5e6ad2;
|
||||
--ion-color-primary-rgb: 94, 106, 210;
|
||||
--ion-color-primary-contrast: #ffffff;
|
||||
--ion-color-primary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-primary-shade: #535db9;
|
||||
--ion-color-primary-tint: #6e79d7;
|
||||
|
||||
--ion-color-secondary: #7a89f4;
|
||||
--ion-color-secondary-rgb: 122, 137, 244;
|
||||
--ion-color-secondary-contrast: #ffffff;
|
||||
--ion-color-secondary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-secondary-shade: #6b79d7;
|
||||
--ion-color-secondary-tint: #8895f5;
|
||||
|
||||
--ion-color-tertiary: #8f78d8;
|
||||
--ion-color-tertiary-rgb: 143, 120, 216;
|
||||
--ion-color-tertiary-contrast: #ffffff;
|
||||
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-tertiary-shade: #7e6ac0;
|
||||
--ion-color-tertiary-tint: #9a86dc;
|
||||
|
||||
--ion-color-success: #3aa67a;
|
||||
--ion-color-success-rgb: 58, 166, 122;
|
||||
--ion-color-success-contrast: #ffffff;
|
||||
--ion-color-success-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-success-shade: #33926b;
|
||||
--ion-color-success-tint: #4eb088;
|
||||
|
||||
--ion-color-warning: #dfb25a;
|
||||
--ion-color-warning-rgb: 223, 178, 90;
|
||||
--ion-color-warning-contrast: #2d2520;
|
||||
--ion-color-warning-contrast-rgb: 45, 37, 32;
|
||||
--ion-color-warning-shade: #c49d4f;
|
||||
--ion-color-warning-tint: #e2bb6b;
|
||||
|
||||
--ion-color-danger: #cf6679;
|
||||
--ion-color-danger-rgb: 207, 102, 121;
|
||||
--ion-color-danger-contrast: #ffffff;
|
||||
--ion-color-danger-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-danger-shade: #b65a6a;
|
||||
--ion-color-danger-tint: #d57587;
|
||||
|
||||
--ion-color-dark: #1f2430;
|
||||
--ion-color-dark-rgb: 31, 36, 48;
|
||||
--ion-color-dark-contrast: #ffffff;
|
||||
--ion-color-dark-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-dark-shade: #1b202a;
|
||||
--ion-color-dark-tint: #353946;
|
||||
|
||||
--ion-color-medium: #7f8697;
|
||||
--ion-color-medium-rgb: 127, 134, 151;
|
||||
--ion-color-medium-contrast: #ffffff;
|
||||
--ion-color-medium-contrast-rgb: 255, 255, 255;
|
||||
--ion-color-medium-shade: #707686;
|
||||
--ion-color-medium-tint: #8c93a2;
|
||||
|
||||
--ion-color-light: #f3f5fb;
|
||||
--ion-color-light-rgb: 243, 245, 251;
|
||||
--ion-color-light-contrast: #1f2430;
|
||||
--ion-color-light-contrast-rgb: 31, 36, 48;
|
||||
--ion-color-light-shade: #d6d8dd;
|
||||
--ion-color-light-tint: #f4f6fb;
|
||||
|
||||
--app-background: #ffffff;
|
||||
--app-surface: #ffffff;
|
||||
--app-surface-secondary: #f6f7fb;
|
||||
--app-text-primary: #202532;
|
||||
--app-text-secondary: rgba(32, 37, 50, 0.68);
|
||||
--app-text-tertiary: rgba(32, 37, 50, 0.46);
|
||||
--app-accent-soft: rgba(94, 106, 210, 0.12);
|
||||
--app-accent-softer: rgba(94, 106, 210, 0.08);
|
||||
--app-success-soft: rgba(58, 166, 122, 0.12);
|
||||
--app-shadow: 0 18px 48px rgba(82, 95, 140, 0.08);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--app-background);
|
||||
color: var(--app-text-primary);
|
||||
}
|
||||
|
||||
ion-content.home-content {
|
||||
--background: var(--app-background);
|
||||
}
|
||||
|
||||
.home-toolbar {
|
||||
--background: rgba(255, 255, 255, 0.92);
|
||||
--border-width: 0;
|
||||
--border-color: transparent;
|
||||
--color: var(--app-text-primary);
|
||||
box-shadow: none;
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
ion-header.home-header::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.home-shell {
|
||||
padding: calc(88px + var(--ion-safe-area-top)) 24px
|
||||
calc(32px + var(--ion-safe-area-bottom));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.minimal-home-shell {
|
||||
min-height: 100%;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.minimal-welcome-card {
|
||||
text-align: left;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.subtle-welcome-card {
|
||||
max-width: 420px;
|
||||
margin: 0;
|
||||
padding: 28px 24px;
|
||||
background: var(--app-surface-secondary);
|
||||
border-radius: 28px;
|
||||
}
|
||||
|
||||
.minimal-welcome-card h1 {
|
||||
margin: 0;
|
||||
color: var(--app-text-primary);
|
||||
font-size: 32px;
|
||||
line-height: 1.12;
|
||||
letter-spacing: -0.03em;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.minimal-subtitle {
|
||||
margin: 0;
|
||||
color: var(--app-text-secondary);
|
||||
font-size: 15px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
@media (min-width: 680px) {
|
||||
.home-shell {
|
||||
max-width: 720px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.minimal-welcome-card h1 {
|
||||
font-size: 40px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { StatusBar, Style } from '@capacitor/status-bar';
|
||||
export { Style };
|
||||
``
|
||||
const _origin = (() => {
|
||||
try { if (document.referrer) return new URL(document.referrer).origin; } catch {}
|
||||
return window.location.origin;
|
||||
})();
|
||||
|
||||
/** Set status bar icon/text colour. Use instead of StatusBar.setStyle directly. */
|
||||
export async function setStatusBarStyle(style: Style): Promise<void> {
|
||||
try { await StatusBar.setStyle({ style }); } catch {}
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({ type: '__apsuite_statusbar', style }, _origin);
|
||||
}
|
||||
}
|
||||
|
||||
/** Set status bar background colour (Android). Use instead of StatusBar.setBackgroundColor directly. */
|
||||
export async function setStatusBarBackground(color: string): Promise<void> {
|
||||
try { await (StatusBar as any).setBackgroundColor({ color }); } catch {}
|
||||
if (window.parent !== window) {
|
||||
window.parent.postMessage({ type: '__apsuite_statusbar_bg', color }, _origin);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user