Layout 100%, Notificações, SetupWizard

This commit is contained in:
Leonardo
2026-03-17 21:08:14 -03:00
parent 84d65e49c0
commit 66f67cd40f
77 changed files with 35823 additions and 15023 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -2,137 +2,149 @@
<Toast />
<!-- Sentinel -->
<div ref="headerSentinelRef" class="extlink-sentinel" />
<div ref="headerSentinelRef" class="h-px" />
<!-- Hero sticky -->
<div ref="headerEl" class="extlink-hero mx-3 md:mx-5 mb-4" :class="{ 'extlink-hero--stuck': headerStuck }">
<div class="extlink-hero__blobs" aria-hidden="true">
<div class="extlink-hero__blob extlink-hero__blob--1" />
<div class="extlink-hero__blob extlink-hero__blob--2" />
<!--
HERO sticky
-->
<section
ref="headerEl"
class="sticky mx-3 md:mx-4 mb-3 z-20 overflow-hidden rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] px-3 py-2.5 transition-[border-radius] duration-200"
:class="{ 'rounded-tl-none rounded-tr-none': headerStuck }"
:style="{ top: 'var(--layout-sticky-top, 56px)' }"
>
<!-- Blobs -->
<div class="absolute inset-0 pointer-events-none overflow-hidden" aria-hidden="true">
<div class="absolute w-64 h-64 -top-16 -right-8 rounded-full blur-[60px] bg-indigo-500/10" />
<div class="absolute w-72 h-72 top-0 -left-16 rounded-full blur-[60px] bg-emerald-400/[0.08]" />
</div>
<!-- Row 1 -->
<div class="extlink-hero__row1">
<div class="extlink-hero__brand">
<div class="extlink-hero__icon"><i class="pi pi-link text-lg" /></div>
<div class="min-w-0">
<div class="extlink-hero__title">Link de Cadastro</div>
<div class="extlink-hero__sub">Compartilhe com o paciente para preencher o pré-cadastro com calma e segurança</div>
<div class="relative z-[1] flex items-center gap-3">
<!-- Brand -->
<div class="flex items-center gap-2 flex-shrink-0">
<div class="grid place-items-center w-9 h-9 rounded-md flex-shrink-0 bg-indigo-500/10 text-indigo-500">
<i class="pi pi-link text-base" />
</div>
<div class="min-w-0 hidden lg:block">
<div class="text-[1rem] font-bold tracking-tight text-[var(--text-color)]">Link de Cadastro</div>
<div class="text-[1rem] text-[var(--text-color-secondary)]">Compartilhe com o paciente para preencher o pré-cadastro</div>
</div>
</div>
<!-- Desktop (1200px) -->
<div class="hidden xl:flex items-center gap-2 shrink-0">
<!-- Status + link rápido desktop -->
<div class="hidden xl:flex flex-1 min-w-0 mx-2 items-center gap-3">
<!-- Badge de status -->
<span
class="inline-flex items-center gap-2 text-xs px-3 py-1.5 rounded-full border transition-colors"
class="inline-flex items-center gap-1.5 text-[0.75rem] px-2.5 py-1 rounded-full border flex-shrink-0 transition-colors"
:class="inviteToken
? 'border-emerald-200 text-emerald-700 bg-emerald-50'
: 'border-[var(--surface-border)] text-[var(--text-color-secondary)] bg-[var(--surface-ground)]'"
>
<span
class="h-2 w-2 rounded-full"
:class="inviteToken ? 'bg-emerald-500 animate-pulse' : 'bg-[var(--text-color-secondary)]'"
/>
<span class="h-1.5 w-1.5 rounded-full" :class="inviteToken ? 'bg-emerald-500 animate-pulse' : 'bg-[var(--text-color-secondary)]'" />
{{ inviteToken ? 'Link ativo' : 'Gerando…' }}
</span>
<Button
label="Gerar novo link"
icon="pi pi-refresh"
severity="secondary"
outlined
class="rounded-full"
:loading="rotating"
@click="rotateLink"
/>
<!-- Link inline -->
<div v-if="!inviteToken" class="flex items-center gap-2 text-[1rem] text-[var(--text-color-secondary)]">
<i class="pi pi-spin pi-spinner" /> Gerando link
</div>
<InputGroup v-else class="max-w-xl">
<InputGroupAddon><i class="pi pi-link" /></InputGroupAddon>
<InputText readonly :value="publicUrl" class="font-mono text-[0.75rem]" />
<Button icon="pi pi-copy" severity="secondary" title="Copiar link" @click="copyLink" />
<Button icon="pi pi-external-link" severity="secondary" title="Abrir no navegador" @click="openLink" />
</InputGroup>
</div>
<!-- Mobile (<1200px) -->
<div class="flex xl:hidden items-center shrink-0">
<!-- Ações desktop -->
<div class="hidden xl:flex items-center gap-1 flex-shrink-0">
<Button label="Gerar novo link" icon="pi pi-refresh" severity="secondary" outlined class="rounded-full" :loading="rotating" @click="rotateLink" />
</div>
<!-- Mobile -->
<div class="flex xl:hidden items-center gap-1 flex-shrink-0 ml-auto">
<Button label="Ações" icon="pi pi-ellipsis-v" severity="secondary" size="small" class="rounded-full" @click="(e) => mobileMenuRef.toggle(e)" />
<Menu ref="mobileMenuRef" :model="mobileMenuItems" :popup="true" />
</div>
</div>
</section>
<!-- Divider -->
<Divider class="extlink-hero__divider my-2" />
<!--
CONTEÚDO
-->
<div class="flex flex-col lg:flex-row gap-3 px-3 md:px-4 pb-5">
<!-- Row 2: link rápido (oculto no mobile) -->
<div class="extlink-hero__row2">
<div v-if="!inviteToken" class="flex items-center gap-2 text-sm text-[var(--text-color-secondary)]">
<i class="pi pi-spin pi-spinner text-xs" /> Gerando link
</div>
<InputGroup v-else class="max-w-2xl">
<InputGroupAddon><i class="pi pi-link" /></InputGroupAddon>
<InputText readonly :value="publicUrl" class="font-mono text-xs" />
<Button icon="pi pi-copy" severity="secondary" title="Copiar link" @click="copyLink" />
<Button icon="pi pi-external-link" severity="secondary" title="Abrir no navegador" @click="openLink" />
</InputGroup>
</div>
</div>
<!-- Conteúdo -->
<div class="flex flex-col lg:flex-row gap-4 px-3 md:px-5 mb-5">
<!-- Esquerda: ações do link -->
<div class="flex-1 min-w-0 flex flex-col gap-4">
<!-- ESQUERDA: link + mensagem -->
<div class="flex-1 min-w-0 flex flex-col gap-3">
<!-- Card principal: link -->
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
<div class="p-5 border-b border-[var(--surface-border)] flex items-center justify-between gap-3 flex-wrap">
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] overflow-hidden">
<!-- Header da seção -->
<div class="flex items-center justify-between gap-3 flex-wrap px-4 pt-3.5 pb-3 border-b border-[var(--surface-border,#e2e8f0)]">
<div>
<div class="font-semibold text-[var(--text-color)]">Seu link público</div>
<div class="text-sm text-[var(--text-color-secondary)] mt-0.5">Envie ao paciente por WhatsApp, e-mail ou mensagem direta</div>
<div class="text-[1rem] text-[var(--text-color-secondary)] mt-0.5">Envie ao paciente por WhatsApp, e-mail ou mensagem direta</div>
</div>
<span
class="inline-flex items-center gap-2 text-xs px-3 py-1.5 rounded-full border"
class="inline-flex items-center gap-1.5 text-[0.75rem] px-2.5 py-1 rounded-full border flex-shrink-0"
:class="inviteToken
? 'border-emerald-200 text-emerald-700 bg-emerald-50'
: 'border-[var(--surface-border)] text-[var(--text-color-secondary)] bg-[var(--surface-ground)]'"
>
<span class="h-2 w-2 rounded-full" :class="inviteToken ? 'bg-emerald-500 animate-pulse' : 'bg-[var(--text-color-secondary)]'" />
<span class="h-1.5 w-1.5 rounded-full" :class="inviteToken ? 'bg-emerald-500 animate-pulse' : 'bg-[var(--text-color-secondary)]'" />
{{ inviteToken ? 'Ativo' : 'Gerando…' }}
</span>
</div>
<div class="p-5 space-y-4">
<div class="p-4 flex flex-col gap-4">
<!-- Skeleton -->
<div v-if="!inviteToken" class="space-y-3">
<div class="h-10 rounded-xl bg-[var(--surface-ground)] animate-pulse" />
<div class="h-10 rounded-xl bg-[var(--surface-ground)] animate-pulse" />
<div v-if="!inviteToken" class="flex flex-col gap-3">
<div class="h-10 rounded-md bg-[var(--surface-ground,#f8fafc)] animate-pulse" />
<div class="h-10 rounded-md bg-[var(--surface-ground,#f8fafc)] animate-pulse" />
</div>
<div v-else class="space-y-4">
<!-- Link com ações -->
<div v-else class="flex flex-col gap-4">
<!-- InputGroup do link -->
<InputGroup>
<InputGroupAddon><i class="pi pi-link" /></InputGroupAddon>
<InputText readonly :value="publicUrl" class="font-mono text-xs" />
<InputText readonly :value="publicUrl" class="font-mono text-[0.75rem]" />
<Button icon="pi pi-copy" severity="secondary" title="Copiar" @click="copyLink" />
<Button icon="pi pi-external-link" severity="secondary" title="Abrir" @click="openLink" />
</InputGroup>
<div class="text-xs text-[var(--text-color-secondary)]">
Token: <span class="font-mono select-all">{{ inviteToken }}</span>
<div class="text-[0.75rem] text-[var(--text-color-secondary)]">
Token: <span class="font-mono select-all opacity-60">{{ inviteToken }}</span>
</div>
<!-- CTAs rápidas -->
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
<button class="extlink-cta-btn" @click="copyLink">
<div class="extlink-cta-btn__icon bg-[color-mix(in_srgb,var(--p-primary-500,#6366f1)_12%,transparent)] text-[var(--p-primary-500,#6366f1)]">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2.5">
<!-- Copiar link -->
<button
class="flex items-center gap-3 px-3.5 py-3 rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-ground,#f8fafc)] cursor-pointer text-left transition-[background,box-shadow,transform] duration-150 hover:bg-[var(--surface-hover,#f1f5f9)] hover:shadow-[0_2px_12px_rgba(0,0,0,0.06)] hover:-translate-y-px active:translate-y-0"
@click="copyLink"
>
<div class="grid place-items-center w-9 h-9 rounded-md flex-shrink-0 bg-indigo-500/10 text-indigo-500">
<i class="pi pi-copy" />
</div>
<div class="text-left min-w-0">
<div class="font-semibold text-sm text-[var(--text-color)]">Copiar link</div>
<div class="text-xs text-[var(--text-color-secondary)]">Cole no WhatsApp ou e-mail</div>
<div class="min-w-0">
<div class="font-semibold text-[1rem] text-[var(--text-color)]">Copiar link</div>
<div class="text-[0.75rem] text-[var(--text-color-secondary)]">Cole no WhatsApp ou e-mail</div>
</div>
</button>
<button class="extlink-cta-btn" @click="copyInviteMessage">
<div class="extlink-cta-btn__icon bg-emerald-500/10 text-emerald-600">
<!-- Copiar mensagem pronta -->
<button
class="flex items-center gap-3 px-3.5 py-3 rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-ground,#f8fafc)] cursor-pointer text-left transition-[background,box-shadow,transform] duration-150 hover:bg-[var(--surface-hover,#f1f5f9)] hover:shadow-[0_2px_12px_rgba(0,0,0,0.06)] hover:-translate-y-px active:translate-y-0"
@click="copyInviteMessage"
>
<div class="grid place-items-center w-9 h-9 rounded-md flex-shrink-0 bg-emerald-500/10 text-emerald-600">
<i class="pi pi-comment" />
</div>
<div class="text-left min-w-0">
<div class="font-semibold text-sm text-[var(--text-color)]">Copiar mensagem pronta</div>
<div class="text-xs text-[var(--text-color-secondary)]">Texto formatado com o link incluso</div>
<div class="min-w-0">
<div class="font-semibold text-[1rem] text-[var(--text-color)]">Copiar mensagem pronta</div>
<div class="text-[0.75rem] text-[var(--text-color-secondary)]">Texto formatado com o link incluso</div>
</div>
</button>
</div>
@@ -146,84 +158,66 @@
</div>
<!-- Mensagem pronta -->
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] p-4">
<div class="font-semibold text-[var(--text-color)] flex items-center gap-2 mb-1">
<i class="pi pi-comment text-sm text-[var(--text-color-secondary)]" />
<i class="pi pi-comment text-[1rem] text-[var(--text-color-secondary)]" />
Mensagem pronta para envio
</div>
<div class="text-sm text-[var(--text-color-secondary)] mb-3">Copie e cole ao enviar o link ao paciente:</div>
<div class="rounded-xl bg-[var(--surface-ground)] border border-[var(--surface-border)] p-4 text-sm text-[var(--text-color)] leading-relaxed">
<div class="text-[1rem] text-[var(--text-color-secondary)] mb-3">Copie e cole ao enviar o link ao paciente:</div>
<div class="rounded-md bg-[var(--surface-ground,#f8fafc)] border border-[var(--surface-border,#e2e8f0)] p-4 text-[1rem] text-[var(--text-color)] leading-relaxed">
Olá! Segue o link para seu pré-cadastro. Preencha com calma campos opcionais podem ficar em branco:
<span class="block mt-2 font-mono text-xs break-all text-[var(--text-color-secondary)]">{{ publicUrl || '…aguardando link…' }}</span>
<span class="block mt-2 font-mono text-[0.72rem] break-all text-[var(--text-color-secondary)]">{{ publicUrl || '…aguardando link…' }}</span>
</div>
<div class="mt-3">
<Button
icon="pi pi-copy"
label="Copiar mensagem"
severity="secondary"
outlined
class="rounded-full"
:disabled="!publicUrl"
@click="copyInviteMessage"
/>
<Button icon="pi pi-copy" label="Copiar mensagem" severity="secondary" outlined class="rounded-full" :disabled="!publicUrl" @click="copyInviteMessage" />
</div>
</div>
</div>
<!-- Direita: instruções -->
<div class="lg:w-80 shrink-0 flex flex-col gap-4">
<!-- DIREITA: instruções -->
<div class="w-full lg:w-[272px] lg:flex-shrink-0 flex flex-col gap-3">
<!-- Como funciona -->
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
<div class="p-5 border-b border-[var(--surface-border)]">
<div class="font-semibold text-[var(--text-color)]">Como funciona</div>
<div class="text-sm text-[var(--text-color-secondary)] mt-0.5">Simples e sem fricção para o paciente</div>
</div>
<div class="p-5">
<ol class="space-y-4">
<li class="flex gap-3">
<div class="extlink-step shrink-0">1</div>
<div class="min-w-0">
<div class="font-semibold text-sm text-[var(--text-color)]">Você envia o link</div>
<div class="text-sm text-[var(--text-color-secondary)] mt-0.5">Por WhatsApp, e-mail ou mensagem direta.</div>
</div>
</li>
<li class="flex gap-3">
<div class="extlink-step shrink-0">2</div>
<div class="min-w-0">
<div class="font-semibold text-sm text-[var(--text-color)]">O paciente preenche</div>
<div class="text-sm text-[var(--text-color-secondary)] mt-0.5">Campos opcionais podem ficar em branco. Menos fricção, mais adesão.</div>
</div>
</li>
<li class="flex gap-3">
<div class="extlink-step shrink-0">3</div>
<div class="min-w-0">
<div class="font-semibold text-sm text-[var(--text-color)]">Você recebe e converte</div>
<div class="text-sm text-[var(--text-color-secondary)] mt-0.5">O cadastro aparece em "Cadastros recebidos". Revise e converta em paciente quando quiser.</div>
</div>
</li>
</ol>
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] overflow-hidden">
<div class="flex items-center gap-2.5 px-3.5 pt-3 pb-2.5 border-b border-[var(--surface-border,#f1f5f9)]">
<div class="w-8 h-8 rounded-md flex items-center justify-center flex-shrink-0 bg-indigo-500/10 text-indigo-500">
<i class="pi pi-list-check text-[0.9rem]" />
</div>
<div class="min-w-0">
<span class="block text-[1rem] font-bold text-[var(--text-color)]">Como funciona</span>
<span class="block text-[0.72rem] text-[var(--text-color-secondary)]">Simples e sem fricção para o paciente</span>
</div>
</div>
<ol class="flex flex-col divide-y divide-[var(--surface-border,#f1f5f9)]">
<li v-for="step in howItWorks" :key="step.n" class="flex items-start gap-3 px-3.5 py-3">
<div class="grid place-items-center w-7 h-7 rounded-md flex-shrink-0 bg-indigo-500/10 text-indigo-500 text-[0.75rem] font-bold mt-px">
{{ step.n }}
</div>
<div class="min-w-0">
<div class="font-semibold text-[1rem] text-[var(--text-color)]">{{ step.title }}</div>
<div class="text-[0.75rem] text-[var(--text-color-secondary)] mt-0.5 leading-relaxed">{{ step.desc }}</div>
</div>
</li>
</ol>
</div>
<!-- Boas práticas -->
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-5">
<div class="font-semibold text-[var(--text-color)] flex items-center gap-2 mb-3">
<i class="pi pi-shield text-sm text-[var(--text-color-secondary)]" />
Boas práticas
<div class="rounded-md border border-[var(--surface-border,#e2e8f0)] bg-[var(--surface-card,#fff)] overflow-hidden">
<div class="flex items-center gap-2.5 px-3.5 pt-3 pb-2.5 border-b border-[var(--surface-border,#f1f5f9)]">
<div class="w-8 h-8 rounded-md flex items-center justify-center flex-shrink-0 bg-emerald-500/10 text-emerald-600">
<i class="pi pi-shield text-[0.9rem]" />
</div>
<div class="min-w-0">
<span class="block text-[1rem] font-bold text-[var(--text-color)]">Boas práticas</span>
<span class="block text-[0.72rem] text-[var(--text-color-secondary)]">Segurança e privacidade</span>
</div>
</div>
<ul class="space-y-2.5">
<li class="flex gap-2 text-sm text-[var(--text-color-secondary)]">
<i class="pi pi-check text-emerald-500 mt-0.5 shrink-0" />
<span>Gere um novo link se suspeitar que ele foi repassado indevidamente.</span>
</li>
<li class="flex gap-2 text-sm text-[var(--text-color-secondary)]">
<i class="pi pi-check text-emerald-500 mt-0.5 shrink-0" />
<span>Informe o paciente que campos opcionais podem ficar em branco.</span>
</li>
<li class="flex gap-2 text-sm text-[var(--text-color-secondary)]">
<i class="pi pi-check text-emerald-500 mt-0.5 shrink-0" />
<span>Evite divulgar em público; é um link para compartilhamento individual.</span>
<ul class="flex flex-col divide-y divide-[var(--surface-border,#f1f5f9)]">
<li v-for="tip in goodPractices" :key="tip" class="flex items-start gap-2.5 px-3.5 py-2.5">
<i class="pi pi-check text-emerald-500 mt-0.5 flex-shrink-0 text-[1rem]" />
<span class="text-[1rem] text-[var(--text-color-secondary)] leading-relaxed">{{ tip }}</span>
</li>
</ul>
</div>
@@ -241,26 +235,39 @@ import { supabase } from '@/lib/supabase/client'
const toast = useToast()
const inviteToken = ref('')
const rotating = ref(false)
const rotating = ref(false)
// ── Hero sticky ───────────────────────────────────────────
const headerEl = ref(null)
// ── Hero sticky ───────────────────────────────────────────
const headerEl = ref(null)
const headerSentinelRef = ref(null)
const headerStuck = ref(false)
const headerStuck = ref(false)
let _observer = null
// ── Mobile menu ───────────────────────────────────────────
// ── Mobile menu ───────────────────────────────────────────
const mobileMenuRef = ref(null)
const mobileMenuItems = computed(() => [
{ label: 'Copiar link', icon: 'pi pi-copy', command: () => copyLink(), disabled: !inviteToken.value },
{ label: 'Copiar mensagem', icon: 'pi pi-comment', command: () => copyInviteMessage(), disabled: !inviteToken.value },
{ label: 'Abrir no navegador', icon: 'pi pi-external-link', command: () => openLink(), disabled: !inviteToken.value },
{ label: 'Copiar link', icon: 'pi pi-copy', command: () => copyLink(), disabled: !inviteToken.value },
{ label: 'Copiar mensagem', icon: 'pi pi-comment', command: () => copyInviteMessage(), disabled: !inviteToken.value },
{ label: 'Abrir no navegador', icon: 'pi pi-external-link', command: () => openLink(), disabled: !inviteToken.value },
{ separator: true },
{ label: 'Gerar novo link', icon: 'pi pi-refresh', command: () => rotateLink() }
{ label: 'Gerar novo link', icon: 'pi pi-refresh', command: () => rotateLink() }
])
// ── URL base ────────────────────────────────────────────────
// ── Conteúdo estático ─────────────────────────────────────
const howItWorks = [
{ n: 1, title: 'Você envia o link', desc: 'Por WhatsApp, e-mail ou mensagem direta.' },
{ n: 2, title: 'O paciente preenche', desc: 'Campos opcionais podem ficar em branco. Menos fricção, mais adesão.' },
{ n: 3, title: 'Você recebe e converte', desc: 'O cadastro aparece em "Cadastros recebidos". Revise e converta em paciente quando quiser.' },
]
const goodPractices = [
'Gere um novo link se suspeitar que ele foi repassado indevidamente.',
'Informe o paciente que campos opcionais podem ficar em branco.',
'Evite divulgar em público; é um link para compartilhamento individual.',
]
// ── URL base ──────────────────────────────────────────────
const PUBLIC_BASE_URL = ''
const origin = computed(() => {
@@ -273,13 +280,13 @@ const publicUrl = computed(() => {
return `${origin.value}/cadastro/paciente?t=${encodeURIComponent(inviteToken.value)}`
})
// ── Token helpers ───────────────────────────────────────────
function newToken() {
// ── Token helpers ─────────────────────────────────────────
function newToken () {
if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID()
return 'tok_' + Math.random().toString(36).slice(2) + Date.now().toString(36)
}
async function requireUserId() {
async function requireUserId () {
const { data, error } = await supabase.auth.getUser()
if (error) throw error
const uid = data?.user?.id
@@ -287,7 +294,7 @@ async function requireUserId() {
return uid
}
async function loadOrCreateInvite() {
async function loadOrCreateInvite () {
const uid = await requireUserId()
const { data, error } = await supabase
@@ -301,10 +308,7 @@ async function loadOrCreateInvite() {
if (error) throw error
const token = data?.[0]?.token
if (token) {
inviteToken.value = token
return
}
if (token) { inviteToken.value = token; return }
const t = newToken()
const { error: insErr } = await supabase
@@ -315,19 +319,18 @@ async function loadOrCreateInvite() {
inviteToken.value = t
}
async function rotateLink() {
async function rotateLink () {
rotating.value = true
try {
const uid = await requireUserId()
const t = newToken()
const t = newToken()
const rpc = await supabase.rpc('rotate_patient_invite_token', { p_new_token: t })
if (rpc.error) {
const { error: e1 } = await supabase
.from('patient_invites')
.update({ active: false, updated_at: new Date().toISOString() })
.eq('owner_id', uid)
.eq('active', true)
.eq('owner_id', uid).eq('active', true)
if (e1) throw e1
const { error: e2 } = await supabase
@@ -345,7 +348,7 @@ async function rotateLink() {
}
}
async function copyLink() {
async function copyLink () {
try {
if (!publicUrl.value) return
await navigator.clipboard.writeText(publicUrl.value)
@@ -355,12 +358,12 @@ async function copyLink() {
}
}
function openLink() {
function openLink () {
if (!publicUrl.value) return
window.open(publicUrl.value, '_blank', 'noopener')
}
async function copyInviteMessage() {
async function copyInviteMessage () {
try {
if (!publicUrl.value) return
const msg = `Olá! Segue o link para seu pré-cadastro. Preencha com calma — campos opcionais podem ficar em branco:\n${publicUrl.value}`
@@ -387,96 +390,4 @@ onMounted(async () => {
})
onBeforeUnmount(() => { _observer?.disconnect() })
</script>
<style scoped>
/* ── Sentinel ─────────────────────────────────────── */
.extlink-sentinel { height: 1px; }
/* ── Hero ─────────────────────────────────────────── */
.extlink-hero {
position: sticky;
top: var(--layout-sticky-top, 56px);
z-index: 20;
overflow: hidden;
border-radius: 1.75rem;
border: 1px solid var(--surface-border);
background: var(--surface-card);
padding: 1.25rem 1.5rem;
}
.extlink-hero--stuck {
margin-left: 0; margin-right: 0;
border-top-left-radius: 0; border-top-right-radius: 0;
}
/* Blobs decorativos */
.extlink-hero__blobs { position: absolute; inset: 0; pointer-events: none; overflow: hidden; }
.extlink-hero__blob { position: absolute; border-radius: 50%; filter: blur(70px); }
.extlink-hero__blob--1 { width: 18rem; height: 18rem; top: -4rem; right: -3rem; background: rgba(99,102,241,0.10); }
.extlink-hero__blob--2 { width: 20rem; height: 20rem; top: 0.5rem; left: -5rem; background: rgba(16,185,129,0.08); }
/* Linha 1 */
.extlink-hero__row1 {
position: relative; z-index: 1;
display: flex; align-items: center; gap: 1rem;
}
.extlink-hero__brand {
display: flex; align-items: center; gap: 0.75rem;
flex: 1; min-width: 0;
}
.extlink-hero__icon {
display: grid; place-items: center;
width: 2.5rem; height: 2.5rem; border-radius: 0.875rem; flex-shrink: 0;
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 12%, transparent);
color: var(--p-primary-500, #6366f1);
}
.extlink-hero__title { font-size: 1.1rem; font-weight: 700; letter-spacing: -0.02em; color: var(--text-color); }
.extlink-hero__sub { font-size: 0.78rem; color: var(--text-color-secondary); margin-top: 2px; }
/* Linha 2 */
.extlink-hero__row2 {
position: relative; z-index: 1;
display: flex; align-items: center; gap: 0.75rem;
}
@media (max-width: 767px) {
.extlink-hero__divider,
.extlink-hero__row2 { display: none; }
}
/* ── CTA button ───────────────────────────────────── */
.extlink-cta-btn {
display: flex;
align-items: center;
gap: 0.875rem;
padding: 0.875rem 1rem;
border-radius: 1rem;
border: 1px solid var(--surface-border);
background: var(--surface-ground);
cursor: pointer;
transition: background 0.15s ease, box-shadow 0.15s ease, transform 0.1s ease;
text-align: left;
}
.extlink-cta-btn:hover {
background: var(--surface-hover);
box-shadow: 0 2px 12px rgba(0,0,0,0.08);
transform: translateY(-1px);
}
.extlink-cta-btn:active { transform: translateY(0); }
.extlink-cta-btn__icon {
display: grid; place-items: center;
width: 2.25rem; height: 2.25rem;
border-radius: 0.75rem; flex-shrink: 0;
font-size: 1rem;
}
/* ── Step numbers ─────────────────────────────────── */
.extlink-step {
display: grid; place-items: center;
width: 2rem; height: 2rem;
border-radius: 0.625rem;
font-size: 0.8rem; font-weight: 700;
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 12%, transparent);
color: var(--p-primary-500, #6366f1);
}
</style>
</script>
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff