Refactor Settings, Login, and Server URL screens to use Tailwind CSS for styling; remove unused styles and theme properties; integrate NativeWind for utility-first styling approach; update theme context to support dark mode; enhance accessibility and maintainability.

This commit is contained in:
Nawaz Dhandala
2026-02-12 21:31:38 +00:00
parent 3a514969dc
commit 2b64dd0b1d
42 changed files with 1889 additions and 3486 deletions

View File

@@ -6,12 +6,12 @@
"orientation": "portrait",
"icon": "./assets/icon.png",
"scheme": "oneuptime",
"userInterfaceStyle": "dark",
"userInterfaceStyle": "automatic",
"newArchEnabled": true,
"splash": {
"image": "./assets/splash-icon.png",
"resizeMode": "contain",
"backgroundColor": "#0D1117"
"backgroundColor": "#FFFFFF"
},
"ios": {
"supportsTablet": true,

View File

@@ -0,0 +1,7 @@
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: ["nativewind/babel"],
};
};

83
MobileApp/global.css Normal file
View File

@@ -0,0 +1,83 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--color-bg-primary: #FFFFFF;
--color-bg-secondary: #F9FAFB;
--color-bg-tertiary: #F3F4F6;
--color-bg-elevated: #FFFFFF;
--color-border-default: #E5E7EB;
--color-border-subtle: #F3F4F6;
--color-text-primary: #111827;
--color-text-secondary: #6B7280;
--color-text-tertiary: #9CA3AF;
--color-text-inverse: #FFFFFF;
--color-severity-critical: #CF222E;
--color-severity-critical-bg: #CF222E1A;
--color-severity-major: #BC4C00;
--color-severity-major-bg: #BC4C001A;
--color-severity-minor: #9A6700;
--color-severity-minor-bg: #9A67001A;
--color-severity-warning: #BF8700;
--color-severity-warning-bg: #BF87001A;
--color-severity-info: #0969DA;
--color-severity-info-bg: #0969DA1A;
--color-state-created: #CF222E;
--color-state-acknowledged: #9A6700;
--color-state-resolved: #1A7F37;
--color-state-investigating: #BC4C00;
--color-state-muted: #8C959F;
--color-oncall-active: #1A7F37;
--color-oncall-active-bg: #1A7F371A;
--color-oncall-inactive: #8C959F;
--color-oncall-inactive-bg: #8C959F1A;
--color-action-primary: #6366F1;
--color-action-primary-pressed: #4F46E5;
--color-action-destructive: #CF222E;
--color-action-destructive-pressed: #A40E26;
--color-status-success: #1A7F37;
--color-status-success-bg: #1A7F371A;
--color-status-error: #CF222E;
--color-status-error-bg: #CF222E1A;
}
.dark {
--color-bg-primary: #0D1117;
--color-bg-secondary: #161B22;
--color-bg-tertiary: #21262D;
--color-bg-elevated: #1C2128;
--color-border-default: #30363D;
--color-border-subtle: #21262D;
--color-text-primary: #E6EDF3;
--color-text-secondary: #8B949E;
--color-text-tertiary: #6E7681;
--color-text-inverse: #0D1117;
--color-severity-critical: #F85149;
--color-severity-critical-bg: #F8514926;
--color-severity-major: #F0883E;
--color-severity-major-bg: #F0883E26;
--color-severity-minor: #D29922;
--color-severity-minor-bg: #D2992226;
--color-severity-warning: #E3B341;
--color-severity-warning-bg: #E3B34126;
--color-severity-info: #58A6FF;
--color-severity-info-bg: #58A6FF26;
--color-state-created: #F85149;
--color-state-acknowledged: #D29922;
--color-state-resolved: #3FB950;
--color-state-investigating: #F0883E;
--color-state-muted: #6E7681;
--color-oncall-active: #3FB950;
--color-oncall-active-bg: #3FB95026;
--color-oncall-inactive: #6E7681;
--color-oncall-inactive-bg: #6E768126;
--color-action-primary: #6366F1;
--color-action-primary-pressed: #4F46E5;
--color-action-destructive: #F85149;
--color-action-destructive-pressed: #DA3633;
--color-status-success: #3FB950;
--color-status-success-bg: #3FB95026;
--color-status-error: #F85149;
--color-status-error-bg: #F8514926;
}

View File

@@ -0,0 +1,6 @@
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require("nativewind/metro");
const config = getDefaultConfig(__dirname);
module.exports = withNativeWind(config, { input: "./global.css" });

3
MobileApp/nativewind-env.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
/// <reference types="nativewind/types" />
// NOTE: This file should not be edited and should be committed with your source code. It is generated by NativeWind.

File diff suppressed because it is too large Load Diff

View File

@@ -29,13 +29,16 @@
"expo-splash-screen": "^31.0.13",
"expo-status-bar": "~3.0.9",
"expo-system-ui": "~6.0.9",
"nativewind": "^4.2.1",
"postcss": "^8.5.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-keychain": "^10.0.0",
"react-native-safe-area-context": "^5.6.2",
"react-native-screens": "~4.16.0",
"react-native-web": "^0.21.0"
"react-native-web": "^0.21.0",
"tailwindcss": "^3.4.19"
},
"devDependencies": {
"@types/react": "~19.1.0",

View File

@@ -1,5 +1,6 @@
import "../global.css";
import React from "react";
import { View, StyleSheet, ViewStyle } from "react-native";
import { View } from "react-native";
import { StatusBar } from "expo-status-bar";
import { QueryClient } from "@tanstack/react-query";
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client";
@@ -29,12 +30,7 @@ function AppContent(): React.JSX.Element {
const { theme } = useTheme();
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<View className="flex-1 bg-bg-primary">
<StatusBar style={theme.isDark ? "light" : "dark"} />
<RootNavigator />
<OfflineBanner />
@@ -58,9 +54,3 @@ export default function App(): React.JSX.Element {
</PersistQueryClientProvider>
);
}
const styles: { container: ViewStyle } = StyleSheet.create({
container: {
flex: 1,
},
});

View File

@@ -6,7 +6,6 @@ import {
TouchableOpacity,
Modal,
ActivityIndicator,
StyleSheet,
KeyboardAvoidingView,
Platform,
} from "react-native";
@@ -49,35 +48,17 @@ export default function AddNoteModal({
onRequestClose={handleClose}
>
<KeyboardAvoidingView
style={styles.overlay}
className="flex-1 justify-end"
style={{ backgroundColor: "rgba(0,0,0,0.4)" }}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<View
style={[
styles.container,
{
backgroundColor: theme.colors.backgroundPrimary,
},
]}
>
<Text
style={[
theme.typography.titleMedium,
{ color: theme.colors.textPrimary, marginBottom: 16 },
]}
>
<View className="rounded-t-3xl p-5 pb-9 bg-bg-primary">
<Text className="text-title-md text-text-primary mb-4">
Add Note
</Text>
<TextInput
style={[
styles.input,
{
backgroundColor: theme.colors.backgroundPrimary,
color: theme.colors.textPrimary,
borderColor: theme.colors.borderDefault,
},
]}
className="min-h-[120px] rounded-[14px] border p-3 text-[15px] bg-bg-primary text-text-primary border-border-default"
placeholder="Add a note..."
placeholderTextColor={theme.colors.textTertiary}
value={noteText}
@@ -87,39 +68,25 @@ export default function AddNoteModal({
editable={!isSubmitting}
/>
<View style={styles.buttonRow}>
<View className="flex-row gap-3 mt-4">
<TouchableOpacity
style={[
styles.button,
{
backgroundColor: theme.colors.backgroundTertiary,
borderColor: theme.colors.borderSubtle,
borderWidth: 1,
},
]}
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[48px] bg-bg-tertiary border border-border-subtle"
onPress={handleClose}
disabled={isSubmitting}
>
<Text
style={[
styles.buttonText,
{ color: theme.colors.textSecondary },
]}
>
<Text className="text-[15px] font-bold text-text-secondary">
Cancel
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[
styles.button,
{
backgroundColor:
noteText.trim() && !isSubmitting
? theme.colors.actionPrimary
: theme.colors.backgroundTertiary,
},
]}
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[48px]"
style={{
backgroundColor:
noteText.trim() && !isSubmitting
? theme.colors.actionPrimary
: theme.colors.backgroundTertiary,
}}
onPress={handleSubmit}
disabled={!noteText.trim() || isSubmitting}
>
@@ -130,14 +97,12 @@ export default function AddNoteModal({
/>
) : (
<Text
style={[
styles.buttonText,
{
color: noteText.trim()
? theme.colors.textInverse
: theme.colors.textTertiary,
},
]}
className="text-[15px] font-bold"
style={{
color: noteText.trim()
? theme.colors.textInverse
: theme.colors.textTertiary,
}}
>
Submit
</Text>
@@ -149,41 +114,3 @@ export default function AddNoteModal({
</Modal>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
overlay: {
flex: 1,
backgroundColor: "rgba(0,0,0,0.4)",
justifyContent: "flex-end",
},
container: {
borderTopLeftRadius: 24,
borderTopRightRadius: 24,
padding: 20,
paddingBottom: 36,
},
input: {
minHeight: 120,
borderRadius: 14,
borderWidth: 1,
padding: 12,
fontSize: 15,
},
buttonRow: {
flexDirection: "row",
gap: 12,
marginTop: 16,
},
button: {
flex: 1,
paddingVertical: 14,
borderRadius: 14,
alignItems: "center",
justifyContent: "center",
minHeight: 48,
},
buttonText: {
fontSize: 15,
fontWeight: "700",
},
});

View File

@@ -1,5 +1,5 @@
import React from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
import { View, Text, TouchableOpacity } from "react-native";
import { useTheme } from "../theme";
import { rgbToHex } from "../utils/color";
import { formatRelativeTime } from "../utils/date";
@@ -28,49 +28,34 @@ export default function AlertCard({
return (
<TouchableOpacity
style={[
styles.card,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
className="p-[18px] rounded-2xl mb-3 bg-bg-elevated shadow-sm"
onPress={onPress}
activeOpacity={0.7}
accessibilityRole="button"
accessibilityLabel={`Alert ${alert.alertNumberWithPrefix || alert.alertNumber}, ${alert.title}. State: ${alert.currentAlertState?.name ?? "unknown"}. Severity: ${alert.alertSeverity?.name ?? "unknown"}.`}
>
<View style={styles.topRow}>
<Text style={[styles.number, { color: theme.colors.textTertiary }]}>
<View className="flex-row justify-between items-center mb-1.5">
<Text className="text-[13px] font-semibold text-text-tertiary">
{alert.alertNumberWithPrefix || `#${alert.alertNumber}`}
</Text>
<Text style={[styles.time, { color: theme.colors.textTertiary }]}>
{timeString}
</Text>
<Text className="text-xs text-text-tertiary">{timeString}</Text>
</View>
<Text
style={[
theme.typography.bodyLarge,
{ color: theme.colors.textPrimary, fontWeight: "600" },
]}
className="text-body-lg text-text-primary font-semibold"
numberOfLines={2}
>
{alert.title}
</Text>
<View style={styles.badgeRow}>
<View className="flex-row flex-wrap gap-2 mt-2.5">
{alert.currentAlertState ? (
<View
style={[
styles.badge,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
>
<View style={[styles.dot, { backgroundColor: stateColor }]} />
<Text
style={[styles.badgeText, { color: theme.colors.textPrimary }]}
>
<View className="flex-row items-center px-2 py-1 rounded-md bg-bg-tertiary">
<View
className="w-2 h-2 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text className="text-xs font-semibold text-text-primary">
{alert.currentAlertState.name}
</Text>
</View>
@@ -78,9 +63,13 @@ export default function AlertCard({
{alert.alertSeverity ? (
<View
style={[styles.badge, { backgroundColor: severityColor + "26" }]}
className="flex-row items-center px-2 py-1 rounded-md"
style={{ backgroundColor: severityColor + "26" }}
>
<Text style={[styles.badgeText, { color: severityColor }]}>
<Text
className="text-xs font-semibold"
style={{ color: severityColor }}
>
{alert.alertSeverity.name}
</Text>
</View>
@@ -88,61 +77,10 @@ export default function AlertCard({
</View>
{alert.monitor ? (
<Text
style={[styles.monitor, { color: theme.colors.textSecondary }]}
numberOfLines={1}
>
<Text className="text-xs text-text-secondary mt-2" numberOfLines={1}>
{alert.monitor.name}
</Text>
) : null}
</TouchableOpacity>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
card: {
padding: 18,
borderRadius: 16,
marginBottom: 12,
},
topRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 6,
},
number: {
fontSize: 13,
fontWeight: "600",
},
time: {
fontSize: 12,
},
badgeRow: {
flexDirection: "row",
flexWrap: "wrap",
gap: 8,
marginTop: 10,
},
badge: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 6,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 6,
},
badgeText: {
fontSize: 12,
fontWeight: "600",
},
monitor: {
fontSize: 12,
marginTop: 8,
},
});

View File

@@ -1,5 +1,5 @@
import React from "react";
import { View, Text, StyleSheet } from "react-native";
import { View, Text } from "react-native";
import { useTheme } from "../theme";
type EmptyIcon = "incidents" | "alerts" | "episodes" | "notes" | "default";
@@ -10,22 +10,32 @@ interface EmptyStateProps {
icon?: EmptyIcon;
}
function EmptyIcon({
function EmptyIconView({
icon,
color,
}: {
icon: EmptyIcon;
color: string;
}): React.JSX.Element {
/*
* Simple geometric SVG-style icons using View primitives
* Monochrome, clean, professional — not cartoon/playful
*/
if (icon === "incidents") {
return (
<View style={styles.iconContainer}>
<View style={[styles.iconShield, { borderColor: color }]}>
<View style={[styles.iconCheckmark, { backgroundColor: color }]} />
<View className="w-16 h-16 items-center justify-center">
<View
className="w-[44px] h-[52px] rounded-md items-center justify-center"
style={{
borderWidth: 1.5,
borderColor: color,
borderBottomLeftRadius: 22,
borderBottomRightRadius: 22,
}}
>
<View
className="w-4 h-[3px] rounded-sm"
style={{
backgroundColor: color,
transform: [{ rotate: "-45deg" }],
}}
/>
</View>
</View>
);
@@ -33,9 +43,21 @@ function EmptyIcon({
if (icon === "alerts") {
return (
<View style={styles.iconContainer}>
<View style={[styles.iconBell, { borderColor: color }]}>
<View style={[styles.iconBellClapper, { backgroundColor: color }]} />
<View className="w-16 h-16 items-center justify-center">
<View
className="w-9 h-9 items-center justify-end pb-1"
style={{
borderWidth: 1.5,
borderColor: color,
borderRadius: 18,
borderBottomLeftRadius: 4,
borderBottomRightRadius: 4,
}}
>
<View
className="w-2 h-2 rounded-full"
style={{ backgroundColor: color }}
/>
</View>
</View>
);
@@ -43,20 +65,29 @@ function EmptyIcon({
if (icon === "episodes") {
return (
<View style={styles.iconContainer}>
<View style={[styles.iconStack, { borderColor: color }]} />
<View className="w-16 h-16 items-center justify-center">
<View
style={[styles.iconStackBack, { borderColor: color, opacity: 0.4 }]}
className="w-10 h-8 rounded-lg absolute top-3"
style={{ borderWidth: 1.5, borderColor: color }}
/>
<View
className="w-8 h-7 rounded-md absolute top-1.5"
style={{ borderWidth: 1.5, borderColor: color, opacity: 0.4 }}
/>
</View>
);
}
// Default: simple circle with line through it
return (
<View style={styles.iconContainer}>
<View style={[styles.iconCircle, { borderColor: color }]}>
<View style={[styles.iconLine, { backgroundColor: color }]} />
<View className="w-16 h-16 items-center justify-center">
<View
className="w-12 h-12 rounded-full items-center justify-center"
style={{ borderWidth: 1.5, borderColor: color }}
>
<View
className="w-5 h-0.5 rounded-sm"
style={{ backgroundColor: color }}
/>
</View>
</View>
);
@@ -70,116 +101,16 @@ export default function EmptyState({
const { theme } = useTheme();
return (
<View style={styles.container}>
<EmptyIcon icon={icon} color={theme.colors.textTertiary} />
<Text
style={[
theme.typography.titleSmall,
{
color: theme.colors.textPrimary,
textAlign: "center",
marginTop: 20,
},
]}
>
<View className="flex-1 items-center justify-center px-10 py-20">
<EmptyIconView icon={icon} color={theme.colors.textTertiary} />
<Text className="text-title-sm text-text-primary text-center mt-5">
{title}
</Text>
{subtitle ? (
<Text
style={[
theme.typography.bodyMedium,
{
color: theme.colors.textSecondary,
textAlign: "center",
marginTop: theme.spacing.sm,
lineHeight: 20,
},
]}
>
<Text className="text-body-md text-text-secondary text-center mt-2 leading-5">
{subtitle}
</Text>
) : null}
</View>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 40,
paddingVertical: 80,
},
iconContainer: {
width: 64,
height: 64,
alignItems: "center",
justifyContent: "center",
},
// Shield icon (incidents)
iconShield: {
width: 44,
height: 52,
borderWidth: 1.5,
borderRadius: 6,
borderBottomLeftRadius: 22,
borderBottomRightRadius: 22,
alignItems: "center",
justifyContent: "center",
},
iconCheckmark: {
width: 16,
height: 3,
borderRadius: 1.5,
transform: [{ rotate: "-45deg" }],
},
// Bell icon (alerts)
iconBell: {
width: 36,
height: 36,
borderWidth: 1.5,
borderRadius: 18,
borderBottomLeftRadius: 4,
borderBottomRightRadius: 4,
alignItems: "center",
justifyContent: "flex-end",
paddingBottom: 4,
},
iconBellClapper: {
width: 8,
height: 8,
borderRadius: 4,
},
// Stack icon (episodes)
iconStack: {
width: 40,
height: 32,
borderWidth: 1.5,
borderRadius: 8,
position: "absolute",
top: 12,
},
iconStackBack: {
width: 32,
height: 28,
borderWidth: 1.5,
borderRadius: 6,
position: "absolute",
top: 6,
},
// Default circle icon
iconCircle: {
width: 48,
height: 48,
borderWidth: 1.5,
borderRadius: 24,
alignItems: "center",
justifyContent: "center",
},
iconLine: {
width: 20,
height: 2,
borderRadius: 1,
},
});

View File

@@ -1,5 +1,5 @@
import React from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
import { View, Text, TouchableOpacity } from "react-native";
import { useTheme } from "../theme";
import { rgbToHex } from "../utils/color";
import { formatRelativeTime } from "../utils/date";
@@ -56,47 +56,32 @@ export default function EpisodeCard(
return (
<TouchableOpacity
style={[
styles.card,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
className="p-[18px] rounded-2xl mb-3 bg-bg-elevated shadow-sm"
onPress={onPress}
activeOpacity={0.7}
>
<View style={styles.topRow}>
<Text style={[styles.number, { color: theme.colors.textTertiary }]}>
<View className="flex-row justify-between items-center mb-1.5">
<Text className="text-[13px] font-semibold text-text-tertiary">
{episode.episodeNumberWithPrefix || `#${episode.episodeNumber}`}
</Text>
<Text style={[styles.time, { color: theme.colors.textTertiary }]}>
{timeString}
</Text>
<Text className="text-xs text-text-tertiary">{timeString}</Text>
</View>
<Text
style={[
theme.typography.bodyLarge,
{ color: theme.colors.textPrimary, fontWeight: "600" },
]}
className="text-body-lg text-text-primary font-semibold"
numberOfLines={2}
>
{episode.title}
</Text>
<View style={styles.badgeRow}>
<View className="flex-row flex-wrap gap-2 mt-2.5">
{state ? (
<View
style={[
styles.badge,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
>
<View style={[styles.dot, { backgroundColor: stateColor }]} />
<Text
style={[styles.badgeText, { color: theme.colors.textPrimary }]}
>
<View className="flex-row items-center px-2 py-1 rounded-md bg-bg-tertiary">
<View
className="w-2 h-2 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text className="text-xs font-semibold text-text-primary">
{state.name}
</Text>
</View>
@@ -104,9 +89,13 @@ export default function EpisodeCard(
{severity ? (
<View
style={[styles.badge, { backgroundColor: severityColor + "26" }]}
className="flex-row items-center px-2 py-1 rounded-md"
style={{ backgroundColor: severityColor + "26" }}
>
<Text style={[styles.badgeText, { color: severityColor }]}>
<Text
className="text-xs font-semibold"
style={{ color: severityColor }}
>
{severity.name}
</Text>
</View>
@@ -114,9 +103,7 @@ export default function EpisodeCard(
</View>
{childCount > 0 ? (
<Text
style={[styles.childCount, { color: theme.colors.textSecondary }]}
>
<Text className="text-xs text-text-secondary mt-2">
{childCount} {type === "incident" ? "incident" : "alert"}
{childCount !== 1 ? "s" : ""}
</Text>
@@ -124,51 +111,3 @@ export default function EpisodeCard(
</TouchableOpacity>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
card: {
padding: 18,
borderRadius: 16,
marginBottom: 12,
},
topRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 6,
},
number: {
fontSize: 13,
fontWeight: "600",
},
time: {
fontSize: 12,
},
badgeRow: {
flexDirection: "row",
flexWrap: "wrap",
gap: 8,
marginTop: 10,
},
badge: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 6,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 6,
},
badgeText: {
fontSize: 12,
fontWeight: "600",
},
childCount: {
fontSize: 12,
marginTop: 8,
},
});

View File

@@ -1,5 +1,5 @@
import React from "react";
import { View, Text, StyleSheet, TouchableOpacity } from "react-native";
import { View, Text, TouchableOpacity } from "react-native";
import { useTheme } from "../theme";
import { rgbToHex } from "../utils/color";
import { formatRelativeTime } from "../utils/date";
@@ -31,49 +31,34 @@ export default function IncidentCard({
return (
<TouchableOpacity
style={[
styles.card,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
className="p-[18px] rounded-2xl mb-3 bg-bg-elevated shadow-sm"
onPress={onPress}
activeOpacity={0.7}
accessibilityRole="button"
accessibilityLabel={`Incident ${incident.incidentNumberWithPrefix || incident.incidentNumber}, ${incident.title}. State: ${incident.currentIncidentState?.name ?? "unknown"}. Severity: ${incident.incidentSeverity?.name ?? "unknown"}.`}
>
<View style={styles.topRow}>
<Text style={[styles.number, { color: theme.colors.textTertiary }]}>
<View className="flex-row justify-between items-center mb-1.5">
<Text className="text-[13px] font-semibold text-text-tertiary">
{incident.incidentNumberWithPrefix || `#${incident.incidentNumber}`}
</Text>
<Text style={[styles.time, { color: theme.colors.textTertiary }]}>
{timeString}
</Text>
<Text className="text-xs text-text-tertiary">{timeString}</Text>
</View>
<Text
style={[
theme.typography.bodyLarge,
{ color: theme.colors.textPrimary, fontWeight: "600" },
]}
className="text-body-lg text-text-primary font-semibold"
numberOfLines={2}
>
{incident.title}
</Text>
<View style={styles.badgeRow}>
<View className="flex-row flex-wrap gap-2 mt-2.5">
{incident.currentIncidentState ? (
<View
style={[
styles.badge,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
>
<View style={[styles.dot, { backgroundColor: stateColor }]} />
<Text
style={[styles.badgeText, { color: theme.colors.textPrimary }]}
>
<View className="flex-row items-center px-2 py-1 rounded-md bg-bg-tertiary">
<View
className="w-2 h-2 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text className="text-xs font-semibold text-text-primary">
{incident.currentIncidentState.name}
</Text>
</View>
@@ -81,9 +66,13 @@ export default function IncidentCard({
{incident.incidentSeverity ? (
<View
style={[styles.badge, { backgroundColor: severityColor + "26" }]}
className="flex-row items-center px-2 py-1 rounded-md"
style={{ backgroundColor: severityColor + "26" }}
>
<Text style={[styles.badgeText, { color: severityColor }]}>
<Text
className="text-xs font-semibold"
style={{ color: severityColor }}
>
{incident.incidentSeverity.name}
</Text>
</View>
@@ -91,10 +80,7 @@ export default function IncidentCard({
</View>
{monitorCount > 0 ? (
<Text
style={[styles.monitors, { color: theme.colors.textSecondary }]}
numberOfLines={1}
>
<Text className="text-xs text-text-secondary mt-2" numberOfLines={1}>
{incident.monitors
.map((m: NamedEntity) => {
return m.name;
@@ -105,51 +91,3 @@ export default function IncidentCard({
</TouchableOpacity>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
card: {
padding: 18,
borderRadius: 16,
marginBottom: 12,
},
topRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 6,
},
number: {
fontSize: 13,
fontWeight: "600",
},
time: {
fontSize: 12,
},
badgeRow: {
flexDirection: "row",
flexWrap: "wrap",
gap: 8,
marginTop: 10,
},
badge: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 6,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 6,
},
badgeText: {
fontSize: 12,
fontWeight: "600",
},
monitors: {
fontSize: 12,
marginTop: 8,
},
});

View File

@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from "react";
import { View, Text, StyleSheet, Animated } from "react-native";
import { View, Text, Animated } from "react-native";
import { useTheme } from "../theme";
import { useNetworkStatus } from "../hooks/useNetworkStatus";
@@ -25,51 +25,18 @@ export default function OfflineBanner(): React.JSX.Element | null {
return (
<Animated.View
style={[
styles.container,
{
backgroundColor: theme.colors.statusError,
transform: [{ translateY: slideAnim }],
},
]}
className="absolute top-0 left-0 right-0 z-[100] pt-[50px] pb-2 px-4"
style={{
backgroundColor: theme.colors.statusError,
transform: [{ translateY: slideAnim }],
}}
>
<View style={styles.content}>
<View style={styles.dot} />
<Text style={[styles.text, { color: "#FFFFFF" }]}>
<View className="flex-row items-center justify-center">
<View className="w-1.5 h-1.5 rounded-full bg-white mr-2 opacity-80" />
<Text className="text-[13px] font-semibold tracking-tight text-white">
No internet connection
</Text>
</View>
</Animated.View>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
container: {
position: "absolute",
top: 0,
left: 0,
right: 0,
zIndex: 100,
paddingTop: 50,
paddingBottom: 8,
paddingHorizontal: 16,
},
content: {
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
},
dot: {
width: 6,
height: 6,
borderRadius: 3,
backgroundColor: "#FFFFFF",
marginRight: 8,
opacity: 0.8,
},
text: {
fontSize: 13,
fontWeight: "600",
letterSpacing: 0.2,
},
});

View File

@@ -1,6 +1,5 @@
import React from "react";
import { View, Text, StyleSheet } from "react-native";
import { useTheme } from "../theme";
import { View, Text } from "react-native";
interface ProjectBadgeProps {
name: string;
@@ -11,35 +10,15 @@ export default function ProjectBadge({
name,
color,
}: ProjectBadgeProps): React.JSX.Element {
const { theme } = useTheme();
const dotColor: string = color || theme.colors.actionPrimary;
return (
<View style={styles.container}>
<View style={[styles.dot, { backgroundColor: dotColor }]} />
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textSecondary },
]}
numberOfLines={1}
>
<View className="flex-row items-center">
<View
className="w-2 h-2 rounded-full mr-1.5"
style={color ? { backgroundColor: color } : undefined}
/>
<Text className="text-body-sm text-text-secondary" numberOfLines={1}>
{name}
</Text>
</View>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
container: {
flexDirection: "row",
alignItems: "center",
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 6,
},
});

View File

@@ -1,5 +1,5 @@
import React from "react";
import { View, Text, StyleSheet } from "react-native";
import { View, Text } from "react-native";
import { useTheme } from "../theme";
export type SeverityLevel = "critical" | "major" | "minor" | "warning" | "info";
@@ -42,24 +42,16 @@ export default function SeverityBadge({
const displayLabel: string = label || severity;
return (
<View style={[styles.badge, { backgroundColor: colors.bg }]}>
<Text style={[styles.text, { color: colors.text }]}>
<View
className="px-2 py-1 rounded-md self-start"
style={{ backgroundColor: colors.bg }}
>
<Text
className="text-xs font-semibold tracking-wide"
style={{ color: colors.text }}
>
{displayLabel.toUpperCase()}
</Text>
</View>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
badge: {
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 6,
alignSelf: "flex-start",
},
text: {
fontSize: 12,
fontWeight: "600",
letterSpacing: 0.5,
},
});

View File

@@ -1,11 +1,5 @@
import React, { useEffect, useRef } from "react";
import {
View,
StyleSheet,
Animated,
DimensionValue,
AccessibilityInfo,
} from "react-native";
import { View, Animated, DimensionValue, AccessibilityInfo } from "react-native";
import { useTheme } from "../theme";
interface SkeletonCardProps {
@@ -60,36 +54,19 @@ export default function SkeletonCard({
if (variant === "compact") {
return (
<Animated.View
style={[
styles.card,
{
backgroundColor: theme.colors.backgroundElevated,
opacity,
},
]}
className="p-[18px] rounded-2xl mb-3"
style={{
backgroundColor: theme.colors.backgroundElevated,
opacity,
}}
accessibilityLabel="Loading content"
accessibilityRole="progressbar"
>
<View style={styles.compactRow}>
<View
style={[
styles.compactBadge,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
/>
<View
style={[
styles.compactTime,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
/>
<View className="flex-row justify-between items-center mb-2.5">
<View className="h-3.5 w-12 rounded bg-bg-tertiary" />
<View className="h-3 w-8 rounded bg-bg-tertiary" />
</View>
<View
style={[
styles.titleLine,
{ backgroundColor: theme.colors.backgroundTertiary, width: "75%" },
]}
/>
<View className="h-[18px] rounded w-3/4 mb-3 bg-bg-tertiary" />
</Animated.View>
);
}
@@ -97,57 +74,28 @@ export default function SkeletonCard({
if (variant === "detail") {
return (
<Animated.View
style={[styles.detailContainer, { opacity }]}
className="p-5"
style={{ opacity }}
accessibilityLabel="Loading content"
accessibilityRole="progressbar"
>
{/* Badge row */}
<View style={styles.badgeRow}>
<View
style={[
styles.badgeSkeleton,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
/>
<View
style={[
styles.badgeSkeleton,
{ backgroundColor: theme.colors.backgroundTertiary, width: 64 },
]}
/>
<View className="flex-row gap-2 mb-3">
<View className="h-6 w-20 rounded-md bg-bg-tertiary" />
<View className="h-6 w-16 rounded-md bg-bg-tertiary" />
</View>
{/* Title */}
<View className="h-6 w-4/5 rounded mb-5 bg-bg-tertiary" />
<View
style={[
styles.detailTitle,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
/>
{/* Detail card */}
<View
style={[
styles.detailCard,
{
backgroundColor: theme.colors.backgroundSecondary,
borderColor: theme.colors.borderSubtle,
},
]}
className="rounded-2xl p-4"
style={{
backgroundColor: theme.colors.backgroundSecondary,
borderColor: theme.colors.borderSubtle,
}}
>
{Array.from({ length: 3 }).map((_: unknown, index: number) => {
return (
<View key={index} style={styles.detailRow}>
<View
style={[
styles.detailLabel,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
/>
<View
style={[
styles.detailValue,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
/>
<View key={index} className="flex-row mb-3">
<View className="h-3.5 w-20 rounded mr-4 bg-bg-tertiary" />
<View className="h-3.5 w-[120px] rounded bg-bg-tertiary" />
</View>
);
})}
@@ -158,67 +106,31 @@ export default function SkeletonCard({
return (
<Animated.View
style={[
styles.card,
{
backgroundColor: theme.colors.backgroundSecondary,
borderColor: theme.colors.borderSubtle,
opacity,
},
]}
className="p-[18px] rounded-2xl mb-3"
style={{
backgroundColor: theme.colors.backgroundSecondary,
borderColor: theme.colors.borderSubtle,
opacity,
}}
accessibilityLabel="Loading content"
accessibilityRole="progressbar"
>
{/* Top row: badge + time */}
<View style={styles.topRow}>
<View
style={[
styles.numberSkeleton,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
/>
<View
style={[
styles.timeSkeleton,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
/>
<View className="flex-row justify-between items-center mb-3">
<View className="h-3.5 w-12 rounded bg-bg-tertiary" />
<View className="h-3 w-9 rounded bg-bg-tertiary" />
</View>
{/* Title */}
<View
style={[
styles.titleLine,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
/>
{/* Badge row */}
<View style={styles.badgeRow}>
<View
style={[
styles.badgeSkeleton,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
/>
<View
style={[
styles.badgeSkeleton,
{ backgroundColor: theme.colors.backgroundTertiary, width: 56 },
]}
/>
<View className="h-[18px] rounded w-[70%] mb-3 bg-bg-tertiary" />
<View className="flex-row gap-2 mb-3">
<View className="h-6 w-20 rounded-md bg-bg-tertiary" />
<View className="h-6 w-14 rounded-md bg-bg-tertiary" />
</View>
{/* Body lines */}
{Array.from({ length: Math.max(lines - 1, 1) }).map(
(_: unknown, index: number) => {
return (
<View
key={index}
style={[
styles.line,
{
backgroundColor: theme.colors.backgroundTertiary,
width: lineWidths[index % lineWidths.length],
},
]}
className="h-3 rounded mb-2 bg-bg-tertiary"
style={{ width: lineWidths[index % lineWidths.length] }}
/>
);
},
@@ -226,94 +138,3 @@ export default function SkeletonCard({
</Animated.View>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
card: {
padding: 18,
borderRadius: 16,
marginBottom: 12,
},
topRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 12,
},
numberSkeleton: {
height: 14,
width: 48,
borderRadius: 4,
},
timeSkeleton: {
height: 12,
width: 36,
borderRadius: 4,
},
titleLine: {
height: 18,
borderRadius: 4,
width: "70%",
marginBottom: 12,
},
badgeRow: {
flexDirection: "row",
gap: 8,
marginBottom: 12,
},
badgeSkeleton: {
height: 24,
width: 80,
borderRadius: 6,
},
line: {
height: 12,
borderRadius: 4,
marginBottom: 8,
},
// Compact variant
compactRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 10,
},
compactBadge: {
height: 14,
width: 48,
borderRadius: 4,
},
compactTime: {
height: 12,
width: 32,
borderRadius: 4,
},
// Detail variant
detailContainer: {
padding: 20,
},
detailTitle: {
height: 24,
width: "80%",
borderRadius: 4,
marginBottom: 20,
},
detailCard: {
borderRadius: 16,
padding: 16,
},
detailRow: {
flexDirection: "row",
marginBottom: 12,
},
detailLabel: {
height: 14,
width: 80,
borderRadius: 4,
marginRight: 16,
},
detailValue: {
height: 14,
width: 120,
borderRadius: 4,
},
});

View File

@@ -1,5 +1,5 @@
import React from "react";
import { View, Text, StyleSheet } from "react-native";
import { View, Text } from "react-native";
import { useTheme } from "../theme";
export type StateType =
@@ -32,39 +32,14 @@ export default function StateBadge({
const displayLabel: string = label || state;
return (
<View
style={[
styles.badge,
{
backgroundColor: theme.colors.backgroundTertiary,
},
]}
>
<View style={[styles.dot, { backgroundColor: color }]} />
<Text style={[styles.text, { color: theme.colors.textPrimary }]}>
<View className="flex-row items-center px-2 py-1 rounded-md self-start bg-bg-tertiary">
<View
className="w-2 h-2 rounded-full mr-1.5"
style={{ backgroundColor: color }}
/>
<Text className="text-xs font-semibold text-text-primary">
{displayLabel.charAt(0).toUpperCase() + displayLabel.slice(1)}
</Text>
</View>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
badge: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 8,
paddingVertical: 4,
borderRadius: 6,
alignSelf: "flex-start",
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 6,
},
text: {
fontSize: 12,
fontWeight: "600",
},
});

View File

@@ -2,7 +2,6 @@ import React, { useRef } from "react";
import {
View,
Text,
StyleSheet,
Animated,
PanResponder,
type GestureResponderEvent,
@@ -48,7 +47,6 @@ export default function SwipeableCard({
_: GestureResponderEvent,
gestureState: PanResponderGestureState,
) => {
// Limit swipe range
const maxSwipe: number = 120;
let dx: number = gestureState.dx;
if (!rightAction && dx < 0) {
@@ -60,7 +58,6 @@ export default function SwipeableCard({
dx = Math.max(-maxSwipe, Math.min(maxSwipe, dx));
translateX.setValue(dx);
// Haptic feedback at threshold
if (Math.abs(dx) >= SWIPE_THRESHOLD && !hasTriggeredHaptic.current) {
hasTriggeredHaptic.current = true;
mediumImpact();
@@ -97,34 +94,35 @@ export default function SwipeableCard({
).current;
return (
<View style={styles.container}>
<View className="overflow-hidden rounded-xl">
{/* Background actions */}
<View style={styles.actionsContainer}>
<View className="absolute inset-0 flex-row justify-between items-center">
{leftAction ? (
<View
style={[styles.actionLeft, { backgroundColor: leftAction.color }]}
className="flex-1 h-full justify-center pl-5 rounded-xl"
style={{ backgroundColor: leftAction.color }}
>
<Text style={styles.actionText}>{leftAction.label}</Text>
<Text className="text-white text-sm font-bold tracking-tight">
{leftAction.label}
</Text>
</View>
) : null}
{rightAction ? (
<View
style={[styles.actionRight, { backgroundColor: rightAction.color }]}
className="flex-1 h-full justify-center items-end pr-5 rounded-xl"
style={{ backgroundColor: rightAction.color }}
>
<Text style={styles.actionText}>{rightAction.label}</Text>
<Text className="text-white text-sm font-bold tracking-tight">
{rightAction.label}
</Text>
</View>
) : null}
</View>
{/* Foreground content */}
<Animated.View
style={[
styles.foreground,
{
backgroundColor: theme.colors.backgroundPrimary,
transform: [{ translateX }],
},
]}
className="z-[1] bg-bg-primary"
style={{ transform: [{ translateX }] }}
{...panResponder.panHandlers}
>
{children}
@@ -132,40 +130,3 @@ export default function SwipeableCard({
</View>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
container: {
overflow: "hidden",
borderRadius: 12,
},
actionsContainer: {
...StyleSheet.absoluteFillObject,
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
actionLeft: {
flex: 1,
height: "100%",
justifyContent: "center",
paddingLeft: 20,
borderRadius: 12,
},
actionRight: {
flex: 1,
height: "100%",
justifyContent: "center",
alignItems: "flex-end",
paddingRight: 20,
borderRadius: 12,
},
actionText: {
color: "#FFFFFF",
fontSize: 14,
fontWeight: "700",
letterSpacing: 0.3,
},
foreground: {
zIndex: 1,
},
});

View File

@@ -15,7 +15,7 @@ import AuthStackNavigator from "./AuthStackNavigator";
import MainTabNavigator from "./MainTabNavigator";
import ProjectSelectionScreen from "../screens/ProjectSelectionScreen";
import BiometricLockScreen from "../screens/BiometricLockScreen";
import { View, ActivityIndicator, StyleSheet } from "react-native";
import { View, ActivityIndicator } from "react-native";
const prefix: string = Linking.createURL("/");
@@ -96,12 +96,7 @@ export default function RootNavigator(): React.JSX.Element {
if (isLoading || !biometricChecked) {
return (
<View
style={[
styles.loading,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<View className="flex-1 items-center justify-center bg-bg-primary">
<ActivityIndicator size="large" color={theme.colors.actionPrimary} />
</View>
);
@@ -130,12 +125,7 @@ export default function RootNavigator(): React.JSX.Element {
if (isLoadingProjects) {
return (
<View
style={[
styles.loading,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<View className="flex-1 items-center justify-center bg-bg-primary">
<ActivityIndicator size="large" color={theme.colors.actionPrimary} />
</View>
);
@@ -158,11 +148,3 @@ export default function RootNavigator(): React.JSX.Element {
</NavigationContainer>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
loading: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
});

View File

@@ -7,7 +7,6 @@ import {
ActivityIndicator,
RefreshControl,
Alert,
StyleSheet,
} from "react-native";
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import { useTheme } from "../theme";
@@ -128,9 +127,7 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
if (isLoading) {
return (
<View
style={[{ flex: 1, backgroundColor: theme.colors.backgroundPrimary }]}
>
<View className="flex-1 bg-bg-primary">
<SkeletonCard variant="detail" />
</View>
);
@@ -138,18 +135,8 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
if (!alert) {
return (
<View
style={[
styles.centered,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textSecondary },
]}
>
<View className="flex-1 items-center justify-center bg-bg-primary">
<Text className="text-body-md text-text-secondary">
Alert not found.
</Text>
</View>
@@ -180,46 +167,32 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
return (
<ScrollView
style={[{ backgroundColor: theme.colors.backgroundPrimary }]}
contentContainerStyle={styles.content}
className="bg-bg-primary"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
refreshControl={
<RefreshControl refreshing={false} onRefresh={onRefresh} />
}
>
{/* Header */}
<View
style={[
styles.numberBadge,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
>
<Text style={[styles.number, { color: theme.colors.textSecondary }]}>
<View className="self-start px-2.5 py-1 rounded-lg bg-bg-tertiary">
<Text className="text-sm font-semibold text-text-secondary">
{alert.alertNumberWithPrefix || `#${alert.alertNumber}`}
</Text>
</View>
<Text
style={[
theme.typography.titleLarge,
{ color: theme.colors.textPrimary, marginTop: 4 },
]}
>
<Text className="text-title-lg text-text-primary mt-1">
{alert.title}
</Text>
{/* Badges */}
<View style={styles.badgeRow}>
<View className="flex-row flex-wrap gap-2 mt-3">
{alert.currentAlertState ? (
<View
style={[
styles.badge,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
>
<View style={[styles.dot, { backgroundColor: stateColor }]} />
<Text
style={[styles.badgeText, { color: theme.colors.textPrimary }]}
>
<View className="flex-row items-center px-2.5 py-[5px] rounded-md bg-bg-tertiary">
<View
className="w-2 h-2 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text className="text-[13px] font-semibold text-text-primary">
{alert.currentAlertState.name}
</Text>
</View>
@@ -227,9 +200,13 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
{alert.alertSeverity ? (
<View
style={[styles.badge, { backgroundColor: severityColor + "26" }]}
className="flex-row items-center px-2.5 py-[5px] rounded-md"
style={{ backgroundColor: severityColor + "26" }}
>
<Text style={[styles.badgeText, { color: severityColor }]}>
<Text
className="text-[13px] font-semibold"
style={{ color: severityColor }}
>
{alert.alertSeverity.name}
</Text>
</View>
@@ -238,69 +215,38 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
{/* Description */}
{alert.description ? (
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
Description
</Text>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textPrimary },
]}
>
<Text className="text-body-md text-text-primary">
{alert.description}
</Text>
</View>
) : null}
{/* Details */}
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
Details
</Text>
<View
style={[
styles.detailCard,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
>
<View style={styles.detailRow}>
<Text
style={[styles.detailLabel, { color: theme.colors.textTertiary }]}
>
<View className="rounded-2xl p-4 bg-bg-elevated shadow-sm">
<View className="flex-row mb-2.5">
<Text className="text-sm w-[90px] text-text-tertiary">
Created
</Text>
<Text
style={[styles.detailValue, { color: theme.colors.textPrimary }]}
>
<Text className="text-sm text-text-primary">
{formatDateTime(alert.createdAt)}
</Text>
</View>
{alert.monitor ? (
<View style={styles.detailRow}>
<Text
style={[
styles.detailLabel,
{ color: theme.colors.textTertiary },
]}
>
<View className="flex-row mb-2.5">
<Text className="text-sm w-[90px] text-text-tertiary">
Monitor
</Text>
<Text
style={[
styles.detailValue,
{ color: theme.colors.textPrimary },
]}
>
<Text className="text-sm text-text-primary">
{alert.monitor.name}
</Text>
</View>
@@ -310,20 +256,15 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
{/* State Change Actions */}
{!isResolved ? (
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
Actions
</Text>
<View style={styles.actionRow}>
<View className="flex-row gap-3">
{!isAcknowledged && !isResolved && acknowledgeState ? (
<TouchableOpacity
style={[
styles.actionButton,
theme.shadows.md,
{ backgroundColor: theme.colors.stateAcknowledged },
]}
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
style={{ backgroundColor: theme.colors.stateAcknowledged }}
onPress={() => {
return handleStateChange(
acknowledgeState._id,
@@ -340,12 +281,7 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
color={theme.colors.textInverse}
/>
) : (
<Text
style={[
styles.actionButtonText,
{ color: theme.colors.textInverse },
]}
>
<Text className="text-[15px] font-bold text-text-inverse">
Acknowledge
</Text>
)}
@@ -354,11 +290,8 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
{resolveState ? (
<TouchableOpacity
style={[
styles.actionButton,
theme.shadows.md,
{ backgroundColor: theme.colors.stateResolved },
]}
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
style={{ backgroundColor: theme.colors.stateResolved }}
onPress={() => {
return handleStateChange(resolveState._id, resolveState.name);
}}
@@ -372,12 +305,7 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
color={theme.colors.textInverse}
/>
) : (
<Text
style={[
styles.actionButtonText,
{ color: theme.colors.textInverse },
]}
>
<Text className="text-[15px] font-bold text-text-inverse">
Resolve
</Text>
)}
@@ -389,10 +317,8 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
{/* State Timeline */}
{timeline && timeline.length > 0 ? (
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
State Timeline
</Text>
{timeline.map((entry: StateTimelineItem) => {
@@ -402,32 +328,17 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
return (
<View
key={entry._id}
style={[
styles.timelineEntry,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
className="flex-row items-center p-3.5 rounded-xl mb-2 bg-bg-elevated shadow-sm"
>
<View
style={[styles.timelineDot, { backgroundColor: entryColor }]}
className="w-2.5 h-2.5 rounded-full mr-3"
style={{ backgroundColor: entryColor }}
/>
<View style={styles.timelineInfo}>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textPrimary, fontWeight: "600" },
]}
>
<View className="flex-1">
<Text className="text-body-md text-text-primary font-semibold">
{entry.alertState?.name ?? "Unknown"}
</Text>
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
{formatDateTime(entry.createdAt)}
</Text>
</View>
@@ -438,31 +349,19 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
) : null}
{/* Internal Notes */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text
style={[
styles.sectionTitle,
{ color: theme.colors.textSecondary, marginBottom: 0 },
]}
>
<View className="mt-6">
<View className="flex-row justify-between items-center mb-2.5">
<Text className="text-[13px] font-semibold uppercase tracking-wide text-text-secondary">
Internal Notes
</Text>
<TouchableOpacity
style={[
styles.addNoteButton,
{ backgroundColor: theme.colors.actionPrimary },
]}
className="px-3 py-1.5 rounded-lg"
style={{ backgroundColor: theme.colors.actionPrimary }}
onPress={() => {
return setNoteModalVisible(true);
}}
>
<Text
style={[
styles.addNoteButtonText,
{ color: theme.colors.textInverse },
]}
>
<Text className="text-[13px] font-semibold text-text-inverse">
Add Note
</Text>
</TouchableOpacity>
@@ -473,39 +372,18 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
return (
<View
key={note._id}
style={[
styles.noteCard,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
className="rounded-xl p-3.5 mb-2 bg-bg-elevated shadow-sm"
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textPrimary },
]}
>
<Text className="text-body-md text-text-primary">
{note.note}
</Text>
<View style={styles.noteMeta}>
<View className="flex-row justify-between mt-2">
{note.createdByUser ? (
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
{note.createdByUser.name}
</Text>
) : null}
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
{formatDateTime(note.createdAt)}
</Text>
</View>
@@ -515,12 +393,7 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
: null}
{notes && notes.length === 0 ? (
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
No notes yet.
</Text>
) : null}
@@ -537,130 +410,3 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
</ScrollView>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
centered: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
content: {
padding: 20,
paddingBottom: 40,
},
numberBadge: {
alignSelf: "flex-start",
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 8,
},
number: {
fontSize: 14,
fontWeight: "600",
},
badgeRow: {
flexDirection: "row",
flexWrap: "wrap",
gap: 8,
marginTop: 12,
},
badge: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 6,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 6,
},
badgeText: {
fontSize: 13,
fontWeight: "600",
},
section: {
marginTop: 24,
},
sectionHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 10,
},
sectionTitle: {
fontSize: 13,
fontWeight: "600",
textTransform: "uppercase",
letterSpacing: 0.5,
marginBottom: 10,
},
detailCard: {
borderRadius: 16,
padding: 16,
},
detailRow: {
flexDirection: "row",
marginBottom: 10,
},
detailLabel: {
fontSize: 14,
width: 90,
},
detailValue: {
fontSize: 14,
},
actionRow: {
flexDirection: "row",
gap: 12,
},
actionButton: {
flex: 1,
paddingVertical: 14,
borderRadius: 14,
alignItems: "center",
justifyContent: "center",
minHeight: 50,
},
actionButtonText: {
fontSize: 15,
fontWeight: "700",
},
timelineEntry: {
flexDirection: "row",
alignItems: "center",
padding: 14,
borderRadius: 12,
marginBottom: 8,
},
timelineDot: {
width: 10,
height: 10,
borderRadius: 5,
marginRight: 12,
},
timelineInfo: {
flex: 1,
},
addNoteButton: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 8,
},
addNoteButtonText: {
fontSize: 13,
fontWeight: "600",
},
noteCard: {
borderRadius: 12,
padding: 14,
marginBottom: 8,
},
noteMeta: {
flexDirection: "row",
justifyContent: "space-between",
marginTop: 8,
},
});

View File

@@ -7,7 +7,6 @@ import {
ActivityIndicator,
RefreshControl,
Alert,
StyleSheet,
} from "react-native";
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import { useTheme } from "../theme";
@@ -135,9 +134,7 @@ export default function AlertEpisodeDetailScreen({
if (isLoading) {
return (
<View
style={[{ flex: 1, backgroundColor: theme.colors.backgroundPrimary }]}
>
<View className="flex-1 bg-bg-primary">
<SkeletonCard variant="detail" />
</View>
);
@@ -145,18 +142,8 @@ export default function AlertEpisodeDetailScreen({
if (!episode) {
return (
<View
style={[
styles.centered,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textSecondary },
]}
>
<View className="flex-1 items-center justify-center bg-bg-primary">
<Text className="text-body-md text-text-secondary">
Episode not found.
</Text>
</View>
@@ -186,46 +173,32 @@ export default function AlertEpisodeDetailScreen({
return (
<ScrollView
style={[{ backgroundColor: theme.colors.backgroundPrimary }]}
contentContainerStyle={styles.content}
className="bg-bg-primary"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
refreshControl={
<RefreshControl refreshing={false} onRefresh={onRefresh} />
}
>
{/* Header */}
<View
style={[
styles.numberBadge,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
>
<Text style={[styles.number, { color: theme.colors.textSecondary }]}>
<View className="self-start px-2.5 py-1 rounded-lg bg-bg-tertiary">
<Text className="text-sm font-semibold text-text-secondary">
{episode.episodeNumberWithPrefix || `#${episode.episodeNumber}`}
</Text>
</View>
<Text
style={[
theme.typography.titleLarge,
{ color: theme.colors.textPrimary, marginTop: 4 },
]}
>
<Text className="text-title-lg text-text-primary mt-1">
{episode.title}
</Text>
{/* Badges */}
<View style={styles.badgeRow}>
<View className="flex-row flex-wrap gap-2 mt-3">
{episode.currentAlertState ? (
<View
style={[
styles.badge,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
>
<View style={[styles.dot, { backgroundColor: stateColor }]} />
<Text
style={[styles.badgeText, { color: theme.colors.textPrimary }]}
>
<View className="flex-row items-center px-2.5 py-[5px] rounded-md bg-bg-tertiary">
<View
className="w-2 h-2 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text className="text-[13px] font-semibold text-text-primary">
{episode.currentAlertState.name}
</Text>
</View>
@@ -233,9 +206,13 @@ export default function AlertEpisodeDetailScreen({
{episode.alertSeverity ? (
<View
style={[styles.badge, { backgroundColor: severityColor + "26" }]}
className="flex-row items-center px-2.5 py-[5px] rounded-md"
style={{ backgroundColor: severityColor + "26" }}
>
<Text style={[styles.badgeText, { color: severityColor }]}>
<Text
className="text-[13px] font-semibold"
style={{ color: severityColor }}
>
{episode.alertSeverity.name}
</Text>
</View>
@@ -244,62 +221,37 @@ export default function AlertEpisodeDetailScreen({
{/* Description */}
{episode.description ? (
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
Description
</Text>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textPrimary },
]}
>
<Text className="text-body-md text-text-primary">
{episode.description}
</Text>
</View>
) : null}
{/* Details */}
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
Details
</Text>
<View
style={[
styles.detailCard,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
>
<View style={styles.detailRow}>
<Text
style={[styles.detailLabel, { color: theme.colors.textTertiary }]}
>
<View className="rounded-2xl p-4 bg-bg-elevated shadow-sm">
<View className="flex-row mb-2.5">
<Text className="text-sm w-[90px] text-text-tertiary">
Created
</Text>
<Text
style={[styles.detailValue, { color: theme.colors.textPrimary }]}
>
<Text className="text-sm text-text-primary">
{formatDateTime(episode.createdAt)}
</Text>
</View>
<View style={styles.detailRow}>
<Text
style={[styles.detailLabel, { color: theme.colors.textTertiary }]}
>
<View className="flex-row mb-2.5">
<Text className="text-sm w-[90px] text-text-tertiary">
Alerts
</Text>
<Text
style={[styles.detailValue, { color: theme.colors.textPrimary }]}
>
<Text className="text-sm text-text-primary">
{episode.alertCount ?? 0}
</Text>
</View>
@@ -308,20 +260,15 @@ export default function AlertEpisodeDetailScreen({
{/* State Change Actions */}
{!isResolved ? (
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
Actions
</Text>
<View style={styles.actionRow}>
<View className="flex-row gap-3">
{!isAcknowledged && !isResolved && acknowledgeState ? (
<TouchableOpacity
style={[
styles.actionButton,
theme.shadows.md,
{ backgroundColor: theme.colors.stateAcknowledged },
]}
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
style={{ backgroundColor: theme.colors.stateAcknowledged }}
onPress={() => {
return handleStateChange(
acknowledgeState._id,
@@ -336,12 +283,7 @@ export default function AlertEpisodeDetailScreen({
color={theme.colors.textInverse}
/>
) : (
<Text
style={[
styles.actionButtonText,
{ color: theme.colors.textInverse },
]}
>
<Text className="text-[15px] font-bold text-text-inverse">
Acknowledge
</Text>
)}
@@ -350,11 +292,8 @@ export default function AlertEpisodeDetailScreen({
{resolveState ? (
<TouchableOpacity
style={[
styles.actionButton,
theme.shadows.md,
{ backgroundColor: theme.colors.stateResolved },
]}
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
style={{ backgroundColor: theme.colors.stateResolved }}
onPress={() => {
return handleStateChange(resolveState._id, resolveState.name);
}}
@@ -366,12 +305,7 @@ export default function AlertEpisodeDetailScreen({
color={theme.colors.textInverse}
/>
) : (
<Text
style={[
styles.actionButtonText,
{ color: theme.colors.textInverse },
]}
>
<Text className="text-[15px] font-bold text-text-inverse">
Resolve
</Text>
)}
@@ -383,10 +317,8 @@ export default function AlertEpisodeDetailScreen({
{/* State Timeline */}
{timeline && timeline.length > 0 ? (
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
State Timeline
</Text>
{timeline.map((entry: StateTimelineItem) => {
@@ -396,35 +328,17 @@ export default function AlertEpisodeDetailScreen({
return (
<View
key={entry._id}
style={[
styles.timelineEntry,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
className="flex-row items-center p-3.5 rounded-xl mb-2 bg-bg-elevated shadow-sm"
>
<View
style={[styles.timelineDot, { backgroundColor: entryColor }]}
className="w-2.5 h-2.5 rounded-full mr-3"
style={{ backgroundColor: entryColor }}
/>
<View style={styles.timelineInfo}>
<Text
style={[
theme.typography.bodyMedium,
{
color: theme.colors.textPrimary,
fontWeight: "600",
},
]}
>
<View className="flex-1">
<Text className="text-body-md text-text-primary font-semibold">
{entry.alertState?.name ?? "Unknown"}
</Text>
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
{formatDateTime(entry.createdAt)}
</Text>
</View>
@@ -435,31 +349,19 @@ export default function AlertEpisodeDetailScreen({
) : null}
{/* Internal Notes */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text
style={[
styles.sectionTitle,
{ color: theme.colors.textSecondary, marginBottom: 0 },
]}
>
<View className="mt-6">
<View className="flex-row justify-between items-center mb-2.5">
<Text className="text-[13px] font-semibold uppercase tracking-wide text-text-secondary">
Internal Notes
</Text>
<TouchableOpacity
style={[
styles.addNoteButton,
{ backgroundColor: theme.colors.actionPrimary },
]}
className="px-3 py-1.5 rounded-lg"
style={{ backgroundColor: theme.colors.actionPrimary }}
onPress={() => {
return setNoteModalVisible(true);
}}
>
<Text
style={[
styles.addNoteButtonText,
{ color: theme.colors.textInverse },
]}
>
<Text className="text-[13px] font-semibold text-text-inverse">
Add Note
</Text>
</TouchableOpacity>
@@ -470,39 +372,18 @@ export default function AlertEpisodeDetailScreen({
return (
<View
key={note._id}
style={[
styles.noteCard,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
className="rounded-xl p-3.5 mb-2 bg-bg-elevated shadow-sm"
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textPrimary },
]}
>
<Text className="text-body-md text-text-primary">
{note.note}
</Text>
<View style={styles.noteMeta}>
<View className="flex-row justify-between mt-2">
{note.createdByUser ? (
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
{note.createdByUser.name}
</Text>
) : null}
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
{formatDateTime(note.createdAt)}
</Text>
</View>
@@ -512,12 +393,7 @@ export default function AlertEpisodeDetailScreen({
: null}
{notes && notes.length === 0 ? (
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
No notes yet.
</Text>
) : null}
@@ -534,130 +410,3 @@ export default function AlertEpisodeDetailScreen({
</ScrollView>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
centered: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
content: {
padding: 20,
paddingBottom: 40,
},
numberBadge: {
alignSelf: "flex-start",
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 8,
},
number: {
fontSize: 14,
fontWeight: "600",
},
badgeRow: {
flexDirection: "row",
flexWrap: "wrap",
gap: 8,
marginTop: 12,
},
badge: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 6,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 6,
},
badgeText: {
fontSize: 13,
fontWeight: "600",
},
section: {
marginTop: 24,
},
sectionHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 10,
},
sectionTitle: {
fontSize: 13,
fontWeight: "600",
textTransform: "uppercase",
letterSpacing: 0.5,
marginBottom: 10,
},
detailCard: {
borderRadius: 16,
padding: 16,
},
detailRow: {
flexDirection: "row",
marginBottom: 10,
},
detailLabel: {
fontSize: 14,
width: 90,
},
detailValue: {
fontSize: 14,
},
actionRow: {
flexDirection: "row",
gap: 12,
},
actionButton: {
flex: 1,
paddingVertical: 14,
borderRadius: 14,
alignItems: "center",
justifyContent: "center",
minHeight: 50,
},
actionButtonText: {
fontSize: 15,
fontWeight: "700",
},
timelineEntry: {
flexDirection: "row",
alignItems: "center",
padding: 14,
borderRadius: 12,
marginBottom: 8,
},
timelineDot: {
width: 10,
height: 10,
borderRadius: 5,
marginRight: 12,
},
timelineInfo: {
flex: 1,
},
addNoteButton: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 8,
},
addNoteButtonText: {
fontSize: 13,
fontWeight: "600",
},
noteCard: {
borderRadius: 12,
padding: 14,
marginBottom: 8,
},
noteMeta: {
flexDirection: "row",
justifyContent: "space-between",
marginTop: 8,
},
});

View File

@@ -5,7 +5,6 @@ import {
RefreshControl,
TouchableOpacity,
Text,
StyleSheet,
ListRenderItemInfo,
} from "react-native";
import { useNavigation } from "@react-navigation/native";
@@ -72,13 +71,8 @@ export default function AlertEpisodesScreen(): React.JSX.Element {
if (isLoading && episodes.length === 0) {
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<View style={styles.skeletonList}>
<View className="flex-1 bg-bg-primary">
<View className="p-4">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
@@ -89,36 +83,18 @@ export default function AlertEpisodesScreen(): React.JSX.Element {
if (isError) {
return (
<View
style={[
styles.centered,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textSecondary, textAlign: "center" },
]}
>
<View className="flex-1 items-center justify-center px-8 bg-bg-primary">
<Text className="text-body-md text-text-secondary text-center">
Failed to load alert episodes.
</Text>
<TouchableOpacity
style={[
styles.retryButton,
theme.shadows.md,
{ backgroundColor: theme.colors.actionPrimary },
]}
className="mt-4 px-6 py-3 rounded-[10px] shadow-md"
style={{ backgroundColor: theme.colors.actionPrimary }}
onPress={() => {
return refetch();
}}
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textInverse, fontWeight: "600" },
]}
>
<Text className="text-body-md text-text-inverse font-semibold">
Retry
</Text>
</TouchableOpacity>
@@ -127,19 +103,14 @@ export default function AlertEpisodesScreen(): React.JSX.Element {
}
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<View className="flex-1 bg-bg-primary">
<FlatList
data={episodes}
keyExtractor={(item: AlertEpisodeItem) => {
return item._id;
}}
contentContainerStyle={
episodes.length === 0 ? styles.emptyContainer : styles.list
episodes.length === 0 ? { flex: 1 } : { padding: 16 }
}
renderItem={({ item }: ListRenderItemInfo<AlertEpisodeItem>) => {
return (
@@ -168,30 +139,3 @@ export default function AlertEpisodesScreen(): React.JSX.Element {
</View>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
container: {
flex: 1,
},
centered: {
flex: 1,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 32,
},
list: {
padding: 16,
},
emptyContainer: {
flex: 1,
},
skeletonList: {
padding: 16,
},
retryButton: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 10,
},
});

View File

@@ -5,7 +5,6 @@ import {
RefreshControl,
TouchableOpacity,
Text,
StyleSheet,
ListRenderItemInfo,
} from "react-native";
import { useNavigation } from "@react-navigation/native";
@@ -103,13 +102,8 @@ export default function AlertsScreen(): React.JSX.Element {
if (isLoading && alerts.length === 0) {
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<View style={styles.skeletonList}>
<View className="flex-1 bg-bg-primary">
<View className="p-4">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
@@ -120,36 +114,18 @@ export default function AlertsScreen(): React.JSX.Element {
if (isError) {
return (
<View
style={[
styles.centered,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textSecondary, textAlign: "center" },
]}
>
<View className="flex-1 items-center justify-center px-8 bg-bg-primary">
<Text className="text-body-md text-text-secondary text-center">
Failed to load alerts.
</Text>
<TouchableOpacity
style={[
styles.retryButton,
theme.shadows.md,
{ backgroundColor: theme.colors.actionPrimary },
]}
className="mt-4 px-6 py-3 rounded-[10px] shadow-md"
style={{ backgroundColor: theme.colors.actionPrimary }}
onPress={() => {
return refetch();
}}
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textInverse, fontWeight: "600" },
]}
>
<Text className="text-body-md text-text-inverse font-semibold">
Retry
</Text>
</TouchableOpacity>
@@ -158,19 +134,14 @@ export default function AlertsScreen(): React.JSX.Element {
}
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<View className="flex-1 bg-bg-primary">
<FlatList
data={alerts}
keyExtractor={(item: AlertItem) => {
return item._id;
}}
contentContainerStyle={
alerts.length === 0 ? styles.emptyContainer : styles.list
alerts.length === 0 ? { flex: 1 } : { padding: 16 }
}
renderItem={({ item }: ListRenderItemInfo<AlertItem>) => {
return (
@@ -213,30 +184,3 @@ export default function AlertsScreen(): React.JSX.Element {
</View>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
container: {
flex: 1,
},
centered: {
flex: 1,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 32,
},
list: {
padding: 16,
},
emptyContainer: {
flex: 1,
},
skeletonList: {
padding: 16,
},
retryButton: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 10,
},
});

View File

@@ -1,5 +1,5 @@
import React, { useEffect } from "react";
import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
import { View, Text, TouchableOpacity } from "react-native";
import { useTheme } from "../theme";
import * as LocalAuthentication from "expo-local-authentication";
@@ -32,117 +32,44 @@ export default function BiometricLockScreen({
}, []);
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<View className="flex-1 items-center justify-center px-10 bg-bg-primary">
{/* Lock icon */}
<View
style={[
styles.iconContainer,
{ borderColor: theme.colors.borderDefault },
]}
className="w-20 h-20 rounded-full items-center justify-center"
style={{ borderWidth: 1.5, borderColor: theme.colors.borderDefault }}
>
<View
style={[
styles.lockBody,
{ backgroundColor: theme.colors.textTertiary },
]}
className="w-7 h-[22px] rounded mt-2"
style={{ backgroundColor: theme.colors.textTertiary }}
/>
<View
style={[
styles.lockShackle,
{ borderColor: theme.colors.textTertiary },
]}
className="w-5 h-4 absolute top-4"
style={{
borderWidth: 3,
borderBottomWidth: 0,
borderTopLeftRadius: 10,
borderTopRightRadius: 10,
borderColor: theme.colors.textTertiary,
}}
/>
</View>
<Text
style={[
theme.typography.titleMedium,
{
color: theme.colors.textPrimary,
marginTop: 24,
textAlign: "center",
},
]}
>
<Text className="text-title-md text-text-primary mt-6 text-center">
OneUptime is Locked
</Text>
<Text
style={[
theme.typography.bodyMedium,
{
color: theme.colors.textSecondary,
marginTop: 8,
textAlign: "center",
},
]}
>
<Text className="text-body-md text-text-secondary mt-2 text-center">
Use {biometricType.toLowerCase()} to unlock
</Text>
<TouchableOpacity
style={[
styles.unlockButton,
theme.shadows.md,
{ backgroundColor: theme.colors.actionPrimary },
]}
className="mt-8 py-4 px-12 rounded-[14px] min-w-[200px] items-center shadow-md"
style={{ backgroundColor: theme.colors.actionPrimary }}
onPress={authenticate}
activeOpacity={0.8}
>
<Text style={[styles.unlockButtonText, { color: "#FFFFFF" }]}>
Unlock
</Text>
<Text className="text-[17px] font-semibold text-white">Unlock</Text>
</TouchableOpacity>
</View>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
container: {
flex: 1,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 40,
},
iconContainer: {
width: 80,
height: 80,
borderRadius: 40,
borderWidth: 1.5,
alignItems: "center",
justifyContent: "center",
},
lockBody: {
width: 28,
height: 22,
borderRadius: 4,
marginTop: 8,
},
lockShackle: {
width: 20,
height: 16,
borderWidth: 3,
borderBottomWidth: 0,
borderTopLeftRadius: 10,
borderTopRightRadius: 10,
position: "absolute",
top: 16,
},
unlockButton: {
marginTop: 32,
paddingVertical: 16,
paddingHorizontal: 48,
borderRadius: 14,
minWidth: 200,
alignItems: "center",
},
unlockButtonText: {
fontSize: 17,
fontWeight: "600",
},
});

View File

@@ -5,7 +5,6 @@ import {
TouchableOpacity,
ScrollView,
RefreshControl,
StyleSheet,
} from "react-native";
import { useTheme } from "../theme";
import { useProject } from "../hooks/useProject";
@@ -35,7 +34,6 @@ function StatCard({
isLoading,
onPress,
}: StatCardProps): React.JSX.Element {
const { theme } = useTheme();
const { lightImpact } = useHaptics();
const handlePress: () => void = (): void => {
@@ -45,22 +43,19 @@ function StatCard({
return (
<TouchableOpacity
style={[
styles.summaryCard,
theme.shadows.md,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
className="flex-1 p-5 rounded-2xl items-center bg-bg-elevated shadow-md"
onPress={handlePress}
activeOpacity={0.7}
accessibilityLabel={`${count ?? 0} ${label}. Tap to view.`}
accessibilityRole="button"
>
<Text style={[styles.cardCount, { color }]}>
<Text
className="text-[40px] font-bold"
style={{ color, fontVariant: ["tabular-nums"] }}
>
{isLoading ? "--" : count ?? 0}
</Text>
<Text style={[styles.cardLabel, { color: theme.colors.textSecondary }]}>
<Text className="text-sm font-medium mt-1 text-text-secondary">
{label}
</Text>
</TouchableOpacity>
@@ -73,27 +68,15 @@ interface QuickLinkProps {
}
function QuickLink({ label, onPress }: QuickLinkProps): React.JSX.Element {
const { theme } = useTheme();
return (
<TouchableOpacity
style={[
styles.linkCard,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
className="flex-row justify-between items-center p-[18px] rounded-2xl mb-2.5 bg-bg-elevated shadow-sm"
onPress={onPress}
activeOpacity={0.7}
accessibilityRole="button"
>
<Text style={[styles.linkLabel, { color: theme.colors.textPrimary }]}>
{label}
</Text>
<Text style={[styles.chevron, { color: theme.colors.textTertiary }]}>
</Text>
<Text className="text-base font-medium text-text-primary">{label}</Text>
<Text className="text-2xl font-light text-text-tertiary">{">"}</Text>
</TouchableOpacity>
);
}
@@ -142,8 +125,8 @@ export default function HomeScreen(): React.JSX.Element {
return (
<ScrollView
style={[{ backgroundColor: theme.colors.backgroundPrimary }]}
contentContainerStyle={styles.content}
className="bg-bg-primary"
contentContainerStyle={{ padding: 24, paddingBottom: 40 }}
refreshControl={
<RefreshControl
refreshing={false}
@@ -152,27 +135,14 @@ export default function HomeScreen(): React.JSX.Element {
/>
}
>
{/* Header */}
<Text
style={[
theme.typography.titleLarge,
{ color: theme.colors.textPrimary },
]}
accessibilityRole="header"
>
<Text className="text-title-lg text-text-primary" accessibilityRole="header">
{selectedProject?.name ?? "OneUptime"}
</Text>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textSecondary, marginTop: theme.spacing.xs },
]}
>
<Text className="text-body-md text-text-secondary mt-1">
Project overview
</Text>
{/* Stats Grid */}
<View style={styles.cardRow}>
<View className="flex-row gap-3 mt-4">
<StatCard
count={incidentCount}
label="Active Incidents"
@@ -193,7 +163,7 @@ export default function HomeScreen(): React.JSX.Element {
/>
</View>
<View style={styles.cardRow}>
<View className="flex-row gap-3 mt-4">
<StatCard
count={incidentEpisodeCount}
label="Inc Episodes"
@@ -214,11 +184,8 @@ export default function HomeScreen(): React.JSX.Element {
/>
</View>
{/* Quick Links */}
<View style={styles.quickLinksSection}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-8">
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-3 ml-1 text-text-secondary">
Quick Links
</Text>
<QuickLink
@@ -249,58 +216,3 @@ export default function HomeScreen(): React.JSX.Element {
</ScrollView>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
content: {
padding: 24,
paddingBottom: 40,
},
cardRow: {
flexDirection: "row",
gap: 12,
marginTop: 16,
},
summaryCard: {
flex: 1,
padding: 20,
borderRadius: 16,
alignItems: "center",
},
cardCount: {
fontSize: 40,
fontWeight: "700",
fontVariant: ["tabular-nums"],
},
cardLabel: {
fontSize: 14,
fontWeight: "500",
marginTop: 4,
},
quickLinksSection: {
marginTop: 32,
},
sectionTitle: {
fontSize: 13,
fontWeight: "600",
textTransform: "uppercase",
letterSpacing: 0.8,
marginBottom: 12,
marginLeft: 4,
},
linkCard: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 18,
borderRadius: 16,
marginBottom: 10,
},
linkLabel: {
fontSize: 16,
fontWeight: "500",
},
chevron: {
fontSize: 24,
fontWeight: "300",
},
});

View File

@@ -7,10 +7,9 @@ import {
ActivityIndicator,
RefreshControl,
Alert,
StyleSheet,
} from "react-native";
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import { useTheme, type Theme } from "../theme";
import { useTheme } from "../theme";
import { useProject } from "../hooks/useProject";
import {
useIncidentDetail,
@@ -41,7 +40,7 @@ export default function IncidentDetailScreen({
route,
}: Props): React.JSX.Element {
const { incidentId } = route.params;
const { theme }: { theme: Theme } = useTheme();
const { theme } = useTheme();
const { selectedProject } = useProject();
const projectId: string = selectedProject?._id ?? "";
const queryClient: QueryClient = useQueryClient();
@@ -139,9 +138,7 @@ export default function IncidentDetailScreen({
if (isLoading) {
return (
<View
style={[{ flex: 1, backgroundColor: theme.colors.backgroundPrimary }]}
>
<View className="flex-1 bg-bg-primary">
<SkeletonCard variant="detail" />
</View>
);
@@ -149,18 +146,8 @@ export default function IncidentDetailScreen({
if (!incident) {
return (
<View
style={[
styles.centered,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textSecondary },
]}
>
<View className="flex-1 items-center justify-center bg-bg-primary">
<Text className="text-body-md text-text-secondary">
Incident not found.
</Text>
</View>
@@ -193,46 +180,32 @@ export default function IncidentDetailScreen({
return (
<ScrollView
style={[{ backgroundColor: theme.colors.backgroundPrimary }]}
contentContainerStyle={styles.content}
className="bg-bg-primary"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
refreshControl={
<RefreshControl refreshing={false} onRefresh={onRefresh} />
}
>
{/* Header */}
<View
style={[
styles.numberBadge,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
>
<Text style={[styles.number, { color: theme.colors.textSecondary }]}>
<View className="self-start px-2.5 py-1 rounded-lg bg-bg-tertiary">
<Text className="text-sm font-semibold text-text-secondary">
{incident.incidentNumberWithPrefix || `#${incident.incidentNumber}`}
</Text>
</View>
<Text
style={[
theme.typography.titleLarge,
{ color: theme.colors.textPrimary, marginTop: 4 },
]}
>
<Text className="text-title-lg text-text-primary mt-1">
{incident.title}
</Text>
{/* Badges */}
<View style={styles.badgeRow}>
<View className="flex-row flex-wrap gap-2 mt-3">
{incident.currentIncidentState ? (
<View
style={[
styles.badge,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
>
<View style={[styles.dot, { backgroundColor: stateColor }]} />
<Text
style={[styles.badgeText, { color: theme.colors.textPrimary }]}
>
<View className="flex-row items-center px-2.5 py-[5px] rounded-md bg-bg-tertiary">
<View
className="w-2 h-2 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text className="text-[13px] font-semibold text-text-primary">
{incident.currentIncidentState.name}
</Text>
</View>
@@ -240,9 +213,13 @@ export default function IncidentDetailScreen({
{incident.incidentSeverity ? (
<View
style={[styles.badge, { backgroundColor: severityColor + "26" }]}
className="flex-row items-center px-2.5 py-[5px] rounded-md"
style={{ backgroundColor: severityColor + "26" }}
>
<Text style={[styles.badgeText, { color: severityColor }]}>
<Text
className="text-[13px] font-semibold"
style={{ color: severityColor }}
>
{incident.incidentSeverity.name}
</Text>
</View>
@@ -251,90 +228,49 @@ export default function IncidentDetailScreen({
{/* Description */}
{incident.description ? (
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
Description
</Text>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textPrimary },
]}
>
<Text className="text-body-md text-text-primary">
{incident.description}
</Text>
</View>
) : null}
{/* Details */}
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
Details
</Text>
<View
style={[
styles.detailCard,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
>
<View className="rounded-2xl p-4 bg-bg-elevated shadow-sm">
{incident.declaredAt ? (
<View style={styles.detailRow}>
<Text
style={[
styles.detailLabel,
{ color: theme.colors.textTertiary },
]}
>
<View className="flex-row mb-2.5">
<Text className="text-sm w-[90px] text-text-tertiary">
Declared
</Text>
<Text
style={[
styles.detailValue,
{ color: theme.colors.textPrimary },
]}
>
<Text className="text-sm text-text-primary">
{formatDateTime(incident.declaredAt)}
</Text>
</View>
) : null}
<View style={styles.detailRow}>
<Text
style={[styles.detailLabel, { color: theme.colors.textTertiary }]}
>
<View className="flex-row mb-2.5">
<Text className="text-sm w-[90px] text-text-tertiary">
Created
</Text>
<Text
style={[styles.detailValue, { color: theme.colors.textPrimary }]}
>
<Text className="text-sm text-text-primary">
{formatDateTime(incident.createdAt)}
</Text>
</View>
{incident.monitors?.length > 0 ? (
<View style={styles.detailRow}>
<Text
style={[
styles.detailLabel,
{ color: theme.colors.textTertiary },
]}
>
<View className="flex-row mb-2.5">
<Text className="text-sm w-[90px] text-text-tertiary">
Monitors
</Text>
<Text
style={[
styles.detailValue,
{ color: theme.colors.textPrimary, flex: 1 },
]}
>
<Text className="text-sm text-text-primary flex-1">
{incident.monitors
.map((m: NamedEntity) => {
return m.name;
@@ -348,20 +284,15 @@ export default function IncidentDetailScreen({
{/* State Change Actions */}
{!isResolved ? (
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
Actions
</Text>
<View style={styles.actionRow}>
<View className="flex-row gap-3">
{!isAcknowledged && !isResolved && acknowledgeState ? (
<TouchableOpacity
style={[
styles.actionButton,
theme.shadows.md,
{ backgroundColor: theme.colors.stateAcknowledged },
]}
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
style={{ backgroundColor: theme.colors.stateAcknowledged }}
onPress={() => {
return handleStateChange(
acknowledgeState._id,
@@ -378,12 +309,7 @@ export default function IncidentDetailScreen({
color={theme.colors.textInverse}
/>
) : (
<Text
style={[
styles.actionButtonText,
{ color: theme.colors.textInverse },
]}
>
<Text className="text-[15px] font-bold text-text-inverse">
Acknowledge
</Text>
)}
@@ -392,11 +318,8 @@ export default function IncidentDetailScreen({
{resolveState ? (
<TouchableOpacity
style={[
styles.actionButton,
theme.shadows.md,
{ backgroundColor: theme.colors.stateResolved },
]}
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
style={{ backgroundColor: theme.colors.stateResolved }}
onPress={() => {
return handleStateChange(resolveState._id, resolveState.name);
}}
@@ -410,12 +333,7 @@ export default function IncidentDetailScreen({
color={theme.colors.textInverse}
/>
) : (
<Text
style={[
styles.actionButtonText,
{ color: theme.colors.textInverse },
]}
>
<Text className="text-[15px] font-bold text-text-inverse">
Resolve
</Text>
)}
@@ -427,10 +345,8 @@ export default function IncidentDetailScreen({
{/* State Timeline */}
{timeline && timeline.length > 0 ? (
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
State Timeline
</Text>
{timeline.map((entry: StateTimelineItem) => {
@@ -440,32 +356,17 @@ export default function IncidentDetailScreen({
return (
<View
key={entry._id}
style={[
styles.timelineEntry,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
className="flex-row items-center p-3.5 rounded-xl mb-2 bg-bg-elevated shadow-sm"
>
<View
style={[styles.timelineDot, { backgroundColor: entryColor }]}
className="w-2.5 h-2.5 rounded-full mr-3"
style={{ backgroundColor: entryColor }}
/>
<View style={styles.timelineInfo}>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textPrimary, fontWeight: "600" },
]}
>
<View className="flex-1">
<Text className="text-body-md text-text-primary font-semibold">
{entry.incidentState?.name ?? "Unknown"}
</Text>
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
{formatDateTime(entry.createdAt)}
</Text>
</View>
@@ -476,31 +377,19 @@ export default function IncidentDetailScreen({
) : null}
{/* Internal Notes */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text
style={[
styles.sectionTitle,
{ color: theme.colors.textSecondary, marginBottom: 0 },
]}
>
<View className="mt-6">
<View className="flex-row justify-between items-center mb-2.5">
<Text className="text-[13px] font-semibold uppercase tracking-wide text-text-secondary">
Internal Notes
</Text>
<TouchableOpacity
style={[
styles.addNoteButton,
{ backgroundColor: theme.colors.actionPrimary },
]}
className="px-3 py-1.5 rounded-lg"
style={{ backgroundColor: theme.colors.actionPrimary }}
onPress={() => {
return setNoteModalVisible(true);
}}
>
<Text
style={[
styles.addNoteButtonText,
{ color: theme.colors.textInverse },
]}
>
<Text className="text-[13px] font-semibold text-text-inverse">
Add Note
</Text>
</TouchableOpacity>
@@ -511,39 +400,18 @@ export default function IncidentDetailScreen({
return (
<View
key={note._id}
style={[
styles.noteCard,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
className="rounded-xl p-3.5 mb-2 bg-bg-elevated shadow-sm"
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textPrimary },
]}
>
<Text className="text-body-md text-text-primary">
{note.note}
</Text>
<View style={styles.noteMeta}>
<View className="flex-row justify-between mt-2">
{note.createdByUser ? (
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
{note.createdByUser.name}
</Text>
) : null}
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
{formatDateTime(note.createdAt)}
</Text>
</View>
@@ -553,12 +421,7 @@ export default function IncidentDetailScreen({
: null}
{notes && notes.length === 0 ? (
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
No notes yet.
</Text>
) : null}
@@ -575,130 +438,3 @@ export default function IncidentDetailScreen({
</ScrollView>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
centered: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
content: {
padding: 20,
paddingBottom: 40,
},
numberBadge: {
alignSelf: "flex-start",
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 8,
},
number: {
fontSize: 14,
fontWeight: "600",
},
badgeRow: {
flexDirection: "row",
flexWrap: "wrap",
gap: 8,
marginTop: 12,
},
badge: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 6,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 6,
},
badgeText: {
fontSize: 13,
fontWeight: "600",
},
section: {
marginTop: 24,
},
sectionHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 10,
},
sectionTitle: {
fontSize: 13,
fontWeight: "600",
textTransform: "uppercase",
letterSpacing: 0.5,
marginBottom: 10,
},
detailCard: {
borderRadius: 16,
padding: 16,
},
detailRow: {
flexDirection: "row",
marginBottom: 10,
},
detailLabel: {
fontSize: 14,
width: 90,
},
detailValue: {
fontSize: 14,
},
actionRow: {
flexDirection: "row",
gap: 12,
},
actionButton: {
flex: 1,
paddingVertical: 14,
borderRadius: 14,
alignItems: "center",
justifyContent: "center",
minHeight: 50,
},
actionButtonText: {
fontSize: 15,
fontWeight: "700",
},
timelineEntry: {
flexDirection: "row",
alignItems: "center",
padding: 14,
borderRadius: 12,
marginBottom: 8,
},
timelineDot: {
width: 10,
height: 10,
borderRadius: 5,
marginRight: 12,
},
timelineInfo: {
flex: 1,
},
addNoteButton: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 8,
},
addNoteButtonText: {
fontSize: 13,
fontWeight: "600",
},
noteCard: {
borderRadius: 12,
padding: 14,
marginBottom: 8,
},
noteMeta: {
flexDirection: "row",
justifyContent: "space-between",
marginTop: 8,
},
});

View File

@@ -7,7 +7,6 @@ import {
ActivityIndicator,
RefreshControl,
Alert,
StyleSheet,
} from "react-native";
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import { useTheme } from "../theme";
@@ -138,9 +137,7 @@ export default function IncidentEpisodeDetailScreen({
if (isLoading) {
return (
<View
style={[{ flex: 1, backgroundColor: theme.colors.backgroundPrimary }]}
>
<View className="flex-1 bg-bg-primary">
<SkeletonCard variant="detail" />
</View>
);
@@ -148,18 +145,8 @@ export default function IncidentEpisodeDetailScreen({
if (!episode) {
return (
<View
style={[
styles.centered,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textSecondary },
]}
>
<View className="flex-1 items-center justify-center bg-bg-primary">
<Text className="text-body-md text-text-secondary">
Episode not found.
</Text>
</View>
@@ -191,46 +178,32 @@ export default function IncidentEpisodeDetailScreen({
return (
<ScrollView
style={[{ backgroundColor: theme.colors.backgroundPrimary }]}
contentContainerStyle={styles.content}
className="bg-bg-primary"
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
refreshControl={
<RefreshControl refreshing={false} onRefresh={onRefresh} />
}
>
{/* Header */}
<View
style={[
styles.numberBadge,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
>
<Text style={[styles.number, { color: theme.colors.textSecondary }]}>
<View className="self-start px-2.5 py-1 rounded-lg bg-bg-tertiary">
<Text className="text-sm font-semibold text-text-secondary">
{episode.episodeNumberWithPrefix || `#${episode.episodeNumber}`}
</Text>
</View>
<Text
style={[
theme.typography.titleLarge,
{ color: theme.colors.textPrimary, marginTop: 4 },
]}
>
<Text className="text-title-lg text-text-primary mt-1">
{episode.title}
</Text>
{/* Badges */}
<View style={styles.badgeRow}>
<View className="flex-row flex-wrap gap-2 mt-3">
{episode.currentIncidentState ? (
<View
style={[
styles.badge,
{ backgroundColor: theme.colors.backgroundTertiary },
]}
>
<View style={[styles.dot, { backgroundColor: stateColor }]} />
<Text
style={[styles.badgeText, { color: theme.colors.textPrimary }]}
>
<View className="flex-row items-center px-2.5 py-[5px] rounded-md bg-bg-tertiary">
<View
className="w-2 h-2 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text className="text-[13px] font-semibold text-text-primary">
{episode.currentIncidentState.name}
</Text>
</View>
@@ -238,9 +211,13 @@ export default function IncidentEpisodeDetailScreen({
{episode.incidentSeverity ? (
<View
style={[styles.badge, { backgroundColor: severityColor + "26" }]}
className="flex-row items-center px-2.5 py-[5px] rounded-md"
style={{ backgroundColor: severityColor + "26" }}
>
<Text style={[styles.badgeText, { color: severityColor }]}>
<Text
className="text-[13px] font-semibold"
style={{ color: severityColor }}
>
{episode.incidentSeverity.name}
</Text>
</View>
@@ -249,83 +226,48 @@ export default function IncidentEpisodeDetailScreen({
{/* Description */}
{episode.description ? (
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
Description
</Text>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textPrimary },
]}
>
<Text className="text-body-md text-text-primary">
{episode.description}
</Text>
</View>
) : null}
{/* Details */}
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
Details
</Text>
<View
style={[
styles.detailCard,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
>
<View className="rounded-2xl p-4 bg-bg-elevated shadow-sm">
{episode.declaredAt ? (
<View style={styles.detailRow}>
<Text
style={[
styles.detailLabel,
{ color: theme.colors.textTertiary },
]}
>
<View className="flex-row mb-2.5">
<Text className="text-sm w-[90px] text-text-tertiary">
Declared
</Text>
<Text
style={[
styles.detailValue,
{ color: theme.colors.textPrimary },
]}
>
<Text className="text-sm text-text-primary">
{formatDateTime(episode.declaredAt)}
</Text>
</View>
) : null}
<View style={styles.detailRow}>
<Text
style={[styles.detailLabel, { color: theme.colors.textTertiary }]}
>
<View className="flex-row mb-2.5">
<Text className="text-sm w-[90px] text-text-tertiary">
Created
</Text>
<Text
style={[styles.detailValue, { color: theme.colors.textPrimary }]}
>
<Text className="text-sm text-text-primary">
{formatDateTime(episode.createdAt)}
</Text>
</View>
<View style={styles.detailRow}>
<Text
style={[styles.detailLabel, { color: theme.colors.textTertiary }]}
>
<View className="flex-row mb-2.5">
<Text className="text-sm w-[90px] text-text-tertiary">
Incidents
</Text>
<Text
style={[styles.detailValue, { color: theme.colors.textPrimary }]}
>
<Text className="text-sm text-text-primary">
{episode.incidentCount ?? 0}
</Text>
</View>
@@ -334,20 +276,15 @@ export default function IncidentEpisodeDetailScreen({
{/* State Change Actions */}
{!isResolved ? (
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
Actions
</Text>
<View style={styles.actionRow}>
<View className="flex-row gap-3">
{!isAcknowledged && !isResolved && acknowledgeState ? (
<TouchableOpacity
style={[
styles.actionButton,
theme.shadows.md,
{ backgroundColor: theme.colors.stateAcknowledged },
]}
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
style={{ backgroundColor: theme.colors.stateAcknowledged }}
onPress={() => {
return handleStateChange(
acknowledgeState._id,
@@ -362,12 +299,7 @@ export default function IncidentEpisodeDetailScreen({
color={theme.colors.textInverse}
/>
) : (
<Text
style={[
styles.actionButtonText,
{ color: theme.colors.textInverse },
]}
>
<Text className="text-[15px] font-bold text-text-inverse">
Acknowledge
</Text>
)}
@@ -376,11 +308,8 @@ export default function IncidentEpisodeDetailScreen({
{resolveState ? (
<TouchableOpacity
style={[
styles.actionButton,
theme.shadows.md,
{ backgroundColor: theme.colors.stateResolved },
]}
className="flex-1 py-3.5 rounded-[14px] items-center justify-center min-h-[50px] shadow-md"
style={{ backgroundColor: theme.colors.stateResolved }}
onPress={() => {
return handleStateChange(resolveState._id, resolveState.name);
}}
@@ -392,12 +321,7 @@ export default function IncidentEpisodeDetailScreen({
color={theme.colors.textInverse}
/>
) : (
<Text
style={[
styles.actionButtonText,
{ color: theme.colors.textInverse },
]}
>
<Text className="text-[15px] font-bold text-text-inverse">
Resolve
</Text>
)}
@@ -409,10 +333,8 @@ export default function IncidentEpisodeDetailScreen({
{/* State Timeline */}
{timeline && timeline.length > 0 ? (
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mt-6">
<Text className="text-[13px] font-semibold uppercase tracking-wide mb-2.5 text-text-secondary">
State Timeline
</Text>
{timeline.map((entry: StateTimelineItem) => {
@@ -422,35 +344,17 @@ export default function IncidentEpisodeDetailScreen({
return (
<View
key={entry._id}
style={[
styles.timelineEntry,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
className="flex-row items-center p-3.5 rounded-xl mb-2 bg-bg-elevated shadow-sm"
>
<View
style={[styles.timelineDot, { backgroundColor: entryColor }]}
className="w-2.5 h-2.5 rounded-full mr-3"
style={{ backgroundColor: entryColor }}
/>
<View style={styles.timelineInfo}>
<Text
style={[
theme.typography.bodyMedium,
{
color: theme.colors.textPrimary,
fontWeight: "600",
},
]}
>
<View className="flex-1">
<Text className="text-body-md text-text-primary font-semibold">
{entry.incidentState?.name ?? "Unknown"}
</Text>
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
{formatDateTime(entry.createdAt)}
</Text>
</View>
@@ -461,31 +365,19 @@ export default function IncidentEpisodeDetailScreen({
) : null}
{/* Internal Notes */}
<View style={styles.section}>
<View style={styles.sectionHeader}>
<Text
style={[
styles.sectionTitle,
{ color: theme.colors.textSecondary, marginBottom: 0 },
]}
>
<View className="mt-6">
<View className="flex-row justify-between items-center mb-2.5">
<Text className="text-[13px] font-semibold uppercase tracking-wide text-text-secondary">
Internal Notes
</Text>
<TouchableOpacity
style={[
styles.addNoteButton,
{ backgroundColor: theme.colors.actionPrimary },
]}
className="px-3 py-1.5 rounded-lg"
style={{ backgroundColor: theme.colors.actionPrimary }}
onPress={() => {
return setNoteModalVisible(true);
}}
>
<Text
style={[
styles.addNoteButtonText,
{ color: theme.colors.textInverse },
]}
>
<Text className="text-[13px] font-semibold text-text-inverse">
Add Note
</Text>
</TouchableOpacity>
@@ -496,39 +388,18 @@ export default function IncidentEpisodeDetailScreen({
return (
<View
key={note._id}
style={[
styles.noteCard,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
className="rounded-xl p-3.5 mb-2 bg-bg-elevated shadow-sm"
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textPrimary },
]}
>
<Text className="text-body-md text-text-primary">
{note.note}
</Text>
<View style={styles.noteMeta}>
<View className="flex-row justify-between mt-2">
{note.createdByUser ? (
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
{note.createdByUser.name}
</Text>
) : null}
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
{formatDateTime(note.createdAt)}
</Text>
</View>
@@ -538,12 +409,7 @@ export default function IncidentEpisodeDetailScreen({
: null}
{notes && notes.length === 0 ? (
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
No notes yet.
</Text>
) : null}
@@ -560,130 +426,3 @@ export default function IncidentEpisodeDetailScreen({
</ScrollView>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
centered: {
flex: 1,
alignItems: "center",
justifyContent: "center",
},
content: {
padding: 20,
paddingBottom: 40,
},
numberBadge: {
alignSelf: "flex-start",
paddingHorizontal: 10,
paddingVertical: 4,
borderRadius: 8,
},
number: {
fontSize: 14,
fontWeight: "600",
},
badgeRow: {
flexDirection: "row",
flexWrap: "wrap",
gap: 8,
marginTop: 12,
},
badge: {
flexDirection: "row",
alignItems: "center",
paddingHorizontal: 10,
paddingVertical: 5,
borderRadius: 6,
},
dot: {
width: 8,
height: 8,
borderRadius: 4,
marginRight: 6,
},
badgeText: {
fontSize: 13,
fontWeight: "600",
},
section: {
marginTop: 24,
},
sectionHeader: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 10,
},
sectionTitle: {
fontSize: 13,
fontWeight: "600",
textTransform: "uppercase",
letterSpacing: 0.5,
marginBottom: 10,
},
detailCard: {
borderRadius: 16,
padding: 16,
},
detailRow: {
flexDirection: "row",
marginBottom: 10,
},
detailLabel: {
fontSize: 14,
width: 90,
},
detailValue: {
fontSize: 14,
},
actionRow: {
flexDirection: "row",
gap: 12,
},
actionButton: {
flex: 1,
paddingVertical: 14,
borderRadius: 14,
alignItems: "center",
justifyContent: "center",
minHeight: 50,
},
actionButtonText: {
fontSize: 15,
fontWeight: "700",
},
timelineEntry: {
flexDirection: "row",
alignItems: "center",
padding: 14,
borderRadius: 12,
marginBottom: 8,
},
timelineDot: {
width: 10,
height: 10,
borderRadius: 5,
marginRight: 12,
},
timelineInfo: {
flex: 1,
},
addNoteButton: {
paddingHorizontal: 12,
paddingVertical: 6,
borderRadius: 8,
},
addNoteButtonText: {
fontSize: 13,
fontWeight: "600",
},
noteCard: {
borderRadius: 12,
padding: 14,
marginBottom: 8,
},
noteMeta: {
flexDirection: "row",
justifyContent: "space-between",
marginTop: 8,
},
});

View File

@@ -5,7 +5,6 @@ import {
RefreshControl,
TouchableOpacity,
Text,
StyleSheet,
ListRenderItemInfo,
} from "react-native";
import { useNavigation } from "@react-navigation/native";
@@ -73,13 +72,8 @@ export default function IncidentEpisodesScreen(): React.JSX.Element {
if (isLoading && episodes.length === 0) {
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<View style={styles.skeletonList}>
<View className="flex-1 bg-bg-primary">
<View className="p-4">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
@@ -90,36 +84,18 @@ export default function IncidentEpisodesScreen(): React.JSX.Element {
if (isError) {
return (
<View
style={[
styles.centered,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textSecondary, textAlign: "center" },
]}
>
<View className="flex-1 items-center justify-center px-8 bg-bg-primary">
<Text className="text-body-md text-text-secondary text-center">
Failed to load incident episodes.
</Text>
<TouchableOpacity
style={[
styles.retryButton,
theme.shadows.md,
{ backgroundColor: theme.colors.actionPrimary },
]}
className="mt-4 px-6 py-3 rounded-[10px] shadow-md"
style={{ backgroundColor: theme.colors.actionPrimary }}
onPress={() => {
return refetch();
}}
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textInverse, fontWeight: "600" },
]}
>
<Text className="text-body-md text-text-inverse font-semibold">
Retry
</Text>
</TouchableOpacity>
@@ -128,19 +104,14 @@ export default function IncidentEpisodesScreen(): React.JSX.Element {
}
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<View className="flex-1 bg-bg-primary">
<FlatList
data={episodes}
keyExtractor={(item: IncidentEpisodeItem) => {
return item._id;
}}
contentContainerStyle={
episodes.length === 0 ? styles.emptyContainer : styles.list
episodes.length === 0 ? { flex: 1 } : { padding: 16 }
}
renderItem={({ item }: ListRenderItemInfo<IncidentEpisodeItem>) => {
return (
@@ -169,30 +140,3 @@ export default function IncidentEpisodesScreen(): React.JSX.Element {
</View>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
container: {
flex: 1,
},
centered: {
flex: 1,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 32,
},
list: {
padding: 16,
},
emptyContainer: {
flex: 1,
},
skeletonList: {
padding: 16,
},
retryButton: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 10,
},
});

View File

@@ -5,12 +5,11 @@ import {
RefreshControl,
TouchableOpacity,
Text,
StyleSheet,
ListRenderItemInfo,
} from "react-native";
import { useNavigation } from "@react-navigation/native";
import type { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { useTheme, type Theme } from "../theme";
import { useTheme } from "../theme";
import { useProject } from "../hooks/useProject";
import { useIncidents } from "../hooks/useIncidents";
import { useIncidentStates } from "../hooks/useIncidentDetail";
@@ -32,7 +31,7 @@ type NavProp = NativeStackNavigationProp<
>;
export default function IncidentsScreen(): React.JSX.Element {
const { theme }: { theme: Theme } = useTheme();
const { theme } = useTheme();
const { selectedProject } = useProject();
const projectId: string = selectedProject?._id ?? "";
const navigation: NativeStackNavigationProp<
@@ -114,13 +113,8 @@ export default function IncidentsScreen(): React.JSX.Element {
if (isLoading && incidents.length === 0) {
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<View style={styles.skeletonList}>
<View className="flex-1 bg-bg-primary">
<View className="p-4">
<SkeletonCard />
<SkeletonCard />
<SkeletonCard />
@@ -131,36 +125,18 @@ export default function IncidentsScreen(): React.JSX.Element {
if (isError) {
return (
<View
style={[
styles.centered,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textSecondary, textAlign: "center" },
]}
>
<View className="flex-1 items-center justify-center px-8 bg-bg-primary">
<Text className="text-body-md text-text-secondary text-center">
Failed to load incidents.
</Text>
<TouchableOpacity
style={[
styles.retryButton,
theme.shadows.md,
{ backgroundColor: theme.colors.actionPrimary },
]}
className="mt-4 px-6 py-3 rounded-[10px] shadow-md"
style={{ backgroundColor: theme.colors.actionPrimary }}
onPress={() => {
return refetch();
}}
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textInverse, fontWeight: "600" },
]}
>
<Text className="text-body-md text-text-inverse font-semibold">
Retry
</Text>
</TouchableOpacity>
@@ -169,19 +145,14 @@ export default function IncidentsScreen(): React.JSX.Element {
}
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<View className="flex-1 bg-bg-primary">
<FlatList
data={incidents}
keyExtractor={(item: IncidentItem) => {
return item._id;
}}
contentContainerStyle={
incidents.length === 0 ? styles.emptyContainer : styles.list
incidents.length === 0 ? { flex: 1 } : { padding: 16 }
}
renderItem={({ item }: ListRenderItemInfo<IncidentItem>) => {
return (
@@ -224,30 +195,3 @@ export default function IncidentsScreen(): React.JSX.Element {
</View>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
container: {
flex: 1,
},
centered: {
flex: 1,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 32,
},
list: {
padding: 16,
},
emptyContainer: {
flex: 1,
},
skeletonList: {
padding: 16,
},
retryButton: {
marginTop: 16,
paddingHorizontal: 24,
paddingVertical: 12,
borderRadius: 10,
},
});

View File

@@ -1,13 +1,5 @@
import React, { useState, useEffect, useCallback } from "react";
import {
View,
Text,
ScrollView,
Switch,
StyleSheet,
ViewStyle,
TextStyle,
} from "react-native";
import { View, Text, ScrollView, Switch } from "react-native";
import { useTheme } from "../theme";
import { useHaptics } from "../hooks/useHaptics";
import {
@@ -33,24 +25,16 @@ function PrefRow({
return (
<View
style={[
styles.row,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
className="flex-row justify-between items-center p-4 rounded-2xl mb-2.5 bg-bg-elevated shadow-sm"
accessibilityRole="switch"
accessibilityState={{ checked: value }}
accessibilityLabel={`${label}. ${description}`}
>
<View style={styles.rowText}>
<Text style={[styles.rowLabel, { color: theme.colors.textPrimary }]}>
<View className="flex-1 mr-3">
<Text className="text-base font-medium text-text-primary">
{label}
</Text>
<Text
style={[styles.rowDescription, { color: theme.colors.textTertiary }]}
>
<Text className="text-[13px] mt-0.5 leading-[18px] text-text-tertiary">
{description}
</Text>
</View>
@@ -102,33 +86,27 @@ export default function NotificationPreferencesScreen(): React.JSX.Element {
if (!loaded) {
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
className="flex-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
/>
);
}
return (
<ScrollView
style={[{ backgroundColor: theme.colors.backgroundPrimary }]}
contentContainerStyle={styles.content}
className="bg-bg-primary"
contentContainerStyle={{ padding: 20, paddingBottom: 60 }}
>
{/* Event Types */}
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mb-7">
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-1 ml-1 text-text-secondary">
Event Types
</Text>
<Text
style={[styles.sectionHint, { color: theme.colors.textTertiary }]}
>
<Text className="text-xs mb-3 ml-1 leading-4 text-text-tertiary">
Choose which event types send push notifications
</Text>
<View style={styles.rowGroup}>
<View className="gap-px">
<PrefRow
label="Incidents"
description="New incidents and state changes"
@@ -165,14 +143,12 @@ export default function NotificationPreferencesScreen(): React.JSX.Element {
</View>
{/* Priority Filter */}
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mb-7">
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
Priority
</Text>
<View style={styles.rowGroup}>
<View className="gap-px">
<PrefRow
label="Critical Only"
description="Only receive notifications for critical and high severity events"
@@ -185,8 +161,8 @@ export default function NotificationPreferencesScreen(): React.JSX.Element {
</View>
{/* Info */}
<View style={styles.infoSection}>
<Text style={[styles.infoText, { color: theme.colors.textTertiary }]}>
<View className="mt-1 px-1">
<Text className="text-xs leading-[18px] text-text-tertiary">
Notification preferences are stored locally on this device.
Server-side notification rules configured in your project settings
take precedence.
@@ -195,75 +171,3 @@ export default function NotificationPreferencesScreen(): React.JSX.Element {
</ScrollView>
);
}
const styles: {
container: ViewStyle;
content: ViewStyle;
section: ViewStyle;
sectionTitle: TextStyle;
sectionHint: TextStyle;
rowGroup: ViewStyle;
row: ViewStyle;
rowText: ViewStyle;
rowLabel: TextStyle;
rowDescription: TextStyle;
infoSection: ViewStyle;
infoText: TextStyle;
} = StyleSheet.create({
container: {
flex: 1,
},
content: {
padding: 20,
paddingBottom: 60,
},
section: {
marginBottom: 28,
},
sectionTitle: {
fontSize: 13,
fontWeight: "600",
textTransform: "uppercase",
letterSpacing: 0.8,
marginBottom: 4,
marginLeft: 4,
},
sectionHint: {
fontSize: 12,
marginBottom: 12,
marginLeft: 4,
lineHeight: 16,
},
rowGroup: {
gap: 1,
},
row: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 16,
borderRadius: 16,
marginBottom: 10,
},
rowText: {
flex: 1,
marginRight: 12,
},
rowLabel: {
fontSize: 16,
fontWeight: "500",
},
rowDescription: {
fontSize: 13,
marginTop: 2,
lineHeight: 18,
},
infoSection: {
marginTop: 4,
paddingHorizontal: 4,
},
infoText: {
fontSize: 12,
lineHeight: 18,
},
});

View File

@@ -5,7 +5,6 @@ import {
FlatList,
TouchableOpacity,
ActivityIndicator,
StyleSheet,
ListRenderItemInfo,
} from "react-native";
import { useTheme } from "../theme";
@@ -25,19 +24,9 @@ export default function ProjectSelectionScreen(): React.JSX.Element {
if (isLoadingProjects) {
return (
<View
style={[
styles.centered,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<View className="flex-1 items-center justify-center px-8 bg-bg-primary">
<ActivityIndicator size="large" color={theme.colors.actionPrimary} />
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textSecondary, marginTop: theme.spacing.md },
]}
>
<Text className="text-body-md text-text-secondary mt-4">
Loading projects...
</Text>
</View>
@@ -46,45 +35,19 @@ export default function ProjectSelectionScreen(): React.JSX.Element {
if (projectList.length === 0) {
return (
<View
style={[
styles.centered,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<Text
style={[
theme.typography.titleSmall,
{ color: theme.colors.textPrimary, textAlign: "center" },
]}
>
<View className="flex-1 items-center justify-center px-8 bg-bg-primary">
<Text className="text-title-sm text-text-primary text-center">
No Projects Found
</Text>
<Text
style={[
theme.typography.bodyMedium,
{
color: theme.colors.textSecondary,
textAlign: "center",
marginTop: theme.spacing.sm,
},
]}
>
<Text className="text-body-md text-text-secondary text-center mt-2">
{"You don't have access to any projects."}
</Text>
<TouchableOpacity
style={[
styles.retryButton,
{ backgroundColor: theme.colors.actionPrimary },
]}
className="mt-6 px-8 py-3.5 rounded-xl"
style={{ backgroundColor: theme.colors.actionPrimary }}
onPress={refreshProjects}
>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textInverse, fontWeight: "600" },
]}
>
<Text className="text-body-md text-text-inverse font-semibold">
Retry
</Text>
</TouchableOpacity>
@@ -93,27 +56,12 @@ export default function ProjectSelectionScreen(): React.JSX.Element {
}
return (
<View
style={[
styles.container,
{ backgroundColor: theme.colors.backgroundPrimary },
]}
>
<View style={styles.header}>
<Text
style={[
theme.typography.titleLarge,
{ color: theme.colors.textPrimary },
]}
>
<View className="flex-1 bg-bg-primary">
<View className="px-5 pt-4 pb-2">
<Text className="text-title-lg text-text-primary">
Select Project
</Text>
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textSecondary, marginTop: theme.spacing.xs },
]}
>
<Text className="text-body-md text-text-secondary mt-1">
Choose a project to view incidents and alerts.
</Text>
</View>
@@ -123,44 +71,26 @@ export default function ProjectSelectionScreen(): React.JSX.Element {
keyExtractor={(item: ProjectItem) => {
return item._id;
}}
contentContainerStyle={styles.list}
contentContainerStyle={{ padding: 20, paddingTop: 12 }}
renderItem={({ item }: ListRenderItemInfo<ProjectItem>) => {
return (
<TouchableOpacity
style={[
styles.projectCard,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
className="flex-row items-center p-[18px] rounded-2xl mb-3 bg-bg-elevated shadow-sm"
onPress={() => {
return handleSelect(item);
}}
activeOpacity={0.7}
>
<View
style={[
styles.projectDot,
{ backgroundColor: theme.colors.actionPrimary },
]}
className="w-3.5 h-3.5 rounded-full mr-3"
style={{ backgroundColor: theme.colors.actionPrimary }}
/>
<View style={styles.projectInfo}>
<Text
style={[
theme.typography.bodyLarge,
{ color: theme.colors.textPrimary, fontWeight: "600" },
]}
>
<View className="flex-1">
<Text className="text-body-lg text-text-primary font-semibold">
{item.name}
</Text>
{item.slug ? (
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.textTertiary },
]}
>
<Text className="text-body-sm text-text-tertiary">
{item.slug}
</Text>
) : null}
@@ -172,46 +102,3 @@ export default function ProjectSelectionScreen(): React.JSX.Element {
</View>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
container: {
flex: 1,
},
centered: {
flex: 1,
alignItems: "center",
justifyContent: "center",
paddingHorizontal: 32,
},
header: {
paddingHorizontal: 20,
paddingTop: 16,
paddingBottom: 8,
},
list: {
padding: 20,
paddingTop: 12,
},
projectCard: {
flexDirection: "row",
alignItems: "center",
padding: 18,
borderRadius: 16,
marginBottom: 12,
},
projectDot: {
width: 14,
height: 14,
borderRadius: 7,
marginRight: 12,
},
projectInfo: {
flex: 1,
},
retryButton: {
marginTop: 24,
paddingHorizontal: 32,
paddingVertical: 14,
borderRadius: 12,
},
});

View File

@@ -5,7 +5,6 @@ import {
TouchableOpacity,
ScrollView,
Switch,
StyleSheet,
} from "react-native";
import { useTheme, ThemeMode } from "../theme";
import { useNavigation } from "@react-navigation/native";
@@ -44,38 +43,22 @@ function SettingsRow({
const { theme } = useTheme();
const content: React.JSX.Element = (
<View
style={[
styles.row,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
>
<View className="flex-row justify-between items-center p-4 rounded-2xl min-h-[52px] bg-bg-elevated shadow-sm">
<Text
style={[
styles.rowLabel,
{
color: destructive
? theme.colors.actionDestructive
: textColor || theme.colors.textPrimary,
},
]}
className="text-base font-medium"
style={{
color: destructive
? theme.colors.actionDestructive
: textColor || theme.colors.textPrimary,
}}
>
{label}
</Text>
{rightElement ??
(value ? (
<Text
style={[styles.rowValue, { color: theme.colors.textSecondary }]}
>
{value}
</Text>
<Text className="text-[15px] text-text-secondary">{value}</Text>
) : onPress ? (
<Text style={[styles.chevron, { color: theme.colors.textTertiary }]}>
</Text>
<Text className="text-2xl font-light text-text-tertiary">{">"}</Text>
) : null)}
</View>
);
@@ -126,61 +109,47 @@ export default function SettingsScreen(): React.JSX.Element {
return (
<ScrollView
style={[{ backgroundColor: theme.colors.backgroundPrimary }]}
contentContainerStyle={styles.content}
className="bg-bg-primary"
contentContainerStyle={{ padding: 20, paddingBottom: 60 }}
>
{/* Appearance */}
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mb-7">
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
Appearance
</Text>
<View
style={[
styles.themeSelector,
theme.shadows.sm,
{
backgroundColor: theme.colors.backgroundElevated,
},
]}
>
<View className="flex-row rounded-2xl p-1 gap-1 bg-bg-elevated shadow-sm">
{(["dark", "light", "system"] as ThemeMode[]).map(
(mode: ThemeMode) => {
const isActive: boolean = themeMode === mode;
return (
<TouchableOpacity
key={mode}
style={[
styles.themeOption,
isActive && {
backgroundColor: theme.colors.actionPrimary,
},
]}
className="flex-1 flex-row items-center justify-center py-2.5 rounded-lg gap-1.5"
style={
isActive
? { backgroundColor: theme.colors.actionPrimary }
: undefined
}
onPress={() => {
return handleThemeChange(mode);
}}
activeOpacity={0.7}
>
<Text
style={[
styles.themeOptionIcon,
{
color: isActive
? "#FFFFFF"
: theme.colors.textSecondary,
},
]}
className="text-base"
style={{
color: isActive
? "#FFFFFF"
: theme.colors.textSecondary,
}}
>
{mode === "dark" ? "" : mode === "light" ? "" : ""}
{mode === "dark" ? "\u25D7" : mode === "light" ? "\u25CB" : "\u25D1"}
</Text>
<Text
style={[
styles.themeOptionLabel,
{
color: isActive ? "#FFFFFF" : theme.colors.textPrimary,
},
]}
className="text-sm font-semibold"
style={{
color: isActive ? "#FFFFFF" : theme.colors.textPrimary,
}}
>
{mode.charAt(0).toUpperCase() + mode.slice(1)}
</Text>
@@ -192,10 +161,8 @@ export default function SettingsScreen(): React.JSX.Element {
</View>
{/* Notifications */}
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mb-7">
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
Notifications
</Text>
<SettingsRow
@@ -208,10 +175,8 @@ export default function SettingsScreen(): React.JSX.Element {
{/* Security */}
{biometric.isAvailable ? (
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mb-7">
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
Security
</Text>
<SettingsRow
@@ -228,9 +193,7 @@ export default function SettingsScreen(): React.JSX.Element {
/>
}
/>
<Text
style={[styles.sectionHint, { color: theme.colors.textTertiary }]}
>
<Text className="text-xs mt-2 ml-1 leading-4 text-text-tertiary">
Require {biometric.biometricType.toLowerCase()} to unlock the app
</Text>
</View>
@@ -238,10 +201,8 @@ export default function SettingsScreen(): React.JSX.Element {
{/* Project */}
{selectedProject ? (
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mb-7">
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
Project
</Text>
<SettingsRow
@@ -252,118 +213,37 @@ export default function SettingsScreen(): React.JSX.Element {
) : null}
{/* Server */}
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mb-7">
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
Server
</Text>
<SettingsRow label="Server URL" value={serverUrl || "oneuptime.com"} />
</View>
{/* Account */}
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mb-7">
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
Account
</Text>
<SettingsRow label="Log Out" onPress={logout} destructive />
</View>
{/* About */}
<View style={styles.section}>
<Text
style={[styles.sectionTitle, { color: theme.colors.textSecondary }]}
>
<View className="mb-7">
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
About
</Text>
<SettingsRow label="Version" value={APP_VERSION} />
<View style={{ height: 1 }} />
<View className="h-px" />
<SettingsRow label="Build" value="1" />
</View>
{/* Footer branding */}
<View style={styles.footer}>
<Text style={[styles.footerText, { color: theme.colors.textTertiary }]}>
<View className="items-center pt-3">
<Text className="text-xs font-medium text-text-tertiary">
OneUptime On-Call
</Text>
</View>
</ScrollView>
);
}
const styles: ReturnType<typeof StyleSheet.create> = StyleSheet.create({
content: {
padding: 20,
paddingBottom: 60,
},
section: {
marginBottom: 28,
},
sectionTitle: {
fontSize: 13,
fontWeight: "600",
textTransform: "uppercase",
letterSpacing: 0.8,
marginBottom: 10,
marginLeft: 4,
},
sectionHint: {
fontSize: 12,
marginTop: 8,
marginLeft: 4,
lineHeight: 16,
},
row: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
padding: 16,
borderRadius: 16,
minHeight: 52,
},
rowLabel: {
fontSize: 16,
fontWeight: "500",
},
rowValue: {
fontSize: 15,
},
chevron: {
fontSize: 24,
fontWeight: "300",
},
// Theme selector
themeSelector: {
flexDirection: "row",
borderRadius: 16,
padding: 4,
gap: 4,
},
themeOption: {
flex: 1,
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
paddingVertical: 10,
borderRadius: 8,
gap: 6,
},
themeOptionIcon: {
fontSize: 16,
},
themeOptionLabel: {
fontSize: 14,
fontWeight: "600",
},
// Footer
footer: {
alignItems: "center",
paddingTop: 12,
},
footerText: {
fontSize: 12,
fontWeight: "500",
},
});

View File

@@ -4,13 +4,10 @@ import {
Text,
TextInput,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
KeyboardAvoidingView,
Platform,
ScrollView,
ViewStyle,
TextStyle,
} from "react-native";
import { useTheme } from "../../theme";
import { useAuth } from "../../hooks/useAuth";
@@ -74,62 +71,29 @@ export default function LoginScreen(): React.JSX.Element {
return (
<KeyboardAvoidingView
style={[styles.flex, { backgroundColor: theme.colors.backgroundPrimary }]}
className="flex-1 bg-bg-primary"
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
>
<View style={styles.container}>
<View style={styles.header}>
<Text
style={[
theme.typography.titleLarge,
{
color: theme.colors.textPrimary,
fontWeight: "800",
fontSize: 28,
letterSpacing: -0.5,
},
]}
>
<View className="flex-1 justify-center px-6">
<View className="items-center mb-12">
<Text className="text-text-primary font-extrabold text-[28px] tracking-tight">
OneUptime
</Text>
<Text
style={[
theme.typography.bodySmall,
{
color: theme.colors.textTertiary,
marginTop: theme.spacing.xs,
},
]}
>
<Text className="text-body-sm text-text-tertiary mt-1">
{serverUrl}
</Text>
</View>
<View style={styles.form}>
<Text
style={[
theme.typography.bodySmall,
{
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
},
]}
>
<View className="w-full">
<Text className="text-body-sm text-text-secondary mb-1">
Email
</Text>
<TextInput
style={[
styles.input,
{
backgroundColor: theme.colors.backgroundPrimary,
borderColor: theme.colors.borderDefault,
color: theme.colors.textPrimary,
},
]}
className="h-14 border rounded-[14px] px-4 text-base bg-bg-primary text-text-primary border-border-default"
value={email}
onChangeText={(text: string) => {
setEmail(text);
@@ -144,27 +108,11 @@ export default function LoginScreen(): React.JSX.Element {
returnKeyType="next"
/>
<Text
style={[
theme.typography.bodySmall,
{
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
marginTop: theme.spacing.md,
},
]}
>
<Text className="text-body-sm text-text-secondary mb-1 mt-4">
Password
</Text>
<TextInput
style={[
styles.input,
{
backgroundColor: theme.colors.backgroundPrimary,
borderColor: theme.colors.borderDefault,
color: theme.colors.textPrimary,
},
]}
className="h-14 border rounded-[14px] px-4 text-base bg-bg-primary text-text-primary border-border-default"
value={password}
onChangeText={(text: string) => {
setPassword(text);
@@ -179,55 +127,34 @@ export default function LoginScreen(): React.JSX.Element {
/>
{error ? (
<Text
style={[
theme.typography.bodySmall,
{
color: theme.colors.statusError,
marginTop: theme.spacing.sm,
},
]}
>
<Text className="text-body-sm mt-2" style={{ color: theme.colors.statusError }}>
{error}
</Text>
) : null}
<TouchableOpacity
style={[
styles.button,
theme.shadows.md,
{
backgroundColor: theme.colors.actionPrimary,
opacity: isLoading ? 0.7 : 1,
},
]}
className="h-[52px] rounded-[14px] items-center justify-center mt-6 shadow-md"
style={{
backgroundColor: theme.colors.actionPrimary,
opacity: isLoading ? 0.7 : 1,
}}
onPress={handleLogin}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color={theme.colors.textInverse} />
) : (
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textInverse, fontWeight: "700" },
]}
>
<Text className="text-body-md text-text-inverse font-bold">
Log In
</Text>
)}
</TouchableOpacity>
<TouchableOpacity
style={styles.changeServer}
className="items-center mt-6 py-2"
onPress={handleChangeServer}
>
<Text
style={[
theme.typography.bodySmall,
{ color: theme.colors.actionPrimary },
]}
>
<Text className="text-body-sm text-action-primary">
Change Server
</Text>
</TouchableOpacity>
@@ -237,52 +164,3 @@ export default function LoginScreen(): React.JSX.Element {
</KeyboardAvoidingView>
);
}
const styles: {
flex: ViewStyle;
scrollContent: ViewStyle;
container: ViewStyle;
header: ViewStyle;
form: ViewStyle;
input: TextStyle;
button: ViewStyle;
changeServer: ViewStyle;
} = StyleSheet.create({
flex: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
},
container: {
flex: 1,
justifyContent: "center",
paddingHorizontal: 24,
},
header: {
alignItems: "center",
marginBottom: 48,
},
form: {
width: "100%",
},
input: {
height: 56,
borderWidth: 1,
borderRadius: 14,
paddingHorizontal: 16,
fontSize: 16,
},
button: {
height: 52,
borderRadius: 14,
alignItems: "center",
justifyContent: "center",
marginTop: 24,
},
changeServer: {
alignItems: "center",
marginTop: 24,
paddingVertical: 8,
},
});

View File

@@ -4,13 +4,10 @@ import {
Text,
TextInput,
TouchableOpacity,
StyleSheet,
ActivityIndicator,
KeyboardAvoidingView,
Platform,
ScrollView,
ViewStyle,
TextStyle,
} from "react-native";
import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { useNavigation } from "@react-navigation/native";
@@ -66,65 +63,34 @@ export default function ServerUrlScreen(): React.JSX.Element {
return (
<KeyboardAvoidingView
style={[styles.flex, { backgroundColor: theme.colors.backgroundPrimary }]}
className="flex-1 bg-bg-primary"
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
>
<View style={styles.container}>
<View style={styles.header}>
<Text
style={[
theme.typography.titleLarge,
{
color: theme.colors.textPrimary,
fontWeight: "800",
fontSize: 28,
letterSpacing: -0.5,
},
]}
>
<View className="flex-1 justify-center px-6">
<View className="items-center mb-12">
<Text className="text-text-primary font-extrabold text-[28px] tracking-tight">
OneUptime
</Text>
<Text
style={[
theme.typography.bodyMedium,
{
color: theme.colors.textSecondary,
marginTop: theme.spacing.sm,
textAlign: "center",
},
]}
>
<Text className="text-body-md text-text-secondary mt-2 text-center">
Connect to your OneUptime instance
</Text>
</View>
<View style={styles.form}>
<Text
style={[
theme.typography.bodySmall,
{
color: theme.colors.textSecondary,
marginBottom: theme.spacing.xs,
},
]}
>
<View className="w-full">
<Text className="text-body-sm text-text-secondary mb-1">
Server URL
</Text>
<TextInput
style={[
styles.input,
{
backgroundColor: theme.colors.backgroundPrimary,
borderColor: error
? theme.colors.statusError
: theme.colors.borderDefault,
color: theme.colors.textPrimary,
},
]}
className="h-14 border rounded-[14px] px-4 text-base bg-bg-primary text-text-primary"
style={{
borderColor: error
? theme.colors.statusError
: theme.colors.borderDefault,
}}
value={url}
onChangeText={(text: string) => {
setUrl(text);
@@ -140,55 +106,30 @@ export default function ServerUrlScreen(): React.JSX.Element {
/>
{error ? (
<Text
style={[
theme.typography.bodySmall,
{
color: theme.colors.statusError,
marginTop: theme.spacing.sm,
},
]}
>
<Text className="text-body-sm mt-2" style={{ color: theme.colors.statusError }}>
{error}
</Text>
) : null}
<TouchableOpacity
style={[
styles.button,
theme.shadows.md,
{
backgroundColor: theme.colors.actionPrimary,
opacity: isLoading ? 0.7 : 1,
},
]}
className="h-[52px] rounded-[14px] items-center justify-center mt-6 shadow-md"
style={{
backgroundColor: theme.colors.actionPrimary,
opacity: isLoading ? 0.7 : 1,
}}
onPress={handleConnect}
disabled={isLoading}
>
{isLoading ? (
<ActivityIndicator color={theme.colors.textInverse} />
) : (
<Text
style={[
theme.typography.bodyMedium,
{ color: theme.colors.textInverse, fontWeight: "700" },
]}
>
<Text className="text-body-md text-text-inverse font-bold">
Connect
</Text>
)}
</TouchableOpacity>
<Text
style={[
theme.typography.caption,
{
color: theme.colors.textTertiary,
textAlign: "center",
marginTop: theme.spacing.lg,
},
]}
>
<Text className="text-caption text-text-tertiary text-center mt-6">
Self-hosting? Enter your OneUptime server URL above.
</Text>
</View>
@@ -197,46 +138,3 @@ export default function ServerUrlScreen(): React.JSX.Element {
</KeyboardAvoidingView>
);
}
const styles: {
flex: ViewStyle;
scrollContent: ViewStyle;
container: ViewStyle;
header: ViewStyle;
form: ViewStyle;
input: TextStyle;
button: ViewStyle;
} = StyleSheet.create({
flex: {
flex: 1,
},
scrollContent: {
flexGrow: 1,
},
container: {
flex: 1,
justifyContent: "center",
paddingHorizontal: 24,
},
header: {
alignItems: "center",
marginBottom: 48,
},
form: {
width: "100%",
},
input: {
height: 56,
borderWidth: 1,
borderRadius: 14,
paddingHorizontal: 16,
fontSize: 16,
},
button: {
height: 52,
borderRadius: 14,
alignItems: "center",
justifyContent: "center",
marginTop: 24,
},
});

View File

@@ -6,11 +6,8 @@ import React, {
useMemo,
ReactNode,
} from "react";
import { useColorScheme } from "react-native";
import { View, useColorScheme } from "react-native";
import { ColorTokens, darkColors, lightColors } from "./colors";
import { typography } from "./typography";
import { spacing, radius } from "./spacing";
import { shadows, ShadowTokens } from "./shadows";
import {
getThemeMode as loadThemeMode,
setThemeMode as saveThemeMode,
@@ -20,10 +17,6 @@ export type ThemeMode = "dark" | "light" | "system";
export interface Theme {
colors: ColorTokens;
typography: typeof typography;
spacing: typeof spacing;
radius: typeof radius;
shadows: ShadowTokens;
isDark: boolean;
}
@@ -70,10 +63,6 @@ export function ThemeProvider({
return {
colors: isDark ? darkColors : lightColors,
typography,
spacing,
radius,
shadows,
isDark,
};
}, [themeMode, systemColorScheme]);
@@ -87,7 +76,11 @@ export function ThemeProvider({
}, [theme, themeMode]);
return (
<ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
<ThemeContext.Provider value={value}>
<View className={theme.isDark ? "dark flex-1" : "flex-1"}>
{children}
</View>
</ThemeContext.Provider>
);
}

View File

@@ -1,8 +1,4 @@
export { darkColors, lightColors } from "./colors";
export type { ColorTokens } from "./colors";
export { typography } from "./typography";
export { spacing, radius } from "./spacing";
export { shadows } from "./shadows";
export type { ShadowTokens } from "./shadows";
export { ThemeProvider, useTheme } from "./ThemeContext";
export type { Theme, ThemeMode } from "./ThemeContext";

View File

@@ -1,31 +0,0 @@
import { ViewStyle } from "react-native";
export interface ShadowTokens {
sm: ViewStyle;
md: ViewStyle;
lg: ViewStyle;
}
export const shadows: ShadowTokens = {
sm: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.04,
shadowRadius: 6,
elevation: 1,
},
md: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.06,
shadowRadius: 12,
elevation: 3,
},
lg: {
shadowColor: "#000",
shadowOffset: { width: 0, height: 4 },
shadowOpacity: 0.08,
shadowRadius: 16,
elevation: 5,
},
};

View File

@@ -1,28 +0,0 @@
export const spacing: {
readonly xs: 4;
readonly sm: 8;
readonly md: 16;
readonly lg: 24;
readonly xl: 32;
readonly xxl: 48;
} = {
xs: 4,
sm: 8,
md: 16,
lg: 24,
xl: 32,
xxl: 48,
} as const;
export const radius: {
readonly sm: 6;
readonly md: 12;
readonly lg: 16;
} = {
sm: 6,
md: 12,
lg: 16,
} as const;
export type Spacing = typeof spacing;
export type Radius = typeof radius;

View File

@@ -1,91 +0,0 @@
import { Platform, TextStyle } from "react-native";
const fontFamily: string = Platform.OS === "ios" ? "System" : "Roboto";
const monoFontFamily: string = Platform.OS === "ios" ? "Menlo" : "monospace";
interface TypographyStyles {
titleLarge: TextStyle;
titleMedium: TextStyle;
titleSmall: TextStyle;
bodyLarge: TextStyle;
bodyMedium: TextStyle;
bodySmall: TextStyle;
monoLarge: TextStyle;
monoMedium: TextStyle;
monoSmall: TextStyle;
label: TextStyle;
caption: TextStyle;
}
export const typography: TypographyStyles = {
titleLarge: {
fontFamily,
fontSize: 28,
fontWeight: "700",
lineHeight: 34,
},
titleMedium: {
fontFamily,
fontSize: 22,
fontWeight: "600",
lineHeight: 28,
},
titleSmall: {
fontFamily,
fontSize: 17,
fontWeight: "600",
lineHeight: 22,
},
bodyLarge: {
fontFamily,
fontSize: 17,
fontWeight: "400",
lineHeight: 24,
},
bodyMedium: {
fontFamily,
fontSize: 15,
fontWeight: "400",
lineHeight: 20,
},
bodySmall: {
fontFamily,
fontSize: 13,
fontWeight: "400",
lineHeight: 18,
},
monoLarge: {
fontFamily: monoFontFamily,
fontSize: 17,
fontWeight: "400",
lineHeight: 24,
},
monoMedium: {
fontFamily: monoFontFamily,
fontSize: 15,
fontWeight: "400",
lineHeight: 20,
},
monoSmall: {
fontFamily: monoFontFamily,
fontSize: 13,
fontWeight: "400",
lineHeight: 18,
},
label: {
fontFamily,
fontSize: 12,
fontWeight: "600",
lineHeight: 16,
letterSpacing: 0.5,
textTransform: "uppercase",
},
caption: {
fontFamily,
fontSize: 12,
fontWeight: "400",
lineHeight: 16,
},
};
export default typography;

View File

@@ -0,0 +1,80 @@
import type { Config } from "tailwindcss";
const config: Config = {
presets: [require("nativewind/preset")],
darkMode: "class",
content: ["./App.tsx", "./src/**/*.{ts,tsx}"],
theme: {
extend: {
colors: {
"bg-primary": "var(--color-bg-primary)",
"bg-secondary": "var(--color-bg-secondary)",
"bg-tertiary": "var(--color-bg-tertiary)",
"bg-elevated": "var(--color-bg-elevated)",
"border-default": "var(--color-border-default)",
"border-subtle": "var(--color-border-subtle)",
"text-primary": "var(--color-text-primary)",
"text-secondary": "var(--color-text-secondary)",
"text-tertiary": "var(--color-text-tertiary)",
"text-inverse": "var(--color-text-inverse)",
"severity-critical": "var(--color-severity-critical)",
"severity-critical-bg": "var(--color-severity-critical-bg)",
"severity-major": "var(--color-severity-major)",
"severity-major-bg": "var(--color-severity-major-bg)",
"severity-minor": "var(--color-severity-minor)",
"severity-minor-bg": "var(--color-severity-minor-bg)",
"severity-warning": "var(--color-severity-warning)",
"severity-warning-bg": "var(--color-severity-warning-bg)",
"severity-info": "var(--color-severity-info)",
"severity-info-bg": "var(--color-severity-info-bg)",
"state-created": "var(--color-state-created)",
"state-acknowledged": "var(--color-state-acknowledged)",
"state-resolved": "var(--color-state-resolved)",
"state-investigating": "var(--color-state-investigating)",
"state-muted": "var(--color-state-muted)",
"oncall-active": "var(--color-oncall-active)",
"oncall-active-bg": "var(--color-oncall-active-bg)",
"oncall-inactive": "var(--color-oncall-inactive)",
"oncall-inactive-bg": "var(--color-oncall-inactive-bg)",
"action-primary": "var(--color-action-primary)",
"action-primary-pressed": "var(--color-action-primary-pressed)",
"action-destructive": "var(--color-action-destructive)",
"action-destructive-pressed": "var(--color-action-destructive-pressed)",
"status-success": "var(--color-status-success)",
"status-success-bg": "var(--color-status-success-bg)",
"status-error": "var(--color-status-error)",
"status-error-bg": "var(--color-status-error-bg)",
},
fontSize: {
"title-lg": [
"28px",
{ lineHeight: "34px", fontWeight: "700" },
],
"title-md": [
"22px",
{ lineHeight: "28px", fontWeight: "600" },
],
"title-sm": [
"17px",
{ lineHeight: "22px", fontWeight: "600" },
],
"body-lg": ["17px", { lineHeight: "24px" }],
"body-md": ["15px", { lineHeight: "20px" }],
"body-sm": ["13px", { lineHeight: "18px" }],
label: [
"12px",
{ lineHeight: "16px", fontWeight: "600", letterSpacing: "0.5px" },
],
caption: ["12px", { lineHeight: "16px" }],
},
boxShadow: {
sm: "0 1px 6px rgba(0,0,0,0.04)",
md: "0 2px 12px rgba(0,0,0,0.06)",
lg: "0 4px 16px rgba(0,0,0,0.08)",
},
},
},
plugins: [],
};
export default config;

View File

@@ -5,6 +5,7 @@
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"types": ["nativewind/types"]
}
}