86311ef305
Sprints 04-29 + 04-30 acumuladas. - MelissaConfiguracoes: hub 2-col com 6 grupos (Layout/Conta/Agenda/ Financeiro/WhatsApp/Sistema), tudo embedado via MelissaEmbed. - MelissaEmbed: wrapper generico que injeta layout-variant=melissa e remove cromos pra reaproveitar Pages tradicionais. - 9 Melissa Pages novas: CadastrosRecebidos, Compromissos, Configuracoes, Conversas, Embed, Grupos, Medicos, Recorrencias, Tags. - Dialog blueprint atualizado: bg-gray-100 (hardcoded light) -> bg-[var(--surface-ground)] (tema-aware). 22 dialogs migrados em 9 arquivos. Anti-pattern documentado. - PatientsCadastroPage: bug fix dropdown Grupo (optionLabel nome->name), toggle vertical/abas com persist localStorage, sticky margin-top. - Surface picker no popover do MelissaLayout (8 swatches). - useTopbarPlanMenu, useMelissaWhatsapp, useMelissaPacientesAside novos. - Migration: status agenda remarcado/confirmado. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
810 lines
25 KiB
Vue
810 lines
25 KiB
Vue
<!--
|
|
|--------------------------------------------------------------------------
|
|
| Agência PSI
|
|
|--------------------------------------------------------------------------
|
|
| Criado e desenvolvido por Leonardo Nohama
|
|
|
|
|
| Tecnologia aplicada à escuta.
|
|
| Estrutura para o cuidado.
|
|
|
|
|
| Arquivo: src/layout/AppRailSidebar.vue
|
|
| Data: 2026
|
|
| Local: São Carlos/SP — Brasil
|
|
|--------------------------------------------------------------------------
|
|
| © 2026 — Todos os direitos reservados
|
|
|--------------------------------------------------------------------------
|
|
-->
|
|
<!-- Drawer mobile para Layout Rail -->
|
|
<script setup>
|
|
import { computed, ref, watch, nextTick } from 'vue';
|
|
import { useRouter, useRoute } from 'vue-router';
|
|
|
|
import { useMenuStore } from '@/stores/menuStore';
|
|
import { useLayout } from './composables/layout';
|
|
import { useEntitlementsStore } from '@/stores/entitlementsStore';
|
|
import { useMenuBadges } from '@/composables/useMenuBadges';
|
|
|
|
import PatientCreatePopover from '@/components/ui/PatientCreatePopover.vue';
|
|
import ComponentCadastroRapido from '@/components/ComponentCadastroRapido.vue';
|
|
|
|
const menuStore = useMenuStore();
|
|
const { layoutState, hideMobileMenu } = useLayout();
|
|
const entitlements = useEntitlementsStore();
|
|
const menuBadges = useMenuBadges();
|
|
const router = useRouter();
|
|
const route = useRoute();
|
|
|
|
function menuBadgeLabel(item) {
|
|
const key = item?.badgeKey;
|
|
if (!key) return null;
|
|
const val = menuBadges[key]?.value || 0;
|
|
if (!val) return null;
|
|
return key === 'agendaHoje' ? `${val} hoje` : String(val);
|
|
}
|
|
|
|
const sections = computed(() => {
|
|
const model = menuStore.model || [];
|
|
return model
|
|
.filter((s) => s.label && Array.isArray(s.items) && s.items.length)
|
|
.map((s) => ({
|
|
key: s.label,
|
|
label: s.label,
|
|
icon: s.icon || s.items.find((i) => i.icon)?.icon || 'pi pi-circle',
|
|
items: s.items
|
|
}));
|
|
});
|
|
|
|
// Seções expandidas no accordion
|
|
const openSections = ref([]);
|
|
|
|
// Ao abrir o drawer, expande a seção ativa (ou a primeira)
|
|
watch(
|
|
() => layoutState.mobileMenuActive,
|
|
(open) => {
|
|
if (!open) return;
|
|
if (openSections.value.length === 0 && sections.value.length > 0) {
|
|
const activeKey = layoutState.railSectionKey;
|
|
openSections.value = [activeKey || sections.value[0].key];
|
|
}
|
|
}
|
|
);
|
|
|
|
function expandAll() {
|
|
openSections.value = sections.value.map((s) => s.key);
|
|
}
|
|
function collapseAll() {
|
|
openSections.value = [];
|
|
}
|
|
|
|
// ── Busca ────────────────────────────────────────────────────
|
|
const query = ref('');
|
|
const showResults = ref(false);
|
|
const activeIndex = ref(-1);
|
|
const forcedOpen = ref(false);
|
|
const searchEl = ref(null);
|
|
const searchWrapEl = ref(null);
|
|
|
|
const RECENT_KEY = 'menu_search_recent';
|
|
const recent = ref([]);
|
|
function loadRecent() {
|
|
try {
|
|
recent.value = JSON.parse(localStorage.getItem(RECENT_KEY) || '[]');
|
|
} catch {
|
|
recent.value = [];
|
|
}
|
|
}
|
|
function saveRecent(q) {
|
|
const v = String(q || '').trim();
|
|
if (!v) return;
|
|
const list = [v, ...recent.value.filter((x) => x !== v)].slice(0, 8);
|
|
recent.value = list;
|
|
localStorage.setItem(RECENT_KEY, JSON.stringify(list));
|
|
}
|
|
function clearRecent() {
|
|
recent.value = [];
|
|
try {
|
|
localStorage.removeItem(RECENT_KEY);
|
|
} catch {}
|
|
}
|
|
loadRecent();
|
|
|
|
watch(query, (v) => {
|
|
const hasText = !!v?.trim();
|
|
if (hasText) {
|
|
forcedOpen.value = false;
|
|
showResults.value = true;
|
|
return;
|
|
}
|
|
showResults.value = forcedOpen.value;
|
|
});
|
|
|
|
function clearSearch() {
|
|
query.value = '';
|
|
activeIndex.value = -1;
|
|
showResults.value = false;
|
|
forcedOpen.value = false;
|
|
}
|
|
|
|
function norm(s) {
|
|
return String(s || '')
|
|
.toLowerCase()
|
|
.normalize('NFD')
|
|
.replace(/\p{Diacritic}/gu, '')
|
|
.trim();
|
|
}
|
|
|
|
function isVisibleItem(it) {
|
|
const v = it?.visible;
|
|
if (typeof v === 'function') return !!v();
|
|
if (v === undefined || v === null) return true;
|
|
return v !== false;
|
|
}
|
|
|
|
function flattenMenu(items, trail = []) {
|
|
const out = [];
|
|
for (const it of items || []) {
|
|
if (!isVisibleItem(it)) continue;
|
|
const nextTrail = [...trail, it?.label].filter(Boolean);
|
|
if (it?.to && !it?.items?.length) {
|
|
out.push({ label: it.label || it.to, to: it.to, icon: it.icon, trail: nextTrail, proBadge: !!(it.__showProBadge ?? it.proBadge), feature: it.feature || null });
|
|
}
|
|
if (it?.items?.length) out.push(...flattenMenu(it.items, nextTrail));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
const allLinks = computed(() => flattenMenu(menuStore.model || []));
|
|
|
|
const results = computed(() => {
|
|
const q = norm(query.value);
|
|
if (!q) return [];
|
|
const wantPro = q === 'pro' || q.startsWith('pro ') || q.includes(' pro');
|
|
return allLinks.value
|
|
.filter((r) => {
|
|
const hay = `${norm(r.label)} ${norm(r.trail.join(' > '))} ${norm(r.to)}`;
|
|
if (hay.includes(q)) return true;
|
|
if (wantPro && (r.proBadge || r.feature)) return true;
|
|
return false;
|
|
})
|
|
.slice(0, 12);
|
|
});
|
|
|
|
watch(results, (list) => {
|
|
activeIndex.value = list.length ? 0 : -1;
|
|
});
|
|
|
|
function escapeHtml(s) {
|
|
return String(s || '')
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
function highlight(text, q) {
|
|
const queryNorm = norm(q);
|
|
const raw = String(text || '');
|
|
if (!queryNorm) return escapeHtml(raw);
|
|
const rawNorm = norm(raw);
|
|
const idx = rawNorm.indexOf(queryNorm);
|
|
if (idx < 0) return escapeHtml(raw);
|
|
const before = escapeHtml(raw.slice(0, idx));
|
|
const mid = escapeHtml(raw.slice(idx, idx + queryNorm.length));
|
|
const after = escapeHtml(raw.slice(idx + queryNorm.length));
|
|
return `${before}<mark class="px-1 rounded bg-yellow-200/40">${mid}</mark>${after}`;
|
|
}
|
|
|
|
function onSearchKeydown(e) {
|
|
if (e.key === 'Escape') {
|
|
showResults.value = false;
|
|
forcedOpen.value = false;
|
|
return;
|
|
}
|
|
if (e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
if (!results.value.length) return;
|
|
showResults.value = true;
|
|
activeIndex.value = (activeIndex.value + 1) % results.value.length;
|
|
return;
|
|
}
|
|
if (e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
if (!results.value.length) return;
|
|
showResults.value = true;
|
|
activeIndex.value = (activeIndex.value - 1 + results.value.length) % results.value.length;
|
|
return;
|
|
}
|
|
if (e.key === 'Enter' && showResults.value && results.value.length && activeIndex.value >= 0) {
|
|
e.preventDefault();
|
|
goToResult(results.value[activeIndex.value]);
|
|
}
|
|
}
|
|
|
|
function onSearchFocus() {
|
|
if (!query.value?.trim()) {
|
|
forcedOpen.value = true;
|
|
showResults.value = true;
|
|
}
|
|
}
|
|
|
|
function applyRecent(q) {
|
|
query.value = q;
|
|
forcedOpen.value = true;
|
|
showResults.value = true;
|
|
activeIndex.value = 0;
|
|
nextTick(() => searchEl.value?.$el?.querySelector?.('input')?.focus?.());
|
|
}
|
|
|
|
async function goToResult(r) {
|
|
saveRecent(query.value);
|
|
query.value = '';
|
|
showResults.value = false;
|
|
activeIndex.value = -1;
|
|
forcedOpen.value = false;
|
|
await router.push(r.to);
|
|
hideMobileMenu();
|
|
}
|
|
|
|
function isSectionOpen(key) {
|
|
return openSections.value.includes(key);
|
|
}
|
|
|
|
function toggleSection(key) {
|
|
const idx = openSections.value.indexOf(key);
|
|
if (idx >= 0) {
|
|
openSections.value.splice(idx, 1);
|
|
} else {
|
|
openSections.value.push(key);
|
|
}
|
|
}
|
|
|
|
function isLocked(item) {
|
|
if (!item.proBadge || !item.feature) return false;
|
|
try {
|
|
return !entitlements.has(item.feature);
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function toPath(to) {
|
|
if (!to) return '';
|
|
if (typeof to === 'string') return to;
|
|
try {
|
|
return router.resolve(to).path || '';
|
|
} catch {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
function isActive(item) {
|
|
const active = String(layoutState.activePath || route.path || '');
|
|
if (!item.to) return false;
|
|
const p = toPath(item.to);
|
|
if (!p) return false;
|
|
if (active === p) return true;
|
|
const segments = p.split('/').filter(Boolean);
|
|
return segments.length >= 2 && active.startsWith(p + '/');
|
|
}
|
|
|
|
function navigate(item) {
|
|
if (isLocked(item)) {
|
|
router.push({ name: 'upgrade', query: { feature: item.feature || '' } });
|
|
hideMobileMenu();
|
|
return;
|
|
}
|
|
if (item.to) {
|
|
layoutState.activePath = toPath(item.to);
|
|
router.push(item.to);
|
|
hideMobileMenu();
|
|
}
|
|
}
|
|
|
|
// Fecha ao navegar
|
|
watch(
|
|
() => route.path,
|
|
() => hideMobileMenu()
|
|
);
|
|
|
|
// ── QuickCreate (Pacientes) ───────────────────────────────
|
|
const createPopover = ref(null);
|
|
const quickDialog = ref(false);
|
|
|
|
function openQuickCreate(event, item) {
|
|
createPopover.value?.toggle(event);
|
|
}
|
|
function onQuickCreate() {
|
|
quickDialog.value = true;
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Transition name="rs-slide">
|
|
<aside v-if="layoutState.mobileMenuActive" class="rs">
|
|
<!-- Header -->
|
|
<div class="rs__head">
|
|
<span class="rs__brand">Agência PSI</span>
|
|
<button class="rs__close" aria-label="Fechar menu" @click="hideMobileMenu">
|
|
<i class="pi pi-times" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Busca + ações -->
|
|
<div ref="searchWrapEl" class="rs__search-area">
|
|
<!-- Campo de busca -->
|
|
<div class="rs__search-field">
|
|
<div aria-hidden="true" style="position: absolute; left: -9999px; top: -9999px; width: 1px; height: 1px; overflow: hidden">
|
|
<input type="email" name="email" autocomplete="username" tabindex="-1" disabled />
|
|
<input type="password" name="password" autocomplete="current-password" tabindex="-1" disabled />
|
|
</div>
|
|
|
|
<FloatLabel variant="on" class="w-full">
|
|
<IconField class="w-full">
|
|
<InputIcon class="pi pi-search" />
|
|
<!-- type="text" (não "search"): vide AppMenu.vue —
|
|
o "search" nativo dá um X de clear próprio que
|
|
duplica o botão custom abaixo. -->
|
|
<InputText
|
|
ref="searchEl"
|
|
id="rs_menu_search"
|
|
name="rs_menu_search"
|
|
type="text"
|
|
inputmode="search"
|
|
autocomplete="off"
|
|
autocapitalize="off"
|
|
autocorrect="off"
|
|
spellcheck="false"
|
|
data-lpignore="true"
|
|
data-1p-ignore="true"
|
|
v-model="query"
|
|
class="w-full pr-8"
|
|
variant="filled"
|
|
@focus="onSearchFocus"
|
|
@keydown="onSearchKeydown"
|
|
/>
|
|
</IconField>
|
|
<label for="rs_menu_search">Encontrar menu...</label>
|
|
</FloatLabel>
|
|
|
|
<button v-if="query.trim()" type="button" class="rs__search-clear" @mousedown.prevent="clearSearch" aria-label="Limpar busca">
|
|
<i class="pi pi-times" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Dropdown de resultados recentes -->
|
|
<div v-if="showResults && !query.trim() && recent.length" class="rs__dropdown">
|
|
<div class="rs__dropdown-header">
|
|
<span>Recentes</span>
|
|
<button type="button" class="rs__dropdown-action" @mousedown.prevent="clearRecent" aria-label="Limpar recentes">
|
|
<i class="pi pi-trash" />
|
|
</button>
|
|
</div>
|
|
<button v-for="q in recent" :key="q" class="rs__dropdown-item" type="button" @click.stop.prevent="applyRecent(q)">
|
|
<i class="pi pi-history rs__dropdown-item-icon" />
|
|
<span class="flex-1">{{ q }}</span>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Dropdown de resultados de busca -->
|
|
<div v-else-if="showResults && results.length" class="rs__dropdown">
|
|
<button v-for="(r, i) in results" :key="String(r.to)" type="button" @mousedown.prevent="goToResult(r)" :class="['rs__dropdown-item', i === activeIndex ? 'rs__dropdown-item--active' : '']">
|
|
<i v-if="r.icon" :class="r.icon" class="rs__dropdown-item-icon" />
|
|
<div class="flex flex-col flex-1">
|
|
<span class="rs__dropdown-item-label" v-html="highlight(r.label, query)" />
|
|
<small class="rs__dropdown-item-trail">{{ r.trail.join(' > ') }}</small>
|
|
</div>
|
|
<span v-if="r.proBadge" class="rs__pro">PRO</span>
|
|
</button>
|
|
</div>
|
|
|
|
<div v-else-if="showResults && query && !results.length" class="rs__no-results">Nenhum item encontrado.</div>
|
|
|
|
<!-- Botões expandir/contrair -->
|
|
<div class="rs__actions">
|
|
<button class="rs__action-btn" type="button" @click="expandAll">
|
|
<i class="pi pi-chevron-down" />
|
|
<span>Expandir tudo</span>
|
|
</button>
|
|
<button class="rs__action-btn" type="button" @click="collapseAll">
|
|
<i class="pi pi-chevron-up" />
|
|
<span>Contrair tudo</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Nav -->
|
|
<nav class="rs__nav">
|
|
<template v-for="section in sections" :key="section.key">
|
|
<!-- Cabeçalho da seção -->
|
|
<button class="rs__sec" :class="{ 'rs__sec--open': isSectionOpen(section.key) }" @click="toggleSection(section.key)">
|
|
<i :class="section.icon" class="rs__sec-icon" />
|
|
<span class="rs__sec-label">{{ section.label }}</span>
|
|
<i class="pi pi-chevron-down rs__sec-arrow" />
|
|
</button>
|
|
|
|
<!-- Items da seção -->
|
|
<div v-show="isSectionOpen(section.key)" class="rs__items">
|
|
<template v-for="item in section.items" :key="item.to || item.label">
|
|
<!-- Sub-grupo -->
|
|
<template v-if="item.items?.length">
|
|
<div class="rs__group-label">{{ item.label }}</div>
|
|
<button
|
|
v-for="child in item.items"
|
|
:key="child.to || child.label"
|
|
class="rs__item"
|
|
:class="{
|
|
'rs__item--active': isActive(child),
|
|
'rs__item--locked': isLocked(child)
|
|
}"
|
|
@click="navigate(child)"
|
|
>
|
|
<i v-if="child.icon" :class="child.icon" class="rs__item-icon" />
|
|
<span>{{ child.label }}</span>
|
|
<span v-if="isLocked(child)" class="rs__pro">PRO</span>
|
|
<span v-if="menuBadgeLabel(child)" class="rs__badge">{{ menuBadgeLabel(child) }}</span>
|
|
</button>
|
|
</template>
|
|
|
|
<!-- Item folha -->
|
|
<div v-else class="flex items-center gap-1">
|
|
<button
|
|
class="rs__item flex-1"
|
|
:class="{
|
|
'rs__item--active': isActive(item),
|
|
'rs__item--locked': isLocked(item)
|
|
}"
|
|
@click="navigate(item)"
|
|
>
|
|
<i v-if="item.icon" :class="item.icon" class="rs__item-icon" />
|
|
<span>{{ item.label }}</span>
|
|
<span v-if="isLocked(item)" class="rs__pro">PRO</span>
|
|
<span v-if="menuBadgeLabel(item)" class="rs__badge">{{ menuBadgeLabel(item) }}</span>
|
|
</button>
|
|
<button v-if="item.quickCreate" class="rs__quick-add" @click.stop="openQuickCreate($event, item)" title="Novo paciente">
|
|
<i class="pi pi-plus" />
|
|
</button>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
</nav>
|
|
|
|
<!-- PatientCreatePopover (shared) -->
|
|
<PatientCreatePopover ref="createPopover" @quick-create="onQuickCreate" />
|
|
|
|
<!-- Cadastro Rápido Dialog -->
|
|
<ComponentCadastroRapido
|
|
v-model="quickDialog"
|
|
title="Cadastro Rápido"
|
|
table-name="patients"
|
|
name-field="nome_completo"
|
|
email-field="email_principal"
|
|
phone-field="telefone"
|
|
:extra-payload="{ status: 'Ativo' }"
|
|
@created="quickDialog = false"
|
|
/>
|
|
</aside>
|
|
</Transition>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.rs {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 280px;
|
|
height: 100vh;
|
|
z-index: 99; /* acima do overlay (98), abaixo da topbar (100) quando necessário */
|
|
background: var(--surface-card);
|
|
border-right: 1px solid var(--surface-border);
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ── Header ──────────────────────────────────────────────── */
|
|
.rs__head {
|
|
height: 56px;
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 0 16px;
|
|
border-bottom: 1px solid var(--surface-border);
|
|
}
|
|
.rs__brand {
|
|
font-size: 0.9rem;
|
|
font-weight: 700;
|
|
color: var(--text-color);
|
|
}
|
|
.rs__close {
|
|
width: 28px;
|
|
height: 28px;
|
|
border-radius: 7px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--text-color-secondary);
|
|
cursor: pointer;
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 0.75rem;
|
|
transition:
|
|
background 0.15s,
|
|
color 0.15s;
|
|
}
|
|
.rs__close:hover {
|
|
background: var(--surface-ground);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
/* ── Search area ──────────────────────────────────────────── */
|
|
.rs__search-area {
|
|
flex-shrink: 0;
|
|
padding: 10px 12px 0;
|
|
border-bottom: 1px solid var(--surface-border);
|
|
}
|
|
.rs__search-field {
|
|
position: relative;
|
|
}
|
|
.rs__search-clear {
|
|
position: absolute;
|
|
right: 8px;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
background: transparent;
|
|
border: none;
|
|
cursor: pointer;
|
|
opacity: 0.6;
|
|
color: var(--text-color-secondary);
|
|
font-size: 0.75rem;
|
|
display: grid;
|
|
place-items: center;
|
|
padding: 2px;
|
|
}
|
|
.rs__search-clear:hover {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* ── Dropdown ─────────────────────────────────────────────── */
|
|
.rs__dropdown {
|
|
margin-top: 6px;
|
|
border: 1px solid var(--surface-border);
|
|
border-radius: 10px;
|
|
background: var(--surface-card);
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
overflow: hidden;
|
|
}
|
|
.rs__dropdown-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 6px 12px;
|
|
font-size: 0.72rem;
|
|
opacity: 0.65;
|
|
color: var(--text-color-secondary);
|
|
}
|
|
.rs__dropdown-action {
|
|
background: transparent;
|
|
border: none;
|
|
cursor: pointer;
|
|
opacity: 0.7;
|
|
color: inherit;
|
|
font-size: 0.72rem;
|
|
}
|
|
.rs__dropdown-action:hover {
|
|
opacity: 1;
|
|
}
|
|
.rs__dropdown-item {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 7px 12px;
|
|
background: transparent;
|
|
border: none;
|
|
cursor: pointer;
|
|
color: var(--text-color);
|
|
font-size: 0.82rem;
|
|
text-align: left;
|
|
transition: background 0.12s;
|
|
}
|
|
.rs__dropdown-item:hover,
|
|
.rs__dropdown-item--active {
|
|
background: var(--surface-hover);
|
|
}
|
|
.rs__dropdown-item-icon {
|
|
font-size: 0.82rem;
|
|
opacity: 0.7;
|
|
flex-shrink: 0;
|
|
}
|
|
.rs__dropdown-item-label {
|
|
font-weight: 500;
|
|
line-height: 1.3;
|
|
}
|
|
.rs__dropdown-item-trail {
|
|
font-size: 0.7rem;
|
|
opacity: 0.6;
|
|
}
|
|
.rs__no-results {
|
|
padding: 8px 4px;
|
|
font-size: 0.8rem;
|
|
opacity: 0.6;
|
|
color: var(--text-color-secondary);
|
|
}
|
|
|
|
/* ── Botões expandir/contrair ─────────────────────────────── */
|
|
.rs__actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
padding: 8px 0 10px;
|
|
}
|
|
.rs__action-btn {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 5px;
|
|
padding: 5px 8px;
|
|
border-radius: 8px;
|
|
border: 1px solid var(--surface-border);
|
|
background: transparent;
|
|
color: var(--text-color-secondary);
|
|
font-size: 0.75rem;
|
|
font-weight: 500;
|
|
cursor: pointer;
|
|
transition:
|
|
background 0.13s,
|
|
color 0.13s;
|
|
}
|
|
.rs__action-btn:hover {
|
|
background: var(--surface-ground);
|
|
color: var(--text-color);
|
|
}
|
|
.rs__action-btn .pi {
|
|
font-size: 0.65rem;
|
|
}
|
|
|
|
.rs__nav {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 8px;
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--surface-border) transparent;
|
|
}
|
|
|
|
/* ── Section accordion ───────────────────────────────────── */
|
|
.rs__sec {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
padding: 9px 10px;
|
|
border-radius: 9px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--text-color);
|
|
cursor: pointer;
|
|
font-size: 0.83rem;
|
|
font-weight: 600;
|
|
text-align: left;
|
|
transition: background 0.13s;
|
|
}
|
|
.rs__sec:hover {
|
|
background: var(--surface-ground);
|
|
}
|
|
.rs__sec-icon {
|
|
font-size: 0.9rem;
|
|
color: var(--text-color-secondary);
|
|
flex-shrink: 0;
|
|
}
|
|
.rs__sec-label {
|
|
flex: 1;
|
|
}
|
|
.rs__sec-arrow {
|
|
font-size: 0.65rem;
|
|
color: var(--text-color-secondary);
|
|
transition: transform 0.2s;
|
|
}
|
|
.rs__sec--open .rs__sec-arrow {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
/* ── Items container ─────────────────────────────────────── */
|
|
.rs__items {
|
|
padding-left: 10px;
|
|
padding-bottom: 4px;
|
|
}
|
|
.rs__group-label {
|
|
font-size: 0.62rem;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.1em;
|
|
color: var(--text-color-secondary);
|
|
opacity: 0.55;
|
|
padding: 8px 10px 4px;
|
|
}
|
|
.rs__item {
|
|
width: 100%;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 9px;
|
|
padding: 7px 10px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--text-color-secondary);
|
|
cursor: pointer;
|
|
font-size: 0.82rem;
|
|
font-weight: 500;
|
|
text-align: left;
|
|
transition:
|
|
background 0.13s,
|
|
color 0.13s;
|
|
}
|
|
.rs__item:hover {
|
|
background: var(--surface-ground);
|
|
color: var(--text-color);
|
|
}
|
|
.rs__item--active {
|
|
background: color-mix(in srgb, var(--primary-color) 10%, transparent);
|
|
color: var(--primary-color);
|
|
font-weight: 600;
|
|
}
|
|
.rs__item--locked {
|
|
opacity: 0.55;
|
|
}
|
|
.rs__item-icon {
|
|
font-size: 0.82rem;
|
|
flex-shrink: 0;
|
|
opacity: 0.75;
|
|
}
|
|
.rs__pro {
|
|
font-size: 0.58rem;
|
|
font-weight: 800;
|
|
text-transform: uppercase;
|
|
padding: 1px 5px;
|
|
border-radius: 4px;
|
|
border: 1px solid var(--surface-border);
|
|
color: var(--text-color-secondary);
|
|
}
|
|
.rs__badge {
|
|
font-size: 0.62rem;
|
|
font-weight: 700;
|
|
padding: 1px 6px;
|
|
border-radius: 999px;
|
|
background: var(--primary-color);
|
|
color: #fff;
|
|
line-height: 1;
|
|
}
|
|
.rs__quick-add {
|
|
width: 24px;
|
|
height: 24px;
|
|
flex-shrink: 0;
|
|
border-radius: 6px;
|
|
border: none;
|
|
background: transparent;
|
|
color: var(--text-color-secondary);
|
|
cursor: pointer;
|
|
display: grid;
|
|
place-items: center;
|
|
font-size: 0.7rem;
|
|
transition:
|
|
background 0.13s,
|
|
color 0.13s;
|
|
}
|
|
.rs__quick-add:hover {
|
|
background: var(--surface-ground);
|
|
color: var(--text-color);
|
|
}
|
|
|
|
/* ── Slide-in da esquerda ────────────────────────────────── */
|
|
.rs-slide-enter-active,
|
|
.rs-slide-leave-active {
|
|
transition: transform 0.25s cubic-bezier(0.22, 1, 0.36, 1);
|
|
}
|
|
.rs-slide-enter-from,
|
|
.rs-slide-leave-to {
|
|
transform: translateX(-100%);
|
|
}
|
|
</style>
|