blocks(testimonials): una cita sola no va en una tarjeta

La brecha que el dossier le apuntaba a este block: la cita única es la variante
modal de todas las referencias y el grid la minoría, dos de ocho en Tailwind Plus.

Entra como disposición, no como hermano. La anatomía no cambia —cita, autor,
avatar—, sólo cómo se colocan, y el precedente es el fondo del hero, que también
redefine lo que hacen sus partes. El hermano se reabre sólo si algún día pide
rotación o carrusel, que es otro componente.

Viaja por contexto de configuración, el patrón del align de feature-grid y team,
no la coordinación de pricing. Y viaja porque el spotlight no es una piel sobre
las mismas cajas: la rejilla deja de serlo y de estampar el ritmo estructural —que
es para una FILA, no para una cita—, el ítem pierde la tarjeta, la cita sube de
tamaño y se centra, y el autor pasa debajo, donde ya no hay una hilera de caras
que alinear. Pedirle a la app que repita esa decisión en cinco partes es el
agujero que A-84 cerró en feature-grid.

Enmarcar una cita sola la hace parecer un ítem de una lista que no está ahí, así
que ahí no hay tarjeta y el tipo lo dice, en vez de aceptar una superficie que
luego descarta. La cita se centra con un párrafo de verdad: sobre una caja en
línea el centrado no aplica, y sería mentira.

Slot nuevo para la marca de quien firma, en las dos disposiciones: es lo que hace
que una cita se lea como evidencia y no como una opinión, y es la pieza que ponen
todas las referencias.

Medido: en grid, cinco tarjetas, veinte píxeles, medida de mil veinticuatro y
ritmo escalonado; en spotlight, cero tarjetas, una cita de veintiocho centrada en
párrafo, medida de setecientos sesenta y ocho, autor centrado y sin escalonado; a
375 la cita cae a veinticuatro. El grid queda idéntico a antes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
alpha-0.1-background
dev 2 months ago
parent b92dc3bdb2
commit f3bfcf2dd9

@ -255,8 +255,9 @@ del ancho; él lo ve centrado y tiene razón), y **A-65/A-75 son el mismo patró
**La fuente es `PLAN-blocks.md` §F2b** — no dupliques aquí sus tablas. Cuatro **La fuente es `PLAN-blocks.md` §F2b** — no dupliques aquí sus tablas. Cuatro
tramos, en orden: **A** variantes de suelo (~~V4 hero `form`~~ ✅ · ~~V8 tramos, en orden: **A** variantes de suelo (~~V4 hero `form`~~ ✅ · ~~V8
single-price~~ ✅ · ~~V5 cta split~~ ✅ **las tres HECHAS 2026-08-17** → **entra single-price~~ ✅ · ~~V5 cta split~~ ✅ · ~~V2 testimonials spotlight~~ ✅ **las
por V2 spotlight** → V3 faq-lista → V1 `Pricing.Compare` en tanda propia) · **B** la matriz de cuatro HECHAS 2026-08-17** → **entra por V3 faq-lista** → V1 `Pricing.Compare` en
tanda propia) · **B** la matriz de
equivalencia variante-a-variante en el README de cada block, con recetas equivalencia variante-a-variante en el README de cada block, con recetas
demostradas en demo (el entregable central: responde a la brecha de demostradas en demo (el entregable central: responde a la brecha de
cardinalidad sin competir en dumps) · **C** tres blocks nuevos — `logo-cloud` cardinalidad sin competir en dumps) · **C** tres blocks nuevos — `logo-cloud`
@ -299,6 +300,13 @@ solo: card de 315px con 677px de vacío al lado). Nadie había mirado ese block
con menos de tres planes. Antes de dar por buena una disposición vieja de con menos de tres planes. Antes de dar por buena una disposición vieja de
cualquier README, mídela. cualquier README, mídela.
⚠️ **Y dos cosas del árbol COMPARTIDO, medidas el 2026-08-17**: otra sesión está
a medias con un componente **`background`** (toca `morfo/`, `langs/` y
`eidos/generated`), así que `svelte-check` da 73 con cosas que no son del tier —
la base real sin nadie es **60**. Y **no hagas `git stash --include-untracked`
aquí**: arrastra su trabajo sin commitear. Si necesitas una base, mide por
fichero.
### e) Cabos y fases ### e) Cabos y fases
1. **Tres cabos concretos**: 1. **Tres cabos concretos**:

@ -2187,3 +2187,26 @@ type="button">` es una pista de MIME falsa) — el morfo lo declara
dueños, una propiedad, un nodo) y alcanza a TODA `Card` deshabilitada del dueños, una propiedad, un nodo) y alcanza a TODA `Card` deshabilitada del
ecosistema. Reparación natural: atenuar por `filter: opacity(...)`, que la ecosistema. Reparación natural: atenuar por `filter: opacity(...)`, que la
animación no posee, sin tokens nuevos. Espera firma. animación no posee, sin tokens nuevos. Espera firma.
- 2026-08-17 — **F2b · V2 `testimonials` spotlight HECHA**. La brecha del dossier
para este block: la cita única es la variante MODAL de las referencias y el grid
la minoría (2 de 8 en TW). Entra como **`layout="grid" | "spotlight"`**, no como
hermano — la anatomía no cambia (cita · autor · avatar), sólo la disposición, y
el precedente es el `background` del `hero`, que también redefine lo que hacen
sus partes; el hermano se reabre SÓLO si un día pide rotación o carrusel, que es
otro componente. **Viaja por contexto de CONFIGURACIÓN** (el patrón del `align`
de `feature-grid`/`team`, no la coordinación de `pricing`) porque `spotlight` no
es una piel: `Items` deja de ser rejilla **y de estampar `data-stagger`** —el
ritmo estructural es para una FILA—, `Item` **pierde la `Card`** —enmarcar una
cita sola la hace parecer un ítem de una lista que no existe—, `Quote` sube a
`xl` y se centra **con `as="p"`** (F17: sin él el centrado sería mentira) y
`Author` pasa debajo. La medida de la sección se estrecha a `md`. Slot nuevo
**`logo`** en `.Item`, en las dos disposiciones: la marca de quien firma es lo
que hace que la cita se lea como evidencia. Medido: grid → 5 tarjetas, 20px,
container 1024, stagger; spotlight → **0 tarjetas**, una cita de 28px centrada en
`<p>`, container 768, autor centrado, sin stagger; a 375 la cita cae a 24px. El
grid queda idéntico.
⚠️ **Aviso de árbol compartido**: `svelte-check` marcó 73 durante esta tanda y
ninguno es del tier — otra sesión está a medias con un componente `background`
(morfo, langs y `eidos/generated` tocados). Medido con todo en stash: la base
real sin nadie es 60. **No volver a hacer `git stash --include-untracked` en este
árbol**: arrastra el trabajo sin commitear de la otra sesión.

@ -8,16 +8,16 @@ section, usually below features or pricing.
## Composition map ## Composition map
| Part | Composes | Notes | | Part | Composes | Notes |
| ------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | | ------------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| root | bare `<section>` + `Section` + `Container` + `Stack` | places the header and the grid | | root | bare `<section>` + `Section` + `Container` + `Stack` | places the header and the quotes; owns `layout` and shares it with the parts through context. Under `spotlight` the measure narrows to `md` — one quote at the section's full width is a line nobody finishes |
| `.Header` | `Box` + `Stack` | the app's section `Heading` + `Text` | | `.Header` | `Box` + `Stack` | the app's section `Heading` + `Text` |
| `.Items` | `AutoGrid` + `data-stagger` | fluid row of equal-height quote cards; the stagger rhythm is structural | | `.Items` | `AutoGrid` + `data-stagger` (grid) · a plain `Box` (spotlight) | fluid row of equal-height quote cards. Under `spotlight` there is neither grid nor stagger: one quote has nothing to lay out and nothing to deal one after another |
| `.Item` | `Motion` (`trigger="viewport"`) wrapping a `Card` (outline) | IS the reveal, so it must stay the grid's direct child to get its structural index; fills the row height | | `.Item` | `Motion` wrapping a `Card` (grid) · `Motion` + centred `Stack`, NO card (spotlight) | IS the reveal, so under `grid` it must stay the grid's direct child to get its structural index. Takes the `logo` slot in both |
| `.Quote` | `Text` (lg) | the quotation | | `.Quote` | `Text` (lg · **xl centred `as="p"` under spotlight**) | the quotation. `as="p"` because `text-align` is inert on an inline box (F17) — a `span` would make the centring a lie |
| `.Author` | `Group` + `Stack` | the `avatar` slot + name/role, pinned to the card's bottom | | `.Author` | `Group` + `Stack` | the `avatar` slot + name/role, pinned to the card's bottom under `grid`; centred UNDER the quote in `spotlight`, where there is no row of cards to line faces up along |
| `.AuthorName` | `Text` (medium, sm) | | | `.AuthorName` | `Text` (medium, sm) | |
| `.AuthorRole` | `Text` (muted, sm) | role / company | | `.AuthorRole` | `Text` (muted, sm) | role / company |
**Landmark + headings**: a bare `<section>` with NO accessible name of its own, **Landmark + headings**: a bare `<section>` with NO accessible name of its own,
so it is not an exposed landmark — the app names it by passing `aria-label` / so it is not an exposed landmark — the app names it by passing `aria-label` /
@ -58,13 +58,40 @@ Untitled UI «Testimonials 26» · Flowbite):
avatar fallbacks. Mini-page in `TestimonialsSite.svelte`, shared by both avatar fallbacks. Mini-page in `TestimonialsSite.svelte`, shared by both
surfaces. surfaces.
### The spotlight arrangement — 2026-08-17, plan F2b · V2
The dossier's gap for this block, and the arrangement every reference catalog
leads with: the grid was the minority (2 of 8 in Tailwind Plus). What was decided:
- **A `layout`, not a sibling block.** The anatomy is the same — quote, author,
avatar — and only the arrangement changes; the precedent is `hero`'s
`background`, which also redefines what its parts do. A sibling gets reopened
only if this ever needs rotation or a carousel, which IS a different component.
- **It travels by CONTEXT** (configuration, like `feature-grid`'s `align`),
because `spotlight` is not a skin over the same boxes: `Items` stops being a
grid, `Item` drops the card, `Quote` steps up a size and centres, `Author`
moves under the quote. Asking the app to repeat that on five parts is the hole
A-84 closed on `feature-grid`.
- **No card in the spotlight.** A single quote framed in a box reads as one item
of a list that is not there. The `Card` props are ignored, and the type says so
rather than accepting a surface it discards (A-94).
- **No `data-stagger` either**: the structural rhythm is for a ROW.
- **The `logo` slot** on `.Item` (both arrangements): the customer's mark above
the quote is what makes it read as evidence rather than an opinion — the piece
every reference puts there.
**Measured** (1280, light and dark): grid → 5 cards, 20px quote, start-aligned,
container 1024, stagger present. Spotlight → **0 cards**, one quote at **28px**
centred in a `<p>`, container **768**, author centred, no stagger. At 375 the
quote lands at 24px. The grid arrangement is byte-identical to before.
## Gaps ## Gaps
| Gap | Disposition | | 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 | | **Single quote in spotlight** (one large quote, logo + avatar + author, centered) | **SHIPPED 2026-08-17** (F2b · V2) — as `layout="spotlight"`, plus the `logo` slot on `.Item`. See «The spotlight arrangement» below |
| **Logo strip** (customer logos instead of quotes) | **out** — that is the `logo-cloud` category (deferred at F2 scope), not this block | | **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 | | **Carousel variant** (quotes in a scroller) | **deferred** — the canon `Carousel` exists; it enters when a demo asks |
## Found while composing ## Found while composing

@ -0,0 +1,32 @@
import { getContext, setContext } from 'svelte';
import type { TestimonialsLayout } from './types';
/**
* What `Testimonials` shares with its parts: the section's ARRANGEMENT.
*
* Configuration, not state — the same shape `feature-grid` and `team` use for
* their `align`. It travels because `spotlight` is not a different skin over the
* same boxes: the row of cards becomes ONE quote, so `Items` stops being a grid,
* `Item` drops the card that framed it, the quote steps up in size and the author
* moves under it. Asking the app to repeat that decision on five parts is exactly
* the hole A-84 closed on `feature-grid`.
*/
export interface TestimonialsContext {
/** `grid` (the default) or `spotlight`. A getter, so reads track the source. */
readonly layout: TestimonialsLayout;
}
const KEY = Symbol('uix.testimonials');
export function setTestimonialsContext(ctx: TestimonialsContext): void {
setContext(KEY, ctx);
}
export function getTestimonialsContext(): TestimonialsContext | undefined {
return getContext<TestimonialsContext | undefined>(KEY);
}
/** The arrangement a part should assume when it is used outside the block. */
export function testimonialsLayout(): TestimonialsLayout {
return getTestimonialsContext()?.layout ?? 'grid';
}

@ -7,14 +7,26 @@
*/ */
import { Group } from '$uix/eidos/components/group'; import { Group } from '$uix/eidos/components/group';
import { Stack } from '$uix/eidos/components/stack'; import { Stack } from '$uix/eidos/components/stack';
import { testimonialsLayout } from './context';
import type { TestimonialsAuthorProps } from './types'; import type { TestimonialsAuthorProps } from './types';
let { avatar, children, ...rest }: TestimonialsAuthorProps = $props(); let { avatar, children, ...rest }: TestimonialsAuthorProps = $props();
// Under `spotlight` the author sits UNDER the quote, centred: there is no row of
// cards to line faces up along, so the `margin-block-start: auto` that pins it
// to a card's bottom edge has nothing to push against.
const spotlight = $derived(testimonialsLayout() === 'spotlight');
</script> </script>
<Group gap={3} align="center" justify="start" marginTop="auto" {...rest}> <Group
gap={3}
align="center"
justify={spotlight ? 'center' : 'start'}
marginTop={spotlight ? undefined : 'auto'}
{...rest}
>
{@render avatar?.()} {@render avatar?.()}
<Stack gap={0} align="start"> <Stack gap={0} align={spotlight ? 'center' : 'start'}>
{@render children?.()} {@render children?.()}
</Stack> </Stack>
</Group> </Group>

@ -7,23 +7,44 @@
import { Card } from '$uix/eidos/components/card'; import { Card } from '$uix/eidos/components/card';
import { Stack } from '$uix/eidos/components/stack'; import { Stack } from '$uix/eidos/components/stack';
import { Motion } from '$uix/eidos/components/motion'; import { Motion } from '$uix/eidos/components/motion';
import { testimonialsLayout } from './context';
import type { TestimonialsItemProps } from './types'; import type { TestimonialsItemProps } from './types';
let { let {
variant = 'outline', variant = 'outline',
color = 'neutral', color = 'neutral',
size = 'lg', size = 'lg',
logo,
children, children,
...rest ...rest
}: TestimonialsItemProps = $props(); }: TestimonialsItemProps = $props();
const layout = $derived(testimonialsLayout());
</script> </script>
<!-- The Item IS the viewport Motion: direct child of the `[data-stagger]` grid, so the {#if layout === 'spotlight'}
quotes deal themselves in one after another. --> <!-- NO card. A single quote framed in a box reads as one more item of a list
<Motion trigger="viewport"> that is not there; what the arrangement is for is the quote standing on the
<Card {variant} {color} {size} height="100%" {...rest}> page. The `Card` props are ignored here on purpose, and the type says so. -->
<Stack gap={6} height="100%" align="start"> <Motion trigger="viewport" motion="scale-fade">
<Stack gap={6} align="center" width="100%">
{#if logo}
{@render logo()}
{/if}
{@render children?.()} {@render children?.()}
</Stack> </Stack>
</Card> </Motion>
</Motion> {:else}
<!-- The Item IS the viewport Motion: direct child of the `[data-stagger]` grid, so the
quotes deal themselves in one after another. -->
<Motion trigger="viewport">
<Card {variant} {color} {size} height="100%" {...rest}>
<Stack gap={6} height="100%" align="start">
{#if logo}
{@render logo()}
{/if}
{@render children?.()}
</Stack>
</Card>
</Motion>
{/if}

@ -2,6 +2,8 @@
/** Testimonials.Items — the responsive grid the quote cards lay out in. Fluid /** Testimonials.Items — the responsive grid the quote cards lay out in. Fluid
* by default; the cards stretch to equal height. */ * by default; the cards stretch to equal height. */
import { AutoGrid } from '$uix/eidos/components/auto-grid'; import { AutoGrid } from '$uix/eidos/components/auto-grid';
import { Box } from '$uix/eidos/components/box';
import { testimonialsLayout } from './context';
import type { TestimonialsItemsProps } from './types'; import type { TestimonialsItemsProps } from './types';
let { let {
@ -11,16 +13,26 @@
children, children,
...rest ...rest
}: TestimonialsItemsProps = $props(); }: TestimonialsItemsProps = $props();
const layout = $derived(testimonialsLayout());
</script> </script>
<AutoGrid {#if layout === 'spotlight'}
{...rest} <!-- No grid and no `data-stagger`: a single quote has nothing to lay out and
minChildWidth={columns ? undefined : minChildWidth} nothing to deal one after another. The structural rhythm is for a ROW. -->
{columns} <Box width="100%">
{gap} {@render children?.()}
align="stretch" </Box>
width="100%" {:else}
data-stagger <AutoGrid
> {...rest}
{@render children?.()} minChildWidth={columns ? undefined : minChildWidth}
</AutoGrid> {columns}
{gap}
align="stretch"
width="100%"
data-stagger
>
{@render children?.()}
</AutoGrid>
{/if}

@ -1,11 +1,24 @@
<script lang="ts"> <script lang="ts">
/** Testimonials.Quote — the quotation. The app supplies the words. */ /** Testimonials.Quote — the quotation. The app supplies the words. */
import { Text } from '$uix/eidos/components/text'; import { Text } from '$uix/eidos/components/text';
import { testimonialsLayout } from './context';
import type { TestimonialsQuoteProps } from './types'; import type { TestimonialsQuoteProps } from './types';
let { children, ...rest }: TestimonialsQuoteProps = $props(); let { children, ...rest }: TestimonialsQuoteProps = $props();
const layout = $derived(testimonialsLayout());
// The spotlight quote carries the section on its own, so it steps up one size
// and centres. `as="p"` because `text-align` is inert on an inline box (F17) —
// a `span` would keep the lines flush start and the centring would be a lie.
const spotlight = $derived(layout === 'spotlight');
</script> </script>
<Text size="lg" wrap="pretty" {...rest}> <Text
as={spotlight ? 'p' : undefined}
size={spotlight ? 'xl' : 'lg'}
align={spotlight ? 'center' : undefined}
wrap="pretty"
{...rest}
>
{@render children?.()} {@render children?.()}
</Text> </Text>

@ -11,14 +11,33 @@
import { Section } from '$uix/eidos/components/section'; import { Section } from '$uix/eidos/components/section';
import { Container } from '$uix/eidos/components/container'; import { Container } from '$uix/eidos/components/container';
import { Stack } from '$uix/eidos/components/stack'; import { Stack } from '$uix/eidos/components/stack';
import { setTestimonialsContext } from './context';
import type { TestimonialsProps } from './types'; import type { TestimonialsProps } from './types';
let { containerSize = 'lg', sectionSize = 'lg', children, ...rest }: TestimonialsProps = $props(); let {
layout = 'grid',
containerSize = 'lg',
sectionSize = 'lg',
children,
...rest
}: TestimonialsProps = $props();
// Configuration, not state: set once here, read by the parts. `spotlight` is
// not a skin — it changes what four parts do, and making the app repeat that
// decision on each one is the hole A-84 closed on `feature-grid`.
setTestimonialsContext({
get layout() {
return layout;
}
});
</script> </script>
<section {...rest}> <section {...rest}>
<Section size={sectionSize}> <Section size={sectionSize}>
<Container size={containerSize}> <!-- The spotlight takes a NARROWER measure: one quote set at the section's
full width is a line nobody finishes reading. The cap rides the canon's
own container scale instead of a hand-written number. -->
<Container size={layout === 'spotlight' ? 'md' : containerSize}>
<Stack gap={10} align="center"> <Stack gap={10} align="center">
{@render children?.()} {@render children?.()}
</Stack> </Stack>

@ -8,7 +8,29 @@ import type { CardProps } from '$uix/eidos/components/card';
import type { TextProps } from '$uix/eidos/components/text'; import type { TextProps } from '$uix/eidos/components/text';
import type { GroupProps } from '$uix/eidos/components/group'; import type { GroupProps } from '$uix/eidos/components/group';
/**
* How the section arranges its quotes.
*
* `grid` is a row of equal-height cards — the comparison view. `spotlight` is the
* arrangement every reference catalog leads with and the dossier named as this
* block's gap: ONE quote at full attention, its logo above and its author below.
* The dossier counted the grid as the minority (2 of 8 in Tailwind Plus).
*
* It is a `layout` and not a sibling block because the anatomy is the same —
* quote, author, avatar — and only the arrangement changes; the precedent is
* `hero`'s `background`, which also redefines what its parts do. A sibling is
* reopened only if this ever needs rotation or a carousel, which IS a different
* component.
*/
export type TestimonialsLayout = 'grid' | 'spotlight';
export type TestimonialsProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & { export type TestimonialsProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
/**
* The arrangement. Travels to the parts through context, so it is set ONCE
* here — `Items` stops being a grid, `Item` drops its card, the quote steps up
* and the author centres. @default 'grid'
*/
layout?: TestimonialsLayout;
/** Content measure. Passed straight to `Container`. @default 'lg' */ /** Content measure. Passed straight to `Container`. @default 'lg' */
containerSize?: ContainerSize; containerSize?: ContainerSize;
/** Block-axis padding. Passed straight to `Section`. @default 'lg' */ /** Block-axis padding. Passed straight to `Section`. @default 'lg' */
@ -36,11 +58,23 @@ export type TestimonialsItemsProps = Pick<
>; >;
/** /**
* Wraps a `Card` (the quote card). `variant` defaults to **`outline`** (A-27): * Wraps a `Card` (the quote card) under `grid`. Under `spotlight` there is NO
* card — the quote stands on the page, and the `Card` props are ignored: framing
* a single quote in a box is the thing the arrangement exists to avoid.
*
* `variant` defaults to **`outline`** (A-27):
* `soft` neutral is all but invisible on a light canvas, which is why the block * `soft` neutral is all but invisible on a light canvas, which is why the block
* picked the outline card — the typedoc said `soft` and the code never did. * picked the outline card — the typedoc said `soft` and the code never did.
*/ */
export type TestimonialsItemProps = CardProps; export type TestimonialsItemProps = CardProps & {
/**
* The customer's logo, above the quote — the piece the spotlight arrangement
* converges on across every reference (it is what makes a quote read as
* evidence and not as an opinion). The app composes it: an `Image`, an inline
* `<svg>`, a `Text` wordmark. Optional in both arrangements.
*/
logo?: Snippet;
};
/** Wraps a `Text` — the quotation. `size` defaults to `lg`. */ /** Wraps a `Text` — the quotation. `size` defaults to `lg`. */
export type TestimonialsQuoteProps = TextProps; export type TestimonialsQuoteProps = TextProps;

@ -6,10 +6,15 @@
import { Text } from '$uix/eidos/components/text'; import { Text } from '$uix/eidos/components/text';
import { Code } from '$uix/eidos/components/code'; import { Code } from '$uix/eidos/components/code';
import BlockDemo from '../_lib/BlockDemo.svelte'; import BlockDemo from '../_lib/BlockDemo.svelte';
import { Group } from '$uix/eidos/components/group';
import { Wrap } from '$uix/eidos/components/wrap';
import { ToggleGroup } from '$uix/eidos/components/toggle-group';
import TestimonialsSite from './TestimonialsSite.svelte'; import TestimonialsSite from './TestimonialsSite.svelte';
import DocRow from '../_lib/DocRow.svelte'; import DocRow from '../_lib/DocRow.svelte';
const previewSrc = '/blocks/testimonials/preview'; let layout = $state<'grid' | 'spotlight'>('grid');
const previewSrc = $derived(`/blocks/testimonials/preview?layout=${layout}`);
</script> </script>
<BlockDemo <BlockDemo
@ -25,7 +30,26 @@
]} ]}
> >
{#snippet preview()} {#snippet preview()}
<TestimonialsSite /> <TestimonialsSite {layout} />
{/snippet}
{#snippet controls()}
<Wrap gap={5}>
<Group gap={2} align="center" justify="start">
<Text size="sm" color="muted">layout</Text>
<ToggleGroup
selectionMode="single"
size="sm"
attached
value={[layout]}
onValueChange={(v) => (layout = (v[0] ?? layout) as typeof layout)}
aria-label="disposición"
>
<ToggleGroup.Item value="grid">grid</ToggleGroup.Item>
<ToggleGroup.Item value="spotlight">spotlight</ToggleGroup.Item>
</ToggleGroup>
</Group>
</Wrap>
{/snippet} {/snippet}
{#snippet lede()} {#snippet lede()}
@ -64,9 +88,18 @@
{#snippet api()} {#snippet api()}
<Stack gap={3}> <Stack gap={3}>
<DocRow term="Testimonials"> <DocRow term="Testimonials">
<Code>layout</Code> (<Code>'grid' | 'spotlight'</Code>, def. <Code>grid</Code>) ·
<Code>containerSize</Code> (def. <Code>lg</Code>) · <Code>sectionSize</Code> (def. <Code <Code>containerSize</Code> (def. <Code>lg</Code>) · <Code>sectionSize</Code> (def. <Code
>lg</Code >lg</Code
>). >). <Code>spotlight</Code> viaja por CONTEXTO a las partes, porque no es una piel sobre las mismas
cajas: <Code>.Items</Code> deja de ser rejilla, <Code>.Item</Code> pierde su tarjeta, la cita
sube de tamaño y se centra, y el autor pasa debajo. Además la medida se estrecha a
<Code>md</Code>: una cita al ancho de la sección es una línea que nadie termina.
</DocRow>
<DocRow term=".Item logo">
Snippet: la marca de quien firma la cita, encima. Es lo que hace que se lea como evidencia y
no como una opinión — la pieza que ponen todas las referencias. Sirve en las dos
disposiciones.
</DocRow> </DocRow>
<DocRow term=".Items"> <DocRow term=".Items">
<Code>minChildWidth</Code> (def. <Code>20rem</Code>) o <Code>columns</Code> · <Code <Code>minChildWidth</Code> (def. <Code>20rem</Code>) o <Code>columns</Code> · <Code
@ -93,9 +126,11 @@
{#snippet gaps()} {#snippet gaps()}
<Stack gap={3}> <Stack gap={3}>
<DocRow term="Cita única en spotlight"> <DocRow term="Cita única en spotlight">
<strong>Scope-approval pendiente</strong> — la brecha del dossier en testimonials: una cita <strong>Hecho (V2, 2026-08-17)</strong> — <Code>layout="spotlight"</Code> más el slot
grande centrada (logo + avatar + autor), la variante modal en TODAS las refs; el grid es <Code>logo</Code>. Es un <Code>layout</Code> y no un hermano porque la anatomía no cambia —cita,
minoría. Es otra disposición → candidato a hermano <Code>testimonial-spotlight</Code>. autor, avatar—, sólo la disposición; el precedente es el <Code>background</Code> del hero, que
también redefine lo que hacen sus partes. El hermano se reabre sólo si algún día pide rotación
o carrusel, que ES otro componente. Pruébalo con el control de arriba.
</DocRow> </DocRow>
<DocRow term="Carrusel"> <DocRow term="Carrusel">
<strong>Diferido</strong> — el <Code>Carousel</Code> del canon existe; entra cuando una demo lo <strong>Diferido</strong> — el <Code>Carousel</Code> del canon existe; entra cuando una demo lo
@ -106,9 +141,12 @@
{#snippet notes()} {#snippet notes()}
<Stack gap={3}> <Stack gap={3}>
<DocRow term="Compound sin contexto"> <DocRow term="Compound con contexto de CONFIGURACIÓN">
Como <Code>feature-grid</Code>/<Code>feature-split</Code>: la <Code>.Item</Code> se repite pero La <Code>.Item</Code> se repite, como en <Code>feature-grid</Code>/<Code>feature-split</Code
las partes no se hablan, así que no hay contexto (a diferencia de <Code>pricing</Code>). >, y las partes no comparten estado. Lo que sí comparten desde V2 es la disposición: un
contexto de configuración, igual que el <Code>align</Code> de <Code>feature-grid</Code> y de
<Code>team</Code> — no el contexto de coordinación de <Code>pricing</Code>, donde una parte
escribe lo que otra lee.
</DocRow> </DocRow>
</Stack> </Stack>
{/snippet} {/snippet}

@ -12,6 +12,8 @@
import { Heading } from '$uix/eidos/components/heading'; import { Heading } from '$uix/eidos/components/heading';
import { Text } from '$uix/eidos/components/text'; import { Text } from '$uix/eidos/components/text';
let { layout = 'grid' as 'grid' | 'spotlight' }: { layout?: 'grid' | 'spotlight' } = $props();
type Quote = { quote: string; name: string; role: string; initials: string; color: string }; type Quote = { quote: string; name: string; role: string; initials: string; color: string };
const quotes: Quote[] = [ const quotes: Quote[] = [
@ -51,9 +53,12 @@
color: 'plum' color: 'plum'
} }
]; ];
// El spotlight lleva UNA cita — la que el equipo quiera destacar.
const shown = $derived(layout === 'spotlight' ? [quotes[1]] : quotes);
</script> </script>
<Testimonials> <Testimonials {layout}>
<Testimonials.Header> <Testimonials.Header>
<Heading level={2} align="center">Equipos de producto que ya no vuelan a ciegas</Heading> <Heading level={2} align="center">Equipos de producto que ya no vuelan a ciegas</Heading>
<Text size="lg" color="muted" align="center"> <Text size="lg" color="muted" align="center">
@ -62,8 +67,15 @@
</Testimonials.Header> </Testimonials.Header>
<Testimonials.Items> <Testimonials.Items>
{#each quotes as q (q.name)} <!-- El spotlight es UNA cita: la app decide cuál, el block sólo la coloca. -->
{#each shown as q (q.name)}
<Testimonials.Item> <Testimonials.Item>
{#snippet logo()}
<!-- La marca de quien firma la cita: lo que hace que se lea como
evidencia y no como una opinión. Aquí es una palabra compuesta con
el canon; una app real pondría su `Image`. -->
<Text size="lg" weight="semibold" color="muted">{q.role.split(' en ')[1] ?? ''}</Text>
{/snippet}
<Testimonials.Quote>«{q.quote}»</Testimonials.Quote> <Testimonials.Quote>«{q.quote}»</Testimonials.Quote>
<Testimonials.Author> <Testimonials.Author>
{#snippet avatar()} {#snippet avatar()}

@ -3,7 +3,10 @@
* Standalone page for the testimonials mini-site — same component the demo * Standalone page for the testimonials mini-site — same component the demo
* renders inline, served as its OWN document for the device-width frame. * renders inline, served as its OWN document for the device-width frame.
*/ */
import { page } from '$app/state';
import TestimonialsSite from '../TestimonialsSite.svelte'; import TestimonialsSite from '../TestimonialsSite.svelte';
const layout = $derived((page.url.searchParams.get('layout') ?? 'grid') as 'grid' | 'spotlight');
</script> </script>
<TestimonialsSite /> <TestimonialsSite {layout} />

Loading…
Cancel
Save

Powered by TurnKey Linux.