Skip to content

Commit f17660a

Browse files
committed
refactor(mobile/theme): danger + gold tokens in Tailwind config
Promotes gold to a nested token (DEFAULT + tint) and adds a danger token (DEFAULT + tint) for red error states. Text gets a danger tone, Toast's error tone points at bg-danger, and the toast's white copy uses the existing tone="white" so CVA merging doesn't silently ink it out. Sets up the semantic token surface that a future dark-mode pass can re-theme in one config swap.
1 parent d2356f2 commit f17660a

3 files changed

Lines changed: 102 additions & 2 deletions

File tree

apps/mobile/components/ui/text.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export const textVariants = cva("text-ink", {
2222
gold: "text-gold",
2323
white: "text-white",
2424
"white-muted": "text-white/60",
25+
danger: "text-danger",
2526
},
2627
},
2728
defaultVariants: {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { useCallback, useEffect, useState } from "react";
2+
import { Pressable } from "react-native";
3+
import Animated, { FadeInDown, FadeOutUp } from "react-native-reanimated";
4+
import { SafeAreaView } from "react-native-safe-area-context";
5+
import { Text } from "./text";
6+
7+
export type ToastTone = "error" | "warning" | "success" | "info";
8+
9+
export type ToastConfig = {
10+
title: string;
11+
body?: string;
12+
tone?: ToastTone;
13+
/** ms before the toast auto-dismisses. Default 4000. */
14+
duration?: number;
15+
};
16+
17+
// Warning uses the brand gold token — reads as "heads up, needs attention"
18+
// without the aggressive feel of the error red.
19+
const TONE_BG: Record<ToastTone, string> = {
20+
error: "bg-danger",
21+
warning: "bg-gold",
22+
success: "bg-green",
23+
info: "bg-ink",
24+
};
25+
26+
type CardProps = {
27+
config: ToastConfig;
28+
onDismiss: () => void;
29+
};
30+
31+
function ToastCard({ config, onDismiss }: CardProps) {
32+
useEffect(() => {
33+
const timer = setTimeout(onDismiss, config.duration ?? 4000);
34+
return () => clearTimeout(timer);
35+
}, [config, onDismiss]);
36+
37+
const bgClass = TONE_BG[config.tone ?? "info"];
38+
39+
return (
40+
<SafeAreaView
41+
edges={["top"]}
42+
pointerEvents="box-none"
43+
className="absolute inset-x-0 top-0 z-50"
44+
>
45+
<Animated.View entering={FadeInDown} exiting={FadeOutUp}>
46+
<Pressable
47+
onPress={onDismiss}
48+
className={`mx-s-4 mt-s-2 rounded-md px-s-4 py-s-3 ${bgClass}`}
49+
style={{
50+
shadowColor: "#1a2a22",
51+
shadowOffset: { width: 0, height: 6 },
52+
shadowOpacity: 0.2,
53+
shadowRadius: 12,
54+
elevation: 6,
55+
}}
56+
accessibilityLiveRegion="polite"
57+
>
58+
<Text variant="label" tone="white">
59+
{config.title}
60+
</Text>
61+
{config.body ? (
62+
<Text variant="caption" tone="white-muted" className="mt-s-1">
63+
{config.body}
64+
</Text>
65+
) : null}
66+
</Pressable>
67+
</Animated.View>
68+
</SafeAreaView>
69+
);
70+
}
71+
72+
/**
73+
* Imperative-feeling toast API on top of <ToastCard>:
74+
* const toast = useAppToast();
75+
* toast.show({ title: "Missing info", body: "…", tone: "error" });
76+
* {toast.element}
77+
*/
78+
export function useAppToast() {
79+
const [config, setConfig] = useState<ToastConfig | null>(null);
80+
81+
const show = useCallback((c: ToastConfig) => setConfig(c), []);
82+
const hide = useCallback(() => setConfig(null), []);
83+
84+
const element = config ? (
85+
<ToastCard config={config} onDismiss={hide} />
86+
) : null;
87+
88+
return { show, hide, element };
89+
}

apps/mobile/tailwind.config.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,10 @@ module.exports = {
1010
theme: {
1111
extend: {
1212
colors: {
13-
// Qibla design tokens — keep in sync with packages/ui/src/styles/globals.css
13+
// Qibla design tokens — keep in sync with packages/ui/src/styles/globals.css.
14+
// Semantic tokens live here so a future dark-mode pass only has to swap
15+
// these values (or wire them to CSS variables) — components should use
16+
// the semantic classes, not raw hex.
1417
cream: "#faf6ec",
1518
surface: "#fffbf1",
1619
ink: "#1a2a22",
@@ -21,7 +24,14 @@ module.exports = {
2124
dark: "#1f4330",
2225
tint: "#eef3e6",
2326
},
24-
gold: "#b68a3c",
27+
gold: {
28+
DEFAULT: "#b68a3c",
29+
tint: "#f4ead0",
30+
},
31+
danger: {
32+
DEFAULT: "#b04a3a",
33+
tint: "#f8e4df",
34+
},
2535
dark: "#0d1612",
2636
},
2737
fontFamily: {

0 commit comments

Comments
 (0)