From fd6c55a13b163fb348af7760f5501bf15ae25cad Mon Sep 17 00:00:00 2001 From: dev Date: Mon, 17 Aug 2026 15:07:17 +0200 Subject: [PATCH] =?UTF-8?q?blocks(pricing):=20un=20plan=20solo=20no=20se?= =?UTF-8?q?=20sosten=C3=ADa=20=E2=80=94=20la=20fila=20necesitaba=20un=20to?= =?UTF-8?q?pe?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Segunda fila de la ola F2b, y la que existía para comprobar una sospecha. El README llevaba desde julio diciendo que el plan único «ya lo cubre el layout», diferido por eso. La ficha exigía medirlo antes de darlo por gratis, y no lo cubría. La causa está en la lista de pistas, no en cuántos planes hay: la fila es repeat(auto-fill, minmax(17rem, 1fr)), y auto-fill RESERVA toda pista que quepa, la ocupe alguien o no. Medido a 1280 sobre 992px de fila, un plan solo daba una tarjeta de 315px pegada al borde inicial con 677px de vacío al lado; fijar columns a 1 solo cambiaba eso por una tarjeta de 992px, que es un cartel. Meterlo en un Container sm tampoco: a 640px vuelven a caber dos pistas. Lo que faltaba era un tope sobre la FILA, y la parte no lo dejaba pasar: sus props están cerradas a propósito desde A-94, porque el envoltorio esparce el resto antes de las props que fija y un width del consumidor tipaba y se descartaba en silencio. Así que se abre nombrando: maxWidth entra en el tipo y viaja explícito al AutoGrid, que es exactamente lo que A-94 dejó dicho — el tipo dice lo que la parte honra. El marginX auto va con él sin condición: sin tope la fila ya llena la medida y auto no resuelve a nada. El número lo pone la app. Un block que horneara uno estaría decidiendo cuánto mide un plan; la demo pasa 28rem para uno y 44rem para dos, que es lo que las referencias le dan a cada arreglo, y gana un control vivo de recuento que también viaja en la URL de la vista previa. Medido después: un plan 448px centrado, dos planes 704px en dos pistas de 340 centradas, y tres planes 992px idénticos a antes — el tope es opt-in y no toca el caso que ya existía. A 375 es inerte, porque allí una sola pista ya toma el ancho. Queda anotada la lección en el README y en el handoff, porque no es de este block: una fila «diferida» puede ser una medición equivocada y no un aplazamiento. Nadie había mirado pricing con menos de tres planes. Co-Authored-By: Claude Opus 5 --- docs/process/CONTINUE-blocks.md | 15 +++-- docs/process/PLAN-blocks.md | 32 +++++++--- src/uix/blocks/pricing/README.md | 64 +++++++++++++++---- src/uix/blocks/pricing/pricing-plans.svelte | 13 +++- src/uix/blocks/pricing/types.ts | 15 +++++ web/routes/blocks/pricing/+page.svelte | 49 ++++++++++++-- web/routes/blocks/pricing/PricingSite.svelte | 25 +++++++- .../blocks/pricing/preview/+page.svelte | 8 ++- 8 files changed, 185 insertions(+), 36 deletions(-) diff --git a/docs/process/CONTINUE-blocks.md b/docs/process/CONTINUE-blocks.md index cc71130ae..fef7fce03 100644 --- a/docs/process/CONTINUE-blocks.md +++ b/docs/process/CONTINUE-blocks.md @@ -254,10 +254,9 @@ del ancho; él lo ve centrado y tiene razón), y **A-65/A-75 son el mismo patró ### d) La ola F2b — FIRMADA 2026-08-17, sin ejecutar **La fuente es `PLAN-blocks.md` §F2b** — no dupliques aquí sus tablas. Cuatro -tramos, en orden: **A** variantes de suelo (~~V4 hero `form`~~ ✅ **HECHA -2026-08-17** → **entra por V8 single-price**, que SE MIDE antes de declararse -gratis → V5 cta split → V2 spotlight → V3 -faq-lista → V1 `Pricing.Compare` en tanda propia) · **B** la matriz de +tramos, en orden: **A** variantes de suelo (~~V4 hero `form`~~ ✅ · ~~V8 +single-price~~ ✅ **ambas HECHAS 2026-08-17** → **entra por V5 cta split** → V2 +spotlight → 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 demostradas en demo (el entregable central: responde a la brecha de cardinalidad sin competir en dumps) · **C** tres blocks nuevos — `logo-cloud` @@ -292,6 +291,14 @@ block «Paridad OK». ni tocando `data-theme` a mano (el boot es el dueño; forzar el atributo NO repinta y da dos medidas idénticas que parecen «no hay diferencia»). +⚠️ **Y lo que dejó V8, que vale para TODA la ola**: **una fila «diferida» puede +ser una medición equivocada, no un aplazamiento.** El README de `pricing` decía +desde julio que el plan único «ya lo cubre el layout» y era falso — la rejilla +es `auto-fill` y RESERVA las pistas que caben aunque nadie las ocupe (un plan +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 +cualquier README, mídela. + ### e) Cabos y fases 1. **Tres cabos concretos**: diff --git a/docs/process/PLAN-blocks.md b/docs/process/PLAN-blocks.md index 60c8e3e55..1bf053a5d 100644 --- a/docs/process/PLAN-blocks.md +++ b/docs/process/PLAN-blocks.md @@ -727,14 +727,14 @@ términos, y el primero es el que más trabajo hace: ### Tramo A — las variantes de suelo -| # | Block | Qué falta | Forma firmada | Coste | -| ------ | -------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | -| **V1** | `pricing` | tabla comparativa features×planes | **`Pricing.Compare`** (parte, por el término 4). **Tanda propia con fase 0**: dos decisiones de diseño que la ficha no puede prejuzgar — (a) **quién declara las columnas** si `.Plans` y `.Compare` conviven (doble declaración de planes) y (b) **qué pasa en móvil** (las refs colapsan a lista-por-plan o a scroll) | **alto** | -| **V2** | `testimonials` | cita única en spotlight | **`layout="grid" \| "spotlight"`**. ⚠️ No es «misma anatomía» exacta: gana un slot `logo`, pierde la `Card` y `Items` pasa a un solo protagonista. Aun así `layout`, con el precedente de `hero layout="background"`, que también redefine partes. El hermano se reabre SÓLO si un día pide rotación / carousel | medio | -| **V3** | `faq` | lista estática 2/3 columnas | **`layout="accordion" \| "list"`**, con `.List` conmutando entre `Accordion` y una rejilla de Q&A siempre abiertas. **El punto fino es el TIPO**: hoy `FaqListProps = AccordionProps`, y bajo `list` no aplican ni esas props ni `value`/`disabled` de `.Item` → unión discriminada, jamás superficie descartada en silencio (A-94) | medio | -| **V4** | `hero` | `form-in-hero` | ✅ **HECHA 2026-08-17.** Slot `form`, el app compone `Form` + `Field` + `Form.Submit` entero (el hero NO toma el handle: tomarlo lo convertiría en un block que coordina). Aterriza en el sitio del CTA — tras la descripción, antes de `actions` — con medida propia `sm`; los dos conviven si llegan los dos. Detalle y medidas: README del block §«The sign-up slot» | bajo | -| **V5** | `cta` | split-with-media | tercer **`layout="split"`** + slot `media` (el nombre que ya usa `hero`); el `Mockup` existe. Es suelo: el dossier la lista | bajo | -| **V8** | `pricing` | single-price | **PRIMERO SE MIDE.** «Un solo `.Plan` y el layout ya lo sostiene» es una asunción sin verificar: en rejilla fluida un plan solo o se estira al container o se queda en su pista, y ninguna de las dos es la card centrada de las referencias — además `PricingPlansProps` es un `Pick` cerrado sin salida. Si no lo sostiene, deja de ser ~0 y vuelve como decisión | ~0 → ? | +| # | Block | Qué falta | Forma firmada | Coste | +| ------ | -------------- | --------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | +| **V1** | `pricing` | tabla comparativa features×planes | **`Pricing.Compare`** (parte, por el término 4). **Tanda propia con fase 0**: dos decisiones de diseño que la ficha no puede prejuzgar — (a) **quién declara las columnas** si `.Plans` y `.Compare` conviven (doble declaración de planes) y (b) **qué pasa en móvil** (las refs colapsan a lista-por-plan o a scroll) | **alto** | +| **V2** | `testimonials` | cita única en spotlight | **`layout="grid" \| "spotlight"`**. ⚠️ No es «misma anatomía» exacta: gana un slot `logo`, pierde la `Card` y `Items` pasa a un solo protagonista. Aun así `layout`, con el precedente de `hero layout="background"`, que también redefine partes. El hermano se reabre SÓLO si un día pide rotación / carousel | medio | +| **V3** | `faq` | lista estática 2/3 columnas | **`layout="accordion" \| "list"`**, con `.List` conmutando entre `Accordion` y una rejilla de Q&A siempre abiertas. **El punto fino es el TIPO**: hoy `FaqListProps = AccordionProps`, y bajo `list` no aplican ni esas props ni `value`/`disabled` de `.Item` → unión discriminada, jamás superficie descartada en silencio (A-94) | medio | +| **V4** | `hero` | `form-in-hero` | ✅ **HECHA 2026-08-17.** Slot `form`, el app compone `Form` + `Field` + `Form.Submit` entero (el hero NO toma el handle: tomarlo lo convertiría en un block que coordina). Aterriza en el sitio del CTA — tras la descripción, antes de `actions` — con medida propia `sm`; los dos conviven si llegan los dos. Detalle y medidas: README del block §«The sign-up slot» | bajo | +| **V5** | `cta` | split-with-media | tercer **`layout="split"`** + slot `media` (el nombre que ya usa `hero`); el `Mockup` existe. Es suelo: el dossier la lista | bajo | +| **V8** | `pricing` | single-price | ✅ **HECHA 2026-08-17 — y NO era gratis.** Medido: la fila es `auto-fill`, así que RESERVA las pistas que caben aunque nadie las ocupe → un plan solo daba 315px pegados al borde con 677px de vacío, y `columns={1}` lo cambiaba por una card de 992px. Resuelto con **`.Plans maxWidth`** (tope de fila, centrado, reenviado nombrado por A-94); el número lo pone el app. Detalle: README del block §«The row cap» | ~0 → bajo | **Fuera de la ola, a F5 con disparador** — el dossier declara `stats-band` **«Paridad OK»**, así que estas dos son _adoptables_, no suelo, y meterlas aquí @@ -2110,3 +2110,19 @@ type="button">` es una pista de MIME falsa) — el morfo lo declara tampoco normaliza oklch aquí → medía NEGRO contra todo); la señal de que la sonda es honesta es que la etiqueta del submit sale a 5.18:1, la cifra que el repo ya documenta para `primary`. +- 2026-08-17 — **F2b · V8 `single-price` HECHA, y la ficha acertó al exigir que + se midiera ANTES**: «un solo `.Plan` y el layout ya lo sostiene» —la + disposición que el README del block llevaba desde julio— era falsa. La fila es + `repeat(auto-fill, minmax(17rem, 1fr))` y **`auto-fill` reserva toda pista que + quepa**, la ocupe alguien o no: medido a 1280 sobre 992px de fila, un plan solo + daba una card de 315px pegada al borde con 677px de vacío al lado, y + `columns={1}` sólo cambiaba eso por una card de 992px. Ninguna de las dos es la + card centrada de las referencias. Resuelto abriendo `.Plans` a **`maxWidth`** + (tope de la FILA, con `marginX="auto"` incondicional para centrarla), + reenviado NOMBRADO al `AutoGrid` — nunca por `{...rest}`, que es justo lo que + A-94 dejó dicho. El número lo pone el app: la demo pasa `28rem` para un plan y + `44rem` para dos. Medido después: 448px centrados con uno, 704px (dos pistas de 340) con dos, y **992px idénticos con tres** — el tope es opt-in y no toca el + caso que ya existía; a 375 es inerte. Control vivo nuevo en la demo (nº de + planes) y en la URL del preview. ⚠️ Lección que se queda: **una fila + «diferida» puede ser una medición equivocada, no un aplazamiento** — nadie + había mirado este block con menos de tres planes. diff --git a/src/uix/blocks/pricing/README.md b/src/uix/blocks/pricing/README.md index bc6a6ed30..0bd08341d 100644 --- a/src/uix/blocks/pricing/README.md +++ b/src/uix/blocks/pricing/README.md @@ -8,18 +8,18 @@ monthly ↔ annual with the prices updating in step. ## Composition map -| Part | Composes | Notes | -| -------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| root | bare `
` + `Section` + `Container` + `Stack` | `bind:period`; provides the period context | -| `.Header` | `Box` + `Stack` | the app's section `Heading` + `Text` | -| `.Switch` | `ToggleGroup` | writes the period; reads it back to stay in sync | -| `.Plans` | `AutoGrid` + `data-stagger` | fluid row of equal-height plan cards; the stagger rhythm is structural | -| `.Plan` | `Motion` (`trigger="viewport"`) wrapping a `Card` (outline) | IS the reveal (direct child of the staggered row); `featured` → accent colour; `badge` chip | -| `.PlanName` | `Heading` (level 3) | | -| `.PlanDescription` | `Text` (muted, sm) | one-line tagline | -| `.PlanPrice` | `Group` + `Text` | shows the `monthly` / `annual` snippet per the period; `suffix` beside it | -| `.PlanFeatures` / `.PlanFeature` | `Stack` / `Group` + `Icon.Check` + `Text` | the checklist; the check is decorative (aria-hidden) | -| `.PlanAction` | `Box` (`margin-block-start: auto`) | the CTA, pinned to the card's bottom so a row lines up | +| Part | Composes | Notes | +| -------------------------------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | +| root | bare `
` + `Section` + `Container` + `Stack` | `bind:period`; provides the period context | +| `.Header` | `Box` + `Stack` | the app's section `Heading` + `Text` | +| `.Switch` | `ToggleGroup` | writes the period; reads it back to stay in sync | +| `.Plans` | `AutoGrid` + `data-stagger` | fluid row of equal-height plan cards; the stagger rhythm is structural. `maxWidth` caps and centres the row for short plan counts (V8) | +| `.Plan` | `Motion` (`trigger="viewport"`) wrapping a `Card` (outline) | IS the reveal (direct child of the staggered row); `featured` → accent colour; `badge` chip | +| `.PlanName` | `Heading` (level 3) | | +| `.PlanDescription` | `Text` (muted, sm) | one-line tagline | +| `.PlanPrice` | `Group` + `Text` | shows the `monthly` / `annual` snippet per the period; `suffix` beside it | +| `.PlanFeatures` / `.PlanFeature` | `Stack` / `Group` + `Icon.Check` + `Text` | the checklist; the check is decorative (aria-hidden) | +| `.PlanAction` | `Box` (`margin-block-start: auto`) | the CTA, pinned to the card's bottom so a row lines up | **Landmark + headings**: a bare `
` with NO accessible name of its own, so it is not an exposed landmark — the app names it by passing `aria-label` / @@ -75,10 +75,46 @@ the middle), the live monthly/annual toggle switching every price. Mini-page in | Gap | Disposition | | ------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Feature-comparison table** (features × plans matrix) | **scope-approval pending** — the dossier's recurring pricing gap. It is a table, not a row of cards, so likely a sibling (`pricing-table`) or a `.Compare` sub-block — a user decision, not a silent add | -| **Single-price variant** (one plan, centered) | **deferred** — one `.Plan` in `.Plans`; the layout already handles it | +| **Single-price variant** (one plan, centered) | **SHIPPED 2026-08-17** (F2b · V8) — as `.Plans maxWidth`. The old disposition here («the layout already handles it») was WRONG and is kept visible on purpose; see «The row cap» below | | **Per-period savings note** ("2 meses gratis") | **app-land** — the app puts it in the `annualLabel` or a `.PlanDescription` | | **Elevation on the featured plan** | **canon candidate** — `Card` exposes no elevation prop, and a block paints nothing of its own. Marking the recommended tier by depth (not just colour) needs the prop in `Card`; until then `featured` is colour only | +### The row cap (`.Plans maxWidth`) — 2026-08-17, plan F2b · V8 + +This row existed in the Gaps table as «deferred — one `.Plan` in `.Plans`; the +layout already handles it». **It did not.** Measured before touching anything, +at 1280 with the `lg` measure (992px of row): + +| Arrangement | What it measured | +| ------------------------------- | ------------------------------------------------------------------- | +| one plan, as it was | card of **315px at the start edge, 677px of emptiness** beside it | +| one plan, `columns={1}` | one card **992px wide** — a poster, not a price card | +| one plan, row capped at `28rem` | card of **448px, centred** — the arrangement the references ship | +| one plan, inside `Container sm` | 640px still fits two tracks → card of 308px with 332px of emptiness | + +The cause is in the track list, not in the plan count: the fluid row is +`repeat(auto-fill, minmax(17rem, 1fr))`, and **`auto-fill` RESERVES every track +that fits** whether or not a child occupies it. So the fix is a cap on the ROW, +which the part now takes as `maxWidth` and forwards to the `AutoGrid` +explicitly — never through `{...rest}`, because these props are closed on +purpose (A-94) and a promised surface has to be honoured. `marginX="auto"` goes +with it, unconditionally: with no cap the row already fills the measure and +`auto` resolves to nothing. + +**The number is the app's.** A block that baked one would be deciding how wide a +plan looks; the demo passes `28rem` for one plan and `44rem` for two, which is +what the references give each. + +**Measured after**: one plan 448px centred · two plans 704px (two 340px tracks), +centred · three plans 992px, byte-identical to before — the cap is opt-in and +changes nothing when it is absent · at 375 the cap is inert, since a single +track already takes the width. + ## Found while composing -- _(none yet — recorded here as they surface.)_ +- **A «deferred» disposition can be a wrong measurement, not a postponed one** + (2026-08-17). The single-price row said the layout already covered it, and the + layout did the opposite — reserving two empty tracks next to the only card. + Nobody had measured it: the block shipped with three plans and was only ever + looked at with three. Worth remembering when reading any other row of this + table. diff --git a/src/uix/blocks/pricing/pricing-plans.svelte b/src/uix/blocks/pricing/pricing-plans.svelte index 5745a6c68..982b759b7 100644 --- a/src/uix/blocks/pricing/pricing-plans.svelte +++ b/src/uix/blocks/pricing/pricing-plans.svelte @@ -4,15 +4,26 @@ import { AutoGrid } from '$uix/eidos/components/auto-grid'; import type { PricingPlansProps } from './types'; - let { minChildWidth = '17rem', columns, gap = 6, children }: PricingPlansProps = $props(); + let { + minChildWidth = '17rem', + columns, + maxWidth, + gap = 6, + children + }: PricingPlansProps = $props(); + {@render children?.()} diff --git a/src/uix/blocks/pricing/types.ts b/src/uix/blocks/pricing/types.ts index 5111f1efb..ce147f6c0 100644 --- a/src/uix/blocks/pricing/types.ts +++ b/src/uix/blocks/pricing/types.ts @@ -46,6 +46,21 @@ export type PricingPlansProps = { minChildWidth?: string; /** Fixed column count instead of fluid. */ columns?: number; + /** + * Cap for the ROW, centred inside the section's measure. It is what a short + * plan count needs: the fluid track list is `auto-fill`, so it RESERVES the + * tracks that fit even when no plan occupies them — measured at 1280 with a + * single plan, the card sat at the start edge of a 992px row with 677px of + * emptiness beside it, and `columns={1}` only traded that for one 992px-wide + * card. With a cap, one plan reads as the centred card the references ship + * (measured 448px at `28rem`), and two read as a pair. + * + * The app owns the number: a block that baked one would be deciding how wide + * a plan looks. Passed to the row EXPLICITLY, never through `{...rest}` — + * this part's props are closed on purpose (A-94), and a surface that is + * promised has to be honoured. + */ + maxWidth?: BoxProps['maxWidth']; /** Gap between plans (space scale). @default 6 */ gap?: number; children?: Snippet; diff --git a/web/routes/blocks/pricing/+page.svelte b/web/routes/blocks/pricing/+page.svelte index 15dc201b4..ac375f88e 100644 --- a/web/routes/blocks/pricing/+page.svelte +++ b/web/routes/blocks/pricing/+page.svelte @@ -3,15 +3,21 @@ * Pricing demo. Full-bleed on the page; device widths from `./preview`. */ import { Stack } from '$uix/eidos/components/stack'; + import { Group } from '$uix/eidos/components/group'; + import { Wrap } from '$uix/eidos/components/wrap'; import { Text } from '$uix/eidos/components/text'; import { Code } from '$uix/eidos/components/code'; + import { ToggleGroup } from '$uix/eidos/components/toggle-group'; import BlockDemo from '../_lib/BlockDemo.svelte'; import PricingSite from './PricingSite.svelte'; import DocRow from '../_lib/DocRow.svelte'; - // The billing toggle is the block's own live control — no demo chrome needed - // beyond the section's axes. Device widths drive the preview URL. - const previewSrc = '/blocks/pricing/preview'; + // The billing toggle is the block's own live control; the plan COUNT is the + // app's, and it is what shows the single-price and two-plan arrangements the + // reference catalogs ship. + let planCount = $state<1 | 2 | 3>(3); + + const previewSrc = $derived(`/blocks/pricing/preview?plans=${planCount}`); {#snippet preview()} - + + {/snippet} + + {#snippet controls()} + + + planes + (planCount = (Number(v[0] ?? planCount) || 3) as 1 | 2 | 3)} + aria-label="número de planes" + > + {#each [1, 2, 3] as value (value)} + {value} + {/each} + + + {/snippet} {#snippet lede()} @@ -77,6 +103,13 @@ monthlyLabel · annualLabel · aria-label. + + minChildWidth (def. 17rem) · columns · + gap (def. 6) · maxWidth. El último acota la FILA y la + centra en la medida de la sección: es lo que necesita un recuento corto de planes, porque la + rejilla fluida reserva las pistas que caben aunque estén vacías. El número lo pone la app — + un block que lo horneara estaría decidiendo cuánto mide un plan. + featured (bool) · badge (snippet). @@ -116,8 +149,12 @@ hermano (pricing-table) — decisión tuya, no un añadido aquí. - Diferido — un solo .Plan en .Plans; el layout ya - lo cubre. + Hecho (V8, 2026-08-17) — y no como estaba anotado. «El layout ya lo cubre» + era falso: la fila es auto-fill, así que RESERVA las pistas que caben aunque + nadie las ocupe — medido a 1280, un plan solo daba una tarjeta de 315px pegada al borde con + 677px de vacío al lado, y fijar columns a 1 solo cambiaba eso por una tarjeta de + 992px. Se cubre con el tope de fila (.Plans maxWidth): prueba el control + planes de arriba. {/snippet} diff --git a/web/routes/blocks/pricing/PricingSite.svelte b/web/routes/blocks/pricing/PricingSite.svelte index 3b4a1f4cf..7609d0d53 100644 --- a/web/routes/blocks/pricing/PricingSite.svelte +++ b/web/routes/blocks/pricing/PricingSite.svelte @@ -18,6 +18,8 @@ const uix = getActiveUix(); + let { planCount = 3 }: { planCount?: 1 | 2 | 3 } = $props(); + // The full semantic arc of choosing a plan. Pressing already emits // `contact-activate` (the Button's own morfo event, for free by composition); // what NO reference can express is the CONSEQUENCE — so the app emits the @@ -96,6 +98,25 @@ ] } ]; + + /** + * The single-price and two-plan arrangements the reference catalogs ship. With + * ONE plan the recommended tier is the one that stands alone; with two, the + * entry tier joins it. + */ + const shown = $derived( + planCount === 3 ? plans : planCount === 2 ? [plans[0], plans[1]] : [plans[1]] + ); + + /** + * The cap for the row, measured, not guessed: the fluid track list is + * `auto-fill`, so without it a single plan sits at the start edge of the + * measure with two empty tracks beside it (measured 315px of card against + * 992px of row at 1280). `28rem` is what the references give a lone price + * card; `44rem` holds a pair without letting either grow into a poster. + * Three plans fill the measure, so there is nothing to cap. + */ + const rowMaxWidth = $derived(planCount === 1 ? '28rem' : planCount === 2 ? '44rem' : undefined); @@ -114,8 +135,8 @@ aria-label="Periodo de facturación" /> - - {#each plans as plan (plan.name)} + + {#each shown as plan (plan.name)} {#snippet badge()} {#if plan.featured} diff --git a/web/routes/blocks/pricing/preview/+page.svelte b/web/routes/blocks/pricing/preview/+page.svelte index 43570e6ec..399eef7e2 100644 --- a/web/routes/blocks/pricing/preview/+page.svelte +++ b/web/routes/blocks/pricing/preview/+page.svelte @@ -2,8 +2,14 @@ /** * Standalone page for the pricing mini-site — same component the demo renders * inline, served as its OWN document for the device-width frame. + * + * The plan count travels in the URL so the device frame shows the same + * arrangement the demo does. */ + import { page } from '$app/state'; import PricingSite from '../PricingSite.svelte'; + + const planCount = $derived((Number(page.url.searchParams.get('plans') ?? 3) || 3) as 1 | 2 | 3); - +