refactor: update UI components for consistency and improved theming

- Refactored IncidentsScreen to use theme colors for backgrounds and text.
- Adjusted font sizes and styles across various components for better readability.
- Updated SettingsScreen to enhance layout and visual hierarchy, including removing GlassCard and using View components with theme colors.
- Modified LoginScreen and ServerUrlScreen to improve input field styling and overall layout.
- Revised color tokens in theme/colors.ts for better contrast and accessibility.
- Improved button labels for clarity and consistency.
This commit is contained in:
Nawaz Dhandala
2026-02-13 21:24:31 +00:00
parent b8cd3ce1c1
commit e32d4395a3
27 changed files with 1391 additions and 1401 deletions

View File

@@ -4,86 +4,86 @@
:root {
--color-bg-primary: #FFFFFF;
--color-bg-secondary: #F8F8FA;
--color-bg-tertiary: #F0F0F2;
--color-bg-secondary: #F9FAFB;
--color-bg-tertiary: #F3F4F6;
--color-bg-elevated: #FFFFFF;
--color-card-accent: rgba(0, 0, 0, 0.04);
--color-bg-glass: rgba(255, 255, 255, 0.85);
--color-icon-bg: rgba(0, 0, 0, 0.05);
--color-border-default: #E5E5EA;
--color-border-subtle: #F0F0F2;
--color-text-primary: #111111;
--color-text-secondary: #6B6B6B;
--color-text-tertiary: #9A9A9A;
--color-card-accent: rgba(0, 0, 0, 0.02);
--color-bg-glass: rgba(255, 255, 255, 0.80);
--color-icon-bg: rgba(99, 102, 241, 0.08);
--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: #1A1A1A;
--color-action-primary-pressed: #333333;
--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;
--color-severity-critical: #DC2626;
--color-severity-critical-bg: rgba(220, 38, 38, 0.08);
--color-severity-major: #EA580C;
--color-severity-major-bg: rgba(234, 88, 12, 0.08);
--color-severity-minor: #CA8A04;
--color-severity-minor-bg: rgba(202, 138, 4, 0.08);
--color-severity-warning: #D97706;
--color-severity-warning-bg: rgba(217, 119, 6, 0.08);
--color-severity-info: #2563EB;
--color-severity-info-bg: rgba(37, 99, 235, 0.08);
--color-state-created: #DC2626;
--color-state-acknowledged: #D97706;
--color-state-resolved: #16A34A;
--color-state-investigating: #EA580C;
--color-state-muted: #9CA3AF;
--color-oncall-active: #16A34A;
--color-oncall-active-bg: rgba(22, 163, 74, 0.08);
--color-oncall-inactive: #9CA3AF;
--color-oncall-inactive-bg: rgba(156, 163, 175, 0.08);
--color-action-primary: #4F46E5;
--color-action-primary-pressed: #4338CA;
--color-action-destructive: #DC2626;
--color-action-destructive-pressed: #B91C1C;
--color-status-success: #16A34A;
--color-status-success-bg: rgba(22, 163, 74, 0.08);
--color-status-error: #DC2626;
--color-status-error-bg: rgba(220, 38, 38, 0.08);
}
.dark {
--color-bg-primary: #000000;
--color-bg-secondary: #0A0A0A;
--color-bg-tertiary: #161616;
--color-bg-elevated: #0F0F0F;
--color-card-accent: rgba(255, 255, 255, 0.07);
--color-bg-glass: rgba(255, 255, 255, 0.05);
--color-icon-bg: rgba(255, 255, 255, 0.07);
--color-border-default: #1C1C1E;
--color-border-subtle: #141414;
--color-text-primary: #F0F0F0;
--color-text-secondary: #8E8E93;
--color-text-tertiary: #636366;
--color-text-inverse: #000000;
--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: #636366;
--color-oncall-active: #3FB950;
--color-oncall-active-bg: #3FB95026;
--color-oncall-inactive: #636366;
--color-oncall-inactive-bg: #63636626;
--color-action-primary: #FFFFFF;
--color-action-primary-pressed: #D4D4D4;
--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;
--color-bg-primary: #09090B;
--color-bg-secondary: #0F0F12;
--color-bg-tertiary: #18181F;
--color-bg-elevated: #141418;
--color-card-accent: rgba(255, 255, 255, 0.04);
--color-bg-glass: rgba(255, 255, 255, 0.03);
--color-icon-bg: rgba(99, 102, 241, 0.12);
--color-border-default: rgba(255, 255, 255, 0.06);
--color-border-subtle: rgba(255, 255, 255, 0.04);
--color-text-primary: #FAFAFA;
--color-text-secondary: #A1A1AA;
--color-text-tertiary: #52525B;
--color-text-inverse: #FFFFFF;
--color-severity-critical: #EF4444;
--color-severity-critical-bg: rgba(239, 68, 68, 0.12);
--color-severity-major: #F97316;
--color-severity-major-bg: rgba(249, 115, 22, 0.12);
--color-severity-minor: #EAB308;
--color-severity-minor-bg: rgba(234, 179, 8, 0.12);
--color-severity-warning: #F59E0B;
--color-severity-warning-bg: rgba(245, 158, 11, 0.12);
--color-severity-info: #3B82F6;
--color-severity-info-bg: rgba(59, 130, 246, 0.12);
--color-state-created: #EF4444;
--color-state-acknowledged: #F59E0B;
--color-state-resolved: #22C55E;
--color-state-investigating: #F97316;
--color-state-muted: #52525B;
--color-oncall-active: #22C55E;
--color-oncall-active-bg: rgba(34, 197, 94, 0.12);
--color-oncall-inactive: #52525B;
--color-oncall-inactive-bg: rgba(82, 82, 91, 0.12);
--color-action-primary: #6366F1;
--color-action-primary-pressed: #4F46E5;
--color-action-destructive: #EF4444;
--color-action-destructive-pressed: #DC2626;
--color-status-success: #22C55E;
--color-status-success-bg: rgba(34, 197, 94, 0.12);
--color-status-error: #EF4444;
--color-status-error-bg: rgba(239, 68, 68, 0.12);
}

View File

@@ -54,7 +54,7 @@ export default function AddNoteModal({
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<View
className="rounded-t-[28px] p-5 pb-9"
className="rounded-t-3xl p-5 pb-9"
style={{
backgroundColor: theme.isDark
? theme.colors.backgroundElevated
@@ -62,48 +62,46 @@ export default function AddNoteModal({
borderWidth: 1,
borderBottomWidth: 0,
borderColor: theme.colors.borderGlass,
shadowColor: "#000",
shadowOpacity: 0.2,
shadowOffset: { width: 0, height: -8 },
shadowRadius: 24,
elevation: 16,
}}
>
{/* Drag Handle */}
<View className="items-center pt-1 pb-5">
<View
className="w-10 h-1.5 rounded-full"
className="w-9 h-1 rounded-full"
style={{ backgroundColor: theme.colors.borderDefault }}
/>
</View>
<View className="flex-row items-center mb-5">
<View
className="w-9 h-9 rounded-lg items-center justify-center mr-3"
className="w-8 h-8 rounded-lg items-center justify-center mr-3"
style={{
backgroundColor: theme.colors.iconBackground,
}}
>
<Ionicons
name="chatbubble-outline"
size={18}
color={theme.colors.textPrimary}
size={16}
color={theme.colors.actionPrimary}
/>
</View>
<Text
className="text-title-md text-text-primary"
style={{ letterSpacing: -0.3 }}
className="text-[18px] font-bold"
style={{
color: theme.colors.textPrimary,
letterSpacing: -0.3,
}}
>
Add Note
</Text>
</View>
<TextInput
className="min-h-[120px] rounded-xl p-4 text-[15px] text-text-primary"
className="min-h-[120px] rounded-xl p-4 text-[15px]"
style={{
backgroundColor: theme.colors.backgroundSecondary,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
borderColor: theme.colors.borderDefault,
color: theme.colors.textPrimary,
}}
placeholder="Write a note..."
placeholderTextColor={theme.colors.textTertiary}
@@ -116,17 +114,19 @@ export default function AddNoteModal({
<View className="flex-row gap-3 mt-5">
<TouchableOpacity
className="flex-1 py-3.5 rounded-xl items-center justify-center min-h-[48px]"
className="flex-1 py-3 rounded-xl items-center justify-center min-h-[48px]"
style={{
backgroundColor: theme.colors.backgroundGlass,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
borderColor: theme.colors.borderDefault,
}}
onPress={handleClose}
disabled={isSubmitting}
activeOpacity={0.7}
>
<Text className="text-[15px] font-bold text-text-secondary">
<Text
className="text-[15px] font-semibold"
style={{ color: theme.colors.textSecondary }}
>
Cancel
</Text>
</TouchableOpacity>

View File

@@ -1,12 +1,10 @@
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import { useTheme } from "../theme";
import { rgbToHex } from "../utils/color";
import { formatRelativeTime } from "../utils/date";
import ProjectBadge from "./ProjectBadge";
import GlassCard from "./GlassCard";
import type { AlertItem } from "../api/types";
interface AlertCardProps {
@@ -38,100 +36,114 @@ export default function AlertCard({
<TouchableOpacity
className="mb-3"
style={{
opacity: muted ? 0.55 : 1,
opacity: muted ? 0.5 : 1,
}}
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"}.`}
>
<GlassCard opaque>
<View className="flex-row">
<LinearGradient
colors={[stateColor, stateColor + "40"]}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
style={{ width: 3 }}
/>
<View className="flex-1 p-4">
{projectName ? (
<View className="mb-2">
<View
className="rounded-2xl overflow-hidden"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<View
style={{
height: 3,
backgroundColor: stateColor,
opacity: 0.8,
}}
/>
<View className="p-4">
<View className="flex-row justify-between items-center mb-2.5">
<View className="flex-row items-center gap-2">
{projectName ? (
<ProjectBadge name={projectName} />
</View>
) : null}
<View className="flex-row justify-between items-center mb-2">
<View
className="px-2.5 py-0.5 rounded-full"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
) : null}
<Text
className="text-[12px] font-medium"
style={{ color: theme.colors.textTertiary }}
>
<Text className="text-[12px] font-semibold text-text-tertiary">
{alert.alertNumberWithPrefix || `#${alert.alertNumber}`}
</Text>
</View>
<Text className="text-[12px] text-text-tertiary">
{timeString}
{alert.alertNumberWithPrefix || `#${alert.alertNumber}`}
</Text>
</View>
<Text
className="text-body-lg text-text-primary font-semibold mt-0.5"
numberOfLines={2}
className="text-[12px]"
style={{ color: theme.colors.textTertiary }}
>
{alert.title}
{timeString}
</Text>
</View>
<View className="flex-row flex-wrap gap-2 mt-3">
{alert.currentAlertState ? (
<Text
className="text-[16px] font-semibold mt-0.5"
style={{ color: theme.colors.textPrimary, letterSpacing: -0.2 }}
numberOfLines={2}
>
{alert.title}
</Text>
<View className="flex-row flex-wrap gap-2 mt-3">
{alert.currentAlertState ? (
<View
className="flex-row items-center px-2 py-1 rounded-md"
style={{
backgroundColor: stateColor + "14",
}}
>
<View
className="flex-row items-center px-2.5 py-1 rounded-full"
style={{
backgroundColor: theme.colors.backgroundTertiary,
}}
>
<View
className="w-2 h-2 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text className="text-[12px] font-semibold text-text-primary">
{alert.currentAlertState.name}
</Text>
</View>
) : null}
{alert.alertSeverity ? (
<View
className="flex-row items-center px-2.5 py-1 rounded-full"
style={{ backgroundColor: severityColor + "15" }}
>
<Text
className="text-[12px] font-semibold"
style={{ color: severityColor }}
>
{alert.alertSeverity.name}
</Text>
</View>
) : null}
</View>
{alert.monitor ? (
<View className="flex-row items-center mt-3">
<Ionicons
name="desktop-outline"
size={12}
color={theme.colors.textTertiary}
style={{ marginRight: 5 }}
className="w-1.5 h-1.5 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text
className="text-[12px] text-text-secondary flex-1"
numberOfLines={1}
className="text-[11px] font-semibold"
style={{ color: stateColor }}
>
{alert.monitor.name}
{alert.currentAlertState.name}
</Text>
</View>
) : null}
{alert.alertSeverity ? (
<View
className="flex-row items-center px-2 py-1 rounded-md"
style={{ backgroundColor: severityColor + "14" }}
>
<Text
className="text-[11px] font-semibold"
style={{ color: severityColor }}
>
{alert.alertSeverity.name}
</Text>
</View>
) : null}
</View>
{alert.monitor ? (
<View className="flex-row items-center mt-2.5 pt-2.5"
style={{ borderTopWidth: 1, borderTopColor: theme.colors.borderSubtle }}
>
<Ionicons
name="desktop-outline"
size={11}
color={theme.colors.textTertiary}
style={{ marginRight: 5 }}
/>
<Text
className="text-[12px] flex-1"
style={{ color: theme.colors.textTertiary }}
numberOfLines={1}
>
{alert.monitor.name}
</Text>
</View>
) : null}
</View>
</GlassCard>
</View>
</TouchableOpacity>
);
}

View File

@@ -33,49 +33,34 @@ export default function EmptyState({
return (
<View className="flex-1 items-center justify-center px-10 py-28">
{/* Outer gradient glow ring */}
<View className="w-28 h-28 rounded-full items-center justify-center overflow-hidden">
<View
style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: theme.colors.surfaceGlow,
borderRadius: 56,
}}
<View
className="w-20 h-20 rounded-2xl items-center justify-center"
style={{
backgroundColor: theme.colors.iconBackground,
}}
>
<Ionicons
name={iconMap[icon]}
size={32}
color={theme.colors.actionPrimary}
/>
{/* Inner icon container */}
<View
className="w-20 h-20 rounded-full items-center justify-center"
style={{
backgroundColor: theme.colors.backgroundTertiary,
}}
>
<Ionicons
name={iconMap[icon]}
size={36}
color={theme.colors.textSecondary}
/>
</View>
</View>
<Text
className="text-title-md text-text-primary text-center mt-7"
className="text-[20px] font-bold text-text-primary text-center mt-6"
style={{ letterSpacing: -0.3 }}
>
{title}
</Text>
{subtitle ? (
<Text className="text-body-md text-text-secondary text-center mt-2.5 leading-6 max-w-[280px]">
<Text className="text-[15px] text-text-secondary text-center mt-2 leading-[22px] max-w-[280px]">
{subtitle}
</Text>
) : null}
{actionLabel && onAction ? (
<View className="mt-6 w-[200px]">
<View className="mt-6 w-[180px]">
<GradientButton label={actionLabel} onPress={onAction} />
</View>
) : null}

View File

@@ -1,11 +1,9 @@
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { LinearGradient } from "expo-linear-gradient";
import { useTheme } from "../theme";
import { rgbToHex } from "../utils/color";
import { formatRelativeTime } from "../utils/date";
import ProjectBadge from "./ProjectBadge";
import GlassCard from "./GlassCard";
import type {
IncidentEpisodeItem,
AlertEpisodeItem,
@@ -65,94 +63,108 @@ export default function EpisodeCard(
<TouchableOpacity
className="mb-3"
style={{
opacity: muted ? 0.55 : 1,
opacity: muted ? 0.5 : 1,
}}
onPress={onPress}
activeOpacity={0.7}
>
<GlassCard opaque>
<View className="flex-row">
<LinearGradient
colors={[stateColor, stateColor + "40"]}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
style={{ width: 3 }}
/>
<View className="flex-1 p-4">
{projectName ? (
<View className="mb-2">
<View
className="rounded-2xl overflow-hidden"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<View
style={{
height: 3,
backgroundColor: stateColor,
opacity: 0.8,
}}
/>
<View className="p-4">
<View className="flex-row justify-between items-center mb-2.5">
<View className="flex-row items-center gap-2">
{projectName ? (
<ProjectBadge name={projectName} />
</View>
) : null}
<View className="flex-row justify-between items-center mb-2">
<View
className="px-2.5 py-0.5 rounded-full"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
) : null}
<Text
className="text-[12px] font-medium"
style={{ color: theme.colors.textTertiary }}
>
<Text className="text-[12px] font-semibold text-text-tertiary">
{episode.episodeNumberWithPrefix ||
`#${episode.episodeNumber}`}
</Text>
</View>
<Text className="text-[12px] text-text-tertiary">
{timeString}
{episode.episodeNumberWithPrefix ||
`#${episode.episodeNumber}`}
</Text>
</View>
<Text
className="text-body-lg text-text-primary font-semibold mt-0.5"
numberOfLines={2}
className="text-[12px]"
style={{ color: theme.colors.textTertiary }}
>
{episode.title}
{timeString}
</Text>
</View>
<View className="flex-row flex-wrap gap-2 mt-3">
{state ? (
<View
className="flex-row items-center px-2.5 py-1 rounded-full"
style={{
backgroundColor: theme.colors.backgroundTertiary,
}}
>
<View
className="w-2 h-2 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text className="text-[12px] font-semibold text-text-primary">
{state.name}
</Text>
</View>
) : null}
<Text
className="text-[16px] font-semibold mt-0.5"
style={{ color: theme.colors.textPrimary, letterSpacing: -0.2 }}
numberOfLines={2}
>
{episode.title}
</Text>
{severity ? (
<View className="flex-row flex-wrap gap-2 mt-3">
{state ? (
<View
className="flex-row items-center px-2 py-1 rounded-md"
style={{
backgroundColor: stateColor + "14",
}}
>
<View
className="flex-row items-center px-2.5 py-1 rounded-full"
style={{ backgroundColor: severityColor + "15" }}
className="w-1.5 h-1.5 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text
className="text-[11px] font-semibold"
style={{ color: stateColor }}
>
<Text
className="text-[12px] font-semibold"
style={{ color: severityColor }}
>
{severity.name}
</Text>
</View>
) : null}
{state.name}
</Text>
</View>
) : null}
{childCount > 0 ? (
<View
className="flex-row items-center px-2.5 py-1 rounded-full"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
{severity ? (
<View
className="flex-row items-center px-2 py-1 rounded-md"
style={{ backgroundColor: severityColor + "14" }}
>
<Text
className="text-[11px] font-semibold"
style={{ color: severityColor }}
>
<Text className="text-[12px] font-semibold text-text-secondary">
{childCount} {type === "incident" ? "incident" : "alert"}
{childCount !== 1 ? "s" : ""}
</Text>
</View>
) : null}
</View>
{severity.name}
</Text>
</View>
) : null}
{childCount > 0 ? (
<View
className="flex-row items-center px-2 py-1 rounded-md"
style={{ backgroundColor: theme.colors.iconBackground }}
>
<Text
className="text-[11px] font-semibold"
style={{ color: theme.colors.actionPrimary }}
>
{childCount} {type === "incident" ? "incident" : "alert"}
{childCount !== 1 ? "s" : ""}
</Text>
</View>
) : null}
</View>
</View>
</GlassCard>
</View>
</TouchableOpacity>
);
}

View File

@@ -30,7 +30,7 @@ export default function FeedTimeline({
{feed.map((entry: FeedItem, index: number) => {
const entryColor: string = entry.displayColor
? rgbToHex(entry.displayColor)
: theme.colors.textTertiary;
: theme.colors.actionPrimary;
const isLast: boolean = index === feed.length - 1;
const timeString: string = formatDateTime(
entry.postedAt || entry.createdAt,
@@ -42,42 +42,40 @@ export default function FeedTimeline({
return (
<View key={entry._id} className="flex-row">
{/* Timeline connector */}
<View className="items-center mr-3.5">
<View
className="w-3 h-3 rounded-full mt-0.5"
style={{
backgroundColor: entryColor,
shadowColor: entryColor,
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 1 },
shadowRadius: 4,
elevation: 2,
}}
className="w-2.5 h-2.5 rounded-full mt-1"
style={{ backgroundColor: entryColor }}
/>
{!isLast ? (
<View
className="w-0.5 flex-1 my-1.5"
className="w-px flex-1 my-1.5"
style={{
backgroundColor: theme.colors.borderDefault,
}}
/>
) : null}
</View>
{/* Content */}
<View className="flex-1 pb-5">
<Text className="text-body-md text-text-primary leading-5">
<Text
className="text-[14px] leading-5"
style={{ color: theme.colors.textPrimary }}
>
{mainText}
</Text>
{moreText ? (
<Text
className="text-body-sm text-text-secondary mt-1.5 leading-5"
className="text-[13px] mt-1.5 leading-5"
style={{ color: theme.colors.textSecondary }}
numberOfLines={3}
>
{moreText}
</Text>
) : null}
<Text className="text-body-sm text-text-tertiary mt-1.5">
<Text
className="text-[12px] mt-1.5"
style={{ color: theme.colors.textTertiary }}
>
{timeString}
</Text>
</View>

View File

@@ -25,11 +25,6 @@ export default function GlassCard({
: theme.colors.backgroundGlass,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
shadowColor: "#000000",
shadowOpacity: theme.isDark ? 0.3 : 0.08,
shadowOffset: { width: 0, height: 2 },
shadowRadius: 12,
elevation: 3,
},
style,
]}

View File

@@ -34,13 +34,13 @@ export default function GradientButton({
if (variant === "secondary") {
return (
<TouchableOpacity
className="h-[52px] rounded-xl items-center justify-center overflow-hidden"
className="h-[50px] rounded-xl items-center justify-center overflow-hidden"
style={[
{
backgroundColor: theme.colors.backgroundGlass,
backgroundColor: "transparent",
borderWidth: 1,
borderColor: theme.colors.borderGlass,
opacity: disabled || loading ? 0.6 : 1,
borderColor: theme.colors.borderDefault,
opacity: disabled || loading ? 0.5 : 1,
},
style,
]}
@@ -50,20 +50,20 @@ export default function GradientButton({
>
<View className="flex-row items-center">
{loading ? (
<ActivityIndicator color={theme.colors.textPrimary} />
<ActivityIndicator color={theme.colors.textSecondary} />
) : (
<>
{icon ? (
<Ionicons
name={icon}
size={18}
color={theme.colors.textPrimary}
color={theme.colors.textSecondary}
style={{ marginRight: 8 }}
/>
) : null}
<Text
className="text-[16px] font-bold"
style={{ color: theme.colors.textPrimary }}
className="text-[15px] font-semibold"
style={{ color: theme.colors.textSecondary }}
>
{label}
</Text>
@@ -76,12 +76,12 @@ export default function GradientButton({
return (
<TouchableOpacity
className="h-[52px] rounded-xl overflow-hidden"
className="h-[50px] rounded-xl overflow-hidden"
style={[
{
opacity: disabled || loading ? 0.6 : 1,
shadowColor: theme.colors.accentGradientStart,
shadowOpacity: theme.isDark ? 0.15 : 0.2,
opacity: disabled || loading ? 0.5 : 1,
shadowColor: theme.colors.accentGradientMid,
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 4 },
shadowRadius: 12,
elevation: 4,
@@ -102,20 +102,20 @@ export default function GradientButton({
className="flex-1 items-center justify-center flex-row"
>
{loading ? (
<ActivityIndicator color={theme.colors.textInverse} />
<ActivityIndicator color="#FFFFFF" />
) : (
<>
{icon ? (
<Ionicons
name={icon}
size={18}
color={theme.colors.textInverse}
color="#FFFFFF"
style={{ marginRight: 8 }}
/>
) : null}
<Text
className="text-[16px] font-bold"
style={{ color: theme.colors.textInverse }}
className="text-[15px] font-bold"
style={{ color: "#FFFFFF", letterSpacing: 0.2 }}
>
{label}
</Text>

View File

@@ -1,12 +1,10 @@
import React from "react";
import { View, Text, TouchableOpacity } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import { useTheme } from "../theme";
import { rgbToHex } from "../utils/color";
import { formatRelativeTime } from "../utils/date";
import ProjectBadge from "./ProjectBadge";
import GlassCard from "./GlassCard";
import type { IncidentItem, NamedEntity } from "../api/types";
interface IncidentCardProps {
@@ -41,105 +39,119 @@ export default function IncidentCard({
<TouchableOpacity
className="mb-3"
style={{
opacity: muted ? 0.55 : 1,
opacity: muted ? 0.5 : 1,
}}
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"}.`}
>
<GlassCard opaque>
<View className="flex-row">
<LinearGradient
colors={[stateColor, stateColor + "40"]}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
style={{ width: 3 }}
/>
<View className="flex-1 p-4">
{projectName ? (
<View className="mb-2">
<View
className="rounded-2xl overflow-hidden"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<View
style={{
height: 3,
backgroundColor: stateColor,
opacity: 0.8,
}}
/>
<View className="p-4">
<View className="flex-row justify-between items-center mb-2.5">
<View className="flex-row items-center gap-2">
{projectName ? (
<ProjectBadge name={projectName} />
</View>
) : null}
<View className="flex-row justify-between items-center mb-2">
<View
className="px-2.5 py-0.5 rounded-full"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
) : null}
<Text
className="text-[12px] font-medium"
style={{ color: theme.colors.textTertiary }}
>
<Text className="text-[12px] font-semibold text-text-tertiary">
{incident.incidentNumberWithPrefix ||
`#${incident.incidentNumber}`}
</Text>
</View>
<Text className="text-[12px] text-text-tertiary">
{timeString}
{incident.incidentNumberWithPrefix ||
`#${incident.incidentNumber}`}
</Text>
</View>
<Text
className="text-body-lg text-text-primary font-semibold mt-0.5"
numberOfLines={2}
className="text-[12px]"
style={{ color: theme.colors.textTertiary }}
>
{incident.title}
{timeString}
</Text>
</View>
<View className="flex-row flex-wrap gap-2 mt-3">
{incident.currentIncidentState ? (
<Text
className="text-[16px] font-semibold mt-0.5"
style={{ color: theme.colors.textPrimary, letterSpacing: -0.2 }}
numberOfLines={2}
>
{incident.title}
</Text>
<View className="flex-row flex-wrap gap-2 mt-3">
{incident.currentIncidentState ? (
<View
className="flex-row items-center px-2 py-1 rounded-md"
style={{
backgroundColor: stateColor + "14",
}}
>
<View
className="flex-row items-center px-2.5 py-1 rounded-full"
style={{
backgroundColor: theme.colors.backgroundTertiary,
}}
>
<View
className="w-2 h-2 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text className="text-[12px] font-semibold text-text-primary">
{incident.currentIncidentState.name}
</Text>
</View>
) : null}
{incident.incidentSeverity ? (
<View
className="flex-row items-center px-2.5 py-1 rounded-full"
style={{ backgroundColor: severityColor + "15" }}
>
<Text
className="text-[12px] font-semibold"
style={{ color: severityColor }}
>
{incident.incidentSeverity.name}
</Text>
</View>
) : null}
</View>
{monitorCount > 0 ? (
<View className="flex-row items-center mt-3">
<Ionicons
name="desktop-outline"
size={12}
color={theme.colors.textTertiary}
style={{ marginRight: 5 }}
className="w-1.5 h-1.5 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text
className="text-[12px] text-text-secondary flex-1"
numberOfLines={1}
className="text-[11px] font-semibold"
style={{ color: stateColor }}
>
{incident.monitors
.map((m: NamedEntity) => {
return m.name;
})
.join(", ")}
{incident.currentIncidentState.name}
</Text>
</View>
) : null}
{incident.incidentSeverity ? (
<View
className="flex-row items-center px-2 py-1 rounded-md"
style={{ backgroundColor: severityColor + "14" }}
>
<Text
className="text-[11px] font-semibold"
style={{ color: severityColor }}
>
{incident.incidentSeverity.name}
</Text>
</View>
) : null}
</View>
{monitorCount > 0 ? (
<View className="flex-row items-center mt-2.5 pt-2.5"
style={{ borderTopWidth: 1, borderTopColor: theme.colors.borderSubtle }}
>
<Ionicons
name="desktop-outline"
size={11}
color={theme.colors.textTertiary}
style={{ marginRight: 5 }}
/>
<Text
className="text-[12px] flex-1"
style={{ color: theme.colors.textTertiary }}
numberOfLines={1}
>
{incident.monitors
.map((m: NamedEntity) => {
return m.name;
})
.join(", ")}
</Text>
</View>
) : null}
</View>
</GlassCard>
</View>
</TouchableOpacity>
);
}

View File

@@ -4,7 +4,6 @@ import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import { useTheme } from "../theme";
import { formatDateTime } from "../utils/date";
import GlassCard from "./GlassCard";
import type { NoteItem } from "../api/types";
interface NotesSectionProps {
@@ -24,11 +23,14 @@ export default function NotesSection({
<View className="flex-row items-center">
<Ionicons
name="chatbubble-outline"
size={15}
color={theme.colors.textSecondary}
size={14}
color={theme.colors.textTertiary}
style={{ marginRight: 6 }}
/>
<Text className="text-[13px] font-semibold uppercase tracking-wide text-text-secondary">
<Text
className="text-[12px] font-semibold uppercase"
style={{ color: theme.colors.textTertiary, letterSpacing: 0.8 }}
>
Internal Notes
</Text>
</View>
@@ -46,17 +48,17 @@ export default function NotesSection({
]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
className="flex-row items-center px-3.5 py-2"
className="flex-row items-center px-3 py-1.5"
>
<Ionicons
name="add"
size={16}
color={theme.colors.textInverse}
size={14}
color="#FFFFFF"
style={{ marginRight: 4 }}
/>
<Text
className="text-[13px] font-semibold"
style={{ color: theme.colors.textInverse }}
className="text-[12px] font-semibold"
style={{ color: "#FFFFFF" }}
>
Add Note
</Text>
@@ -67,42 +69,60 @@ export default function NotesSection({
{notes && notes.length > 0
? notes.map((note: NoteItem) => {
return (
<GlassCard
<View
key={note._id}
className="rounded-xl overflow-hidden mb-2.5"
style={{
marginBottom: 10,
borderTopWidth: 2,
borderTopColor: theme.colors.accentGradientStart + "30",
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<View className="p-4">
<Text className="text-body-md text-text-primary leading-6">
<Text
className="text-[14px] leading-[22px]"
style={{ color: theme.colors.textPrimary }}
>
{note.note}
</Text>
<View className="flex-row justify-between mt-2.5">
{note.createdByUser ? (
<Text className="text-body-sm text-text-tertiary">
<Text
className="text-[12px]"
style={{ color: theme.colors.textTertiary }}
>
{note.createdByUser.name}
</Text>
) : null}
<Text className="text-body-sm text-text-tertiary">
<Text
className="text-[12px]"
style={{ color: theme.colors.textTertiary }}
>
{formatDateTime(note.createdAt)}
</Text>
</View>
</View>
</GlassCard>
</View>
);
})
: null}
{notes && notes.length === 0 ? (
<GlassCard>
<View className="p-4 items-center">
<Text className="text-body-sm text-text-tertiary">
No notes yet.
</Text>
</View>
</GlassCard>
<View
className="rounded-xl p-4 items-center"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<Text
className="text-[13px]"
style={{ color: theme.colors.textTertiary }}
>
No notes yet.
</Text>
</View>
) : null}
</View>
);

View File

@@ -1,5 +1,6 @@
import React from "react";
import { View, Text } from "react-native";
import { useTheme } from "../theme";
interface ProjectBadgeProps {
name: string;
@@ -10,13 +11,18 @@ export default function ProjectBadge({
name,
color,
}: ProjectBadgeProps): React.JSX.Element {
const { theme } = useTheme();
return (
<View className="flex-row items-center">
<View
className="w-2 h-2 rounded-full mr-1.5"
style={color ? { backgroundColor: color } : undefined}
style={{ backgroundColor: color || theme.colors.actionPrimary }}
/>
<Text className="text-body-sm text-text-secondary" numberOfLines={1}>
<Text
className="text-[12px] font-medium"
style={{ color: theme.colors.textSecondary }}
numberOfLines={1}
>
{name}
</Text>
</View>

View File

@@ -17,11 +17,17 @@ export default function SectionHeader({
<View className="flex-row items-center mb-3">
<Ionicons
name={iconName}
size={15}
color={theme.colors.textSecondary}
size={14}
color={theme.colors.textTertiary}
style={{ marginRight: 6 }}
/>
<Text className="text-[13px] font-semibold uppercase tracking-wide text-text-secondary">
<Text
className="text-[12px] font-semibold uppercase"
style={{
color: theme.colors.textTertiary,
letterSpacing: 0.8,
}}
>
{title}
</Text>
</View>

View File

@@ -24,7 +24,7 @@ export default function SegmentedControl<T extends string>({
return (
<View
className="flex-row mx-4 mt-3 mb-1 rounded-xl p-1"
style={{ backgroundColor: theme.colors.backgroundSecondary }}
style={{ backgroundColor: theme.colors.backgroundTertiary }}
>
{segments.map((segment: Segment<T>) => {
const isActive: boolean = segment.key === selected;
@@ -35,8 +35,8 @@ export default function SegmentedControl<T extends string>({
style={
isActive
? {
shadowColor: "#000000",
shadowOpacity: theme.isDark ? 0.4 : 0.15,
shadowColor: theme.colors.accentGradientMid,
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 2 },
shadowRadius: 6,
elevation: 3,
@@ -68,9 +68,7 @@ export default function SegmentedControl<T extends string>({
<Text
className="text-body-sm font-semibold"
style={{
color: isActive
? theme.colors.textInverse
: theme.colors.textTertiary,
color: isActive ? "#FFFFFF" : theme.colors.textTertiary,
}}
>
{segment.label}

View File

@@ -63,33 +63,33 @@ export default function SkeletonCard({
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderSubtle,
borderColor: theme.colors.borderGlass,
opacity,
}}
accessibilityLabel="Loading content"
accessibilityRole="progressbar"
>
<View className="flex-row">
<View
className="w-1"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View className="flex-1 p-4">
<View className="flex-row justify-between items-center mb-2.5">
<View
className="h-4 w-14 rounded-full"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View
className="h-3 w-8 rounded"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
</View>
<View
style={{
height: 3,
backgroundColor: theme.colors.backgroundTertiary,
}}
/>
<View className="p-4">
<View className="flex-row justify-between items-center mb-2.5">
<View
className="h-[18px] rounded w-3/4 mb-3"
className="h-4 w-14 rounded"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View
className="h-3 w-8 rounded"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
</View>
<View
className="h-[18px] rounded w-3/4 mb-3"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
</View>
</Animated.View>
);
@@ -103,55 +103,67 @@ export default function SkeletonCard({
accessibilityLabel="Loading content"
accessibilityRole="progressbar"
>
{/* Header area */}
<View
className="rounded-2xl p-5 mb-5"
style={{ backgroundColor: theme.colors.surfaceGlow }}
>
<View className="flex-row gap-2 mb-3">
<View
className="h-7 w-20 rounded-full"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
</View>
<View
className="h-7 w-4/5 rounded mb-3"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View className="flex-row gap-2">
<View
className="h-7 w-24 rounded-full"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View
className="h-7 w-16 rounded-full"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
</View>
</View>
{/* Details card */}
<View
className="rounded-2xl p-4"
className="rounded-2xl overflow-hidden mb-5"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderSubtle,
borderColor: theme.colors.borderGlass,
}}
>
{Array.from({ length: 3 }).map((_: unknown, index: number) => {
return (
<View key={index} className="flex-row mb-3">
<View
className="h-3.5 w-20 rounded mr-4"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View
className="h-3.5 w-[120px] rounded"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
</View>
);
})}
<View
style={{
height: 3,
backgroundColor: theme.colors.backgroundTertiary,
}}
/>
<View className="p-5">
<View
className="h-4 w-16 rounded mb-3"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View
className="h-7 w-4/5 rounded mb-3"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View className="flex-row gap-2">
<View
className="h-6 w-20 rounded-md"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View
className="h-6 w-14 rounded-md"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
</View>
</View>
</View>
<View
className="rounded-xl overflow-hidden"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
borderLeftWidth: 3,
borderLeftColor: theme.colors.backgroundTertiary,
}}
>
<View className="p-4">
{Array.from({ length: 3 }).map((_: unknown, index: number) => {
return (
<View key={index} className="flex-row mb-3">
<View
className="h-3.5 w-20 rounded mr-4"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View
className="h-3.5 w-[120px] rounded"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
</View>
);
})}
</View>
</View>
</Animated.View>
);
@@ -163,57 +175,57 @@ export default function SkeletonCard({
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderSubtle,
borderColor: theme.colors.borderGlass,
opacity,
}}
accessibilityLabel="Loading content"
accessibilityRole="progressbar"
>
<View className="flex-row">
<View
className="w-1"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View className="flex-1 p-4">
<View className="flex-row justify-between items-center mb-3">
<View
className="h-4 w-14 rounded-full"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View
className="h-3 w-10 rounded"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
</View>
<View
style={{
height: 3,
backgroundColor: theme.colors.backgroundTertiary,
}}
/>
<View className="p-4">
<View className="flex-row justify-between items-center mb-3">
<View
className="h-[18px] rounded w-[70%] mb-3"
className="h-3.5 w-14 rounded"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View
className="h-3 w-10 rounded"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View className="flex-row gap-2 mb-3">
<View
className="h-7 w-24 rounded-full"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View
className="h-7 w-16 rounded-full"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
</View>
{Array.from({ length: Math.max(lines - 1, 1) }).map(
(_: unknown, index: number) => {
return (
<View
key={index}
className="h-3 rounded mb-2"
style={{
width: lineWidths[index % lineWidths.length],
backgroundColor: theme.colors.backgroundTertiary,
}}
/>
);
},
)}
</View>
<View
className="h-[18px] rounded w-[70%] mb-3"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View className="flex-row gap-2 mb-3">
<View
className="h-6 w-20 rounded-md"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
<View
className="h-6 w-14 rounded-md"
style={{ backgroundColor: theme.colors.backgroundTertiary }}
/>
</View>
{Array.from({ length: Math.max(lines - 1, 1) }).map(
(_: unknown, index: number) => {
return (
<View
key={index}
className="h-3 rounded mb-2"
style={{
width: lineWidths[index % lineWidths.length],
backgroundColor: theme.colors.backgroundTertiary,
}}
/>
);
},
)}
</View>
</Animated.View>
);

View File

@@ -1,5 +1,5 @@
import React from "react";
import { View, StyleSheet, Platform } from "react-native";
import { View, Platform } from "react-native";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import { Ionicons } from "@expo/vector-icons";
import { MainTabParamList } from "./types";
@@ -27,17 +27,12 @@ function TabIcon({
}): React.JSX.Element {
return (
<View className="items-center">
<Ionicons name={focused ? focusedName : name} size={24} color={color} />
<Ionicons name={focused ? focusedName : name} size={22} color={color} />
{focused ? (
<View
className="w-1 h-1 rounded-full mt-1"
style={{
backgroundColor: accentColor,
shadowColor: accentColor,
shadowOpacity: 0.6,
shadowOffset: { width: 0, height: 0 },
shadowRadius: 4,
elevation: 2,
}}
/>
) : null}
@@ -53,42 +48,23 @@ export default function MainTabNavigator(): React.JSX.Element {
screenOptions={{
headerStyle: {
backgroundColor: theme.colors.backgroundPrimary,
borderBottomWidth: StyleSheet.hairlineWidth,
borderBottomWidth: 1,
borderBottomColor: theme.colors.borderSubtle,
...Platform.select({
ios: {
shadowColor: "#000",
shadowOpacity: 0.04,
shadowOffset: { width: 0, height: 1 },
shadowRadius: 6,
},
default: { elevation: 2 },
}),
},
headerShadowVisible: false,
headerTintColor: theme.colors.textPrimary,
headerTitleStyle: {
fontWeight: "700",
fontSize: 17,
letterSpacing: -0.3,
},
tabBarStyle: {
backgroundColor: theme.isDark
? theme.colors.backgroundGlass
: theme.colors.backgroundPrimary,
borderTopColor: theme.colors.borderGlass,
borderTopWidth: StyleSheet.hairlineWidth,
backgroundColor: theme.colors.backgroundPrimary,
borderTopColor: theme.colors.borderSubtle,
borderTopWidth: 1,
height: Platform.OS === "ios" ? 88 : 64,
paddingBottom: Platform.OS === "ios" ? 28 : 8,
paddingTop: 8,
...Platform.select({
ios: {
shadowColor: "#000",
shadowOpacity: 0.08,
shadowOffset: { width: 0, height: -4 },
shadowRadius: 12,
},
default: { elevation: 8 },
}),
},
tabBarActiveTintColor: theme.colors.actionPrimary,
tabBarInactiveTintColor: theme.colors.textTertiary,

View File

@@ -9,7 +9,6 @@ import {
Alert,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import { useTheme } from "../theme";
import {
@@ -31,7 +30,6 @@ import FeedTimeline from "../components/FeedTimeline";
import SkeletonCard from "../components/SkeletonCard";
import SectionHeader from "../components/SectionHeader";
import NotesSection from "../components/NotesSection";
import GlassCard from "../components/GlassCard";
import { useHaptics } from "../hooks/useHaptics";
type Props = NativeStackScreenProps<AlertsStackParamList, "AlertDetail">;
@@ -138,7 +136,10 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
if (isLoading) {
return (
<View className="flex-1 bg-bg-primary">
<View
className="flex-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<SkeletonCard variant="detail" />
</View>
);
@@ -146,8 +147,14 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
if (!alert) {
return (
<View className="flex-1 items-center justify-center bg-bg-primary">
<Text className="text-body-md text-text-secondary">
<View
className="flex-1 items-center justify-center"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<Text
className="text-[15px]"
style={{ color: theme.colors.textSecondary }}
>
Alert not found.
</Text>
</View>
@@ -177,35 +184,41 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
return (
<ScrollView
className="bg-bg-primary"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
contentContainerStyle={{ padding: 20, paddingBottom: 48 }}
refreshControl={
<RefreshControl refreshing={false} onRefresh={onRefresh} />
}
>
{/* Header with glass card */}
<GlassCard style={{ marginBottom: 20 }}>
<LinearGradient
colors={[theme.colors.gradientStart, theme.colors.gradientEnd]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
className="p-5"
>
<View
className="self-start px-3 py-1.5 rounded-full mb-3"
style={{ backgroundColor: stateColor + "1A" }}
{/* Header card */}
<View
className="rounded-2xl overflow-hidden mb-5"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<View
style={{
height: 3,
backgroundColor: stateColor,
}}
/>
<View className="p-5">
<Text
className="text-[13px] font-semibold mb-2"
style={{ color: stateColor }}
>
<Text
className="text-[13px] font-bold"
style={{ color: stateColor }}
>
{alert.alertNumberWithPrefix || `#${alert.alertNumber}`}
</Text>
</View>
{alert.alertNumberWithPrefix || `#${alert.alertNumber}`}
</Text>
<Text
className="text-title-lg text-text-primary"
style={{ letterSpacing: -0.5 }}
className="text-[22px] font-bold"
style={{
color: theme.colors.textPrimary,
letterSpacing: -0.5,
}}
>
{alert.title}
</Text>
@@ -213,18 +226,17 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
<View className="flex-row flex-wrap gap-2 mt-3">
{alert.currentAlertState ? (
<View
className="flex-row items-center px-3 py-1.5 rounded-full"
style={{
backgroundColor: theme.colors.backgroundGlass,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
className="flex-row items-center px-2.5 py-1 rounded-md"
style={{ backgroundColor: stateColor + "14" }}
>
<View
className="w-2.5 h-2.5 rounded-full mr-2"
className="w-2 h-2 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text className="text-[13px] font-semibold text-text-primary">
<Text
className="text-[12px] font-semibold"
style={{ color: stateColor }}
>
{alert.currentAlertState.name}
</Text>
</View>
@@ -232,11 +244,11 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
{alert.alertSeverity ? (
<View
className="flex-row items-center px-3 py-1.5 rounded-full"
style={{ backgroundColor: severityColor + "1A" }}
className="flex-row items-center px-2.5 py-1 rounded-md"
style={{ backgroundColor: severityColor + "14" }}
>
<Text
className="text-[13px] font-semibold"
className="text-[12px] font-semibold"
style={{ color: severityColor }}
>
{alert.alertSeverity.name}
@@ -244,14 +256,17 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
</View>
) : null}
</View>
</LinearGradient>
</GlassCard>
</View>
</View>
{/* Description */}
{alert.description ? (
<View className="mb-6">
<SectionHeader title="Description" iconName="document-text-outline" />
<Text className="text-body-md text-text-primary leading-6">
<Text
className="text-[14px] leading-[22px]"
style={{ color: theme.colors.textPrimary }}
>
{alert.description}
</Text>
</View>
@@ -260,34 +275,50 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
{/* Details */}
<View className="mb-6">
<SectionHeader title="Details" iconName="information-circle-outline" />
<GlassCard
<View
className="rounded-xl overflow-hidden"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
borderLeftWidth: 3,
borderLeftColor: theme.colors.actionPrimary,
}}
>
<View className="p-4">
<View className="flex-row mb-3">
<Text className="text-sm w-[90px] text-text-tertiary">
<Text
className="text-[13px] w-[90px]"
style={{ color: theme.colors.textTertiary }}
>
Created
</Text>
<Text className="text-sm text-text-primary">
<Text
className="text-[13px]"
style={{ color: theme.colors.textPrimary }}
>
{formatDateTime(alert.createdAt)}
</Text>
</View>
{alert.monitor ? (
<View className="flex-row">
<Text className="text-sm w-[90px] text-text-tertiary">
<Text
className="text-[13px] w-[90px]"
style={{ color: theme.colors.textTertiary }}
>
Monitor
</Text>
<Text className="text-sm text-text-primary">
<Text
className="text-[13px]"
style={{ color: theme.colors.textPrimary }}
>
{alert.monitor.name}
</Text>
</View>
) : null}
</View>
</GlassCard>
</View>
</View>
{/* State Change Actions */}
@@ -297,14 +328,9 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
<View className="flex-row gap-3">
{!isAcknowledged && !isResolved && acknowledgeState ? (
<TouchableOpacity
className="flex-1 flex-row py-3.5 rounded-xl items-center justify-center min-h-[50px]"
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px]"
style={{
backgroundColor: theme.colors.stateAcknowledged,
shadowColor: theme.colors.stateAcknowledged,
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 4 },
shadowRadius: 12,
elevation: 4,
}}
onPress={() => {
return handleStateChange(
@@ -318,19 +344,19 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
accessibilityLabel="Acknowledge alert"
>
{changingState ? (
<ActivityIndicator
size="small"
color={theme.colors.textInverse}
/>
<ActivityIndicator size="small" color="#FFFFFF" />
) : (
<>
<Ionicons
name="checkmark-circle-outline"
size={18}
color={theme.colors.textInverse}
size={17}
color="#FFFFFF"
style={{ marginRight: 6 }}
/>
<Text className="text-[15px] font-bold text-text-inverse">
<Text
className="text-[14px] font-bold"
style={{ color: "#FFFFFF" }}
>
Acknowledge
</Text>
</>
@@ -340,14 +366,9 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
{resolveState ? (
<TouchableOpacity
className="flex-1 flex-row py-3.5 rounded-xl items-center justify-center min-h-[50px]"
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px]"
style={{
backgroundColor: theme.colors.stateResolved,
shadowColor: theme.colors.stateResolved,
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 4 },
shadowRadius: 12,
elevation: 4,
}}
onPress={() => {
return handleStateChange(resolveState._id, resolveState.name);
@@ -358,19 +379,19 @@ export default function AlertDetailScreen({ route }: Props): React.JSX.Element {
accessibilityLabel="Resolve alert"
>
{changingState ? (
<ActivityIndicator
size="small"
color={theme.colors.textInverse}
/>
<ActivityIndicator size="small" color="#FFFFFF" />
) : (
<>
<Ionicons
name="checkmark-done-outline"
size={18}
color={theme.colors.textInverse}
size={17}
color="#FFFFFF"
style={{ marginRight: 6 }}
/>
<Text className="text-[15px] font-bold text-text-inverse">
<Text
className="text-[14px] font-bold"
style={{ color: "#FFFFFF" }}
>
Resolve
</Text>
</>

View File

@@ -9,7 +9,6 @@ import {
Alert,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import { useTheme } from "../theme";
import {
@@ -33,7 +32,6 @@ import FeedTimeline from "../components/FeedTimeline";
import SkeletonCard from "../components/SkeletonCard";
import SectionHeader from "../components/SectionHeader";
import NotesSection from "../components/NotesSection";
import GlassCard from "../components/GlassCard";
import { useHaptics } from "../hooks/useHaptics";
type Props = NativeStackScreenProps<AlertsStackParamList, "AlertEpisodeDetail">;
@@ -147,7 +145,10 @@ export default function AlertEpisodeDetailScreen({
if (isLoading) {
return (
<View className="flex-1 bg-bg-primary">
<View
className="flex-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<SkeletonCard variant="detail" />
</View>
);
@@ -155,8 +156,14 @@ export default function AlertEpisodeDetailScreen({
if (!episode) {
return (
<View className="flex-1 items-center justify-center bg-bg-primary">
<Text className="text-body-md text-text-secondary">
<View
className="flex-1 items-center justify-center"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<Text
className="text-[15px]"
style={{ color: theme.colors.textSecondary }}
>
Episode not found.
</Text>
</View>
@@ -186,194 +193,118 @@ export default function AlertEpisodeDetailScreen({
return (
<ScrollView
className="bg-bg-primary"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
refreshControl={
<RefreshControl refreshing={false} onRefresh={onRefresh} />
}
>
{/* Header with glass card */}
<GlassCard style={{ marginBottom: 20 }}>
<LinearGradient
colors={[theme.colors.gradientStart, theme.colors.gradientEnd]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
className="p-5"
>
<View
className="self-start px-3 py-1.5 rounded-full mb-3"
style={{ backgroundColor: stateColor + "1A" }}
>
<Text
className="text-[13px] font-bold"
style={{ color: stateColor }}
>
{episode.episodeNumberWithPrefix || `#${episode.episodeNumber}`}
</Text>
</View>
<View
className="rounded-2xl overflow-hidden mb-5"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<View style={{ height: 3, backgroundColor: stateColor }} />
<View className="p-5">
<Text
className="text-title-lg text-text-primary"
style={{ letterSpacing: -0.5 }}
className="text-[13px] font-semibold mb-2"
style={{ color: stateColor }}
>
{episode.episodeNumberWithPrefix || `#${episode.episodeNumber}`}
</Text>
<Text
className="text-[22px] font-bold"
style={{ color: theme.colors.textPrimary, letterSpacing: -0.5 }}
>
{episode.title}
</Text>
<View className="flex-row flex-wrap gap-2 mt-3">
{episode.currentAlertState ? (
<View
className="flex-row items-center px-3 py-1.5 rounded-full"
style={{
backgroundColor: theme.colors.backgroundGlass,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
className="flex-row items-center px-2.5 py-1 rounded-md"
style={{ backgroundColor: stateColor + "14" }}
>
<View
className="w-2.5 h-2.5 rounded-full mr-2"
style={{ backgroundColor: stateColor }}
/>
<Text className="text-[13px] font-semibold text-text-primary">
{episode.currentAlertState.name}
</Text>
<View className="w-2 h-2 rounded-full mr-1.5" style={{ backgroundColor: stateColor }} />
<Text className="text-[12px] font-semibold" style={{ color: stateColor }}>{episode.currentAlertState.name}</Text>
</View>
) : null}
{episode.alertSeverity ? (
<View
className="flex-row items-center px-3 py-1.5 rounded-full"
style={{ backgroundColor: severityColor + "1A" }}
className="flex-row items-center px-2.5 py-1 rounded-md"
style={{ backgroundColor: severityColor + "14" }}
>
<Text
className="text-[13px] font-semibold"
style={{ color: severityColor }}
>
{episode.alertSeverity.name}
</Text>
<Text className="text-[12px] font-semibold" style={{ color: severityColor }}>{episode.alertSeverity.name}</Text>
</View>
) : null}
</View>
</LinearGradient>
</GlassCard>
</View>
</View>
{/* Description */}
{episode.description ? (
<View className="mb-6">
<SectionHeader title="Description" iconName="document-text-outline" />
<Text className="text-body-md text-text-primary leading-6">
{episode.description}
</Text>
<Text className="text-[14px] leading-[22px]" style={{ color: theme.colors.textPrimary }}>{episode.description}</Text>
</View>
) : null}
{/* Details */}
<View className="mb-6">
<SectionHeader title="Details" iconName="information-circle-outline" />
<GlassCard
<View
className="rounded-xl overflow-hidden"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
borderLeftWidth: 3,
borderLeftColor: theme.colors.actionPrimary,
}}
>
<View className="p-4">
<View className="flex-row mb-3">
<Text className="text-sm w-[90px] text-text-tertiary">
Created
</Text>
<Text className="text-sm text-text-primary">
{formatDateTime(episode.createdAt)}
</Text>
<Text className="text-[13px] w-[90px]" style={{ color: theme.colors.textTertiary }}>Created</Text>
<Text className="text-[13px]" style={{ color: theme.colors.textPrimary }}>{formatDateTime(episode.createdAt)}</Text>
</View>
<View className="flex-row">
<Text className="text-sm w-[90px] text-text-tertiary">
Alerts
</Text>
<Text className="text-sm text-text-primary">
{episode.alertCount ?? 0}
</Text>
<Text className="text-[13px] w-[90px]" style={{ color: theme.colors.textTertiary }}>Alerts</Text>
<Text className="text-[13px]" style={{ color: theme.colors.textPrimary }}>{episode.alertCount ?? 0}</Text>
</View>
</View>
</GlassCard>
</View>
</View>
{/* State Change Actions */}
{!isResolved ? (
<View className="mb-6">
<SectionHeader title="Actions" iconName="flash-outline" />
<View className="flex-row gap-3">
{!isAcknowledged && !isResolved && acknowledgeState ? (
<TouchableOpacity
className="flex-1 flex-row py-3.5 rounded-xl items-center justify-center min-h-[50px]"
style={{
backgroundColor: theme.colors.stateAcknowledged,
shadowColor: theme.colors.stateAcknowledged,
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 4 },
shadowRadius: 8,
elevation: 4,
}}
onPress={() => {
return handleStateChange(
acknowledgeState._id,
acknowledgeState.name,
);
}}
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px]"
style={{ backgroundColor: theme.colors.stateAcknowledged }}
onPress={() => { return handleStateChange(acknowledgeState._id, acknowledgeState.name); }}
disabled={changingState}
>
{changingState ? (
<ActivityIndicator
size="small"
color={theme.colors.textInverse}
/>
) : (
{changingState ? (<ActivityIndicator size="small" color="#FFFFFF" />) : (
<>
<Ionicons
name="checkmark-circle-outline"
size={18}
color={theme.colors.textInverse}
style={{ marginRight: 6 }}
/>
<Text className="text-[15px] font-bold text-text-inverse">
Acknowledge
</Text>
<Ionicons name="checkmark-circle-outline" size={17} color="#FFFFFF" style={{ marginRight: 6 }} />
<Text className="text-[14px] font-bold" style={{ color: "#FFFFFF" }}>Acknowledge</Text>
</>
)}
</TouchableOpacity>
) : null}
{resolveState ? (
<TouchableOpacity
className="flex-1 flex-row py-3.5 rounded-xl items-center justify-center min-h-[50px]"
style={{
backgroundColor: theme.colors.stateResolved,
shadowColor: theme.colors.stateResolved,
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 4 },
shadowRadius: 8,
elevation: 4,
}}
onPress={() => {
return handleStateChange(resolveState._id, resolveState.name);
}}
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px]"
style={{ backgroundColor: theme.colors.stateResolved }}
onPress={() => { return handleStateChange(resolveState._id, resolveState.name); }}
disabled={changingState}
>
{changingState ? (
<ActivityIndicator
size="small"
color={theme.colors.textInverse}
/>
) : (
{changingState ? (<ActivityIndicator size="small" color="#FFFFFF" />) : (
<>
<Ionicons
name="checkmark-done-outline"
size={18}
color={theme.colors.textInverse}
style={{ marginRight: 6 }}
/>
<Text className="text-[15px] font-bold text-text-inverse">
Resolve
</Text>
<Ionicons name="checkmark-done-outline" size={17} color="#FFFFFF" style={{ marginRight: 6 }} />
<Text className="text-[14px] font-bold" style={{ color: "#FFFFFF" }}>Resolve</Text>
</>
)}
</TouchableOpacity>
@@ -382,7 +313,6 @@ export default function AlertEpisodeDetailScreen({
</View>
) : null}
{/* Activity Feed */}
{feed && feed.length > 0 ? (
<View className="mb-6">
<SectionHeader title="Activity Feed" iconName="list-outline" />
@@ -390,17 +320,8 @@ export default function AlertEpisodeDetailScreen({
</View>
) : null}
{/* Internal Notes */}
<NotesSection notes={notes} setNoteModalVisible={setNoteModalVisible} />
<AddNoteModal
visible={noteModalVisible}
onClose={() => {
return setNoteModalVisible(false);
}}
onSubmit={handleAddNote}
isSubmitting={submittingNote}
/>
<AddNoteModal visible={noteModalVisible} onClose={() => { return setNoteModalVisible(false); }} onSubmit={handleAddNote} isSubmitting={submittingNote} />
</ScrollView>
);
}

View File

@@ -59,30 +59,34 @@ function SectionHeader({
}): React.JSX.Element {
const { theme } = useTheme();
return (
<View className="flex-row items-center pb-2 pt-1 bg-bg-primary">
<View
className="flex-row items-center pb-2 pt-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<Ionicons
name={isActive ? "flame" : "checkmark-done"}
size={14}
size={13}
color={
isActive ? theme.colors.severityCritical : theme.colors.textTertiary
}
style={{ marginRight: 6 }}
/>
<Text
className="text-[13px] font-semibold uppercase tracking-wide"
className="text-[12px] font-semibold uppercase"
style={{
color: isActive
? theme.colors.textPrimary
: theme.colors.textTertiary,
letterSpacing: 0.6,
}}
>
{title}
</Text>
<View
className="ml-2 px-1.5 py-0.5 rounded-full"
className="ml-2 px-1.5 py-0.5 rounded"
style={{
backgroundColor: isActive
? theme.colors.severityCritical + "1A"
? theme.colors.severityCritical + "18"
: theme.colors.backgroundTertiary,
}}
>
@@ -102,6 +106,7 @@ function SectionHeader({
}
export default function AlertsScreen(): React.JSX.Element {
const { theme } = useTheme();
const navigation: NavProp = useNavigation<NavProp>();
const [segment, setSegment] = useState<Segment>("alerts");
@@ -288,7 +293,10 @@ export default function AlertsScreen(): React.JSX.Element {
if (showLoading) {
return (
<View className="flex-1 bg-bg-primary">
<View
className="flex-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<SegmentedControl
segments={[
{ key: "alerts" as const, label: "Alerts" },
@@ -316,7 +324,10 @@ export default function AlertsScreen(): React.JSX.Element {
return refetchEpisodes();
};
return (
<View className="flex-1 bg-bg-primary">
<View
className="flex-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<SegmentedControl
segments={[
{ key: "alerts" as const, label: "Alerts" },
@@ -341,7 +352,10 @@ export default function AlertsScreen(): React.JSX.Element {
}
return (
<View className="flex-1 bg-bg-primary">
<View
className="flex-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<SegmentedControl
segments={[
{ key: "alerts" as const, label: "Alerts" },
@@ -394,7 +408,7 @@ export default function AlertsScreen(): React.JSX.Element {
wrapped.item.currentAlertState?._id !== acknowledgeState._id
? {
label: "Acknowledge",
color: "#2EA043",
color: "#22C55E",
onAction: () => {
return handleAcknowledge(wrapped);
},

View File

@@ -3,7 +3,6 @@ import { View, Text } from "react-native";
import { useTheme } from "../theme";
import * as LocalAuthentication from "expo-local-authentication";
import Logo from "../components/Logo";
import GradientHeader from "../components/GradientHeader";
import GradientButton from "../components/GradientButton";
interface BiometricLockScreenProps {
@@ -34,42 +33,37 @@ export default function BiometricLockScreen({
}, []);
return (
<View className="flex-1 items-center justify-center px-10 bg-bg-primary">
<GradientHeader height={400} />
{/* Outer glow ring */}
<View
className="flex-1 items-center justify-center px-10"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<View
className="w-[120px] h-[120px] rounded-full items-center justify-center"
style={{ backgroundColor: theme.colors.surfaceGlow }}
className="w-20 h-20 rounded-2xl items-center justify-center mb-6"
style={{
backgroundColor: theme.colors.iconBackground,
}}
>
{/* Inner icon container */}
<View
className="w-[88px] h-[88px] rounded-[22px] items-center justify-center"
style={{
backgroundColor: theme.colors.actionPrimary + "18",
shadowColor: theme.colors.actionPrimary,
shadowOpacity: 0.2,
shadowOffset: { width: 0, height: 8 },
shadowRadius: 24,
elevation: 8,
}}
>
<Logo size={48} />
</View>
<Logo size={40} />
</View>
<Text
className="text-title-md text-text-primary mt-7 text-center"
style={{ letterSpacing: -0.3 }}
className="text-[20px] font-bold text-center"
style={{
color: theme.colors.textPrimary,
letterSpacing: -0.3,
}}
>
OneUptime is Locked
</Text>
<Text className="text-body-md text-text-secondary mt-2.5 text-center leading-6">
<Text
className="text-[15px] mt-2 text-center"
style={{ color: theme.colors.textSecondary }}
>
Use {biometricType.toLowerCase()} to unlock
</Text>
<View className="mt-10 w-full" style={{ maxWidth: 280 }}>
<View className="mt-10 w-full" style={{ maxWidth: 260 }}>
<GradientButton
label="Unlock"
onPress={authenticate}

View File

@@ -8,7 +8,6 @@ import {
ActivityIndicator,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import { useTheme } from "../theme";
import { useAllProjectCounts } from "../hooks/useAllProjectCounts";
import { useProject } from "../hooks/useProject";
@@ -17,8 +16,6 @@ import { useNavigation } from "@react-navigation/native";
import type { BottomTabNavigationProp } from "@react-navigation/bottom-tabs";
import type { MainTabParamList } from "../navigation/types";
import Logo from "../components/Logo";
import GlassCard from "../components/GlassCard";
import GradientHeader from "../components/GradientHeader";
import GradientButton from "../components/GradientButton";
type HomeNavProp = BottomTabNavigationProp<MainTabParamList, "Home">;
@@ -50,57 +47,55 @@ function StatCard({
return (
<TouchableOpacity
className="flex-1 overflow-hidden rounded-2xl"
style={{
shadowColor: "#000",
shadowOpacity: theme.isDark ? 0.2 : 0.06,
shadowOffset: { width: 0, height: 4 },
shadowRadius: 12,
elevation: 3,
}}
className="flex-1 rounded-2xl overflow-hidden"
onPress={handlePress}
activeOpacity={0.7}
accessibilityLabel={`${count ?? 0} ${label}. Tap to view.`}
accessibilityRole="button"
>
<GlassCard opaque>
<View className="flex-row">
<LinearGradient
colors={[accentColor, accentColor + "40"]}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
style={{ width: 3 }}
/>
<View className="flex-1 p-4">
<View className="flex-row items-center justify-between mb-3">
<View
className="w-10 h-10 rounded-xl items-center justify-center"
style={{ backgroundColor: accentColor + "18" }}
>
<Ionicons name={iconName} size={20} color={accentColor} />
</View>
<Ionicons
name="chevron-forward"
size={16}
color={theme.colors.textTertiary}
/>
</View>
<Text
className="text-[32px] font-bold text-text-primary"
style={{ fontVariant: ["tabular-nums"], letterSpacing: -1.2 }}
>
{isLoading ? "--" : count ?? 0}
</Text>
<Text
className="text-[12px] font-medium text-text-secondary mt-0.5"
style={{ letterSpacing: 0.3 }}
numberOfLines={1}
>
{label}
</Text>
<View
className="p-4"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
borderRadius: 16,
}}
>
<View className="flex-row items-center justify-between mb-3">
<View
className="w-9 h-9 rounded-xl items-center justify-center"
style={{ backgroundColor: accentColor + "14" }}
>
<Ionicons name={iconName} size={18} color={accentColor} />
</View>
<Ionicons
name="chevron-forward"
size={14}
color={theme.colors.textTertiary}
/>
</View>
</GlassCard>
<Text
className="text-[28px] font-bold"
style={{
color: theme.colors.textPrimary,
fontVariant: ["tabular-nums"],
letterSpacing: -1,
}}
>
{isLoading ? "--" : count ?? 0}
</Text>
<Text
className="text-[12px] font-medium mt-1"
style={{
color: theme.colors.textSecondary,
letterSpacing: 0.2,
}}
numberOfLines={1}
>
{label}
</Text>
</View>
</TouchableOpacity>
);
}
@@ -137,11 +132,10 @@ export default function HomeScreen(): React.JSX.Element {
await Promise.all([refetch(), refreshProjects()]);
};
// No projects state
if (!isLoadingProjects && projectList.length === 0) {
return (
<ScrollView
className="bg-bg-primary"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
contentContainerStyle={{ flexGrow: 1 }}
refreshControl={
<RefreshControl
@@ -151,34 +145,29 @@ export default function HomeScreen(): React.JSX.Element {
/>
}
>
<GradientHeader height={400} />
<View className="flex-1 items-center justify-center px-8">
<View
className="w-28 h-28 rounded-full items-center justify-center mb-6"
style={{ backgroundColor: theme.colors.surfaceGlow }}
className="w-20 h-20 rounded-2xl items-center justify-center mb-6"
style={{
backgroundColor: theme.colors.iconBackground,
}}
>
<View
className="w-20 h-20 rounded-[22px] items-center justify-center"
style={{
backgroundColor: theme.colors.backgroundTertiary,
shadowColor: "#000000",
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 4 },
shadowRadius: 16,
elevation: 6,
}}
>
<Logo size={48} />
</View>
<Logo size={40} />
</View>
<Text
className="text-title-lg text-text-primary text-center"
style={{ letterSpacing: -0.5 }}
className="text-[22px] font-bold text-center"
style={{
color: theme.colors.textPrimary,
letterSpacing: -0.5,
}}
>
No Projects Found
</Text>
<Text className="text-body-md text-text-secondary text-center mt-3 leading-6 max-w-[300px]">
<Text
className="text-[15px] text-center mt-2 leading-[22px] max-w-[300px]"
style={{ color: theme.colors.textSecondary }}
>
You don&apos;t have access to any projects. Contact your
administrator or pull to refresh.
</Text>
@@ -195,11 +184,12 @@ export default function HomeScreen(): React.JSX.Element {
);
}
// Loading state
if (isLoadingProjects) {
return (
<View className="flex-1 bg-bg-primary items-center justify-center">
<GradientHeader height={400} />
<View
className="flex-1 items-center justify-center"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<ActivityIndicator size="large" color={theme.colors.actionPrimary} />
</View>
);
@@ -212,7 +202,7 @@ export default function HomeScreen(): React.JSX.Element {
return (
<ScrollView
className="bg-bg-primary"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
contentContainerStyle={{ paddingBottom: 48 }}
refreshControl={
<RefreshControl
@@ -222,48 +212,40 @@ export default function HomeScreen(): React.JSX.Element {
/>
}
>
{/* Header area with gradient background */}
<LinearGradient
colors={[theme.colors.gradientStart, theme.colors.gradientEnd]}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
className="px-6 pt-6 pb-5"
>
<View className="flex-row items-center mb-1">
<View className="px-5 pt-4 pb-4">
<View className="flex-row items-center">
<View
className="w-10 h-10 rounded-full items-center justify-center mr-3"
className="w-10 h-10 rounded-xl items-center justify-center mr-3"
style={{
backgroundColor: theme.colors.iconBackground,
}}
>
<Logo size={24} />
<Logo size={22} />
</View>
<View className="flex-1">
<Text
className="text-body-md text-text-secondary"
style={{ letterSpacing: 0.2 }}
className="text-[13px] font-medium"
style={{
color: theme.colors.textTertiary,
}}
>
{getGreeting()}
</Text>
<Text
className="text-title-lg text-text-primary"
className="text-[22px] font-bold"
accessibilityRole="header"
style={{ letterSpacing: -0.5 }}
style={{
color: theme.colors.textPrimary,
letterSpacing: -0.5,
}}
>
{subtitle}
</Text>
</View>
<Text
className="text-[11px] font-semibold text-text-tertiary"
style={{ letterSpacing: 0.5 }}
>
OneUptime
</Text>
</View>
</LinearGradient>
</View>
{/* Stat Cards - 2x2 Grid */}
<View className="gap-3 px-6 mt-2">
<View className="gap-3 px-5">
<View className="flex-row gap-3">
<StatCard
count={incidentCount}

View File

@@ -9,7 +9,6 @@ import {
Alert,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import { useTheme } from "../theme";
import {
@@ -30,7 +29,6 @@ import FeedTimeline from "../components/FeedTimeline";
import SkeletonCard from "../components/SkeletonCard";
import SectionHeader from "../components/SectionHeader";
import NotesSection from "../components/NotesSection";
import GlassCard from "../components/GlassCard";
import { useHaptics } from "../hooks/useHaptics";
import type { IncidentItem, IncidentState, NamedEntity } from "../api/types";
@@ -150,7 +148,10 @@ export default function IncidentDetailScreen({
if (isLoading) {
return (
<View className="flex-1 bg-bg-primary">
<View
className="flex-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<SkeletonCard variant="detail" />
</View>
);
@@ -158,8 +159,14 @@ export default function IncidentDetailScreen({
if (!incident) {
return (
<View className="flex-1 items-center justify-center bg-bg-primary">
<Text className="text-body-md text-text-secondary">
<View
className="flex-1 items-center justify-center"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<Text
className="text-[15px]"
style={{ color: theme.colors.textSecondary }}
>
Incident not found.
</Text>
</View>
@@ -191,36 +198,42 @@ export default function IncidentDetailScreen({
return (
<ScrollView
className="bg-bg-primary"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
contentContainerStyle={{ padding: 20, paddingBottom: 48 }}
refreshControl={
<RefreshControl refreshing={false} onRefresh={onRefresh} />
}
>
{/* Header with glass card */}
<GlassCard style={{ marginBottom: 20 }}>
<LinearGradient
colors={[theme.colors.gradientStart, theme.colors.gradientEnd]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
className="p-5"
>
<View
className="self-start px-3 py-1.5 rounded-full mb-3"
style={{ backgroundColor: stateColor + "1A" }}
{/* Header card */}
<View
className="rounded-2xl overflow-hidden mb-5"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<View
style={{
height: 3,
backgroundColor: stateColor,
}}
/>
<View className="p-5">
<Text
className="text-[13px] font-semibold mb-2"
style={{ color: stateColor }}
>
<Text
className="text-[13px] font-bold"
style={{ color: stateColor }}
>
{incident.incidentNumberWithPrefix ||
`#${incident.incidentNumber}`}
</Text>
</View>
{incident.incidentNumberWithPrefix ||
`#${incident.incidentNumber}`}
</Text>
<Text
className="text-title-lg text-text-primary"
style={{ letterSpacing: -0.5 }}
className="text-[22px] font-bold"
style={{
color: theme.colors.textPrimary,
letterSpacing: -0.5,
}}
>
{incident.title}
</Text>
@@ -228,18 +241,17 @@ export default function IncidentDetailScreen({
<View className="flex-row flex-wrap gap-2 mt-3">
{incident.currentIncidentState ? (
<View
className="flex-row items-center px-3 py-1.5 rounded-full"
style={{
backgroundColor: theme.colors.backgroundGlass,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
className="flex-row items-center px-2.5 py-1 rounded-md"
style={{ backgroundColor: stateColor + "14" }}
>
<View
className="w-2.5 h-2.5 rounded-full mr-2"
className="w-2 h-2 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text className="text-[13px] font-semibold text-text-primary">
<Text
className="text-[12px] font-semibold"
style={{ color: stateColor }}
>
{incident.currentIncidentState.name}
</Text>
</View>
@@ -247,11 +259,11 @@ export default function IncidentDetailScreen({
{incident.incidentSeverity ? (
<View
className="flex-row items-center px-3 py-1.5 rounded-full"
style={{ backgroundColor: severityColor + "1A" }}
className="flex-row items-center px-2.5 py-1 rounded-md"
style={{ backgroundColor: severityColor + "14" }}
>
<Text
className="text-[13px] font-semibold"
className="text-[12px] font-semibold"
style={{ color: severityColor }}
>
{incident.incidentSeverity.name}
@@ -259,14 +271,17 @@ export default function IncidentDetailScreen({
</View>
) : null}
</View>
</LinearGradient>
</GlassCard>
</View>
</View>
{/* Description */}
{incident.description ? (
<View className="mb-6">
<SectionHeader title="Description" iconName="document-text-outline" />
<Text className="text-body-md text-text-primary leading-6">
<Text
className="text-[14px] leading-[22px]"
style={{ color: theme.colors.textPrimary }}
>
{incident.description}
</Text>
</View>
@@ -275,8 +290,12 @@ export default function IncidentDetailScreen({
{/* Details */}
<View className="mb-6">
<SectionHeader title="Details" iconName="information-circle-outline" />
<GlassCard
<View
className="rounded-xl overflow-hidden"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
borderLeftWidth: 3,
borderLeftColor: theme.colors.actionPrimary,
}}
@@ -284,30 +303,48 @@ export default function IncidentDetailScreen({
<View className="p-4">
{incident.declaredAt ? (
<View className="flex-row mb-3">
<Text className="text-sm w-[90px] text-text-tertiary">
<Text
className="text-[13px] w-[90px]"
style={{ color: theme.colors.textTertiary }}
>
Declared
</Text>
<Text className="text-sm text-text-primary">
<Text
className="text-[13px]"
style={{ color: theme.colors.textPrimary }}
>
{formatDateTime(incident.declaredAt)}
</Text>
</View>
) : null}
<View className="flex-row mb-3">
<Text className="text-sm w-[90px] text-text-tertiary">
<Text
className="text-[13px] w-[90px]"
style={{ color: theme.colors.textTertiary }}
>
Created
</Text>
<Text className="text-sm text-text-primary">
<Text
className="text-[13px]"
style={{ color: theme.colors.textPrimary }}
>
{formatDateTime(incident.createdAt)}
</Text>
</View>
{incident.monitors?.length > 0 ? (
<View className="flex-row">
<Text className="text-sm w-[90px] text-text-tertiary">
<Text
className="text-[13px] w-[90px]"
style={{ color: theme.colors.textTertiary }}
>
Monitors
</Text>
<Text className="text-sm text-text-primary flex-1">
<Text
className="text-[13px] flex-1"
style={{ color: theme.colors.textPrimary }}
>
{incident.monitors
.map((m: NamedEntity) => {
return m.name;
@@ -317,7 +354,7 @@ export default function IncidentDetailScreen({
</View>
) : null}
</View>
</GlassCard>
</View>
</View>
{/* State Change Actions */}
@@ -327,14 +364,9 @@ export default function IncidentDetailScreen({
<View className="flex-row gap-3">
{!isAcknowledged && !isResolved && acknowledgeState ? (
<TouchableOpacity
className="flex-1 flex-row py-3.5 rounded-xl items-center justify-center min-h-[50px]"
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px]"
style={{
backgroundColor: theme.colors.stateAcknowledged,
shadowColor: theme.colors.stateAcknowledged,
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 4 },
shadowRadius: 12,
elevation: 4,
}}
onPress={() => {
return handleStateChange(
@@ -348,19 +380,19 @@ export default function IncidentDetailScreen({
accessibilityLabel="Acknowledge incident"
>
{changingState ? (
<ActivityIndicator
size="small"
color={theme.colors.textInverse}
/>
<ActivityIndicator size="small" color="#FFFFFF" />
) : (
<>
<Ionicons
name="checkmark-circle-outline"
size={18}
color={theme.colors.textInverse}
size={17}
color="#FFFFFF"
style={{ marginRight: 6 }}
/>
<Text className="text-[15px] font-bold text-text-inverse">
<Text
className="text-[14px] font-bold"
style={{ color: "#FFFFFF" }}
>
Acknowledge
</Text>
</>
@@ -370,14 +402,9 @@ export default function IncidentDetailScreen({
{resolveState ? (
<TouchableOpacity
className="flex-1 flex-row py-3.5 rounded-xl items-center justify-center min-h-[50px]"
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px]"
style={{
backgroundColor: theme.colors.stateResolved,
shadowColor: theme.colors.stateResolved,
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 4 },
shadowRadius: 12,
elevation: 4,
}}
onPress={() => {
return handleStateChange(resolveState._id, resolveState.name);
@@ -388,19 +415,19 @@ export default function IncidentDetailScreen({
accessibilityLabel="Resolve incident"
>
{changingState ? (
<ActivityIndicator
size="small"
color={theme.colors.textInverse}
/>
<ActivityIndicator size="small" color="#FFFFFF" />
) : (
<>
<Ionicons
name="checkmark-done-outline"
size={18}
color={theme.colors.textInverse}
size={17}
color="#FFFFFF"
style={{ marginRight: 6 }}
/>
<Text className="text-[15px] font-bold text-text-inverse">
<Text
className="text-[14px] font-bold"
style={{ color: "#FFFFFF" }}
>
Resolve
</Text>
</>

View File

@@ -9,7 +9,6 @@ import {
Alert,
} from "react-native";
import { Ionicons } from "@expo/vector-icons";
import { LinearGradient } from "expo-linear-gradient";
import type { NativeStackScreenProps } from "@react-navigation/native-stack";
import { useTheme } from "../theme";
import {
@@ -33,7 +32,6 @@ import FeedTimeline from "../components/FeedTimeline";
import SkeletonCard from "../components/SkeletonCard";
import SectionHeader from "../components/SectionHeader";
import NotesSection from "../components/NotesSection";
import GlassCard from "../components/GlassCard";
import { useHaptics } from "../hooks/useHaptics";
type Props = NativeStackScreenProps<
@@ -153,7 +151,10 @@ export default function IncidentEpisodeDetailScreen({
if (isLoading) {
return (
<View className="flex-1 bg-bg-primary">
<View
className="flex-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<SkeletonCard variant="detail" />
</View>
);
@@ -161,8 +162,14 @@ export default function IncidentEpisodeDetailScreen({
if (!episode) {
return (
<View className="flex-1 items-center justify-center bg-bg-primary">
<Text className="text-body-md text-text-secondary">
<View
className="flex-1 items-center justify-center"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<Text
className="text-[15px]"
style={{ color: theme.colors.textSecondary }}
>
Episode not found.
</Text>
</View>
@@ -194,66 +201,59 @@ export default function IncidentEpisodeDetailScreen({
return (
<ScrollView
className="bg-bg-primary"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
contentContainerStyle={{ padding: 20, paddingBottom: 40 }}
refreshControl={
<RefreshControl refreshing={false} onRefresh={onRefresh} />
}
>
{/* Header with glass card */}
<GlassCard style={{ marginBottom: 20 }}>
<LinearGradient
colors={[theme.colors.gradientStart, theme.colors.gradientEnd]}
start={{ x: 0, y: 0 }}
end={{ x: 1, y: 1 }}
className="p-5"
>
<View
className="self-start px-3 py-1.5 rounded-full mb-3"
style={{ backgroundColor: stateColor + "1A" }}
>
<Text
className="text-[13px] font-bold"
style={{ color: stateColor }}
>
{episode.episodeNumberWithPrefix || `#${episode.episodeNumber}`}
</Text>
</View>
<View
className="rounded-2xl overflow-hidden mb-5"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<View style={{ height: 3, backgroundColor: stateColor }} />
<View className="p-5">
<Text
className="text-title-lg text-text-primary"
style={{ letterSpacing: -0.5 }}
className="text-[13px] font-semibold mb-2"
style={{ color: stateColor }}
>
{episode.episodeNumberWithPrefix || `#${episode.episodeNumber}`}
</Text>
<Text
className="text-[22px] font-bold"
style={{ color: theme.colors.textPrimary, letterSpacing: -0.5 }}
>
{episode.title}
</Text>
<View className="flex-row flex-wrap gap-2 mt-3">
{episode.currentIncidentState ? (
<View
className="flex-row items-center px-3 py-1.5 rounded-full"
style={{
backgroundColor: theme.colors.backgroundGlass,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
className="flex-row items-center px-2.5 py-1 rounded-md"
style={{ backgroundColor: stateColor + "14" }}
>
<View
className="w-2.5 h-2.5 rounded-full mr-2"
className="w-2 h-2 rounded-full mr-1.5"
style={{ backgroundColor: stateColor }}
/>
<Text className="text-[13px] font-semibold text-text-primary">
<Text
className="text-[12px] font-semibold"
style={{ color: stateColor }}
>
{episode.currentIncidentState.name}
</Text>
</View>
) : null}
{episode.incidentSeverity ? (
<View
className="flex-row items-center px-3 py-1.5 rounded-full"
style={{ backgroundColor: severityColor + "1A" }}
className="flex-row items-center px-2.5 py-1 rounded-md"
style={{ backgroundColor: severityColor + "14" }}
>
<Text
className="text-[13px] font-semibold"
className="text-[12px] font-semibold"
style={{ color: severityColor }}
>
{episode.incidentSeverity.name}
@@ -261,24 +261,29 @@ export default function IncidentEpisodeDetailScreen({
</View>
) : null}
</View>
</LinearGradient>
</GlassCard>
</View>
</View>
{/* Description */}
{episode.description ? (
<View className="mb-6">
<SectionHeader title="Description" iconName="document-text-outline" />
<Text className="text-body-md text-text-primary leading-6">
<Text
className="text-[14px] leading-[22px]"
style={{ color: theme.colors.textPrimary }}
>
{episode.description}
</Text>
</View>
) : null}
{/* Details */}
<View className="mb-6">
<SectionHeader title="Details" iconName="information-circle-outline" />
<GlassCard
<View
className="rounded-xl overflow-hidden"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
borderLeftWidth: 3,
borderLeftColor: theme.colors.actionPrimary,
}}
@@ -286,113 +291,52 @@ export default function IncidentEpisodeDetailScreen({
<View className="p-4">
{episode.declaredAt ? (
<View className="flex-row mb-3">
<Text className="text-sm w-[90px] text-text-tertiary">
Declared
</Text>
<Text className="text-sm text-text-primary">
{formatDateTime(episode.declaredAt)}
</Text>
<Text className="text-[13px] w-[90px]" style={{ color: theme.colors.textTertiary }}>Declared</Text>
<Text className="text-[13px]" style={{ color: theme.colors.textPrimary }}>{formatDateTime(episode.declaredAt)}</Text>
</View>
) : null}
<View className="flex-row mb-3">
<Text className="text-sm w-[90px] text-text-tertiary">
Created
</Text>
<Text className="text-sm text-text-primary">
{formatDateTime(episode.createdAt)}
</Text>
<Text className="text-[13px] w-[90px]" style={{ color: theme.colors.textTertiary }}>Created</Text>
<Text className="text-[13px]" style={{ color: theme.colors.textPrimary }}>{formatDateTime(episode.createdAt)}</Text>
</View>
<View className="flex-row">
<Text className="text-sm w-[90px] text-text-tertiary">
Incidents
</Text>
<Text className="text-sm text-text-primary">
{episode.incidentCount ?? 0}
</Text>
<Text className="text-[13px] w-[90px]" style={{ color: theme.colors.textTertiary }}>Incidents</Text>
<Text className="text-[13px]" style={{ color: theme.colors.textPrimary }}>{episode.incidentCount ?? 0}</Text>
</View>
</View>
</GlassCard>
</View>
</View>
{/* State Change Actions */}
{!isResolved ? (
<View className="mb-6">
<SectionHeader title="Actions" iconName="flash-outline" />
<View className="flex-row gap-3">
{!isAcknowledged && !isResolved && acknowledgeState ? (
<TouchableOpacity
className="flex-1 flex-row py-3.5 rounded-xl items-center justify-center min-h-[50px]"
style={{
backgroundColor: theme.colors.stateAcknowledged,
shadowColor: theme.colors.stateAcknowledged,
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 4 },
shadowRadius: 8,
elevation: 4,
}}
onPress={() => {
return handleStateChange(
acknowledgeState._id,
acknowledgeState.name,
);
}}
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px]"
style={{ backgroundColor: theme.colors.stateAcknowledged }}
onPress={() => { return handleStateChange(acknowledgeState._id, acknowledgeState.name); }}
disabled={changingState}
>
{changingState ? (
<ActivityIndicator
size="small"
color={theme.colors.textInverse}
/>
) : (
{changingState ? (<ActivityIndicator size="small" color="#FFFFFF" />) : (
<>
<Ionicons
name="checkmark-circle-outline"
size={18}
color={theme.colors.textInverse}
style={{ marginRight: 6 }}
/>
<Text className="text-[15px] font-bold text-text-inverse">
Acknowledge
</Text>
<Ionicons name="checkmark-circle-outline" size={17} color="#FFFFFF" style={{ marginRight: 6 }} />
<Text className="text-[14px] font-bold" style={{ color: "#FFFFFF" }}>Acknowledge</Text>
</>
)}
</TouchableOpacity>
) : null}
{resolveState ? (
<TouchableOpacity
className="flex-1 flex-row py-3.5 rounded-xl items-center justify-center min-h-[50px]"
style={{
backgroundColor: theme.colors.stateResolved,
shadowColor: theme.colors.stateResolved,
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 4 },
shadowRadius: 8,
elevation: 4,
}}
onPress={() => {
return handleStateChange(resolveState._id, resolveState.name);
}}
className="flex-1 flex-row py-3 rounded-xl items-center justify-center min-h-[48px]"
style={{ backgroundColor: theme.colors.stateResolved }}
onPress={() => { return handleStateChange(resolveState._id, resolveState.name); }}
disabled={changingState}
>
{changingState ? (
<ActivityIndicator
size="small"
color={theme.colors.textInverse}
/>
) : (
{changingState ? (<ActivityIndicator size="small" color="#FFFFFF" />) : (
<>
<Ionicons
name="checkmark-done-outline"
size={18}
color={theme.colors.textInverse}
style={{ marginRight: 6 }}
/>
<Text className="text-[15px] font-bold text-text-inverse">
Resolve
</Text>
<Ionicons name="checkmark-done-outline" size={17} color="#FFFFFF" style={{ marginRight: 6 }} />
<Text className="text-[14px] font-bold" style={{ color: "#FFFFFF" }}>Resolve</Text>
</>
)}
</TouchableOpacity>
@@ -401,7 +345,6 @@ export default function IncidentEpisodeDetailScreen({
</View>
) : null}
{/* Activity Feed */}
{feed && feed.length > 0 ? (
<View className="mb-6">
<SectionHeader title="Activity Feed" iconName="list-outline" />
@@ -409,17 +352,8 @@ export default function IncidentEpisodeDetailScreen({
</View>
) : null}
{/* Internal Notes */}
<NotesSection notes={notes} setNoteModalVisible={setNoteModalVisible} />
<AddNoteModal
visible={noteModalVisible}
onClose={() => {
return setNoteModalVisible(false);
}}
onSubmit={handleAddNote}
isSubmitting={submittingNote}
/>
<AddNoteModal visible={noteModalVisible} onClose={() => { return setNoteModalVisible(false); }} onSubmit={handleAddNote} isSubmitting={submittingNote} />
</ScrollView>
);
}

View File

@@ -62,30 +62,34 @@ function SectionHeader({
}): React.JSX.Element {
const { theme } = useTheme();
return (
<View className="flex-row items-center pb-2 pt-1 bg-bg-primary">
<View
className="flex-row items-center pb-2 pt-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<Ionicons
name={isActive ? "flame" : "checkmark-done"}
size={14}
size={13}
color={
isActive ? theme.colors.severityCritical : theme.colors.textTertiary
}
style={{ marginRight: 6 }}
/>
<Text
className="text-[13px] font-semibold uppercase tracking-wide"
className="text-[12px] font-semibold uppercase"
style={{
color: isActive
? theme.colors.textPrimary
: theme.colors.textTertiary,
letterSpacing: 0.6,
}}
>
{title}
</Text>
<View
className="ml-2 px-1.5 py-0.5 rounded-full"
className="ml-2 px-1.5 py-0.5 rounded"
style={{
backgroundColor: isActive
? theme.colors.severityCritical + "1A"
? theme.colors.severityCritical + "18"
: theme.colors.backgroundTertiary,
}}
>
@@ -105,6 +109,7 @@ function SectionHeader({
}
export default function IncidentsScreen(): React.JSX.Element {
const { theme } = useTheme();
const navigation: NavProp = useNavigation<NavProp>();
const [segment, setSegment] = useState<Segment>("incidents");
@@ -293,7 +298,10 @@ export default function IncidentsScreen(): React.JSX.Element {
if (showLoading) {
return (
<View className="flex-1 bg-bg-primary">
<View
className="flex-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<SegmentedControl
segments={[
{ key: "incidents" as const, label: "Incidents" },
@@ -321,7 +329,10 @@ export default function IncidentsScreen(): React.JSX.Element {
return refetchEpisodes();
};
return (
<View className="flex-1 bg-bg-primary">
<View
className="flex-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<SegmentedControl
segments={[
{ key: "incidents" as const, label: "Incidents" },
@@ -346,7 +357,10 @@ export default function IncidentsScreen(): React.JSX.Element {
}
return (
<View className="flex-1 bg-bg-primary">
<View
className="flex-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
>
<SegmentedControl
segments={[
{ key: "incidents" as const, label: "Incidents" },
@@ -400,7 +414,7 @@ export default function IncidentsScreen(): React.JSX.Element {
acknowledgeState._id
? {
label: "Acknowledge",
color: "#2EA043",
color: "#22C55E",
onAction: () => {
return handleAcknowledge(wrapped);
},

View File

@@ -8,7 +8,6 @@ import { useBiometric } from "../hooks/useBiometric";
import { useHaptics } from "../hooks/useHaptics";
import { getServerUrl } from "../storage/serverUrl";
import Logo from "../components/Logo";
import GlassCard from "../components/GlassCard";
const APP_VERSION: string = "1.0.0";
@@ -35,7 +34,7 @@ function SettingsRow({
const content: React.JSX.Element = (
<View
className="flex-row justify-between items-center px-4 min-h-[52px]"
className="flex-row justify-between items-center px-4 min-h-[48px]"
style={
!isLast
? {
@@ -47,19 +46,27 @@ function SettingsRow({
>
<View className="flex-row items-center flex-1">
{iconName ? (
<Ionicons
name={iconName}
size={20}
color={
destructive
? theme.colors.actionDestructive
: theme.colors.textSecondary
}
style={{ marginRight: 12 }}
/>
<View
className="w-7 h-7 rounded-lg items-center justify-center mr-3"
style={{
backgroundColor: destructive
? theme.colors.statusErrorBg
: theme.colors.iconBackground,
}}
>
<Ionicons
name={iconName}
size={15}
color={
destructive
? theme.colors.actionDestructive
: theme.colors.actionPrimary
}
/>
</View>
) : null}
<Text
className="text-base font-medium py-3.5"
className="text-[15px] font-medium py-3"
style={{
color: destructive
? theme.colors.actionDestructive
@@ -71,11 +78,16 @@ function SettingsRow({
</View>
{rightElement ??
(value ? (
<Text className="text-[15px] text-text-secondary">{value}</Text>
<Text
className="text-[14px]"
style={{ color: theme.colors.textTertiary }}
>
{value}
</Text>
) : onPress ? (
<Ionicons
name="chevron-forward"
size={20}
size={18}
color={theme.colors.textTertiary}
/>
) : null)}
@@ -122,43 +134,46 @@ export default function SettingsScreen(): React.JSX.Element {
return (
<ScrollView
className="bg-bg-primary"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
contentContainerStyle={{ padding: 20, paddingBottom: 60 }}
>
{/* Profile Header */}
<GlassCard style={{ marginBottom: 28 }}>
<LinearGradient
colors={[theme.colors.gradientStart, theme.colors.gradientEnd]}
start={{ x: 0.5, y: 0 }}
end={{ x: 0.5, y: 1 }}
className="items-center py-6"
{/* Header */}
<View className="items-center py-4 mb-6">
<View
className="w-14 h-14 rounded-2xl items-center justify-center mb-3"
style={{
backgroundColor: theme.colors.iconBackground,
}}
>
<View
className="w-16 h-16 rounded-full items-center justify-center mb-3"
style={{
backgroundColor: theme.colors.iconBackground,
}}
>
<Logo size={32} />
</View>
<Text
className="text-title-md text-text-primary"
style={{ letterSpacing: -0.3 }}
>
Settings
</Text>
<Text className="text-body-sm text-text-tertiary mt-1">
{serverUrl || "oneuptime.com"}
</Text>
</LinearGradient>
</GlassCard>
<Logo size={28} />
</View>
<Text
className="text-[11px] font-semibold uppercase"
style={{
color: theme.colors.textTertiary,
letterSpacing: 1.2,
}}
>
{serverUrl || "oneuptime.com"}
</Text>
</View>
{/* Appearance */}
<View className="mb-7">
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
<View className="mb-6">
<Text
className="text-[12px] font-semibold uppercase mb-2 ml-1"
style={{ color: theme.colors.textTertiary, letterSpacing: 0.8 }}
>
Appearance
</Text>
<GlassCard>
<View
className="rounded-2xl overflow-hidden"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<View className="p-1.5">
<View className="flex-row rounded-xl gap-1">
{(["dark", "light", "system"] as ThemeMode[]).map(
@@ -169,15 +184,15 @@ export default function SettingsScreen(): React.JSX.Element {
key={mode}
className="flex-1 flex-row items-center justify-center py-2.5 rounded-[10px] gap-1.5 overflow-hidden"
style={
!isActive
? undefined
: {
shadowColor: "#000000",
shadowOpacity: theme.isDark ? 0.4 : 0.15,
isActive
? {
shadowColor: theme.colors.accentGradientMid,
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 2 },
shadowRadius: 6,
elevation: 3,
}
: undefined
}
onPress={() => {
return handleThemeChange(mode);
@@ -209,18 +224,16 @@ export default function SettingsScreen(): React.JSX.Element {
? "sunny-outline"
: "phone-portrait-outline"
}
size={16}
size={15}
color={
isActive
? theme.colors.textInverse
: theme.colors.textSecondary
isActive ? "#FFFFFF" : theme.colors.textSecondary
}
/>
<Text
className="text-sm font-semibold"
className="text-[13px] font-semibold"
style={{
color: isActive
? theme.colors.textInverse
? "#FFFFFF"
: theme.colors.textPrimary,
}}
>
@@ -232,16 +245,26 @@ export default function SettingsScreen(): React.JSX.Element {
)}
</View>
</View>
</GlassCard>
</View>
</View>
{/* Security */}
{biometric.isAvailable ? (
<View className="mb-7">
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
<View className="mb-6">
<Text
className="text-[12px] font-semibold uppercase mb-2 ml-1"
style={{ color: theme.colors.textTertiary, letterSpacing: 0.8 }}
>
Security
</Text>
<GlassCard>
<View
className="rounded-2xl overflow-hidden"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<SettingsRow
label="Biometrics Login"
iconName="finger-print-outline"
@@ -258,34 +281,57 @@ export default function SettingsScreen(): React.JSX.Element {
/>
}
/>
</GlassCard>
<Text className="text-xs mt-2 ml-1 leading-4 text-text-tertiary">
</View>
<Text
className="text-[12px] mt-1.5 ml-1 leading-4"
style={{ color: theme.colors.textTertiary }}
>
Require biometrics to unlock the app
</Text>
</View>
) : null}
{/* Server */}
<View className="mb-7">
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
<View className="mb-6">
<Text
className="text-[12px] font-semibold uppercase mb-2 ml-1"
style={{ color: theme.colors.textTertiary, letterSpacing: 0.8 }}
>
Server
</Text>
<GlassCard>
<View
className="rounded-2xl overflow-hidden"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<SettingsRow
label="Server URL"
iconName="globe-outline"
value={serverUrl || "oneuptime.com"}
isLast
/>
</GlassCard>
</View>
</View>
{/* Account */}
<View className="mb-7">
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
<View className="mb-6">
<Text
className="text-[12px] font-semibold uppercase mb-2 ml-1"
style={{ color: theme.colors.textTertiary, letterSpacing: 0.8 }}
>
Account
</Text>
<GlassCard>
<View
className="rounded-2xl overflow-hidden"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<SettingsRow
label="Log Out"
iconName="log-out-outline"
@@ -293,40 +339,42 @@ export default function SettingsScreen(): React.JSX.Element {
destructive
isLast
/>
</GlassCard>
</View>
</View>
{/* About */}
<View className="mb-7">
<Text className="text-[13px] font-semibold uppercase tracking-widest mb-2.5 ml-1 text-text-secondary">
<View className="mb-6">
<Text
className="text-[12px] font-semibold uppercase mb-2 ml-1"
style={{ color: theme.colors.textTertiary, letterSpacing: 0.8 }}
>
About
</Text>
<GlassCard>
<View
className="rounded-2xl overflow-hidden"
style={{
backgroundColor: theme.colors.backgroundElevated,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
}}
>
<SettingsRow
label="Version"
iconName="information-circle-outline"
value={APP_VERSION}
isLast
/>
</GlassCard>
</View>
</View>
{/* Footer branding */}
{/* Footer */}
<View className="items-center pt-4 pb-2">
<View
className="w-10 h-10 rounded-xl items-center justify-center mb-2"
style={{
backgroundColor: theme.colors.iconBackground,
}}
<Text
className="text-[11px] font-medium"
style={{ color: theme.colors.textTertiary }}
>
<Logo size={28} />
</View>
<Text className="text-xs font-semibold text-text-tertiary">
OneUptime
</Text>
<Text className="text-[10px] text-text-tertiary mt-0.5">
On-Call Management
</Text>
</View>
</ScrollView>
);

View File

@@ -16,8 +16,6 @@ import { NativeStackNavigationProp } from "@react-navigation/native-stack";
import { useNavigation } from "@react-navigation/native";
import { AuthStackParamList } from "../../navigation/types";
import Logo from "../../components/Logo";
import GradientHeader from "../../components/GradientHeader";
import GlassCard from "../../components/GlassCard";
import GradientButton from "../../components/GradientButton";
type LoginNavigationProp = NativeStackNavigationProp<
@@ -76,11 +74,10 @@ export default function LoginScreen(): React.JSX.Element {
return (
<KeyboardAvoidingView
className="flex-1 bg-bg-primary"
className="flex-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<GradientHeader />
<ScrollView
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
@@ -88,53 +85,58 @@ export default function LoginScreen(): React.JSX.Element {
<View className="flex-1 justify-center px-7">
<View className="items-center mb-12">
<View
className="w-20 h-20 rounded-[22px] items-center justify-center mb-6"
className="w-16 h-16 rounded-2xl items-center justify-center mb-5"
style={{
backgroundColor: theme.colors.backgroundTertiary,
shadowColor: "#000000",
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 4 },
shadowRadius: 16,
elevation: 6,
backgroundColor: theme.colors.iconBackground,
}}
>
<Logo size={48} />
<Logo size={36} />
</View>
<Text
className="text-text-primary font-extrabold text-[34px]"
style={{ letterSpacing: -1.2 }}
className="text-[30px] font-bold"
style={{
color: theme.colors.textPrimary,
letterSpacing: -1,
}}
>
OneUptime
</Text>
<Text className="text-body-md text-text-secondary mt-1">
On-Call Management
<Text
className="text-[15px] mt-1"
style={{ color: theme.colors.textSecondary }}
>
Sign in to continue
</Text>
{serverUrl ? (
<View
className="mt-2 px-4 py-1.5 rounded-full"
className="mt-3 px-3 py-1 rounded-lg"
style={{
backgroundColor: theme.colors.backgroundGlass,
borderWidth: 1,
borderColor: theme.colors.borderGlass,
backgroundColor: theme.colors.backgroundTertiary,
}}
>
<Text className="text-body-sm text-text-tertiary">
<Text
className="text-[12px]"
style={{ color: theme.colors.textTertiary }}
>
{serverUrl}
</Text>
</View>
) : null}
</View>
<GlassCard style={{ padding: 20 }}>
<Text className="text-body-sm text-text-secondary mb-2 font-semibold">
<View>
<Text
className="text-[13px] font-semibold mb-2"
style={{ color: theme.colors.textSecondary }}
>
Email
</Text>
<View
className="flex-row items-center h-[52px] rounded-xl px-3.5"
className="flex-row items-center h-[48px] rounded-xl px-3.5"
style={{
backgroundColor: theme.colors.backgroundPrimary,
backgroundColor: theme.colors.backgroundSecondary,
borderWidth: 1.5,
borderColor: emailFocused
? theme.colors.actionPrimary
@@ -143,7 +145,7 @@ export default function LoginScreen(): React.JSX.Element {
>
<Ionicons
name="mail-outline"
size={20}
size={18}
color={
emailFocused
? theme.colors.actionPrimary
@@ -152,7 +154,8 @@ export default function LoginScreen(): React.JSX.Element {
style={{ marginRight: 10 }}
/>
<TextInput
className="flex-1 text-base text-text-primary"
className="flex-1 text-[15px]"
style={{ color: theme.colors.textPrimary }}
value={email}
onChangeText={(text: string) => {
setEmail(text);
@@ -174,13 +177,16 @@ export default function LoginScreen(): React.JSX.Element {
/>
</View>
<Text className="text-body-sm text-text-secondary mb-2 mt-4 font-semibold">
<Text
className="text-[13px] font-semibold mb-2 mt-4"
style={{ color: theme.colors.textSecondary }}
>
Password
</Text>
<View
className="flex-row items-center h-[52px] rounded-xl px-3.5"
className="flex-row items-center h-[48px] rounded-xl px-3.5"
style={{
backgroundColor: theme.colors.backgroundPrimary,
backgroundColor: theme.colors.backgroundSecondary,
borderWidth: 1.5,
borderColor: passwordFocused
? theme.colors.actionPrimary
@@ -189,7 +195,7 @@ export default function LoginScreen(): React.JSX.Element {
>
<Ionicons
name="lock-closed-outline"
size={20}
size={18}
color={
passwordFocused
? theme.colors.actionPrimary
@@ -198,7 +204,8 @@ export default function LoginScreen(): React.JSX.Element {
style={{ marginRight: 10 }}
/>
<TextInput
className="flex-1 text-base text-text-primary"
className="flex-1 text-[15px]"
style={{ color: theme.colors.textPrimary }}
value={password}
onChangeText={(text: string) => {
setPassword(text);
@@ -220,7 +227,7 @@ export default function LoginScreen(): React.JSX.Element {
</View>
{error ? (
<View className="flex-row items-start mt-2.5">
<View className="flex-row items-start mt-3">
<Ionicons
name="alert-circle"
size={14}
@@ -228,7 +235,7 @@ export default function LoginScreen(): React.JSX.Element {
style={{ marginRight: 6, marginTop: 2 }}
/>
<Text
className="text-body-sm flex-1"
className="text-[13px] flex-1"
style={{ color: theme.colors.statusError }}
>
{error}
@@ -236,17 +243,17 @@ export default function LoginScreen(): React.JSX.Element {
</View>
) : null}
<View className="mt-5">
<View className="mt-6">
<GradientButton
label="Log In"
label="Sign In"
onPress={handleLogin}
loading={isLoading}
disabled={isLoading}
/>
</View>
</GlassCard>
</View>
<View className="mt-5">
<View className="mt-4">
<GradientButton
label="Change Server"
onPress={handleChangeServer}

View File

@@ -16,8 +16,6 @@ import { useAuth } from "../../hooks/useAuth";
import { setServerUrl } from "../../storage/serverUrl";
import { validateServerUrl } from "../../api/auth";
import Logo from "../../components/Logo";
import GradientHeader from "../../components/GradientHeader";
import GlassCard from "../../components/GlassCard";
import GradientButton from "../../components/GradientButton";
type ServerUrlNavigationProp = NativeStackNavigationProp<
@@ -67,11 +65,10 @@ export default function ServerUrlScreen(): React.JSX.Element {
return (
<KeyboardAvoidingView
className="flex-1 bg-bg-primary"
className="flex-1"
style={{ backgroundColor: theme.colors.backgroundPrimary }}
behavior={Platform.OS === "ios" ? "padding" : "height"}
>
<GradientHeader />
<ScrollView
contentContainerStyle={{ flexGrow: 1 }}
keyboardShouldPersistTaps="handled"
@@ -79,38 +76,42 @@ export default function ServerUrlScreen(): React.JSX.Element {
<View className="flex-1 justify-center px-7">
<View className="items-center mb-14">
<View
className="w-20 h-20 rounded-[22px] items-center justify-center mb-6"
className="w-16 h-16 rounded-2xl items-center justify-center mb-5"
style={{
backgroundColor: theme.colors.backgroundTertiary,
shadowColor: "#000000",
shadowOpacity: 0.3,
shadowOffset: { width: 0, height: 4 },
shadowRadius: 16,
elevation: 6,
backgroundColor: theme.colors.iconBackground,
}}
>
<Logo size={48} />
<Logo size={36} />
</View>
<Text
className="text-text-primary font-extrabold text-[34px]"
style={{ letterSpacing: -1.2 }}
className="text-[30px] font-bold"
style={{
color: theme.colors.textPrimary,
letterSpacing: -1,
}}
>
OneUptime
</Text>
<Text className="text-body-md text-text-secondary mt-2 text-center leading-6">
<Text
className="text-[15px] mt-2 text-center leading-[22px]"
style={{ color: theme.colors.textSecondary }}
>
Connect to your OneUptime instance
</Text>
</View>
<GlassCard style={{ padding: 20 }}>
<Text className="text-body-sm text-text-secondary mb-2 font-semibold">
<View>
<Text
className="text-[13px] font-semibold mb-2"
style={{ color: theme.colors.textSecondary }}
>
Server URL
</Text>
<View
className="flex-row items-center h-[52px] rounded-xl px-3.5"
className="flex-row items-center h-[48px] rounded-xl px-3.5"
style={{
backgroundColor: theme.colors.backgroundPrimary,
backgroundColor: theme.colors.backgroundSecondary,
borderWidth: 1.5,
borderColor: error
? theme.colors.statusError
@@ -121,7 +122,7 @@ export default function ServerUrlScreen(): React.JSX.Element {
>
<Ionicons
name="globe-outline"
size={20}
size={18}
color={
urlFocused
? theme.colors.actionPrimary
@@ -130,7 +131,8 @@ export default function ServerUrlScreen(): React.JSX.Element {
style={{ marginRight: 10 }}
/>
<TextInput
className="flex-1 text-base text-text-primary"
className="flex-1 text-[15px]"
style={{ color: theme.colors.textPrimary }}
value={url}
onChangeText={(text: string) => {
setUrl(text);
@@ -153,7 +155,7 @@ export default function ServerUrlScreen(): React.JSX.Element {
</View>
{error ? (
<View className="flex-row items-center mt-2.5">
<View className="flex-row items-center mt-3">
<Ionicons
name="alert-circle"
size={14}
@@ -161,7 +163,7 @@ export default function ServerUrlScreen(): React.JSX.Element {
style={{ marginRight: 6 }}
/>
<Text
className="text-body-sm flex-1"
className="text-[13px] flex-1"
style={{ color: theme.colors.statusError }}
>
{error}
@@ -169,7 +171,7 @@ export default function ServerUrlScreen(): React.JSX.Element {
</View>
) : null}
<View className="mt-5">
<View className="mt-6">
<GradientButton
label="Connect"
onPress={handleConnect}
@@ -177,20 +179,14 @@ export default function ServerUrlScreen(): React.JSX.Element {
disabled={isLoading}
/>
</View>
</GlassCard>
</View>
<Text className="text-caption text-text-tertiary text-center mt-7 leading-5">
<Text
className="text-[12px] text-center mt-6 leading-5"
style={{ color: theme.colors.textTertiary }}
>
Self-hosting? Enter your OneUptime server URL above.
</Text>
<View className="items-center mt-10">
<View className="flex-row items-center">
<Logo size={16} style={{ marginRight: 6 }} />
<Text className="text-[11px] text-text-tertiary">
Powered by OneUptime
</Text>
</View>
</View>
</View>
</ScrollView>
</KeyboardAvoidingView>

View File

@@ -71,145 +71,145 @@ export interface ColorTokens {
}
export const darkColors: ColorTokens = {
// Background
backgroundPrimary: "#000000",
backgroundSecondary: "#0A0A0A",
backgroundTertiary: "#161616",
backgroundElevated: "#0F0F0F",
// Background — rich near-black, not pure black
backgroundPrimary: "#09090B",
backgroundSecondary: "#0F0F12",
backgroundTertiary: "#18181F",
backgroundElevated: "#141418",
// Accent
cardAccent: "rgba(255, 255, 255, 0.07)",
backgroundGlass: "rgba(255, 255, 255, 0.05)",
iconBackground: "rgba(255, 255, 255, 0.07)",
cardAccent: "rgba(255, 255, 255, 0.04)",
backgroundGlass: "rgba(255, 255, 255, 0.03)",
iconBackground: "rgba(99, 102, 241, 0.12)",
// Gradient
accentGradientStart: "#FFFFFF",
accentGradientMid: "#E0E0E0",
accentGradientEnd: "#C8C8C8",
// Gradient — indigo-based brand accent
accentGradientStart: "#818CF8",
accentGradientMid: "#6366F1",
accentGradientEnd: "#4F46E5",
accentCyan: "#06B6D4",
accentCyanBg: "rgba(6, 182, 212, 0.12)",
surfaceGlow: "rgba(255, 255, 255, 0.04)",
headerGradient: "rgba(255, 255, 255, 0.03)",
gradientStart: "rgba(255, 255, 255, 0.05)",
accentCyanBg: "rgba(6, 182, 212, 0.10)",
surfaceGlow: "rgba(99, 102, 241, 0.06)",
headerGradient: "rgba(99, 102, 241, 0.04)",
gradientStart: "rgba(99, 102, 241, 0.08)",
gradientEnd: "transparent",
// Border
borderDefault: "#1C1C1E",
borderSubtle: "#141414",
borderGlass: "rgba(255, 255, 255, 0.08)",
borderDefault: "rgba(255, 255, 255, 0.06)",
borderSubtle: "rgba(255, 255, 255, 0.04)",
borderGlass: "rgba(255, 255, 255, 0.06)",
// Text
textPrimary: "#F0F0F0",
textSecondary: "#8E8E93",
textTertiary: "#636366",
textInverse: "#000000",
// Severity
severityCritical: "#F85149",
severityCriticalBg: "#F8514926",
severityMajor: "#F0883E",
severityMajorBg: "#F0883E26",
severityMinor: "#D29922",
severityMinorBg: "#D2992226",
severityWarning: "#E3B341",
severityWarningBg: "#E3B34126",
severityInfo: "#58A6FF",
severityInfoBg: "#58A6FF26",
// State
stateCreated: "#F85149",
stateAcknowledged: "#D29922",
stateResolved: "#3FB950",
stateInvestigating: "#F0883E",
stateMuted: "#636366",
// On-Call
oncallActive: "#3FB950",
oncallActiveBg: "#3FB95026",
oncallInactive: "#636366",
oncallInactiveBg: "#63636626",
// Action
actionPrimary: "#FFFFFF",
actionPrimaryPressed: "#D4D4D4",
actionDestructive: "#F85149",
actionDestructivePressed: "#DA3633",
// Status
statusSuccess: "#3FB950",
statusSuccessBg: "#3FB95026",
statusError: "#F85149",
statusErrorBg: "#F8514926",
};
export const lightColors: ColorTokens = {
// Background
backgroundPrimary: "#FFFFFF",
backgroundSecondary: "#F8F8FA",
backgroundTertiary: "#F0F0F2",
backgroundElevated: "#FFFFFF",
// Accent
cardAccent: "rgba(0, 0, 0, 0.04)",
backgroundGlass: "rgba(255, 255, 255, 0.85)",
iconBackground: "rgba(0, 0, 0, 0.05)",
// Gradient
accentGradientStart: "#1A1A1A",
accentGradientMid: "#2D2D2D",
accentGradientEnd: "#3A3A3A",
accentCyan: "#0891B2",
accentCyanBg: "rgba(8, 145, 178, 0.08)",
surfaceGlow: "rgba(0, 0, 0, 0.03)",
headerGradient: "rgba(0, 0, 0, 0.02)",
gradientStart: "rgba(0, 0, 0, 0.03)",
gradientEnd: "transparent",
// Border
borderDefault: "#E5E5EA",
borderSubtle: "#F0F0F2",
borderGlass: "rgba(0, 0, 0, 0.06)",
// Text
textPrimary: "#111111",
textSecondary: "#6B6B6B",
textTertiary: "#9A9A9A",
textPrimary: "#FAFAFA",
textSecondary: "#A1A1AA",
textTertiary: "#52525B",
textInverse: "#FFFFFF",
// Severity
severityCritical: "#CF222E",
severityCriticalBg: "#CF222E1A",
severityMajor: "#BC4C00",
severityMajorBg: "#BC4C001A",
severityMinor: "#9A6700",
severityMinorBg: "#9A67001A",
severityWarning: "#BF8700",
severityWarningBg: "#BF87001A",
severityInfo: "#0969DA",
severityInfoBg: "#0969DA1A",
severityCritical: "#EF4444",
severityCriticalBg: "rgba(239, 68, 68, 0.12)",
severityMajor: "#F97316",
severityMajorBg: "rgba(249, 115, 22, 0.12)",
severityMinor: "#EAB308",
severityMinorBg: "rgba(234, 179, 8, 0.12)",
severityWarning: "#F59E0B",
severityWarningBg: "rgba(245, 158, 11, 0.12)",
severityInfo: "#3B82F6",
severityInfoBg: "rgba(59, 130, 246, 0.12)",
// State
stateCreated: "#CF222E",
stateAcknowledged: "#9A6700",
stateResolved: "#1A7F37",
stateInvestigating: "#BC4C00",
stateMuted: "#8C959F",
stateCreated: "#EF4444",
stateAcknowledged: "#F59E0B",
stateResolved: "#22C55E",
stateInvestigating: "#F97316",
stateMuted: "#52525B",
// On-Call
oncallActive: "#1A7F37",
oncallActiveBg: "#1A7F371A",
oncallInactive: "#8C959F",
oncallInactiveBg: "#8C959F1A",
oncallActive: "#22C55E",
oncallActiveBg: "rgba(34, 197, 94, 0.12)",
oncallInactive: "#52525B",
oncallInactiveBg: "rgba(82, 82, 91, 0.12)",
// Action
actionPrimary: "#1A1A1A",
actionPrimaryPressed: "#333333",
actionDestructive: "#CF222E",
actionDestructivePressed: "#A40E26",
// Action — indigo accent is the signature change
actionPrimary: "#6366F1",
actionPrimaryPressed: "#4F46E5",
actionDestructive: "#EF4444",
actionDestructivePressed: "#DC2626",
// Status
statusSuccess: "#1A7F37",
statusSuccessBg: "#1A7F371A",
statusError: "#CF222E",
statusErrorBg: "#CF222E1A",
statusSuccess: "#22C55E",
statusSuccessBg: "rgba(34, 197, 94, 0.12)",
statusError: "#EF4444",
statusErrorBg: "rgba(239, 68, 68, 0.12)",
};
export const lightColors: ColorTokens = {
// Background — clean white with warm gray tones
backgroundPrimary: "#FFFFFF",
backgroundSecondary: "#F9FAFB",
backgroundTertiary: "#F3F4F6",
backgroundElevated: "#FFFFFF",
// Accent
cardAccent: "rgba(0, 0, 0, 0.02)",
backgroundGlass: "rgba(255, 255, 255, 0.80)",
iconBackground: "rgba(99, 102, 241, 0.08)",
// Gradient — indigo-based brand accent
accentGradientStart: "#6366F1",
accentGradientMid: "#4F46E5",
accentGradientEnd: "#4338CA",
accentCyan: "#0891B2",
accentCyanBg: "rgba(8, 145, 178, 0.06)",
surfaceGlow: "rgba(99, 102, 241, 0.04)",
headerGradient: "rgba(99, 102, 241, 0.03)",
gradientStart: "rgba(99, 102, 241, 0.04)",
gradientEnd: "transparent",
// Border
borderDefault: "#E5E7EB",
borderSubtle: "#F3F4F6",
borderGlass: "rgba(0, 0, 0, 0.05)",
// Text
textPrimary: "#111827",
textSecondary: "#6B7280",
textTertiary: "#9CA3AF",
textInverse: "#FFFFFF",
// Severity
severityCritical: "#DC2626",
severityCriticalBg: "rgba(220, 38, 38, 0.08)",
severityMajor: "#EA580C",
severityMajorBg: "rgba(234, 88, 12, 0.08)",
severityMinor: "#CA8A04",
severityMinorBg: "rgba(202, 138, 4, 0.08)",
severityWarning: "#D97706",
severityWarningBg: "rgba(217, 119, 6, 0.08)",
severityInfo: "#2563EB",
severityInfoBg: "rgba(37, 99, 235, 0.08)",
// State
stateCreated: "#DC2626",
stateAcknowledged: "#D97706",
stateResolved: "#16A34A",
stateInvestigating: "#EA580C",
stateMuted: "#9CA3AF",
// On-Call
oncallActive: "#16A34A",
oncallActiveBg: "rgba(22, 163, 74, 0.08)",
oncallInactive: "#9CA3AF",
oncallInactiveBg: "rgba(156, 163, 175, 0.08)",
// Action — indigo accent
actionPrimary: "#4F46E5",
actionPrimaryPressed: "#4338CA",
actionDestructive: "#DC2626",
actionDestructivePressed: "#B91C1C",
// Status
statusSuccess: "#16A34A",
statusSuccessBg: "rgba(22, 163, 74, 0.08)",
statusError: "#DC2626",
statusErrorBg: "rgba(220, 38, 38, 0.08)",
};