@checkmate-monitor/theme-frontend 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,25 @@
1
+ # @checkmate-monitor/theme-frontend
2
+
3
+ ## 0.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d673ab4: Add theme persistence for non-logged-in users via local storage
8
+
9
+ - Added `NavbarThemeToggle` component that shows a Sun/Moon button in the navbar for non-logged-in users
10
+ - Added `ThemeSynchronizer` component that loads theme from backend for logged-in users on page load
11
+ - Theme is now applied immediately on page load for logged-in users (no need to open user menu first)
12
+ - Non-logged-in users can now toggle theme, which persists in local storage
13
+ - Logged-in user's backend-saved theme takes precedence over local storage
14
+
15
+ ### Patch Changes
16
+
17
+ - Updated dependencies [eff5b4e]
18
+ - Updated dependencies [ffc28f6]
19
+ - Updated dependencies [32f2535]
20
+ - Updated dependencies [b354ab3]
21
+ - @checkmate-monitor/ui@0.1.0
22
+ - @checkmate-monitor/common@0.1.0
23
+ - @checkmate-monitor/auth-frontend@0.1.0
24
+ - @checkmate-monitor/frontend-api@0.0.2
25
+ - @checkmate-monitor/theme-common@0.0.2
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@checkmate-monitor/theme-frontend",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "src/index.tsx",
6
+ "scripts": {
7
+ "typecheck": "tsc --noEmit",
8
+ "lint": "bun run lint:code",
9
+ "lint:code": "eslint . --max-warnings 0"
10
+ },
11
+ "dependencies": {
12
+ "@checkmate-monitor/theme-common": "workspace:*",
13
+ "@checkmate-monitor/frontend-api": "workspace:*",
14
+ "@checkmate-monitor/auth-frontend": "workspace:*",
15
+ "@checkmate-monitor/common": "workspace:*",
16
+ "@checkmate-monitor/ui": "workspace:*",
17
+ "react": "^18.2.0",
18
+ "lucide-react": "^0.344.0"
19
+ },
20
+ "devDependencies": {
21
+ "typescript": "^5.0.0",
22
+ "@types/react": "^18.2.0",
23
+ "@checkmate-monitor/tsconfig": "workspace:*",
24
+ "@checkmate-monitor/scripts": "workspace:*"
25
+ }
26
+ }
@@ -0,0 +1,47 @@
1
+ import { Moon, Sun } from "lucide-react";
2
+ import { useApi } from "@checkmate-monitor/frontend-api";
3
+ import { authApiRef } from "@checkmate-monitor/auth-frontend/api";
4
+ import { Button, useTheme } from "@checkmate-monitor/ui";
5
+
6
+ /**
7
+ * Navbar theme toggle button for non-logged-in users.
8
+ *
9
+ * Shows a Sun/Moon icon button that toggles between light and dark themes.
10
+ * Only renders when user is NOT logged in (logged-in users use the toggle in UserMenu).
11
+ *
12
+ * Theme changes are saved to local storage via ThemeProvider.
13
+ */
14
+ export const NavbarThemeToggle = () => {
15
+ const { theme, setTheme } = useTheme();
16
+ const authApi = useApi(authApiRef);
17
+ const { data: session, isPending } = authApi.useSession();
18
+
19
+ // Don't render while loading session
20
+ if (isPending) {
21
+ return;
22
+ }
23
+
24
+ // Don't render for logged-in users (they use UserMenu toggle)
25
+ if (session?.user) {
26
+ return;
27
+ }
28
+
29
+ const isDark = theme === "dark";
30
+
31
+ const handleToggle = () => {
32
+ const newTheme = isDark ? "light" : "dark";
33
+ setTheme(newTheme);
34
+ };
35
+
36
+ return (
37
+ <Button
38
+ variant="ghost"
39
+ size="icon"
40
+ onClick={handleToggle}
41
+ aria-label={isDark ? "Switch to light mode" : "Switch to dark mode"}
42
+ className="rounded-full"
43
+ >
44
+ {isDark ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
45
+ </Button>
46
+ );
47
+ };
@@ -0,0 +1,68 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useApi, rpcApiRef } from "@checkmate-monitor/frontend-api";
3
+ import { authApiRef } from "@checkmate-monitor/auth-frontend/api";
4
+ import { useTheme } from "@checkmate-monitor/ui";
5
+ import { ThemeApi } from "@checkmate-monitor/theme-common";
6
+
7
+ /**
8
+ * Headless component that synchronizes theme on app initialization.
9
+ *
10
+ * - For logged-in users: fetches theme from backend and applies it
11
+ * - For non-logged-in users: theme is already read from local storage by ThemeProvider
12
+ * - Also syncs backend theme to local storage for continuity when logging out
13
+ *
14
+ * Must be rendered early in the app (e.g., via NavbarSlot) to ensure theme
15
+ * is applied before the user sees the page.
16
+ */
17
+ export const ThemeSynchronizer = () => {
18
+ const { setTheme } = useTheme();
19
+ const authApi = useApi(authApiRef);
20
+ const rpcApi = useApi(rpcApiRef);
21
+ const themeClient = rpcApi.forPlugin(ThemeApi);
22
+ const { data: session, isPending } = authApi.useSession();
23
+
24
+ // Track if we've already synced for this session to avoid repeated API calls
25
+ const hasSyncedRef = useRef(false);
26
+ const lastUserIdRef = useRef<string | undefined>(undefined);
27
+
28
+ useEffect(() => {
29
+ // Wait for session to load
30
+ if (isPending) {
31
+ return;
32
+ }
33
+
34
+ const currentUserId = session?.user?.id ?? undefined;
35
+
36
+ // Reset sync state when user changes (login/logout)
37
+ if (currentUserId !== lastUserIdRef.current) {
38
+ hasSyncedRef.current = false;
39
+ lastUserIdRef.current = currentUserId;
40
+ }
41
+
42
+ // Only sync once per session
43
+ if (hasSyncedRef.current) {
44
+ return;
45
+ }
46
+
47
+ // For logged-in users, fetch theme from backend
48
+ if (session?.user) {
49
+ themeClient
50
+ .getTheme()
51
+ .then(({ theme }) => {
52
+ setTheme(theme);
53
+ hasSyncedRef.current = true;
54
+ })
55
+ .catch((error) => {
56
+ console.error("Failed to sync theme from backend:", error);
57
+ hasSyncedRef.current = true; // Still mark as synced to prevent retry loops
58
+ });
59
+ } else {
60
+ // For non-logged-in users, local storage theme is already applied
61
+ // by ThemeProvider, so nothing to do
62
+ hasSyncedRef.current = true;
63
+ }
64
+ }, [session, isPending, setTheme, themeClient]);
65
+
66
+ // Headless component - renders nothing
67
+ return <></>;
68
+ };
@@ -0,0 +1,69 @@
1
+ import { useEffect, useState } from "react";
2
+ import { Moon, Sun } from "lucide-react";
3
+ import { Toggle, useTheme, useToast } from "@checkmate-monitor/ui";
4
+ import { useApi, rpcApiRef } from "@checkmate-monitor/frontend-api";
5
+ import { ThemeApi } from "@checkmate-monitor/theme-common";
6
+
7
+ /**
8
+ * Theme toggle menu item for logged-in users (displayed in UserMenu).
9
+ *
10
+ * Saves theme to both backend (for persistence across devices) and
11
+ * local storage (for continuity when logging out).
12
+ *
13
+ * Theme initialization is handled by ThemeSynchronizer component.
14
+ */
15
+ export const ThemeToggleMenuItem = () => {
16
+ const { theme, setTheme } = useTheme();
17
+ const rpcApi = useApi(rpcApiRef);
18
+ const themeClient = rpcApi.forPlugin(ThemeApi);
19
+
20
+ const [saving, setSaving] = useState(false);
21
+ const [isDark, setIsDark] = useState(theme === "dark");
22
+ const toast = useToast();
23
+
24
+ // Update local state when theme changes (e.g., from ThemeSynchronizer)
25
+ useEffect(() => {
26
+ setIsDark(theme === "dark");
27
+ }, [theme]);
28
+
29
+ const handleToggle = async (checked: boolean) => {
30
+ const newTheme = checked ? "dark" : "light";
31
+
32
+ // Update UI immediately
33
+ setIsDark(checked);
34
+ setTheme(newTheme); // Also updates local storage via ThemeProvider
35
+
36
+ // Save to backend
37
+ setSaving(true);
38
+ try {
39
+ await themeClient.setTheme({ theme: newTheme });
40
+ } catch (error) {
41
+ const message =
42
+ error instanceof Error
43
+ ? error.message
44
+ : "Failed to save theme preference";
45
+ toast.error(message);
46
+ console.error("Failed to save theme preference:", error);
47
+ // Revert on error
48
+ setIsDark(!checked);
49
+ setTheme(checked ? "light" : "dark");
50
+ } finally {
51
+ setSaving(false);
52
+ }
53
+ };
54
+
55
+ return (
56
+ <div className="flex items-center justify-between w-full px-4 py-2 text-sm text-popover-foreground">
57
+ <div className="flex items-center gap-2">
58
+ {isDark ? <Moon className="h-4 w-4" /> : <Sun className="h-4 w-4" />}
59
+ <span>Dark Mode</span>
60
+ </div>
61
+ <Toggle
62
+ checked={isDark}
63
+ onCheckedChange={handleToggle}
64
+ disabled={saving}
65
+ aria-label="Toggle dark mode"
66
+ />
67
+ </div>
68
+ );
69
+ };
package/src/index.tsx ADDED
@@ -0,0 +1,34 @@
1
+ import {
2
+ createFrontendPlugin,
3
+ NavbarSlot,
4
+ UserMenuItemsBottomSlot,
5
+ } from "@checkmate-monitor/frontend-api";
6
+ import { pluginMetadata } from "@checkmate-monitor/theme-common";
7
+ import { ThemeToggleMenuItem } from "./components/ThemeToggleMenuItem";
8
+ import { ThemeSynchronizer } from "./components/ThemeSynchronizer";
9
+ import { NavbarThemeToggle } from "./components/NavbarThemeToggle";
10
+
11
+ export const themePlugin = createFrontendPlugin({
12
+ metadata: pluginMetadata,
13
+ routes: [],
14
+ extensions: [
15
+ // Theme toggle in user menu (for logged-in users)
16
+ {
17
+ id: "theme.user-menu.toggle",
18
+ slot: UserMenuItemsBottomSlot,
19
+ component: ThemeToggleMenuItem,
20
+ },
21
+ // Theme synchronizer - headless component that syncs theme from backend on load
22
+ {
23
+ id: "theme.navbar.synchronizer",
24
+ slot: NavbarSlot,
25
+ component: ThemeSynchronizer,
26
+ },
27
+ // Theme toggle button in navbar (for non-logged-in users)
28
+ {
29
+ id: "theme.navbar.toggle",
30
+ slot: NavbarSlot,
31
+ component: NavbarThemeToggle,
32
+ },
33
+ ],
34
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "@checkmate-monitor/tsconfig/frontend.json",
3
+ "include": [
4
+ "src"
5
+ ]
6
+ }