@snapdragonsnursery/react-components 1.0.1 → 1.0.4
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/package.json +2 -1
- package/src/AuthButtons.jsx +58 -78
- package/src/ThemeToggle.jsx +3 -2
- package/src/ThemeToggleTest.jsx +37 -0
- package/src/index.js +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@snapdragonsnursery/react-components",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"homepage": "https://github.com/Snapdragons-Nursery/react-components#readme",
|
|
21
21
|
"dependencies": {
|
|
22
|
+
"@headlessui/react": "^2.2.4",
|
|
22
23
|
"react": "^18.3.1",
|
|
23
24
|
"react-dom": "^18.3.1"
|
|
24
25
|
},
|
package/src/AuthButtons.jsx
CHANGED
|
@@ -7,34 +7,19 @@ import {
|
|
|
7
7
|
clearUserContext,
|
|
8
8
|
} from "./telemetry";
|
|
9
9
|
import ThemeToggle from "./ThemeToggle";
|
|
10
|
+
import { Menu } from "@headlessui/react";
|
|
10
11
|
|
|
11
12
|
const isMobile = window.innerWidth < 640;
|
|
12
13
|
|
|
13
14
|
function AuthButtons({ theme, setTheme, authState, setAuthState }) {
|
|
14
15
|
const [isLoggingIn, setIsLoggingIn] = useState(false);
|
|
15
16
|
const [profilePicture, setProfilePicture] = useState(null);
|
|
16
|
-
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
|
17
|
-
const menuRef = useRef();
|
|
18
17
|
const isLocalhost = window.location.hostname === "localhost";
|
|
19
18
|
|
|
20
19
|
// Use MSAL hooks - parent app should ensure MSAL is initialized
|
|
21
20
|
const { instance, accounts } = useMsal();
|
|
22
21
|
const isAuthenticated = useIsAuthenticated();
|
|
23
22
|
|
|
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
23
|
// Update auth state when MSAL state changes
|
|
39
24
|
useEffect(() => {
|
|
40
25
|
if (instance) {
|
|
@@ -250,70 +235,65 @@ function AuthButtons({ theme, setTheme, authState, setAuthState }) {
|
|
|
250
235
|
}
|
|
251
236
|
|
|
252
237
|
return (
|
|
253
|
-
<div className="relative">
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
238
|
+
<Menu as="div" className="relative inline-block text-left">
|
|
239
|
+
<div>
|
|
240
|
+
<Menu.Button className="focus:outline-none">
|
|
241
|
+
{profilePicture ? (
|
|
242
|
+
<img
|
|
243
|
+
src={profilePicture}
|
|
244
|
+
alt={`${displayName}'s profile picture`}
|
|
245
|
+
className="w-8 h-8 rounded-full object-cover border-2 border-blue-500 cursor-pointer"
|
|
246
|
+
/>
|
|
247
|
+
) : (
|
|
248
|
+
<div className="w-8 h-8 rounded-full bg-blue-500 flex items-center justify-center cursor-pointer border-2 border-blue-500">
|
|
249
|
+
<span className="text-white text-sm font-medium">
|
|
250
|
+
{displayName
|
|
251
|
+
.split(" ")
|
|
252
|
+
.map((n) => n[0])
|
|
253
|
+
.join("")
|
|
254
|
+
.toUpperCase()
|
|
255
|
+
.slice(0, 2)}
|
|
256
|
+
</span>
|
|
257
|
+
</div>
|
|
258
|
+
)}
|
|
259
|
+
</Menu.Button>
|
|
260
|
+
</div>
|
|
261
|
+
<Menu.Items 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 focus:outline-none">
|
|
262
|
+
<div className="px-4 py-2 text-xs text-gray-500 dark:text-gray-300 border-b border-gray-100 dark:border-gray-700">
|
|
263
|
+
Signed in as <span className="font-semibold">{displayName}</span>
|
|
275
264
|
</div>
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
<div
|
|
279
|
-
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"
|
|
281
|
-
>
|
|
282
|
-
<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
|
-
Signed in as <span className="font-semibold">{displayName}</span>
|
|
284
|
-
</div>
|
|
285
|
-
{/* Theme toggle inside dropdown */}
|
|
286
|
-
<div className="px-4 py-2">
|
|
287
|
-
<ThemeToggle theme={theme} setTheme={setTheme} />
|
|
288
|
-
</div>
|
|
289
|
-
<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
|
-
}}
|
|
295
|
-
>
|
|
296
|
-
<span className="mr-2">
|
|
297
|
-
{/* Logout icon */}
|
|
298
|
-
<svg
|
|
299
|
-
className="w-5 h-5"
|
|
300
|
-
fill="none"
|
|
301
|
-
stroke="currentColor"
|
|
302
|
-
viewBox="0 0 24 24"
|
|
303
|
-
>
|
|
304
|
-
<path
|
|
305
|
-
strokeLinecap="round"
|
|
306
|
-
strokeLinejoin="round"
|
|
307
|
-
strokeWidth={2}
|
|
308
|
-
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
|
309
|
-
/>
|
|
310
|
-
</svg>
|
|
311
|
-
</span>
|
|
312
|
-
Logout
|
|
313
|
-
</button>
|
|
265
|
+
<div className="px-4 py-2">
|
|
266
|
+
<ThemeToggle theme={theme} setTheme={setTheme} />
|
|
314
267
|
</div>
|
|
315
|
-
|
|
316
|
-
|
|
268
|
+
<Menu.Item>
|
|
269
|
+
{({ active }) => (
|
|
270
|
+
<button
|
|
271
|
+
className={`flex items-center w-full px-4 py-2 text-sm transition-colors ${
|
|
272
|
+
active ? "bg-gray-100 dark:bg-gray-700" : ""
|
|
273
|
+
}`}
|
|
274
|
+
onClick={handleLogout}
|
|
275
|
+
>
|
|
276
|
+
<span className="mr-2">
|
|
277
|
+
<svg
|
|
278
|
+
className="w-5 h-5"
|
|
279
|
+
fill="none"
|
|
280
|
+
stroke="currentColor"
|
|
281
|
+
viewBox="0 0 24 24"
|
|
282
|
+
>
|
|
283
|
+
<path
|
|
284
|
+
strokeLinecap="round"
|
|
285
|
+
strokeLinejoin="round"
|
|
286
|
+
strokeWidth={2}
|
|
287
|
+
d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
|
|
288
|
+
/>
|
|
289
|
+
</svg>
|
|
290
|
+
</span>
|
|
291
|
+
Logout
|
|
292
|
+
</button>
|
|
293
|
+
)}
|
|
294
|
+
</Menu.Item>
|
|
295
|
+
</Menu.Items>
|
|
296
|
+
</Menu>
|
|
317
297
|
);
|
|
318
298
|
}
|
|
319
299
|
|
package/src/ThemeToggle.jsx
CHANGED
|
@@ -59,17 +59,18 @@ function ThemeToggle({ theme, setTheme }) {
|
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
return (
|
|
62
|
-
<div className="flex justify-center items-center bg-gray-200 dark:bg-gray-700 rounded-full p-
|
|
62
|
+
<div className="flex justify-center items-center bg-gray-200 dark:bg-gray-700 rounded-full p-2 w-full gap-2">
|
|
63
63
|
{options.map((opt) => (
|
|
64
64
|
<button
|
|
65
65
|
key={opt.value}
|
|
66
66
|
onClick={() => handleThemeChange(opt.value)}
|
|
67
|
-
className={`flex-1 flex items-center justify-center h-
|
|
67
|
+
className={`flex-1 flex items-center justify-center h-12 min-w-[44px] rounded-full transition-all duration-200 focus:outline-none mx-1
|
|
68
68
|
${
|
|
69
69
|
theme === opt.value
|
|
70
70
|
? "bg-white dark:bg-gray-900 shadow text-blue-600 dark:text-yellow-400"
|
|
71
71
|
: "bg-transparent text-gray-700 dark:text-gray-300 hover:bg-gray-300 dark:hover:bg-gray-600"
|
|
72
72
|
}`}
|
|
73
|
+
style={{ padding: "0 12px" }}
|
|
73
74
|
title={opt.label + " mode"}
|
|
74
75
|
aria-pressed={theme === opt.value}
|
|
75
76
|
>
|
|
@@ -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";
|