feat: settings window polish, sync widget fix, dark form controls

- Fix: settings overlay uses on:click|self so sidebar clicks don't close it
- Fix: openSettings(section) supports opening at specific tab
- Fix: 'Настроить' opens Settings → Синхронизация instead of Общие
- Style: dark theme select with custom arrow, global :global() CSS
- Style: settings cards, section descriptions, button/layout polish
- Style: settings gear buttons (icon-button pattern, 32px, soft hover)
- Style: settings sidebar with disabled stubs, consistent icons
- i18n: add generalDesc, workspaceDesc, appearance, localization keys
This commit is contained in:
2026-06-03 23:09:40 +08:00
parent e30a75c5a0
commit f92394e3d7
26 changed files with 2747 additions and 327 deletions
+139
View File
@@ -0,0 +1,139 @@
package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
const (
AppConfigVersion = 1
ConfigDirName = "verstak"
AppConfigFileName = "config.json"
)
// AppConfig is the global application config stored at ~/.config/verstak/config.json.
type AppConfig struct {
Version int `json:"version"`
VaultPath string `json:"vault_path"`
Theme string `json:"theme"`
Language string `json:"language"`
EnabledTemplates []string `json:"enabled_templates"`
EnabledPlugins []string `json:"enabled_plugins"`
FirstRunCompleted bool `json:"first_run_completed"`
Window WindowConfig `json:"window,omitempty"`
Vault VaultAppConfig `json:"vault,omitempty"`
}
type WindowConfig struct {
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
}
// VaultAppConfig holds per-vault settings in the global config.
type VaultAppConfig struct {
VaultID string `json:"vault_id,omitempty"`
CreatedAt string `json:"created_at,omitempty"`
Sync SyncSettings `json:"sync,omitempty"`
}
// SyncSettings holds sync configuration for the current vault.
type SyncSettings struct {
Enabled bool `json:"enabled"`
ServerURL string `json:"server_url"`
DeviceID string `json:"device_id"`
DeviceName string `json:"device_name"`
SyncInterval int `json:"sync_interval"`
LastStatus string `json:"last_status"`
LastSyncAt string `json:"last_sync_at"`
LastError string `json:"last_error,omitempty"`
}
func DefaultAppConfig() *AppConfig {
return &AppConfig{
Version: AppConfigVersion,
Theme: "system",
Language: "ru",
EnabledTemplates: []string{"folder", "project", "client", "document", "recipe"},
EnabledPlugins: []string{},
}
}
// ConfigDir returns ~/.config/verstak (or platform equivalent).
func ConfigDir() (string, error) {
cfgDir, err := os.UserConfigDir()
if err != nil {
return "", err
}
return filepath.Join(cfgDir, ConfigDirName), nil
}
// AppConfigPath returns the path to config.json.
func AppConfigPath() (string, error) {
dir, err := ConfigDir()
if err != nil {
return "", err
}
return filepath.Join(dir, AppConfigFileName), nil
}
// EnsureConfigDir creates the config directory if it doesn't exist.
func EnsureConfigDir() (string, error) {
dir, err := ConfigDir()
if err != nil {
return "", err
}
if err := os.MkdirAll(dir, 0o750); err != nil {
return "", err
}
return dir, nil
}
// LoadAppConfig reads the global config from disk. Returns nil if the file doesn't exist.
func LoadAppConfig() (*AppConfig, error) {
path, err := AppConfigPath()
if err != nil {
return nil, err
}
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil
}
return nil, fmt.Errorf("read config: %w", err)
}
if len(data) == 0 {
return nil, nil
}
var cfg AppConfig
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config: %w", err)
}
return &cfg, nil
}
// SaveAppConfig writes the global config to disk.
func SaveAppConfig(cfg *AppConfig) error {
path, err := AppConfigPath()
if err != nil {
return err
}
if _, err := EnsureConfigDir(); err != nil {
return err
}
data, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return err
}
return os.WriteFile(path, data, 0o640)
}
// DefaultVaultPath returns the default vault location.
func DefaultVaultPath() (string, error) {
dir, err := ConfigDir()
if err != nil {
return "", err
}
return filepath.Join(dir, "vault"), nil
}