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,286 @@
1
+ 'use client';
2
+
3
+ import { useEffect, useRef, useState, useCallback } from 'react';
4
+
5
+ interface GoogleMapProps {
6
+ address: string;
7
+ locationName?: string;
8
+ className?: string;
9
+ }
10
+
11
+ declare global {
12
+ interface Window {
13
+ google: any;
14
+ [key: string]: any; // Allow dynamic callback properties
15
+ }
16
+ }
17
+
18
+ const GOOGLE_MAPS_API_KEY = 'AIzaSyAb4-zSsPFx-QGi4cAiCGaRrzsAJC6e348';
19
+
20
+ export default function GoogleMap({ address, locationName, className = '' }: GoogleMapProps) {
21
+ const mapRef = useRef<HTMLDivElement>(null);
22
+ const [mapError, setMapError] = useState<string | null>(null);
23
+ const [isLoaded, setIsLoaded] = useState(false);
24
+ const geocoderRef = useRef<any>(null);
25
+ const mapInstanceRef = useRef<any>(null);
26
+ const isInitializingRef = useRef(false);
27
+ const callbackNameRef = useRef<string | null>(null);
28
+
29
+ // Initialize map once Google Maps API is loaded
30
+ const initMap = useCallback(async () => {
31
+ // Prevent multiple simultaneous initializations
32
+ if (isInitializingRef.current) return;
33
+
34
+ // Ensure DOM element exists and Google Maps is fully loaded
35
+ if (!mapRef.current || !window.google || !window.google.maps) {
36
+ return;
37
+ }
38
+
39
+ // Check if Geocoder is available (required for async loading)
40
+ if (!window.google.maps.Geocoder) {
41
+ console.warn('Google Maps Geocoder not yet available, retrying...');
42
+ setTimeout(() => {
43
+ if (mapRef.current && window.google?.maps?.Geocoder) {
44
+ initMap();
45
+ }
46
+ }, 100);
47
+ return;
48
+ }
49
+
50
+ // Double-check the element is still in the DOM
51
+ if (!document.body.contains(mapRef.current)) {
52
+ return;
53
+ }
54
+
55
+ isInitializingRef.current = true;
56
+
57
+ try {
58
+ // Clear any existing map instance
59
+ if (mapInstanceRef.current) {
60
+ // Clear markers and listeners
61
+ const markers = mapInstanceRef.current.markers || [];
62
+ markers.forEach((marker: any) => {
63
+ if (marker.map) marker.map = null;
64
+ });
65
+ mapInstanceRef.current = null;
66
+ }
67
+
68
+ // Import marker library for AdvancedMarkerElement
69
+ const { AdvancedMarkerElement } = await window.google.maps.importLibrary("marker") as any;
70
+
71
+ // Create geocoder - now we know it's available
72
+ geocoderRef.current = new window.google.maps.Geocoder();
73
+
74
+ // Store AdvancedMarkerElement class for use in callback
75
+ const MarkerClass = AdvancedMarkerElement;
76
+
77
+ // Geocode the address
78
+ geocoderRef.current.geocode({ address }, (results: any[], status: string) => {
79
+ // Check if component is still mounted and ref is still valid
80
+ if (!mapRef.current || !document.body.contains(mapRef.current)) {
81
+ isInitializingRef.current = false;
82
+ return;
83
+ }
84
+
85
+ if (status === 'OK' && results && results[0]) {
86
+ const location = results[0].geometry.location;
87
+
88
+ // Create map centered on the geocoded location
89
+ // Advanced markers require a mapId
90
+ // Note: When using mapId, styles are controlled via Google Cloud Console, not in code
91
+ const map = new window.google.maps.Map(mapRef.current, {
92
+ zoom: 15,
93
+ center: location,
94
+ mapId: 'DEMO_MAP_ID', // Can be replaced with a custom Map ID from Google Cloud Console
95
+ mapTypeControl: false,
96
+ streetViewControl: false,
97
+ fullscreenControl: true,
98
+ zoomControl: true,
99
+ // Styles cannot be set when mapId is present - configure in Google Cloud Console instead
100
+ });
101
+
102
+ // Create AdvancedMarkerElement (modern, non-deprecated approach)
103
+ const marker = new MarkerClass({
104
+ map: map,
105
+ position: location,
106
+ title: locationName || address,
107
+ });
108
+
109
+ // Store markers for cleanup
110
+ map.markers = [marker];
111
+ mapInstanceRef.current = map;
112
+ setIsLoaded(true);
113
+ setMapError(null);
114
+ } else {
115
+ // If geocoding fails, show error
116
+ setMapError('Unable to find the address. Please check the address and try again.');
117
+ setIsLoaded(false);
118
+ }
119
+
120
+ isInitializingRef.current = false;
121
+ });
122
+ } catch (error) {
123
+ console.error('Error initializing map:', error);
124
+ setMapError('Error loading map. Please try again later.');
125
+ isInitializingRef.current = false;
126
+ }
127
+ }, [address, locationName]);
128
+
129
+ // Load Google Maps script with proper callback
130
+ useEffect(() => {
131
+ let mounted = true;
132
+ let checkInterval: NodeJS.Timeout | null = null;
133
+
134
+ // Generate unique callback name for this component instance
135
+ const callbackName = `initGoogleMap_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
136
+ callbackNameRef.current = callbackName;
137
+
138
+ // Create callback function that will be called when API loads
139
+ (window as any)[callbackName] = () => {
140
+ if (mounted && mapRef.current) {
141
+ // Small delay to ensure everything is ready
142
+ setTimeout(() => {
143
+ if (mounted && mapRef.current) {
144
+ initMap();
145
+ }
146
+ }, 100);
147
+ }
148
+ };
149
+
150
+ // Check if script is already loaded
151
+ if (window.google && window.google.maps && window.google.maps.Geocoder) {
152
+ setTimeout(() => {
153
+ if (mounted && mapRef.current) {
154
+ initMap();
155
+ }
156
+ }, 100);
157
+ return () => {
158
+ mounted = false;
159
+ delete (window as any)[callbackName];
160
+ };
161
+ }
162
+
163
+ // Check if script tag already exists (shared script)
164
+ const existingScript = document.querySelector(`script[src*="maps.googleapis.com"]`) as HTMLScriptElement;
165
+ if (existingScript) {
166
+ // Script is loading or loaded, wait for it
167
+ checkInterval = setInterval(() => {
168
+ if (window.google && window.google.maps && window.google.maps.Geocoder && mounted) {
169
+ clearInterval(checkInterval!);
170
+ setTimeout(() => {
171
+ if (mounted && mapRef.current) {
172
+ initMap();
173
+ }
174
+ }, 100);
175
+ }
176
+ }, 100);
177
+
178
+ return () => {
179
+ mounted = false;
180
+ if (checkInterval) {
181
+ clearInterval(checkInterval);
182
+ }
183
+ delete (window as any)[callbackName];
184
+ };
185
+ }
186
+
187
+ // Create and load script with callback
188
+ // Include 'marker' library for AdvancedMarkerElement
189
+ const script = document.createElement('script');
190
+ script.src = `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places,marker&loading=async&callback=${callbackName}`;
191
+ script.async = true;
192
+ script.defer = true;
193
+
194
+ script.onerror = () => {
195
+ if (mounted) {
196
+ setMapError('Failed to load Google Maps. Please check your internet connection.');
197
+ delete (window as any)[callbackName];
198
+ }
199
+ };
200
+
201
+ document.head.appendChild(script);
202
+
203
+ return () => {
204
+ mounted = false;
205
+ if (checkInterval) {
206
+ clearInterval(checkInterval);
207
+ }
208
+ // Clean up callback
209
+ if (callbackNameRef.current) {
210
+ delete (window as any)[callbackNameRef.current];
211
+ callbackNameRef.current = null;
212
+ }
213
+ // Never remove the script - it's shared and removing it causes React errors
214
+ };
215
+ }, []); // Only run once on mount
216
+
217
+ // Re-initialize map if address changes (but only after API is loaded)
218
+ useEffect(() => {
219
+ if (window.google && window.google.maps && window.google.maps.Geocoder && mapRef.current && !isInitializingRef.current) {
220
+ // Small delay to ensure DOM is stable
221
+ const timeoutId = setTimeout(() => {
222
+ if (mapRef.current) {
223
+ initMap();
224
+ }
225
+ }, 100);
226
+
227
+ return () => clearTimeout(timeoutId);
228
+ }
229
+ }, [address, locationName, initMap]);
230
+
231
+ // Cleanup map instance on unmount
232
+ useEffect(() => {
233
+ return () => {
234
+ if (mapInstanceRef.current) {
235
+ // Clear markers
236
+ const markers = mapInstanceRef.current.markers || [];
237
+ markers.forEach((marker: any) => {
238
+ if (marker.map) marker.map = null;
239
+ });
240
+ mapInstanceRef.current = null;
241
+ }
242
+ isInitializingRef.current = false;
243
+ };
244
+ }, []);
245
+
246
+ return (
247
+ <div className={`h-full w-full rounded-lg ${className}`} style={{ minHeight: '240px', position: 'relative' }}>
248
+ {/* Map container - always present, Google Maps will render into it */}
249
+ <div
250
+ ref={mapRef}
251
+ className="h-full w-full rounded-lg"
252
+ style={{ minHeight: '240px' }}
253
+ />
254
+
255
+ {/* Loading overlay - positioned absolutely to not interfere with map DOM */}
256
+ {!isLoaded && !mapError && (
257
+ <div
258
+ className="absolute inset-0 bg-secondary_alt rounded-lg flex items-center justify-center z-10"
259
+ style={{ pointerEvents: 'none' }}
260
+ >
261
+ <p className="text-tertiary">Loading map...</p>
262
+ </div>
263
+ )}
264
+
265
+ {/* Error overlay - positioned absolutely to not interfere with map DOM */}
266
+ {mapError && (
267
+ <div
268
+ className="absolute inset-0 bg-secondary_alt rounded-lg flex items-center justify-center z-10"
269
+ style={{ pointerEvents: 'auto' }}
270
+ >
271
+ <div className="text-center p-4">
272
+ <p className="text-tertiary mb-2">{mapError}</p>
273
+ <a
274
+ href={`https://maps.google.com/?q=${encodeURIComponent(address)}`}
275
+ target="_blank"
276
+ rel="noopener noreferrer"
277
+ className="text-sm text-brand-secondary hover:underline"
278
+ >
279
+ Open in Google Maps
280
+ </a>
281
+ </div>
282
+ </div>
283
+ )}
284
+ </div>
285
+ );
286
+ }
@@ -0,0 +1,155 @@
1
+ 'use client';
2
+
3
+ import ReactMarkdown from 'react-markdown';
4
+ import remarkGfm from 'remark-gfm';
5
+
6
+ interface MarkdownRendererProps {
7
+ content: string;
8
+ className?: string;
9
+ }
10
+
11
+ /**
12
+ * Generate a URL-friendly ID from a heading text
13
+ */
14
+ function slugify(text: string): string {
15
+ return text
16
+ .toLowerCase()
17
+ .trim()
18
+ .replace(/[^\w\s-]/g, '') // Remove special characters
19
+ .replace(/[\s_-]+/g, '-') // Replace spaces and underscores with hyphens
20
+ .replace(/^-+|-+$/g, ''); // Remove leading/trailing hyphens
21
+ }
22
+
23
+ /**
24
+ * Extract text content from React children (handles nested structures)
25
+ */
26
+ function extractTextFromChildren(children: any): string {
27
+ if (typeof children === 'string') {
28
+ return children;
29
+ }
30
+ if (Array.isArray(children)) {
31
+ return children.map((child) => extractTextFromChildren(child)).join('');
32
+ }
33
+ if (children && typeof children === 'object' && 'props' in children) {
34
+ return extractTextFromChildren(children.props.children);
35
+ }
36
+ return '';
37
+ }
38
+
39
+ export default function MarkdownRenderer({ content, className = '' }: MarkdownRendererProps) {
40
+ return (
41
+ <div className={className}>
42
+ <ReactMarkdown
43
+ remarkPlugins={[remarkGfm]}
44
+ components={{
45
+ // Headings with IDs for table of contents
46
+ h1: ({ node, children, ...props }: any) => {
47
+ const text = extractTextFromChildren(children);
48
+ const id = slugify(text);
49
+ return <h1 id={id} className="text-3xl md:text-4xl font-bold text-gray-900 mb-6" {...props}>{children}</h1>;
50
+ },
51
+ h2: ({ node, children, ...props }: any) => {
52
+ const text = extractTextFromChildren(children);
53
+ const id = slugify(text);
54
+ return <h2 id={id} className="text-2xl font-semibold text-gray-900 mb-4" {...props}>{children}</h2>;
55
+ },
56
+ h3: ({ node, children, ...props }: any) => {
57
+ const text = extractTextFromChildren(children);
58
+ const id = slugify(text);
59
+ return <h3 id={id} className="text-xl font-semibold text-gray-900 mb-3" {...props}>{children}</h3>;
60
+ },
61
+ h4: ({ node, children, ...props }: any) => {
62
+ const text = extractTextFromChildren(children);
63
+ const id = slugify(text);
64
+ return <h4 id={id} className="text-lg font-semibold text-gray-900 mb-2" {...props}>{children}</h4>;
65
+ },
66
+ h5: ({ node, children, ...props }: any) => {
67
+ const text = extractTextFromChildren(children);
68
+ const id = slugify(text);
69
+ return <h5 id={id} className="text-base font-semibold text-gray-900 mb-2" {...props}>{children}</h5>;
70
+ },
71
+ h6: ({ node, children, ...props }: any) => {
72
+ const text = extractTextFromChildren(children);
73
+ const id = slugify(text);
74
+ return <h6 id={id} className="text-sm font-semibold text-gray-900 mb-2" {...props}>{children}</h6>;
75
+ },
76
+
77
+ // Paragraphs and text
78
+ p: ({ node, ...props }) => (
79
+ <p className="text-gray-700 leading-relaxed mb-4 last:mb-0" {...props} />
80
+ ),
81
+ strong: ({ node, ...props }) => (
82
+ <strong className="font-semibold text-gray-900" {...props} />
83
+ ),
84
+ em: ({ node, ...props }) => (
85
+ <em className="italic text-gray-700" {...props} />
86
+ ),
87
+
88
+
89
+ // Lists
90
+ ul: ({ node, ...props }) => (
91
+ <ul className="list-disc list-inside space-y-1 mb-4 last:mb-0" {...props} />
92
+ ),
93
+ ol: ({ node, ...props }) => (
94
+ <ol className="list-decimal list-inside space-y-1 mb-4 last:mb-0" {...props} />
95
+ ),
96
+ li: ({ node, ...props }) => (
97
+ <li className="text-gray-700 leading-relaxed" {...props} />
98
+ ),
99
+
100
+ // Blockquotes
101
+ blockquote: ({ node, ...props }) => (
102
+ <blockquote className="border-l-4 border-blue-500 pl-4 italic text-gray-600 bg-blue-50 py-2 mb-4" {...props} />
103
+ ),
104
+
105
+ // Links
106
+ a: ({ node, ...props }) => (
107
+ <a className="text-blue-600 hover:text-blue-800 hover:underline" {...props} />
108
+ ),
109
+
110
+ // Images
111
+ img: ({ node, ...props }) => (
112
+ <img className="max-w-full h-auto rounded-lg shadow-md my-4" {...props} />
113
+ ),
114
+
115
+ // Horizontal rules
116
+ hr: () => <hr className="my-6 border-gray-200" />,
117
+
118
+ // Tables
119
+ table: ({ node, ...props }) => (
120
+ <div className="overflow-x-auto my-4">
121
+ <table className="min-w-full bg-white border border-gray-200 rounded-lg" {...props} />
122
+ </div>
123
+ ),
124
+ thead: ({ node, ...props }) => (
125
+ <thead className="bg-gray-50" {...props} />
126
+ ),
127
+ tbody: ({ node, ...props }) => (
128
+ <tbody {...props} />
129
+ ),
130
+ tr: ({ node, ...props }) => (
131
+ <tr className="border-b border-gray-200" {...props} />
132
+ ),
133
+ th: ({ node, ...props }) => (
134
+ <th className="px-4 py-3 text-left text-sm font-semibold text-gray-900" {...props} />
135
+ ),
136
+ td: ({ node, ...props }) => (
137
+ <td className="px-4 py-3 text-sm text-gray-700" {...props} />
138
+ ),
139
+
140
+ // Code blocks
141
+ pre: ({ node, ...props }) => (
142
+ <pre className="bg-gray-100 p-4 rounded-lg overflow-x-auto my-4" {...props} />
143
+ ),
144
+
145
+ // Code
146
+ code: ({ node, ...props }) => (
147
+ <code className="bg-gray-100 px-1.5 py-0.5 rounded text-sm font-mono text-gray-800" {...props} />
148
+ ),
149
+ }}
150
+ >
151
+ {content}
152
+ </ReactMarkdown>
153
+ </div>
154
+ );
155
+ }
@@ -0,0 +1,41 @@
1
+ "use client";
2
+
3
+ import type { DialogProps as AriaDialogProps, ModalOverlayProps as AriaModalOverlayProps } from "react-aria-components";
4
+ import { Dialog as AriaDialog, DialogTrigger as AriaDialogTrigger, Modal as AriaModal, ModalOverlay as AriaModalOverlay } from "react-aria-components";
5
+ import { cx } from '../../../utils/cx';
6
+
7
+ export const DialogTrigger = AriaDialogTrigger;
8
+
9
+ export const ModalOverlay = (props: AriaModalOverlayProps) => {
10
+ return (
11
+ <AriaModalOverlay
12
+ {...props}
13
+ className={(state) =>
14
+ cx(
15
+ "fixed inset-0 z-50 flex min-h-dvh w-full items-end justify-center overflow-y-auto bg-overlay/70 px-4 pt-4 pb-[clamp(16px,8vh,64px)] outline-hidden backdrop-blur-[6px] sm:items-center sm:justify-center sm:p-8",
16
+ state.isEntering && "duration-300 ease-out animate-in fade-in",
17
+ state.isExiting && "duration-200 ease-in animate-out fade-out",
18
+ typeof props.className === "function" ? props.className(state) : props.className,
19
+ )
20
+ }
21
+ />
22
+ );
23
+ };
24
+
25
+ export const Modal = (props: AriaModalOverlayProps) => (
26
+ <AriaModal
27
+ {...props}
28
+ className={(state) =>
29
+ cx(
30
+ "max-h-full w-full align-middle outline-hidden max-sm:overflow-y-auto max-sm:rounded-xl",
31
+ state.isEntering && "duration-300 ease-out animate-in zoom-in-95",
32
+ state.isExiting && "duration-200 ease-in animate-out zoom-out-95",
33
+ typeof props.className === "function" ? props.className(state) : props.className,
34
+ )
35
+ }
36
+ />
37
+ );
38
+
39
+ export const Dialog = (props: AriaDialogProps) => (
40
+ <AriaDialog {...props} className={cx("flex w-full items-center justify-center outline-hidden", props.className)} />
41
+ );