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 228a8cd6b4
commit f3d9a938ac

View File

@@ -1,12 +1,40 @@
<template> <template>
<div class="p-6 max-w-xl"> <div class="flex h-full">
<h1 class="text-[1.5rem] font-medium text-text-primary mb-8">Settings</h1> <!-- 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>
<!-- 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>
<!-- 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"> <div class="flex items-center justify-between">
<div>
<p class="text-[0.8125rem] text-text-primary">UI Scale</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"> <div class="flex items-center gap-2">
<button <button
@click="decreaseZoom" @click="decreaseZoom"
@@ -25,15 +53,19 @@
</button> </button>
</div> </div>
</div> </div>
</section> </div>
<!-- 2. Timer behavioral settings --> <!-- Timer -->
<section class="bg-bg-surface rounded-lg p-5 mb-4"> <div v-if="activeTab === 'timer'">
<h2 class="text-xs text-text-tertiary uppercase tracking-[0.08em] font-medium mb-4">Timer</h2> <h2 class="text-[1.125rem] font-medium text-text-primary mb-6">Timer</h2>
<div class="space-y-4">
<div class="space-y-5">
<!-- Idle Detection toggle --> <!-- Idle Detection toggle -->
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div>
<p class="text-[0.8125rem] text-text-primary">Idle Detection</p> <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 <button
@click="toggleIdleDetection" @click="toggleIdleDetection"
:class="[ :class="[
@@ -50,7 +82,7 @@
</button> </button>
</div> </div>
<!-- Reminder Interval indented child of idle detection, progressive disclosure --> <!-- Reminder Intervalprogressive disclosure -->
<div <div
v-if="idleDetection" v-if="idleDetection"
class="flex items-center justify-between pl-4 border-l-2 border-border-subtle ml-1" class="flex items-center justify-between pl-4 border-l-2 border-border-subtle ml-1"
@@ -69,11 +101,12 @@
/> />
</div> </div>
</div> </div>
</section> </div>
<!-- Billing -->
<div v-if="activeTab === 'billing'">
<h2 class="text-[1.125rem] font-medium text-text-primary mb-6">Billing</h2>
<!-- 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 class="flex items-center justify-between">
<div> <div>
<p class="text-[0.8125rem] text-text-primary">Default Hourly Rate</p> <p class="text-[0.8125rem] text-text-primary">Default Hourly Rate</p>
@@ -92,11 +125,14 @@
/> />
</div> </div>
</div> </div>
</section> </div>
<!-- 4. Data export and management --> <!-- Data -->
<section class="bg-bg-surface rounded-lg p-5 mb-4"> <div v-if="activeTab === 'data'">
<h2 class="text-xs text-text-tertiary uppercase tracking-[0.08em] font-medium mb-4">Data</h2> <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 class="flex items-center justify-between">
<div> <div>
<p class="text-[0.8125rem] text-text-primary">Export All Data</p> <p class="text-[0.8125rem] text-text-primary">Export All Data</p>
@@ -109,11 +145,10 @@
Export Export
</button> </button>
</div> </div>
</section>
<!-- 5. Danger Zone destructive actions, isolated at bottom --> <!-- Danger Zone -->
<section class="mt-8 rounded-lg border border-status-error/20 p-5"> <div 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> <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 class="flex items-center justify-between">
<div> <div>
<p class="text-[0.8125rem] text-text-primary">Clear All Data</p> <p class="text-[0.8125rem] text-text-primary">Clear All Data</p>
@@ -126,7 +161,10 @@
Clear Data Clear Data
</button> </button>
</div> </div>
</section> </div>
</div>
</div>
</div>
<!-- Clear Data Confirmation Dialog --> <!-- Clear Data Confirmation Dialog -->
<div <div
@@ -162,15 +200,25 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted } from 'vue' import { ref, onMounted, markRaw } from 'vue'
import { invoke } from '@tauri-apps/api/core' 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 { useSettingsStore } from '../stores/settings'
import { useToastStore } from '../stores/toast' import { useToastStore } from '../stores/toast'
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const toastStore = useToastStore() 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 // Settings state
const hourlyRate = ref(0) const hourlyRate = ref(0)
const idleDetection = ref(true) const idleDetection = ref(true)