@neetru/page-tour
v0.1.0
Published
Tour guiado (onboarding) reutilizavel para produtos SaaS Neetru — destaca elementos da pagina por id, com mascara SVG e navegacao passo a passo. Extraido do pdv-agiliza; headless por padrao (voce estiliza), com um card default pronto pra uso.
Readme
@neetru/page-tour
Tour guiado (onboarding) reutilizável para produtos SaaS Neetru. Destaca um elemento por vez na página atual, recorta um "spotlight" ao redor dele com máscara SVG, e mostra um card com o passo atual + navegação Voltar/Próximo.
Extraído do produto pdv-agiliza, que tinha essa lógica implementada do zero
(portal + máscara + posicionamento por getBoundingClientRect). Generalizado
aqui pra qualquer produto reusar em vez de reimplementar.
Por que existe
Onboarding guiado é uma necessidade transversal — todo produto SaaS Neetru com uma UI de várias páginas eventualmente precisa explicar "isso aqui é X, aquilo é Y" pro usuário novo. A parte difícil (rastrear a posição do elemento- alvo em tempo real, reagir a scroll/resize, recortar a máscara) é sempre a mesma; só a aparência do card muda de produto pra produto.
Instalação
npm install @neetru/page-tour framer-motionreact, react-dom e framer-motion são peer dependencies — o produto que
consome já deve ter os dois primeiros; framer-motion é a única dependência
nova que este pacote introduz.
Uso básico
- Marque os elementos que vão virar passos do tour com um
id:
<input id="tour-search" ... />
<div id="tour-cart" ...>...</div>- Declare os passos e monte o
<PageTour>:
import { useState } from 'react';
import { PageTour, type TourStep } from '@neetru/page-tour';
const steps: TourStep[] = [
{ targetId: 'tour-search', title: 'Busca', content: 'Encontre qualquer item pelo nome ou código de barras.', position: 'bottom' },
{ targetId: 'tour-cart', title: 'Carrinho', content: 'Ajuste quantidades e finalize a venda por aqui.', position: 'top' },
];
function MinhaPagina() {
const [tourOpen, setTourOpen] = useState(false);
return (
<>
<button onClick={() => setTourOpen(true)}>Ver guia</button>
<PageTour steps={steps} isOpen={tourOpen} onClose={() => setTourOpen(false)} />
</>
);
}Isso já funciona sem nenhum CSS — o card default vem sem estilos (sem
Tailwind, sem design system embutido), então na primeira vez que testar vai
aparecer como HTML puro. Estilize com classNames (abaixo) ou substitua o
card inteiro com renderCard.
Estilizando o card default
<PageTour
steps={steps}
isOpen={tourOpen}
onClose={() => setTourOpen(false)}
classNames={{
card: 'meu-card-do-tour',
title: 'meu-titulo',
content: 'meu-texto',
nextButton: 'meu-botao-primario',
backButton: 'meu-botao-secundario',
finishButton: 'meu-botao-sucesso',
closeButton: 'meu-botao-fechar',
stepBadge: 'meu-indicador-de-etapa',
}}
/>Cada chave vira a prop className do elemento correspondente — estilize com
Tailwind, CSS Modules, styled-components, o que o produto já usar.
Substituindo o card inteiro (renderCard)
Pra controle total sobre a aparência (ex: reusar componentes de design system
do produto), passe renderCard. O PageTour continua responsável pelo
overlay, pela máscara de destaque e pelo posicionamento — só o conteúdo do
card muda:
<PageTour
steps={steps}
isOpen={tourOpen}
onClose={() => setTourOpen(false)}
renderCard={({ step, stepIndex, totalSteps, isFirstStep, isLastStep, goBack, goNext, close }) => (
<MeuCardDeDesignSystem>
<h3>{step.title}</h3>
<p>{step.content}</p>
<Progresso atual={stepIndex + 1} total={totalSteps} />
{!isFirstStep && <Botao onClick={goBack}>Voltar</Botao>}
<Botao onClick={isLastStep ? close : goNext}>{isLastStep ? 'Concluir' : 'Próximo'}</Botao>
</MeuCardDeDesignSystem>
)}
/>i18n
Os textos padrão do card default (Voltar, Próximo, Concluir, "Etapa N /
M") já vêm em PT-BR. Pra trocar:
<PageTour
steps={steps}
isOpen={tourOpen}
onClose={close}
labels={{ back: 'Back', next: 'Next', finish: 'Done', stepOf: (c, t) => `${c} of ${t}` }}
/>Catálogo de tours por rota
Padrão recomendado (usado no pdv-agiliza): um mapa rota -> TourStep[] num
componente de layout central (ex: o cabeçalho da aplicação), que só mostra o
botão de ajuda quando a rota atual tem tour cadastrado:
const pageTours: Record<string, TourStep[]> = {
'/': [...],
'/pos': [...],
'/finance': [...],
};
const steps = pageTours[pathname];
{steps && <BotaoAjuda onClick={() => setTourOpen(true)} />}API
<PageTour />
| Prop | Tipo | Descrição |
|---|---|---|
| steps | TourStep[] | Passos do tour, em ordem. |
| isOpen | boolean | Controla visibilidade. |
| onClose | () => void | Chamado ao fechar (X, "Concluir", ou via renderCard). |
| onStepChange? | (index: number) => void | Chamado a cada mudança de passo (inclui abertura, passo 0). |
| renderCard? | (props: TourCardRenderProps) => ReactNode | Substitui o card default. |
| classNames? | Partial<PageTourClassNames> | Classes CSS pro card default. Ignorado se renderCard for usado. |
| labels? | Partial<PageTourLabels> | Textos dos botões/indicador (i18n). |
| zIndex? | number | zIndex base do overlay (card usa zIndex + 10). Default 999999 — de propósito bem alto, pra vencer qualquer coisa que a página já tenha com z-index próprio (ex.: Leaflet usa 1000 nos controles de zoom por padrão). |
TourStep
interface TourStep {
targetId: string; // id do elemento-alvo no DOM
title: string;
content: string;
position?: 'top' | 'bottom' | 'left' | 'right'; // default 'bottom'
}useTourPosition(targetId, active, stepKey)
Hook headless usado internamente — exportado pra quem quiser construir uma UI
de tour totalmente própria sem usar <PageTour>. Retorna { coords, isMobile
}, recalculado em resize/scroll, com scroll-into-view automático quando o
stepKey muda e o alvo está fora da área confortável da viewport.
Limitações conhecidas (v0.0.0 — versão mínima deliberada)
- Sem persistência: não lembra se o usuário já viu o tour. Fica a cargo do
produto (ex: salvar em
localStorageou no perfil do usuário). - Um elemento-alvo por passo — não há suporte a destacar múltiplos elementos simultaneamente.
framer-motioné peer dependency obrigatória, não opcional.
