bolt.diy/app/components/@settings/tabs/settings/SettingsTab.tsx
Stijnus fc3dd8c84c Final UI V3
# UI V3 Changelog

Major updates and improvements in this release:

## Core Changes
- Complete NEW REWRITTEN UI system overhaul (V3) with semantic design tokens
- New settings management system with drag-and-drop capabilities
- Enhanced provider system supporting multiple AI services
- Improved theme system with better dark mode support
- New component library with consistent design patterns

## Technical Updates
- Reorganized project architecture for better maintainability
- Performance optimizations and bundle size improvements
- Enhanced security features and access controls
- Improved developer experience with better tooling
- Comprehensive testing infrastructure

## New Features
- Background rays effect for improved visual feedback
- Advanced tab management system
- Automatic and manual update support
- Enhanced error handling and visualization
- Improved accessibility across all components

For detailed information about all changes and improvements, please see the full changelog.
2025-02-02 01:42:30 +01:00

280 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { toast } from 'react-toastify';
import { classNames } from '~/utils/classNames';
import { Switch } from '~/components/ui/Switch';
import { themeStore, kTheme } from '~/lib/stores/theme';
import type { UserProfile } from '~/components/@settings/core/types';
import { useStore } from '@nanostores/react';
import { shortcutsStore } from '~/lib/stores/settings';
export default function SettingsTab() {
const [currentTimezone, setCurrentTimezone] = useState('');
const [settings, setSettings] = useState<UserProfile>(() => {
const saved = localStorage.getItem('bolt_user_profile');
return saved
? JSON.parse(saved)
: {
theme: 'system',
notifications: true,
language: 'en',
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
};
});
useEffect(() => {
setCurrentTimezone(Intl.DateTimeFormat().resolvedOptions().timeZone);
}, []);
// Apply theme when settings changes
useEffect(() => {
if (settings.theme === 'system') {
// Remove theme override
localStorage.removeItem(kTheme);
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
document.querySelector('html')?.setAttribute('data-theme', prefersDark ? 'dark' : 'light');
themeStore.set(prefersDark ? 'dark' : 'light');
} else {
themeStore.set(settings.theme);
localStorage.setItem(kTheme, settings.theme);
document.querySelector('html')?.setAttribute('data-theme', settings.theme);
}
}, [settings.theme]);
// Save settings automatically when they change
useEffect(() => {
try {
// Get existing profile data
const existingProfile = JSON.parse(localStorage.getItem('bolt_user_profile') || '{}');
// Merge with new settings
const updatedProfile = {
...existingProfile,
theme: settings.theme,
notifications: settings.notifications,
language: settings.language,
timezone: settings.timezone,
};
localStorage.setItem('bolt_user_profile', JSON.stringify(updatedProfile));
toast.success('Settings updated');
} catch (error) {
console.error('Error saving settings:', error);
toast.error('Failed to update settings');
}
}, [settings]);
return (
<div className="space-y-4">
{/* Theme & Language */}
<motion.div
className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4 space-y-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.1 }}
>
<div className="flex items-center gap-2 mb-4">
<div className="i-ph:palette-fill w-4 h-4 text-purple-500" />
<span className="text-sm font-medium text-bolt-elements-textPrimary">Appearance</span>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<div className="i-ph:paint-brush-fill w-4 h-4 text-bolt-elements-textSecondary" />
<label className="block text-sm text-bolt-elements-textSecondary">Theme</label>
</div>
<div className="flex gap-2">
{(['light', 'dark', 'system'] as const).map((theme) => (
<button
key={theme}
onClick={() => {
setSettings((prev) => ({ ...prev, theme }));
if (theme !== 'system') {
themeStore.set(theme);
}
}}
className={classNames(
'inline-flex items-center gap-2 px-4 py-2 rounded-lg text-sm disabled:opacity-50 disabled:cursor-not-allowed',
settings.theme === theme
? 'bg-purple-500 text-white hover:bg-purple-600 dark:bg-purple-500 dark:text-white dark:hover:bg-purple-600'
: 'bg-bolt-elements-hover dark:bg-[#1A1A1A] text-bolt-elements-textSecondary hover:bg-purple-500/10 hover:text-purple-500 dark:hover:bg-purple-500/20 dark:text-bolt-elements-textPrimary dark:hover:text-purple-500',
)}
>
<div
className={`w-4 h-4 ${
theme === 'light'
? 'i-ph:sun-fill'
: theme === 'dark'
? 'i-ph:moon-stars-fill'
: 'i-ph:monitor-fill'
}`}
/>
<span className="capitalize">{theme}</span>
</button>
))}
</div>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<div className="i-ph:translate-fill w-4 h-4 text-bolt-elements-textSecondary" />
<label className="block text-sm text-bolt-elements-textSecondary">Language</label>
</div>
<select
value={settings.language}
onChange={(e) => setSettings((prev) => ({ ...prev, language: e.target.value }))}
className={classNames(
'w-full px-3 py-2 rounded-lg text-sm',
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
'text-bolt-elements-textPrimary',
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
'transition-all duration-200',
)}
>
<option value="en">English</option>
<option value="es">Español</option>
<option value="fr">Français</option>
<option value="de">Deutsch</option>
<option value="it">Italiano</option>
<option value="pt">Português</option>
<option value="ru">Русский</option>
<option value="zh"></option>
<option value="ja"></option>
<option value="ko"></option>
</select>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<div className="i-ph:bell-fill w-4 h-4 text-bolt-elements-textSecondary" />
<label className="block text-sm text-bolt-elements-textSecondary">Notifications</label>
</div>
<div className="flex items-center justify-between">
<span className="text-sm text-bolt-elements-textSecondary">
{settings.notifications ? 'Notifications are enabled' : 'Notifications are disabled'}
</span>
<Switch
checked={settings.notifications}
onCheckedChange={(checked) => {
// Update local state
setSettings((prev) => ({ ...prev, notifications: checked }));
// Update localStorage immediately
const existingProfile = JSON.parse(localStorage.getItem('bolt_user_profile') || '{}');
const updatedProfile = {
...existingProfile,
notifications: checked,
};
localStorage.setItem('bolt_user_profile', JSON.stringify(updatedProfile));
// Dispatch storage event for other components
window.dispatchEvent(
new StorageEvent('storage', {
key: 'bolt_user_profile',
newValue: JSON.stringify(updatedProfile),
}),
);
toast.success(`Notifications ${checked ? 'enabled' : 'disabled'}`);
}}
/>
</div>
</div>
</motion.div>
{/* Timezone */}
<motion.div
className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.2 }}
>
<div className="flex items-center gap-2 mb-4">
<div className="i-ph:clock-fill w-4 h-4 text-purple-500" />
<span className="text-sm font-medium text-bolt-elements-textPrimary">Time Settings</span>
</div>
<div>
<div className="flex items-center gap-2 mb-2">
<div className="i-ph:globe-fill w-4 h-4 text-bolt-elements-textSecondary" />
<label className="block text-sm text-bolt-elements-textSecondary">Timezone</label>
</div>
<select
value={settings.timezone}
onChange={(e) => setSettings((prev) => ({ ...prev, timezone: e.target.value }))}
className={classNames(
'w-full px-3 py-2 rounded-lg text-sm',
'bg-[#FAFAFA] dark:bg-[#0A0A0A]',
'border border-[#E5E5E5] dark:border-[#1A1A1A]',
'text-bolt-elements-textPrimary',
'focus:outline-none focus:ring-2 focus:ring-purple-500/30',
'transition-all duration-200',
)}
>
<option value={currentTimezone}>{currentTimezone}</option>
</select>
</div>
</motion.div>
{/* Keyboard Shortcuts */}
<motion.div
className="bg-white dark:bg-[#0A0A0A] rounded-lg shadow-sm dark:shadow-none p-4"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 0.3 }}
>
<div className="flex items-center gap-2 mb-4">
<div className="i-ph:keyboard-fill w-4 h-4 text-purple-500" />
<span className="text-sm font-medium text-bolt-elements-textPrimary">Keyboard Shortcuts</span>
</div>
<div className="space-y-2">
{Object.entries(useStore(shortcutsStore)).map(([name, shortcut]) => (
<div
key={name}
className="flex items-center justify-between p-2 rounded-lg bg-[#FAFAFA] dark:bg-[#1A1A1A] hover:bg-purple-50 dark:hover:bg-purple-500/10 transition-colors"
>
<span className="text-sm text-bolt-elements-textPrimary capitalize">
{name.replace(/([A-Z])/g, ' $1').toLowerCase()}
</span>
<div className="flex items-center gap-1">
{shortcut.ctrlOrMetaKey && (
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
{navigator.platform.includes('Mac') ? '⌘' : 'Ctrl'}
</kbd>
)}
{shortcut.ctrlKey && (
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
Ctrl
</kbd>
)}
{shortcut.metaKey && (
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
</kbd>
)}
{shortcut.altKey && (
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
{navigator.platform.includes('Mac') ? '⌥' : 'Alt'}
</kbd>
)}
{shortcut.shiftKey && (
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
</kbd>
)}
<kbd className="px-2 py-1 text-xs font-semibold text-bolt-elements-textSecondary bg-white dark:bg-[#0A0A0A] border border-[#E5E5E5] dark:border-[#1A1A1A] rounded shadow-sm">
{shortcut.key.toUpperCase()}
</kbd>
</div>
</div>
))}
</div>
</motion.div>
</div>
);
}