Documentos Pacientes, Template Documentos Pacientes Saas, Documentos prontuários, Documentos Externos, Visualização Externa, Permissão de Visualização, Render Otimização
This commit is contained in:
@@ -50,6 +50,7 @@ const presetModel = computed({
|
||||
|
||||
applyThemeEngine(layoutConfig);
|
||||
queuePatch?.({ preset: val });
|
||||
saveThemeToStorage();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -68,11 +69,23 @@ const menuModeModel = computed({
|
||||
}
|
||||
});
|
||||
|
||||
function saveThemeToStorage() {
|
||||
try {
|
||||
localStorage.setItem('ui_theme_config', JSON.stringify({
|
||||
preset: layoutConfig.preset,
|
||||
primary: layoutConfig.primary,
|
||||
surface: layoutConfig.surface,
|
||||
menuMode: layoutConfig.menuMode
|
||||
}));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function updateColors(type, item) {
|
||||
if (type === 'primary') {
|
||||
layoutConfig.primary = item.name;
|
||||
applyThemeEngine(layoutConfig);
|
||||
queuePatch?.({ primary_color: item.name });
|
||||
saveThemeToStorage();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,6 +93,7 @@ function updateColors(type, item) {
|
||||
layoutConfig.surface = item.name;
|
||||
applyThemeEngine(layoutConfig);
|
||||
queuePatch?.({ surface_color: item.name });
|
||||
saveThemeToStorage();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
|
||||
<template>
|
||||
<div class="layout-footer">
|
||||
SAKAI by
|
||||
Agência PSI
|
||||
<a href="https://primevue.org" target="_blank" rel="noopener noreferrer" class="text-primary font-bold hover:underline">PrimeVue</a>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -176,7 +176,7 @@ onBeforeUnmount(() => {
|
||||
<style>
|
||||
/* ──────────────────────────────────────────────
|
||||
LAYOUT CLÁSSICO — ajustes globais (não scoped)
|
||||
para sobrescrever o tema PrimeVue/Sakai
|
||||
para sobrescrever o tema PrimeVue
|
||||
────────────────────────────────────────────── */
|
||||
|
||||
/* ── Global Notice Banner: variável de altura ─────────────
|
||||
|
||||
@@ -46,11 +46,24 @@ function isDarkNow() {
|
||||
return document.documentElement.classList.contains('app-dark');
|
||||
}
|
||||
|
||||
async function waitForDarkFlip(before, timeoutMs = 900) {
|
||||
const start = performance.now();
|
||||
while (performance.now() - start < timeoutMs) {
|
||||
await nextTick();
|
||||
await new Promise((r) => requestAnimationFrame(r));
|
||||
const now = isDarkNow();
|
||||
if (now !== before) return now;
|
||||
}
|
||||
return isDarkNow();
|
||||
}
|
||||
|
||||
async function toggleDarkAndPersist() {
|
||||
try {
|
||||
const before = isDarkNow();
|
||||
toggleDarkMode();
|
||||
await nextTick();
|
||||
const theme_mode = isDarkNow() ? 'dark' : 'light';
|
||||
const after = await waitForDarkFlip(before);
|
||||
const theme_mode = after ? 'dark' : 'light';
|
||||
try { localStorage.setItem('ui_theme_mode', theme_mode); } catch {}
|
||||
await queuePatch({ theme_mode }, { flushNow: true });
|
||||
} catch (e) {
|
||||
console.error('[FooterPanel][theme] falhou:', e?.message || e);
|
||||
|
||||
@@ -46,6 +46,7 @@ const emit = defineEmits(['quick-create']);
|
||||
|
||||
const props = defineProps({
|
||||
item: { type: Object, default: () => ({}) },
|
||||
index: { type: Number, default: 0 },
|
||||
root: { type: Boolean, default: false },
|
||||
parentPath: { type: String, default: null }
|
||||
});
|
||||
|
||||
+14
-8
@@ -95,6 +95,7 @@ const userName = computed(() => sessionUser.value?.user_metadata?.full_name || s
|
||||
|
||||
// ── Início (fixo) ────────────────────────────────────────────
|
||||
function selectHome() {
|
||||
if (layoutConfig.railOpenMode === 'hover') return;
|
||||
if (layoutState.railSectionKey === '__home__' && layoutState.railPanelOpen) {
|
||||
layoutState.railPanelOpen = false;
|
||||
} else {
|
||||
@@ -107,6 +108,7 @@ const isHomeActive = computed(() => layoutState.railSectionKey === '__home__' &&
|
||||
|
||||
// ── Seleção de seção ─────────────────────────────────────────
|
||||
function selectSection(section) {
|
||||
if (layoutConfig.railOpenMode === 'hover') return;
|
||||
if (layoutState.railSectionKey === section.key && layoutState.railPanelOpen) {
|
||||
layoutState.railPanelOpen = false;
|
||||
} else {
|
||||
@@ -115,13 +117,21 @@ function selectSection(section) {
|
||||
}
|
||||
}
|
||||
|
||||
// Verifica recursivamente se alguma rota do grupo está ativa
|
||||
function _matchesActive(items, active) {
|
||||
return items.some((i) => {
|
||||
const p = typeof i.to === 'string' ? i.to : '';
|
||||
if (p && active.startsWith(p)) return true;
|
||||
if (Array.isArray(i.items) && i.items.length) return _matchesActive(i.items, active);
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
function isActiveSectionOrChild(section) {
|
||||
if (layoutState.railSectionKey === section.key && layoutState.railPanelOpen) return true;
|
||||
const active = String(layoutState.activePath || '');
|
||||
return section.items.some((i) => {
|
||||
const p = typeof i.to === 'string' ? i.to : '';
|
||||
return p && active.startsWith(p);
|
||||
});
|
||||
if (!active) return false;
|
||||
return _matchesActive(section.items, active);
|
||||
}
|
||||
|
||||
// ── Menu do usuário (rodapé) ─────────────────────────────────
|
||||
@@ -144,7 +154,6 @@ function toggleUserMenu(e) {
|
||||
<button
|
||||
class="rail-btn relative w-10 h-10 rounded-[10px] grid place-items-center border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer text-base shrink-0 transition-[background,color,transform] duration-150 hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)] hover:scale-105"
|
||||
:class="isHomeActive ? 'rail-btn--active bg-[color-mix(in_srgb,var(--primary-color)_12%,transparent)] !text-[var(--primary-color)]' : ''"
|
||||
v-tooltip.right="{ value: 'Início', showDelay: 0 }"
|
||||
aria-label="Início"
|
||||
@click="selectHome"
|
||||
@mouseenter="onHomeHover"
|
||||
@@ -157,7 +166,6 @@ function toggleUserMenu(e) {
|
||||
:key="section.key"
|
||||
class="rail-btn relative w-10 h-10 rounded-[10px] grid place-items-center border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer text-base shrink-0 transition-[background,color,transform] duration-150 hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)] hover:scale-105"
|
||||
:class="isActiveSectionOrChild(section) ? 'rail-btn--active bg-[color-mix(in_srgb,var(--primary-color)_12%,transparent)] !text-[var(--primary-color)]' : ''"
|
||||
v-tooltip.right="{ value: section.label, showDelay: 0 }"
|
||||
:aria-label="section.label"
|
||||
@click="selectSection(section)"
|
||||
@mouseenter="onSectionHover(section)"
|
||||
@@ -170,7 +178,6 @@ function toggleUserMenu(e) {
|
||||
<div class="w-full flex flex-col items-center gap-1.5 py-2 pb-3 border-t border-[var(--surface-border)]">
|
||||
<button
|
||||
class="w-9 h-9 rounded-[10px] grid place-items-center border-none bg-transparent text-[var(--text-color-secondary)] cursor-pointer text-[0.875rem] shrink-0 transition-[background,color,transform] duration-150 hover:bg-[var(--surface-ground)] hover:text-[var(--text-color)] hover:scale-105"
|
||||
v-tooltip.right="{ value: 'Configurações', showDelay: 0 }"
|
||||
aria-label="Configurações"
|
||||
@click="$router.push('/configuracoes')"
|
||||
>
|
||||
@@ -180,7 +187,6 @@ function toggleUserMenu(e) {
|
||||
<!-- Avatar — trigger do menu de usuário -->
|
||||
<button
|
||||
class="w-9 h-9 rounded-[10px] border-none cursor-pointer overflow-hidden shrink-0 bg-[var(--surface-ground)] grid place-items-center transition-[transform,box-shadow] duration-150 hover:scale-105 hover:shadow-[0_0_0_2px_var(--primary-color)]"
|
||||
v-tooltip.right="{ value: userName, showDelay: 0 }"
|
||||
:aria-label="userName"
|
||||
@click="toggleUserMenu"
|
||||
>
|
||||
|
||||
@@ -114,12 +114,14 @@ function onPanelMouseEnter() {
|
||||
}
|
||||
function onPanelMouseLeave() {
|
||||
if (layoutConfig.railOpenMode !== 'hover') return;
|
||||
if (popoverOpen.value) return; // popover flutuante aberto — não fechar
|
||||
scheduleRailHoverClose(200);
|
||||
}
|
||||
|
||||
// ── QuickCreate (Pacientes) ───────────────────────────────
|
||||
const createPopover = ref(null);
|
||||
const quickDialog = ref(false);
|
||||
const popoverOpen = ref(false);
|
||||
|
||||
function openQuickCreate(event, item) {
|
||||
createPopover.value?.toggle(event);
|
||||
@@ -482,7 +484,7 @@ async function goToResult(r) {
|
||||
</nav>
|
||||
|
||||
<!-- PatientCreatePopover (shared) -->
|
||||
<PatientCreatePopover ref="createPopover" @quick-create="onQuickCreate" />
|
||||
<PatientCreatePopover ref="createPopover" @quick-create="onQuickCreate" @show="popoverOpen = true" @hide="popoverOpen = false" />
|
||||
|
||||
<!-- Cadastro Rápido Dialog -->
|
||||
<ComponentCadastroRapido
|
||||
|
||||
@@ -57,6 +57,7 @@ const presetModel = computed({
|
||||
layoutConfig.preset = v;
|
||||
applyThemeEngine(layoutConfig);
|
||||
queuePatch?.({ preset: v });
|
||||
saveThemeToStorage();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -68,16 +69,29 @@ const menuModeModel = computed({
|
||||
}
|
||||
});
|
||||
|
||||
function saveThemeToStorage() {
|
||||
try {
|
||||
localStorage.setItem('ui_theme_config', JSON.stringify({
|
||||
preset: layoutConfig.preset,
|
||||
primary: layoutConfig.primary,
|
||||
surface: layoutConfig.surface,
|
||||
menuMode: layoutConfig.menuMode
|
||||
}));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
function updateColors(type, item) {
|
||||
if (type === 'primary') {
|
||||
layoutConfig.primary = item.name;
|
||||
applyThemeEngine(layoutConfig);
|
||||
queuePatch?.({ primary_color: item.name });
|
||||
saveThemeToStorage();
|
||||
}
|
||||
if (type === 'surface') {
|
||||
layoutConfig.surface = item.name;
|
||||
applyThemeEngine(layoutConfig);
|
||||
queuePatch?.({ surface_color: item.name });
|
||||
saveThemeToStorage();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -140,6 +140,16 @@ async function loadAndApplyUserSettings() {
|
||||
// 3) aplica engine UMA vez
|
||||
applyThemeEngine(layoutConfig);
|
||||
|
||||
// 4) persiste no localStorage para carregamento instantâneo no próximo boot
|
||||
try {
|
||||
localStorage.setItem('ui_theme_config', JSON.stringify({
|
||||
preset: layoutConfig.preset,
|
||||
primary: layoutConfig.primary,
|
||||
surface: layoutConfig.surface,
|
||||
menuMode: layoutConfig.menuMode
|
||||
}));
|
||||
} catch {}
|
||||
|
||||
// ✅ IMPORTANTE:
|
||||
// changeMenuMode NÃO é só "setar menuMode".
|
||||
// Ele reseta estados do sidebar/overlay/mobile e previne drift.
|
||||
@@ -165,6 +175,7 @@ async function toggleDarkAndPersistSilently() {
|
||||
toggleDarkMode();
|
||||
const after = await waitForDarkFlip(before);
|
||||
const theme_mode = after ? 'dark' : 'light';
|
||||
try { localStorage.setItem('ui_theme_mode', theme_mode); } catch {}
|
||||
await queuePatch({ theme_mode }, { flushNow: true });
|
||||
} catch (e) {
|
||||
console.error('[Topbar][theme] falhou:', e?.message || e);
|
||||
@@ -632,7 +643,7 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
/* Hamburguer: visível apenas em ≤ xl (1280px)
|
||||
!important necessário para sobrescrever CSS do tema Sakai (.layout-menu-button) */
|
||||
!important necessário para sobrescrever CSS do tema (.layout-menu-button) */
|
||||
.rail-topbar__hamburger {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
@@ -36,12 +36,23 @@ function _loadRailOpenMode() {
|
||||
return 'hover';
|
||||
}
|
||||
|
||||
// ── resolve tema (preset/primary/surface) salvo no localStorage ─
|
||||
function _loadSavedTheme() {
|
||||
try {
|
||||
const raw = localStorage.getItem('ui_theme_config');
|
||||
if (raw) return JSON.parse(raw);
|
||||
} catch {}
|
||||
return {};
|
||||
}
|
||||
|
||||
const _savedTheme = _loadSavedTheme();
|
||||
|
||||
const layoutConfig = reactive({
|
||||
preset: 'Aura',
|
||||
primary: 'emerald',
|
||||
surface: null,
|
||||
preset: _savedTheme.preset || 'Aura',
|
||||
primary: _savedTheme.primary || 'emerald',
|
||||
surface: _savedTheme.surface || null,
|
||||
darkTheme: false,
|
||||
menuMode: 'static',
|
||||
menuMode: _savedTheme.menuMode || 'static',
|
||||
variant: _loadVariant(), // 'classic' | 'rail'
|
||||
railOpenMode: _loadRailOpenMode() // 'click' | 'hover'
|
||||
});
|
||||
|
||||
@@ -15,18 +15,18 @@
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { computed, ref, watch, onMounted, nextTick } from 'vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { useTenantStore } from '@/stores/tenantStore';
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import DatePicker from 'primevue/datepicker';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
|
||||
import PausasChipsEditor from '@/components/agenda/PausasChipsEditor.vue';
|
||||
|
||||
import FullCalendar from '@fullcalendar/vue3';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
import ptBrLocale from '@fullcalendar/core/locales/pt-br';
|
||||
import timeGridPlugin from '@fullcalendar/timegrid';
|
||||
import FullCalendar from '@fullcalendar/vue3';
|
||||
|
||||
const toast = useToast();
|
||||
const tenantStore = useTenantStore();
|
||||
@@ -1385,7 +1385,7 @@ const jornadaEndDate = computed({
|
||||
<div class="anim-child [--delay:120ms] xl:w-[42%] xl:top-4 xl:self-start">
|
||||
<div class="rounded-[6px] border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden shadow-sm agenda-altura">
|
||||
<!-- Header do preview -->
|
||||
<div class="sticky top-0 z-10 bg-white">
|
||||
<div class="sticky top-0 z-10">
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border)]">
|
||||
<div class="font-semibold text-sm">Preview da agenda</div>
|
||||
<div class="flex gap-1">
|
||||
|
||||
@@ -19,7 +19,7 @@ import { ref, computed, onMounted } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import Editor from 'primevue/editor';
|
||||
import JoditEmailEditor from '@/components/ui/JoditEmailEditor.vue';
|
||||
import { supabase } from '@/lib/supabase/client';
|
||||
import { renderEmail, renderTemplate, generateLayoutSection } from '@/lib/email/emailTemplateService';
|
||||
import { MOCK_DATA, TEMPLATE_DOMAINS } from '@/lib/email/emailTemplateConstants';
|
||||
@@ -135,19 +135,6 @@ const DOMAIN_SEVERITY = { session: 'info', intake: 'warning', billing: 'success'
|
||||
// ── Dialog layout (header/footer global) ──────────────────────
|
||||
const layoutDlg = ref({ open: false, saving: false });
|
||||
const layoutForm = ref({ header: defaultSection(), footer: defaultSection() });
|
||||
const headerEditorRef = ref(null);
|
||||
const footerEditorRef = ref(null);
|
||||
|
||||
const LAYOUT_OPTIONS = [
|
||||
{ value: 'logo-left', label: 'Logo à esquerda' },
|
||||
{ value: 'logo-right', label: 'Logo à direita' },
|
||||
{ value: 'logo-center', label: 'Logo centralizada' }
|
||||
];
|
||||
const TEXT_OPTIONS = [
|
||||
{ value: 'text-left', label: 'Texto à esquerda' },
|
||||
{ value: 'text-center', label: 'Texto centralizado' },
|
||||
{ value: 'text-right', label: 'Texto à direita' }
|
||||
];
|
||||
|
||||
function openLayoutDlg() {
|
||||
layoutForm.value = {
|
||||
@@ -157,10 +144,6 @@ function openLayoutDlg() {
|
||||
layoutDlg.value = { open: true, saving: false };
|
||||
}
|
||||
|
||||
function selectLayout(which, type) {
|
||||
layoutForm.value[which].layout = type;
|
||||
}
|
||||
|
||||
const headerLayoutPreview = computed(() => generateLayoutSection(layoutForm.value.header, profileLogoUrl.value, true));
|
||||
const footerLayoutPreview = computed(() => generateLayoutSection(layoutForm.value.footer, profileLogoUrl.value, false));
|
||||
|
||||
@@ -206,7 +189,6 @@ function openEdit(row) {
|
||||
subject: ov?.subject ?? row.subject,
|
||||
body_html: ov?.body_html ?? row.body_html,
|
||||
body_text: ov?.body_text ?? '',
|
||||
enabled: ov?.enabled ?? true,
|
||||
synced_version: row.version,
|
||||
variables: row.variables || {}
|
||||
};
|
||||
@@ -225,15 +207,11 @@ const formVariables = computed(() => {
|
||||
|
||||
function insertVar(varName) {
|
||||
const snippet = `{{${varName}}}`;
|
||||
const quill = editorRef.value?.quill;
|
||||
if (!quill) {
|
||||
if (editorRef.value?.insertHTML) {
|
||||
editorRef.value.insertHTML(snippet);
|
||||
} else {
|
||||
form.value.body_html = (form.value.body_html || '') + snippet;
|
||||
return;
|
||||
}
|
||||
const range = quill.getSelection(true);
|
||||
const index = range ? range.index : quill.getLength() - 1;
|
||||
quill.insertText(index, snippet, 'user');
|
||||
quill.setSelection(index + snippet.length, 0);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
@@ -251,7 +229,7 @@ async function save() {
|
||||
subject: form.value.use_custom_subject ? form.value.subject : null,
|
||||
body_html: form.value.use_custom_body ? form.value.body_html : null,
|
||||
body_text: form.value.use_custom_body && form.value.body_text ? form.value.body_text : null,
|
||||
enabled: form.value.enabled,
|
||||
enabled: true,
|
||||
synced_version: form.value.synced_version
|
||||
};
|
||||
if (dlg.value.mode === 'create') {
|
||||
@@ -323,9 +301,19 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Filtro -->
|
||||
<div class="flex gap-2 flex-wrap">
|
||||
<!-- Filtro + Layout global -->
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<Button v-for="opt in DOMAIN_OPTIONS" :key="String(opt.value)" :label="opt.label" size="small" :severity="filterDomain === opt.value ? 'primary' : 'secondary'" :outlined="filterDomain !== opt.value" @click="filterDomain = opt.value" />
|
||||
<div class="ml-auto">
|
||||
<Button
|
||||
label="Layout global"
|
||||
icon="pi pi-palette"
|
||||
size="small"
|
||||
:severity="layoutActive ? 'success' : 'secondary'"
|
||||
:outlined="!layoutActive"
|
||||
@click="openLayoutDlg"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ SKELETON ══════════════════════════════════════════════ -->
|
||||
@@ -394,88 +382,21 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<div v-if="layoutForm.header.enabled" class="px-4 py-4 flex flex-col gap-4">
|
||||
<!-- Cards de layout -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-xs font-semibold">Com logo</p>
|
||||
<div class="flex gap-2">
|
||||
<button v-for="opt in LAYOUT_OPTIONS" :key="opt.value" class="layout-card" :class="{ 'layout-card--active': layoutForm.header.layout === opt.value }" @click="selectLayout('header', opt.value)">
|
||||
<div class="layout-card__thumb">
|
||||
<template v-if="opt.value === 'logo-left'">
|
||||
<div class="lc-logo" />
|
||||
<div class="lc-spacer" />
|
||||
<div class="lc-lines">
|
||||
<div class="lc-line" />
|
||||
<div class="lc-line short" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="opt.value === 'logo-right'">
|
||||
<div class="lc-lines">
|
||||
<div class="lc-line" />
|
||||
<div class="lc-line short" />
|
||||
</div>
|
||||
<div class="lc-spacer" />
|
||||
<div class="lc-logo" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="lc-center">
|
||||
<div class="lc-logo" />
|
||||
<div class="lc-line" style="width: 70%; margin-top: 5px" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<span class="layout-card__label">{{ opt.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs font-semibold mt-1">Só texto</p>
|
||||
<div class="flex gap-2">
|
||||
<button v-for="opt in TEXT_OPTIONS" :key="opt.value" class="layout-card" :class="{ 'layout-card--active': layoutForm.header.layout === opt.value }" @click="selectLayout('header', opt.value)">
|
||||
<div class="layout-card__thumb">
|
||||
<template v-if="opt.value === 'text-left'">
|
||||
<div class="lc-lines">
|
||||
<div class="lc-line" />
|
||||
<div class="lc-line short" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="opt.value === 'text-center'">
|
||||
<div class="lc-lines lc-lines--center">
|
||||
<div class="lc-line" style="width: 85%" />
|
||||
<div class="lc-line short" style="width: 55%; align-self: center" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="lc-lines lc-lines--right">
|
||||
<div class="lc-line" />
|
||||
<div class="lc-line short" style="align-self: flex-end" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<span class="layout-card__label">{{ opt.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor de texto -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-semibold">Texto</label>
|
||||
<Editor ref="headerEditorRef" v-model="layoutForm.header.content" editor-style="min-height:100px;font-size:0.85rem;">
|
||||
<template #toolbar>
|
||||
<span class="ql-formats">
|
||||
<button class="ql-bold" type="button" />
|
||||
<button class="ql-italic" type="button" />
|
||||
<button class="ql-underline" type="button" />
|
||||
</span>
|
||||
<span class="ql-formats">
|
||||
<select class="ql-color" />
|
||||
</span>
|
||||
</template>
|
||||
</Editor>
|
||||
</div>
|
||||
<!-- Editor Jodit — use os botões "▣ Logo Esq./Dir./Centro" na toolbar para inserir layouts prontos -->
|
||||
<JoditEmailEditor
|
||||
v-model="layoutForm.header.content"
|
||||
:min-height="160"
|
||||
:layout-buttons="true"
|
||||
:logo-url="profileLogoUrl"
|
||||
/>
|
||||
|
||||
<!-- Preview -->
|
||||
<div v-if="headerLayoutPreview" class="rounded border border-[var(--surface-border)] bg-white p-3 text-sm text-gray-800">
|
||||
<span class="text-[0.65rem] text-gray-400 uppercase tracking-widest block mb-2">Preview</span>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="headerLayoutPreview" />
|
||||
<div v-if="headerLayoutPreview" class="rounded-lg border border-(--surface-border) bg-(--surface-ground) p-3">
|
||||
<span class="text-[0.65rem] text-(--text-color-secondary) uppercase tracking-widest block mb-2">Preview</span>
|
||||
<div class="rounded border border-(--surface-border) bg-white p-3 text-sm text-gray-800 shadow-sm">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="headerLayoutPreview" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -494,88 +415,21 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<div v-if="layoutForm.footer.enabled" class="px-4 py-4 flex flex-col gap-4">
|
||||
<!-- Cards de layout -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-xs font-semibold">Com logo</p>
|
||||
<div class="flex gap-2">
|
||||
<button v-for="opt in LAYOUT_OPTIONS" :key="opt.value" class="layout-card" :class="{ 'layout-card--active': layoutForm.footer.layout === opt.value }" @click="selectLayout('footer', opt.value)">
|
||||
<div class="layout-card__thumb">
|
||||
<template v-if="opt.value === 'logo-left'">
|
||||
<div class="lc-logo" />
|
||||
<div class="lc-spacer" />
|
||||
<div class="lc-lines">
|
||||
<div class="lc-line" />
|
||||
<div class="lc-line short" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="opt.value === 'logo-right'">
|
||||
<div class="lc-lines">
|
||||
<div class="lc-line" />
|
||||
<div class="lc-line short" />
|
||||
</div>
|
||||
<div class="lc-spacer" />
|
||||
<div class="lc-logo" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="lc-center">
|
||||
<div class="lc-logo" />
|
||||
<div class="lc-line" style="width: 70%; margin-top: 5px" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<span class="layout-card__label">{{ opt.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
<p class="text-xs font-semibold mt-1">Só texto</p>
|
||||
<div class="flex gap-2">
|
||||
<button v-for="opt in TEXT_OPTIONS" :key="opt.value" class="layout-card" :class="{ 'layout-card--active': layoutForm.footer.layout === opt.value }" @click="selectLayout('footer', opt.value)">
|
||||
<div class="layout-card__thumb">
|
||||
<template v-if="opt.value === 'text-left'">
|
||||
<div class="lc-lines">
|
||||
<div class="lc-line" />
|
||||
<div class="lc-line short" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="opt.value === 'text-center'">
|
||||
<div class="lc-lines lc-lines--center">
|
||||
<div class="lc-line" style="width: 85%" />
|
||||
<div class="lc-line short" style="width: 55%; align-self: center" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="lc-lines lc-lines--right">
|
||||
<div class="lc-line" />
|
||||
<div class="lc-line short" style="align-self: flex-end" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<span class="layout-card__label">{{ opt.label }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor de texto -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<label class="text-xs font-semibold">Texto</label>
|
||||
<Editor ref="footerEditorRef" v-model="layoutForm.footer.content" editor-style="min-height:100px;font-size:0.85rem;">
|
||||
<template #toolbar>
|
||||
<span class="ql-formats">
|
||||
<button class="ql-bold" type="button" />
|
||||
<button class="ql-italic" type="button" />
|
||||
<button class="ql-underline" type="button" />
|
||||
</span>
|
||||
<span class="ql-formats">
|
||||
<select class="ql-color" />
|
||||
</span>
|
||||
</template>
|
||||
</Editor>
|
||||
</div>
|
||||
<!-- Editor Jodit — use os botões "▣ Logo Esq./Dir./Centro" na toolbar para inserir layouts prontos -->
|
||||
<JoditEmailEditor
|
||||
v-model="layoutForm.footer.content"
|
||||
:min-height="160"
|
||||
:layout-buttons="true"
|
||||
:logo-url="profileLogoUrl"
|
||||
/>
|
||||
|
||||
<!-- Preview -->
|
||||
<div v-if="footerLayoutPreview" class="rounded border border-[var(--surface-border)] bg-white p-3 text-sm text-gray-800">
|
||||
<span class="text-[0.65rem] text-gray-400 uppercase tracking-widest block mb-2">Preview</span>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="footerLayoutPreview" />
|
||||
<div v-if="footerLayoutPreview" class="rounded-lg border border-(--surface-border) bg-(--surface-ground) p-3">
|
||||
<span class="text-[0.65rem] text-(--text-color-secondary) uppercase tracking-widest block mb-2">Preview</span>
|
||||
<div class="rounded border border-(--surface-border) bg-white p-3 text-sm text-gray-800 shadow-sm">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-html="footerLayoutPreview" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -616,35 +470,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
<div class="px-4 py-3 flex flex-col gap-3">
|
||||
<template v-if="form.use_custom_body">
|
||||
<Editor ref="editorRef" v-model="form.body_html" editor-style="min-height:200px;font-size:0.85rem;">
|
||||
<template #toolbar>
|
||||
<span class="ql-formats">
|
||||
<select class="ql-header">
|
||||
<option value="1">Título</option>
|
||||
<option value="2">Subtítulo</option>
|
||||
<option selected>Normal</option>
|
||||
</select>
|
||||
</span>
|
||||
<span class="ql-formats">
|
||||
<button class="ql-bold" type="button" />
|
||||
<button class="ql-italic" type="button" />
|
||||
<button class="ql-underline" type="button" />
|
||||
</span>
|
||||
<span class="ql-formats">
|
||||
<select class="ql-align" />
|
||||
<select class="ql-color" />
|
||||
<select class="ql-background" />
|
||||
</span>
|
||||
<span class="ql-formats">
|
||||
<button class="ql-list" value="ordered" type="button" />
|
||||
<button class="ql-list" value="bullet" type="button" />
|
||||
</span>
|
||||
<span class="ql-formats">
|
||||
<button class="ql-link" type="button" />
|
||||
<button class="ql-clean" type="button" />
|
||||
</span>
|
||||
</template>
|
||||
</Editor>
|
||||
<JoditEmailEditor ref="editorRef" v-model="form.body_html" :min-height="220" />
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-xs text-[var(--text-color-secondary)]">Inserir variável no cursor:</span>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
@@ -660,11 +486,6 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Override ativo -->
|
||||
<div class="flex items-center gap-2">
|
||||
<ToggleSwitch v-model="form.enabled" inputId="sw-enabled" />
|
||||
<label for="sw-enabled" class="text-sm cursor-pointer select-none">Override ativo</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
@@ -699,104 +520,4 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ── Layout cards ───────────────────────────────────────── */
|
||||
.layout-card {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 8px;
|
||||
border: 1.5px solid var(--surface-border);
|
||||
border-radius: 8px;
|
||||
background: var(--surface-card);
|
||||
cursor: pointer;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s,
|
||||
background 0.15s;
|
||||
}
|
||||
.layout-card:hover {
|
||||
border-color: color-mix(in srgb, var(--primary-color, #6366f1) 50%, transparent);
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 4%, var(--surface-card));
|
||||
}
|
||||
.layout-card--active {
|
||||
border-color: var(--primary-color, #6366f1);
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 8%, var(--surface-card));
|
||||
box-shadow: 0 0 0 3px color-mix(in srgb, var(--primary-color, #6366f1) 15%, transparent);
|
||||
}
|
||||
.layout-card__thumb {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
height: 38px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 5px;
|
||||
padding: 6px;
|
||||
background: #f9fafb;
|
||||
}
|
||||
.layout-card__label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-color-secondary);
|
||||
text-align: center;
|
||||
line-height: 1.3;
|
||||
}
|
||||
.layout-card--active .layout-card__label {
|
||||
color: var(--primary-color, #6366f1);
|
||||
}
|
||||
|
||||
/* Elementos internos dos cards */
|
||||
.lc-logo {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 35%, #e5e7eb);
|
||||
border: 1px solid color-mix(in srgb, var(--primary-color, #6366f1) 20%, transparent);
|
||||
}
|
||||
.lc-spacer {
|
||||
flex: 1;
|
||||
min-width: 4px;
|
||||
}
|
||||
.lc-lines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
flex: 1;
|
||||
}
|
||||
.lc-lines--center {
|
||||
align-items: center;
|
||||
}
|
||||
.lc-lines--right {
|
||||
align-items: flex-end;
|
||||
}
|
||||
.lc-line {
|
||||
height: 3px;
|
||||
background: #d1d5db;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.lc-line.short {
|
||||
width: 60%;
|
||||
}
|
||||
.lc-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 2px;
|
||||
}
|
||||
.lc-center .lc-logo {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.lc-center .lc-line {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Esconde botão de imagem do Quill em todos os editores desta página */
|
||||
:deep(.ql-image) {
|
||||
display: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -378,6 +378,42 @@ function confirmRestoreTemplate(tpl) {
|
||||
});
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// ABA 2 — Emojis rápidos para o guia de formatação
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
|
||||
const QUICK_EMOJIS = [
|
||||
{ char: '📅', label: 'Calendário' },
|
||||
{ char: '⏰', label: 'Relógio / Lembrete' },
|
||||
{ char: '✅', label: 'Confirmado' },
|
||||
{ char: '❌', label: 'Cancelado' },
|
||||
{ char: '🔔', label: 'Notificação' },
|
||||
{ char: '💬', label: 'Mensagem' },
|
||||
{ char: '💙', label: 'Cuidado / Saúde' },
|
||||
{ char: '🌿', label: 'Bem-estar' },
|
||||
{ char: '🤝', label: 'Parceria / Encontro' },
|
||||
{ char: '📋', label: 'Formulário / Triagem' },
|
||||
{ char: '💰', label: 'Financeiro' },
|
||||
{ char: '🔗', label: 'Link' },
|
||||
{ char: '📍', label: 'Local' },
|
||||
{ char: '📞', label: 'Telefone' },
|
||||
{ char: '🏥', label: 'Clínica' },
|
||||
{ char: '🧠', label: 'Psicologia' },
|
||||
{ char: '👤', label: 'Paciente' },
|
||||
{ char: '🌟', label: 'Destaque' },
|
||||
{ char: '⚠️', label: 'Atenção' },
|
||||
{ char: '➡️', label: 'Seta / Próximo passo' },
|
||||
{ char: '🗓️', label: 'Agenda' },
|
||||
{ char: '🕐', label: 'Hora' },
|
||||
{ char: '📩', label: 'Recebido' },
|
||||
{ char: '🔄', label: 'Reagendamento' }
|
||||
];
|
||||
|
||||
function copyEmoji(char) {
|
||||
navigator.clipboard?.writeText(char).catch(() => {});
|
||||
toast.add({ severity: 'info', summary: `${char} copiado!`, life: 1500 });
|
||||
}
|
||||
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
// ABA 3 — Logs de envio
|
||||
// ══════════════════════════════════════════════════════════════
|
||||
@@ -533,47 +569,152 @@ onBeforeUnmount(() => {
|
||||
|
||||
<!-- ══ ABA 2 — Templates ══════════════════════════════════ -->
|
||||
<TabPanel :value="1">
|
||||
<div class="flex flex-col gap-3 pt-3">
|
||||
<!-- Skeleton loading -->
|
||||
<template v-if="templatesLoading">
|
||||
<div v-for="n in 4" :key="n" class="border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] p-4">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<Skeleton width="5rem" height="1.4rem" border-radius="999px" />
|
||||
<Skeleton width="10rem" height="1rem" />
|
||||
<div class="flex gap-4 pt-3 items-start">
|
||||
|
||||
<!-- ── Coluna esquerda: cards de templates (65%) ── -->
|
||||
<div class="flex flex-col gap-3 min-w-0" style="flex: 0 0 65%;">
|
||||
<!-- Skeleton loading -->
|
||||
<template v-if="templatesLoading">
|
||||
<div v-for="n in 4" :key="n" class="border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] p-4">
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
<Skeleton width="5rem" height="1.4rem" border-radius="999px" />
|
||||
<Skeleton width="10rem" height="1rem" />
|
||||
</div>
|
||||
<Skeleton width="100%" height="5rem" class="mb-2" />
|
||||
<div class="flex gap-1">
|
||||
<Skeleton v-for="i in 3" :key="i" width="6rem" height="1.6rem" border-radius="999px" />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton width="100%" height="5rem" class="mb-2" />
|
||||
<div class="flex gap-1">
|
||||
<Skeleton v-for="i in 3" :key="i" width="6rem" height="1.6rem" border-radius="999px" />
|
||||
</template>
|
||||
|
||||
<!-- Cards de templates -->
|
||||
<div v-else v-for="tpl in templates" :key="tpl.key" class="border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] p-4 flex flex-col gap-3">
|
||||
<!-- Header do card -->
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="font-semibold text-sm">{{ tpl.label }}</span>
|
||||
<Tag :value="tpl.type" :severity="tpl.type_severity" class="text-[0.65rem]" />
|
||||
<Tag v-if="tpl.is_custom" value="Personalizado" severity="success" class="text-[0.65rem]" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Cards de templates -->
|
||||
<div v-else v-for="tpl in templates" :key="tpl.key" class="border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] p-4 flex flex-col gap-3">
|
||||
<!-- Header do card -->
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="font-semibold text-sm">{{ tpl.label }}</span>
|
||||
<Tag :value="tpl.type" :severity="tpl.type_severity" class="text-[0.65rem]" />
|
||||
<Tag v-if="tpl.is_custom" value="Personalizado" severity="success" class="text-[0.65rem]" />
|
||||
</div>
|
||||
<!-- Textarea editável -->
|
||||
<Textarea :ref="(el) => setTextareaRef(tpl.key, el)" v-model="tpl.body_text" rows="5" auto-resize class="w-full text-sm font-mono" />
|
||||
|
||||
<!-- Textarea editável -->
|
||||
<Textarea :ref="(el) => setTextareaRef(tpl.key, el)" v-model="tpl.body_text" rows="5" auto-resize class="w-full text-sm font-mono" />
|
||||
|
||||
<!-- Variáveis clicáveis -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-xs text-[var(--text-color-secondary)]">Inserir variável no cursor:</span>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<Button v-for="v in tpl.variables" :key="v" :label="`{{${v}}}`" size="small" severity="secondary" outlined class="font-mono !text-[0.68rem] !py-1 !px-2" @click="insertVariable(tpl.key, v)" />
|
||||
<!-- Variáveis clicáveis -->
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<span class="text-xs text-[var(--text-color-secondary)]">Inserir variável no cursor:</span>
|
||||
<div class="flex flex-wrap gap-1.5">
|
||||
<Button v-for="v in tpl.variables" :key="v" :label="`{{${v}}}`" size="small" severity="secondary" outlined class="font-mono !text-[0.68rem] !py-1 !px-2" @click="insertVariable(tpl.key, v)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ações -->
|
||||
<div class="flex items-center gap-2 justify-end">
|
||||
<Button v-if="isTemplateModified(tpl)" label="Restaurar original" icon="pi pi-undo" size="small" severity="secondary" outlined @click="confirmRestoreTemplate(tpl)" />
|
||||
<Button label="Salvar" icon="pi pi-check" size="small" :loading="templateSaving[tpl.key]" :disabled="templateSaving[tpl.key]" @click="saveTemplate(tpl)" />
|
||||
<!-- Ações -->
|
||||
<div class="flex items-center gap-2 justify-end">
|
||||
<Button v-if="isTemplateModified(tpl)" label="Restaurar original" icon="pi pi-undo" size="small" severity="secondary" outlined @click="confirmRestoreTemplate(tpl)" />
|
||||
<Button label="Salvar" icon="pi pi-check" size="small" :loading="templateSaving[tpl.key]" :disabled="templateSaving[tpl.key]" @click="saveTemplate(tpl)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Coluna direita: guia de formatação (35%) ── -->
|
||||
<div class="flex flex-col gap-3 sticky top-4" style="flex: 0 0 35%;">
|
||||
<div class="border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] p-4 flex flex-col gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="pi pi-book text-[var(--primary-color)]" />
|
||||
<span class="font-semibold text-sm">Guia de formatação</span>
|
||||
</div>
|
||||
|
||||
<!-- Formatação oficial -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-1.5 mb-1">
|
||||
<span class="text-[0.7rem] font-semibold uppercase tracking-wider text-[var(--text-color-secondary)]">Formatação oficial</span>
|
||||
<div class="flex-1 h-px bg-[var(--surface-border)]" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-mono text-xs bg-[var(--surface-ground)] px-2 py-0.5 rounded text-[var(--text-color-secondary)]">*texto*</span>
|
||||
<span class="text-xs font-bold">Negrito</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-mono text-xs bg-[var(--surface-ground)] px-2 py-0.5 rounded text-[var(--text-color-secondary)]">_texto_</span>
|
||||
<span class="text-xs italic">Itálico</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-mono text-xs bg-[var(--surface-ground)] px-2 py-0.5 rounded text-[var(--text-color-secondary)]">~texto~</span>
|
||||
<span class="text-xs line-through">Tachado</span>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="font-mono text-xs bg-[var(--surface-ground)] px-2 py-0.5 rounded text-[var(--text-color-secondary)]">`texto`</span>
|
||||
<span class="text-xs font-mono bg-[var(--surface-ground)] px-1 rounded">Monoespaçado</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Efeitos extras Unicode -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-1.5 mb-1">
|
||||
<span class="text-[0.7rem] font-semibold uppercase tracking-wider text-[var(--text-color-secondary)]">Efeitos extras (Unicode)</span>
|
||||
<div class="flex-1 h-px bg-[var(--surface-border)]" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-[var(--text-color-secondary)]">Negrito Unicode</span>
|
||||
<span class="text-xs">𝙝𝙤𝙡𝙖</span>
|
||||
</div>
|
||||
<span class="text-[0.65rem] text-[var(--text-color-secondary)] opacity-70">Copie de sites de "font generator"</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-[var(--text-color-secondary)]">Cursiva Unicode</span>
|
||||
<span class="text-xs">𝓽𝓮𝔁𝓽𝓸</span>
|
||||
</div>
|
||||
<span class="text-[0.65rem] text-[var(--text-color-secondary)] opacity-70">Cada letra é um caractere diferente</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-[var(--text-color-secondary)]">Small Caps</span>
|
||||
<span class="text-xs">ᴛᴇxᴛᴏ</span>
|
||||
</div>
|
||||
<span class="text-[0.65rem] text-[var(--text-color-secondary)] opacity-70">Bom para títulos curtos</span>
|
||||
</div>
|
||||
<div class="flex flex-col gap-0.5">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<span class="text-xs text-[var(--text-color-secondary)]">Sublinhado</span>
|
||||
<span class="text-xs">t̲e̲x̲t̲o̲</span>
|
||||
</div>
|
||||
<span class="text-[0.65rem] text-[var(--text-color-secondary)] opacity-70">U+0332 após cada letra</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Emojis mais usados -->
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-1.5 mb-1">
|
||||
<span class="text-[0.7rem] font-semibold uppercase tracking-wider text-[var(--text-color-secondary)]">Emojis mais usados</span>
|
||||
<div class="flex-1 h-px bg-[var(--surface-border)]" />
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1">
|
||||
<button
|
||||
v-for="emoji in QUICK_EMOJIS"
|
||||
:key="emoji.char"
|
||||
v-tooltip.top="emoji.label"
|
||||
class="text-base leading-none p-1 rounded hover:bg-[var(--surface-hover)] transition-colors cursor-pointer border-0 bg-transparent"
|
||||
@click="copyEmoji(emoji.char)"
|
||||
>{{ emoji.char }}</button>
|
||||
</div>
|
||||
<span class="text-[0.65rem] text-[var(--text-color-secondary)] opacity-70">Clique para copiar</span>
|
||||
</div>
|
||||
|
||||
<!-- Dica -->
|
||||
<div class="flex items-start gap-2 px-3 py-2.5 rounded-lg bg-[var(--surface-ground)] border border-[var(--surface-border)]">
|
||||
<i class="pi pi-lightbulb text-amber-500 text-xs mt-0.5 shrink-0" />
|
||||
<p class="text-[0.68rem] text-[var(--text-color-secondary)] m-0 leading-relaxed">
|
||||
Use <strong>*negrito*</strong> para destacar horários e datas. Evite excesso de formatação — mensagens simples têm maior taxa de leitura.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</TabPanel>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user