Skip to content

Commit baf06c5

Browse files
chore: release v1.3.1 - fix duplicate deepMerge and implement theme support
1 parent 07abb59 commit baf06c5

6 files changed

Lines changed: 226 additions & 18 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "clawwizard",
33
"private": true,
4-
"version": "1.3.0",
4+
"version": "1.3.1",
55
"type": "module",
66
"scripts": {
77
"dev": "concurrently \"vite\" \"node src/server/bridge.js\"",

src/App.jsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import ChannelsPage from './pages/ChannelsPage'
88
import ToolsSkillsPage from './pages/ToolsSkillsPage'
99
import PreviewDeployPage from './pages/PreviewDeployPage'
1010
import DiagnosticsPage from './pages/DiagnosticsPage'
11+
import ThemeSwitcher from './components/ThemeSwitcher'
1112
import { useEffect } from 'react'
1213

1314
const STEP_ROUTES = ['/', '/model', '/workspace', '/gateway', '/channels', '/tools', '/deploy']
@@ -28,6 +29,7 @@ export default function App() {
2829
<span className="logo-icon">🦞</span>
2930
<span className="logo-text">ClawWizard</span>
3031
<span className="logo-version">v1.0</span>
32+
<ThemeSwitcher />
3133
</div>
3234

3335
<ul className="step-list">

src/components/ThemeSwitcher.jsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { useState } from 'react'
2+
import { useWizard } from '../context/WizardContext'
3+
4+
export default function ThemeSwitcher() {
5+
const { state, dispatch } = useWizard()
6+
const [isAnimating, setIsAnimating] = useState(false)
7+
8+
const toggleTheme = () => {
9+
if (isAnimating) return
10+
11+
setIsAnimating(true)
12+
const newTheme = state.theme === 'dark' ? 'light' : 'dark'
13+
14+
// Dispatch theme change
15+
dispatch({ type: 'SET_THEME', payload: newTheme })
16+
17+
// Reset animation state after transition
18+
setTimeout(() => setIsAnimating(false), 300)
19+
}
20+
21+
return (
22+
<button
23+
onClick={toggleTheme}
24+
disabled={isAnimating}
25+
className="theme-switcher"
26+
aria-label={`Switch to ${state.theme === 'dark' ? 'light' : 'dark'} theme`}
27+
title={`Switch to ${state.theme === 'dark' ? 'light' : 'dark'} theme`}
28+
>
29+
<div className="switch-track">
30+
<div className="switch-thumb" />
31+
</div>
32+
<span className="switch-label">
33+
{state.theme === 'dark' ? '🌙' : '☀️'}
34+
</span>
35+
</button>
36+
)
37+
}

src/context/WizardContext.jsx

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ const initialState = {
100100
provider: 'anthropic',
101101
apiKey: '',
102102
skippedFields: [],
103+
theme: 'dark', // Add theme state
103104
}
104105

105106
function loadState() {
@@ -113,6 +114,21 @@ function loadState() {
113114
return initialState
114115
}
115116

117+
function deepMerge(target, source) {
118+
const result = { ...target }
119+
for (const key of Object.keys(source)) {
120+
if (
121+
source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) &&
122+
target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])
123+
) {
124+
result[key] = deepMerge(target[key], source[key])
125+
} else {
126+
result[key] = source[key]
127+
}
128+
}
129+
return result
130+
}
131+
116132
function wizardReducer(state, action) {
117133
switch (action.type) {
118134
case 'SET_STEP':
@@ -187,27 +203,16 @@ function wizardReducer(state, action) {
187203
selectedTemplate: action.payload.id,
188204
soulMd: action.payload.soulMd || state.soulMd,
189205
}
206+
case 'SET_THEME':
207+
return { ...state, theme: action.payload }
190208
case 'RESET':
191209
return { ...initialState }
192210
default:
193211
return state
194212
}
195213
}
196214

197-
function deepMerge(target, source) {
198-
const result = { ...target }
199-
for (const key of Object.keys(source)) {
200-
if (
201-
source[key] && typeof source[key] === 'object' && !Array.isArray(source[key]) &&
202-
target[key] && typeof target[key] === 'object' && !Array.isArray(target[key])
203-
) {
204-
result[key] = deepMerge(target[key], source[key])
205-
} else {
206-
result[key] = source[key]
207-
}
208-
}
209-
return result
210-
}
215+
211216

212217
const WizardContext = createContext(null)
213218

src/index.css

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
/* --- CSS Custom Properties --- */
77
:root {
8+
color-scheme: dark;
9+
810
/* Background layers */
911
--bg-primary: #06080f;
1012
--bg-secondary: #0c1020;
@@ -79,6 +81,56 @@
7981
--sidebar-width: 280px;
8082
}
8183

84+
/* Light Theme */
85+
:root[data-theme="light"] {
86+
color-scheme: light;
87+
88+
/* Background layers */
89+
--bg-primary: #ffffff;
90+
--bg-secondary: #f8fafc;
91+
--bg-tertiary: #f1f5f9;
92+
--bg-surface: rgba(15, 20, 40, 0.05);
93+
94+
/* Glass */
95+
--glass-bg: rgba(15, 20, 40, 0.04);
96+
--glass-bg-hover: rgba(15, 20, 40, 0.07);
97+
--glass-bg-active: rgba(15, 20, 40, 0.1);
98+
--glass-border: rgba(15, 20, 40, 0.08);
99+
--glass-border-hover: rgba(15, 20, 40, 0.15);
100+
--glass-blur: 20px;
101+
102+
/* Accent gradient */
103+
--accent-start: #ff6b35;
104+
--accent-end: #ff2d78;
105+
--accent-gradient: linear-gradient(
106+
135deg,
107+
var(--accent-start),
108+
var(--accent-end)
109+
);
110+
--accent-glow: rgba(255, 107, 53, 0.3);
111+
112+
/* Secondary accent */
113+
--accent2-start: #6366f1;
114+
--accent2-end: #8b5cf6;
115+
--accent2-gradient: linear-gradient(
116+
135deg,
117+
var(--accent2-start),
118+
var(--accent2-end)
119+
);
120+
121+
/* Text */
122+
--text-primary: #0f172a;
123+
--text-secondary: #475569;
124+
--text-tertiary: #94a3b8;
125+
--text-accent: #ff6b35;
126+
127+
/* Status */
128+
--status-success: #22c55e;
129+
--status-warning: #f59e0b;
130+
--status-error: #ef4444;
131+
--status-info: #3b82f6;
132+
}
133+
82134
/* --- Reset --- */
83135
*,
84136
*::before,
@@ -101,6 +153,9 @@ body {
101153
min-height: 100vh;
102154
overflow-x: hidden;
103155
line-height: 1.6;
156+
transition:
157+
background-color 0.3s ease,
158+
color 0.3s ease;
104159
}
105160

106161
/* Background gradient mesh */
@@ -1082,3 +1137,94 @@ a:hover {
10821137
--terminal-blue: #60a5fa;
10831138
--terminal-red: #f87171;
10841139
}
1140+
1141+
/* --- Theme Switcher --- */
1142+
.theme-switcher {
1143+
display: inline-flex;
1144+
align-items: center;
1145+
gap: var(--space-sm);
1146+
padding: 8px 12px;
1147+
border-radius: var(--radius-md);
1148+
border: 1px solid var(--glass-border);
1149+
background: var(--glass-bg);
1150+
color: var(--text-secondary);
1151+
cursor: pointer;
1152+
transition: all var(--transition-base);
1153+
font-size: 14px;
1154+
font-weight: 600;
1155+
user-select: none;
1156+
-webkit-tap-highlight-color: transparent;
1157+
}
1158+
1159+
.theme-switcher:hover {
1160+
border-color: var(--glass-border-hover);
1161+
background: var(--glass-bg-hover);
1162+
color: var(--text-primary);
1163+
}
1164+
1165+
.theme-switcher:active {
1166+
transform: scale(0.98);
1167+
}
1168+
1169+
.theme-switcher:disabled {
1170+
opacity: 0.6;
1171+
cursor: not-allowed;
1172+
transform: none !important;
1173+
}
1174+
1175+
.switch-track {
1176+
width: 44px;
1177+
height: 24px;
1178+
border-radius: var(--radius-full);
1179+
background: var(--bg-tertiary);
1180+
border: 2px solid var(--glass-border);
1181+
position: relative;
1182+
transition: all var(--transition-base);
1183+
flex-shrink: 0;
1184+
}
1185+
1186+
.switch-thumb {
1187+
width: 16px;
1188+
height: 16px;
1189+
border-radius: 50%;
1190+
background: var(--text-tertiary);
1191+
position: absolute;
1192+
top: 2px;
1193+
left: 2px;
1194+
transition: all var(--transition-base);
1195+
}
1196+
1197+
[data-theme="light"] .switch-track {
1198+
background: var(--bg-tertiary);
1199+
border-color: var(--glass-border);
1200+
}
1201+
1202+
[data-theme="light"] .switch-thumb {
1203+
background: var(--text-tertiary);
1204+
transform: translateX(20px);
1205+
}
1206+
1207+
.switch-label {
1208+
display: inline-flex;
1209+
align-items: center;
1210+
justify-content: center;
1211+
width: 24px;
1212+
height: 24px;
1213+
border-radius: var(--radius-md);
1214+
background: var(--glass-bg);
1215+
border: 1px solid var(--glass-border);
1216+
transition: all var(--transition-base);
1217+
}
1218+
1219+
[data-theme="light"] .switch-label {
1220+
background: var(--glass-bg);
1221+
border-color: var(--glass-border);
1222+
}
1223+
1224+
/* Smooth transitions for theme switcher */
1225+
.theme-switcher,
1226+
.switch-track,
1227+
.switch-thumb,
1228+
.switch-label {
1229+
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
1230+
}

src/main.jsx

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,33 @@
1-
import React from 'react'
1+
import React, { useEffect } from 'react'
22
import ReactDOM from 'react-dom/client'
33
import { BrowserRouter } from 'react-router-dom'
4-
import { WizardProvider } from './context/WizardContext'
4+
import { WizardProvider, useWizard } from './context/WizardContext'
55
import App from './App'
66
import './index.css'
77

8+
// Theme wrapper component
9+
function ThemeWrapper({ children }) {
10+
const { state } = useWizard()
11+
12+
useEffect(() => {
13+
const root = document.documentElement
14+
if (state.theme === 'light') {
15+
root.setAttribute('data-theme', 'light')
16+
} else {
17+
root.removeAttribute('data-theme')
18+
}
19+
}, [state.theme])
20+
21+
return children
22+
}
23+
824
ReactDOM.createRoot(document.getElementById('root')).render(
925
<React.StrictMode>
1026
<BrowserRouter>
1127
<WizardProvider>
12-
<App />
28+
<ThemeWrapper>
29+
<App />
30+
</ThemeWrapper>
1331
</WizardProvider>
1432
</BrowserRouter>
1533
</React.StrictMode>

0 commit comments

Comments
 (0)