MelissaLayout: extrai Settings/Hero/Timeline + composables wallpaper/toques + push-back veil perf

- MelissaSettingsPanel.vue: painel Personalizar (Plano de Fundo, Relogio & Som, Tema com preset Lara/Nora)
- MelissaHeroClock.vue: relogio gigante + saudacao + cronometro + resumo do dia
- MelissaTimelineHoje.vue: timeline horizontal (lg+) e vertical (mobile) com eco/cursor agora
- useMelissaWallpaper(): bgUrl/overlayOpacity/bgImageOpacity + onFileChange/clearBg + photoStyle/defaultBgStyle
- useMelissaToques(): toqueTermino + testarToque (preferencia, nao instance state do cronometro)
- Push-back perf: filter:blur animado no .win11-summary substituido por veil unico com backdrop-filter
  (1 backdrop pass por frame em vez de N glass-panels re-blurados; will-change + contain:strict +
  transform/opacity GPU-friendly; 60fps em mobile)

MelissaLayout: 4114 -> 2861 linhas (-1253, -30%)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Leonardo
2026-05-07 10:37:24 -03:00
parent 63340d1226
commit 95b2535d3d
6 changed files with 1770 additions and 1187 deletions
@@ -0,0 +1,41 @@
/*
* useMelissaToques — preferencia de toque de termino do Melissa
* -------------------------------------------------------------
* Encapsula apenas a preferencia (qual toque tocar) e o botao de
* teste do painel Personalizar. NAO controla o cronometro em si —
* o componente <MelissaCronometro> recebe `toque-termino` como prop
* e dispara o som ao final da sessao com a propria logica.
*
* Estado:
* - toqueTermino: string — id do toque selecionado (default 'sino')
*
* Acao:
* - testarToque(): toca o toque selecionado (preview no Personalizar)
*
* Constante exportada:
* - TOQUE_IDS: Set<string> — ids validos, usado pra sanitizar payload
* vindo de localStorage/DB no MelissaLayout
*
* Persistencia: NAO eh responsabilidade deste composable. O pai
* (MelissaLayout) persiste `toqueTermino` junto com outras prefs em
* user_settings.melissa_prefs.
*/
import { ref } from 'vue';
import { TOQUES, playToque } from '../melissaToques';
export const TOQUE_IDS = new Set(TOQUES.map((t) => t.id));
export function useMelissaToques(initialId = 'sino') {
// Sanitiza o default — se passarem id invalido cai pro 'sino'
const safeInitial = TOQUE_IDS.has(initialId) ? initialId : 'sino';
const toqueTermino = ref(safeInitial);
function testarToque() {
playToque(toqueTermino.value);
}
return {
toqueTermino,
testarToque
};
}
@@ -0,0 +1,94 @@
/*
* useMelissaWallpaper — wallpaper/background do MelissaLayout
* -----------------------------------------------------------
* Encapsula o estado e as operacoes do plano de fundo:
* - bgUrl: data URL da imagem custom (vazio = usa gradiente default)
* - overlayOpacity: 00.8, escurecedor sobre o bg (sempre aplicado)
* - bgImageOpacity: 0.011, transparencia da foto custom (so quando bgUrl)
*
* Operacoes:
* - onFileChange(e): valida tipo + tamanho, gera data URL
* - clearBg(): zera bgUrl pra voltar ao gradiente default
*
* Estilos prontos:
* - defaultBgStyle: gradiente bloom radial + linear, sempre renderizado
* atras de tudo (cores via CSS vars que flipam com dark/light)
* - photoStyle: computed que liga url(bgUrl) + opacity(bgImageOpacity)
*
* Persistencia: NAO eh responsabilidade deste composable. O pai
* (MelissaLayout) persiste estes refs junto com outras prefs em
* localStorage + user_settings.melissa_prefs.
*/
import { ref, computed } from 'vue';
import { useToast } from 'primevue/usetoast';
// Limite de upload — protege quota do localStorage (~5MB) e evita data URL
// gigante atravessando a UI. JPG/PNG 1920×1080 cabe folgado nesse teto.
export const MAX_BG_BYTES = 2 * 1024 * 1024; // 2 MB
// Gradiente default — sempre renderizado no .win11-root (atras de tudo).
// Quando o user faz upload, .win11-photo aparece por cima com opacidade
// controlada pelo slider — permite blend natural com o gradiente abaixo.
// Cores vem de CSS vars que flipam com dark/light AND seguem o preset
// (ver style global no MelissaLayout: --bloom-c1/c2/base-1/base-2).
export const defaultBgStyle = Object.freeze({
backgroundImage:
'radial-gradient(circle at 70% 30%, var(--bloom-c1) 0%, transparent 55%), radial-gradient(circle at 25% 75%, var(--bloom-c2) 0%, transparent 50%), linear-gradient(135deg, var(--bloom-base-1) 0%, var(--bloom-base-2) 50%, var(--bloom-base-1) 100%)',
backgroundSize: 'cover'
});
export function useMelissaWallpaper() {
const toast = useToast();
const bgUrl = ref(''); // vazio = usa gradiente default
const overlayOpacity = ref(0.35); // 00.8 — escurecedor sobre o bg
const bgImageOpacity = ref(1); // 0.011 — transparencia da foto custom
function onFileChange(e) {
const file = e.target.files?.[0];
if (!file) return;
if (!file.type.startsWith('image/')) {
toast.add({
severity: 'warn',
summary: 'Formato inválido',
detail: 'Selecione um arquivo de imagem (JPG, PNG, WEBP).',
life: 4000
});
e.target.value = '';
return;
}
if (file.size > MAX_BG_BYTES) {
toast.add({
severity: 'warn',
summary: 'Imagem muito grande',
detail: 'Máximo 2 MB. Reduza a resolução ou compressão e tente novamente.',
life: 4500
});
e.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = (ev) => (bgUrl.value = ev.target.result);
reader.readAsDataURL(file);
}
function clearBg() {
bgUrl.value = '';
}
const photoStyle = computed(() => ({
backgroundImage: bgUrl.value ? `url(${bgUrl.value})` : 'none',
opacity: bgImageOpacity.value
}));
return {
bgUrl,
overlayOpacity,
bgImageOpacity,
MAX_BG_BYTES,
defaultBgStyle,
photoStyle,
onFileChange,
clearBg
};
}