keystone-design-bootstrap 1.0.3

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 (182) hide show
  1. package/README.md +179 -0
  2. package/package.json +59 -0
  3. package/src/contexts/ThemeContext.tsx +34 -0
  4. package/src/contexts/index.ts +1 -0
  5. package/src/design_system/elements/IconComponent.tsx +98 -0
  6. package/src/design_system/elements/avatar/avatar-label-group.tsx +30 -0
  7. package/src/design_system/elements/avatar/avatar-profile-photo.tsx +125 -0
  8. package/src/design_system/elements/avatar/avatar.tsx +131 -0
  9. package/src/design_system/elements/avatar/base-components/avatar-add-button.tsx +34 -0
  10. package/src/design_system/elements/avatar/base-components/avatar-company-icon.tsx +26 -0
  11. package/src/design_system/elements/avatar/base-components/avatar-online-indicator.tsx +31 -0
  12. package/src/design_system/elements/avatar/base-components/index.tsx +4 -0
  13. package/src/design_system/elements/avatar/base-components/verified-tick.tsx +34 -0
  14. package/src/design_system/elements/avatar/utils.ts +12 -0
  15. package/src/design_system/elements/badges/avatar.tsx +132 -0
  16. package/src/design_system/elements/badges/badge-groups.tsx +176 -0
  17. package/src/design_system/elements/badges/badge-types.ts +266 -0
  18. package/src/design_system/elements/badges/badges.tsx +430 -0
  19. package/src/design_system/elements/breadcrumb/Breadcrumb.tsx +33 -0
  20. package/src/design_system/elements/button-group/button-group.tsx +106 -0
  21. package/src/design_system/elements/buttons/app-store-buttons-outline.tsx +378 -0
  22. package/src/design_system/elements/buttons/app-store-buttons.tsx +567 -0
  23. package/src/design_system/elements/buttons/button-utility.tsx +116 -0
  24. package/src/design_system/elements/buttons/button.aman.tsx +174 -0
  25. package/src/design_system/elements/buttons/button.tsx +271 -0
  26. package/src/design_system/elements/buttons/close-button.tsx +42 -0
  27. package/src/design_system/elements/buttons/round-button.tsx +29 -0
  28. package/src/design_system/elements/buttons/social-button.tsx +148 -0
  29. package/src/design_system/elements/buttons/social-logos.tsx +115 -0
  30. package/src/design_system/elements/carousel/carousel-base.tsx +308 -0
  31. package/src/design_system/elements/carousel/carousel.tsx +308 -0
  32. package/src/design_system/elements/checkbox/checkbox.tsx +120 -0
  33. package/src/design_system/elements/date-picker/calendar.tsx +101 -0
  34. package/src/design_system/elements/date-picker/cell.tsx +106 -0
  35. package/src/design_system/elements/date-picker/date-input.tsx +32 -0
  36. package/src/design_system/elements/date-picker/date-picker.tsx +86 -0
  37. package/src/design_system/elements/date-picker/date-range-picker.tsx +163 -0
  38. package/src/design_system/elements/date-picker/range-calendar.tsx +161 -0
  39. package/src/design_system/elements/date-picker/range-preset.tsx +28 -0
  40. package/src/design_system/elements/featured-icon/featured-icon.tsx +154 -0
  41. package/src/design_system/elements/form/form.tsx +10 -0
  42. package/src/design_system/elements/form/hook-form.tsx +75 -0
  43. package/src/design_system/elements/hint-text/hint-text.tsx +33 -0
  44. package/src/design_system/elements/index.tsx +158 -0
  45. package/src/design_system/elements/input/hint-text.tsx +33 -0
  46. package/src/design_system/elements/input/input-group.tsx +133 -0
  47. package/src/design_system/elements/input/input.aman.tsx +172 -0
  48. package/src/design_system/elements/input/input.tsx +271 -0
  49. package/src/design_system/elements/input/label.tsx +50 -0
  50. package/src/design_system/elements/label/label.tsx +50 -0
  51. package/src/design_system/elements/loading-indicator/loading-indicator.tsx +123 -0
  52. package/src/design_system/elements/map/GoogleMap.tsx +286 -0
  53. package/src/design_system/elements/markdown-renderer/MarkdownRenderer.tsx +155 -0
  54. package/src/design_system/elements/modals/modal.tsx +41 -0
  55. package/src/design_system/elements/pagination/pagination-base.tsx +378 -0
  56. package/src/design_system/elements/pagination/pagination-dot.tsx +54 -0
  57. package/src/design_system/elements/pagination/pagination-line.tsx +50 -0
  58. package/src/design_system/elements/pagination/pagination.tsx +330 -0
  59. package/src/design_system/elements/photo-fallback/photo-fallback.tsx +143 -0
  60. package/src/design_system/elements/progress-indicators/progress-circles.tsx +176 -0
  61. package/src/design_system/elements/progress-indicators/progress-indicators.tsx +123 -0
  62. package/src/design_system/elements/progress-indicators/simple-circle.tsx +29 -0
  63. package/src/design_system/elements/radio-buttons/radio-buttons.tsx +129 -0
  64. package/src/design_system/elements/rating/rating-badge.tsx +144 -0
  65. package/src/design_system/elements/rating/rating-stars.tsx +77 -0
  66. package/src/design_system/elements/select/combobox.tsx +152 -0
  67. package/src/design_system/elements/select/multi-select.tsx +363 -0
  68. package/src/design_system/elements/select/popover.tsx +34 -0
  69. package/src/design_system/elements/select/select-item.tsx +97 -0
  70. package/src/design_system/elements/select/select-native.tsx +69 -0
  71. package/src/design_system/elements/select/select.aman.tsx +75 -0
  72. package/src/design_system/elements/select/select.tsx +146 -0
  73. package/src/design_system/elements/shared-assets/credit-card/credit-card.tsx +237 -0
  74. package/src/design_system/elements/shared-assets/credit-card/icons.tsx +75 -0
  75. package/src/design_system/elements/shared-assets/iphone-mockup.tsx +172 -0
  76. package/src/design_system/elements/shared-assets/section-divider.tsx +12 -0
  77. package/src/design_system/elements/slideout-menus/slideout-menu.tsx +122 -0
  78. package/src/design_system/elements/tabs/tabs.tsx +225 -0
  79. package/src/design_system/elements/tags/base-components/tag-checkbox.tsx +45 -0
  80. package/src/design_system/elements/tags/base-components/tag-close-x.tsx +34 -0
  81. package/src/design_system/elements/tags/tags.tsx +176 -0
  82. package/src/design_system/elements/textarea/textarea.aman.tsx +52 -0
  83. package/src/design_system/elements/textarea/textarea.tsx +111 -0
  84. package/src/design_system/elements/toggle/toggle.tsx +140 -0
  85. package/src/design_system/elements/tooltip/tooltip.tsx +109 -0
  86. package/src/design_system/hooks/use-breakpoint.ts +37 -0
  87. package/src/design_system/hooks/use-resize-observer.ts +68 -0
  88. package/src/design_system/logo/keystone-logo-minimal.tsx +93 -0
  89. package/src/design_system/logo/keystone-logo.tsx +22 -0
  90. package/src/design_system/sections/about-home.aman.tsx +85 -0
  91. package/src/design_system/sections/about-home.tsx +115 -0
  92. package/src/design_system/sections/blog-cards.tsx +848 -0
  93. package/src/design_system/sections/blog-gallery.aman.tsx +77 -0
  94. package/src/design_system/sections/blog-gallery.tsx +204 -0
  95. package/src/design_system/sections/blog-home.aman.tsx +84 -0
  96. package/src/design_system/sections/blog-home.tsx +153 -0
  97. package/src/design_system/sections/blog-post.aman.tsx +74 -0
  98. package/src/design_system/sections/blog-post.tsx +301 -0
  99. package/src/design_system/sections/blog-section.aman.tsx +101 -0
  100. package/src/design_system/sections/blog-section.tsx +179 -0
  101. package/src/design_system/sections/contact-home.tsx +25 -0
  102. package/src/design_system/sections/contact-section.aman.tsx +173 -0
  103. package/src/design_system/sections/contact-section.tsx +143 -0
  104. package/src/design_system/sections/faq-grid.aman.tsx +79 -0
  105. package/src/design_system/sections/faq-grid.tsx +102 -0
  106. package/src/design_system/sections/faq-home.aman.tsx +92 -0
  107. package/src/design_system/sections/faq-home.tsx +134 -0
  108. package/src/design_system/sections/feature-tab.tsx +43 -0
  109. package/src/design_system/sections/feature-text.tsx +284 -0
  110. package/src/design_system/sections/footer-home.aman.tsx +62 -0
  111. package/src/design_system/sections/footer-home.tsx +259 -0
  112. package/src/design_system/sections/generic-header-component.tsx +103 -0
  113. package/src/design_system/sections/header-navigation.aman.tsx +360 -0
  114. package/src/design_system/sections/header-navigation.tsx +334 -0
  115. package/src/design_system/sections/hero-faq.aman.tsx +38 -0
  116. package/src/design_system/sections/hero-faq.tsx +55 -0
  117. package/src/design_system/sections/hero-generic-text.aman.tsx +49 -0
  118. package/src/design_system/sections/hero-generic-text.tsx +51 -0
  119. package/src/design_system/sections/hero-home.aman.tsx +84 -0
  120. package/src/design_system/sections/hero-home.tsx +246 -0
  121. package/src/design_system/sections/hero-location-detail.aman.tsx +33 -0
  122. package/src/design_system/sections/hero-location-detail.tsx +72 -0
  123. package/src/design_system/sections/hero-service-detail.aman.tsx +53 -0
  124. package/src/design_system/sections/hero-service-detail.tsx +51 -0
  125. package/src/design_system/sections/hero-social-media.aman.tsx +42 -0
  126. package/src/design_system/sections/hero-social-media.tsx +35 -0
  127. package/src/design_system/sections/hero-testimonials.aman.tsx +38 -0
  128. package/src/design_system/sections/hero-testimonials.tsx +55 -0
  129. package/src/design_system/sections/home-hero-component.tsx +228 -0
  130. package/src/design_system/sections/index.tsx +131 -0
  131. package/src/design_system/sections/job-gallery.aman.tsx +91 -0
  132. package/src/design_system/sections/job-gallery.tsx +183 -0
  133. package/src/design_system/sections/location-details-section.aman.tsx +179 -0
  134. package/src/design_system/sections/location-details-section.tsx +196 -0
  135. package/src/design_system/sections/location-grid.aman.tsx +76 -0
  136. package/src/design_system/sections/location-grid.tsx +123 -0
  137. package/src/design_system/sections/services-grid.aman.tsx +85 -0
  138. package/src/design_system/sections/services-grid.tsx +104 -0
  139. package/src/design_system/sections/services-home.aman.tsx +78 -0
  140. package/src/design_system/sections/services-home.tsx +131 -0
  141. package/src/design_system/sections/social-media-grid.aman.tsx +132 -0
  142. package/src/design_system/sections/social-media-grid.tsx +189 -0
  143. package/src/design_system/sections/statistics-section.aman.tsx +79 -0
  144. package/src/design_system/sections/statistics-section.tsx +97 -0
  145. package/src/design_system/sections/team-grid.aman.tsx +85 -0
  146. package/src/design_system/sections/team-grid.tsx +88 -0
  147. package/src/design_system/sections/testimonials-home.aman.tsx +113 -0
  148. package/src/design_system/sections/testimonials-home.tsx +90 -0
  149. package/src/design_system/sections/values-section.aman.tsx +73 -0
  150. package/src/design_system/sections/values-section.tsx +128 -0
  151. package/src/design_system/utils/icon-mapping.tsx +28 -0
  152. package/src/index.ts +7 -0
  153. package/src/lib/component-registry.ts +53 -0
  154. package/src/lib/hooks/index.ts +8 -0
  155. package/src/lib/hooks/use-breakpoint.ts +37 -0
  156. package/src/lib/hooks/use-clipboard.ts +79 -0
  157. package/src/lib/hooks/use-resize-observer.ts +68 -0
  158. package/src/lib/server-api.ts +115 -0
  159. package/src/styles/style-overrides.aman.css +101 -0
  160. package/src/styles/theme.css +224 -0
  161. package/src/styles/typography.css +430 -0
  162. package/src/themes/index.ts +23 -0
  163. package/src/types/api/blog-post.ts +53 -0
  164. package/src/types/api/company-information.ts +44 -0
  165. package/src/types/api/contact.ts +63 -0
  166. package/src/types/api/faq.ts +37 -0
  167. package/src/types/api/job-posting.ts +34 -0
  168. package/src/types/api/location.ts +36 -0
  169. package/src/types/api/photos.ts +28 -0
  170. package/src/types/api/service.ts +37 -0
  171. package/src/types/api/social-post.ts +28 -0
  172. package/src/types/api/team-member.ts +29 -0
  173. package/src/types/api/testimonial.ts +29 -0
  174. package/src/types/api/website-photos.ts +22 -0
  175. package/src/types/config.ts +21 -0
  176. package/src/types/index.ts +21 -0
  177. package/src/utils/countries.tsx +1351 -0
  178. package/src/utils/cx.ts +25 -0
  179. package/src/utils/gradient-placeholder.ts +59 -0
  180. package/src/utils/is-react-component.ts +33 -0
  181. package/src/utils/markdown-toc.ts +54 -0
  182. package/src/utils/photo-helpers.ts +94 -0
@@ -0,0 +1,360 @@
1
+ "use client";
2
+
3
+ import React, { useState, useRef, useCallback } from 'react';
4
+ import Link from 'next/link';
5
+ import { Button } from '../elements';
6
+ import { cx } from '../../utils/cx';
7
+ import { getLogoUrl } from '../../utils/photo-helpers';
8
+ import type { HeaderComponentProps } from './header-navigation';
9
+
10
+ // Maximum items to show before "View All" link
11
+ const MAX_DROPDOWN_ITEMS = 3;
12
+
13
+ export function HeaderNavigation({
14
+ variant,
15
+ props,
16
+ navigation: navigationOverride,
17
+ logoImage: logoImageOverride,
18
+ logoText: logoTextOverride,
19
+ config,
20
+ companyInformation,
21
+ websitePhotos,
22
+ }: HeaderComponentProps) {
23
+ const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
24
+ const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
25
+ const [dropdownTop, setDropdownTop] = useState(0);
26
+ const [isScrolled, setIsScrolled] = useState(false);
27
+
28
+ // Timeout ref for delayed dropdown closing
29
+ const closeTimeoutRef = useRef<NodeJS.Timeout | null>(null);
30
+
31
+ // Track scroll position for header animation
32
+ React.useEffect(() => {
33
+ const handleScroll = () => {
34
+ setIsScrolled(window.scrollY > 10);
35
+ };
36
+
37
+ window.addEventListener('scroll', handleScroll);
38
+ return () => window.removeEventListener('scroll', handleScroll);
39
+ }, []);
40
+
41
+ // Cleanup timeout on unmount
42
+ React.useEffect(() => {
43
+ return () => {
44
+ if (closeTimeoutRef.current) {
45
+ clearTimeout(closeTimeoutRef.current);
46
+ }
47
+ };
48
+ }, []);
49
+
50
+ const apiCompanyInfo = companyInformation as any;
51
+
52
+ const logoUrl = logoImageOverride || getLogoUrl(websitePhotos) || props?.logo?.image;
53
+ const companyName = logoTextOverride || apiCompanyInfo?.company_name || props?.logo?.text || '';
54
+
55
+ // Use navigation from config or override
56
+ const navigation = navigationOverride || config?.navigation?.header || [];
57
+
58
+ // Cancel any pending close timeout
59
+ const cancelCloseTimeout = useCallback(() => {
60
+ if (closeTimeoutRef.current) {
61
+ clearTimeout(closeTimeoutRef.current);
62
+ closeTimeoutRef.current = null;
63
+ }
64
+ }, []);
65
+
66
+ // Open dropdown immediately
67
+ const handleMouseEnter = useCallback((item: any, e: React.MouseEvent<HTMLDivElement>) => {
68
+ cancelCloseTimeout();
69
+ if (item.children?.length > 0) {
70
+ const target = e.currentTarget.closest('nav');
71
+ if (target) {
72
+ const rect = target.getBoundingClientRect();
73
+ setDropdownTop(rect.bottom);
74
+ }
75
+ setActiveDropdown(item.label);
76
+ }
77
+ }, [cancelCloseTimeout]);
78
+
79
+ // Close dropdown with delay (allows moving to dropdown)
80
+ const handleMouseLeave = useCallback(() => {
81
+ cancelCloseTimeout();
82
+ closeTimeoutRef.current = setTimeout(() => {
83
+ setActiveDropdown(null);
84
+ }, 150); // 150ms delay before closing
85
+ }, [cancelCloseTimeout]);
86
+
87
+ // Keep dropdown open when hovering over it
88
+ const handleDropdownMouseEnter = useCallback(() => {
89
+ cancelCloseTimeout();
90
+ }, [cancelCloseTimeout]);
91
+
92
+ // Close dropdown when leaving the dropdown area
93
+ const handleDropdownMouseLeave = useCallback(() => {
94
+ handleMouseLeave();
95
+ }, [handleMouseLeave]);
96
+
97
+ // Get display items for dropdown (limit + "View All" if needed)
98
+ const getDropdownItems = (item: any) => {
99
+ const children = item.children || [];
100
+ const isDynamicMenu = item.label === 'Services' || item.label === 'Locations';
101
+
102
+ if (isDynamicMenu && children.length > MAX_DROPDOWN_ITEMS + 1) {
103
+ // Show first MAX_DROPDOWN_ITEMS items + "View All" link
104
+ const displayItems = children.slice(0, MAX_DROPDOWN_ITEMS);
105
+ const viewAllHref = item.label === 'Services' ? '/services' : '/locations';
106
+ return {
107
+ items: displayItems,
108
+ showViewAll: true,
109
+ viewAllHref,
110
+ viewAllLabel: `View All ${item.label}`,
111
+ };
112
+ }
113
+
114
+ return {
115
+ items: children,
116
+ showViewAll: false,
117
+ viewAllHref: '',
118
+ viewAllLabel: '',
119
+ };
120
+ };
121
+
122
+ return (
123
+ <>
124
+ {/* Desktop Header */}
125
+ <header className="hidden md:block sticky top-0 z-50 bg-primary border-b border-secondary transition-all duration-300">
126
+ <div className="mx-auto max-w-container px-4 md:px-8">
127
+ {/* Top Row: Logo (left), Company Name (center), Contact Button (right) */}
128
+ <div className={cx(
129
+ "relative flex items-center justify-between transition-all duration-300",
130
+ isScrolled ? "py-2" : "py-8"
131
+ )}>
132
+ {/* Left: Logo Image */}
133
+ <Link href="/" className="flex items-center">
134
+ {logoUrl && (
135
+ <img
136
+ src={logoUrl}
137
+ alt={companyName}
138
+ className="h-8 md:h-10 w-auto object-contain"
139
+ />
140
+ )}
141
+ </Link>
142
+
143
+ {/* Center: Company Name Text */}
144
+ <Link href="/" className="absolute left-1/2 transform -translate-x-1/2 font-display text-2xl md:text-3xl font-normal uppercase tracking-widest text-fg-primary" suppressHydrationWarning>
145
+ {companyName}
146
+ </Link>
147
+
148
+ {/* Right: Contact Button */}
149
+ <div className="flex items-center gap-6">
150
+ <Button
151
+ href="/contact"
152
+ size="sm"
153
+ className="bg-fg-primary text-white font-body text-sm uppercase tracking-wide px-6 py-2 rounded-sm hover:opacity-90"
154
+ >
155
+ Contact
156
+ </Button>
157
+ </div>
158
+ </div>
159
+
160
+ {/* Navigation Row with Dropdowns - Centered */}
161
+ <nav className="border-b border-secondary">
162
+ <div className="flex items-center justify-center gap-8 py-4">
163
+ {navigation.map((item: any, i: number) => (
164
+ <div
165
+ key={i}
166
+ className="relative"
167
+ onMouseEnter={(e) => handleMouseEnter(item, e)}
168
+ onMouseLeave={handleMouseLeave}
169
+ >
170
+ <Link
171
+ href={item.href}
172
+ className={cx(
173
+ "font-body text-sm uppercase tracking-wide text-fg-primary pb-4",
174
+ activeDropdown === item.label && "border-b-2 border-fg-primary"
175
+ )}
176
+ >
177
+ {item.label}
178
+ </Link>
179
+
180
+ {/* Dropdown Second Row - Horizontal Layout - Centered */}
181
+ {item.children && item.children.length > 0 && activeDropdown === item.label && (
182
+ <div
183
+ className="fixed left-0 right-0 w-full pt-6 pb-6 border-b border-secondary bg-primary z-50"
184
+ style={{ top: `${dropdownTop}px` }}
185
+ onMouseEnter={handleDropdownMouseEnter}
186
+ onMouseLeave={handleDropdownMouseLeave}
187
+ >
188
+ <div className="mx-auto max-w-container px-4 md:px-8">
189
+ <div className="flex items-center justify-center gap-8">
190
+ {(() => {
191
+ const { items, showViewAll, viewAllHref, viewAllLabel } = getDropdownItems(item);
192
+ return (
193
+ <>
194
+ {items.map((link: any, j: number) => (
195
+ <Link
196
+ key={j}
197
+ href={link.href}
198
+ className="font-body text-sm text-fg-primary hover:underline whitespace-nowrap"
199
+ >
200
+ {link.label}
201
+ </Link>
202
+ ))}
203
+ {showViewAll && (
204
+ <Link
205
+ href={viewAllHref}
206
+ className="font-body text-sm text-fg-primary hover:underline whitespace-nowrap font-semibold"
207
+ >
208
+ {viewAllLabel} →
209
+ </Link>
210
+ )}
211
+ </>
212
+ );
213
+ })()}
214
+ </div>
215
+ </div>
216
+ </div>
217
+ )}
218
+ </div>
219
+ ))}
220
+ </div>
221
+ </nav>
222
+ </div>
223
+ </header>
224
+
225
+ {/* Mobile Header */}
226
+ <header className="md:hidden sticky top-0 z-50 bg-primary border-b border-secondary">
227
+ <div className="flex items-center justify-between px-4 py-4">
228
+ <button
229
+ onClick={() => setIsMobileMenuOpen(true)}
230
+ className="text-fg-primary"
231
+ aria-label="Open menu"
232
+ >
233
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
234
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
235
+ </svg>
236
+ </button>
237
+
238
+ <Link href="/" className="flex items-center">
239
+ {logoUrl ? (
240
+ <img
241
+ src={logoUrl}
242
+ alt={companyName}
243
+ className="h-8 w-auto object-contain"
244
+ />
245
+ ) : (
246
+ <span className="font-display text-xl font-normal uppercase tracking-widest text-fg-primary">
247
+ {companyName}
248
+ </span>
249
+ )}
250
+ </Link>
251
+
252
+ <div className="w-6" />
253
+ </div>
254
+ </header>
255
+
256
+ {/* Mobile Full-Screen Menu Overlay */}
257
+ {isMobileMenuOpen && (
258
+ <div className="fixed inset-0 bg-white z-50 md:hidden">
259
+ <div className="flex flex-col h-full">
260
+ {/* Mobile Menu Header */}
261
+ <div className="flex items-center justify-between px-4 py-4 border-b border-secondary">
262
+ <button
263
+ onClick={() => setIsMobileMenuOpen(false)}
264
+ className="text-fg-primary"
265
+ aria-label="Close menu"
266
+ >
267
+ <svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
268
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
269
+ </svg>
270
+ </button>
271
+
272
+ <div className="flex items-center">
273
+ {logoUrl ? (
274
+ <img
275
+ src={logoUrl}
276
+ alt={companyName}
277
+ className="h-8 w-auto object-contain"
278
+ />
279
+ ) : (
280
+ <span className="font-display text-xl font-normal uppercase tracking-widest text-fg-primary">
281
+ {companyName}
282
+ </span>
283
+ )}
284
+ </div>
285
+
286
+ <div className="w-6" />
287
+ </div>
288
+
289
+ {/* Mobile Menu Links */}
290
+ <nav className="flex-1 overflow-y-auto px-4 py-8">
291
+ <ul className="space-y-4">
292
+ {navigation.map((item: any, i: number) => {
293
+ const { items, showViewAll, viewAllHref, viewAllLabel } = getDropdownItems(item);
294
+ return (
295
+ <li key={i}>
296
+ <Link
297
+ href={item.href}
298
+ className="block font-body text-base text-fg-primary py-2"
299
+ onClick={() => setIsMobileMenuOpen(false)}
300
+ >
301
+ {item.label}
302
+ </Link>
303
+ {items.length > 0 && (
304
+ <ul className="ml-4 mt-2 space-y-2">
305
+ {items.map((link: any, j: number) => (
306
+ <li key={j}>
307
+ <Link
308
+ href={link.href}
309
+ className="block font-body text-sm text-tertiary py-1"
310
+ onClick={() => setIsMobileMenuOpen(false)}
311
+ >
312
+ {link.label}
313
+ </Link>
314
+ </li>
315
+ ))}
316
+ {showViewAll && (
317
+ <li>
318
+ <Link
319
+ href={viewAllHref}
320
+ className="block font-body text-sm text-tertiary py-1 font-semibold"
321
+ onClick={() => setIsMobileMenuOpen(false)}
322
+ >
323
+ {viewAllLabel} →
324
+ </Link>
325
+ </li>
326
+ )}
327
+ </ul>
328
+ )}
329
+ </li>
330
+ );
331
+ })}
332
+ </ul>
333
+ </nav>
334
+
335
+ {/* Mobile Menu Footer */}
336
+ <div className="border-t border-secondary px-4 py-4">
337
+ <select className="w-full font-body text-sm text-fg-primary bg-transparent border-b border-secondary py-2">
338
+ <option>English</option>
339
+ </select>
340
+ </div>
341
+ </div>
342
+ </div>
343
+ )}
344
+
345
+ {/* Sticky Contact Button (Mobile) */}
346
+ <div className="fixed bottom-0 left-0 right-0 z-40 md:hidden" style={{ backgroundColor: '#1E1E1E' }}>
347
+ <Button
348
+ href="/contact"
349
+ className="w-full font-body text-sm uppercase tracking-wide py-4 rounded-none"
350
+ style={{ backgroundColor: '#1E1E1E', color: '#FFFFFF' }}
351
+ >
352
+ Contact
353
+ </Button>
354
+ </div>
355
+ </>
356
+ );
357
+ }
358
+
359
+ import { registerThemeVariant } from '../../lib/component-registry';
360
+ registerThemeVariant('header-navigation', 'aman', HeaderNavigation);
@@ -0,0 +1,334 @@
1
+ 'use client';
2
+ import React, { useRef, useState } from 'react';
3
+ import Link from 'next/link';
4
+ import { ChevronDown } from "@untitledui/icons";
5
+ import { Button as AriaButton, Dialog as AriaDialog, DialogTrigger as AriaDialogTrigger, Popover as AriaPopover } from "react-aria-components";
6
+ import { Button } from '../elements';
7
+ import { GenericHeaderComponent } from './generic-header-component';
8
+ import { cx } from '../../utils/cx';
9
+ import { getLogoUrl } from '../../utils/photo-helpers';
10
+ import type { CompanyInformation } from '../../types/api/company-information';
11
+ import type { WebsitePhotos } from '../../types/api/website-photos';
12
+ import type { NavItem, SiteConfig } from '../../types/config';
13
+
14
+ export interface HeaderProps {
15
+ logo: {
16
+ text: string;
17
+ href: string;
18
+ image?: string;
19
+ };
20
+ cta_button?: {
21
+ label: string;
22
+ href: string;
23
+ secondary_label?: string;
24
+ secondary_href?: string;
25
+ };
26
+ }
27
+
28
+ export interface HeaderComponentProps {
29
+ variant?: 'minimal' | 'centered' | 'standard';
30
+ props?: HeaderProps;
31
+ navigation?: NavItem[];
32
+ logoImage?: string;
33
+ logoText?: string;
34
+ config?: SiteConfig;
35
+ companyInformation?: CompanyInformation | null;
36
+ websitePhotos?: WebsitePhotos | null;
37
+ }
38
+
39
+ const MobileNavItem = ({ label, href, children, onClose }: {
40
+ label: string;
41
+ href?: string;
42
+ children?: React.ReactNode;
43
+ onClose?: () => void;
44
+ }) => {
45
+ const [isOpen, setIsOpen] = useState(false);
46
+
47
+ if (href) {
48
+ return (
49
+ <li>
50
+ <Link
51
+ href={href}
52
+ className="flex items-center justify-between px-4 py-3 text-sm font-medium text-primary hover:bg-primary_hover"
53
+ onClick={onClose}
54
+ >
55
+ {label}
56
+ </Link>
57
+ </li>
58
+ );
59
+ }
60
+
61
+ return (
62
+ <li className="flex flex-col gap-0.5">
63
+ <button
64
+ aria-expanded={isOpen}
65
+ onClick={() => setIsOpen(!isOpen)}
66
+ className="flex w-full items-center justify-between px-4 py-3 text-sm font-medium text-primary hover:bg-primary_hover"
67
+ >
68
+ {label}
69
+ <ChevronDown
70
+ className={cx("size-4 stroke-[2.625px] text-fg-quaternary transition duration-100 ease-linear", isOpen ? "-rotate-180" : "rotate-0")}
71
+ />
72
+ </button>
73
+ {isOpen && <div>{children}</div>}
74
+ </li>
75
+ );
76
+ };
77
+
78
+ export function HeaderNavigation({
79
+ variant = 'standard',
80
+ props,
81
+ navigation: navigationOverride,
82
+ logoImage: logoImageOverride,
83
+ logoText: logoTextOverride,
84
+ config,
85
+ companyInformation,
86
+ websitePhotos,
87
+ }: HeaderComponentProps) {
88
+ const headerRef = useRef<HTMLElement>(null);
89
+
90
+ const apiCompanyInfo = companyInformation as any;
91
+
92
+ // Use navigation from config or override
93
+ const navigation = navigationOverride || config?.navigation?.header || [];
94
+
95
+ // Build logo
96
+ const logoImage = logoImageOverride || getLogoUrl(websitePhotos) || props?.logo?.image;
97
+ const logoText = logoTextOverride || apiCompanyInfo?.company_name || props?.logo?.text || '';
98
+ const cta_button = props?.cta_button;
99
+
100
+ const logo = {
101
+ text: logoText || '',
102
+ href: props?.logo?.href || '/',
103
+ image: logoImage,
104
+ };
105
+
106
+ const dynamicNavigation = navigation;
107
+
108
+ const getVariantClasses = () => {
109
+ switch (variant) {
110
+ case 'minimal':
111
+ return 'py-4';
112
+ case 'centered':
113
+ return 'py-6';
114
+ case 'standard':
115
+ default:
116
+ return 'py-4';
117
+ }
118
+ };
119
+
120
+ const HoverDropdown = ({ label, href, children }: { label: string; href?: string; children: React.ReactNode }) => {
121
+ const [isOpen, setIsOpen] = useState(false);
122
+ const timeoutRef = React.useRef<NodeJS.Timeout | null>(null);
123
+
124
+ const handleMouseEnter = () => {
125
+ if (timeoutRef.current) {
126
+ clearTimeout(timeoutRef.current);
127
+ timeoutRef.current = null;
128
+ }
129
+ setIsOpen(true);
130
+ };
131
+
132
+ const handleMouseLeave = () => {
133
+ const id = setTimeout(() => {
134
+ setIsOpen(false);
135
+ }, 100);
136
+ timeoutRef.current = id;
137
+ };
138
+
139
+ React.useEffect(() => {
140
+ return () => {
141
+ if (timeoutRef.current) {
142
+ clearTimeout(timeoutRef.current);
143
+ }
144
+ };
145
+ }, []);
146
+
147
+ return (
148
+ <div
149
+ className="relative"
150
+ onMouseEnter={handleMouseEnter}
151
+ onMouseLeave={handleMouseLeave}
152
+ >
153
+ <Link
154
+ href={href || '#'}
155
+ className="flex cursor-pointer items-center gap-0.5 rounded-lg px-1.5 py-1 text-sm font-medium text-secondary outline-focus-ring transition duration-100 ease-linear hover:text-secondary_hover focus-visible:outline-2 focus-visible:outline-offset-2"
156
+ >
157
+ <span className="px-0.5">{label}</span>
158
+ <ChevronDown className={cx("size-4 stroke-[2.625px] text-fg-quaternary transition duration-100 ease-linear", isOpen ? "-rotate-180" : "rotate-0")} />
159
+ </Link>
160
+
161
+ {isOpen && (
162
+ <div
163
+ className="absolute top-full left-0 z-50 mt-1 origin-top animate-in fade-in slide-in-from-top-1 duration-75 ease-out"
164
+ onMouseEnter={handleMouseEnter}
165
+ onMouseLeave={handleMouseLeave}
166
+ >
167
+ <div className="w-max max-w-sm">
168
+ {children}
169
+ </div>
170
+ </div>
171
+ )}
172
+ </div>
173
+ );
174
+ };
175
+
176
+ const headerNavItems = dynamicNavigation.map((navItem) => {
177
+ if (navItem.children && navItem.children.length > 0) {
178
+ return {
179
+ label: navItem.label,
180
+ href: navItem.href,
181
+ menu: (
182
+ <GenericHeaderComponent
183
+ items={navItem.children.map(child => ({
184
+ label: child.label,
185
+ href: child.href,
186
+ subtitle: child.subtitle,
187
+ }))}
188
+ />
189
+ ),
190
+ };
191
+ }
192
+ return {
193
+ label: navItem.label,
194
+ href: navItem.href,
195
+ };
196
+ });
197
+
198
+ return (
199
+ <header
200
+ ref={headerRef}
201
+ className={cx(
202
+ "sticky top-0 z-50 flex h-18 w-full items-center justify-center backdrop-blur-md bg-primary/80 md:h-20",
203
+ getVariantClasses()
204
+ )}
205
+ >
206
+ <div className="flex size-full max-w-container flex-1 items-center pr-3 pl-4 md:px-8">
207
+ <div className="flex w-full justify-between gap-4">
208
+ <div className="flex flex-1 items-center gap-5">
209
+ <Link href={logo?.href || '/'} className="flex items-center space-x-2">
210
+ {logoImage ? (
211
+ <img
212
+ src={logoImage}
213
+ alt={logoText}
214
+ className="h-8 w-8 object-contain"
215
+ />
216
+ ) : (
217
+ <div className="h-8 w-8 bg-blue-600 rounded-lg flex items-center justify-center">
218
+ <span className="text-white font-bold text-lg">
219
+ {logoText?.charAt(0)?.toUpperCase() || ''}
220
+ </span>
221
+ </div>
222
+ )}
223
+ <span className="text-xl font-bold text-primary hidden md:block">{logoText}</span>
224
+ </Link>
225
+
226
+ <nav className="max-md:hidden">
227
+ <ul className="flex items-center gap-0.5">
228
+ {headerNavItems.map((navItem) => (
229
+ <li key={navItem.label}>
230
+ {navItem.menu ? (
231
+ <HoverDropdown label={navItem.label} href={navItem.href}>
232
+ {navItem.menu}
233
+ </HoverDropdown>
234
+ ) : (
235
+ <Link
236
+ href={navItem.href || '#'}
237
+ className="flex cursor-pointer items-center gap-0.5 rounded-lg px-1.5 py-1 text-sm font-medium text-secondary outline-focus-ring transition duration-100 ease-linear hover:text-secondary_hover focus:outline-offset-2 focus-visible:outline-2"
238
+ >
239
+ <span className="px-0.5">{navItem.label}</span>
240
+ </Link>
241
+ )}
242
+ </li>
243
+ ))}
244
+ </ul>
245
+ </nav>
246
+ </div>
247
+
248
+ <div className="hidden items-center gap-3 md:flex">
249
+ <Button
250
+ href={cta_button?.href || "/contact"}
251
+ color="primary"
252
+ size="lg"
253
+ >
254
+ {cta_button?.label || "Get Started"}
255
+ </Button>
256
+ </div>
257
+
258
+ <AriaDialogTrigger>
259
+ <AriaButton
260
+ aria-label="Toggle navigation menu"
261
+ className={({ isFocusVisible, isHovered }) =>
262
+ cx(
263
+ "group ml-auto cursor-pointer rounded-lg p-2 md:hidden",
264
+ isHovered && "bg-primary_hover",
265
+ isFocusVisible && "outline-2 outline-offset-2 outline-focus-ring",
266
+ )
267
+ }
268
+ >
269
+ <svg aria-hidden="true" width="24" height="24" viewBox="0 0 24 24" fill="none">
270
+ <path
271
+ className="hidden text-secondary group-aria-expanded:block"
272
+ d="M18 6L6 18M6 6L18 18"
273
+ stroke="currentColor"
274
+ strokeWidth="2"
275
+ strokeLinecap="round"
276
+ strokeLinejoin="round"
277
+ />
278
+ <path
279
+ className="text-secondary group-aria-expanded:hidden"
280
+ d="M3 12H21M3 6H21M3 18H21"
281
+ stroke="currentColor"
282
+ strokeWidth="2"
283
+ strokeLinecap="round"
284
+ strokeLinejoin="round"
285
+ />
286
+ </svg>
287
+ </AriaButton>
288
+ <AriaPopover
289
+ triggerRef={headerRef}
290
+ className="h-calc(100%-72px) scrollbar-hide w-full overflow-y-auto shadow-lg md:hidden"
291
+ offset={0}
292
+ crossOffset={20}
293
+ containerPadding={0}
294
+ placement="bottom left"
295
+ >
296
+ <AriaDialog className="outline-hidden">
297
+ <nav className="w-full bg-primary shadow-lg">
298
+ <ul className="flex flex-col gap-0.5 py-5">
299
+ {headerNavItems.map((navItem) =>
300
+ navItem.menu ? (
301
+ <MobileNavItem
302
+ key={navItem.label}
303
+ label={navItem.label}
304
+ >
305
+ {navItem.menu}
306
+ </MobileNavItem>
307
+ ) : (
308
+ <MobileNavItem
309
+ key={navItem.label}
310
+ label={navItem.label}
311
+ href={navItem.href}
312
+ />
313
+ ),
314
+ )}
315
+ </ul>
316
+
317
+ <div className="flex flex-col gap-3 border-t border-secondary px-4 py-6">
318
+ <Button
319
+ href={cta_button?.href || "/contact"}
320
+ color="primary"
321
+ size="lg"
322
+ >
323
+ {cta_button?.label || "Get Started"}
324
+ </Button>
325
+ </div>
326
+ </nav>
327
+ </AriaDialog>
328
+ </AriaPopover>
329
+ </AriaDialogTrigger>
330
+ </div>
331
+ </div>
332
+ </header>
333
+ );
334
+ }