/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Arquivo: src/layout/melissa/composables/useMelissaDockPins.js | Data: 2026-05-04 | | Pins dinâmicos do dock Melissa — modelo híbrido: | | - PINNED (manual, max 4): user fixa via menu de contexto, persiste | entre sessões em localStorage. Sempre visíveis, ordenados por ordem | de fixação. | - RECENT (MRU automático, max 3): toda vez que o user abre uma seção | que NÃO é built-in (agenda/conversas) e NÃO tá pinned, vira o pin | temporário mais recente, empurrando os mais antigos pra fora. | | Persistência: localStorage com chave `melissa.dock.pins.v1`. Salva só | slugs de seção (string), nada de dado clínico — LGPD-safe. Singleton via | módulo (estado fora da função) pra todas as instâncias compartilharem. | | Builtin (não-pinnável, não-recente): agenda, conversas — esses já têm | pin permanente próprio no template (.dock-pin com hardcode). |-------------------------------------------------------------------------- */ import { ref, watch } from 'vue'; const STORAGE_KEY = 'melissa.dock.pins.v1'; const MAX_PINNED = 4; const MAX_RECENT = 3; const BUILTIN_SLUGS = new Set(['agenda', 'conversas']); // Estado singleton compartilhado entre todas as instâncias. const pinned = ref([]); const recent = ref([]); let _hydrated = false; function _hydrate() { if (_hydrated) return; _hydrated = true; try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return; const parsed = JSON.parse(raw); if (Array.isArray(parsed?.pinned)) { pinned.value = parsed.pinned.filter((s) => typeof s === 'string').slice(0, MAX_PINNED); } if (Array.isArray(parsed?.recent)) { recent.value = parsed.recent.filter((s) => typeof s === 'string').slice(0, MAX_RECENT); } } catch { /* localStorage corrompido — ignora silenciosamente */ } } function _persist() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ pinned: pinned.value, recent: recent.value })); } catch { /* quota excedida ou storage desabilitado — ok, em memória */ } } let _persistWatcherActive = false; function _ensurePersistWatcher() { if (_persistWatcherActive) return; _persistWatcherActive = true; watch([pinned, recent], _persist, { deep: true }); } export function useMelissaDockPins() { _hydrate(); _ensurePersistWatcher(); function isBuiltin(slug) { return BUILTIN_SLUGS.has(slug); } function isPinned(slug) { return pinned.value.includes(slug); } function isRecent(slug) { return recent.value.includes(slug); } // Chamado quando o user abre uma seção. Builtins e já-pinned não viram // recent (não duplica). Mais recente entra no topo, expulsa o mais // antigo se passar do limite. function pushRecent(slug) { if (!slug || isBuiltin(slug) || isPinned(slug)) return; recent.value = [slug, ...recent.value.filter((s) => s !== slug)].slice(0, MAX_RECENT); } // Move um slug de "recent" pra "pinned" (ou cria pinned direto). // Retorna { ok, reason } — reason='full' quando já tem 4 pinned. function pin(slug) { if (!slug || isBuiltin(slug)) return { ok: false, reason: 'builtin' }; if (isPinned(slug)) return { ok: true, reason: 'already' }; if (pinned.value.length >= MAX_PINNED) return { ok: false, reason: 'full' }; recent.value = recent.value.filter((s) => s !== slug); pinned.value = [...pinned.value, slug]; return { ok: true }; } // Tira de "pinned" — não volta automaticamente pra recent (o user // explicitamente desafixou). Próxima abertura da seção vai pra recent // pelo fluxo normal de pushRecent. function unpin(slug) { pinned.value = pinned.value.filter((s) => s !== slug); } // Remove completamente (de ambas as listas). Usado pelo "Remover" do menu. function remove(slug) { pinned.value = pinned.value.filter((s) => s !== slug); recent.value = recent.value.filter((s) => s !== slug); } function clearAll() { pinned.value = []; recent.value = []; } return { pinned, recent, isBuiltin, isPinned, isRecent, pushRecent, pin, unpin, remove, clearAll, MAX_PINNED, MAX_RECENT }; }