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:
@@ -1,12 +1,40 @@
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
<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"
|
||||
@@ -25,15 +53,19 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<!-- Timer -->
|
||||
<div v-if="activeTab === 'timer'">
|
||||
<h2 class="text-[1.125rem] font-medium text-text-primary mb-6">Timer</h2>
|
||||
|
||||
<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="[
|
||||
@@ -50,7 +82,7 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Reminder Interval — indented child of idle detection, progressive disclosure -->
|
||||
<!-- Reminder Interval — progressive disclosure -->
|
||||
<div
|
||||
v-if="idleDetection"
|
||||
class="flex items-center justify-between pl-4 border-l-2 border-border-subtle ml-1"
|
||||
@@ -69,11 +101,12 @@
|
||||
/>
|
||||
</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>
|
||||
<p class="text-[0.8125rem] text-text-primary">Default Hourly Rate</p>
|
||||
@@ -92,11 +125,14 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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>
|
||||
@@ -109,11 +145,10 @@
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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>
|
||||
@@ -126,7 +161,10 @@
|
||||
Clear Data
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</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)
|
||||
|
||||
Reference in New Issue
Block a user