Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions src/theme/Root.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/* Base styles for the toast container */
.toastContainer {
position: fixed;
top: 20px;
right: 24px;
border: 1px solid oklch(0.69 0 0);
border-radius: 8px; /* Docusaurus --radius var is often 8px */
padding: 12px 16px;
font-size: 15px;
font-weight: 500;
z-index: 2147483647;
display: flex;
align-items: center;
gap: 12px;
max-width: 380px;
line-height: 1.4;
backdrop-filter: blur(8px);
animation: slideDownFadeOut 10s ease-in-out forwards;
transition:
background 0.3s ease,
color 0.3s ease,
box-shadow 0.3s ease;
}

/* Light theme specific styles */
.toastLight {
background: oklch(0.145 0 0 / 0.85); /* Added transparency for blur */
color: oklch(0.985 0 0);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
}

/* Dark theme specific styles */
.toastDark {
background: oklch(0.985 0 0 / 0.85); /* Added transparency for blur */
color: oklch(0.145 0 0);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}

/* Style for the link inside the toast */
.toastLink {
text-decoration: underline;
font-weight: 600;
}
.toastLight .toastLink {
color: oklch(0.985 0 0); /* White in light mode */
}
.toastDark .toastLink {
color: oklch(0.205 0 0); /* Dark in dark mode */
}

/* Styles for the content area */
.toastContent {
flex-grow: 1; /* Allow content to fill space */
}

/* Styles for the new icon */
.toastIcon {
flex-shrink: 0; /* Prevent icon from shrinking */
/* Match icon color to text color */
color: currentColor;
opacity: 0.8;
}

/* Minimalist close button */
.toastCloseButton {
flex-shrink: 0;
background: none;
border: none;
padding: 4px;
margin: -4px; /* Offset padding to align */
line-height: 1;
font-size: 20px;
font-weight: 600;
color: currentColor;
opacity: 0.5;
cursor: pointer;
border-radius: 4px;
transition:
opacity 0.2s ease,
background 0.2s ease;
}

.toastCloseButton:hover {
opacity: 1;
}
.toastLight .toastCloseButton:hover {
background: oklch(1 0 0 / 0.1); /* Faint white bg on hover */
}
.toastDark .toastCloseButton:hover {
background: oklch(0 0 0 / 0.1); /* Faint black bg on hover */
}

/* Animation keyframes with a subtle scale for "pop" */
@keyframes slideDownFadeOut {
0% {
opacity: 0;
transform: translateY(-10px) scale(0.95);
}
10%,
90% {
opacity: 1;
transform: translateY(0) scale(1);
}
100% {
opacity: 0;
transform: translateY(-10px) scale(0.95);
}
}
101 changes: 97 additions & 4 deletions src/theme/Root.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,105 @@
import React from "react";
import React, { useEffect, useState, useRef } from "react";
import Link from "@docusaurus/Link";
import { Analytics } from "@vercel/analytics/react";
import clsx from "clsx"; // Import clsx for conditional classes
import styles from "./Root.module.css"; // Import the CSS module

// A simple Trophy SVG icon component
function TrophyIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className={styles.toastIcon}
>
<path d="M6 9H4.5a2.5 2.5 0 0 1 0-5H6" />
<path d="M18 9h1.5a2.5 2.5 0 0 0 0-5H18" />
<path d="M4 22h16" />
<path d="M10 14.66V17c0 .55-.47.98-.97 1.21C7.85 18.75 7 20.24 7 22" />
<path d="M14 14.66V17c0 .55.47.98.97 1.21C16.15 18.75 17 20.24 17 22" />
<path d="M18 2H6v7a6 6 0 0 0 12 0V2Z" />
</svg>
);
}

export default function Root({ children }: { children: React.ReactNode }) {
const [showToast, setShowToast] = useState(false);
const [isDark, setIsDark] = useState(false);
const timerRef = useRef<NodeJS.Timeout | null>(null);

// Theme detection logic
useEffect(() => {
const html = document.documentElement;
const checkTheme = () => {
const attrDark = html.getAttribute("data-theme") === "dark";
const classDark = html.classList.contains("dark");
setIsDark(attrDark || classDark);
};

checkTheme();
const observer = new MutationObserver(checkTheme);
observer.observe(html, {
attributes: true,
attributeFilter: ["data-theme", "class"],
});

// Show toast and set timer
setShowToast(true);
timerRef.current = setTimeout(() => setShowToast(false), 10000);

return () => {
if (timerRef.current) {
clearTimeout(timerRef.current);
}
observer.disconnect();
};
}, []);

// Handle manual close
const handleCloseToast = () => {
setShowToast(false);
if (timerRef.current) {
clearTimeout(timerRef.current);
}
};

// Default implementation, that you can customize
export default function Root({ children }) {
return (
<>
{children}
{/* Only load analytics in production */}

{showToast && (
<div
// Use clsx to combine base class with theme-specific class
className={clsx(
styles.toastContainer,
isDark ? styles.toastDark : styles.toastLight,
)}
>
<TrophyIcon />
<div className={styles.toastContent}>
Check out our latest{" "}
<Link to="/dashboard#leaderboard" className={styles.toastLink}>
leaderboard
</Link>
!
</div>
<button
onClick={handleCloseToast}
className={styles.toastCloseButton}
aria-label="Close notification"
>
&times;
</button>
</div>
)}

{process.env.NODE_ENV === "production" && <Analytics />}
</>
);
Expand Down
Loading