@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.
Files changed (178) hide show
  1. package/CHANGELOG.md +155 -0
  2. package/README.md +228 -68
  3. package/docs/standards/data-contracts.md +13 -2
  4. package/package.json +26 -10
  5. package/scripts/fix-esm-extensions.mjs +40 -0
  6. package/src/blocks/application/login-forms/login-page.test.tsx +190 -0
  7. package/src/blocks/application/login-forms/login-page.tsx +240 -0
  8. package/src/blocks/application/password-recovery/forgot-password-page-1.tsx +6 -8
  9. package/src/blocks/application/signup-forms/signup-page-1.tsx +127 -129
  10. package/src/blocks/application/verification/verify-1.tsx +2 -2
  11. package/src/blocks/application/viewport-ownership.test.tsx +41 -0
  12. package/src/blocks/e-commerce/category-filters/category-listing-filter-fields.tsx +218 -0
  13. package/src/blocks/e-commerce/category-filters/{category-filter-2.test.tsx → category-listing.test.tsx} +287 -199
  14. package/src/blocks/e-commerce/category-filters/category-listing.tsx +934 -0
  15. package/src/blocks/e-commerce/checkout-forms/checkout-contact-step.tsx +149 -0
  16. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-country-field.tsx → checkout-country-field.tsx} +10 -8
  17. package/src/blocks/e-commerce/checkout-forms/checkout-order-summary.tsx +230 -0
  18. package/src/blocks/e-commerce/checkout-forms/checkout-payment-step.tsx +100 -0
  19. package/src/blocks/e-commerce/checkout-forms/checkout-progress.tsx +62 -0
  20. package/src/blocks/e-commerce/checkout-forms/checkout-review-step.tsx +59 -0
  21. package/src/blocks/e-commerce/checkout-forms/{checkout-form-1-shipping-step.tsx → checkout-shipping-step.tsx} +65 -41
  22. package/src/blocks/e-commerce/checkout-forms/checkout.test.tsx +478 -0
  23. package/src/blocks/e-commerce/checkout-forms/checkout.tsx +550 -0
  24. package/src/blocks/e-commerce/navigation/storefront-navbar-account-menu.test.tsx +56 -0
  25. package/src/blocks/{marketing/navbars/navbar-4-account-menu.tsx → e-commerce/navigation/storefront-navbar-account-menu.tsx} +3 -3
  26. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.test.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.test.tsx} +22 -22
  27. package/src/blocks/{marketing/navbars/navbar-4-cart-sheet.tsx → e-commerce/navigation/storefront-navbar-cart-sheet.tsx} +3 -3
  28. package/src/blocks/{marketing/navbars/navbar-4-mobile-nav.tsx → e-commerce/navigation/storefront-navbar-mobile-nav.tsx} +4 -12
  29. package/src/blocks/{marketing/navbars/navbar-4-search-bar.tsx → e-commerce/navigation/storefront-navbar-search-bar.tsx} +3 -3
  30. package/src/blocks/{marketing/navbars/navbar-4.test.tsx → e-commerce/navigation/storefront-navbar.test.tsx} +67 -36
  31. package/src/blocks/{marketing/navbars/navbar-4.tsx → e-commerce/navigation/storefront-navbar.tsx} +43 -25
  32. package/src/blocks/e-commerce/product-categories/category-card-grid.test.tsx +122 -0
  33. package/src/blocks/e-commerce/product-categories/category-card.test.tsx +68 -0
  34. package/src/blocks/e-commerce/product-categories/category-card.tsx +172 -0
  35. package/src/blocks/e-commerce/product-categories/category-grid.test.tsx +67 -0
  36. package/src/blocks/e-commerce/product-categories/category-shared.tsx +109 -0
  37. package/src/blocks/e-commerce/product-categories/product-category-grid.tsx +170 -0
  38. package/src/blocks/e-commerce/product-categories/product-category.test.tsx +266 -0
  39. package/src/blocks/e-commerce/product-categories/product-category.tsx +462 -0
  40. package/src/blocks/e-commerce/product-list/product-card.test.tsx +263 -0
  41. package/src/blocks/e-commerce/product-list/product-card.tsx +447 -0
  42. package/src/blocks/e-commerce/product-list/product-grid.test.tsx +162 -0
  43. package/src/blocks/e-commerce/product-list/product-grid.tsx +114 -0
  44. package/src/blocks/e-commerce/product-overview/product-overview-gallery.tsx +71 -0
  45. package/src/blocks/e-commerce/product-overview/{product-overview-5-lightbox-tab.tsx → product-overview-lightbox-tab.tsx} +35 -23
  46. package/src/blocks/e-commerce/product-overview/{product-overview-5-reviews-tab.tsx → product-overview-reviews-tab.tsx} +40 -17
  47. package/src/blocks/e-commerce/product-overview/{product-overview-5-tabs.tsx → product-overview-tabs.tsx} +8 -7
  48. package/src/blocks/e-commerce/product-overview/product-overview.test.tsx +283 -0
  49. package/src/blocks/e-commerce/product-overview/product-overview.tsx +1186 -0
  50. package/src/blocks/e-commerce/reviews/review-rating-2.tsx +5 -6
  51. package/src/blocks/e-commerce/shopping-carts/shopping-cart.test.tsx +399 -0
  52. package/src/blocks/e-commerce/shopping-carts/{shopping-cart-4.tsx → shopping-cart.tsx} +119 -85
  53. package/src/blocks/e-commerce/storefront-hero/storefront-hero.test.tsx +412 -0
  54. package/src/blocks/e-commerce/storefront-hero/storefront-hero.tsx +1283 -0
  55. package/src/blocks/marketing/features/feature.test.tsx +153 -0
  56. package/src/blocks/marketing/features/feature.tsx +195 -0
  57. package/src/blocks/marketing/footers/footer-section-accessibility.test.tsx +118 -0
  58. package/src/blocks/marketing/footers/footer-section-actions.test.tsx +131 -0
  59. package/src/blocks/marketing/footers/footer-section-data.test.tsx +127 -0
  60. package/src/blocks/marketing/footers/footer-section-native-links.test.tsx +100 -0
  61. package/src/blocks/marketing/footers/footer-section-public-api.test.tsx +11 -0
  62. package/src/blocks/marketing/footers/footer-section.test.tsx +46 -0
  63. package/src/blocks/marketing/footers/footer-section.tsx +532 -0
  64. package/src/blocks/marketing/navbars/navbar-public-api.test.tsx +57 -0
  65. package/src/blocks/marketing/navbars/navbar.mega.test.tsx +149 -0
  66. package/src/blocks/marketing/navbars/navbar.test.tsx +155 -0
  67. package/src/blocks/marketing/navbars/navbar.tsx +735 -0
  68. package/src/blocks/marketing/testimonials/testimonial-section.test.tsx +171 -0
  69. package/src/blocks/marketing/testimonials/testimonial-section.tsx +237 -0
  70. package/src/index.ts +156 -128
  71. package/src/lib/contracts-curated.test.ts +47 -29
  72. package/src/lib/contracts.test.ts +198 -2
  73. package/src/lib/contracts.ts +410 -16
  74. package/src/ui/button-group.tsx +1 -1
  75. package/src/ui/carousel.tsx +1 -1
  76. package/src/ui/dialog.tsx +1 -1
  77. package/src/ui/field.tsx +2 -2
  78. package/src/ui/input-group.tsx +3 -3
  79. package/src/ui/pagination.tsx +1 -1
  80. package/src/ui/sidebar.tsx +7 -7
  81. package/src/ui/toggle-group.tsx +1 -1
  82. package/tsconfig.build.json +12 -0
  83. package/tsup.config.ts +11 -7
  84. package/.turbo/turbo-build.log +0 -24
  85. package/.turbo/turbo-test.log +0 -47
  86. package/.turbo/turbo-type-check.log +0 -0
  87. package/dist/.tsbuildinfo +0 -1
  88. package/dist/contracts.d.mts +0 -1732
  89. package/dist/contracts.d.ts +0 -1732
  90. package/dist/contracts.js +0 -423
  91. package/dist/contracts.js.map +0 -1
  92. package/dist/contracts.mjs +0 -365
  93. package/dist/contracts.mjs.map +0 -1
  94. package/dist/index.d.mts +0 -3068
  95. package/dist/index.d.ts +0 -3068
  96. package/dist/index.js +0 -17834
  97. package/dist/index.js.map +0 -1
  98. package/dist/index.mjs +0 -17740
  99. package/dist/index.mjs.map +0 -1
  100. package/src/blocks/application/login-forms/login-page-1.tsx +0 -246
  101. package/src/blocks/application/login-forms/login-page-2.tsx +0 -207
  102. package/src/blocks/e-commerce/category-filters/category-filter-1.tsx +0 -260
  103. package/src/blocks/e-commerce/category-filters/category-filter-2-filter-panel.tsx +0 -222
  104. package/src/blocks/e-commerce/category-filters/category-filter-2.tsx +0 -921
  105. package/src/blocks/e-commerce/category-filters/category-filter-4-chip-rail.tsx +0 -57
  106. package/src/blocks/e-commerce/category-filters/category-filter-4-drawer.tsx +0 -124
  107. package/src/blocks/e-commerce/category-filters/category-filter-4-fields.tsx +0 -170
  108. package/src/blocks/e-commerce/category-filters/category-filter-4-sidebar.tsx +0 -52
  109. package/src/blocks/e-commerce/category-filters/category-filter-4-sort-sheet.tsx +0 -57
  110. package/src/blocks/e-commerce/category-filters/category-filter-4-state.ts +0 -68
  111. package/src/blocks/e-commerce/category-filters/category-filter-4.tsx +0 -298
  112. package/src/blocks/e-commerce/checkout-forms/card-formatting.test.ts +0 -47
  113. package/src/blocks/e-commerce/checkout-forms/card-formatting.ts +0 -9
  114. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-contact-step.tsx +0 -96
  115. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-order-summary.tsx +0 -252
  116. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-payment-step.tsx +0 -188
  117. package/src/blocks/e-commerce/checkout-forms/checkout-form-1-progress.tsx +0 -36
  118. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.test.tsx +0 -216
  119. package/src/blocks/e-commerce/checkout-forms/checkout-form-1.tsx +0 -338
  120. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-order-summary.tsx +0 -130
  121. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-payment-card.tsx +0 -175
  122. package/src/blocks/e-commerce/checkout-forms/checkout-form-2-shipping-card.tsx +0 -190
  123. package/src/blocks/e-commerce/checkout-forms/checkout-form-2.tsx +0 -312
  124. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-information-step.tsx +0 -91
  125. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-order-summary.tsx +0 -110
  126. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-payment-step.tsx +0 -137
  127. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-progress.tsx +0 -68
  128. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-review-step.tsx +0 -79
  129. package/src/blocks/e-commerce/checkout-forms/checkout-form-3-shipping-step.tsx +0 -172
  130. package/src/blocks/e-commerce/checkout-forms/checkout-form-3.tsx +0 -392
  131. package/src/blocks/e-commerce/product-categories/avatar-group.tsx +0 -86
  132. package/src/blocks/e-commerce/product-categories/product-category-1.tsx +0 -126
  133. package/src/blocks/e-commerce/product-categories/product-category-2.test.tsx +0 -51
  134. package/src/blocks/e-commerce/product-categories/product-category-2.tsx +0 -120
  135. package/src/blocks/e-commerce/product-categories/product-category-4.tsx +0 -129
  136. package/src/blocks/e-commerce/product-categories/product-category-5.tsx +0 -218
  137. package/src/blocks/e-commerce/product-list/product-card-1.test.tsx +0 -88
  138. package/src/blocks/e-commerce/product-list/product-card-1.tsx +0 -192
  139. package/src/blocks/e-commerce/product-list/product-card-2.tsx +0 -71
  140. package/src/blocks/e-commerce/product-list/product-card-3-reaction-toggle.tsx +0 -166
  141. package/src/blocks/e-commerce/product-list/product-card-3.tsx +0 -215
  142. package/src/blocks/e-commerce/product-list/product-card-5-checkbox-check.tsx +0 -98
  143. package/src/blocks/e-commerce/product-list/product-card-5-reaction-toggle.tsx +0 -171
  144. package/src/blocks/e-commerce/product-list/product-card-5.tsx +0 -367
  145. package/src/blocks/e-commerce/product-overview/product-overview-1.test.tsx +0 -182
  146. package/src/blocks/e-commerce/product-overview/product-overview-1.tsx +0 -370
  147. package/src/blocks/e-commerce/product-overview/product-overview-2.tsx +0 -272
  148. package/src/blocks/e-commerce/product-overview/product-overview-5-gallery.tsx +0 -59
  149. package/src/blocks/e-commerce/product-overview/product-overview-5.tsx +0 -274
  150. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1-spinning-counter.tsx +0 -142
  151. package/src/blocks/e-commerce/shopping-carts/shopping-cart-1.tsx +0 -291
  152. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-item-row.tsx +0 -110
  153. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-recommendations.tsx +0 -51
  154. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2-spinning-counter.tsx +0 -146
  155. package/src/blocks/e-commerce/shopping-carts/shopping-cart-2.tsx +0 -295
  156. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3-spinning-counter.tsx +0 -142
  157. package/src/blocks/e-commerce/shopping-carts/shopping-cart-3.tsx +0 -342
  158. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4-spinning-counter.tsx +0 -146
  159. package/src/blocks/e-commerce/shopping-carts/shopping-cart-4.test.tsx +0 -229
  160. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.test.tsx +0 -72
  161. package/src/blocks/e-commerce/storefront-hero/storefront-hero-1.tsx +0 -263
  162. package/src/blocks/e-commerce/storefront-hero/storefront-hero-2.tsx +0 -283
  163. package/src/blocks/e-commerce/storefront-hero/storefront-hero-5.tsx +0 -318
  164. package/src/blocks/e-commerce/storefront-hero/storefront-hero-6.tsx +0 -205
  165. package/src/blocks/marketing/features/feature-1.tsx +0 -97
  166. package/src/blocks/marketing/features/feature-2.tsx +0 -82
  167. package/src/blocks/marketing/footers/footer-section-1.test.tsx +0 -117
  168. package/src/blocks/marketing/footers/footer-section-1.tsx +0 -178
  169. package/src/blocks/marketing/footers/footer-section-3.tsx +0 -192
  170. package/src/blocks/marketing/footers/footer-section-4.tsx +0 -355
  171. package/src/blocks/marketing/navbars/navbar-1.tsx +0 -267
  172. package/src/blocks/marketing/navbars/navbar-2.tsx +0 -351
  173. package/src/blocks/marketing/navbars/navbar-3.tsx +0 -303
  174. package/src/blocks/marketing/navbars/navbar-4-account-menu.test.tsx +0 -38
  175. package/src/blocks/marketing/navbars/navbar-4-like-button.tsx +0 -177
  176. package/src/blocks/marketing/navbars/navbar-4-product-grid.tsx +0 -219
  177. package/src/blocks/marketing/testimonials/testimonial-1.tsx +0 -141
  178. package/src/blocks/marketing/testimonials/testimonial-2.tsx +0 -108
@@ -1,303 +0,0 @@
1
- 'use client'
2
-
3
- import { useEffect, useRef, useState, type ComponentProps, type Ref } from 'react'
4
- import { Menu } from 'lucide-react'
5
-
6
- import { Button } from '../../../ui/button'
7
- import { Separator } from '../../../ui/separator'
8
- import {
9
- Sheet,
10
- SheetClose,
11
- SheetContent,
12
- SheetHeader,
13
- SheetTitle,
14
- SheetTrigger,
15
- } from '../../../ui/sheet'
16
- import { cn } from '../../../lib/utils'
17
- import type { CTA } from '../../../lib/types'
18
-
19
- export interface NavLink {
20
- label: string
21
- href?: string
22
- }
23
-
24
- export interface Navbar3Props {
25
- navLinks: NavLink[]
26
- /** Brand text shown next to the logo mark. */
27
- logoText: string
28
- logoHref?: string
29
- /** Current path used to highlight the active nav link. */
30
- currentPath?: string
31
- /** Sign-in action. Rendered as a link when `href` is set, otherwise as a button. */
32
- signIn?: CTA
33
- /** Primary call-to-action. Rendered as a link when `href` is set, otherwise as a button. */
34
- cta?: CTA
35
- /**
36
- * When `true` (default), the header condenses into a floating rounded pill once scrolled.
37
- * Set to `false` for a plain solid bar instead of the condensing pill.
38
- */
39
- floatingPill?: boolean
40
- /** Fired when the sign-in button (no `href`) is clicked. */
41
- onSignIn?: () => void
42
- /** Fired when the CTA button (no `href`) is clicked. */
43
- onCtaClick?: () => void
44
- }
45
-
46
- /**
47
- * Composition contract
48
- * --------------------
49
- * The header is `fixed`, so the first section of the page must clear it with `pt-16`,
50
- * and `html { scroll-padding-top: 4rem }` keeps in-page anchors from landing underneath it.
51
- *
52
- * Set `floatingPill` to false for a plain solid bar on scroll instead of the condensing pill.
53
- */
54
- function Logo({ text, href }: { text: string; href?: string }) {
55
- const className =
56
- 'focus-visible:ring-ring flex items-center gap-2 rounded-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none'
57
-
58
- const content = (
59
- <>
60
- <svg viewBox='0 0 32 32' aria-hidden='true' className='text-primary size-7' fill='none' xmlns='http://www.w3.org/2000/svg'>
61
- <path
62
- d='M26 24.75C26.4142 24.75 26.75 24.4142 26.75 24C26.75 23.5858 26.4142 23.25 26 23.25V24.75ZM26 23.25H11V24.75H26V23.25ZM8.75 21V15H7.25V21H8.75ZM11 23.25C9.75736 23.25 8.75 22.2426 8.75 21H7.25C7.25 23.0711 8.92893 24.75 11 24.75V23.25Z'
63
- fill='currentColor'
64
- />
65
- <path
66
- d='M1.5 3.25C1.08579 3.25 0.75 3.58579 0.75 4C0.75 4.41421 1.08579 4.75 1.5 4.75V3.25ZM1.5 4.75H6V3.25H1.5V4.75ZM7.25 6V21H8.75V6H7.25ZM6 4.75C6.69036 4.75 7.25 5.30964 7.25 6H8.75C8.75 4.48122 7.51878 3.25 6 3.25V4.75Z'
67
- fill='currentColor'
68
- />
69
- <path
70
- d='M22 21.75C22.4142 21.75 22.75 21.4142 22.75 21C22.75 20.5858 22.4142 20.25 22 20.25V21.75ZM22 20.25H11V21.75H22V20.25ZM8.75 18V12H7.25V18H8.75ZM11 20.25C9.75736 20.25 8.75 19.2426 8.75 18H7.25C7.25 20.0711 8.92893 21.75 11 21.75V20.25Z'
71
- fill='currentColor'
72
- />
73
- <path
74
- d='M27.2057 19.754C27.0654 20.1438 26.6357 20.346 26.246 20.2057C25.8562 20.0654 25.654 19.6357 25.7943 19.246L27.2057 19.754ZM30.0361 9.67744L29.3305 9.4234L29.3305 9.4234L30.0361 9.67744ZM25.7943 19.246L29.3305 9.4234L30.7418 9.93148L27.2057 19.754L25.7943 19.246ZM28.1543 7.75L8 7.75V6.25L28.1543 6.25V7.75ZM29.3305 9.4234C29.6237 8.60882 29.0201 7.75 28.1543 7.75V6.25C30.059 6.25 31.3869 8.13941 30.7418 9.93148L29.3305 9.4234Z'
75
- fill='currentColor'
76
- />
77
- <path
78
- d='M13.5 21.75C13.0858 21.75 12.75 21.4142 12.75 21C12.75 20.5858 13.0858 20.25 13.5 20.25V21.75ZM26.7111 19.009L27.4174 19.2613L27.4174 19.2613L26.7111 19.009ZM13.5 20.25H23.8858V21.75H13.5V20.25ZM26.0048 18.7568L27.7937 13.7477L29.2063 14.2523L27.4174 19.2613L26.0048 18.7568ZM23.8858 20.25C24.8367 20.25 25.6849 19.6522 26.0048 18.7568L27.4174 19.2613C26.8843 20.7537 25.4706 21.75 23.8858 21.75V20.25Z'
79
- fill='currentColor'
80
- />
81
- <path d='M21.1694 10.5806L14.5651 17.1849' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' />
82
- <path d='M22.1694 14.5806L18.5632 18.1868' stroke='currentColor' strokeWidth='1.5' strokeLinecap='round' />
83
- <circle cx='13.1' cy='26.1' r='1.7' stroke='currentColor' strokeWidth='1.2' />
84
- <circle cx='22.1' cy='26.1' r='1.7' stroke='currentColor' strokeWidth='1.2' />
85
- </svg>
86
- <span className='text-base font-semibold tracking-tight'>{text}</span>
87
- </>
88
- )
89
-
90
- if (!href) {
91
- return <span className={className}>{content}</span>
92
- }
93
-
94
- return (
95
- <a href={href} className={className}>
96
- {content}
97
- </a>
98
- )
99
- }
100
-
101
- function ActionButton({
102
- cta,
103
- onClick,
104
- variant,
105
- className,
106
- rounded,
107
- ref,
108
- }: {
109
- cta: CTA
110
- onClick?: () => void
111
- variant?: ComponentProps<typeof Button>['variant']
112
- className?: string
113
- rounded?: boolean
114
- ref?: Ref<HTMLButtonElement>
115
- }) {
116
- const classes = cn(rounded && 'rounded-full', className)
117
-
118
- if (cta.href && !onClick) {
119
- return (
120
- <Button variant={variant} size='sm' className={classes} asChild ref={ref}>
121
- <a href={cta.href}>{cta.label}</a>
122
- </Button>
123
- )
124
- }
125
-
126
- return (
127
- <Button variant={variant} size='sm' className={classes} onClick={onClick} ref={ref}>
128
- {cta.label}
129
- </Button>
130
- )
131
- }
132
-
133
- export function Navbar3({
134
- navLinks,
135
- logoText,
136
- logoHref,
137
- currentPath,
138
- signIn,
139
- cta,
140
- floatingPill = true,
141
- onSignIn,
142
- onCtaClick,
143
- }: Navbar3Props) {
144
- const sentinelRef = useRef<HTMLDivElement>(null)
145
- const [scrolled, setScrolled] = useState(false)
146
- const [open, setOpen] = useState(false)
147
-
148
- /**
149
- * Scroll detection:
150
- * - Inside the store preview the scroller is an overflow ancestor (not the viewport).
151
- * IntersectionObserver + an out-of-flow sentinel is unreliable there, so use scrollTop.
152
- * - On a normal page, an IntersectionObserver on the 8px sentinel gives hysteresis without
153
- * flickering at a hard `scrollY > 8` threshold.
154
- */
155
- useEffect(() => {
156
- const sentinel = sentinelRef.current
157
- if (!sentinel) {
158
- return
159
- }
160
-
161
- let scrollParent: HTMLElement | null = null
162
- let ancestor: HTMLElement | null = sentinel.parentElement
163
- while (ancestor) {
164
- const { overflowY } = getComputedStyle(ancestor)
165
- if (
166
- (overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay') &&
167
- ancestor.scrollHeight > ancestor.clientHeight + 1
168
- ) {
169
- scrollParent = ancestor
170
- break
171
- }
172
- ancestor = ancestor.parentElement
173
- }
174
-
175
- if (scrollParent) {
176
- const scroller = scrollParent
177
- const update = () => setScrolled(scroller.scrollTop > 8)
178
- update()
179
- scroller.addEventListener('scroll', update, { passive: true })
180
- return () => scroller.removeEventListener('scroll', update)
181
- }
182
-
183
- const observer = new IntersectionObserver(([entry]) => setScrolled(!entry.isIntersecting), {
184
- threshold: 0,
185
- })
186
-
187
- observer.observe(sentinel)
188
- return () => observer.disconnect()
189
- }, [])
190
-
191
- return (
192
- <>
193
- {/*
194
- Absolute + out of flow: keeps an 8px hysteresis target for the observer
195
- without painting a blank strip under the transparent (unscrolled) header.
196
- */}
197
- <div
198
- ref={sentinelRef}
199
- aria-hidden='true'
200
- className='pointer-events-none absolute inset-x-0 top-0 h-2'
201
- />
202
-
203
- <header className='fixed inset-x-0 top-0 z-50'>
204
- <div
205
- className={cn(
206
- 'mx-auto flex h-14 items-center justify-between gap-4 transition-all duration-300 ease-out motion-reduce:transition-none',
207
- scrolled
208
- ? floatingPill
209
- ? 'border-border/60 bg-background/80 mt-3 max-w-5xl rounded-full border px-4 shadow-sm backdrop-blur-md sm:px-5'
210
- : 'border-border/60 bg-background/90 max-w-full border-b px-4 backdrop-blur-md sm:px-6 lg:px-8'
211
- : 'max-w-7xl border border-transparent px-4 sm:px-6 lg:px-8'
212
- )}
213
- >
214
- <Logo text={logoText} href={logoHref} />
215
-
216
- <nav aria-label='Main' className='hidden lg:block'>
217
- <ul className='flex items-center gap-1'>
218
- {navLinks.map(link => {
219
- const isActive = link.href !== undefined && link.href === currentPath
220
- const className = cn(
221
- 'hover:text-foreground focus-visible:ring-ring rounded-full px-3 py-2 text-sm font-medium transition-colors duration-150 ease-out focus-visible:ring-2 focus-visible:outline-none',
222
- isActive ? 'text-foreground' : 'text-muted-foreground'
223
- )
224
-
225
- return (
226
- <li key={link.label}>
227
- {link.href ? (
228
- <a href={link.href} aria-current={isActive ? 'page' : undefined} className={className}>
229
- {link.label}
230
- </a>
231
- ) : (
232
- <span className={className}>{link.label}</span>
233
- )}
234
- </li>
235
- )
236
- })}
237
- </ul>
238
- </nav>
239
-
240
- <div className='hidden items-center gap-2 lg:flex'>
241
- {signIn && <ActionButton cta={signIn} onClick={onSignIn} variant='ghost' />}
242
- {cta && <ActionButton cta={cta} onClick={onCtaClick} rounded={floatingPill} />}
243
- </div>
244
-
245
- <Sheet open={open} onOpenChange={setOpen}>
246
- <SheetTrigger asChild>
247
- <Button variant='ghost' size='icon' aria-label='Open menu' className='lg:hidden'>
248
- <Menu />
249
- </Button>
250
- </SheetTrigger>
251
- <SheetContent side='right' className='w-full max-w-xs p-0'>
252
- <SheetHeader className='border-border border-b'>
253
- <SheetTitle className='text-left text-base'>Menu</SheetTitle>
254
- </SheetHeader>
255
-
256
- <nav aria-label='Mobile' className='flex flex-col px-2 py-2'>
257
- {navLinks.map(link => {
258
- const isActive = link.href !== undefined && link.href === currentPath
259
- const className = cn(
260
- 'hover:bg-accent hover:text-accent-foreground focus-visible:ring-ring rounded-md px-3 py-3 text-base font-medium transition-colors duration-150 ease-out focus-visible:ring-2 focus-visible:outline-none',
261
- isActive ? 'text-foreground' : 'text-muted-foreground'
262
- )
263
-
264
- return (
265
- <SheetClose key={link.label} asChild>
266
- {link.href ? (
267
- <a href={link.href} aria-current={isActive ? 'page' : undefined} className={className}>
268
- {link.label}
269
- </a>
270
- ) : (
271
- <button type='button' className={className}>
272
- {link.label}
273
- </button>
274
- )}
275
- </SheetClose>
276
- )
277
- })}
278
- </nav>
279
-
280
- <div className='mt-auto flex flex-col gap-4 p-4'>
281
- <Separator />
282
- <div className='flex flex-col gap-2'>
283
- {signIn && (
284
- <SheetClose asChild>
285
- <ActionButton cta={signIn} onClick={onSignIn} variant='outline' className='w-full' />
286
- </SheetClose>
287
- )}
288
- {cta && (
289
- <SheetClose asChild>
290
- <ActionButton cta={cta} onClick={onCtaClick} className='w-full' />
291
- </SheetClose>
292
- )}
293
- </div>
294
- </div>
295
- </SheetContent>
296
- </Sheet>
297
- </div>
298
- </header>
299
- </>
300
- )
301
- }
302
-
303
- export default Navbar3
@@ -1,38 +0,0 @@
1
- import { Children, isValidElement, type ReactNode } from 'react'
2
- import { describe, expect, it } from 'vitest'
3
- import { Navbar4AccountMenu } from './navbar-4-account-menu'
4
- import { DropdownMenuItem } from '../../../ui/dropdown-menu'
5
-
6
- // Inspect the block-owned menu items without mounting Radix's browser portal.
7
- function menuItems(node: ReactNode): Record<string, unknown>[] {
8
- return Children.toArray(node).flatMap(child => {
9
- if (!isValidElement<{ children?: ReactNode }>(child)) return []
10
- return [
11
- ...(child.type === DropdownMenuItem ? [child.props as Record<string, unknown>] : []),
12
- ...menuItems(child.props.children),
13
- ]
14
- })
15
- }
16
-
17
- const user = { name: 'Ada', email: 'ada@example.test', initials: 'A' }
18
-
19
- describe('Navbar4AccountMenu consumer actions', () => {
20
- it('omits help and sign-out when no consumer actions exist', () => {
21
- expect(menuItems(Navbar4AccountMenu({ user }))).toEqual([])
22
- })
23
-
24
- it('dispatches sign-out to its consumer', () => {
25
- let calls = 0
26
- const [item] = menuItems(Navbar4AccountMenu({ user, onSignOut: () => { calls++ } }))
27
- ;(item.onSelect as () => void)()
28
- expect(calls).toBe(1)
29
- })
30
-
31
- it('rejects further sign-out selection while the consumer is pending', () => {
32
- let calls = 0
33
- const [item] = menuItems(Navbar4AccountMenu({ user, pending: true, onSignOut: () => { calls++ } }))
34
- expect(item.disabled).toBe(true)
35
- ;(item.onSelect as () => void)()
36
- expect(calls).toBe(0)
37
- })
38
- })
@@ -1,177 +0,0 @@
1
- 'use client'
2
-
3
- import { useRef, useState } from 'react'
4
- import { useReducedMotion } from 'motion/react'
5
-
6
- import { Button } from '../../../ui/button'
7
- import { cn } from '../../../lib/utils'
8
-
9
- const PARTICLE_COUNT = 8
10
- const PARTICLE_DIST = 20
11
-
12
- export interface Navbar4LikeButtonProps {
13
- liked?: boolean
14
- onLikedChange?: (liked: boolean) => void
15
- className?: string
16
- 'aria-label'?: string
17
- }
18
-
19
- /**
20
- * Heart like control — Transitions.dev `like-button`:
21
- * fill + spring pop on an HTML wrapper (never the SVG) + 8-dot particle burst.
22
- */
23
- export function Navbar4LikeButton({
24
- liked: likedProp,
25
- onLikedChange,
26
- className,
27
- 'aria-label': ariaLabel,
28
- }: Navbar4LikeButtonProps) {
29
- const [uncontrolledLiked, setUncontrolledLiked] = useState(false)
30
- const [bursting, setBursting] = useState(false)
31
- const particlesRef = useRef<HTMLSpanElement>(null)
32
- const reduceMotion = useReducedMotion()
33
-
34
- const liked = likedProp ?? uncontrolledLiked
35
-
36
- const setLiked = (next: boolean) => {
37
- onLikedChange?.(next)
38
- if (likedProp === undefined) {
39
- setUncontrolledLiked(next)
40
- }
41
- }
42
-
43
- const seedParticles = () => {
44
- const dots = particlesRef.current?.querySelectorAll<HTMLElement>('i')
45
- if (!dots) {
46
- return
47
- }
48
-
49
- dots.forEach((dot, index) => {
50
- const angle = (360 / PARTICLE_COUNT) * index + (Math.random() * 2 - 1) * 16
51
- const mag = PARTICLE_DIST * (0.68 + Math.random() * 0.5)
52
- const rad = (angle * Math.PI) / 180
53
- dot.style.setProperty('--px', `${(Math.cos(rad) * mag).toFixed(2)}px`)
54
- dot.style.setProperty('--py', `${(Math.sin(rad) * mag).toFixed(2)}px`)
55
- dot.style.setProperty('--pdur', `${Math.round(600 * (0.78 + Math.random() * 0.44))}ms`)
56
- dot.style.setProperty('--pdelay', `${Math.round(Math.random() * 70)}ms`)
57
- dot.style.setProperty('--p-end-scale', (0.35 + Math.random() * 0.4).toFixed(2))
58
- dot.style.setProperty('--psize', (0.6 + Math.random() * 0.8).toFixed(2))
59
- })
60
- }
61
-
62
- const toggle = () => {
63
- if (liked) {
64
- setLiked(false)
65
- setBursting(false)
66
- return
67
- }
68
-
69
- setLiked(true)
70
- if (reduceMotion) {
71
- return
72
- }
73
-
74
- setBursting(false)
75
- seedParticles()
76
- requestAnimationFrame(() => setBursting(true))
77
- }
78
-
79
- return (
80
- <Button
81
- type='button'
82
- variant='ghost'
83
- size='icon'
84
- className={cn(
85
- 'navbar-4-like bg-background/80 relative size-8 cursor-pointer overflow-visible rounded-full p-0 backdrop-blur-xs',
86
- className,
87
- )}
88
- data-liked={liked ? 'true' : 'false'}
89
- data-bursting={bursting ? 'true' : 'false'}
90
- aria-pressed={liked}
91
- aria-label={ariaLabel ?? (liked ? 'Remove from wishlist' : 'Add to wishlist')}
92
- onClick={toggle}
93
- >
94
- <span
95
- className={cn(
96
- 'navbar-4-like-icon inline-flex size-4 items-center justify-center',
97
- liked && !reduceMotion && 'navbar-4-like-icon--pop',
98
- )}
99
- >
100
- <svg width='16' height='16' viewBox='0 0 16 16' fill='none' aria-hidden='true'>
101
- <path
102
- d='M7.99511 3.42388C6.66221 1.8656 4.4395 1.44643 2.76947 2.87334C1.09944 4.30026 0.86432 6.68598 2.17581 8.3736C3.26622 9.77674 6.56619 12.7361 7.64774 13.6939C7.76874 13.801 7.82925 13.8546 7.89982 13.8757C7.96141 13.8941 8.02881 13.8941 8.0904 13.8757C8.16097 13.8546 8.22147 13.801 8.34248 13.6939C9.42403 12.7361 12.724 9.77674 13.8144 8.3736C15.1259 6.68598 14.9195 4.28525 13.2207 2.87334C11.522 1.46144 9.32801 1.8656 7.99511 3.42388Z'
103
- className='navbar-4-like-path'
104
- stroke='currentColor'
105
- strokeWidth='1.5'
106
- strokeLinecap='round'
107
- strokeLinejoin='round'
108
- />
109
- </svg>
110
- </span>
111
- <span ref={particlesRef} className='navbar-4-like-particles' aria-hidden='true'>
112
- {Array.from({ length: PARTICLE_COUNT }, (_, index) => (
113
- <i key={index} />
114
- ))}
115
- </span>
116
-
117
- <style>{`
118
- .navbar-4-like {
119
- --like-color: oklch(0.6 0.24 15);
120
- color: var(--foreground);
121
- }
122
- .navbar-4-like[data-liked='true'] {
123
- color: var(--like-color);
124
- }
125
- .navbar-4-like-path {
126
- fill: transparent;
127
- stroke: currentColor;
128
- transition: fill 150ms cubic-bezier(0.22, 1, 0.36, 1), stroke 150ms cubic-bezier(0.22, 1, 0.36, 1);
129
- }
130
- .navbar-4-like[data-liked='true'] .navbar-4-like-path {
131
- fill: currentColor;
132
- }
133
- .navbar-4-like-icon--pop {
134
- animation: navbar-4-like-pop 350ms cubic-bezier(0.34, 1.96, 0.64, 1);
135
- }
136
- @keyframes navbar-4-like-pop {
137
- 0% { transform: scale(1); }
138
- 30% { transform: scale(0.82); }
139
- 100% { transform: scale(1); }
140
- }
141
- .navbar-4-like-particles {
142
- position: absolute;
143
- left: 50%;
144
- top: 50%;
145
- width: 0;
146
- height: 0;
147
- pointer-events: none;
148
- color: var(--like-color);
149
- }
150
- .navbar-4-like-particles i {
151
- position: absolute;
152
- left: calc(2.5px * var(--psize, 1) / -2);
153
- top: calc(2.5px * var(--psize, 1) / -2);
154
- width: calc(2.5px * var(--psize, 1));
155
- height: calc(2.5px * var(--psize, 1));
156
- border-radius: 50%;
157
- background: currentColor;
158
- opacity: 0;
159
- }
160
- @keyframes navbar-4-like-burst {
161
- 0% { opacity: 0; transform: translate(0, 0) scale(0.4); }
162
- 20% { opacity: 1; transform: translate(calc(var(--px) * 0.25), calc(var(--py) * 0.25)) scale(1); }
163
- 100% { opacity: 0; transform: translate(var(--px), var(--py)) scale(var(--p-end-scale, 0.6)); }
164
- }
165
- .navbar-4-like[data-bursting='true'] .navbar-4-like-particles i {
166
- animation: navbar-4-like-burst var(--pdur, 600ms) ease-out var(--pdelay, 0ms) forwards;
167
- }
168
- @media (prefers-reduced-motion: reduce) {
169
- .navbar-4-like-icon--pop,
170
- .navbar-4-like-particles i {
171
- animation: none !important;
172
- }
173
- }
174
- `}</style>
175
- </Button>
176
- )
177
- }