feat: redesign Settings — amber save, UI zoom, toasts
This commit is contained in:
279
src/views/Settings.vue
Normal file
279
src/views/Settings.vue
Normal file
@@ -0,0 +1,279 @@
|
||||
<template>
|
||||
<div class="p-6">
|
||||
<h1 class="text-[1.5rem] font-medium text-text-primary mb-6">Settings</h1>
|
||||
|
||||
<div class="space-y-6 max-w-lg">
|
||||
<!-- Billing -->
|
||||
<div>
|
||||
<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>
|
||||
<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-xs text-text-secondary 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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Data Management -->
|
||||
<div>
|
||||
<h2 class="text-[1rem] font-medium text-text-primary mb-4">Data</h2>
|
||||
<div class="space-y-4">
|
||||
<!-- Export Data -->
|
||||
<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-secondary mt-0.5">Download all your data as JSON</p>
|
||||
</div>
|
||||
<button
|
||||
@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"
|
||||
>
|
||||
Export
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Clear Data -->
|
||||
<div class="flex items-center justify-between pt-4 border-t border-border-subtle">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<button
|
||||
@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"
|
||||
>
|
||||
Clear Data
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</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 -->
|
||||
<div
|
||||
v-if="showClearDataDialog"
|
||||
class="fixed inset-0 bg-black/70 backdrop-blur-[4px] flex items-center justify-center z-50"
|
||||
@click.self="showClearDataDialog = false"
|
||||
>
|
||||
<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>
|
||||
<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.
|
||||
</p>
|
||||
<p class="text-[0.6875rem] text-status-error mb-6">
|
||||
This will permanently delete all time entries, projects, clients, and invoices.
|
||||
</p>
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
@click="showClearDataDialog = false"
|
||||
class="px-4 py-2 border border-border-subtle text-text-secondary rounded hover:bg-bg-elevated transition-colors duration-150"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
@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"
|
||||
>
|
||||
Delete Everything
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
import { Plus, Minus } from 'lucide-vue-next'
|
||||
import { useSettingsStore } from '../stores/settings'
|
||||
import { useToastStore } from '../stores/toast'
|
||||
|
||||
const settingsStore = useSettingsStore()
|
||||
const toastStore = useToastStore()
|
||||
|
||||
// Settings state
|
||||
const hourlyRate = ref(0)
|
||||
const idleDetection = ref(true)
|
||||
const reminderInterval = ref(15)
|
||||
const zoomLevel = ref(100)
|
||||
|
||||
// Dialog state
|
||||
const showClearDataDialog = ref(false)
|
||||
|
||||
// Zoom steps
|
||||
const zoomSteps = [80, 90, 100, 110, 120, 130, 150]
|
||||
|
||||
function increaseZoom() {
|
||||
const currentIndex = zoomSteps.indexOf(zoomLevel.value)
|
||||
if (currentIndex < zoomSteps.length - 1) {
|
||||
zoomLevel.value = zoomSteps[currentIndex + 1]
|
||||
} else if (currentIndex === -1) {
|
||||
const next = zoomSteps.find(s => s > zoomLevel.value)
|
||||
if (next) zoomLevel.value = next
|
||||
}
|
||||
applyZoom()
|
||||
}
|
||||
|
||||
function decreaseZoom() {
|
||||
const currentIndex = zoomSteps.indexOf(zoomLevel.value)
|
||||
if (currentIndex > 0) {
|
||||
zoomLevel.value = zoomSteps[currentIndex - 1]
|
||||
} else if (currentIndex === -1) {
|
||||
const prev = [...zoomSteps].reverse().find(s => s < zoomLevel.value)
|
||||
if (prev) zoomLevel.value = prev
|
||||
}
|
||||
applyZoom()
|
||||
}
|
||||
|
||||
function applyZoom() {
|
||||
const app = document.getElementById('app')
|
||||
if (app) {
|
||||
(app.style as any).zoom = `${zoomLevel.value}%`
|
||||
}
|
||||
settingsStore.updateSetting('ui_zoom', zoomLevel.value.toString())
|
||||
}
|
||||
|
||||
// Save settings
|
||||
async function saveSettings() {
|
||||
try {
|
||||
await settingsStore.updateSetting('hourly_rate', hourlyRate.value.toString())
|
||||
await settingsStore.updateSetting('idle_detection', idleDetection.value.toString())
|
||||
await settingsStore.updateSetting('reminder_interval', reminderInterval.value.toString())
|
||||
toastStore.success('Settings saved')
|
||||
} catch (error) {
|
||||
console.error('Failed to save settings:', error)
|
||||
toastStore.error('Failed to save settings')
|
||||
}
|
||||
}
|
||||
|
||||
// Export all data
|
||||
async function exportData() {
|
||||
try {
|
||||
const data = await invoke('export_data')
|
||||
|
||||
// Create and download JSON file
|
||||
const json = JSON.stringify(data, null, 2)
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `zeroclock-export-${new Date().toISOString().split('T')[0]}.json`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch (error) {
|
||||
console.error('Failed to export data:', error)
|
||||
toastStore.error('Failed to export data')
|
||||
}
|
||||
}
|
||||
|
||||
// Clear all data
|
||||
async function clearAllData() {
|
||||
try {
|
||||
await invoke('clear_all_data')
|
||||
showClearDataDialog.value = false
|
||||
toastStore.success('All data has been cleared')
|
||||
} catch (error) {
|
||||
console.error('Failed to clear data:', error)
|
||||
toastStore.error('Failed to clear data')
|
||||
}
|
||||
}
|
||||
|
||||
// Load settings on mount
|
||||
onMounted(async () => {
|
||||
await settingsStore.fetchSettings()
|
||||
|
||||
// Set values from store
|
||||
hourlyRate.value = parseFloat(settingsStore.settings.hourly_rate) || 0
|
||||
idleDetection.value = settingsStore.settings.idle_detection !== 'false'
|
||||
reminderInterval.value = parseInt(settingsStore.settings.reminder_interval) || 15
|
||||
zoomLevel.value = parseInt(settingsStore.settings.ui_zoom) || 100
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user