@thorprovider/blocks 0.1.3 → 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.
- package/CHANGELOG.md +155 -0
- package/README.md +228 -68
- package/docs/standards/data-contracts.md +13 -2
- package/package.json +26 -10
- package/scripts/fix-esm-extensions.mjs +40 -0
- package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
- package/src/blocks/application/login-forms/login-page.tsx +240 -0
- package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
- package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
- package/src/blocks/application/verification/verify-1.tsx +2 -2
- package/src/blocks/application/viewport-ownership.test.tsx +41 -0
- package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
- package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
- package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
- package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
- package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
- package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
- package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
- package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
- package/src/blocks/e-commerce/navigation/storefront-navbar-account-menu.test.tsx +56 -0
- package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
- package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
- package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
- package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +67 -36
- package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +43 -25
- 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 +447 -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/footers/footer-section-accessibility.test.tsx +118 -0
- package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
- package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
- package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
- package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
- package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
- package/src/blocks/marketing/footers/footer-section.tsx +532 -0
- package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
- package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
- package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
- package/src/blocks/marketing/navbars/navbar.tsx +735 -0
- package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
- package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
- package/src/index.ts +156 -128
- package/src/lib/contracts-curated.test.ts +47 -29
- package/src/lib/contracts.test.ts +198 -2
- package/src/lib/contracts.ts +410 -16
- 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/application/login-forms/login-page-1.tsx +0 -246
- package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
- package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
- package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
- package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
- package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
- package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
- package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
- package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
- package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
- package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
- package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
- package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
- package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
- package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
- package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
- 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/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
- package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
- package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
- package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
- package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
- package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
- package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
- package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +0 -38
- package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
- package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
- package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
- package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import {
|
|
3
|
+
import { StorefrontNavbarCartSheet, type CartItem } from './storefront-navbar-cart-sheet'
|
|
4
4
|
|
|
5
5
|
const items: CartItem[] = [
|
|
6
6
|
{ id: 'line_1', name: 'Taladro', variant: 'Rojo', price: 45, qty: 2, image: 'https://cdn.example/t.jpg' },
|
|
@@ -11,10 +11,10 @@ const formatMoney = (money: { amount: string; currencyCode: string }) =>
|
|
|
11
11
|
|
|
12
12
|
const noop = () => {}
|
|
13
13
|
|
|
14
|
-
describe('
|
|
14
|
+
describe('StorefrontNavbarCartSheet — authoritative subtotal', () => {
|
|
15
15
|
it('never renders a fake zero for a supplied but absent/malformed subtotal', () => {
|
|
16
16
|
const html = renderToStaticMarkup(
|
|
17
|
-
<
|
|
17
|
+
<StorefrontNavbarCartSheet
|
|
18
18
|
items={items}
|
|
19
19
|
onQtyChange={noop}
|
|
20
20
|
onRemove={noop}
|
|
@@ -28,7 +28,7 @@ describe('Navbar4CartSheet — authoritative subtotal', () => {
|
|
|
28
28
|
|
|
29
29
|
it('preserves a legitimate zero subtotal', () => {
|
|
30
30
|
const html = renderToStaticMarkup(
|
|
31
|
-
<
|
|
31
|
+
<StorefrontNavbarCartSheet
|
|
32
32
|
items={items}
|
|
33
33
|
onQtyChange={noop}
|
|
34
34
|
onRemove={noop}
|
|
@@ -42,7 +42,7 @@ describe('Navbar4CartSheet — authoritative subtotal', () => {
|
|
|
42
42
|
|
|
43
43
|
it('falls back to the legacy local subtotal only when none is supplied', () => {
|
|
44
44
|
const html = renderToStaticMarkup(
|
|
45
|
-
<
|
|
45
|
+
<StorefrontNavbarCartSheet
|
|
46
46
|
items={items}
|
|
47
47
|
onQtyChange={noop}
|
|
48
48
|
onRemove={noop}
|
|
@@ -53,10 +53,10 @@ describe('Navbar4CartSheet — authoritative subtotal', () => {
|
|
|
53
53
|
})
|
|
54
54
|
})
|
|
55
55
|
|
|
56
|
-
describe('
|
|
56
|
+
describe('StorefrontNavbarCartSheet — canonical line amounts', () => {
|
|
57
57
|
it('renders the canonical backend line amount in its currency, not price times qty', () => {
|
|
58
58
|
const html = renderToStaticMarkup(
|
|
59
|
-
<
|
|
59
|
+
<StorefrontNavbarCartSheet
|
|
60
60
|
items={[{ ...items[0], price: 0, lineTotal: { amount: '90.00', currencyCode: 'EUR' } }]}
|
|
61
61
|
onQtyChange={noop}
|
|
62
62
|
onRemove={noop}
|
|
@@ -70,7 +70,7 @@ describe('Navbar4CartSheet — canonical line amounts', () => {
|
|
|
70
70
|
|
|
71
71
|
it('never falls back to a numeric amount when a canonical line amount is missing', () => {
|
|
72
72
|
const html = renderToStaticMarkup(
|
|
73
|
-
<
|
|
73
|
+
<StorefrontNavbarCartSheet
|
|
74
74
|
items={[{ ...items[0], price: 45, lineTotal: undefined }]}
|
|
75
75
|
onQtyChange={noop}
|
|
76
76
|
onRemove={noop}
|
|
@@ -84,7 +84,7 @@ describe('Navbar4CartSheet — canonical line amounts', () => {
|
|
|
84
84
|
|
|
85
85
|
it('shows the unavailable label for a malformed canonical line amount', () => {
|
|
86
86
|
const html = renderToStaticMarkup(
|
|
87
|
-
<
|
|
87
|
+
<StorefrontNavbarCartSheet
|
|
88
88
|
items={[{ ...items[0], lineTotal: { amount: '', currencyCode: 'USD' } }]}
|
|
89
89
|
onQtyChange={noop}
|
|
90
90
|
onRemove={noop}
|
|
@@ -98,7 +98,7 @@ describe('Navbar4CartSheet — canonical line amounts', () => {
|
|
|
98
98
|
|
|
99
99
|
it('preserves legacy numeric rendering when canonical mode is omitted', () => {
|
|
100
100
|
const html = renderToStaticMarkup(
|
|
101
|
-
<
|
|
101
|
+
<StorefrontNavbarCartSheet
|
|
102
102
|
items={[{ ...items[0], lineTotal: { amount: '90.00', currencyCode: 'EUR' } }]}
|
|
103
103
|
onQtyChange={noop}
|
|
104
104
|
onRemove={noop}
|
|
@@ -110,10 +110,10 @@ describe('Navbar4CartSheet — canonical line amounts', () => {
|
|
|
110
110
|
})
|
|
111
111
|
})
|
|
112
112
|
|
|
113
|
-
describe('
|
|
113
|
+
describe('StorefrontNavbarCartSheet — read/mutation lifecycle', () => {
|
|
114
114
|
it('shows a pending state instead of claiming an empty cart', () => {
|
|
115
115
|
const html = renderToStaticMarkup(
|
|
116
|
-
<
|
|
116
|
+
<StorefrontNavbarCartSheet items={[]} onQtyChange={noop} onRemove={noop} state='pending' />,
|
|
117
117
|
)
|
|
118
118
|
expect(html).toContain('Actualizando el carrito')
|
|
119
119
|
expect(html).not.toContain('Your cart is empty')
|
|
@@ -121,7 +121,7 @@ describe('Navbar4CartSheet — read/mutation lifecycle', () => {
|
|
|
121
121
|
|
|
122
122
|
it('shows a recoverable error state with its retry action', () => {
|
|
123
123
|
const html = renderToStaticMarkup(
|
|
124
|
-
<
|
|
124
|
+
<StorefrontNavbarCartSheet
|
|
125
125
|
items={items}
|
|
126
126
|
onQtyChange={noop}
|
|
127
127
|
onRemove={noop}
|
|
@@ -138,17 +138,17 @@ describe('Navbar4CartSheet — read/mutation lifecycle', () => {
|
|
|
138
138
|
|
|
139
139
|
it('disables mutation controls while pending', () => {
|
|
140
140
|
const html = renderToStaticMarkup(
|
|
141
|
-
<
|
|
141
|
+
<StorefrontNavbarCartSheet items={items} onQtyChange={noop} onRemove={noop} state='pending' />,
|
|
142
142
|
)
|
|
143
143
|
expect(html).toContain('aria-label="Increase quantity, Taladro"')
|
|
144
144
|
expect(html).toContain('disabled')
|
|
145
145
|
})
|
|
146
146
|
})
|
|
147
147
|
|
|
148
|
-
describe('
|
|
148
|
+
describe('StorefrontNavbarCartSheet — action availability', () => {
|
|
149
149
|
it('suppresses the checkout action when checkout is not installed', () => {
|
|
150
150
|
const html = renderToStaticMarkup(
|
|
151
|
-
<
|
|
151
|
+
<StorefrontNavbarCartSheet
|
|
152
152
|
items={items}
|
|
153
153
|
onQtyChange={noop}
|
|
154
154
|
onRemove={noop}
|
|
@@ -162,7 +162,7 @@ describe('Navbar4CartSheet — action availability', () => {
|
|
|
162
162
|
|
|
163
163
|
it('disables the checkout action while the cart is not ready', () => {
|
|
164
164
|
const html = renderToStaticMarkup(
|
|
165
|
-
<
|
|
165
|
+
<StorefrontNavbarCartSheet items={items} onQtyChange={noop} onRemove={noop} checkoutDisabled onCheckout={noop} />,
|
|
166
166
|
)
|
|
167
167
|
expect(html).toContain('>Checkout</button>')
|
|
168
168
|
expect(html).toMatch(/disabled[^>]*>Checkout<\/button>/)
|
|
@@ -170,7 +170,7 @@ describe('Navbar4CartSheet — action availability', () => {
|
|
|
170
170
|
|
|
171
171
|
it('suppresses the review action when the cart view is not installed', () => {
|
|
172
172
|
const html = renderToStaticMarkup(
|
|
173
|
-
<
|
|
173
|
+
<StorefrontNavbarCartSheet
|
|
174
174
|
items={items}
|
|
175
175
|
onQtyChange={noop}
|
|
176
176
|
onRemove={noop}
|
|
@@ -182,10 +182,10 @@ describe('Navbar4CartSheet — action availability', () => {
|
|
|
182
182
|
})
|
|
183
183
|
})
|
|
184
184
|
|
|
185
|
-
describe('
|
|
185
|
+
describe('StorefrontNavbarCartSheet — accessibility', () => {
|
|
186
186
|
it('names the line mutation controls', () => {
|
|
187
187
|
const html = renderToStaticMarkup(
|
|
188
|
-
<
|
|
188
|
+
<StorefrontNavbarCartSheet items={items} onQtyChange={noop} onRemove={noop} />,
|
|
189
189
|
)
|
|
190
190
|
expect(html).toMatch(/aria-label="[^"]*(Increase|Decrease|Aumentar|Disminuir)[^"]*"/)
|
|
191
191
|
expect(html).toMatch(/aria-label="[^"]*(Remove|Quitar)[^"]*"/)
|
|
@@ -193,11 +193,11 @@ describe('Navbar4CartSheet — accessibility', () => {
|
|
|
193
193
|
|
|
194
194
|
it('announces pending reads politely and errors as an alert', () => {
|
|
195
195
|
const pending = renderToStaticMarkup(
|
|
196
|
-
<
|
|
196
|
+
<StorefrontNavbarCartSheet items={[]} onQtyChange={noop} onRemove={noop} state='pending' />,
|
|
197
197
|
)
|
|
198
198
|
expect(pending).toMatch(/role="status"[^>]*aria-live="polite"/)
|
|
199
199
|
const error = renderToStaticMarkup(
|
|
200
|
-
<
|
|
200
|
+
<StorefrontNavbarCartSheet items={items} onQtyChange={noop} onRemove={noop} state='error' />,
|
|
201
201
|
)
|
|
202
202
|
expect(error).toMatch(/role="alert"/)
|
|
203
203
|
})
|
|
@@ -37,7 +37,7 @@ export interface CartItem {
|
|
|
37
37
|
|
|
38
38
|
export type CartSheetState = 'idle' | 'pending' | 'error'
|
|
39
39
|
|
|
40
|
-
export interface
|
|
40
|
+
export interface StorefrontNavbarCartSheetProps {
|
|
41
41
|
items: CartItem[]
|
|
42
42
|
/**
|
|
43
43
|
* Quantity DELTA for a backend cart line-item id. The boundary converts the
|
|
@@ -104,7 +104,7 @@ const revealHidden = { opacity: 0, y: 12, filter: 'blur(3px)' }
|
|
|
104
104
|
const revealShown = { opacity: 1, y: 0, filter: 'blur(0px)' }
|
|
105
105
|
const revealExit = { opacity: 0, y: 0, filter: 'blur(0px)', transition: { duration: 0.2, ease: 'easeOut' as const } }
|
|
106
106
|
|
|
107
|
-
export function
|
|
107
|
+
export function StorefrontNavbarCartSheet({
|
|
108
108
|
items,
|
|
109
109
|
onQtyChange,
|
|
110
110
|
onRemove,
|
|
@@ -124,7 +124,7 @@ export function Navbar4CartSheet({
|
|
|
124
124
|
checkoutUnavailable = false,
|
|
125
125
|
checkoutDisabled = false,
|
|
126
126
|
viewCartUnavailable = false,
|
|
127
|
-
}:
|
|
127
|
+
}: StorefrontNavbarCartSheetProps) {
|
|
128
128
|
const reduceMotion = useReducedMotion()
|
|
129
129
|
const format = formatPrice ?? (value => usdFormatter.format(value))
|
|
130
130
|
const localSubtotal = items.reduce((total, item) => total + item.price * item.qty, 0)
|
|
@@ -2,33 +2,25 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '..
|
|
|
2
2
|
import { Separator } from '../../../ui/separator'
|
|
3
3
|
import { SheetClose } from '../../../ui/sheet'
|
|
4
4
|
import { cn } from '../../../lib/utils'
|
|
5
|
-
|
|
6
|
-
export interface StoreCategory {
|
|
7
|
-
label: string
|
|
8
|
-
href: string
|
|
9
|
-
/** Rendered as an accordion inside the mobile sheet; omit for a flat link. */
|
|
10
|
-
children?: { label: string; href: string }[]
|
|
11
|
-
/** Styles the link with `text-destructive` — reserve it for one entry. */
|
|
12
|
-
emphasis?: boolean
|
|
13
|
-
}
|
|
5
|
+
import type { StoreCategory } from '../../../lib/contracts'
|
|
14
6
|
|
|
15
7
|
export interface UtilityLink {
|
|
16
8
|
label: string
|
|
17
9
|
href: string
|
|
18
10
|
}
|
|
19
11
|
|
|
20
|
-
export interface
|
|
12
|
+
export interface StorefrontNavbarMobileNavProps {
|
|
21
13
|
storeCategories: StoreCategory[]
|
|
22
14
|
utilityLinks: UtilityLink[]
|
|
23
15
|
/** When provided, an extra "Account" link is appended after the utility links. */
|
|
24
16
|
accountHref?: string
|
|
25
17
|
}
|
|
26
18
|
|
|
27
|
-
export function
|
|
19
|
+
export function StorefrontNavbarMobileNav({
|
|
28
20
|
storeCategories,
|
|
29
21
|
utilityLinks,
|
|
30
22
|
accountHref,
|
|
31
|
-
}:
|
|
23
|
+
}: StorefrontNavbarMobileNavProps) {
|
|
32
24
|
return (
|
|
33
25
|
<nav aria-label='Store' className='px-2 py-2'>
|
|
34
26
|
<Accordion type='multiple'>
|
|
@@ -2,19 +2,19 @@ import { Search } from 'lucide-react'
|
|
|
2
2
|
|
|
3
3
|
import { InputGroup, InputGroupAddon, InputGroupInput } from '../../../ui/input-group'
|
|
4
4
|
|
|
5
|
-
export interface
|
|
5
|
+
export interface StorefrontNavbarSearchBarProps {
|
|
6
6
|
value: string
|
|
7
7
|
onValueChange: (value: string) => void
|
|
8
8
|
onDismiss?: () => void
|
|
9
9
|
id?: string
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function
|
|
12
|
+
export function StorefrontNavbarSearchBar({
|
|
13
13
|
value,
|
|
14
14
|
onValueChange,
|
|
15
15
|
onDismiss,
|
|
16
16
|
id = 'store-search',
|
|
17
|
-
}:
|
|
17
|
+
}: StorefrontNavbarSearchBarProps) {
|
|
18
18
|
return (
|
|
19
19
|
<form
|
|
20
20
|
role='search'
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import type { ReactElement } from 'react'
|
|
1
2
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
+
import { StorefrontNavbar, StorefrontNavbarAccountRetry } from './storefront-navbar'
|
|
5
|
+
import { StorefrontNavbarAccountMenu } from './storefront-navbar-account-menu'
|
|
6
|
+
import { storefrontNavbarPropsSchema } from '../../../lib/contracts'
|
|
6
7
|
|
|
7
8
|
const base = {
|
|
8
9
|
storeCategories: [],
|
|
@@ -11,42 +12,49 @@ const base = {
|
|
|
11
12
|
logoText: 'ElectroCuba',
|
|
12
13
|
}
|
|
13
14
|
|
|
14
|
-
describe('
|
|
15
|
+
describe('StorefrontNavbar — account affordance', () => {
|
|
15
16
|
const user = { name: 'Ada Lovelace', email: 'ada@example.test', initials: 'AL' }
|
|
16
17
|
|
|
17
18
|
it('offers a visible account destination to anonymous shoppers', () => {
|
|
18
|
-
const html = renderToStaticMarkup(<
|
|
19
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} accountHref="/account" />)
|
|
19
20
|
expect(html).toMatch(/<a[^>]*href="\/account"[^>]*aria-label="Account"/)
|
|
20
21
|
})
|
|
21
22
|
|
|
22
23
|
it('renders the identity trigger without a desktop-only wrapper or empty image', () => {
|
|
23
|
-
const html = renderToStaticMarkup(<
|
|
24
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} user={user} />)
|
|
24
25
|
expect(html).toContain('Account menu for Ada Lovelace')
|
|
25
26
|
expect(html).not.toMatch(/<div class="hidden lg:block"><button[^>]*aria-label="Account/)
|
|
26
27
|
expect(html).not.toContain('src=""')
|
|
27
28
|
})
|
|
28
29
|
|
|
30
|
+
it('renders the fallback initials when the identity has no avatar, never an empty image source', () => {
|
|
31
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} user={user} />)
|
|
32
|
+
expect(html).toContain('>AL<')
|
|
33
|
+
expect(html).not.toContain('src=""')
|
|
34
|
+
expect(html).not.toContain('<img')
|
|
35
|
+
})
|
|
36
|
+
|
|
29
37
|
it('renders no account control without identity or destination', () => {
|
|
30
|
-
const html = renderToStaticMarkup(<
|
|
38
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} />)
|
|
31
39
|
expect(html).not.toContain('aria-label="Account')
|
|
32
|
-
expect(renderToStaticMarkup(<
|
|
40
|
+
expect(renderToStaticMarkup(<StorefrontNavbarAccountMenu />)).toBe('')
|
|
33
41
|
})
|
|
34
42
|
|
|
35
43
|
it('disables the identity trigger while a session operation is pending', () => {
|
|
36
|
-
const html = renderToStaticMarkup(<
|
|
44
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} user={user} accountPending />)
|
|
37
45
|
expect(html).toMatch(/<button[^>]*disabled=""[^>]*aria-label="Account menu for Ada Lovelace"/)
|
|
38
46
|
})
|
|
39
47
|
|
|
40
48
|
it('distinguishes unavailable identity from anonymous and offers retry', () => {
|
|
41
49
|
const html = renderToStaticMarkup(
|
|
42
|
-
<
|
|
50
|
+
<StorefrontNavbar {...base} accountHref="/account" accountUnavailable onAccountRetry={() => {}} />,
|
|
43
51
|
)
|
|
44
52
|
expect(html).toContain('aria-label="Retry account session"')
|
|
45
53
|
expect(html).not.toContain('aria-label="Account"')
|
|
46
54
|
})
|
|
47
55
|
|
|
48
56
|
it('keeps serializable identity and feedback while stripping callbacks', () => {
|
|
49
|
-
const parsed =
|
|
57
|
+
const parsed = storefrontNavbarPropsSchema.parse({
|
|
50
58
|
...base, user, accountHref: '/account', accountMenuItems: [{ label: 'Account', href: '/account' }],
|
|
51
59
|
accountPending: true, accountUnavailable: true, onSignOut: () => {},
|
|
52
60
|
})
|
|
@@ -57,16 +65,39 @@ describe('Navbar4 — account affordance', () => {
|
|
|
57
65
|
})
|
|
58
66
|
})
|
|
59
67
|
|
|
60
|
-
describe('
|
|
68
|
+
describe('StorefrontNavbar — consumer-controlled session retry', () => {
|
|
69
|
+
type RetryProps = { onClick?: () => void; disabled?: boolean }
|
|
70
|
+
|
|
71
|
+
it('dispatches the consumer retry action from the unavailable affordance', () => {
|
|
72
|
+
const onRetry = vi.fn()
|
|
73
|
+
const control = StorefrontNavbarAccountRetry({ onRetry }) as ReactElement<RetryProps>
|
|
74
|
+
expect(control.props.disabled).toBe(false)
|
|
75
|
+
control.props.onClick?.()
|
|
76
|
+
expect(onRetry).toHaveBeenCalledTimes(1)
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('holds retry inert while reconciliation is pending', () => {
|
|
80
|
+
const onRetry = vi.fn()
|
|
81
|
+
const control = StorefrontNavbarAccountRetry({ pending: true, onRetry }) as ReactElement<RetryProps>
|
|
82
|
+
expect(control.props.disabled).toBe(true)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('holds retry inert when no consumer callback is bound (no empty affordance)', () => {
|
|
86
|
+
const control = StorefrontNavbarAccountRetry({}) as ReactElement<RetryProps>
|
|
87
|
+
expect(control.props.disabled).toBe(true)
|
|
88
|
+
})
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
describe('StorefrontNavbar — optional logo accent', () => {
|
|
61
92
|
it('renders the wordmark unchanged when no accent is supplied', () => {
|
|
62
|
-
const html = renderToStaticMarkup(<
|
|
93
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} />)
|
|
63
94
|
expect(html).toContain('>ElectroCuba</span>')
|
|
64
95
|
expect(html).not.toContain('text-brand-promo')
|
|
65
96
|
})
|
|
66
97
|
|
|
67
98
|
it('accents a matched trailing segment without duplicating the wordmark', () => {
|
|
68
99
|
const html = renderToStaticMarkup(
|
|
69
|
-
<
|
|
100
|
+
<StorefrontNavbar {...base} logoAccent={{ text: 'Cuba', className: 'text-brand-promo' }} />,
|
|
70
101
|
)
|
|
71
102
|
expect(html).toContain('Electro<span class="text-brand-promo">Cuba</span>')
|
|
72
103
|
expect(html.match(/Electro/g)).toHaveLength(1)
|
|
@@ -75,54 +106,54 @@ describe('Navbar4 — optional logo accent', () => {
|
|
|
75
106
|
|
|
76
107
|
it('accents the full wordmark when the accent covers all of it', () => {
|
|
77
108
|
const html = renderToStaticMarkup(
|
|
78
|
-
<
|
|
109
|
+
<StorefrontNavbar {...base} logoAccent={{ text: 'ElectroCuba', className: 'text-brand-promo' }} />,
|
|
79
110
|
)
|
|
80
111
|
expect(html).toContain('<span class="text-brand-promo">ElectroCuba</span>')
|
|
81
112
|
})
|
|
82
113
|
|
|
83
114
|
it('does not accent when the segment is not a trailing match', () => {
|
|
84
|
-
const html = renderToStaticMarkup(<
|
|
115
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} logoAccent={{ text: 'Electro' }} />)
|
|
85
116
|
expect(html).toContain('>ElectroCuba</span>')
|
|
86
117
|
expect(html).not.toContain('<span class="">')
|
|
87
118
|
})
|
|
88
119
|
|
|
89
120
|
it('keeps the wordmark as the accessible text of the logo link', () => {
|
|
90
121
|
const html = renderToStaticMarkup(
|
|
91
|
-
<
|
|
122
|
+
<StorefrontNavbar {...base} logoHref='/' logoAccent={{ text: 'Cuba', className: 'text-brand-promo' }} />,
|
|
92
123
|
)
|
|
93
124
|
expect(html).toContain('href="/"')
|
|
94
125
|
expect(html).toContain('Electro<span class="text-brand-promo">Cuba</span>')
|
|
95
126
|
})
|
|
96
127
|
})
|
|
97
128
|
|
|
98
|
-
describe('
|
|
129
|
+
describe('StorefrontNavbar — optional utility strip', () => {
|
|
99
130
|
// The desktop utility strip is the only element with this inner track class.
|
|
100
131
|
const strip = (html: string) => html.includes('h-9 w-full max-w-7xl')
|
|
101
132
|
|
|
102
133
|
it('renders no utility strip when the note is empty and there are no utility links', () => {
|
|
103
134
|
const html = renderToStaticMarkup(
|
|
104
|
-
<
|
|
135
|
+
<StorefrontNavbar storeCategories={[]} shippingNote='' utilityLinks={[]} logoText='ElectroCuba' />,
|
|
105
136
|
)
|
|
106
137
|
expect(strip(html)).toBe(false)
|
|
107
138
|
})
|
|
108
139
|
|
|
109
140
|
it('renders no utility strip when the note is omitted', () => {
|
|
110
141
|
const html = renderToStaticMarkup(
|
|
111
|
-
<
|
|
142
|
+
<StorefrontNavbar storeCategories={[]} utilityLinks={[]} logoText='ElectroCuba' />,
|
|
112
143
|
)
|
|
113
144
|
expect(strip(html)).toBe(false)
|
|
114
145
|
})
|
|
115
146
|
|
|
116
147
|
it('treats a whitespace-only note as empty', () => {
|
|
117
148
|
const html = renderToStaticMarkup(
|
|
118
|
-
<
|
|
149
|
+
<StorefrontNavbar storeCategories={[]} shippingNote=' ' utilityLinks={[]} logoText='ElectroCuba' />,
|
|
119
150
|
)
|
|
120
151
|
expect(strip(html)).toBe(false)
|
|
121
152
|
})
|
|
122
153
|
|
|
123
154
|
it('renders the utility strip with a non-empty note', () => {
|
|
124
155
|
const html = renderToStaticMarkup(
|
|
125
|
-
<
|
|
156
|
+
<StorefrontNavbar storeCategories={[]} shippingNote='Entrega segura' utilityLinks={[]} logoText='ElectroCuba' />,
|
|
126
157
|
)
|
|
127
158
|
expect(strip(html)).toBe(true)
|
|
128
159
|
expect(html).toContain('Entrega segura')
|
|
@@ -130,7 +161,7 @@ describe('Navbar4 — optional utility strip', () => {
|
|
|
130
161
|
|
|
131
162
|
it('renders the utility strip when there is a utility link even with an empty note', () => {
|
|
132
163
|
const html = renderToStaticMarkup(
|
|
133
|
-
<
|
|
164
|
+
<StorefrontNavbar
|
|
134
165
|
storeCategories={[]}
|
|
135
166
|
shippingNote=''
|
|
136
167
|
utilityLinks={[{ label: 'Ayuda', href: '/help' }]}
|
|
@@ -149,9 +180,9 @@ const category = { href: '/herramientas', label: 'Herramientas' }
|
|
|
149
180
|
const navLinkClass = (html: string) =>
|
|
150
181
|
html.match(/<a href="\/herramientas" class="([^"]*hover:bg-muted[^"]*)"/)?.[1] ?? ''
|
|
151
182
|
|
|
152
|
-
describe('
|
|
183
|
+
describe('StorefrontNavbar — optional outer surface className', () => {
|
|
153
184
|
it('keeps the default surface (incl. backdrop-filter variant) and nav link color when className is omitted', () => {
|
|
154
|
-
const html = renderToStaticMarkup(<
|
|
185
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} storeCategories={[category]} />)
|
|
155
186
|
expect(html).toContain('bg-background/95')
|
|
156
187
|
expect(html).toContain('supports-backdrop-filter:bg-background/80')
|
|
157
188
|
expect(navLinkClass(html)).toContain('text-foreground')
|
|
@@ -160,7 +191,7 @@ describe('Navbar4 — optional outer surface className', () => {
|
|
|
160
191
|
|
|
161
192
|
it('gives the consumer full surface ownership when className is supplied (no default background or variant)', () => {
|
|
162
193
|
const html = renderToStaticMarkup(
|
|
163
|
-
<
|
|
194
|
+
<StorefrontNavbar {...base} storeCategories={[category]} className='bg-brand-nav text-brand-nav-foreground' />,
|
|
164
195
|
)
|
|
165
196
|
expect(html).toContain('bg-brand-nav')
|
|
166
197
|
expect(html).toContain('text-brand-nav-foreground')
|
|
@@ -174,7 +205,7 @@ describe('Navbar4 — optional outer surface className', () => {
|
|
|
174
205
|
|
|
175
206
|
it('lets nav links inherit the branded surface text color with a correct hover foreground', () => {
|
|
176
207
|
const html = renderToStaticMarkup(
|
|
177
|
-
<
|
|
208
|
+
<StorefrontNavbar {...base} storeCategories={[category]} className='bg-brand-nav text-brand-nav-foreground' />,
|
|
178
209
|
)
|
|
179
210
|
expect(navLinkClass(html)).toContain('text-inherit')
|
|
180
211
|
expect(navLinkClass(html)).toContain('hover:text-foreground')
|
|
@@ -184,15 +215,15 @@ describe('Navbar4 — optional outer surface className', () => {
|
|
|
184
215
|
const cartTrigger = (html: string) =>
|
|
185
216
|
html.match(/<button[^>]*aria-label="Cart,[^"]*"[^>]*>/)?.[0] ?? ''
|
|
186
217
|
|
|
187
|
-
describe('
|
|
218
|
+
describe('StorefrontNavbar — controlled cart sheet visibility', () => {
|
|
188
219
|
it('keeps the uncontrolled sheet closed by default (legacy consumers unaffected)', () => {
|
|
189
|
-
const html = renderToStaticMarkup(<
|
|
220
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} />)
|
|
190
221
|
expect(cartTrigger(html)).toContain('data-state="closed"')
|
|
191
222
|
})
|
|
192
223
|
|
|
193
224
|
it('reflects a controlled open state on the trigger', () => {
|
|
194
225
|
const html = renderToStaticMarkup(
|
|
195
|
-
<
|
|
226
|
+
<StorefrontNavbar {...base} cartOpen={true} onCartOpenChange={() => {}} />,
|
|
196
227
|
)
|
|
197
228
|
expect(cartTrigger(html)).toContain('data-state="open"')
|
|
198
229
|
expect(cartTrigger(html)).toContain('aria-expanded="true"')
|
|
@@ -200,23 +231,23 @@ describe('Navbar4 — controlled cart sheet visibility', () => {
|
|
|
200
231
|
|
|
201
232
|
it('reflects a controlled closed state on the trigger', () => {
|
|
202
233
|
const html = renderToStaticMarkup(
|
|
203
|
-
<
|
|
234
|
+
<StorefrontNavbar {...base} cartOpen={false} onCartOpenChange={() => {}} />,
|
|
204
235
|
)
|
|
205
236
|
expect(cartTrigger(html)).toContain('data-state="closed"')
|
|
206
237
|
expect(cartTrigger(html)).toContain('aria-expanded="false"')
|
|
207
238
|
})
|
|
208
239
|
})
|
|
209
240
|
|
|
210
|
-
describe('
|
|
241
|
+
describe('StorefrontNavbar — optional showSearch (E2 task 5.4)', () => {
|
|
211
242
|
it('renders the search control by default (legacy consumers unaffected)', () => {
|
|
212
|
-
const html = renderToStaticMarkup(<
|
|
243
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} />)
|
|
213
244
|
expect(html).toContain('role="search"')
|
|
214
245
|
expect(html).toContain('type="search"')
|
|
215
246
|
expect(html).toContain('aria-label="Search products"')
|
|
216
247
|
})
|
|
217
248
|
|
|
218
249
|
it('renders no search control when showSearch is false, keeping the rest of the chrome', () => {
|
|
219
|
-
const html = renderToStaticMarkup(<
|
|
250
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} showSearch={false} />)
|
|
220
251
|
expect(html).not.toContain('role="search"')
|
|
221
252
|
expect(html).not.toContain('type="search"')
|
|
222
253
|
expect(html).not.toContain('aria-label="Search products"')
|
|
@@ -226,7 +257,7 @@ describe('Navbar4 — optional showSearch (E2 task 5.4)', () => {
|
|
|
226
257
|
})
|
|
227
258
|
|
|
228
259
|
it('renders the search control when showSearch is explicitly true', () => {
|
|
229
|
-
const html = renderToStaticMarkup(<
|
|
260
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} showSearch={true} />)
|
|
230
261
|
expect(html).toContain('role="search"')
|
|
231
262
|
expect(html).toContain('type="search"')
|
|
232
263
|
})
|
package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx}
RENAMED
|
@@ -8,12 +8,12 @@ import { Button } from '../../../ui/button'
|
|
|
8
8
|
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from '../../../ui/sheet'
|
|
9
9
|
import { cn } from '../../../lib/utils'
|
|
10
10
|
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import type { UtilityLink } from './navbar-
|
|
14
|
-
import {
|
|
15
|
-
import { MegaMenu } from '
|
|
16
|
-
import { MobileCategoryDrawer } from '
|
|
11
|
+
import { StorefrontNavbarAccountMenu, type AccountMenuItem, type StoreUser } from './storefront-navbar-account-menu'
|
|
12
|
+
import { StorefrontNavbarCartSheet, type CartItem, type CartSheetState } from './storefront-navbar-cart-sheet'
|
|
13
|
+
import type { UtilityLink } from './storefront-navbar-mobile-nav'
|
|
14
|
+
import { StorefrontNavbarSearchBar } from './storefront-navbar-search-bar'
|
|
15
|
+
import { MegaMenu } from './mega-menu'
|
|
16
|
+
import { MobileCategoryDrawer } from './mobile-category-drawer'
|
|
17
17
|
import type { MenuItem } from '../../../lib/types/menu'
|
|
18
18
|
import type { Money } from '@thorprovider/types'
|
|
19
19
|
import type { StoreCategory } from '../../../lib/contracts'
|
|
@@ -161,7 +161,35 @@ function CartBadge({ count }: CartBadgeProps) {
|
|
|
161
161
|
)
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
export interface
|
|
164
|
+
export interface StorefrontNavbarAccountRetryProps {
|
|
165
|
+
/** Consumer-owned reconciliation in flight; holds the affordance inert. */
|
|
166
|
+
pending?: boolean
|
|
167
|
+
/** Consumer-owned recovery action. Without it the affordance is inert. */
|
|
168
|
+
onRetry?: () => void
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Recovery affordance for an unavailable session. The block never resolves the
|
|
173
|
+
* session itself: it surfaces the state and dispatches the consumer's retry.
|
|
174
|
+
* Extracted (hooks-free) so the dispatch is testable without a DOM.
|
|
175
|
+
*/
|
|
176
|
+
export function StorefrontNavbarAccountRetry({ pending = false, onRetry }: StorefrontNavbarAccountRetryProps) {
|
|
177
|
+
return (
|
|
178
|
+
<Button
|
|
179
|
+
variant='ghost'
|
|
180
|
+
size='icon'
|
|
181
|
+
className='size-11'
|
|
182
|
+
disabled={pending || !onRetry}
|
|
183
|
+
aria-label='Retry account session'
|
|
184
|
+
title='Account session unavailable'
|
|
185
|
+
onClick={onRetry}
|
|
186
|
+
>
|
|
187
|
+
<RotateCcw aria-hidden='true' />
|
|
188
|
+
</Button>
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export interface StorefrontNavbarProps {
|
|
165
193
|
storeCategories: StoreCategory[]
|
|
166
194
|
/**
|
|
167
195
|
* Optional note shown in the desktop utility strip. When omitted (or empty)
|
|
@@ -257,7 +285,7 @@ export interface Navbar4Props {
|
|
|
257
285
|
className?: string
|
|
258
286
|
}
|
|
259
287
|
|
|
260
|
-
export function
|
|
288
|
+
export function StorefrontNavbar({
|
|
261
289
|
storeCategories,
|
|
262
290
|
shippingNote = '',
|
|
263
291
|
utilityLinks,
|
|
@@ -295,7 +323,7 @@ export function Navbar4({
|
|
|
295
323
|
checkoutDisabled,
|
|
296
324
|
viewCartUnavailable,
|
|
297
325
|
className,
|
|
298
|
-
}:
|
|
326
|
+
}: StorefrontNavbarProps) {
|
|
299
327
|
/**
|
|
300
328
|
* Integration seam
|
|
301
329
|
* ----------------
|
|
@@ -467,7 +495,7 @@ export function Navbar4({
|
|
|
467
495
|
{showSearch ? (
|
|
468
496
|
<>
|
|
469
497
|
<div className='hidden lg:block lg:w-64'>
|
|
470
|
-
<
|
|
498
|
+
<StorefrontNavbarSearchBar
|
|
471
499
|
id='store-search'
|
|
472
500
|
value={searchQuery}
|
|
473
501
|
onValueChange={setSearchQuery}
|
|
@@ -489,19 +517,9 @@ export function Navbar4({
|
|
|
489
517
|
) : null}
|
|
490
518
|
|
|
491
519
|
{accountUnavailable ? (
|
|
492
|
-
<
|
|
493
|
-
variant='ghost'
|
|
494
|
-
size='icon'
|
|
495
|
-
className='size-11'
|
|
496
|
-
disabled={accountPending || !onAccountRetry}
|
|
497
|
-
aria-label='Retry account session'
|
|
498
|
-
title='Account session unavailable'
|
|
499
|
-
onClick={onAccountRetry}
|
|
500
|
-
>
|
|
501
|
-
<RotateCcw aria-hidden='true' />
|
|
502
|
-
</Button>
|
|
520
|
+
<StorefrontNavbarAccountRetry pending={accountPending} onRetry={onAccountRetry} />
|
|
503
521
|
) : user ? (
|
|
504
|
-
<
|
|
522
|
+
<StorefrontNavbarAccountMenu
|
|
505
523
|
user={user}
|
|
506
524
|
items={accountMenuItems}
|
|
507
525
|
onHelp={onHelp}
|
|
@@ -542,7 +560,7 @@ export function Navbar4({
|
|
|
542
560
|
Cart{count > 0 ? ` (${count})` : ''}
|
|
543
561
|
</SheetTitle>
|
|
544
562
|
</SheetHeader>
|
|
545
|
-
<
|
|
563
|
+
<StorefrontNavbarCartSheet
|
|
546
564
|
items={items}
|
|
547
565
|
onQtyChange={changeQty}
|
|
548
566
|
onRemove={removeItem}
|
|
@@ -571,7 +589,7 @@ export function Navbar4({
|
|
|
571
589
|
|
|
572
590
|
{showSearch && searchOpen ? (
|
|
573
591
|
<div className='border-border flex flex-col gap-0 border-t p-4 lg:hidden'>
|
|
574
|
-
<
|
|
592
|
+
<StorefrontNavbarSearchBar
|
|
575
593
|
id='store-search-mobile'
|
|
576
594
|
value={searchQuery}
|
|
577
595
|
onValueChange={setSearchQuery}
|
|
@@ -583,4 +601,4 @@ export function Navbar4({
|
|
|
583
601
|
)
|
|
584
602
|
}
|
|
585
603
|
|
|
586
|
-
export default
|
|
604
|
+
export default StorefrontNavbar
|