From 908e7f054a2a8aad84ec6cdb0e18bc17eb0cbb73 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Jun 2025 02:59:50 +0000 Subject: [PATCH 1/4] Initial plan for issue From e0b77c181e7a19410a69e596964135d4b4096187 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Jun 2025 03:15:19 +0000 Subject: [PATCH 2/4] Implement VS Code-inspired dark theme with complete system Co-authored-by: unbug <799578+unbug@users.noreply.github.com> --- __static/app/resources/css/prettify.css | 64 ++++++- docs/DARK_THEME.md | 216 ++++++++++++++++++++++++ src/App.js | 5 +- src/components/ThemeToggle.js | 50 ++++++ src/containers/NavBarContainer.js | 2 + src/hooks/useTheme.js | 112 ++++++++++++ styles/_common.scss | 8 +- styles/_constants.scss | 45 ++++- styles/_donate.scss | 4 +- styles/_main-container.scss | 34 ++-- styles/_nav-bar-container.scss | 28 ++- styles/_notice-container.scss | 14 +- styles/_semantic-ui-overrides.scss | 164 ++++++++++++++++++ styles/_source-code.scss | 19 ++- styles/_theme-variables.scss | 54 ++++++ styles/_utils.scss | 6 +- styles/app.scss | 2 + 17 files changed, 786 insertions(+), 41 deletions(-) create mode 100644 docs/DARK_THEME.md create mode 100644 src/components/ThemeToggle.js create mode 100644 src/hooks/useTheme.js create mode 100644 styles/_semantic-ui-overrides.scss create mode 100644 styles/_theme-variables.scss diff --git a/__static/app/resources/css/prettify.css b/__static/app/resources/css/prettify.css index 9c2959af..29848dec 100644 --- a/__static/app/resources/css/prettify.css +++ b/__static/app/resources/css/prettify.css @@ -2,6 +2,11 @@ color: #000 } +/* Dark theme code highlighting */ +body.dark .pln { + color: #cccccc; +} + @media screen { .str { color: #080 @@ -27,6 +32,31 @@ color: #660 } + /* Dark theme syntax highlighting */ + body.dark .str { + color: #ce9178; /* VS Code string color */ + } + + body.dark .kwd { + color: #569cd6; /* VS Code keyword color */ + } + + body.dark .com { + color: #6a9955; /* VS Code comment color */ + } + + body.dark .typ { + color: #4ec9b0; /* VS Code type color */ + } + + body.dark .lit { + color: #b5cea8; /* VS Code literal color */ + } + + body.dark .pun, body.dark .opn, body.dark .clo { + color: #cccccc; /* VS Code punctuation color */ + } + .tag { color: #008 } @@ -46,6 +76,27 @@ .fun { color: red } + + /* Dark theme additional syntax highlighting */ + body.dark .tag { + color: #569cd6; /* VS Code tag color */ + } + + body.dark .atn { + color: #9cdcfe; /* VS Code attribute name color */ + } + + body.dark .atv { + color: #ce9178; /* VS Code attribute value color */ + } + + body.dark .dec, body.dark .var { + color: #9cdcfe; /* VS Code declaration/variable color */ + } + + body.dark .fun { + color: #dcdcaa; /* VS Code function color */ + } } @media print, projection { @@ -92,7 +143,14 @@ pre.prettyprint { padding: 2px; - border: 1px solid #888 + border: 1px solid #888; + background: #f8f8f8; +} + +body.dark pre.prettyprint { + background: #1e1e1e; + border: 1px solid #3e3e3e; + color: #cccccc; } ol.linenums { @@ -107,3 +165,7 @@ li.L0, li.L1, li.L2, li.L3, li.L5, li.L6, li.L7, li.L8 { li.L1, li.L3, li.L5, li.L7, li.L9 { background: #eee } + +body.dark li.L1, body.dark li.L3, body.dark li.L5, body.dark li.L7, body.dark li.L9 { + background: #2d2d30; +} diff --git a/docs/DARK_THEME.md b/docs/DARK_THEME.md new file mode 100644 index 00000000..77246c0c --- /dev/null +++ b/docs/DARK_THEME.md @@ -0,0 +1,216 @@ +# Dark Theme Implementation for Codelf + +## Overview + +This document describes the implementation of a VS Code-inspired dark theme for the Codelf project. The implementation includes a complete theme system with automatic system theme detection, manual theme switching, and comprehensive styling for all UI components. + +## Features + +### Theme System +- **Auto Detection**: Automatically detects user's system theme preference (`prefers-color-scheme`) +- **Manual Override**: Users can manually switch between light, dark, and auto modes +- **Persistence**: Theme preference is saved in localStorage and persists across sessions +- **Smooth Transitions**: All theme switches include smooth CSS transitions + +### Theme Options +1. **Light Theme**: Clean, bright interface (default) +2. **Dark Theme**: VS Code-inspired dark interface with proper contrast +3. **Auto Theme**: Follows system preference and responds to system theme changes + +## Implementation Details + +### File Structure + +``` +styles/ +├── _constants.scss # VS Code color constants +├── _theme-variables.scss # CSS custom properties for theming +├── _semantic-ui-overrides.scss # Dark theme overrides for Semantic UI +└── app.scss # Main stylesheet importing all theme files + +src/ +├── hooks/ +│ └── useTheme.js # React theme management hook +├── components/ +│ └── ThemeToggle.js # Theme toggle dropdown component +└── App.js # Updated with ThemeProvider +``` + +### Color Scheme + +The dark theme uses VS Code's color palette: + +#### Background Colors +- **Primary Background**: `#1e1e1e` (editor background) +- **Secondary Background**: `#252526` (sidebar background) +- **Input Background**: `#3c3c3c` (input fields) +- **Hover Background**: `#2a2d2e` (hover states) + +#### Text Colors +- **Primary Text**: `#cccccc` (main text) +- **Secondary Text**: `#969696` (muted text) +- **Active Text**: `#ffffff` (highlighted text) + +#### Accent Colors +- **Primary Accent**: `#007acc` (links, buttons) +- **Focus Accent**: `#0e639c` (focus states) +- **Selection**: `#264f78` (selections) + +### CSS Variables System + +The theme system uses CSS custom properties that change based on the `body.dark` class: + +```scss +:root { + --bg-primary: #ffffff; /* Light theme */ + --text-primary: #373a3c; + // ... other variables +} + +body.dark { + --bg-primary: #1e1e1e; /* Dark theme */ + --text-primary: #cccccc; + // ... other variables +} +``` + +### React Theme Hook + +The `useTheme` hook provides: + +```javascript +const { + theme, // Current theme preference ('light'|'dark'|'auto') + actualTheme, // Resolved theme ('light'|'dark') + setTheme, // Function to set theme preference + toggleTheme, // Function to toggle between light/dark + isDark, // Boolean indicating if dark theme is active + isAuto // Boolean indicating if auto mode is enabled +} = useTheme(); +``` + +### Theme Toggle Component + +The `ThemeToggle` component provides a dropdown with three options: +- ☀️ Light +- 🌙 Dark +- ⚪ Auto + +## Usage + +### For Developers + +#### Using the Theme Hook +```javascript +import { useTheme } from '../hooks/useTheme'; + +function MyComponent() { + const { isDark, toggleTheme } = useTheme(); + + return ( +
+ +
+ ); +} +``` + +#### Using CSS Variables +```scss +.my-component { + background: var(--bg-primary); + color: var(--text-primary); + border: 1px solid var(--border-primary); + + &:hover { + background: var(--bg-hover); + } +} +``` + +### Extending the Theme + +#### Adding New CSS Variables +1. Add the variable to both light and dark sections in `_theme-variables.scss` +2. Use the variable in your component styles + +#### Adding New Colors +1. Define the color in `_constants.scss` +2. Add it to the CSS variables system +3. Use it in your components + +#### Custom Component Theming +For custom components, follow this pattern: + +```scss +.my-custom-component { + background: var(--bg-primary); + color: var(--text-primary); + + .my-element { + border-color: var(--border-primary); + + &:hover { + background: var(--bg-hover); + } + } +} +``` + +## Code Syntax Highlighting + +The dark theme includes VS Code-inspired syntax highlighting for code blocks: + +- **Strings**: `#ce9178` (light orange) +- **Keywords**: `#569cd6` (light blue) +- **Comments**: `#6a9955` (green) +- **Types**: `#4ec9b0` (cyan) +- **Functions**: `#dcdcaa` (yellow) + +## Testing + +To test the theme implementation: + +1. **Manual Testing**: + - Click the theme toggle in the top-right corner + - Test all three modes (Light, Dark, Auto) + - Verify smooth transitions + - Check all UI components in both themes + +2. **System Theme Testing**: + - Set theme to "Auto" + - Change your OS theme preference + - Verify the app follows system changes + +3. **Persistence Testing**: + - Change theme preference + - Refresh the page + - Verify theme preference is maintained + +## Browser Support + +The theme system supports: +- Modern browsers with CSS custom properties support +- `prefers-color-scheme` media query support +- localStorage for theme persistence + +## Future Enhancements + +Potential improvements for the theme system: +1. Additional theme variants (high contrast, custom themes) +2. Theme editor for custom color schemes +3. Keyboard shortcuts for theme switching +4. More granular component-level theming options +5. Theme preview without applying changes + +## Migration Notes + +### From Legacy Dark Theme +The old dark theme was a simple `body.dark` class with hardcoded colors. The new system: +- Maintains backward compatibility +- Provides more comprehensive coverage +- Uses CSS variables for easier maintenance +- Includes proper VS Code color scheme + +### Breaking Changes +None - the new system is fully backward compatible with existing dark theme usage. \ No newline at end of file diff --git a/src/App.js b/src/App.js index 3d9cb4f3..21fbc0f8 100644 --- a/src/App.js +++ b/src/App.js @@ -4,15 +4,16 @@ import MainContainer from './containers/MainContainer'; // import CopybookContainer from './containers/CopybookContainer'; import NoticeContainer from './containers/NoticeContainer'; import NavBarContainer from './containers/NavBarContainer'; +import { ThemeProvider } from './hooks/useTheme'; function App() { return ( - <> + {/* */} - + ); } diff --git a/src/components/ThemeToggle.js b/src/components/ThemeToggle.js new file mode 100644 index 00000000..68803cc0 --- /dev/null +++ b/src/components/ThemeToggle.js @@ -0,0 +1,50 @@ +import React from 'react'; +import { Icon, Dropdown } from 'semantic-ui-react'; +import { useTheme, THEMES } from '../hooks/useTheme'; + +const ThemeToggle = () => { + const { theme, setTheme, isDark, isAuto } = useTheme(); + + const themeOptions = [ + { + key: THEMES.LIGHT, + text: 'Light', + value: THEMES.LIGHT, + icon: 'sun' + }, + { + key: THEMES.DARK, + text: 'Dark', + value: THEMES.DARK, + icon: 'moon' + }, + { + key: THEMES.AUTO, + text: 'Auto', + value: THEMES.AUTO, + icon: 'circle outline' + } + ]; + + const getCurrentIcon = () => { + if (isAuto) return 'circle outline'; + return isDark ? 'moon' : 'sun'; + }; + + return ( + + + + } + options={themeOptions} + value={theme} + onChange={(e, { value }) => setTheme(value)} + direction="left" + pointing="top right" + /> + ); +}; + +export default ThemeToggle; \ No newline at end of file diff --git a/src/containers/NavBarContainer.js b/src/containers/NavBarContainer.js index b06d9499..cab2bf61 100644 --- a/src/containers/NavBarContainer.js +++ b/src/containers/NavBarContainer.js @@ -1,5 +1,6 @@ import React from 'react'; import { Container, Icon, Popup } from 'semantic-ui-react'; +import ThemeToggle from '../components/ThemeToggle'; // import CopybookModel from '../models/CopybookModel'; export default function NavBarContainer() { @@ -29,6 +30,7 @@ export default function NavBarContainer() { Sorry, GitHub stars organize tool currently is not available, new version is coming soon :) */} + diff --git a/src/hooks/useTheme.js b/src/hooks/useTheme.js new file mode 100644 index 00000000..bcb2b504 --- /dev/null +++ b/src/hooks/useTheme.js @@ -0,0 +1,112 @@ +import React, { createContext, useContext, useEffect, useState } from 'react'; + +// Theme types +export const THEMES = { + LIGHT: 'light', + DARK: 'dark', + AUTO: 'auto' +}; + +const ThemeContext = createContext(); + +// Theme detection utilities +const getSystemTheme = () => { + if (typeof window !== 'undefined' && window.matchMedia) { + return window.matchMedia('(prefers-color-scheme: dark)').matches ? THEMES.DARK : THEMES.LIGHT; + } + return THEMES.LIGHT; +}; + +const getStoredTheme = () => { + if (typeof window !== 'undefined' && window.localStorage) { + const stored = localStorage.getItem('codelf-theme'); + return stored && Object.values(THEMES).includes(stored) ? stored : THEMES.AUTO; + } + return THEMES.AUTO; +}; + +const setStoredTheme = (theme) => { + if (typeof window !== 'undefined' && window.localStorage) { + localStorage.setItem('codelf-theme', theme); + } +}; + +const applyTheme = (theme) => { + if (typeof document !== 'undefined') { + const isDark = theme === THEMES.DARK; + document.body.classList.toggle('dark', isDark); + } +}; + +export function ThemeProvider({ children }) { + const [themePreference, setThemePreference] = useState(THEMES.AUTO); + const [actualTheme, setActualTheme] = useState(THEMES.LIGHT); + + useEffect(() => { + // Initialize theme from storage + const storedTheme = getStoredTheme(); + setThemePreference(storedTheme); + + const systemTheme = getSystemTheme(); + const currentTheme = storedTheme === THEMES.AUTO ? systemTheme : storedTheme; + setActualTheme(currentTheme); + applyTheme(currentTheme); + }, []); + + useEffect(() => { + // Listen for system theme changes + if (typeof window !== 'undefined' && window.matchMedia) { + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + const handleChange = (e) => { + if (themePreference === THEMES.AUTO) { + const newTheme = e.matches ? THEMES.DARK : THEMES.LIGHT; + setActualTheme(newTheme); + applyTheme(newTheme); + } + }; + + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + } + }, [themePreference]); + + const setTheme = (newTheme) => { + setThemePreference(newTheme); + setStoredTheme(newTheme); + + const systemTheme = getSystemTheme(); + const actualNewTheme = newTheme === THEMES.AUTO ? systemTheme : newTheme; + setActualTheme(actualNewTheme); + applyTheme(actualNewTheme); + }; + + const toggleTheme = () => { + const currentTheme = themePreference === THEMES.AUTO ? getSystemTheme() : themePreference; + const newTheme = currentTheme === THEMES.DARK ? THEMES.LIGHT : THEMES.DARK; + setTheme(newTheme); + }; + + const value = { + theme: themePreference, + actualTheme, + setTheme, + toggleTheme, + isDark: actualTheme === THEMES.DARK, + isAuto: themePreference === THEMES.AUTO + }; + + return ( + + {children} + + ); +} + +export function useTheme() { + const context = useContext(ThemeContext); + if (!context) { + throw new Error('useTheme must be used within a ThemeProvider'); + } + return context; +} \ No newline at end of file diff --git a/styles/_common.scss b/styles/_common.scss index 2a6a51db..5c241226 100644 --- a/styles/_common.scss +++ b/styles/_common.scss @@ -13,13 +13,9 @@ html, body { } body { - background: #fff; - color: #373a3c; + background: var(--bg-primary); + color: var(--text-primary); transition: all 350ms $anim-cubic-bezier; - &.dark { - background: $dark; - color: #fff; - } } /* diff --git a/styles/_constants.scss b/styles/_constants.scss index 7b6be2cb..bdb56af3 100644 --- a/styles/_constants.scss +++ b/styles/_constants.scss @@ -2,8 +2,49 @@ $anim-cubic-bezier: cubic-bezier(0.4, 0, 0.2, 1); $anim-default-time: 0.6s; -/* color */ +/* VS Code Dark Theme Colors */ +// Background colors +$vscode-dark-bg: #1e1e1e; +$vscode-dark-sidebar: #252526; +$vscode-dark-input-bg: #3c3c3c; +$vscode-dark-hover: #2a2d2e; +$vscode-dark-border: #3e3e3e; + +// Text colors +$vscode-dark-text: #cccccc; +$vscode-dark-text-muted: #969696; +$vscode-dark-text-active: #ffffff; + +// Accent colors +$vscode-dark-blue: #007acc; +$vscode-dark-focus: #0e639c; +$vscode-dark-selection: #264f78; + +// Status colors +$vscode-dark-error: #f48771; +$vscode-dark-warning: #ddb76d; +$vscode-dark-success: #89d185; + +/* Light Theme Colors */ +// Background colors +$vscode-light-bg: #ffffff; +$vscode-light-sidebar: #f3f3f3; +$vscode-light-input-bg: #ffffff; +$vscode-light-hover: #e8e8e8; +$vscode-light-border: #d4d4d4; + +// Text colors +$vscode-light-text: #373a3c; +$vscode-light-text-muted: #6c6c6c; +$vscode-light-text-active: #000000; + +// Accent colors +$vscode-light-blue: #0066cc; +$vscode-light-focus: #005a9e; +$vscode-light-selection: #add6ff; + +/* Legacy colors (keeping for compatibility) */ $box-shadow: rgb(187,187,187) 0px 2px 8px !important; -$dark: #272b38; +$dark: $vscode-dark-bg; // Updated to use VS Code dark color /* dimensions */ diff --git a/styles/_donate.scss b/styles/_donate.scss index 95506e4b..ca6b499b 100755 --- a/styles/_donate.scss +++ b/styles/_donate.scss @@ -9,6 +9,7 @@ .hd { padding-bottom: .8rem; text-align: center; + color: var(--text-primary); } .bd { @@ -22,7 +23,8 @@ .paypal { margin: 0 0.5rem; width: 5rem; - background: #fff; + background: var(--bg-primary); border-radius: 0.1rem; + border: 1px solid var(--border-primary); } } diff --git a/styles/_main-container.scss b/styles/_main-container.scss index ab04b478..1af98d32 100644 --- a/styles/_main-container.scss +++ b/styles/_main-container.scss @@ -1,10 +1,10 @@ .main-container { - $dark-theme: 'body.dark &'; position: relative; display: flex; align-items: center; flex-direction: column; padding-bottom: 3rem; + .title { text-align: center; margin-top: 5rem; @@ -110,18 +110,23 @@ } &__input { - background: #F1F3F4; + background: var(--bg-input); border-radius: 5rem; transition: box-shadow 350ms $anim-cubic-bezier; &:focus, &:active, &:hover { - box-shadow: 0 3px 8px 0 rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(0, 0, 0, 0.08); + box-shadow: 0 3px 8px 0 var(--shadow-focus); } input { - background: #F1F3F4; + background: var(--bg-input); border-radius: 5rem; border: 0 !important; + color: var(--text-primary); + + &::placeholder { + color: var(--text-secondary); + } } } @@ -162,16 +167,6 @@ } } - #{$dark-theme} { - .search-bar { - &__input { - &:focus, &:active, &:hover { - box-shadow: $box-shadow; - } - } - } - } - .suggestion { padding-bottom: 2rem; max-width: 100%; @@ -179,8 +174,10 @@ min-width: 43rem; } a { + color: var(--accent-primary); &:hover { text-decoration: underline; + color: var(--accent-focus); } } } @@ -192,8 +189,13 @@ .label { @extend .pointer-link; margin: 0.4rem 0.4rem 0 0; + background-color: var(--bg-hover); + color: var(--text-primary); + border: 1px solid var(--border-primary); + &:hover { - box-shadow: $box-shadow; + box-shadow: 0 2px 8px var(--shadow-primary); + background-color: var(--accent-selection); } &.animated { transform: translateZ(0); @@ -203,7 +205,7 @@ hr { width: 70%; - border-color: rgba(0, 0, 0, .16); + border-color: var(--border-primary); margin-top: 1rem; } } diff --git a/styles/_nav-bar-container.scss b/styles/_nav-bar-container.scss index e1d6fcc4..e8ce776d 100755 --- a/styles/_nav-bar-container.scss +++ b/styles/_nav-bar-container.scss @@ -23,10 +23,36 @@ margin: 0 !important; font-size: 2rem; color: #70B7FD; + transition: color 0.2s ease; &:hover,&:active{ color: #59AAF9; } } + + // Theme toggle button styles + .theme-toggle-btn { + display: inline-block; + cursor: pointer; + margin-right: 0.4rem; + i { + color: var(--text-secondary); + transition: color 0.2s ease; + &:hover { + color: var(--accent-primary); + } + } + } + + // Dark theme icon colors + body.dark & { + i { + color: var(--text-secondary); + &:hover, &:active { + color: var(--accent-primary); + } + } + } + .github-corner { margin-top: -0.2rem; i { @@ -38,7 +64,7 @@ } $anim-delay: 150ms; $anim-duration: 500ms; - @for $i from 1 through 5 { + @for $i from 1 through 6 { // Updated to 6 to include theme toggle >*:nth-child(#{$i}) { animation-duration: $anim-duration; animation-delay: 500 + $anim-delay*($i - 1); diff --git a/styles/_notice-container.scss b/styles/_notice-container.scss index e53f45fb..94bb1ed3 100755 --- a/styles/_notice-container.scss +++ b/styles/_notice-container.scss @@ -1,5 +1,4 @@ .notice-container { - $dark-theme: 'body.dark &'; position: fixed; bottom: 0; left: 0; @@ -7,18 +6,17 @@ padding-bottom: 1rem; padding-top: 0.5rem; text-align: center; - background-color: #fff; + background-color: var(--bg-primary); transition: background-color 150ms ease-in-out; - @at-root { - #{$dark-theme} { - background-color: $dark; - } - } + a { display: none; - color: #70B7FD; + color: var(--accent-primary); &.show { display: block; } + &:hover { + color: var(--accent-focus); + } } } diff --git a/styles/_semantic-ui-overrides.scss b/styles/_semantic-ui-overrides.scss new file mode 100644 index 00000000..308f0c14 --- /dev/null +++ b/styles/_semantic-ui-overrides.scss @@ -0,0 +1,164 @@ +/* Semantic UI Dark Theme Overrides */ + +/* Dropdown menus */ +body.dark .ui.dropdown .menu { + background: var(--bg-primary); + border: 1px solid var(--border-primary); + box-shadow: 0 2px 8px var(--shadow-primary); +} + +body.dark .ui.dropdown .menu > .item { + background: var(--bg-primary); + color: var(--text-primary); + border-top: 1px solid var(--border-primary); +} + +body.dark .ui.dropdown .menu > .item:hover { + background: var(--bg-hover); + color: var(--text-active); +} + +body.dark .ui.dropdown .menu > .item.selected { + background: var(--accent-selection); + color: var(--text-active); +} + +/* Modals */ +body.dark .ui.modal { + background: var(--bg-primary); + border: 1px solid var(--border-primary); + box-shadow: 0 8px 32px var(--shadow-primary); +} + +body.dark .ui.modal > .header { + background: var(--bg-secondary); + color: var(--text-active); + border-bottom: 1px solid var(--border-primary); +} + +body.dark .ui.modal > .content { + background: var(--bg-primary); + color: var(--text-primary); +} + +/* Buttons */ +body.dark .ui.button { + background: var(--bg-hover); + color: var(--text-primary); + border: 1px solid var(--border-primary); +} + +body.dark .ui.button:hover { + background: var(--accent-selection); + color: var(--text-active); +} + +body.dark .ui.primary.button { + background: var(--accent-primary); + color: white; +} + +body.dark .ui.primary.button:hover { + background: var(--accent-focus); +} + +/* Input fields */ +body.dark .ui.input input { + background: var(--bg-input); + color: var(--text-primary); + border: 1px solid var(--border-primary); +} + +body.dark .ui.input input:focus { + border-color: var(--accent-primary); + box-shadow: 0 0 0 2px var(--accent-selection); +} + +/* Labels */ +body.dark .ui.label { + background: var(--bg-hover); + color: var(--text-primary); + border: 1px solid var(--border-primary); +} + +/* Popups */ +body.dark .ui.popup { + background: var(--bg-primary); + border: 1px solid var(--border-primary); + color: var(--text-primary); + box-shadow: 0 4px 16px var(--shadow-primary); +} + +body.dark .ui.popup:before { + background: var(--bg-primary); + border: 1px solid var(--border-primary); +} + +/* Form elements */ +body.dark .ui.form .field > label { + color: var(--text-primary); +} + +body.dark .ui.form textarea, +body.dark .ui.form input[type="text"], +body.dark .ui.form input[type="email"], +body.dark .ui.form input[type="search"], +body.dark .ui.form input[type="password"] { + background: var(--bg-input); + color: var(--text-primary); + border: 1px solid var(--border-primary); +} + +body.dark .ui.form textarea:focus, +body.dark .ui.form input:focus { + border-color: var(--accent-primary); + box-shadow: 0 0 0 2px var(--accent-selection); +} + +/* Menu */ +body.dark .ui.menu { + background: var(--bg-secondary); + border: 1px solid var(--border-primary); +} + +body.dark .ui.menu .item { + color: var(--text-primary); +} + +body.dark .ui.menu .item:hover { + background: var(--bg-hover); + color: var(--text-active); +} + +/* Checkbox */ +body.dark .ui.checkbox input:checked ~ label:before { + background: var(--accent-primary); + border-color: var(--accent-primary); +} + +body.dark .ui.checkbox label { + color: var(--text-primary); +} + +/* Segment */ +body.dark .ui.segment { + background: var(--bg-secondary); + border: 1px solid var(--border-primary); + color: var(--text-primary); +} + +/* Dimmer */ +body.dark .ui.dimmer { + background-color: rgba(0, 0, 0, 0.85); +} + +/* Message */ +body.dark .ui.message { + background: var(--bg-secondary); + color: var(--text-primary); + border: 1px solid var(--border-primary); +} + +body.dark .ui.message .header { + color: var(--text-active); +} \ No newline at end of file diff --git a/styles/_source-code.scss b/styles/_source-code.scss index f2e51880..9147cee2 100755 --- a/styles/_source-code.scss +++ b/styles/_source-code.scss @@ -1,21 +1,38 @@ @at-root { .source-code { + background: var(--bg-primary); + color: var(--text-primary); + &#{&}> .header { padding: 0.8rem 1rem; + background: var(--bg-secondary); + border-bottom: 1px solid var(--border-primary); } > .content { + background: var(--bg-primary); + pre { display: inline-block; margin: 0; padding: 0; + background: var(--bg-input); + color: var(--text-primary); } code { display: inline-block; - color: #000; + color: var(--text-primary); + background: var(--bg-input); } } &#{&} .ui.dropdown .menu>.item { padding: 0.4rem 0 0 0.4rem !important; + background: var(--bg-primary); + color: var(--text-primary); + + &:hover { + background: var(--bg-hover); + } + &:last-child { padding-bottom: 0.4rem !important; } diff --git a/styles/_theme-variables.scss b/styles/_theme-variables.scss new file mode 100644 index 00000000..5cbd8a08 --- /dev/null +++ b/styles/_theme-variables.scss @@ -0,0 +1,54 @@ +/* CSS Custom Properties for Theme System */ + +:root { + /* Light Theme (Default) */ + --bg-primary: #{$vscode-light-bg}; + --bg-secondary: #{$vscode-light-sidebar}; + --bg-input: #{$vscode-light-input-bg}; + --bg-hover: #{$vscode-light-hover}; + + --text-primary: #{$vscode-light-text}; + --text-secondary: #{$vscode-light-text-muted}; + --text-active: #{$vscode-light-text-active}; + + --border-primary: #{$vscode-light-border}; + --shadow-primary: rgba(0, 0, 0, 0.1); + --shadow-focus: rgba(0, 102, 204, 0.2); + + --accent-primary: #{$vscode-light-blue}; + --accent-focus: #{$vscode-light-focus}; + --accent-selection: #{$vscode-light-selection}; + + --error-color: #d73a49; + --warning-color: #e36209; + --success-color: #28a745; +} + +body.dark { + /* Dark Theme */ + --bg-primary: #{$vscode-dark-bg}; + --bg-secondary: #{$vscode-dark-sidebar}; + --bg-input: #{$vscode-dark-input-bg}; + --bg-hover: #{$vscode-dark-hover}; + + --text-primary: #{$vscode-dark-text}; + --text-secondary: #{$vscode-dark-text-muted}; + --text-active: #{$vscode-dark-text-active}; + + --border-primary: #{$vscode-dark-border}; + --shadow-primary: rgba(0, 0, 0, 0.3); + --shadow-focus: rgba(14, 99, 156, 0.4); + + --accent-primary: #{$vscode-dark-blue}; + --accent-focus: #{$vscode-dark-focus}; + --accent-selection: #{$vscode-dark-selection}; + + --error-color: #{$vscode-dark-error}; + --warning-color: #{$vscode-dark-warning}; + --success-color: #{$vscode-dark-success}; +} + +/* Theme transition for smooth switching */ +* { + transition: background-color 0.2s ease, color 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease; +} \ No newline at end of file diff --git a/styles/_utils.scss b/styles/_utils.scss index e78bb877..fe7dd7ad 100755 --- a/styles/_utils.scss +++ b/styles/_utils.scss @@ -29,10 +29,10 @@ .hidden-link { &#{&} { - color: #000; + color: var(--text-primary); } &#{&}:hover { - color: #1e70bf; + color: var(--accent-primary); } } @@ -63,7 +63,7 @@ .close.icon { top: 0.5rem; right: 0.5rem; - color: rgba(0,0,0,.87); + color: var(--text-secondary); } &#{&} > .content { height: auto; diff --git a/styles/app.scss b/styles/app.scss index 77b3fcc5..3ac01c1a 100644 --- a/styles/app.scss +++ b/styles/app.scss @@ -1,5 +1,6 @@ @charset "UTF-8"; @import 'constants'; +@import 'theme-variables'; @import 'common'; @import 'utils'; @import "animation"; @@ -7,4 +8,5 @@ position: relative; @import 'components'; @import 'containers'; + @import 'semantic-ui-overrides'; } From ecc2f4563af9140234e4c693aab69a94ccda68d0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Jun 2025 03:17:58 +0000 Subject: [PATCH 3/4] Enhance theme system with better accessibility and error handling Co-authored-by: unbug <799578+unbug@users.noreply.github.com> --- README.md | 2 ++ src/components/ThemeToggle.js | 48 +++++++++++++++++++++++----------- src/hooks/useTheme.js | 31 ++++++++++++++++++---- styles/_nav-bar-container.scss | 41 ++++++++++++++++++++++++----- 4 files changed, 95 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 50c778fb..2888839d 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,8 @@ CODELF(变量命名神器) Also a GitHub stars, repositories tagger and organizer tool. + ✨ **New**: VS Code-inspired dark theme with automatic system theme detection! Toggle between light, dark, and auto modes. + >There are only two hard things in Computer Science: cache invalidation and naming things.-- Phil Karlton > >![twohardtings](https://user-images.githubusercontent.com/799578/50462942-8075fe80-09c3-11e9-9c7f-b38d495b925d.jpg) diff --git a/src/components/ThemeToggle.js b/src/components/ThemeToggle.js index 68803cc0..a8911288 100644 --- a/src/components/ThemeToggle.js +++ b/src/components/ThemeToggle.js @@ -10,19 +10,22 @@ const ThemeToggle = () => { key: THEMES.LIGHT, text: 'Light', value: THEMES.LIGHT, - icon: 'sun' + icon: 'sun', + description: 'Light theme' }, { key: THEMES.DARK, text: 'Dark', value: THEMES.DARK, - icon: 'moon' + icon: 'moon', + description: 'Dark theme' }, { key: THEMES.AUTO, text: 'Auto', value: THEMES.AUTO, - icon: 'circle outline' + icon: 'circle outline', + description: 'Follow system preference' } ]; @@ -31,19 +34,34 @@ const ThemeToggle = () => { return isDark ? 'moon' : 'sun'; }; + const getCurrentLabel = () => { + if (isAuto) return 'Auto'; + return isDark ? 'Dark' : 'Light'; + }; + return ( - - - - } - options={themeOptions} - value={theme} - onChange={(e, { value }) => setTheme(value)} - direction="left" - pointing="top right" - /> +
+ + +
+ } + options={themeOptions} + value={theme} + onChange={(e, { value }) => setTheme(value)} + direction="left" + pointing="top right" + className="theme-dropdown" + aria-label="Select theme" + /> + ); }; diff --git a/src/hooks/useTheme.js b/src/hooks/useTheme.js index bcb2b504..f80e51db 100644 --- a/src/hooks/useTheme.js +++ b/src/hooks/useTheme.js @@ -19,15 +19,24 @@ const getSystemTheme = () => { const getStoredTheme = () => { if (typeof window !== 'undefined' && window.localStorage) { - const stored = localStorage.getItem('codelf-theme'); - return stored && Object.values(THEMES).includes(stored) ? stored : THEMES.AUTO; + try { + const stored = localStorage.getItem('codelf-theme'); + return stored && Object.values(THEMES).includes(stored) ? stored : THEMES.AUTO; + } catch (error) { + console.warn('Failed to read theme from localStorage:', error); + return THEMES.AUTO; + } } return THEMES.AUTO; }; const setStoredTheme = (theme) => { if (typeof window !== 'undefined' && window.localStorage) { - localStorage.setItem('codelf-theme', theme); + try { + localStorage.setItem('codelf-theme', theme); + } catch (error) { + console.warn('Failed to save theme to localStorage:', error); + } } }; @@ -66,12 +75,24 @@ export function ThemeProvider({ children }) { } }; - mediaQuery.addEventListener('change', handleChange); - return () => mediaQuery.removeEventListener('change', handleChange); + // Support both modern and legacy event listener methods + if (mediaQuery.addEventListener) { + mediaQuery.addEventListener('change', handleChange); + return () => mediaQuery.removeEventListener('change', handleChange); + } else if (mediaQuery.addListener) { + // Fallback for older browsers + mediaQuery.addListener(handleChange); + return () => mediaQuery.removeListener(handleChange); + } } }, [themePreference]); const setTheme = (newTheme) => { + if (!Object.values(THEMES).includes(newTheme)) { + console.warn('Invalid theme:', newTheme); + return; + } + setThemePreference(newTheme); setStoredTheme(newTheme); diff --git a/styles/_nav-bar-container.scss b/styles/_nav-bar-container.scss index e8ce776d..3c25badc 100755 --- a/styles/_nav-bar-container.scss +++ b/styles/_nav-bar-container.scss @@ -30,15 +30,42 @@ } // Theme toggle button styles - .theme-toggle-btn { + .theme-toggle-wrapper { display: inline-block; - cursor: pointer; margin-right: 0.4rem; - i { - color: var(--text-secondary); - transition: color 0.2s ease; - &:hover { - color: var(--accent-primary); + + .theme-toggle-btn { + display: inline-block; + cursor: pointer; + i { + color: var(--text-secondary); + transition: color 0.2s ease; + &:hover { + color: var(--accent-primary); + } + } + } + + .theme-dropdown { + .menu { + background: var(--bg-primary); + border: 1px solid var(--border-primary); + box-shadow: 0 4px 16px var(--shadow-primary); + + .item { + color: var(--text-primary); + background: var(--bg-primary); + + &:hover { + background: var(--bg-hover); + color: var(--text-active); + } + + &.selected { + background: var(--accent-selection); + color: var(--text-active); + } + } } } } From d3ef9759d4adecdf1d772c2652fae23ce643d9b7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Jun 2025 04:10:59 +0000 Subject: [PATCH 4/4] Improve dark theme toggle for easier user control Co-authored-by: unbug <799578+unbug@users.noreply.github.com> --- src/components/ThemeToggle.js | 48 +++++++++++++++++++++++++++++----- src/hooks/useTheme.js | 8 +++--- styles/_nav-bar-container.scss | 2 ++ 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/src/components/ThemeToggle.js b/src/components/ThemeToggle.js index a8911288..c4039f6f 100644 --- a/src/components/ThemeToggle.js +++ b/src/components/ThemeToggle.js @@ -8,21 +8,21 @@ const ThemeToggle = () => { const themeOptions = [ { key: THEMES.LIGHT, - text: 'Light', + text: '☀️ Light', value: THEMES.LIGHT, icon: 'sun', - description: 'Light theme' + description: 'Always use light theme' }, { key: THEMES.DARK, - text: 'Dark', + text: '🌙 Dark', value: THEMES.DARK, icon: 'moon', - description: 'Dark theme' + description: 'Always use dark theme' }, { key: THEMES.AUTO, - text: 'Auto', + text: '⚪ Auto', value: THEMES.AUTO, icon: 'circle outline', description: 'Follow system preference' @@ -39,18 +39,54 @@ const ThemeToggle = () => { return isDark ? 'Dark' : 'Light'; }; + const getTooltipText = () => { + if (isDark) { + return `Current: ${getCurrentLabel()} theme. Click to turn off dark mode, or hold to see all options`; + } else { + return `Current: ${getCurrentLabel()} theme. Click to change theme`; + } + }; + + // Simple toggle for easier dark mode control + const handleQuickToggle = () => { + if (isDark) { + // If in dark mode (manual or auto), switch to light + setTheme(THEMES.LIGHT); + } else { + // If in light mode, switch to dark + setTheme(THEMES.DARK); + } + }; + return (
+ {/* Add a small indicator when in dark mode to make it clearer */} + {isDark && ( + + )}
} options={themeOptions} diff --git a/src/hooks/useTheme.js b/src/hooks/useTheme.js index f80e51db..30738fce 100644 --- a/src/hooks/useTheme.js +++ b/src/hooks/useTheme.js @@ -21,13 +21,13 @@ const getStoredTheme = () => { if (typeof window !== 'undefined' && window.localStorage) { try { const stored = localStorage.getItem('codelf-theme'); - return stored && Object.values(THEMES).includes(stored) ? stored : THEMES.AUTO; + return stored && Object.values(THEMES).includes(stored) ? stored : THEMES.LIGHT; } catch (error) { console.warn('Failed to read theme from localStorage:', error); - return THEMES.AUTO; + return THEMES.LIGHT; } } - return THEMES.AUTO; + return THEMES.LIGHT; }; const setStoredTheme = (theme) => { @@ -48,7 +48,7 @@ const applyTheme = (theme) => { }; export function ThemeProvider({ children }) { - const [themePreference, setThemePreference] = useState(THEMES.AUTO); + const [themePreference, setThemePreference] = useState(THEMES.LIGHT); const [actualTheme, setActualTheme] = useState(THEMES.LIGHT); useEffect(() => { diff --git a/styles/_nav-bar-container.scss b/styles/_nav-bar-container.scss index 3c25badc..a2195c9b 100755 --- a/styles/_nav-bar-container.scss +++ b/styles/_nav-bar-container.scss @@ -33,10 +33,12 @@ .theme-toggle-wrapper { display: inline-block; margin-right: 0.4rem; + position: relative; .theme-toggle-btn { display: inline-block; cursor: pointer; + position: relative; i { color: var(--text-secondary); transition: color 0.2s ease;