Prueba social: una cabecera sobre una rejilla responsive de tarjetas de cita, cada una con su autor (avatar · nombre · cargo). Compound —la `.Item` se repite, como feature-grid— y SIN contexto (las partes no coordinan). `<Testimonials>` + `.Header` + `.Items` + `.Item` + `.Quote` + `.Author`(slot `avatar` + `.AuthorName`/`.AuthorRole`). - `AutoGrid` de tarjetas de igual alto; `.Author` fijada al borde inferior de la tarjeta (`margin-block-start: auto`) para que una fila de citas de distinto largo alinee las caras. - La cara es del app: un `<Avatar>` con imagen o un `Avatar.Fallback` de iniciales va en el slot `avatar` (B-7). - La tarjeta del quote usa `variant="outline"` — el `soft neutral` es casi invisible en claro. Encontrado al componer (en el README del block): el tema activa solo un SUBCONJUNTO de escalas donor (`green/indigo/orange/plum/teal`); una escala no activada (`cyan/ruby/amber/jade`) en `color` cae en SILENCIO a `primary` (la regla `[data-color]` hace `var(--scale-…, primary)`). Config del tema, no bug de componente; la demo usa escalas activadas. Hueco a decisión del usuario (Gaps): la **cita única en spotlight** (grande, centrada, logo+avatar+autor) — la variante modal del dossier, el grid es minoría. Otra disposición → candidato a hermano `testimonial-spotlight`. Demo (`web/routes/blocks/testimonials/`): full-bleed + ruta `preview`, cinco citas con avatares de iniciales en cinco colores distintos. Verificado en claro/oscuro × LTR/RTL, caras alineadas al fondo. `blocks:check` verde (6 blocks) · `svelte-check` sin errores propios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>alpha-0.1-sec-dom
parent
594e5a5db4
commit
b59332132f
@ -0,0 +1,64 @@
|
||||
# Testimonials
|
||||
|
||||
## Function
|
||||
|
||||
Social proof: a header over a responsive grid of quote cards, each carrying a
|
||||
quotation and its author (avatar · name · role). The "others trust this"
|
||||
section, usually below features or pricing.
|
||||
|
||||
## Composition map
|
||||
|
||||
| Part | Composes | Notes |
|
||||
| --- | --- | --- |
|
||||
| root | bare `<section>` + `Section` + `Container` + `Stack` | places the header and the grid |
|
||||
| `.Header` | `Box` + `Stack` | the app's section `Heading` + `Text` |
|
||||
| `.Items` | `AutoGrid` | fluid row of equal-height quote cards |
|
||||
| `.Item` | `Card` | one quote; fills the row height |
|
||||
| `.Quote` | `Text` (lg) | the quotation |
|
||||
| `.Author` | `Group` + `Stack` | the `avatar` slot + name/role, pinned to the card's bottom |
|
||||
| `.AuthorName` | `Text` (medium, sm) | |
|
||||
| `.AuthorRole` | `Text` (muted, sm) | role / company |
|
||||
|
||||
## Why compound
|
||||
|
||||
Its `Item` **repeats** — the app maps over N quotes — so it earns a compound
|
||||
API, like `feature-grid`. The parts do not coordinate (no shared state), so
|
||||
there is no context; the grid is the parent's, the cards the app's. `.Author`
|
||||
pins to the card's bottom (`margin-block-start: auto`) so a row of uneven quotes
|
||||
still lines its faces up.
|
||||
|
||||
## Decisions
|
||||
|
||||
**2026-07-24 — reference floor** (dossier §P1: Tailwind Plus «Testimonials 8» ·
|
||||
Untitled UI «Testimonials 26» · Flowbite):
|
||||
|
||||
- **Adopted**: the quote-card grid (quote + avatar + name + role) — the format
|
||||
the references ship, with equal-height cards and bottom-aligned authors.
|
||||
- **Discarded**: static dumps; the quotes are the app's markup, not a variant
|
||||
matrix.
|
||||
- **The avatar is the app's**: `<Avatar>` with an image or an `Avatar.Fallback`
|
||||
(initials) goes in the `avatar` slot — the block does not host faces (B-7).
|
||||
|
||||
## Demo
|
||||
|
||||
`web/routes/blocks/testimonials/` — the block full-bleed, a grid of quotes with
|
||||
avatar fallbacks. Mini-page in `TestimonialsSite.svelte`, shared by both
|
||||
surfaces.
|
||||
|
||||
## Gaps
|
||||
|
||||
| Gap | Disposition |
|
||||
| --- | --- |
|
||||
| **Single quote in spotlight** (one large quote, logo + avatar + author, centered) | **scope-approval pending** — the dossier's testimonials gap: it is the modal variant across the references, and the grid is the minority (2 of 8 in TW). It is a different arrangement (one big quote, not a grid), so likely a sibling (`testimonial-spotlight`) or a `layout` — a user decision, not a silent add |
|
||||
| **Logo strip** (customer logos instead of quotes) | **out** — that is the `logo-cloud` category (deferred at F2 scope), not this block |
|
||||
| **Carousel variant** (quotes in a scroller) | **deferred** — the canon `Carousel` exists; it enters when a demo asks |
|
||||
|
||||
## Found while composing
|
||||
|
||||
- **The base theme activates only a subset of donor palette scales** — `green`,
|
||||
`indigo`, `orange`, `plum`, `teal` (the `--scale-{name}-*` tokens are generated
|
||||
for these). A `color` set to an unactivated scale (`cyan`, `ruby`, `amber`,
|
||||
`jade`…) silently falls back to **primary** via the `[data-color]` rule's
|
||||
`var(--scale-…, primary)` — no error, just the wrong colour. The demo's avatars
|
||||
use activated scales. (Config, not a component bug; a louder fallback would be
|
||||
a theming nicety.)
|
||||
@ -0,0 +1,65 @@
|
||||
// Testimonials — a header over a responsive grid of quote cards, each with an
|
||||
// author (avatar + name + role). Compound (its `Item` repeats), like
|
||||
// feature-grid.
|
||||
//
|
||||
// import { Testimonials } from '$blocks/testimonials';
|
||||
// import { Avatar } from '$uix/eidos/components/avatar';
|
||||
//
|
||||
// <Testimonials>
|
||||
// <Testimonials.Header>
|
||||
// <Heading level={2} align="center">Lo que dicen los equipos</Heading>
|
||||
// </Testimonials.Header>
|
||||
// <Testimonials.Items>
|
||||
// <Testimonials.Item>
|
||||
// <Testimonials.Quote>«Migramos en una tarde.»</Testimonials.Quote>
|
||||
// <Testimonials.Author>
|
||||
// {#snippet avatar()}<Avatar><Avatar.Fallback>AR</Avatar.Fallback></Avatar>{/snippet}
|
||||
// <Testimonials.AuthorName>Ana Ruiz</Testimonials.AuthorName>
|
||||
// <Testimonials.AuthorRole>CTO, Northwind</Testimonials.AuthorRole>
|
||||
// </Testimonials.Author>
|
||||
// </Testimonials.Item>
|
||||
// </Testimonials.Items>
|
||||
// </Testimonials>
|
||||
//
|
||||
// Every visible string is the app's (B-7).
|
||||
import TestimonialsComponent from './testimonials.svelte';
|
||||
import Header from './testimonials-header.svelte';
|
||||
import Items from './testimonials-items.svelte';
|
||||
import Item from './testimonials-item.svelte';
|
||||
import Quote from './testimonials-quote.svelte';
|
||||
import Author from './testimonials-author.svelte';
|
||||
import AuthorName from './testimonials-author-name.svelte';
|
||||
import AuthorRole from './testimonials-author-role.svelte';
|
||||
|
||||
type TestimonialsNamespace = typeof TestimonialsComponent & {
|
||||
Header: typeof Header;
|
||||
Items: typeof Items;
|
||||
Item: typeof Item;
|
||||
Quote: typeof Quote;
|
||||
Author: typeof Author;
|
||||
AuthorName: typeof AuthorName;
|
||||
AuthorRole: typeof AuthorRole;
|
||||
};
|
||||
|
||||
const Testimonials = TestimonialsComponent as TestimonialsNamespace;
|
||||
Testimonials.Header = Header;
|
||||
Testimonials.Items = Items;
|
||||
Testimonials.Item = Item;
|
||||
Testimonials.Quote = Quote;
|
||||
Testimonials.Author = Author;
|
||||
Testimonials.AuthorName = AuthorName;
|
||||
Testimonials.AuthorRole = AuthorRole;
|
||||
|
||||
export { Testimonials };
|
||||
export default Testimonials;
|
||||
|
||||
export type {
|
||||
TestimonialsProps,
|
||||
TestimonialsHeaderProps as HeaderProps,
|
||||
TestimonialsItemsProps as ItemsProps,
|
||||
TestimonialsItemProps as ItemProps,
|
||||
TestimonialsQuoteProps as QuoteProps,
|
||||
TestimonialsAuthorProps as AuthorProps,
|
||||
TestimonialsAuthorNameProps as AuthorNameProps,
|
||||
TestimonialsAuthorRoleProps as AuthorRoleProps
|
||||
} from './types';
|
||||
@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
/** Testimonials.AuthorName — the author's name. */
|
||||
import { Text } from '$uix/eidos/components/text';
|
||||
import type { TestimonialsAuthorNameProps } from './types';
|
||||
|
||||
let { children, ...rest }: TestimonialsAuthorNameProps = $props();
|
||||
</script>
|
||||
|
||||
<Text size="sm" weight="medium" {...rest}>
|
||||
{@render children?.()}
|
||||
</Text>
|
||||
@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
/** Testimonials.AuthorRole — the author's role / company, muted. */
|
||||
import { Text } from '$uix/eidos/components/text';
|
||||
import type { TestimonialsAuthorRoleProps } from './types';
|
||||
|
||||
let { children, ...rest }: TestimonialsAuthorRoleProps = $props();
|
||||
</script>
|
||||
|
||||
<Text size="sm" color="muted" {...rest}>
|
||||
{@render children?.()}
|
||||
</Text>
|
||||
@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Testimonials.Author — the avatar + name / role, pinned to the card's bottom
|
||||
* (`margin-block-start: auto`) so a row of quotes lines its authors up. The app
|
||||
* puts an `<Avatar>` in the `avatar` slot and `AuthorName` / `AuthorRole` as
|
||||
* children.
|
||||
*/
|
||||
import { Group } from '$uix/eidos/components/group';
|
||||
import { Stack } from '$uix/eidos/components/stack';
|
||||
import type { TestimonialsAuthorProps } from './types';
|
||||
|
||||
let { avatar, children, ...rest }: TestimonialsAuthorProps = $props();
|
||||
</script>
|
||||
|
||||
<Group gap={3} align="center" justify="start" marginTop="auto" {...rest}>
|
||||
{@render avatar?.()}
|
||||
<Stack gap={0} align="start">
|
||||
{@render children?.()}
|
||||
</Stack>
|
||||
</Group>
|
||||
@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
/** Testimonials.Header — the section title + description, measure-capped and
|
||||
* centered by the parent. The app supplies a `Heading` (level 2) and a `Text`. */
|
||||
import { Box } from '$uix/eidos/components/box';
|
||||
import { Stack } from '$uix/eidos/components/stack';
|
||||
import type { TestimonialsHeaderProps } from './types';
|
||||
|
||||
let { maxWidth = '48rem', children, ...rest }: TestimonialsHeaderProps = $props();
|
||||
</script>
|
||||
|
||||
<Box {maxWidth} width="100%" {...rest}>
|
||||
<Stack gap={3}>
|
||||
{@render children?.()}
|
||||
</Stack>
|
||||
</Box>
|
||||
@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Testimonials.Item — one quote card. Fills the row's height (the grid
|
||||
* stretches it) with the quote at the top and the author at the bottom, so a
|
||||
* row of cards lines up.
|
||||
*/
|
||||
import { Card } from '$uix/eidos/components/card';
|
||||
import { Stack } from '$uix/eidos/components/stack';
|
||||
import type { TestimonialsItemProps } from './types';
|
||||
|
||||
let {
|
||||
variant = 'outline',
|
||||
color = 'neutral',
|
||||
size = 'lg',
|
||||
children,
|
||||
...rest
|
||||
}: TestimonialsItemProps = $props();
|
||||
</script>
|
||||
|
||||
<Card {variant} {color} {size} height="100%" {...rest}>
|
||||
<Stack gap={6} height="100%" align="start">
|
||||
{@render children?.()}
|
||||
</Stack>
|
||||
</Card>
|
||||
@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
/** Testimonials.Items — the responsive grid the quote cards lay out in. Fluid
|
||||
* by default; the cards stretch to equal height. */
|
||||
import { AutoGrid } from '$uix/eidos/components/auto-grid';
|
||||
import type { TestimonialsItemsProps } from './types';
|
||||
|
||||
let { minChildWidth = '20rem', columns, gap = 6, children, ...rest }: TestimonialsItemsProps =
|
||||
$props();
|
||||
</script>
|
||||
|
||||
<AutoGrid
|
||||
{...rest}
|
||||
minChildWidth={columns ? undefined : minChildWidth}
|
||||
{columns}
|
||||
{gap}
|
||||
align="stretch"
|
||||
width="100%"
|
||||
>
|
||||
{@render children?.()}
|
||||
</AutoGrid>
|
||||
@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
/** Testimonials.Quote — the quotation. The app supplies the words. */
|
||||
import { Text } from '$uix/eidos/components/text';
|
||||
import type { TestimonialsQuoteProps } from './types';
|
||||
|
||||
let { children, ...rest }: TestimonialsQuoteProps = $props();
|
||||
</script>
|
||||
|
||||
<Text size="lg" wrap="pretty" {...rest}>
|
||||
{@render children?.()}
|
||||
</Text>
|
||||
@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Testimonials — a header over a responsive grid of quote cards, each with an
|
||||
* author. Compound: its `Item` repeats (the app maps over N quotes), like
|
||||
* feature-grid — the parts do not coordinate.
|
||||
*
|
||||
* B contract: composes canon components only — no `.css`, no morfo, no
|
||||
* strings. The block places (Section · Container · AutoGrid); the app supplies
|
||||
* the words and the faces.
|
||||
*/
|
||||
import { Section } from '$uix/eidos/components/section';
|
||||
import { Container } from '$uix/eidos/components/container';
|
||||
import { Stack } from '$uix/eidos/components/stack';
|
||||
import type { TestimonialsProps } from './types';
|
||||
|
||||
let { container = 'lg', size = 'lg', children, ...rest }: TestimonialsProps = $props();
|
||||
</script>
|
||||
|
||||
<section {...rest}>
|
||||
<Section {size}>
|
||||
<Container size={container}>
|
||||
<Stack gap={10} align="center">
|
||||
{@render children?.()}
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
</section>
|
||||
@ -0,0 +1,45 @@
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { HTMLAttributes } from 'svelte/elements';
|
||||
import type { ContainerSize } from '$uix/eidos/components/container';
|
||||
import type { SectionSize } from '$uix/eidos/components/section';
|
||||
import type { BoxProps } from '$uix/eidos/components/box';
|
||||
import type { CardProps } from '$uix/eidos/components/card';
|
||||
import type { TextProps } from '$uix/eidos/components/text';
|
||||
import type { GroupProps } from '$uix/eidos/components/group';
|
||||
import type { AutoGridProps } from '$uix/eidos/components/auto-grid';
|
||||
|
||||
export type TestimonialsProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
|
||||
/** Content measure. Passed straight to `Container`. @default 'lg' */
|
||||
container?: ContainerSize;
|
||||
/** Block-axis padding. Passed straight to `Section`. @default 'lg' */
|
||||
size?: SectionSize;
|
||||
/** `Header` and `Items` in composition order. */
|
||||
children?: Snippet;
|
||||
};
|
||||
|
||||
/** Wraps a `Box` (measure-cap, centered). `maxWidth` defaults to `48rem`. */
|
||||
export type TestimonialsHeaderProps = BoxProps;
|
||||
|
||||
/** Wraps an `AutoGrid`. Fluid via `minChildWidth` (default `20rem`) or fixed `columns`. */
|
||||
export type TestimonialsItemsProps = AutoGridProps;
|
||||
|
||||
/** Wraps a `Card` (the quote card). `variant` defaults to `soft`. */
|
||||
export type TestimonialsItemProps = CardProps;
|
||||
|
||||
/** Wraps a `Text` — the quotation. `size` defaults to `lg`. */
|
||||
export type TestimonialsQuoteProps = TextProps;
|
||||
|
||||
/** Wraps a `Group`: the `avatar` slot + the name / role stack. Pinned to the
|
||||
* card's bottom so a row of quotes lines its authors up. */
|
||||
export type TestimonialsAuthorProps = Omit<GroupProps, 'children'> & {
|
||||
/** The avatar — an `<Avatar>` (with an image or an `Avatar.Fallback`). */
|
||||
avatar?: Snippet;
|
||||
/** `AuthorName` / `AuthorRole`, or free children. */
|
||||
children?: Snippet;
|
||||
};
|
||||
|
||||
/** Wraps a `Text` — the author's name. `weight` defaults to `medium`. */
|
||||
export type TestimonialsAuthorNameProps = TextProps;
|
||||
|
||||
/** Wraps a muted `Text` — the author's role / company. */
|
||||
export type TestimonialsAuthorRoleProps = TextProps;
|
||||
@ -0,0 +1,107 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Testimonials demo. Full-bleed on the page; device widths from `./preview`.
|
||||
*/
|
||||
import { Stack } from '$uix/eidos/components/stack';
|
||||
import { Text } from '$uix/eidos/components/text';
|
||||
import { Code } from '$uix/eidos/components/code';
|
||||
import BlockDemo from '../_lib/BlockDemo.svelte';
|
||||
import TestimonialsSite from './TestimonialsSite.svelte';
|
||||
import DocRow from '../_lib/DocRow.svelte';
|
||||
|
||||
const previewSrc = '/blocks/testimonials/preview';
|
||||
</script>
|
||||
|
||||
<BlockDemo
|
||||
name="Testimonials"
|
||||
slug="$blocks/testimonials"
|
||||
{previewSrc}
|
||||
previewHeight="760px"
|
||||
meta={[
|
||||
{ key: 'compone', value: 'Section · Container · AutoGrid · Card · Avatar · Text' },
|
||||
{ key: 'tipo', value: 'compound (.Item repite)' },
|
||||
{ key: 'partes', value: 'Header · Items · Item · Quote · Author*' },
|
||||
{ key: 'strings propias', value: '0' }
|
||||
]}
|
||||
>
|
||||
{#snippet preview()}
|
||||
<TestimonialsSite />
|
||||
{/snippet}
|
||||
|
||||
{#snippet lede()}
|
||||
Prueba social: una rejilla de citas, cada una con su autor (avatar · nombre · cargo). Compound
|
||||
—la <Code>.Item</Code> se repite— como <Code>feature-grid</Code>; las partes no coordinan, así que
|
||||
sin contexto.
|
||||
{/snippet}
|
||||
|
||||
{#snippet composition()}
|
||||
<Stack gap={4}>
|
||||
<Text color="muted">El block coloca las tarjetas; las palabras y las caras son de la app.</Text>
|
||||
<Stack gap={3}>
|
||||
<DocRow term=".Items · .Item">
|
||||
<Code>AutoGrid</Code> de tarjetas de igual alto; cada <Code>.Item</Code> es una
|
||||
<Code>Card</Code> que rellena la altura de la fila.
|
||||
</DocRow>
|
||||
<DocRow term=".Quote">
|
||||
<Code>Text</Code> (lg) con la cita. La app pone las comillas y las palabras.
|
||||
</DocRow>
|
||||
<DocRow term=".Author">
|
||||
<Code>Group</Code> con el slot <Code>avatar</Code> + <Code>.AuthorName</Code>/<Code>.AuthorRole</Code>,
|
||||
fijado al borde inferior de la tarjeta (<Code>margin-block-start: auto</Code>) para alinear
|
||||
las caras aunque las citas midan distinto.
|
||||
</DocRow>
|
||||
<DocRow term="avatar">
|
||||
La cara es del app: un <Code><Avatar></Code> con imagen o un
|
||||
<Code>Avatar.Fallback</Code> de iniciales. El block no aloja caras.
|
||||
</DocRow>
|
||||
</Stack>
|
||||
</Stack>
|
||||
{/snippet}
|
||||
|
||||
{#snippet api()}
|
||||
<Stack gap={3}>
|
||||
<DocRow term="Testimonials">
|
||||
<Code>container</Code> (def. <Code>lg</Code>) · <Code>size</Code> (def. <Code>lg</Code>).
|
||||
</DocRow>
|
||||
<DocRow term=".Items">
|
||||
<Code>minChildWidth</Code> (def. <Code>20rem</Code>) o <Code>columns</Code> · <Code>gap</Code>
|
||||
(def. <Code>6</Code>).
|
||||
</DocRow>
|
||||
<DocRow term=".Author">
|
||||
<Code>avatar</Code> (snippet) + children (<Code>.AuthorName</Code>/<Code>.AuthorRole</Code>).
|
||||
</DocRow>
|
||||
</Stack>
|
||||
{/snippet}
|
||||
|
||||
{#snippet a11y()}
|
||||
<Stack gap={3}>
|
||||
<DocRow term="Estructura">
|
||||
Un <Code><section></Code> con un <Code>h2</Code>. Las citas son <Code>Text</Code>; el
|
||||
avatar lleva su nombre accesible vía el <Code>Avatar</Code> del canon.
|
||||
</DocRow>
|
||||
</Stack>
|
||||
{/snippet}
|
||||
|
||||
{#snippet gaps()}
|
||||
<Stack gap={3}>
|
||||
<DocRow term="Cita única en spotlight">
|
||||
<strong>Scope-approval pendiente</strong> — la brecha del dossier en testimonials: una cita
|
||||
grande centrada (logo + avatar + autor), la variante modal en TODAS las refs; el grid es
|
||||
minoría. Es otra disposición → candidato a hermano <Code>testimonial-spotlight</Code>.
|
||||
</DocRow>
|
||||
<DocRow term="Carrusel">
|
||||
<strong>Diferido</strong> — el <Code>Carousel</Code> del canon existe; entra cuando una demo
|
||||
lo pida.
|
||||
</DocRow>
|
||||
</Stack>
|
||||
{/snippet}
|
||||
|
||||
{#snippet notes()}
|
||||
<Stack gap={3}>
|
||||
<DocRow term="Compound sin contexto">
|
||||
Como <Code>feature-grid</Code>/<Code>feature-split</Code>: la <Code>.Item</Code> se repite
|
||||
pero las partes no se hablan, así que no hay contexto (a diferencia de <Code>pricing</Code>).
|
||||
</DocRow>
|
||||
</Stack>
|
||||
{/snippet}
|
||||
</BlockDemo>
|
||||
@ -0,0 +1,96 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* The testimonials mini-page: a grid of quotes with avatar fallbacks, plus a
|
||||
* page floor.
|
||||
*/
|
||||
import { Testimonials } from '$blocks/testimonials';
|
||||
import { Avatar } from '$uix/eidos/components/avatar';
|
||||
import { Section } from '$uix/eidos/components/section';
|
||||
import { Container } from '$uix/eidos/components/container';
|
||||
import { Stack } from '$uix/eidos/components/stack';
|
||||
import { Wrap } from '$uix/eidos/components/wrap';
|
||||
import { Heading } from '$uix/eidos/components/heading';
|
||||
import { Text } from '$uix/eidos/components/text';
|
||||
|
||||
type Quote = { quote: string; name: string; role: string; initials: string; color: string };
|
||||
|
||||
const quotes: Quote[] = [
|
||||
{
|
||||
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: 'orange'
|
||||
},
|
||||
{
|
||||
quote: 'Sin coste por asiento: por fin invité a todo el equipo al panel.',
|
||||
name: 'Diego Sáez',
|
||||
role: 'Fundador en Volta',
|
||||
initials: 'DS',
|
||||
color: 'green'
|
||||
},
|
||||
{
|
||||
quote: 'El SSO y la auditoría pasaron la revisión de seguridad sin una sola fricción.',
|
||||
name: 'Sofía Marín',
|
||||
role: 'Engineering Lead en Marisol',
|
||||
initials: 'SM',
|
||||
color: 'plum'
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
<Testimonials>
|
||||
<Testimonials.Header>
|
||||
<Heading level={2} align="center">Equipos de producto que ya no vuelan a ciegas</Heading>
|
||||
<Text size="lg" color="muted" align="center">
|
||||
Lo que cambia cuando los eventos entran crudos y salen en cohortes.
|
||||
</Text>
|
||||
</Testimonials.Header>
|
||||
|
||||
<Testimonials.Items>
|
||||
{#each quotes as q (q.name)}
|
||||
<Testimonials.Item>
|
||||
<Testimonials.Quote>«{q.quote}»</Testimonials.Quote>
|
||||
<Testimonials.Author>
|
||||
{#snippet avatar()}
|
||||
<Avatar variant="solid" color={q.color}>
|
||||
<Avatar.Fallback>{q.initials}</Avatar.Fallback>
|
||||
</Avatar>
|
||||
{/snippet}
|
||||
<Testimonials.AuthorName>{q.name}</Testimonials.AuthorName>
|
||||
<Testimonials.AuthorRole>{q.role}</Testimonials.AuthorRole>
|
||||
</Testimonials.Author>
|
||||
</Testimonials.Item>
|
||||
{/each}
|
||||
</Testimonials.Items>
|
||||
</Testimonials>
|
||||
|
||||
<!-- A floor so the page reads as a page. -->
|
||||
<Section size="lg">
|
||||
<Container size="lg">
|
||||
<Stack gap={4} align="center">
|
||||
<Text size="sm" weight="medium" color="muted" align="center">
|
||||
Con la confianza de equipos de producto en
|
||||
</Text>
|
||||
<Wrap gap={8} justify="center">
|
||||
{#each ['Northwind', 'Umbra', 'Kessel', 'Volta', 'Marisol'] as brand (brand)}
|
||||
<Text size="lg" weight="semibold" color="muted">{brand}</Text>
|
||||
{/each}
|
||||
</Wrap>
|
||||
</Stack>
|
||||
</Container>
|
||||
</Section>
|
||||
@ -0,0 +1,30 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* The preview is its OWN page — `@` resets the layout. Axes arrive via the URL
|
||||
* so the demo's iframe can drive them.
|
||||
*/
|
||||
import { page } from '$app/state';
|
||||
import BootUix, { type BootLanguage } from '../../_lib/BootUix.svelte';
|
||||
import '@/uix/eidos/index.css';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
const mode = $derived(page.url.searchParams.get('mode') === 'dark' ? 'dark' : 'light');
|
||||
const dir = $derived(page.url.searchParams.get('dir') === 'rtl' ? 'rtl' : 'ltr');
|
||||
const language = $derived((page.url.searchParams.get('lang') ?? 'es') as BootLanguage);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
</svelte:head>
|
||||
|
||||
<BootUix {mode} {dir} {language}>
|
||||
<div
|
||||
data-theme={mode}
|
||||
data-mode={mode}
|
||||
{dir}
|
||||
style="min-block-size: 100dvh; background: var(--color-surface-default); color: var(--color-content-primary);"
|
||||
>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</BootUix>
|
||||
@ -0,0 +1,9 @@
|
||||
<script lang="ts">
|
||||
/**
|
||||
* Standalone page for the testimonials mini-site — same component the demo
|
||||
* renders inline, served as its OWN document for the device-width frame.
|
||||
*/
|
||||
import TestimonialsSite from '../TestimonialsSite.svelte';
|
||||
</script>
|
||||
|
||||
<TestimonialsSite />
|
||||
Loading…
Reference in new issue