Skip to content

Commit f944356

Browse files
committed
fix: improve type safety, theme consistency, and Android UI issues
- Fix Android cursor visibility in dark theme by adding caret-color CSS - Update support URLs to https://typelets.com/support in mobile and web apps - Add dynamic theme-color meta tag updates for mobile browsers using oklch colors - Prevent white flash on page load with inline critical CSS and blocking JS - Fix mobile app download page theme colors and add loading state for logo - Improve type safety in MainLayout.tsx by properly converting ApiNote to Note - Fix TypeScript errors in SettingsScreen.tsx with proper type definitions
1 parent cb06edf commit f944356

File tree

15 files changed

+451
-64
lines changed

15 files changed

+451
-64
lines changed

app.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"expo": {
3+
"name": "Typelets",
4+
"slug": "typelets-app"
5+
}
6+
}

apps/mobile/v1/src/screens/SettingsScreen.tsx

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { useUser } from '@clerk/clerk-expo';
55
import { useRouter } from 'expo-router';
66
import { useTheme } from '../theme';
77
import { LIGHT_THEME_PRESETS, DARK_THEME_PRESETS } from '../theme/presets';
8-
import { Card } from '../components/ui/Card';
8+
import { Card } from '@/src/components/ui';
99
import { Ionicons } from '@expo/vector-icons';
1010
import { clearUserEncryptionData } from '../lib/encryption';
1111
import { forceGlobalMasterPasswordRefresh } from '../hooks/useMasterPassword';
@@ -21,6 +21,16 @@ interface Props {
2121
onLogout?: () => void;
2222
}
2323

24+
interface SettingItem {
25+
title: string;
26+
subtitle: string;
27+
icon: string;
28+
onPress?: (() => void) | (() => Promise<void>) | undefined;
29+
isDestructive?: boolean;
30+
toggle?: boolean;
31+
value?: boolean;
32+
}
33+
2434
export default function SettingsScreen({ onLogout }: Props) {
2535
const theme = useTheme();
2636
const { user } = useUser();
@@ -116,7 +126,7 @@ export default function SettingsScreen({ onLogout }: Props) {
116126
);
117127
};
118128

119-
const settingsItems = [
129+
const settingsItems: { section: string; items: SettingItem[] }[] = [
120130
{
121131
section: 'ACCOUNT',
122132
items: [
@@ -228,7 +238,7 @@ export default function SettingsScreen({ onLogout }: Props) {
228238
title: 'Support',
229239
subtitle: 'Get help and report issues',
230240
icon: 'help-circle-outline',
231-
onPress: () => Linking.openURL('https://github.com/typelets/typelets-app/issues'),
241+
onPress: () => Linking.openURL('https://typelets.com/support'),
232242
},
233243
{
234244
title: 'Privacy Policy',
@@ -375,11 +385,11 @@ export default function SettingsScreen({ onLogout }: Props) {
375385
</View>
376386
<View style={[styles.divider, { backgroundColor: theme.colors.border }]} />
377387
<View style={[styles.bottomSheetContent, { paddingTop: 16 }]}>
378-
{[
379-
{ mode: 'light', title: 'Light', subtitle: 'Always use light theme', icon: 'sunny-outline' },
380-
{ mode: 'dark', title: 'Dark', subtitle: 'Always use dark theme', icon: 'moon-outline' },
381-
{ mode: 'system', title: 'System', subtitle: 'Follow system setting', icon: 'phone-portrait-outline' }
382-
].map((option) => (
388+
{([
389+
{ mode: 'light' as const, title: 'Light', subtitle: 'Always use light theme', icon: 'sunny-outline' },
390+
{ mode: 'dark' as const, title: 'Dark', subtitle: 'Always use dark theme', icon: 'moon-outline' },
391+
{ mode: 'system' as const, title: 'System', subtitle: 'Follow system setting', icon: 'phone-portrait-outline' }
392+
] as const).map((option) => (
383393
<TouchableOpacity
384394
key={option.mode}
385395
style={[styles.optionItem, { backgroundColor: theme.colors.card, borderColor: theme.colors.border }]}
@@ -450,7 +460,7 @@ export default function SettingsScreen({ onLogout }: Props) {
450460
{ backgroundColor: theme.colors.card, borderColor: theme.colors.border }
451461
]}
452462
onPress={() => {
453-
theme.setLightTheme(preset.id);
463+
theme.setLightTheme(preset.id as any);
454464
}}
455465
>
456466
<View style={[styles.colorPreview, {
@@ -490,7 +500,7 @@ export default function SettingsScreen({ onLogout }: Props) {
490500
{ backgroundColor: theme.colors.card, borderColor: theme.colors.border }
491501
]}
492502
onPress={() => {
493-
theme.setDarkTheme(preset.id);
503+
theme.setDarkTheme(preset.id as any);
494504
}}
495505
>
496506
<View style={[styles.colorPreview, {
@@ -657,7 +667,7 @@ export default function SettingsScreen({ onLogout }: Props) {
657667
</BottomSheetModal>
658668

659669
{/* Usage Bottom Sheet */}
660-
<UsageBottomSheet sheetRef={usageSheetRef} snapPoints={usageSnapPoints} />
670+
<UsageBottomSheet sheetRef={usageSheetRef as React.RefObject<BottomSheetModal>} snapPoints={usageSnapPoints} />
661671

662672
</SafeAreaView>
663673
);
@@ -686,6 +696,7 @@ const styles = StyleSheet.create({
686696
width: 34,
687697
},
688698
headerDivider: {
699+
// @ts-ignore - StyleSheet.hairlineWidth is intentionally used for height (ultra-thin divider)
689700
height: StyleSheet.hairlineWidth,
690701
},
691702
scrollView: {
@@ -922,4 +933,4 @@ const styles = StyleSheet.create({
922933
marginBottom: 12,
923934
marginTop: 8,
924935
},
925-
});
936+
});

eas.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"cli": {
3+
"version": ">= 16.20.0",
4+
"appVersionSource": "remote"
5+
},
6+
"build": {
7+
"development": {
8+
"developmentClient": true,
9+
"distribution": "internal"
10+
},
11+
"preview": {
12+
"distribution": "internal"
13+
},
14+
"production": {
15+
"autoIncrement": true
16+
}
17+
},
18+
"submit": {
19+
"production": {}
20+
}
21+
}

index.html

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
<!-- Favicons and Icons -->
1313
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
1414
<link rel="shortcut icon" href="/favicon.ico" />
15-
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
1615
<link rel="manifest" href="/manifest.json" />
1716
<!-- Theme and App Meta -->
1817
<meta name="theme-color" content="#3b82f6" />
@@ -46,9 +45,60 @@
4645
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
4746
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&display=swap" rel="stylesheet">
4847
<link href="https://fonts.googleapis.com/css2?family=Geist:wght@100..900&display=swap" rel="stylesheet">
48+
<!-- Prevent flash of white background -->
49+
<style>
50+
/* Inline critical styles to prevent flash */
51+
:root {
52+
--background: oklch(1 0 0);
53+
--foreground: oklch(0.145 0 0);
54+
}
55+
html {
56+
background-color: oklch(0.145 0 0);
57+
color-scheme: dark;
58+
}
59+
/* noinspection CssUnusedSymbol */
60+
html.light {
61+
--background: oklch(1 0 0);
62+
--foreground: oklch(0.145 0 0);
63+
background-color: oklch(1 0 0);
64+
color-scheme: light;
65+
}
66+
/* noinspection CssUnusedSymbol */
67+
html.dark {
68+
--background: oklch(0.145 0 0);
69+
--foreground: oklch(0.985 0 0);
70+
background-color: oklch(0.145 0 0);
71+
color-scheme: dark;
72+
}
73+
body {
74+
margin: 0;
75+
padding: 0;
76+
background-color: var(--background);
77+
}
78+
</style>
79+
<script>
80+
(function() {
81+
const storageKey = 'typelets-ui-theme';
82+
const theme = localStorage.getItem(storageKey) || 'system';
83+
let resolvedTheme = theme;
84+
85+
if (theme === 'system') {
86+
resolvedTheme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
87+
}
88+
89+
document.documentElement.classList.add(resolvedTheme);
90+
91+
// Update theme-color meta tag
92+
const metaThemeColor = document.querySelector('meta[name="theme-color"]');
93+
if (metaThemeColor) {
94+
metaThemeColor.setAttribute('content', resolvedTheme === 'dark' ? 'oklch(0.145 0 0)' : 'oklch(1 0 0)');
95+
}
96+
})();
97+
</script>
4998
</head>
5099
<body>
51100
<div id="root"></div>
101+
<!-- noinspection HtmlUnknownTarget -->
52102
<script type="module" src="/src/main.tsx"></script>
53103
</body>
54104
</html>

src/App.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import {
99
useUser,
1010
} from '@clerk/clerk-react';
1111
import Index from '@/components/common/SEO';
12+
import { MobileAppDownload } from '@/components/common/MobileAppDownload';
13+
import { useIsMobileDevice } from '@/hooks/useIsMobile';
1214
import { SEO_CONFIG } from '@/constants';
1315
import { api } from '@/lib/api/api.ts';
1416
import { fileService } from '@/services/fileService';
@@ -20,6 +22,7 @@ function AppContent() {
2022
const { getToken, isSignedIn } = useAuth();
2123
const { user } = useUser();
2224
const previousUserId = useRef<string | null>(null);
25+
const isMobileDevice = useIsMobileDevice();
2326

2427
useEffect(() => {
2528
api.setTokenProvider(getToken);
@@ -42,6 +45,10 @@ function AppContent() {
4245
const isSignInPage = window.location.pathname === '/sign-in';
4346
const isSignUpPage = window.location.pathname === '/sign-up';
4447

48+
if (isMobileDevice && !isSignedIn) {
49+
return <MobileAppDownload />;
50+
}
51+
4552
if (isSignInPage) {
4653
return (
4754
<div className="flex min-h-screen items-center justify-center">
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { useState, useEffect } from 'react';
2+
import { X, Smartphone } from 'lucide-react';
3+
import { Button } from '@/components/ui/button';
4+
import { useMobilePlatform } from '@/hooks/useIsMobile';
5+
6+
const ANDROID_STORE_URL = 'https://play.google.com/store/apps/details?id=com.typelets.notes&pcampaignid=web_share';
7+
const BANNER_DISMISSED_KEY = 'typelets_mobile_banner_dismissed';
8+
9+
export function MobileAppBanner() {
10+
const platform = useMobilePlatform();
11+
const [isDismissed, setIsDismissed] = useState(true);
12+
13+
useEffect(() => {
14+
// Check if banner was previously dismissed
15+
const dismissed = localStorage.getItem(BANNER_DISMISSED_KEY);
16+
if (!dismissed && platform) {
17+
setIsDismissed(false);
18+
}
19+
}, [platform]);
20+
21+
const handleDismiss = () => {
22+
setIsDismissed(true);
23+
localStorage.setItem(BANNER_DISMISSED_KEY, 'true');
24+
};
25+
26+
const handleDownload = () => {
27+
if (platform === 'android') {
28+
window.open(ANDROID_STORE_URL, '_blank');
29+
}
30+
// iOS will show "coming soon" message
31+
};
32+
33+
if (isDismissed || !platform) {
34+
return null;
35+
}
36+
37+
return (
38+
<div className="bg-primary text-primary-foreground fixed top-0 left-0 right-0 z-50 border-b shadow-lg">
39+
<div className="mx-auto flex max-w-7xl items-center justify-between gap-4 p-4">
40+
<div className="flex flex-1 items-center gap-3">
41+
<Smartphone className="h-6 w-6 flex-shrink-0" />
42+
<div className="min-w-0 flex-1">
43+
<p className="text-sm font-semibold">Get the Typelets Mobile App</p>
44+
<p className="text-xs opacity-90">
45+
{platform === 'android'
46+
? 'Better experience on Android'
47+
: 'iOS app coming soon'}
48+
</p>
49+
</div>
50+
</div>
51+
52+
<div className="flex items-center gap-2">
53+
{platform === 'android' ? (
54+
<Button
55+
size="sm"
56+
variant="secondary"
57+
onClick={handleDownload}
58+
className="h-8 whitespace-nowrap text-xs"
59+
>
60+
Download
61+
</Button>
62+
) : (
63+
<span className="text-xs font-medium opacity-90">Coming Soon</span>
64+
)}
65+
<Button
66+
size="sm"
67+
variant="ghost"
68+
onClick={handleDismiss}
69+
className="h-8 w-8 p-0"
70+
>
71+
<X className="h-4 w-4" />
72+
</Button>
73+
</div>
74+
</div>
75+
</div>
76+
);
77+
}

0 commit comments

Comments
 (0)