Skip to content

Commit 6ba053a

Browse files
committed
Add theme support for widget
1 parent 1e26e0e commit 6ba053a

5 files changed

Lines changed: 107 additions & 24 deletions

File tree

apps/web/src/components/embed/BrandingBadge.tsx

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
1-
export default function BrandingBadge() {
1+
import type { ThemeColors } from "./useEmbedTheme";
2+
3+
interface Props {
4+
theme?: ThemeColors;
5+
}
6+
7+
export default function BrandingBadge({ theme }: Props) {
8+
const bg = theme?.bg ?? "rgba(10, 10, 10, 0.78)";
9+
const border = theme?.border ?? "rgba(255, 255, 255, 0.12)";
10+
const text = theme?.text ?? "#fafafa";
11+
const textSecondary = theme?.brandingText ?? "rgba(255, 255, 255, 0.72)";
12+
213
return (
314
<a
415
href="https://livedot.dev"
@@ -14,12 +25,12 @@ export default function BrandingBadge() {
1425
gap: 8,
1526
padding: "7px 10px",
1627
borderRadius: 999,
17-
background: "rgba(10, 10, 10, 0.78)",
18-
border: "1px solid rgba(255, 255, 255, 0.12)",
28+
background: bg,
29+
border: `1px solid ${border}`,
1930
backdropFilter: "blur(12px)",
20-
color: "#fafafa",
31+
color: text,
2132
textDecoration: "none",
22-
boxShadow: "0 10px 30px rgba(0, 0, 0, 0.28)",
33+
boxShadow: "none",
2334
}}
2435
>
2536
<img
@@ -28,8 +39,8 @@ export default function BrandingBadge() {
2839
aria-hidden="true"
2940
style={{ width: 18, height: 18, borderRadius: 5, flexShrink: 0 }}
3041
/>
31-
<span style={{ fontSize: 11, lineHeight: 1, color: "rgba(255, 255, 255, 0.72)" }}>Powered by</span>
32-
<span style={{ fontSize: 12, lineHeight: 1, fontWeight: 700, letterSpacing: "-0.02em" }}>livedot</span>
42+
<span style={{ fontSize: 11, lineHeight: 1, color: textSecondary }}>Powered by</span>
43+
<span style={{ fontSize: 12, lineHeight: 1, fontWeight: 700, letterSpacing: "-0.02em" }}>{" "}livedot</span>
3344
</a>
3445
);
3546
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { useEffect, useState } from "react";
2+
3+
export type EmbedTheme = "dark" | "light" | "system";
4+
export type ResolvedTheme = "dark" | "light";
5+
6+
export interface ThemeColors {
7+
bg: string;
8+
border: string;
9+
text: string;
10+
textSecondary: string;
11+
textMuted: string;
12+
brandingBg: string;
13+
brandingBorder: string;
14+
brandingText: string;
15+
}
16+
17+
const DARK: ThemeColors = {
18+
bg: "rgba(10, 10, 10, 0.85)",
19+
border: "rgba(255, 255, 255, 0.1)",
20+
text: "#fafafa",
21+
textSecondary: "rgba(255, 255, 255, 0.8)",
22+
textMuted: "#a1a1aa",
23+
brandingBg: "rgba(255, 255, 255, 0.06)",
24+
brandingBorder: "rgba(255, 255, 255, 0.08)",
25+
brandingText: "rgba(255, 255, 255, 0.72)",
26+
};
27+
28+
const LIGHT: ThemeColors = {
29+
bg: "rgba(255, 255, 255, 0.85)",
30+
border: "rgba(0, 0, 0, 0.1)",
31+
text: "#1a1a1a",
32+
textSecondary: "rgba(0, 0, 0, 0.7)",
33+
textMuted: "#71717a",
34+
brandingBg: "rgba(0, 0, 0, 0.04)",
35+
brandingBorder: "rgba(0, 0, 0, 0.08)",
36+
brandingText: "rgba(0, 0, 0, 0.55)",
37+
};
38+
39+
function getSystemTheme(): ResolvedTheme {
40+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
41+
}
42+
43+
export function useEmbedTheme(theme: EmbedTheme): ThemeColors {
44+
const [resolved, setResolved] = useState<ResolvedTheme>(
45+
theme === "system" ? getSystemTheme() : theme,
46+
);
47+
48+
useEffect(() => {
49+
if (theme !== "system") {
50+
setResolved(theme);
51+
return;
52+
}
53+
54+
const mq = window.matchMedia("(prefers-color-scheme: dark)");
55+
setResolved(mq.matches ? "dark" : "light");
56+
57+
const handler = (e: MediaQueryListEvent) => setResolved(e.matches ? "dark" : "light");
58+
mq.addEventListener("change", handler);
59+
return () => mq.removeEventListener("change", handler);
60+
}, [theme]);
61+
62+
return resolved === "dark" ? DARK : LIGHT;
63+
}

apps/web/src/routes/embed/chart.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { createFileRoute } from "@tanstack/react-router";
22
import { useWebSocket } from "@/hooks/useWebSocket";
33
import { aggregateHistoryPoints } from "@/lib/chart";
44
import { useEmbedBranding } from "@/components/embed/useEmbedBranding";
5+
import { useEmbedTheme, type EmbedTheme } from "@/components/embed/useEmbedTheme";
56

67
export const Route = createFileRoute("/embed/chart")({
78
component: EmbedChart,
@@ -12,6 +13,7 @@ export const Route = createFileRoute("/embed/chart")({
1213
accent: (search.accent as string) ?? "#96E421",
1314
branding: String(search.branding ?? ""),
1415
scale: Number(search.scale ?? 0.9),
16+
theme: (String(search.theme ?? "system") as EmbedTheme),
1517
}),
1618
});
1719

@@ -47,11 +49,12 @@ function buildPath(points: { count: number }[], close: boolean): string {
4749
}
4850

4951
function EmbedChart() {
50-
const { website, token, bg, accent, branding, scale } = Route.useSearch();
52+
const { website, token, bg, accent, branding, scale, theme } = Route.useSearch();
5153
const { count, connected, history } = useWebSocket(website || null, { token: token || undefined, recent: "10m" });
5254
const explicitBranding = branding === "1" || branding === "true";
5355
const showBranding = useEmbedBranding(website, token, explicitBranding);
5456
const size = Number.isFinite(scale) ? Math.min(Math.max(scale, 0.6), 1.4) : 0.9;
57+
const t = useEmbedTheme(theme === "dark" || theme === "light" ? theme : "system");
5558

5659
if (!website || !token) {
5760
return null;
@@ -74,8 +77,8 @@ function EmbedChart() {
7477
style={{
7578
width: 240 * size,
7679
borderRadius: 18 * size,
77-
border: "1px solid rgba(255, 255, 255, 0.1)",
78-
background: bg === "transparent" ? "rgba(10, 10, 10, 0.85)" : bg,
80+
border: `1px solid ${t.border}`,
81+
background: bg === "transparent" ? t.bg : bg,
7982
backdropFilter: "blur(18px)",
8083
boxShadow: "none",
8184
overflow: "hidden",
@@ -84,7 +87,7 @@ function EmbedChart() {
8487
>
8588
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 8 * size }}>
8689
<div style={{ minWidth: 0 }}>
87-
<p style={{ fontSize: 12 * size, color: "rgba(250,250,250,0.8)", fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
90+
<p style={{ fontSize: 12 * size, color: t.textSecondary, fontWeight: 500, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
8891
Live visitors
8992
</p>
9093
</div>
@@ -98,13 +101,13 @@ function EmbedChart() {
98101
boxShadow: connected ? `0 0 0 ${4 * size}px color-mix(in srgb, ${accent} 18%, transparent)` : "none",
99102
}}
100103
/>
101-
<span style={{ fontSize: 12 * size, color: "rgba(161,161,170,0.75)" }}>10m</span>
104+
<span style={{ fontSize: 12 * size, color: t.textMuted }}>10m</span>
102105
</div>
103106
</div>
104107

105108
<div style={{ display: "flex", alignItems: "baseline", gap: 8 * size, marginBottom: 8 * size }}>
106-
<span style={{ fontSize: 28 * size, fontWeight: 750, color: "#fafafa", lineHeight: 1, letterSpacing: "-0.04em" }}>{count}</span>
107-
<span style={{ fontSize: 12 * size, color: "#a1a1aa" }}>visitors</span>
109+
<span style={{ fontSize: 28 * size, fontWeight: 750, color: t.text, lineHeight: 1, letterSpacing: "-0.04em" }}>{count}</span>
110+
<span style={{ fontSize: 12 * size, color: t.textMuted }}>visitors</span>
108111
</div>
109112

110113
<svg viewBox={`0 0 ${W} ${H}`} preserveAspectRatio="none" style={{ width: "100%", height: 48 * size }}>
@@ -156,7 +159,7 @@ function EmbedChart() {
156159
display: "inline-block",
157160
marginTop: 6 * size,
158161
fontSize: 10 * size,
159-
color: "rgba(255,255,255,0.48)",
162+
color: t.brandingText,
160163
textDecoration: "none",
161164
letterSpacing: "0.01em",
162165
}}

apps/web/src/routes/embed/live.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createFileRoute } from "@tanstack/react-router";
22
import { useWebSocket } from "@/hooks/useWebSocket";
33
import { useEmbedBranding } from "@/components/embed/useEmbedBranding";
4+
import { useEmbedTheme, type EmbedTheme } from "@/components/embed/useEmbedTheme";
45

56
export const Route = createFileRoute("/embed/live")({
67
component: EmbedLive,
@@ -11,15 +12,17 @@ export const Route = createFileRoute("/embed/live")({
1112
accent: (search.accent as string) ?? "#96E421",
1213
branding: String(search.branding ?? ""),
1314
scale: Number(search.scale ?? 0.85),
15+
theme: (String(search.theme ?? "system") as EmbedTheme),
1416
}),
1517
});
1618

1719
function EmbedLive() {
18-
const { website, token, bg, accent, branding, scale } = Route.useSearch();
20+
const { website, token, bg, accent, branding, scale, theme } = Route.useSearch();
1921
const { count, connected } = useWebSocket(website || null, token || undefined);
2022
const explicitBranding = branding === "1" || branding === "true";
2123
const showBranding = useEmbedBranding(website, token, explicitBranding);
2224
const size = Number.isFinite(scale) ? Math.min(Math.max(scale, 0.55), 1.4) : 0.85;
25+
const t = useEmbedTheme(theme === "dark" || theme === "light" ? theme : "system");
2326

2427
if (!website || !token) {
2528
return null;
@@ -33,8 +36,8 @@ function EmbedLive() {
3336
gap: 12 * size,
3437
padding: `${12 * size}px ${14 * size}px`,
3538
borderRadius: 18 * size,
36-
backgroundColor: bg === "transparent" ? "rgba(10, 10, 10, 0.85)" : bg,
37-
border: "1px solid rgba(255, 255, 255, 0.1)",
39+
backgroundColor: bg === "transparent" ? t.bg : bg,
40+
border: `1px solid ${t.border}`,
3841
backdropFilter: "blur(18px)",
3942
boxShadow: "none",
4043
fontFamily: "system-ui, -apple-system, sans-serif",
@@ -51,7 +54,7 @@ function EmbedLive() {
5154
flexShrink: 0,
5255
}}
5356
/>
54-
<span style={{ fontSize: 34 * size, lineHeight: 1, fontWeight: 750, color: "#fafafa", letterSpacing: "-0.04em" }}>
57+
<span style={{ fontSize: 34 * size, lineHeight: 1, fontWeight: 750, color: t.text, letterSpacing: "-0.04em" }}>
5558
{count}
5659
</span>
5760
{showBranding && (
@@ -67,8 +70,8 @@ function EmbedLive() {
6770
padding: `0 ${10 * size}px`,
6871
height: 28 * size,
6972
borderRadius: 999,
70-
background: "rgba(255, 255, 255, 0.06)",
71-
border: "1px solid rgba(255, 255, 255, 0.08)",
73+
background: t.brandingBg,
74+
border: `1px solid ${t.brandingBorder}`,
7275
flexShrink: 0,
7376
textDecoration: "none",
7477
}}
@@ -79,7 +82,7 @@ function EmbedLive() {
7982
aria-hidden="true"
8083
style={{ width: 18 * size, height: 18 * size, borderRadius: 6 * size, display: "block" }}
8184
/>
82-
<span style={{ fontSize: 11 * size, fontWeight: 600, color: "rgba(255, 255, 255, 0.72)", whiteSpace: "nowrap" }}>
85+
<span style={{ fontSize: 11 * size, fontWeight: 600, color: t.brandingText, whiteSpace: "nowrap" }}>
8386
Powered by Livedot
8487
</span>
8588
</a>

apps/web/src/routes/embed/map.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { useWebSocket } from "@/hooks/useWebSocket";
33
import Map from "@/components/dashboard/Map";
44
import BrandingBadge from "@/components/embed/BrandingBadge";
55
import { useEmbedBranding } from "@/components/embed/useEmbedBranding";
6+
import { useEmbedTheme, type EmbedTheme } from "@/components/embed/useEmbedTheme";
67

78
export const Route = createFileRoute("/embed/map")({
89
component: EmbedMap,
@@ -11,14 +12,16 @@ export const Route = createFileRoute("/embed/map")({
1112
token: (search.token as string) ?? "",
1213
bg: (search.bg as string) ?? "transparent",
1314
branding: String(search.branding ?? ""),
15+
theme: (String(search.theme ?? "system") as EmbedTheme),
1416
}),
1517
});
1618

1719
function EmbedMap() {
18-
const { website, token, bg, branding } = Route.useSearch();
20+
const { website, token, bg, branding, theme } = Route.useSearch();
1921
const { sessions } = useWebSocket(website || null, token || undefined);
2022
const explicitBranding = branding === "1" || branding === "true";
2123
const showBranding = useEmbedBranding(website, token, explicitBranding);
24+
const t = useEmbedTheme(theme === "dark" || theme === "light" ? theme : "system");
2225

2326
if (!website || !token) {
2427
return null;
@@ -27,7 +30,7 @@ function EmbedMap() {
2730
return (
2831
<div style={{ width: "100%", height: "100vh", background: bg, overflow: "hidden", position: "relative" }}>
2932
<Map sessions={Array.from(sessions.values())} showAvatars={false} />
30-
{showBranding && <BrandingBadge />}
33+
{showBranding && <BrandingBadge theme={t} />}
3134
</div>
3235
);
3336
}

0 commit comments

Comments
 (0)