@snapdragonsnursery/react-components 1.0.2 → 1.0.5

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/README.md CHANGED
@@ -12,52 +12,466 @@ Reusable React components for Snapdragons projects.
12
12
  Install via npm (local path or published package):
13
13
 
14
14
  ```
15
- npm install /path/to/react-components
15
+ npm install @snapdragonsnursery/react-components
16
16
  ```
17
17
 
18
- Or, if published:
18
+ ## Required Dependencies
19
19
 
20
+ Your project must have the following installed:
21
+
22
+ ### Core Dependencies
23
+
24
+ ```bash
25
+ npm install react react-dom
26
+ npm install @azure/msal-browser @azure/msal-react
27
+ npm install @headlessui/react @heroicons/react
20
28
  ```
21
- npm install snaps-react-components
29
+
30
+ ### CSS Framework (Required for AuthButtons styling)
31
+
32
+ ```bash
33
+ npm install -D tailwindcss postcss autoprefixer
34
+ npx tailwindcss init -p
22
35
  ```
23
36
 
24
- ## Peer Dependencies
37
+ ## Complete Setup Guide for New Projects
25
38
 
26
- Your project must have the following installed:
27
- - `react` (>=18)
28
- - `react-dom` (>=18)
29
- - `@azure/msal-react` (>=1.0.0)
39
+ ### 1. Install Dependencies
30
40
 
31
- ## Usage
41
+ ```bash
42
+ # Core authentication dependencies
43
+ npm install @azure/msal-browser @azure/msal-react
44
+
45
+ # UI dependencies for AuthButtons
46
+ npm install @headlessui/react @heroicons/react
47
+
48
+ # Tailwind CSS (required for styling)
49
+ npm install -D tailwindcss postcss autoprefixer
50
+ npx tailwindcss init -p
51
+
52
+ # Install this component library
53
+ npm install @snapdragonsnursery/react-components
54
+ ```
32
55
 
33
- ```jsx
34
- import { AuthButtons, ThemeToggle } from 'react-components';
56
+ ### 2. Configure Tailwind CSS
35
57
 
36
- // Example usage in your header:
37
- <AuthButtons
38
- theme={theme}
39
- setTheme={setTheme}
40
- authState={authState}
41
- setAuthState={setAuthState}
42
- />
58
+ Update `tailwind.config.js`:
43
59
 
44
- // Standalone theme toggle:
45
- <ThemeToggle theme={theme} setTheme={setTheme} />
60
+ ```javascript
61
+ /** @type {import('tailwindcss').Config} */
62
+ export default {
63
+ content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
64
+ darkMode: "class",
65
+ theme: {
66
+ extend: {},
67
+ },
68
+ plugins: [],
69
+ };
46
70
  ```
47
71
 
48
- - `theme` and `setTheme` should be managed by your app (e.g., via context or state).
49
- - `authState` and `setAuthState` should be managed by your app, typically using MSAL hooks.
72
+ Update `src/index.css`:
73
+
74
+ ```css
75
+ @tailwind base;
76
+ @tailwind components;
77
+ @tailwind utilities;
78
+
79
+ /* Your custom styles */
80
+ body {
81
+ margin: 0;
82
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
83
+ "Oxygen", "Ubuntu", "Cantarell", "Open Sans", "Helvetica Neue", sans-serif;
84
+ -webkit-font-smoothing: antialiased;
85
+ -moz-osx-font-smoothing: grayscale;
86
+ }
87
+
88
+ #root {
89
+ min-height: 100vh;
90
+ }
91
+ ```
92
+
93
+ ### 3. Create Authentication Configuration
94
+
95
+ Create `src/utils/authConfig.js`:
96
+
97
+ ```javascript
98
+ import { LogLevel } from "@azure/msal-browser";
99
+
100
+ export const msalConfig = {
101
+ auth: {
102
+ clientId: "2e2d9456-a0b7-4651-a8f8-c979ed1486b0", // Snapdragons Azure AD App
103
+ authority:
104
+ "https://login.microsoftonline.com/d098ee48-9874-4959-8229-da24c99c36fb",
105
+ redirectUri: window.location.origin,
106
+ postLogoutRedirectUri: window.location.origin,
107
+ navigateToLoginRequestUrl: false,
108
+ },
109
+ cache: {
110
+ cacheLocation: "sessionStorage",
111
+ storeAuthStateInCookie: false,
112
+ },
113
+ system: {
114
+ loggerOptions: {
115
+ loggerCallback: (level, message, containsPii) => {
116
+ if (containsPii) return;
117
+ switch (level) {
118
+ case LogLevel.Error:
119
+ console.error(message);
120
+ return;
121
+ case LogLevel.Warning:
122
+ console.warn(message);
123
+ return;
124
+ default:
125
+ return;
126
+ }
127
+ },
128
+ logLevel: LogLevel.Warning,
129
+ },
130
+ },
131
+ };
132
+
133
+ export const loginRequest = {
134
+ scopes: ["User.Read", "email", "profile", "Directory.Read.All"],
135
+ };
136
+
137
+ export const adminConfig = {
138
+ adminEmailDomains: [],
139
+ adminEmails: [
140
+ "james@snapdragonsnursery.com",
141
+ "accounts@snapdragonsnursery.com",
142
+ ],
143
+ adminGroupIds: [],
144
+ adminRoles: ["FundingAdmin", "Admin"],
145
+ };
146
+
147
+ export const isUserAdmin = (account, idTokenClaims = null) => {
148
+ if (!account) return false;
149
+ const claims = idTokenClaims || account.idTokenClaims;
150
+ const email = account.username || claims?.email || claims?.preferred_username;
151
+
152
+ if (
153
+ email &&
154
+ adminConfig.adminEmailDomains.some((domain) =>
155
+ email.toLowerCase().endsWith(`@${domain.toLowerCase()}`)
156
+ )
157
+ ) {
158
+ return true;
159
+ }
160
+
161
+ if (
162
+ email &&
163
+ adminConfig.adminEmails.some(
164
+ (adminEmail) => email.toLowerCase() === adminEmail.toLowerCase()
165
+ )
166
+ ) {
167
+ return true;
168
+ }
169
+
170
+ if (claims?.groups && adminConfig.adminGroupIds.length > 0) {
171
+ const userGroups = Array.isArray(claims.groups)
172
+ ? claims.groups
173
+ : [claims.groups];
174
+ if (
175
+ userGroups.some((groupId) => adminConfig.adminGroupIds.includes(groupId))
176
+ ) {
177
+ return true;
178
+ }
179
+ }
180
+
181
+ if (claims?.roles && adminConfig.adminRoles.length > 0) {
182
+ const userRoles = Array.isArray(claims.roles)
183
+ ? claims.roles
184
+ : [claims.roles];
185
+ if (userRoles.some((role) => adminConfig.adminRoles.includes(role))) {
186
+ return true;
187
+ }
188
+ }
189
+
190
+ return false;
191
+ };
192
+ ```
193
+
194
+ ### 4. Create Authentication Provider
195
+
196
+ Create `src/utils/AuthProvider.jsx`:
197
+
198
+ ```javascript
199
+ import React, { createContext, useContext, useEffect, useState } from "react";
200
+ import { useMsal, useAccount } from "@azure/msal-react";
201
+ import { isUserAdmin } from "./authConfig";
202
+
203
+ const AuthContext = createContext();
204
+
205
+ export const useAuth = () => {
206
+ const context = useContext(AuthContext);
207
+ if (!context) {
208
+ throw new Error("useAuth must be used within an AuthProvider");
209
+ }
210
+ return context;
211
+ };
212
+
213
+ export const AuthProvider = ({ children }) => {
214
+ const { instance, accounts } = useMsal();
215
+ const account = useAccount(accounts[0] || {});
216
+ const [isLoading, setIsLoading] = useState(true);
217
+ const [userRole, setUserRole] = useState(null);
218
+
219
+ useEffect(() => {
220
+ const checkUserRole = async () => {
221
+ if (account) {
222
+ const adminStatus = isUserAdmin(account);
223
+ setUserRole(adminStatus ? "admin" : "employee");
224
+ } else {
225
+ setUserRole(null);
226
+ }
227
+ setIsLoading(false);
228
+ };
229
+ checkUserRole();
230
+ }, [account]);
231
+
232
+ const login = async () => {
233
+ try {
234
+ await instance.loginPopup();
235
+ } catch (error) {
236
+ console.error("Login failed:", error);
237
+ throw error;
238
+ }
239
+ };
240
+
241
+ const logout = async () => {
242
+ try {
243
+ await instance.logoutPopup();
244
+ } catch (error) {
245
+ console.error("Logout failed:", error);
246
+ throw error;
247
+ }
248
+ };
249
+
250
+ const isAuthenticated = !!account;
251
+ const isAdmin = userRole === "admin";
252
+
253
+ const value = {
254
+ account,
255
+ isAuthenticated,
256
+ isAdmin,
257
+ userRole,
258
+ isLoading,
259
+ login,
260
+ logout,
261
+ instance,
262
+ };
263
+
264
+ return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
265
+ };
266
+ ```
50
267
 
51
- ## Telemetry
268
+ ### 5. Create Theme Provider
52
269
 
53
- This package includes stub telemetry functions by default. You can replace or extend these in your app as needed by providing your own implementations in place of `src/telemetry.js`.
270
+ Create `src/utils/theme.jsx`:
54
271
 
55
- ## Development
272
+ ```javascript
273
+ import { createContext, useContext, useEffect, useState } from "react";
56
274
 
57
- - Components are in `src/`.
58
- - Exported via `src/index.js`.
59
- - Update or add new components as needed.
275
+ const ThemeContext = createContext();
60
276
 
61
- ---
277
+ export function ThemeProvider({ children }) {
278
+ const [theme, setTheme] = useState("system");
279
+
280
+ useEffect(() => {
281
+ const savedTheme = localStorage.getItem("theme");
282
+ if (savedTheme && ["light", "dark", "system"].includes(savedTheme)) {
283
+ setTheme(savedTheme);
284
+ } else {
285
+ setTheme("system");
286
+ }
287
+ }, []);
288
+
289
+ useEffect(() => {
290
+ const applyTheme = () => {
291
+ if (theme === "system") {
292
+ const systemPrefersDark = window.matchMedia(
293
+ "(prefers-color-scheme: dark)"
294
+ ).matches;
295
+ if (systemPrefersDark) {
296
+ document.documentElement.classList.add("dark");
297
+ } else {
298
+ document.documentElement.classList.remove("dark");
299
+ }
300
+ } else if (theme === "dark") {
301
+ document.documentElement.classList.add("dark");
302
+ } else {
303
+ document.documentElement.classList.remove("dark");
304
+ }
305
+ };
306
+
307
+ applyTheme();
308
+ localStorage.setItem("theme", theme);
309
+ }, [theme]);
310
+
311
+ useEffect(() => {
312
+ if (theme !== "system") return;
313
+
314
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
315
+ const handleChange = () => {
316
+ if (theme === "system") {
317
+ if (mediaQuery.matches) {
318
+ document.documentElement.classList.add("dark");
319
+ } else {
320
+ document.documentElement.classList.remove("dark");
321
+ }
322
+ }
323
+ };
324
+
325
+ mediaQuery.addEventListener("change", handleChange);
326
+ return () => mediaQuery.removeEventListener("change", handleChange);
327
+ }, [theme]);
328
+
329
+ return (
330
+ <ThemeContext.Provider value={{ theme, setTheme }}>
331
+ {children}
332
+ </ThemeContext.Provider>
333
+ );
334
+ }
335
+
336
+ export function useTheme() {
337
+ const context = useContext(ThemeContext);
338
+ if (context === undefined) {
339
+ throw new Error("useTheme must be used within a ThemeProvider");
340
+ }
341
+ return context;
342
+ }
343
+ ```
344
+
345
+ ### 6. Setup Main Entry Point
346
+
347
+ Update `src/main.jsx`:
348
+
349
+ ```javascript
350
+ import React from "react";
351
+ import ReactDOM from "react-dom/client";
352
+ import App from "./App.jsx";
353
+ import "./index.css";
354
+ import { MsalProvider } from "@azure/msal-react";
355
+ import { PublicClientApplication } from "@azure/msal-browser";
356
+ import { msalConfig } from "./utils/authConfig";
357
+ import { AuthProvider } from "./utils/AuthProvider";
358
+ import { ThemeProvider } from "./utils/theme";
359
+
360
+ const msalInstance = new PublicClientApplication(msalConfig);
361
+
362
+ ReactDOM.createRoot(document.getElementById("root")).render(
363
+ <React.StrictMode>
364
+ <MsalProvider instance={msalInstance}>
365
+ <AuthProvider>
366
+ <ThemeProvider>
367
+ <App />
368
+ </ThemeProvider>
369
+ </AuthProvider>
370
+ </MsalProvider>
371
+ </React.StrictMode>
372
+ );
373
+ ```
374
+
375
+ ### 7. Create Navbar Component
376
+
377
+ Create `src/components/Navbar.jsx`:
378
+
379
+ ```javascript
380
+ import React, { useState, useEffect } from "react";
381
+ import { AuthButtons } from "@snapdragonsnursery/react-components";
382
+ import { useAuth } from "../utils/AuthProvider";
383
+ import { useTheme } from "../utils/theme";
384
+
385
+ const Navbar = () => {
386
+ const { account, userRole, instance } = useAuth();
387
+ const { theme, setTheme } = useTheme();
388
+ const [authState, setAuthState] = useState({
389
+ isAuthenticated: !!account,
390
+ accountsCount: account ? 1 : 0,
391
+ account,
392
+ instance,
393
+ });
394
+
395
+ useEffect(() => {
396
+ setAuthState({
397
+ isAuthenticated: !!account,
398
+ accountsCount: account ? 1 : 0,
399
+ account,
400
+ instance,
401
+ });
402
+ }, [account, instance]);
403
+
404
+ return (
405
+ <nav className="bg-white dark:bg-gray-800 shadow-md sticky top-0 z-40 px-4 py-2 flex justify-between items-center">
406
+ <div className="flex items-center space-x-4">
407
+ <span className="font-bold text-lg text-gray-800 dark:text-gray-100">
408
+ Your App Name
409
+ </span>
410
+ </div>
411
+ <div className="flex items-center space-x-4">
412
+ <AuthButtons
413
+ theme={theme}
414
+ setTheme={setTheme}
415
+ authState={authState}
416
+ setAuthState={setAuthState}
417
+ />
418
+ </div>
419
+ </nav>
420
+ );
421
+ };
422
+
423
+ export default Navbar;
424
+ ```
425
+
426
+ ### 8. Use in Your App
427
+
428
+ Update `src/App.jsx`:
429
+
430
+ ```javascript
431
+ import Navbar from "./components/Navbar";
432
+
433
+ function App() {
434
+ return (
435
+ <>
436
+ <Navbar />
437
+ {/* Your app content */}
438
+ </>
439
+ );
440
+ }
441
+
442
+ export default App;
443
+ ```
444
+
445
+ ## Usage
446
+
447
+ Once setup is complete, the AuthButtons component will:
448
+
449
+ - Show a user icon when not authenticated (clickable to login)
450
+ - Show user profile picture or initials when authenticated
451
+ - Provide a dropdown menu with theme toggle and logout option
452
+ - Handle all authentication flows automatically
453
+
454
+ ## Troubleshooting
455
+
456
+ ### Common Issues
457
+
458
+ 1. **Blank icon**: Ensure Tailwind CSS is properly configured and included in your build
459
+ 2. **Authentication errors**: Check that all MSAL dependencies are installed and providers are set up correctly
460
+ 3. **Theme not working**: Ensure ThemeProvider is wrapping your app and `darkMode: 'class'` is set in Tailwind config
461
+
462
+ ### Required File Structure
463
+
464
+ ```
465
+ src/
466
+ ├── utils/
467
+ │ ├── authConfig.js
468
+ │ ├── AuthProvider.jsx
469
+ │ └── theme.jsx
470
+ ├── components/
471
+ │ └── Navbar.jsx
472
+ ├── main.jsx
473
+ ├── App.jsx
474
+ └── index.css
475
+ ```
62
476
 
63
- For questions or contributions, contact the Snapdragons development team.
477
+ For questions or contributions, contact the Snapdragons development team.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snapdragonsnursery/react-components",
3
- "version": "1.0.2",
3
+ "version": "1.0.5",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -19,6 +19,8 @@
19
19
  },
20
20
  "homepage": "https://github.com/Snapdragons-Nursery/react-components#readme",
21
21
  "dependencies": {
22
+ "@headlessui/react": "^2.2.4",
23
+ "@popperjs/core": "^2.11.8",
22
24
  "react": "^18.3.1",
23
25
  "react-dom": "^18.3.1"
24
26
  },
@@ -1,3 +1,20 @@
1
+ // AuthButtons.jsx
2
+ //
3
+ // Authentication/profile button with dropdown menu for user actions and theme toggle.
4
+ // Uses Popper.js for smart dropdown positioning to prevent overflow.
5
+ //
6
+ // Example usage:
7
+ // import AuthButtons from './AuthButtons';
8
+ // <AuthButtons theme={theme} setTheme={setTheme} authState={authState} setAuthState={setAuthState} />
9
+ //
10
+ // Props:
11
+ // - theme: current theme string
12
+ // - setTheme: function to update theme
13
+ // - authState: authentication state object
14
+ // - setAuthState: function to update auth state
15
+ //
16
+ // The dropdown menu will always remain within the viewport, even if the button is near the edge.
17
+
1
18
  import { useEffect, useState, useRef } from "react";
2
19
  import { useMsal, useIsAuthenticated } from "@azure/msal-react";
3
20
  import {
@@ -7,34 +24,20 @@ import {
7
24
  clearUserContext,
8
25
  } from "./telemetry";
9
26
  import ThemeToggle from "./ThemeToggle";
27
+ import { Menu } from "@headlessui/react";
28
+ import { createPopper } from "@popperjs/core";
10
29
 
11
30
  const isMobile = window.innerWidth < 640;
12
31
 
13
32
  function AuthButtons({ theme, setTheme, authState, setAuthState }) {
14
33
  const [isLoggingIn, setIsLoggingIn] = useState(false);
15
34
  const [profilePicture, setProfilePicture] = useState(null);
16
- const [showProfileMenu, setShowProfileMenu] = useState(false);
17
- const menuRef = useRef();
18
35
  const isLocalhost = window.location.hostname === "localhost";
19
36
 
20
37
  // Use MSAL hooks - parent app should ensure MSAL is initialized
21
38
  const { instance, accounts } = useMsal();
22
39
  const isAuthenticated = useIsAuthenticated();
23
40
 
24
- // Close profile menu when clicking outside
25
- useEffect(() => {
26
- const handleClickOutside = (event) => {
27
- if (menuRef.current && !menuRef.current.contains(event.target)) {
28
- setShowProfileMenu(false);
29
- }
30
- };
31
-
32
- document.addEventListener("mousedown", handleClickOutside);
33
- return () => {
34
- document.removeEventListener("mousedown", handleClickOutside);
35
- };
36
- }, []);
37
-
38
41
  // Update auth state when MSAL state changes
39
42
  useEffect(() => {
40
43
  if (instance) {
@@ -213,6 +216,59 @@ function AuthButtons({ theme, setTheme, authState, setAuthState }) {
213
216
  const displayName =
214
217
  authState.account?.name ?? authState.account?.idTokenClaims?.name ?? "User";
215
218
 
219
+ // Popper.js refs and logic
220
+ const buttonRef = useRef(null);
221
+ const menuRef = useRef(null);
222
+ const popperInstance = useRef(null);
223
+
224
+ // Show/hide state for menu
225
+ const [menuOpen, setMenuOpen] = useState(false);
226
+
227
+ // Setup Popper when menu is open
228
+ useEffect(() => {
229
+ if (menuOpen && buttonRef.current && menuRef.current) {
230
+ popperInstance.current = createPopper(
231
+ buttonRef.current,
232
+ menuRef.current,
233
+ {
234
+ placement: "bottom-end",
235
+ modifiers: [
236
+ { name: "preventOverflow", options: { boundary: "viewport" } },
237
+ {
238
+ name: "flip",
239
+ options: {
240
+ fallbackPlacements: ["bottom-start", "top-end", "top-start"],
241
+ },
242
+ },
243
+ ],
244
+ }
245
+ );
246
+ }
247
+ return () => {
248
+ if (popperInstance.current) {
249
+ popperInstance.current.destroy();
250
+ popperInstance.current = null;
251
+ }
252
+ };
253
+ }, [menuOpen]);
254
+
255
+ // Close menu on outside click
256
+ useEffect(() => {
257
+ if (!menuOpen) return;
258
+ function handleClick(e) {
259
+ if (
260
+ menuRef.current &&
261
+ !menuRef.current.contains(e.target) &&
262
+ buttonRef.current &&
263
+ !buttonRef.current.contains(e.target)
264
+ ) {
265
+ setMenuOpen(false);
266
+ }
267
+ }
268
+ document.addEventListener("mousedown", handleClick);
269
+ return () => document.removeEventListener("mousedown", handleClick);
270
+ }, [menuOpen]);
271
+
216
272
  if (!authState.account) {
217
273
  return (
218
274
  <div className="flex items-center gap-2">
@@ -250,51 +306,51 @@ function AuthButtons({ theme, setTheme, authState, setAuthState }) {
250
306
  }
251
307
 
252
308
  return (
253
- <div className="relative">
254
- {/* Profile picture as trigger */}
255
- {profilePicture ? (
256
- <img
257
- src={profilePicture}
258
- alt={`${displayName}'s profile picture`}
259
- className="w-8 h-8 rounded-full object-cover border-2 border-blue-500 cursor-pointer"
260
- onClick={() => setShowProfileMenu((v) => !v)}
261
- />
262
- ) : (
263
- <div
264
- className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center cursor-pointer border-2 border-blue-500"
265
- onClick={() => setShowProfileMenu((v) => !v)}
309
+ <div className="relative inline-block text-left">
310
+ <div>
311
+ <Menu.Button
312
+ as="div"
313
+ ref={buttonRef}
314
+ className="focus:outline-none"
315
+ onClick={() => setMenuOpen((open) => !open)}
266
316
  >
267
- <span className="text-white text-sm font-medium">
268
- {displayName
269
- .split(" ")
270
- .map((n) => n[0])
271
- .join("")
272
- .toUpperCase()
273
- .slice(0, 2)}
274
- </span>
275
- </div>
276
- )}
277
- {showProfileMenu && (
317
+ {profilePicture ? (
318
+ <img
319
+ src={profilePicture}
320
+ alt={`${displayName}'s profile picture`}
321
+ className="w-8 h-8 rounded-full object-cover border-2 border-blue-500 cursor-pointer"
322
+ />
323
+ ) : (
324
+ <div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center cursor-pointer border-2 border-blue-500">
325
+ <span className="text-white text-sm font-medium">
326
+ {displayName
327
+ .split(" ")
328
+ .map((n) => n[0])
329
+ .join("")
330
+ .toUpperCase()
331
+ .slice(0, 2)}
332
+ </span>
333
+ </div>
334
+ )}
335
+ </Menu.Button>
336
+ </div>
337
+ {menuOpen && (
278
338
  <div
279
339
  ref={menuRef}
280
- className="absolute right-0 mt-2 min-w-[220px] max-w-xs w-auto bg-white dark:bg-gray-800 rounded-lg shadow-lg z-50 py-2 border border-gray-200 dark:border-gray-700"
340
+ className="z-50 mt-2 min-w-[220px] max-w-xs w-auto bg-white dark:bg-gray-800 rounded-lg shadow-lg py-2 border border-gray-200 dark:border-gray-700 focus:outline-none"
341
+ style={{ position: "absolute" }}
281
342
  >
282
343
  <div className="px-4 py-2 text-xs text-gray-500 dark:text-gray-300 border-b border-gray-100 dark:border-gray-700">
283
344
  Signed in as <span className="font-semibold">{displayName}</span>
284
345
  </div>
285
- {/* Theme toggle inside dropdown */}
286
346
  <div className="px-4 py-2">
287
347
  <ThemeToggle theme={theme} setTheme={setTheme} />
288
348
  </div>
289
349
  <button
290
- className="flex items-center w-full px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
291
- onClick={() => {
292
- setShowProfileMenu(false);
293
- handleLogout();
294
- }}
350
+ className="flex items-center w-full px-4 py-2 text-sm transition-colors hover:bg-gray-100 dark:hover:bg-gray-700"
351
+ onClick={handleLogout}
295
352
  >
296
353
  <span className="mr-2">
297
- {/* Logout icon */}
298
354
  <svg
299
355
  className="w-5 h-5"
300
356
  fill="none"
@@ -0,0 +1,37 @@
1
+ import React, { useState } from "react";
2
+ import ThemeToggle from "./ThemeToggle";
3
+
4
+ export default function ThemeToggleTest() {
5
+ const [theme, setTheme] = useState("system");
6
+
7
+ return (
8
+ <div
9
+ style={{
10
+ minHeight: "100vh",
11
+ background:
12
+ theme === "dark"
13
+ ? "#1a202c"
14
+ : theme === "light"
15
+ ? "#f9fafb"
16
+ : "#e5e7eb",
17
+ display: "flex",
18
+ alignItems: "center",
19
+ justifyContent: "center",
20
+ transition: "background 0.3s",
21
+ }}
22
+ >
23
+ <div style={{ width: 300 }}>
24
+ <ThemeToggle theme={theme} setTheme={setTheme} />
25
+ <div
26
+ style={{
27
+ marginTop: 24,
28
+ textAlign: "center",
29
+ color: theme === "dark" ? "#fff" : "#222",
30
+ }}
31
+ >
32
+ <strong>Current theme:</strong> {theme}
33
+ </div>
34
+ </div>
35
+ </div>
36
+ );
37
+ }
package/src/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { default as AuthButtons } from "./AuthButtons";
2
2
  export { default as ThemeToggle } from "./ThemeToggle";
3
3
  export { default as ChildSearchModal } from "./ChildSearchModal";
4
+ export { default as ThemeToggleTest } from "./ThemeToggleTest";
4
5
  export { configureTelemetry } from "./telemetry";