@thorprovider/blocks 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +48 -0
- package/README.md +156 -40
- package/package.json +1 -1
- 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/{marketing/navbars/navbar-4-account-menu.test.tsx → e-commerce/navigation/storefront-navbar-account-menu.test.tsx} +8 -8
- 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} +40 -40
- package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +17 -17
- package/src/blocks/e-commerce/product-list/product-card.tsx +63 -40
- package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +2 -2
- 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 +80 -81
- package/src/lib/contracts-curated.test.ts +5 -5
- package/src/lib/contracts.test.ts +55 -0
- package/src/lib/contracts.ts +64 -2
- package/tsup.config.ts +1 -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/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-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,9 +1,9 @@
|
|
|
1
1
|
import type { ReactElement } from 'react'
|
|
2
2
|
import { renderToStaticMarkup } from 'react-dom/server'
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest'
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
4
|
+
import { StorefrontNavbar, StorefrontNavbarAccountRetry } from './storefront-navbar'
|
|
5
|
+
import { StorefrontNavbarAccountMenu } from './storefront-navbar-account-menu'
|
|
6
|
+
import { storefrontNavbarPropsSchema } from '../../../lib/contracts'
|
|
7
7
|
|
|
8
8
|
const base = {
|
|
9
9
|
storeCategories: [],
|
|
@@ -12,49 +12,49 @@ const base = {
|
|
|
12
12
|
logoText: 'ElectroCuba',
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
describe('
|
|
15
|
+
describe('StorefrontNavbar — account affordance', () => {
|
|
16
16
|
const user = { name: 'Ada Lovelace', email: 'ada@example.test', initials: 'AL' }
|
|
17
17
|
|
|
18
18
|
it('offers a visible account destination to anonymous shoppers', () => {
|
|
19
|
-
const html = renderToStaticMarkup(<
|
|
19
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} accountHref="/account" />)
|
|
20
20
|
expect(html).toMatch(/<a[^>]*href="\/account"[^>]*aria-label="Account"/)
|
|
21
21
|
})
|
|
22
22
|
|
|
23
23
|
it('renders the identity trigger without a desktop-only wrapper or empty image', () => {
|
|
24
|
-
const html = renderToStaticMarkup(<
|
|
24
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} user={user} />)
|
|
25
25
|
expect(html).toContain('Account menu for Ada Lovelace')
|
|
26
26
|
expect(html).not.toMatch(/<div class="hidden lg:block"><button[^>]*aria-label="Account/)
|
|
27
27
|
expect(html).not.toContain('src=""')
|
|
28
28
|
})
|
|
29
29
|
|
|
30
30
|
it('renders the fallback initials when the identity has no avatar, never an empty image source', () => {
|
|
31
|
-
const html = renderToStaticMarkup(<
|
|
31
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} user={user} />)
|
|
32
32
|
expect(html).toContain('>AL<')
|
|
33
33
|
expect(html).not.toContain('src=""')
|
|
34
34
|
expect(html).not.toContain('<img')
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
it('renders no account control without identity or destination', () => {
|
|
38
|
-
const html = renderToStaticMarkup(<
|
|
38
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} />)
|
|
39
39
|
expect(html).not.toContain('aria-label="Account')
|
|
40
|
-
expect(renderToStaticMarkup(<
|
|
40
|
+
expect(renderToStaticMarkup(<StorefrontNavbarAccountMenu />)).toBe('')
|
|
41
41
|
})
|
|
42
42
|
|
|
43
43
|
it('disables the identity trigger while a session operation is pending', () => {
|
|
44
|
-
const html = renderToStaticMarkup(<
|
|
44
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} user={user} accountPending />)
|
|
45
45
|
expect(html).toMatch(/<button[^>]*disabled=""[^>]*aria-label="Account menu for Ada Lovelace"/)
|
|
46
46
|
})
|
|
47
47
|
|
|
48
48
|
it('distinguishes unavailable identity from anonymous and offers retry', () => {
|
|
49
49
|
const html = renderToStaticMarkup(
|
|
50
|
-
<
|
|
50
|
+
<StorefrontNavbar {...base} accountHref="/account" accountUnavailable onAccountRetry={() => {}} />,
|
|
51
51
|
)
|
|
52
52
|
expect(html).toContain('aria-label="Retry account session"')
|
|
53
53
|
expect(html).not.toContain('aria-label="Account"')
|
|
54
54
|
})
|
|
55
55
|
|
|
56
56
|
it('keeps serializable identity and feedback while stripping callbacks', () => {
|
|
57
|
-
const parsed =
|
|
57
|
+
const parsed = storefrontNavbarPropsSchema.parse({
|
|
58
58
|
...base, user, accountHref: '/account', accountMenuItems: [{ label: 'Account', href: '/account' }],
|
|
59
59
|
accountPending: true, accountUnavailable: true, onSignOut: () => {},
|
|
60
60
|
})
|
|
@@ -65,12 +65,12 @@ describe('Navbar4 — account affordance', () => {
|
|
|
65
65
|
})
|
|
66
66
|
})
|
|
67
67
|
|
|
68
|
-
describe('
|
|
68
|
+
describe('StorefrontNavbar — consumer-controlled session retry', () => {
|
|
69
69
|
type RetryProps = { onClick?: () => void; disabled?: boolean }
|
|
70
70
|
|
|
71
71
|
it('dispatches the consumer retry action from the unavailable affordance', () => {
|
|
72
72
|
const onRetry = vi.fn()
|
|
73
|
-
const control =
|
|
73
|
+
const control = StorefrontNavbarAccountRetry({ onRetry }) as ReactElement<RetryProps>
|
|
74
74
|
expect(control.props.disabled).toBe(false)
|
|
75
75
|
control.props.onClick?.()
|
|
76
76
|
expect(onRetry).toHaveBeenCalledTimes(1)
|
|
@@ -78,26 +78,26 @@ describe('Navbar4 — consumer-controlled session retry', () => {
|
|
|
78
78
|
|
|
79
79
|
it('holds retry inert while reconciliation is pending', () => {
|
|
80
80
|
const onRetry = vi.fn()
|
|
81
|
-
const control =
|
|
81
|
+
const control = StorefrontNavbarAccountRetry({ pending: true, onRetry }) as ReactElement<RetryProps>
|
|
82
82
|
expect(control.props.disabled).toBe(true)
|
|
83
83
|
})
|
|
84
84
|
|
|
85
85
|
it('holds retry inert when no consumer callback is bound (no empty affordance)', () => {
|
|
86
|
-
const control =
|
|
86
|
+
const control = StorefrontNavbarAccountRetry({}) as ReactElement<RetryProps>
|
|
87
87
|
expect(control.props.disabled).toBe(true)
|
|
88
88
|
})
|
|
89
89
|
})
|
|
90
90
|
|
|
91
|
-
describe('
|
|
91
|
+
describe('StorefrontNavbar — optional logo accent', () => {
|
|
92
92
|
it('renders the wordmark unchanged when no accent is supplied', () => {
|
|
93
|
-
const html = renderToStaticMarkup(<
|
|
93
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} />)
|
|
94
94
|
expect(html).toContain('>ElectroCuba</span>')
|
|
95
95
|
expect(html).not.toContain('text-brand-promo')
|
|
96
96
|
})
|
|
97
97
|
|
|
98
98
|
it('accents a matched trailing segment without duplicating the wordmark', () => {
|
|
99
99
|
const html = renderToStaticMarkup(
|
|
100
|
-
<
|
|
100
|
+
<StorefrontNavbar {...base} logoAccent={{ text: 'Cuba', className: 'text-brand-promo' }} />,
|
|
101
101
|
)
|
|
102
102
|
expect(html).toContain('Electro<span class="text-brand-promo">Cuba</span>')
|
|
103
103
|
expect(html.match(/Electro/g)).toHaveLength(1)
|
|
@@ -106,54 +106,54 @@ describe('Navbar4 — optional logo accent', () => {
|
|
|
106
106
|
|
|
107
107
|
it('accents the full wordmark when the accent covers all of it', () => {
|
|
108
108
|
const html = renderToStaticMarkup(
|
|
109
|
-
<
|
|
109
|
+
<StorefrontNavbar {...base} logoAccent={{ text: 'ElectroCuba', className: 'text-brand-promo' }} />,
|
|
110
110
|
)
|
|
111
111
|
expect(html).toContain('<span class="text-brand-promo">ElectroCuba</span>')
|
|
112
112
|
})
|
|
113
113
|
|
|
114
114
|
it('does not accent when the segment is not a trailing match', () => {
|
|
115
|
-
const html = renderToStaticMarkup(<
|
|
115
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} logoAccent={{ text: 'Electro' }} />)
|
|
116
116
|
expect(html).toContain('>ElectroCuba</span>')
|
|
117
117
|
expect(html).not.toContain('<span class="">')
|
|
118
118
|
})
|
|
119
119
|
|
|
120
120
|
it('keeps the wordmark as the accessible text of the logo link', () => {
|
|
121
121
|
const html = renderToStaticMarkup(
|
|
122
|
-
<
|
|
122
|
+
<StorefrontNavbar {...base} logoHref='/' logoAccent={{ text: 'Cuba', className: 'text-brand-promo' }} />,
|
|
123
123
|
)
|
|
124
124
|
expect(html).toContain('href="/"')
|
|
125
125
|
expect(html).toContain('Electro<span class="text-brand-promo">Cuba</span>')
|
|
126
126
|
})
|
|
127
127
|
})
|
|
128
128
|
|
|
129
|
-
describe('
|
|
129
|
+
describe('StorefrontNavbar — optional utility strip', () => {
|
|
130
130
|
// The desktop utility strip is the only element with this inner track class.
|
|
131
131
|
const strip = (html: string) => html.includes('h-9 w-full max-w-7xl')
|
|
132
132
|
|
|
133
133
|
it('renders no utility strip when the note is empty and there are no utility links', () => {
|
|
134
134
|
const html = renderToStaticMarkup(
|
|
135
|
-
<
|
|
135
|
+
<StorefrontNavbar storeCategories={[]} shippingNote='' utilityLinks={[]} logoText='ElectroCuba' />,
|
|
136
136
|
)
|
|
137
137
|
expect(strip(html)).toBe(false)
|
|
138
138
|
})
|
|
139
139
|
|
|
140
140
|
it('renders no utility strip when the note is omitted', () => {
|
|
141
141
|
const html = renderToStaticMarkup(
|
|
142
|
-
<
|
|
142
|
+
<StorefrontNavbar storeCategories={[]} utilityLinks={[]} logoText='ElectroCuba' />,
|
|
143
143
|
)
|
|
144
144
|
expect(strip(html)).toBe(false)
|
|
145
145
|
})
|
|
146
146
|
|
|
147
147
|
it('treats a whitespace-only note as empty', () => {
|
|
148
148
|
const html = renderToStaticMarkup(
|
|
149
|
-
<
|
|
149
|
+
<StorefrontNavbar storeCategories={[]} shippingNote=' ' utilityLinks={[]} logoText='ElectroCuba' />,
|
|
150
150
|
)
|
|
151
151
|
expect(strip(html)).toBe(false)
|
|
152
152
|
})
|
|
153
153
|
|
|
154
154
|
it('renders the utility strip with a non-empty note', () => {
|
|
155
155
|
const html = renderToStaticMarkup(
|
|
156
|
-
<
|
|
156
|
+
<StorefrontNavbar storeCategories={[]} shippingNote='Entrega segura' utilityLinks={[]} logoText='ElectroCuba' />,
|
|
157
157
|
)
|
|
158
158
|
expect(strip(html)).toBe(true)
|
|
159
159
|
expect(html).toContain('Entrega segura')
|
|
@@ -161,7 +161,7 @@ describe('Navbar4 — optional utility strip', () => {
|
|
|
161
161
|
|
|
162
162
|
it('renders the utility strip when there is a utility link even with an empty note', () => {
|
|
163
163
|
const html = renderToStaticMarkup(
|
|
164
|
-
<
|
|
164
|
+
<StorefrontNavbar
|
|
165
165
|
storeCategories={[]}
|
|
166
166
|
shippingNote=''
|
|
167
167
|
utilityLinks={[{ label: 'Ayuda', href: '/help' }]}
|
|
@@ -180,9 +180,9 @@ const category = { href: '/herramientas', label: 'Herramientas' }
|
|
|
180
180
|
const navLinkClass = (html: string) =>
|
|
181
181
|
html.match(/<a href="\/herramientas" class="([^"]*hover:bg-muted[^"]*)"/)?.[1] ?? ''
|
|
182
182
|
|
|
183
|
-
describe('
|
|
183
|
+
describe('StorefrontNavbar — optional outer surface className', () => {
|
|
184
184
|
it('keeps the default surface (incl. backdrop-filter variant) and nav link color when className is omitted', () => {
|
|
185
|
-
const html = renderToStaticMarkup(<
|
|
185
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} storeCategories={[category]} />)
|
|
186
186
|
expect(html).toContain('bg-background/95')
|
|
187
187
|
expect(html).toContain('supports-backdrop-filter:bg-background/80')
|
|
188
188
|
expect(navLinkClass(html)).toContain('text-foreground')
|
|
@@ -191,7 +191,7 @@ describe('Navbar4 — optional outer surface className', () => {
|
|
|
191
191
|
|
|
192
192
|
it('gives the consumer full surface ownership when className is supplied (no default background or variant)', () => {
|
|
193
193
|
const html = renderToStaticMarkup(
|
|
194
|
-
<
|
|
194
|
+
<StorefrontNavbar {...base} storeCategories={[category]} className='bg-brand-nav text-brand-nav-foreground' />,
|
|
195
195
|
)
|
|
196
196
|
expect(html).toContain('bg-brand-nav')
|
|
197
197
|
expect(html).toContain('text-brand-nav-foreground')
|
|
@@ -205,7 +205,7 @@ describe('Navbar4 — optional outer surface className', () => {
|
|
|
205
205
|
|
|
206
206
|
it('lets nav links inherit the branded surface text color with a correct hover foreground', () => {
|
|
207
207
|
const html = renderToStaticMarkup(
|
|
208
|
-
<
|
|
208
|
+
<StorefrontNavbar {...base} storeCategories={[category]} className='bg-brand-nav text-brand-nav-foreground' />,
|
|
209
209
|
)
|
|
210
210
|
expect(navLinkClass(html)).toContain('text-inherit')
|
|
211
211
|
expect(navLinkClass(html)).toContain('hover:text-foreground')
|
|
@@ -215,15 +215,15 @@ describe('Navbar4 — optional outer surface className', () => {
|
|
|
215
215
|
const cartTrigger = (html: string) =>
|
|
216
216
|
html.match(/<button[^>]*aria-label="Cart,[^"]*"[^>]*>/)?.[0] ?? ''
|
|
217
217
|
|
|
218
|
-
describe('
|
|
218
|
+
describe('StorefrontNavbar — controlled cart sheet visibility', () => {
|
|
219
219
|
it('keeps the uncontrolled sheet closed by default (legacy consumers unaffected)', () => {
|
|
220
|
-
const html = renderToStaticMarkup(<
|
|
220
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} />)
|
|
221
221
|
expect(cartTrigger(html)).toContain('data-state="closed"')
|
|
222
222
|
})
|
|
223
223
|
|
|
224
224
|
it('reflects a controlled open state on the trigger', () => {
|
|
225
225
|
const html = renderToStaticMarkup(
|
|
226
|
-
<
|
|
226
|
+
<StorefrontNavbar {...base} cartOpen={true} onCartOpenChange={() => {}} />,
|
|
227
227
|
)
|
|
228
228
|
expect(cartTrigger(html)).toContain('data-state="open"')
|
|
229
229
|
expect(cartTrigger(html)).toContain('aria-expanded="true"')
|
|
@@ -231,23 +231,23 @@ describe('Navbar4 — controlled cart sheet visibility', () => {
|
|
|
231
231
|
|
|
232
232
|
it('reflects a controlled closed state on the trigger', () => {
|
|
233
233
|
const html = renderToStaticMarkup(
|
|
234
|
-
<
|
|
234
|
+
<StorefrontNavbar {...base} cartOpen={false} onCartOpenChange={() => {}} />,
|
|
235
235
|
)
|
|
236
236
|
expect(cartTrigger(html)).toContain('data-state="closed"')
|
|
237
237
|
expect(cartTrigger(html)).toContain('aria-expanded="false"')
|
|
238
238
|
})
|
|
239
239
|
})
|
|
240
240
|
|
|
241
|
-
describe('
|
|
241
|
+
describe('StorefrontNavbar — optional showSearch (E2 task 5.4)', () => {
|
|
242
242
|
it('renders the search control by default (legacy consumers unaffected)', () => {
|
|
243
|
-
const html = renderToStaticMarkup(<
|
|
243
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} />)
|
|
244
244
|
expect(html).toContain('role="search"')
|
|
245
245
|
expect(html).toContain('type="search"')
|
|
246
246
|
expect(html).toContain('aria-label="Search products"')
|
|
247
247
|
})
|
|
248
248
|
|
|
249
249
|
it('renders no search control when showSearch is false, keeping the rest of the chrome', () => {
|
|
250
|
-
const html = renderToStaticMarkup(<
|
|
250
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} showSearch={false} />)
|
|
251
251
|
expect(html).not.toContain('role="search"')
|
|
252
252
|
expect(html).not.toContain('type="search"')
|
|
253
253
|
expect(html).not.toContain('aria-label="Search products"')
|
|
@@ -257,7 +257,7 @@ describe('Navbar4 — optional showSearch (E2 task 5.4)', () => {
|
|
|
257
257
|
})
|
|
258
258
|
|
|
259
259
|
it('renders the search control when showSearch is explicitly true', () => {
|
|
260
|
-
const html = renderToStaticMarkup(<
|
|
260
|
+
const html = renderToStaticMarkup(<StorefrontNavbar {...base} showSearch={true} />)
|
|
261
261
|
expect(html).toContain('role="search"')
|
|
262
262
|
expect(html).toContain('type="search"')
|
|
263
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,7 @@ function CartBadge({ count }: CartBadgeProps) {
|
|
|
161
161
|
)
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
export interface
|
|
164
|
+
export interface StorefrontNavbarAccountRetryProps {
|
|
165
165
|
/** Consumer-owned reconciliation in flight; holds the affordance inert. */
|
|
166
166
|
pending?: boolean
|
|
167
167
|
/** Consumer-owned recovery action. Without it the affordance is inert. */
|
|
@@ -173,7 +173,7 @@ export interface Navbar4AccountRetryProps {
|
|
|
173
173
|
* session itself: it surfaces the state and dispatches the consumer's retry.
|
|
174
174
|
* Extracted (hooks-free) so the dispatch is testable without a DOM.
|
|
175
175
|
*/
|
|
176
|
-
export function
|
|
176
|
+
export function StorefrontNavbarAccountRetry({ pending = false, onRetry }: StorefrontNavbarAccountRetryProps) {
|
|
177
177
|
return (
|
|
178
178
|
<Button
|
|
179
179
|
variant='ghost'
|
|
@@ -189,7 +189,7 @@ export function Navbar4AccountRetry({ pending = false, onRetry }: Navbar4Account
|
|
|
189
189
|
)
|
|
190
190
|
}
|
|
191
191
|
|
|
192
|
-
export interface
|
|
192
|
+
export interface StorefrontNavbarProps {
|
|
193
193
|
storeCategories: StoreCategory[]
|
|
194
194
|
/**
|
|
195
195
|
* Optional note shown in the desktop utility strip. When omitted (or empty)
|
|
@@ -285,7 +285,7 @@ export interface Navbar4Props {
|
|
|
285
285
|
className?: string
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
export function
|
|
288
|
+
export function StorefrontNavbar({
|
|
289
289
|
storeCategories,
|
|
290
290
|
shippingNote = '',
|
|
291
291
|
utilityLinks,
|
|
@@ -323,7 +323,7 @@ export function Navbar4({
|
|
|
323
323
|
checkoutDisabled,
|
|
324
324
|
viewCartUnavailable,
|
|
325
325
|
className,
|
|
326
|
-
}:
|
|
326
|
+
}: StorefrontNavbarProps) {
|
|
327
327
|
/**
|
|
328
328
|
* Integration seam
|
|
329
329
|
* ----------------
|
|
@@ -495,7 +495,7 @@ export function Navbar4({
|
|
|
495
495
|
{showSearch ? (
|
|
496
496
|
<>
|
|
497
497
|
<div className='hidden lg:block lg:w-64'>
|
|
498
|
-
<
|
|
498
|
+
<StorefrontNavbarSearchBar
|
|
499
499
|
id='store-search'
|
|
500
500
|
value={searchQuery}
|
|
501
501
|
onValueChange={setSearchQuery}
|
|
@@ -517,9 +517,9 @@ export function Navbar4({
|
|
|
517
517
|
) : null}
|
|
518
518
|
|
|
519
519
|
{accountUnavailable ? (
|
|
520
|
-
<
|
|
520
|
+
<StorefrontNavbarAccountRetry pending={accountPending} onRetry={onAccountRetry} />
|
|
521
521
|
) : user ? (
|
|
522
|
-
<
|
|
522
|
+
<StorefrontNavbarAccountMenu
|
|
523
523
|
user={user}
|
|
524
524
|
items={accountMenuItems}
|
|
525
525
|
onHelp={onHelp}
|
|
@@ -560,7 +560,7 @@ export function Navbar4({
|
|
|
560
560
|
Cart{count > 0 ? ` (${count})` : ''}
|
|
561
561
|
</SheetTitle>
|
|
562
562
|
</SheetHeader>
|
|
563
|
-
<
|
|
563
|
+
<StorefrontNavbarCartSheet
|
|
564
564
|
items={items}
|
|
565
565
|
onQtyChange={changeQty}
|
|
566
566
|
onRemove={removeItem}
|
|
@@ -589,7 +589,7 @@ export function Navbar4({
|
|
|
589
589
|
|
|
590
590
|
{showSearch && searchOpen ? (
|
|
591
591
|
<div className='border-border flex flex-col gap-0 border-t p-4 lg:hidden'>
|
|
592
|
-
<
|
|
592
|
+
<StorefrontNavbarSearchBar
|
|
593
593
|
id='store-search-mobile'
|
|
594
594
|
value={searchQuery}
|
|
595
595
|
onValueChange={setSearchQuery}
|
|
@@ -601,4 +601,4 @@ export function Navbar4({
|
|
|
601
601
|
)
|
|
602
602
|
}
|
|
603
603
|
|
|
604
|
-
export default
|
|
604
|
+
export default StorefrontNavbar
|