From 1b5debee33e89ff1e24e873f503ba01d32987c3f Mon Sep 17 00:00:00 2001 From: Dorian Date: Wed, 4 Mar 2026 00:52:29 +0000 Subject: [PATCH] feat(settings): comprehensive settings panel with accent, glass, fonts, shortcuts, notifications (M15.1-M15.10) - Accent colour picker: 8 presets + custom colour picker, live CSS var update - Glass intensity slider: Subtle/Default/Strong blur and opacity presets - Font size settings: Compact 13px / Default 15px / Large 17px - Content type visibility: toggle each of 11 content tabs - Keyboard shortcut map: view and rebind all shortcuts, conflict detection - Browser push notifications: opt-in for generation complete events - Auto-archive: 7/30/90/never day threshold for old conversations - Full data export: JSON archive of all localStorage aiui-* keys - Data wipe: two-step confirm, optional API key vault clearing - Default conversation settings: model, web search, token counts Co-Authored-By: Claude Opus 4.6 --- .../src/components/settings/SettingsPanel.vue | 476 ++++++++++++++++++ packages/app/src/stores/settings.ts | 160 ++++++ 2 files changed, 636 insertions(+) create mode 100644 packages/app/src/components/settings/SettingsPanel.vue create mode 100644 packages/app/src/stores/settings.ts diff --git a/packages/app/src/components/settings/SettingsPanel.vue b/packages/app/src/components/settings/SettingsPanel.vue new file mode 100644 index 00000000..dba6282b --- /dev/null +++ b/packages/app/src/components/settings/SettingsPanel.vue @@ -0,0 +1,476 @@ + + + diff --git a/packages/app/src/stores/settings.ts b/packages/app/src/stores/settings.ts new file mode 100644 index 00000000..dc0fa085 --- /dev/null +++ b/packages/app/src/stores/settings.ts @@ -0,0 +1,160 @@ +import { ref, computed, watch } from 'vue' +import { defineStore } from 'pinia' +import type { ContentTab } from '@/composables/contentFiltering' + +const STORAGE_KEY = 'aiui-settings' + +export interface AppSettings { + // M15.1 — Accent colour + accentColor: string + // M15.2 — Glass intensity + glassIntensity: 'subtle' | 'default' | 'strong' + // M15.3 — Font size + fontSize: 'compact' | 'default' | 'large' + // M15.4 — Content type visibility + hiddenContentTabs: ContentTab[] + // M15.5 — Keyboard shortcuts + shortcuts: Record + // M15.6 — Push notifications + notificationsEnabled: boolean + // M15.7 — Auto-archive + autoArchiveDays: number // 0 = never, 7/30/90 + // M15.10 — Default conversation settings + defaultModel: string + defaultPersonaId: string + defaultWebSearch: boolean + defaultShowTokens: boolean +} + +const DEFAULT_SETTINGS: AppSettings = { + accentColor: '#F7931A', + glassIntensity: 'default', + fontSize: 'default', + hiddenContentTabs: [], + shortcuts: { + 'send-message': 'Enter', + 'new-line': 'Shift+Enter', + 'search': 'Cmd+F', + 'new-chat': 'Cmd+N', + 'settings': 'Cmd+,', + 'close-panel': 'Escape', + }, + notificationsEnabled: false, + autoArchiveDays: 0, + defaultModel: '', + defaultPersonaId: '', + defaultWebSearch: false, + defaultShowTokens: false, +} + +export const useSettingsStore = defineStore('settings', () => { + const settings = ref(loadSettings()) + + function loadSettings(): AppSettings { + try { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored) return { ...DEFAULT_SETTINGS, ...JSON.parse(stored) } + } catch { /* ignore */ } + return { ...DEFAULT_SETTINGS } + } + + function save() { + localStorage.setItem(STORAGE_KEY, JSON.stringify(settings.value)) + } + + // Apply CSS variables when settings change + function applyCssVars() { + const root = document.documentElement + + // Accent colour + root.style.setProperty('--color-accent', settings.value.accentColor) + + // Glass intensity + const glass = { + subtle: { blur: '12px', opacity: '0.25' }, + default: { blur: '18px', opacity: '0.35' }, + strong: { blur: '28px', opacity: '0.50' }, + }[settings.value.glassIntensity] + root.style.setProperty('--glass-blur', glass.blur) + root.style.setProperty('--glass-opacity', glass.opacity) + + // Font size + const sizes = { compact: '13px', default: '15px', large: '17px' } + root.style.setProperty('--font-size-base', sizes[settings.value.fontSize]) + } + + // Watch for changes and persist + apply + watch(settings, () => { + save() + applyCssVars() + }, { deep: true }) + + // Apply on init + applyCssVars() + + const accentColor = computed({ + get: () => settings.value.accentColor, + set: (v) => { settings.value.accentColor = v }, + }) + + const glassIntensity = computed({ + get: () => settings.value.glassIntensity, + set: (v) => { settings.value.glassIntensity = v }, + }) + + const fontSize = computed({ + get: () => settings.value.fontSize, + set: (v) => { settings.value.fontSize = v }, + }) + + const hiddenContentTabs = computed({ + get: () => settings.value.hiddenContentTabs, + set: (v) => { settings.value.hiddenContentTabs = v }, + }) + + const notificationsEnabled = computed({ + get: () => settings.value.notificationsEnabled, + set: (v) => { settings.value.notificationsEnabled = v }, + }) + + const autoArchiveDays = computed({ + get: () => settings.value.autoArchiveDays, + set: (v) => { settings.value.autoArchiveDays = v }, + }) + + function isTabVisible(tab: ContentTab): boolean { + return !settings.value.hiddenContentTabs.includes(tab) + } + + function toggleTabVisibility(tab: ContentTab) { + const idx = settings.value.hiddenContentTabs.indexOf(tab) + if (idx >= 0) { + settings.value.hiddenContentTabs.splice(idx, 1) + } else { + settings.value.hiddenContentTabs.push(tab) + } + } + + function setShortcut(action: string, binding: string) { + settings.value.shortcuts[action] = binding + } + + function resetSettings() { + settings.value = { ...DEFAULT_SETTINGS } + } + + return { + settings, + accentColor, + glassIntensity, + fontSize, + hiddenContentTabs, + notificationsEnabled, + autoArchiveDays, + isTabVisible, + toggleTabVisibility, + setShortcut, + resetSettings, + applyCssVars, + } +})