Files
agenciapsilmno/src/layout/melissa/melissaToques.js
T
Leonardo 1bcb969f72 Layout Melissa (Direção B): preview, /profile, Agenda, dock, cadastro
Sandbox completo do novo layout Win11 lockscreen-style. Não troca o
AppLayout atual — Fase 5 (router wire-up) fica pra sessão dedicada.

Estrutura
- src/layout/melissa/ — MelissaLayout (bg+ψ+overlays), MelissaCronometro,
  MelissaAgenda (fullscreen), MelissaCard, MelissaMenu, MelissaBusca
- composables/useMelissaEventos.js — semana real do FC + range mensal
  pros dots do mini-cal
- composables/useMelissaPacientes.js — agora retorna created_at p/ "novo"
- melissaToques.js — toques Web Audio do término

Rota e persistência
- /preview/melissa (sem auth, sem AppLayout)
- /account/profile ganha 3º card "Melissa" com badge "Em construção"
- bootstrapUserSettings + layout composable aceitam variant='melissa'
- Migration: CHECK constraint user_settings.layout_variant aceita 'melissa'

Light mode
- Gradiente Bloom flipa via CSS vars (--bloom-c1/c2/base-1/base-2)
  Dark: 400/300/950 · Light: 200/100/0
- Cronômetro/Personalização: color: white → var(--m-text)
- Pílula psi-kbd ganha tokens --m-kbd-bg/--m-kbd-text
- Override mapeia text-X-200/300/400 → text-X-600 (17 cores Tailwind)

Agenda fullscreen
- Mini-cal funcional: click pula FC, range visível destacado, dots reais
- Feriados nacional/municipal/personalizado (rose/amber/violet)
- Dias fechados (workRules) cinza apagado, mutex feriado vence
- Card "Hoje" (stats+sessões) mesclado e movido pra sidebar esquerda
- ProximosFeriadosCard reaproveitado entre mini-cal e Hoje
- Avatar paciente: bg --m-accent-strong → --m-accent (saturado em light)
- Cores light: 12 substituições color:white → var(--m-text)

Dock taskbar Win11-style
- .melissa-dock 76px fixed bottom (CSS global, não scoped — Vue static
  hoisting perderia data-v-{hash})
- ψ centralizado vertical na faixa (bottom:10px)
- Chip cronômetro teleportado pro dock + animação minimize macOS
  (dialog encolhe + voa pro canto bottom-left, 340ms cubic-bezier)
- transform-origin: 96px calc(100% - 38px) (posição do chip no dock)

Pacientes na sidebar
- Botão fake "+" no topo abre PatientCreatePopover (rápido/completo/link)
- Reaproveita PatientCadastroDialog + ComponentCadastroRapido
- Pacientes criados nos últimos 7d sobem pro topo + badge "novo"

Dock contextual (ações do paciente selecionado)
- Avatar + nome + count + 5 ações (sessões/whatsapp/prontuário/editar/fechar)
- Teleportado pro .melissa-dock quando há paciente selecionado
- Em mobile, ações vivem em <Menu> kebab por linha
- Pattern <Transition><Teleport v-if> obrigatório (NUNCA o contrário)
  pra evitar comment placeholder + emitsOptions:null no reconciler

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 08:10:53 -03:00

117 lines
3.7 KiB
JavaScript

/*
* melissaToques — geração de toques de término via Web Audio API
* --------------------------------------------------------------
* Não usa arquivos de áudio externos. Tudo gerado em runtime com
* osciladores. Mantém self-hosted, leve e sem build de assets.
*
* Uso:
* import { TOQUES, playToque } from './melissaToques';
* playToque('sino');
*/
export const TOQUES = [
{ id: 'sino', label: 'Sino' },
{ id: 'acorde', label: 'Acorde' },
{ id: 'tic-tac', label: 'Tic-tac' },
{ id: 'suave', label: 'Suave' },
{ id: 'nenhum', label: 'Nenhum (silencioso)' }
];
function getCtx() {
const Ctx = window.AudioContext || window.webkitAudioContext;
return Ctx ? new Ctx() : null;
}
// Sino: clássico ding com decaimento longo
function playSino(ctx) {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'sine';
osc.frequency.value = 880; // A5
gain.gain.setValueAtTime(0.0001, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.25, ctx.currentTime + 0.01);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 1.6);
osc.start();
osc.stop(ctx.currentTime + 1.7);
setTimeout(() => ctx.close(), 1900);
}
// Acorde: C maior arpejado (C5 E5 G5)
function playAcorde(ctx) {
const freqs = [523.25, 659.25, 783.99];
freqs.forEach((freq, i) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'sine';
osc.frequency.value = freq;
const start = ctx.currentTime + i * 0.07;
gain.gain.setValueAtTime(0.0001, start);
gain.gain.exponentialRampToValueAtTime(0.18, start + 0.01);
gain.gain.exponentialRampToValueAtTime(0.001, start + 1.5);
osc.start(start);
osc.stop(start + 1.6);
});
setTimeout(() => ctx.close(), 2100);
}
// Tic-tac: dois cliques curtos e secos
function playTicTac(ctx) {
[0, 0.18].forEach((delay) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'square';
osc.frequency.value = 1500;
const start = ctx.currentTime + delay;
gain.gain.setValueAtTime(0.0001, start);
gain.gain.exponentialRampToValueAtTime(0.12, start + 0.005);
gain.gain.exponentialRampToValueAtTime(0.001, start + 0.06);
osc.start(start);
osc.stop(start + 0.07);
});
setTimeout(() => ctx.close(), 500);
}
// Suave: fade-in/fade-out lento, quase respiração
function playSuave(ctx) {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.connect(gain);
gain.connect(ctx.destination);
osc.type = 'sine';
osc.frequency.value = 660; // E5
gain.gain.setValueAtTime(0.0001, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.18, ctx.currentTime + 0.5);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 2.5);
osc.start();
osc.stop(ctx.currentTime + 2.6);
setTimeout(() => ctx.close(), 2800);
}
const PLAYERS = {
sino: playSino,
acorde: playAcorde,
'tic-tac': playTicTac,
suave: playSuave,
nenhum: () => {}
};
export function playToque(id) {
if (id === 'nenhum') return;
const fn = PLAYERS[id] || PLAYERS.sino;
try {
const ctx = getCtx();
if (!ctx) return;
// Web Audio em alguns browsers começa suspended até primeira interação
if (ctx.state === 'suspended') ctx.resume?.();
fn(ctx);
} catch {
// Falha silenciosa: não há nada útil a fazer aqui
}
}