@bailierich/booking-components 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 (83) hide show
  1. package/README.md +319 -0
  2. package/TENANT_DATA_INTEGRATION.md +402 -0
  3. package/TENANT_SETUP.md +316 -0
  4. package/components/BookingFlow/BookingFlow.tsx +790 -0
  5. package/components/BookingFlow/index.ts +5 -0
  6. package/components/BookingFlow/steps/AddonsSelection.tsx +118 -0
  7. package/components/BookingFlow/steps/Confirmation.tsx +185 -0
  8. package/components/BookingFlow/steps/ContactForm.tsx +292 -0
  9. package/components/BookingFlow/steps/CycleAwareDateSelection.tsx +277 -0
  10. package/components/BookingFlow/steps/DateSelection.tsx +473 -0
  11. package/components/BookingFlow/steps/ServiceSelection.tsx +315 -0
  12. package/components/BookingFlow/steps/TimeSelection.tsx +230 -0
  13. package/components/BookingFlow/steps/index.ts +10 -0
  14. package/components/BottomSheet/index.tsx +120 -0
  15. package/components/Forms/FormBlock.tsx +283 -0
  16. package/components/Forms/FormField.tsx +385 -0
  17. package/components/Forms/FormRenderer.tsx +216 -0
  18. package/components/Forms/FormValidation.ts +122 -0
  19. package/components/Forms/index.ts +4 -0
  20. package/components/HoldTimer/HoldTimer.tsx +266 -0
  21. package/components/HoldTimer/index.ts +2 -0
  22. package/components/SectionRenderer.tsx +558 -0
  23. package/components/Sections/About.tsx +145 -0
  24. package/components/Sections/BeforeAfter.tsx +81 -0
  25. package/components/Sections/BookingSection.tsx +76 -0
  26. package/components/Sections/Contact.tsx +103 -0
  27. package/components/Sections/FAQSection.tsx +239 -0
  28. package/components/Sections/FeatureContent.tsx +113 -0
  29. package/components/Sections/FeaturedLink.tsx +103 -0
  30. package/components/Sections/FixedInfoCard.tsx +189 -0
  31. package/components/Sections/Gallery.tsx +83 -0
  32. package/components/Sections/Header.tsx +78 -0
  33. package/components/Sections/Hero.tsx +178 -0
  34. package/components/Sections/ImageSection.tsx +147 -0
  35. package/components/Sections/InstagramFeed.tsx +38 -0
  36. package/components/Sections/LinkList.tsx +76 -0
  37. package/components/Sections/LocationMap.tsx +202 -0
  38. package/components/Sections/Logo.tsx +61 -0
  39. package/components/Sections/MinimalFooter.tsx +78 -0
  40. package/components/Sections/MinimalHeader.tsx +81 -0
  41. package/components/Sections/MinimalNavigation.tsx +63 -0
  42. package/components/Sections/Navbar.tsx +258 -0
  43. package/components/Sections/PricingTable.tsx +106 -0
  44. package/components/Sections/ScrollingTextDivider.tsx +138 -0
  45. package/components/Sections/ScrollingTextDivider.tsx.bak +138 -0
  46. package/components/Sections/ServicesPreview.tsx +129 -0
  47. package/components/Sections/SocialBar.tsx +177 -0
  48. package/components/Sections/Team.tsx +80 -0
  49. package/components/Sections/Testimonials.tsx +92 -0
  50. package/components/Sections/TextSection.tsx +116 -0
  51. package/components/Sections/VideoSection.tsx +178 -0
  52. package/components/Sections/index.ts +57 -0
  53. package/components/index.ts +21 -0
  54. package/dist/index-DAai7Glf.d.mts +474 -0
  55. package/dist/index-DAai7Glf.d.ts +474 -0
  56. package/dist/index.d.mts +1075 -0
  57. package/dist/index.d.ts +1075 -0
  58. package/dist/index.js +22 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/index.mjs +22 -0
  61. package/dist/index.mjs.map +1 -0
  62. package/dist/styles/index.d.mts +1 -0
  63. package/dist/styles/index.d.ts +1 -0
  64. package/dist/styles/index.js +2 -0
  65. package/dist/styles/index.js.map +1 -0
  66. package/dist/styles/index.mjs +2 -0
  67. package/dist/styles/index.mjs.map +1 -0
  68. package/docs/API.md +849 -0
  69. package/docs/CALLBACKS.md +760 -0
  70. package/docs/COMPLETE_SESSION_SUMMARY.md +404 -0
  71. package/docs/DATA_SHAPES.md +684 -0
  72. package/docs/MIGRATION.md +662 -0
  73. package/docs/PAYMENT_INTEGRATION.md +766 -0
  74. package/docs/SESSION_SUMMARY.md +185 -0
  75. package/docs/STYLING.md +735 -0
  76. package/index.ts +4 -0
  77. package/lib/storage.ts +239 -0
  78. package/package.json +59 -0
  79. package/styles/animations.ts +210 -0
  80. package/styles/index.ts +1 -0
  81. package/tsconfig.json +32 -0
  82. package/tsup.config.ts +13 -0
  83. package/types/index.ts +369 -0
@@ -0,0 +1,63 @@
1
+ 'use client';
2
+
3
+ export interface NavLink {
4
+ id: string;
5
+ label: string;
6
+ url: string;
7
+ linkType?: 'internal' | 'external';
8
+ }
9
+
10
+ export interface MinimalNavigationProps {
11
+ links: NavLink[];
12
+ style?: 'horizontal' | 'vertical';
13
+ colors: {
14
+ text: string;
15
+ };
16
+ enableInlineEditing?: boolean;
17
+ sectionId?: string;
18
+ }
19
+
20
+ export function MinimalNavigation({
21
+ links,
22
+ style = 'horizontal',
23
+ colors,
24
+ enableInlineEditing = false,
25
+ sectionId = ''
26
+ }: MinimalNavigationProps) {
27
+ const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, url: string) => {
28
+ if (url.startsWith('#')) {
29
+ e.preventDefault();
30
+ const sectionId = url.substring(1);
31
+ const element = document.querySelector(`[data-section-id="${sectionId}"]`);
32
+ if (element) {
33
+ element.scrollIntoView({ behavior: 'smooth', block: 'start' });
34
+ }
35
+ }
36
+ };
37
+
38
+ return (
39
+ <nav className="container mx-auto px-4 py-4">
40
+ <div className={`flex ${style === 'vertical' ? 'flex-col space-y-2' : 'flex-row space-x-6 justify-center'}`}>
41
+ {links.map((link, index) => (
42
+ <a
43
+ key={link.id}
44
+ href={link.url}
45
+ onClick={(e) => handleNavClick(e, link.url)}
46
+ className="text-base font-medium transition-colors hover:opacity-70"
47
+ style={{ color: colors.text }}
48
+ {...(link.linkType === 'external' && !link.url.startsWith('#') ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
49
+ {...(enableInlineEditing && {
50
+ 'data-editable': true,
51
+ 'data-section-id': sectionId,
52
+ 'data-field-path': `settings.links.${index}.label`
53
+ })}
54
+ >
55
+ {link.label}
56
+ </a>
57
+ ))}
58
+ </div>
59
+ </nav>
60
+ );
61
+ }
62
+
63
+ export default MinimalNavigation;
@@ -0,0 +1,258 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect } from 'react';
4
+
5
+ export interface NavbarLink {
6
+ id: string;
7
+ label: string;
8
+ url: string;
9
+ linkType?: 'internal' | 'external';
10
+ }
11
+
12
+ export interface NavbarProps {
13
+ brandName?: string;
14
+ logoUrl?: string;
15
+ showLogo?: boolean;
16
+ showBrandName?: boolean;
17
+ links: NavbarLink[];
18
+ ctaText?: string;
19
+ ctaUrl?: string;
20
+ sticky?: boolean;
21
+ transparentOnTop?: boolean;
22
+ colors: {
23
+ primary: string;
24
+ text: string;
25
+ buttonText?: string;
26
+ };
27
+ typography?: {
28
+ headingFont?: string;
29
+ bodyFont?: string;
30
+ };
31
+ enableInlineEditing?: boolean;
32
+ sectionId?: string;
33
+ }
34
+
35
+ export function Navbar({
36
+ brandName = 'Your Brand',
37
+ logoUrl,
38
+ showLogo = true,
39
+ showBrandName = true,
40
+ links = [],
41
+ ctaText,
42
+ ctaUrl,
43
+ sticky = true,
44
+ transparentOnTop = false,
45
+ colors,
46
+ typography,
47
+ enableInlineEditing = false,
48
+ sectionId = ''
49
+ }: NavbarProps) {
50
+ const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
51
+ const [scrolled, setScrolled] = useState(false);
52
+ const [activeSection, setActiveSection] = useState('');
53
+
54
+ // Handle scroll for sticky navbar effects
55
+ useEffect(() => {
56
+ const handleScroll = () => {
57
+ setScrolled(window.scrollY > 20);
58
+ };
59
+
60
+ window.addEventListener('scroll', handleScroll);
61
+ return () => window.removeEventListener('scroll', handleScroll);
62
+ }, []);
63
+
64
+ // Track active section for navigation highlighting
65
+ useEffect(() => {
66
+ const handleScroll = () => {
67
+ const sections = links
68
+ .filter(link => link.url.startsWith('#'))
69
+ .map(link => document.querySelector(`[data-section-id="${link.url.substring(1)}"]`))
70
+ .filter(Boolean);
71
+
72
+ let currentSection = '';
73
+ sections.forEach((section) => {
74
+ if (section) {
75
+ const rect = section.getBoundingClientRect();
76
+ if (rect.top <= 100 && rect.bottom >= 100) {
77
+ currentSection = section.getAttribute('data-section-id') || '';
78
+ }
79
+ }
80
+ });
81
+
82
+ setActiveSection(currentSection);
83
+ };
84
+
85
+ window.addEventListener('scroll', handleScroll);
86
+ return () => window.removeEventListener('scroll', handleScroll);
87
+ }, [links]);
88
+
89
+ const handleNavClick = (e: React.MouseEvent<HTMLAnchorElement>, url: string) => {
90
+ if (url.startsWith('#')) {
91
+ e.preventDefault();
92
+ const sectionId = url.substring(1);
93
+ const element = document.querySelector(`[data-section-id="${sectionId}"]`);
94
+ if (element) {
95
+ element.scrollIntoView({ behavior: 'smooth', block: 'start' });
96
+ }
97
+ setMobileMenuOpen(false);
98
+ }
99
+ };
100
+
101
+ const isTransparent = transparentOnTop && !scrolled;
102
+ const navbarBg = isTransparent
103
+ ? 'bg-transparent'
104
+ : sticky
105
+ ? 'bg-white/80 backdrop-blur-md border-b'
106
+ : 'bg-white border-b';
107
+
108
+ return (
109
+ <nav
110
+ className={`${sticky ? 'sticky top-0 z-50' : ''} ${navbarBg} transition-all duration-300`}
111
+ style={{
112
+ borderColor: isTransparent ? 'transparent' : `${colors.primary}20`
113
+ }}
114
+ >
115
+ <div className="container mx-auto px-4">
116
+ <div className="flex items-center justify-between h-16 md:h-20">
117
+ {/* Logo & Brand */}
118
+ <div className="flex items-center gap-3">
119
+ {showLogo && logoUrl && (
120
+ <img
121
+ src={logoUrl}
122
+ alt={brandName}
123
+ className="h-8 md:h-10 w-auto object-contain"
124
+ />
125
+ )}
126
+ {showBrandName && (
127
+ <h1
128
+ className="text-xl md:text-2xl font-bold"
129
+ style={{
130
+ fontFamily: typography?.headingFont || 'inherit',
131
+ color: colors.text
132
+ }}
133
+ {...(enableInlineEditing && {
134
+ 'data-editable': true,
135
+ 'data-section-id': sectionId,
136
+ 'data-field-path': 'settings.brandName'
137
+ })}
138
+ >
139
+ {brandName}
140
+ </h1>
141
+ )}
142
+ </div>
143
+
144
+ {/* Desktop Navigation */}
145
+ <div className="hidden md:flex items-center gap-8">
146
+ {links.map((link, index) => {
147
+ const isActive = link.url.startsWith('#') && link.url.substring(1) === activeSection;
148
+ return (
149
+ <a
150
+ key={link.id}
151
+ href={link.url}
152
+ onClick={(e) => handleNavClick(e, link.url)}
153
+ className="relative text-base font-medium transition-colors group"
154
+ style={{
155
+ color: isActive ? colors.primary : colors.text,
156
+ fontFamily: typography?.bodyFont || 'inherit'
157
+ }}
158
+ {...(link.linkType === 'external' && !link.url.startsWith('#') ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
159
+ {...(enableInlineEditing && {
160
+ 'data-editable': true,
161
+ 'data-section-id': sectionId,
162
+ 'data-field-path': `settings.links.${index}.label`
163
+ })}
164
+ >
165
+ {link.label}
166
+ {/* Active indicator */}
167
+ <span
168
+ className={`absolute -bottom-1 left-0 h-0.5 transition-all ${isActive ? 'w-full' : 'w-0 group-hover:w-full'}`}
169
+ style={{ backgroundColor: colors.primary }}
170
+ />
171
+ </a>
172
+ );
173
+ })}
174
+
175
+ {/* CTA Button */}
176
+ {ctaText && ctaUrl && (
177
+ <a
178
+ href={ctaUrl}
179
+ className="px-6 py-2.5 rounded-full font-semibold transition-all hover:opacity-90 hover:scale-105"
180
+ style={{
181
+ backgroundColor: colors.primary,
182
+ color: colors.buttonText || '#FFFFFF',
183
+ fontFamily: typography?.bodyFont || 'inherit'
184
+ }}
185
+ >
186
+ {ctaText}
187
+ </a>
188
+ )}
189
+ </div>
190
+
191
+ {/* Mobile Menu Button */}
192
+ <button
193
+ onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
194
+ className="md:hidden p-2 rounded-lg transition-colors hover:bg-black/5"
195
+ aria-label="Toggle menu"
196
+ >
197
+ <svg
198
+ className="w-6 h-6"
199
+ fill="none"
200
+ stroke={colors.text}
201
+ viewBox="0 0 24 24"
202
+ >
203
+ {mobileMenuOpen ? (
204
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
205
+ ) : (
206
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
207
+ )}
208
+ </svg>
209
+ </button>
210
+ </div>
211
+
212
+ {/* Mobile Menu */}
213
+ <div
214
+ className={`md:hidden overflow-hidden transition-all duration-300 ${mobileMenuOpen ? 'max-h-96 pb-4' : 'max-h-0'}`}
215
+ >
216
+ <div className="flex flex-col space-y-3 pt-4 border-t" style={{ borderColor: `${colors.primary}20` }}>
217
+ {links.map((link, index) => {
218
+ const isActive = link.url.startsWith('#') && link.url.substring(1) === activeSection;
219
+ return (
220
+ <a
221
+ key={link.id}
222
+ href={link.url}
223
+ onClick={(e) => handleNavClick(e, link.url)}
224
+ className="text-base font-medium py-2 px-4 rounded-lg transition-colors"
225
+ style={{
226
+ color: isActive ? colors.primary : colors.text,
227
+ backgroundColor: isActive ? `${colors.primary}10` : 'transparent',
228
+ fontFamily: typography?.bodyFont || 'inherit'
229
+ }}
230
+ {...(link.linkType === 'external' && !link.url.startsWith('#') ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
231
+ >
232
+ {link.label}
233
+ </a>
234
+ );
235
+ })}
236
+
237
+ {/* Mobile CTA Button */}
238
+ {ctaText && ctaUrl && (
239
+ <a
240
+ href={ctaUrl}
241
+ className="px-6 py-3 rounded-full font-semibold text-center transition-all hover:opacity-90"
242
+ style={{
243
+ backgroundColor: colors.primary,
244
+ color: colors.buttonText || '#FFFFFF',
245
+ fontFamily: typography?.bodyFont || 'inherit'
246
+ }}
247
+ >
248
+ {ctaText}
249
+ </a>
250
+ )}
251
+ </div>
252
+ </div>
253
+ </div>
254
+ </nav>
255
+ );
256
+ }
257
+
258
+ export default Navbar;
@@ -0,0 +1,106 @@
1
+ 'use client';
2
+
3
+ export interface PricingPlan {
4
+ id: string | number;
5
+ name: string;
6
+ price: number;
7
+ features: string[];
8
+ featured?: boolean;
9
+ }
10
+
11
+ export interface PricingTableProps {
12
+ title?: string;
13
+ plans?: PricingPlan[];
14
+ colors: {
15
+ primary: string;
16
+ text: string;
17
+ };
18
+ typography: {
19
+ headingFont?: string;
20
+ };
21
+ enableInlineEditing?: boolean;
22
+ sectionId?: string;
23
+ }
24
+
25
+ export function PricingTable({
26
+ title,
27
+ plans = [],
28
+ colors,
29
+ typography,
30
+ enableInlineEditing = false,
31
+ sectionId = ''
32
+ }: PricingTableProps) {
33
+ const mockPlans = plans.length > 0 ? plans : [
34
+ { id: 1, name: 'Basic', price: 50, features: ['Service 1', 'Service 2'] },
35
+ { id: 2, name: 'Premium', price: 100, features: ['Service 1', 'Service 2', 'Service 3'], featured: true },
36
+ { id: 3, name: 'Deluxe', price: 150, features: ['All Services', 'Priority Booking'] }
37
+ ];
38
+
39
+ return (
40
+ <section className="py-16 px-6">
41
+ <div className="max-w-7xl mx-auto">
42
+ <h2
43
+ className="text-3xl md:text-4xl font-bold text-center mb-12"
44
+ style={{ fontFamily: typography.headingFont, color: colors.text }}
45
+ {...(enableInlineEditing && {
46
+ 'data-editable': true,
47
+ 'data-section-id': sectionId,
48
+ 'data-field-path': 'settings.title'
49
+ })}
50
+ >
51
+ {title || 'Pricing'}
52
+ </h2>
53
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-8">
54
+ {mockPlans.map((plan) => (
55
+ <div
56
+ key={plan.id}
57
+ className={`p-8 rounded-lg border-2 ${
58
+ plan.featured ? 'scale-105 shadow-xl' : ''
59
+ }`}
60
+ style={{
61
+ borderColor: plan.featured ? colors.primary : 'rgba(255,255,255,0.1)',
62
+ backgroundColor: plan.featured ? `${colors.primary}15` : 'rgba(255,255,255,0.05)'
63
+ }}
64
+ >
65
+ <h3
66
+ className="text-2xl font-bold mb-2 text-center"
67
+ style={{ color: colors.text }}
68
+ >
69
+ {plan.name}
70
+ </h3>
71
+ <div className="text-center mb-6">
72
+ <span
73
+ className="text-4xl font-bold"
74
+ style={{ color: colors.primary }}
75
+ >
76
+ ${plan.price}
77
+ </span>
78
+ </div>
79
+ <ul className="space-y-3 mb-6">
80
+ {plan.features.map((feature, i) => (
81
+ <li key={i} className="flex items-center gap-2">
82
+ <svg className="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" style={{ color: colors.primary }}>
83
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
84
+ </svg>
85
+ <span style={{ color: colors.text }}>{feature}</span>
86
+ </li>
87
+ ))}
88
+ </ul>
89
+ <button
90
+ className="w-full px-6 py-3 rounded-lg font-semibold transition-all hover:scale-105"
91
+ style={{
92
+ backgroundColor: plan.featured ? colors.primary : 'rgba(255,255,255,0.1)',
93
+ color: plan.featured ? '#FFFFFF' : colors.text
94
+ }}
95
+ >
96
+ Choose Plan
97
+ </button>
98
+ </div>
99
+ ))}
100
+ </div>
101
+ </div>
102
+ </section>
103
+ );
104
+ }
105
+
106
+ export default PricingTable;
@@ -0,0 +1,138 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ export interface ScrollingTextDividerProps {
6
+ text: string;
7
+ scrollDirection?: 'left' | 'right';
8
+ scrollSpeed?: 'slow' | 'medium' | 'fast' | number;
9
+ orientation?: 'horizontal' | 'diagonal-left' | 'diagonal-right';
10
+ dividerIcon?: 'bullet' | 'star' | 'diamond' | 'custom' | 'none';
11
+ customIcon?: string;
12
+ textSize?: 'small' | 'medium' | 'large' | 'xl';
13
+ textColor?: string;
14
+ fontWeight?: 'light' | 'normal' | 'bold';
15
+ backgroundColor?: string;
16
+ backgroundGradient?: string;
17
+ pauseOnHover?: boolean;
18
+ colors: {
19
+ primary: string;
20
+ text: string;
21
+ };
22
+ typography?: {
23
+ headingFont?: string;
24
+ };
25
+ enableInlineEditing?: boolean;
26
+ sectionId?: string;
27
+ }
28
+
29
+ export function ScrollingTextDivider({
30
+ text = 'SCROLL TEXT',
31
+ scrollDirection = 'left',
32
+ scrollSpeed = 'medium',
33
+ orientation = 'horizontal',
34
+ dividerIcon = 'bullet',
35
+ customIcon,
36
+ textSize = 'medium',
37
+ textColor,
38
+ fontWeight = 'bold',
39
+ backgroundColor,
40
+ backgroundGradient,
41
+ pauseOnHover = true,
42
+ colors,
43
+ typography,
44
+ enableInlineEditing = false,
45
+ sectionId = ''
46
+ }: ScrollingTextDividerProps) {
47
+ const [isPaused, setIsPaused] = useState(false);
48
+
49
+ // Icon mapping
50
+ const iconMap = {
51
+ bullet: '•',
52
+ star: '★',
53
+ diamond: '◆',
54
+ custom: customIcon || '•',
55
+ none: ''
56
+ };
57
+
58
+ const icon = iconMap[dividerIcon];
59
+
60
+ // Speed mapping (duration in seconds)
61
+ const speedMap = {
62
+ slow: 60,
63
+ medium: 30,
64
+ fast: 15
65
+ };
66
+
67
+ const duration = typeof scrollSpeed === 'number' ? scrollSpeed : speedMap[scrollSpeed];
68
+
69
+ // Text size mapping
70
+ const textSizeMap = {
71
+ small: 'text-xl md:text-2xl',
72
+ medium: 'text-2xl md:text-4xl',
73
+ large: 'text-3xl md:text-5xl',
74
+ xl: 'text-4xl md:text-6xl'
75
+ };
76
+
77
+ // Font weight mapping
78
+ const fontWeightMap = {
79
+ light: 'font-light',
80
+ normal: 'font-normal',
81
+ bold: 'font-bold'
82
+ };
83
+
84
+ // Orientation angle
85
+ const rotationMap = {
86
+ horizontal: '0deg',
87
+ 'diagonal-left': '-5deg',
88
+ 'diagonal-right': '5deg'
89
+ };
90
+
91
+ // Create repeating text with icons
92
+ const repeatingText = icon
93
+ ? `${text} ${icon} `.repeat(20)
94
+ : `${text} `.repeat(20);
95
+
96
+ // Background style
97
+ const bgStyle = backgroundGradient
98
+ ? { background: backgroundGradient }
99
+ : { backgroundColor: backgroundColor || 'transparent' };
100
+
101
+ return (
102
+ <div
103
+ className="relative overflow-hidden py-3"
104
+ style={{
105
+ ...bgStyle,
106
+ transform: `rotate(${rotationMap[orientation]})`
107
+ }}
108
+ onMouseEnter={() => pauseOnHover && setIsPaused(true)}
109
+ onMouseLeave={() => pauseOnHover && setIsPaused(false)}
110
+ >
111
+ <style>{`
112
+ @keyframes scroll-${scrollDirection} {
113
+ 0% {
114
+ transform: translateX(${scrollDirection === 'left' ? '0%' : '-50%'});
115
+ }
116
+ 100% {
117
+ transform: translateX(${scrollDirection === 'left' ? '-50%' : '0%'});
118
+ }
119
+ }
120
+ `}</style>
121
+
122
+ <div
123
+ className={`flex items-center whitespace-nowrap ${textSizeMap[textSize]} ${fontWeightMap[fontWeight]} uppercase tracking-wider`}
124
+ style={{
125
+ fontFamily: typography?.headingFont || 'inherit',
126
+ color: textColor || colors.text,
127
+ animation: `scroll-${scrollDirection} ${duration}s linear infinite`,
128
+ animationPlayState: isPaused ? 'paused' : 'running'
129
+ }}
130
+ >
131
+ <span className="inline-block">{repeatingText}</span>
132
+ <span className="inline-block" aria-hidden="true">{repeatingText}</span>
133
+ </div>
134
+ </div>
135
+ );
136
+ }
137
+
138
+ export default ScrollingTextDivider;
@@ -0,0 +1,138 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+
5
+ export interface ScrollingTextDividerProps {
6
+ text: string;
7
+ scrollDirection?: 'left' | 'right';
8
+ scrollSpeed?: 'slow' | 'medium' | 'fast' | number;
9
+ orientation?: 'horizontal' | 'diagonal-left' | 'diagonal-right';
10
+ dividerIcon?: 'bullet' | 'star' | 'diamond' | 'custom' | 'none';
11
+ customIcon?: string;
12
+ textSize?: 'small' | 'medium' | 'large' | 'xl';
13
+ textColor?: string;
14
+ fontWeight?: 'light' | 'normal' | 'bold';
15
+ backgroundColor?: string;
16
+ backgroundGradient?: string;
17
+ pauseOnHover?: boolean;
18
+ colors: {
19
+ primary: string;
20
+ text: string;
21
+ };
22
+ typography?: {
23
+ headingFont?: string;
24
+ };
25
+ enableInlineEditing?: boolean;
26
+ sectionId?: string;
27
+ }
28
+
29
+ export function ScrollingTextDivider({
30
+ text = 'SCROLL TEXT',
31
+ scrollDirection = 'left',
32
+ scrollSpeed = 'medium',
33
+ orientation = 'horizontal',
34
+ dividerIcon = 'bullet',
35
+ customIcon,
36
+ textSize = 'medium',
37
+ textColor,
38
+ fontWeight = 'bold',
39
+ backgroundColor,
40
+ backgroundGradient,
41
+ pauseOnHover = true,
42
+ colors,
43
+ typography,
44
+ enableInlineEditing = false,
45
+ sectionId = ''
46
+ }: ScrollingTextDividerProps) {
47
+ const [isPaused, setIsPaused] = useState(false);
48
+
49
+ // Icon mapping
50
+ const iconMap = {
51
+ bullet: '"',
52
+ star: '',
53
+ diamond: '�',
54
+ custom: customIcon || '"',
55
+ none: ''
56
+ };
57
+
58
+ const icon = iconMap[dividerIcon];
59
+
60
+ // Speed mapping (duration in seconds)
61
+ const speedMap = {
62
+ slow: 60,
63
+ medium: 30,
64
+ fast: 15
65
+ };
66
+
67
+ const duration = typeof scrollSpeed === 'number' ? scrollSpeed : speedMap[scrollSpeed];
68
+
69
+ // Text size mapping
70
+ const textSizeMap = {
71
+ small: 'text-xl md:text-2xl',
72
+ medium: 'text-2xl md:text-4xl',
73
+ large: 'text-3xl md:text-5xl',
74
+ xl: 'text-4xl md:text-6xl'
75
+ };
76
+
77
+ // Font weight mapping
78
+ const fontWeightMap = {
79
+ light: 'font-light',
80
+ normal: 'font-normal',
81
+ bold: 'font-bold'
82
+ };
83
+
84
+ // Orientation angle
85
+ const rotationMap = {
86
+ horizontal: '0deg',
87
+ 'diagonal-left': '-5deg',
88
+ 'diagonal-right': '5deg'
89
+ };
90
+
91
+ // Create repeating text with icons
92
+ const repeatingText = icon
93
+ ? `${text} ${icon} `.repeat(20)
94
+ : `${text} `.repeat(20);
95
+
96
+ // Background style
97
+ const bgStyle = backgroundGradient
98
+ ? { background: backgroundGradient }
99
+ : { backgroundColor: backgroundColor || 'transparent' };
100
+
101
+ return (
102
+ <div
103
+ className="relative overflow-hidden py-3"
104
+ style={{
105
+ ...bgStyle,
106
+ transform: `rotate(${rotationMap[orientation]})`
107
+ }}
108
+ onMouseEnter={() => pauseOnHover && setIsPaused(true)}
109
+ onMouseLeave={() => pauseOnHover && setIsPaused(false)}
110
+ >
111
+ <style>{`
112
+ @keyframes scroll-${scrollDirection} {
113
+ 0% {
114
+ transform: translateX(${scrollDirection === 'left' ? '0%' : '-50%'});
115
+ }
116
+ 100% {
117
+ transform: translateX(${scrollDirection === 'left' ? '-50%' : '0%'});
118
+ }
119
+ }
120
+ `}</style>
121
+
122
+ <div
123
+ className={`flex items-center whitespace-nowrap ${textSizeMap[textSize]} ${fontWeightMap[fontWeight]} uppercase tracking-wider`}
124
+ style={{
125
+ fontFamily: typography?.headingFont || 'inherit',
126
+ color: textColor || colors.text,
127
+ animation: `scroll-${scrollDirection} ${duration}s linear infinite`,
128
+ animationPlayState: isPaused ? 'paused' : 'running'
129
+ }}
130
+ >
131
+ <span className="inline-block">{repeatingText}</span>
132
+ <span className="inline-block" aria-hidden="true">{repeatingText}</span>
133
+ </div>
134
+ </div>
135
+ );
136
+ }
137
+
138
+ export default ScrollingTextDivider;