Files
pictureFrame-webApp/frontend/src/views/SettingsView.vue
T
football2801 5a0db3cd60 fix(uploader,setup): beta-test polish — crop overlay, sticker delete, emoji keyboard, copy
- crop: invert overlay shading; the destination-out trick on a
  semi-transparent fill was leaving the *inside* of the crop more
  transparent than the outside, so the keep-area read as darker
  than the discard-area. Replace with 4 explicit dim-strips.
- stickers: floating trash handle now glues to the selected
  sticker's top-right corner instead of an off-canvas X that
  testers missed.
- stickers: replace the curated grid with an emoji-keyboard
  picker — recently-used row, custom-sprite row (santa hat as
  inline SVG), then an input that pops the OS emoji keyboard.
  Recents persist in localStorage; legacy stickers fall back to
  the old STICKERS table.
- pwa-install modal: drop "browser chrome" — beta tester read it
  as the literal Chrome browser.
- /setup landing page: tighten "Set up your frame" copy.
2026-05-09 15:17:06 -04:00

337 lines
8.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<main class="settings">
<h1 class="settings__title">Settings</h1>
<section v-if="!isStandalone" class="settings__section">
<h2 class="settings__section-title">Install app</h2>
<p class="settings__hint">
Pin pictureFrame to your home screen so it opens like a native app.
</p>
<button
v-if="canPromptInstall"
type="button"
class="settings__install"
@click="onNativeInstall"
>
Install pictureFrame
</button>
<button
v-else
type="button"
class="settings__install"
@click="showIosInstructions = true"
>
Add to Home Screen
</button>
</section>
<section class="settings__section">
<h2 class="settings__section-title">Theme</h2>
<div class="theme-grid" role="radiogroup" aria-label="Choose theme">
<button
v-for="t in THEMES"
:key="t.id"
type="button"
role="radio"
:aria-checked="currentTheme === t.id"
:aria-label="t.label"
:class="['theme-swatch', { 'theme-swatch--active': currentTheme === t.id }]"
:style="{ '--swatch-bg': t.bg, '--swatch-primary': t.primary, '--swatch-text': t.text }"
@click="select(t.id)"
>
<span class="theme-swatch__preview" aria-hidden="true">
<span class="theme-swatch__bar" />
<span class="theme-swatch__dot" />
</span>
<span class="theme-swatch__label">{{ t.label }}</span>
<span v-if="currentTheme === t.id" class="theme-swatch__check" aria-hidden="true"></span>
</button>
</div>
</section>
<section class="settings__section">
<h2 class="settings__section-title">Account</h2>
<div class="settings__row">
<span class="settings__row-label">Signed in as</span>
<span class="settings__row-value">{{ auth.user?.email }}</span>
</div>
<a href="/logout" class="settings__logout">Sign out</a>
</section>
<div
v-if="showIosInstructions"
class="install-modal"
role="dialog"
aria-modal="true"
aria-labelledby="install-modal-title"
@click.self="showIosInstructions = false"
>
<div class="install-modal__card">
<button
type="button"
class="install-modal__close"
aria-label="Close"
@click="showIosInstructions = false"
>×</button>
<h2 id="install-modal-title" class="install-modal__title">
{{ isIOS ? 'Add to your iPhone home screen' : 'Add to your home screen' }}
</h2>
<ol class="install-modal__steps">
<li v-if="isIOS">
Tap the <strong>Share</strong> icon at the bottom of Safari
(the square with an up-arrow).
</li>
<li v-else>
Open your browser's menu (usually the three dots
<strong></strong> in the top right).
</li>
<li>
Scroll down and tap <strong>Add to Home Screen</strong>
<span v-if="!isIOS">or <strong>Install app</strong></span>.
</li>
<li>
Tap <strong>Add</strong> in the top right to confirm.
</li>
</ol>
<p class="install-modal__footer">
The app will appear on your home screen. Open it from there
and it runs like a regular app no address bar, no tabs.
</p>
</div>
</div>
</main>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { useTheme, THEMES } from '@/composables/useTheme'
import { usePwaInstall } from '@/composables/usePwaInstall'
const auth = useAuthStore()
const { saveTheme } = useTheme()
const { isStandalone, isIOS, canPromptInstall, install } = usePwaInstall()
const currentTheme = computed(() => auth.user?.theme ?? 'warm-craft')
const showIosInstructions = ref(false)
function select(themeId: string) {
saveTheme(themeId)
}
async function onNativeInstall() {
const accepted = await install()
// If the native prompt failed for any reason (rare — e.g. the event
// expired between render and click), fall back to the same modal we
// show on iOS. Less abrupt than a silent no-op.
if (!accepted && !canPromptInstall.value) {
showIosInstructions.value = true
}
}
</script>
<style scoped lang="scss">
.settings {
padding: var(--space-4) var(--space-4) calc(var(--bottom-nav-height) + var(--space-6));
max-width: 480px;
margin: 0 auto;
&__title {
font-size: var(--text-xl);
font-weight: 700;
margin-bottom: var(--space-6);
}
&__section {
margin-bottom: var(--space-6);
}
&__section-title {
font-size: var(--text-sm);
font-weight: 700;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: var(--space-3);
}
&__row {
display: flex;
justify-content: space-between;
align-items: center;
padding: var(--space-3) 0;
border-bottom: 1px solid var(--color-border);
font-size: var(--text-base);
}
&__row-label { color: var(--color-text-muted); }
&__row-value { font-weight: 600; }
&__logout {
display: flex;
align-items: center;
min-height: var(--touch-min);
padding: var(--space-3) 0;
color: var(--color-destructive);
font-weight: 600;
text-decoration: none;
font-size: var(--text-base);
}
&__hint {
color: var(--color-text-muted);
font-size: var(--text-sm);
line-height: 1.4;
margin-bottom: var(--space-3);
}
&__install {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
min-height: var(--touch-min);
background: var(--color-primary);
color: var(--color-on-primary);
border: none;
border-radius: var(--radius-pill, 9999px);
font-size: var(--text-base);
font-weight: 700;
cursor: pointer;
}
}
.install-modal {
position: fixed;
inset: 0;
background: rgb(0 0 0 / 0.5);
display: flex;
align-items: center;
justify-content: center;
padding: var(--space-4);
z-index: 100;
&__card {
position: relative;
background: var(--color-surface);
color: var(--color-text);
border-radius: var(--radius-lg, 16px);
padding: var(--space-5);
max-width: 380px;
width: 100%;
box-shadow: 0 20px 60px rgb(0 0 0 / 0.25);
}
&__close {
position: absolute;
top: var(--space-2);
right: var(--space-3);
background: transparent;
border: none;
font-size: 1.75rem;
line-height: 1;
color: var(--color-text-muted);
cursor: pointer;
padding: var(--space-1) var(--space-2);
}
&__title {
font-size: var(--text-lg);
font-weight: 700;
margin-bottom: var(--space-3);
padding-right: var(--space-5);
}
&__steps {
margin: 0 0 var(--space-3) var(--space-4);
padding: 0;
line-height: 1.5;
li {
margin-bottom: var(--space-2);
}
}
&__footer {
color: var(--color-text-muted);
font-size: var(--text-sm);
line-height: 1.4;
margin-top: var(--space-3);
border-top: 1px solid var(--color-border);
padding-top: var(--space-3);
}
}
.theme-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--space-3);
}
.theme-swatch {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: var(--space-2);
padding: var(--space-3);
background: var(--swatch-bg);
border: 2px solid transparent;
border-radius: var(--radius-md);
cursor: pointer;
transition: border-color var(--duration-fast);
min-height: var(--touch-min);
&--active {
border-color: var(--swatch-primary);
}
&__preview {
width: 100%;
height: 36px;
border-radius: var(--radius-sm);
background: var(--swatch-bg);
display: flex;
flex-direction: column;
justify-content: center;
gap: 4px;
padding: 0 6px;
border: 1px solid color-mix(in srgb, var(--swatch-text) 15%, transparent);
}
&__bar {
display: block;
height: 6px;
border-radius: 3px;
background: var(--swatch-primary);
width: 60%;
}
&__dot {
display: block;
height: 4px;
border-radius: 2px;
background: var(--swatch-text);
width: 80%;
opacity: 0.4;
}
&__label {
font-size: var(--text-xs);
font-weight: 600;
color: var(--color-text);
text-align: center;
line-height: 1.2;
}
&__check {
position: absolute;
top: var(--space-1);
right: var(--space-2);
font-size: var(--text-sm);
color: var(--swatch-primary);
font-weight: 700;
}
}
</style>