feat(blocks): F2.5 `testimonials` — rejilla de citas con autor

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
dev 3 months ago
parent 594e5a5db4
commit b59332132f

@ -8,17 +8,24 @@ commit `ae7b4f8c3`.
## Lo siguiente
**F2.5 `testimonials`** (ficha en `PLAN-blocks.md` §F2.5). COMPOUND (`.Item`
repite): `Section` + `AutoGrid` + `Card` + `Avatar` + `Text` + `Group`. v1 =
grid; la cita única en spotlight (logo+quote+avatar) es la brecha del dossier
(scope-approval). Después: faq → stats-band → cta → newsletter → site-footer →
banner → team → contact → content-section.
**F2.4 `pricing` HECHO** — primer compound CON CONTEXTO del tier (el `Switch`
escribe el periodo, los `PlanPrice` lo leen). Precedente para cualquier block
cuyas partes COORDINEN: contexto reactivo vía `context.ts` (getter sobre un
`$bindable`), como `CardGroup`. El block NUNCA formatea moneda — la app compone
`FormatNumber` en los snippets `monthly`/`annual`.
**F2.6 `faq`** (ficha en `PLAN-blocks.md` §F2.6). Compone `Section` + `Container`
(medida estrecha) + `Heading` + `Accordion`. `.Item` = proxy fino de
`Accordion.Item` (LEER el README del Accordion antes — respetar su API real).
v1 = una columna; la brecha del dossier es la lista estática 2/3 columnas + la
cola «Still have questions?» (scope-approval). Después: stats-band → cta →
newsletter → site-footer → banner → team → contact → content-section.
**F2.4 `pricing` + F2.5 `testimonials` HECHOS.** pricing = primer compound CON
CONTEXTO (el `Switch` escribe el periodo, los `PlanPrice` lo leen; contexto
reactivo vía `context.ts`, getter sobre `$bindable`, como `CardGroup`; el block
NUNCA formatea moneda). testimonials = compound SIN contexto (`.Item` repite).
Lecciones nuevas para los que vienen:
- **Escalas donor activadas** = `green/indigo/orange/plum/teal`. Una escala NO
activada (`cyan/ruby/amber/jade`) en `color` cae en SILENCIO a `primary`. Usa
las activadas.
- **`Card` NO tiene variant `surface`** (solo `soft/solid/outline/ghost`); `soft
neutral` es casi invisible en claro → usa `outline` para tarjetas que deben
leerse.
**F2.1 site-header · F2.2 hero · F2.3 feature-grid · F2.3b feature-split HECHOS**,
**+ primitivo canon `Mockup`** (`$uix/eidos/components/mockup`: `chrome` browser/

@ -1177,3 +1177,20 @@ Blocks diferidos: `scheduler` (bloqueado por `chronos`) · `logo-cloud`
(features × planes) — otra disposición, candidato a hermano `pricing-table`.
Gates: `blocks:check` verde (5 blocks) · `svelte-check` sin errores propios.
**Siguiente**: F2.5 `testimonials`.
- 2026-07-24 — **F2.5 `testimonials` HECHO**. Compound (`.Item` repite, SIN
contexto — como feature-grid): `<Testimonials>` + `.Header` + `.Items` +
`.Item` + `.Quote` + `.Author`(slot `avatar` + `.AuthorName`/`.AuthorRole`).
`AutoGrid` de tarjetas de igual alto; `.Author` fijada al borde inferior
(`marginTop="auto"`) para alinear las caras aunque las citas midan distinto. La
cara es del app (`Avatar` con imagen o `Avatar.Fallback` de iniciales). `Card`
para el quote = `variant="outline"` (soft neutral es casi invisible en claro).
**Encontrado**: 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)`). No es bug de componente, es config del tema; usar
escalas activadas. **Hueco a decisión del usuario**: la cita única en spotlight
(grande, centrada, logo+avatar+autor) — la variante modal del dossier, otra
disposición → candidato a hermano `testimonial-spotlight`. Verificado en
claro/oscuro/RTL, caras alineadas, 5 colores distintos. Gates: `blocks:check`
verde (6 blocks) · `svelte-check` sin errores propios. **Siguiente**: F2.6 `faq`.

@ -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;

@ -24,7 +24,7 @@ export const BLOCK_CATALOG: BlockGroup[] = [
{ slug: 'feature-grid', label: 'Feature grid', shipped: true },
{ slug: 'feature-split', label: 'Feature split', shipped: true },
{ slug: 'pricing', label: 'Pricing', shipped: true },
{ slug: 'testimonials', label: 'Testimonials', shipped: false },
{ slug: 'testimonials', label: 'Testimonials', shipped: true },
{ slug: 'faq', label: 'FAQ', shipped: false },
{ slug: 'stats-band', label: 'Stats band', shipped: false },
{ slug: 'cta', label: 'CTA', shipped: false },

@ -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>&lt;Avatar&gt;</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>&lt;section&gt;</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…
Cancel
Save

Powered by TurnKey Linux.