@thorprovider/blocks 1.0.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/README.md +156 -40
  3. package/package.json +1 -1
  4. package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
  5. package/src/blocks/application/login-forms/login-page.tsx +240 -0
  6. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
  7. package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
  8. package/src/blocks/application/verification/verify-1.tsx +2 -2
  9. package/src/blocks/application/viewport-ownership.test.tsx +41 -0
  10. package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
  11. package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
  12. package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
  13. package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
  14. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
  15. package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
  16. package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
  17. package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
  18. package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
  19. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
  20. package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
  21. package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
  22. package/src/blocks/{marketing/navbars/navbar-4-account-menu.test.tsx → e-commerce/navigation/storefront-navbar-account-menu.test.tsx} +8 -8
  23. package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
  24. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
  25. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
  26. package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
  27. package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
  28. package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +40 -40
  29. package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +17 -17
  30. package/src/blocks/e-commerce/product-list/product-card.tsx +63 -40
  31. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +2 -2
  32. package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
  33. package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
  34. package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
  35. package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
  36. package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
  37. package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
  38. package/src/blocks/marketing/footers/footer-section.tsx +532 -0
  39. package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
  40. package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
  41. package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
  42. package/src/blocks/marketing/navbars/navbar.tsx +735 -0
  43. package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
  44. package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
  45. package/src/index.ts +80 -81
  46. package/src/lib/contracts-curated.test.ts +5 -5
  47. package/src/lib/contracts.test.ts +55 -0
  48. package/src/lib/contracts.ts +64 -2
  49. package/tsup.config.ts +1 -1
  50. package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
  51. package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
  52. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
  53. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
  54. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
  55. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
  56. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
  57. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
  58. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
  59. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
  60. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
  61. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
  62. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
  63. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
  64. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
  65. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
  66. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
  67. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
  68. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
  69. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
  70. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
  71. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
  72. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
  73. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
  74. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
  75. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
  76. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
  77. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
  78. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
  79. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
  80. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
  81. package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
  82. package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
  83. package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
  84. package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
  85. package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
  86. package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
  87. package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
  88. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
  89. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
  90. package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
  91. package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
package/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # @thorprovider/blocks
2
2
 
3
+ ## 2.0.0
4
+
5
+ ### Major Changes
6
+
7
+ - dc82f71: Consolidate the category filter family into one canonical `CategoryListing` block.
8
+
9
+ - Adds `CategoryListing` (export + `CategoryListingProps` and the `CategoryListing*` option, facet, status, filter and active-filter types). It is the single controlled listing controller: products, pagination and the selected query are props; `on*Change` / `onClearFilters` / `onRepair` / `onRetry` report intent; the block never fetches, parses the URL, or keeps a private query.
10
+ - Two bounded presentations share one contract: `sidebar` (default; persistent desktop panel plus mobile sheet) and `toolbar` (sticky mobile toolbar, filter/sort drawers and chip rail). Presentation changes chrome only.
11
+ - Results render through the canonical `ProductCard` (`minimal`), so the listing owns no card markup. Facets are generic option-only groups supplied by the host; a group renders only from real metadata, a real `0` count is displayed, and an absent/invalid count is omitted. Price renders only from a real finite range, sort only from advertised options, and no result total is fabricated.
12
+ - `ProductCard` gains `hideAddToCart` for listing cells and renders `data-slot='product-image-placeholder'` instead of an image with an empty source.
13
+ - Runtime: `toFilterProduct` → `toListingProduct` (returns canonical `ProductPresentation` with `Money`), `CategoryFilterPresentation(Input)` → `CategoryListingPresentation(Input)`, `mapCatalogResultsToFilterProps` → `mapCatalogResultsToCategoryListing`, and `resolveFilterPresentationSupport` → `resolveCategoryListingSupport` plus a separate `resolveCategoryFacetSupport`. The old numbered presentation metadata is gone.
14
+
15
+ BREAKING: removes the `CategoryFilter1`, `CategoryFilter2` and `CategoryFilter4` exports, their types, and the `category-filter-4-*` helpers. Consumers must migrate to `CategoryListing`:
16
+
17
+ | Removed | Migration |
18
+ | ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
19
+ | `CategoryFilter1` | `CategoryListing` with `presentation='sidebar'`; replace counted categories/price bands with real `categories` / `priceRange` metadata. |
20
+ | `CategoryFilter2` | `CategoryListing` with `presentation='sidebar'` (the default). Products become canonical `ProductPresentation` items with `Money` prices; `filterPrice` string prices are no longer accepted. |
21
+ | `CategoryFilter4` | `CategoryListing` with `presentation='toolbar'`; replace bike-specific facets/frame/model fields with generic `facets` groups and remove its private grid/state. |
22
+
23
+ - 28a3bd7: Consolidate the numbered checkout-form family into one canonical page-level `Checkout` block.
24
+
25
+ - Adds `Checkout` (export plus `CheckoutProps`, `CheckoutValues`, `CheckoutPresentation`, `CheckoutStepId`, `CheckoutLabels`, `CheckoutValuesFieldErrors`, `CheckoutFieldDescriptions`, `CheckoutChangeHandler`, `CheckoutOrderSummary`, `CheckoutOrderLine`, `CheckoutTotals`, `CheckoutTotalsState`, `CheckoutCountryOption`, `CheckoutShippingOption`) with a required bounded `presentation` (`single-page` | `stepper`) and an orthogonal `showReviewStep` flow option. `single-page` is the former `CheckoutForm2` structure; `stepper` is the former `CheckoutForm1` wizard; `stepper` plus the review option is the former `CheckoutForm3`. Field values are controlled (`values` + `onChange`), the step index is ephemeral block state, and `validateStep(step: CheckoutStepId)` is injected — the block contains no validation logic.
26
+ - Totals are canonical `Money` only and render verbatim from the host; the legacy numeric `price`/`shipping`/`tax`/`discount`/`promoDiscount` fields and their fallback summation are removed. A missing or malformed required total renders as unavailable with recovery, never as zero.
27
+ - Removes raw card capture: `cardNumber`, `expiryDate`, `cvv`, `cardName`, the `collectCardFields` escape hatch, the CVV visibility toggle, and `card-formatting.ts`. Payment methods remain data (`PaymentMethod[]`); card collection belongs to the host payment-provider surface.
28
+ - Removes the numbered exports `CheckoutForm1`, `CheckoutForm2`, `CheckoutForm3` and their `CheckoutForm1*`, `CheckoutForm2*`, `CheckoutForm3*` types.
29
+
30
+ BREAKING: consumers importing a numbered checkout must migrate to `Checkout` with the `presentation` prop (and `showReviewStep` for the review flow). Values lose the card fields and `sameAsBilling`; totals must be supplied as backend `Money`; `validateStep` now receives a `CheckoutStepId` instead of a step number.
31
+
32
+ - 06333a8: Consolidate the navigation family into two canonical components and retire the numbered navbars.
33
+
34
+ - `Navbar` is the canonical page-level header with bounded `bar`, `mega`, and `floating` presentations. Content is serializable (`links`, `items`, `featured`, `signIn`, `cta`, `logoText`, `logoHref`, `currentPath`); icons resolve through a host-bound `iconMap`; `floating` uses `sticky` so it reserves its own space.
35
+ - `StorefrontNavbar` is the canonical commerce shell (formerly `Navbar4`) and keeps its full storefront contract: utility strip, category navigation, optional search, account states, and the controlled cart sheet with canonical `Money`.
36
+ - Removed identifiers: `Navbar1`, `Navbar2`, `Navbar3`, `Navbar4`, `navbar4PropsSchema`, `Navbar4SpecProps`, `Navbar4LikeButton`, `Navbar4ProductGrid`, and the `navbar-4-*` helper modules (now `storefront-navbar-cart-sheet`, `storefront-navbar-account-menu`, `storefront-navbar-search-bar`, `storefront-navbar-mobile-nav`).
37
+ - The page-level header no longer owns cart state: `cartCount` and `onOpenCart` are gone. Use the commerce shell for the cart affordance.
38
+
39
+ - 65fac4b: Remove the numbered compatibility adapters `Testimonial1`, `Testimonial2`, `LoginPage1`, `LoginPage2`, `FooterSection1`, `FooterSection3`, and `FooterSection4`, including their deep module paths and legacy prop types. Migrate testimonials to `TestimonialSection` with `carousel` or `bento`, login pages to `LoginPage` with `card` or `social`, and footers to `FooterSection` with `brand-columns`, `newsletter-grid`, or `contact`. Select native footer link behavior explicitly where browser navigation is intended.
40
+
41
+ ### Minor Changes
42
+
43
+ - 5d32763: The auth blocks render into the host page instead of owning it.
44
+
45
+ `LoginPage` (both `card` and `social`), `SignUpPage1`, `ForgotPasswordPage1` and `Verify1` no longer paint a full-viewport surface. They drop the `min-h-dvh` canvas, the page landmark (the `social` presentation no longer renders a root `<main>`), the `bg-linear-to-br` backdrop with `isolate`/`overflow-hidden`, the vertical centring and the page-level padding. Each block now renders a neutral, content-sized root that keeps its width cap (`max-w-md` for login and recovery, `max-w-lg` for registration) and its card styling, including the shadow.
46
+
47
+ Why: a generated storefront mounts these blocks inside the default chrome (notice + banner + navbar + footer). A viewport-height canvas inside that chrome centres the form roughly 100 px low and, for the `social` presentation, emits a second `<main>` landmark inside the page's own `<main>`.
48
+
49
+ No prop, type or export changed, so existing call sites keep compiling. What changes is visual ownership: a host that relied on the block's canvas must now supply one (a route shell, a modal, a split screen, or a template the host owns), using the same theme tokens. `@thorprovider/storefront` does exactly that for the generated `account` view, which keeps the shopper-visible result unchanged.
50
+
3
51
  ## 1.0.0
4
52
 
5
53
  ### Major Changes
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  title: "@thorprovider/blocks"
3
3
  purpose: "Reusable, framework-agnostic storefront blocks built from ShadcnStore UI, decoupled from data and backend"
4
- lastReviewed: "2026-10-17"
4
+ lastReviewed: "2026-10-21"
5
5
  ---
6
6
 
7
7
  # @thorprovider/blocks
@@ -39,22 +39,18 @@ Blocks follow `src/blocks/<category>/<section>/<block>.tsx`. Each block co-locat
39
39
 
40
40
  | Export | Category | Description |
41
41
  | --- | --- | --- |
42
- | `Navbar1` | Marketing | Simple navbar with logo, links, sign-in, CTA and cart count |
43
- | `Navbar2` | Marketing | Mega-menu navbar with grouped flyouts and featured card |
44
- | `Navbar3` | Marketing | Floating navbar that condenses into a rounded pill on scroll |
45
- | `Navbar4` | Marketing | Storefront navbar: utility bar, category links, search, account menu, cart sheet |
42
+ | `Navbar` | Marketing | Page-level navigation header: `bar`, `mega` (grouped flyouts plus optional featured promo) or `floating` (self-reserving pinned pill) |
43
+ | `StorefrontNavbar` | E-commerce | Commerce navigation shell: utility strip, category mega menu, optional search, account menu, cart sheet |
46
44
  | `Banner1` | Marketing | Announcement banner (bar or pill) with dismiss |
47
45
  | `HeroSection1` | Marketing | Hero with badge, headline, CTA, reviewer avatars, rating and image |
48
46
  | `CtaSection1` | Marketing | Full-width call-to-action with badge, headline and two CTAs |
49
47
  | `Feature` | Marketing | Canonical benefits section: `cards` (icon feature grid) or `split` (editorial split with a required image) presentation |
50
- | `Testimonial1` | Marketing | Testimonial carousel with rating stars |
51
- | `Testimonial2` | Marketing | Bento testimonials grid |
48
+ | `TestimonialSection` | Marketing | Canonical testimonial collection section with `carousel` and `bento` presentations |
52
49
  | `FaqBlock1` | Marketing | FAQ accordion (single or multiple open) |
53
50
  | `PricingSection1` | Marketing | Pricing tiers with popular badge |
54
51
  | `Statistic1` | Marketing | Metrics grid with progress bars |
55
52
  | `LogoCloud1` | Marketing | Logo cloud with skeleton fallback |
56
- | `FooterSection1` | Marketing | Footer with brand, link columns and newsletter |
57
- | `FooterSection3` | Marketing | Newsletter + six-column footer with social links |
53
+ | `FooterSection` | Marketing | Canonical page-level footer with `brand-columns`, `newsletter-grid` or `contact` presentation |
58
54
  | `ProductCard` | E-commerce | Canonical product card (item cell of a listing); `presentation` `classic`/`minimal`/`rich`, canonical `ProductPresentation` items with `Money` prices, evidence-only merchandising |
59
55
  | `ProductShowcase1` | E-commerce | Combined grid-and-carousel of product cards with manual paging |
60
56
  | `ProductOverview` | E-commerce | Canonical product detail; bounded `presentation` `standard`/`gallery`/`rich` (rich adds optional specs/features/reviews tabs) |
@@ -62,35 +58,138 @@ Blocks follow `src/blocks/<category>/<section>/<block>.tsx`. Each block co-locat
62
58
  | `ReviewRating1` | E-commerce | Review list with rating summary |
63
59
  | `ReviewRating2` | E-commerce | Rating summary with distribution and review carousel |
64
60
  | `ProductCategory` | E-commerce | Canonical category block; bounded `presentation` `grid` (image cards) / `icons` (compact icon grid) / `list` (rows with subcategory chips and optional controlled search) |
65
- | `CategoryFilter1` | E-commerce | Category filter bar |
66
- | `CategoryFilter2` | E-commerce | Filter sidebar with mobile sheet |
61
+ | `CategoryListing` | E-commerce | Canonical category listing (results + pagination) with a controlled query and bounded `presentation` `sidebar` (persistent desktop panel + mobile sheet) / `toolbar` (sticky mobile toolbar + drawers); generic facet groups render only from real metadata |
67
62
  | `ShoppingCart` | E-commerce | Canonical cart; backend-authoritative `Money` totals and item lines, optional promo entry and recently-viewed suggestions; `presentation` `compact` (collapsible card) or `page` (full-width review) |
68
- | `CheckoutForm1` | E-commerce | Essential checkout fields |
69
- | `CheckoutForm2` | E-commerce | Single-page checkout with order summary beside |
63
+ | `Checkout` | E-commerce | Canonical page-level checkout; controlled values, backend-authoritative `Money` totals, `PaymentMethod[]` data with no raw card input; `presentation` `single-page` or `stepper`, optional `showReviewStep` |
70
64
  | `CheckoutConfirmation` | E-commerce | Order confirmation with fulfilment timeline |
71
65
  | `OrderHistory1` | E-commerce | Orders list |
72
66
  | `OrderHistory2` | E-commerce | Order cards with carousel items |
73
67
  | `StorefrontHero` | E-commerce | Canonical storefront hero; `presentation` `compact` (gradient headline, CTAs, product grid), `showcase` (search + rotating product carousel), `editorial` (departments, featured deals, store locator) or `promotional` (rotating deals bar, category cards) |
74
68
  | `AppShell1` | Application | Application shell with navigation slots |
75
- | `LoginPage1` | Application | Login card with remember-me and social buttons |
76
- | `LoginPage2` | Application | Login with prominent social options |
77
- | `SignUpPage1` | Application | Registration card with social options |
69
+ | `LoginPage` | Application | Canonical sign-in form with explicit `card` or `social` presentation |
70
+ | `SignUpPage1` | Application | Registration form with social options (same profile as `LoginPage`) |
78
71
  | `ForgotPasswordPage1` | Application | Four-step password recovery wizard |
79
72
  | `Verify1` | Application | Two-factor verification (OTP or backup code) |
80
73
 
81
- Interactions are exposed as callbacks (e.g. `onAddToCart`, `onBuyNow`, `onSelectColor`). `StorefrontHero` renders each affordance only when its handler is supplied (no dead controls); other blocks keep elements in the DOM with no-op behavior when a callback is absent.
74
+ Interactions are exposed as callbacks (e.g. `onAddToCart`, `onBuyNow`, `onSelectColor`). `StorefrontHero` and `LoginPage` render optional affordances only when their handlers are supplied; other blocks may keep elements in the DOM with no-op behavior when a callback is absent.
75
+
76
+ ### Category listing
77
+
78
+ `CategoryListing` is the single controlled listing controller. Products, pagination, the selected query values, active-filter chips and the status are props; the block emits `on*Change`/`onClearFilters`/`onRepair`/`onRetry` and never fetches, parses the URL, or keeps a private query. `presentation` changes only the chrome: `sidebar` keeps a persistent desktop panel plus a mobile sheet, `toolbar` adds a sticky mobile toolbar with filter/sort drawers. Results render through the canonical `ProductCard`, so the listing never owns card markup. Facets are option-only groups (`{ id, label, options: [{ value, label, count? }] }`) supplied by the host; a group renders only when real metadata provides it, a real `0` count is displayed, and an absent/invalid count is omitted rather than invented.
79
+
80
+ ### Footer sections
81
+
82
+ `FooterSection` is the canonical page-level footer. Its required `presentation`
83
+ is `brand-columns`, `newsletter-grid`, or `contact`; provide serializable text,
84
+ links, and social records as props. Social records use `{ label, href?, icon }`
85
+ with an optional `ariaLabel`; callbacks and visible fallback text keep using the
86
+ raw `label`. Bind icon IDs to `LucideIcon` components through `socialIcons`, and
87
+ keep that map, `logo` nodes, and callbacks outside JSON fixtures.
88
+
89
+ Column/legal and social links have separate `linkBehavior` and
90
+ `socialLinkBehavior` props, both defaulting to `host`. Host mode requires the
91
+ matching callback and renders callback-less destinations as text; `native`
92
+ preserves browser navigation. Contact address, phone, and email hrefs are native.
93
+ Newsletter controls stay visible but disabled without `onNewsletterSubmit`;
94
+ the callback receives the entered email and the block performs no network request.
95
+
96
+ The numbered `FooterSection1`, `FooterSection3`, and `FooterSection4` exports,
97
+ deep module paths, and legacy prop contracts have been removed. Migrate to
98
+ `FooterSection` by supplying an explicit presentation and canonical serializable
99
+ content props. Select `linkBehavior="native"` or
100
+ `socialLinkBehavior="native"` when browser navigation is intended; both default
101
+ to host-controlled behavior. Canonical link types are exported as
102
+ `CanonicalFooterLink` and `CanonicalFooterLinkColumn` to avoid collisions with
103
+ other package contracts.
104
+
105
+ ### Login page
106
+
107
+ `LoginPage` is a sign-in composition with one shared credential form and a
108
+ required `presentation` of `card` or `social`. Both presentations submit the
109
+ same `{ email, password, remember }` payload. `remember` starts as `false` and
110
+ is reported to the host; the block does not decide session persistence.
111
+
112
+ The block renders into the host page and does not own it: no viewport height, no
113
+ page landmark, no page backdrop, no vertical centring. `card` caps its width at
114
+ `max-w-md` and `social` at the same cap, and the mounting page supplies the
115
+ canvas — a route shell, a modal, a split screen, or a template the host owns.
116
+ Before the shell was removed, each of these blocks painted a full-viewport
117
+ surface; a host that relied on that canvas must now provide it.
118
+
119
+ Provider data stays serializable. Pass provider IDs and labels separately from
120
+ the icon components, and provide a selection callback so provider buttons are
121
+ only rendered when they can act. IDs must be unique within the list; an
122
+ unmapped icon falls back to the visible provider label:
123
+
124
+ ```tsx
125
+ import { AtSign } from 'lucide-react'
126
+ import { LoginPage } from '@thorprovider/blocks'
127
+
128
+ <LoginPage
129
+ presentation='social'
130
+ providers={[{ id: 'email', label: 'Email' }]}
131
+ providerIcons={{ email: AtSign }}
132
+ onProviderSelect={providerId => startProviderLogin(providerId)}
133
+ onSubmit={({ email, password, remember }) => signIn({ email, password, remember })}
134
+ />
135
+ ```
136
+
137
+ The numbered `LoginPage1` and `LoginPage2` exports, deep module paths, and legacy
138
+ prop contracts have been removed. Migrate to `LoginPage` and select `card` or
139
+ `social` explicitly; both presentations use the canonical
140
+ `{ email, password, remember }` host callback contract.
141
+
142
+ ## Testimonials
143
+
144
+ `TestimonialSection` renders a collection of testimonials in one of two explicit
145
+ presentations: `carousel` or `bento`. Each item requires a stable, collection-unique
146
+ `id`, `name`, and `content`. `role`, `avatar`, and `rating` are optional and are
147
+ rendered only when supplied; the block never invents attribution or ratings and
148
+ does not emit an empty image source.
149
+
150
+ ```tsx
151
+ import { TestimonialSection } from '@thorprovider/blocks'
152
+
153
+ const testimonials = [
154
+ { id: 't-1', name: 'Rosa Díaz', role: 'Propietaria', content: 'Excelente servicio.' },
155
+ ]
156
+
157
+ <TestimonialSection
158
+ presentation="carousel"
159
+ items={testimonials}
160
+ title="Lo que dicen nuestros clientes"
161
+ carouselLabels={{
162
+ previous: 'Testimonio anterior',
163
+ next: 'Testimonio siguiente',
164
+ slide: 'Ir al testimonio {n}',
165
+ }}
166
+ />
167
+ ```
168
+
169
+ For `carousel`, `carouselLabels` is required and supplies localized previous,
170
+ next, and slide-button labels; `{n}` is replaced with the one-based item number.
171
+ The native buttons support keyboard activation, and the selected item is exposed
172
+ with `aria-current`. For `bento`, omit `carouselLabels`; one-to-three items render
173
+ without empty cards, and collections larger than four repeat the four-tile visual
174
+ rhythm without dropping entries. An empty `items` array renders no section or
175
+ controls. `title`, `description`, and `className` are optional for either
176
+ presentation.
177
+
178
+ The numbered `Testimonial1` and `Testimonial2` exports, deep module paths, and
179
+ legacy prop contracts have been removed. Migrate to `TestimonialSection` with
180
+ shared testimonial items and select `carousel` or `bento` explicitly.
82
181
 
83
182
  ## Optional brand props
84
183
 
85
184
  Three existing blocks expose an optional brand seam. Omitting the prop renders
86
185
  the block's current output unchanged; no demo content is added.
87
186
 
88
- - `Navbar4` — `logoAccent?: { text: string; className?: string }` accents a
187
+ - `StorefrontNavbar` — `logoAccent?: { text: string; className?: string }` accents a
89
188
  trailing segment of `logoText`. With `logoText="ElectroCuba"` and
90
189
  `logoAccent={{ text: 'Cuba', className: 'text-brand-promo' }}`, the block
91
190
  renders `Electro` plus an accented `Cuba` span. If `text` is not a trailing
92
191
  segment of `logoText`, the wordmark renders unchanged.
93
- - `FooterSection1` — `brandAccent?: { text: string; className?: string }` applies
192
+ - `FooterSection` — `brandAccent?: { text: string; className?: string }` applies
94
193
  the same trailing-segment accent to `brandName`.
95
194
  - `StorefrontHero` — `heroImage?: { src: string; alt: string }` renders a native
96
195
  `<img>` when `src` is a non-empty string, and nothing when the prop is absent
@@ -101,13 +200,13 @@ the library hardcodes no brand color.
101
200
 
102
201
  ## Custom surface `className`
103
202
 
104
- `Navbar4` (outer `<header>`) and `FooterSection1` (outer `<footer>`) accept an
203
+ `Navbar4` (outer `<header>`) and `FooterSection` (outer `<footer>`) accept an
105
204
  optional `className`, merged with the existing `cn` helper. It is purely
106
205
  presentational: no layout, structure, or behavior prop, and no hardcoded color.
107
206
 
108
207
  - **Omitted `className`** — the block renders its default surface unchanged.
109
208
  `Navbar4` keeps `bg-background/95` **and** its
110
- `supports-backdrop-filter:bg-background/80` variant; `FooterSection1` keeps
209
+ `supports-backdrop-filter:bg-background/80` variant; `FooterSection` keeps
111
210
  `bg-secondary`.
112
211
  - **Supplied `className`** — the consumer owns the surface. The default
113
212
  background **and its backdrop-filter variant** are not applied, so a semantic
@@ -116,13 +215,13 @@ presentational: no layout, structure, or behavior prop, and no hardcoded color.
116
215
  it). Layout, border, sticky, and `backdrop-blur-md` classes stay internal.
117
216
 
118
217
  The consumer is responsible for the text, transparency, and context tokens of a
119
- supplied surface. `Navbar4` adapts its primary nav links: they inherit the
218
+ supplied surface. `StorefrontNavbar` adapts its primary nav links: they inherit the
120
219
  surface text color and add `hover:text-foreground` so `hover:bg-muted` never
121
220
  paints white-on-light. No magic colors are introduced by the block.
122
221
 
123
- ## Navbar4 account affordance
222
+ ## StorefrontNavbar account affordance
124
223
 
125
- `Navbar4` renders a **session-driven** account affordance exclusively from
224
+ `StorefrontNavbar` renders a **session-driven** account affordance exclusively from
126
225
  consumer-supplied props — it never resolves a session or authenticates itself.
127
226
  Given a `user` (`StoreUser`; `avatar` is optional) and `accountMenuItems`, it
128
227
  renders an authenticated account menu with the identity, the configured items,
@@ -140,10 +239,10 @@ authentication.
140
239
 
141
240
  ## Footer newsletter
142
241
 
143
- `FooterSection1` renders the interactive newsletter form (input + submit button)
144
- **only** when `newsletter.onSubmit` is supplied. A `newsletter` object without
145
- `onSubmit` renders no form, input, or button — never a fake form. Omit
146
- `newsletter` entirely to hide the newsletter block.
242
+ `FooterSection` renders the newsletter form disabled when
243
+ `onNewsletterSubmit` is absent. When supplied, the callback receives the entered
244
+ email; the block performs no network request. Omit `newsletter` to hide the
245
+ newsletter block.
147
246
 
148
247
  ## Cart presentation (controlled)
149
248
 
@@ -175,10 +274,10 @@ either is visible in the other.
175
274
  suggestion list (name, optional image, optional `Money` price) rendered only
176
275
  when supplied.
177
276
 
178
- `Navbar4` owns the lateral. Pass `cartOpen` + `onCartOpenChange` to control the
277
+ `StorefrontNavbar` owns the lateral. Pass `cartOpen` + `onCartOpenChange` to control the
179
278
  sheet's visibility from host state (omit both to keep the legacy uncontrolled
180
279
  sheet); the block keeps `aria-expanded`/`data-state` in sync. Its cart sheet
181
- (`Navbar4CartSheet`) takes an optional `canonical` mode where each line shows the
280
+ (`StorefrontNavbarCartSheet`) takes an optional `canonical` mode where each line shows the
182
281
  canonical `lineTotal` (never the placeholder numeric price), plus
183
282
  `checkoutUnavailable`/`viewCartUnavailable` to hide actions the store cannot
184
283
  serve, and a `state` (`'idle' | 'pending' | 'error'`) with retry for read/mutation
@@ -210,18 +309,35 @@ affordance only when supplied. Prices render through the canonical
210
309
  `formatMoney` path (no hardcoded currency), and every label is overridable
211
310
  with Spanish defaults (`Agregar al carrito`, `Precio no disponible`, …).
212
311
 
213
- ## Checkout blocks (controlled)
312
+ ## Checkout block (page-level, controlled)
313
+
314
+ `Checkout` is the single public checkout surface; it replaces the numbered `CheckoutForm1`, `CheckoutForm2`, and `CheckoutForm3` blocks. It is a **controlled component**: field values arrive through `values` and every edit is reported through `onChange`, so the host owns all commercial state. The block owns only the ephemeral step index of the `stepper` presentation.
315
+
316
+ - `presentation: 'single-page' | 'stepper'` (required) — `single-page` renders every section visible at once (summary first in DOM order on mobile); `stepper` shows one step at a time with an accessible progress indicator. Both accept the same props and emit the same callbacks.
317
+ - `showReviewStep?: boolean` — appends a confirmation step (`stepper`) or section (`single-page`) before submission.
318
+ - `values: CheckoutValues` — `email`, `firstName`, `lastName`, `phone`, `address`, `city`, `state`, `zipCode`, `country`, `paymentMethod`, `saveInfo`, `newsletter`, `promoCode`. There are no card fields.
319
+ - `onChange: (field, value) => void` — reported per edit. The block holds no private copy of the form data and applies no masking.
320
+ - `orderSummary?: { items, totals? }` — line identity, name, quantity, optional variant/image, optional canonical `Money` `lineTotal` and `unitPrice`, plus backend `totals` (`subtotal`, optional `discount`/`tax`/`shipping`, `total`). Amounts render verbatim; required totals that are absent or malformed render as unavailable with the retry action, never as zero. No local arithmetic, tax rate, shipping cost, threshold, or hardcoded currency.
321
+ - `totalsState?: 'idle' | 'pending' | 'error'` and `onRetryTotals?` — controlled reads; pending and failed states render status/alert copy without inventing amounts.
322
+ - `countries?: { value, label }[]`, `shippingOptions?`, `selectedShippingOptionId?`, `onSelectShippingOption?` — host-owned options and selection. With no country list the block renders a text input; with no shipping options it renders no method section; nothing is pre-selected.
323
+ - `paymentMethods?: PaymentMethod[]` — payment methods as data (canonical `@thorprovider/types`). One option per method, the selected method's description, an empty state for an empty list, and a non-selectable rendering for `isEnabled === false`. The block renders **no raw card fields**: card collection belongs to the host payment-provider surface.
324
+ - `labels?: Partial<CheckoutLabels>` and `formatMoney?: (money: Money) => string` — every user-facing string is overridable (neutral English defaults) and amounts format through canonical `Money` with no hardcoded symbol.
325
+ - `fieldErrors?` / `fieldDescriptions?` — per-field messages and helper text, rendered with `role="alert"` and `aria-invalid`/`aria-describedby` wiring.
326
+ - `validateStep?: (step: CheckoutStepId) => boolean` and `onStepChange?` — the block consults `validateStep` before advancing and for every applicable step before submitting; `onStepChange` is a notification only, never a controlled value. Without the callback the block advances and leaves submit protection to the host.
327
+ - `onSubmit?`, `submitting?`, `submitError?`, `onRetrySubmit?` — submission is reported, with a pending guard and a recoverable error state; the block never creates an order, clears a cart, or claims success.
328
+
329
+ Validation itself is **not** implemented in the block. Declare checks in your store spec's validation configuration (built-in `required`/`email`/`pattern`/`minLength`/`maxLength`/cross-field `requiredIf`/`equalTo`/`lessThan`/`greaterThan`, plus catalog-registered custom functions) and evaluate them with the renderer; the messages live in the spec so they are per-store and per-language.
330
+
331
+ The order summary is an internal projection of `Checkout`, not a separate block. Order receipt and fulfilment progress remain `CheckoutConfirmation`.
214
332
 
215
- `CheckoutForm1` (3-step wizard) and `CheckoutForm2` (single page) are **controlled components**: they receive the current form values through a `values` prop and report every edit through `onChange`. They hold no internal form state, so a renderer's validation context can read field values from its own state model. `CheckoutForm3` (4-step wizard) keeps the same contract with internal step state.
333
+ ### Migrating from the numbered checkout blocks
216
334
 
217
- - `paymentMethods?: PaymentMethod[]` — available payment methods as data (canonical `@thorprovider/types` `PaymentMethod`). The blocks render a radio list from this prop, show the selected method's name + `description`, and only render the card fields (`cardNumber`, `expiryDate`, `cvv`, `cardName`) when the selected method's `type === 'card'`. No payment method is hardcoded; pass an empty list to trigger the empty state.
218
- - `values: CheckoutForm1FormData` / `CheckoutForm2Data` — current field values (including `cardNumber`, `expiryDate`, `cvv`, `cardName`, and `paymentMethod` for `CheckoutForm1`/`CheckoutForm3`).
219
- - `onChange: (field, value) => void` — reported per edit; card grouping and MM/YY masking are applied by the block before the value reaches `onChange` (presentation only, no validation logic in the block).
220
- - `fieldErrors?: Partial<Record<FieldKey, Array<{ message?: string }>>>` — per-field error messages rendered via `FieldError` (`role="alert"`, `aria-invalid`/`aria-describedby` wiring). The same channel carries async/server errors (e.g. payment decline, invalid promo).
221
- - `fieldDescriptions?: Partial<Record<FieldKey, string>>` — helper text rendered via `FieldDescription`.
222
- - `validateStep?: (step: number) => boolean` (wizard only) — the block calls it on "Next"; advance only happens when it returns `true`, and the final submit only fires when every step validates.
335
+ | Retired | Replacement |
336
+ | --- | --- |
337
+ | `CheckoutForm1` | `<Checkout presentation="stepper" />` — same controlled values minus `cardNumber`/`expiryDate`/`cvv`/`cardName`/`sameAsBilling`; `validateStep` now receives a `CheckoutStepId` (`'contact' \| 'shipping' \| 'payment' \| 'review'`) instead of a number |
338
+ | `CheckoutForm2` | `<Checkout presentation="single-page" />` — the host supplies `Money` totals; the block no longer computes tax, shipping, or free-shipping thresholds |
339
+ | `CheckoutForm3` | `<Checkout presentation="stepper" showReviewStep />` — values become controlled; `apartment`, card fields, per-step icons, and internal `isStepComplete` state are gone |
223
340
 
224
- Validation itself is **not** implemented in the blocks. Declare checks in your store spec's validation configuration (built-in `required`/`email`/`pattern`/`minLength`/`maxLength`/cross-field `requiredIf`/`equalTo`/`lessThan`/`greaterThan`, plus catalog-registered custom functions like `luhn`/`cvv`/`expiryDate`) and evaluate them with the renderer; the messages live in the spec so they are per-store and per-language. Without a validation provider the blocks render with no inline errors.
225
341
 
226
342
  ## Usage
227
343
 
@@ -295,4 +411,4 @@ callbacks (`useCallback`); a fresh object literal always notifies a reference-on
295
411
  store guard; and every registered action completes its round-trip (handler → set +
296
412
  emit → spec `on` → recompute → bind). See
297
413
  [`docs/standards/interaction-contracts.md`](docs/standards/interaction-contracts.md)
298
- for the rules and the review checklist.
414
+ for the rules and the review checklist.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thorprovider/blocks",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "Reusable, framework-agnostic React component blocks for Thor Commerce storefronts.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -0,0 +1,190 @@
1
+ // @vitest-environment jsdom
2
+ import { beforeAll, describe, expect, it, vi } from 'vitest'
3
+ import { render, screen } from '@testing-library/react'
4
+ import userEvent from '@testing-library/user-event'
5
+ import { AtSign } from 'lucide-react'
6
+ import * as blockExports from '../../../index'
7
+ import { LoginPage, type LoginPageProps } from './login-page'
8
+
9
+ beforeAll(() => {
10
+ Object.assign(SVGElement.prototype, { getTotalLength: () => 15 })
11
+ })
12
+
13
+ describe('LoginPage', () => {
14
+ it('is the only exported login page API', () => {
15
+ expect(blockExports.LoginPage).toBe(LoginPage)
16
+ expect('LoginPage1' in blockExports).toBe(false)
17
+ expect('LoginPage2' in blockExports).toBe(false)
18
+ })
19
+
20
+ it.each(['card', 'social'] as const)('submits the same structured credentials for the %s presentation', async presentation => {
21
+ const user = userEvent.setup()
22
+ const onSubmit = vi.fn()
23
+
24
+ render(
25
+ <LoginPage
26
+ presentation={presentation}
27
+ emailLabel='Email'
28
+ passwordLabel='Password'
29
+ rememberMeLabel='Remember me'
30
+ signInLabel='Sign in'
31
+ onSubmit={onSubmit}
32
+ />,
33
+ )
34
+
35
+ await user.type(screen.getByLabelText('Email'), 'shopper@example.com')
36
+ await user.type(screen.getByLabelText('Password'), 'correct-horse')
37
+ await user.click(screen.getByRole('checkbox', { name: 'Remember me' }))
38
+ await user.click(screen.getByRole('button', { name: 'Sign in' }))
39
+
40
+ expect(onSubmit).toHaveBeenCalledWith({
41
+ email: 'shopper@example.com',
42
+ password: 'correct-horse',
43
+ remember: true,
44
+ })
45
+ })
46
+
47
+ it('rejects a missing presentation instead of silently choosing a layout', () => {
48
+ expect(() => render(<LoginPage {...({} as LoginPageProps)} />)).toThrow(/presentation/i)
49
+ })
50
+
51
+ it('round-trips serializable provider data and reports the selected provider id', async () => {
52
+ const user = userEvent.setup()
53
+ const onProviderSelect = vi.fn()
54
+ const providers = JSON.parse(JSON.stringify([{ id: 'github', label: 'GitHub' }])) as LoginPageProps['providers']
55
+
56
+ render(
57
+ <LoginPage
58
+ presentation='social'
59
+ providers={providers}
60
+ providerIcons={{ github: AtSign }}
61
+ onProviderSelect={onProviderSelect}
62
+ />,
63
+ )
64
+
65
+ await user.click(screen.getByRole('button', { name: 'GitHub' }))
66
+
67
+ expect(onProviderSelect).toHaveBeenCalledWith('github')
68
+ })
69
+
70
+ it('renders provider labels when an icon is not mapped', () => {
71
+ render(
72
+ <LoginPage
73
+ presentation='card'
74
+ providers={[{ id: 'custom', label: 'Custom provider' }]}
75
+ onProviderSelect={vi.fn()}
76
+ />,
77
+ )
78
+
79
+ expect(screen.getByRole('button', { name: 'Custom provider' }).textContent).toBe('Custom provider')
80
+ })
81
+
82
+ it('omits the provider group when there are no providers', () => {
83
+ render(<LoginPage presentation='card' onProviderSelect={vi.fn()} />)
84
+
85
+ expect(screen.queryByRole('group', { name: 'O continúa con' })).toBeNull()
86
+ })
87
+
88
+ it('rejects duplicate provider IDs so provider React keys remain unique', () => {
89
+ expect(() =>
90
+ render(
91
+ <LoginPage
92
+ presentation='card'
93
+ providers={[
94
+ { id: 'duplicate', label: 'First' },
95
+ { id: 'duplicate', label: 'Second' },
96
+ ]}
97
+ onProviderSelect={vi.fn()}
98
+ />,
99
+ ),
100
+ ).toThrow(/provider ids must be unique/i)
101
+ })
102
+
103
+ it('omits optional actions and providers when their handlers are absent', () => {
104
+ render(
105
+ <LoginPage
106
+ presentation='card'
107
+ forgotPasswordLabel='Forgot password'
108
+ createAccountLabel='Create account'
109
+ providers={[{ id: 'github', label: 'GitHub' }]}
110
+ />,
111
+ )
112
+
113
+ expect(screen.queryByRole('button', { name: 'Forgot password' })).toBeNull()
114
+ expect(screen.queryByRole('button', { name: 'Create account' })).toBeNull()
115
+ expect(screen.queryByRole('group', { name: 'O continúa con' })).toBeNull()
116
+ })
117
+
118
+ it('invokes optional action handlers', async () => {
119
+ const user = userEvent.setup()
120
+ const onForgotPassword = vi.fn()
121
+ const onCreateAccount = vi.fn()
122
+
123
+ render(
124
+ <LoginPage
125
+ presentation='card'
126
+ forgotPasswordLabel='Forgot password'
127
+ createAccountLabel='Create account'
128
+ onForgotPassword={onForgotPassword}
129
+ onCreateAccount={onCreateAccount}
130
+ />,
131
+ )
132
+
133
+ await user.click(screen.getByRole('button', { name: 'Forgot password' }))
134
+ await user.click(screen.getByRole('button', { name: 'Create account' }))
135
+
136
+ expect(onForgotPassword).toHaveBeenCalledOnce()
137
+ expect(onCreateAccount).toHaveBeenCalledOnce()
138
+ })
139
+
140
+ it('updates the password toggle accessible name with its state', async () => {
141
+ const user = userEvent.setup()
142
+
143
+ render(
144
+ <LoginPage
145
+ presentation='card'
146
+ passwordLabel='Password'
147
+ showPasswordAriaLabel='Show password'
148
+ hidePasswordAriaLabel='Hide password'
149
+ />,
150
+ )
151
+
152
+ expect(screen.getByRole('button', { name: 'Show password' })).toBeTruthy()
153
+ await user.click(screen.getByRole('button', { name: 'Show password' }))
154
+ expect(screen.getByRole('button', { name: 'Hide password' })).toBeTruthy()
155
+ expect(screen.getByLabelText('Password').getAttribute('type')).toBe('text')
156
+ })
157
+
158
+ it('keeps fields and secondary actions reachable in keyboard order', async () => {
159
+ const user = userEvent.setup()
160
+
161
+ render(
162
+ <LoginPage
163
+ presentation='card'
164
+ emailLabel='Email'
165
+ passwordLabel='Password'
166
+ rememberMeLabel='Remember me'
167
+ forgotPasswordLabel='Forgot password'
168
+ signInLabel='Sign in'
169
+ createAccountLabel='Create account'
170
+ onForgotPassword={vi.fn()}
171
+ onCreateAccount={vi.fn()}
172
+ />,
173
+ )
174
+
175
+ const expectedFocusOrder = [
176
+ screen.getByLabelText('Email'),
177
+ screen.getByLabelText('Password'),
178
+ screen.getByRole('button', { name: 'Mostrar contraseña' }),
179
+ screen.getByRole('checkbox', { name: 'Remember me' }),
180
+ screen.getByRole('button', { name: 'Forgot password' }),
181
+ screen.getByRole('button', { name: 'Sign in' }),
182
+ screen.getByRole('button', { name: 'Create account' }),
183
+ ]
184
+
185
+ for (const element of expectedFocusOrder) {
186
+ await user.tab()
187
+ expect(document.activeElement).toBe(element)
188
+ }
189
+ })
190
+ })