You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
svelte-kit-vice/src/uix/blocks/pricing/README.md

9.3 KiB

Pricing

Function

The plan-comparison section: a billing-period toggle over a row of plans, one usually featured. It answers "what does it cost" and lets the visitor flip monthly ↔ annual with the prices updating in step.

Composition map

Part Composes Notes
root bare <section> + 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 <section> with NO accessible name of its own, so it is not an exposed landmark — the app names it by passing aria-label / aria-labelledby through ...rest. The section heading is the app's (h2, in .Header); .PlanName emits h3 and takes a level prop to move under a different parent.

Why compound WITH a context

feature-grid and feature-split are compound because their parts repeat but do not coordinate. Pricing is the tier's first compound whose parts talk to each other: the Switch writes the billing period and every PlanPrice reads it. That coordination is exactly what earns a shared context (context.ts) — the admission rule for the strongest form of compound.

The block never formats currency. PlanPrice takes monthly / annual snippets; the app composes FormatNumber (locale, currency, rounding) inside them. The block owns the switching, not the money.

Coordination

Position under the 2026-07-31 doctrine (architecture/blocks.md §«Coordination»).

It coordinates, and it already did before the doctrine had a name. The billing period travels by context from .Switch to every PlanPrice, so the app never re-derives which price to show — the case §«Coordination» describes.

What it does NOT need is a state machine or words of its own: nothing in a pricing table can be blocked, so there is no reason to explain and no mute state to make impossible. Coordination is not always a machine.

Decisions

2026-07-24 — reference floor (dossier §P1: Tailwind Plus «Pricing 12» · Untitled UI «Pricing 22» · Flowbite):

  • Adopted: the monthly/annual toggle, the featured plan, and the per-plan feature checklist + CTA — the dossier's "toggle + featured = paridad OK". The prices switch through a shared context so the toggle is honest.
  • The app owns the money: FormatNumber in the price snippets, per B-7. period is bindable so the app can observe (analytics, a persisted choice).
  • Discarded: static dumps; the plans are the app's markup, not a variant matrix.

Demo

web/routes/blocks/pricing/ — the block full-bleed, three plans (featured in the middle), the live monthly/annual toggle switching every price. Mini-page in PricingSite.svelte, shared by both surfaces.

Gaps

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) 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

  • 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.

Powered by TurnKey Linux.