diff --git a/copy/COPY-GUIDE.md b/copy/COPY-GUIDE.md
index 8463403..91d9769 100644
--- a/copy/COPY-GUIDE.md
+++ b/copy/COPY-GUIDE.md
@@ -714,6 +714,40 @@ del espacio. `[url]` apunta a su formulario personal del funnel. Tono: una sola
---
+## Onboarding del panel (tour guiado)
+
+> Tooltips del tour del panel (driver.js). Tono cercano y útil, una idea por paso, frases cortas. Las pestañas secundarias se explican "de pasada" (una línea). Copy usado en `src/lib/onboarding/panel-tour.ts`.
+
+### Pestaña Leads (`/panel`)
+
+- **Intro** — *Tu panel de Reformix* · "Te enseño en 30 segundos qué hay en cada sitio. Puedes saltártelo cuando quieras con la X."
+- **Leads** — "Aquí caen tus clientes con su presupuesto y su render ya preparados. Es tu día a día."
+- **Precios y baremo** — "Tu tabla de precios, la mano de obra y el baremo de rentabilidad. De aquí salen los presupuestos."
+- **Galería** — "Tus fotos de trabajos para enseñar en la web."
+- **Opiniones** — "Reseñas de tus clientes; las apruebas tú antes de publicarlas."
+- **Empresa** — "Tu marca, logo y datos de contacto."
+- **Filtra por estado** — "Nuevos, contactados, presupuestados… para centrarte en lo que toca ahora."
+- **Tus leads** — "Cada cliente con su estado y su presupuesto. Ábrelo para ver el detalle completo."
+
+### Ficha del lead (`/panel/{id}`)
+
+- **Presupuesto estimado** — "Lo que costaría la reforma según tu catálogo. Si no llega a tu baremo, lo verás en rojo."
+- **Estado del lead** — "Avanza el lead por el funnel: contactado, presupuestado, ganado…"
+- **Render de la reforma** — "La imagen del «después» que ve tu cliente, generada a partir de su foto y sus gustos."
+- **Presupuesto desglosado** — "Edita las partidas, recalcula desde el catálogo y envíaselo al cliente por WhatsApp."
+
+### Precios y baremo (`/panel/precios`)
+
+- **Baremo de rentabilidad** — "El trabajo mínimo que te compensa. Solo te avisa a ti: marca en rojo los leads por debajo."
+- **Mano de obra** — "Tus precios de mano de obra por m². El motor los usa para calcular cada presupuesto."
+- **Tu catálogo** — "Materiales y precios por calidad. Puedes importarlos en bloque por CSV."
+
+### Botón para repetir
+
+- **Botón flotante** — "❓ Tour" (relanza el tour de la pestaña actual).
+
+---
+
## Principios aplicados en todo el documento
1. **Beneficios > Features** — "Tus clientes verán su reforma" > "Render IA con SDXL"
diff --git a/mvp/b2c/package-lock.json b/mvp/b2c/package-lock.json
index 4ca9c0f..6c0a51a 100644
--- a/mvp/b2c/package-lock.json
+++ b/mvp/b2c/package-lock.json
@@ -11,6 +11,7 @@
"@react-pdf/renderer": "^4.5.1",
"@tailwindcss/postcss": "^4.3.0",
"bcryptjs": "^3.0.3",
+ "driver.js": "^1.4.0",
"drizzle-orm": "^0.45.2",
"next": "16.2.6",
"nodemailer": "^8.0.10",
@@ -4546,6 +4547,12 @@
"url": "https://dotenvx.com"
}
},
+ "node_modules/driver.js": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/driver.js/-/driver.js-1.4.0.tgz",
+ "integrity": "sha512-Gm64jm6PmcU+si21sQhBrTAM1JvUrR0QhNmjkprNLxohOBzul9+pNHXgQaT9lW84gwg9GMLB3NZGuGolsz5uew==",
+ "license": "MIT"
+ },
"node_modules/drizzle-kit": {
"version": "0.31.10",
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz",
diff --git a/mvp/b2c/package.json b/mvp/b2c/package.json
index 8199518..962d3cb 100644
--- a/mvp/b2c/package.json
+++ b/mvp/b2c/package.json
@@ -21,6 +21,7 @@
"@react-pdf/renderer": "^4.5.1",
"@tailwindcss/postcss": "^4.3.0",
"bcryptjs": "^3.0.3",
+ "driver.js": "^1.4.0",
"drizzle-orm": "^0.45.2",
"next": "16.2.6",
"nodemailer": "^8.0.10",
diff --git a/mvp/b2c/src/app/panel/[id]/page.tsx b/mvp/b2c/src/app/panel/[id]/page.tsx
index ada2a1c..d4e30ba 100644
--- a/mvp/b2c/src/app/panel/[id]/page.tsx
+++ b/mvp/b2c/src/app/panel/[id]/page.tsx
@@ -20,9 +20,20 @@ import type { BudgetResult } from '@/budget/types';
export const dynamic = 'force-dynamic';
-function Section({ title, children }: { title: string; children: React.ReactNode }) {
+function Section({
+ title,
+ children,
+ tour,
+}: {
+ title: string;
+ children: React.ReactNode;
+ tour?: string;
+}) {
return (
- {title}
{children}
Importe mínimo de un trabajo para que te resulte rentable. Es solo orientativo para ti: en la
@@ -285,7 +285,7 @@ export default async function PreciosPage() {
})}
{/* Import CSV */}
-
Cabecera: Importar catálogo (CSV)
categoria,nombre,calidad,precio,unidad,descriptor_render,sku. El
diff --git a/mvp/b2c/src/components/AppNav.tsx b/mvp/b2c/src/components/AppNav.tsx
index 2d47f81..953fbd8 100644
--- a/mvp/b2c/src/components/AppNav.tsx
+++ b/mvp/b2c/src/components/AppNav.tsx
@@ -116,6 +116,7 @@ export default function AppNav({ links }: { links: readonly AppNavLink[] }) {
{l.label}
diff --git a/mvp/b2c/src/components/panel/PanelTour.tsx b/mvp/b2c/src/components/panel/PanelTour.tsx
new file mode 100644
index 0000000..9e62b38
--- /dev/null
+++ b/mvp/b2c/src/components/panel/PanelTour.tsx
@@ -0,0 +1,71 @@
+'use client';
+
+import { useEffect, useState } from 'react';
+import { usePathname } from 'next/navigation';
+import { driver, type DriveStep } from 'driver.js';
+import 'driver.js/dist/driver.css';
+import { tourForPath } from '@/lib/onboarding/panel-tour';
+
+const SEEN_PREFIX = 'reformix_tour_v1_';
+
+// Onboarding del panel con driver.js. Lanza el tour de la pestaña actual la primera vez que se
+// visita (flag por pestaña en localStorage) y deja un botón flotante para repetirlo. Los pasos
+// cuyo elemento no exista o esté oculto (p. ej. la nav de escritorio en móvil) se descartan.
+export default function PanelTour() {
+ const pathname = usePathname();
+ const [hayTour, setHayTour] = useState(false);
+
+ useEffect(() => {
+ const tour = tourForPath(pathname);
+ setHayTour(Boolean(tour));
+ if (!tour) return;
+ if (localStorage.getItem(SEEN_PREFIX + tour.key) === '1') return;
+
+ // Espera a que el contenido de la página esté montado antes de resaltar.
+ const t = setTimeout(() => {
+ localStorage.setItem(SEEN_PREFIX + tour.key, '1');
+ lanzar(tour.steps);
+ }, 700);
+ return () => clearTimeout(t);
+ }, [pathname]);
+
+ function visibles(steps: DriveStep[]): DriveStep[] {
+ return steps.filter((s) => {
+ const sel = s.element;
+ if (!sel || typeof sel !== 'string') return true; // paso centrado (intro)
+ const el = document.querySelector(sel) as HTMLElement | null;
+ return !!el && el.offsetParent !== null;
+ });
+ }
+
+ function lanzar(steps: DriveStep[]) {
+ const pasos = visibles(steps);
+ if (pasos.length === 0) return;
+ driver({
+ showProgress: true,
+ overlayColor: '#0b1220',
+ nextBtnText: 'Siguiente',
+ prevBtnText: 'Atrás',
+ doneBtnText: 'Listo',
+ progressText: '{{current}} de {{total}}',
+ steps: pasos,
+ }).drive();
+ }
+
+ function repetir() {
+ const tour = tourForPath(pathname);
+ if (tour) lanzar(tour.steps);
+ }
+
+ if (!hayTour) return null;
+ return (
+
+ );
+}
diff --git a/mvp/b2c/src/lib/onboarding/panel-tour.ts b/mvp/b2c/src/lib/onboarding/panel-tour.ts
new file mode 100644
index 0000000..8a634bd
--- /dev/null
+++ b/mvp/b2c/src/lib/onboarding/panel-tour.ts
@@ -0,0 +1,147 @@
+import type { DriveStep } from 'driver.js';
+
+// Pasos del onboarding del panel, por pestaña. El copy vive también en copy/COPY-GUIDE.md
+// (sección "Onboarding del panel"). Los pasos cuyo elemento no exista o no esté visible se
+// descartan en PanelTour (degrada con naturalidad en móvil o si una sección no aparece).
+
+const PASOS_PANEL: DriveStep[] = [
+ {
+ popover: {
+ title: 'Tu panel de Reformix',
+ description:
+ 'Te enseño en 30 segundos qué hay en cada sitio. Puedes saltártelo cuando quieras con la X.',
+ },
+ },
+ {
+ element: '[data-tour="nav-leads"]',
+ popover: {
+ title: 'Leads',
+ description:
+ 'Aquí caen tus clientes con su presupuesto y su render ya preparados. Es tu día a día.',
+ side: 'bottom',
+ },
+ },
+ {
+ element: '[data-tour="nav-precios"]',
+ popover: {
+ title: 'Precios y baremo',
+ description:
+ 'Tu tabla de precios, la mano de obra y el baremo de rentabilidad. De aquí salen los presupuestos.',
+ side: 'bottom',
+ },
+ },
+ {
+ element: '[data-tour="nav-galeria"]',
+ popover: { title: 'Galería', description: 'Tus fotos de trabajos para enseñar en la web.', side: 'bottom' },
+ },
+ {
+ element: '[data-tour="nav-opiniones"]',
+ popover: {
+ title: 'Opiniones',
+ description: 'Reseñas de tus clientes; las apruebas tú antes de publicarlas.',
+ side: 'bottom',
+ },
+ },
+ {
+ element: '[data-tour="nav-empresa"]',
+ popover: { title: 'Empresa', description: 'Tu marca, logo y datos de contacto.', side: 'bottom' },
+ },
+ {
+ element: '[data-tour="leads-filtros"]',
+ popover: {
+ title: 'Filtra por estado',
+ description: 'Nuevos, contactados, presupuestados… para centrarte en lo que toca ahora.',
+ side: 'bottom',
+ },
+ },
+ {
+ element: '[data-tour="leads-tabla"]',
+ popover: {
+ title: 'Tus leads',
+ description: 'Cada cliente con su estado y su presupuesto. Ábrelo para ver el detalle completo.',
+ side: 'top',
+ },
+ },
+];
+
+const PASOS_FICHA: DriveStep[] = [
+ {
+ element: '[data-tour="ficha-presupuesto"]',
+ popover: {
+ title: 'Presupuesto estimado',
+ description:
+ 'Lo que costaría la reforma según tu catálogo. Si no llega a tu baremo, lo verás en rojo.',
+ side: 'bottom',
+ },
+ },
+ {
+ element: '[data-tour="ficha-estado"]',
+ popover: {
+ title: 'Estado del lead',
+ description: 'Avanza el lead por el funnel: contactado, presupuestado, ganado…',
+ side: 'bottom',
+ },
+ },
+ {
+ element: '[data-tour="ficha-render"]',
+ popover: {
+ title: 'Render de la reforma',
+ description:
+ 'La imagen del “después” que ve tu cliente, generada a partir de su foto y sus gustos.',
+ side: 'top',
+ },
+ },
+ {
+ element: '[data-tour="ficha-desglose"]',
+ popover: {
+ title: 'Presupuesto desglosado',
+ description:
+ 'Edita las partidas, recalcula desde el catálogo y envíaselo al cliente por WhatsApp.',
+ side: 'top',
+ },
+ },
+];
+
+const PASOS_PRECIOS: DriveStep[] = [
+ {
+ element: '[data-tour="precios-baremo"]',
+ popover: {
+ title: 'Baremo de rentabilidad',
+ description:
+ 'El trabajo mínimo que te compensa. Solo te avisa a ti: marca en rojo los leads por debajo.',
+ side: 'bottom',
+ },
+ },
+ {
+ element: '[data-tour="precios-config"]',
+ popover: {
+ title: 'Mano de obra',
+ description: 'Tus precios de mano de obra por m². El motor los usa para calcular cada presupuesto.',
+ side: 'top',
+ },
+ },
+ {
+ element: '[data-tour="precios-catalogo"]',
+ popover: {
+ title: 'Tu catálogo',
+ description: 'Materiales y precios por calidad. Puedes importarlos en bloque por CSV.',
+ side: 'top',
+ },
+ },
+];
+
+export interface PanelTour {
+ key: string;
+ steps: DriveStep[];
+}
+
+// Devuelve el tour que corresponde a la ruta actual del panel, o null si esa ruta no tiene tour.
+export function tourForPath(pathname: string): PanelTour | null {
+ if (pathname === '/panel') return { key: 'panel', steps: PASOS_PANEL };
+ if (pathname === '/panel/precios') return { key: 'precios', steps: PASOS_PRECIOS };
+ const m = pathname.match(/^\/panel\/([^/]+)\/?$/);
+ if (m && !['precios', 'galeria', 'opiniones', 'empresa'].includes(m[1])) {
+ return { key: 'ficha', steps: PASOS_FICHA };
+ }
+ return null;
+}