feat: redesign Settings with left sidebar tabs per Apple HIG

Four tabs (General, Timer, Billing, Data) with icon + label sidebar,
amber active indicator, auto-save on change, progressive disclosure
for timer settings, and danger zone isolated within Data tab.
This commit is contained in:
Your Name
2026-02-17 21:49:48 +02:00
parent 71d3d9ba8b
commit a8bec56d96

View File

@@ -1,132 +1,170 @@
<template>
<div class="p-6 max-w-xl">
<h1 class="text-[1.5rem] font-medium text-text-primary mb-8">Settings</h1>
<div class="flex h-full">
<!-- Sidebar -->
<aside class="w-44 bg-bg-surface border-r border-border-subtle shrink-0 p-3">
<h1 class="text-[0.8125rem] font-medium text-text-tertiary uppercase tracking-[0.08em] px-2 mb-3">Settings</h1>
<nav class="flex flex-col gap-0.5">
<button
v-for="tab in tabs"
:key="tab.id"
@click="activeTab = tab.id"
class="relative flex items-center gap-2.5 px-2.5 py-2 rounded text-[0.8125rem] transition-colors duration-150"
:class="activeTab === tab.id
? 'bg-bg-elevated text-text-primary'
: 'text-text-secondary hover:bg-bg-elevated hover:text-text-primary'"
>
<!-- Active indicator -->
<div
v-if="activeTab === tab.id"
class="absolute left-0 top-1.5 bottom-1.5 w-[2px] rounded-full bg-accent"
/>
<component :is="tab.icon" class="w-4 h-4 shrink-0" :stroke-width="1.5" />
{{ tab.label }}
</button>
</nav>
</aside>
<!-- 1. Appearance most frequently adjusted -->
<section class="bg-bg-surface rounded-lg p-5 mb-4">
<h2 class="text-xs text-text-tertiary uppercase tracking-[0.08em] font-medium mb-4">Appearance</h2>
<div class="flex items-center justify-between">
<p class="text-[0.8125rem] text-text-primary">UI Scale</p>
<div class="flex items-center gap-2">
<button
@click="decreaseZoom"
class="w-8 h-8 flex items-center justify-center border border-border-visible rounded text-text-secondary hover:text-text-primary hover:bg-bg-elevated transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
:disabled="zoomLevel <= 80"
>
<Minus class="w-3.5 h-3.5" />
</button>
<span class="w-12 text-center text-sm font-mono text-text-primary">{{ zoomLevel }}%</span>
<button
@click="increaseZoom"
class="w-8 h-8 flex items-center justify-center border border-border-visible rounded text-text-secondary hover:text-text-primary hover:bg-bg-elevated transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
:disabled="zoomLevel >= 150"
>
<Plus class="w-3.5 h-3.5" />
</button>
</div>
</div>
</section>
<!-- Content pane -->
<div class="flex-1 p-6 overflow-y-auto">
<!-- General -->
<div v-if="activeTab === 'general'">
<h2 class="text-[1.125rem] font-medium text-text-primary mb-6">General</h2>
<!-- 2. Timer behavioral settings -->
<section class="bg-bg-surface rounded-lg p-5 mb-4">
<h2 class="text-xs text-text-tertiary uppercase tracking-[0.08em] font-medium mb-4">Timer</h2>
<div class="space-y-4">
<!-- Idle Detection toggle -->
<div class="flex items-center justify-between">
<p class="text-[0.8125rem] text-text-primary">Idle Detection</p>
<button
@click="toggleIdleDetection"
:class="[
'relative inline-flex h-5 w-9 items-center rounded-full transition-colors duration-150',
idleDetection ? 'bg-status-running' : 'bg-bg-elevated'
]"
>
<span
:class="[
'inline-block h-3.5 w-3.5 transform rounded-full bg-text-primary transition-transform duration-150',
idleDetection ? 'translate-x-[18px]' : 'translate-x-[3px]'
]"
/>
</button>
</div>
<!-- Reminder Interval indented child of idle detection, progressive disclosure -->
<div
v-if="idleDetection"
class="flex items-center justify-between pl-4 border-l-2 border-border-subtle ml-1"
>
<div>
<p class="text-[0.8125rem] text-text-primary">Reminder Interval</p>
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Minutes between reminders while running</p>
<p class="text-[0.8125rem] text-text-primary">UI Scale</p>
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Adjust the interface zoom level</p>
</div>
<div class="flex items-center gap-2">
<button
@click="decreaseZoom"
class="w-8 h-8 flex items-center justify-center border border-border-visible rounded text-text-secondary hover:text-text-primary hover:bg-bg-elevated transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
:disabled="zoomLevel <= 80"
>
<Minus class="w-3.5 h-3.5" />
</button>
<span class="w-12 text-center text-sm font-mono text-text-primary">{{ zoomLevel }}%</span>
<button
@click="increaseZoom"
class="w-8 h-8 flex items-center justify-center border border-border-visible rounded text-text-secondary hover:text-text-primary hover:bg-bg-elevated transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
:disabled="zoomLevel >= 150"
>
<Plus class="w-3.5 h-3.5" />
</button>
</div>
<input
v-model.number="reminderInterval"
type="number"
min="0"
max="120"
class="w-20 px-3 py-1.5 bg-bg-inset border border-border-subtle rounded text-[0.8125rem] text-text-primary text-right font-mono focus:outline-none focus:border-border-visible"
@change="saveSettings"
/>
</div>
</div>
</section>
<!-- 3. Billing less frequently changed -->
<section class="bg-bg-surface rounded-lg p-5 mb-4">
<h2 class="text-xs text-text-tertiary uppercase tracking-[0.08em] font-medium mb-4">Billing</h2>
<div class="flex items-center justify-between">
<div>
<p class="text-[0.8125rem] text-text-primary">Default Hourly Rate</p>
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Applied to new projects</p>
</div>
<div class="flex items-center gap-1">
<span class="text-[0.8125rem] text-text-tertiary">$</span>
<input
v-model.number="hourlyRate"
type="number"
min="0"
step="0.01"
class="w-24 px-3 py-1.5 bg-bg-inset border border-border-subtle rounded text-[0.8125rem] text-text-primary text-right font-mono focus:outline-none focus:border-border-visible"
placeholder="0.00"
@change="saveSettings"
/>
</div>
</div>
</section>
<!-- Timer -->
<div v-if="activeTab === 'timer'">
<h2 class="text-[1.125rem] font-medium text-text-primary mb-6">Timer</h2>
<!-- 4. Data export and management -->
<section class="bg-bg-surface rounded-lg p-5 mb-4">
<h2 class="text-xs text-text-tertiary uppercase tracking-[0.08em] font-medium mb-4">Data</h2>
<div class="flex items-center justify-between">
<div>
<p class="text-[0.8125rem] text-text-primary">Export All Data</p>
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Download as JSON backup</p>
</div>
<button
@click="exportData"
class="px-4 py-1.5 border border-border-visible text-text-primary text-[0.8125rem] rounded hover:bg-bg-elevated transition-colors duration-150"
>
Export
</button>
</div>
</section>
<div class="space-y-5">
<!-- Idle Detection toggle -->
<div class="flex items-center justify-between">
<div>
<p class="text-[0.8125rem] text-text-primary">Idle Detection</p>
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Detect when you're idle and prompt to continue or stop</p>
</div>
<button
@click="toggleIdleDetection"
:class="[
'relative inline-flex h-5 w-9 items-center rounded-full transition-colors duration-150',
idleDetection ? 'bg-status-running' : 'bg-bg-elevated'
]"
>
<span
:class="[
'inline-block h-3.5 w-3.5 transform rounded-full bg-text-primary transition-transform duration-150',
idleDetection ? 'translate-x-[18px]' : 'translate-x-[3px]'
]"
/>
</button>
</div>
<!-- 5. Danger Zone destructive actions, isolated at bottom -->
<section class="mt-8 rounded-lg border border-status-error/20 p-5">
<h2 class="text-xs text-status-error uppercase tracking-[0.08em] font-medium mb-4">Danger Zone</h2>
<div class="flex items-center justify-between">
<div>
<p class="text-[0.8125rem] text-text-primary">Clear All Data</p>
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Permanently delete all entries, projects, clients, and invoices</p>
<!-- Reminder Interval — progressive disclosure -->
<div
v-if="idleDetection"
class="flex items-center justify-between pl-4 border-l-2 border-border-subtle ml-1"
>
<div>
<p class="text-[0.8125rem] text-text-primary">Reminder Interval</p>
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Minutes between reminders while running</p>
</div>
<input
v-model.number="reminderInterval"
type="number"
min="0"
max="120"
class="w-20 px-3 py-1.5 bg-bg-inset border border-border-subtle rounded text-[0.8125rem] text-text-primary text-right font-mono focus:outline-none focus:border-border-visible"
@change="saveSettings"
/>
</div>
</div>
<button
@click="showClearDataDialog = true"
class="px-4 py-1.5 border border-status-error text-status-error text-[0.8125rem] font-medium rounded hover:bg-status-error/10 transition-colors duration-150"
>
Clear Data
</button>
</div>
</section>
<!-- Billing -->
<div v-if="activeTab === 'billing'">
<h2 class="text-[1.125rem] font-medium text-text-primary mb-6">Billing</h2>
<div class="flex items-center justify-between">
<div>
<p class="text-[0.8125rem] text-text-primary">Default Hourly Rate</p>
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Applied to new projects</p>
</div>
<div class="flex items-center gap-1">
<span class="text-[0.8125rem] text-text-tertiary">$</span>
<input
v-model.number="hourlyRate"
type="number"
min="0"
step="0.01"
class="w-24 px-3 py-1.5 bg-bg-inset border border-border-subtle rounded text-[0.8125rem] text-text-primary text-right font-mono focus:outline-none focus:border-border-visible"
placeholder="0.00"
@change="saveSettings"
/>
</div>
</div>
</div>
<!-- Data -->
<div v-if="activeTab === 'data'">
<h2 class="text-[1.125rem] font-medium text-text-primary mb-6">Data</h2>
<div class="space-y-6">
<!-- Export -->
<div class="flex items-center justify-between">
<div>
<p class="text-[0.8125rem] text-text-primary">Export All Data</p>
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Download as JSON backup</p>
</div>
<button
@click="exportData"
class="px-4 py-1.5 border border-border-visible text-text-primary text-[0.8125rem] rounded hover:bg-bg-elevated transition-colors duration-150"
>
Export
</button>
</div>
<!-- Danger Zone -->
<div class="mt-8 rounded-lg border border-status-error/20 p-5">
<h3 class="text-xs text-status-error uppercase tracking-[0.08em] font-medium mb-4">Danger Zone</h3>
<div class="flex items-center justify-between">
<div>
<p class="text-[0.8125rem] text-text-primary">Clear All Data</p>
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Permanently delete all entries, projects, clients, and invoices</p>
</div>
<button
@click="showClearDataDialog = true"
class="px-4 py-1.5 border border-status-error text-status-error text-[0.8125rem] font-medium rounded hover:bg-status-error/10 transition-colors duration-150"
>
Clear Data
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Clear Data Confirmation Dialog -->
<div
@@ -162,15 +200,25 @@
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { ref, onMounted, markRaw } from 'vue'
import { invoke } from '@tauri-apps/api/core'
import { Plus, Minus } from 'lucide-vue-next'
import { Settings as SettingsIcon, Clock, Receipt, Database, Plus, Minus } from 'lucide-vue-next'
import { useSettingsStore } from '../stores/settings'
import { useToastStore } from '../stores/toast'
const settingsStore = useSettingsStore()
const toastStore = useToastStore()
// Tabs
const tabs = [
{ id: 'general', label: 'General', icon: markRaw(SettingsIcon) },
{ id: 'timer', label: 'Timer', icon: markRaw(Clock) },
{ id: 'billing', label: 'Billing', icon: markRaw(Receipt) },
{ id: 'data', label: 'Data', icon: markRaw(Database) },
]
const activeTab = ref('general')
// Settings state
const hourlyRate = ref(0)
const idleDetection = ref(true)