refactor: reorganize Settings per Apple HIG — auto-save, progressive disclosure, danger zone
This commit is contained in:
@@ -1,75 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-6">
|
<div class="p-6 max-w-xl">
|
||||||
<h1 class="text-[1.5rem] font-medium text-text-primary mb-6">Settings</h1>
|
<h1 class="text-[1.5rem] font-medium text-text-primary mb-8">Settings</h1>
|
||||||
|
|
||||||
<div class="space-y-6 max-w-lg">
|
<!-- 1. Appearance — most frequently adjusted -->
|
||||||
<!-- Billing -->
|
<section class="bg-bg-surface rounded-lg p-5 mb-4">
|
||||||
<div>
|
<h2 class="text-xs text-text-tertiary uppercase tracking-[0.08em] font-medium mb-4">Appearance</h2>
|
||||||
<h2 class="text-[1rem] font-medium text-text-primary mb-4">Billing</h2>
|
|
||||||
<div class="pb-6 border-b border-border-subtle">
|
|
||||||
<label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Default Hourly Rate ($)</label>
|
|
||||||
<input
|
|
||||||
v-model.number="hourlyRate"
|
|
||||||
type="number"
|
|
||||||
min="0"
|
|
||||||
step="0.01"
|
|
||||||
class="w-48 px-3 py-2 bg-bg-inset border border-border-subtle rounded text-[0.8125rem] text-text-primary focus:outline-none focus:border-border-visible"
|
|
||||||
placeholder="0.00"
|
|
||||||
/>
|
|
||||||
<p class="text-[0.6875rem] text-text-tertiary mt-1.5">Default rate for new projects</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Timer -->
|
|
||||||
<div>
|
|
||||||
<h2 class="text-[1rem] font-medium text-text-primary mb-4">Timer</h2>
|
|
||||||
<div class="pb-6 border-b border-border-subtle space-y-6">
|
|
||||||
<!-- Idle Detection -->
|
|
||||||
<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.6875rem] text-text-secondary mt-0.5">Detect when idle and prompt to continue or stop</p>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
@click="idleDetection = !idleDetection"
|
|
||||||
: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 -->
|
|
||||||
<div>
|
|
||||||
<label class="block text-[0.6875rem] text-text-tertiary uppercase tracking-[0.08em] mb-1.5">Reminder Interval (minutes)</label>
|
|
||||||
<input
|
|
||||||
v-model.number="reminderInterval"
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="120"
|
|
||||||
class="w-48 px-3 py-2 bg-bg-inset border border-border-subtle rounded text-[0.8125rem] text-text-primary focus:outline-none focus:border-border-visible"
|
|
||||||
/>
|
|
||||||
<p class="text-[0.6875rem] text-text-tertiary mt-1.5">How often to remind while timer is running (0 to disable)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Appearance -->
|
|
||||||
<div>
|
|
||||||
<h2 class="text-[1rem] font-medium text-text-primary mb-4">Appearance</h2>
|
|
||||||
<div class="pb-6 border-b border-border-subtle">
|
|
||||||
<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-xs text-text-secondary 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"
|
||||||
@@ -88,53 +25,108 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
|
|
||||||
|
<!-- 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>
|
</div>
|
||||||
|
|
||||||
<!-- Data Management -->
|
<!-- 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>
|
<div>
|
||||||
<h2 class="text-[1rem] font-medium text-text-primary mb-4">Data</h2>
|
<p class="text-[0.8125rem] text-text-primary">Reminder Interval</p>
|
||||||
<div class="space-y-4">
|
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Minutes between reminders while running</p>
|
||||||
<!-- Export Data -->
|
</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>
|
||||||
|
|
||||||
|
<!-- 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 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>
|
||||||
<p class="text-[0.6875rem] text-text-secondary mt-0.5">Download all your data as JSON</p>
|
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Download as JSON backup</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="exportData"
|
@click="exportData"
|
||||||
class="px-4 py-2 border border-border-visible text-text-primary text-[0.8125rem] rounded hover:bg-bg-elevated transition-colors duration-150"
|
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
|
Export
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Clear Data -->
|
<!-- 5. Danger Zone — destructive actions, isolated at bottom -->
|
||||||
<div class="flex items-center justify-between pt-4 border-t border-border-subtle">
|
<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>
|
<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>
|
||||||
<p class="text-[0.6875rem] text-text-secondary mt-0.5">Permanently delete all entries, projects, and clients</p>
|
<p class="text-[0.6875rem] text-text-tertiary mt-0.5">Permanently delete all entries, projects, clients, and invoices</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@click="showClearDataDialog = true"
|
@click="showClearDataDialog = true"
|
||||||
class="px-4 py-2 border border-status-error text-status-error text-[0.8125rem] font-medium rounded hover:bg-status-error/10 transition-colors duration-150"
|
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
|
Clear Data
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Save Button -->
|
|
||||||
<div class="flex justify-end pt-4">
|
|
||||||
<button
|
|
||||||
@click="saveSettings"
|
|
||||||
class="px-6 py-2 bg-accent text-bg-base text-[0.8125rem] font-medium rounded hover:bg-accent-hover transition-colors duration-150"
|
|
||||||
>
|
|
||||||
Save Settings
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Clear Data Confirmation Dialog -->
|
<!-- Clear Data Confirmation Dialog -->
|
||||||
<div
|
<div
|
||||||
@@ -145,10 +137,10 @@
|
|||||||
<div class="bg-bg-surface border border-border-subtle rounded shadow-[0_1px_3px_rgba(0,0,0,0.3)] w-full max-w-sm mx-4 p-6 animate-modal-enter">
|
<div class="bg-bg-surface border border-border-subtle rounded shadow-[0_1px_3px_rgba(0,0,0,0.3)] w-full max-w-sm mx-4 p-6 animate-modal-enter">
|
||||||
<h2 class="text-[1rem] font-semibold text-text-primary mb-2">Clear All Data</h2>
|
<h2 class="text-[1rem] font-semibold text-text-primary mb-2">Clear All Data</h2>
|
||||||
<p class="text-[0.75rem] text-text-secondary mb-4">
|
<p class="text-[0.75rem] text-text-secondary mb-4">
|
||||||
Are you sure you want to delete all your data? This action cannot be undone.
|
Are you sure? This action cannot be undone.
|
||||||
</p>
|
</p>
|
||||||
<p class="text-[0.6875rem] text-status-error mb-6">
|
<p class="text-[0.6875rem] text-status-error mb-6">
|
||||||
This will permanently delete all time entries, projects, clients, and invoices.
|
All time entries, projects, clients, and invoices will be permanently deleted.
|
||||||
</p>
|
</p>
|
||||||
<div class="flex justify-end gap-3">
|
<div class="flex justify-end gap-3">
|
||||||
<button
|
<button
|
||||||
@@ -159,7 +151,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="clearAllData"
|
@click="clearAllData"
|
||||||
class="px-4 py-2 border border-status-error text-status-error font-medium rounded hover:bg-status-error/10 transition-colors duration-150"
|
class="px-4 py-2 bg-status-error text-white font-medium rounded hover:bg-status-error/80 transition-colors duration-150"
|
||||||
>
|
>
|
||||||
Delete Everything
|
Delete Everything
|
||||||
</button>
|
</button>
|
||||||
@@ -221,13 +213,18 @@ function applyZoom() {
|
|||||||
settingsStore.updateSetting('ui_zoom', zoomLevel.value.toString())
|
settingsStore.updateSetting('ui_zoom', zoomLevel.value.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save settings
|
// Toggle idle detection with auto-save
|
||||||
|
function toggleIdleDetection() {
|
||||||
|
idleDetection.value = !idleDetection.value
|
||||||
|
saveSettings()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save settings (called on any change)
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
try {
|
try {
|
||||||
await settingsStore.updateSetting('hourly_rate', hourlyRate.value.toString())
|
await settingsStore.updateSetting('hourly_rate', hourlyRate.value.toString())
|
||||||
await settingsStore.updateSetting('idle_detection', idleDetection.value.toString())
|
await settingsStore.updateSetting('idle_detection', idleDetection.value.toString())
|
||||||
await settingsStore.updateSetting('reminder_interval', reminderInterval.value.toString())
|
await settingsStore.updateSetting('reminder_interval', reminderInterval.value.toString())
|
||||||
toastStore.success('Settings saved')
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save settings:', error)
|
console.error('Failed to save settings:', error)
|
||||||
toastStore.error('Failed to save settings')
|
toastStore.error('Failed to save settings')
|
||||||
@@ -239,7 +236,6 @@ async function exportData() {
|
|||||||
try {
|
try {
|
||||||
const data = await invoke('export_data')
|
const data = await invoke('export_data')
|
||||||
|
|
||||||
// Create and download JSON file
|
|
||||||
const json = JSON.stringify(data, null, 2)
|
const json = JSON.stringify(data, null, 2)
|
||||||
const blob = new Blob([json], { type: 'application/json' })
|
const blob = new Blob([json], { type: 'application/json' })
|
||||||
const url = URL.createObjectURL(blob)
|
const url = URL.createObjectURL(blob)
|
||||||
@@ -270,7 +266,6 @@ async function clearAllData() {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await settingsStore.fetchSettings()
|
await settingsStore.fetchSettings()
|
||||||
|
|
||||||
// Set values from store
|
|
||||||
hourlyRate.value = parseFloat(settingsStore.settings.hourly_rate) || 0
|
hourlyRate.value = parseFloat(settingsStore.settings.hourly_rate) || 0
|
||||||
idleDetection.value = settingsStore.settings.idle_detection !== 'false'
|
idleDetection.value = settingsStore.settings.idle_detection !== 'false'
|
||||||
reminderInterval.value = parseInt(settingsStore.settings.reminder_interval) || 15
|
reminderInterval.value = parseInt(settingsStore.settings.reminder_interval) || 15
|
||||||
|
|||||||
Reference in New Issue
Block a user