mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
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:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
]}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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'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}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)",
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user