Skip to content

Commit 450c422

Browse files
authored
Merge pull request #6 from TibetOS/claude/theme-engine
refactor: centralize device-skin colors into a theme engine
2 parents 27a1f71 + 2ea9184 commit 450c422

6 files changed

Lines changed: 192 additions & 67 deletions

File tree

src/components/ChatBubble.tsx

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,22 @@
11
import type { Message } from "../types"
2+
import type { ChatTheme } from "../theme"
23
import { CheckMark } from "./CheckMark"
34

45
type ChatBubbleProps = {
56
message: Message
6-
darkMode: boolean
7+
theme: ChatTheme
78
showTail: boolean
89
}
910

10-
export function ChatBubble({ message, darkMode, showTail }: ChatBubbleProps) {
11+
export function ChatBubble({ message, theme, showTail }: ChatBubbleProps) {
1112
const isSent = message.sender === "me"
1213

1314
const bgColor = isSent
14-
? darkMode
15-
? "#005c4b"
16-
: "#d9fdd3"
17-
: darkMode
18-
? "#202c33"
19-
: "#ffffff"
15+
? theme.bubble.sentBackground
16+
: theme.bubble.receivedBackground
2017

21-
const textColor = darkMode ? "#e9edef" : "#111b21"
22-
const timestampColor = darkMode ? "#8696a0" : "#667781"
18+
const textColor = theme.bubble.text
19+
const timestampColor = theme.bubble.timestamp
2320

2421
return (
2522
<div
@@ -74,7 +71,7 @@ export function ChatBubble({ message, darkMode, showTail }: ChatBubbleProps) {
7471
{message.timestamp}
7572
</span>
7673
{isSent && (
77-
<CheckMark status={message.status} darkMode={darkMode} />
74+
<CheckMark status={message.status} theme={theme} />
7875
)}
7976
</div>
8077
</div>

src/components/ChatHeader.tsx

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import type { PhoneType } from "../types"
2+
import type { ChatTheme } from "../theme"
23

34
type ChatHeaderProps = {
45
contactName: string
56
contactStatus: string
6-
darkMode: boolean
7+
theme: ChatTheme
78
phoneType: PhoneType
89
}
910

@@ -39,41 +40,50 @@ function Avatar({ size = 36 }: { size?: number }) {
3940
function AndroidHeader({
4041
contactName,
4142
contactStatus,
42-
darkMode,
43+
theme,
4344
}: Omit<ChatHeaderProps, "phoneType">) {
44-
const bgColor = darkMode ? "#202c33" : "#008069"
45+
const bgColor = theme.header.background
46+
const textColor = theme.header.text
47+
const subtitleColor = theme.header.subtitle
48+
const iconColor = theme.header.accent
4549

4650
return (
4751
<div
4852
className="flex items-center gap-2 px-2 py-1.5"
4953
style={{ backgroundColor: bgColor, minHeight: "52px" }}
5054
>
5155
{/* Back arrow */}
52-
<svg viewBox="0 0 24 24" width="24" height="24" fill="white">
56+
<svg viewBox="0 0 24 24" width="24" height="24" fill={iconColor}>
5357
<path d="M12 4l1.4 1.4L7.8 11H20v2H7.8l5.6 5.6L12 20l-8-8z" />
5458
</svg>
5559

5660
<Avatar size={38} />
5761

5862
<div className="flex-1 min-w-0">
59-
<p className="text-white text-[16px] font-normal leading-tight truncate">
63+
<p
64+
className="text-[16px] font-normal leading-tight truncate"
65+
style={{ color: textColor }}
66+
>
6067
{contactName}
6168
</p>
6269
{contactStatus && (
63-
<p className="text-[12px] leading-tight truncate text-white/70">
70+
<p
71+
className="text-[12px] leading-tight truncate"
72+
style={{ color: subtitleColor }}
73+
>
6474
{contactStatus}
6575
</p>
6676
)}
6777
</div>
6878

6979
<div className="flex items-center gap-4 mr-1">
70-
<svg viewBox="0 0 24 24" width="22" height="22" fill="white">
80+
<svg viewBox="0 0 24 24" width="22" height="22" fill={iconColor}>
7181
<path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z" />
7282
</svg>
73-
<svg viewBox="0 0 24 24" width="22" height="22" fill="white">
83+
<svg viewBox="0 0 24 24" width="22" height="22" fill={iconColor}>
7484
<path d="M6.62 10.79c1.44 2.83 3.76 5.14 6.59 6.59l2.2-2.2c.27-.27.67-.36 1.02-.24 1.12.37 2.33.57 3.57.57.55 0 1 .45 1 1V20c0 .55-.45 1-1 1-9.39 0-17-7.61-17-17 0-.55.45-1 1-1h3.5c.55 0 1 .45 1 1 0 1.25.2 2.45.57 3.57.11.35.03.74-.25 1.02l-2.2 2.2z" />
7585
</svg>
76-
<svg viewBox="0 0 24 24" width="22" height="22" fill="white">
86+
<svg viewBox="0 0 24 24" width="22" height="22" fill={iconColor}>
7787
<path d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
7888
</svg>
7989
</div>
@@ -84,13 +94,13 @@ function AndroidHeader({
8494
function IPhoneHeader({
8595
contactName,
8696
contactStatus,
87-
darkMode,
97+
theme,
8898
}: Omit<ChatHeaderProps, "phoneType">) {
89-
const bgColor = darkMode ? "#1a1a1e" : "#f6f6f6"
90-
const textColor = darkMode ? "#ffffff" : "#000000"
91-
const accentColor = darkMode ? "#0a84ff" : "#007aff"
92-
const subtitleColor = darkMode ? "#8e8e93" : "#8e8e93"
93-
const borderColor = darkMode ? "#38383a" : "#d1d1d6"
99+
const bgColor = theme.header.background
100+
const textColor = theme.header.text
101+
const accentColor = theme.header.accent
102+
const subtitleColor = theme.header.subtitle
103+
const borderColor = theme.header.border
94104

95105
return (
96106
<div
@@ -151,10 +161,10 @@ function IPhoneHeader({
151161
export function ChatHeader({
152162
contactName,
153163
contactStatus,
154-
darkMode,
164+
theme,
155165
phoneType,
156166
}: ChatHeaderProps) {
157-
const props = { contactName, contactStatus, darkMode }
167+
const props = { contactName, contactStatus, theme }
158168
if (phoneType === "iphone") {
159169
return <IPhoneHeader {...props} />
160170
}

src/components/ChatPreview.tsx

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { forwardRef, useEffect, useRef } from "react"
22
import type { ReactNode } from "react"
33
import type { Message, ChatConfig } from "../types"
4+
import { getChatTheme } from "../theme"
5+
import type { ChatTheme } from "../theme"
46
import { StatusBar } from "./StatusBar"
57
import { ChatHeader } from "./ChatHeader"
68
import { ChatBubble } from "./ChatBubble"
@@ -119,11 +121,11 @@ function InputBar({
119121
// Centered "pill" used for both date separators and system notices.
120122
function CenterPill({
121123
children,
122-
darkMode,
124+
theme,
123125
wide = false,
124126
}: {
125127
children: ReactNode
126-
darkMode: boolean
128+
theme: ChatTheme
127129
wide?: boolean
128130
}) {
129131
return (
@@ -133,8 +135,8 @@ function CenterPill({
133135
wide ? "max-w-[85%]" : ""
134136
}`}
135137
style={{
136-
backgroundColor: darkMode ? "#182229" : "#e1f2fb",
137-
color: darkMode ? "#8696a0" : "#54656f",
138+
backgroundColor: theme.pill.background,
139+
color: theme.pill.text,
138140
}}
139141
>
140142
{children}
@@ -177,7 +179,8 @@ export const ChatPreview = forwardRef<HTMLDivElement, ChatPreviewProps>(
177179
function ChatPreview({ messages, config }, ref) {
178180
const messagesEndRef = useRef<HTMLDivElement>(null)
179181
const isIPhone = config.phoneType === "iphone"
180-
const chatBgColor = config.darkMode ? "#0b141a" : "#efeae2"
182+
const theme = getChatTheme(config.phoneType, config.darkMode)
183+
const chatBgColor = theme.chatBackground
181184

182185
useEffect(() => {
183186
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" })
@@ -198,15 +201,15 @@ export const ChatPreview = forwardRef<HTMLDivElement, ChatPreviewProps>(
198201
}}
199202
>
200203
<StatusBar
201-
darkMode={config.darkMode}
204+
theme={theme}
202205
phoneType={config.phoneType}
203206
time={config.statusBarTime}
204207
batteryLevel={config.batteryLevel}
205208
/>
206209
<ChatHeader
207210
contactName={config.contactName}
208211
contactStatus={config.contactStatus}
209-
darkMode={config.darkMode}
212+
theme={theme}
210213
phoneType={config.phoneType}
211214
/>
212215

@@ -224,14 +227,14 @@ export const ChatPreview = forwardRef<HTMLDivElement, ChatPreviewProps>(
224227
{buildRenderItems(messages).map((item) => {
225228
if (item.kind === "divider") {
226229
return (
227-
<CenterPill key={item.key} darkMode={config.darkMode}>
230+
<CenterPill key={item.key} theme={theme}>
228231
{item.label}
229232
</CenterPill>
230233
)
231234
}
232235
if (item.kind === "system") {
233236
return (
234-
<CenterPill key={item.key} darkMode={config.darkMode} wide>
237+
<CenterPill key={item.key} theme={theme} wide>
235238
{item.text}
236239
</CenterPill>
237240
)
@@ -240,7 +243,7 @@ export const ChatPreview = forwardRef<HTMLDivElement, ChatPreviewProps>(
240243
<ChatBubble
241244
key={item.key}
242245
message={item.message}
243-
darkMode={config.darkMode}
246+
theme={theme}
244247
showTail={item.showTail}
245248
/>
246249
)

src/components/CheckMark.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import type { MessageStatus } from "../types"
2+
import type { ChatTheme } from "../theme"
23

34
type CheckMarkProps = {
45
status: MessageStatus
5-
darkMode: boolean
6+
theme: ChatTheme
67
}
78

8-
export function CheckMark({ status, darkMode }: CheckMarkProps) {
9-
const grayColor = darkMode ? "#8696a0" : "#667781"
10-
const blueColor = "#53bdeb"
9+
export function CheckMark({ status, theme }: CheckMarkProps) {
10+
const grayColor = theme.check.gray
11+
const blueColor = theme.check.blue
1112
const color = status === "read" ? blueColor : grayColor
1213

1314
if (status === "sent") {

src/components/StatusBar.tsx

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import type { PhoneType } from "../types"
2+
import type { ChatTheme } from "../theme"
23

34
type StatusBarProps = {
4-
darkMode: boolean
5+
theme: ChatTheme
56
phoneType: PhoneType
67
time: string
78
batteryLevel: number
@@ -56,21 +57,21 @@ function BatteryIcon({ color, level }: { color: string; level: number }) {
5657
}
5758

5859
function IPhoneStatusBar({
59-
darkMode,
60+
theme,
6061
time,
6162
batteryLevel,
6263
}: {
63-
darkMode: boolean
64+
theme: ChatTheme
6465
time: string
6566
batteryLevel: number
6667
}) {
67-
const textColor = darkMode ? "#ffffff" : "#000000"
68+
const textColor = theme.statusBar.text
6869

6970
return (
7071
<div
7172
className="relative flex items-end justify-between px-6 text-white"
7273
style={{
73-
backgroundColor: darkMode ? "#000000" : "#f6f6f6",
74+
backgroundColor: theme.statusBar.background,
7475
height: "54px",
7576
fontSize: "15px",
7677
paddingBottom: "4px",
@@ -98,51 +99,49 @@ function IPhoneStatusBar({
9899
}
99100

100101
function AndroidStatusBar({
101-
darkMode,
102+
theme,
102103
time,
103104
batteryLevel,
104105
}: {
105-
darkMode: boolean
106+
theme: ChatTheme
106107
time: string
107108
batteryLevel: number
108109
}) {
109-
const bgColor = darkMode ? "#1a262d" : "#006b57"
110+
const iconColor = theme.statusBar.text
110111

111112
return (
112113
<div
113-
className="flex items-center justify-between px-4 text-white"
114-
style={{ backgroundColor: bgColor, height: "26px", fontSize: "12px" }}
114+
className="flex items-center justify-between px-4"
115+
style={{
116+
backgroundColor: theme.statusBar.background,
117+
height: "26px",
118+
fontSize: "12px",
119+
}}
115120
>
116-
<span className="font-medium">{time}</span>
121+
<span className="font-medium" style={{ color: iconColor }}>
122+
{time}
123+
</span>
117124
<div className="flex items-center gap-1">
118-
<SignalBars color="white" />
119-
<WiFiIcon color="white" />
120-
<BatteryIcon color="white" level={batteryLevel} />
125+
<SignalBars color={iconColor} />
126+
<WiFiIcon color={iconColor} />
127+
<BatteryIcon color={iconColor} level={batteryLevel} />
121128
</div>
122129
</div>
123130
)
124131
}
125132

126133
export function StatusBar({
127-
darkMode,
134+
theme,
128135
phoneType,
129136
time,
130137
batteryLevel,
131138
}: StatusBarProps) {
132139
if (phoneType === "iphone") {
133140
return (
134-
<IPhoneStatusBar
135-
darkMode={darkMode}
136-
time={time}
137-
batteryLevel={batteryLevel}
138-
/>
141+
<IPhoneStatusBar theme={theme} time={time} batteryLevel={batteryLevel} />
139142
)
140143
}
141144
return (
142-
<AndroidStatusBar
143-
darkMode={darkMode}
144-
time={time}
145-
batteryLevel={batteryLevel}
146-
/>
145+
<AndroidStatusBar theme={theme} time={time} batteryLevel={batteryLevel} />
147146
)
148147
}

0 commit comments

Comments
 (0)