feat(story-1.5): theme selection and persistence

- SpaController: injects data-theme on <html> and window.__PF_USER__ before JS
  hydrates — theme applied without FOUC; no initial API call needed for user data
- UserApiController: PATCH /api/user/theme validates against 6 allowed theme IDs,
  persists to user.theme column, returns {theme}
- useTheme composable: applyTheme() sets html[data-theme], saveTheme() calls API
  and falls back with toast on error
- SettingsView: 3-col theme grid with swatch previews, aria-checked radio semantics,
  active indicator; Sign out link; signed-in email display
- App.vue: onMounted syncs Pinia theme state with SpaController-stamped html[data-theme]

Verified: data-theme injected on / load; PATCH saves to DB; reload shows persisted theme

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-28 00:37:59 -04:00
parent 3c1d5f0eae
commit 15bab87998
5 changed files with 297 additions and 7 deletions
+16
View File
@@ -5,6 +5,22 @@
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
import BottomNav from '@/components/BottomNav.vue'
import BaseToast from '@/components/BaseToast.vue'
import { useAuthStore } from '@/stores/auth'
import { useTheme } from '@/composables/useTheme'
const auth = useAuthStore()
const { applyTheme } = useTheme()
onMounted(() => {
// Sync Vue's theme state with whatever SpaController stamped on <html>
const stamped = document.documentElement.dataset.theme
if (stamped && auth.user) {
auth.user.theme = stamped
} else if (auth.user?.theme) {
applyTheme(auth.user.theme)
}
})
</script>