11---
2- // Theme Toggle Component
2+ // Theme Toggle Component - 3 state slider: Auto | Dark | Light
33---
44
5- <button
6- id =" theme-toggle"
7- class =" theme-toggle"
8- aria-label =" Toggle dark mode"
9- title =" Toggle dark mode (Cmd/Ctrl + Shift + L)"
10- >
11- <span class =" theme-toggle-icon theme-toggle-sun" aria-hidden =" true" >
12- <svg viewBox =" 0 0 24 24" width =" 20" height =" 20" fill =" none" stroke =" currentColor" stroke-width =" 2" stroke-linecap =" round" stroke-linejoin =" round" >
13- <circle cx =" 12" cy =" 12" r =" 5" />
14- <line x1 =" 12" y1 =" 1" x2 =" 12" y2 =" 3" />
15- <line x1 =" 12" y1 =" 21" x2 =" 12" y2 =" 23" />
16- <line x1 =" 4.22" y1 =" 4.22" x2 =" 5.64" y2 =" 5.64" />
17- <line x1 =" 18.36" y1 =" 18.36" x2 =" 19.78" y2 =" 19.78" />
18- <line x1 =" 1" y1 =" 12" x2 =" 3" y2 =" 12" />
19- <line x1 =" 21" y1 =" 12" x2 =" 23" y2 =" 12" />
20- <line x1 =" 4.22" y1 =" 19.78" x2 =" 5.64" y2 =" 18.36" />
21- <line x1 =" 18.36" y1 =" 5.64" x2 =" 19.78" y2 =" 4.22" />
22- </svg >
23- </span >
24- <span class =" theme-toggle-icon theme-toggle-moon" aria-hidden =" true" >
25- <svg viewBox =" 0 0 24 24" width =" 20" height =" 20" fill =" none" stroke =" currentColor" stroke-width =" 2" stroke-linecap =" round" stroke-linejoin =" round" >
26- <path d =" M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
27- </svg >
28- </span >
29- </button >
5+ <div class =" theme-toggle-container" >
6+ <button
7+ id =" theme-toggle"
8+ class =" theme-toggle"
9+ aria-label =" Toggle theme"
10+ title =" Change theme"
11+ >
12+ <span class =" theme-icon moon" aria-hidden =" true" >
13+ <svg viewBox =" 0 0 24 24" width =" 18" height =" 18" fill =" none" stroke =" currentColor" stroke-width =" 2" >
14+ <path d =" M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
15+ </svg >
16+ </span >
17+ <span class =" theme-icon auto" aria-hidden =" true" >
18+ <svg viewBox =" 0 0 24 24" width =" 18" height =" 18" fill =" none" stroke =" currentColor" stroke-width =" 2" >
19+ <circle cx =" 12" cy =" 12" r =" 10" />
20+ <path d =" M12 2v10l4.5 4.5" />
21+ </svg >
22+ </span >
23+ <span class =" theme-icon sun" aria-hidden =" true" >
24+ <svg viewBox =" 0 0 24 24" width =" 18" height =" 18" fill =" none" stroke =" currentColor" stroke-width =" 2" >
25+ <circle cx =" 12" cy =" 12" r =" 5" />
26+ <line x1 =" 12" y1 =" 1" x2 =" 12" y2 =" 3" />
27+ <line x1 =" 12" y1 =" 21" x2 =" 12" y2 =" 23" />
28+ <line x1 =" 4.22" y1 =" 4.22" x2 =" 5.64" y2 =" 5.64" />
29+ <line x1 =" 18.36" y1 =" 18.36" x2 =" 19.78" y2 =" 19.78" />
30+ <line x1 =" 1" y1 =" 12" x2 =" 3" y2 =" 12" />
31+ <line x1 =" 21" y1 =" 12" x2 =" 23" y2 =" 12" />
32+ <line x1 =" 4.22" y1 =" 19.78" x2 =" 5.64" y2 =" 18.36" />
33+ <line x1 =" 18.36" y1 =" 5.64" x2 =" 19.78" y2 =" 4.22" />
34+ </svg >
35+ </span >
36+ <span class =" theme-slider" ></span >
37+ </button >
38+ </div >
3039
3140<script >
3241 (function() {
42+ // Move theme toggle to body level to escape any stacking contexts
43+ const container = document.querySelector('.theme-toggle-container');
44+ if (container && container.parentElement !== document.body) {
45+ document.body.appendChild(container);
46+ }
47+
3348 const STORAGE_KEY = 'awesome-copilot-theme';
3449 const toggle = document.getElementById('theme-toggle');
3550 const html = document.documentElement;
3651
37- // Get initial theme
38- function getTheme() {
52+ const themes = ['dark', 'auto', 'light'];
53+ const icons = ['moon', 'auto', 'sun'];
54+
55+ function getThemeIndex() {
3956 const stored = localStorage.getItem(STORAGE_KEY);
40- if (stored === 'dark' || stored === 'light') return stored;
41- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
57+ if (stored && themes.includes(stored)) {
58+ return themes.indexOf(stored);
59+ }
60+ // Default to auto (system preference)
61+ return 1;
4262 }
4363
44- // Apply theme
45- function applyTheme(theme: string) {
46- if (theme === 'dark') {
47- html.setAttribute('data-theme', 'dark');
48- document.body.classList.remove('light');
49- document.body.classList.add('dark');
64+ function applyTheme(index: number) {
65+ const theme = themes[index];
66+ if (theme === 'auto') {
67+ const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
68+ html.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
5069 } else {
51- html.setAttribute('data-theme', 'light');
52- document.body.classList.remove('dark');
53- document.body.classList.add('light');
70+ html.setAttribute('data-theme', theme);
71+ }
72+
73+ // Move slider
74+ const slider = toggle?.querySelector('.theme-slider') as HTMLElement;
75+ if (slider) {
76+ slider.style.transform = `translateX(${index * 100}%)`;
5477 }
78+
79+ // Highlight active icon
80+ const themeToggle = document.querySelector('.theme-toggle');
81+ themeToggle?.setAttribute('data-active', String(index));
5582 }
5683
57- // Toggle theme
58- function toggleTheme() {
59- const current = getTheme();
60- const next = current === 'dark' ? 'light' : 'dark';
61- localStorage.setItem(STORAGE_KEY, next);
62- applyTheme(next);
84+ function cycleTheme() {
85+ const currentIndex = getThemeIndex();
86+ const nextIndex = (currentIndex + 1) % themes.length;
87+ localStorage.setItem(STORAGE_KEY, themes[nextIndex]);
88+ applyTheme(nextIndex);
6389 }
6490
6591 // Initialize
66- applyTheme(getTheme ());
92+ applyTheme(getThemeIndex ());
6793
6894 // Click handler
69- toggle?.addEventListener('click', toggleTheme );
95+ toggle?.addEventListener('click', cycleTheme );
7096
71- // Keyboard shortcut: Cmd/Ctrl + Shift + L
97+ // Keyboard shortcut
7298 document.addEventListener('keydown', (e) => {
7399 if ((e.metaKey || e.ctrlKey) && e.shiftKey && e.key === 'L') {
74100 e.preventDefault();
75- toggleTheme ();
101+ cycleTheme ();
76102 }
77103 });
78104
79105 // Listen for system theme changes
80106 window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
81- if (!localStorage.getItem(STORAGE_KEY)) {
82- applyTheme(e.matches ? 'dark' : 'light');
107+ if (localStorage.getItem(STORAGE_KEY) === 'auto' || !localStorage.getItem(STORAGE_KEY)) {
108+ applyTheme(1); // auto
83109 }
84110 });
85111 })();
86112</script >
87113
88114<style >
89- .theme-toggle {
115+ .theme-toggle-container {
90116 display: flex;
91117 align-items: center;
92- justify-content: center;
93- width: 40px;
94- height: 40px;
95- padding: 0;
96- border: 1px solid var(--color-glass-border);
97- border-radius: var(--border-radius);
98- background: var(--color-glass);
99- color: var(--color-text);
118+ }
119+
120+ .theme-toggle {
121+ display: flex;
122+ position: relative;
123+ width: 108px;
124+ height: 36px;
125+ padding: 3px;
126+ border: 1px solid var(--color-border);
127+ border-radius: 20px;
128+ background: var(--color-bg-secondary);
100129 cursor: pointer;
101130 transition: all 0.2s ease;
102- position: relative;
103131 overflow: hidden;
104132 }
105133
106134 .theme-toggle:hover {
107- background: var(--color-bg-tertiary);
108135 border-color: var(--color-accent);
109- transform: translateY(-1px);
110- }
111-
112- .theme-toggle:active {
113- transform: translateY(0);
114136 }
115137
116138 .theme-toggle:focus-visible {
117139 outline: 2px solid var(--color-accent);
118140 outline-offset: 2px;
119141 }
120142
121- .theme-toggle-icon {
122- position: absolute;
123- transition: all 0.3s ease;
143+ .theme-icon {
124144 display: flex;
125145 align-items: center;
126146 justify-content: center;
147+ width: 36px;
148+ height: 30px;
149+ border-radius: 16px;
150+ color: var(--color-text-muted);
151+ transition: all 0.2s ease;
152+ z-index: 1;
153+ font-weight: 400;
127154 }
128155
129- /* Sun icon - shown in dark mode */
130- :root[data-theme="dark"] .theme-toggle-sun,
131- body.dark .theme-toggle-sun {
132- opacity: 1;
133- transform: rotate(0deg) scale(1);
134- }
156+ .theme-icon.moon { color: #9898a6; }
157+ .theme-icon.auto { color: #9898a6; }
158+ .theme-icon.sun { color: #9898a6; }
135159
136- :root[data-theme="dark"] .theme-toggle-moon,
137- body.dark .theme-toggle-moon {
138- opacity: 0;
139- transform: rotate(90deg) scale(0.5);
140- }
141-
142- /* Moon icon - shown in light mode */
143- :root[data-theme="light"] .theme-toggle-sun,
144- body.light .theme-toggle-sun,
145- :root:not([data-theme]) .theme-toggle-sun,
146- body:not(.dark):not(.light) .theme-toggle-sun {
147- opacity: 0;
148- transform: rotate(-90deg) scale(0.5);
160+ .theme-toggle[data-active="0"] .moon,
161+ .theme-toggle[data-active="1"] .auto,
162+ .theme-toggle[data-active="2"] .sun {
163+ color: var(--color-text);
164+ font-weight: 700;
149165 }
150166
151- :root[data-theme="light"] .theme-toggle-moon,
152- body.light .theme-toggle-moon,
153- :root:not([data-theme]) .theme-toggle-moon,
154- body:not(.dark):not(.light) .theme-toggle-moon {
155- opacity: 1;
156- transform: rotate(0deg) scale(1);
167+ .theme-slider {
168+ display: none;
157169 }
158- </style >
170+ </style >
0 commit comments