@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.
- package/README.md +319 -0
- package/TENANT_DATA_INTEGRATION.md +402 -0
- package/TENANT_SETUP.md +316 -0
- package/components/BookingFlow/BookingFlow.tsx +790 -0
- package/components/BookingFlow/index.ts +5 -0
- package/components/BookingFlow/steps/AddonsSelection.tsx +118 -0
- package/components/BookingFlow/steps/Confirmation.tsx +185 -0
- package/components/BookingFlow/steps/ContactForm.tsx +292 -0
- package/components/BookingFlow/steps/CycleAwareDateSelection.tsx +277 -0
- package/components/BookingFlow/steps/DateSelection.tsx +473 -0
- package/components/BookingFlow/steps/ServiceSelection.tsx +315 -0
- package/components/BookingFlow/steps/TimeSelection.tsx +230 -0
- package/components/BookingFlow/steps/index.ts +10 -0
- package/components/BottomSheet/index.tsx +120 -0
- package/components/Forms/FormBlock.tsx +283 -0
- package/components/Forms/FormField.tsx +385 -0
- package/components/Forms/FormRenderer.tsx +216 -0
- package/components/Forms/FormValidation.ts +122 -0
- package/components/Forms/index.ts +4 -0
- package/components/HoldTimer/HoldTimer.tsx +266 -0
- package/components/HoldTimer/index.ts +2 -0
- package/components/SectionRenderer.tsx +558 -0
- package/components/Sections/About.tsx +145 -0
- package/components/Sections/BeforeAfter.tsx +81 -0
- package/components/Sections/BookingSection.tsx +76 -0
- package/components/Sections/Contact.tsx +103 -0
- package/components/Sections/FAQSection.tsx +239 -0
- package/components/Sections/FeatureContent.tsx +113 -0
- package/components/Sections/FeaturedLink.tsx +103 -0
- package/components/Sections/FixedInfoCard.tsx +189 -0
- package/components/Sections/Gallery.tsx +83 -0
- package/components/Sections/Header.tsx +78 -0
- package/components/Sections/Hero.tsx +178 -0
- package/components/Sections/ImageSection.tsx +147 -0
- package/components/Sections/InstagramFeed.tsx +38 -0
- package/components/Sections/LinkList.tsx +76 -0
- package/components/Sections/LocationMap.tsx +202 -0
- package/components/Sections/Logo.tsx +61 -0
- package/components/Sections/MinimalFooter.tsx +78 -0
- package/components/Sections/MinimalHeader.tsx +81 -0
- package/components/Sections/MinimalNavigation.tsx +63 -0
- package/components/Sections/Navbar.tsx +258 -0
- package/components/Sections/PricingTable.tsx +106 -0
- package/components/Sections/ScrollingTextDivider.tsx +138 -0
- package/components/Sections/ScrollingTextDivider.tsx.bak +138 -0
- package/components/Sections/ServicesPreview.tsx +129 -0
- package/components/Sections/SocialBar.tsx +177 -0
- package/components/Sections/Team.tsx +80 -0
- package/components/Sections/Testimonials.tsx +92 -0
- package/components/Sections/TextSection.tsx +116 -0
- package/components/Sections/VideoSection.tsx +178 -0
- package/components/Sections/index.ts +57 -0
- package/components/index.ts +21 -0
- package/dist/index-DAai7Glf.d.mts +474 -0
- package/dist/index-DAai7Glf.d.ts +474 -0
- package/dist/index.d.mts +1075 -0
- package/dist/index.d.ts +1075 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +22 -0
- package/dist/index.mjs.map +1 -0
- package/dist/styles/index.d.mts +1 -0
- package/dist/styles/index.d.ts +1 -0
- package/dist/styles/index.js +2 -0
- package/dist/styles/index.js.map +1 -0
- package/dist/styles/index.mjs +2 -0
- package/dist/styles/index.mjs.map +1 -0
- package/docs/API.md +849 -0
- package/docs/CALLBACKS.md +760 -0
- package/docs/COMPLETE_SESSION_SUMMARY.md +404 -0
- package/docs/DATA_SHAPES.md +684 -0
- package/docs/MIGRATION.md +662 -0
- package/docs/PAYMENT_INTEGRATION.md +766 -0
- package/docs/SESSION_SUMMARY.md +185 -0
- package/docs/STYLING.md +735 -0
- package/index.ts +4 -0
- package/lib/storage.ts +239 -0
- package/package.json +59 -0
- package/styles/animations.ts +210 -0
- package/styles/index.ts +1 -0
- package/tsconfig.json +32 -0
- package/tsup.config.ts +13 -0
- package/types/index.ts +369 -0
package/lib/storage.ts
ADDED
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Form Image Storage Utilities for OVIAH Booking Components
|
|
3
|
+
*
|
|
4
|
+
* Handles image uploads for form photo fields using Supabase Storage
|
|
5
|
+
* Targets the "form-uploads" bucket for form submission attachments
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { createClient } from '@supabase/supabase-js';
|
|
9
|
+
|
|
10
|
+
// Supabase client will be initialized from environment variables
|
|
11
|
+
let supabaseClient: ReturnType<typeof createClient> | null = null;
|
|
12
|
+
|
|
13
|
+
export function getSupabaseClient() {
|
|
14
|
+
if (!supabaseClient) {
|
|
15
|
+
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL;
|
|
16
|
+
const supabaseKey = import.meta.env.VITE_SUPABASE_ANON_KEY || process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
|
|
17
|
+
|
|
18
|
+
if (!supabaseUrl || !supabaseKey) {
|
|
19
|
+
throw new Error('Supabase URL and Anon Key must be provided');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
supabaseClient = createClient(supabaseUrl, supabaseKey);
|
|
23
|
+
}
|
|
24
|
+
return supabaseClient;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Storage configuration for form uploads
|
|
28
|
+
export const FORM_UPLOAD_CONFIG = {
|
|
29
|
+
bucket: 'form-uploads',
|
|
30
|
+
structure: '{clientId}/{formId}/{timestamp}_{filename}',
|
|
31
|
+
limits: {
|
|
32
|
+
maxSize: 5 * 1024 * 1024, // 5MB in bytes
|
|
33
|
+
allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/jpg'],
|
|
34
|
+
}
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
export interface UploadedFormImage {
|
|
38
|
+
url: string;
|
|
39
|
+
path: string;
|
|
40
|
+
filename: string;
|
|
41
|
+
size: number;
|
|
42
|
+
type: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Upload a form image to Supabase Storage
|
|
47
|
+
*/
|
|
48
|
+
export async function uploadFormImage(
|
|
49
|
+
file: File,
|
|
50
|
+
clientId: string,
|
|
51
|
+
formId: string
|
|
52
|
+
): Promise<UploadedFormImage> {
|
|
53
|
+
console.log('📦 uploadFormImage called:', { fileName: file.name, clientId, formId });
|
|
54
|
+
|
|
55
|
+
// Validate file size
|
|
56
|
+
if (file.size > FORM_UPLOAD_CONFIG.limits.maxSize) {
|
|
57
|
+
throw new Error(`File size exceeds maximum of ${FORM_UPLOAD_CONFIG.limits.maxSize / 1024 / 1024}MB`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Validate file type
|
|
61
|
+
if (!FORM_UPLOAD_CONFIG.limits.allowedTypes.includes(file.type)) {
|
|
62
|
+
throw new Error(`File type ${file.type} not allowed. Accepted types: JPG, PNG, WebP`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log('🎨 Starting image optimization...');
|
|
66
|
+
// Optimize image before upload
|
|
67
|
+
const optimizedFile = await optimizeImage(file);
|
|
68
|
+
console.log('✅ Image optimized:', { originalSize: file.size, optimizedSize: optimizedFile.size });
|
|
69
|
+
|
|
70
|
+
// Generate unique filename
|
|
71
|
+
const timestamp = Date.now();
|
|
72
|
+
const fileExt = file.name.split('.').pop()?.toLowerCase() || 'jpg';
|
|
73
|
+
const sanitizedName = file.name.replace(/[^a-zA-Z0-9.-]/g, '_').substring(0, 50);
|
|
74
|
+
const fileName = `${timestamp}_${sanitizedName}`;
|
|
75
|
+
const filePath = `${clientId}/${formId}/${fileName}`;
|
|
76
|
+
|
|
77
|
+
console.log('☁️ Uploading to Supabase:', { filePath, bucket: FORM_UPLOAD_CONFIG.bucket });
|
|
78
|
+
|
|
79
|
+
// Get Supabase client
|
|
80
|
+
const supabase = getSupabaseClient();
|
|
81
|
+
|
|
82
|
+
// Upload to Supabase Storage
|
|
83
|
+
const { data, error } = await supabase.storage
|
|
84
|
+
.from(FORM_UPLOAD_CONFIG.bucket)
|
|
85
|
+
.upload(filePath, optimizedFile, {
|
|
86
|
+
cacheControl: '3600',
|
|
87
|
+
upsert: false,
|
|
88
|
+
contentType: optimizedFile.type
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
console.log('📤 Upload result:', { data, error });
|
|
92
|
+
|
|
93
|
+
if (error) {
|
|
94
|
+
console.error('❌ Supabase upload error:', error);
|
|
95
|
+
throw new Error(`Upload failed: ${error.message}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
console.log('✅ Upload successful, getting public URL...');
|
|
99
|
+
|
|
100
|
+
// Get public URL
|
|
101
|
+
const { data: { publicUrl } } = supabase.storage
|
|
102
|
+
.from(FORM_UPLOAD_CONFIG.bucket)
|
|
103
|
+
.getPublicUrl(filePath);
|
|
104
|
+
|
|
105
|
+
console.log('✅ Public URL obtained:', publicUrl);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
url: publicUrl,
|
|
109
|
+
path: filePath,
|
|
110
|
+
filename: file.name,
|
|
111
|
+
size: optimizedFile.size,
|
|
112
|
+
type: optimizedFile.type
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Optimize image before upload
|
|
118
|
+
* - Resize to max 1920px width
|
|
119
|
+
* - Compress to 85% quality
|
|
120
|
+
*/
|
|
121
|
+
export async function optimizeImage(
|
|
122
|
+
file: File,
|
|
123
|
+
maxWidth: number = 1920,
|
|
124
|
+
quality: number = 0.85
|
|
125
|
+
): Promise<File> {
|
|
126
|
+
console.log('🖼️ optimizeImage: Starting optimization');
|
|
127
|
+
|
|
128
|
+
// Determine if the image supports transparency
|
|
129
|
+
const supportsTransparency = file.type === 'image/png' || file.type === 'image/webp';
|
|
130
|
+
const outputType = supportsTransparency ? file.type : 'image/jpeg';
|
|
131
|
+
|
|
132
|
+
console.log('🖼️ optimizeImage: Output type:', outputType, 'Transparency:', supportsTransparency);
|
|
133
|
+
|
|
134
|
+
return new Promise((resolve, reject) => {
|
|
135
|
+
// Set a timeout to prevent hanging
|
|
136
|
+
const timeout = setTimeout(() => {
|
|
137
|
+
console.error('⏱️ optimizeImage: Timeout after 30 seconds');
|
|
138
|
+
reject(new Error('Image optimization timed out'));
|
|
139
|
+
}, 30000);
|
|
140
|
+
|
|
141
|
+
const img = new Image();
|
|
142
|
+
const canvas = document.createElement('canvas');
|
|
143
|
+
const ctx = canvas.getContext('2d', { alpha: supportsTransparency });
|
|
144
|
+
|
|
145
|
+
if (!ctx) {
|
|
146
|
+
clearTimeout(timeout);
|
|
147
|
+
console.error('❌ optimizeImage: Canvas context not available');
|
|
148
|
+
reject(new Error('Canvas context not available'));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
console.log('🖼️ optimizeImage: Created canvas and context');
|
|
153
|
+
|
|
154
|
+
img.onload = () => {
|
|
155
|
+
console.log('🖼️ optimizeImage: Image loaded', { width: img.width, height: img.height });
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
// Calculate new dimensions
|
|
159
|
+
const ratio = Math.min(maxWidth / img.width, 1);
|
|
160
|
+
canvas.width = img.width * ratio;
|
|
161
|
+
canvas.height = img.height * ratio;
|
|
162
|
+
|
|
163
|
+
console.log('🖼️ optimizeImage: Resizing to', { width: canvas.width, height: canvas.height });
|
|
164
|
+
|
|
165
|
+
// Clear canvas with transparency if needed
|
|
166
|
+
if (supportsTransparency) {
|
|
167
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Draw resized image
|
|
171
|
+
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
|
172
|
+
|
|
173
|
+
console.log('🖼️ optimizeImage: Image drawn, converting to blob...');
|
|
174
|
+
|
|
175
|
+
// Convert to blob
|
|
176
|
+
canvas.toBlob(
|
|
177
|
+
(blob) => {
|
|
178
|
+
clearTimeout(timeout);
|
|
179
|
+
|
|
180
|
+
if (!blob) {
|
|
181
|
+
console.error('❌ optimizeImage: Blob conversion failed');
|
|
182
|
+
reject(new Error('Image optimization failed'));
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.log('✅ optimizeImage: Blob created', { size: blob.size, type: outputType });
|
|
187
|
+
|
|
188
|
+
// Create new file with optimized blob
|
|
189
|
+
const optimizedFile = new File([blob], file.name, {
|
|
190
|
+
type: outputType,
|
|
191
|
+
lastModified: Date.now()
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
console.log('✅ optimizeImage: Optimization complete');
|
|
195
|
+
resolve(optimizedFile);
|
|
196
|
+
},
|
|
197
|
+
outputType,
|
|
198
|
+
quality
|
|
199
|
+
);
|
|
200
|
+
} catch (err) {
|
|
201
|
+
clearTimeout(timeout);
|
|
202
|
+
console.error('❌ optimizeImage: Error during processing', err);
|
|
203
|
+
reject(err);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
img.onerror = (err) => {
|
|
208
|
+
clearTimeout(timeout);
|
|
209
|
+
console.error('❌ optimizeImage: Failed to load image', err);
|
|
210
|
+
reject(new Error('Failed to load image'));
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
console.log('🖼️ optimizeImage: Creating object URL and loading image...');
|
|
214
|
+
img.src = URL.createObjectURL(file);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Validate image file
|
|
220
|
+
*/
|
|
221
|
+
export function validateImageFile(file: File): { valid: boolean; error?: string } {
|
|
222
|
+
// Check file size
|
|
223
|
+
if (file.size > FORM_UPLOAD_CONFIG.limits.maxSize) {
|
|
224
|
+
return {
|
|
225
|
+
valid: false,
|
|
226
|
+
error: `File size exceeds maximum of ${FORM_UPLOAD_CONFIG.limits.maxSize / 1024 / 1024}MB`
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Check file type
|
|
231
|
+
if (!FORM_UPLOAD_CONFIG.limits.allowedTypes.includes(file.type)) {
|
|
232
|
+
return {
|
|
233
|
+
valid: false,
|
|
234
|
+
error: `File type not allowed. Accepted: JPG, PNG, WebP`
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return { valid: true };
|
|
239
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@bailierich/booking-components",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Shared booking flow components for OVIAH platform",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"module": "dist/index.mjs",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/bailierich/oviah-saas-dashboard.git",
|
|
11
|
+
"directory": "packages/booking-components"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"registry": "https://registry.npmjs.org",
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.mjs",
|
|
21
|
+
"require": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./styles": {
|
|
24
|
+
"types": "./dist/styles/index.d.ts",
|
|
25
|
+
"import": "./dist/styles/index.mjs",
|
|
26
|
+
"require": "./dist/styles/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup",
|
|
31
|
+
"dev": "tsup --watch",
|
|
32
|
+
"type-check": "tsc --noEmit"
|
|
33
|
+
},
|
|
34
|
+
"peerDependencies": {
|
|
35
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
36
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"framer-motion": "^12.23.24",
|
|
40
|
+
"lucide-react": "^0.542.0",
|
|
41
|
+
"clsx": "^2.1.1",
|
|
42
|
+
"@supabase/supabase-js": "^2.39.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@types/react": "^19",
|
|
46
|
+
"@types/react-dom": "^19",
|
|
47
|
+
"typescript": "^5",
|
|
48
|
+
"tsup": "^8.0.0"
|
|
49
|
+
},
|
|
50
|
+
"keywords": [
|
|
51
|
+
"booking",
|
|
52
|
+
"appointments",
|
|
53
|
+
"react",
|
|
54
|
+
"components",
|
|
55
|
+
"oviah"
|
|
56
|
+
],
|
|
57
|
+
"author": "OVIAH",
|
|
58
|
+
"license": "UNLICENSED"
|
|
59
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import type { AnimationVariants, TransitionDirection } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Animation duration calculator based on speed and reduced motion preference
|
|
5
|
+
*/
|
|
6
|
+
export function getAnimationDuration(
|
|
7
|
+
speed: 'fast' | 'normal' | 'slow',
|
|
8
|
+
reducedMotion: boolean = false
|
|
9
|
+
): number {
|
|
10
|
+
if (reducedMotion) return 0.15;
|
|
11
|
+
|
|
12
|
+
switch (speed) {
|
|
13
|
+
case 'fast':
|
|
14
|
+
return 0.2;
|
|
15
|
+
case 'slow':
|
|
16
|
+
return 0.5;
|
|
17
|
+
default:
|
|
18
|
+
return 0.3;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Slide animation variants for step transitions
|
|
24
|
+
*/
|
|
25
|
+
export function getSlideVariants(slideDistance: number = 50): AnimationVariants {
|
|
26
|
+
return {
|
|
27
|
+
enter: (direction: TransitionDirection) => ({
|
|
28
|
+
x: direction === 'forward' ? slideDistance : -slideDistance,
|
|
29
|
+
opacity: 0,
|
|
30
|
+
}),
|
|
31
|
+
center: {
|
|
32
|
+
x: 0,
|
|
33
|
+
opacity: 1,
|
|
34
|
+
},
|
|
35
|
+
exit: (direction: TransitionDirection) => ({
|
|
36
|
+
x: direction === 'forward' ? -slideDistance : slideDistance,
|
|
37
|
+
opacity: 0,
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fade animation variants for step transitions
|
|
44
|
+
*/
|
|
45
|
+
export function getFadeVariants(): AnimationVariants {
|
|
46
|
+
return {
|
|
47
|
+
enter: () => ({
|
|
48
|
+
opacity: 0,
|
|
49
|
+
scale: 0.95,
|
|
50
|
+
}),
|
|
51
|
+
center: {
|
|
52
|
+
opacity: 1,
|
|
53
|
+
scale: 1,
|
|
54
|
+
},
|
|
55
|
+
exit: () => ({
|
|
56
|
+
opacity: 0,
|
|
57
|
+
scale: 0.95,
|
|
58
|
+
}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Get transition variants based on style preference
|
|
64
|
+
*/
|
|
65
|
+
export function getTransitionVariants(
|
|
66
|
+
style: 'slide' | 'fade',
|
|
67
|
+
slideDistance?: number
|
|
68
|
+
): AnimationVariants {
|
|
69
|
+
return style === 'fade' ? getFadeVariants() : getSlideVariants(slideDistance);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Common animation configurations
|
|
74
|
+
*/
|
|
75
|
+
export const animations = {
|
|
76
|
+
/**
|
|
77
|
+
* Smooth easing curve for natural motion
|
|
78
|
+
*/
|
|
79
|
+
easing: [0.4, 0, 0.2, 1] as const,
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Spring configuration for elastic animations
|
|
83
|
+
*/
|
|
84
|
+
spring: {
|
|
85
|
+
type: 'spring' as const,
|
|
86
|
+
damping: 30,
|
|
87
|
+
stiffness: 300,
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Stagger children animation
|
|
92
|
+
*/
|
|
93
|
+
staggerChildren: {
|
|
94
|
+
staggerChildren: 0.05,
|
|
95
|
+
delayChildren: 0.1,
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Card hover animation
|
|
100
|
+
*/
|
|
101
|
+
cardHover: {
|
|
102
|
+
scale: 1.02,
|
|
103
|
+
transition: { duration: 0.2 },
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Button hover animation
|
|
108
|
+
*/
|
|
109
|
+
buttonHover: {
|
|
110
|
+
scale: 1.05,
|
|
111
|
+
transition: { duration: 0.2 },
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Fade in animation
|
|
116
|
+
*/
|
|
117
|
+
fadeIn: {
|
|
118
|
+
initial: { opacity: 0 },
|
|
119
|
+
animate: { opacity: 1 },
|
|
120
|
+
exit: { opacity: 0 },
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Slide up animation (for bottom sheets)
|
|
125
|
+
*/
|
|
126
|
+
slideUp: {
|
|
127
|
+
initial: { y: '100%' },
|
|
128
|
+
animate: { y: 0 },
|
|
129
|
+
exit: { y: '100%' },
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Scale in animation
|
|
134
|
+
*/
|
|
135
|
+
scaleIn: {
|
|
136
|
+
initial: { opacity: 0, scale: 0.9 },
|
|
137
|
+
animate: { opacity: 1, scale: 1 },
|
|
138
|
+
exit: { opacity: 0, scale: 0.9 },
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Slide in from left
|
|
143
|
+
*/
|
|
144
|
+
slideInLeft: {
|
|
145
|
+
initial: { opacity: 0, x: -10 },
|
|
146
|
+
animate: { opacity: 1, x: 0 },
|
|
147
|
+
},
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Slide in from right
|
|
151
|
+
*/
|
|
152
|
+
slideInRight: {
|
|
153
|
+
initial: { opacity: 0, x: 10 },
|
|
154
|
+
animate: { opacity: 1, x: 0 },
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Backdrop animation
|
|
159
|
+
*/
|
|
160
|
+
backdrop: {
|
|
161
|
+
initial: { opacity: 0 },
|
|
162
|
+
animate: { opacity: 1 },
|
|
163
|
+
exit: { opacity: 0 },
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Accessibility: Check for reduced motion preference
|
|
169
|
+
*/
|
|
170
|
+
export function shouldReduceMotion(): boolean {
|
|
171
|
+
if (typeof window === 'undefined') return false;
|
|
172
|
+
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Create stagger animation for list items
|
|
177
|
+
*/
|
|
178
|
+
export function createStaggerAnimation(
|
|
179
|
+
itemCount: number,
|
|
180
|
+
baseDelay: number = 0.1,
|
|
181
|
+
delayIncrement: number = 0.05
|
|
182
|
+
) {
|
|
183
|
+
return Array.from({ length: itemCount }, (_, index) => ({
|
|
184
|
+
initial: { opacity: 0, y: 10 },
|
|
185
|
+
animate: { opacity: 1, y: 0 },
|
|
186
|
+
transition: {
|
|
187
|
+
duration: 0.3,
|
|
188
|
+
delay: baseDelay + index * delayIncrement,
|
|
189
|
+
ease: animations.easing,
|
|
190
|
+
},
|
|
191
|
+
}));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Utility to create entrance animation with delay
|
|
196
|
+
*/
|
|
197
|
+
export function createEntranceAnimation(
|
|
198
|
+
delay: number = 0,
|
|
199
|
+
duration: number = 0.3
|
|
200
|
+
) {
|
|
201
|
+
return {
|
|
202
|
+
initial: { opacity: 0, y: 10 },
|
|
203
|
+
animate: { opacity: 1, y: 0 },
|
|
204
|
+
transition: {
|
|
205
|
+
duration,
|
|
206
|
+
delay,
|
|
207
|
+
ease: animations.easing,
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
}
|
package/styles/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './animations';
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"declarationMap": true,
|
|
9
|
+
"outDir": "./dist",
|
|
10
|
+
"rootDir": ".",
|
|
11
|
+
"strict": true,
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"esModuleInterop": true,
|
|
17
|
+
"skipLibCheck": true,
|
|
18
|
+
"forceConsistentCasingInFileNames": true,
|
|
19
|
+
"noEmit": true,
|
|
20
|
+
"paths": {
|
|
21
|
+
"@/*": ["./*"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"include": [
|
|
25
|
+
"components/**/*",
|
|
26
|
+
"types/**/*",
|
|
27
|
+
"styles/**/*",
|
|
28
|
+
"hooks/**/*",
|
|
29
|
+
"index.ts"
|
|
30
|
+
],
|
|
31
|
+
"exclude": ["node_modules", "dist"]
|
|
32
|
+
}
|
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['index.ts', 'styles/index.ts'],
|
|
5
|
+
format: ['cjs', 'esm'],
|
|
6
|
+
dts: true,
|
|
7
|
+
sourcemap: true,
|
|
8
|
+
clean: true,
|
|
9
|
+
external: ['react', 'react-dom'],
|
|
10
|
+
treeshake: true,
|
|
11
|
+
splitting: false,
|
|
12
|
+
minify: true,
|
|
13
|
+
});
|