light mode and accessibility feat

This commit is contained in:
Madison 2025-07-02 12:05:53 -05:00
parent 14dab47fd5
commit ffbdae7ac9

View File

@ -1,5 +1,6 @@
'use client';
import { Palette } from 'lucide-react';
import React, { useEffect, useRef, useState } from 'react';
import { codeToHtml } from "shiki";
import './clickable-code-styles.css';
@ -22,8 +23,64 @@ function ClickableCodeblock({
}) {
const [highlightedCode, setHighlightedCode] = useState<string>("");
const [linePositions, setLinePositions] = useState<Array<{ top: number, height: number }>>([]);
const [selectedLightTheme, setSelectedLightTheme] = useState<string>('github-light-default');
const [selectedDarkTheme, setSelectedDarkTheme] = useState<string>('github-dark');
const [isThemeSwitcherOpen, setIsThemeSwitcherOpen] = useState(false);
const codeRef = useRef<HTMLDivElement>(null);
// Available themes
const lightThemes = [
{ value: 'github-light-default', label: 'GitHub Light' },
{ value: 'light-plus', label: 'VS Code Light' },
{ value: 'min-light', label: 'Minimal Light' },
{ value: 'slack-ochin', label: 'Slack Light' },
];
const darkThemes = [
{ value: 'github-dark', label: 'GitHub Dark' },
{ value: 'github-dark-dimmed', label: 'GitHub Dimmed' },
{ value: 'dark-plus', label: 'VS Code Dark' },
{ value: 'dracula', label: 'Dracula' },
];
// Load saved theme preferences on mount
useEffect(() => {
const savedLightTheme = localStorage.getItem('stack-docs-light-theme');
const savedDarkTheme = localStorage.getItem('stack-docs-dark-theme');
if (savedLightTheme) {
setSelectedLightTheme(savedLightTheme);
}
if (savedDarkTheme) {
setSelectedDarkTheme(savedDarkTheme);
}
}, []);
// Close theme switcher when clicking outside
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (isThemeSwitcherOpen && !(event.target as Element).closest('.theme-switcher-container')) {
setIsThemeSwitcherOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, [isThemeSwitcherOpen]);
// Save theme preferences when they change
const handleThemeChange = (isDark: boolean, theme: string) => {
if (isDark) {
setSelectedDarkTheme(theme);
localStorage.setItem('stack-docs-dark-theme', theme);
} else {
setSelectedLightTheme(theme);
localStorage.setItem('stack-docs-light-theme', theme);
}
};
// Measure actual line positions after code is rendered
useEffect(() => {
if (codeRef.current && highlightedCode) {
@ -102,12 +159,15 @@ function ClickableCodeblock({
const updateHighlightedCode = async () => {
try {
// Detect if we're in dark mode by checking CSS custom properties or document class
const isDarkMode = document.documentElement.classList.contains('dark') ||
const isDarkMode = document.documentElement.classList.contains('dark') ||
getComputedStyle(document.documentElement).getPropertyValue('--fd-background').includes('0 0% 3.9%');
// Use selected themes instead of hardcoded ones
const themeToUse = isDarkMode ? selectedDarkTheme : selectedLightTheme;
const html = await codeToHtml(code, {
lang: language,
theme: isDarkMode ? 'github-dark' : 'github-light-default',
theme: themeToUse,
transformers: [{
pre(node) {
// Remove background styles from pre element
@ -156,7 +216,7 @@ function ClickableCodeblock({
return () => {
observer.disconnect();
};
}, [code, language]);
}, [code, language, selectedLightTheme, selectedDarkTheme]);
return (
<div className="space-y-4 mb-6">
@ -168,6 +228,70 @@ function ClickableCodeblock({
lineHeight: '1.5',
}}
>
{/* Theme Switcher */}
<div className="absolute top-2 right-2 z-10 theme-switcher-container">
<div className="relative">
<button
onClick={() => setIsThemeSwitcherOpen(!isThemeSwitcherOpen)}
className="p-1.5 rounded-md bg-fd-muted/80 hover:bg-fd-muted text-fd-muted-foreground hover:text-fd-foreground transition-colors border border-fd-border/50 group"
title="Choose code syntax highlighting theme for better readability and accessibility"
aria-label="Code theme selector for accessibility"
>
<Palette className="w-4 h-4" />
{/* Hover tooltip */}
<div className="absolute bottom-full right-0 mb-2 px-2 py-1 bg-fd-popover text-fd-popover-foreground text-xs rounded border border-fd-border opacity-0 group-hover:opacity-100 pointer-events-none whitespace-nowrap z-30">
Choose syntax theme for accessibility
</div>
</button>
{isThemeSwitcherOpen && (
<div className="absolute top-full right-0 mt-1 bg-fd-background border border-fd-border rounded-md shadow-md min-w-40 z-20">
<div className="p-2">
{/* Title */}
<div className="text-xs font-medium text-fd-foreground mb-2 pb-1 border-b border-fd-border/30">
Code Theme
</div>
{/* Current mode indicator */}
<div>
<div className="flex items-center gap-1.5 text-xs text-fd-muted-foreground mb-1.5">
<div className={`w-1.5 h-1.5 rounded-full ${document.documentElement.classList.contains('dark') ? 'bg-slate-600' : 'bg-yellow-400'}`}></div>
<span>
{document.documentElement.classList.contains('dark') ? 'Dark Mode' : 'Light Mode'}
</span>
</div>
<div className="space-y-0.5">
{(document.documentElement.classList.contains('dark') ? darkThemes : lightThemes).map((theme) => {
const isDark = document.documentElement.classList.contains('dark');
const isSelected = isDark ? selectedDarkTheme === theme.value : selectedLightTheme === theme.value;
return (
<button
key={theme.value}
onClick={() => {
handleThemeChange(isDark, theme.value);
setIsThemeSwitcherOpen(false);
}}
className={`w-full flex items-center justify-between px-2 py-1 rounded text-xs transition-all duration-150 ${
isSelected
? 'bg-fd-primary text-fd-primary-foreground font-medium'
: 'hover:bg-fd-accent hover:text-fd-accent-foreground text-fd-foreground'
}`}
style={{ height: '24px' }}
>
<span className="flex items-center">{theme.label}</span>
{isSelected && (
<span className="flex items-center"></span>
)}
</button>
);
})}
</div>
</div>
</div>
</div>
)}
</div>
</div>
<div
ref={codeRef}
className="[&_*]:!bg-transparent [&_pre]:!bg-transparent [&_code]:!bg-transparent [&_pre]:!leading-6 [&_code]:!leading-6"