@thorprovider/blocks 0.1.3 → 1.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.
- package/CHANGELOG.md +107 -0
- package/README.md +73 -29
- package/docs/standards/data-contracts.md +13 -2
- package/package.json +26 -10
- package/scripts/fix-esm-extensions.mjs +40 -0
- package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
- package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
- package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
- package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
- package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
- package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
- package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
- package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
- package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
- package/src/blocks/e-commerce/product-list/product-card.tsx +424 -0
- package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
- package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
- package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
- package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
- package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
- package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
- package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
- package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
- package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
- package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
- package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
- package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
- package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
- package/src/blocks/marketing/features/feature.test.tsx +153 -0
- package/src/blocks/marketing/features/feature.tsx +195 -0
- package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +27 -9
- package/src/blocks/marketing/navbars/navbar-4.test.tsx +33 -2
- package/src/blocks/marketing/navbars/navbar-4.tsx +29 -11
- package/src/index.ts +77 -48
- package/src/lib/contracts-curated.test.ts +42 -24
- package/src/lib/contracts.test.ts +143 -2
- package/src/lib/contracts.ts +346 -14
- package/src/ui/button-group.tsx +1 -1
- package/src/ui/carousel.tsx +1 -1
- package/src/ui/dialog.tsx +1 -1
- package/src/ui/field.tsx +2 -2
- package/src/ui/input-group.tsx +3 -3
- package/src/ui/pagination.tsx +1 -1
- package/src/ui/sidebar.tsx +7 -7
- package/src/ui/toggle-group.tsx +1 -1
- package/tsconfig.build.json +12 -0
- package/tsup.config.ts +11 -7
- package/.turbo/turbo-build.log +0 -24
- package/.turbo/turbo-test.log +0 -47
- package/.turbo/turbo-type-check.log +0 -0
- package/dist/.tsbuildinfo +0 -1
- package/dist/contracts.d.mts +0 -1732
- package/dist/contracts.d.ts +0 -1732
- package/dist/contracts.js +0 -423
- package/dist/contracts.js.map +0 -1
- package/dist/contracts.mjs +0 -365
- package/dist/contracts.mjs.map +0 -1
- package/dist/index.d.mts +0 -3068
- package/dist/index.d.ts +0 -3068
- package/dist/index.js +0 -17834
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -17740
- package/dist/index.mjs.map +0 -1
- package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
- package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
- package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
- package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
- package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
- package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
- package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
- package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
- package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
- package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
- package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
- package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
- package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
- package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
- package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
- package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
- package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
- package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
- package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
- package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
- package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
- package/src/blocks/marketing/features/feature-1.tsx +0 -97
- package/src/blocks/marketing/features/feature-2.tsx +0 -82
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# @thorprovider/blocks
|
|
2
|
+
|
|
3
|
+
## 1.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- f011717: Consolidate the `home.benefits` feature family into one canonical `Feature` block.
|
|
8
|
+
|
|
9
|
+
- Adds `Feature` (export + `featurePropsSchema` / `FeatureProps` in the contracts surface) with a bounded `presentation` prop (`cards` | `split`, default `cards`). `cards` is the former header-badge card grid with per-item learn-more and an optional section CTA; `split` is the former split layout with the optional grayscale image column. `split` deliberately ignores `badge` and per-item learn-more; an absent or empty image `src` renders no image column (never a blank `src`).
|
|
10
|
+
- The storefront catalog declares the two presentations as the `home.benefits` variants with `legacyAliases` (`Feature1` → `cards`, `Feature2` → `split`), so existing manifests storing the numbered names keep regenerating to the same presentation; the benefits feature's `compact` / `detailed` configuration values are unchanged and map onto `cards` / `split`.
|
|
11
|
+
|
|
12
|
+
BREAKING: removes the `Feature1` and `Feature2` exports (components and their `Feature1Props`/`Feature1Item`/`Feature2Props`/`Feature2Item` types) that this canonical block supersedes. Consumers must migrate to `Feature` with the `presentation` prop.
|
|
13
|
+
|
|
14
|
+
- f011717: Consolidate the product-card family into one canonical `ProductCard` block.
|
|
15
|
+
|
|
16
|
+
- Adds `ProductCard` (export + `productCardPropsSchema` / `ProductCardSpecProps` in the contracts surface) with a bounded `presentation` prop (`classic` | `minimal` | `rich`, default `classic`). `classic` is the former `ProductCard1` structure (badge, optional rating, compare-at, add-to-cart footer, reduced-motion-safe motion); `minimal` is the former `ProductCard2` lean look; `rich` is the former `ProductCard3` overlay actions (wishlist heart with host-owned controlled state or uncontrolled visual fallback, and view-similar rendered only when a handler is supplied). The comparison-table surface of the former `ProductCard5` is deliberately retired (a listing controller, not an item cell).
|
|
17
|
+
- Items are the canonical `ProductPresentation` contract only (identity, title, optional image with alt, canonical `Money` price, optional `compareAtPrice`/`rating`/`badge`). An unresolved price renders an unavailable state with add-to-cart disabled — never a zero. Ratings, badges and crossed-out prices render only from host-supplied data; the block invents no evidence. Prices render through the canonical `formatMoney` path (no hardcoded currency symbol) and all user-facing copy is overridable with the previous Spanish defaults.
|
|
18
|
+
- Removes the numbered exports `ProductCard1`, `ProductCard2`, `ProductCard3`, `ProductCard5`, their types `ProductCard1Product` / `ProductCard1Props`, the contracts surface `productCard1PropsSchema` / `ProductCard1SpecProps`, and the `product-card-3-reaction-toggle` / `product-card-5-reaction-toggle` / `product-card-5-checkbox-check` subcomponents.
|
|
19
|
+
- Migration: supply item data in the canonical presentation contract (canonical `Money` amounts as decimal major-unit strings plus `currencyCode`). The former numeric-price props (`price: number`, `originalPrice?: number`, mandatory demo merchandising fields) are not retained; map them to `Money` at your boundary. Consumer code importing a numbered card selects the equivalent bounded presentation (`1` → `classic`, `2` → `minimal`, `3` → `rich`); the former comparison table has no replacement in this block.
|
|
20
|
+
|
|
21
|
+
- f011717: Consolidate the `home.categories` family into one canonical `ProductCategory` block.
|
|
22
|
+
|
|
23
|
+
- Adds `ProductCategory` (export + `productCategoryPropsSchema` / `ProductCategoryProps`) built on the canonical `CategoryCard` shape. It exposes a bounded `presentation` prop (`grid` | `icons` | `list`); optional per-category data (`icon` as a serializable key, `subcategories`, `featured`, `itemCount`) degrades gracefully when absent.
|
|
24
|
+
- Interaction is controlled: `onBrowse` / `onViewAll` / `onQueryChange` report from handlers with no private query state; a category count is omitted when absent or invalid and a real zero is preserved; a missing image renders a neutral placeholder, never `src=""`; no copy, currency, or demo metrics are hardcoded.
|
|
25
|
+
|
|
26
|
+
BREAKING: removes the `ProductCategory1`, `ProductCategory2`, `ProductCategory4`, and `ProductCategory5` exports (and the `product-categories/avatar-group` helper) that this canonical block supersedes. Consumers must migrate to `ProductCategory` with the `presentation` prop.
|
|
27
|
+
|
|
28
|
+
- ece154d: Consolidate the `product.overview` family into one canonical `ProductOverview` block.
|
|
29
|
+
|
|
30
|
+
- Adds `ProductOverview` (export + `productOverviewPropsSchema` / `ProductOverviewProps`) built on the canonical `ProductDetailProduct` projection and canonical `Money`. It exposes a bounded `presentation` prop (`standard` | `gallery` | `rich`); `rich` accepts optional `specifications` / `features` / `reviews` props and degrades gracefully when they are absent.
|
|
31
|
+
- Selection and quantity are controlled; `onAddToCart` emits the explicitly selected backend variant id and quantity and never substitutes the first variant. Missing price renders as unavailable, never `$0`; currency is never hardcoded.
|
|
32
|
+
|
|
33
|
+
BREAKING: removes the `ProductOverview1`, `ProductOverview2`, and `ProductOverview5` exports (and the `product-overview-5-*` subcomponents) that this canonical block supersedes. Consumers must migrate to `ProductOverview` with the `presentation` prop.
|
|
34
|
+
|
|
35
|
+
- b41cc50: Consolidate the numbered shopping-cart family into one canonical `ShoppingCart` block.
|
|
36
|
+
|
|
37
|
+
- Adds `ShoppingCart` (export + `shoppingCartPropsSchema` / `ShoppingCartSpecProps` in the contracts surface) with a bounded `presentation` prop (`compact` | `page`). `compact` is the former `ShoppingCart4` review card; `page` is the same contract with the page-level structure. The optional promo-entry affordance reports the entered code via `onApplyPromo(code)` and renders only authoritative promotion/discount data — it is hidden when the callback is absent, and the block never validates promotion codes itself. Host-supplied `recentlyViewed` suggestions replace the former `ShoppingCart2` recommendations, priced with canonical `Money`.
|
|
38
|
+
- Items are the canonical contract only: identity, title, quantity, optional image (omitted when absent, never `src=""`), and canonical `Money` amounts. Each line carries the backend-authoritative `lineTotal` (rendered verbatim; a malformed amount renders as unavailable — never a zero or free-shipping claim) and an optional `unitPrice` carried verbatim but never used for local arithmetic. `totals` come from the backend and are rendered verbatim; no local tax/shipping/threshold math, no fabricated merchandising, no hardcoded currency.
|
|
39
|
+
- Removes the numbered exports `ShoppingCart1`, `ShoppingCart2`, `ShoppingCart3`, `ShoppingCart4` (and their `ShoppingCart1CartItem`, `ShoppingCart2Props`/`ShoppingCart2Item`, `ShoppingCart3Props`/`ShoppingCart3CartItem`, `ShoppingCart4Props`/`ShoppingCart4Item` types), the contracts surface `shoppingCart4PropsSchema` / `ShoppingCart4SpecProps`, and the `shopping-cart-{1,2,3,4}-spinning-counter`, `shopping-cart-2-recommendations`, and `shopping-cart-2-item-row` subcomponents.
|
|
40
|
+
|
|
41
|
+
BREAKING: consumers importing a numbered cart must migrate to `ShoppingCart` with the `presentation` prop. The former numeric-price props (`price: number`, local `taxRate`/shipping-threshold arithmetic) are not retained; map item amounts to canonical `Money` (`{ amount, currencyCode }`) at your boundary and pass backend `totals` verbatim.
|
|
42
|
+
|
|
43
|
+
- f011717: Consolidate the `home.hero` family into one canonical `StorefrontHero` block.
|
|
44
|
+
|
|
45
|
+
- Adds `StorefrontHero` (export + `storefrontHeroPropsSchema` / `StorefrontHeroProps` / `StorefrontHeroProduct` in the contracts surface) with a bounded `presentation` prop (`compact` | `showcase` | `editorial` | `promotional`) reusing the registry's archetype intent vocabulary. `compact` is the former `StorefrontHero1` text-forward hero with product grid and editorial image seam; `showcase` the former `StorefrontHero2` search + rotating product carousel (the search input renders only when `onSearch` is supplied); `editorial` the former `StorefrontHero5` department grid with optional featured deals and store locator; `promotional` the former `StorefrontHero6` deals bar + category grid.
|
|
46
|
+
- Truthful evidence only: ratings, review counts, category/department counts, deal descriptions, and countdowns render only from host-supplied data; discount treatments render only from a real `originalPrice > price` pair (`isOnSale` is dropped); `currency` (ISO 4217) formats all amounts via `Intl.NumberFormat` — no hardcoded `$`. Rotation respects reduced motion and is presentation-internal.
|
|
47
|
+
- The storefront catalog declares `StorefrontHero` as the single `home.hero` variant with `legacyAliases` (`StorefrontHero1`/`2`/`5`/`6` → the canonical), so existing manifests storing the numbered names keep regenerating to a working composition; `archetypes.json` intents map each intent to its matching presentation.
|
|
48
|
+
|
|
49
|
+
BREAKING: removes the `StorefrontHero1`, `StorefrontHero2`, `StorefrontHero5`, and `StorefrontHero6` exports (and the `StorefrontHero1Props` type) that this canonical block supersedes. The `FeaturedProduct` type is now exported from the canonical source and its evidence fields (`brand`, `colors`, `rating`, `reviewCount`, `originalPrice`) are optional. Consumers must migrate to `StorefrontHero` with the `presentation` prop.
|
|
50
|
+
|
|
51
|
+
- dc2b11b: Split the category collection into an item cell and a listing.
|
|
52
|
+
|
|
53
|
+
- `CategoryCard` is the item cell: one `category` at a time, with the existing `grid`, `icons` and `list` presentations. It renders no section, heading, search, CTA or empty state.
|
|
54
|
+
- `CategoryGrid` is the listing: it owns the collection, `maxItems`, section copy, controlled search (list presentation), loading skeletons and the empty state, and renders `CategoryCard` for every item.
|
|
55
|
+
- BREAKING: `ProductCategory` (collection) is replaced by `CategoryGrid`; the single-item surface is `CategoryCard`. `ProductCategoryProps` is replaced by `CategoryGridProps` / `CategoryCardProps`.
|
|
56
|
+
|
|
57
|
+
Migration: replace `<ProductCategory categories={items} ... />` with `<CategoryGrid categories={items} ... />`. For a single category, render `<CategoryCard category={item} ... />`.
|
|
58
|
+
|
|
59
|
+
- 3e43eb0: BREAKING: `ProductCard` is now an item cell and a new `ProductGrid` listing owns the collection.
|
|
60
|
+
|
|
61
|
+
- `ProductCard` (`packages/blocks/src/blocks/e-commerce/product-list/product-card.tsx`) now takes a single `item: ProductPresentation` (plus `presentation?` and `wishlisted?`) and renders exactly one cell — no section wrapper, no section heading, no grid, and no item cap. The `items`, `heading`, and `maxItems` props and the `wishlistedIds` collection prop are removed; `wishlisted` is now a single host-resolved boolean.
|
|
62
|
+
- The serializable contract `productCardPropsSchema` / `ProductCardSpecProps` changes accordingly (`{ item, presentation?, wishlisted? }`), still without callbacks or label strings.
|
|
63
|
+
- Adds `ProductGrid` (export + `productGridPropsSchema` / `ProductGridSpecProps`) as the canonical listing: it takes `items: ProductPresentation[]`, an optional `presentation`, `heading`, `maxItems`, `wishlistedIds`, and owns the `loading` (skeletons), `empty` (overridable copy), and ready states. It renders `ProductCard` cells (single source of cell markup) and reports actions through the same callbacks. `error` remains the host's responsibility.
|
|
64
|
+
- Migration: pass a listing's data to `ProductGrid` with `items`, `heading`, and `maxItems`; pass a single item to `ProductCard` via `item`. Resolve rich-presentation wishlist state per cell through `wishlistedIds` on the grid or `wishlisted` on the cell. The delimited showcase `ProductShowcase1` is unchanged.
|
|
65
|
+
|
|
66
|
+
- b41cc50: Require an image for the `Feature` block's `split` presentation.
|
|
67
|
+
|
|
68
|
+
- `featureSplitSpecSchema` now requires `presentation: 'split'` and a non-empty, trimmed `image.src`; `featureCardsSpecSchema` keeps `image` out of its contract. `featurePropsSchema` is the union of both branches, so TypeScript narrows `image` only after checking `presentation`.
|
|
69
|
+
- The component throws a configuration error when an untyped caller renders `split` without a non-empty image, instead of silently dropping the media column. `cards` is unchanged.
|
|
70
|
+
- Editorial fallbacks belong to the consumer, not the block library. The showcase now owns `public/demo/editorial-fallback.svg`; the storefront template seeds a neutral image in user-owned `content.ts`.
|
|
71
|
+
|
|
72
|
+
Migration: pass `{ image: { src, alt } }` when using `presentation="split"`, or switch to `presentation="cards"` if the section is icon-only. Replace the storefront's neutral seed with real editorial imagery.
|
|
73
|
+
|
|
74
|
+
### Minor Changes
|
|
75
|
+
|
|
76
|
+
- 80d4137: Add optional, backward-compatible brand customization to existing surfaces (M5):
|
|
77
|
+
|
|
78
|
+
- `@thorprovider/blocks`: `Navbar4` and `FooterSection1` accept an optional wordmark accent `{ text, className }` (trailing segment of the brand name) and an optional surface `className`; a supplied surface `className` overrides the default surface paint (default background/backdrop not emitted) so a branded surface paints correctly. `StorefrontHero1` accepts an optional editorial `heroImage { src, alt }` rendered with a native, guarded image. `ProductCategory2` accepts an optional `itemCount`: an unknown count is omitted (no fabricated `0`), a real `0` is preserved, and non-finite/negative values render no count. All props are optional and omit to the current rendering, so existing consumers are unchanged; no new block type or variant is added, and the blocks stay framework-, backend-, and data-free.
|
|
79
|
+
- `@thorprovider/storefront`: the scaffold generates a user-owned `lib/<name>/shell/{content,props}.ts` seam (brand accents, surface classes, footer description/links, newsletter) that the derived spec composes last, so `storefront sync` regenerates only derived wiring and preserves user-owned edits. The scaffold ships no newsletter submit callback, so the newsletter is omitted by default rather than rendering a fake form; default footer copy makes no delivery/shipping promise and links only to routes the storefront serves (no assumed route). No new identity CLI command is added — `create --site-name` already covers the site name.
|
|
80
|
+
|
|
81
|
+
This changeset adds release metadata only; it does not bump versions or publish packages.
|
|
82
|
+
|
|
83
|
+
- 3b3c24c: Add validated single-store commerce contracts, explicit real/mock policy, directed variant pricing, persistent guest carts, backend-authoritative totals, manual TEST checkout, and capability-authorized receipt reconciliation in generated storefronts.
|
|
84
|
+
|
|
85
|
+
Breaking migration: `@thorprovider/types` is now entirely type-only. Import `SupportedProviderType`, `PROVIDER_METADATA`, `isSupportedProviderType`, `getProviderMetadata`, and `StorefrontConfigError` from `@thorprovider/adapters` instead. The pre-1.0 curated blocks/runtime APIs also change: product detail uses canonical options/variants/Money and explicit variant selection; confirmation totals and lines use Money; `formatMoney` returns `null` for unavailable values. Real-mode failures no longer silently return mock data.
|
|
86
|
+
|
|
87
|
+
The local packed MVP candidate retains its exact current version pins until a separate release updates the compatible toolset. This changeset does not publish packages or establish public-sales readiness.
|
|
88
|
+
|
|
89
|
+
- b41cc50: Deliver the global shopper account experience (M5, change `storefront-navbar-account-menu`):
|
|
90
|
+
|
|
91
|
+
- `@thorprovider/blocks`: `Navbar4` renders a session-driven account affordance from consumer props only. An authenticated `user` (optional `avatar`) with `accountMenuItems` gets an account menu (identity + configured items + consumer-supplied sign-out); no `user` with a configured `accountHref` gets a signed-out entry point on desktop and mobile; neither configured renders no account affordance or empty container. Optional `accountPending`/`accountUnavailable` drive a pending/disabled state and an accessible unavailable/retry state (`onAccountRetry`) without presenting either as anonymous, and sign-out is suppressed while pending. Absent avatar uses the initials fallback (no empty image `src`). All callbacks stay consumer bindings outside the serializable JSON prop schema; the block never authenticates.
|
|
92
|
+
- `@thorprovider/storefront`: a mounted `customer-auth` resolves the shopper server-side per request and seeds an allowlisted display identity (`id`, `email`, `firstName`, `lastName`; the auth token stays in its httpOnly cookie). One shared client lifecycle (`account-lifecycle.client.tsx`) owns identity, guarded claims, login/register/logout, and retry with no identity fetch on mount or navigation; it reconciles same-store tabs through non-secret store-scoped signals (`BroadcastChannel` with a storage-event fallback) and on foreground/resume, discards stale generations, and shows a neutral pending state instead of stale identity/orders or a fake sign-out. `/account` is the only account surface — sign-in, registration, identity, sign-out, and real paginated orders + detail (route-scoped commerce, no direct HTTP); no address book/profile/wishlist/social/recovery/cart-merge. Account wiring is delivered through derived files (`account-session*.ts`, `account-lifecycle/account-boundary/account-mutation/account-reconcile.client.*`, `account-private.core.ts`, `account-user.core.ts`, `projection-ownership*.ts`, `registry.tsx`, `schema.ts`, `spec/index.ts`); user-owned files (`views/*/{schema,content,props,data}.ts`, `mappers.ts`, `icon-map.ts`, `shell/**`) survive `sync`. `storefront feature add|remove customer-auth` toggles the `/account` route, session reads, listeners, and navbar chrome together; without the feature the generated registry carries no account-lifecycle/account-reconcile reference and reads no session.
|
|
93
|
+
- `@thorprovider/adapters`: `getCurrentCustomer()` resolves `null` only for a confirmed unauthenticated response (HTTP 401 / `"Unauthorized"`) and throws a typed `ProviderAPIError` (`GET_CURRENT_CUSTOMER_FAILED`) on operational failures (network, timeout, 5xx), so a host can distinguish an anonymous shopper from a backend outage and render the unavailable state instead of a false sign-out. `isAuthenticated()` delegates to it and can reject. Signature and framework-agnostic boundary are unchanged.
|
|
94
|
+
|
|
95
|
+
This changeset adds release metadata only; it does not bump versions or publish packages.
|
|
96
|
+
|
|
97
|
+
- ece154d: Gate the `ProductOverview` wishlist affordance on a wired handler.
|
|
98
|
+
|
|
99
|
+
- `ProductOverview` renders its wishlist button only when `onToggleWishlist` is supplied; an unwired button is a dead control and is no longer rendered in any presentation (`standard`, `gallery`, `rich`).
|
|
100
|
+
- The storefront product template and the demo/electrostore registries drop their no-op `onToggleWishlist` wiring, so a storefront without wishlist backend support no longer shows a non-functional heart.
|
|
101
|
+
|
|
102
|
+
### Patch Changes
|
|
103
|
+
|
|
104
|
+
- ece154d: Align the `Breadcrumb` block with the page content: the trail now renders inside the shared responsive container (`mx-auto w-full max-w-7xl px-4 py-4 sm:px-6 lg:px-8`) that sibling blocks use, instead of sitting flush against the viewport edge. The container is merged with the incoming `className` via `cn`, so a custom `className` can still override it.
|
|
105
|
+
- Updated dependencies [23f876f]
|
|
106
|
+
- Updated dependencies [3b3c24c]
|
|
107
|
+
- @thorprovider/types@6.0.0
|
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-
|
|
4
|
+
lastReviewed: "2026-10-17"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# @thorprovider/blocks
|
|
@@ -30,7 +30,7 @@ src/
|
|
|
30
30
|
│ └── hero-sections/hero-section-1.tsx
|
|
31
31
|
└── e-commerce/
|
|
32
32
|
├── product-list/product-card-1.tsx
|
|
33
|
-
└── storefront-hero/storefront-hero
|
|
33
|
+
└── storefront-hero/storefront-hero.tsx
|
|
34
34
|
```
|
|
35
35
|
|
|
36
36
|
Blocks follow `src/blocks/<category>/<section>/<block>.tsx`. Each block co-locates its own prop types unless a contract is shared by two or more blocks.
|
|
@@ -46,8 +46,7 @@ Blocks follow `src/blocks/<category>/<section>/<block>.tsx`. Each block co-locat
|
|
|
46
46
|
| `Banner1` | Marketing | Announcement banner (bar or pill) with dismiss |
|
|
47
47
|
| `HeroSection1` | Marketing | Hero with badge, headline, CTA, reviewer avatars, rating and image |
|
|
48
48
|
| `CtaSection1` | Marketing | Full-width call-to-action with badge, headline and two CTAs |
|
|
49
|
-
| `
|
|
50
|
-
| `Feature2` | Marketing | Split features with image |
|
|
49
|
+
| `Feature` | Marketing | Canonical benefits section: `cards` (icon feature grid) or `split` (editorial split with a required image) presentation |
|
|
51
50
|
| `Testimonial1` | Marketing | Testimonial carousel with rating stars |
|
|
52
51
|
| `Testimonial2` | Marketing | Bento testimonials grid |
|
|
53
52
|
| `FaqBlock1` | Marketing | FAQ accordion (single or multiple open) |
|
|
@@ -56,33 +55,22 @@ Blocks follow `src/blocks/<category>/<section>/<block>.tsx`. Each block co-locat
|
|
|
56
55
|
| `LogoCloud1` | Marketing | Logo cloud with skeleton fallback |
|
|
57
56
|
| `FooterSection1` | Marketing | Footer with brand, link columns and newsletter |
|
|
58
57
|
| `FooterSection3` | Marketing | Newsletter + six-column footer with social links |
|
|
59
|
-
| `
|
|
60
|
-
| `ProductCard2` | E-commerce | Clean three-up grid with ratings |
|
|
61
|
-
| `ProductCard3` | E-commerce | Deals grid with badges, low-stock and wishlist |
|
|
62
|
-
| `ProductCard5` | E-commerce | Table-style comparison with sorting and filtering |
|
|
58
|
+
| `ProductCard` | E-commerce | Canonical product card (item cell of a listing); `presentation` `classic`/`minimal`/`rich`, canonical `ProductPresentation` items with `Money` prices, evidence-only merchandising |
|
|
63
59
|
| `ProductShowcase1` | E-commerce | Combined grid-and-carousel of product cards with manual paging |
|
|
64
|
-
| `
|
|
60
|
+
| `ProductOverview` | E-commerce | Canonical product detail; bounded `presentation` `standard`/`gallery`/`rich` (rich adds optional specs/features/reviews tabs) |
|
|
65
61
|
| `ProductQuickView` | E-commerce | Quick-view dialog with gallery, variants and quantity |
|
|
66
62
|
| `ReviewRating1` | E-commerce | Review list with rating summary |
|
|
67
63
|
| `ReviewRating2` | E-commerce | Rating summary with distribution and review carousel |
|
|
68
|
-
| `
|
|
69
|
-
| `ProductCategory2` | E-commerce | Image-based category cards |
|
|
70
|
-
| `ProductCategory4` | E-commerce | Icon grid with customer/product counts |
|
|
71
|
-
| `ProductCategory5` | E-commerce | Category list with search and subcategories |
|
|
64
|
+
| `ProductCategory` | E-commerce | Canonical category block; bounded `presentation` `grid` (image cards) / `icons` (compact icon grid) / `list` (rows with subcategory chips and optional controlled search) |
|
|
72
65
|
| `CategoryFilter1` | E-commerce | Category filter bar |
|
|
73
66
|
| `CategoryFilter2` | E-commerce | Filter sidebar with mobile sheet |
|
|
74
|
-
| `
|
|
75
|
-
| `ShoppingCart2` | E-commerce | Cart with promo code and shipping |
|
|
76
|
-
| `ShoppingCart4` | E-commerce | Cart review with backend-authoritative totals; `presentation` `compact` (collapsible card) or `page` (full-width review) |
|
|
67
|
+
| `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) |
|
|
77
68
|
| `CheckoutForm1` | E-commerce | Essential checkout fields |
|
|
78
69
|
| `CheckoutForm2` | E-commerce | Single-page checkout with order summary beside |
|
|
79
70
|
| `CheckoutConfirmation` | E-commerce | Order confirmation with fulfilment timeline |
|
|
80
71
|
| `OrderHistory1` | E-commerce | Orders list |
|
|
81
72
|
| `OrderHistory2` | E-commerce | Order cards with carousel items |
|
|
82
|
-
| `
|
|
83
|
-
| `StorefrontHero2` | E-commerce | Search field above a trending product carousel |
|
|
84
|
-
| `StorefrontHero5` | E-commerce | Departments grid with featured deals |
|
|
85
|
-
| `StorefrontHero6` | E-commerce | Rotating deals bar beside category cards |
|
|
73
|
+
| `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) |
|
|
86
74
|
| `AppShell1` | Application | Application shell with navigation slots |
|
|
87
75
|
| `LoginPage1` | Application | Login card with remember-me and social buttons |
|
|
88
76
|
| `LoginPage2` | Application | Login with prominent social options |
|
|
@@ -90,7 +78,7 @@ Blocks follow `src/blocks/<category>/<section>/<block>.tsx`. Each block co-locat
|
|
|
90
78
|
| `ForgotPasswordPage1` | Application | Four-step password recovery wizard |
|
|
91
79
|
| `Verify1` | Application | Two-factor verification (OTP or backup code) |
|
|
92
80
|
|
|
93
|
-
Interactions are exposed as callbacks (e.g. `onAddToCart`, `onBuyNow`, `onSelectColor`).
|
|
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.
|
|
94
82
|
|
|
95
83
|
## Optional brand props
|
|
96
84
|
|
|
@@ -104,7 +92,7 @@ the block's current output unchanged; no demo content is added.
|
|
|
104
92
|
segment of `logoText`, the wordmark renders unchanged.
|
|
105
93
|
- `FooterSection1` — `brandAccent?: { text: string; className?: string }` applies
|
|
106
94
|
the same trailing-segment accent to `brandName`.
|
|
107
|
-
- `
|
|
95
|
+
- `StorefrontHero` — `heroImage?: { src: string; alt: string }` renders a native
|
|
108
96
|
`<img>` when `src` is a non-empty string, and nothing when the prop is absent
|
|
109
97
|
or `src` is blank (never an empty `src`). The product grid is unaffected.
|
|
110
98
|
|
|
@@ -132,6 +120,24 @@ supplied surface. `Navbar4` adapts its primary nav links: they inherit the
|
|
|
132
120
|
surface text color and add `hover:text-foreground` so `hover:bg-muted` never
|
|
133
121
|
paints white-on-light. No magic colors are introduced by the block.
|
|
134
122
|
|
|
123
|
+
## Navbar4 account affordance
|
|
124
|
+
|
|
125
|
+
`Navbar4` renders a **session-driven** account affordance exclusively from
|
|
126
|
+
consumer-supplied props — it never resolves a session or authenticates itself.
|
|
127
|
+
Given a `user` (`StoreUser`; `avatar` is optional) and `accountMenuItems`, it
|
|
128
|
+
renders an authenticated account menu with the identity, the configured items,
|
|
129
|
+
and the consumer's sign-out action on desktop and mobile. Given no `user` but a
|
|
130
|
+
configured `accountHref`, it renders a signed-out account entry point on both
|
|
131
|
+
viewports; with neither configured it renders no account affordance and no empty
|
|
132
|
+
container. An absent `avatar` uses the initials fallback and emits no empty image
|
|
133
|
+
`src`. Optional `accountPending` and `accountUnavailable` props drive a
|
|
134
|
+
pending/disabled state and an accessible unavailable/retry state (via
|
|
135
|
+
`onAccountRetry`) without presenting either as an anonymous session, and sign-out
|
|
136
|
+
is suppressed while pending. All callbacks (`onSignOut`, `onAccountRetry`,
|
|
137
|
+
`onHelp`) are consumer bindings kept outside the serializable JSON prop schema;
|
|
138
|
+
the sign-out action is supplied by the consumer, so the block never performs
|
|
139
|
+
authentication.
|
|
140
|
+
|
|
135
141
|
## Footer newsletter
|
|
136
142
|
|
|
137
143
|
`FooterSection1` renders the interactive newsletter form (input + submit button)
|
|
@@ -144,15 +150,16 @@ paints white-on-light. No magic colors are introduced by the block.
|
|
|
144
150
|
The cart is presented in two shapes that share one data contract, so a change in
|
|
145
151
|
either is visible in the other.
|
|
146
152
|
|
|
147
|
-
- `
|
|
153
|
+
- `ShoppingCart` is the cart block. Its optional `presentation` prop selects
|
|
148
154
|
`'compact'` (default: a collapsible `max-w-md` card with an internal scroller —
|
|
149
155
|
the navbar lateral) or `'page'` (always expanded, full-width `max-w-3xl`, no
|
|
150
156
|
collapse control, ordinary page scrolling — the `/cart` review route).
|
|
151
157
|
Omitting `presentation` renders the compact card byte-identically to before.
|
|
152
|
-
- Line amounts
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
- Line amounts render the authoritative `lineTotal: Money` verbatim (backend
|
|
159
|
+
line total, never a local `unitPrice × quantity` computation); a malformed
|
|
160
|
+
amount renders the `amountUnavailableLabel` (never a fabricated zero or a
|
|
161
|
+
currency fallback). An optional `unitPrice: Money` is carried verbatim but
|
|
162
|
+
never used for any local arithmetic.
|
|
156
163
|
- `totalsState` (`'idle' | 'pending' | 'error'`) plus `onRetryTotals` make the
|
|
157
164
|
totals controlled: pending retains the last confirmed totals and disables
|
|
158
165
|
conflicting mutations; error shows the error label and a retry.
|
|
@@ -160,6 +167,13 @@ either is visible in the other.
|
|
|
160
167
|
by backend line id) and `onRemove(id)` removes by the same id.
|
|
161
168
|
`checkoutUnavailable` hides the checkout action (e.g. no checkout installed)
|
|
162
169
|
and `checkoutDisabled` disables it (e.g. pending or invalid totals).
|
|
170
|
+
- Promo entry is an optional affordance: with `onApplyPromo(code)` it renders an
|
|
171
|
+
input that reports the entered code and shows only the authoritative applied
|
|
172
|
+
promotion/discount already present in `totals`; without the callback it is not
|
|
173
|
+
rendered, and the block never validates or resolves a code itself.
|
|
174
|
+
`onApplyPromo` never gates `recentlyViewed`, an optional host-supplied
|
|
175
|
+
suggestion list (name, optional image, optional `Money` price) rendered only
|
|
176
|
+
when supplied.
|
|
163
177
|
|
|
164
178
|
`Navbar4` owns the lateral. Pass `cartOpen` + `onCartOpenChange` to control the
|
|
165
179
|
sheet's visibility from host state (omit both to keep the legacy uncontrolled
|
|
@@ -170,6 +184,32 @@ canonical `lineTotal` (never the placeholder numeric price), plus
|
|
|
170
184
|
serve, and a `state` (`'idle' | 'pending' | 'error'`) with retry for read/mutation
|
|
171
185
|
feedback.
|
|
172
186
|
|
|
187
|
+
## Product card (canonical)
|
|
188
|
+
|
|
189
|
+
`ProductCard` is the item cell of a product listing and the canonical member of
|
|
190
|
+
the product-card family. Its items are `ProductPresentation` only (canonical
|
|
191
|
+
`Money`): a missing price renders the `unavailablePriceLabel` with add-to-cart
|
|
192
|
+
disabled — never a zero amount — and optional image, rating, badge, and
|
|
193
|
+
compare-at price are omitted when absent (no empty `src`, no empty stars, no
|
|
194
|
+
empty chip). A compare-at price renders only when it is valid and strictly
|
|
195
|
+
higher than the selling price.
|
|
196
|
+
|
|
197
|
+
The bounded `presentation` option selects the structure — `classic` (boxed card
|
|
198
|
+
with an add-to-cart footer), `minimal` (lean image-forward tile with an
|
|
199
|
+
icon-only cart action), `rich` (overlay actions: wishlist heart and
|
|
200
|
+
view-similar) — while the data and action contract stays identical. Ratings,
|
|
201
|
+
badges, and scarcity copy render only from host-supplied data (the `badge`
|
|
202
|
+
copy and `compareAtPrice` are the evidence channels); the block never invents
|
|
203
|
+
stars, chips, or urgency.
|
|
204
|
+
|
|
205
|
+
The wishlist heart is host-owned: pass `wishlistedIds` to control it, and
|
|
206
|
+
`onToggleWishlist(item, nextState)` receives every toggle from the click
|
|
207
|
+
handler (never from an effect). Without host state the heart keeps an
|
|
208
|
+
uncontrolled visual fallback and still reports. `onViewSimilar` renders its
|
|
209
|
+
affordance only when supplied. Prices render through the canonical
|
|
210
|
+
`formatMoney` path (no hardcoded currency), and every label is overridable
|
|
211
|
+
with Spanish defaults (`Agregar al carrito`, `Precio no disponible`, …).
|
|
212
|
+
|
|
173
213
|
## Checkout blocks (controlled)
|
|
174
214
|
|
|
175
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.
|
|
@@ -229,11 +269,11 @@ unique by contract, not by convention. See
|
|
|
229
269
|
rules and the review checklist.
|
|
230
270
|
|
|
231
271
|
Optional numeric projections are omitted rather than fabricated: e.g.
|
|
232
|
-
`
|
|
272
|
+
`ProductCategoryItem.itemCount` renders a count only when it is a supplied
|
|
233
273
|
finite non-negative number (a real `0` is preserved); an unknown count renders
|
|
234
274
|
no count text, never a fabricated `0`.
|
|
235
275
|
|
|
236
|
-
Product presentations (`
|
|
276
|
+
Product presentations (`ProductGrid`, `ProductShowcase1`) accept either the
|
|
237
277
|
legacy numeric-price props or a canonical `ProductPresentation` (`price`/
|
|
238
278
|
`compareAtPrice` as `Money`, `{ amount: string, currencyCode: string }` in
|
|
239
279
|
decimal major units). The canonical path formats with the store currency and
|
|
@@ -242,6 +282,10 @@ locale, renders a truthful "unavailable" state for an unresolved price (never
|
|
|
242
282
|
valid and strictly higher. Optional rating/badge/image appear only when
|
|
243
283
|
supplied; a missing image is omitted, never `src=""`.
|
|
244
284
|
|
|
285
|
+
The canonical `ProductCard` accepts only the canonical `ProductPresentation`
|
|
286
|
+
contract — the legacy numeric-price props of the numbered cards are retired
|
|
287
|
+
(migrate item data to canonical `Money`).
|
|
288
|
+
|
|
245
289
|
## Block interaction contracts
|
|
246
290
|
|
|
247
291
|
State and events between a block, its host registry, and the store must follow
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: "Block data contracts"
|
|
3
3
|
purpose: "Warn block authors and hosts about the two boundary data-hygiene rules that prevent empty-src and duplicate-key console errors"
|
|
4
|
-
lastReviewed: "2026-
|
|
4
|
+
lastReviewed: "2026-10-17"
|
|
5
5
|
type: standards
|
|
6
6
|
tags: [blocks, contracts, data-boundary, images, react-keys]
|
|
7
7
|
---
|
|
@@ -60,6 +60,16 @@ Even with correct boundaries, blocks should render
|
|
|
60
60
|
`src={value || undefined}` for every image prop. This is a cheap invariant
|
|
61
61
|
that makes the whole catalog immune to one misbehaving host.
|
|
62
62
|
|
|
63
|
+
## Canonical product card: evidence comes from the host
|
|
64
|
+
|
|
65
|
+
`ProductCard` renders ratings, badges, discount labels, and stock claims only
|
|
66
|
+
from host-supplied data (`rating`, `badge` copy, `compareAtPrice`); it never
|
|
67
|
+
invents stars, chips, scarcity copy, or crossed-out prices, and a compare-at
|
|
68
|
+
price renders only when it is valid and strictly higher. Boundaries must not
|
|
69
|
+
compensate by fabricating evidence: if the host lacks a rating, the card
|
|
70
|
+
renders none — do not serialize a default `5` or a stock-urgency string to
|
|
71
|
+
"fill" the UI.
|
|
72
|
+
|
|
63
73
|
## Review checklist (boundary code)
|
|
64
74
|
|
|
65
75
|
- [ ] No `?? ''` or `|| ''` on visual fields (image, avatar, icon) in mappers/transforms.
|
|
@@ -67,4 +77,5 @@ that makes the whole catalog immune to one misbehaving host.
|
|
|
67
77
|
- [ ] Optional fields emit `undefined`, never `""`.
|
|
68
78
|
- [ ] Arrays passed as React children have unique keys guaranteed by contract or deduped at the boundary.
|
|
69
79
|
- [ ] Concatenated arrays (categories + links) are deduped before render.
|
|
70
|
-
- [ ] Every `<img>`/`<AvatarImage>` in the block uses `src={expr || undefined}`.
|
|
80
|
+
- [ ] Every `<img>`/`<AvatarImage>` in the block uses `src={expr || undefined}`.
|
|
81
|
+
- [ ] No fabricated ratings, badges, stock claims, or crossed-out prices are serialized to fill absent evidence.
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thorprovider/blocks",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.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",
|
|
7
|
+
"sideEffects": [
|
|
8
|
+
"**/*.css",
|
|
9
|
+
"./styles.css"
|
|
10
|
+
],
|
|
7
11
|
"exports": {
|
|
8
12
|
".": {
|
|
9
13
|
"types": "./dist/index.d.ts",
|
|
@@ -13,16 +17,23 @@
|
|
|
13
17
|
"default": "./dist/index.mjs"
|
|
14
18
|
},
|
|
15
19
|
"./contracts": {
|
|
16
|
-
"types": "./dist/contracts.d.ts",
|
|
17
|
-
"development": "./dist/contracts.mjs",
|
|
18
|
-
"import": "./dist/contracts.mjs",
|
|
19
|
-
"require": "./dist/contracts.js",
|
|
20
|
-
"default": "./dist/contracts.mjs"
|
|
20
|
+
"types": "./dist/lib/contracts.d.ts",
|
|
21
|
+
"development": "./dist/lib/contracts.mjs",
|
|
22
|
+
"import": "./dist/lib/contracts.mjs",
|
|
23
|
+
"require": "./dist/lib/contracts.js",
|
|
24
|
+
"default": "./dist/lib/contracts.mjs"
|
|
21
25
|
},
|
|
22
|
-
"./styles.css": "./styles.css"
|
|
26
|
+
"./styles.css": "./styles.css",
|
|
27
|
+
"./*": {
|
|
28
|
+
"types": "./dist/*.d.ts",
|
|
29
|
+
"development": "./dist/*.mjs",
|
|
30
|
+
"import": "./dist/*.mjs",
|
|
31
|
+
"require": "./dist/*.js",
|
|
32
|
+
"default": "./dist/*.mjs"
|
|
33
|
+
}
|
|
23
34
|
},
|
|
24
35
|
"scripts": {
|
|
25
|
-
"build": "tsup",
|
|
36
|
+
"build": "tsup && tsc -p tsconfig.build.json && node scripts/fix-esm-extensions.mjs",
|
|
26
37
|
"dev": "tsup --watch",
|
|
27
38
|
"type-check": "tsc --noEmit",
|
|
28
39
|
"test": "vitest run --config vitest.config.ts",
|
|
@@ -40,7 +51,7 @@
|
|
|
40
51
|
"license": "MIT",
|
|
41
52
|
"dependencies": {
|
|
42
53
|
"@base-ui/react": "^1.8.0",
|
|
43
|
-
"@thorprovider/types": "
|
|
54
|
+
"@thorprovider/types": "workspace:^",
|
|
44
55
|
"class-variance-authority": "^0.7.1",
|
|
45
56
|
"clsx": "^2.1.1",
|
|
46
57
|
"cn": "^0.4.0",
|
|
@@ -54,10 +65,15 @@
|
|
|
54
65
|
"zod": "^3.25.0"
|
|
55
66
|
},
|
|
56
67
|
"devDependencies": {
|
|
68
|
+
"@testing-library/dom": "^10",
|
|
69
|
+
"@testing-library/react": "^16",
|
|
70
|
+
"@testing-library/user-event": "^14",
|
|
71
|
+
"@types/jsdom": "^30",
|
|
57
72
|
"@types/react": "^19",
|
|
58
73
|
"@types/react-dom": "^19",
|
|
74
|
+
"jsdom": "^30.1.3",
|
|
59
75
|
"tsup": "^8.0.0",
|
|
60
76
|
"typescript": "^5.0.0",
|
|
61
77
|
"vitest": "^3.2.4"
|
|
62
78
|
}
|
|
63
|
-
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { readdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { join, extname } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const distDir = fileURLToPath(new URL("../dist/", import.meta.url));
|
|
6
|
+
|
|
7
|
+
const RELATIVE = /^\.{1,2}\//;
|
|
8
|
+
const HAS_EXT = /\.(mjs|cjs|js|jsx|ts|tsx|css|json|svg|png|webp|jpg|jpeg|gif|woff2?)$/;
|
|
9
|
+
|
|
10
|
+
const SPECIFIER =
|
|
11
|
+
/(\bfrom\s*|\bimport\s*\(|\brequire\s*\(|\bimport\s+)(["'])(\.{1,2}\/[^"']*?)\2/g;
|
|
12
|
+
|
|
13
|
+
async function* walk(dir) {
|
|
14
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
15
|
+
const full = join(dir, entry.name);
|
|
16
|
+
if (entry.isDirectory()) yield* walk(full);
|
|
17
|
+
else yield full;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function fixFile(file) {
|
|
22
|
+
const ext = extname(file);
|
|
23
|
+
if (ext !== ".mjs" && ext !== ".js") return 0;
|
|
24
|
+
const target = ext === ".mjs" ? ".mjs" : ".js";
|
|
25
|
+
const source = await readFile(file, "utf8");
|
|
26
|
+
let changed = 0;
|
|
27
|
+
const out = source.replace(SPECIFIER, (match, prefix, quote, spec) => {
|
|
28
|
+
if (!RELATIVE.test(spec) || HAS_EXT.test(spec)) return match;
|
|
29
|
+
changed += 1;
|
|
30
|
+
return `${prefix}${quote}${spec}${target}${quote}`;
|
|
31
|
+
});
|
|
32
|
+
if (changed > 0) await writeFile(file, out);
|
|
33
|
+
return changed;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let total = 0;
|
|
37
|
+
for await (const file of walk(distDir)) {
|
|
38
|
+
total += await fixFile(file);
|
|
39
|
+
}
|
|
40
|
+
console.log(`fix-esm-extensions: rewrote ${total} relative specifier(s)`);
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
|
+
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { render, screen } from '@testing-library/react'
|
|
5
|
+
import userEvent from '@testing-library/user-event'
|
|
6
|
+
import { CategoryCard } from './category-card'
|
|
7
|
+
import { CategoryGrid } from './product-category-grid'
|
|
8
|
+
import type { ProductCategoryItem } from '../../../lib/contracts'
|
|
9
|
+
|
|
10
|
+
beforeAll(() => {
|
|
11
|
+
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const category: ProductCategoryItem = {
|
|
15
|
+
id: 'pcat_herramientas',
|
|
16
|
+
handle: 'herramientas',
|
|
17
|
+
name: 'Herramientas',
|
|
18
|
+
description: 'Taladros y accesorios',
|
|
19
|
+
image: '/products/drill.png',
|
|
20
|
+
itemCount: 12,
|
|
21
|
+
trending: true,
|
|
22
|
+
featured: true,
|
|
23
|
+
subcategories: ['Taladros', 'Accesorios'],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const categories: ProductCategoryItem[] = [
|
|
27
|
+
category,
|
|
28
|
+
{ ...category, id: 'pcat_pintura', handle: 'pintura', name: 'Pintura', image: undefined },
|
|
29
|
+
{ ...category, id: 'pcat_jardin', handle: 'jardin', name: 'Jardín' },
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
const card = (extra: Record<string, unknown> = {}) =>
|
|
33
|
+
renderToStaticMarkup(<CategoryCard category={category} {...extra} />)
|
|
34
|
+
|
|
35
|
+
const grid = (extra: Record<string, unknown> = {}) =>
|
|
36
|
+
renderToStaticMarkup(<CategoryGrid categories={categories} {...extra} />)
|
|
37
|
+
|
|
38
|
+
describe('CategoryCard — item cell', () => {
|
|
39
|
+
it('renders a single category without section chrome', () => {
|
|
40
|
+
const html = card()
|
|
41
|
+
expect(html).toContain('Herramientas')
|
|
42
|
+
expect(html).not.toContain('<section')
|
|
43
|
+
expect(html).not.toContain('<h2')
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('renders the image for the grid presentation and omits it for icons', () => {
|
|
47
|
+
expect(card()).toContain('/products/drill.png')
|
|
48
|
+
expect(card({ presentation: 'icons' })).not.toContain('<img')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('keeps counts honest (zero and unknown are not invented)', () => {
|
|
52
|
+
expect(card()).toContain('12')
|
|
53
|
+
expect(renderToStaticMarkup(<CategoryCard category={{ ...category, itemCount: undefined }} />)).not.toContain('items</span>')
|
|
54
|
+
expect(renderToStaticMarkup(<CategoryCard category={{ ...category, itemCount: 0 }} />)).not.toContain('items</span>')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('only exposes browse when a handler exists', () => {
|
|
58
|
+
expect(card({ browseLabel: 'Ver' })).not.toContain('Ver Herramientas')
|
|
59
|
+
const html = card({ browseLabel: 'Ver', onBrowse: vi.fn() })
|
|
60
|
+
expect(html).toContain('aria-label="Ver Herramientas"')
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('renders the list cell with description, chips and featured badge', () => {
|
|
64
|
+
const html = card({ presentation: 'list', featuredLabel: 'Destacada', itemsLabel: 'productos' })
|
|
65
|
+
expect(html).toContain('Taladros y accesorios')
|
|
66
|
+
expect(html).toContain('Taladros')
|
|
67
|
+
expect(html).toContain('Destacada')
|
|
68
|
+
expect(html).toContain('productos')
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('CategoryGrid — listing', () => {
|
|
73
|
+
it('renders one cell per category', () => {
|
|
74
|
+
const html = grid()
|
|
75
|
+
expect(html).toContain('Herramientas')
|
|
76
|
+
expect(html).toContain('Pintura')
|
|
77
|
+
expect(html).toContain('Jardín')
|
|
78
|
+
expect(html.match(/data-slot="card"/g) ?? []).toHaveLength(3)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('caps the rendered collection with maxItems', () => {
|
|
82
|
+
const html = grid({ maxItems: 2 })
|
|
83
|
+
expect(html).toContain('Herramientas')
|
|
84
|
+
expect(html).toContain('Pintura')
|
|
85
|
+
expect(html).not.toContain('Jardín')
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('owns its loading state with skeletons and no cells', () => {
|
|
89
|
+
const html = grid({ loading: true, skeletonCount: 3 })
|
|
90
|
+
expect(html).toContain('aria-busy="true"')
|
|
91
|
+
expect(html).not.toContain('data-slot="card"')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('owns its empty state', () => {
|
|
95
|
+
expect(renderToStaticMarkup(<CategoryGrid categories={[]} emptyLabel="Sin categorías" />)).toContain('Sin categorías')
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('renders the search control only in the list presentation', () => {
|
|
99
|
+
expect(grid({ presentation: 'grid', onQueryChange: vi.fn() })).not.toContain('type="search"')
|
|
100
|
+
expect(grid({ presentation: 'list', onQueryChange: vi.fn(), searchPlaceholder: 'Buscar' })).toContain('type="search"')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('renders view-all only outside the grid and the section cta only outside the list', () => {
|
|
104
|
+
const withBoth = { cta: { label: 'Comprar', href: '/tienda' }, onViewAll: vi.fn(), viewAllLabel: 'Ver todo' }
|
|
105
|
+
const gridHtml = grid({ presentation: 'grid', ...withBoth })
|
|
106
|
+
expect(gridHtml).toContain('Comprar')
|
|
107
|
+
expect(gridHtml).not.toContain('Ver todo')
|
|
108
|
+
const listHtml = grid({ presentation: 'list', ...withBoth })
|
|
109
|
+
expect(listHtml).toContain('Ver todo')
|
|
110
|
+
expect(listHtml).not.toContain('Comprar')
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('forwards the controlled query to the search input', async () => {
|
|
114
|
+
const user = userEvent.setup()
|
|
115
|
+
const onQueryChange = vi.fn()
|
|
116
|
+
render(<CategoryGrid categories={categories} presentation="list" query="tal" onQueryChange={onQueryChange} searchLabel="Buscar categorías" />)
|
|
117
|
+
const input = screen.getByLabelText('Buscar categorías') as HTMLInputElement
|
|
118
|
+
expect(input.value).toBe('tal')
|
|
119
|
+
await user.type(input, 'a')
|
|
120
|
+
expect(onQueryChange).toHaveBeenCalled()
|
|
121
|
+
})
|
|
122
|
+
})
|