|
|
|
|
@ -0,0 +1,760 @@
|
|
|
|
|
<script lang="ts">
|
|
|
|
|
/**
|
|
|
|
|
* The composed page — F2b's closing gate.
|
|
|
|
|
*
|
|
|
|
|
* The tier's claim is that its blocks assemble without friction, and that
|
|
|
|
|
* claim is **indemostrable block by block**: the heading outline only exists
|
|
|
|
|
* ACROSS sections, landmarks only collide when there are two, vertical rhythm
|
|
|
|
|
* only has a seam when something sits underneath. The 16px defect of
|
|
|
|
|
* `content-section` appeared the first time another section went below it, and
|
|
|
|
|
* A-95 (the affixed strip covering the pinned header) is a defect that only
|
|
|
|
|
* exists crossed.
|
|
|
|
|
*
|
|
|
|
|
* So this is not a parade of blocks: it is a believable landing for one
|
|
|
|
|
* product, with the content the demos already ship — same company, same
|
|
|
|
|
* customers, same prices — and the 14 blocks a real landing carries. The four
|
|
|
|
|
* that do not fit naturally (team · contact · content-section · article-grid)
|
|
|
|
|
* are registered as NOT composed in the page's README; a second composed page
|
|
|
|
|
* is a gap with a trigger, not this page's scope.
|
|
|
|
|
*
|
|
|
|
|
* The heading policy, which is the thing being demonstrated: ONE `h1` (the
|
|
|
|
|
* hero) and one `h2` per section. Where a block owns its title it takes
|
|
|
|
|
* `level`; where it does not, the app passes its own `Heading`.
|
|
|
|
|
*/
|
|
|
|
|
import { SiteBanner } from '$blocks/banner';
|
|
|
|
|
import { SiteHeader } from '$blocks/site-header';
|
|
|
|
|
import { Hero } from '$blocks/hero';
|
|
|
|
|
import { LogoCloud } from '$blocks/logo-cloud';
|
|
|
|
|
import { FeatureGrid } from '$blocks/feature-grid';
|
|
|
|
|
import { FeatureSplit } from '$blocks/feature-split';
|
|
|
|
|
import { StatsBand } from '$blocks/stats-band';
|
|
|
|
|
import { Testimonials } from '$blocks/testimonials';
|
|
|
|
|
import { Pricing } from '$blocks/pricing';
|
|
|
|
|
import { Faq } from '$blocks/faq';
|
|
|
|
|
import { Cta } from '$blocks/cta';
|
|
|
|
|
import { Newsletter } from '$blocks/newsletter';
|
|
|
|
|
import { SiteFooter } from '$blocks/site-footer';
|
|
|
|
|
import { CookieConsent, type CookieConsentValue } from '$blocks/cookie-consent';
|
|
|
|
|
|
|
|
|
|
import { NavigationMenu } from '$uix/eidos/components/navigation-menu';
|
|
|
|
|
import { Drawer } from '$uix/eidos/components/drawer';
|
|
|
|
|
import { Stack } from '$uix/eidos/components/stack';
|
|
|
|
|
import { Group } from '$uix/eidos/components/group';
|
|
|
|
|
import { Wrap } from '$uix/eidos/components/wrap';
|
|
|
|
|
import { Flex } from '$uix/eidos/components/flex';
|
|
|
|
|
import { Surface } from '$uix/eidos/components/surface';
|
|
|
|
|
import { Mockup } from '$uix/eidos/components/mockup';
|
|
|
|
|
import { AspectRatio } from '$uix/eidos/components/aspect-ratio';
|
|
|
|
|
import { Heading } from '$uix/eidos/components/heading';
|
|
|
|
|
import { Text } from '$uix/eidos/components/text';
|
|
|
|
|
import { Link } from '$uix/eidos/components/link';
|
|
|
|
|
import { Button } from '$uix/eidos/components/button';
|
|
|
|
|
import { IconButton } from '$uix/eidos/components/icon-button';
|
|
|
|
|
import { Badge } from '$uix/eidos/components/badge';
|
|
|
|
|
import { Avatar } from '$uix/eidos/components/avatar';
|
|
|
|
|
import { Separator } from '$uix/eidos/components/separator';
|
|
|
|
|
import { Callout } from '$uix/eidos/components/callout';
|
|
|
|
|
import { Field } from '$uix/eidos/components/field';
|
|
|
|
|
import * as Icon from '$uix/eidos/components/icon';
|
|
|
|
|
import { getActiveUix } from '$active-uix';
|
|
|
|
|
import { createForm, type FormIssue } from '$libs/forms';
|
|
|
|
|
import { object, string, pipe, email, meta } from '$sium/core';
|
|
|
|
|
|
|
|
|
|
let {
|
|
|
|
|
/**
|
|
|
|
|
* Re-confirmation vehicle for **A-95**, off by default. With `affix="top"`
|
|
|
|
|
* the strip pins to the same edge as the header and covers it — the
|
|
|
|
|
* defect's own scenario, kept reachable so this page can MEASURE it instead
|
|
|
|
|
* of citing it. The page's real composition is the strip in flow: it
|
|
|
|
|
* scrolls away and the header pins to a clear edge.
|
|
|
|
|
*/
|
|
|
|
|
bannerAffix = false
|
|
|
|
|
}: { bannerAffix?: boolean } = $props();
|
|
|
|
|
|
|
|
|
|
const uix = getActiveUix();
|
|
|
|
|
|
|
|
|
|
let noticeOpen = $state(true);
|
|
|
|
|
let mobileOpen = $state(false);
|
|
|
|
|
let period = $state<'monthly' | 'annual'>('monthly');
|
|
|
|
|
let consent = $state<CookieConsentValue | undefined>(undefined);
|
|
|
|
|
let cookiePanelOpen = $state(false);
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------- content
|
|
|
|
|
|
|
|
|
|
const product = [
|
|
|
|
|
{
|
|
|
|
|
href: '#analitica',
|
|
|
|
|
title: 'Analítica',
|
|
|
|
|
blurb: 'Cohortes, embudos y retención sobre tus eventos.'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
href: '#analitica',
|
|
|
|
|
title: 'Automatizaciones',
|
|
|
|
|
blurb: 'Reglas que reaccionan a lo que hacen tus clientes.'
|
|
|
|
|
},
|
|
|
|
|
{ href: '#novedades', title: 'Novedades', blurb: 'Lo que cambió cada semana.' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const solutions = [
|
|
|
|
|
{ href: '#precios', title: 'Startups', blurb: 'Del primer cliente a los mil.' },
|
|
|
|
|
{ href: '#precios', title: 'Enterprise', blurb: 'SSO, auditoría y residencia de datos.' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const marks = [
|
|
|
|
|
{ name: 'Northwind', w: 128 },
|
|
|
|
|
{ name: 'Umbra', w: 86 },
|
|
|
|
|
{ name: 'Kessel', w: 104 },
|
|
|
|
|
{ name: 'Volta', w: 72 },
|
|
|
|
|
{ name: 'Marisol', w: 118 },
|
|
|
|
|
{ name: 'Cobalt', w: 96 }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const features = [
|
|
|
|
|
{
|
|
|
|
|
icon: Icon.Gauge,
|
|
|
|
|
title: 'Sin muestreo',
|
|
|
|
|
text: 'Todos los eventos, no una fracción: los números cuadran con tu base de datos.'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
icon: Icon.Users,
|
|
|
|
|
title: 'Cohortes al día',
|
|
|
|
|
text: 'Qué hicieron, cuándo dejaron de hacerlo y qué cambió la semana en que lanzaste.'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
icon: Icon.Workflow,
|
|
|
|
|
title: 'Automatizaciones',
|
|
|
|
|
text: 'Reglas que reaccionan al flujo de eventos: avisar, mover, abrir un ticket.'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
icon: Icon.BellRing,
|
|
|
|
|
title: 'Alertas que importan',
|
|
|
|
|
text: 'Un umbral sobre cualquier métrica y el canal donde de verdad lo lees.'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
icon: Icon.ShieldCheck,
|
|
|
|
|
title: 'SSO y auditoría',
|
|
|
|
|
text: 'SAML, SCIM y un registro de quién vio qué, listo para tu equipo de seguridad.'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
icon: Icon.Clock,
|
|
|
|
|
title: 'Sin reprocesar',
|
|
|
|
|
text: 'Consulta cualquier ventana temporal al instante: el motor mantiene los agregados.'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const chart = ['3rem', '5rem', '3.6rem', '6.4rem', '4.4rem', '7.4rem', '5.6rem'];
|
|
|
|
|
|
|
|
|
|
const stats = [
|
|
|
|
|
{ count: 12500, display: '12.500', label: 'Eventos por segundo' },
|
|
|
|
|
{ count: 340, display: '340', label: 'Equipos de producto' },
|
|
|
|
|
{ count: undefined, display: '99,98 %', label: 'Disponibilidad medida' },
|
|
|
|
|
{ count: 48, display: '48', label: 'Integraciones nativas' }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const quotes = [
|
|
|
|
|
{
|
|
|
|
|
quote: 'Migramos en una tarde y los números por fin cuadran con nuestra base de datos.',
|
|
|
|
|
name: 'Ana Ruiz',
|
|
|
|
|
role: 'CTO en Northwind',
|
|
|
|
|
initials: 'AR',
|
|
|
|
|
color: 'indigo'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
quote: 'Las cohortes que antes tardaban un día ahora salen al instante, sin reprocesar.',
|
|
|
|
|
name: 'Marco Vidal',
|
|
|
|
|
role: 'Head of Growth en Umbra',
|
|
|
|
|
initials: 'MV',
|
|
|
|
|
color: 'teal'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
quote: 'Configuramos una alerta y cazamos una fuga de conversión el mismo día.',
|
|
|
|
|
name: 'Lucía Peña',
|
|
|
|
|
role: 'Product Manager en Kessel',
|
|
|
|
|
initials: 'LP',
|
|
|
|
|
color: 'amber'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
type Plan = {
|
|
|
|
|
name: string;
|
|
|
|
|
desc: string;
|
|
|
|
|
mo: string;
|
|
|
|
|
yr: string;
|
|
|
|
|
cta: string;
|
|
|
|
|
featured: boolean;
|
|
|
|
|
features: string[];
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const plans: Plan[] = [
|
|
|
|
|
{
|
|
|
|
|
name: 'Starter',
|
|
|
|
|
desc: 'Para empezar a medir',
|
|
|
|
|
mo: '0 €',
|
|
|
|
|
yr: '0 €',
|
|
|
|
|
cta: 'Empezar gratis',
|
|
|
|
|
featured: false,
|
|
|
|
|
features: ['Hasta 10k eventos/mes', '1 proyecto', 'Retención 7 días']
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Pro',
|
|
|
|
|
desc: 'Para equipos de producto',
|
|
|
|
|
mo: '29 €',
|
|
|
|
|
yr: '23 €',
|
|
|
|
|
cta: 'Probar 14 días',
|
|
|
|
|
featured: true,
|
|
|
|
|
features: [
|
|
|
|
|
'Eventos ilimitados',
|
|
|
|
|
'Proyectos ilimitados',
|
|
|
|
|
'Cohortes, embudos y retención',
|
|
|
|
|
'Alertas y automatizaciones',
|
|
|
|
|
'Retención de 12 meses'
|
|
|
|
|
]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Enterprise',
|
|
|
|
|
desc: 'SSO, auditoría y SLA',
|
|
|
|
|
mo: '99 €',
|
|
|
|
|
yr: '79 €',
|
|
|
|
|
cta: 'Hablar con ventas',
|
|
|
|
|
featured: false,
|
|
|
|
|
features: [
|
|
|
|
|
'Todo lo de Pro',
|
|
|
|
|
'SSO/SAML + SCIM',
|
|
|
|
|
'Residencia de datos',
|
|
|
|
|
'SLA 99,9 %',
|
|
|
|
|
'Soporte dedicado'
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const faqs = [
|
|
|
|
|
{
|
|
|
|
|
q: '¿Cómo empiezo a medir?',
|
|
|
|
|
a: 'Instalas un único SDK, envías tu primer evento y ya lo ves en el panel. Sin configurar pipelines ni esperar a un procesamiento nocturno.'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
q: '¿Cobráis por asiento?',
|
|
|
|
|
a: 'No. El plan se elige por volumen de eventos, no por número de personas, así que puedes invitar a todo el equipo sin que suba la factura.'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
q: '¿Se muestrean los eventos?',
|
|
|
|
|
a: 'Nunca. Guardamos todos los eventos, así que los números cuadran con tu base de datos hasta el último registro.'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
q: '¿Puedo consultar periodos antiguos sin reprocesar?',
|
|
|
|
|
a: 'Sí. El motor mantiene los agregados al día, así que cualquier ventana temporal responde al instante.'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
q: '¿Ofrecéis SSO y auditoría?',
|
|
|
|
|
a: 'En el plan Enterprise: SAML, SCIM, registro de auditoría y residencia de datos, listos para tu equipo de seguridad.'
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
const footerGroups = [
|
|
|
|
|
{ title: 'Producto', links: ['Analítica', 'Automatizaciones', 'Integraciones', 'Novedades'] },
|
|
|
|
|
{ title: 'Recursos', links: ['Documentación', 'Guías', 'Estado del servicio'] },
|
|
|
|
|
{ title: 'Empresa', links: ['Sobre nosotros', 'Empleo', 'Contacto'] }
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// ------------------------------------------------------------------ form
|
|
|
|
|
// The APP owns the schema and the handler — the block's form pass-through.
|
|
|
|
|
const schema = object({
|
|
|
|
|
email: pipe(string(), email(), meta({ label: 'Correo electrónico' }))
|
|
|
|
|
});
|
|
|
|
|
let subscribed = $state<string | null>(null);
|
|
|
|
|
const form = createForm({
|
|
|
|
|
schema,
|
|
|
|
|
defaults: { email: '' },
|
|
|
|
|
validationBehaviour: 'progressive',
|
|
|
|
|
onValidSubmit: (values) => {
|
|
|
|
|
subscribed = (values as { email: string }).email;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
const emailError = $derived.by(() => {
|
|
|
|
|
const issue = (form.issues.email as FormIssue[] | undefined)?.[0];
|
|
|
|
|
if (issue) return uix.langs.t(issue.message, issue.params);
|
|
|
|
|
const raw = form.errors.email?.[0];
|
|
|
|
|
return raw ? uix.langs.t(raw) : undefined;
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
TWO banner landmarks, and only one of them is avoidable. The canon `Banner`
|
|
|
|
|
stamps `role="banner"` AFTER its rest props, so neither the block nor this page
|
|
|
|
|
can take it off, and a page that also has a `<header>` — every page — ends up
|
|
|
|
|
with two. Naming BOTH is the whole of what an app can do, and this page does it
|
|
|
|
|
(`aria-label` here, `aria-label="Acme"` on the header). Measured: without the
|
|
|
|
|
name, one of the two banners is anonymous in the accessibility tree.
|
|
|
|
|
|
|
|
|
|
The strip in FLOW, above the header: it scrolls away and the header pins to a
|
|
|
|
|
clear edge. `affix="top"` is A-95's scenario — both compete for the same edge
|
|
|
|
|
and the strip wins — and it stays reachable through `bannerAffix` so this page
|
|
|
|
|
can re-measure the defect rather than cite it.
|
|
|
|
|
-->
|
|
|
|
|
{#if noticeOpen}
|
|
|
|
|
<SiteBanner
|
|
|
|
|
intent="primary"
|
|
|
|
|
variant="solid"
|
|
|
|
|
affix={bannerAffix ? 'top' : undefined}
|
|
|
|
|
onDismiss={() => (noticeOpen = false)}
|
|
|
|
|
aria-label="Aviso del producto"
|
|
|
|
|
>
|
|
|
|
|
{#snippet badge()}<Badge size="sm" variant="soft">Nuevo</Badge>{/snippet}
|
|
|
|
|
Las cohortes en directo ya están disponibles en todos los planes.
|
|
|
|
|
{#snippet action()}<Link href="#novedades" color="currentColor">Ver qué cambia</Link>{/snippet}
|
|
|
|
|
</SiteBanner>
|
|
|
|
|
{/if}
|
|
|
|
|
|
|
|
|
|
<SiteHeader
|
|
|
|
|
bind:mobileOpen
|
|
|
|
|
aria-label="Acme"
|
|
|
|
|
style="background: var(--color-surface-default); border-block-end: var(--border-width) solid var(--color-border-subtle);"
|
|
|
|
|
>
|
|
|
|
|
{#snippet brand()}
|
|
|
|
|
<Link href="#top" variant="plain" underline="none">
|
|
|
|
|
<Group align="center" gap={2} justify="start">
|
|
|
|
|
<Icon.Boxes size="sm" />
|
|
|
|
|
<Text weight="semibold">Acme</Text>
|
|
|
|
|
</Group>
|
|
|
|
|
</Link>
|
|
|
|
|
{/snippet}
|
|
|
|
|
|
|
|
|
|
{#snippet nav()}
|
|
|
|
|
<NavigationMenu>
|
|
|
|
|
<NavigationMenu.List>
|
|
|
|
|
<NavigationMenu.Item value="product">
|
|
|
|
|
<NavigationMenu.Trigger>Producto</NavigationMenu.Trigger>
|
|
|
|
|
<NavigationMenu.Content>
|
|
|
|
|
<Stack gap={3} style="inline-size: 20rem;">
|
|
|
|
|
{#each product as entry (entry.title)}
|
|
|
|
|
<Stack gap={1}>
|
|
|
|
|
<Link href={entry.href}>{entry.title}</Link>
|
|
|
|
|
<Text size="sm" color="muted">{entry.blurb}</Text>
|
|
|
|
|
</Stack>
|
|
|
|
|
{/each}
|
|
|
|
|
</Stack>
|
|
|
|
|
</NavigationMenu.Content>
|
|
|
|
|
</NavigationMenu.Item>
|
|
|
|
|
<NavigationMenu.Item value="solutions">
|
|
|
|
|
<NavigationMenu.Trigger>Soluciones</NavigationMenu.Trigger>
|
|
|
|
|
<NavigationMenu.Content>
|
|
|
|
|
<Stack gap={3} style="inline-size: 20rem;">
|
|
|
|
|
{#each solutions as entry (entry.title)}
|
|
|
|
|
<Stack gap={1}>
|
|
|
|
|
<Link href={entry.href}>{entry.title}</Link>
|
|
|
|
|
<Text size="sm" color="muted">{entry.blurb}</Text>
|
|
|
|
|
</Stack>
|
|
|
|
|
{/each}
|
|
|
|
|
</Stack>
|
|
|
|
|
</NavigationMenu.Content>
|
|
|
|
|
</NavigationMenu.Item>
|
|
|
|
|
<NavigationMenu.Item value="pricing">
|
|
|
|
|
<NavigationMenu.Link href="#precios">Precios</NavigationMenu.Link>
|
|
|
|
|
</NavigationMenu.Item>
|
|
|
|
|
<NavigationMenu.Item value="faq">
|
|
|
|
|
<NavigationMenu.Link href="#preguntas">Preguntas</NavigationMenu.Link>
|
|
|
|
|
</NavigationMenu.Item>
|
|
|
|
|
</NavigationMenu.List>
|
|
|
|
|
</NavigationMenu>
|
|
|
|
|
{/snippet}
|
|
|
|
|
|
|
|
|
|
{#snippet actions()}
|
|
|
|
|
<Link href="#precios" variant="subtle">Entrar</Link>
|
|
|
|
|
<Button variant="solid" size="sm">
|
|
|
|
|
{#snippet endIcon()}<Icon.ArrowRight size="xs" />{/snippet}
|
|
|
|
|
{#snippet child({ props, content })}
|
|
|
|
|
<a href="#precios" {...props}>{@render content()}</a>
|
|
|
|
|
{/snippet}
|
|
|
|
|
Empezar gratis
|
|
|
|
|
</Button>
|
|
|
|
|
{/snippet}
|
|
|
|
|
|
|
|
|
|
{#snippet mobileTrigger()}
|
|
|
|
|
<Drawer.Trigger>
|
|
|
|
|
{#snippet child({ props })}
|
|
|
|
|
<Button {...props} variant="ghost" size="sm" aria-label="Abrir navegación">
|
|
|
|
|
<Icon.Menu size="sm" />
|
|
|
|
|
</Button>
|
|
|
|
|
{/snippet}
|
|
|
|
|
</Drawer.Trigger>
|
|
|
|
|
{/snippet}
|
|
|
|
|
|
|
|
|
|
{#snippet mobileNav()}
|
|
|
|
|
<Stack gap={3}>
|
|
|
|
|
<Text size="sm" weight="medium" color="muted">Producto</Text>
|
|
|
|
|
{#each product as entry (entry.title)}
|
|
|
|
|
<Link href={entry.href} onclick={() => (mobileOpen = false)}>{entry.title}</Link>
|
|
|
|
|
{/each}
|
|
|
|
|
<Separator />
|
|
|
|
|
<Text size="sm" weight="medium" color="muted">Soluciones</Text>
|
|
|
|
|
{#each solutions as entry (entry.title)}
|
|
|
|
|
<Link href={entry.href} onclick={() => (mobileOpen = false)}>{entry.title}</Link>
|
|
|
|
|
{/each}
|
|
|
|
|
<Separator />
|
|
|
|
|
<Button variant="solid" size="sm" block onclick={() => (mobileOpen = false)}>
|
|
|
|
|
{#snippet child({ props, content })}
|
|
|
|
|
<a href="#precios" {...props}>{@render content()}</a>
|
|
|
|
|
{/snippet}
|
|
|
|
|
Empezar gratis
|
|
|
|
|
</Button>
|
|
|
|
|
</Stack>
|
|
|
|
|
{/snippet}
|
|
|
|
|
</SiteHeader>
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
`<main>` is the APP's, not any block's: the tier ships sections, and only the
|
|
|
|
|
page knows which of them is the main content. Without it the sections between
|
|
|
|
|
the header and the footer belong to no landmark at all.
|
|
|
|
|
-->
|
|
|
|
|
<main id="top">
|
|
|
|
|
<Hero layout="split" decor="glow">
|
|
|
|
|
{#snippet eyebrow()}<Badge size="sm" variant="soft">Beta pública</Badge>{/snippet}
|
|
|
|
|
{#snippet title()}Analítica de producto sin sorpresas en la factura{/snippet}
|
|
|
|
|
{#snippet description()}
|
|
|
|
|
Eventos crudos, cohortes al día y automatizaciones que reaccionan solas. Sin muestreo, sin
|
|
|
|
|
reprocesar y sin un contrato que crece con cada pico de tráfico.
|
|
|
|
|
{/snippet}
|
|
|
|
|
{#snippet actions()}
|
|
|
|
|
<Button variant="solid" size="lg">
|
|
|
|
|
{#snippet endIcon()}<Icon.ArrowRight size="xs" />{/snippet}
|
|
|
|
|
{#snippet child({ props, content })}
|
|
|
|
|
<a href="#precios" {...props}>{@render content()}</a>
|
|
|
|
|
{/snippet}
|
|
|
|
|
Empezar gratis
|
|
|
|
|
</Button>
|
|
|
|
|
<Link href="#analitica" variant="subtle">Ver cómo funciona</Link>
|
|
|
|
|
{/snippet}
|
|
|
|
|
{#snippet media()}
|
|
|
|
|
<Mockup chrome="browser" url="acme.app/cohortes">
|
|
|
|
|
<AspectRatio ratio={16 / 10}>
|
|
|
|
|
<Surface color="primary" variant="soft" padding={6} height="100%">
|
|
|
|
|
<Stack gap={4} height="100%">
|
|
|
|
|
<Group gap={3} align="center">
|
|
|
|
|
<Icon.Activity size="sm" />
|
|
|
|
|
<Text size="sm" weight="medium">Cohorte de marzo</Text>
|
|
|
|
|
</Group>
|
|
|
|
|
<Flex align="end" gap={3} grow="1">
|
|
|
|
|
{#each chart as height, i (i)}
|
|
|
|
|
<Surface color="primary" variant="solid" width="100%" {height} rounded />
|
|
|
|
|
{/each}
|
|
|
|
|
</Flex>
|
|
|
|
|
</Stack>
|
|
|
|
|
</Surface>
|
|
|
|
|
</AspectRatio>
|
|
|
|
|
</Mockup>
|
|
|
|
|
{/snippet}
|
|
|
|
|
</Hero>
|
|
|
|
|
|
|
|
|
|
<LogoCloud>
|
|
|
|
|
{#snippet eyebrow()}
|
|
|
|
|
<Text size="sm" color="muted">Equipos de producto que ya no muestrean</Text>
|
|
|
|
|
{/snippet}
|
|
|
|
|
<LogoCloud.Items>
|
|
|
|
|
{#each marks as mark (mark.name)}
|
|
|
|
|
<LogoCloud.Item>
|
|
|
|
|
<svg
|
|
|
|
|
viewBox="0 0 {mark.w} 24"
|
|
|
|
|
style="height: 100%; width: auto;"
|
|
|
|
|
role="img"
|
|
|
|
|
aria-label={mark.name}
|
|
|
|
|
>
|
|
|
|
|
<title>{mark.name}</title>
|
|
|
|
|
<text x="0" y="18" font-size="18" font-weight="700" fill="currentColor">
|
|
|
|
|
{mark.name}
|
|
|
|
|
</text>
|
|
|
|
|
</svg>
|
|
|
|
|
</LogoCloud.Item>
|
|
|
|
|
{/each}
|
|
|
|
|
</LogoCloud.Items>
|
|
|
|
|
</LogoCloud>
|
|
|
|
|
|
|
|
|
|
<FeatureGrid id="analitica">
|
|
|
|
|
<FeatureGrid.Header>
|
|
|
|
|
<Heading level={2} align="center">Todo lo que necesitas para entender tu producto</Heading>
|
|
|
|
|
<Text size="lg" color="muted" align="center">
|
|
|
|
|
Sin pipelines que mantener y sin esperar al procesamiento de la noche.
|
|
|
|
|
</Text>
|
|
|
|
|
</FeatureGrid.Header>
|
|
|
|
|
<FeatureGrid.Items minChildWidth="17rem">
|
|
|
|
|
{#each features as feature (feature.title)}
|
|
|
|
|
{@const Glyph = feature.icon}
|
|
|
|
|
<FeatureGrid.Item>
|
|
|
|
|
<FeatureGrid.ItemIcon><Glyph size="sm" /></FeatureGrid.ItemIcon>
|
|
|
|
|
<FeatureGrid.ItemTitle>{feature.title}</FeatureGrid.ItemTitle>
|
|
|
|
|
<FeatureGrid.ItemText>{feature.text}</FeatureGrid.ItemText>
|
|
|
|
|
</FeatureGrid.Item>
|
|
|
|
|
{/each}
|
|
|
|
|
</FeatureGrid.Items>
|
|
|
|
|
</FeatureGrid>
|
|
|
|
|
|
|
|
|
|
<!--
|
|
|
|
|
`feature-split` is the one section block with no `.Header` part, so the
|
|
|
|
|
section heading is hand-composed as its first child and the rows drop to
|
|
|
|
|
`level={3}`. Left at their default the three rows emit three sibling `h2`
|
|
|
|
|
and the section has no name — recorded as a gap in this page's README.
|
|
|
|
|
-->
|
|
|
|
|
<FeatureSplit>
|
|
|
|
|
<Stack gap={3} align="center">
|
|
|
|
|
<Heading level={2} align="center">Del evento crudo a la decisión</Heading>
|
|
|
|
|
<Text size="lg" color="muted" align="center">
|
|
|
|
|
Dos piezas que trabajan sobre el mismo flujo, sin copiar datos de una a otra.
|
|
|
|
|
</Text>
|
|
|
|
|
</Stack>
|
|
|
|
|
|
|
|
|
|
<FeatureSplit.Row>
|
|
|
|
|
{#snippet media()}
|
|
|
|
|
<Mockup chrome="browser" url="acme.app/cohortes">
|
|
|
|
|
<AspectRatio ratio={4 / 3}>
|
|
|
|
|
<Surface color="teal" variant="soft" padding={6} height="100%">
|
|
|
|
|
<Flex align="end" gap={3} height="100%">
|
|
|
|
|
{#each chart as height, i (i)}
|
|
|
|
|
<Surface color="teal" variant="solid" width="100%" {height} rounded />
|
|
|
|
|
{/each}
|
|
|
|
|
</Flex>
|
|
|
|
|
</Surface>
|
|
|
|
|
</AspectRatio>
|
|
|
|
|
</Mockup>
|
|
|
|
|
{/snippet}
|
|
|
|
|
<FeatureSplit.Eyebrow>Analítica</FeatureSplit.Eyebrow>
|
|
|
|
|
<FeatureSplit.Title level={3}>Cohortes al día, sin configurar pipelines</FeatureSplit.Title>
|
|
|
|
|
<FeatureSplit.Text>
|
|
|
|
|
Los eventos llegan crudos y salen en cohortes: qué hicieron, cuándo dejaron de hacerlo y qué
|
|
|
|
|
cambió la semana en que lanzaste.
|
|
|
|
|
</FeatureSplit.Text>
|
|
|
|
|
<FeatureSplit.Features>
|
|
|
|
|
<FeatureSplit.Feature>Sin muestreo</FeatureSplit.Feature>
|
|
|
|
|
<FeatureSplit.Feature>Ventanas sin reprocesar</FeatureSplit.Feature>
|
|
|
|
|
</FeatureSplit.Features>
|
|
|
|
|
</FeatureSplit.Row>
|
|
|
|
|
|
|
|
|
|
<FeatureSplit.Row reversed>
|
|
|
|
|
{#snippet media()}
|
|
|
|
|
<!-- The phone's screen takes a FIXED height, never an `AspectRatio`: in a
|
|
|
|
|
split column ~560px wide a 9/16 ratio makes a phone 995px tall — taller
|
|
|
|
|
than the viewport, with an empty screen. Measured on this page. -->
|
|
|
|
|
<Mockup chrome="phone">
|
|
|
|
|
<Surface color="amber" variant="soft" padding={5} height="22rem">
|
|
|
|
|
<Stack gap={4}>
|
|
|
|
|
<Text size="sm" weight="semibold">09:41</Text>
|
|
|
|
|
<Stack gap={1}>
|
|
|
|
|
<Text size="sm" weight="medium">Conversión −18 %</Text>
|
|
|
|
|
<Text size="sm" color="muted">Umbral superado a las 09:14</Text>
|
|
|
|
|
</Stack>
|
|
|
|
|
<Surface color="amber" variant="solid" rounded height="2.5rem" />
|
|
|
|
|
<Surface color="amber" variant="soft" rounded height="2.5rem" />
|
|
|
|
|
</Stack>
|
|
|
|
|
</Surface>
|
|
|
|
|
</Mockup>
|
|
|
|
|
{/snippet}
|
|
|
|
|
<FeatureSplit.Eyebrow>En el bolsillo</FeatureSplit.Eyebrow>
|
|
|
|
|
<FeatureSplit.Title level={3}>Las alertas que importan, donde las lees</FeatureSplit.Title>
|
|
|
|
|
<FeatureSplit.Text>
|
|
|
|
|
Un umbral sobre cualquier métrica y el canal —o el móvil— donde de verdad lo ves. Sin abrir
|
|
|
|
|
el panel.
|
|
|
|
|
</FeatureSplit.Text>
|
|
|
|
|
<FeatureSplit.Features>
|
|
|
|
|
<FeatureSplit.Feature>Umbral por métrica</FeatureSplit.Feature>
|
|
|
|
|
<FeatureSplit.Feature>Al canal que ya usas</FeatureSplit.Feature>
|
|
|
|
|
</FeatureSplit.Features>
|
|
|
|
|
</FeatureSplit.Row>
|
|
|
|
|
</FeatureSplit>
|
|
|
|
|
|
|
|
|
|
<StatsBand>
|
|
|
|
|
{#each stats as stat (stat.label)}
|
|
|
|
|
<StatsBand.Stat>
|
|
|
|
|
{#if stat.count === undefined}
|
|
|
|
|
<StatsBand.Value>{stat.display}</StatsBand.Value>
|
|
|
|
|
{:else}
|
|
|
|
|
<StatsBand.Value count={stat.count} />
|
|
|
|
|
{/if}
|
|
|
|
|
<StatsBand.Label>{stat.label}</StatsBand.Label>
|
|
|
|
|
</StatsBand.Stat>
|
|
|
|
|
{/each}
|
|
|
|
|
</StatsBand>
|
|
|
|
|
|
|
|
|
|
<Testimonials>
|
|
|
|
|
<Testimonials.Header>
|
|
|
|
|
<Heading level={2} align="center">Lo que dicen los equipos que ya migraron</Heading>
|
|
|
|
|
</Testimonials.Header>
|
|
|
|
|
<Testimonials.Items>
|
|
|
|
|
{#each quotes as entry (entry.name)}
|
|
|
|
|
<Testimonials.Item>
|
|
|
|
|
<Testimonials.Quote>«{entry.quote}»</Testimonials.Quote>
|
|
|
|
|
<Testimonials.Author>
|
|
|
|
|
{#snippet avatar()}
|
|
|
|
|
<Avatar color={entry.color}>
|
|
|
|
|
<Avatar.Fallback>{entry.initials}</Avatar.Fallback>
|
|
|
|
|
</Avatar>
|
|
|
|
|
{/snippet}
|
|
|
|
|
<Testimonials.AuthorName>{entry.name}</Testimonials.AuthorName>
|
|
|
|
|
<Testimonials.AuthorRole>{entry.role}</Testimonials.AuthorRole>
|
|
|
|
|
</Testimonials.Author>
|
|
|
|
|
</Testimonials.Item>
|
|
|
|
|
{/each}
|
|
|
|
|
</Testimonials.Items>
|
|
|
|
|
</Testimonials>
|
|
|
|
|
|
|
|
|
|
<Pricing id="precios" bind:period>
|
|
|
|
|
<Pricing.Header>
|
|
|
|
|
<Heading level={2} align="center">Precios que siguen a tus eventos, no a tu plantilla</Heading
|
|
|
|
|
>
|
|
|
|
|
<Text size="lg" color="muted" align="center">
|
|
|
|
|
Sin coste por asiento: invita a todo el equipo.
|
|
|
|
|
</Text>
|
|
|
|
|
</Pricing.Header>
|
|
|
|
|
<Pricing.Switch
|
|
|
|
|
monthlyLabel="Mensual"
|
|
|
|
|
annualLabel="Anual (−20 %)"
|
|
|
|
|
aria-label="Periodo de facturación"
|
|
|
|
|
/>
|
|
|
|
|
<Pricing.Plans>
|
|
|
|
|
{#each plans as plan (plan.name)}
|
|
|
|
|
<Pricing.Plan featured={plan.featured}>
|
|
|
|
|
{#snippet badge()}
|
|
|
|
|
{#if plan.featured}<Badge color="primary" size="sm">Más popular</Badge>{/if}
|
|
|
|
|
{/snippet}
|
|
|
|
|
<Pricing.PlanName>{plan.name}</Pricing.PlanName>
|
|
|
|
|
<Pricing.PlanDescription>{plan.desc}</Pricing.PlanDescription>
|
|
|
|
|
<Pricing.PlanPrice>
|
|
|
|
|
{#snippet monthly()}{plan.mo}{/snippet}
|
|
|
|
|
{#snippet annual()}{plan.yr}{/snippet}
|
|
|
|
|
{#snippet suffix()}/mes{/snippet}
|
|
|
|
|
</Pricing.PlanPrice>
|
|
|
|
|
<Pricing.PlanFeatures>
|
|
|
|
|
{#each plan.features as feature (feature)}
|
|
|
|
|
<Pricing.PlanFeature>{feature}</Pricing.PlanFeature>
|
|
|
|
|
{/each}
|
|
|
|
|
</Pricing.PlanFeatures>
|
|
|
|
|
<Pricing.PlanAction>
|
|
|
|
|
<Button block variant={plan.featured ? 'solid' : 'surface'}>{plan.cta}</Button>
|
|
|
|
|
</Pricing.PlanAction>
|
|
|
|
|
</Pricing.Plan>
|
|
|
|
|
{/each}
|
|
|
|
|
</Pricing.Plans>
|
|
|
|
|
</Pricing>
|
|
|
|
|
|
|
|
|
|
<Faq id="preguntas">
|
|
|
|
|
<Faq.Header>
|
|
|
|
|
<Heading level={2} align="center">Preguntas frecuentes</Heading>
|
|
|
|
|
</Faq.Header>
|
|
|
|
|
<Faq.List type="single" collapsible>
|
|
|
|
|
{#each faqs as entry (entry.q)}
|
|
|
|
|
<Faq.Item>
|
|
|
|
|
{#snippet question()}{entry.q}{/snippet}
|
|
|
|
|
{entry.a}
|
|
|
|
|
</Faq.Item>
|
|
|
|
|
{/each}
|
|
|
|
|
</Faq.List>
|
|
|
|
|
</Faq>
|
|
|
|
|
|
|
|
|
|
<Cta id="novedades">
|
|
|
|
|
{#snippet title()}Empieza a medir esta misma tarde{/snippet}
|
|
|
|
|
{#snippet description()}
|
|
|
|
|
Conecta el SDK, envía tu primer evento y ve la cohorte formarse en directo. Catorce días de
|
|
|
|
|
Pro, sin compromiso.
|
|
|
|
|
{/snippet}
|
|
|
|
|
{#snippet actions()}
|
|
|
|
|
<Button variant="solid" size="lg" intent="fulfill">
|
|
|
|
|
{#snippet endIcon()}<Icon.ArrowRight size="xs" />{/snippet}
|
|
|
|
|
{#snippet child({ props, content })}
|
|
|
|
|
<a href="#precios" {...props}>{@render content()}</a>
|
|
|
|
|
{/snippet}
|
|
|
|
|
Empezar gratis
|
|
|
|
|
</Button>
|
|
|
|
|
{/snippet}
|
|
|
|
|
</Cta>
|
|
|
|
|
|
|
|
|
|
<Newsletter {form} {schema} sent={subscribed !== null}>
|
|
|
|
|
{#snippet eyebrow()}<Badge size="sm" variant="soft" color="primary">Cada martes</Badge
|
|
|
|
|
>{/snippet}
|
|
|
|
|
{#snippet title()}Una carta cada martes{/snippet}
|
|
|
|
|
{#snippet description()}
|
|
|
|
|
Cambios del producto, decisiones de diseño y lo que aprendimos rompiéndolo. Sin relleno y sin
|
|
|
|
|
promociones.
|
|
|
|
|
{/snippet}
|
|
|
|
|
{#snippet field()}
|
|
|
|
|
<Field name="email" label="Correo electrónico" required floatingLabel size="lg">
|
|
|
|
|
<Field.Label>Correo electrónico</Field.Label>
|
|
|
|
|
<Field.Control>
|
|
|
|
|
<Field.Input type="email" placeholder=" " bind:value={form.values.email} />
|
|
|
|
|
</Field.Control>
|
|
|
|
|
{#if emailError}
|
|
|
|
|
<Field.ErrorText>{emailError}</Field.ErrorText>
|
|
|
|
|
{/if}
|
|
|
|
|
</Field>
|
|
|
|
|
{/snippet}
|
|
|
|
|
{#snippet submit()}<Newsletter.Submit size="lg" />{/snippet}
|
|
|
|
|
{#snippet note()}Nada de spam. Te das de baja en un clic.{/snippet}
|
|
|
|
|
{#snippet children()}
|
|
|
|
|
<Newsletter.Reason />
|
|
|
|
|
{#if subscribed}
|
|
|
|
|
<Callout intent="affirm">
|
|
|
|
|
<Callout.Icon />
|
|
|
|
|
<Callout.Title>Confirmado</Callout.Title>
|
|
|
|
|
<Callout.Content>
|
|
|
|
|
Te hemos enviado un correo a <strong>{subscribed}</strong> para confirmar.
|
|
|
|
|
</Callout.Content>
|
|
|
|
|
</Callout>
|
|
|
|
|
{/if}
|
|
|
|
|
{/snippet}
|
|
|
|
|
</Newsletter>
|
|
|
|
|
</main>
|
|
|
|
|
|
|
|
|
|
{#snippet footerBrand()}
|
|
|
|
|
<Stack gap={3} align="start">
|
|
|
|
|
<Group align="center" gap={2} justify="start">
|
|
|
|
|
<Icon.Boxes size="sm" />
|
|
|
|
|
<Text weight="semibold">Acme</Text>
|
|
|
|
|
</Group>
|
|
|
|
|
<Text size="sm" color="muted">Analítica de producto sin muestreo.</Text>
|
|
|
|
|
</Stack>
|
|
|
|
|
{/snippet}
|
|
|
|
|
|
|
|
|
|
{#snippet footerLegal()}
|
|
|
|
|
<Wrap gap={4} align="center">
|
|
|
|
|
<Text size="sm" color="muted">© 2026 Acme. Todos los derechos reservados.</Text>
|
|
|
|
|
<Link href="#top" variant="subtle" size="sm" color="var(--color-content-muted)">Privacidad</Link
|
|
|
|
|
>
|
|
|
|
|
<Link href="#top" variant="subtle" size="sm" color="var(--color-content-muted)">Términos</Link>
|
|
|
|
|
</Wrap>
|
|
|
|
|
{/snippet}
|
|
|
|
|
|
|
|
|
|
{#snippet footerSocial()}
|
|
|
|
|
<Group gap={1} align="center" justify="end">
|
|
|
|
|
<IconButton aria-label="Boletín RSS" variant="ghost" size="sm">
|
|
|
|
|
<Icon.Rss size="sm" />
|
|
|
|
|
</IconButton>
|
|
|
|
|
<IconButton aria-label="Foro de la comunidad" variant="ghost" size="sm">
|
|
|
|
|
<Icon.MessageCircle size="sm" />
|
|
|
|
|
</IconButton>
|
|
|
|
|
</Group>
|
|
|
|
|
{/snippet}
|
|
|
|
|
|
|
|
|
|
{#snippet footerExtra()}
|
|
|
|
|
<!-- The «cookie settings» control every footer owes, and the one that proves
|
|
|
|
|
withdrawing consent is as easy as giving it: it reopens the same panel. -->
|
|
|
|
|
<Button variant="ghost" size="sm" onclick={() => (cookiePanelOpen = true)}>
|
|
|
|
|
Preferencias de cookies
|
|
|
|
|
</Button>
|
|
|
|
|
{/snippet}
|
|
|
|
|
|
|
|
|
|
<SiteFooter brand={footerBrand} legal={footerLegal} social={footerSocial} extra={footerExtra}>
|
|
|
|
|
{#each footerGroups as group (group.title)}
|
|
|
|
|
<SiteFooter.Column>
|
|
|
|
|
<SiteFooter.ColumnTitle>{group.title}</SiteFooter.ColumnTitle>
|
|
|
|
|
{#each group.links as label (label)}
|
|
|
|
|
<Link href="#top" variant="subtle" size="sm" color="var(--color-content-secondary)">
|
|
|
|
|
{label}
|
|
|
|
|
</Link>
|
|
|
|
|
{/each}
|
|
|
|
|
</SiteFooter.Column>
|
|
|
|
|
{/each}
|
|
|
|
|
</SiteFooter>
|
|
|
|
|
|
|
|
|
|
<CookieConsent {consent} bind:open={cookiePanelOpen} onDecision={(next) => (consent = next)}>
|
|
|
|
|
Usamos cookies necesarias para que el sitio funcione y, con tu permiso, otras para recordar tus
|
|
|
|
|
preferencias y medir el uso.
|
|
|
|
|
{#snippet policy()}
|
|
|
|
|
<Link href="#top">Leer la política de cookies</Link>
|
|
|
|
|
{/snippet}
|
|
|
|
|
</CookieConsent>
|