From b59332132f551065f4a9c9a908e3ec0769d72bcc Mon Sep 17 00:00:00 2001 From: dev Date: Fri, 24 Jul 2026 01:12:55 +0200 Subject: [PATCH] =?UTF-8?q?feat(blocks):=20F2.5=20`testimonials`=20?= =?UTF-8?q?=E2=80=94=20rejilla=20de=20citas=20con=20autor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). `` + `.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 `` 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 --- docs/process/CONTINUE-blocks.md | 29 +++-- docs/process/PLAN-blocks.md | 17 +++ src/uix/blocks/testimonials/README.md | 64 +++++++++++ src/uix/blocks/testimonials/index.ts | 65 +++++++++++ .../testimonials-author-name.svelte | 11 ++ .../testimonials-author-role.svelte | 11 ++ .../testimonials/testimonials-author.svelte | 20 ++++ .../testimonials/testimonials-header.svelte | 15 +++ .../testimonials/testimonials-item.svelte | 24 ++++ .../testimonials/testimonials-items.svelte | 20 ++++ .../testimonials/testimonials-quote.svelte | 11 ++ .../blocks/testimonials/testimonials.svelte | 27 +++++ src/uix/blocks/testimonials/types.ts | 45 ++++++++ web/routes/blocks/_lib/catalog.ts | 2 +- web/routes/blocks/testimonials/+page.svelte | 107 ++++++++++++++++++ .../testimonials/TestimonialsSite.svelte | 96 ++++++++++++++++ .../testimonials/preview/+layout@.svelte | 30 +++++ .../blocks/testimonials/preview/+page.svelte | 9 ++ 18 files changed, 591 insertions(+), 12 deletions(-) create mode 100644 src/uix/blocks/testimonials/README.md create mode 100644 src/uix/blocks/testimonials/index.ts create mode 100644 src/uix/blocks/testimonials/testimonials-author-name.svelte create mode 100644 src/uix/blocks/testimonials/testimonials-author-role.svelte create mode 100644 src/uix/blocks/testimonials/testimonials-author.svelte create mode 100644 src/uix/blocks/testimonials/testimonials-header.svelte create mode 100644 src/uix/blocks/testimonials/testimonials-item.svelte create mode 100644 src/uix/blocks/testimonials/testimonials-items.svelte create mode 100644 src/uix/blocks/testimonials/testimonials-quote.svelte create mode 100644 src/uix/blocks/testimonials/testimonials.svelte create mode 100644 src/uix/blocks/testimonials/types.ts create mode 100644 web/routes/blocks/testimonials/+page.svelte create mode 100644 web/routes/blocks/testimonials/TestimonialsSite.svelte create mode 100644 web/routes/blocks/testimonials/preview/+layout@.svelte create mode 100644 web/routes/blocks/testimonials/preview/+page.svelte diff --git a/docs/process/CONTINUE-blocks.md b/docs/process/CONTINUE-blocks.md index 45a7c2fe5..45c208d70 100644 --- a/docs/process/CONTINUE-blocks.md +++ b/docs/process/CONTINUE-blocks.md @@ -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/ diff --git a/docs/process/PLAN-blocks.md b/docs/process/PLAN-blocks.md index 6f26d52c6..861864297 100644 --- a/docs/process/PLAN-blocks.md +++ b/docs/process/PLAN-blocks.md @@ -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): `` + `.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`. diff --git a/src/uix/blocks/testimonials/README.md b/src/uix/blocks/testimonials/README.md new file mode 100644 index 000000000..54637823b --- /dev/null +++ b/src/uix/blocks/testimonials/README.md @@ -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` + `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**: `` 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.) diff --git a/src/uix/blocks/testimonials/index.ts b/src/uix/blocks/testimonials/index.ts new file mode 100644 index 000000000..8cf19642c --- /dev/null +++ b/src/uix/blocks/testimonials/index.ts @@ -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'; +// +// +// +// Lo que dicen los equipos +// +// +// +// «Migramos en una tarde.» +// +// {#snippet avatar()}AR{/snippet} +// Ana Ruiz +// CTO, Northwind +// +// +// +// +// +// 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'; diff --git a/src/uix/blocks/testimonials/testimonials-author-name.svelte b/src/uix/blocks/testimonials/testimonials-author-name.svelte new file mode 100644 index 000000000..d0d703621 --- /dev/null +++ b/src/uix/blocks/testimonials/testimonials-author-name.svelte @@ -0,0 +1,11 @@ + + + + {@render children?.()} + diff --git a/src/uix/blocks/testimonials/testimonials-author-role.svelte b/src/uix/blocks/testimonials/testimonials-author-role.svelte new file mode 100644 index 000000000..01f32a567 --- /dev/null +++ b/src/uix/blocks/testimonials/testimonials-author-role.svelte @@ -0,0 +1,11 @@ + + + + {@render children?.()} + diff --git a/src/uix/blocks/testimonials/testimonials-author.svelte b/src/uix/blocks/testimonials/testimonials-author.svelte new file mode 100644 index 000000000..77108ecbc --- /dev/null +++ b/src/uix/blocks/testimonials/testimonials-author.svelte @@ -0,0 +1,20 @@ + + + + {@render avatar?.()} + + {@render children?.()} + + diff --git a/src/uix/blocks/testimonials/testimonials-header.svelte b/src/uix/blocks/testimonials/testimonials-header.svelte new file mode 100644 index 000000000..73e5e1036 --- /dev/null +++ b/src/uix/blocks/testimonials/testimonials-header.svelte @@ -0,0 +1,15 @@ + + + + + {@render children?.()} + + diff --git a/src/uix/blocks/testimonials/testimonials-item.svelte b/src/uix/blocks/testimonials/testimonials-item.svelte new file mode 100644 index 000000000..5653ca83c --- /dev/null +++ b/src/uix/blocks/testimonials/testimonials-item.svelte @@ -0,0 +1,24 @@ + + + + + {@render children?.()} + + diff --git a/src/uix/blocks/testimonials/testimonials-items.svelte b/src/uix/blocks/testimonials/testimonials-items.svelte new file mode 100644 index 000000000..d06523d3e --- /dev/null +++ b/src/uix/blocks/testimonials/testimonials-items.svelte @@ -0,0 +1,20 @@ + + + + {@render children?.()} + diff --git a/src/uix/blocks/testimonials/testimonials-quote.svelte b/src/uix/blocks/testimonials/testimonials-quote.svelte new file mode 100644 index 000000000..49f8123b8 --- /dev/null +++ b/src/uix/blocks/testimonials/testimonials-quote.svelte @@ -0,0 +1,11 @@ + + + + {@render children?.()} + diff --git a/src/uix/blocks/testimonials/testimonials.svelte b/src/uix/blocks/testimonials/testimonials.svelte new file mode 100644 index 000000000..d91ad2142 --- /dev/null +++ b/src/uix/blocks/testimonials/testimonials.svelte @@ -0,0 +1,27 @@ + + +
+
+ + + {@render children?.()} + + +
+
diff --git a/src/uix/blocks/testimonials/types.ts b/src/uix/blocks/testimonials/types.ts new file mode 100644 index 000000000..f56a35db5 --- /dev/null +++ b/src/uix/blocks/testimonials/types.ts @@ -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, '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 & { + /** The avatar — an `` (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; diff --git a/web/routes/blocks/_lib/catalog.ts b/web/routes/blocks/_lib/catalog.ts index 70aa11026..39372f21a 100644 --- a/web/routes/blocks/_lib/catalog.ts +++ b/web/routes/blocks/_lib/catalog.ts @@ -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 }, diff --git a/web/routes/blocks/testimonials/+page.svelte b/web/routes/blocks/testimonials/+page.svelte new file mode 100644 index 000000000..3a55c5107 --- /dev/null +++ b/web/routes/blocks/testimonials/+page.svelte @@ -0,0 +1,107 @@ + + + + {#snippet preview()} + + {/snippet} + + {#snippet lede()} + Prueba social: una rejilla de citas, cada una con su autor (avatar · nombre · cargo). Compound + —la .Item se repite— como feature-grid; las partes no coordinan, así que + sin contexto. + {/snippet} + + {#snippet composition()} + + El block coloca las tarjetas; las palabras y las caras son de la app. + + + AutoGrid de tarjetas de igual alto; cada .Item es una + Card que rellena la altura de la fila. + + + Text (lg) con la cita. La app pone las comillas y las palabras. + + + Group con el slot avatar + .AuthorName/.AuthorRole, + fijado al borde inferior de la tarjeta (margin-block-start: auto) para alinear + las caras aunque las citas midan distinto. + + + La cara es del app: un <Avatar> con imagen o un + Avatar.Fallback de iniciales. El block no aloja caras. + + + + {/snippet} + + {#snippet api()} + + + container (def. lg) · size (def. lg). + + + minChildWidth (def. 20rem) o columns · gap + (def. 6). + + + avatar (snippet) + children (.AuthorName/.AuthorRole). + + + {/snippet} + + {#snippet a11y()} + + + Un <section> con un h2. Las citas son Text; el + avatar lleva su nombre accesible vía el Avatar del canon. + + + {/snippet} + + {#snippet gaps()} + + + Scope-approval pendiente — 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 testimonial-spotlight. + + + Diferido — el Carousel del canon existe; entra cuando una demo + lo pida. + + + {/snippet} + + {#snippet notes()} + + + Como feature-grid/feature-split: la .Item se repite + pero las partes no se hablan, así que no hay contexto (a diferencia de pricing). + + + {/snippet} + diff --git a/web/routes/blocks/testimonials/TestimonialsSite.svelte b/web/routes/blocks/testimonials/TestimonialsSite.svelte new file mode 100644 index 000000000..0a51dd220 --- /dev/null +++ b/web/routes/blocks/testimonials/TestimonialsSite.svelte @@ -0,0 +1,96 @@ + + + + + Equipos de producto que ya no vuelan a ciegas + + Lo que cambia cuando los eventos entran crudos y salen en cohortes. + + + + + {#each quotes as q (q.name)} + + «{q.quote}» + + {#snippet avatar()} + + {q.initials} + + {/snippet} + {q.name} + {q.role} + + + {/each} + + + + +
+ + + + Con la confianza de equipos de producto en + + + {#each ['Northwind', 'Umbra', 'Kessel', 'Volta', 'Marisol'] as brand (brand)} + {brand} + {/each} + + + +
diff --git a/web/routes/blocks/testimonials/preview/+layout@.svelte b/web/routes/blocks/testimonials/preview/+layout@.svelte new file mode 100644 index 000000000..8da588099 --- /dev/null +++ b/web/routes/blocks/testimonials/preview/+layout@.svelte @@ -0,0 +1,30 @@ + + + + + + + +
+ {@render children?.()} +
+
diff --git a/web/routes/blocks/testimonials/preview/+page.svelte b/web/routes/blocks/testimonials/preview/+page.svelte new file mode 100644 index 000000000..4a66ac335 --- /dev/null +++ b/web/routes/blocks/testimonials/preview/+page.svelte @@ -0,0 +1,9 @@ + + +