Compare commits

...

21 Commits

Author SHA1 Message Date
Timo Knuth
673ead2e3a feat(app): finish onboarding auth flow 2026-07-06 14:32:20 +02:00
Timo Knuth
a1a3367ce4 feat(app): Stitch question screens with shared layout, pre-auth answer buffering
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:04:48 +02:00
Timo Knuth
58b4ad9314 feat(app): personalizing progress screen, auto-advances to paywall
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:00:45 +02:00
Timo Knuth
3bad23c258 feat(app): benefit slides with native overlays
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 14:00:41 +02:00
Timo Knuth
3675f573e1 feat(app): Stitch welcome screen with social proof
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:51:26 +02:00
Timo Knuth
c24544a1b7 feat(app): buffer onboarding answers before auth, flush to profile after sign-up/login
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:50:22 +02:00
Timo Knuth
84624e50cf fix(app): free-tier billing management view, loading guard, dead paywall code cleanup
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:47:56 +02:00
Timo Knuth
91dddc5137 fix(app): restore tappable Privacy/Terms links on paywall
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:34:20 +02:00
Timo Knuth
30a2537eaa feat(app): Stitch paywall — single plan card with free-trial toggle
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:31:37 +02:00
Timo Knuth
b2dbd3e652 feat(app): paywall is param-driven and dismissible; onboarding context routes X to sign-up
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:31:02 +02:00
Timo Knuth
88b12aeb00 fix(app): review fixes — billing-unknown pre-check, free-user top-ups, pro out-of-credits copy
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:19:15 +02:00
Timo Knuth
4ddf16ed20 feat(app): scanner uses real credits for free users, demo mode only for guests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:08:46 +02:00
Timo Knuth
396477660b feat(app): out-of-credits bottom sheet (Stitch design)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:06:40 +02:00
Timo Knuth
6e6041bb5b feat(app): remove hard-paywall redirect — signed-in free users reach the app
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:05:25 +02:00
Timo Knuth
eda0ac98d9 fix(server): pre-check credit balance before health-check AI analysis
The health-check endpoint runs the paid OpenAI analysis before charging
(failed analyses are intentionally not charged). Without a balance
pre-check, a user below HEALTH_CHECK_COST could trigger unlimited free
analyses with fresh idempotency keys.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 13:02:36 +02:00
Timo Knuth
3fea520317 fix(server): make npm test work on Windows (bare node --test)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:40:51 +02:00
Timo Knuth
e36628d49d feat(server): remove hard paywall — credits gate scans, guests stay blocked, free tier gets pro model
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:38:12 +02:00
Timo Knuth
3ebcc94349 feat(server): free tier with 3 monthly credits
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:37:33 +02:00
Timo Knuth
4b79bbcbc9 test(server): add node --test harness with failing free-tier billing tests
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:37:11 +02:00
Timo Knuth
222d885107 Add implementation plan for onboarding redesign, soft paywall and free tier
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:30:36 +02:00
Timo Knuth
c99a08d6a3 Add onboarding/soft-paywall/free-tier design spec + Stitch design reference
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 12:09:48 +02:00
65 changed files with 8735 additions and 2537 deletions

View File

@@ -30,7 +30,7 @@ describe('server auth account deletion', () => {
));
expect(billingAccountDeletes).toHaveLength(1);
const signupChecks = get.mock.calls.filter(([, sql], params) => (
const signupChecks = get.mock.calls.filter(([, sql, params]) => (
typeof sql === 'string'
&& sql.includes('SELECT id FROM auth_users WHERE LOWER(email)')
&& params?.[0] === email

View File

@@ -17,8 +17,8 @@ describe('server billing timestamp normalization', () => {
userId: 'usr_mnjcdwpo_ax9lf68b',
plan: 'free',
provider: 'revenuecat',
cycleStartedAt: new Date('2026-04-01T00:00:00.000Z'),
cycleEndsAt: new Date('2026-05-01T00:00:00.000Z'),
cycleStartedAt: new Date('2027-04-01T00:00:00.000Z'),
cycleEndsAt: new Date('2027-05-01T00:00:00.000Z'),
monthlyAllowance: 15,
usedThisCycle: 0,
topupBalance: 0,
@@ -37,8 +37,8 @@ describe('server billing timestamp normalization', () => {
expect(upsertCall).toBeTruthy();
const params = upsertCall[2];
expect(params[3]).toBe('2026-04-01T00:00:00.000Z');
expect(params[4]).toBe('2026-05-01T00:00:00.000Z');
expect(params[3]).toBe('2027-04-01T00:00:00.000Z');
expect(params[4]).toBe('2027-05-01T00:00:00.000Z');
expect(params[3]).not.toContain('Coordinated Universal Time');
expect(params[4]).not.toContain('Coordinated Universal Time');
});

View File

@@ -103,17 +103,17 @@ describe('StorageService', () => {
expect(result).toBe('en');
});
it('defaults to de when no language stored', async () => {
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(null);
const result = await StorageService.getLanguage();
expect(result).toBe('de');
});
it('defaults to de on error', async () => {
(AsyncStorage.getItem as jest.Mock).mockRejectedValue(new Error('fail'));
const result = await StorageService.getLanguage();
expect(result).toBe('de');
});
it('defaults to en when no language stored', async () => {
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(null);
const result = await StorageService.getLanguage();
expect(result).toBe('en');
});
it('defaults to en on error', async () => {
(AsyncStorage.getItem as jest.Mock).mockRejectedValue(new Error('fail'));
const result = await StorageService.getLanguage();
expect(result).toBe('en');
});
});
describe('saveLanguage', () => {
@@ -175,11 +175,11 @@ describe('StorageService', () => {
expect(result).toBe('Taylor');
});
it('falls back to default profile name when empty', async () => {
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(' ');
const result = await StorageService.getProfileName();
expect(result).toBe('Alex Rivera');
});
it('falls back to default profile name when empty', async () => {
(AsyncStorage.getItem as jest.Mock).mockResolvedValue(' ');
const result = await StorageService.getProfileName();
expect(result).toBe('GreenLens User');
});
it('stores normalized profile name', async () => {
(AsyncStorage.setItem as jest.Mock).mockResolvedValue(undefined);

View File

@@ -96,11 +96,8 @@ function RootLayoutInner() {
colorPalette,
signOut,
session,
billingSummary,
isActivatingEntitlement,
isInitializing,
isLoadingPlants,
isLoadingBilling,
} = useApp();
const colors = useColors(isDarkMode, colorPalette);
const pathname = usePathname();
@@ -118,19 +115,11 @@ function RootLayoutInner() {
}, [signOut]);
const isAppReady = installCheckDone && !isInitializing && !isLoadingPlants;
const hasActiveEntitlement = isActivatingEntitlement
|| (billingSummary?.entitlement?.plan === 'pro'
&& billingSummary?.entitlement?.status === 'active');
const isAllowedWithoutSession = pathname.includes('onboarding')
|| pathname.includes('auth/')
|| pathname.includes('scanner')
|| isShareIntentCallbackPath(pathname)
|| pathname.includes('profile/billing');
const isAllowedWithoutEntitlement = pathname.includes('auth/')
|| pathname.includes('onboarding')
|| pathname.includes('scanner')
|| isShareIntentCallbackPath(pathname)
|| pathname.includes('profile/billing');
let content = null;
@@ -148,10 +137,12 @@ function RootLayoutInner() {
}}
>
<Stack.Screen name="onboarding" options={{ animation: 'none' }} />
<Stack.Screen name="onboarding/slides" options={{ animation: 'slide_from_right' }} />
<Stack.Screen name="onboarding/source" options={{ animation: 'slide_from_right' }} />
<Stack.Screen name="onboarding/goal" options={{ animation: 'slide_from_right' }} />
<Stack.Screen name="onboarding/experience" options={{ animation: 'slide_from_right' }} />
<Stack.Screen name="onboarding/customize" options={{ animation: 'slide_from_right' }} />
<Stack.Screen name="onboarding/personalizing" options={{ animation: 'slide_from_right' }} />
<Stack.Screen name="auth/login" options={{ animation: 'slide_from_right' }} />
<Stack.Screen name="auth/signup" options={{ animation: 'slide_from_right' }} />
<Stack.Screen
@@ -166,12 +157,6 @@ function RootLayoutInner() {
</Stack>
);
}
} else if (!hasActiveEntitlement && !isLoadingBilling && billingSummary && !isAllowedWithoutEntitlement) {
// Signed-in but confirmed non-pro: send to the paywall, not back to
// onboarding — bouncing a logged-in user to onboarding looks like an
// app restart. billingSummary === null means "unknown" (fetch failed),
// never redirect on unknown.
content = <Redirect href="/profile/billing" />;
} else {
content = (
<>
@@ -182,10 +167,12 @@ function RootLayoutInner() {
}}
>
<Stack.Screen name="onboarding" options={{ animation: 'none' }} />
<Stack.Screen name="onboarding/slides" options={{ animation: 'slide_from_right' }} />
<Stack.Screen name="onboarding/source" options={{ animation: 'slide_from_right' }} />
<Stack.Screen name="onboarding/goal" options={{ animation: 'slide_from_right' }} />
<Stack.Screen name="onboarding/experience" options={{ animation: 'slide_from_right' }} />
<Stack.Screen name="onboarding/customize" options={{ animation: 'slide_from_right' }} />
<Stack.Screen name="onboarding/personalizing" options={{ animation: 'slide_from_right' }} />
<Stack.Screen name="auth/login" options={{ animation: 'slide_from_right' }} />
<Stack.Screen name="auth/signup" options={{ animation: 'slide_from_right' }} />
<Stack.Screen name="(tabs)" options={{ animation: 'none' }} />

View File

@@ -1,423 +1,385 @@
import React, { useEffect, useState } from 'react';
import {
View,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
KeyboardAvoidingView,
Platform,
ActivityIndicator,
ScrollView,
Image,
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import { useApp } from '../../context/AppContext';
import { useColors } from '../../constants/Colors';
import { AuthService } from '../../services/authService';
import * as AppleAuthentication from 'expo-apple-authentication';
import Constants from 'expo-constants';
import { useSafeAnalytics } from '../../services/analytics';
const ONBOARDING_AUTH_BACKGROUND = {
light: '#fbfaf3',
dark: '#0a110b',
};
export default function LoginScreen() {
const { isDarkMode, colorPalette, hydrateSession, t } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const posthog = useSafeAnalytics();
const screenBackground = isDarkMode
? ONBOARDING_AUTH_BACKGROUND.dark
: ONBOARDING_AUTH_BACKGROUND.light;
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [appleAvailable, setAppleAvailable] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const isExpoGo = Constants.appOwnership === 'expo';
useEffect(() => {
if (isExpoGo) {
setAppleAvailable(false);
return;
}
let mounted = true;
AppleAuthentication.isAvailableAsync()
.then((available) => {
if (mounted) setAppleAvailable(available);
})
.catch(() => {
if (mounted) setAppleAvailable(false);
});
return () => {
mounted = false;
};
}, [isExpoGo]);
const handleLogin = async () => {
if (!email.trim() || !password) {
setError(t.errFillAllFields);
return;
}
setLoading(true);
setError(null);
try {
const session = await AuthService.login(email, password);
const billing = await hydrateSession(session);
const isPro = billing?.entitlement?.plan === 'pro' && billing?.entitlement?.status === 'active';
// Non-pro accounts land on the paywall with context instead of being
// bounced through the root redirect (which looks like an app restart).
router.replace(isPro || !billing ? '/(tabs)' : '/profile/billing');
} catch (e: any) {
if (e.message === 'USER_NOT_FOUND') {
setError(t.errUserNotFound);
} else if (e.message === 'WRONG_PASSWORD') {
setError(t.errWrongPassword);
} else if (e.message === 'BACKEND_URL_MISSING') {
setError(t.errNetworkError);
} else if (e.message === 'NETWORK_ERROR') {
setError(t.errNetworkError);
} else {
setError(t.errLoginFailed);
}
} finally {
setLoading(false);
}
};
const handleAppleSignIn = async () => {
setLoading(true);
setError(null);
posthog.capture('apple_login_started', { surface: 'login' });
try {
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
if (!credential.identityToken) {
throw new Error('APPLE_AUTH_INVALID');
}
const fullName = [
credential.fullName?.givenName,
credential.fullName?.familyName,
].filter(Boolean).join(' ');
const session = await AuthService.signInWithApple({
identityToken: credential.identityToken,
appleUser: credential.user,
email: credential.email,
name: fullName || undefined,
});
const billing = await hydrateSession(session);
if (session.isNewUser) {
await AsyncStorage.setItem('greenlens_show_tour', 'true');
}
posthog.capture('apple_login_succeeded', { surface: 'login' });
const isPro = billing?.entitlement?.plan === 'pro' && billing?.entitlement?.status === 'active';
if (session.isNewUser) {
router.replace('/onboarding/source');
} else {
router.replace(isPro || !billing ? '/(tabs)' : '/profile/billing');
}
} catch (e: any) {
if (e?.code === 'ERR_REQUEST_CANCELED') {
return;
}
posthog.capture('apple_login_failed', {
surface: 'login',
error: e instanceof Error ? e.message : String(e),
});
setError(e?.message === 'APPLE_BACKEND_UNAVAILABLE'
? 'Apple Login ist auf dem Backend noch nicht aktiviert. Bitte Backend neu starten oder deployen.'
: t.errAuthError);
} finally {
setLoading(false);
}
};
return (
<KeyboardAvoidingView
style={[styles.flex, { backgroundColor: screenBackground }]}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView
contentContainerStyle={styles.scroll}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Logo / Header */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.backBtn, { backgroundColor: colors.surface, borderColor: colors.border }]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={20} color={colors.text} />
</TouchableOpacity>
<Image
source={require('../../assets/icon.png')}
style={styles.logoIcon}
resizeMode="contain"
/>
<Text style={[styles.appName, { color: colors.text }]}>GreenLens</Text>
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>
{t.welcomeBack}
</Text>
</View>
{/* Card */}
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.cardBorder, shadowColor: colors.cardShadow }]}>
{appleAvailable ? (
<AppleAuthentication.AppleAuthenticationButton
buttonType={AppleAuthentication.AppleAuthenticationButtonType.CONTINUE}
buttonStyle={isDarkMode
? AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
: AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
cornerRadius={12}
style={styles.appleButton}
onPress={handleAppleSignIn}
/>
) : null}
{appleAvailable ? (
<View style={styles.dividerRowCompact}>
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
<Text style={[styles.dividerText, { color: colors.textMuted }]}>{t.orDivider}</Text>
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
</View>
) : null}
{/* Email */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>E-Mail</Text>
<View style={[styles.inputRow, { backgroundColor: colors.inputBg, borderColor: colors.inputBorder }]}>
<Ionicons name="mail-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.emailPlaceholder}
placeholderTextColor={colors.textMuted}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
autoComplete="email"
returnKeyType="next"
/>
</View>
</View>
{/* Password */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>{t.passwordLabel}</Text>
<View style={[styles.inputRow, { backgroundColor: colors.inputBg, borderColor: colors.inputBorder }]}>
<Ionicons name="lock-closed-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder="••••••••"
placeholderTextColor={colors.textMuted}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
autoComplete="password"
returnKeyType="done"
onSubmitEditing={handleLogin}
/>
<TouchableOpacity onPress={() => setShowPassword((v) => !v)} style={styles.eyeBtn}>
<Ionicons
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
size={18}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
</View>
{/* Error */}
{error && (
<View style={[styles.errorBox, { backgroundColor: colors.dangerSoft }]}>
<Ionicons name="alert-circle-outline" size={15} color={colors.danger} />
<Text style={[styles.errorText, { color: colors.danger }]}>{error}</Text>
</View>
)}
{/* Login Button */}
<TouchableOpacity
style={[styles.primaryBtn, { backgroundColor: colors.primary, opacity: loading ? 0.7 : 1 }]}
onPress={handleLogin}
activeOpacity={0.82}
disabled={loading}
>
{loading ? (
<ActivityIndicator color={colors.onPrimary} size="small" />
) : (
<Text style={[styles.primaryBtnText, { color: colors.onPrimary }]}>{t.onboardingLogin}</Text>
)}
</TouchableOpacity>
</View>
{/* Divider */}
<View style={styles.dividerRow}>
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
<Text style={[styles.dividerText, { color: colors.textMuted }]}>{t.orDivider}</Text>
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
</View>
{/* Sign Up Link */}
<TouchableOpacity
style={[styles.secondaryBtn, { borderColor: colors.borderStrong, backgroundColor: colors.surface }]}
onPress={() => router.replace('/auth/signup')}
activeOpacity={0.82}
>
<Text style={[styles.secondaryBtnText, { color: colors.text }]}>
{t.noAccountYet}{' '}
<Text style={{ color: colors.primary, fontWeight: '600' }}>{t.onboardingRegister}</Text>
</Text>
</TouchableOpacity>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
flex: { flex: 1 },
scroll: {
flexGrow: 1,
justifyContent: 'center',
paddingHorizontal: 24,
paddingVertical: 48,
},
header: {
alignItems: 'center',
marginBottom: 32,
},
backBtn: {
position: 'absolute',
left: 0,
top: 0,
width: 40,
height: 40,
borderRadius: 20,
borderWidth: 1,
justifyContent: 'center',
alignItems: 'center',
},
logoIcon: {
width: 84,
height: 84,
borderRadius: 20,
marginBottom: 16,
},
appName: {
fontSize: 30,
fontWeight: '700',
letterSpacing: -0.5,
marginBottom: 6,
},
subtitle: {
fontSize: 15,
fontWeight: '400',
},
card: {
borderRadius: 20,
borderWidth: 1,
padding: 24,
gap: 16,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 1,
shadowRadius: 12,
elevation: 4,
},
appleButton: {
width: '100%',
height: 50,
marginBottom: 2,
},
dividerRowCompact: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
marginVertical: 2,
},
fieldGroup: {
gap: 6,
},
label: {
fontSize: 13,
fontWeight: '500',
marginLeft: 2,
},
inputRow: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
},
inputIcon: {
marginRight: 10,
},
input: {
flex: 1,
fontSize: 15,
height: 50,
},
eyeBtn: {
padding: 4,
marginLeft: 6,
},
errorBox: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 10,
},
errorText: {
fontSize: 13,
flex: 1,
},
primaryBtn: {
height: 52,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
marginTop: 4,
},
primaryBtnText: {
fontSize: 16,
fontWeight: '600',
},
dividerRow: {
flexDirection: 'row',
alignItems: 'center',
marginVertical: 20,
gap: 12,
},
dividerLine: {
flex: 1,
height: 1,
},
dividerText: {
fontSize: 13,
},
secondaryBtn: {
height: 52,
borderRadius: 14,
borderWidth: 1,
justifyContent: 'center',
alignItems: 'center',
},
secondaryBtnText: {
fontSize: 15,
},
});
import React, { useEffect, useState } from 'react';
import {
ActivityIndicator,
Alert,
ImageBackground,
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
View,
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import * as AppleAuthentication from 'expo-apple-authentication';
import Constants from 'expo-constants';
import { useApp } from '../../context/AppContext';
import { useColors } from '../../constants/Colors';
import { AuthService } from '../../services/authService';
import { useSafeAnalytics } from '../../services/analytics';
import { PreAuthOnboardingService } from '../../services/preAuthOnboardingService';
import { Language } from '../../types';
const HERO_IMAGE = require('../../assets/welcome_botanical_hero.png');
const getCopy = (language: Language) => {
if (language === 'de') {
return {
headline: 'Welcome back!',
subline: 'Melde dich an und mache mit deiner Pflanzenpflege weiter.',
loginCta: 'Anmelden',
forgot: 'Passwort vergessen?',
forgotTitle: 'Passwort zuruecksetzen',
forgotBody: 'Ein Reset-Link ist noch nicht in der App verfuegbar. Bitte nutze aktuell deine gespeicherten Login-Daten.',
newHere: 'Neu hier?',
create: 'Account erstellen',
emailLabel: 'E-Mail',
};
}
if (language === 'es') {
return {
headline: 'Welcome back!',
subline: 'Inicia sesion para continuar con el cuidado de tus plantas.',
loginCta: 'Iniciar sesion',
forgot: 'Olvidaste tu contrasena?',
forgotTitle: 'Restablecer contrasena',
forgotBody: 'El enlace de restablecimiento aun no esta disponible en la app. Usa tus datos guardados por ahora.',
newHere: 'Nuevo aqui?',
create: 'Crear cuenta',
emailLabel: 'Email',
};
}
return {
headline: 'Welcome back!',
subline: 'Log in to keep scanning, saving and caring for your plants.',
loginCta: 'Log in',
forgot: 'Forgot password?',
forgotTitle: 'Reset password',
forgotBody: 'Password reset is not available in the app yet. Please use your saved login details for now.',
newHere: 'New here?',
create: 'Create account',
emailLabel: 'Email',
};
};
export default function LoginScreen() {
const { isDarkMode, colorPalette, hydrateSession, language, t } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const copy = getCopy(language);
const posthog = useSafeAnalytics();
const isExpoGo = Constants.appOwnership === 'expo';
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [appleAvailable, setAppleAvailable] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (isExpoGo) {
setAppleAvailable(false);
return;
}
let mounted = true;
AppleAuthentication.isAvailableAsync()
.then((available) => {
if (mounted) setAppleAvailable(available);
})
.catch(() => {
if (mounted) setAppleAvailable(false);
});
return () => {
mounted = false;
};
}, [isExpoGo]);
const finishAuth = async (session: Awaited<ReturnType<typeof AuthService.login>>) => {
await hydrateSession(session);
if (session?.userId) {
await PreAuthOnboardingService.flushToProfile(session.userId).catch(() => {});
}
if (session.isNewUser) {
await AsyncStorage.setItem('greenlens_show_tour', 'true');
}
router.replace('/(tabs)');
};
const handleLogin = async () => {
if (!email.trim() || !password) {
setError(t.errFillAllFields);
return;
}
setLoading(true);
setError(null);
try {
const session = await AuthService.login(email, password);
await finishAuth(session);
} catch (e: any) {
if (e.message === 'USER_NOT_FOUND') {
setError(t.errUserNotFound);
} else if (e.message === 'WRONG_PASSWORD') {
setError(t.errWrongPassword);
} else if (e.message === 'BACKEND_URL_MISSING' || e.message === 'NETWORK_ERROR') {
setError(t.errNetworkError);
} else {
setError(t.errLoginFailed);
}
} finally {
setLoading(false);
}
};
const handleAppleSignIn = async () => {
setLoading(true);
setError(null);
posthog.capture('apple_login_started', { surface: 'login' });
try {
const credential = await AppleAuthentication.signInAsync({
requestedScopes: [
AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
AppleAuthentication.AppleAuthenticationScope.EMAIL,
],
});
if (!credential.identityToken) {
throw new Error('APPLE_AUTH_INVALID');
}
const fullName = [
credential.fullName?.givenName,
credential.fullName?.familyName,
].filter(Boolean).join(' ');
const session = await AuthService.signInWithApple({
identityToken: credential.identityToken,
appleUser: credential.user,
email: credential.email,
name: fullName || undefined,
});
posthog.capture('apple_login_succeeded', { surface: 'login' });
await finishAuth(session);
} catch (e: any) {
if (e?.code === 'ERR_REQUEST_CANCELED') return;
posthog.capture('apple_login_failed', {
surface: 'login',
error: e instanceof Error ? e.message : String(e),
});
setError(e?.message === 'APPLE_BACKEND_UNAVAILABLE'
? 'Apple Login ist auf dem Backend noch nicht aktiviert. Bitte Backend neu starten oder deployen.'
: t.errAuthError);
} finally {
setLoading(false);
}
};
return (
<KeyboardAvoidingView
style={[styles.flex, { backgroundColor: isDarkMode ? '#0a110b' : '#fbfaf3' }]}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false}>
<ImageBackground source={HERO_IMAGE} style={styles.hero} imageStyle={styles.heroImage}>
<View style={styles.heroOverlay} />
<TouchableOpacity style={styles.backBtn} onPress={() => router.back()} activeOpacity={0.8}>
<Ionicons name="arrow-back" size={20} color="#ffffff" />
</TouchableOpacity>
<View style={styles.heroCopy}>
<Text style={styles.heroTitle}>{copy.headline}</Text>
<Text style={styles.heroSubline}>{copy.subline}</Text>
</View>
</ImageBackground>
<View style={[styles.sheet, { backgroundColor: isDarkMode ? '#101a12' : '#fbfaf3' }]}>
{appleAvailable ? (
<AppleAuthentication.AppleAuthenticationButton
buttonType={AppleAuthentication.AppleAuthenticationButtonType.CONTINUE}
buttonStyle={isDarkMode
? AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
: AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
cornerRadius={14}
style={styles.appleButton}
onPress={handleAppleSignIn}
/>
) : null}
{appleAvailable ? (
<View style={styles.dividerRow}>
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
<Text style={[styles.dividerText, { color: colors.textMuted }]}>{t.orDivider}</Text>
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
</View>
) : null}
<View style={styles.form}>
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>{copy.emailLabel}</Text>
<View style={[styles.inputRow, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Ionicons name="mail-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.emailPlaceholder}
placeholderTextColor={colors.textMuted}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
autoComplete="email"
returnKeyType="next"
/>
</View>
</View>
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>{t.passwordLabel}</Text>
<View style={[styles.inputRow, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Ionicons name="lock-closed-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder="Password"
placeholderTextColor={colors.textMuted}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
autoComplete="password"
returnKeyType="done"
onSubmitEditing={handleLogin}
/>
<TouchableOpacity onPress={() => setShowPassword((v) => !v)} style={styles.eyeBtn}>
<Ionicons name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={18} color={colors.textMuted} />
</TouchableOpacity>
</View>
</View>
</View>
<TouchableOpacity
style={styles.forgotBtn}
onPress={() => Alert.alert(copy.forgotTitle, copy.forgotBody)}
activeOpacity={0.78}
>
<Text style={[styles.forgotText, { color: colors.primary }]}>{copy.forgot}</Text>
</TouchableOpacity>
{error ? (
<View style={[styles.errorBox, { backgroundColor: colors.dangerSoft }]}>
<Ionicons name="alert-circle-outline" size={15} color={colors.danger} />
<Text style={[styles.errorText, { color: colors.danger }]} selectable>{error}</Text>
</View>
) : null}
<TouchableOpacity
style={[styles.primaryBtn, { backgroundColor: colors.primary, opacity: loading ? 0.72 : 1 }]}
onPress={handleLogin}
activeOpacity={0.84}
disabled={loading}
>
{loading ? (
<ActivityIndicator color={colors.onPrimary} size="small" />
) : (
<Text style={[styles.primaryBtnText, { color: colors.onPrimary }]}>{copy.loginCta}</Text>
)}
</TouchableOpacity>
<TouchableOpacity style={styles.signupLink} onPress={() => router.replace('/auth/signup')} activeOpacity={0.78}>
<Text style={[styles.signupLinkText, { color: colors.textSecondary }]}>
{copy.newHere}{' '}
<Text style={{ color: colors.primary, fontWeight: '800' }}>{copy.create}</Text>
</Text>
</TouchableOpacity>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
}
const styles = StyleSheet.create({
flex: { flex: 1 },
scroll: { flexGrow: 1 },
hero: {
minHeight: 290,
justifyContent: 'flex-end',
paddingHorizontal: 24,
paddingTop: 56,
paddingBottom: 32,
},
heroImage: { resizeMode: 'cover' },
heroOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(5, 12, 7, 0.48)',
},
backBtn: {
position: 'absolute',
top: 54,
left: 22,
width: 42,
height: 42,
borderRadius: 21,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.28)',
},
heroCopy: { gap: 10 },
heroTitle: {
color: '#ffffff',
fontSize: 36,
lineHeight: 41,
fontWeight: '900',
},
heroSubline: {
color: 'rgba(255, 255, 255, 0.88)',
fontSize: 16,
lineHeight: 22,
fontWeight: '700',
},
sheet: {
flex: 1,
marginTop: -24,
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
paddingHorizontal: 22,
paddingTop: 24,
paddingBottom: 34,
gap: 14,
},
appleButton: { width: '100%', height: 56 },
dividerRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
dividerLine: { flex: 1, height: 1 },
dividerText: { fontSize: 12, fontWeight: '800' },
form: { gap: 12 },
fieldGroup: { gap: 6 },
label: { fontSize: 13, fontWeight: '800', marginLeft: 2 },
inputRow: {
height: 54,
borderWidth: 1,
borderRadius: 14,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 14,
},
inputIcon: { marginRight: 10 },
input: { flex: 1, height: 54, fontSize: 15 },
eyeBtn: { padding: 5, marginLeft: 6 },
forgotBtn: { alignItems: 'flex-end', marginTop: -4 },
forgotText: { fontSize: 14, fontWeight: '800' },
errorBox: {
flexDirection: 'row',
alignItems: 'center',
gap: 7,
borderRadius: 12,
paddingHorizontal: 12,
paddingVertical: 10,
},
errorText: { flex: 1, fontSize: 13, lineHeight: 18, fontWeight: '700' },
primaryBtn: {
height: 56,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
primaryBtnText: { fontSize: 17, fontWeight: '900' },
signupLink: { alignItems: 'center', paddingTop: 8 },
signupLinkText: { fontSize: 15, fontWeight: '700' },
});

View File

@@ -1,39 +1,80 @@
import React, { useEffect, useState } from 'react';
import {
View,
ActivityIndicator,
ImageBackground,
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
Text,
TextInput,
TouchableOpacity,
StyleSheet,
KeyboardAvoidingView,
Platform,
ActivityIndicator,
ScrollView,
Image,
View,
} from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import * as AppleAuthentication from 'expo-apple-authentication';
import Constants from 'expo-constants';
import { useApp } from '../../context/AppContext';
import { useColors } from '../../constants/Colors';
import { AuthService } from '../../services/authService';
import AsyncStorage from '@react-native-async-storage/async-storage';
import * as AppleAuthentication from 'expo-apple-authentication';
import Constants from 'expo-constants';
import { useSafeAnalytics } from '../../services/analytics';
import { PreAuthOnboardingService } from '../../services/preAuthOnboardingService';
import { Language } from '../../types';
const ONBOARDING_AUTH_BACKGROUND = {
light: '#fbfaf3',
dark: '#0a110b',
const HERO_IMAGE = require('../../assets/welcome_botanical_hero.png');
const getCopy = (language: Language) => {
if (language === 'de') {
return {
headline: "Let's finish your setup!",
subline: 'Erstelle einen Account, speichere deine Pflanzen und sichere dir 3 Gratis-Scans pro Monat.',
emailCta: 'Mit E-Mail fortfahren',
createCta: 'Account erstellen',
already: 'Schon einen Account?',
login: 'Anmelden',
legal: 'Mit dem Fortfahren akzeptierst du Datenschutz und Nutzungsbedingungen.',
nameLabel: 'Name',
emailLabel: 'E-Mail',
savePlantPrefix: 'Dein Scan wird nach der Registrierung gespeichert:',
};
}
if (language === 'es') {
return {
headline: "Let's finish your setup!",
subline: 'Crea una cuenta para guardar tus plantas y recibir 3 escaneos gratis al mes.',
emailCta: 'Continuar con email',
createCta: 'Crear cuenta',
already: 'Ya tienes cuenta?',
login: 'Iniciar sesion',
legal: 'Al continuar aceptas la Politica de privacidad y los Terminos.',
nameLabel: 'Nombre',
emailLabel: 'Email',
savePlantPrefix: 'Guardaremos tu escaneo despues del registro:',
};
}
return {
headline: "Let's finish your setup!",
subline: 'Create an account to save your plants and 3 free scans per month.',
emailCta: 'Continue with Email',
createCta: 'Create Account',
already: 'Already have an account?',
login: 'Log in',
legal: 'By continuing you agree to our Privacy Policy and Terms.',
nameLabel: 'Name',
emailLabel: 'Email',
savePlantPrefix: 'Your scan will be saved after signup:',
};
};
export default function SignupScreen() {
const { isDarkMode, colorPalette, hydrateSession, getPendingPlant, t } = useApp();
const { isDarkMode, colorPalette, hydrateSession, getPendingPlant, language, t } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const copy = getCopy(language);
const posthog = useSafeAnalytics();
const pendingPlant = getPendingPlant();
const screenBackground = isDarkMode
? ONBOARDING_AUTH_BACKGROUND.dark
: ONBOARDING_AUTH_BACKGROUND.light;
const isExpoGo = Constants.appOwnership === 'expo';
const [name, setName] = useState('');
const [email, setEmail] = useState('');
@@ -41,10 +82,14 @@ export default function SignupScreen() {
const [passwordConfirm, setPasswordConfirm] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [showPasswordConfirm, setShowPasswordConfirm] = useState(false);
const [emailExpanded, setEmailExpanded] = useState(false);
const [appleAvailable, setAppleAvailable] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const isExpoGo = Constants.appOwnership === 'expo';
useEffect(() => {
posthog.capture('signup_screen_viewed', { context: 'onboarding' });
}, [posthog]);
useEffect(() => {
if (isExpoGo) {
@@ -64,6 +109,15 @@ export default function SignupScreen() {
};
}, [isExpoGo]);
const finishAuth = async (session: Awaited<ReturnType<typeof AuthService.signUp>>) => {
await hydrateSession(session);
if (session?.userId) {
await PreAuthOnboardingService.flushToProfile(session.userId).catch(() => {});
}
await AsyncStorage.setItem('greenlens_show_tour', 'true');
router.replace('/(tabs)');
};
const validate = (): string | null => {
if (!name.trim()) return t.errNameRequired;
if (!email.trim() || !email.includes('@')) return t.errEmailInvalid;
@@ -76,27 +130,21 @@ export default function SignupScreen() {
const validationError = validate();
if (validationError) {
setError(validationError);
setEmailExpanded(true);
return;
}
setLoading(true);
setError(null);
try {
const session = await AuthService.signUp(email, name, password);
await hydrateSession(session);
// Flag setzen: Tour beim nächsten App-Öffnen anzeigen
await AsyncStorage.setItem('greenlens_show_tour', 'true');
router.replace('/onboarding/source');
await finishAuth(session);
} catch (e: any) {
if (e.message === 'EMAIL_TAKEN') {
setError(t.errEmailTaken);
} else if (e.message === 'BACKEND_URL_MISSING') {
setError(t.errNetworkError);
} else if (e.message === 'NETWORK_ERROR') {
} else if (e.message === 'BACKEND_URL_MISSING' || e.message === 'NETWORK_ERROR') {
setError(t.errNetworkError);
} else if (e.message === 'SERVER_ERROR') {
setError(t.errServerError);
} else if (e.message === 'AUTH_ERROR') {
setError(t.errAuthError);
} else {
setError(t.errAuthError);
}
@@ -131,21 +179,10 @@ export default function SignupScreen() {
email: credential.email,
name: fullName || undefined,
});
const billing = await hydrateSession(session);
await AsyncStorage.setItem('greenlens_show_tour', 'true');
posthog.capture('apple_login_succeeded', { surface: 'signup' });
const isPro = billing?.entitlement?.plan === 'pro' && billing?.entitlement?.status === 'active';
if (session.isNewUser) {
router.replace('/onboarding/source');
} else {
// Same routing as login: existing non-pro accounts go to the paywall
// directly instead of bouncing through the root entitlement redirect.
router.replace(isPro || !billing ? '/(tabs)' : '/profile/billing');
}
await finishAuth(session);
} catch (e: any) {
if (e?.code === 'ERR_REQUEST_CANCELED') {
return;
}
if (e?.code === 'ERR_REQUEST_CANCELED') return;
posthog.capture('apple_login_failed', {
surface: 'signup',
error: e instanceof Error ? e.message : String(e),
@@ -160,222 +197,177 @@ export default function SignupScreen() {
return (
<KeyboardAvoidingView
style={[styles.flex, { backgroundColor: screenBackground }]}
style={[styles.flex, { backgroundColor: isDarkMode ? '#0a110b' : '#fbfaf3' }]}
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
>
<ScrollView
contentContainerStyle={styles.scroll}
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
>
{/* Header */}
<View style={styles.header}>
<TouchableOpacity
style={[styles.backBtn, { backgroundColor: colors.surface, borderColor: colors.border }]}
onPress={() => router.back()}
>
<Ionicons name="arrow-back" size={20} color={colors.text} />
<ScrollView contentContainerStyle={styles.scroll} keyboardShouldPersistTaps="handled" showsVerticalScrollIndicator={false}>
<ImageBackground source={HERO_IMAGE} style={styles.hero} imageStyle={styles.heroImage}>
<View style={styles.heroOverlay} />
<TouchableOpacity style={styles.backBtn} onPress={() => router.back()} activeOpacity={0.8}>
<Ionicons name="arrow-back" size={20} color="#ffffff" />
</TouchableOpacity>
<Image
source={require('../../assets/icon.png')}
style={styles.logoIcon}
resizeMode="contain"
/>
<Text style={[styles.appName, { color: colors.text }]}>GreenLens</Text>
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>
{t.createAccount}
</Text>
</View>
{/* Pending Plant Hint */}
{pendingPlant && (
<View style={[styles.pendingHint, { backgroundColor: `${colors.primarySoft}40`, borderColor: `${colors.primaryDark}40` }]}>
<Ionicons name="sparkles" size={18} color={colors.primaryDark} />
<Text style={[styles.pendingHintText, { color: colors.primaryDark }]}>
{t.pendingPlantHint.replace('{0}', pendingPlant.result.name)}
</Text>
<View style={styles.heroCopy}>
<Text style={styles.heroTitle}>{copy.headline}</Text>
<Text style={styles.heroSubline}>{copy.subline}</Text>
</View>
)}
</ImageBackground>
<View style={[styles.sheet, { backgroundColor: isDarkMode ? '#101a12' : '#fbfaf3' }]}>
{pendingPlant ? (
<View style={[styles.pendingHint, { backgroundColor: colors.primarySoft, borderColor: colors.border }]}>
<Ionicons name="sparkles" size={18} color={colors.primary} />
<Text style={[styles.pendingHintText, { color: colors.text }]}>
{copy.savePlantPrefix} {pendingPlant.result.name}
</Text>
</View>
) : null}
{/* Card */}
<View style={[styles.card, { backgroundColor: colors.surface, borderColor: colors.cardBorder, shadowColor: colors.cardShadow }]}>
{appleAvailable ? (
<AppleAuthentication.AppleAuthenticationButton
buttonType={AppleAuthentication.AppleAuthenticationButtonType.CONTINUE}
buttonStyle={isDarkMode
? AppleAuthentication.AppleAuthenticationButtonStyle.WHITE
: AppleAuthentication.AppleAuthenticationButtonStyle.BLACK}
cornerRadius={12}
cornerRadius={14}
style={styles.appleButton}
onPress={handleAppleSignIn}
/>
) : null}
{appleAvailable ? (
<View style={styles.dividerRowCompact}>
<View style={styles.dividerRow}>
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
<Text style={[styles.dividerText, { color: colors.textMuted }]}>{t.orDivider}</Text>
<View style={[styles.dividerLine, { backgroundColor: colors.border }]} />
</View>
) : null}
{/* Name */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>Name</Text>
<View style={[styles.inputRow, { backgroundColor: colors.inputBg, borderColor: colors.inputBorder }]}>
<Ionicons name="person-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.namePlaceholder}
placeholderTextColor={colors.textMuted}
value={name}
onChangeText={setName}
autoCapitalize="words"
autoComplete="name"
returnKeyType="next"
/>
</View>
</View>
{!emailExpanded ? (
<TouchableOpacity
style={[styles.emailChoiceBtn, { backgroundColor: colors.surface, borderColor: colors.borderStrong }]}
onPress={() => setEmailExpanded(true)}
activeOpacity={0.84}
>
<Ionicons name="mail-outline" size={19} color={colors.primary} />
<Text style={[styles.emailChoiceText, { color: colors.text }]}>{copy.emailCta}</Text>
</TouchableOpacity>
) : (
<View style={styles.form}>
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>{copy.nameLabel}</Text>
<View style={[styles.inputRow, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Ionicons name="person-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.namePlaceholder}
placeholderTextColor={colors.textMuted}
value={name}
onChangeText={setName}
autoCapitalize="words"
autoComplete="name"
returnKeyType="next"
/>
</View>
</View>
{/* Email */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>E-Mail</Text>
<View style={[styles.inputRow, { backgroundColor: colors.inputBg, borderColor: colors.inputBorder }]}>
<Ionicons name="mail-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.emailPlaceholder}
placeholderTextColor={colors.textMuted}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
autoComplete="email"
returnKeyType="next"
/>
</View>
</View>
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>{copy.emailLabel}</Text>
<View style={[styles.inputRow, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Ionicons name="mail-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.emailPlaceholder}
placeholderTextColor={colors.textMuted}
value={email}
onChangeText={setEmail}
autoCapitalize="none"
keyboardType="email-address"
autoComplete="email"
returnKeyType="next"
/>
</View>
</View>
{/* Password */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>{t.passwordLabel}</Text>
<View style={[styles.inputRow, { backgroundColor: colors.inputBg, borderColor: colors.inputBorder }]}>
<Ionicons name="lock-closed-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.passwordPlaceholder}
placeholderTextColor={colors.textMuted}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
autoComplete="new-password"
returnKeyType="next"
/>
<TouchableOpacity onPress={() => setShowPassword((v) => !v)} style={styles.eyeBtn}>
<Ionicons
name={showPassword ? 'eye-off-outline' : 'eye-outline'}
size={18}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
</View>
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>{t.passwordLabel}</Text>
<View style={[styles.inputRow, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Ionicons name="lock-closed-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.passwordPlaceholder}
placeholderTextColor={colors.textMuted}
value={password}
onChangeText={setPassword}
secureTextEntry={!showPassword}
autoComplete="new-password"
returnKeyType="next"
/>
<TouchableOpacity onPress={() => setShowPassword((v) => !v)} style={styles.eyeBtn}>
<Ionicons name={showPassword ? 'eye-off-outline' : 'eye-outline'} size={18} color={colors.textMuted} />
</TouchableOpacity>
</View>
</View>
{/* Password Confirm */}
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>{t.confirmPasswordLabel}</Text>
<View style={[
styles.inputRow,
{
backgroundColor: colors.inputBg,
borderColor: passwordConfirm && password !== passwordConfirm ? colors.danger : colors.inputBorder,
},
]}>
<Ionicons name="lock-closed-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.confirmPasswordPlaceholder}
placeholderTextColor={colors.textMuted}
value={passwordConfirm}
onChangeText={setPasswordConfirm}
secureTextEntry={!showPasswordConfirm}
autoComplete="new-password"
returnKeyType="done"
onSubmitEditing={handleSignup}
/>
<TouchableOpacity onPress={() => setShowPasswordConfirm((v) => !v)} style={styles.eyeBtn}>
<Ionicons
name={showPasswordConfirm ? 'eye-off-outline' : 'eye-outline'}
size={18}
color={colors.textMuted}
/>
</TouchableOpacity>
</View>
</View>
{/* Password strength hint */}
{password.length > 0 && (
<View style={styles.strengthRow}>
{[1, 2, 3, 4].map((level) => (
<View
key={level}
style={[
styles.strengthBar,
{
backgroundColor:
password.length >= level * 3
? level <= 1
? colors.danger
: level === 2
? colors.warning
: colors.success
: colors.border,
},
]}
/>
))}
<Text style={[styles.strengthText, { color: colors.textMuted }]}>
{password.length < 4
? t.strengthTooShort
: password.length < 7
? t.strengthWeak
: password.length < 10
? t.strengthMedium
: t.strengthStrong}
</Text>
<View style={styles.fieldGroup}>
<Text style={[styles.label, { color: colors.textSecondary }]}>{t.confirmPasswordLabel}</Text>
<View style={[
styles.inputRow,
{
backgroundColor: colors.surface,
borderColor: passwordConfirm && password !== passwordConfirm ? colors.danger : colors.border,
},
]}>
<Ionicons name="lock-closed-outline" size={18} color={colors.textMuted} style={styles.inputIcon} />
<TextInput
style={[styles.input, { color: colors.text }]}
placeholder={t.confirmPasswordPlaceholder}
placeholderTextColor={colors.textMuted}
value={passwordConfirm}
onChangeText={setPasswordConfirm}
secureTextEntry={!showPasswordConfirm}
autoComplete="new-password"
returnKeyType="done"
onSubmitEditing={handleSignup}
/>
<TouchableOpacity onPress={() => setShowPasswordConfirm((v) => !v)} style={styles.eyeBtn}>
<Ionicons name={showPasswordConfirm ? 'eye-off-outline' : 'eye-outline'} size={18} color={colors.textMuted} />
</TouchableOpacity>
</View>
</View>
</View>
)}
{/* Error */}
{error && (
{error ? (
<View style={[styles.errorBox, { backgroundColor: colors.dangerSoft }]}>
<Ionicons name="alert-circle-outline" size={15} color={colors.danger} />
<Text style={[styles.errorText, { color: colors.danger }]}>{error}</Text>
<Text style={[styles.errorText, { color: colors.danger }]} selectable>{error}</Text>
</View>
)}
) : null}
{/* Signup Button */}
<TouchableOpacity
style={[styles.primaryBtn, { backgroundColor: colors.primary, opacity: loading ? 0.7 : 1 }]}
onPress={handleSignup}
activeOpacity={0.82}
disabled={loading}
>
{loading ? (
<ActivityIndicator color={colors.onPrimary} size="small" />
) : (
<Text style={[styles.primaryBtnText, { color: colors.onPrimary }]}>{t.onboardingRegister}</Text>
)}
{emailExpanded ? (
<TouchableOpacity
style={[styles.primaryBtn, { backgroundColor: colors.primary, opacity: loading ? 0.72 : 1 }]}
onPress={handleSignup}
activeOpacity={0.84}
disabled={loading}
>
{loading ? (
<ActivityIndicator color={colors.onPrimary} size="small" />
) : (
<Text style={[styles.primaryBtnText, { color: colors.onPrimary }]}>{copy.createCta}</Text>
)}
</TouchableOpacity>
) : null}
<TouchableOpacity style={styles.loginLink} onPress={() => router.replace('/auth/login')} activeOpacity={0.78}>
<Text style={[styles.loginLinkText, { color: colors.textSecondary }]}>
{copy.already}{' '}
<Text style={{ color: colors.primary, fontWeight: '800' }}>{copy.login}</Text>
</Text>
</TouchableOpacity>
</View>
{/* Login link */}
<TouchableOpacity style={styles.loginLink} onPress={() => router.replace('/auth/login')}>
<Text style={[styles.loginLinkText, { color: colors.textSecondary }]}>
{t.alreadyHaveAccount}{' '}
<Text style={{ color: colors.primary, fontWeight: '600' }}>{t.onboardingLogin}</Text>
</Text>
</TouchableOpacity>
<Text style={[styles.legal, { color: colors.textMuted }]}>{copy.legal}</Text>
</View>
</ScrollView>
</KeyboardAvoidingView>
);
@@ -383,160 +375,108 @@ export default function SignupScreen() {
const styles = StyleSheet.create({
flex: { flex: 1 },
scroll: {
flexGrow: 1,
justifyContent: 'center',
scroll: { flexGrow: 1 },
hero: {
minHeight: 330,
justifyContent: 'flex-end',
paddingHorizontal: 24,
paddingVertical: 48,
paddingTop: 56,
paddingBottom: 34,
},
header: {
alignItems: 'center',
marginBottom: 32,
heroImage: { resizeMode: 'cover' },
heroOverlay: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(5, 12, 7, 0.46)',
},
backBtn: {
position: 'absolute',
left: 0,
top: 0,
width: 40,
height: 40,
borderRadius: 20,
borderWidth: 1,
top: 54,
left: 22,
width: 42,
height: 42,
borderRadius: 21,
alignItems: 'center',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: 'rgba(0, 0, 0, 0.28)',
},
logoIcon: {
width: 84,
height: 84,
borderRadius: 20,
marginBottom: 16,
heroCopy: { gap: 10 },
heroTitle: {
color: '#ffffff',
fontSize: 34,
lineHeight: 39,
fontWeight: '900',
},
appName: {
fontSize: 30,
fontWeight: '700',
letterSpacing: -0.5,
marginBottom: 6,
},
subtitle: {
fontSize: 15,
fontWeight: '400',
},
card: {
borderRadius: 20,
borderWidth: 1,
padding: 24,
gap: 14,
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 1,
shadowRadius: 12,
elevation: 4,
},
appleButton: {
width: '100%',
height: 50,
marginBottom: 2,
},
dividerRowCompact: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
marginVertical: 2,
},
dividerLine: {
flex: 1,
height: 1,
},
dividerText: {
fontSize: 12,
fontWeight: '500',
},
fieldGroup: {
gap: 6,
},
label: {
fontSize: 13,
fontWeight: '500',
marginLeft: 2,
},
inputRow: {
flexDirection: 'row',
alignItems: 'center',
borderWidth: 1,
borderRadius: 12,
paddingHorizontal: 14,
height: 50,
},
inputIcon: {
marginRight: 10,
},
input: {
flex: 1,
fontSize: 15,
height: 50,
},
eyeBtn: {
padding: 4,
marginLeft: 6,
},
strengthRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 4,
marginTop: -4,
},
strengthBar: {
flex: 1,
height: 3,
borderRadius: 2,
},
strengthText: {
fontSize: 11,
marginLeft: 4,
width: 40,
},
errorBox: {
flexDirection: 'row',
alignItems: 'center',
gap: 6,
borderRadius: 10,
paddingHorizontal: 12,
paddingVertical: 10,
},
errorText: {
fontSize: 13,
flex: 1,
},
primaryBtn: {
height: 52,
borderRadius: 14,
justifyContent: 'center',
alignItems: 'center',
marginTop: 4,
},
primaryBtnText: {
heroSubline: {
color: 'rgba(255, 255, 255, 0.88)',
fontSize: 16,
fontWeight: '600',
lineHeight: 22,
fontWeight: '700',
},
loginLink: {
alignItems: 'center',
marginTop: 24,
paddingVertical: 8,
},
loginLinkText: {
fontSize: 15,
sheet: {
flex: 1,
marginTop: -24,
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
paddingHorizontal: 22,
paddingTop: 24,
paddingBottom: 32,
gap: 14,
},
pendingHint: {
flexDirection: 'row',
alignItems: 'center',
padding: 16,
gap: 10,
borderRadius: 16,
borderWidth: 1,
marginBottom: 20,
gap: 12,
paddingHorizontal: 14,
paddingVertical: 12,
},
pendingHintText: {
flex: 1,
fontSize: 13,
fontWeight: '600',
lineHeight: 18,
pendingHintText: { flex: 1, fontSize: 13, lineHeight: 18, fontWeight: '700' },
appleButton: { width: '100%', height: 56 },
dividerRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
dividerLine: { flex: 1, height: 1 },
dividerText: { fontSize: 12, fontWeight: '800' },
emailChoiceBtn: {
height: 56,
borderRadius: 14,
borderWidth: 1.5,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 10,
},
emailChoiceText: { fontSize: 16, fontWeight: '800' },
form: { gap: 12 },
fieldGroup: { gap: 6 },
label: { fontSize: 13, fontWeight: '800', marginLeft: 2 },
inputRow: {
height: 52,
borderWidth: 1,
borderRadius: 14,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 14,
},
inputIcon: { marginRight: 10 },
input: { flex: 1, height: 52, fontSize: 15 },
eyeBtn: { padding: 5, marginLeft: 6 },
errorBox: {
flexDirection: 'row',
alignItems: 'center',
gap: 7,
borderRadius: 12,
paddingHorizontal: 12,
paddingVertical: 10,
},
errorText: { flex: 1, fontSize: 13, lineHeight: 18, fontWeight: '700' },
primaryBtn: {
height: 56,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
primaryBtnText: { fontSize: 17, fontWeight: '900' },
loginLink: { alignItems: 'center', paddingTop: 4, paddingBottom: 2 },
loginLinkText: { fontSize: 15, fontWeight: '700' },
legal: { textAlign: 'center', fontSize: 11.5, lineHeight: 16, paddingHorizontal: 12 },
});

View File

@@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect } from 'react';
import {
Image,
ImageBackground,
@@ -11,153 +11,131 @@ import {
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import Svg, { Path } from 'react-native-svg';
import { useApp } from '../context/AppContext';
import { useSafeAnalytics } from '../services/analytics';
import { Language } from '../types';
type Feature = {
icon: keyof typeof Ionicons.glyphMap;
title: string;
description: string;
const getWelcomeCopy = (language: Language) => {
if (language === 'de') {
return {
headline: 'Willkommen bei GreenLens!',
subline: 'Pflanzen erkennen, verstehen und pflegen — ganz einfach.',
testimonial: '„Endlich überleben meine Pflanzen! Absolute Empfehlung."',
testimonialAuthor: 'Anna M.',
cta: "Los geht's",
login: 'Anmelden',
demoScan: 'Oder direkt eine Pflanze scannen',
legal: 'Mit dem Fortfahren akzeptierst du unsere Datenschutzerklärung und AGB.',
rating: '4,8',
};
}
if (language === 'es') {
return {
headline: '¡Bienvenido a GreenLens!',
subline: 'Identifica, entiende y cuida tus plantas — sin esfuerzo.',
testimonial: '"¡Por fin mis plantas sobreviven! Muy recomendable."',
testimonialAuthor: 'Anna M.',
cta: 'Empezar',
login: 'Iniciar sesión',
demoScan: 'O escanea una planta ahora',
legal: 'Al continuar aceptas nuestra Política de privacidad y Términos.',
rating: '4.8',
};
}
return {
headline: 'Welcome to GreenLens!',
subline: 'Identify, understand and care for your plants — effortlessly.',
testimonial: '"Finally my plants stay alive! Highly recommend."',
testimonialAuthor: 'Anna M.',
cta: "Let's Go",
login: 'Log in',
demoScan: 'Or scan a plant right now',
legal: 'By continuing you agree to our Privacy Policy and Terms.',
rating: '4.8',
};
};
export default function OnboardingScreen() {
const { t } = useApp();
const { height, width } = useWindowDimensions();
const compact = height < 760;
const sheetTop = compact ? 142 : 156;
const waveHeight = compact ? 148 : 170;
const bodyOffset = waveHeight - 2;
const contentTop = compact ? 94 : 108;
const { language } = useApp();
const { height } = useWindowDimensions();
const compact = height < 700;
const posthog = useSafeAnalytics();
const copy = getWelcomeCopy(language);
const features: Feature[] = [
{
icon: 'scan-outline',
title: t.welcomeFeatureIdentifyTitle,
description: t.welcomeFeatureIdentifyDesc,
},
{
icon: 'notifications-outline',
title: t.welcomeFeatureReminderTitle,
description: t.welcomeFeatureReminderDesc,
},
{
icon: 'book-outline',
title: t.welcomeFeatureLibraryTitle,
description: t.welcomeFeatureLibraryDesc,
},
];
useEffect(() => {
posthog.capture('onboarding_welcome_viewed');
}, [posthog]);
return (
<View style={styles.container}>
<ImageBackground
source={require('../assets/welcome_botanical_hero.png')}
style={styles.heroImage}
style={[styles.hero, { height: compact ? '48%' : '55%' }]}
imageStyle={styles.heroImageContent}
resizeMode="cover"
>
<View style={styles.heroShadeTop} />
<View style={styles.heroShadeBottom} />
<SafeAreaView style={styles.safeArea}>
<View style={[styles.brandRow, compact && styles.brandRowCompact]}>
<Image
source={require('../assets/icon.png')}
style={styles.logo}
resizeMode="cover"
/>
<Text style={styles.brandName}>
Green<Text style={styles.brandAccent}>Lens</Text>
</Text>
<SafeAreaView style={styles.heroSafe}>
<View style={styles.heroTopRow}>
<View style={styles.brandRow}>
<Image
source={require('../assets/icon.png')}
style={styles.logo}
resizeMode="cover"
/>
<Text style={styles.brandName}>
Green<Text style={styles.brandAccent}>Lens</Text>
</Text>
</View>
<View style={styles.ratingPill}>
<Ionicons name="star" size={13} color="#f5c04e" />
<Text style={styles.ratingText}>{copy.rating}</Text>
</View>
</View>
<View style={[styles.testimonialCard, compact && styles.testimonialCardCompact]}>
<Text style={styles.testimonialText}>{copy.testimonial}</Text>
<View style={styles.testimonialMeta}>
<Text style={styles.testimonialAuthor}>{copy.testimonialAuthor}</Text>
<View style={styles.starsRow}>
{[0, 1, 2, 3, 4].map((i) => (
<Ionicons key={i} name="star" size={14} color="#f5c04e" />
))}
</View>
</View>
</View>
</SafeAreaView>
</ImageBackground>
<View style={[styles.sheet, { top: sheetTop }]}>
<Svg
width={width}
height={waveHeight}
viewBox={`0 0 ${width} ${waveHeight}`}
preserveAspectRatio="none"
style={styles.sheetWave}
>
<Path
d={`M0 34 C ${width * 0.08} 76 ${width * 0.14} 82 ${width * 0.24} 82 C ${width * 0.38} 82 ${width * 0.52} 82 ${width * 0.64} 82 C ${width * 0.78} 86 ${width * 0.88} 132 ${width} 156 L ${width} ${waveHeight} L 0 ${waveHeight} Z`}
fill="#fbfaf3"
/>
</Svg>
<View style={[styles.sheetBody, { top: bodyOffset }]} />
<View
style={[
styles.sheetContent,
{ top: contentTop },
compact && styles.sheetContentCompact,
]}
>
<Text style={[styles.headline, compact && styles.headlineCompact]}>
{t.welcomeHeadline}
</Text>
<Text style={styles.subheadline}>{t.welcomeSubheadline}</Text>
<View style={styles.sheet}>
<View style={styles.sheetHandle} />
<View style={styles.sheetContent}>
<Text style={[styles.headline, compact && styles.headlineCompact]}>{copy.headline}</Text>
<Text style={styles.subline}>{copy.subline}</Text>
<View style={styles.features}>
{features.map((feature, index) => (
<View
key={feature.title}
style={[
styles.featureRow,
index === features.length - 1 && styles.featureRowLast,
]}
>
<View style={styles.featureIcon}>
<Ionicons name={feature.icon} size={22} color="#a6d66f" />
</View>
<View style={styles.featureCopy}>
<Text style={styles.featureTitle}>{feature.title}</Text>
<Text style={styles.featureDescription}>{feature.description}</Text>
</View>
</View>
))}
</View>
<View style={styles.spacer} />
<TouchableOpacity
style={styles.demoButton}
onPress={() => router.push('/scanner')}
style={styles.cta}
onPress={() => {
posthog.capture('onboarding_started');
router.push('/onboarding/slides');
}}
activeOpacity={0.86}
>
<Ionicons name="scan" size={25} color="#f8f7ef" />
<Text style={styles.demoButtonText}>{t.welcomeDemoScan}</Text>
<Ionicons name="chevron-forward" size={26} color="#f8f7ef" />
<Text style={styles.ctaText}>{copy.cta}</Text>
</TouchableOpacity>
<View style={styles.authRow}>
<TouchableOpacity
style={styles.authButton}
onPress={() => router.push('/auth/signup')}
activeOpacity={0.82}
>
<Text style={styles.authButtonText}>{t.onboardingRegister}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.authButton, styles.loginButton]}
onPress={() => router.push('/auth/login')}
activeOpacity={0.82}
>
<Text style={[styles.authButtonText, styles.loginButtonText]}>
{t.onboardingLogin}
</Text>
</TouchableOpacity>
</View>
<TouchableOpacity
style={styles.subscriptionLink}
onPress={() => router.push('/profile/billing')}
activeOpacity={0.8}
>
<Ionicons name="leaf-outline" size={21} color="#4b7c31" />
<Text style={styles.subscriptionText}>{t.welcomeSubscriptionPlans}</Text>
<Ionicons name="chevron-forward" size={20} color="#4b7c31" />
<TouchableOpacity onPress={() => router.push('/auth/login')} style={styles.loginLink}>
<Text style={styles.loginText}>{copy.login}</Text>
</TouchableOpacity>
<Text style={styles.legalText}>{t.welcomeLegal}</Text>
<TouchableOpacity onPress={() => router.push('/scanner')} style={styles.demoLink}>
<Ionicons name="scan-outline" size={16} color="#4b7c31" />
<Text style={styles.demoText}>{copy.demoScan}</Text>
</TouchableOpacity>
<Text style={styles.legal}>{copy.legal}</Text>
</View>
</View>
</View>
@@ -169,195 +147,182 @@ const styles = StyleSheet.create({
flex: 1,
backgroundColor: '#0a110b',
},
heroImage: {
height: '60%',
minHeight: 430,
hero: {
width: '100%',
},
heroImageContent: {
backgroundColor: '#0a110b',
transform: [{ scale: 1.04 }],
},
heroShadeTop: {
...StyleSheet.absoluteFillObject,
backgroundColor: 'rgba(0,0,0,0.08)',
},
heroShadeBottom: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
height: 190,
backgroundColor: 'rgba(7,12,7,0.2)',
},
safeArea: {
flex: 1,
},
brandRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 15,
paddingHorizontal: 30,
paddingTop: 62,
},
brandRowCompact: {
paddingTop: 42,
},
logo: {
width: 68,
height: 68,
borderRadius: 16,
backgroundColor: '#fff',
},
brandName: {
color: '#f8f7ef',
fontSize: 36,
fontWeight: '900',
},
brandAccent: {
color: '#9bc76e',
},
sheet: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
},
sheetWave: {
position: 'absolute',
left: 0,
right: 0,
top: 0,
height: 120,
backgroundColor: 'rgba(10,17,11,0.4)',
},
sheetBody: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
backgroundColor: '#fbfaf3',
},
sheetContent: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
paddingHorizontal: 24,
paddingBottom: 10,
},
sheetContentCompact: {
paddingHorizontal: 22,
paddingBottom: 8,
},
headline: {
color: '#101c12',
fontSize: 40,
lineHeight: 43,
fontWeight: '900',
marginBottom: 6,
maxWidth: 310,
},
headlineCompact: {
fontSize: 34,
lineHeight: 37,
},
subheadline: {
color: '#5f625d',
fontSize: 15,
lineHeight: 19,
fontWeight: '500',
marginBottom: 11,
},
features: {
marginBottom: 10,
},
featureRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 11,
paddingVertical: 6,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomColor: 'rgba(16,28,18,0.14)',
},
featureRowLast: {
borderBottomWidth: 0,
},
featureIcon: {
width: 46,
height: 46,
borderRadius: 10,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#173817',
},
featureCopy: {
heroSafe: {
flex: 1,
justifyContent: 'space-between',
paddingHorizontal: 20,
paddingTop: 8,
paddingBottom: 24,
},
featureTitle: {
color: '#101c12',
fontSize: 16,
fontWeight: '800',
marginBottom: 2,
},
featureDescription: {
color: '#696b65',
fontSize: 13,
lineHeight: 16,
fontWeight: '500',
},
demoButton: {
height: 60,
borderRadius: 7,
backgroundColor: '#437824',
heroTopRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 18,
marginBottom: 8,
},
demoButtonText: {
color: '#f8f7ef',
fontSize: 21,
fontWeight: '800',
},
authRow: {
brandRow: {
flexDirection: 'row',
gap: 8,
marginBottom: 8,
},
authButton: {
flex: 1,
height: 50,
borderRadius: 7,
borderWidth: 1.4,
borderColor: '#4b7c31',
alignItems: 'center',
justifyContent: 'center',
gap: 10,
},
loginButton: {
borderColor: '#101c12',
logo: {
width: 36,
height: 36,
borderRadius: 10,
backgroundColor: '#fff',
},
authButtonText: {
color: '#4b7c31',
fontSize: 17,
brandName: {
color: '#ffffff',
fontSize: 24,
fontWeight: '900',
},
brandAccent: {
color: '#a6d66f',
},
ratingPill: {
flexDirection: 'row',
alignItems: 'center',
gap: 5,
backgroundColor: 'rgba(255,255,255,0.18)',
borderWidth: 1,
borderColor: 'rgba(255,255,255,0.3)',
borderRadius: 999,
paddingHorizontal: 12,
paddingVertical: 6,
},
ratingText: {
color: '#ffffff',
fontSize: 12,
fontWeight: '700',
},
loginButtonText: {
color: '#101c12',
testimonialCard: {
backgroundColor: 'rgba(255,255,255,0.97)',
borderRadius: 16,
padding: 16,
shadowColor: '#000',
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.18,
shadowRadius: 14,
elevation: 4,
},
subscriptionLink: {
minHeight: 24,
testimonialCardCompact: {
padding: 12,
},
testimonialText: {
color: '#191d16',
fontSize: 14.5,
lineHeight: 20,
fontStyle: 'italic',
marginBottom: 10,
},
testimonialMeta: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
testimonialAuthor: {
color: '#42493c',
fontSize: 13,
fontWeight: '700',
},
starsRow: {
flexDirection: 'row',
gap: 1,
},
sheet: {
flex: 1,
backgroundColor: '#fbfaf3',
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
marginTop: -20,
},
sheetHandle: {
alignSelf: 'center',
width: 44,
height: 5,
borderRadius: 3,
backgroundColor: 'rgba(16,28,18,0.15)',
marginTop: 12,
marginBottom: 8,
},
sheetContent: {
flex: 1,
paddingHorizontal: 24,
paddingTop: 8,
paddingBottom: 12,
},
headline: {
color: '#101c12',
fontSize: 28,
lineHeight: 34,
fontWeight: '900',
textAlign: 'center',
marginBottom: 10,
},
headlineCompact: {
fontSize: 24,
lineHeight: 29,
},
subline: {
color: '#5f625d',
fontSize: 16,
lineHeight: 22,
fontWeight: '500',
textAlign: 'center',
},
spacer: {
flex: 1,
minHeight: 12,
},
cta: {
height: 60,
borderRadius: 16,
backgroundColor: '#437824',
alignItems: 'center',
justifyContent: 'center',
marginBottom: 6,
},
ctaText: {
color: '#f8f7ef',
fontSize: 18,
fontWeight: '800',
},
loginLink: {
alignItems: 'center',
paddingVertical: 10,
},
loginText: {
color: '#437824',
fontSize: 15,
fontWeight: '800',
},
demoLink: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
gap: 8,
marginBottom: 7,
gap: 7,
paddingVertical: 6,
marginBottom: 8,
},
subscriptionText: {
demoText: {
color: '#4b7c31',
fontSize: 15,
fontWeight: '800',
textAlign: 'center',
fontSize: 13.5,
fontWeight: '700',
},
legalText: {
legal: {
color: '#6b6d68',
fontSize: 11,
lineHeight: 14,

View File

@@ -1,83 +1,44 @@
import React, { useMemo, useState } from 'react';
import { ImageBackground, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import React, { useState } from 'react';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useSafeAnalytics } from '../../services/analytics';
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
import { useColors } from '../../constants/Colors';
import { useApp } from '../../context/AppContext';
import { OnboardingProgressService } from '../../services/onboardingProgressService';
const ONBOARDING_BACKGROUND = {
light: '#fbfaf3',
dark: '#0a110b',
};
import { PreAuthOnboardingService } from '../../services/preAuthOnboardingService';
import { OnboardingQuestion, QuestionOption } from '../../components/OnboardingQuestion';
const EXPERIENCE_OPTIONS = [
{ id: 'beginner', icon: 'leaf-outline' as const },
{ id: 'intermediate', icon: 'sunny-outline' as const },
{ id: 'advanced', icon: 'flask-outline' as const },
{ id: 'beginner', emoji: '🌱' },
{ id: 'intermediate', emoji: '☀️' },
{ id: 'advanced', emoji: '🧪' },
];
const getExperienceScreenCopy = (language: 'de' | 'en' | 'es') => {
if (language === 'de') {
return {
step: 'Schritt 3 von 4',
heroBadge: 'Pflege-Tiefe',
subtitles: {
beginner: 'Klare Sprache, sichere Defaults, weniger Fachbegriffe.',
intermediate: 'Praktische Schritte mit genug Kontext.',
advanced: 'Mehr botanische Details und engere Diagnose.',
},
};
}
if (language === 'es') {
return {
step: 'Paso 3 de 4',
heroBadge: 'Nivel de cuidado',
subtitles: {
beginner: 'Lenguaje claro y recomendaciones seguras.',
intermediate: 'Pasos practicos con suficiente contexto.',
advanced: 'Mas detalle botanico y diagnostico preciso.',
},
};
}
return {
step: 'Step 3 of 4',
heroBadge: 'Care depth',
subtitles: {
beginner: 'Clear language, fewer assumptions, safer defaults.',
intermediate: 'Practical care steps with enough detail.',
advanced: 'More botanical context and tighter diagnosis.',
},
};
};
export default function OnboardingExperienceScreen() {
const router = useRouter();
const posthog = useSafeAnalytics();
const { session, isDarkMode, colorPalette, language, t } = useApp();
const { session, isDarkMode, colorPalette, t } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const screenBackground = isDarkMode ? ONBOARDING_BACKGROUND.dark : ONBOARDING_BACKGROUND.light;
const [selectedLevel, setSelectedLevel] = useState<string | null>(null);
const copy = getExperienceScreenCopy(language);
const levelLabels = useMemo(
() => ({
beginner: t.experienceOptionBeginner,
intermediate: t.experienceOptionIntermediate,
advanced: t.experienceOptionAdvanced,
}),
[t.experienceOptionAdvanced, t.experienceOptionBeginner, t.experienceOptionIntermediate],
);
const levelLabels: Record<string, string> = {
beginner: t.experienceOptionBeginner,
intermediate: t.experienceOptionIntermediate,
advanced: t.experienceOptionAdvanced,
};
const options: QuestionOption[] = EXPERIENCE_OPTIONS.map((option) => ({
id: option.id,
emoji: option.emoji,
label: levelLabels[option.id],
}));
const finish = (level: string | null) => {
if (session?.userId && level) {
OnboardingProgressService.setExperienceLevel(session.userId, level);
}
if (level) {
void PreAuthOnboardingService.setAnswer('experienceLevel', level);
}
posthog.capture('onboarding_experience_completed', {
experience_level: level ?? 'skipped',
@@ -86,113 +47,21 @@ export default function OnboardingExperienceScreen() {
};
return (
<View style={[styles.container, { backgroundColor: screenBackground }]}>
{isDarkMode ? <ThemeBackdrop colors={colors} /> : null}
<SafeAreaView style={styles.safeArea} edges={['top', 'left', 'right', 'bottom']}>
<View style={styles.header}>
<View style={[styles.stepPill, { backgroundColor: colors.primarySoft, borderColor: colors.border }]}>
<Text style={[styles.stepLabel, { color: colors.primaryDark }]}>{copy.step}</Text>
</View>
<ImageBackground
source={require('../../assets/onboarding_experience_mockup.png')}
style={[styles.heroPreview, { borderColor: colors.border }]}
imageStyle={styles.heroImage}
resizeMode="cover"
>
<View style={[styles.heroOverlay, { backgroundColor: isDarkMode ? 'rgba(8, 14, 9, 0.4)' : 'rgba(251, 250, 243, 0.24)' }]} />
<View style={[styles.heroMetric, { backgroundColor: colors.surface, borderColor: colors.border }]}>
<Ionicons name="sparkles-outline" size={18} color={colors.primary} />
<Text style={[styles.heroMetricText, { color: colors.text }]}>{copy.heroBadge}</Text>
</View>
</ImageBackground>
<Text style={[styles.title, { color: colors.text }]}>{t.experienceOnboardingTitle}</Text>
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>{t.experienceOnboardingSubtitle}</Text>
</View>
<View style={styles.options}>
{EXPERIENCE_OPTIONS.map((option) => {
const isActive = selectedLevel === option.id;
return (
<TouchableOpacity
key={option.id}
style={[
styles.optionCard,
{
backgroundColor: isActive ? colors.primarySoft : colors.surface,
borderColor: isActive ? colors.primary : colors.border,
},
]}
onPress={() => setSelectedLevel(option.id)}
activeOpacity={0.85}
>
<View style={[styles.optionIcon, { backgroundColor: isActive ? colors.primary : colors.surfaceMuted }]}>
<Ionicons name={option.icon} size={18} color={isActive ? colors.onPrimary : colors.textMuted} />
</View>
<View style={styles.optionCopy}>
<Text style={[styles.optionLabel, { color: colors.text }]}>{levelLabels[option.id as keyof typeof levelLabels]}</Text>
<Text style={[styles.optionSubtitle, { color: colors.textMuted }]}>
{copy.subtitles[option.id as keyof typeof copy.subtitles]}
</Text>
</View>
{isActive && <Ionicons name="checkmark-circle" size={18} color={colors.primary} />}
</TouchableOpacity>
);
})}
</View>
<View style={styles.footer}>
<TouchableOpacity
style={[styles.secondaryBtn, { borderColor: colors.borderStrong, backgroundColor: colors.surface }]}
onPress={() => finish(null)}
>
<Text style={[styles.secondaryBtnText, { color: colors.text }]}>{t.experienceOnboardingSkip}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.primaryBtn, { backgroundColor: selectedLevel ? colors.primary : colors.surfaceMuted }]}
onPress={() => finish(selectedLevel)}
disabled={!selectedLevel}
>
<Text style={[styles.primaryBtnText, { color: selectedLevel ? colors.onPrimary : colors.textMuted }]}>
{t.experienceOnboardingContinue}
</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
</View>
<OnboardingQuestion
colors={colors}
isDarkMode={isDarkMode}
step={3}
totalSteps={4}
title={t.experienceOnboardingTitle}
subtitle={t.experienceOnboardingSubtitle}
options={options}
selectedId={selectedLevel}
onSelect={setSelectedLevel}
onContinue={() => finish(selectedLevel)}
onBack={() => router.back()}
continueLabel={t.experienceOnboardingContinue}
skipLabel={t.experienceOnboardingSkip}
onSkip={() => finish(null)}
/>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
safeArea: { flex: 1, paddingHorizontal: 20, paddingTop: 12, paddingBottom: 14 },
header: { alignItems: 'center', gap: 9, marginBottom: 14 },
stepPill: { borderWidth: 1, borderRadius: 999, paddingHorizontal: 12, paddingVertical: 7 },
stepLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.4 },
heroPreview: { width: '100%', height: 175, borderRadius: 24, borderWidth: 1, overflow: 'hidden', justifyContent: 'flex-end', alignItems: 'flex-start' },
heroImage: { borderRadius: 24 },
heroOverlay: { ...StyleSheet.absoluteFillObject },
heroMetric: { margin: 12, borderRadius: 999, borderWidth: 1, paddingHorizontal: 11, paddingVertical: 7, flexDirection: 'row', alignItems: 'center', gap: 6 },
heroMetricText: { fontSize: 12, fontWeight: '800' },
title: { fontSize: 25, fontWeight: '800', textAlign: 'center', lineHeight: 29 },
subtitle: { fontSize: 13, textAlign: 'center', lineHeight: 18, maxWidth: 320 },
options: { gap: 8, flex: 1 },
optionCard: {
flex: 1,
borderRadius: 15,
borderWidth: 1.5,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 14,
paddingVertical: 16,
gap: 10,
},
optionIcon: { width: 34, height: 34, borderRadius: 17, alignItems: 'center', justifyContent: 'center' },
optionCopy: { flex: 1, gap: 3 },
optionLabel: { fontSize: 14, fontWeight: '700' },
optionSubtitle: { fontSize: 10.5, lineHeight: 14 },
footer: { flexDirection: 'row', gap: 12, marginTop: 10 },
secondaryBtn: { flex: 1, height: 50, borderRadius: 16, borderWidth: 1.5, alignItems: 'center', justifyContent: 'center' },
secondaryBtnText: { fontSize: 15, fontWeight: '600' },
primaryBtn: { flex: 1.2, height: 50, borderRadius: 16, alignItems: 'center', justifyContent: 'center' },
primaryBtnText: { fontSize: 15, fontWeight: '700' },
});

View File

@@ -1,88 +1,46 @@
import React, { useMemo, useState } from 'react';
import { ImageBackground, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import React, { useState } from 'react';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useSafeAnalytics } from '../../services/analytics';
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
import { useColors } from '../../constants/Colors';
import { useApp } from '../../context/AppContext';
import { OnboardingProgressService } from '../../services/onboardingProgressService';
const ONBOARDING_BACKGROUND = {
light: '#fbfaf3',
dark: '#0a110b',
};
import { PreAuthOnboardingService } from '../../services/preAuthOnboardingService';
import { OnboardingQuestion, QuestionOption } from '../../components/OnboardingQuestion';
const GOAL_OPTIONS = [
{ id: 'identify', icon: 'scan-outline' as const },
{ id: 'care', icon: 'water-outline' as const },
{ id: 'collection', icon: 'albums-outline' as const },
{ id: 'learn', icon: 'book-outline' as const },
{ id: 'identify', emoji: '🔍' },
{ id: 'care', emoji: '💧' },
{ id: 'collection', emoji: '🗂️' },
{ id: 'learn', emoji: '📚' },
];
const getGoalScreenCopy = (language: 'de' | 'en' | 'es') => {
if (language === 'de') {
return {
step: 'Schritt 2 von 4',
heroBadge: 'Erstes Ziel',
subtitles: {
identify: 'Schnell erkennen, Pflege danach klaeren.',
care: 'Aus Symptomen konkrete Schritte machen.',
collection: 'Eine saubere Pflanzenbibliothek aufbauen.',
learn: 'Pflanzenwissen einfacher einsortieren.',
},
};
}
if (language === 'es') {
return {
step: 'Paso 2 de 4',
heroBadge: 'Primer objetivo',
subtitles: {
identify: 'Respuesta rapida primero, cuidado despues.',
care: 'Convertir sintomas en pasos claros.',
collection: 'Crear una biblioteca de plantas ordenada.',
learn: 'Aprender plantas con explicaciones simples.',
},
};
}
return {
step: 'Step 2 of 4',
heroBadge: 'First goal',
subtitles: {
identify: 'Fast answer first, care details after.',
care: 'Turn symptoms into a clear next step.',
collection: 'Build a tidy plant library over time.',
learn: 'Browse plants with simpler explanations.',
},
};
};
export default function OnboardingGoalScreen() {
const router = useRouter();
const posthog = useSafeAnalytics();
const { session, isDarkMode, colorPalette, language, t } = useApp();
const { session, isDarkMode, colorPalette, t } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const screenBackground = isDarkMode ? ONBOARDING_BACKGROUND.dark : ONBOARDING_BACKGROUND.light;
const [selectedGoal, setSelectedGoal] = useState<string | null>(null);
const copy = getGoalScreenCopy(language);
const goalLabels = useMemo(
() => ({
identify: t.goalOptionIdentify,
care: t.goalOptionCare,
collection: t.goalOptionCollection,
learn: t.goalOptionLearn,
}),
[t.goalOptionCare, t.goalOptionCollection, t.goalOptionIdentify, t.goalOptionLearn],
);
const goalLabels: Record<string, string> = {
identify: t.goalOptionIdentify,
care: t.goalOptionCare,
collection: t.goalOptionCollection,
learn: t.goalOptionLearn,
};
const options: QuestionOption[] = GOAL_OPTIONS.map((option) => ({
id: option.id,
emoji: option.emoji,
label: goalLabels[option.id],
}));
const finish = (goal: string | null) => {
if (session?.userId && goal) {
OnboardingProgressService.setPrimaryGoal(session.userId, goal);
}
if (goal) {
void PreAuthOnboardingService.setAnswer('primaryGoal', goal);
}
posthog.capture('onboarding_goal_completed', {
goal: goal ?? 'skipped',
@@ -91,113 +49,21 @@ export default function OnboardingGoalScreen() {
};
return (
<View style={[styles.container, { backgroundColor: screenBackground }]}>
{isDarkMode ? <ThemeBackdrop colors={colors} /> : null}
<SafeAreaView style={styles.safeArea} edges={['top', 'left', 'right', 'bottom']}>
<View style={styles.header}>
<View style={[styles.stepPill, { backgroundColor: colors.primarySoft, borderColor: colors.border }]}>
<Text style={[styles.stepLabel, { color: colors.primaryDark }]}>{copy.step}</Text>
</View>
<ImageBackground
source={require('../../assets/onboarding_goal_mockup.png')}
style={[styles.heroPreview, { borderColor: colors.border }]}
imageStyle={styles.heroImage}
resizeMode="cover"
>
<View style={[styles.heroOverlay, { backgroundColor: isDarkMode ? 'rgba(8, 14, 9, 0.22)' : 'rgba(251, 250, 243, 0.28)' }]} />
<View style={[styles.heroBadge, { backgroundColor: colors.primary }]}>
<Ionicons name="flag-outline" size={16} color={colors.onPrimary} />
<Text style={[styles.heroBadgeText, { color: colors.onPrimary }]}>{copy.heroBadge}</Text>
</View>
</ImageBackground>
<Text style={[styles.title, { color: colors.text }]}>{t.goalOnboardingTitle}</Text>
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>{t.goalOnboardingSubtitle}</Text>
</View>
<View style={styles.options}>
{GOAL_OPTIONS.map((option) => {
const isActive = selectedGoal === option.id;
return (
<TouchableOpacity
key={option.id}
style={[
styles.optionCard,
{
backgroundColor: isActive ? colors.primarySoft : colors.surface,
borderColor: isActive ? colors.primary : colors.border,
},
]}
onPress={() => setSelectedGoal(option.id)}
activeOpacity={0.85}
>
<View style={[styles.optionIcon, { backgroundColor: isActive ? colors.primary : colors.surfaceMuted }]}>
<Ionicons name={option.icon} size={18} color={isActive ? colors.onPrimary : colors.textMuted} />
</View>
<View style={styles.optionCopy}>
<Text style={[styles.optionLabel, { color: colors.text }]}>{goalLabels[option.id as keyof typeof goalLabels]}</Text>
<Text style={[styles.optionSubtitle, { color: colors.textMuted }]}>
{copy.subtitles[option.id as keyof typeof copy.subtitles]}
</Text>
</View>
{isActive && <Ionicons name="checkmark-circle" size={18} color={colors.primary} />}
</TouchableOpacity>
);
})}
</View>
<View style={styles.footer}>
<TouchableOpacity
style={[styles.secondaryBtn, { borderColor: colors.borderStrong, backgroundColor: colors.surface }]}
onPress={() => finish(null)}
>
<Text style={[styles.secondaryBtnText, { color: colors.text }]}>{t.goalOnboardingSkip}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.primaryBtn, { backgroundColor: selectedGoal ? colors.primary : colors.surfaceMuted }]}
onPress={() => finish(selectedGoal)}
disabled={!selectedGoal}
>
<Text style={[styles.primaryBtnText, { color: selectedGoal ? colors.onPrimary : colors.textMuted }]}>
{t.goalOnboardingContinue}
</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
</View>
<OnboardingQuestion
colors={colors}
isDarkMode={isDarkMode}
step={2}
totalSteps={4}
title={t.goalOnboardingTitle}
subtitle={t.goalOnboardingSubtitle}
options={options}
selectedId={selectedGoal}
onSelect={setSelectedGoal}
onContinue={() => finish(selectedGoal)}
onBack={() => router.back()}
continueLabel={t.goalOnboardingContinue}
skipLabel={t.goalOnboardingSkip}
onSkip={() => finish(null)}
/>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
safeArea: { flex: 1, paddingHorizontal: 20, paddingTop: 12, paddingBottom: 14 },
header: { alignItems: 'center', gap: 9, marginBottom: 14 },
stepPill: { borderWidth: 1, borderRadius: 999, paddingHorizontal: 12, paddingVertical: 7 },
stepLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.4 },
heroPreview: { width: '100%', height: 175, borderRadius: 24, borderWidth: 1, overflow: 'hidden', justifyContent: 'flex-end', alignItems: 'flex-start' },
heroImage: { borderRadius: 24 },
heroOverlay: { ...StyleSheet.absoluteFillObject },
heroBadge: { margin: 12, borderRadius: 999, paddingHorizontal: 11, paddingVertical: 7, flexDirection: 'row', alignItems: 'center', gap: 6 },
heroBadgeText: { fontSize: 12, fontWeight: '800' },
title: { fontSize: 25, fontWeight: '800', textAlign: 'center', lineHeight: 29 },
subtitle: { fontSize: 13, textAlign: 'center', lineHeight: 18, maxWidth: 320 },
options: { gap: 8, flex: 1 },
optionCard: {
flex: 1,
borderRadius: 15,
borderWidth: 1.5,
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 14,
paddingVertical: 12,
gap: 10,
},
optionIcon: { width: 34, height: 34, borderRadius: 17, alignItems: 'center', justifyContent: 'center' },
optionCopy: { flex: 1, gap: 3 },
optionLabel: { fontSize: 14, fontWeight: '700' },
optionSubtitle: { fontSize: 10.5, lineHeight: 14 },
footer: { flexDirection: 'row', gap: 12, marginTop: 10 },
secondaryBtn: { flex: 1, height: 50, borderRadius: 16, borderWidth: 1.5, alignItems: 'center', justifyContent: 'center' },
secondaryBtnText: { fontSize: 15, fontWeight: '600' },
primaryBtn: { flex: 1.2, height: 50, borderRadius: 16, alignItems: 'center', justifyContent: 'center' },
primaryBtnText: { fontSize: 15, fontWeight: '700' },
});

View File

@@ -16,7 +16,6 @@ const ONBOARDING_BACKGROUND = {
const getHealthOnboardingCopy = (language: 'de' | 'en' | 'es') => {
if (language === 'de') {
return {
step: 'Schritt 4 von 4',
title: 'Wo ist der Health-Scan?',
subtitle: 'Du findest ihn auf jeder gespeicherten Pflanze, direkt unter der Beschreibung.',
buttonPreview: 'Health-Scan starten',
@@ -36,7 +35,6 @@ const getHealthOnboardingCopy = (language: 'de' | 'en' | 'es') => {
if (language === 'es') {
return {
step: 'Paso 4 de 4',
title: 'Donde esta el health-scan?',
subtitle: 'Lo encuentras en cada planta guardada, justo debajo de la descripcion.',
buttonPreview: 'Iniciar health-scan',
@@ -55,7 +53,6 @@ const getHealthOnboardingCopy = (language: 'de' | 'en' | 'es') => {
}
return {
step: 'Step 4 of 4',
title: 'Where is the health scan?',
subtitle: 'It lives on every saved plant, directly below the plant description.',
buttonPreview: 'Start health scan',
@@ -86,19 +83,23 @@ export default function HealthCheckOnboardingScreen() {
skipped,
plan: billingSummary?.entitlement?.plan ?? 'free',
});
const hasActiveEntitlement = billingSummary?.entitlement?.plan === 'pro'
&& billingSummary?.entitlement?.status === 'active';
router.replace(hasActiveEntitlement ? '/(tabs)' : '/profile/billing');
router.replace('/onboarding/personalizing');
};
return (
<View style={[styles.container, { backgroundColor: screenBackground }]}>
{isDarkMode ? <ThemeBackdrop colors={colors} /> : null}
<SafeAreaView style={styles.safeArea} edges={['top', 'left', 'right', 'bottom']}>
<View style={styles.header}>
<View style={[styles.stepPill, { backgroundColor: colors.primarySoft, borderColor: colors.border }]}>
<Text style={[styles.stepLabel, { color: colors.primaryDark }]}>{copy.step}</Text>
<View style={styles.topBar}>
<TouchableOpacity onPress={() => router.back()} style={[styles.backBtn, { backgroundColor: colors.surface }]}>
<Ionicons name="arrow-back" size={20} color={colors.primary} />
</TouchableOpacity>
<View style={[styles.progressTrack, { backgroundColor: colors.primarySoft }]}>
<View style={[styles.progressFill, { backgroundColor: colors.primary, width: '100%' }]} />
</View>
<View style={styles.backBtn} />
</View>
<View style={styles.header}>
<Text style={[styles.title, { color: colors.text }]}>{copy.title}</Text>
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>{copy.subtitle}</Text>
</View>
@@ -161,9 +162,11 @@ export default function HealthCheckOnboardingScreen() {
const styles = StyleSheet.create({
container: { flex: 1 },
safeArea: { flex: 1, paddingHorizontal: 20, paddingTop: 24, paddingBottom: 20 },
header: { gap: 9, marginBottom: 18 },
stepPill: { alignSelf: 'flex-start', borderWidth: 1, borderRadius: 999, paddingHorizontal: 12, paddingVertical: 7 },
stepLabel: { fontSize: 12, fontWeight: '800', textTransform: 'uppercase', letterSpacing: 0.4 },
topBar: { flexDirection: 'row', alignItems: 'center', gap: 14, paddingVertical: 10 },
backBtn: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center' },
progressTrack: { flex: 1, height: 6, borderRadius: 3, overflow: 'hidden' },
progressFill: { height: 6, borderRadius: 3 },
header: { gap: 9, marginTop: 8, marginBottom: 18 },
title: { fontSize: 30, lineHeight: 34, fontWeight: '900' },
subtitle: { fontSize: 14, lineHeight: 20 },
content: { gap: 14, paddingBottom: 12 },

View File

@@ -0,0 +1,165 @@
import React, { useEffect, useRef, useState } from 'react';
import { Animated, Easing, Image, StyleSheet, Text, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { router } from 'expo-router';
import Svg, { Circle } from 'react-native-svg';
import { useApp } from '../../context/AppContext';
import { useColors } from '../../constants/Colors';
import { useSafeAnalytics } from '../../services/analytics';
import { Language } from '../../types';
const getCopy = (language: Language) => {
if (language === 'de') {
return {
status: 'Dein Pflegeplan wird personalisiert…',
steps: ['Antworten werden analysiert', 'Pflegeplan wird erstellt', 'Scan-Credits werden vorbereitet', 'Plan wird finalisiert'],
testimonial: '„GreenLens hat meine Geigenfeige gerettet. Die täglichen Routinen sind unglaublich präzise."',
author: 'Elena R.',
rating: '4,8 APP-STORE-BEWERTUNG',
};
}
if (language === 'es') {
return {
status: 'Personalizando tu plan de cuidados…',
steps: ['Analizando tus respuestas', 'Creando tu plan de cuidados', 'Preparando tus créditos de escaneo', 'Finalizando tu plan'],
testimonial: '"GreenLens salvó mi ficus lyrata. Las rutinas diarias son increíblemente precisas."',
author: 'Elena R.',
rating: '4.8 VALORACIÓN EN APP STORE',
};
}
return {
status: 'Personalizing your care plan…',
steps: ['Analyzing your answers', 'Building your care plan', 'Preparing your scan credits', 'Finalizing your plan'],
testimonial: '"GreenLens completely saved my Fiddle Leaf Fig. The daily routines feel incredibly precise."',
author: 'Elena R.',
rating: '4.8 APP STORE RATING',
};
};
const STEP_THRESHOLDS = [25, 50, 75, 95];
const RING_SIZE = 150;
const RING_STROKE_WIDTH = 7;
const RING_RADIUS = (RING_SIZE - RING_STROKE_WIDTH) / 2;
const RING_CIRCUMFERENCE = 2 * Math.PI * RING_RADIUS;
const AnimatedCircle = Animated.createAnimatedComponent(Circle);
export default function OnboardingPersonalizingScreen() {
const { language, isDarkMode, colorPalette } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const posthog = useSafeAnalytics();
const copy = getCopy(language);
const progress = useRef(new Animated.Value(0)).current;
const [percent, setPercent] = useState(0);
const navigated = useRef(false);
const strokeDashoffset = progress.interpolate({
inputRange: [0, 100],
outputRange: [RING_CIRCUMFERENCE, 0],
});
useEffect(() => {
posthog.capture('onboarding_personalizing_viewed');
const listener = progress.addListener(({ value }) => setPercent(Math.round(value)));
Animated.timing(progress, {
toValue: 100,
duration: 6000,
easing: Easing.inOut(Easing.cubic),
useNativeDriver: false,
}).start(({ finished }) => {
if (finished && !navigated.current) {
navigated.current = true;
setTimeout(() => {
posthog.capture('paywall_opened', { source: 'onboarding' });
router.replace('/profile/billing?view=paywall&context=onboarding');
}, 450);
}
});
return () => progress.removeListener(listener);
}, [progress, posthog]);
return (
<SafeAreaView style={[styles.safe, { backgroundColor: isDarkMode ? '#0a110b' : '#f8fbef' }]}>
<Text style={[styles.percent, { color: colors.primary }]}>{percent}%</Text>
<View style={styles.ringWrap}>
<Svg width={RING_SIZE} height={RING_SIZE} style={StyleSheet.absoluteFill}>
<Circle
cx={RING_SIZE / 2}
cy={RING_SIZE / 2}
r={RING_RADIUS}
stroke={colors.primarySoft}
strokeWidth={RING_STROKE_WIDTH}
fill="none"
/>
<AnimatedCircle
cx={RING_SIZE / 2}
cy={RING_SIZE / 2}
r={RING_RADIUS}
stroke={colors.primary}
strokeWidth={RING_STROKE_WIDTH}
fill="none"
strokeLinecap="round"
strokeDasharray={`${RING_CIRCUMFERENCE}, ${RING_CIRCUMFERENCE}`}
strokeDashoffset={strokeDashoffset}
rotation="-90"
originX={RING_SIZE / 2}
originY={RING_SIZE / 2}
/>
</Svg>
<Image source={require('../../assets/paywall_scan_background.png')} style={styles.ringImage} />
</View>
<View style={[styles.statusPill, { backgroundColor: colors.surfaceMuted }]}>
<Ionicons name="sync-outline" size={15} color={colors.textSecondary} />
<Text style={[styles.statusText, { color: colors.textSecondary }]}>{copy.status}</Text>
</View>
<View style={styles.checklist}>
{copy.steps.map((label, index) => {
const done = percent >= STEP_THRESHOLDS[index];
return (
<View key={label} style={styles.checkRow}>
<Ionicons
name={done ? 'checkmark-circle' : 'ellipse-outline'}
size={24}
color={done ? colors.primary : colors.border}
/>
<Text style={[styles.checkLabel, { color: done ? colors.text : colors.textMuted }]}>{label}</Text>
</View>
);
})}
</View>
<View style={[styles.testimonialCard, { backgroundColor: colors.surface }]}>
<View style={styles.testimonialHeader}>
<Text style={[styles.testimonialAuthor, { color: colors.text }]}>{copy.author}</Text>
<View style={styles.starsRow}>
{[0, 1, 2, 3, 4].map((i) => <Ionicons key={i} name="star" size={13} color="#f5c04e" />)}
</View>
</View>
<Text style={[styles.testimonialText, { color: colors.textSecondary }]}>{copy.testimonial}</Text>
</View>
<View style={[styles.ratingBadge, { borderColor: colors.primary }]}>
<Ionicons name="ribbon-outline" size={16} color={colors.primary} />
<Text style={[styles.ratingText, { color: colors.primary }]}>{copy.rating}</Text>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, alignItems: 'center', paddingHorizontal: 24, paddingTop: 30 },
percent: { fontSize: 56, fontWeight: '900', marginBottom: 16 },
ringWrap: { width: 150, height: 150, alignItems: 'center', justifyContent: 'center', marginBottom: 22 },
ringImage: { width: 112, height: 112, borderRadius: 56 },
statusPill: { flexDirection: 'row', alignItems: 'center', gap: 8, borderRadius: 999, paddingHorizontal: 18, paddingVertical: 11, marginBottom: 26 },
statusText: { fontSize: 14.5, fontWeight: '800' },
checklist: { alignSelf: 'stretch', gap: 15, marginBottom: 26, paddingHorizontal: 8 },
checkRow: { flexDirection: 'row', alignItems: 'center', gap: 12 },
checkLabel: { fontSize: 16.5, fontWeight: '700' },
testimonialCard: { alignSelf: 'stretch', borderRadius: 18, padding: 16, marginBottom: 14 },
testimonialHeader: { flexDirection: 'row', justifyContent: 'space-between', marginBottom: 6 },
testimonialAuthor: { fontSize: 14.5, fontWeight: '800' },
starsRow: { flexDirection: 'row', gap: 2 },
testimonialText: { fontSize: 14, lineHeight: 20, fontStyle: 'italic' },
ratingBadge: { flexDirection: 'row', alignItems: 'center', gap: 7, borderWidth: 1.5, borderRadius: 999, paddingHorizontal: 16, paddingVertical: 9 },
ratingText: { fontSize: 12.5, fontWeight: '900', letterSpacing: 0.6 },
});

495
app/onboarding/slides.tsx Normal file
View File

@@ -0,0 +1,495 @@
import React, { useEffect, useState } from 'react';
import { Image, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useSafeAnalytics } from '../../services/analytics';
import { useColors } from '../../constants/Colors';
import { useApp } from '../../context/AppContext';
import { Language } from '../../types';
type ColorsType = ReturnType<typeof useColors>;
const getSlidesCopy = (language: Language) => {
if (language === 'de') {
return {
slides: [
{
title: 'Scanne jede Pflanze',
body: 'Richte die Kamera auf eine Pflanze und GreenLens erkennt sie in Sekunden.',
},
{
title: 'Health Check & Pflegeplan',
body: 'GreenLens erkennt Probleme früh und erstellt deinen Rettungsplan.',
},
{
title: 'Nie mehr Gießen vergessen',
body: 'Smarte Erinnerungen und deine Pflanzen-Bibliothek halten alles im Blick.',
},
],
resultChip: 'Monstera · 98%',
healthCheckLabel: 'Health Check',
overwateringDetected: 'Überwässerung erkannt',
rescuePlanReady: '7-Tage-Rettungsplan bereit',
waterReminder: 'Monstera gießen — heute',
fertilizeReminder: 'Basilikum düngen — in 3 Tagen',
continueLabel: 'Weiter',
};
}
if (language === 'es') {
return {
slides: [
{
title: 'Escanea cualquier planta',
body: 'Apunta la cámara a una planta y GreenLens la identifica en segundos.',
},
{
title: 'Chequeo de salud y plan de cuidados',
body: 'GreenLens detecta problemas a tiempo y crea tu plan de rescate.',
},
{
title: 'No olvides regar nunca más',
body: 'Recordatorios inteligentes y tu biblioteca de plantas lo mantienen todo al día.',
},
],
resultChip: 'Monstera · 98%',
healthCheckLabel: 'Chequeo de salud',
overwateringDetected: 'Exceso de riego detectado',
rescuePlanReady: 'Plan de rescate de 7 días listo',
waterReminder: 'Regar Monstera — hoy',
fertilizeReminder: 'Abonar albahaca — en 3 días',
continueLabel: 'Continuar',
};
}
return {
slides: [
{
title: 'Scan Any Plant',
body: 'Point your camera at a plant and GreenLens identifies it in seconds.',
},
{
title: 'Health Check & Care Plan',
body: 'GreenLens spots problems early and builds a rescue plan for you.',
},
{
title: 'Never Forget Watering',
body: 'Smart reminders and your personal plant library keep everything on track.',
},
],
resultChip: 'Monstera · 98%',
healthCheckLabel: 'Health Check',
overwateringDetected: 'Overwatering detected',
rescuePlanReady: '7-day rescue plan ready',
waterReminder: 'Water Monstera — today',
fertilizeReminder: 'Fertilize Basil — in 3 days',
continueLabel: 'Continue',
};
};
function ScanFrameOverlay({ resultChip, colors }: { resultChip: string; colors: ColorsType }) {
return (
<>
<View style={styles.scanFrameWrap} pointerEvents="none">
<View style={[styles.cornerTL, { borderColor: colors.primary }]} />
<View style={[styles.cornerTR, { borderColor: colors.primary }]} />
<View style={[styles.cornerBL, { borderColor: colors.primary }]} />
<View style={[styles.cornerBR, { borderColor: colors.primary }]} />
</View>
<View style={styles.resultChip}>
<Ionicons name="leaf" size={15} color={colors.primary} />
<Text style={styles.resultChipText}>{resultChip}</Text>
</View>
</>
);
}
function HealthCardOverlay({
label,
overwateringDetected,
rescuePlanReady,
}: {
label: string;
overwateringDetected: string;
rescuePlanReady: string;
}) {
return (
<View style={styles.healthCard}>
<View style={styles.healthCardHeader}>
<View style={styles.healthCardIcon}>
<Ionicons name="medkit" size={16} color="#C62828" />
</View>
<Text style={styles.healthCardTitle}>{label}</Text>
</View>
<View style={[styles.healthRow, styles.healthRowWarning]}>
<Ionicons name="warning" size={15} color="#C62828" />
<Text style={styles.healthRowWarningText}>{overwateringDetected}</Text>
</View>
<View style={[styles.healthRow, styles.healthRowSuccess]}>
<Ionicons name="checkmark-circle" size={15} color="#2e7d32" />
<Text style={styles.healthRowSuccessText}>{rescuePlanReady}</Text>
</View>
</View>
);
}
function ReminderChipsOverlay({ waterReminder, fertilizeReminder }: { waterReminder: string; fertilizeReminder: string }) {
const [waterLabel, waterMeta] = splitReminder(waterReminder);
const [fertilizeLabel, fertilizeMeta] = splitReminder(fertilizeReminder);
return (
<>
<View style={[styles.reminderChip, styles.reminderChipTop]}>
<View style={[styles.reminderIcon, { backgroundColor: '#dff2e6' }]}>
<Ionicons name="water" size={16} color="#2e7d32" />
</View>
<View>
<Text style={styles.reminderLabel}>{waterLabel}</Text>
<Text style={styles.reminderMeta}>{waterMeta}</Text>
</View>
</View>
<View style={[styles.reminderChip, styles.reminderChipBottom]}>
<View style={[styles.reminderIcon, { backgroundColor: '#e3f3c8' }]}>
<Ionicons name="leaf" size={16} color="#558b2f" />
</View>
<View>
<Text style={styles.reminderLabel}>{fertilizeLabel}</Text>
<Text style={styles.reminderMeta}>{fertilizeMeta}</Text>
</View>
</View>
</>
);
}
// Splits "Water Monstera — today" into ["Water Monstera", "today"] for a two-line chip.
function splitReminder(text: string): [string, string] {
const parts = text.split('—').map((part) => part.trim());
if (parts.length === 2) return [parts[0], parts[1]];
return [text, ''];
}
export default function OnboardingSlidesScreen() {
const router = useRouter();
const { language, isDarkMode, colorPalette } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const posthog = useSafeAnalytics();
const [page, setPage] = useState(0);
const copy = getSlidesCopy(language);
const slide = copy.slides[page];
useEffect(() => {
posthog.capture('onboarding_slide_viewed', { index: page });
}, [page, posthog]);
const next = () => {
if (page < copy.slides.length - 1) {
setPage(page + 1);
} else {
router.replace('/onboarding/source');
}
};
const back = () => {
if (page > 0) {
setPage(page - 1);
} else {
router.back();
}
};
return (
<View style={[styles.container, { backgroundColor: isDarkMode ? '#0a110b' : '#f8fbef' }]}>
<View style={styles.imageArea}>
<Image
source={
page === 0
? require('../../assets/paywall_scan_background.png')
: page === 1
? require('../../assets/onboarding_health_scan_mockup.png')
: require('../../assets/welcome_botanical_header.png')
}
style={styles.image}
resizeMode="cover"
/>
<SafeAreaView style={styles.imageSafeArea} edges={['top']}>
<TouchableOpacity onPress={back} style={styles.backBtn} activeOpacity={0.85}>
<Ionicons name="arrow-back" size={20} color="#1f2520" />
</TouchableOpacity>
</SafeAreaView>
{page === 0 && <ScanFrameOverlay resultChip={copy.resultChip} colors={colors} />}
{page === 1 && (
<HealthCardOverlay
label={copy.healthCheckLabel}
overwateringDetected={copy.overwateringDetected}
rescuePlanReady={copy.rescuePlanReady}
/>
)}
{page === 2 && (
<ReminderChipsOverlay waterReminder={copy.waterReminder} fertilizeReminder={copy.fertilizeReminder} />
)}
</View>
<View style={[styles.sheet, { backgroundColor: colors.surface }]}>
<Text style={[styles.title, { color: colors.text }]}>{slide.title}</Text>
<Text style={[styles.body, { color: colors.textSecondary }]}>{slide.body}</Text>
<View style={styles.dots}>
{copy.slides.map((_, index) => (
<View
key={index}
style={[
styles.dot,
index === page
? [styles.dotActive, { backgroundColor: colors.primary }]
: { backgroundColor: colors.border },
]}
/>
))}
</View>
<TouchableOpacity
style={[styles.cta, { backgroundColor: colors.primary }]}
onPress={next}
activeOpacity={0.86}
>
<Text style={[styles.ctaText, { color: colors.onPrimary }]}>{copy.continueLabel}</Text>
</TouchableOpacity>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
imageArea: {
height: '58%',
position: 'relative',
overflow: 'hidden',
},
image: {
width: '100%',
height: '100%',
position: 'absolute',
},
imageSafeArea: {
position: 'absolute',
top: 0,
left: 0,
right: 0,
},
backBtn: {
marginLeft: 16,
marginTop: 8,
width: 38,
height: 38,
borderRadius: 19,
backgroundColor: 'rgba(255,255,255,0.85)',
alignItems: 'center',
justifyContent: 'center',
},
// Scan frame overlay (slide 1)
scanFrameWrap: {
position: 'absolute',
top: '22%',
left: '20%',
right: '20%',
bottom: '26%',
},
cornerTL: {
position: 'absolute',
top: 0,
left: 0,
width: 30,
height: 30,
borderTopWidth: 4,
borderLeftWidth: 4,
borderTopLeftRadius: 8,
},
cornerTR: {
position: 'absolute',
top: 0,
right: 0,
width: 30,
height: 30,
borderTopWidth: 4,
borderRightWidth: 4,
borderTopRightRadius: 8,
},
cornerBL: {
position: 'absolute',
bottom: 0,
left: 0,
width: 30,
height: 30,
borderBottomWidth: 4,
borderLeftWidth: 4,
borderBottomLeftRadius: 8,
},
cornerBR: {
position: 'absolute',
bottom: 0,
right: 0,
width: 30,
height: 30,
borderBottomWidth: 4,
borderRightWidth: 4,
borderBottomRightRadius: 8,
},
resultChip: {
position: 'absolute',
bottom: '10%',
left: 20,
right: 20,
flexDirection: 'row',
alignItems: 'center',
gap: 8,
backgroundColor: 'rgba(255,255,255,0.94)',
borderRadius: 999,
paddingHorizontal: 16,
paddingVertical: 12,
},
resultChipText: {
fontSize: 15,
fontWeight: '700',
color: '#1f2520',
},
// Health card overlay (slide 2)
healthCard: {
position: 'absolute',
bottom: 16,
left: 16,
right: 16,
backgroundColor: 'rgba(255,255,255,0.96)',
borderRadius: 18,
padding: 14,
gap: 8,
},
healthCardHeader: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginBottom: 2,
},
healthCardIcon: {
width: 28,
height: 28,
borderRadius: 14,
backgroundColor: '#fdeaea',
alignItems: 'center',
justifyContent: 'center',
},
healthCardTitle: {
fontSize: 16,
fontWeight: '800',
color: '#1f2520',
},
healthRow: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
borderRadius: 10,
paddingHorizontal: 10,
paddingVertical: 8,
},
healthRowWarning: {
backgroundColor: '#fdeaea',
},
healthRowWarningText: {
fontSize: 13.5,
fontWeight: '700',
color: '#C62828',
},
healthRowSuccess: {
backgroundColor: '#e8f3e3',
},
healthRowSuccessText: {
fontSize: 13.5,
fontWeight: '700',
color: '#2e7d32',
},
// Reminder chips overlay (slide 3)
reminderChip: {
position: 'absolute',
flexDirection: 'row',
alignItems: 'center',
gap: 10,
backgroundColor: 'rgba(255,255,255,0.96)',
borderRadius: 999,
paddingVertical: 8,
paddingRight: 18,
paddingLeft: 8,
},
reminderChipTop: {
top: '24%',
right: 20,
},
reminderChipBottom: {
top: '42%',
left: 20,
},
reminderIcon: {
width: 32,
height: 32,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
reminderLabel: {
fontSize: 14,
fontWeight: '800',
color: '#1f2520',
},
reminderMeta: {
fontSize: 12,
fontWeight: '600',
color: '#5a8a3d',
},
// Bottom sheet
sheet: {
flex: 1,
borderTopLeftRadius: 28,
borderTopRightRadius: 28,
marginTop: -24,
paddingHorizontal: 24,
paddingTop: 32,
alignItems: 'center',
},
title: {
fontSize: 30,
fontWeight: '900',
textAlign: 'center',
marginBottom: 10,
},
body: {
fontSize: 15.5,
lineHeight: 22,
textAlign: 'center',
maxWidth: 320,
marginBottom: 20,
},
dots: {
flexDirection: 'row',
alignItems: 'center',
gap: 8,
marginBottom: 'auto',
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
},
dotActive: {
width: 26,
height: 8,
borderRadius: 4,
},
cta: {
alignSelf: 'stretch',
height: 58,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
marginBottom: 24,
},
ctaText: {
fontSize: 17,
fontWeight: '800',
},
});

View File

@@ -1,114 +1,50 @@
import React, { useMemo, useState } from 'react';
import { ImageBackground, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import React, { useState } from 'react';
import { useRouter } from 'expo-router';
import { Ionicons } from '@expo/vector-icons';
import { useSafeAnalytics } from '../../services/analytics';
import { ThemeBackdrop } from '../../components/ThemeBackdrop';
import { useColors } from '../../constants/Colors';
import { useApp } from '../../context/AppContext';
import { OnboardingProgressService } from '../../services/onboardingProgressService';
const ONBOARDING_BACKGROUND = {
light: '#fbfaf3',
dark: '#0a110b',
};
import { PreAuthOnboardingService } from '../../services/preAuthOnboardingService';
import { OnboardingQuestion, QuestionOption } from '../../components/OnboardingQuestion';
const SOURCE_OPTIONS = [
{ id: 'app_store', icon: 'storefront-outline' as const, signal: 'organic_store' },
{ id: 'instagram', icon: 'logo-instagram' as const, signal: 'social_visual' },
{ id: 'tiktok', icon: 'musical-notes-outline' as const, signal: 'social_video' },
{ id: 'friend', icon: 'people-outline' as const, signal: 'referral' },
{ id: 'search', icon: 'search-outline' as const, signal: 'high_intent_search' },
{ id: 'other', icon: 'ellipsis-horizontal-circle-outline' as const, signal: 'unclassified' },
{ id: 'app_store', emoji: '🏬', signal: 'organic_store' },
{ id: 'instagram', emoji: '📸', signal: 'social_visual' },
{ id: 'tiktok', emoji: '🎵', signal: 'social_video' },
{ id: 'friend', emoji: '👥', signal: 'referral' },
{ id: 'search', emoji: '🔎', signal: 'high_intent_search' },
{ id: 'other', emoji: '✨', signal: 'unclassified' },
];
const getSourceOnboardingCopy = (language: 'de' | 'en' | 'es') => {
if (language === 'de') {
return {
step: 'Schritt 1 von 4',
heroTitle: 'Dein Start wird danach personalisiert.',
heroMeta: 'Scan, Sammlung und Health-Check passen sich deinem Ziel an.',
valueTitle: 'Warum wir fragen',
valueBody: 'Die Antwort hilft, deinen Einstieg auf das auszurichten, was dich wirklich hierher gebracht hat.',
subtitles: {
app_store: 'Du hast aktiv nach Pflanzen- oder Pflegehilfe gesucht.',
instagram: 'Du kamst ueber visuelle Pflanzen-Inhalte.',
tiktok: 'Du kamst ueber kurze Videos oder Creator.',
friend: 'Persoenliche Empfehlung, hoher Vertrauens-Intent.',
search: 'Konkretes Problem oder schneller Pflanzen-Check.',
other: 'Passt nicht sauber in die anderen Quellen.',
},
};
}
if (language === 'es') {
return {
step: 'Paso 1 de 4',
heroTitle: 'Tu inicio se adapta despues.',
heroMeta: 'Escaneo, coleccion y health-check segun tu objetivo.',
valueTitle: 'Por que preguntamos',
valueBody: 'La respuesta ayuda a adaptar el inicio a lo que realmente te trajo aqui.',
subtitles: {
app_store: 'Buscaste ayuda para plantas o cuidado.',
instagram: 'Llegaste desde contenido visual de plantas.',
tiktok: 'Llegaste desde videos cortos o creadores.',
friend: 'Recomendacion personal con alta confianza.',
search: 'Problema concreto o chequeo rapido.',
other: 'No encaja en las demas fuentes.',
},
};
}
return {
step: 'Step 1 of 4',
heroTitle: 'Your first run adapts next.',
heroMeta: 'Scanner, collection, and health check based on your goal.',
valueTitle: 'Why we ask',
valueBody: 'This helps tailor the first steps to what actually brought you here.',
subtitles: {
app_store: 'You actively searched for plant or care help.',
instagram: 'You came from visual plant content.',
tiktok: 'You came from short videos or creators.',
friend: 'Personal referral with high trust intent.',
search: 'Concrete problem or quick plant check intent.',
other: 'Does not fit the other sources cleanly.',
},
};
};
export default function OnboardingSourceScreen() {
const router = useRouter();
const posthog = useSafeAnalytics();
const { session, isDarkMode, colorPalette, language, t } = useApp();
const { session, isDarkMode, colorPalette, t } = useApp();
const colors = useColors(isDarkMode, colorPalette);
const screenBackground = isDarkMode ? ONBOARDING_BACKGROUND.dark : ONBOARDING_BACKGROUND.light;
const [selectedSource, setSelectedSource] = useState<string | null>(null);
const copy = getSourceOnboardingCopy(language);
const sourceLabels = useMemo(
() => ({
app_store: t.sourceOptionAppStore,
instagram: t.sourceOptionInstagram,
tiktok: t.sourceOptionTikTok,
friend: t.sourceOptionFriend,
search: t.sourceOptionSearch,
other: t.sourceOptionOther,
}),
[
t.sourceOptionAppStore,
t.sourceOptionFriend,
t.sourceOptionInstagram,
t.sourceOptionOther,
t.sourceOptionSearch,
t.sourceOptionTikTok,
],
);
const sourceLabels: Record<string, string> = {
app_store: t.sourceOptionAppStore,
instagram: t.sourceOptionInstagram,
tiktok: t.sourceOptionTikTok,
friend: t.sourceOptionFriend,
search: t.sourceOptionSearch,
other: t.sourceOptionOther,
};
const options: QuestionOption[] = SOURCE_OPTIONS.map((option) => ({
id: option.id,
emoji: option.emoji,
label: sourceLabels[option.id],
}));
const finish = (source: string | null) => {
if (session?.userId && source) {
OnboardingProgressService.setAcquisitionSource(session.userId, source);
}
if (source) {
void PreAuthOnboardingService.setAnswer('acquisitionSource', source);
}
posthog.capture('onboarding_source_completed', {
source: source ?? 'skipped',
@@ -118,241 +54,21 @@ export default function OnboardingSourceScreen() {
};
return (
<View style={[styles.container, { backgroundColor: screenBackground }]}>
{isDarkMode ? <ThemeBackdrop colors={colors} /> : null}
<SafeAreaView style={styles.safeArea} edges={['top', 'left', 'right', 'bottom']}>
<View style={styles.header}>
<View style={[styles.stepPill, { backgroundColor: colors.primarySoft, borderColor: colors.border }]}>
<Text style={[styles.stepLabel, { color: colors.primaryDark }]}>{copy.step}</Text>
</View>
<ImageBackground
source={require('../../assets/onboarding_source_mockup.png')}
style={[styles.heroPreview, { borderColor: colors.border }]}
imageStyle={styles.heroImage}
resizeMode="cover"
>
<View style={[styles.heroOverlay, { backgroundColor: isDarkMode ? 'rgba(8, 14, 9, 0.46)' : 'rgba(251, 250, 243, 0.32)' }]} />
<View style={styles.heroContent}>
<View style={[styles.heroIcon, { backgroundColor: colors.primary }]}>
<Ionicons name="scan-outline" size={20} color={colors.onPrimary} />
</View>
<View style={styles.heroCopy}>
<Text style={[styles.heroTitle, { color: isDarkMode ? colors.textOnImage : colors.text }]}>
{copy.heroTitle}
</Text>
<Text style={[styles.heroMeta, { color: isDarkMode ? '#d7ded9' : colors.textSecondary }]}>
{copy.heroMeta}
</Text>
</View>
</View>
</ImageBackground>
<Text style={[styles.title, { color: colors.text }]}>{t.sourceOnboardingTitle}</Text>
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>{t.sourceOnboardingSubtitle}</Text>
</View>
<View style={styles.options}>
{SOURCE_OPTIONS.map((option) => {
const isActive = selectedSource === option.id;
return (
<TouchableOpacity
key={option.id}
style={[
styles.optionCard,
{
backgroundColor: isActive ? colors.primarySoft : colors.surface,
borderColor: isActive ? colors.primary : colors.border,
},
]}
onPress={() => setSelectedSource(option.id)}
activeOpacity={0.85}
>
<View style={[styles.optionIcon, { backgroundColor: isActive ? colors.primary : colors.surfaceMuted }]}>
<Ionicons name={option.icon} size={18} color={isActive ? colors.onPrimary : colors.textMuted} />
</View>
<View style={styles.optionCopy}>
<Text style={[styles.optionLabel, { color: colors.text }]}>{sourceLabels[option.id as keyof typeof sourceLabels]}</Text>
<Text style={[styles.optionSubtitle, { color: colors.textMuted }]}>
{copy.subtitles[option.id as keyof typeof copy.subtitles]}
</Text>
</View>
{isActive && <Ionicons name="checkmark-circle" size={18} color={colors.primary} style={styles.optionCheck} />}
</TouchableOpacity>
);
})}
</View>
<View style={styles.footer}>
<TouchableOpacity
style={[styles.secondaryBtn, { borderColor: colors.borderStrong, backgroundColor: colors.surface }]}
onPress={() => finish(null)}
>
<Text style={[styles.secondaryBtnText, { color: colors.text }]}>{t.sourceOnboardingSkip}</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.primaryBtn,
{ backgroundColor: selectedSource ? colors.primary : colors.surfaceMuted },
]}
onPress={() => finish(selectedSource)}
disabled={!selectedSource}
>
<Text
style={[
styles.primaryBtnText,
{ color: selectedSource ? colors.onPrimary : colors.textMuted },
]}
>
{t.sourceOnboardingContinue}
</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
</View>
<OnboardingQuestion
colors={colors}
isDarkMode={isDarkMode}
step={1}
totalSteps={4}
title={t.sourceOnboardingTitle}
subtitle={t.sourceOnboardingSubtitle}
options={options}
selectedId={selectedSource}
onSelect={setSelectedSource}
onContinue={() => finish(selectedSource)}
onBack={() => router.back()}
continueLabel={t.sourceOnboardingContinue}
skipLabel={t.sourceOnboardingSkip}
onSkip={() => finish(null)}
/>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
safeArea: {
flex: 1,
paddingHorizontal: 20,
paddingTop: 12,
paddingBottom: 14,
justifyContent: 'space-between',
},
header: {
alignItems: 'center',
gap: 9,
},
stepPill: {
borderWidth: 1,
borderRadius: 999,
paddingHorizontal: 12,
paddingVertical: 7,
},
stepLabel: {
fontSize: 12,
fontWeight: '800',
textTransform: 'uppercase',
letterSpacing: 0.4,
},
heroPreview: {
width: '100%',
height: 175,
borderRadius: 24,
borderWidth: 1,
justifyContent: 'flex-end',
overflow: 'hidden',
},
heroImage: {
borderRadius: 24,
},
heroOverlay: {
...StyleSheet.absoluteFillObject,
},
heroContent: {
flexDirection: 'row',
alignItems: 'flex-end',
gap: 12,
padding: 12,
},
heroIcon: {
width: 36,
height: 36,
borderRadius: 14,
alignItems: 'center',
justifyContent: 'center',
},
heroCopy: {
flex: 1,
gap: 3,
},
heroTitle: {
fontSize: 15,
lineHeight: 18,
fontWeight: '800',
},
heroMeta: {
fontSize: 10.5,
lineHeight: 14,
fontWeight: '600',
},
title: {
fontSize: 25,
fontWeight: '800',
textAlign: 'center',
lineHeight: 29,
},
subtitle: {
fontSize: 13,
textAlign: 'center',
lineHeight: 18,
maxWidth: 320,
},
options: {
flexDirection: 'row',
flexWrap: 'wrap',
gap: 8,
},
optionCard: {
width: '48.8%',
minHeight: 68,
borderRadius: 15,
borderWidth: 1.5,
padding: 9,
gap: 8,
position: 'relative',
},
optionIcon: {
width: 34,
height: 34,
borderRadius: 17,
alignItems: 'center',
justifyContent: 'center',
},
optionCopy: {
gap: 3,
},
optionLabel: {
fontSize: 13,
fontWeight: '700',
},
optionSubtitle: {
fontSize: 10,
lineHeight: 13,
},
optionCheck: {
position: 'absolute',
right: 9,
top: 9,
},
footer: {
flexDirection: 'row',
gap: 12,
},
secondaryBtn: {
flex: 1,
height: 50,
borderRadius: 16,
borderWidth: 1.5,
alignItems: 'center',
justifyContent: 'center',
},
secondaryBtnText: {
fontSize: 15,
fontWeight: '600',
},
primaryBtn: {
flex: 1.2,
height: 50,
borderRadius: 16,
alignItems: 'center',
justifyContent: 'center',
},
primaryBtnText: {
fontSize: 15,
fontWeight: '700',
},
});

File diff suppressed because it is too large Load Diff

View File

@@ -24,17 +24,14 @@ import { createIdempotencyKey } from '../utils/idempotency';
import { AuthService } from '../services/authService';
import { getMockPlantByImage } from '../services/backend/mockCatalog';
import { consumeSharedImageUri, SHARE_INTENT_KEY } from '../utils/shareHandoff';
import { OutOfCreditsSheet } from '../components/OutOfCreditsSheet';
const HEALTH_CHECK_CREDIT_COST = 2;
const DEMO_SCAN_LIMIT = 5;
const getBillingCopy = (language: 'de' | 'en' | 'es') => {
if (language === 'de') {
return {
creditsLabel: 'Credits',
noCreditsTitle: 'Keine Credits mehr',
noCreditsMessage: 'Du hast keine Credits mehr fuer KI-Scans. Upgrade oder Top-up im Profil.',
healthNoCreditsMessage: `Du brauchst ${HEALTH_CHECK_CREDIT_COST} Credits fuer den Health-Check.`,
managePlan: 'Plan verwalten',
dismiss: 'Schliessen',
genericErrorTitle: 'Fehler',
@@ -57,6 +54,7 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
demoNoCreditsTitle: 'Demo-Scans aufgebraucht',
demoNoCreditsMessage: 'Du hast deine 5 kostenlosen Demo-Scans auf diesem Gerät genutzt. Starte Pro, um weiter Pflanzen zu scannen.',
demoCreditsRemaining: (count: number) => `${count} Demo-Scans übrig`,
creditsRemaining: (count: number) => `${count} Scans übrig`,
appleCta: 'Mit Apple fortfahren',
emailCta: 'Mit E-Mail fortfahren',
unlockCta: 'Vollständige Diagnose freischalten',
@@ -66,9 +64,6 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
if (language === 'es') {
return {
creditsLabel: 'Creditos',
noCreditsTitle: 'Sin creditos',
noCreditsMessage: 'No tienes creditos para escaneos AI. Actualiza o compra top-up en Perfil.',
healthNoCreditsMessage: `Necesitas ${HEALTH_CHECK_CREDIT_COST} creditos para el health-check.`,
managePlan: 'Gestionar plan',
dismiss: 'Cerrar',
genericErrorTitle: 'Error',
@@ -91,6 +86,7 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
demoNoCreditsTitle: 'Escaneos demo agotados',
demoNoCreditsMessage: 'Ya usaste tus 5 escaneos demo gratuitos en este dispositivo. Inicia Pro para seguir escaneando plantas.',
demoCreditsRemaining: (count: number) => `${count} escaneos demo restantes`,
creditsRemaining: (count: number) => `${count} escaneos restantes`,
appleCta: 'Continuar con Apple',
emailCta: 'Continuar con email',
unlockCta: 'Desbloquear diagnóstico completo',
@@ -99,9 +95,6 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
return {
creditsLabel: 'Credits',
noCreditsTitle: 'No credits left',
noCreditsMessage: 'You have no AI scan credits left. Upgrade or buy a top-up in Profile.',
healthNoCreditsMessage: `You need ${HEALTH_CHECK_CREDIT_COST} credits for the health check.`,
managePlan: 'Manage plan',
dismiss: 'Close',
genericErrorTitle: 'Error',
@@ -124,6 +117,7 @@ const getBillingCopy = (language: 'de' | 'en' | 'es') => {
demoNoCreditsTitle: 'Demo scans used',
demoNoCreditsMessage: 'You used your 5 free demo scans on this device. Start Pro to keep scanning plants.',
demoCreditsRemaining: (count: number) => `${count} demo scans left`,
creditsRemaining: (count: number) => `${count} scans left`,
appleCta: 'Continue with Apple',
emailCta: 'Continue with email',
unlockCta: 'Unlock full diagnosis',
@@ -167,8 +161,8 @@ export default function ScannerScreen() {
: params.sharedImageKey;
const hasActiveEntitlement = billingSummary?.entitlement?.plan === 'pro'
&& billingSummary?.entitlement?.status === 'active';
const isDemoMode = !hasActiveEntitlement;
const availableCredits = hasActiveEntitlement ? (billingSummary?.credits.available ?? 0) : 0;
const isDemoMode = !session; // guests get the local demo scan; signed-in users burn real credits
const availableCredits = billingSummary?.credits.available ?? 0;
const demoScansRemaining = Math.max(0, DEMO_SCAN_LIMIT - guestScanCount);
const [permission, requestPermission] = useCameraPermissions();
@@ -179,6 +173,7 @@ export default function ScannerScreen() {
const [analysisProgress, setAnalysisProgress] = useState(0);
const [analysisResult, setAnalysisResult] = useState<IdentificationResult | null>(null);
const [demoResultVisible, setDemoResultVisible] = useState(false);
const [outOfCreditsVisible, setOutOfCreditsVisible] = useState(false);
const cameraRef = useRef<CameraView>(null);
const scanLineProgress = useRef(new Animated.Value(0)).current;
const scanPulse = useRef(new Animated.Value(0)).current;
@@ -281,18 +276,13 @@ export default function ScannerScreen() {
return;
}
if (!isDemoMode && availableCredits <= 0) {
Alert.alert(
billingCopy.noCreditsTitle,
isHealthMode ? billingCopy.healthNoCreditsMessage : billingCopy.noCreditsMessage,
[
{ text: billingCopy.dismiss, style: 'cancel' },
{
text: billingCopy.managePlan,
onPress: () => router.replace('/profile/billing'),
},
],
);
// Only pre-block when the billing summary is actually known; if it's null
// (fetch failed / still loading) let the request proceed — the server 402
// path opens the sheet as the safety net.
const requiredCredits = isHealthMode ? 2 : 1;
if (!isDemoMode && billingSummary && availableCredits < requiredCredits) {
posthog.capture('out_of_credits_shown', { trigger: 'pre_check', scan_type: isHealthMode ? 'health_check' : 'identification' });
setOutOfCreditsVisible(true);
return;
}
@@ -412,17 +402,8 @@ export default function ScannerScreen() {
});
if (isInsufficientCreditsError(error)) {
Alert.alert(
billingCopy.noCreditsTitle,
isHealthMode ? billingCopy.healthNoCreditsMessage : billingCopy.noCreditsMessage,
[
{ text: billingCopy.dismiss, style: 'cancel' },
{
text: billingCopy.managePlan,
onPress: () => router.replace('/profile/billing'),
},
],
);
posthog.capture('out_of_credits_shown', { trigger: 'server_402', scan_type: isHealthMode ? 'health_check' : 'identification' });
setOutOfCreditsVisible(true);
} else if (isTimeoutError(error)) {
Alert.alert(
billingCopy.timeoutTitle,
@@ -689,7 +670,11 @@ export default function ScannerScreen() {
<View style={[styles.creditBadge, { backgroundColor: colors.heroButton, borderColor: colors.heroButtonBorder }]}>
<Ionicons name={isDemoMode ? 'sparkles-outline' : 'wallet-outline'} size={12} color={colors.text} />
<Text style={[styles.creditBadgeText, { color: colors.text }]}>
{isDemoMode ? billingCopy.demoCreditsRemaining(demoScansRemaining) : `${billingCopy.creditsLabel}: ${availableCredits}`}
{isDemoMode
? billingCopy.demoCreditsRemaining(demoScansRemaining)
: !hasActiveEntitlement
? billingCopy.creditsRemaining(availableCredits)
: `${billingCopy.creditsLabel}: ${availableCredits}`}
</Text>
</View>
</View>
@@ -865,6 +850,27 @@ export default function ScannerScreen() {
<Text style={[styles.controlLabel, { color: colors.textMuted }]}>{t.help}</Text>
</TouchableOpacity>
</View>
<OutOfCreditsSheet
visible={outOfCreditsVisible}
language={language}
colors={colors}
isPro={hasActiveEntitlement}
renewsAtIso={billingSummary?.credits.cycleEndsAt}
onSeePlans={() => {
setOutOfCreditsVisible(false);
posthog.capture('paywall_opened', { source: 'out_of_credits' });
router.push('/profile/billing?view=paywall');
}}
onTopup={() => {
setOutOfCreditsVisible(false);
router.push('/profile/billing'); // topups live on the billing management screen
}}
onDismiss={() => {
posthog.capture('paywall_dismissed', { source: 'out_of_credits_sheet' });
setOutOfCreditsVisible(false);
}}
/>
</View>
);
}

View File

@@ -0,0 +1,107 @@
import React from 'react';
import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Ionicons } from '@expo/vector-icons';
import { useColors } from '../constants/Colors';
type ColorsType = ReturnType<typeof useColors>;
export type QuestionOption = { id: string; emoji: string; label: string; subtitle?: string };
type Props = {
colors: ColorsType;
isDarkMode: boolean;
step: number; // 1-based
totalSteps: number;
title: string;
subtitle: string;
options: QuestionOption[];
selectedId: string | null;
onSelect: (id: string) => void;
onContinue: () => void;
onBack?: () => void;
continueLabel: string;
skipLabel?: string;
onSkip?: () => void;
};
export function OnboardingQuestion({
colors, isDarkMode, step, totalSteps, title, subtitle, options,
selectedId, onSelect, onContinue, onBack, continueLabel, skipLabel, onSkip,
}: Props) {
return (
<SafeAreaView style={[styles.safe, { backgroundColor: isDarkMode ? '#0a110b' : '#f8fbef' }]} edges={['top', 'left', 'right', 'bottom']}>
<View style={styles.topBar}>
{onBack ? (
<TouchableOpacity onPress={onBack} style={[styles.backBtn, { backgroundColor: colors.surface }]}>
<Ionicons name="arrow-back" size={20} color={colors.primary} />
</TouchableOpacity>
) : <View style={styles.backBtn} />}
<View style={[styles.progressTrack, { backgroundColor: colors.primarySoft }]}>
<View style={[styles.progressFill, { backgroundColor: colors.primary, width: `${Math.round((step / totalSteps) * 100)}%` }]} />
</View>
<View style={styles.backBtn} />
</View>
<Text style={[styles.title, { color: colors.text }]}>{title}</Text>
<Text style={[styles.subtitle, { color: colors.textSecondary }]}>{subtitle}</Text>
<View style={styles.options}>
{options.map((option) => {
const active = selectedId === option.id;
return (
<TouchableOpacity
key={option.id}
onPress={() => onSelect(option.id)}
activeOpacity={0.85}
style={[styles.card, {
backgroundColor: active ? colors.primarySoft : colors.surface,
borderColor: active ? colors.primary : 'transparent',
}]}
>
<Text style={styles.emoji}>{option.emoji}</Text>
<View style={styles.cardCopy}>
<Text style={[styles.cardLabel, { color: active ? colors.primary : colors.text }]}>{option.label}</Text>
{option.subtitle ? <Text style={[styles.cardSubtitle, { color: colors.textMuted }]}>{option.subtitle}</Text> : null}
</View>
</TouchableOpacity>
);
})}
</View>
<View style={styles.footer}>
{skipLabel && onSkip ? (
<TouchableOpacity onPress={onSkip} style={styles.skipBtn}>
<Text style={[styles.skipText, { color: colors.textMuted }]}>{skipLabel}</Text>
</TouchableOpacity>
) : null}
<TouchableOpacity
onPress={onContinue}
disabled={!selectedId}
activeOpacity={0.86}
style={[styles.cta, { backgroundColor: selectedId ? colors.primary : colors.surfaceMuted }]}
>
<Text style={[styles.ctaText, { color: selectedId ? colors.onPrimary : colors.textMuted }]}>{continueLabel}</Text>
</TouchableOpacity>
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
safe: { flex: 1, paddingHorizontal: 22 },
topBar: { flexDirection: 'row', alignItems: 'center', gap: 14, paddingVertical: 10 },
backBtn: { width: 40, height: 40, borderRadius: 20, alignItems: 'center', justifyContent: 'center' },
progressTrack: { flex: 1, height: 6, borderRadius: 3, overflow: 'hidden' },
progressFill: { height: 6, borderRadius: 3 },
title: { fontSize: 30, lineHeight: 35, fontWeight: '900', textAlign: 'center', marginTop: 18, marginBottom: 8 },
subtitle: { fontSize: 15, lineHeight: 20, textAlign: 'center', marginBottom: 22 },
options: { gap: 12, flex: 1 },
card: { flexDirection: 'row', alignItems: 'center', gap: 14, borderRadius: 16, borderWidth: 2, paddingHorizontal: 16, paddingVertical: 18 },
emoji: { fontSize: 26 },
cardCopy: { flex: 1, gap: 2 },
cardLabel: { fontSize: 17, fontWeight: '800' },
cardSubtitle: { fontSize: 12.5, lineHeight: 16 },
footer: { gap: 8, paddingBottom: 6 },
skipBtn: { alignItems: 'center', paddingVertical: 6 },
skipText: { fontSize: 14, fontWeight: '700' },
cta: { height: 56, borderRadius: 14, alignItems: 'center', justifyContent: 'center' },
ctaText: { fontSize: 17, fontWeight: '800' },
});

View File

@@ -0,0 +1,167 @@
import React from 'react';
import { Modal, StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { Language } from '../types';
import { useColors } from '../constants/Colors';
type ColorsType = ReturnType<typeof useColors>;
const getCopy = (language: Language, isPro: boolean) => {
if (language === 'de') {
return {
title: isPro ? 'Deine Credits sind aufgebraucht' : 'Deine Gratis-Scans sind aufgebraucht',
body: (date: string) => (isPro
? `Deine Credits erneuern sich am ${date}. Kauf Credits nach, um weiterzuscannen.`
: `Deine 3 Gratis-Scans erneuern sich am ${date}. Hol dir Pro für unbegrenztes Scannen.`),
bodyNoDate: isPro
? 'Kauf Credits nach, um weiterzuscannen.'
: 'Hol dir Pro für unbegrenztes Scannen und deinen 7-Tage-Rettungsplan.',
cta: 'Pro-Pläne ansehen',
topupsLabel: 'Oder einzelne Credits kaufen',
later: 'Vielleicht später',
best: 'BESTE WAHL',
credits: 'Credits',
};
}
if (language === 'es') {
return {
title: isPro ? 'Se acabaron tus créditos' : 'Se acabaron tus escaneos gratis',
body: (date: string) => (isPro
? `Tus créditos se renuevan el ${date}. Compra créditos para seguir escaneando.`
: `Tus 3 escaneos gratis se renuevan el ${date}. Pásate a Pro para escanear sin límites.`),
bodyNoDate: isPro
? 'Compra créditos para seguir escaneando.'
: 'Pásate a Pro para escanear sin límites.',
cta: 'Ver planes Pro',
topupsLabel: 'O compra créditos sueltos',
later: 'Quizás más tarde',
best: 'MEJOR OPCIÓN',
credits: 'créditos',
};
}
return {
title: isPro ? "You're out of credits" : "You're out of free scans",
body: (date: string) => (isPro
? `Your credits renew on ${date}. Top up to keep scanning.`
: `Your 3 free scans renew on ${date}. Upgrade to Pro for unlimited scanning.`),
bodyNoDate: isPro
? 'Top up to keep scanning.'
: 'Upgrade to Pro for unlimited scanning.',
cta: 'See Pro Plans',
topupsLabel: 'Or buy single credits',
later: 'Maybe later',
best: 'BEST',
credits: 'credits',
};
};
const formatRenewalDate = (iso: string | null | undefined, language: Language): string | null => {
if (!iso) return null;
const date = new Date(iso);
if (Number.isNaN(date.getTime())) return null;
const locale = language === 'de' ? 'de-DE' : language === 'es' ? 'es-ES' : 'en-US';
return date.toLocaleDateString(locale, { day: 'numeric', month: 'long' });
};
type Props = {
visible: boolean;
language: Language;
colors: ColorsType;
isPro?: boolean;
renewsAtIso?: string | null;
onSeePlans: () => void;
onTopup: (productId: 'topup_small' | 'topup_medium' | 'topup_large') => void;
onDismiss: () => void;
};
const TOPUPS = [
{ id: 'topup_small' as const, amount: 30, best: false },
{ id: 'topup_medium' as const, amount: 100, best: false },
{ id: 'topup_large' as const, amount: 250, best: true },
];
export function OutOfCreditsSheet({ visible, language, colors, isPro = false, renewsAtIso, onSeePlans, onTopup, onDismiss }: Props) {
const copy = getCopy(language, isPro);
const renewalDate = formatRenewalDate(renewsAtIso, language);
return (
<Modal visible={visible} transparent animationType="slide" onRequestClose={onDismiss}>
<View style={styles.backdrop}>
<TouchableOpacity
style={styles.backdropTouchable}
activeOpacity={1}
onPress={onDismiss}
accessibilityRole="button"
accessibilityLabel={copy.later}
/>
<View style={[styles.sheet, { backgroundColor: colors.surface }]} accessibilityViewIsModal>
<View style={[styles.handle, { backgroundColor: colors.border }]} />
<View style={styles.iconWrap}>
<View style={[styles.iconCircle, { backgroundColor: colors.primarySoft }]}>
<Ionicons name="leaf-outline" size={34} color={colors.primary} />
</View>
<View style={[styles.zeroBadge, { backgroundColor: colors.danger }]}>
<Text style={styles.zeroBadgeText}>0</Text>
</View>
</View>
<Text style={[styles.title, { color: colors.text }]}>{copy.title}</Text>
<Text style={[styles.body, { color: colors.textSecondary }]}>
{renewalDate ? copy.body(renewalDate) : copy.bodyNoDate}
</Text>
{!isPro && (
<TouchableOpacity style={[styles.cta, { backgroundColor: colors.primary }]} onPress={onSeePlans} activeOpacity={0.86}>
<Ionicons name="ribbon-outline" size={19} color={colors.onPrimary} />
<Text style={[styles.ctaText, { color: colors.onPrimary }]}>{copy.cta}</Text>
</TouchableOpacity>
)}
<Text style={[styles.topupsLabel, { color: colors.textMuted }]}>{copy.topupsLabel.toUpperCase()}</Text>
<View style={styles.topupRow}>
{TOPUPS.map((topup) => (
<TouchableOpacity
key={topup.id}
style={[styles.topupChip, { borderColor: topup.best ? colors.primary : colors.border, backgroundColor: colors.surfaceMuted }]}
onPress={() => onTopup(topup.id)}
activeOpacity={0.85}
>
{topup.best && (
<View style={[styles.bestBadge, { backgroundColor: colors.primary }]}>
<Text style={[styles.bestBadgeText, { color: colors.onPrimary }]}>{copy.best}</Text>
</View>
)}
<Text style={[styles.topupAmount, { color: colors.primary }]}>+{topup.amount}</Text>
<Text style={[styles.topupUnit, { color: colors.textSecondary }]}>{copy.credits}</Text>
</TouchableOpacity>
))}
</View>
<TouchableOpacity onPress={onDismiss} style={styles.laterBtn}>
<Text style={[styles.laterText, { color: colors.primary }]}>{copy.later}</Text>
</TouchableOpacity>
</View>
</View>
</Modal>
);
}
const styles = StyleSheet.create({
backdrop: { flex: 1, backgroundColor: 'rgba(10,17,11,0.45)', justifyContent: 'flex-end' },
backdropTouchable: { flex: 1 },
sheet: { borderTopLeftRadius: 26, borderTopRightRadius: 26, paddingHorizontal: 24, paddingTop: 10, paddingBottom: 34, alignItems: 'center' },
handle: { width: 44, height: 5, borderRadius: 3, marginBottom: 18 },
iconWrap: { marginBottom: 14 },
iconCircle: { width: 76, height: 76, borderRadius: 38, alignItems: 'center', justifyContent: 'center' },
zeroBadge: { position: 'absolute', top: -2, right: -4, width: 26, height: 26, borderRadius: 13, alignItems: 'center', justifyContent: 'center' },
zeroBadgeText: { color: '#fff', fontSize: 13, fontWeight: '900' },
title: { fontSize: 24, fontWeight: '900', textAlign: 'center', marginBottom: 8 },
body: { fontSize: 15, lineHeight: 21, textAlign: 'center', marginBottom: 18, maxWidth: 320 },
cta: { alignSelf: 'stretch', height: 56, borderRadius: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 8, marginBottom: 16 },
ctaText: { fontSize: 17, fontWeight: '800' },
topupsLabel: { fontSize: 11, fontWeight: '800', letterSpacing: 0.8, marginBottom: 10 },
topupRow: { flexDirection: 'row', gap: 10, alignSelf: 'stretch', marginBottom: 14 },
topupChip: { flex: 1, borderWidth: 1.5, borderRadius: 14, paddingVertical: 14, alignItems: 'center', overflow: 'hidden' },
bestBadge: { position: 'absolute', top: 0, left: 0, right: 0, paddingVertical: 3, alignItems: 'center' },
bestBadgeText: { fontSize: 9, fontWeight: '900', letterSpacing: 0.6 },
topupAmount: { fontSize: 22, fontWeight: '900', marginTop: 6 },
topupUnit: { fontSize: 12, fontWeight: '600' },
laterBtn: { paddingVertical: 8 },
laterText: { fontSize: 15, fontWeight: '800' },
});

View File

@@ -0,0 +1,160 @@
---
name: Botanical Vitality
colors:
surface: '#f8fbef'
surface-dim: '#d8dbd0'
surface-bright: '#f8fbef'
surface-container-lowest: '#ffffff'
surface-container-low: '#f2f5e9'
surface-container: '#ecefe4'
surface-container-high: '#e7e9de'
surface-container-highest: '#e1e4d9'
on-surface: '#191d16'
on-surface-variant: '#42493c'
inverse-surface: '#2e322a'
inverse-on-surface: '#eff2e7'
outline: '#72796a'
outline-variant: '#c2c9b7'
surface-tint: '#366a17'
primary: '#2b5f0b'
on-primary: '#ffffff'
primary-container: '#437824'
on-primary-container: '#c1ff99'
inverse-primary: '#9bd776'
secondary: '#446741'
on-secondary: '#ffffff'
secondary-container: '#c2eaba'
on-secondary-container: '#486b45'
tertiary: '#375d00'
on-tertiary: '#ffffff'
tertiary-container: '#4d771b'
on-tertiary-container: '#cbfd91'
error: '#ba1a1a'
on-error: '#ffffff'
error-container: '#ffdad6'
on-error-container: '#93000a'
primary-fixed: '#b6f390'
primary-fixed-dim: '#9bd776'
on-primary-fixed: '#092100'
on-primary-fixed-variant: '#205100'
secondary-fixed: '#c5edbd'
secondary-fixed-dim: '#aad1a2'
on-secondary-fixed: '#012104'
on-secondary-fixed-variant: '#2d4e2b'
tertiary-fixed: '#c0f287'
tertiary-fixed-dim: '#a5d56e'
on-tertiary-fixed: '#0f2000'
on-tertiary-fixed-variant: '#2e4f00'
background: '#f8fbef'
on-background: '#191d16'
surface-variant: '#e1e4d9'
typography:
display:
fontFamily: Plus Jakarta Sans
fontSize: 34px
fontWeight: '800'
lineHeight: 42px
letterSpacing: -0.02em
headline-lg:
fontFamily: Plus Jakarta Sans
fontSize: 28px
fontWeight: '800'
lineHeight: 36px
letterSpacing: -0.01em
headline-md:
fontFamily: Plus Jakarta Sans
fontSize: 22px
fontWeight: '700'
lineHeight: 28px
body-lg:
fontFamily: Plus Jakarta Sans
fontSize: 17px
fontWeight: '500'
lineHeight: 26px
body-md:
fontFamily: Plus Jakarta Sans
fontSize: 15px
fontWeight: '400'
lineHeight: 22px
label-lg:
fontFamily: Plus Jakarta Sans
fontSize: 13px
fontWeight: '700'
lineHeight: 18px
letterSpacing: 0.05em
label-md:
fontFamily: Plus Jakarta Sans
fontSize: 12px
fontWeight: '600'
lineHeight: 16px
rounded:
sm: 0.25rem
DEFAULT: 0.5rem
md: 0.75rem
lg: 1rem
xl: 1.5rem
full: 9999px
spacing:
base: 4px
xs: 8px
sm: 12px
md: 16px
lg: 24px
xl: 32px
container-margin: 20px
gutter: 16px
---
## Brand & Style
The design system is centered on the concept of "Gentle Precision." It balances the organic, living nature of botany with a high-end, structured educational interface. The target audience is urban plant enthusiasts who seek a calm, focused environment to manage their indoor jungles.
The style is **Premium Minimalist with a Tactile Twist**. It draws inspiration from modern learning apps by using high whitespace, generous hit areas, and soft, approachable geometry. While the typography is bold and authoritative, the color palette and rounded corners keep the experience friendly and accessible. The interface should feel like a premium lifestyle magazine: clean, airy, and centered entirely on high-quality botanical photography.
## Colors
This design system utilizes a "Nature-In" palette. The foundation is a warm cream background that reduces eye strain and feels more organic than pure white.
- **Primary Green (#437824):** Used for primary actions, success states, and key navigational highlights.
- **Deep Forest (#173817):** Reserved for secondary buttons, information chips, and high-contrast accents that require a sense of grounded authority.
- **Spring Accent (#A6D66F):** Used for progress bars, illustrative accents, and soft background fills for selected states.
- **Typography:** Headlines use a high-contrast near-black to ensure readability against the cream background, while body text uses a muted gray-green to maintain the "calm" brand pillar.
## Typography
We use **Plus Jakarta Sans** for its exceptional readability and friendly, rounded terminals that mirror the organic curves of plant life.
Headlines are unapologetically bold (Extra-Bold 800), creating a strong visual hierarchy and a confident editorial feel. For mobile, we prioritize vertical rhythm with generous line heights to ensure the "high whitespace" aesthetic is maintained even within text-heavy sections. Display styles utilize negative letter-spacing to keep the bold weights feeling tight and professional on iOS displays.
## Layout & Spacing
This design system follows a fluid-width logic tailored for iOS devices, specifically optimized for the iPhone 15 aspect ratio.
- **Grid:** A standard 4-column mobile grid with 20px side margins.
- **Vertical Rhythm:** Content blocks are separated by 24px or 32px to ensure the UI feels "airy."
- **Safe Areas:** Strict adherence to iOS safe areas for the status bar and home indicator.
- **Content Spacing:** Elements within cards use a 12px or 16px internal padding to maintain a compact but breathable internal structure.
## Elevation & Depth
Depth is achieved through **Tonal Layering** and soft, natural shadows. We avoid harsh dropshadows.
- **Level 0 (Background):** The warm cream (#FBFAF3) base.
- **Level 1 (Cards/Surface):** Pure white (#FFFFFF) surfaces with a very soft, diffused shadow (0px 4px 20px rgba(16, 28, 18, 0.06)). This makes cards appear as if they are resting lightly on the cream surface.
- **Level 2 (Interactive):** When pressed, buttons or cards may subtly scale down (0.98) or the shadow may tighten to simulate physical contact.
- **Glassmorphism:** Used sparingly for top navigation bars or floating action buttons, employing a 20px backdrop blur to allow the botanical photography to peek through.
## Shapes
The shape language is defined by "Friendly Geometry."
- **Standard Radius:** 16px for cards and containers to create a soft, approachable frame for photography.
- **Button Radius:** 14px specifically for the 56px height buttons, creating a balanced, slightly "squircle" feel.
- **Chip Radius:** Fully rounded (pill-shaped) for tags and small indicators.
- **Images:** All plant photography must feature rounded corners (minimum 12px) to prevent the "sharpness" of the images from clashing with the soft UI.
## Components
- **Buttons:**
- *Primary:* 56px height, #437824 background, white text (Bold), 14px radius.
- *Secondary:* 56px height, #173817 background, white text (Bold), 14px radius.
- *Outline:* 56px height, 2px border (#437824), #437824 text.
- **Cards:** White background, 16px radius, soft 6% opacity shadow. Used for plant profiles, "Care Tasks," and "Reminders."
- **Chips:** Small, pill-shaped tags used for plant difficulty (e.g., "Easy Care") or water requirements. Use #173817 with white text for high contrast.
- **Progress Bars:** Thick 8px tracks using the #A6D66F accent color on a #E6EADF (muted version of cream) track.
- **Input Fields:** 56px height, white background, 14px radius, subtle 1px border in #D1D4CC.
- **Care Icons:** Use thick-stroke (2pt) icons with rounded caps to match the typography's softness.
- **Imagery:** Use high-resolution photography with natural lighting. Ensure the subject (the plant) is centered with a clean or naturally blurred background.

View File

@@ -0,0 +1,189 @@
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>GreenLens Pro - Paywall</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary": "#375d00",
"on-surface": "#191d16",
"on-secondary-fixed": "#012104",
"on-secondary-fixed-variant": "#2d4e2b",
"surface-container-lowest": "#ffffff",
"surface-container-low": "#f2f5e9",
"error-container": "#ffdad6",
"secondary": "#446741",
"outline": "#72796a",
"on-secondary": "#ffffff",
"surface-container": "#ecefe4",
"on-error": "#ffffff",
"on-surface-variant": "#42493c",
"background": "#f8fbef",
"error": "#ba1a1a",
"outline-variant": "#c2c9b7",
"on-secondary-container": "#486b45",
"secondary-fixed": "#c5edbd",
"on-tertiary": "#ffffff",
"primary": "#2b5f0b",
"surface-dim": "#d8dbd0",
"primary-container": "#437824",
"tertiary-fixed-dim": "#a5d56e",
"inverse-on-surface": "#eff2e7",
"surface-container-high": "#e7e9de",
"surface-bright": "#f8fbef",
"inverse-surface": "#2e322a",
"on-background": "#191d16",
"on-primary-fixed": "#092100",
"secondary-container": "#c2eaba",
"on-tertiary-container": "#cbfd91",
"primary-fixed": "#b6f390",
"tertiary-container": "#4d771b",
"primary-fixed-dim": "#9bd776",
"tertiary-fixed": "#c0f287",
"surface-tint": "#366a17",
"secondary-fixed-dim": "#aad1a2",
"surface-variant": "#e1e4d9",
"on-error-container": "#93000a",
"on-tertiary-fixed-variant": "#2e4f00",
"on-primary": "#ffffff",
"on-primary-container": "#c1ff99",
"surface-container-highest": "#e1e4d9",
"on-tertiary-fixed": "#0f2000",
"on-primary-fixed-variant": "#205100",
"inverse-primary": "#9bd776",
"surface": "#f8fbef"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"spacing": {
"xs": "8px",
"xl": "32px",
"base": "4px",
"sm": "12px",
"gutter": "16px",
"md": "16px",
"lg": "24px",
"container-margin": "20px"
},
"fontFamily": {
"label-md": ["Plus Jakarta Sans"],
"body-lg": ["Plus Jakarta Sans"],
"label-lg": ["Plus Jakarta Sans"],
"body-md": ["Plus Jakarta Sans"],
"headline-lg": ["Plus Jakarta Sans"],
"display": ["Plus Jakarta Sans"],
"headline-md": ["Plus Jakarta Sans"]
},
"fontSize": {
"label-md": ["12px", {"lineHeight": "16px", "fontWeight": "600"}],
"body-lg": ["17px", {"lineHeight": "26px", "fontWeight": "500"}],
"label-lg": ["13px", {"lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700"}],
"body-md": ["15px", {"lineHeight": "22px", "fontWeight": "400"}],
"headline-lg": ["28px", {"lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800"}],
"display": ["34px", {"lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800"}],
"headline-md": ["22px", {"lineHeight": "28px", "fontWeight": "700"}]
}
}
}
}
</script>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-surface text-on-surface antialiased min-h-screen flex flex-col relative font-body-md overflow-x-hidden selection:bg-primary-container selection:text-on-primary-container">
<!-- Hero Header Background -->
<div class="absolute top-0 left-0 w-full h-80 z-0 pointer-events-none">
<div class="w-full h-full bg-cover bg-center" data-alt="A soft, premium botanical photograph of a lush Monstera deliciosa leaf bathed in warm morning sunlight. The lighting is natural and high-key, creating a bright, airy, light-mode aesthetic. The background softly blurs out, allowing the vibrant, organic greens of the plant to contrast beautifully against a warm, minimalist cream backdrop. The mood is calm, upscale, and inviting." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuDvuJKth4pDT75-D0SOCZSHfyxOmTSYlp2wVfMF_cvw7EojCdrM7InYI9z_zZhxaCkB68Hc8oG_lw0h1lJfm3_8So6u5vT8WQmYsQbx0Vgt0ObahJ5JVfSYz89qYFLbo24xIoTZgLEMAhUTfnCqFSArnc727JG0cvbuWGISnP-BgzC5LS7E2kIIRkq9a2T2wbcXdZlTXWYJ9CoV7_nYSum-sfOI4-yAif6n-VpKO_motQM_ZBL5TOeI6PlhTBjS1zrS-NfT19ziw70')"></div>
<div class="absolute inset-0 bg-gradient-to-b from-surface/10 via-surface/60 to-surface"></div>
</div>
<!-- Top Action Bar -->
<div class="absolute top-0 left-0 w-full px-container-margin pt-14 pb-4 flex justify-between items-center z-20">
<button class="w-10 h-10 rounded-full bg-surface-container-lowest/60 backdrop-blur-md flex items-center justify-center text-on-surface shadow-sm hover:bg-surface-container-lowest/80 transition-colors active:scale-95">
<span class="material-symbols-outlined">close</span>
</button>
<button class="font-label-lg text-label-lg text-on-surface hover:opacity-80 transition-opacity drop-shadow-sm">
Restore
</button>
</div>
<!-- Scrollable Content Canvas -->
<main class="relative z-10 flex-1 flex flex-col px-container-margin pt-56 pb-12 w-full max-w-md mx-auto">
<!-- Typography Header -->
<div class="flex flex-col items-center text-center mb-xl">
<span class="font-label-lg text-label-lg text-primary mb-sm tracking-[0.1em] uppercase">GreenLens Pro</span>
<h1 class="font-display text-display text-on-surface font-extrabold mb-sm leading-tight">Get Unlimited Access</h1>
<p class="font-body-lg text-body-lg text-on-surface-variant max-w-[280px]">Unlimited scans, health checks and your personal care plan.</p>
</div>
<!-- Main Plan Card -->
<div class="bg-surface-container rounded-xl p-lg mb-lg shadow-[0px_4px_20px_rgba(16,28,18,0.06)] flex flex-col gap-sm border border-outline-variant/20">
<h2 class="font-headline-md text-headline-md text-on-surface">GreenLens Pro</h2>
<p class="font-body-md text-body-md text-on-surface-variant leading-relaxed">Unlimited AI scans, health diagnosis, 7-day rescue plans, 100 credits/month</p>
<div class="font-label-lg text-label-lg text-on-surface mt-base pt-sm border-t border-outline-variant/30">
Free for 7 days, then €39.99/year
</div>
</div>
<!-- Toggle Row -->
<div class="bg-surface-container-lowest rounded-xl px-md py-sm mb-xl flex justify-between items-center shadow-[0px_4px_20px_rgba(16,28,18,0.04)]">
<span class="font-label-lg text-label-lg text-on-surface">Free Trial Enabled</span>
<!-- iOS Style Toggle ON -->
<button class="w-14 h-8 bg-primary-container rounded-full relative transition-colors focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 focus:ring-offset-surface">
<div class="absolute right-1 top-1 w-6 h-6 bg-surface-container-lowest rounded-full shadow-sm transition-all"></div>
</button>
</div>
<!-- Timeline / Schedule -->
<div class="flex flex-col gap-lg pl-sm mb-xl relative">
<!-- Connecting Line -->
<div class="absolute left-[19px] top-4 bottom-8 w-0.5 bg-surface-variant z-0"></div>
<!-- Step 1: Active -->
<div class="flex items-start gap-md relative z-10">
<div class="w-4 h-4 rounded-full bg-primary mt-1 border-[3px] border-surface shadow-sm shrink-0"></div>
<div class="flex flex-col">
<span class="font-label-lg text-label-lg text-primary">Due today — 7 days free</span>
<span class="font-body-md text-body-md text-primary mt-base">€0.00</span>
</div>
</div>
<!-- Step 2: Future -->
<div class="flex items-start gap-md relative z-10">
<div class="w-4 h-4 rounded-full bg-surface-variant mt-1 border-[3px] border-surface shadow-sm shrink-0"></div>
<div class="flex flex-col">
<span class="font-body-md text-body-md text-on-surface-variant">Due July 13</span>
<span class="font-label-lg text-label-lg text-on-surface mt-base">€39.99</span>
</div>
</div>
</div>
<!-- Sticky Bottom Actions -->
<div class="mt-auto flex flex-col items-center gap-md pt-md w-full">
<button class="w-full h-14 bg-primary-container text-on-primary rounded-xl font-label-lg text-label-lg flex items-center justify-center shadow-lg hover:bg-tertiary-container hover:shadow-xl active:scale-[0.98] transition-all duration-200">
Try Free
</button>
<div class="flex flex-col items-center gap-sm">
<span class="font-label-md text-label-md text-on-surface-variant">Cancel Anytime</span>
<div class="flex items-center gap-sm text-outline-variant">
<a class="font-label-md text-label-md text-outline hover:text-on-surface transition-colors" href="#">Privacy</a>
<span class="font-label-md text-label-md">|</span>
<a class="font-label-md text-label-md text-outline hover:text-on-surface transition-colors" href="#">Terms</a>
</div>
</div>
</div>
</main>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 350 KiB

View File

@@ -0,0 +1,199 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"/>
<title>GreenLens Onboarding - Health Check</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary": "#375d00",
"on-surface": "#191d16",
"on-secondary-fixed": "#012104",
"on-secondary-fixed-variant": "#2d4e2b",
"surface-container-lowest": "#ffffff",
"surface-container-low": "#f2f5e9",
"error-container": "#ffdad6",
"secondary": "#446741",
"outline": "#72796a",
"on-secondary": "#ffffff",
"surface-container": "#ecefe4",
"on-error": "#ffffff",
"on-surface-variant": "#42493c",
"background": "#f8fbef",
"error": "#ba1a1a",
"outline-variant": "#c2c9b7",
"on-secondary-container": "#486b45",
"secondary-fixed": "#c5edbd",
"on-tertiary": "#ffffff",
"primary": "#2b5f0b",
"surface-dim": "#d8dbd0",
"primary-container": "#437824",
"tertiary-fixed-dim": "#a5d56e",
"inverse-on-surface": "#eff2e7",
"surface-container-high": "#e7e9de",
"surface-bright": "#f8fbef",
"inverse-surface": "#2e322a",
"on-background": "#191d16",
"on-primary-fixed": "#092100",
"secondary-container": "#c2eaba",
"on-tertiary-container": "#cbfd91",
"primary-fixed": "#b6f390",
"tertiary-container": "#4d771b",
"primary-fixed-dim": "#9bd776",
"tertiary-fixed": "#c0f287",
"surface-tint": "#366a17",
"secondary-fixed-dim": "#aad1a2",
"surface-variant": "#e1e4d9",
"on-error-container": "#93000a",
"on-tertiary-fixed-variant": "#2e4f00",
"on-primary": "#ffffff",
"on-primary-container": "#c1ff99",
"surface-container-highest": "#e1e4d9",
"on-tertiary-fixed": "#0f2000",
"on-primary-fixed-variant": "#205100",
"inverse-primary": "#9bd776",
"surface": "#f8fbef"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px",
"2xl": "1rem",
"3xl": "1.5rem"
},
"spacing": {
"xs": "8px",
"xl": "32px",
"base": "4px",
"sm": "12px",
"gutter": "16px",
"md": "16px",
"lg": "24px",
"container-margin": "20px"
},
"fontFamily": {
"label-md": ["Plus Jakarta Sans"],
"body-lg": ["Plus Jakarta Sans"],
"label-lg": ["Plus Jakarta Sans"],
"body-md": ["Plus Jakarta Sans"],
"headline-lg": ["Plus Jakarta Sans"],
"display": ["Plus Jakarta Sans"],
"headline-md": ["Plus Jakarta Sans"]
},
"fontSize": {
"label-md": ["12px", { "lineHeight": "16px", "fontWeight": "600" }],
"body-lg": ["17px", { "lineHeight": "26px", "fontWeight": "500" }],
"label-lg": ["13px", { "lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700" }],
"body-md": ["15px", { "lineHeight": "22px", "fontWeight": "400" }],
"headline-lg": ["28px", { "lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800" }],
"display": ["34px", { "lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800" }],
"headline-md": ["22px", { "lineHeight": "28px", "fontWeight": "700" }]
}
}
}
}
</script>
<style>
body {
-webkit-tap-highlight-color: transparent;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.material-symbols-outlined.fill {
font-variation-settings: 'FILL' 1;
}
/* Soft, natural shadow for cards */
.shadow-soft {
box-shadow: 0px 4px 20px rgba(16, 28, 18, 0.06);
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-background text-on-background h-screen w-full flex justify-center items-center overflow-hidden antialiased">
<!-- Mobile Device Container (iPhone 15 Aspect Ratio roughly 19.5:9) -->
<main class="w-full h-full max-w-md mx-auto relative flex flex-col bg-surface-container-lowest md:rounded-[40px] md:h-[852px] md:w-[393px] md:shadow-2xl overflow-hidden">
<!-- Top 60% Image Area -->
<div class="relative h-[60%] w-full">
<div class="absolute inset-0 bg-cover bg-center w-full h-full" data-alt="A close-up, high-resolution lifestyle photograph of an indoor houseplant with a few visibly yellowing, slightly droopy leaves, set against a bright, airy cream-colored background. Soft, natural daylight streams in from an unseen window, creating a calm, minimalist aesthetic. The focus is sharp on the plant, embodying a premium, organic visual style typical of high-end botanical magazines." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuDpIiAXrV02PGrV2kch25aBf36rzqZdzaUc3ve1x0O45wHQPSl-ZOiRyEK995UDMBRzUpAbTluxb-uEwFy83lmwfoXWhd7vjABtHpIatmirSuuR_22Nn5ixXze3DthIbP-ZKdfS-x7HBrSgcZ3oASanUHs4CUJc56uZvW_QIZX1IQnz2v32cl-Ro1jRK58xBuyWAfbgxmk3UnM1ibKRpufE6HKt0pXZjoqYKmnlYv2303rmMJENdBE-PaUu1BCAuPg3YKXCBpX6y4w')"></div>
<!-- Safe Area Overlay / Gradient -->
<div class="absolute inset-0 bg-gradient-to-b from-black/20 via-transparent to-black/10"></div>
<!-- Diagnosis Card Overlay -->
<div class="absolute inset-x-container-margin bottom-6">
<div class="bg-surface-container-lowest/95 backdrop-blur-xl rounded-2xl p-sm shadow-soft animate-[slideUp_0.5s_ease-out_forwards]">
<!-- Card Header -->
<div class="flex items-center gap-xs mb-sm">
<div class="bg-error-container text-error rounded-full p-2 flex items-center justify-center">
<span class="material-symbols-outlined text-[18px]">medical_services</span>
</div>
<h3 class="font-headline-md text-headline-md text-on-surface">Health Check</h3>
</div>
<!-- Diagnosis Rows -->
<div class="flex flex-col gap-xs">
<!-- Problem Row -->
<div class="flex items-center justify-between bg-error-container/30 px-sm py-xs rounded-lg">
<div class="flex items-center gap-xs">
<span class="material-symbols-outlined text-error text-[20px] fill">warning</span>
<span class="font-body-md text-body-md text-error font-medium">Overwatering detected</span>
</div>
</div>
<!-- Solution Row -->
<div class="flex items-center justify-between bg-surface-container-low px-sm py-xs rounded-lg">
<div class="flex items-center gap-xs">
<span class="material-symbols-outlined text-primary text-[20px] fill">check_circle</span>
<span class="font-body-md text-body-md text-on-surface font-medium">7-day rescue plan ready</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bottom 40% Content Area -->
<div class="relative h-[40%] bg-surface-container-lowest rounded-t-3xl -mt-6 z-10 flex flex-col justify-between p-container-margin shadow-[0px_-8px_24px_rgba(0,0,0,0.04)]">
<!-- Text Content -->
<div class="mt-xs">
<h2 class="font-headline-lg text-headline-lg text-on-surface mb-xs">Health Check &amp; Care Plan</h2>
<p class="font-body-lg text-body-lg text-on-surface-variant">GreenLens spots problems early and builds a rescue plan for you.</p>
</div>
<!-- Footer Controls -->
<div class="flex flex-col gap-lg pb-safe">
<!-- Progress Dots -->
<div class="flex items-center justify-center gap-2">
<div class="w-2 h-2 rounded-full bg-surface-variant"></div>
<!-- Active Dot -->
<div class="w-8 h-2 rounded-full bg-primary-container"></div>
<div class="w-2 h-2 rounded-full bg-surface-variant"></div>
</div>
<!-- Primary Action -->
<button class="w-full h-[56px] bg-primary-container text-on-primary font-label-lg text-label-lg rounded-[14px] flex items-center justify-center active:scale-95 transition-transform duration-150">
Continue
</button>
</div>
</div>
</main>
<style>
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 535 KiB

View File

@@ -0,0 +1,270 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"/>
<title>GreenLens Onboarding - Health Check</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&amp;family=Manrope:wght@400&amp;family=JetBrains+Mono:wght@500&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-primary-fixed-variant": "#2e4f00",
"inverse-primary": "#41690c",
"surface": "#0c160d",
"on-tertiary-fixed": "#171d15",
"primary-container": "#a6d66f",
"on-error-container": "#ffdad6",
"surface-dim": "#0c160d",
"surface-container-lowest": "#071008",
"secondary-fixed-dim": "#b7ccb5",
"secondary": "#b7ccb5",
"secondary-container": "#394b39",
"on-surface": "#dae6d6",
"primary-fixed": "#c0f287",
"primary": "#c1f388",
"on-primary": "#1e3700",
"surface-bright": "#313c31",
"tertiary-fixed": "#dee5d6",
"error-container": "#93000a",
"background": "#0c160d",
"on-tertiary-fixed-variant": "#42493e",
"on-tertiary": "#2c3229",
"on-surface-variant": "#c3c9b6",
"inverse-surface": "#dae6d6",
"on-secondary": "#233424",
"on-primary-container": "#365d00",
"inverse-on-surface": "#283328",
"surface-variant": "#2d372d",
"surface-container-low": "#141e14",
"surface-container-highest": "#2d372d",
"surface-container-high": "#222d22",
"tertiary-fixed-dim": "#c2c9bb",
"tertiary-container": "#c3cabc",
"outline": "#8d9382",
"on-background": "#dae6d6",
"on-secondary-fixed": "#0f1f10",
"on-error": "#690005",
"secondary-fixed": "#d3e8d0",
"outline-variant": "#43493a",
"error": "#ffb4ab",
"on-secondary-fixed-variant": "#394b39",
"on-tertiary-container": "#4e554a",
"surface-tint": "#a5d56e",
"tertiary": "#dfe6d8",
"surface-container": "#182218",
"on-secondary-container": "#a6bba4",
"primary-fixed-dim": "#a5d56e",
"on-primary-fixed": "#0f2000"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"spacing": {
"unit": "8px",
"gutter": "24px",
"margin-mobile": "16px",
"margin-desktop": "64px",
"container-max": "1280px",
"xs": "8px",
"xl": "32px",
"base": "4px",
"sm": "12px",
"md": "16px",
"lg": "24px",
"container-margin": "20px"
},
"fontFamily": {
"display": [
"Epilogue"
],
"headline-md": [
"Epilogue"
],
"body-lg": [
"Manrope"
],
"headline-lg": [
"Epilogue"
],
"label-sm": [
"JetBrains Mono"
],
"headline-lg-mobile": [
"Epilogue"
],
"body-md": [
"Manrope"
],
"label-lg": [
"Epilogue"
]
},
"fontSize": {
"display": [
"64px",
{
"lineHeight": "1.1",
"letterSpacing": "-0.02em",
"fontWeight": "700"
}
],
"headline-md": [
"28px",
{
"lineHeight": "1.3",
"fontWeight": "500"
}
],
"body-lg": [
"18px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
],
"headline-lg": [
"40px",
{
"lineHeight": "1.2",
"letterSpacing": "-0.01em",
"fontWeight": "600"
}
],
"label-sm": [
"12px",
{
"lineHeight": "1.0",
"letterSpacing": "0.05em",
"fontWeight": "500"
}
],
"label-lg": [
"16px",
{
"lineHeight": "1.4",
"fontWeight": "600"
}
],
"headline-lg-mobile": [
"32px",
{
"lineHeight": "1.2",
"fontWeight": "600"
}
],
"body-md": [
"16px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
]
}
},
},
}
</script>
<style>
body {
-webkit-tap-highlight-color: transparent;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.material-symbols-outlined.fill {
font-variation-settings: 'FILL' 1;
}
/* Soft, natural shadow for cards */
.shadow-soft {
box-shadow: 0px 4px 20px rgba(16, 28, 18, 0.06);
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-background text-on-background h-screen w-full flex justify-center items-center overflow-hidden antialiased">
<!-- Mobile Device Container (iPhone 15 Aspect Ratio roughly 19.5:9) -->
<main class="w-full h-full max-w-md mx-auto relative flex flex-col bg-surface-container-lowest md:rounded-[40px] md:h-[852px] md:w-[393px] md:shadow-2xl overflow-hidden">
<!-- Top 60% Image Area -->
<div class="relative h-[60%] w-full">
<div class="absolute inset-0 bg-cover bg-center w-full h-full" data-alt="A close-up, high-resolution lifestyle photograph of an indoor houseplant with a few visibly yellowing, slightly droopy leaves, set against a bright, airy cream-colored background. Soft, natural daylight streams in from an unseen window, creating a calm, minimalist aesthetic. The focus is sharp on the plant, embodying a premium, organic visual style typical of high-end botanical magazines." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuDpIiAXrV02PGrV2kch25aBf36rzqZdzaUc3ve1x0O45wHQPSl-ZOiRyEK995UDMBRzUpAbTluxb-uEwFy83lmwfoXWhd7vjABtHpIatmirSuuR_22Nn5ixXze3DthIbP-ZKdfS-x7HBrSgcZ3oASanUHs4CUJc56uZvW_QIZX1IQnz2v32cl-Ro1jRK58xBuyWAfbgxmk3UnM1ibKRpufE6HKt0pXZjoqYKmnlYv2303rmMJENdBE-PaUu1BCAuPg3YKXCBpX6y4w')"></div>
<!-- Safe Area Overlay / Gradient -->
<div class="absolute inset-0 bg-gradient-to-b from-black/50 via-transparent to-black/60"></div>
<!-- Diagnosis Card Overlay -->
<div class="absolute inset-x-container-margin bottom-6">
<div class="bg-surface-container-lowest/95 backdrop-blur-xl rounded-2xl p-sm shadow-soft animate-[slideUp_0.5s_ease-out_forwards]">
<!-- Card Header -->
<div class="flex items-center gap-xs mb-sm">
<div class="bg-error-container text-error rounded-full p-2 flex items-center justify-center">
<span class="material-symbols-outlined text-[18px]">medical_services</span>
</div>
<h3 class="font-headline-md text-headline-md text-on-surface">Health Check</h3>
</div>
<!-- Diagnosis Rows -->
<div class="flex flex-col gap-xs">
<!-- Problem Row -->
<div class="flex items-center justify-between bg-error-container/30 px-sm py-xs rounded-lg">
<div class="flex items-center gap-xs">
<span class="material-symbols-outlined text-error text-[20px] fill">warning</span>
<span class="font-body-md text-body-md text-error font-medium">Overwatering detected</span>
</div>
</div>
<!-- Solution Row -->
<div class="flex items-center justify-between bg-surface-container-low px-sm py-xs rounded-lg">
<div class="flex items-center gap-xs">
<span class="material-symbols-outlined text-primary text-[20px] fill">check_circle</span>
<span class="font-body-md text-body-md text-on-surface font-medium">7-day rescue plan ready</span>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Bottom 40% Content Area -->
<div class="relative h-[40%] bg-surface-container-lowest rounded-t-3xl -mt-6 z-10 flex flex-col justify-between p-container-margin shadow-[0px_-8px_24px_rgba(0,0,0,0.2)]">
<!-- Text Content -->
<div class="mt-xs">
<h2 class="font-headline-lg text-headline-lg text-on-surface mb-xs">Health Check &amp; Care Plan</h2>
<p class="font-body-lg text-body-lg text-on-surface-variant">GreenLens spots problems early and builds a rescue plan for you.</p>
</div>
<!-- Footer Controls -->
<div class="flex flex-col gap-lg pb-safe">
<!-- Progress Dots -->
<div class="flex items-center justify-center gap-2">
<div class="w-2 h-2 rounded-full bg-surface-variant"></div>
<!-- Active Dot -->
<div class="w-8 h-2 rounded-full bg-primary-container"></div>
<div class="w-2 h-2 rounded-full bg-surface-variant"></div>
</div>
<!-- Primary Action -->
<button class="w-full h-[56px] bg-primary-container text-on-primary font-label-lg text-label-lg rounded-[14px] flex items-center justify-center active:scale-95 transition-transform duration-150">
Continue
</button>
</div>
</div>
</main>
<style>
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 542 KiB

View File

@@ -0,0 +1,343 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, viewport-fit=cover" name="viewport"/>
<title>GreenLens - Login</title>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary": "#375d00",
"on-surface": "#191d16",
"on-secondary-fixed": "#012104",
"on-secondary-fixed-variant": "#2d4e2b",
"surface-container-lowest": "#ffffff",
"surface-container-low": "#f2f5e9",
"error-container": "#ffdad6",
"secondary": "#446741",
"outline": "#72796a",
"on-secondary": "#ffffff",
"surface-container": "#ecefe4",
"on-error": "#ffffff",
"on-surface-variant": "#42493c",
"background": "#f8fbef",
"error": "#ba1a1a",
"outline-variant": "#c2c9b7",
"on-secondary-container": "#486b45",
"secondary-fixed": "#c5edbd",
"on-tertiary": "#ffffff",
"primary": "#2b5f0b",
"surface-dim": "#d8dbd0",
"primary-container": "#437824",
"tertiary-fixed-dim": "#a5d56e",
"inverse-on-surface": "#eff2e7",
"surface-container-high": "#e7e9de",
"surface-bright": "#f8fbef",
"inverse-surface": "#2e322a",
"on-background": "#191d16",
"on-primary-fixed": "#092100",
"secondary-container": "#c2eaba",
"on-tertiary-container": "#cbfd91",
"primary-fixed": "#b6f390",
"tertiary-container": "#4d771b",
"primary-fixed-dim": "#9bd776",
"tertiary-fixed": "#c0f287",
"surface-tint": "#366a17",
"secondary-fixed-dim": "#aad1a2",
"surface-variant": "#e1e4d9",
"on-error-container": "#93000a",
"on-tertiary-fixed-variant": "#2e4f00",
"on-primary": "#ffffff",
"on-primary-container": "#c1ff99",
"surface-container-highest": "#e1e4d9",
"on-tertiary-fixed": "#0f2000",
"on-primary-fixed-variant": "#205100",
"inverse-primary": "#9bd776",
"surface": "#f8fbef"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"spacing": {
"xs": "8px",
"xl": "32px",
"base": "4px",
"sm": "12px",
"gutter": "16px",
"md": "16px",
"lg": "24px",
"container-margin": "20px"
},
"fontFamily": {
"label-md": [
"Plus Jakarta Sans"
],
"body-lg": [
"Plus Jakarta Sans"
],
"label-lg": [
"Plus Jakarta Sans"
],
"body-md": [
"Plus Jakarta Sans"
],
"headline-lg": [
"Plus Jakarta Sans"
],
"display": [
"Plus Jakarta Sans"
],
"headline-md": [
"Plus Jakarta Sans"
]
},
"fontSize": {
"label-md": [
"12px",
{
"lineHeight": "16px",
"fontWeight": "600"
}
],
"body-lg": [
"17px",
{
"lineHeight": "26px",
"fontWeight": "500"
}
],
"label-lg": [
"13px",
{
"lineHeight": "18px",
"letterSpacing": "0.05em",
"fontWeight": "700"
}
],
"body-md": [
"15px",
{
"lineHeight": "22px",
"fontWeight": "400"
}
],
"headline-lg": [
"28px",
{
"lineHeight": "36px",
"letterSpacing": "-0.01em",
"fontWeight": "800"
}
],
"display": [
"34px",
{
"lineHeight": "42px",
"letterSpacing": "-0.02em",
"fontWeight": "800"
}
],
"headline-md": [
"22px",
{
"lineHeight": "28px",
"fontWeight": "700"
}
]
}
},
}
}
</script>
<style>
body {
-webkit-tap-highlight-color: transparent;
background-color: #000; /* For framing */
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.iphone-frame {
width: 100%;
max-width: 393px; /* iPhone 15 width */
height: 100vh;
max-height: 852px; /* iPhone 15 height */
background-color: #FBFAF3; /* surface */
position: relative;
overflow: hidden;
display: flex;
flex-col;
}
@media (min-width: 400px) {
.iphone-frame {
height: 852px;
border-radius: 54px;
box-shadow: 0 0 0 10px #000, 0 20px 40px rgba(0,0,0,0.5);
margin: 20px;
}
}
.hero-section {
height: 35%;
position: relative;
}
.hero-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.hero-gradient {
position: absolute;
inset: 0;
background: linear-gradient(to bottom, rgba(0,0,0,0.2) 0%, rgba(251,250,243,0) 50%, #FBFAF3 100%);
}
.form-sheet {
height: 65%;
background-color: #FBFAF3;
border-top-left-radius: 24px;
border-top-right-radius: 24px;
margin-top: -24px; /* Pull up to overlap hero */
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
padding: 24px 20px;
}
/* Custom Input Styling matching guidelines */
.input-field {
height: 56px;
background-color: #FFFFFF;
border-radius: 14px;
border: 1px solid #D1D4CC;
padding: 0 16px;
font-family: 'Plus Jakarta Sans', sans-serif;
font-size: 15px;
font-weight: 500;
color: #191d16;
width: 100%;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-field:focus {
border-color: #437824;
box-shadow: 0 0 0 2px rgba(67, 120, 36, 0.2);
}
.input-field::placeholder {
color: #72796a;
font-weight: 400;
}
/* Button Styling */
.btn-primary {
height: 56px;
background-color: #437824;
border-radius: 14px;
color: #FFFFFF;
font-family: 'Plus Jakarta Sans', sans-serif;
font-size: 15px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
border: none;
cursor: pointer;
transition: transform 0.1s, opacity 0.2s;
}
.btn-primary:active {
transform: scale(0.98);
}
.btn-apple {
height: 56px;
background-color: #000000;
border-radius: 14px;
color: #FFFFFF;
font-family: 'Plus Jakarta Sans', sans-serif;
font-size: 15px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
border: none;
cursor: pointer;
gap: 8px;
transition: transform 0.1s;
}
.btn-apple:active {
transform: scale(0.98);
}
.divider-line {
height: 1px;
background-color: #e1e4d9;
flex-grow: 1;
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-gray-100">
<div class="iphone-frame flex flex-col relative">
<!-- Hero Image Area (35%) -->
<div class="hero-section">
<img alt="Botanical hero image" class="hero-image" data-alt="A lush, high-resolution close-up of a Monstera Deliciosa leaf, softly backlit by warm morning sunlight. The botanical photography is premium, airy, and calming, featuring rich vibrant greens against a slightly blurred, clean background. The aesthetic is modern minimalist with a tactile organic feel, perfectly suited for a high-end plant care app." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAZ52RoNjzx9-A72eQKmdGgnrltylpxouotb1w9T0WHGj_3lAW7G9L6cV6MgKptlxFZiiYT6AobZ5G7R3rvT86jhDTo5J87nsGwWrcqHnBgUOQ4enGmQ7WSStk2vV4zdWdSHCBfPSndpOUj_DRF3od8ASsFMDcA6VS02INwlqEmewyvYC1lpHPrr1ZrsHut8BAc0tWBa3GB8MKSNcQo03Da-KD06wwLK6kNBFyEHQqt77xxescSLi2CZgi_Zq5c2hNhs9Qd993DV-o"/>
<div class="hero-gradient"></div>
<div class="absolute bottom-10 left-5">
<h1 class="font-display text-display text-on-surface">Welcome back!</h1>
</div>
</div>
<!-- Form Sheet Area (65%) -->
<div class="form-sheet">
<!-- Apple Login -->
<button class="btn-apple mb-lg">
<svg fill="currentColor" height="20" viewbox="0 0 24 24" width="20">
<path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.62-1.496 3.603-2.998 1.156-1.682 1.633-3.314 1.655-3.398-.035-.015-3.182-1.22-3.216-4.858-.026-3.04 2.484-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.693.792-1.353 2.235-1.169 3.603 1.343.104 2.61-.715 3.456-1.59z"></path>
</svg>
Continue with Apple
</button>
<!-- Divider -->
<div class="flex items-center gap-4 mb-lg">
<div class="divider-line"></div>
<span class="font-label-md text-label-md text-outline">OR</span>
<div class="divider-line"></div>
</div>
<!-- Inputs -->
<div class="flex flex-col gap-sm mb-md">
<input autocomplete="email" class="input-field" placeholder="Email address" type="email"/>
<input autocomplete="current-password" class="input-field" placeholder="Password" type="password"/>
</div>
<!-- Forgot Password -->
<div class="flex justify-end mb-lg">
<a class="font-body-md text-body-md text-primary-container font-semibold hover:opacity-80 transition-opacity" href="#">Forgot password?</a>
</div>
<!-- Submit Button -->
<button class="btn-primary mb-lg">
Log in
</button>
<!-- Spacer -->
<div class="flex-grow"></div>
<!-- Sign Up Link -->
<div class="text-center pb-8">
<span class="font-body-md text-body-md text-on-surface-variant">New here? </span>
<a class="font-body-md text-body-md text-primary-container font-bold hover:opacity-80 transition-opacity" href="#">Create account</a>
</div>
</div>
</div>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 419 KiB

View File

@@ -0,0 +1,341 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, viewport-fit=cover" name="viewport"/>
<title>GreenLens - Login</title>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&amp;family=JetBrains+Mono:wght@500&amp;family=Manrope:wght@400&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-primary-fixed-variant": "#2e4f00",
"inverse-primary": "#41690c",
"surface": "#0c160d",
"on-tertiary-fixed": "#171d15",
"primary-container": "#a6d66f",
"on-error-container": "#ffdad6",
"surface-dim": "#0c160d",
"surface-container-lowest": "#071008",
"secondary-fixed-dim": "#b7ccb5",
"secondary": "#b7ccb5",
"secondary-container": "#394b39",
"on-surface": "#dae6d6",
"primary-fixed": "#c0f287",
"primary": "#c1f388",
"on-primary": "#1e3700",
"surface-bright": "#313c31",
"tertiary-fixed": "#dee5d6",
"error-container": "#93000a",
"background": "#0c160d",
"on-tertiary-fixed-variant": "#42493e",
"on-tertiary": "#2c3229",
"on-surface-variant": "#c3c9b6",
"inverse-surface": "#dae6d6",
"on-secondary": "#233424",
"on-primary-container": "#365d00",
"inverse-on-surface": "#283328",
"surface-variant": "#2d372d",
"surface-container-low": "#141e14",
"surface-container-highest": "#2d372d",
"surface-container-high": "#222d22",
"tertiary-fixed-dim": "#c2c9bb",
"tertiary-container": "#c3cabc",
"outline": "#8d9382",
"on-background": "#dae6d6",
"on-secondary-fixed": "#0f1f10",
"on-error": "#690005",
"secondary-fixed": "#d3e8d0",
"outline-variant": "#43493a",
"error": "#ffb4ab",
"on-secondary-fixed-variant": "#394b39",
"on-tertiary-container": "#4e554a",
"surface-tint": "#a5d56e",
"tertiary": "#dfe6d8",
"surface-container": "#182218",
"on-secondary-container": "#a6bba4",
"primary-fixed-dim": "#a5d56e",
"on-primary-fixed": "#0f2000"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"spacing": {
"unit": "8px",
"gutter": "24px",
"margin-mobile": "16px",
"margin-desktop": "64px",
"container-max": "1280px"
},
"fontFamily": {
"display": [
"Epilogue"
],
"headline-md": [
"Epilogue"
],
"body-lg": [
"Manrope"
],
"headline-lg": [
"Epilogue"
],
"label-sm": [
"JetBrains Mono"
],
"headline-lg-mobile": [
"Epilogue"
],
"body-md": [
"Manrope"
]
},
"fontSize": {
"display": [
"64px",
{
"lineHeight": "1.1",
"letterSpacing": "-0.02em",
"fontWeight": "700"
}
],
"headline-md": [
"28px",
{
"lineHeight": "1.3",
"fontWeight": "500"
}
],
"body-lg": [
"18px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
],
"headline-lg": [
"40px",
{
"lineHeight": "1.2",
"letterSpacing": "-0.01em",
"fontWeight": "600"
}
],
"label-sm": [
"12px",
{
"lineHeight": "1.0",
"letterSpacing": "0.05em",
"fontWeight": "500"
}
],
"headline-lg-mobile": [
"32px",
{
"lineHeight": "1.2",
"fontWeight": "600"
}
],
"body-md": [
"16px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
]
}
},
},
}
</script>
<style>
body {
-webkit-tap-highlight-color: transparent;
background-color: #000; /* For framing */
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
margin: 0;
}
.iphone-frame {
width: 100%;
max-width: 393px; /* iPhone 15 width */
height: 100vh;
max-height: 852px; /* iPhone 15 height */
background-color: #0c160d; /* surface */
position: relative;
overflow: hidden;
display: flex;
flex-col;
}
@media (min-width: 400px) {
.iphone-frame {
height: 852px;
border-radius: 54px;
box-shadow: 0 0 0 10px #000, 0 20px 40px rgba(0,0,0,0.5);
margin: 20px;
}
}
.hero-section {
height: 35%;
position: relative;
}
.hero-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.hero-gradient {
position: absolute;
inset: 0;
background: linear-gradient(to bottom, rgba(0,0,0,0.2) 0%, rgba(12,22,13,0) 50%, #0c160d 100%);
}
.form-sheet {
height: 65%;
background-color: #0c160d;
border-top-left-radius: 24px;
border-top-right-radius: 24px;
margin-top: -24px; /* Pull up to overlap hero */
position: relative;
z-index: 10;
display: flex;
flex-direction: column;
padding: 24px 20px;
}
/* Custom Input Styling matching guidelines */
.input-field {
height: 56px;
background-color: #141e14;
border-radius: 14px;
border: 1px solid #43493a;
padding: 0 16px;
font-family: 'Manrope', sans-serif;
font-size: 16px;
font-weight: 400;
color: #dae6d6;
width: 100%;
outline: none;
transition: border-color 0.2s, box-shadow 0.2s;
}
.input-field:focus {
border-color: #c1f388;
box-shadow: 0 0 0 2px rgba(193, 243, 136, 0.2);
}
.input-field::placeholder {
color: #8d9382;
font-weight: 400;
}
/* Button Styling */
.btn-primary {
height: 56px;
background-color: #a6d66f;
border-radius: 14px;
color: #365d00;
font-family: 'Manrope', sans-serif;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
border: none;
cursor: pointer;
transition: transform 0.1s, opacity 0.2s;
}
.btn-primary:active {
transform: scale(0.98);
}
.btn-apple {
height: 56px;
background-color: #dae6d6;
border-radius: 14px;
color: #0c160d;
font-family: 'Manrope', sans-serif;
font-size: 16px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
width: 100%;
border: none;
cursor: pointer;
gap: 8px;
transition: transform 0.1s;
}
.btn-apple:active {
transform: scale(0.98);
}
.divider-line {
height: 1px;
background-color: #2d372d;
flex-grow: 1;
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-gray-100 dark:bg-black">
<div class="iphone-frame flex flex-col relative">
<!-- Hero Image Area (35%) -->
<div class="hero-section">
<img alt="Botanical hero image" class="hero-image" data-alt="A lush, high-resolution close-up of a Monstera Deliciosa leaf, softly backlit by warm morning sunlight. The botanical photography is premium, airy, and calming, featuring rich vibrant greens against a slightly blurred, clean background. The aesthetic is modern minimalist with a tactile organic feel, perfectly suited for a high-end plant care app." src="https://lh3.googleusercontent.com/aida-public/AB6AXuAZ52RoNjzx9-A72eQKmdGgnrltylpxouotb1w9T0WHGj_3lAW7G9L6cV6MgKptlxFZiiYT6AobZ5G7R3rvT86jhDTo5J87nsGwWrcqHnBgUOQ4enGmQ7WSStk2vV4zdWdSHCBfPSndpOUj_DRF3od8ASsFMDcA6VS02INwlqEmewyvYC1lpHPrr1ZrsHut8BAc0tWBa3GB8MKSNcQo03Da-KD06wwLK6kNBFyEHQqt77xxescSLi2CZgi_Zq5c2hNhs9Qd993DV-o"/>
<div class="hero-gradient"></div>
<div class="absolute bottom-10 left-5">
<h1 class="font-display text-display text-on-surface">Welcome back!</h1>
</div>
</div>
<!-- Form Sheet Area (65%) -->
<div class="form-sheet">
<!-- Apple Login -->
<button class="btn-apple mb-6">
<svg fill="currentColor" height="20" viewbox="0 0 24 24" width="20">
<path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.62-1.496 3.603-2.998 1.156-1.682 1.633-3.314 1.655-3.398-.035-.015-3.182-1.22-3.216-4.858-.026-3.04 2.484-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.693.792-1.353 2.235-1.169 3.603 1.343.104 2.61-.715 3.456-1.59z"></path>
</svg>
Continue with Apple
</button>
<!-- Divider -->
<div class="flex items-center gap-4 mb-6">
<div class="divider-line"></div>
<span class="font-label-sm text-label-sm text-outline">OR</span>
<div class="divider-line"></div>
</div>
<!-- Inputs -->
<div class="flex flex-col gap-4 mb-4">
<input autocomplete="email" class="input-field" placeholder="Email address" type="email"/>
<input autocomplete="current-password" class="input-field" placeholder="Password" type="password"/>
</div>
<!-- Forgot Password -->
<div class="flex justify-end mb-6">
<a class="font-body-md text-body-md text-primary-container font-semibold hover:opacity-80 transition-opacity" href="#">Forgot password?</a>
</div>
<!-- Submit Button -->
<button class="btn-primary mb-6">
Log in
</button>
<!-- Spacer -->
<div class="flex-grow"></div>
<!-- Sign Up Link -->
<div class="text-center pb-8">
<span class="font-body-md text-body-md text-on-surface-variant">New here? </span>
<a class="font-body-md text-body-md text-primary-container font-bold hover:opacity-80 transition-opacity" href="#">Create account</a>
</div>
</div>
</div>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB

View File

@@ -0,0 +1,178 @@
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>GreenLens - Onboarding</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@400..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary": "#375d00",
"on-surface": "#191d16",
"on-secondary-fixed": "#012104",
"on-secondary-fixed-variant": "#2d4e2b",
"surface-container-lowest": "#ffffff",
"surface-container-low": "#f2f5e9",
"error-container": "#ffdad6",
"secondary": "#446741",
"outline": "#72796a",
"on-secondary": "#ffffff",
"surface-container": "#ecefe4",
"on-error": "#ffffff",
"on-surface-variant": "#42493c",
"background": "#f8fbef",
"error": "#ba1a1a",
"outline-variant": "#c2c9b7",
"on-secondary-container": "#486b45",
"secondary-fixed": "#c5edbd",
"on-tertiary": "#ffffff",
"primary": "#2b5f0b",
"surface-dim": "#d8dbd0",
"primary-container": "#437824",
"tertiary-fixed-dim": "#a5d56e",
"inverse-on-surface": "#eff2e7",
"surface-container-high": "#e7e9de",
"surface-bright": "#f8fbef",
"inverse-surface": "#2e322a",
"on-background": "#191d16",
"on-primary-fixed": "#092100",
"secondary-container": "#c2eaba",
"on-tertiary-container": "#cbfd91",
"primary-fixed": "#b6f390",
"tertiary-container": "#4d771b",
"primary-fixed-dim": "#9bd776",
"tertiary-fixed": "#c0f287",
"surface-tint": "#366a17",
"secondary-fixed-dim": "#aad1a2",
"surface-variant": "#e1e4d9",
"on-error-container": "#93000a",
"on-tertiary-fixed-variant": "#2e4f00",
"on-primary": "#ffffff",
"on-primary-container": "#c1ff99",
"surface-container-highest": "#e1e4d9",
"on-tertiary-fixed": "#0f2000",
"on-primary-fixed-variant": "#205100",
"inverse-primary": "#9bd776",
"surface": "#f8fbef"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px",
"sheet": "2rem"
},
"spacing": {
"xs": "8px",
"xl": "32px",
"base": "4px",
"sm": "12px",
"gutter": "16px",
"md": "16px",
"lg": "24px",
"container-margin": "20px"
},
"fontFamily": {
"label-md": ["Plus Jakarta Sans"],
"body-lg": ["Plus Jakarta Sans"],
"label-lg": ["Plus Jakarta Sans"],
"body-md": ["Plus Jakarta Sans"],
"headline-lg": ["Plus Jakarta Sans"],
"display": ["Plus Jakarta Sans"],
"headline-md": ["Plus Jakarta Sans"]
},
"fontSize": {
"label-md": ["12px", {"lineHeight": "16px", "fontWeight": "600"}],
"body-lg": ["17px", {"lineHeight": "26px", "fontWeight": "500"}],
"label-lg": ["13px", {"lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700"}],
"body-md": ["15px", {"lineHeight": "22px", "fontWeight": "400"}],
"headline-lg": ["28px", {"lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800"}],
"display": ["34px", {"lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800"}],
"headline-md": ["22px", {"lineHeight": "28px", "fontWeight": "700"}]
}
}
}
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 600, 'GRAD' 0, 'opsz' 24;
}
.icon-fill {
font-variation-settings: 'FILL' 1, 'wght' 600;
}
.fade-in-up-1 { animation: fadeInUp 0.8s ease-out 0.2s both; }
.fade-in-up-2 { animation: fadeInUp 0.8s ease-out 0.4s both; }
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(15px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-surface h-screen w-full flex flex-col overflow-hidden antialiased text-on-surface selection:bg-primary-container selection:text-on-primary">
<!-- Top 60% Image Section -->
<div class="relative h-[574px] w-full flex-shrink-0">
<div class="absolute inset-0 bg-cover bg-center w-full h-full" data-alt="A high-end, softly lit interior photography shot of a sleek wooden shelf filled with lush, healthy indoor houseplants including a vibrant Monstera and a small potted Basil. The setting is bright and airy, illuminated by natural morning sunlight creating a warm, modern aesthetic. The color palette emphasizes fresh natural greens against a soft, creamy background, fitting a premium lifestyle magazine layout." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuA4uAMcvLFEJ2VgQ1TjVJ58xBi4YAa_V5XtYVfbGSpZAqonYJu56TJFY--BKMWxfBnecwSfeXvmKv9EwJKMH5eLLoIsP87j7tqZGUN4axa1C8Zk8Rvblw4tgpygjPRn634PK4nS5yHRkrxk0JXMu0nSl-yIlM4KQmbX_QuNVJaGPm-0RuY4jkSOMaIGg5UUeCuefUZeAhgp50PjNFeRqsXCj4rQPVCyP1BedEFOTx8Tvu-4PXol7WD2DqO-w8IhpHkrdzXMImLPiks')">
</div>
<!-- Gradient Overlay for contrast at the top/bottom -->
<div class="absolute inset-0 bg-gradient-to-b from-black/10 via-transparent to-surface/40"></div>
<!-- Floating Reminder Chips -->
<div class="absolute inset-0 p-container-margin pt-16 flex flex-col gap-4 relative">
<!-- Chip 1 -->
<div class="fade-in-up-1 self-end mt-12 bg-surface-container-lowest/90 backdrop-blur-md px-sm py-2 rounded-full shadow-[0px_4px_20px_rgba(16,28,18,0.08)] flex items-center gap-2 border border-surface-variant max-w-max">
<div class="w-8 h-8 rounded-full bg-secondary-container flex items-center justify-center text-on-secondary-container">
<span class="material-symbols-outlined icon-fill text-[18px]">water_drop</span>
</div>
<div class="pr-2">
<p class="font-label-md text-label-md text-on-surface">Water Monstera</p>
<p class="font-label-md text-[10px] text-tertiary-container mt-0.5">Today</p>
</div>
</div>
<!-- Chip 2 -->
<div class="fade-in-up-2 self-start mt-8 ml-4 bg-surface-container-lowest/90 backdrop-blur-md px-sm py-2 rounded-full shadow-[0px_4px_20px_rgba(16,28,18,0.08)] flex items-center gap-2 border border-surface-variant max-w-max">
<div class="w-8 h-8 rounded-full bg-tertiary-fixed flex items-center justify-center text-on-tertiary-fixed">
<span class="material-symbols-outlined icon-fill text-[18px]">eco</span>
</div>
<div class="pr-2">
<p class="font-label-md text-label-md text-on-surface">Fertilize Basil</p>
<p class="font-label-md text-[10px] text-on-surface-variant mt-0.5">In 3 days</p>
</div>
</div>
</div>
</div>
<!-- Bottom 40% Content Sheet -->
<div class="relative flex-1 bg-surface-container-lowest rounded-t-[32px] -mt-8 z-10 px-container-margin pt-xl pb-10 flex flex-col justify-between shadow-[0px_-8px_30px_rgba(16,28,18,0.04)]">
<!-- Text Content -->
<div class="flex flex-col items-center text-center px-4">
<h1 class="font-headline-lg text-headline-lg text-on-surface mb-3">Never Forget Watering</h1>
<p class="font-body-lg text-body-lg text-on-surface-variant">Smart reminders and your personal plant library keep everything on track.</p>
</div>
<!-- Pagination & Actions -->
<div class="flex flex-col items-center w-full gap-lg mt-auto">
<!-- Progress Dots -->
<div class="flex gap-2 items-center">
<div class="w-2 h-2 rounded-full bg-surface-variant transition-colors duration-300"></div>
<div class="w-2 h-2 rounded-full bg-surface-variant transition-colors duration-300"></div>
<div class="w-8 h-2 rounded-full bg-primary transition-all duration-300"></div>
</div>
<!-- Primary Button -->
<button class="w-full h-[56px] bg-primary-container hover:bg-primary text-on-primary font-label-lg text-label-lg rounded-[14px] flex items-center justify-center transition-all duration-200 active:scale-95 shadow-sm">
Continue
</button>
</div>
</div>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 642 KiB

View File

@@ -0,0 +1,235 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>GreenLens - Onboarding</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@500&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@400..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-primary-fixed-variant": "#2e4f00",
"inverse-primary": "#41690c",
"surface": "#0c160d",
"on-tertiary-fixed": "#171d15",
"primary-container": "#a6d66f",
"on-error-container": "#ffdad6",
"surface-dim": "#0c160d",
"surface-container-lowest": "#071008",
"secondary-fixed-dim": "#b7ccb5",
"secondary": "#b7ccb5",
"secondary-container": "#394b39",
"on-surface": "#dae6d6",
"primary-fixed": "#c0f287",
"primary": "#c1f388",
"on-primary": "#1e3700",
"surface-bright": "#313c31",
"tertiary-fixed": "#dee5d6",
"error-container": "#93000a",
"background": "#0c160d",
"on-tertiary-fixed-variant": "#42493e",
"on-tertiary": "#2c3229",
"on-surface-variant": "#c3c9b6",
"inverse-surface": "#dae6d6",
"on-secondary": "#233424",
"on-primary-container": "#365d00",
"inverse-on-surface": "#283328",
"surface-variant": "#2d372d",
"surface-container-low": "#141e14",
"surface-container-highest": "#2d372d",
"surface-container-high": "#222d22",
"tertiary-fixed-dim": "#c2c9bb",
"tertiary-container": "#c3cabc",
"outline": "#8d9382",
"on-background": "#dae6d6",
"on-secondary-fixed": "#0f1f10",
"on-error": "#690005",
"secondary-fixed": "#d3e8d0",
"outline-variant": "#43493a",
"error": "#ffb4ab",
"on-secondary-fixed-variant": "#394b39",
"on-tertiary-container": "#4e554a",
"surface-tint": "#a5d56e",
"tertiary": "#dfe6d8",
"surface-container": "#182218",
"on-secondary-container": "#a6bba4",
"primary-fixed-dim": "#a5d56e",
"on-primary-fixed": "#0f2000"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"spacing": {
"unit": "8px",
"gutter": "24px",
"margin-mobile": "16px",
"margin-desktop": "64px",
"container-max": "1280px"
},
"fontFamily": {
"display": [
"Epilogue"
],
"headline-md": [
"Epilogue"
],
"body-lg": [
"Manrope"
],
"headline-lg": [
"Epilogue"
],
"label-sm": [
"JetBrains Mono"
],
"headline-lg-mobile": [
"Epilogue"
],
"body-md": [
"Manrope"
]
},
"fontSize": {
"display": [
"64px",
{
"lineHeight": "1.1",
"letterSpacing": "-0.02em",
"fontWeight": "700"
}
],
"headline-md": [
"28px",
{
"lineHeight": "1.3",
"fontWeight": "500"
}
],
"body-lg": [
"18px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
],
"headline-lg": [
"40px",
{
"lineHeight": "1.2",
"letterSpacing": "-0.01em",
"fontWeight": "600"
}
],
"label-sm": [
"12px",
{
"lineHeight": "1.0",
"letterSpacing": "0.05em",
"fontWeight": "500"
}
],
"headline-lg-mobile": [
"32px",
{
"lineHeight": "1.2",
"fontWeight": "600"
}
],
"body-md": [
"16px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 600, 'GRAD' 0, 'opsz' 24;
}
.icon-fill {
font-variation-settings: 'FILL' 1, 'wght' 600;
}
.fade-in-up-1 { animation: fadeInUp 0.8s ease-out 0.2s both; }
.fade-in-up-2 { animation: fadeInUp 0.8s ease-out 0.4s both; }
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(15px); }
to { opacity: 1; transform: translateY(0); }
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-surface h-screen w-full flex flex-col overflow-hidden antialiased text-on-surface selection:bg-primary-container selection:text-on-primary">
<!-- Top 60% Image Section -->
<div class="relative h-[574px] w-full flex-shrink-0">
<div class="absolute inset-0 bg-cover bg-center w-full h-full" data-alt="A high-end, softly lit interior photography shot of a sleek wooden shelf filled with lush, healthy indoor houseplants including a vibrant Monstera and a small potted Basil. The setting is bright and airy, illuminated by natural morning sunlight creating a warm, modern aesthetic. The color palette emphasizes fresh natural greens against a soft, creamy background, fitting a premium lifestyle magazine layout." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuA4uAMcvLFEJ2VgQ1TjVJ58xBi4YAa_V5XtYVfbGSpZAqonYJu56TJFY--BKMWxfBnecwSfeXvmKv9EwJKMH5eLLoIsP87j7tqZGUN4axa1C8Zk8Rvblw4tgpygjPRn634PK4nS5yHRkrxk0JXMu0nSl-yIlM4KQmbX_QuNVJaGPm-0RuY4jkSOMaIGg5UUeCuefUZeAhgp50PjNFeRqsXCj4rQPVCyP1BedEFOTx8Tvu-4PXol7WD2DqO-w8IhpHkrdzXMImLPiks')">
</div>
<!-- Gradient Overlay for contrast at the top/bottom -->
<div class="absolute inset-0 bg-gradient-to-b from-black/40 via-transparent to-surface"></div>
<!-- Floating Reminder Chips -->
<div class="absolute inset-0 p-margin-mobile pt-16 flex flex-col gap-4 relative">
<!-- Chip 1 -->
<div class="fade-in-up-1 self-end mt-12 bg-surface-container-low/90 backdrop-blur-md px-3 py-2 rounded-full shadow-[0px_4px_20px_rgba(0,0,0,0.2)] flex items-center gap-2 border border-surface-variant max-w-max">
<div class="w-8 h-8 rounded-full bg-secondary-container flex items-center justify-center text-on-secondary-container">
<span class="material-symbols-outlined icon-fill text-[18px]">water_drop</span>
</div>
<div class="pr-2">
<p class="font-label-sm text-label-sm text-on-surface">Water Monstera</p>
<p class="font-label-sm text-[10px] text-tertiary-container mt-0.5">Today</p>
</div>
</div>
<!-- Chip 2 -->
<div class="fade-in-up-2 self-start mt-8 ml-4 bg-surface-container-low/90 backdrop-blur-md px-3 py-2 rounded-full shadow-[0px_4px_20px_rgba(0,0,0,0.2)] flex items-center gap-2 border border-surface-variant max-w-max">
<div class="w-8 h-8 rounded-full bg-tertiary-fixed flex items-center justify-center text-on-tertiary-fixed">
<span class="material-symbols-outlined icon-fill text-[18px]">eco</span>
</div>
<div class="pr-2">
<p class="font-label-sm text-label-sm text-on-surface">Fertilize Basil</p>
<p class="font-label-sm text-[10px] text-on-surface-variant mt-0.5">In 3 days</p>
</div>
</div>
</div>
</div>
<!-- Bottom 40% Content Sheet -->
<div class="relative flex-1 bg-surface-container-lowest rounded-t-[32px] -mt-8 z-10 px-margin-mobile pt-8 pb-10 flex flex-col justify-between shadow-[0px_-8px_30px_rgba(0,0,0,0.1)]">
<!-- Text Content -->
<div class="flex flex-col items-center text-center px-4">
<h1 class="font-headline-lg-mobile text-headline-lg-mobile text-on-surface mb-3">Never Forget Watering</h1>
<p class="font-body-lg text-body-lg text-on-surface-variant">Smart reminders and your personal plant library keep everything on track.</p>
</div>
<!-- Pagination & Actions -->
<div class="flex flex-col items-center w-full gap-6 mt-auto">
<!-- Progress Dots -->
<div class="flex gap-2 items-center">
<div class="w-2 h-2 rounded-full bg-surface-variant transition-colors duration-300"></div>
<div class="w-2 h-2 rounded-full bg-surface-variant transition-colors duration-300"></div>
<div class="w-8 h-2 rounded-full bg-primary transition-all duration-300"></div>
</div>
<!-- Primary Button -->
<button class="w-full h-[56px] bg-primary-container hover:bg-primary text-on-primary-container font-label-sm text-label-sm rounded-lg flex items-center justify-center transition-all duration-200 active:scale-95 shadow-sm">
Continue
</button>
</div>
</div>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

View File

@@ -0,0 +1,149 @@
---
name: Nocturnal Botanical
colors:
surface: '#0c160d'
surface-dim: '#0c160d'
surface-bright: '#313c31'
surface-container-lowest: '#071008'
surface-container-low: '#141e14'
surface-container: '#182218'
surface-container-high: '#222d22'
surface-container-highest: '#2d372d'
on-surface: '#dae6d6'
on-surface-variant: '#c3c9b6'
inverse-surface: '#dae6d6'
inverse-on-surface: '#283328'
outline: '#8d9382'
outline-variant: '#43493a'
surface-tint: '#a5d56e'
primary: '#c1f388'
on-primary: '#1e3700'
primary-container: '#a6d66f'
on-primary-container: '#365d00'
inverse-primary: '#41690c'
secondary: '#b7ccb5'
on-secondary: '#233424'
secondary-container: '#394b39'
on-secondary-container: '#a6bba4'
tertiary: '#dfe6d8'
on-tertiary: '#2c3229'
tertiary-container: '#c3cabc'
on-tertiary-container: '#4e554a'
error: '#ffb4ab'
on-error: '#690005'
error-container: '#93000a'
on-error-container: '#ffdad6'
primary-fixed: '#c0f287'
primary-fixed-dim: '#a5d56e'
on-primary-fixed: '#0f2000'
on-primary-fixed-variant: '#2e4f00'
secondary-fixed: '#d3e8d0'
secondary-fixed-dim: '#b7ccb5'
on-secondary-fixed: '#0f1f10'
on-secondary-fixed-variant: '#394b39'
tertiary-fixed: '#dee5d6'
tertiary-fixed-dim: '#c2c9bb'
on-tertiary-fixed: '#171d15'
on-tertiary-fixed-variant: '#42493e'
background: '#0c160d'
on-background: '#dae6d6'
surface-variant: '#2d372d'
typography:
display:
fontFamily: Epilogue
fontSize: 64px
fontWeight: '700'
lineHeight: '1.1'
letterSpacing: -0.02em
headline-lg:
fontFamily: Epilogue
fontSize: 40px
fontWeight: '600'
lineHeight: '1.2'
letterSpacing: -0.01em
headline-lg-mobile:
fontFamily: Epilogue
fontSize: 32px
fontWeight: '600'
lineHeight: '1.2'
headline-md:
fontFamily: Epilogue
fontSize: 28px
fontWeight: '500'
lineHeight: '1.3'
body-lg:
fontFamily: Manrope
fontSize: 18px
fontWeight: '400'
lineHeight: '1.6'
body-md:
fontFamily: Manrope
fontSize: 16px
fontWeight: '400'
lineHeight: '1.6'
label-sm:
fontFamily: JetBrains Mono
fontSize: 12px
fontWeight: '500'
lineHeight: '1.0'
letterSpacing: 0.05em
rounded:
sm: 0.25rem
DEFAULT: 0.5rem
md: 0.75rem
lg: 1rem
xl: 1.5rem
full: 9999px
spacing:
unit: 8px
container-max: 1280px
gutter: 24px
margin-mobile: 16px
margin-desktop: 64px
---
## Brand & Style
This design system captures the essence of a moonlit conservatory, blending premium health and wellness with a sophisticated, late-night aesthetic. It targets an audience that values high-end botanical science, luxury self-care, and deep vitality.
The style is a fusion of **Minimalism** and **Glassmorphism**, utilizing deep, organic layers and sharp typography to create a sense of mystery and scientific precision. The UI should evoke a sense of calm, renewal, and premium exclusivity. Large-scale botanical photography should be treated with low-exposure and high-contrast to integrate seamlessly into the dark environment.
## Colors
The palette is rooted in deep, anaerobic greens to establish a luxurious dark mode experience.
- **Primary:** A vibrant, desaturated lime-green used for high-impact actions and essential highlights.
- **Surface Tiers:** Depth is created through subtle shifts in forest green values rather than pure grays, maintaining the botanical theme throughout the interface.
- **Typography:** Headlines utilize an off-white cream to ensure maximum legibility and a "glow" effect against the dark background, while body text uses a muted sage to reduce eye strain and establish hierarchy.
## Typography
The typography system uses a bold, geometric sans-serif for headlines to convey authority and modern style. Body text is set in a refined, highly legible sans-serif to maintain a professional and calm tone.
For technical data or botanical classifications, a monospaced font is used in all-caps to provide a "lab-certified" aesthetic. Ensure all display text has a slight negative letter-spacing to tighten the visual impact of the bold weights.
## Layout & Spacing
The layout follows a **Fluid Grid** model with generous outer margins to simulate the feeling of an editorial magazine.
- **Desktop:** 12-column grid with a 1280px max-width. Use 64px margins to allow the content to breathe.
- **Tablet:** 8-column grid with 32px margins.
- **Mobile:** 4-column grid with 16px margins.
Spacing should follow a strict 8px base unit. Use larger vertical gaps (80px+) between sections to emphasize the minimalist, premium feel.
## Elevation & Depth
Depth is achieved through **Tonal Layering** and **Backdrop Blurs**. Avoid traditional drop shadows; instead, use slightly lighter green surface colors to indicate elevation.
When an element needs to float (such as a modal or dropdown), apply a high-density background blur (20px+) to the surface and a 1px border using a low-opacity version of the Primary color (#A6D66F at 10% opacity) to define the edge. This "Inner Glow" technique replaces shadows to maintain the clean, organic feel.
## Shapes
The shape language is consistently **Rounded**, reflecting the soft, organic curves found in nature.
- Standard components (buttons, inputs) use a 0.5rem (8px) radius.
- Larger containers and cards use a 1.5rem (24px) radius to create a soft, inviting frame for botanical photography.
- Avoid sharp 90-degree angles to keep the UI feeling approachable and natural.
## Components
- **Buttons:** Primary buttons use the Primary Green background with dark charcoal text for maximum contrast. Secondary buttons use a ghost style with a 1px border.
- **Cards:** Use the Surface Container color (#1D2E1E) with no shadow. Imagery within cards should always have a subtle vignette to blend into the dark container.
- **Input Fields:** Filled style using the Surface color. The bottom border should animate to the Primary color on focus.
- **Chips/Badges:** Use a subtle tint of the primary color (15% opacity) for the background and the full-strength primary color for the text.
- **Progress Indicators:** Use thin, elegant lines. The track should be the neutral color, and the indicator should be the primary green.
- **Botanical Hero:** Full-bleed background images should have a dark gradient overlay (top to bottom) starting at 0% and ending at the background color (#0A140B) to ensure text legibility at the base of the section.

View File

@@ -0,0 +1,190 @@
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Out of Credits</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary": "#375d00",
"on-surface": "#191d16",
"on-secondary-fixed": "#012104",
"on-secondary-fixed-variant": "#2d4e2b",
"surface-container-lowest": "#ffffff",
"surface-container-low": "#f2f5e9",
"error-container": "#ffdad6",
"secondary": "#446741",
"outline": "#72796a",
"on-secondary": "#ffffff",
"surface-container": "#ecefe4",
"on-error": "#ffffff",
"on-surface-variant": "#42493c",
"background": "#f8fbef",
"error": "#ba1a1a",
"outline-variant": "#c2c9b7",
"on-secondary-container": "#486b45",
"secondary-fixed": "#c5edbd",
"on-tertiary": "#ffffff",
"primary": "#2b5f0b",
"surface-dim": "#d8dbd0",
"primary-container": "#437824",
"tertiary-fixed-dim": "#a5d56e",
"inverse-on-surface": "#eff2e7",
"surface-container-high": "#e7e9de",
"surface-bright": "#f8fbef",
"inverse-surface": "#2e322a",
"on-background": "#191d16",
"on-primary-fixed": "#092100",
"secondary-container": "#c2eaba",
"on-tertiary-container": "#cbfd91",
"primary-fixed": "#b6f390",
"tertiary-container": "#4d771b",
"primary-fixed-dim": "#9bd776",
"tertiary-fixed": "#c0f287",
"surface-tint": "#366a17",
"secondary-fixed-dim": "#aad1a2",
"surface-variant": "#e1e4d9",
"on-error-container": "#93000a",
"on-tertiary-fixed-variant": "#2e4f00",
"on-primary": "#ffffff",
"on-primary-container": "#c1ff99",
"surface-container-highest": "#e1e4d9",
"on-tertiary-fixed": "#0f2000",
"on-primary-fixed-variant": "#205100",
"inverse-primary": "#9bd776",
"surface": "#f8fbef"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px",
"modal": "24px"
},
"spacing": {
"xs": "8px",
"xl": "32px",
"base": "4px",
"sm": "12px",
"gutter": "16px",
"md": "16px",
"lg": "24px",
"container-margin": "20px"
},
"fontFamily": {
"label-md": ["Plus Jakarta Sans"],
"body-lg": ["Plus Jakarta Sans"],
"label-lg": ["Plus Jakarta Sans"],
"body-md": ["Plus Jakarta Sans"],
"headline-lg": ["Plus Jakarta Sans"],
"display": ["Plus Jakarta Sans"],
"headline-md": ["Plus Jakarta Sans"]
},
"fontSize": {
"label-md": ["12px", {"lineHeight": "16px", "fontWeight": "600"}],
"body-lg": ["17px", {"lineHeight": "26px", "fontWeight": "500"}],
"label-lg": ["13px", {"lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700"}],
"body-md": ["15px", {"lineHeight": "22px", "fontWeight": "400"}],
"headline-lg": ["28px", {"lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800"}],
"display": ["34px", {"lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800"}],
"headline-md": ["22px", {"lineHeight": "28px", "fontWeight": "700"}]
}
}
}
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.icon-fill {
font-variation-settings: 'FILL' 1;
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-background text-on-background antialiased h-screen w-screen overflow-hidden flex justify-center items-center">
<!-- Mobile Frame Simulation -->
<div class="relative w-full h-full max-w-[430px] max-h-[932px] bg-black md:rounded-[48px] md:border-[8px] md:border-black overflow-hidden shadow-2xl">
<!-- Background (Blurred Camera Feed) -->
<div class="absolute inset-0 w-full h-full">
<img alt="Blurred plant background" class="w-full h-full object-cover blur-md scale-105" data-alt="A slightly blurred view of a lush, vibrant Monstera Deliciosa plant sitting in a modern terracotta pot indoors. The lighting is bright and natural, streaming from a nearby window, creating soft shadows. The overall mood is calm and botanical, fitting a premium plant care app. The focus is soft to act as a background canvas." src="https://lh3.googleusercontent.com/aida-public/AB6AXuA6czRRINu7xjGgvdD-yoc0YmUs2v89tYRPsgr4-uCdRpBWm0bqnfhTbV5KIx_KIm8yGs1AXeEQMoCV1dBUioReXPHv2-Jprro-G33clj1-lW3XLQgYg4ax6Y5Lsy_BJCVYM4uJ1kPHEi6OqGBzjV9M_jfJ3-bazQ2sIdUNJACfgRKoWOjibazJxke5zbQ9-TqG5UUJWRYzUR3UxpgCI0kqUjudZZvHlLPGuUekVZ2XwfdIrGxCip4b3roPP-1ZCFIJSdq1P06eRp8"/>
<!-- Dark Overlay for better contrast with the bottom sheet -->
<div class="absolute inset-0 bg-black/40"></div>
</div>
<!-- Top Status Bar Area (Safe Area) -->
<div class="absolute top-0 w-full h-12 z-20 flex justify-between items-center px-6 text-white">
<span class="font-label-md text-label-md">9:41</span>
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-[16px]">signal_cellular_4_bar</span>
<span class="material-symbols-outlined text-[16px]">wifi</span>
<span class="material-symbols-outlined text-[16px]">battery_full</span>
</div>
</div>
<!-- Bottom Sheet Modal -->
<div class="absolute bottom-0 w-full bg-surface-container-lowest rounded-t-[32px] shadow-[0px_-8px_40px_rgba(0,0,0,0.15)] flex flex-col items-center pt-4 pb-10 px-gutter transform transition-transform duration-300 translate-y-0">
<!-- Drag Handle -->
<div class="w-12 h-1.5 bg-surface-variant rounded-full mb-8"></div>
<!-- Zero Scans Icon -->
<div class="relative mb-6">
<div class="w-20 h-20 bg-secondary-container rounded-full flex items-center justify-center">
<span class="material-symbols-outlined text-4xl text-on-secondary-container icon-fill">eco</span>
</div>
<!-- Badge -->
<div class="absolute -top-1 -right-1 w-8 h-8 bg-error text-on-error rounded-full flex items-center justify-center font-headline-md text-headline-md shadow-sm border-2 border-surface-container-lowest">
0
</div>
</div>
<!-- Text Content -->
<div class="text-center mb-8 px-4">
<h2 class="font-headline-lg text-headline-lg text-on-surface mb-3">You're out of free scans</h2>
<p class="font-body-md text-body-md text-on-surface-variant">Your 3 free scans renew on August 1. Upgrade to Pro for unlimited scanning.</p>
</div>
<!-- Primary Action -->
<button class="w-full h-[56px] bg-primary-container text-on-primary-container font-label-lg text-label-lg rounded-xl mb-4 hover:opacity-90 active:scale-[0.98] transition-all flex items-center justify-center gap-2">
<span class="material-symbols-outlined text-xl">workspace_premium</span>
See Pro Plans
</button>
<!-- Credit Top-ups -->
<div class="w-full mb-6">
<p class="font-label-md text-label-md text-outline mb-3 text-center uppercase tracking-wider">Or buy single credits</p>
<div class="flex justify-between gap-3">
<button class="flex-1 py-3 px-2 bg-surface-container-low border border-outline-variant rounded-xl flex flex-col items-center justify-center hover:bg-surface-container transition-colors">
<span class="font-headline-md text-headline-md text-primary">+30</span>
<span class="font-label-md text-label-md text-on-surface-variant">credits</span>
</button>
<button class="flex-1 py-3 px-2 bg-surface-container-low border border-outline-variant rounded-xl flex flex-col items-center justify-center hover:bg-surface-container transition-colors">
<span class="font-headline-md text-headline-md text-primary">+100</span>
<span class="font-label-md text-label-md text-on-surface-variant">credits</span>
</button>
<button class="flex-1 py-3 px-2 bg-surface-container-low border border-outline-variant rounded-xl flex flex-col items-center justify-center hover:bg-surface-container transition-colors relative overflow-hidden">
<!-- Best Value Badge -->
<div class="absolute top-0 w-full bg-secondary text-on-secondary text-[10px] font-bold py-0.5 text-center">BEST</div>
<span class="font-headline-md text-headline-md text-primary mt-3">+250</span>
<span class="font-label-md text-label-md text-on-surface-variant">credits</span>
</button>
</div>
</div>
<!-- Secondary Action -->
<button class="font-label-lg text-label-lg text-primary hover:text-primary-container transition-colors py-2 px-4">
Maybe later
</button>
</div>
<!-- Bottom Home Indicator Area -->
<div class="absolute bottom-2 w-full flex justify-center z-50">
<div class="w-32 h-1 bg-on-surface rounded-full opacity-50"></div>
</div>
</div>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

View File

@@ -0,0 +1,245 @@
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>Out of Credits</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&amp;family=Manrope:wght@400;500;600&amp;family=JetBrains+Mono:wght@500&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-primary-fixed-variant": "#2e4f00",
"inverse-primary": "#41690c",
"surface": "#0c160d",
"on-tertiary-fixed": "#171d15",
"primary-container": "#a6d66f",
"on-error-container": "#ffdad6",
"surface-dim": "#0c160d",
"surface-container-lowest": "#071008",
"secondary-fixed-dim": "#b7ccb5",
"secondary": "#b7ccb5",
"secondary-container": "#394b39",
"on-surface": "#dae6d6",
"primary-fixed": "#c0f287",
"primary": "#c1f388",
"on-primary": "#1e3700",
"surface-bright": "#313c31",
"tertiary-fixed": "#dee5d6",
"error-container": "#93000a",
"background": "#0c160d",
"on-tertiary-fixed-variant": "#42493e",
"on-tertiary": "#2c3229",
"on-surface-variant": "#c3c9b6",
"inverse-surface": "#dae6d6",
"on-secondary": "#233424",
"on-primary-container": "#365d00",
"inverse-on-surface": "#283328",
"surface-variant": "#2d372d",
"surface-container-low": "#141e14",
"surface-container-highest": "#2d372d",
"surface-container-high": "#222d22",
"tertiary-fixed-dim": "#c2c9bb",
"tertiary-container": "#c3cabc",
"outline": "#8d9382",
"on-background": "#dae6d6",
"on-secondary-fixed": "#0f1f10",
"on-error": "#690005",
"secondary-fixed": "#d3e8d0",
"outline-variant": "#43493a",
"error": "#ffb4ab",
"on-secondary-fixed-variant": "#394b39",
"on-tertiary-container": "#4e554a",
"surface-tint": "#a5d56e",
"tertiary": "#dfe6d8",
"surface-container": "#182218",
"on-secondary-container": "#a6bba4",
"primary-fixed-dim": "#a5d56e",
"on-primary-fixed": "#0f2000"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px",
"modal": "24px"
},
"spacing": {
"unit": "8px",
"gutter": "24px",
"margin-mobile": "16px",
"margin-desktop": "64px",
"container-max": "1280px"
},
"fontFamily": {
"display": [
"Epilogue"
],
"headline-md": [
"Epilogue"
],
"body-lg": [
"Manrope"
],
"headline-lg": [
"Epilogue"
],
"label-sm": [
"JetBrains Mono"
],
"headline-lg-mobile": [
"Epilogue"
],
"body-md": [
"Manrope"
]
},
"fontSize": {
"display": [
"64px",
{
"lineHeight": "1.1",
"letterSpacing": "-0.02em",
"fontWeight": "700"
}
],
"headline-md": [
"28px",
{
"lineHeight": "1.3",
"fontWeight": "500"
}
],
"body-lg": [
"18px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
],
"headline-lg": [
"40px",
{
"lineHeight": "1.2",
"letterSpacing": "-0.01em",
"fontWeight": "600"
}
],
"label-sm": [
"12px",
{
"lineHeight": "1.0",
"letterSpacing": "0.05em",
"fontWeight": "500"
}
],
"headline-lg-mobile": [
"32px",
{
"lineHeight": "1.2",
"fontWeight": "600"
}
],
"body-md": [
"16px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
]
}
},
},
}
</script>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
.icon-fill {
font-variation-settings: 'FILL' 1;
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-background text-on-background antialiased h-screen w-screen overflow-hidden flex justify-center items-center">
<!-- Mobile Frame Simulation -->
<div class="relative w-full h-full max-w-[430px] max-h-[932px] bg-black md:rounded-[48px] md:border-[8px] md:border-black overflow-hidden shadow-2xl">
<!-- Background (Blurred Camera Feed) -->
<div class="absolute inset-0 w-full h-full">
<img alt="Blurred plant background" class="w-full h-full object-cover blur-md scale-105" data-alt="A slightly blurred view of a lush, vibrant Monstera Deliciosa plant sitting in a modern terracotta pot indoors. The lighting is bright and natural, streaming from a nearby window, creating soft shadows. The overall mood is calm and botanical, fitting a premium plant care app. The focus is soft to act as a background canvas." src="https://lh3.googleusercontent.com/aida-public/AB6AXuA6czRRINu7xjGgvdD-yoc0YmUs2v89tYRPsgr4-uCdRpBWm0bqnfhTbV5KIx_KIm8yGs1AXeEQMoCV1dBUioReXPHv2-Jprro-G33clj1-lW3XLQgYg4ax6Y5Lsy_BJCVYM4uJ1kPHEi6OqGBzjV9M_jfJ3-bazQ2sIdUNJACfgRKoWOjibazJxke5zbQ9-TqG5UUJWRYzUR3UxpgCI0kqUjudZZvHlLPGuUekVZ2XwfdIrGxCip4b3roPP-1ZCFIJSdq1P06eRp8"/>
<!-- Dark Overlay for better contrast with the bottom sheet -->
<div class="absolute inset-0 bg-black/60"></div>
</div>
<!-- Top Status Bar Area (Safe Area) -->
<div class="absolute top-0 w-full h-12 z-20 flex justify-between items-center px-6 text-white">
<span class="font-label-sm text-label-sm">9:41</span>
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-[16px]">signal_cellular_4_bar</span>
<span class="material-symbols-outlined text-[16px]">wifi</span>
<span class="material-symbols-outlined text-[16px]">battery_full</span>
</div>
</div>
<!-- Bottom Sheet Modal -->
<div class="absolute bottom-0 w-full bg-surface-container-lowest rounded-t-[32px] shadow-[0px_-8px_40px_rgba(0,0,0,0.15)] flex flex-col items-center pt-4 pb-10 px-gutter transform transition-transform duration-300 translate-y-0">
<!-- Drag Handle -->
<div class="w-12 h-1.5 bg-surface-variant rounded-full mb-8"></div>
<!-- Zero Scans Icon -->
<div class="relative mb-6">
<div class="w-20 h-20 bg-secondary-container rounded-full flex items-center justify-center">
<span class="material-symbols-outlined text-4xl text-on-secondary-container icon-fill">eco</span>
</div>
<!-- Badge -->
<div class="absolute -top-1 -right-1 w-8 h-8 bg-error text-on-error rounded-full flex items-center justify-center font-headline-md text-headline-md shadow-sm border-2 border-surface-container-lowest">
0
</div>
</div>
<!-- Text Content -->
<div class="text-center mb-8 px-4">
<h2 class="font-headline-lg text-headline-lg text-on-surface mb-3">You're out of free scans</h2>
<p class="font-body-md text-body-md text-on-surface-variant">Your 3 free scans renew on August 1. Upgrade to Pro for unlimited scanning.</p>
</div>
<!-- Primary Action -->
<button class="w-full h-[56px] bg-primary-container text-on-primary-container font-body-md text-body-md font-bold rounded-xl mb-4 hover:opacity-90 active:scale-[0.98] transition-all flex items-center justify-center gap-2">
<span class="material-symbols-outlined text-xl">workspace_premium</span>
See Pro Plans
</button>
<!-- Credit Top-ups -->
<div class="w-full mb-6">
<p class="font-label-sm text-label-sm text-outline mb-3 text-center uppercase tracking-wider">Or buy single credits</p>
<div class="flex justify-between gap-3">
<button class="flex-1 py-3 px-2 bg-surface-container-low border border-outline-variant rounded-xl flex flex-col items-center justify-center hover:bg-surface-container transition-colors">
<span class="font-headline-md text-headline-md text-primary">+30</span>
<span class="font-label-sm text-label-sm text-on-surface-variant mt-1">credits</span>
</button>
<button class="flex-1 py-3 px-2 bg-surface-container-low border border-outline-variant rounded-xl flex flex-col items-center justify-center hover:bg-surface-container transition-colors">
<span class="font-headline-md text-headline-md text-primary">+100</span>
<span class="font-label-sm text-label-sm text-on-surface-variant mt-1">credits</span>
</button>
<button class="flex-1 py-3 px-2 bg-surface-container-low border border-outline-variant rounded-xl flex flex-col items-center justify-center hover:bg-surface-container transition-colors relative overflow-hidden">
<!-- Best Value Badge -->
<div class="absolute top-0 w-full bg-secondary text-on-secondary text-[10px] font-bold py-0.5 text-center">BEST</div>
<span class="font-headline-md text-headline-md text-primary mt-3">+250</span>
<span class="font-label-sm text-label-sm text-on-surface-variant mt-1">credits</span>
</button>
</div>
</div>
<!-- Secondary Action -->
<button class="font-body-md text-body-md font-bold text-primary hover:text-primary-container transition-colors py-2 px-4">
Maybe later
</button>
</div>
<!-- Bottom Home Indicator Area -->
<div class="absolute bottom-2 w-full flex justify-center z-50">
<div class="w-32 h-1 bg-on-surface rounded-full opacity-50"></div>
</div>
</div>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

View File

@@ -0,0 +1,227 @@
<!DOCTYPE html>
<html class="h-full" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"/>
<title>GreenLens - Onboarding Plant Count</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com" rel="preconnect"/>
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary": "#375d00",
"on-surface": "#191d16",
"on-secondary-fixed": "#012104",
"on-secondary-fixed-variant": "#2d4e2b",
"surface-container-lowest": "#ffffff",
"surface-container-low": "#f2f5e9",
"error-container": "#ffdad6",
"secondary": "#446741",
"outline": "#72796a",
"on-secondary": "#ffffff",
"surface-container": "#ecefe4",
"on-error": "#ffffff",
"on-surface-variant": "#42493c",
"background": "#f8fbef",
"error": "#ba1a1a",
"outline-variant": "#c2c9b7",
"on-secondary-container": "#486b45",
"secondary-fixed": "#c5edbd",
"on-tertiary": "#ffffff",
"primary": "#2b5f0b",
"surface-dim": "#d8dbd0",
"primary-container": "#437824",
"tertiary-fixed-dim": "#a5d56e",
"inverse-on-surface": "#eff2e7",
"surface-container-high": "#e7e9de",
"surface-bright": "#f8fbef",
"inverse-surface": "#2e322a",
"on-background": "#191d16",
"on-primary-fixed": "#092100",
"secondary-container": "#c2eaba",
"on-tertiary-container": "#cbfd91",
"primary-fixed": "#b6f390",
"tertiary-container": "#4d771b",
"primary-fixed-dim": "#9bd776",
"tertiary-fixed": "#c0f287",
"surface-tint": "#366a17",
"secondary-fixed-dim": "#aad1a2",
"surface-variant": "#e1e4d9",
"on-error-container": "#93000a",
"on-tertiary-fixed-variant": "#2e4f00",
"on-primary": "#ffffff",
"on-primary-container": "#c1ff99",
"surface-container-highest": "#e1e4d9",
"on-tertiary-fixed": "#0f2000",
"on-primary-fixed-variant": "#205100",
"inverse-primary": "#9bd776",
"surface": "#f8fbef"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"spacing": {
"xs": "8px",
"xl": "32px",
"base": "4px",
"sm": "12px",
"gutter": "16px",
"md": "16px",
"lg": "24px",
"container-margin": "20px"
},
"fontFamily": {
"label-md": ["Plus Jakarta Sans"],
"body-lg": ["Plus Jakarta Sans"],
"label-lg": ["Plus Jakarta Sans"],
"body-md": ["Plus Jakarta Sans"],
"headline-lg": ["Plus Jakarta Sans"],
"display": ["Plus Jakarta Sans"],
"headline-md": ["Plus Jakarta Sans"]
},
"fontSize": {
"label-md": ["12px", {"lineHeight": "16px", "fontWeight": "600"}],
"body-lg": ["17px", {"lineHeight": "26px", "fontWeight": "500"}],
"label-lg": ["13px", {"lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700"}],
"body-md": ["15px", {"lineHeight": "22px", "fontWeight": "400"}],
"headline-lg": ["28px", {"lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800"}],
"display": ["34px", {"lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800"}],
"headline-md": ["22px", {"lineHeight": "28px", "fontWeight": "700"}]
}
}
}
}
</script>
<style>
body {
background-color: #FBFAF3; /* Enforced from style guidance level 0 */
-webkit-tap-highlight-color: transparent;
}
/* Custom styles for precise matching */
.card-shadow {
box-shadow: 0px 4px 20px rgba(16, 28, 18, 0.06);
}
.progress-track {
background-color: #E6EADF;
}
.progress-fill {
background-color: #A6D66F;
width: 40%;
}
.option-card {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.option-card:active {
transform: scale(0.98);
}
.selected-card {
border-color: #437824;
background-color: rgba(166, 214, 111, 0.15); /* Tint derived from #A6D66F */
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="h-full flex justify-center bg-[#FBFAF3] font-body-md text-on-surface antialiased">
<!-- iPhone 15 Frame Simulator -->
<div class="relative w-full max-w-[393px] h-[852px] bg-[#FBFAF3] sm:rounded-[3rem] sm:shadow-2xl sm:overflow-hidden sm:my-8 flex flex-col">
<!-- Status Bar Area (Safe Area Top) -->
<div class="h-12 w-full shrink-0"></div>
<!-- Top Navigation Area -->
<div class="px-container-margin pt-2 pb-4 flex items-center shrink-0 relative">
<button aria-label="Go back" class="w-10 h-10 flex items-center justify-center rounded-full bg-surface-container-lowest card-shadow active:scale-95 transition-transform text-primary absolute left-container-margin">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">arrow_back</span>
</button>
<div class="w-full flex justify-center">
<div class="w-24 h-2 rounded-full progress-track overflow-hidden">
<div class="h-full rounded-full progress-fill"></div>
</div>
</div>
</div>
<!-- Main Content Area -->
<main class="flex-1 flex flex-col px-container-margin overflow-y-auto pb-24 pt-4">
<!-- Header Text -->
<div class="text-center mb-xl">
<h1 class="font-headline-lg text-headline-lg text-on-surface mb-xs">
How many plants do you have?
</h1>
<p class="font-body-md text-body-md text-on-surface-variant px-sm">
This helps us personalize your care plan.
</p>
</div>
<!-- Options List -->
<div class="flex flex-col gap-sm">
<!-- Option 1 -->
<button class="option-card w-full text-left bg-surface-container-lowest card-shadow rounded-[16px] p-lg border-2 border-transparent flex items-center gap-md">
<span class="text-[28px] leading-none select-none">🌱</span>
<span class="font-body-lg text-body-lg text-on-surface">Just starting (1-3)</span>
</button>
<!-- Option 2 -->
<button class="option-card w-full text-left bg-surface-container-lowest card-shadow rounded-[16px] p-lg border-2 border-transparent flex items-center gap-md">
<span class="text-[28px] leading-none select-none">🪴</span>
<span class="font-body-lg text-body-lg text-on-surface">A few (4-10)</span>
</button>
<!-- Option 3 (Selected) -->
<button class="option-card w-full text-left card-shadow rounded-[16px] p-lg border-2 flex items-center gap-md selected-card">
<span class="text-[28px] leading-none select-none">🌿</span>
<span class="font-body-lg text-body-lg text-primary-container font-bold">Quite a jungle (11-25)</span>
</button>
<!-- Option 4 -->
<button class="option-card w-full text-left bg-surface-container-lowest card-shadow rounded-[16px] p-lg border-2 border-transparent flex items-center gap-md">
<span class="text-[28px] leading-none select-none">🌳</span>
<span class="font-body-lg text-body-lg text-on-surface">Full jungle (25+)</span>
</button>
</div>
<div class="flex-1 min-h-[32px]"></div>
<!-- Floating Decorative Element (Abstract Blur to keep it visually interesting per styling guidelines) -->
<div class="absolute bottom-32 -right-16 w-64 h-64 bg-tertiary-fixed-dim/20 rounded-full blur-[60px] pointer-events-none z-0"></div>
</main>
<!-- Bottom Action Area -->
<div class="px-container-margin pb-10 pt-4 bg-[#FBFAF3]/90 backdrop-blur-md shrink-0 relative z-10 w-full rounded-b-[3rem]">
<button class="w-full h-[56px] bg-primary-container text-on-primary rounded-[14px] font-label-lg text-label-lg active:scale-98 transition-transform shadow-[0px_8px_16px_rgba(67,120,36,0.2)] flex items-center justify-center">
Continue
</button>
</div>
</div>
<script>
// Simple JS for interaction demonstration
document.querySelectorAll('.option-card').forEach(card => {
card.addEventListener('click', function() {
// Remove selected class from all
document.querySelectorAll('.option-card').forEach(c => {
c.classList.remove('selected-card');
c.classList.add('bg-surface-container-lowest', 'border-transparent');
c.querySelector('span:last-child').classList.remove('text-primary-container', 'font-bold');
c.querySelector('span:last-child').classList.add('text-on-surface');
});
// Add to clicked
this.classList.add('selected-card');
this.classList.remove('bg-surface-container-lowest', 'border-transparent');
let textSpan = this.querySelector('span:last-child');
textSpan.classList.remove('text-on-surface');
textSpan.classList.add('text-primary-container', 'font-bold');
});
});
</script>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 KiB

View File

@@ -0,0 +1,291 @@
<!DOCTYPE html>
<html class="h-full dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" name="viewport"/>
<title>GreenLens - Onboarding Plant Count</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com" rel="preconnect"/>
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&amp;family=JetBrains+Mono:wght@500&amp;family=Manrope:wght@400;500;600&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-primary-fixed-variant": "#2e4f00",
"inverse-primary": "#41690c",
"surface": "#0c160d",
"on-tertiary-fixed": "#171d15",
"primary-container": "#a6d66f",
"on-error-container": "#ffdad6",
"surface-dim": "#0c160d",
"surface-container-lowest": "#071008",
"secondary-fixed-dim": "#b7ccb5",
"secondary": "#b7ccb5",
"secondary-container": "#394b39",
"on-surface": "#dae6d6",
"primary-fixed": "#c0f287",
"primary": "#c1f388",
"on-primary": "#1e3700",
"surface-bright": "#313c31",
"tertiary-fixed": "#dee5d6",
"error-container": "#93000a",
"background": "#0c160d",
"on-tertiary-fixed-variant": "#42493e",
"on-tertiary": "#2c3229",
"on-surface-variant": "#c3c9b6",
"inverse-surface": "#dae6d6",
"on-secondary": "#233424",
"on-primary-container": "#365d00",
"inverse-on-surface": "#283328",
"surface-variant": "#2d372d",
"surface-container-low": "#141e14",
"surface-container-highest": "#2d372d",
"surface-container-high": "#222d22",
"tertiary-fixed-dim": "#c2c9bb",
"tertiary-container": "#c3cabc",
"outline": "#8d9382",
"on-background": "#dae6d6",
"on-secondary-fixed": "#0f1f10",
"on-error": "#690005",
"secondary-fixed": "#d3e8d0",
"outline-variant": "#43493a",
"error": "#ffb4ab",
"on-secondary-fixed-variant": "#394b39",
"on-tertiary-container": "#4e554a",
"surface-tint": "#a5d56e",
"tertiary": "#dfe6d8",
"surface-container": "#182218",
"on-secondary-container": "#a6bba4",
"primary-fixed-dim": "#a5d56e",
"on-primary-fixed": "#0f2000"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"spacing": {
"unit": "8px",
"gutter": "24px",
"margin-mobile": "16px",
"margin-desktop": "64px",
"container-max": "1280px",
"xs": "8px",
"xl": "32px",
"base": "4px",
"sm": "12px",
"md": "16px",
"lg": "24px",
"container-margin": "20px"
},
"fontFamily": {
"display": [
"Epilogue"
],
"headline-md": [
"Epilogue"
],
"body-lg": [
"Manrope"
],
"headline-lg": [
"Epilogue"
],
"label-sm": [
"JetBrains Mono"
],
"headline-lg-mobile": [
"Epilogue"
],
"body-md": [
"Manrope"
],
"label-lg": ["JetBrains Mono"]
},
"fontSize": {
"display": [
"64px",
{
"lineHeight": "1.1",
"letterSpacing": "-0.02em",
"fontWeight": "700"
}
],
"headline-md": [
"28px",
{
"lineHeight": "1.3",
"fontWeight": "500"
}
],
"body-lg": [
"18px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
],
"headline-lg": [
"40px",
{
"lineHeight": "1.2",
"letterSpacing": "-0.01em",
"fontWeight": "600"
}
],
"label-sm": [
"12px",
{
"lineHeight": "1.0",
"letterSpacing": "0.05em",
"fontWeight": "500"
}
],
"headline-lg-mobile": [
"32px",
{
"lineHeight": "1.2",
"fontWeight": "600"
}
],
"body-md": [
"16px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
],
"label-lg": ["14px", {"lineHeight": "1.4", "fontWeight": "600"}]
}
},
},
}
</script>
<style>
body {
background-color: #0c160d; /* Enforced dark mode background */
-webkit-tap-highlight-color: transparent;
}
/* Custom styles for precise matching */
.card-shadow {
box-shadow: 0px 4px 20px rgba(0, 0, 0, 0.4);
}
.progress-track {
background-color: #222d22;
}
.progress-fill {
background-color: #c1f388;
width: 40%;
}
.option-card {
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.option-card:active {
transform: scale(0.98);
}
.selected-card {
border-color: #c1f388;
background-color: rgba(193, 243, 136, 0.15); /* Tint derived from #c1f388 */
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="h-full flex justify-center bg-background font-body-md text-on-surface antialiased">
<!-- iPhone 15 Frame Simulator -->
<div class="relative w-full max-w-[393px] h-[852px] bg-background sm:rounded-[3rem] sm:shadow-2xl sm:overflow-hidden sm:my-8 flex flex-col">
<!-- Status Bar Area (Safe Area Top) -->
<div class="h-12 w-full shrink-0"></div>
<!-- Top Navigation Area -->
<div class="px-container-margin pt-2 pb-4 flex items-center shrink-0 relative">
<button aria-label="Go back" class="w-10 h-10 flex items-center justify-center rounded-full bg-surface-container card-shadow active:scale-95 transition-transform text-primary absolute left-container-margin">
<span class="material-symbols-outlined" style="font-variation-settings: 'FILL' 0;">arrow_back</span>
</button>
<div class="w-full flex justify-center">
<div class="w-24 h-2 rounded-full progress-track overflow-hidden">
<div class="h-full rounded-full progress-fill"></div>
</div>
</div>
</div>
<!-- Main Content Area -->
<main class="flex-1 flex flex-col px-container-margin overflow-y-auto pb-24 pt-4">
<!-- Header Text -->
<div class="text-center mb-xl">
<h1 class="font-headline-lg-mobile text-headline-lg-mobile text-on-surface mb-xs">
How many plants do you have?
</h1>
<p class="font-body-md text-body-md text-on-surface-variant px-sm">
This helps us personalize your care plan.
</p>
</div>
<!-- Options List -->
<div class="flex flex-col gap-sm">
<!-- Option 1 -->
<button class="option-card w-full text-left bg-surface-container card-shadow rounded-[16px] p-lg border-2 border-transparent flex items-center gap-md">
<span class="text-[28px] leading-none select-none">🌱</span>
<span class="font-body-lg text-body-lg text-on-surface">Just starting (1-3)</span>
</button>
<!-- Option 2 -->
<button class="option-card w-full text-left bg-surface-container card-shadow rounded-[16px] p-lg border-2 border-transparent flex items-center gap-md">
<span class="text-[28px] leading-none select-none">🪴</span>
<span class="font-body-lg text-body-lg text-on-surface">A few (4-10)</span>
</button>
<!-- Option 3 (Selected) -->
<button class="option-card w-full text-left card-shadow rounded-[16px] p-lg border-2 flex items-center gap-md selected-card">
<span class="text-[28px] leading-none select-none">🌿</span>
<span class="font-body-lg text-body-lg text-primary font-bold">Quite a jungle (11-25)</span>
</button>
<!-- Option 4 -->
<button class="option-card w-full text-left bg-surface-container card-shadow rounded-[16px] p-lg border-2 border-transparent flex items-center gap-md">
<span class="text-[28px] leading-none select-none">🌳</span>
<span class="font-body-lg text-body-lg text-on-surface">Full jungle (25+)</span>
</button>
</div>
<div class="flex-1 min-h-[32px]"></div>
<!-- Floating Decorative Element (Abstract Blur to keep it visually interesting per styling guidelines) -->
<div class="absolute bottom-32 -right-16 w-64 h-64 bg-primary/10 rounded-full blur-[60px] pointer-events-none z-0"></div>
</main>
<!-- Bottom Action Area -->
<div class="px-container-margin pb-10 pt-4 bg-background/90 backdrop-blur-md shrink-0 relative z-10 w-full rounded-b-[3rem]">
<button class="w-full h-[56px] bg-primary text-on-primary rounded-[14px] font-label-lg text-label-lg active:scale-98 transition-transform shadow-[0px_8px_16px_rgba(193,243,136,0.1)] flex items-center justify-center">
Continue
</button>
</div>
</div>
<script>
// Simple JS for interaction demonstration
document.querySelectorAll('.option-card').forEach(card => {
card.addEventListener('click', function() {
// Remove selected class from all
document.querySelectorAll('.option-card').forEach(c => {
c.classList.remove('selected-card');
c.classList.add('bg-surface-container', 'border-transparent');
c.querySelector('span:last-child').classList.remove('text-primary', 'font-bold');
c.querySelector('span:last-child').classList.add('text-on-surface');
});
// Add to clicked
this.classList.add('selected-card');
this.classList.remove('bg-surface-container', 'border-transparent');
let textSpan = this.querySelector('span:last-child');
textSpan.classList.remove('text-on-surface');
textSpan.classList.add('text-primary', 'font-bold');
});
});
</script>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

View File

@@ -0,0 +1,213 @@
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>GreenLens - Progress</title>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com" rel="preconnect"/>
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary": "#375d00",
"on-surface": "#191d16",
"on-secondary-fixed": "#012104",
"on-secondary-fixed-variant": "#2d4e2b",
"surface-container-lowest": "#ffffff",
"surface-container-low": "#f2f5e9",
"error-container": "#ffdad6",
"secondary": "#446741",
"outline": "#72796a",
"on-secondary": "#ffffff",
"surface-container": "#ecefe4",
"on-error": "#ffffff",
"on-surface-variant": "#42493c",
"background": "#f8fbef",
"error": "#ba1a1a",
"outline-variant": "#c2c9b7",
"on-secondary-container": "#486b45",
"secondary-fixed": "#c5edbd",
"on-tertiary": "#ffffff",
"primary": "#2b5f0b",
"surface-dim": "#d8dbd0",
"primary-container": "#437824",
"tertiary-fixed-dim": "#a5d56e",
"inverse-on-surface": "#eff2e7",
"surface-container-high": "#e7e9de",
"surface-bright": "#f8fbef",
"inverse-surface": "#2e322a",
"on-background": "#191d16",
"on-primary-fixed": "#092100",
"secondary-container": "#c2eaba",
"on-tertiary-container": "#cbfd91",
"primary-fixed": "#b6f390",
"tertiary-container": "#4d771b",
"primary-fixed-dim": "#9bd776",
"tertiary-fixed": "#c0f287",
"surface-tint": "#366a17",
"secondary-fixed-dim": "#aad1a2",
"surface-variant": "#e1e4d9",
"on-error-container": "#93000a",
"on-tertiary-fixed-variant": "#2e4f00",
"on-primary": "#ffffff",
"on-primary-container": "#c1ff99",
"surface-container-highest": "#e1e4d9",
"on-tertiary-fixed": "#0f2000",
"on-primary-fixed-variant": "#205100",
"inverse-primary": "#9bd776",
"surface": "#f8fbef"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"spacing": {
"xs": "8px",
"xl": "32px",
"base": "4px",
"sm": "12px",
"gutter": "16px",
"md": "16px",
"lg": "24px",
"container-margin": "20px"
},
"fontFamily": {
"label-md": ["Plus Jakarta Sans"],
"body-lg": ["Plus Jakarta Sans"],
"label-lg": ["Plus Jakarta Sans"],
"body-md": ["Plus Jakarta Sans"],
"headline-lg": ["Plus Jakarta Sans"],
"display": ["Plus Jakarta Sans"],
"headline-md": ["Plus Jakarta Sans"]
},
"fontSize": {
"label-md": ["12px", { "lineHeight": "16px", "fontWeight": "600" }],
"body-lg": ["17px", { "lineHeight": "26px", "fontWeight": "500" }],
"label-lg": ["13px", { "lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700" }],
"body-md": ["15px", { "lineHeight": "22px", "fontWeight": "400" }],
"headline-lg": ["28px", { "lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800" }],
"display": ["48px", { "lineHeight": "56px", "letterSpacing": "-0.02em", "fontWeight": "800" }],
"headline-md": ["22px", { "lineHeight": "28px", "fontWeight": "700" }]
}
}
}
}
</script>
<style>
/* Custom animations to enhance the "Gentle Precision" feel */
@keyframes subtle-pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.02); opacity: 0.95; }
}
.animate-subtle-pulse {
animation: subtle-pulse 3s ease-in-out infinite;
}
@keyframes float-up {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.animate-float-up {
animation: float-up 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-surface text-on-surface antialiased min-h-screen flex flex-col font-sans overflow-x-hidden selection:bg-primary-container selection:text-on-primary-container">
<!-- Main Canvas Content -->
<main class="flex-1 flex flex-col px-gutter pt-16 pb-12 w-full max-w-md mx-auto relative h-screen max-h-[900px]">
<!-- Top Section: Progress Display -->
<section class="flex flex-col items-center mt-4 mb-12 animate-float-up" style="animation-delay: 0.1s;">
<div class="animate-subtle-pulse flex flex-col items-center">
<h1 class="font-display text-display text-primary-container mb-8">73%</h1>
<div class="relative w-40 h-40 flex items-center justify-center mb-8">
<!-- Progress Ring Background -->
<svg class="absolute inset-0 w-full h-full transform -rotate-90" viewbox="0 0 120 120">
<circle class="stroke-surface-variant" cx="60" cy="60" fill="none" r="54" stroke-width="6"></circle>
<!-- Progress Ring Active (73% of 339.29 circumference = 247.6) -->
<circle class="stroke-primary-container transition-all duration-[2000ms] ease-out" cx="60" cy="60" fill="none" r="54" stroke-dasharray="339.29" stroke-dashoffset="91.6" stroke-linecap="round" stroke-width="6"></circle>
</svg>
<!-- Center Image -->
<div class="w-28 h-28 rounded-full overflow-hidden shadow-[0px_4px_20px_rgba(16,28,18,0.06)] relative z-10 border-4 border-surface-container-lowest bg-surface-container-low">
<img class="w-full h-full object-cover" data-alt="A beautifully lit, highly detailed macro photograph of a fresh, healthy Monstera Deliciosa leaf node emerging. The setting is bright, featuring soft, diffused natural daylight synonymous with premium lifestyle indoor plant photography. The background is a clean, minimal cream wall softly out of focus, allowing the lush green textures to command attention." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDD8Ij49YnpXP0wrHVlraHqQPLHbq5nMP0HJHT-VhpkShra9HVYXZZa0OupKWUhBvR9N4-GIVlLrO55A41z_t3WZQBIupmEkYcnZj9OTKrDeZ5ZAwm1ONH4ZZila5Pww-Njw8Hn0oFBgAtnUZQb0VQi70qgX6xtUzNCLyIUX3OQopUcYBAfEMVKyPExSWfr1X2Kj4YWyP57xnqG5_672eJ2OzoAnrZPMDSBCLx0gqvmmBLurrxksL5O0fTM2afAEQOy-fReEwPLZCM"/>
</div>
</div>
</div>
<!-- Status Pill -->
<div class="bg-surface-variant/60 backdrop-blur-md rounded-full px-5 py-2.5 flex items-center gap-3 shadow-sm">
<span class="material-symbols-outlined text-on-surface-variant text-[18px] animate-spin" style="animation-duration: 3s;">sync</span>
<span class="font-label-lg text-label-lg text-on-surface-variant">Personalizing your care plan…</span>
</div>
</section>
<!-- Middle Section: Vertical Checklist -->
<section class="flex-1 w-full max-w-[280px] mx-auto flex flex-col justify-center gap-5 animate-float-up" style="animation-delay: 0.3s;">
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-primary-container text-[24px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
<span class="font-body-md text-body-md text-on-surface">Analyzing your answers</span>
</div>
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-primary-container text-[24px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
<span class="font-body-md text-body-md text-on-surface">Building your care plan</span>
</div>
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-primary-container text-[24px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
<span class="font-body-md text-body-md text-on-surface">Preparing your scan credits</span>
</div>
<div class="flex items-center gap-4">
<div class="relative flex items-center justify-center w-6 h-6">
<span class="material-symbols-outlined text-outline-variant text-[24px] absolute">radio_button_unchecked</span>
<span class="w-1.5 h-1.5 bg-outline-variant rounded-full animate-pulse"></span>
</div>
<span class="font-body-md text-body-md text-on-surface-variant opacity-80">Finalizing your plan</span>
</div>
</section>
<!-- Bottom Section: Social Proof & Badge -->
<section class="mt-auto flex flex-col items-center animate-float-up w-full" style="animation-delay: 0.5s;">
<!-- Overlapping Testimonial Cards -->
<div class="relative w-full h-[140px] mb-6">
<!-- Background Card (Muted) -->
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-[90%] bg-surface-container-highest rounded-xl h-24 shadow-[0px_4px_20px_rgba(16,28,18,0.06)] transform -translate-y-3 scale-95 opacity-50 z-0"></div>
<!-- Foreground Card (Active) -->
<div class="absolute top-0 left-0 w-full bg-surface-container-lowest rounded-xl shadow-[0px_8px_30px_rgba(16,28,18,0.08)] p-4 flex flex-col gap-2 z-10 border border-surface-variant/50">
<div class="flex items-center gap-3 mb-1">
<img class="w-10 h-10 rounded-full object-cover bg-surface-variant" data-alt="A bright, professional headshot of a smiling woman in her early thirties, bathed in warm, soft natural sunlight. The aesthetic is clean, approachable, and highly polished, fitting a premium digital service. She is framed intimately against a gently blurred background of subtle green foliage." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCwP_g6-fpeohdJjnhG1R_yA-RypkiO6-NENOC640_fORV6ygSF9wlJvjXnQwnUzWnNyLN3AnOErHziNl_el3scfiHFQ0MjcJbOruxYdE0bhxkrPIGHNrH4Dl0nS0WxfeiNPSSiZpuSMM4D-0ZnTZRd5hR6-DnM04dT_mc3beAjFEYeedIrFrSKhi5IsZvK8gTu0x6k5UN627ijnpIaO3WBSeJ1waNUMaZ2OD_tj2pbPohgmE92SunvsOw8qVTP8zPPm79KJgIuniY"/>
<div class="flex flex-col">
<span class="font-label-lg text-label-lg text-on-surface flex items-center gap-1">Elena R. <span class="text-lg">🇺🇸</span></span>
<div class="flex text-tertiary-fixed-dim">
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
</div>
</div>
</div>
<p class="font-body-md text-body-md text-on-surface-variant leading-relaxed line-clamp-2 italic">
"GreenLens completely saved my Fiddle Leaf Fig. The daily routines feel incredibly precise and easy."
</p>
</div>
</div>
<!-- Store Badge -->
<div class="flex items-center justify-center gap-2 text-primary-container bg-surface-container-low px-4 py-2 rounded-full border border-primary-container/20">
<span class="material-symbols-outlined text-[20px]" style="font-variation-settings: 'FILL' 1;">workspace_premium</span>
<span class="font-label-lg text-label-lg uppercase tracking-widest text-[11px]">4.8 App Store Rating</span>
</div>
</section>
</main>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

View File

@@ -0,0 +1,275 @@
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>GreenLens - Progress</title>
<!-- Material Symbols -->
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<!-- Google Fonts -->
<link href="https://fonts.googleapis.com" rel="preconnect"/>
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect"/>
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&amp;family=JetBrains+Mono:wght@500&amp;family=Manrope:wght@400&amp;display=swap" rel="stylesheet"/>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-primary-fixed-variant": "#2e4f00",
"inverse-primary": "#41690c",
"surface": "#0c160d",
"on-tertiary-fixed": "#171d15",
"primary-container": "#a6d66f",
"on-error-container": "#ffdad6",
"surface-dim": "#0c160d",
"surface-container-lowest": "#071008",
"secondary-fixed-dim": "#b7ccb5",
"secondary": "#b7ccb5",
"secondary-container": "#394b39",
"on-surface": "#dae6d6",
"primary-fixed": "#c0f287",
"primary": "#c1f388",
"on-primary": "#1e3700",
"surface-bright": "#313c31",
"tertiary-fixed": "#dee5d6",
"error-container": "#93000a",
"background": "#0c160d",
"on-tertiary-fixed-variant": "#42493e",
"on-tertiary": "#2c3229",
"on-surface-variant": "#c3c9b6",
"inverse-surface": "#dae6d6",
"on-secondary": "#233424",
"on-primary-container": "#365d00",
"inverse-on-surface": "#283328",
"surface-variant": "#2d372d",
"surface-container-low": "#141e14",
"surface-container-highest": "#2d372d",
"surface-container-high": "#222d22",
"tertiary-fixed-dim": "#c2c9bb",
"tertiary-container": "#c3cabc",
"outline": "#8d9382",
"on-background": "#dae6d6",
"on-secondary-fixed": "#0f1f10",
"on-error": "#690005",
"secondary-fixed": "#d3e8d0",
"outline-variant": "#43493a",
"error": "#ffb4ab",
"on-secondary-fixed-variant": "#394b39",
"on-tertiary-container": "#4e554a",
"surface-tint": "#a5d56e",
"tertiary": "#dfe6d8",
"surface-container": "#182218",
"on-secondary-container": "#a6bba4",
"primary-fixed-dim": "#a5d56e",
"on-primary-fixed": "#0f2000"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"spacing": {
"unit": "8px",
"gutter": "24px",
"margin-mobile": "16px",
"margin-desktop": "64px",
"container-max": "1280px",
"xs": "8px",
"xl": "32px",
"base": "4px",
"sm": "12px",
"md": "16px",
"lg": "24px",
"container-margin": "20px"
},
"fontFamily": {
"display": [
"Epilogue"
],
"headline-md": [
"Epilogue"
],
"body-lg": [
"Manrope"
],
"headline-lg": [
"Epilogue"
],
"label-sm": [
"JetBrains Mono"
],
"headline-lg-mobile": [
"Epilogue"
],
"body-md": [
"Manrope"
]
},
"fontSize": {
"display": [
"64px",
{
"lineHeight": "1.1",
"letterSpacing": "-0.02em",
"fontWeight": "700"
}
],
"headline-md": [
"28px",
{
"lineHeight": "1.3",
"fontWeight": "500"
}
],
"body-lg": [
"18px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
],
"headline-lg": [
"40px",
{
"lineHeight": "1.2",
"letterSpacing": "-0.01em",
"fontWeight": "600"
}
],
"label-sm": [
"12px",
{
"lineHeight": "1.0",
"letterSpacing": "0.05em",
"fontWeight": "500"
}
],
"headline-lg-mobile": [
"32px",
{
"lineHeight": "1.2",
"fontWeight": "600"
}
],
"body-md": [
"16px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
]
}
},
},
}
</script>
<style>
/* Custom animations to enhance the "Gentle Precision" feel */
@keyframes subtle-pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.02); opacity: 0.95; }
}
.animate-subtle-pulse {
animation: subtle-pulse 3s ease-in-out infinite;
}
@keyframes float-up {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.animate-float-up {
animation: float-up 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-surface text-on-surface antialiased min-h-screen flex flex-col font-sans overflow-x-hidden selection:bg-primary selection:text-on-primary">
<!-- Main Canvas Content -->
<main class="flex-1 flex flex-col px-gutter pt-16 pb-12 w-full max-w-md mx-auto relative h-screen max-h-[900px]">
<!-- Top Section: Progress Display -->
<section class="flex flex-col items-center mt-4 mb-12 animate-float-up" style="animation-delay: 0.1s;">
<div class="animate-subtle-pulse flex flex-col items-center">
<h1 class="font-display text-display text-primary mb-8">73%</h1>
<div class="relative w-40 h-40 flex items-center justify-center mb-8">
<!-- Progress Ring Background -->
<svg class="absolute inset-0 w-full h-full transform -rotate-90" viewbox="0 0 120 120">
<circle class="stroke-surface-variant" cx="60" cy="60" fill="none" r="54" stroke-width="6"></circle>
<!-- Progress Ring Active (73% of 339.29 circumference = 247.6) -->
<circle class="stroke-primary transition-all duration-[2000ms] ease-out" cx="60" cy="60" fill="none" r="54" stroke-dasharray="339.29" stroke-dashoffset="91.6" stroke-linecap="round" stroke-width="6"></circle>
</svg>
<!-- Center Image -->
<div class="w-28 h-28 rounded-full overflow-hidden shadow-[0px_4px_20px_rgba(16,28,18,0.06)] relative z-10 border-4 border-surface-container-lowest bg-surface-container-low">
<img class="w-full h-full object-cover" data-alt="A beautifully lit, highly detailed macro photograph of a fresh, healthy Monstera Deliciosa leaf node emerging. The setting is bright, featuring soft, diffused natural daylight synonymous with premium lifestyle indoor plant photography. The background is a clean, minimal cream wall softly out of focus, allowing the lush green textures to command attention." src="https://lh3.googleusercontent.com/aida-public/AB6AXuDD8Ij49YnpXP0wrHVlraHqQPLHbq5nMP0HJHT-VhpkShra9HVYXZZa0OupKWUhBvR9N4-GIVlLrO55A41z_t3WZQBIupmEkYcnZj9OTKrDeZ5ZAwm1ONH4ZZila5Pww-Njw8Hn0oFBgAtnUZQb0VQi70qgX6xtUzNCLyIUX3OQopUcYBAfEMVKyPExSWfr1X2Kj4YWyP57xnqG5_672eJ2OzoAnrZPMDSBCLx0gqvmmBLurrxksL5O0fTM2afAEQOy-fReEwPLZCM"/>
</div>
</div>
</div>
<!-- Status Pill -->
<div class="bg-surface-variant/60 backdrop-blur-md rounded-full px-5 py-2.5 flex items-center gap-3 shadow-sm">
<span class="material-symbols-outlined text-on-surface-variant text-[18px] animate-spin" style="animation-duration: 3s;">sync</span>
<span class="font-label-sm text-label-sm text-on-surface-variant">Personalizing your care plan…</span>
</div>
</section>
<!-- Middle Section: Vertical Checklist -->
<section class="flex-1 w-full max-w-[280px] mx-auto flex flex-col justify-center gap-5 animate-float-up" style="animation-delay: 0.3s;">
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-primary text-[24px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
<span class="font-body-md text-body-md text-on-surface">Analyzing your answers</span>
</div>
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-primary text-[24px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
<span class="font-body-md text-body-md text-on-surface">Building your care plan</span>
</div>
<div class="flex items-center gap-4">
<span class="material-symbols-outlined text-primary text-[24px]" style="font-variation-settings: 'FILL' 1;">check_circle</span>
<span class="font-body-md text-body-md text-on-surface">Preparing your scan credits</span>
</div>
<div class="flex items-center gap-4">
<div class="relative flex items-center justify-center w-6 h-6">
<span class="material-symbols-outlined text-outline-variant text-[24px] absolute">radio_button_unchecked</span>
<span class="w-1.5 h-1.5 bg-outline-variant rounded-full animate-pulse"></span>
</div>
<span class="font-body-md text-body-md text-on-surface-variant opacity-80">Finalizing your plan</span>
</div>
</section>
<!-- Bottom Section: Social Proof & Badge -->
<section class="mt-auto flex flex-col items-center animate-float-up w-full" style="animation-delay: 0.5s;">
<!-- Overlapping Testimonial Cards -->
<div class="relative w-full h-[140px] mb-6">
<!-- Background Card (Muted) -->
<div class="absolute top-0 left-1/2 -translate-x-1/2 w-[90%] bg-surface-container-highest rounded-xl h-24 shadow-[0px_4px_20px_rgba(16,28,18,0.06)] transform -translate-y-3 scale-95 opacity-50 z-0"></div>
<!-- Foreground Card (Active) -->
<div class="absolute top-0 left-0 w-full bg-surface-container-lowest rounded-xl shadow-[0px_8px_30px_rgba(16,28,18,0.08)] p-4 flex flex-col gap-2 z-10 border border-surface-variant/50">
<div class="flex items-center gap-3 mb-1">
<img class="w-10 h-10 rounded-full object-cover bg-surface-variant" data-alt="A bright, professional headshot of a smiling woman in her early thirties, bathed in warm, soft natural sunlight. The aesthetic is clean, approachable, and highly polished, fitting a premium digital service. She is framed intimately against a gently blurred background of subtle green foliage." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCwP_g6-fpeohdJjnhG1R_yA-RypkiO6-NENOC640_fORV6ygSF9wlJvjXnQwnUzWnNyLN3AnOErHziNl_el3scfiHFQ0MjcJbOruxYdE0bhxkrPIGHNrH4Dl0nS0WxfeiNPSSiZpuSMM4D-0ZnTZRd5hR6-DnM04dT_mc3beAjFEYeedIrFrSKhi5IsZvK8gTu0x6k5UN627ijnpIaO3WBSeJ1waNUMaZ2OD_tj2pbPohgmE92SunvsOw8qVTP8zPPm79KJgIuniY"/>
<div class="flex flex-col">
<span class="font-label-sm text-label-sm text-on-surface flex items-center gap-1">Elena R. <span class="text-lg">🇺🇸</span></span>
<div class="flex text-tertiary-fixed-dim">
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[14px]" style="font-variation-settings: 'FILL' 1;">star</span>
</div>
</div>
</div>
<p class="font-body-md text-body-md text-on-surface-variant leading-relaxed line-clamp-2 italic">
"GreenLens completely saved my Fiddle Leaf Fig. The daily routines feel incredibly precise and easy."
</p>
</div>
</div>
<!-- Store Badge -->
<div class="flex items-center justify-center gap-2 text-primary bg-surface-container-low px-4 py-2 rounded-full border border-primary/20">
<span class="material-symbols-outlined text-[20px]" style="font-variation-settings: 'FILL' 1;">workspace_premium</span>
<span class="font-label-sm text-label-sm uppercase tracking-widest">4.8 App Store Rating</span>
</div>
</section>
</main>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 229 KiB

View File

@@ -0,0 +1,155 @@
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>GreenLens - Onboarding</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary": "#375d00",
"on-surface": "#191d16",
"on-secondary-fixed": "#012104",
"on-secondary-fixed-variant": "#2d4e2b",
"surface-container-lowest": "#ffffff",
"surface-container-low": "#f2f5e9",
"error-container": "#ffdad6",
"secondary": "#446741",
"outline": "#72796a",
"on-secondary": "#ffffff",
"surface-container": "#ecefe4",
"on-error": "#ffffff",
"on-surface-variant": "#42493c",
"background": "#f8fbef",
"error": "#ba1a1a",
"outline-variant": "#c2c9b7",
"on-secondary-container": "#486b45",
"secondary-fixed": "#c5edbd",
"on-tertiary": "#ffffff",
"primary": "#2b5f0b",
"surface-dim": "#d8dbd0",
"primary-container": "#437824",
"tertiary-fixed-dim": "#a5d56e",
"inverse-on-surface": "#eff2e7",
"surface-container-high": "#e7e9de",
"surface-bright": "#f8fbef",
"inverse-surface": "#2e322a",
"on-background": "#191d16",
"on-primary-fixed": "#092100",
"secondary-container": "#c2eaba",
"on-tertiary-container": "#cbfd91",
"primary-fixed": "#b6f390",
"tertiary-container": "#4d771b",
"primary-fixed-dim": "#9bd776",
"tertiary-fixed": "#c0f287",
"surface-tint": "#366a17",
"secondary-fixed-dim": "#aad1a2",
"surface-variant": "#e1e4d9",
"on-error-container": "#93000a",
"on-tertiary-fixed-variant": "#2e4f00",
"on-primary": "#ffffff",
"on-primary-container": "#c1ff99",
"surface-container-highest": "#e1e4d9",
"on-tertiary-fixed": "#0f2000",
"on-primary-fixed-variant": "#205100",
"inverse-primary": "#9bd776",
"surface": "#f8fbef"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"spacing": {
"xs": "8px",
"xl": "32px",
"base": "4px",
"sm": "12px",
"gutter": "16px",
"md": "16px",
"lg": "24px",
"container-margin": "20px"
},
"fontFamily": {
"label-md": ["Plus Jakarta Sans"],
"body-lg": ["Plus Jakarta Sans"],
"label-lg": ["Plus Jakarta Sans"],
"body-md": ["Plus Jakarta Sans"],
"headline-lg": ["Plus Jakarta Sans"],
"display": ["Plus Jakarta Sans"],
"headline-md": ["Plus Jakarta Sans"]
},
"fontSize": {
"label-md": ["12px", { "lineHeight": "16px", "fontWeight": "600" }],
"body-lg": ["17px", { "lineHeight": "26px", "fontWeight": "500" }],
"label-lg": ["13px", { "lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700" }],
"body-md": ["15px", { "lineHeight": "22px", "fontWeight": "400" }],
"headline-lg": ["28px", { "lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800" }],
"display": ["34px", { "lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800" }],
"headline-md": ["22px", { "lineHeight": "28px", "fontWeight": "700" }]
}
}
}
}
</script>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-surface-dim flex items-center justify-center min-h-screen">
<div class="w-full max-w-md h-[884px] md:h-[844px] md:max-h-[839px] bg-background md:rounded-[40px] md:shadow-2xl relative overflow-hidden flex flex-col mx-auto border-0 md:border-[8px] md:border-black">
<!-- Top 60% Image Area -->
<div class="relative flex-grow h-[60%] w-full" data-alt="A high-resolution, bright, natural lighting photograph of a smartphone held in front of a lush, large green Monstera leaf. The style is premium minimalist lifestyle photography, emphasizing crisp details and fresh greenery. Soft cream and white blurred background behind the plant. A clear focus on the phone screen framing the vibrant leaf." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuAUZPCiN64BDhHkPH2NIYPISrIugWbCp_EkjOkaMh3fiNS7ryUPfGspVCfH2uBadQkMHwjferaQCp08IRybFzVTWEwlamSXufp2jVmvwmyX95-xy2emX1T_vA22-KJ47FEjXNZXdiUrcXXe2IcpYPEJKr3kXqYMWWUcIscNC5ML8XENdrXkQHq9gi9oIewApl-N0tazA5Go8VUUuz3bl_OQKf74TXzd38fEAw7m0V4OMkx5rJhirV4ngl-afOe9exyyM2kmWWqfACg'); background-size: cover; background-position: center;">
<!-- Scan Overlay Frame -->
<div class="absolute inset-0 flex items-center justify-center pointer-events-none pb-12">
<div class="w-56 h-56 border-2 border-primary-fixed/50 rounded-2xl relative shadow-[0_0_40px_rgba(43,95,11,0.2)]">
<!-- Corner Accents -->
<div class="absolute -top-[2px] -left-[2px] w-8 h-8 border-t-[3px] border-l-[3px] border-primary-fixed rounded-tl-2xl"></div>
<div class="absolute -top-[2px] -right-[2px] w-8 h-8 border-t-[3px] border-r-[3px] border-primary-fixed rounded-tr-2xl"></div>
<div class="absolute -bottom-[2px] -left-[2px] w-8 h-8 border-b-[3px] border-l-[3px] border-primary-fixed rounded-bl-2xl"></div>
<div class="absolute -bottom-[2px] -right-[2px] w-8 h-8 border-b-[3px] border-r-[3px] border-primary-fixed rounded-br-2xl"></div>
<!-- Scanning line animation simulation -->
<div class="absolute top-1/2 left-0 w-full h-[2px] bg-primary-fixed shadow-[0_0_8px_rgba(182,243,144,0.8)] opacity-70"></div>
</div>
</div>
<!-- Result Chip -->
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 translate-y-[140px] bg-surface-container-lowest/90 backdrop-blur-xl px-4 py-2.5 rounded-full shadow-[0px_8px_24px_rgba(16,28,18,0.12)] flex items-center space-x-2.5 border border-surface-variant">
<span class="material-symbols-outlined text-primary text-[18px]" style="font-variation-settings: 'FILL' 1;">temp_preferences_eco</span>
<span class="font-label-md text-label-md text-on-surface">Monstera identified · 98%</span>
</div>
</div>
<!-- Bottom 40% Sheet -->
<div class="relative h-[40%] bg-surface-container-lowest rounded-t-[32px] w-full px-gutter py-8 flex flex-col justify-between z-10 shadow-[0px_-8px_32px_rgba(16,28,18,0.06)] -mt-8">
<!-- Text Content -->
<div class="flex flex-col space-y-4 text-center mt-4">
<h1 class="font-display text-display text-on-surface">Scan Any Plant</h1>
<p class="font-body-lg text-body-lg text-on-surface-variant max-w-[300px] mx-auto">
Point your camera at a plant and GreenLens identifies it in seconds.
</p>
</div>
<!-- Bottom Controls -->
<div class="flex flex-col space-y-8 items-center w-full pb-6">
<!-- Progress Dots -->
<div class="flex space-x-2.5">
<div class="w-8 h-2 bg-primary rounded-full transition-all duration-300"></div>
<div class="w-2 h-2 bg-surface-variant rounded-full transition-all duration-300"></div>
<div class="w-2 h-2 bg-surface-variant rounded-full transition-all duration-300"></div>
</div>
<!-- Action Button -->
<button class="w-full h-[56px] bg-primary-container text-on-primary rounded-[14px] font-label-lg text-label-lg flex items-center justify-center hover:opacity-90 active:scale-95 transition-all shadow-sm">
CONTINUE
</button>
</div>
</div>
</div>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 KiB

View File

@@ -0,0 +1,210 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>GreenLens - Onboarding</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&amp;family=JetBrains+Mono:wght@500&amp;family=Manrope:wght@400&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-primary-fixed-variant": "#2e4f00",
"inverse-primary": "#41690c",
"surface": "#0c160d",
"on-tertiary-fixed": "#171d15",
"primary-container": "#a6d66f",
"on-error-container": "#ffdad6",
"surface-dim": "#0c160d",
"surface-container-lowest": "#071008",
"secondary-fixed-dim": "#b7ccb5",
"secondary": "#b7ccb5",
"secondary-container": "#394b39",
"on-surface": "#dae6d6",
"primary-fixed": "#c0f287",
"primary": "#c1f388",
"on-primary": "#1e3700",
"surface-bright": "#313c31",
"tertiary-fixed": "#dee5d6",
"error-container": "#93000a",
"background": "#0c160d",
"on-tertiary-fixed-variant": "#42493e",
"on-tertiary": "#2c3229",
"on-surface-variant": "#c3c9b6",
"inverse-surface": "#dae6d6",
"on-secondary": "#233424",
"on-primary-container": "#365d00",
"inverse-on-surface": "#283328",
"surface-variant": "#2d372d",
"surface-container-low": "#141e14",
"surface-container-highest": "#2d372d",
"surface-container-high": "#222d22",
"tertiary-fixed-dim": "#c2c9bb",
"tertiary-container": "#c3cabc",
"outline": "#8d9382",
"on-background": "#dae6d6",
"on-secondary-fixed": "#0f1f10",
"on-error": "#690005",
"secondary-fixed": "#d3e8d0",
"outline-variant": "#43493a",
"error": "#ffb4ab",
"on-secondary-fixed-variant": "#394b39",
"on-tertiary-container": "#4e554a",
"surface-tint": "#a5d56e",
"tertiary": "#dfe6d8",
"surface-container": "#182218",
"on-secondary-container": "#a6bba4",
"primary-fixed-dim": "#a5d56e",
"on-primary-fixed": "#0f2000"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"spacing": {
"unit": "8px",
"gutter": "24px",
"margin-mobile": "16px",
"margin-desktop": "64px",
"container-max": "1280px"
},
"fontFamily": {
"display": [
"Epilogue"
],
"headline-md": [
"Epilogue"
],
"body-lg": [
"Manrope"
],
"headline-lg": [
"Epilogue"
],
"label-sm": [
"JetBrains Mono"
],
"headline-lg-mobile": [
"Epilogue"
],
"body-md": [
"Manrope"
]
},
"fontSize": {
"display": [
"64px",
{
"lineHeight": "1.1",
"letterSpacing": "-0.02em",
"fontWeight": "700"
}
],
"headline-md": [
"28px",
{
"lineHeight": "1.3",
"fontWeight": "500"
}
],
"body-lg": [
"18px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
],
"headline-lg": [
"40px",
{
"lineHeight": "1.2",
"letterSpacing": "-0.01em",
"fontWeight": "600"
}
],
"label-sm": [
"12px",
{
"lineHeight": "1.0",
"letterSpacing": "0.05em",
"fontWeight": "500"
}
],
"headline-lg-mobile": [
"32px",
{
"lineHeight": "1.2",
"fontWeight": "600"
}
],
"body-md": [
"16px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
]
}
},
},
}
</script>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-surface-dim flex items-center justify-center min-h-screen">
<div class="w-full max-w-md h-[884px] md:h-[844px] md:max-h-[839px] bg-background md:rounded-[40px] md:shadow-2xl relative overflow-hidden flex flex-col mx-auto border-0 md:border-[8px] md:border-black">
<!-- Top 60% Image Area -->
<div class="relative flex-grow h-[60%] w-full" data-alt="A high-resolution, bright, natural lighting photograph of a smartphone held in front of a lush, large green Monstera leaf. The style is premium minimalist lifestyle photography, emphasizing crisp details and fresh greenery. Soft cream and white blurred background behind the plant. A clear focus on the phone screen framing the vibrant leaf." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuAUZPCiN64BDhHkPH2NIYPISrIugWbCp_EkjOkaMh3fiNS7ryUPfGspVCfH2uBadQkMHwjferaQCp08IRybFzVTWEwlamSXufp2jVmvwmyX95-xy2emX1T_vA22-KJ47FEjXNZXdiUrcXXe2IcpYPEJKr3kXqYMWWUcIscNC5ML8XENdrXkQHq9gi9oIewApl-N0tazA5Go8VUUuz3bl_OQKf74TXzd38fEAw7m0V4OMkx5rJhirV4ngl-afOe9exyyM2kmWWqfACg'); background-size: cover; background-position: center;">
<!-- Scan Overlay Frame -->
<div class="absolute inset-0 flex items-center justify-center pointer-events-none pb-12">
<div class="w-56 h-56 border-2 border-primary-fixed/50 rounded-2xl relative shadow-[0_0_40px_rgba(43,95,11,0.2)]">
<!-- Corner Accents -->
<div class="absolute -top-[2px] -left-[2px] w-8 h-8 border-t-[3px] border-l-[3px] border-primary-fixed rounded-tl-2xl"></div>
<div class="absolute -top-[2px] -right-[2px] w-8 h-8 border-t-[3px] border-r-[3px] border-primary-fixed rounded-tr-2xl"></div>
<div class="absolute -bottom-[2px] -left-[2px] w-8 h-8 border-b-[3px] border-l-[3px] border-primary-fixed rounded-bl-2xl"></div>
<div class="absolute -bottom-[2px] -right-[2px] w-8 h-8 border-b-[3px] border-r-[3px] border-primary-fixed rounded-br-2xl"></div>
<!-- Scanning line animation simulation -->
<div class="absolute top-1/2 left-0 w-full h-[2px] bg-primary-fixed shadow-[0_0_8px_rgba(182,243,144,0.8)] opacity-70"></div>
</div>
</div>
<!-- Result Chip -->
<div class="absolute top-1/2 left-1/2 transform -translate-x-1/2 translate-y-[140px] bg-surface-container-lowest/90 backdrop-blur-xl px-4 py-2.5 rounded-full shadow-[0px_8px_24px_rgba(16,28,18,0.12)] flex items-center space-x-2.5 border border-surface-variant">
<span class="material-symbols-outlined text-primary text-[18px]" style="font-variation-settings: 'FILL' 1;">temp_preferences_eco</span>
<span class="font-label-sm text-label-sm text-on-surface uppercase tracking-wider">Monstera identified · 98%</span>
</div>
</div>
<!-- Bottom 40% Sheet -->
<div class="relative h-[40%] bg-surface-container-lowest rounded-t-[32px] w-full px-gutter py-8 flex flex-col justify-between z-10 shadow-[0px_-8px_32px_rgba(16,28,18,0.06)] -mt-8">
<!-- Text Content -->
<div class="flex flex-col space-y-4 text-center mt-4">
<h1 class="font-display text-headline-lg-mobile md:text-display text-on-surface">Scan Any Plant</h1>
<p class="font-body-lg text-body-lg text-on-surface-variant max-w-[300px] mx-auto">
Point your camera at a plant and GreenLens identifies it in seconds.
</p>
</div>
<!-- Bottom Controls -->
<div class="flex flex-col space-y-8 items-center w-full pb-6">
<!-- Progress Dots -->
<div class="flex space-x-2.5">
<div class="w-8 h-2 bg-primary rounded-full transition-all duration-300"></div>
<div class="w-2 h-2 bg-surface-variant rounded-full transition-all duration-300"></div>
<div class="w-2 h-2 bg-surface-variant rounded-full transition-all duration-300"></div>
</div>
<!-- Action Button -->
<button class="w-full h-[56px] bg-primary-container text-on-primary-container rounded-[14px] font-body-md text-body-md font-bold uppercase tracking-widest flex items-center justify-center hover:opacity-90 active:scale-95 transition-all shadow-sm">
CONTINUE
</button>
</div>
</div>
</div>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 KiB

View File

@@ -0,0 +1,166 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>GreenLens - Sign Up</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary": "#375d00",
"on-surface": "#191d16",
"on-secondary-fixed": "#012104",
"on-secondary-fixed-variant": "#2d4e2b",
"surface-container-lowest": "#ffffff",
"surface-container-low": "#f2f5e9",
"error-container": "#ffdad6",
"secondary": "#446741",
"outline": "#72796a",
"on-secondary": "#ffffff",
"surface-container": "#ecefe4",
"on-error": "#ffffff",
"on-surface-variant": "#42493c",
"background": "#f8fbef",
"error": "#ba1a1a",
"outline-variant": "#c2c9b7",
"on-secondary-container": "#486b45",
"secondary-fixed": "#c5edbd",
"on-tertiary": "#ffffff",
"primary": "#2b5f0b",
"surface-dim": "#d8dbd0",
"primary-container": "#437824",
"tertiary-fixed-dim": "#a5d56e",
"inverse-on-surface": "#eff2e7",
"surface-container-high": "#e7e9de",
"surface-bright": "#f8fbef",
"inverse-surface": "#2e322a",
"on-background": "#191d16",
"on-primary-fixed": "#092100",
"secondary-container": "#c2eaba",
"on-tertiary-container": "#cbfd91",
"primary-fixed": "#b6f390",
"tertiary-container": "#4d771b",
"primary-fixed-dim": "#9bd776",
"tertiary-fixed": "#c0f287",
"surface-tint": "#366a17",
"secondary-fixed-dim": "#aad1a2",
"surface-variant": "#e1e4d9",
"on-error-container": "#93000a",
"on-tertiary-fixed-variant": "#2e4f00",
"on-primary": "#ffffff",
"on-primary-container": "#c1ff99",
"surface-container-highest": "#e1e4d9",
"on-tertiary-fixed": "#0f2000",
"on-primary-fixed-variant": "#205100",
"inverse-primary": "#9bd776",
"surface": "#f8fbef"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"2xl": "1rem",
"3xl": "1.5rem",
"4xl": "2rem",
"full": "9999px"
},
"spacing": {
"xs": "8px",
"xl": "32px",
"base": "4px",
"sm": "12px",
"gutter": "20px",
"md": "16px",
"lg": "24px",
"container-margin": "20px"
},
"fontFamily": {
"label-md": ["Plus Jakarta Sans"],
"body-lg": ["Plus Jakarta Sans"],
"label-lg": ["Plus Jakarta Sans"],
"body-md": ["Plus Jakarta Sans"],
"headline-lg": ["Plus Jakarta Sans"],
"display": ["Plus Jakarta Sans"],
"headline-md": ["Plus Jakarta Sans"]
},
"fontSize": {
"label-md": ["12px", { "lineHeight": "16px", "fontWeight": "600" }],
"body-lg": ["17px", { "lineHeight": "26px", "fontWeight": "500" }],
"label-lg": ["13px", { "lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700" }],
"body-md": ["15px", { "lineHeight": "22px", "fontWeight": "400" }],
"headline-lg": ["28px", { "lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800" }],
"display": ["34px", { "lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800" }],
"headline-md": ["22px", { "lineHeight": "28px", "fontWeight": "700" }]
}
}
}
}
</script>
<style>
body { font-family: 'Plus Jakarta Sans', sans-serif; }
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-[#111111] flex items-center justify-center min-h-screen">
<!-- iPhone 15 Frame -->
<main class="w-full h-[884px] md:h-[852px] md:w-[393px] bg-surface relative overflow-hidden md:rounded-[55px] shadow-2xl md:border-[8px] md:border-black">
<!-- Top 45% - Botanical Hero -->
<div class="absolute top-0 left-0 w-full h-[50%] z-0">
<div class="w-full h-full bg-cover bg-center" data-alt="A moody, high-end close-up photograph of vibrant indoor Monstera foliage. Deep, rich shadows contrast with soft, natural highlights reflecting off the glossy dark green leaves. The aesthetic is premium, minimalist, and calming, designed to serve as a cinematic backdrop with a stark light-mode UI overlay." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuB2FpbdibIeASSKYhIYDfwjgsD7FN1aOn1UfzEAbbg8U0xFmZkvNp0NX7FWPO8OYv6Vjzi46sLAmz5HjkexgD3c08MRpBnEmbf1nMU5CkJ8oNIQfMpta-gPg-h9cvojERpIFJcLWD5TKYuwwLJWd0tOqlV8MrqMkS32vRy8hO1m_AOTjVVoVLmW1sUlg8P-e6ojyA96g22IUckm0C9NcJCnoeYRo4u1f2lg-TZIWS4HJ61XG3u1DIrMCmsUmVYgGnz2qjixP46amm0')"></div>
<!-- Dark Gradient Overlay -->
<div class="absolute inset-0 bg-gradient-to-b from-black/20 via-black/50 to-[#121A10]"></div>
<!-- Hero Text Content -->
<div class="absolute bottom-[15%] left-0 w-full px-gutter text-white z-10 flex flex-col gap-1">
<h1 class="font-display text-display text-white">Anna,</h1>
<h2 class="font-headline-md text-headline-md text-white/90">Let's finish your setup!</h2>
<p class="font-body-md text-body-md text-white/80 mt-2 max-w-[90%]">
Create an account to save your plants and 3 free scans per month.
</p>
</div>
</div>
<!-- Bottom 55% - Cream Sheet -->
<div class="absolute bottom-0 left-0 w-full h-[55%] bg-surface rounded-t-4xl z-20 flex flex-col px-gutter pt-8 pb-10 shadow-[0px_-8px_30px_rgba(0,0,0,0.12)]">
<!-- Drag Handle (Visual only) -->
<div class="absolute top-3 left-1/2 -translate-x-1/2 w-12 h-1.5 bg-surface-variant rounded-full"></div>
<div class="flex-1 flex flex-col gap-4 mt-4">
<!-- Apple Button -->
<button class="w-full h-[56px] bg-on-surface text-surface rounded-2xl flex items-center justify-center gap-3 hover:bg-on-surface/90 transition-colors active:scale-[0.98]">
<svg class="w-5 h-5 fill-current" viewbox="0 0 384 512"><path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 24 184.8 8.8 245.4c-21.7 86.8 5.4 191.6 37.1 237.9 19.3 28.2 44.9 57.2 75.3 56.1 29.2-1.1 41.5-18.9 76.5-18.9 34.9 0 46.5 18.9 76.9 18.2 31.2-.7 53.9-26.8 72.8-55.4 23.3-35.1 32.8-69.2 33.3-71-1.3-.4-62.8-24.1-62-93.6zm-41-118c15.6-20.2 26.6-47.6 23.7-75.1-23.7 1-52.9 16.2-69.2 35.8-13.6 16.4-26.6 44.4-23 71.3 26 2 51.5-12.7 68.5-32z"></path></svg>
<span class="font-label-lg text-label-lg">Continue with Apple</span>
</button>
<!-- Divider -->
<div class="flex items-center gap-4 my-2 opacity-60">
<div class="flex-1 h-px bg-outline-variant"></div>
<span class="font-label-md text-label-md text-on-surface-variant uppercase">OR</span>
<div class="flex-1 h-px bg-outline-variant"></div>
</div>
<!-- Email Button -->
<button class="w-full h-[56px] bg-transparent border-2 border-outline-variant text-on-surface rounded-2xl flex items-center justify-center gap-3 hover:bg-surface-variant/50 transition-colors active:scale-[0.98]">
<span class="material-symbols-outlined" data-icon="mail" data-weight="fill" style="font-variation-settings: 'FILL' 1;">mail</span>
<span class="font-label-lg text-label-lg">Continue with Email</span>
</button>
</div>
<!-- Bottom Actions & Legal -->
<div class="mt-auto flex flex-col items-center gap-6">
<p class="font-body-md text-body-md text-on-surface">
Already have an account?
<a class="text-primary-container font-label-lg text-label-lg hover:underline ml-1" href="#">Log in</a>
</p>
<p class="text-[11px] leading-tight text-on-surface-variant text-center px-4">
By continuing, you agree to GreenLens's <a class="underline" href="#">Terms of Service</a> and <a class="underline" href="#">Privacy Policy</a>.
</p>
</div>
</div>
</main>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 501 KiB

View File

@@ -0,0 +1,243 @@
<!DOCTYPE html>
<html class="dark" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>GreenLens - Sign Up</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@500;600;700&amp;family=JetBrains+Mono:wght@500&amp;family=Manrope:wght@400&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-primary-fixed-variant": "#2e4f00",
"inverse-primary": "#41690c",
"surface": "#0c160d",
"on-tertiary-fixed": "#171d15",
"primary-container": "#a6d66f",
"on-error-container": "#ffdad6",
"surface-dim": "#0c160d",
"surface-container-lowest": "#071008",
"secondary-fixed-dim": "#b7ccb5",
"secondary": "#b7ccb5",
"secondary-container": "#394b39",
"on-surface": "#dae6d6",
"primary-fixed": "#c0f287",
"primary": "#c1f388",
"on-primary": "#1e3700",
"surface-bright": "#313c31",
"tertiary-fixed": "#dee5d6",
"error-container": "#93000a",
"background": "#0c160d",
"on-tertiary-fixed-variant": "#42493e",
"on-tertiary": "#2c3229",
"on-surface-variant": "#c3c9b6",
"inverse-surface": "#dae6d6",
"on-secondary": "#233424",
"on-primary-container": "#365d00",
"inverse-on-surface": "#283328",
"surface-variant": "#2d372d",
"surface-container-low": "#141e14",
"surface-container-highest": "#2d372d",
"surface-container-high": "#222d22",
"tertiary-fixed-dim": "#c2c9bb",
"tertiary-container": "#c3cabc",
"outline": "#8d9382",
"on-background": "#dae6d6",
"on-secondary-fixed": "#0f1f10",
"on-error": "#690005",
"secondary-fixed": "#d3e8d0",
"outline-variant": "#43493a",
"error": "#ffb4ab",
"on-secondary-fixed-variant": "#394b39",
"on-tertiary-container": "#4e554a",
"surface-tint": "#a5d56e",
"tertiary": "#dfe6d8",
"surface-container": "#182218",
"on-secondary-container": "#a6bba4",
"primary-fixed-dim": "#a5d56e",
"on-primary-fixed": "#0f2000"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px",
"2xl": "1rem",
"3xl": "1.5rem",
"4xl": "2rem"
},
"spacing": {
"unit": "8px",
"gutter": "24px",
"margin-mobile": "16px",
"margin-desktop": "64px",
"container-max": "1280px"
},
"fontFamily": {
"display": [
"Epilogue"
],
"headline-md": [
"Epilogue"
],
"body-lg": [
"Manrope"
],
"headline-lg": [
"Epilogue"
],
"label-sm": [
"JetBrains Mono"
],
"headline-lg-mobile": [
"Epilogue"
],
"body-md": [
"Manrope"
],
"label-lg": [
"JetBrains Mono"
],
"label-md": [
"JetBrains Mono"
]
},
"fontSize": {
"display": [
"64px",
{
"lineHeight": "1.1",
"letterSpacing": "-0.02em",
"fontWeight": "700"
}
],
"headline-md": [
"28px",
{
"lineHeight": "1.3",
"fontWeight": "500"
}
],
"body-lg": [
"18px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
],
"headline-lg": [
"40px",
{
"lineHeight": "1.2",
"letterSpacing": "-0.01em",
"fontWeight": "600"
}
],
"label-sm": [
"12px",
{
"lineHeight": "1.0",
"letterSpacing": "0.05em",
"fontWeight": "500"
}
],
"headline-lg-mobile": [
"32px",
{
"lineHeight": "1.2",
"fontWeight": "600"
}
],
"body-md": [
"16px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
],
"label-lg": [
"14px",
{
"lineHeight": "1.2",
"letterSpacing": "0.05em",
"fontWeight": "500"
}
],
"label-md": [
"12px",
{
"lineHeight": "1.2",
"letterSpacing": "0.05em",
"fontWeight": "500"
}
]
}
},
},
}
</script>
<style>
body { font-family: 'Manrope', sans-serif; }
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-[#111111] flex items-center justify-center min-h-screen">
<!-- iPhone 15 Frame -->
<main class="w-full h-[884px] md:h-[852px] md:w-[393px] bg-surface relative overflow-hidden md:rounded-[55px] shadow-2xl md:border-[8px] md:border-black">
<!-- Top 45% - Botanical Hero -->
<div class="absolute top-0 left-0 w-full h-[50%] z-0">
<div class="w-full h-full bg-cover bg-center" data-alt="A moody, high-end close-up photograph of vibrant indoor Monstera foliage. Deep, rich shadows contrast with soft, natural highlights reflecting off the glossy dark green leaves. The aesthetic is premium, minimalist, and calming, designed to serve as a cinematic backdrop with a stark light-mode UI overlay." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuB2FpbdibIeASSKYhIYDfwjgsD7FN1aOn1UfzEAbbg8U0xFmZkvNp0NX7FWPO8OYv6Vjzi46sLAmz5HjkexgD3c08MRpBnEmbf1nMU5CkJ8oNIQfMpta-gPg-h9cvojERpIFJcLWD5TKYuwwLJWd0tOqlV8MrqMkS32vRy8hO1m_AOTjVVoVLmW1sUlg8P-e6ojyA96g22IUckm0C9NcJCnoeYRo4u1f2lg-TZIWS4HJ61XG3u1DIrMCmsUmVYgGnz2qjixP46amm0')"></div>
<!-- Dark Gradient Overlay -->
<div class="absolute inset-0 bg-gradient-to-b from-black/20 via-black/50 to-[#121A10]"></div>
<!-- Hero Text Content -->
<div class="absolute bottom-[15%] left-0 w-full px-gutter text-white z-10 flex flex-col gap-1">
<h1 class="font-display text-display text-white">Anna,</h1>
<h2 class="font-headline-md text-headline-md text-white/90">Let's finish your setup!</h2>
<p class="font-body-md text-body-md text-white/80 mt-2 max-w-[90%]">
Create an account to save your plants and 3 free scans per month.
</p>
</div>
</div>
<!-- Bottom 55% - Cream Sheet -->
<div class="absolute bottom-0 left-0 w-full h-[55%] bg-surface rounded-t-4xl z-20 flex flex-col px-gutter pt-8 pb-10 shadow-[0px_-8px_30px_rgba(0,0,0,0.12)]">
<!-- Drag Handle (Visual only) -->
<div class="absolute top-3 left-1/2 -translate-x-1/2 w-12 h-1.5 bg-surface-variant rounded-full"></div>
<div class="flex-1 flex flex-col gap-4 mt-4">
<!-- Apple Button -->
<button class="w-full h-[56px] bg-on-surface text-surface rounded-2xl flex items-center justify-center gap-3 hover:bg-on-surface/90 transition-colors active:scale-[0.98]">
<svg class="w-5 h-5 fill-current" viewbox="0 0 384 512"><path d="M318.7 268.7c-.2-36.7 16.4-64.4 50-84.8-18.8-26.9-47.2-41.7-84.7-44.6-35.5-2.8-74.3 20.7-88.5 20.7-15 0-49.4-19.7-76.4-19.7C63.3 141.2 24 184.8 8.8 245.4c-21.7 86.8 5.4 191.6 37.1 237.9 19.3 28.2 44.9 57.2 75.3 56.1 29.2-1.1 41.5-18.9 76.5-18.9 34.9 0 46.5 18.9 76.9 18.2 31.2-.7 53.9-26.8 72.8-55.4 23.3-35.1 32.8-69.2 33.3-71-1.3-.4-62.8-24.1-62-93.6zm-41-118c15.6-20.2 26.6-47.6 23.7-75.1-23.7 1-52.9 16.2-69.2 35.8-13.6 16.4-26.6 44.4-23 71.3 26 2 51.5-12.7 68.5-32z"></path></svg>
<span class="font-label-lg text-label-lg">Continue with Apple</span>
</button>
<!-- Divider -->
<div class="flex items-center gap-4 my-2 opacity-60">
<div class="flex-1 h-px bg-outline-variant"></div>
<span class="font-label-md text-label-md text-on-surface-variant uppercase">OR</span>
<div class="flex-1 h-px bg-outline-variant"></div>
</div>
<!-- Email Button -->
<button class="w-full h-[56px] bg-transparent border-2 border-outline-variant text-on-surface rounded-2xl flex items-center justify-center gap-3 hover:bg-surface-variant/50 transition-colors active:scale-[0.98]">
<span class="material-symbols-outlined" data-icon="mail" data-weight="fill" style="font-variation-settings: 'FILL' 1;">mail</span>
<span class="font-label-lg text-label-lg">Continue with Email</span>
</button>
</div>
<!-- Bottom Actions & Legal -->
<div class="mt-auto flex flex-col items-center gap-6">
<p class="font-body-md text-body-md text-on-surface">
Already have an account?
<a class="text-primary font-label-lg text-label-lg hover:underline ml-1" href="#">Log in</a>
</p>
<p class="text-[11px] leading-tight text-on-surface-variant text-center px-4">
By continuing, you agree to GreenLens's <a class="underline" href="#">Terms of Service</a> and <a class="underline" href="#">Privacy Policy</a>.
</p>
</div>
</div>
</main>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

View File

@@ -0,0 +1,174 @@
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>GreenLens - Welcome</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary": "#375d00",
"on-surface": "#191d16",
"on-secondary-fixed": "#012104",
"on-secondary-fixed-variant": "#2d4e2b",
"surface-container-lowest": "#ffffff",
"surface-container-low": "#f2f5e9",
"error-container": "#ffdad6",
"secondary": "#446741",
"outline": "#72796a",
"on-secondary": "#ffffff",
"surface-container": "#ecefe4",
"on-error": "#ffffff",
"on-surface-variant": "#42493c",
"background": "#f8fbef",
"error": "#ba1a1a",
"outline-variant": "#c2c9b7",
"on-secondary-container": "#486b45",
"secondary-fixed": "#c5edbd",
"on-tertiary": "#ffffff",
"primary": "#2b5f0b",
"surface-dim": "#d8dbd0",
"primary-container": "#437824",
"tertiary-fixed-dim": "#a5d56e",
"inverse-on-surface": "#eff2e7",
"surface-container-high": "#e7e9de",
"surface-bright": "#f8fbef",
"inverse-surface": "#2e322a",
"on-background": "#191d16",
"on-primary-fixed": "#092100",
"secondary-container": "#c2eaba",
"on-tertiary-container": "#cbfd91",
"primary-fixed": "#b6f390",
"tertiary-container": "#4d771b",
"primary-fixed-dim": "#9bd776",
"tertiary-fixed": "#c0f287",
"surface-tint": "#366a17",
"secondary-fixed-dim": "#aad1a2",
"surface-variant": "#e1e4d9",
"on-error-container": "#93000a",
"on-tertiary-fixed-variant": "#2e4f00",
"on-primary": "#ffffff",
"on-primary-container": "#c1ff99",
"surface-container-highest": "#e1e4d9",
"on-tertiary-fixed": "#0f2000",
"on-primary-fixed-variant": "#205100",
"inverse-primary": "#9bd776",
"surface": "#f8fbef"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"spacing": {
"xs": "8px",
"xl": "32px",
"base": "4px",
"sm": "12px",
"gutter": "16px",
"md": "16px",
"lg": "24px",
"container-margin": "20px"
},
"fontFamily": {
"label-md": ["Plus Jakarta Sans"],
"body-lg": ["Plus Jakarta Sans"],
"label-lg": ["Plus Jakarta Sans"],
"body-md": ["Plus Jakarta Sans"],
"headline-lg": ["Plus Jakarta Sans"],
"display": ["Plus Jakarta Sans"],
"headline-md": ["Plus Jakarta Sans"]
},
"fontSize": {
"label-md": ["12px", { "lineHeight": "16px", "fontWeight": "600" }],
"body-lg": ["17px", { "lineHeight": "26px", "fontWeight": "500" }],
"label-lg": ["13px", { "lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700" }],
"body-md": ["15px", { "lineHeight": "22px", "fontWeight": "400" }],
"headline-lg": ["28px", { "lineHeight": "36px", "letterSpacing": "-0.01em", "fontWeight": "800" }],
"display": ["34px", { "lineHeight": "42px", "letterSpacing": "-0.02em", "fontWeight": "800" }],
"headline-md": ["22px", { "lineHeight": "28px", "fontWeight": "700" }]
}
}
}
}
</script>
<style>
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-background text-on-surface h-screen w-full overflow-hidden flex flex-col font-body-md select-none">
<!-- Top 55%: Hero Image Area -->
<div class="h-[486px] w-full relative shrink-0">
<!-- Hero Background Image -->
<div class="absolute inset-0 bg-cover bg-center" data-alt="A stunning, high-resolution botanical photograph of a lush indoor jungle featuring large Monstera deliciosa and Calathea plants. The composition is shot from a slightly elevated angle, capturing the vibrant green, textured leaves bathed in soft, natural morning light pouring through a nearby window. The background features a subtle, warm cream-colored wall that perfectly aligns with a premium minimalist light-mode app aesthetic. The depth of field is shallow, keeping the foreground leaves razor-sharp while gently blurring the background, creating a calm, focused, and high-end lifestyle magazine mood." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuBx6w-Q0WxDraDoAiIgMxWkqZHEYA2QyGU65IPOu1fB8mwd5-O08Z4VcoQ4LdjcnCsU2ybQtlguuy_01cHv1sVfj5RSFen2jMoqF5NAZe_Xg0NqFQXwLIioixIHU-D9ryaPU-Pe3Qgezzgdcr2DXHxTkITv_zzpjcgW4eHm-I5ms5386DGlPXluAxgD-Iy4bQES2VAU1KXak7ZJuqn74-Ue9xRIP85gxxYoST-BemjrzJCEc9jn_h4Fk7HesA75uZGOyzYuEY2lyNA')"></div>
<!-- Subtle Top Gradient for Text Legibility -->
<div class="absolute top-0 left-0 w-full h-32 bg-gradient-to-b from-on-surface/50 to-transparent"></div>
<!-- Header: Logo & App Store Badge -->
<div class="absolute top-12 left-0 w-full px-container-margin flex items-center justify-between">
<div class="flex items-center gap-2 text-surface-container-lowest">
<span class="material-symbols-outlined text-[28px]" style="font-variation-settings: 'FILL' 1;">spa</span>
<span class="font-display text-display tracking-tight leading-none text-surface-container-lowest pt-1">GreenLens</span>
</div>
<div class="flex items-center gap-1 bg-surface-container-lowest/20 backdrop-blur-md border border-surface-container-lowest/10 rounded-full px-3 py-1.5 shadow-sm">
<span class="material-symbols-outlined text-[14px] text-surface-container-lowest" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="font-label-md text-label-md text-surface-container-lowest">4.8</span>
</div>
</div>
<!-- Floating Testimonial Card -->
<div class="absolute bottom-12 left-container-margin right-container-margin bg-surface-container-lowest/95 backdrop-blur-md rounded-[16px] p-4 shadow-[0px_4px_20px_rgba(16,28,18,0.12)] border border-surface-variant/50">
<p class="font-body-md text-body-md text-on-surface mb-3 italic leading-relaxed">"Finally my plants stay alive! Highly recommend."</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<img class="w-8 h-8 rounded-full object-cover shadow-sm" data-alt="A professional, high-quality portrait photograph of a young woman with a warm, friendly smile, designed to be used as a circular avatar in a premium app interface. She has natural, effortless styling and is softly lit by diffused natural daylight. The background is a very soft, out-of-focus hint of an indoor plant shop or greenhouse, featuring muted greens and warm cream tones to perfectly match a sophisticated, minimalist light-mode aesthetic. The image exudes trust and approachability." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCqKHPbHjvPxBBSroxRXwyNuYGln0ydKqum11jR-kIoUoBeWzrSqByhgdt-maLp0b9HbdyDabUsv2PBnqZy8oIBAGSjHmtG9PhPiWARa9AcQrlRAtKWOAVYdD3y03G9Y0ilbQMz4MvFDBv57IqGO7xcp4cEAddUgjS1sgPskTA6Rl-9vXibnYMo2ScPYD6r0ch02eNjaCfxNq1sP0OGSBIksgW1o1CUuw6Brzl0WQbmoVaRaflVdiDp25qFXfG1woIYJw0qF6LZ3_E"/>
<span class="font-label-md text-label-md text-on-surface-variant">Anna M.</span>
</div>
<div class="flex items-center gap-0.5 text-[#F5B041]">
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
</div>
</div>
</div>
</div>
<!-- Bottom 45%: Cream Sheet Area -->
<div class="flex-1 bg-background rounded-t-[32px] -mt-6 relative z-10 px-container-margin pt-10 pb-8 flex flex-col items-center text-center shadow-[0px_-8px_30px_rgba(0,0,0,0.04)]">
<!-- Sheet Handle (Subtle UI Detail) -->
<div class="absolute top-4 left-1/2 -translate-x-1/2 w-12 h-1.5 bg-surface-variant rounded-full"></div>
<h1 class="font-headline-lg text-headline-lg text-on-surface mb-3 mt-2">Welcome to GreenLens!</h1>
<p class="font-body-lg text-body-lg text-on-surface-variant mb-auto px-4 max-w-sm">Identify, understand and care for your plants — effortlessly.</p>
<!-- Actions -->
<div class="w-full mt-8 mb-6">
<button class="w-full h-[56px] bg-primary-container text-on-primary font-label-lg text-label-lg rounded-[14px] flex items-center justify-center transition-transform hover:scale-[0.98] active:scale-95 shadow-[0px_8px_16px_rgba(67,120,36,0.15)] mb-4">
Let's Go
</button>
<button class="w-full h-[44px] font-label-lg text-label-lg text-primary-container hover:opacity-80 transition-opacity flex items-center justify-center">
Log in
</button>
</div>
<!-- Legal -->
<p class="font-label-md text-label-md text-outline px-4 max-w-xs">
By continuing you agree to our <a class="underline hover:text-on-surface transition-colors" href="#">Privacy Policy</a> and <a class="underline hover:text-on-surface transition-colors" href="#">Terms</a>
</p>
</div>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 647 KiB

View File

@@ -0,0 +1,244 @@
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"/>
<title>GreenLens - Welcome</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Epilogue:wght@400;500;600;700;800&amp;family=JetBrains+Mono:wght@400;500;600&amp;family=Manrope:wght@400;500;600;700&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"on-primary-fixed-variant": "#2e4f00",
"inverse-primary": "#41690c",
"surface": "#0c160d",
"on-tertiary-fixed": "#171d15",
"primary-container": "#a6d66f",
"on-error-container": "#ffdad6",
"surface-dim": "#0c160d",
"surface-container-lowest": "#071008",
"secondary-fixed-dim": "#b7ccb5",
"secondary": "#b7ccb5",
"secondary-container": "#394b39",
"on-surface": "#dae6d6",
"primary-fixed": "#c0f287",
"primary": "#c1f388",
"on-primary": "#1e3700",
"surface-bright": "#313c31",
"tertiary-fixed": "#dee5d6",
"error-container": "#93000a",
"background": "#0c160d",
"on-tertiary-fixed-variant": "#42493e",
"on-tertiary": "#2c3229",
"on-surface-variant": "#c3c9b6",
"inverse-surface": "#dae6d6",
"on-secondary": "#233424",
"on-primary-container": "#365d00",
"inverse-on-surface": "#283328",
"surface-variant": "#2d372d",
"surface-container-low": "#141e14",
"surface-container-highest": "#2d372d",
"surface-container-high": "#222d22",
"tertiary-fixed-dim": "#c2c9bb",
"tertiary-container": "#c3cabc",
"outline": "#8d9382",
"on-background": "#dae6d6",
"on-secondary-fixed": "#0f1f10",
"on-error": "#690005",
"secondary-fixed": "#d3e8d0",
"outline-variant": "#43493a",
"error": "#ffb4ab",
"on-secondary-fixed-variant": "#394b39",
"on-tertiary-container": "#4e554a",
"surface-tint": "#a5d56e",
"tertiary": "#dfe6d8",
"surface-container": "#182218",
"on-secondary-container": "#a6bba4",
"primary-fixed-dim": "#a5d56e",
"on-primary-fixed": "#0f2000"
},
"borderRadius": {
"DEFAULT": "0.25rem",
"lg": "0.5rem",
"xl": "0.75rem",
"full": "9999px"
},
"spacing": {
"unit": "8px",
"gutter": "24px",
"margin-mobile": "16px",
"margin-desktop": "64px",
"container-max": "1280px",
"xs": "8px",
"xl": "32px",
"base": "4px",
"sm": "12px",
"md": "16px",
"lg": "24px",
"container-margin": "20px"
},
"fontFamily": {
"display": [
"Epilogue"
],
"headline-md": [
"Epilogue"
],
"body-lg": [
"Manrope"
],
"headline-lg": [
"Epilogue"
],
"label-sm": [
"JetBrains Mono"
],
"headline-lg-mobile": [
"Epilogue"
],
"body-md": [
"Manrope"
],
"label-md": [
"JetBrains Mono"
],
"label-lg": [
"JetBrains Mono"
]
},
"fontSize": {
"display": [
"64px",
{
"lineHeight": "1.1",
"letterSpacing": "-0.02em",
"fontWeight": "700"
}
],
"headline-md": [
"28px",
{
"lineHeight": "1.3",
"fontWeight": "500"
}
],
"body-lg": [
"18px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
],
"headline-lg": [
"40px",
{
"lineHeight": "1.2",
"letterSpacing": "-0.01em",
"fontWeight": "600"
}
],
"label-sm": [
"12px",
{
"lineHeight": "1.0",
"letterSpacing": "0.05em",
"fontWeight": "500"
}
],
"headline-lg-mobile": [
"32px",
{
"lineHeight": "1.2",
"fontWeight": "600"
}
],
"body-md": [
"16px",
{
"lineHeight": "1.6",
"fontWeight": "400"
}
],
"label-md": ["12px", { "lineHeight": "16px", "fontWeight": "600" }],
"label-lg": ["13px", { "lineHeight": "18px", "letterSpacing": "0.05em", "fontWeight": "700" }]
}
},
},
}
</script>
<style>
body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
}
</style>
<style>
body {
min-height: max(884px, 100dvh);
}
</style>
</head>
<body class="bg-background text-on-surface h-screen w-full overflow-hidden flex flex-col font-body-md select-none">
<!-- Top 55%: Hero Image Area -->
<div class="h-[486px] w-full relative shrink-0">
<!-- Hero Background Image -->
<div class="absolute inset-0 bg-cover bg-center" data-alt="A stunning, high-resolution botanical photograph of a lush indoor jungle featuring large Monstera deliciosa and Calathea plants. The composition is shot from a slightly elevated angle, capturing the vibrant green, textured leaves bathed in soft, natural morning light pouring through a nearby window. The background features a subtle, warm cream-colored wall that perfectly aligns with a premium minimalist light-mode app aesthetic. The depth of field is shallow, keeping the foreground leaves razor-sharp while gently blurring the background, creating a calm, focused, and high-end lifestyle magazine mood." style="background-image: url('https://lh3.googleusercontent.com/aida-public/AB6AXuBx6w-Q0WxDraDoAiIgMxWkqZHEYA2QyGU65IPOu1fB8mwd5-O08Z4VcoQ4LdjcnCsU2ybQtlguuy_01cHv1sVfj5RSFen2jMoqF5NAZe_Xg0NqFQXwLIioixIHU-D9ryaPU-Pe3Qgezzgdcr2DXHxTkITv_zzpjcgW4eHm-I5ms5386DGlPXluAxgD-Iy4bQES2VAU1KXak7ZJuqn74-Ue9xRIP85gxxYoST-BemjrzJCEc9jn_h4Fk7HesA75uZGOyzYuEY2lyNA')"></div>
<!-- Subtle Top Gradient for Text Legibility -->
<div class="absolute top-0 left-0 w-full h-32 bg-gradient-to-b from-surface/80 to-transparent"></div>
<!-- Header: Logo & App Store Badge -->
<div class="absolute top-12 left-0 w-full px-container-margin flex items-center justify-between">
<div class="flex items-center gap-2 text-white">
<span class="material-symbols-outlined text-[28px]" style="font-variation-settings: 'FILL' 1;">spa</span>
<span class="font-display text-display tracking-tight leading-none text-white pt-1">GreenLens</span>
</div>
<div class="flex items-center gap-1 bg-surface-container-lowest/40 backdrop-blur-md border border-white/20 rounded-full px-3 py-1.5 shadow-sm text-white">
<span class="material-symbols-outlined text-[14px] text-white" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="font-label-md text-label-md text-white">4.8</span>
</div>
</div>
<!-- Floating Testimonial Card -->
<div class="absolute bottom-12 left-container-margin right-container-margin bg-surface-container-lowest/95 backdrop-blur-md rounded-[16px] p-4 shadow-[0px_4px_20px_rgba(0,0,0,0.5)] border border-surface-variant/50">
<p class="font-body-md text-body-md text-on-surface mb-3 italic leading-relaxed">"Finally my plants stay alive! Highly recommend."</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
<img class="w-8 h-8 rounded-full object-cover shadow-sm" data-alt="A professional, high-quality portrait photograph of a young woman with a warm, friendly smile, designed to be used as a circular avatar in a premium app interface. She has natural, effortless styling and is softly lit by diffused natural daylight. The background is a very soft, out-of-focus hint of an indoor plant shop or greenhouse, featuring muted greens and warm cream tones to perfectly match a sophisticated, minimalist light-mode aesthetic. The image exudes trust and approachability." src="https://lh3.googleusercontent.com/aida-public/AB6AXuCqKHPbHjvPxBBSroxRXwyNuYGln0ydKqum11jR-kIoUoBeWzrSqByhgdt-maLp0b9HbdyDabUsv2PBnqZy8oIBAGSjHmtG9PhPiWARa9AcQrlRAtKWOAVYdD3y03G9Y0ilbQMz4MvFDBv57IqGO7xcp4cEAddUgjS1sgPskTA6Rl-9vXibnYMo2ScPYD6r0ch02eNjaCfxNq1sP0OGSBIksgW1o1CUuw6Brzl0WQbmoVaRaflVdiDp25qFXfG1woIYJw0qF6LZ3_E"/>
<span class="font-label-md text-label-md text-on-surface-variant">Anna M.</span>
</div>
<div class="flex items-center gap-0.5 text-primary">
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
<span class="material-symbols-outlined text-[16px]" style="font-variation-settings: 'FILL' 1;">star</span>
</div>
</div>
</div>
</div>
<!-- Bottom 45%: Cream Sheet Area -->
<div class="flex-1 bg-background rounded-t-[32px] -mt-6 relative z-10 px-container-margin pt-10 pb-8 flex flex-col items-center text-center shadow-[0px_-8px_30px_rgba(0,0,0,0.2)]">
<!-- Sheet Handle (Subtle UI Detail) -->
<div class="absolute top-4 left-1/2 -translate-x-1/2 w-12 h-1.5 bg-surface-variant rounded-full"></div>
<h1 class="font-headline-lg text-headline-lg text-on-surface mb-3 mt-2">Welcome to GreenLens!</h1>
<p class="font-body-lg text-body-lg text-on-surface-variant mb-auto px-4 max-w-sm">Identify, understand and care for your plants — effortlessly.</p>
<!-- Actions -->
<div class="w-full mt-8 mb-6">
<button class="w-full h-[56px] bg-primary-container text-on-primary-container font-label-lg text-label-lg rounded-[14px] flex items-center justify-center transition-transform hover:scale-[0.98] active:scale-95 shadow-[0px_8px_16px_rgba(0,0,0,0.2)] mb-4">
Let's Go
</button>
<button class="w-full h-[44px] font-label-lg text-label-lg text-primary-container hover:opacity-80 transition-opacity flex items-center justify-center">
Log in
</button>
</div>
<!-- Legal -->
<p class="font-label-md text-label-md text-outline px-4 max-w-xs">
By continuing you agree to our <a class="underline hover:text-on-surface transition-colors" href="#">Privacy Policy</a> and <a class="underline hover:text-on-surface transition-colors" href="#">Terms</a>
</p>
</div>
</body></html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,71 @@
# Onboarding Redesign, Soft Paywall & Free Tier — Design Spec
**Date:** 2026-07-06
**Status:** Implemented
**Design reference:** `design/stitch-onboarding/` (Stitch export: 10 screens, light + dark variants, `code.html` per screen, design tokens in `botanical_vitality/DESIGN.md` (light) and `nocturnal_botanical/DESIGN.md` (dark))
## Goal
Replicate the Learna-style onboarding → soft paywall → sign-up flow, remove the hard paywall (client and server), and introduce a free tier with 3 monthly scan credits. Sign-up moves to the *end* of onboarding, after the paywall (pattern credited with +52% sales in the reference video).
## 1. New user flow
```
Welcome (social proof)
→ Benefit slides (3)
→ Personalization questions (existing: source, goal, experience, customize)
→ "Personalizing your care plan…" progress screen
→ Paywall (dismissible ✕)
→ Sign-up (Continue with Apple / Email)
→ App (tabs), free plan with 3 credits/month
```
- The guest **demo scan stays** available from the welcome screen (existing `isGuest` server handling unchanged).
- Existing users: "Log in" link on the welcome screen → login.
- Paywall ✕ during onboarding → continues to sign-up. Paywall ✕ in-app → simply closes.
## 2. Screens
Each screen follows the Stitch mockup in `design/stitch-onboarding/<name>/screen.png` (+ `_dark_mode` variant). `code.html` is the source of truth for spacing/sizes. Design tokens (colors, Plus Jakarta Sans-style extra-bold typography) come from the two `DESIGN.md` files, mapped into the existing `constants/Colors.ts` light/dark system.
| Screen | Stitch reference | Implementation | Imagery |
|---|---|---|---|
| Welcome | `welcome_to_greenlens` | Redesign `app/onboarding.tsx`: hero photo, rating badge "4.8", testimonial card, "Let's Go" CTA, "Log in" link, legal footer. Keep demo-scan entry. | Bright plant-room photo (new or existing asset) |
| Benefit slide 1 "Scan Any Plant" | `scan_any_plant` | New screen(s), progress dots, Continue | Reuse existing render style (`assets/onboarding_*_mockup.png` language); scan-frame + "Monstera identified · 98%" chip built natively in RN, NOT baked into the photo |
| Benefit slide 2 "Health Check & Care Plan" | `health_check_care_plan` | New screen | Photo + native overlay card (Overwatering detected / 7-day rescue plan ready) |
| Benefit slide 3 "Never Forget Watering" | `never_forget_watering` | New screen | Photo + native reminder chips |
| Personalization questions | `personalization_question` | Restyle existing `app/onboarding/{source,goal,experience,customize}.tsx`: top progress bar + back arrow, white option cards radius 16, selected = green border + tint, Continue button | Emoji/icon per option |
| Personalizing progress | `personalizing_your_plan` | New screen: animated % + progress ring, sequential checkmarks ("Analyzing your answers", "Building your care plan", "Preparing your scan credits", "Finalizing your plan"), testimonial card (filled stars), rating badge. Auto-advances to paywall. | Small round plant photo in ring |
| Paywall | `greenlens_pro_paywall` | Redesign paywall mode of `app/profile/billing.tsx`: ✕ top-left, Restore top-right, "GreenLens Pro / Get Unlimited Access", plan card, **Free Trial toggle** (ON = yearly with 7-day trial → "Due today €0 / Due <date> €39.99"; OFF = monthly, no trial), CTA "Try Free"/"Start Now", Cancel Anytime, Privacy/Terms. Dates dynamic. Credit top-ups remain reachable. | `assets/paywall_scan_background.png` as header |
| Sign-up | `sign_up_for_greenlens` | Restyle `app/auth/signup.tsx`: dark botanical hero with "<Name>, Let's finish your setup!" (name from onboarding answers if given, else generic), "Continue with Apple" (exists), OR divider, "Continue with Email" expanding to email/password fields (two-step), "Already have an account? Log in", legal. | `assets/welcome_botanical_hero.png` (replaces Stitch photo with baked-in fake form) |
| Login | `login_to_greenlens` | Restyle `app/auth/login.tsx`: "Welcome back!", Apple button, email/password, Forgot password?, "New here? Create account". | Clean botanical photo (existing assets) |
| Out of credits | `out_of_credits` | New bottom sheet over scanner: leaf icon with "0" badge, "You're out of free scans", renewal date, "See Pro Plans" → paywall, top-up chips (+30/+100/+250, BEST badge on 250), "Maybe later". | — |
Known mockup fixes (agreed): replace AI-artifact photos (garbled phone UI on scan slide, fake forms baked into sign-up/login backgrounds), filled stars on testimonials, dynamic dates.
## 3. Free tier (backend, `server/lib/billing.js` + `server/index.js`)
- `FREE_MONTHLY_CREDITS: 0 → 3`. Existing monthly-allowance reset logic already covers free accounts; verify reset works for plan `free`.
- **Remove server hard paywall:** drop `ensureActiveProEntitlement` from scan and health-check endpoints (`server/index.js`). Credit consumption becomes the only gate; 0 credits → existing 402 `INSUFFICIENT_CREDITS` error.
- **Same scan model for free and pro** (`getScanModel` returns the pro model for both plans). Cost accepted (~cents/user/month at 3 scans).
- **Low-confidence AI review pass stays pro-only** (unchanged) so a free scan never burns 2 credits, and Pro keeps a quality edge.
- Trial handling unchanged (yearly plan carries 7-day trial, 30 credits during trial).
## 4. Soft paywall (app)
- `app/_layout.tsx`: remove the `!hasActiveEntitlement → Redirect /profile/billing` block (line ~169). Signed-in users always reach the tabs regardless of plan.
- Scanner: on 402 `INSUFFICIENT_CREDITS` → open the out-of-credits bottom sheet (dismissible) instead of blocking.
- Free users see a credits badge (e.g. "3 scans left") on Home/Scanner so the limit is visible before it bites.
- Existing PostHog events (`trial_started`, purchase events) stay; add events for paywall_shown / paywall_dismissed / onboarding step views.
## 5. Out of scope
- Instagram-story auto-advance on benefit slides (manual Continue first)
- Win-back / cancellation flows
- Android Google Sign-In (RevenueCat Android key still placeholder)
- Chat-style onboarding rebuild
## 6. Open questions
- Exact free-credit renewal display date source (billing summary already exposes renewal info for pro; confirm shape for free accounts).
- Whether existing signed-in free users (previously hard-walled) need a one-time "you now have 3 free scans" toast. Nice-to-have.

View File

@@ -22,6 +22,9 @@
],
"setupFiles": [
"./jest.setup.js"
],
"testPathIgnorePatterns": [
"<rootDir>/server/test/"
]
},
"dependencies": {

View File

@@ -51,6 +51,7 @@ const {
getAccountSnapshot,
getBillingSummary,
getEndpointResponse,
ensureSufficientCredits,
isInsufficientCreditsError,
claimNotificationOnce,
simulatePurchase,
@@ -203,17 +204,15 @@ const resolveIdempotencyKey = (request) => {
return '';
};
const createHardPaywallError = (requiredCredits) => {
const error = new Error('Active Pro or trial entitlement required.');
error.code = 'INSUFFICIENT_CREDITS';
error.status = 402;
error.metadata = { required: requiredCredits, available: 0 };
return error;
};
const ensureActiveProEntitlement = (accountSnapshot, requiredCredits) => {
if (!accountSnapshot || accountSnapshot.plan !== 'pro') {
throw createHardPaywallError(requiredCredits);
const ensureNotGuest = (userId, requiredCredits) => {
// Guests use the client-side demo scan; the shared 'guest' billing account
// must never consume real credits or run free AI analyses.
if (isGuest(userId)) {
const error = new Error('Sign in to use scan credits.');
error.code = 'INSUFFICIENT_CREDITS';
error.status = 402;
error.metadata = { required: requiredCredits, available: 0 };
throw error;
}
};
@@ -731,7 +730,7 @@ app.post('/v1/scan', async (request, response) => {
getAccountSnapshot(db, userId),
getCachedCatalogEntries(db),
]);
ensureActiveProEntitlement(accountSnapshot, SCAN_PRIMARY_COST);
ensureNotGuest(userId, SCAN_PRIMARY_COST);
creditsCharged += await consumeCreditsWithIdempotency(
db,
userId,
@@ -739,7 +738,8 @@ app.post('/v1/scan', async (request, response) => {
SCAN_PRIMARY_COST,
);
const scanPlan = accountSnapshot.plan === 'pro' ? 'pro' : 'free';
// Free tier gets the same model quality; quantity (3 credits/month) is the differentiator.
const scanPlan = 'pro';
let result = pickCatalogFallback(catalogEntries, imageUri, false, { silent: true });
let usedOpenAi = false;
let rawPrimaryResult = null;
@@ -903,7 +903,7 @@ app.post('/v1/search/semantic', async (request, response) => {
}
const accountSnapshot = await getAccountSnapshot(db, userId);
ensureActiveProEntitlement(accountSnapshot, SEMANTIC_SEARCH_COST);
ensureNotGuest(userId, SEMANTIC_SEARCH_COST);
const creditsCharged = await consumeCreditsWithIdempotency(
db,
@@ -943,7 +943,11 @@ app.post('/v1/health-check', async (request, response) => {
}
const accountSnapshot = await getAccountSnapshot(db, userId);
ensureActiveProEntitlement(accountSnapshot, HEALTH_CHECK_COST);
ensureNotGuest(userId, HEALTH_CHECK_COST);
// Balance pre-check: the paid AI analysis below runs BEFORE the charge
// (failed analyses are intentionally not charged), so without this check a
// user with insufficient credits could trigger unlimited free analyses.
ensureSufficientCredits(accountSnapshot, HEALTH_CHECK_COST);
if (!isOpenAiConfigured()) {
const error = new Error('OpenAI health check is unavailable. Please configure OPENAI_API_KEY.');

View File

@@ -1,6 +1,6 @@
const { get, run } = require('./postgres');
const FREE_MONTHLY_CREDITS = 0;
const FREE_MONTHLY_CREDITS = 3;
const TRIAL_MONTHLY_CREDITS = 30;
const PRO_MONTHLY_CREDITS = 100;
const TOPUP_DEFAULT_CREDITS = 100;
@@ -255,11 +255,19 @@ const getOrCreateAccount = async (db, userId) => {
};
const getAvailableCredits = (account) => {
if (account.plan !== 'pro') return 0;
const monthlyRemaining = Math.max(0, account.monthlyAllowance - account.usedThisCycle);
return monthlyRemaining + Math.max(0, account.topupBalance);
};
// Pre-flight balance check for endpoints that must not run paid work
// (e.g. OpenAI analyses) before the actual charge happens.
const ensureSufficientCredits = (account, cost) => {
const available = getAvailableCredits(account);
if (available < cost) {
throw createInsufficientCreditsError(cost, available);
}
};
const buildBillingSummary = (account) => {
return {
entitlement: {
@@ -570,9 +578,6 @@ const syncRevenueCatWebhookEvent = async (db, eventPayload) => {
const consumeCredits = (account, cost) => {
if (cost <= 0) return 0;
if (account.plan !== 'pro') {
throw createInsufficientCreditsError(cost, 0);
}
const available = getAvailableCredits(account);
if (available < cost) {
@@ -821,6 +826,7 @@ module.exports = {
getBillingSummary,
getEndpointResponse,
getMonthlyAllowanceForPlan,
ensureSufficientCredits,
isInsufficientCreditsError,
runInTransaction,
simulatePurchase,
@@ -828,4 +834,10 @@ module.exports = {
syncRevenueCatCustomerInfo,
syncRevenueCatWebhookEvent,
storeEndpointResponse,
// exported for tests
buildDefaultAccount,
alignAccountToCurrentCycle,
getAvailableCredits,
consumeCredits,
buildBillingSummary,
};

View File

@@ -8,18 +8,18 @@
"rebuild:batches": "node scripts/rebuild-from-batches.js",
"diagnostics": "node scripts/plant-diagnostics.js",
"images:download": "node scripts/download-plant-images.js",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "node --test"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"minio": "^8.0.5",
"pg": "^8.16.3",
"sharp": "^0.34.5",
"stripe": "^20.3.1"
}
}
"dependencies": {
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"express": "^5.2.1",
"minio": "^8.0.5",
"pg": "^8.16.3",
"sharp": "^0.34.5",
"stripe": "^20.3.1"
}
}

View File

@@ -0,0 +1,90 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const {
buildDefaultAccount,
alignAccountToCurrentCycle,
getAvailableCredits,
consumeCredits,
ensureSufficientCredits,
getMonthlyAllowanceForPlan,
} = require('../lib/billing');
const NOW = new Date('2026-07-06T12:00:00Z');
const freeAccount = (overrides = {}) => ({
...buildDefaultAccount('user-1', NOW),
...overrides,
});
test('free plan gets 3 monthly credits', () => {
assert.equal(getMonthlyAllowanceForPlan('free'), 3);
assert.equal(buildDefaultAccount('u', NOW).monthlyAllowance, 3);
});
test('free account has available credits', () => {
const account = freeAccount({ monthlyAllowance: 3, usedThisCycle: 1 });
assert.equal(getAvailableCredits(account), 2);
});
test('free account topup balance counts as available', () => {
const account = freeAccount({ monthlyAllowance: 3, usedThisCycle: 3, topupBalance: 10 });
assert.equal(getAvailableCredits(account), 10);
});
test('consumeCredits charges a free account from the monthly allowance', () => {
const account = freeAccount({ monthlyAllowance: 3, usedThisCycle: 0 });
const charged = consumeCredits(account, 1);
assert.equal(charged, 1);
assert.equal(account.usedThisCycle, 1);
});
test('consumeCredits throws 402 for an exhausted free account', () => {
const account = freeAccount({ monthlyAllowance: 3, usedThisCycle: 3, topupBalance: 0 });
assert.throws(() => consumeCredits(account, 1), (error) => {
assert.equal(error.code, 'INSUFFICIENT_CREDITS');
assert.equal(error.status, 402);
assert.deepEqual(error.metadata, { required: 1, available: 0 });
return true;
});
});
test('legacy free account with allowance 0 is migrated to 3', () => {
const account = freeAccount({ monthlyAllowance: 0 });
const aligned = alignAccountToCurrentCycle(account, NOW);
assert.equal(aligned.monthlyAllowance, 3);
});
test('pro and trial allowances are unchanged', () => {
assert.equal(getMonthlyAllowanceForPlan('pro'), 100);
const trial = freeAccount({ plan: 'pro', monthlyAllowance: 30, usedThisCycle: 5 });
const aligned = alignAccountToCurrentCycle(trial, NOW);
assert.equal(aligned.monthlyAllowance, 30); // trial allowance stays allowed
assert.equal(getAvailableCredits(aligned), 25);
});
test('monthly cycle rollover resets free usage', () => {
const account = freeAccount({
monthlyAllowance: 3,
usedThisCycle: 3,
cycleEndsAt: '2026-07-01T00:00:00.000Z',
});
const aligned = alignAccountToCurrentCycle(account, NOW);
assert.equal(aligned.usedThisCycle, 0);
assert.equal(aligned.monthlyAllowance, 3);
assert.equal(getAvailableCredits(aligned), 3);
});
test('ensureSufficientCredits throws 402 when balance is below cost', () => {
const account = freeAccount({ monthlyAllowance: 3, usedThisCycle: 2, topupBalance: 0 });
assert.throws(() => ensureSufficientCredits(account, 2), (error) => {
assert.equal(error.code, 'INSUFFICIENT_CREDITS');
assert.equal(error.status, 402);
assert.deepEqual(error.metadata, { required: 2, available: 1 });
return true;
});
});
test('ensureSufficientCredits passes when balance covers cost', () => {
const account = freeAccount({ monthlyAllowance: 3, usedThisCycle: 1, topupBalance: 0 });
assert.doesNotThrow(() => ensureSufficientCredits(account, 2));
});

View File

@@ -0,0 +1,36 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import { OnboardingProgressService } from './onboardingProgressService';
const STORAGE_KEY = 'greenlens_preauth_onboarding_v1';
export type PreAuthAnswers = {
acquisitionSource?: string;
primaryGoal?: string;
experienceLevel?: string;
};
export const PreAuthOnboardingService = {
async setAnswer<K extends keyof PreAuthAnswers>(key: K, value: PreAuthAnswers[K]): Promise<void> {
const answers = await this.getAnswers();
answers[key] = value;
await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(answers));
},
async getAnswers(): Promise<PreAuthAnswers> {
try {
const raw = await AsyncStorage.getItem(STORAGE_KEY);
return raw ? (JSON.parse(raw) as PreAuthAnswers) : {};
} catch {
return {};
}
},
// Persist buffered answers into the per-user profile after sign-up/login.
async flushToProfile(userId: number): Promise<void> {
const answers = await this.getAnswers();
if (answers.acquisitionSource) OnboardingProgressService.setAcquisitionSource(userId, answers.acquisitionSource);
if (answers.primaryGoal) OnboardingProgressService.setPrimaryGoal(userId, answers.primaryGoal);
if (answers.experienceLevel) OnboardingProgressService.setExperienceLevel(userId, answers.experienceLevel);
await AsyncStorage.removeItem(STORAGE_KEY);
},
};