Sistema de Suporte , Documentação

This commit is contained in:
Leonardo
2026-03-16 09:41:18 -03:00
parent f66f6f3fde
commit 84d65e49c0
5 changed files with 1615 additions and 355 deletions
@@ -0,0 +1,801 @@
<!-- src/features/agenda/components/dev/AgendaDevDocs.vue
Documentação técnica da Agenda exibida apenas em modo suporte/dev.
Acessível via SupportDebugBanner botão "Docs". -->
<script setup>
import { ref } from 'vue'
const props = defineProps({
visible: { type: Boolean, default: false }
})
const emit = defineEmits(['update:visible'])
function close () { emit('update:visible', false) }
const activeTab = ref(0)
</script>
<template>
<Dialog
:visible="visible"
@update:visible="emit('update:visible', $event)"
modal
header="📘 Documentação Técnica — Agenda"
:style="{ width: '860px', maxWidth: '96vw' }"
:pt="{ content: { style: 'padding: 0' } }"
>
<TabView v-model:activeIndex="activeTab" class="dev-docs">
<!-- Tab 0: Visão Geral -->
<TabPanel header="Visão Geral">
<div class="dd-section">
<div class="dd-alert">
Este painel é visível <strong>apenas em modo suporte SaaS</strong> ativo.
Nunca é exibido em produção sem um token válido.
</div>
<h3 class="dd-h3">Propósito</h3>
<p class="dd-p">
A <strong>Agenda</strong> é o núcleo do sistema. Permite ao terapeuta gerenciar
sessões, bloqueios, recorrências e compromissos determinísticos tudo integrado
ao sistema de precificação, convênios e agendamento online.
</p>
<h3 class="dd-h3">Stack técnica</h3>
<div class="dd-grid-2">
<div class="dd-card">
<div class="dd-card-title">Frontend</div>
<ul class="dd-list">
<li><span class="dd-tag blue">Vue 3</span> Composition API + <code>&lt;script setup&gt;</code></li>
<li><span class="dd-tag purple">FullCalendar</span> timeGrid / dayGrid + interaction plugin</li>
<li><span class="dd-tag green">PrimeVue</span> Dialog, DataTable, Select, Toast</li>
<li><span class="dd-tag orange">Pinia</span> tenantStore, entitlementsStore, menuStore</li>
</ul>
</div>
<div class="dd-card">
<div class="dd-card-title">Backend</div>
<ul class="dd-list">
<li><span class="dd-tag teal">Supabase</span> PostgreSQL + Row Level Security</li>
<li><span class="dd-tag red">RPCs</span> Funções PL/pgSQL para operações críticas</li>
<li><span class="dd-tag yellow">Realtime</span> Não utilizado na agenda (pull-based)</li>
</ul>
</div>
</div>
<h3 class="dd-h3">Arquitetura em camadas</h3>
<div class="dd-layers">
<div class="dd-layer dd-layer--page">
<strong>Pages</strong>
<span>AgendaTerapeutaPage · AgendaClinicaPage · CompromissosDeterminados</span>
</div>
<div class="dd-layer-arrow"></div>
<div class="dd-layer dd-layer--composable">
<strong>Composables</strong>
<span>useAgendaSettings · useAgendaEvents · useRecurrence · useCommitmentServices · useDeterminedCommitments</span>
</div>
<div class="dd-layer-arrow"></div>
<div class="dd-layer dd-layer--service">
<strong>Services / Repositories</strong>
<span>agendaRepository · agendaClinicRepository · agendaMappers</span>
</div>
<div class="dd-layer-arrow"></div>
<div class="dd-layer dd-layer--db">
<strong>Supabase</strong>
<span>Tables + RPCs + RLS Policies</span>
</div>
</div>
<h3 class="dd-h3">Rotas registradas</h3>
<table class="dd-table">
<thead><tr><th>Path</th><th>Name</th><th>Área</th></tr></thead>
<tbody>
<tr><td><code>/therapist/agenda</code></td><td>therapist-agenda</td><td>Terapeuta</td></tr>
<tr><td><code>/therapist/agenda/recorrencias</code></td><td>therapist-agenda-recorrencias</td><td>Terapeuta</td></tr>
<tr><td><code>/therapist/agenda/compromissos</code></td><td>therapist-agenda-compromissos</td><td>Terapeuta</td></tr>
<tr><td><code>/therapist/agendamentos-recebidos</code></td><td>therapist-agendamentos-recebidos</td><td>Terapeuta</td></tr>
<tr><td><code>/admin/agenda/clinica</code></td><td>admin-agenda-clinica</td><td>Clínica</td></tr>
<tr><td><code>/admin/agenda/compromissos</code></td><td>admin-agenda-compromissos</td><td>Clínica</td></tr>
<tr><td><code>/agendar/:slug</code></td><td>agendador.publico</td><td>Público</td></tr>
</tbody>
</table>
</div>
</TabPanel>
<!-- Tab 1: Tabelas -->
<TabPanel header="Tabelas">
<div class="dd-section">
<p class="dd-p">Todas as tabelas usam <strong>Row Level Security (RLS)</strong> habilitada.</p>
<h3 class="dd-h3">Core</h3>
<table class="dd-table">
<thead><tr><th>Tabela</th><th>Propósito</th><th>Colunas-chave</th></tr></thead>
<tbody>
<tr>
<td><code>agenda_configuracoes</code></td>
<td>Configurações da agenda por owner (terapeuta ou clínica)</td>
<td>owner_id, tenant_id, slot_duration_minutes, start_time, end_time, days_of_week</td>
</tr>
<tr>
<td><code>agenda_eventos</code></td>
<td>Eventos individuais (sessões, bloqueios avulsos)</td>
<td>id, owner_id, tenant_id, patient_id, starts_at, ends_at, status, recurrence_rule_id, tipo</td>
</tr>
<tr>
<td><code>agenda_bloqueios</code></td>
<td>Períodos bloqueados na agenda</td>
<td>owner_id, starts_at, ends_at, motivo, recorrente</td>
</tr>
<tr>
<td><code>agenda_regras_semanais</code></td>
<td>Horários de trabalho semanais por dia</td>
<td>owner_id, day_of_week (06), start_time, end_time</td>
</tr>
</tbody>
</table>
<h3 class="dd-h3">Recorrência</h3>
<table class="dd-table">
<thead><tr><th>Tabela</th><th>Propósito</th><th>Colunas-chave</th></tr></thead>
<tbody>
<tr>
<td><code>recurrence_rules</code></td>
<td>Regra mãe de uma série recorrente</td>
<td>id, owner_id, patient_id, frequency, interval, start_date, end_date, days_of_week, status</td>
</tr>
<tr>
<td><code>recurrence_exceptions</code></td>
<td>Exceções a uma regra (cancelamentos, alterações pontuais)</td>
<td>rule_id, original_date, action (skip | reschedule), new_starts_at</td>
</tr>
<tr>
<td><code>recurrence_rule_services</code></td>
<td>Serviços-template associados a uma regra recorrente</td>
<td>rule_id, service_id, quantity, unit_price</td>
</tr>
</tbody>
</table>
<h3 class="dd-h3">Compromissos e Serviços</h3>
<table class="dd-table">
<thead><tr><th>Tabela</th><th>Propósito</th><th>Colunas-chave</th></tr></thead>
<tbody>
<tr>
<td><code>determined_commitments</code></td>
<td>Tipos de compromisso determinístico (ex: Avaliação, Supervisão)</td>
<td>id, owner_id, tenant_id, name, color, duration_minutes</td>
</tr>
<tr>
<td><code>determined_commitment_fields</code></td>
<td>Campos customizados por tipo de compromisso</td>
<td>commitment_id, field_key, label, type, required</td>
</tr>
<tr>
<td><code>commitment_services</code></td>
<td>Serviços vinculados a um evento específico</td>
<td>evento_id, service_id, quantity, unit_price, insurance_plan_id</td>
</tr>
<tr>
<td><code>services</code></td>
<td>Catálogo de serviços do terapeuta/clínica</td>
<td>id, owner_id, tenant_id, name, default_price, active</td>
</tr>
<tr>
<td><code>professional_pricing</code></td>
<td>Precificação por tipo de compromisso e profissional</td>
<td>owner_id, commitment_type_id, price, insurance_plan_id</td>
</tr>
<tr>
<td><code>insurance_plans</code></td>
<td>Convênios cadastrados</td>
<td>id, owner_id, name, active, notes</td>
</tr>
<tr>
<td><code>insurance_plan_services</code></td>
<td>Serviços cobertos por um convênio</td>
<td>plan_id, service_id, covered_price</td>
</tr>
<tr>
<td><code>patient_discounts</code></td>
<td>Descontos individuais por paciente</td>
<td>owner_id, patient_id, discount_type (pct | fixed), value</td>
</tr>
<tr>
<td><code>financial_exceptions</code></td>
<td>Exceções financeiras globais ou por owner</td>
<td>id, owner_id, scope, rule_key, value</td>
</tr>
</tbody>
</table>
<h3 class="dd-h3">Suporte técnico</h3>
<table class="dd-table">
<thead><tr><th>Tabela</th><th>Propósito</th><th>Colunas-chave</th></tr></thead>
<tbody>
<tr>
<td><code>support_sessions</code></td>
<td>Tokens de acesso temporário gerados pelo SaaS admin</td>
<td>id, tenant_id, token, expires_at, created_at</td>
</tr>
</tbody>
</table>
</div>
</TabPanel>
<!-- Tab 2: Composables -->
<TabPanel header="Composables">
<div class="dd-section">
<p class="dd-p">
Todos os composables ficam em <code>src/features/agenda/composables/</code>.
Cada um é responsável por um domínio isolado.
</p>
<table class="dd-table">
<thead>
<tr><th>Composable</th><th>Responsabilidade</th><th>Retorna / Expõe</th></tr>
</thead>
<tbody>
<tr>
<td><code>useAgendaSettings</code></td>
<td>Carrega e persiste as configurações da agenda (horários, slots)</td>
<td>settings, saveSettings, loading</td>
</tr>
<tr>
<td><code>useAgendaEvents</code></td>
<td>CRUD de eventos em <code>agenda_eventos</code>; mapeia para FullCalendar</td>
<td>events, createEvent, updateEvent, deleteEvent, loadEvents</td>
</tr>
<tr>
<td><code>useRecurrence</code></td>
<td>Geração dinâmica de ocorrências a partir de <code>recurrence_rules</code>; split e cancel de série</td>
<td>rules, loadRules, createRule, splitAt, cancelFrom, exceptions</td>
</tr>
<tr>
<td><code>useCommitmentServices</code></td>
<td>CRUD dos serviços vinculados a um evento (<code>commitment_services</code>)</td>
<td>services, addService, removeService, updateService</td>
</tr>
<tr>
<td><code>useDeterminedCommitments</code></td>
<td>Carrega tipos de compromisso determinístico do tenant</td>
<td>commitments, loading, loadCommitments</td>
</tr>
<tr>
<td><code>useAgendaClinicStaff</code></td>
<td>Carrega membros do staff da clínica via <code>v_tenant_staff</code></td>
<td>staff, loadStaff, loading</td>
</tr>
<tr>
<td><code>useAgendaClinicEvents</code></td>
<td>CRUD de eventos da clínica (visão multi-profissional)</td>
<td>events, loadEvents, createEvent, updateEvent, deleteEvent</td>
</tr>
<tr>
<td><code>useServices</code></td>
<td>CRUD do catálogo de serviços do profissional</td>
<td>services, addService, editService, deleteService</td>
</tr>
<tr>
<td><code>useProfessionalPricing</code></td>
<td>Precificação por tipo de compromisso e convênio</td>
<td>pricing, loadPricing, savePricing</td>
</tr>
<tr>
<td><code>useInsurancePlans</code></td>
<td>CRUD de convênios e seus serviços cobertos</td>
<td>plans, addPlan, editPlan, deletePlan, planServices</td>
</tr>
<tr>
<td><code>usePatientDiscounts</code></td>
<td>Descontos individuais por paciente</td>
<td>discounts, addDiscount, removeDiscount</td>
</tr>
<tr>
<td><code>useFinancialExceptions</code></td>
<td>Exceções financeiras globais ou por owner</td>
<td>exceptions, saveException, deleteException</td>
</tr>
<tr>
<td><code>useFeriados</code></td>
<td>Carrega feriados nacionais/estaduais para bloquear slots</td>
<td>feriados, isHoliday, loadFeriados</td>
</tr>
</tbody>
</table>
<h3 class="dd-h3">Services / Repositories</h3>
<table class="dd-table">
<thead><tr><th>Arquivo</th><th>Propósito</th></tr></thead>
<tbody>
<tr>
<td><code>agendaRepository.js</code></td>
<td>Queries Supabase para terapeuta eventos, bloqueios, configurações</td>
</tr>
<tr>
<td><code>agendaClinicRepository.js</code></td>
<td>Queries Supabase para clínica eventos multi-profissional, staff</td>
</tr>
<tr>
<td><code>agendaMappers.js</code></td>
<td>
Converte objetos do banco para formato FullCalendar.<br>
<code>mapAgendaEventosToCalendarEvents()</code> · <code>buildWeeklyBreakBackgroundEvents()</code> · <code>minutesToDuration()</code>
</td>
</tr>
</tbody>
</table>
</div>
</TabPanel>
<!-- Tab 3: RPCs -->
<TabPanel header="RPCs">
<div class="dd-section">
<p class="dd-p">
Funções PL/pgSQL executadas via <code>supabase.rpc()</code>.
Todas validam permissões server-side sem bypass pelo frontend.
</p>
<div class="dd-rpc-card">
<div class="dd-rpc-name">validate_support_session</div>
<div class="dd-rpc-desc">Valida um token de suporte e retorna o tenant associado. Único ponto de ativação do modo debug.</div>
<div class="dd-rpc-row">
<span class="dd-rpc-label">Parâmetros</span>
<code>p_token TEXT</code>
</div>
<div class="dd-rpc-row">
<span class="dd-rpc-label">Retorna</span>
<code>&#123; valid: boolean, tenant_id: uuid &#125;</code>
</div>
<div class="dd-rpc-row">
<span class="dd-rpc-label">Chamado em</span>
<code>supportDebugStore.validateAndActivate(token)</code>
</div>
</div>
<div class="dd-rpc-card">
<div class="dd-rpc-name">create_support_session</div>
<div class="dd-rpc-desc">Cria uma sessão de suporte com TTL. Requer role saas_admin. Valida e insere em <code>support_sessions</code>.</div>
<div class="dd-rpc-row">
<span class="dd-rpc-label">Parâmetros</span>
<code>p_tenant_id UUID, p_ttl_minutes INT</code>
</div>
<div class="dd-rpc-row">
<span class="dd-rpc-label">Retorna</span>
<code>&#123; token: text, expires_at: timestamptz, session_id: uuid &#125;</code>
</div>
<div class="dd-rpc-row">
<span class="dd-rpc-label">Chamado em</span>
<code>supportSessionService.createSupportSession()</code>
</div>
</div>
<div class="dd-rpc-card">
<div class="dd-rpc-name">revoke_support_session</div>
<div class="dd-rpc-desc">Invalida imediatamente um token de suporte, definindo <code>expires_at = now()</code>.</div>
<div class="dd-rpc-row">
<span class="dd-rpc-label">Parâmetros</span>
<code>p_token TEXT</code>
</div>
<div class="dd-rpc-row">
<span class="dd-rpc-label">Retorna</span>
<code>boolean</code>
</div>
<div class="dd-rpc-row">
<span class="dd-rpc-label">Chamado em</span>
<code>supportSessionService.revokeSupportSession()</code>
</div>
</div>
<div class="dd-rpc-card">
<div class="dd-rpc-name">split_recurrence_at</div>
<div class="dd-rpc-desc">
Divide uma série recorrente em dois: encerra a original na data informada e cria uma nova regra a partir daí.
Preserva exceções anteriores à divisão na regra original.
</div>
<div class="dd-rpc-row">
<span class="dd-rpc-label">Parâmetros</span>
<code>p_rule_id UUID, p_split_date DATE</code>
</div>
<div class="dd-rpc-row">
<span class="dd-rpc-label">Retorna</span>
<code>&#123; old_rule_id: uuid, new_rule_id: uuid &#125;</code>
</div>
<div class="dd-rpc-row">
<span class="dd-rpc-label">Chamado em</span>
<code>useRecurrence.splitAt(ruleId, date)</code>
</div>
</div>
<div class="dd-rpc-card">
<div class="dd-rpc-name">cancel_recurrence_from</div>
<div class="dd-rpc-desc">
Cancela todas as ocorrências de uma série a partir de uma data.
Insere exceções do tipo <code>skip</code> para cada ocorrência futura e marca a regra como <code>cancelled</code>.
</div>
<div class="dd-rpc-row">
<span class="dd-rpc-label">Parâmetros</span>
<code>p_rule_id UUID, p_from_date DATE</code>
</div>
<div class="dd-rpc-row">
<span class="dd-rpc-label">Retorna</span>
<code>boolean</code>
</div>
<div class="dd-rpc-row">
<span class="dd-rpc-label">Chamado em</span>
<code>useRecurrence.cancelFrom(ruleId, date)</code>
</div>
</div>
</div>
</TabPanel>
<!-- Tab 4: Fluxos -->
<TabPanel header="Fluxos">
<div class="dd-section">
<h3 class="dd-h3">1 Criar evento simples</h3>
<ol class="dd-steps">
<li>Usuário clica num slot do FullCalendar evento <code>dateClick</code></li>
<li><code>AgendaTerapeutaPage</code> abre <code>AgendaEventDialog</code> com horário pré-preenchido</li>
<li>Usuário preenche paciente, tipo, serviços confirma</li>
<li><code>useAgendaEvents.createEvent()</code> INSERT em <code>agenda_eventos</code></li>
<li>Retorno <code>mapAgendaEventosToCalendarEvents()</code> evento adicionado ao FullCalendar</li>
</ol>
<h3 class="dd-h3">2 Criar recorrência</h3>
<ol class="dd-steps">
<li>No dialog do evento, usuário ativa "Repetir" e configura frequência/dias</li>
<li><code>useRecurrence.createRule(payload)</code> INSERT em <code>recurrence_rules</code></li>
<li>Opcionalmente, serviços-template são inseridos em <code>recurrence_rule_services</code></li>
<li>Frontend gera ocorrências client-side a partir da regra (sem tabela de ocorrências)</li>
<li>Ocorrências são renderizadas como eventos "virtuais" no FullCalendar</li>
</ol>
<h3 class="dd-h3">3 Editar ocorrência única de uma série</h3>
<ol class="dd-steps">
<li>Usuário clica em um evento recorrente dialog pergunta: "Esta ocorrência" ou "Todas a partir daqui"</li>
<li><strong>Esta ocorrência:</strong> INSERT em <code>recurrence_exceptions</code> com <code>action = reschedule</code></li>
<li><strong>A partir daqui:</strong> chama RPC <code>split_recurrence_at</code> cria nova regra para o trecho futuro</li>
<li>FullCalendar re-renderiza com as exceções aplicadas</li>
</ol>
<h3 class="dd-h3">4 Cancelar série recorrente</h3>
<ol class="dd-steps">
<li>Usuário abre evento recorrente "Cancelar a partir de..."</li>
<li>RPC <code>cancel_recurrence_from(ruleId, fromDate)</code> é chamado</li>
<li>Regra recebe <code>status = cancelled</code>; exceções <code>skip</code> são inseridas</li>
<li>Composable invalida cache local e recarrega regras</li>
</ol>
<h3 class="dd-h3">5 Ativar modo suporte técnico</h3>
<ol class="dd-steps">
<li>SaaS admin acessa <code>/saas/support</code> e gera um token com TTL</li>
<li>Admin envia URL: <code>/therapist/agenda?support=TOKEN</code> para o terapeuta</li>
<li>Terapeuta abre a URL; <code>AgendaTerapeutaPage._initSupportMode()</code> detecta query param</li>
<li>RPC <code>validate_support_session(token)</code> é chamado valida e retorna <code>tenant_id</code></li>
<li><code>supportDebugStore.isActive = true</code> <code>SupportDebugBanner</code> aparece via Teleport</li>
<li>Todos os <code>logEvent / logAPI / logRecurrence</code> passam a ser registrados no painel</li>
<li>Token expira automaticamente após o TTL sem ação do admin necessária</li>
</ol>
<h3 class="dd-h3">6 Precificação de uma sessão</h3>
<ol class="dd-steps">
<li>Ao abrir/criar um evento, <code>useCommitmentServices</code> carrega serviços do evento</li>
<li><code>useProfessionalPricing</code> cruza tipo de compromisso × profissional × convênio</li>
<li>Se paciente tem convênio ativo, <code>useInsurancePlans</code> verifica cobertura</li>
<li>Se paciente tem desconto, <code>usePatientDiscounts</code> aplica sobre o valor base</li>
<li>Valor final é salvo em <code>commitment_services</code> no evento</li>
</ol>
</div>
</TabPanel>
<!-- Tab 5: Suporte -->
<TabPanel header="Sistema de Suporte">
<div class="dd-section">
<h3 class="dd-h3">Níveis de log disponíveis</h3>
<table class="dd-table">
<thead><tr><th>Nível</th><th>Helper</th><th>Quando usar</th><th>Cor no painel</th></tr></thead>
<tbody>
<tr><td><code>event</code></td><td><code>logEvent(source, msg, data)</code></td><td>Lifecycle, mudanças de estado gerais</td><td>Cinza</td></tr>
<tr><td><code>api</code></td><td><code>logAPI(source, msg, data)</code></td><td>Queries Supabase (antes e depois)</td><td>Azul</td></tr>
<tr><td><code>agenda</code></td><td><code>logAgenda(source, msg, data)</code></td><td>Eventos, slots, calendar renders</td><td>Roxo claro</td></tr>
<tr><td><code>recurrence</code></td><td><code>logRecurrence(msg, data)</code></td><td>Geração e manipulação de regras</td><td>Roxo</td></tr>
<tr><td><code>tenant</code></td><td><code>logTenant(source, msg, data)</code></td><td>Carregamento de sessão, role, tenant ativo</td><td>Ciano</td></tr>
<tr><td><code>menu</code></td><td><code>logMenu(msg, data)</code></td><td>Build e reset do menuStore</td><td>Verde limão</td></tr>
<tr><td><code>profile</code></td><td><code>logProfile(msg, data)</code></td><td>Perfil: carga e save de settings</td><td>Rosa</td></tr>
<tr><td><code>auth</code></td><td><code>logAuth(msg, data)</code></td><td>Login, logout, refresh, MFA</td><td>Laranja</td></tr>
<tr><td><code>guard</code></td><td><code>logGuard(msg, data)</code></td><td>Navegação via router guards</td><td>Verde</td></tr>
<tr><td><code>perf</code></td><td><code>logPerf(source, label)</code></td><td>Medição de performance (retorna end())</td><td>Amarelo</td></tr>
<tr><td><code>error</code></td><td><code>logError(source, msg, err)</code></td><td>Erros capturados em catch</td><td>Vermelho</td></tr>
</tbody>
</table>
<h3 class="dd-h3">Como adicionar logs a um composable</h3>
<pre class="dd-code">import &#123; logAPI, logError, logPerf, logAgenda &#125; from '@/support/supportLogger'
async function loadEvents (ownerId, range) &#123;
const end = logPerf('useAgendaEvents', 'loadEvents')
logAPI('useAgendaEvents', 'loadEvents start', &#123; ownerId, range &#125;)
try &#123;
const &#123; data, error &#125; = await supabase
.from('agenda_eventos')
.select('*')
.eq('owner_id', ownerId)
if (error) throw error
end(&#123; count: data.length &#125;)
logAgenda('useAgendaEvents', 'eventos carregados', &#123; count: data.length &#125;)
return data
&#125; catch (e) &#123;
logError('useAgendaEvents', 'loadEvents ERRO', e)
throw e
&#125;
&#125;</pre>
<h3 class="dd-h3">Segurança do modo suporte</h3>
<ul class="dd-list dd-list--spaced">
<li>Token validado <strong>server-side</strong> via RPC sem bypass pelo frontend possível</li>
<li>Token tem TTL máximo de 2 horas; expira automaticamente no banco</li>
<li>Apenas usuários com role <code>saas_admin</code> podem criar tokens (validado no RPC)</li>
<li>Revogar um token invalida imediatamente (<code>expires_at = now()</code>)</li>
<li><code>SupportDebugBanner</code> é renderizado quando <code>supportStore.isActive === true</code></li>
<li>Em produção sem token válido, zero overhead todos os <code>log*()</code> retornam imediatamente</li>
</ul>
</div>
</TabPanel>
</TabView>
</Dialog>
</template>
<style scoped>
.dev-docs {
font-size: 0.85rem;
}
.dd-section {
padding: 1.25rem 1.5rem 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
/* Alert */
.dd-alert {
background: color-mix(in srgb, var(--yellow-500) 12%, transparent);
border: 1px solid color-mix(in srgb, var(--yellow-500) 35%, transparent);
border-left: 4px solid var(--yellow-500);
border-radius: 8px;
padding: 0.6rem 1rem;
font-size: 0.82rem;
color: var(--text-color);
}
/* Typography */
.dd-h3 {
font-size: 0.78rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--text-color-secondary);
opacity: 0.6;
margin: 0.5rem 0 0;
}
.dd-p {
margin: 0;
color: var(--text-color-secondary);
line-height: 1.6;
}
/* Tags */
.dd-tag {
display: inline-block;
padding: 1px 7px;
border-radius: 4px;
font-size: 0.72rem;
font-weight: 700;
margin-right: 4px;
}
.dd-tag.blue { background: #dbeafe; color: #1d4ed8; }
.dd-tag.purple { background: #ede9fe; color: #6d28d9; }
.dd-tag.green { background: #dcfce7; color: #15803d; }
.dd-tag.orange { background: #ffedd5; color: #c2410c; }
.dd-tag.teal { background: #ccfbf1; color: #0f766e; }
.dd-tag.red { background: #fee2e2; color: #b91c1c; }
.dd-tag.yellow { background: #fef9c3; color: #a16207; }
/* Dark mode tags */
:global(.app-dark) .dd-tag.blue { background: #1e3a5f; color: #93c5fd; }
:global(.app-dark) .dd-tag.purple { background: #2e1065; color: #c4b5fd; }
:global(.app-dark) .dd-tag.green { background: #14532d; color: #86efac; }
:global(.app-dark) .dd-tag.orange { background: #431407; color: #fdba74; }
:global(.app-dark) .dd-tag.teal { background: #042f2e; color: #5eead4; }
:global(.app-dark) .dd-tag.red { background: #450a0a; color: #fca5a5; }
:global(.app-dark) .dd-tag.yellow { background: #422006; color: #fde68a; }
/* Grid 2 col */
.dd-grid-2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0.75rem;
}
@media (max-width: 600px) {
.dd-grid-2 { grid-template-columns: 1fr; }
}
/* Cards */
.dd-card {
background: var(--surface-ground);
border: 1px solid var(--surface-border);
border-radius: 10px;
padding: 0.85rem 1rem;
}
.dd-card-title {
font-weight: 700;
font-size: 0.78rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-color-secondary);
margin-bottom: 0.5rem;
}
/* Lists */
.dd-list {
margin: 0;
padding-left: 1.2rem;
display: flex;
flex-direction: column;
gap: 0.3rem;
color: var(--text-color-secondary);
}
.dd-list--spaced { gap: 0.5rem; }
/* Table */
.dd-table {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
}
.dd-table th {
text-align: left;
padding: 0.45rem 0.75rem;
background: var(--surface-ground);
border-bottom: 2px solid var(--surface-border);
font-weight: 700;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-color-secondary);
white-space: nowrap;
}
.dd-table td {
padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--surface-border);
vertical-align: top;
color: var(--text-color);
line-height: 1.5;
}
.dd-table tr:hover td {
background: var(--surface-ground);
}
.dd-table code {
font-size: 0.78rem;
background: var(--surface-ground);
padding: 1px 5px;
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
}
/* Layers */
.dd-layers {
display: flex;
flex-direction: column;
align-items: stretch;
gap: 0;
}
.dd-layer {
display: flex;
align-items: baseline;
gap: 1rem;
padding: 0.6rem 1rem;
border-radius: 8px;
font-size: 0.82rem;
}
.dd-layer strong {
flex-shrink: 0;
min-width: 120px;
font-weight: 700;
}
.dd-layer span {
color: var(--text-color-secondary);
font-size: 0.78rem;
}
.dd-layer--page { background: color-mix(in srgb, var(--primary-color) 10%, transparent); }
.dd-layer--composable { background: color-mix(in srgb, var(--purple-500, #8b5cf6) 10%, transparent); }
.dd-layer--service { background: color-mix(in srgb, var(--teal-500, #14b8a6) 10%, transparent); }
.dd-layer--db { background: color-mix(in srgb, var(--orange-500, #f97316) 10%, transparent); }
.dd-layer-arrow {
text-align: center;
color: var(--text-color-secondary);
opacity: 0.4;
line-height: 1.4;
font-size: 0.75rem;
}
/* RPC cards */
.dd-rpc-card {
background: var(--surface-ground);
border: 1px solid var(--surface-border);
border-left: 4px solid var(--primary-color);
border-radius: 8px;
padding: 0.85rem 1rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.dd-rpc-name {
font-weight: 700;
font-family: 'JetBrains Mono', monospace;
font-size: 0.88rem;
color: var(--primary-color);
}
.dd-rpc-desc {
color: var(--text-color-secondary);
font-size: 0.8rem;
line-height: 1.5;
}
.dd-rpc-row {
display: flex;
align-items: baseline;
gap: 0.75rem;
font-size: 0.78rem;
}
.dd-rpc-label {
font-weight: 700;
min-width: 90px;
color: var(--text-color-secondary);
flex-shrink: 0;
}
.dd-rpc-row code {
font-family: 'JetBrains Mono', monospace;
background: var(--surface-card);
padding: 1px 6px;
border-radius: 4px;
font-size: 0.77rem;
}
/* Steps */
.dd-steps {
margin: 0;
padding-left: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.35rem;
color: var(--text-color-secondary);
line-height: 1.5;
}
.dd-steps code {
background: var(--surface-ground);
padding: 1px 5px;
border-radius: 4px;
font-family: 'JetBrains Mono', monospace;
font-size: 0.77rem;
}
/* Code block */
.dd-code {
background: #0f172a;
color: #cbd5e1;
border-radius: 8px;
padding: 1rem 1.1rem;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
font-size: 0.75rem;
overflow-x: auto;
white-space: pre;
line-height: 1.6;
margin: 0;
}
</style>