feat: implement dark/light mode with system detection
- Install and configure next-themes for theme management - Add ThemeProvider wrapper component with suppressHydrationWarning - Define light theme CSS variables (off-white backgrounds, green accents) - Define dark theme CSS variables (hacker theme: terminal blacks, matrix green) - Update scrollbar styles for both themes - Add CRT scanline/glow effects (dark mode only) - Create ThemeToggle component with system/light/dark cycling - Add theme toggle to Toolbox sidebar - Update CandleChart to apply theme-specific colors - Chart colors update dynamically on theme change - All components use CSS variables for theme compatibility - FOUC prevention via next-themes inline script
This commit is contained in:
parent
4dad24845e
commit
0d235602af
9 changed files with 310 additions and 59 deletions
|
|
@ -1,45 +1,45 @@
|
||||||
## 1. Install and configure next-themes
|
## 1. Install and configure next-themes
|
||||||
|
|
||||||
- [ ] 1.1 Install `next-themes` package
|
- [x] 1.1 Install `next-themes` package
|
||||||
- [ ] 1.2 Create `src/components/ThemeProvider.tsx` client component wrapping `next-themes` ThemeProvider with `attribute="class"`, `defaultTheme="system"`, `enableSystem=true`
|
- [x] 1.2 Create `src/components/ThemeProvider.tsx` client component wrapping `next-themes` ThemeProvider with `attribute="class"`, `defaultTheme="system"`, `enableSystem=true`
|
||||||
- [ ] 1.3 Update `src/app/layout.tsx` to wrap children with ThemeProvider and add `suppressHydrationWarning` to `<html>`
|
- [x] 1.3 Update `src/app/layout.tsx` to wrap children with ThemeProvider and add `suppressHydrationWarning` to `<html>`
|
||||||
|
|
||||||
## 2. Define dark/light CSS variables
|
## 2. Define dark/light CSS variables
|
||||||
|
|
||||||
- [ ] 2.1 Update `src/app/globals.css` `:root` block with light theme HSL values (off-white backgrounds, green-600 accents, near-black text, gray borders)
|
- [x] 2.1 Update `src/app/globals.css` `:root` block with light theme HSL values (off-white backgrounds, green-600 accents, near-black text, gray borders)
|
||||||
- [ ] 2.2 Add `.dark` block in `src/app/globals.css` with hacker-theme dark palette values (terminal blacks, matrix green #00ff41 accents, neon text)
|
- [x] 2.2 Add `.dark` block in `src/app/globals.css` with hacker-theme dark palette values (terminal blacks, matrix green #00ff41 accents, neon text)
|
||||||
- [ ] 2.3 Update scrollbar styles in `globals.css` to use theme-aware colors for both light and dark modes
|
- [x] 2.3 Update scrollbar styles in `globals.css` to use theme-aware colors for both light and dark modes
|
||||||
- [ ] 2.4 Conditionally apply CRT scanline/glow CSS effects only inside `.dark` selector
|
- [x] 2.4 Conditionally apply CRT scanline/glow CSS effects only inside `.dark` selector
|
||||||
|
|
||||||
## 3. Update Tailwind config
|
## 3. Update Tailwind config
|
||||||
|
|
||||||
- [ ] 3.1 Verify `tailwind.config.ts` has `darkMode: ["class"]` (already set)
|
- [x] 3.1 Verify `tailwind.config.ts` has `darkMode: ["class"]` (already set)
|
||||||
- [ ] 3.2 Ensure all custom color tokens reference CSS variables so they auto-switch with theme
|
- [x] 3.2 Ensure all custom color tokens reference CSS variables so they auto-switch with theme
|
||||||
|
|
||||||
## 4. Add theme toggle to sidebar
|
## 4. Add theme toggle to sidebar
|
||||||
|
|
||||||
- [ ] 4.1 Create theme toggle button component using `useTheme()` from `next-themes` that cycles system → light → dark
|
- [x] 4.1 Create theme toggle button component using `useTheme()` from `next-themes` that cycles system → light → dark
|
||||||
- [ ] 4.2 Display correct icon per mode: `Monitor` (system), `Sun` (light), `Moon` (dark) from lucide-react
|
- [x] 4.2 Display correct icon per mode: `Monitor` (system), `Sun` (light), `Moon` (dark) from lucide-react
|
||||||
- [ ] 4.3 Add tooltip showing current mode name on hover
|
- [x] 4.3 Add tooltip showing current mode name on hover
|
||||||
- [ ] 4.4 Place the toggle button at the bottom of `src/components/Toolbox.tsx`, visually separated from tool buttons
|
- [x] 4.4 Place the toggle button at the bottom of `src/components/Toolbox.tsx`, visually separated from tool buttons
|
||||||
|
|
||||||
## 5. Update chart theming
|
## 5. Update chart theming
|
||||||
|
|
||||||
- [ ] 5.1 Update `src/components/CandleChart.tsx` to read current theme and apply matching chart colors (background, grid, text, crosshair)
|
- [x] 5.1 Update `src/components/CandleChart.tsx` to read current theme and apply matching chart colors (background, grid, text, crosshair)
|
||||||
- [ ] 5.2 Listen for theme changes and re-apply chart colors when theme switches without full remount
|
- [x] 5.2 Listen for theme changes and re-apply chart colors when theme switches without full remount
|
||||||
|
|
||||||
## 6. Audit and fix hardcoded colors
|
## 6. Audit and fix hardcoded colors
|
||||||
|
|
||||||
- [ ] 6.1 Audit `src/components/Toolbox.tsx` for hardcoded color values and replace with CSS variable references
|
- [x] 6.1 Audit `src/components/Toolbox.tsx` for hardcoded color values and replace with CSS variable references
|
||||||
- [ ] 6.2 Audit `src/components/SvgOverlay.tsx` for hardcoded colors and update for theme compatibility
|
- [x] 6.2 Audit `src/components/SvgOverlay.tsx` for hardcoded colors and update for theme compatibility
|
||||||
- [ ] 6.3 Audit `src/app/page.tsx` and `src/app/annotation-types/page.tsx` for hardcoded colors
|
- [x] 6.3 Audit `src/app/page.tsx` and `src/app/annotation-types/page.tsx` for hardcoded colors
|
||||||
- [ ] 6.4 Verify shadcn-ui components (`button.tsx`, `input.tsx`, `tooltip.tsx`) use theme variables correctly
|
- [x] 6.4 Verify shadcn-ui components (`button.tsx`, `input.tsx`, `tooltip.tsx`) use theme variables correctly
|
||||||
|
|
||||||
## 7. Test and verify
|
## 7. Test and verify
|
||||||
|
|
||||||
- [ ] 7.1 Verify no FOUC — page loads in correct theme without flash
|
- [x] 7.1 Verify no FOUC — page loads in correct theme without flash
|
||||||
- [ ] 7.2 Test toggle cycles correctly through system → light → dark → system
|
- [x] 7.2 Test toggle cycles correctly through system → light → dark → system
|
||||||
- [ ] 7.3 Verify localStorage persistence survives page refresh
|
- [x] 7.3 Verify localStorage persistence survives page refresh
|
||||||
- [ ] 7.4 Verify system preference detection works (change OS theme while set to "system")
|
- [x] 7.4 Verify system preference detection works (change OS theme while set to "system")
|
||||||
- [ ] 7.5 Verify chart colors update when theme switches
|
- [x] 7.5 Verify chart colors update when theme switches
|
||||||
- [ ] 7.6 Verify all components render correctly in both themes
|
- [x] 7.6 Verify all components render correctly in both themes
|
||||||
|
|
|
||||||
11
package-lock.json
generated
11
package-lock.json
generated
|
|
@ -24,6 +24,7 @@
|
||||||
"lightweight-charts": "^4.2.3",
|
"lightweight-charts": "^4.2.3",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next": "^16.1.6",
|
"next": "^16.1.6",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
|
|
@ -5230,6 +5231,16 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/next-themes": {
|
||||||
|
"version": "0.4.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
|
||||||
|
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/next/node_modules/postcss": {
|
"node_modules/next/node_modules/postcss": {
|
||||||
"version": "8.4.31",
|
"version": "8.4.31",
|
||||||
"funding": [
|
"funding": [
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
"lightweight-charts": "^4.2.3",
|
"lightweight-charts": "^4.2.3",
|
||||||
"lucide-react": "^0.563.0",
|
"lucide-react": "^0.563.0",
|
||||||
"next": "^16.1.6",
|
"next": "^16.1.6",
|
||||||
|
"next-themes": "^0.4.6",
|
||||||
"papaparse": "^5.5.3",
|
"papaparse": "^5.5.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
"react": "^19.2.4",
|
"react": "^19.2.4",
|
||||||
|
|
|
||||||
|
|
@ -4,25 +4,50 @@
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 0 0% 100%;
|
/* Light theme - off-white backgrounds, green-600 accents, near-black text, gray borders */
|
||||||
--foreground: 222 47% 11%;
|
--background: 120 25% 97%; /* #f8faf8 */
|
||||||
--card: 0 0% 100%;
|
--foreground: 240 15% 10%; /* #1a1a2e near-black */
|
||||||
--card-foreground: 222 47% 11%;
|
--card: 0 0% 100%; /* #ffffff */
|
||||||
|
--card-foreground: 240 15% 10%;
|
||||||
--popover: 0 0% 100%;
|
--popover: 0 0% 100%;
|
||||||
--popover-foreground: 222 47% 11%;
|
--popover-foreground: 240 15% 10%;
|
||||||
--primary: 221 83% 53%;
|
--primary: 142 76% 36%; /* #16a34a green-600 */
|
||||||
--primary-foreground: 210 40% 98%;
|
--primary-foreground: 0 0% 100%;
|
||||||
--secondary: 210 40% 96%;
|
--secondary: 120 10% 95%; /* #f0f4f0 */
|
||||||
--secondary-foreground: 222 47% 11%;
|
--secondary-foreground: 240 15% 10%;
|
||||||
--muted: 210 40% 96%;
|
--muted: 120 10% 95%;
|
||||||
--muted-foreground: 215 16% 47%;
|
--muted-foreground: 220 9% 46%; /* #4a5568 gray-600 */
|
||||||
--accent: 210 40% 96%;
|
--accent: 142 76% 36%; /* green-600 */
|
||||||
--accent-foreground: 222 47% 11%;
|
--accent-foreground: 0 0% 100%;
|
||||||
--destructive: 0 84% 60%;
|
--destructive: 348 100% 50%; /* #ff0040 neon red */
|
||||||
--destructive-foreground: 210 40% 98%;
|
--destructive-foreground: 0 0% 100%;
|
||||||
--border: 214 32% 91%;
|
--border: 220 13% 84%; /* #d1d5db gray-300 */
|
||||||
--input: 214 32% 91%;
|
--input: 220 13% 84%;
|
||||||
--ring: 221 83% 53%;
|
--ring: 142 76% 36%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
/* Dark hacker theme - terminal blacks, matrix green #00ff41 accents, neon text */
|
||||||
|
--background: 120 38% 3%; /* #0a0e0a near-black */
|
||||||
|
--foreground: 120 100% 63%; /* #00ff41 matrix green */
|
||||||
|
--card: 120 29% 5%; /* #0d110d */
|
||||||
|
--card-foreground: 120 100% 63%;
|
||||||
|
--popover: 120 29% 5%;
|
||||||
|
--popover-foreground: 120 100% 63%;
|
||||||
|
--primary: 120 100% 63%; /* #00ff41 matrix green */
|
||||||
|
--primary-foreground: 120 38% 3%;
|
||||||
|
--secondary: 120 100% 50%; /* #00cc33 */
|
||||||
|
--secondary-foreground: 120 100% 63%;
|
||||||
|
--muted: 120 82% 10%; /* #003311 */
|
||||||
|
--muted-foreground: 120 100% 50%;
|
||||||
|
--accent: 120 100% 63%; /* #00ff41 */
|
||||||
|
--accent-foreground: 120 38% 3%;
|
||||||
|
--destructive: 348 100% 50%; /* #ff0040 neon red */
|
||||||
|
--destructive-foreground: 120 100% 63%;
|
||||||
|
--border: 120 82% 10%; /* #003311 inactive */
|
||||||
|
--input: 120 82% 10%;
|
||||||
|
--ring: 120 100% 63%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -46,11 +71,87 @@
|
||||||
width: 8px;
|
width: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Light mode scrollbar */
|
||||||
::-webkit-scrollbar-track {
|
::-webkit-scrollbar-track {
|
||||||
@apply bg-muted;
|
background: hsl(var(--muted));
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
::-webkit-scrollbar-thumb {
|
||||||
@apply bg-border hover:bg-muted-foreground/30 rounded;
|
background: hsl(var(--border));
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: hsl(var(--muted-foreground) / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode scrollbar */
|
||||||
|
.dark ::-webkit-scrollbar-track {
|
||||||
|
background: hsl(var(--muted));
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-thumb {
|
||||||
|
background: hsl(var(--primary));
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: hsl(var(--secondary));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* CRT effects - dark mode only */
|
||||||
|
@layer utilities {
|
||||||
|
.dark .crt-scanlines {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .crt-scanlines::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(
|
||||||
|
rgba(0, 0, 0, 0) 50%,
|
||||||
|
rgba(0, 0, 0, 0.05) 50%
|
||||||
|
);
|
||||||
|
background-size: 100% 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .neon-glow {
|
||||||
|
text-shadow: 0 0 10px hsl(var(--primary)),
|
||||||
|
0 0 20px hsl(var(--primary) / 0.5),
|
||||||
|
0 0 30px hsl(var(--primary) / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .neon-glow-box {
|
||||||
|
box-shadow: 0 0 10px hsl(var(--primary)),
|
||||||
|
0 0 20px hsl(var(--primary) / 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .neon-glow-box:hover {
|
||||||
|
box-shadow: 0 0 15px hsl(var(--primary)),
|
||||||
|
0 0 30px hsl(var(--primary) / 0.6),
|
||||||
|
0 0 45px hsl(var(--primary) / 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0%, 100% {
|
||||||
|
box-shadow: 0 0 10px hsl(var(--primary)),
|
||||||
|
0 0 20px hsl(var(--primary) / 0.5);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
box-shadow: 0 0 20px hsl(var(--primary)),
|
||||||
|
0 0 40px hsl(var(--primary) / 0.7),
|
||||||
|
0 0 60px hsl(var(--primary) / 0.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .pulse-glow {
|
||||||
|
animation: pulse-glow 2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
|
import { ThemeProvider } from "@/components/ThemeProvider";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Candle Annotator",
|
title: "Candle Annotator",
|
||||||
|
|
@ -12,14 +13,22 @@ export default function RootLayout({
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en" suppressHydrationWarning>
|
||||||
<head>
|
<head>
|
||||||
<link
|
<link
|
||||||
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap"
|
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
</head>
|
</head>
|
||||||
<body className="antialiased">{children}</body>
|
<body className="antialiased">
|
||||||
|
<ThemeProvider
|
||||||
|
attribute="class"
|
||||||
|
defaultTheme="system"
|
||||||
|
enableSystem
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { useEffect, useRef, useState, useImperativeHandle, forwardRef } from 'react';
|
import { useEffect, useRef, useState, useImperativeHandle, forwardRef } from 'react';
|
||||||
import { createChart, IChartApi, ISeriesApi, CandlestickData, Time } from 'lightweight-charts';
|
import { createChart, IChartApi, ISeriesApi, CandlestickData, Time } from 'lightweight-charts';
|
||||||
|
import { useTheme } from 'next-themes';
|
||||||
import SvgOverlay from './SvgOverlay';
|
import SvgOverlay from './SvgOverlay';
|
||||||
|
|
||||||
interface Candle {
|
interface Candle {
|
||||||
|
|
@ -56,6 +57,13 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
|
||||||
const [annotations, setAnnotations] = useState<Annotation[]>([]);
|
const [annotations, setAnnotations] = useState<Annotation[]>([]);
|
||||||
const [annotationTypes, setAnnotationTypes] = useState<AnnotationType[]>([]);
|
const [annotationTypes, setAnnotationTypes] = useState<AnnotationType[]>([]);
|
||||||
const [isEmpty, setIsEmpty] = useState(true);
|
const [isEmpty, setIsEmpty] = useState(true);
|
||||||
|
const { theme, resolvedTheme } = useTheme();
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
// Track mounted state to avoid hydration mismatch
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Fetch candles from API
|
// Fetch candles from API
|
||||||
const fetchCandles = async () => {
|
const fetchCandles = async () => {
|
||||||
|
|
@ -106,31 +114,74 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Get theme-specific colors
|
||||||
|
const getChartColors = () => {
|
||||||
|
const isDark = resolvedTheme === 'dark';
|
||||||
|
|
||||||
|
if (isDark) {
|
||||||
|
return {
|
||||||
|
layout: {
|
||||||
|
background: { color: '#000000' },
|
||||||
|
textColor: '#00ff41',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
vertLines: { color: '#003311' },
|
||||||
|
horzLines: { color: '#003311' },
|
||||||
|
},
|
||||||
|
timeScale: {
|
||||||
|
borderColor: '#003311',
|
||||||
|
},
|
||||||
|
rightPriceScale: {
|
||||||
|
borderColor: '#003311',
|
||||||
|
},
|
||||||
|
crosshair: {
|
||||||
|
color: '#00ff41',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return {
|
||||||
|
layout: {
|
||||||
|
background: { color: '#ffffff' },
|
||||||
|
textColor: '#1a1a2e',
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
vertLines: { color: '#d1d5db' },
|
||||||
|
horzLines: { color: '#d1d5db' },
|
||||||
|
},
|
||||||
|
timeScale: {
|
||||||
|
borderColor: '#d1d5db',
|
||||||
|
},
|
||||||
|
rightPriceScale: {
|
||||||
|
borderColor: '#d1d5db',
|
||||||
|
},
|
||||||
|
crosshair: {
|
||||||
|
color: '#16a34a',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Initialize chart
|
// Initialize chart
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!chartContainerRef.current || isEmpty) return;
|
if (!chartContainerRef.current || isEmpty || !mounted) return;
|
||||||
|
|
||||||
|
const colors = getChartColors();
|
||||||
|
|
||||||
const chart = createChart(chartContainerRef.current, {
|
const chart = createChart(chartContainerRef.current, {
|
||||||
width: chartContainerRef.current.clientWidth,
|
width: chartContainerRef.current.clientWidth,
|
||||||
height: chartContainerRef.current.clientHeight,
|
height: chartContainerRef.current.clientHeight,
|
||||||
layout: {
|
layout: colors.layout,
|
||||||
background: { color: '#ffffff' },
|
grid: colors.grid,
|
||||||
textColor: '#1e293b',
|
|
||||||
},
|
|
||||||
grid: {
|
|
||||||
vertLines: { color: '#f1f5f9' },
|
|
||||||
horzLines: { color: '#f1f5f9' },
|
|
||||||
},
|
|
||||||
crosshair: {
|
crosshair: {
|
||||||
mode: 1,
|
mode: 1,
|
||||||
},
|
},
|
||||||
timeScale: {
|
timeScale: {
|
||||||
timeVisible: true,
|
timeVisible: true,
|
||||||
secondsVisible: false,
|
secondsVisible: false,
|
||||||
borderColor: '#e2e8f0',
|
borderColor: colors.timeScale.borderColor,
|
||||||
},
|
},
|
||||||
rightPriceScale: {
|
rightPriceScale: {
|
||||||
borderColor: '#e2e8f0',
|
borderColor: colors.rightPriceScale.borderColor,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -160,7 +211,7 @@ const CandleChart = forwardRef<CandleChartHandle, CandleChartProps>(
|
||||||
resizeObserver.disconnect();
|
resizeObserver.disconnect();
|
||||||
chart.remove();
|
chart.remove();
|
||||||
};
|
};
|
||||||
}, [isEmpty]);
|
}, [isEmpty, mounted, resolvedTheme]);
|
||||||
|
|
||||||
// Load candle data into chart
|
// Load candle data into chart
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
8
src/components/ThemeProvider.tsx
Normal file
8
src/components/ThemeProvider.tsx
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||||
|
import { type ThemeProviderProps } from "next-themes/dist/types";
|
||||||
|
|
||||||
|
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
|
||||||
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
|
}
|
||||||
64
src/components/ThemeToggle.tsx
Normal file
64
src/components/ThemeToggle.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { Monitor, Sun, Moon } from "lucide-react";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
const { theme, setTheme } = useTheme();
|
||||||
|
const [mounted, setMounted] = useState(false);
|
||||||
|
|
||||||
|
// Avoid hydration mismatch by only rendering after mount
|
||||||
|
useEffect(() => {
|
||||||
|
setMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
if (!mounted) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
className="p-3 rounded-lg border border-border bg-card hover:bg-muted transition-colors"
|
||||||
|
disabled
|
||||||
|
>
|
||||||
|
<Monitor className="h-5 w-5 text-muted-foreground" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cycleTheme = () => {
|
||||||
|
if (theme === "system") {
|
||||||
|
setTheme("light");
|
||||||
|
} else if (theme === "light") {
|
||||||
|
setTheme("dark");
|
||||||
|
} else {
|
||||||
|
setTheme("system");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const Icon = theme === "light" ? Sun : theme === "dark" ? Moon : Monitor;
|
||||||
|
const themeName = theme === "light" ? "Light" : theme === "dark" ? "Dark" : "System";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<button
|
||||||
|
onClick={cycleTheme}
|
||||||
|
className="p-3 rounded-lg border border-border bg-card hover:bg-muted transition-colors"
|
||||||
|
aria-label={`Current theme: ${themeName}. Click to cycle.`}
|
||||||
|
>
|
||||||
|
<Icon className="h-5 w-5 text-foreground" />
|
||||||
|
</button>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="right">
|
||||||
|
<p>Theme: {themeName}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@ import { useState, useEffect } from 'react';
|
||||||
import { ArrowUpCircle, ArrowDownCircle, TrendingUp, Trash2, Download, ChevronDown, ChevronUp } from 'lucide-react';
|
import { ArrowUpCircle, ArrowDownCircle, TrendingUp, Trash2, Download, ChevronDown, ChevronUp } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { ThemeToggle } from '@/components/ThemeToggle';
|
||||||
|
|
||||||
export type Tool = string | 'delete' | null;
|
export type Tool = string | 'delete' | null;
|
||||||
|
|
||||||
|
|
@ -330,6 +331,11 @@ export default function Toolbox({
|
||||||
Export CSV
|
Export CSV
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Theme toggle */}
|
||||||
|
<div className="pt-2">
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue