@pixelated-tech/components 3.7.3 → 3.7.4
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/dist/components/admin/site-health/site-health-on-site-seo.integration.js +15 -0
- package/dist/components/general/callout.js +4 -3
- package/dist/components/general/intersection-observer.js +117 -0
- package/dist/components/general/microinteractions.js +39 -50
- package/dist/components/general/schema-services.js +17 -4
- package/dist/components/general/schema-website.js +100 -7
- package/dist/components/general/splitscroll.css +125 -0
- package/dist/components/general/splitscroll.js +89 -0
- package/dist/components/sitebuilder/config/ConfigBuilder.js +142 -25
- package/dist/components/sitebuilder/config/services-form.json +51 -0
- package/dist/components/sitebuilder/config/siteinfo-form.json +68 -0
- package/dist/index.js +2 -0
- package/dist/scripts/setup-remotes.sh +69 -0
- package/dist/types/components/admin/site-health/site-health-on-site-seo.integration.d.ts.map +1 -1
- package/dist/types/components/config/config.types.d.ts +8 -1
- package/dist/types/components/config/config.types.d.ts.map +1 -1
- package/dist/types/components/general/callout.d.ts +3 -2
- package/dist/types/components/general/callout.d.ts.map +1 -1
- package/dist/types/components/general/intersection-observer.d.ts +73 -0
- package/dist/types/components/general/intersection-observer.d.ts.map +1 -0
- package/dist/types/components/general/microinteractions.d.ts +1 -1
- package/dist/types/components/general/microinteractions.d.ts.map +1 -1
- package/dist/types/components/general/schema-services.d.ts +25 -6
- package/dist/types/components/general/schema-services.d.ts.map +1 -1
- package/dist/types/components/general/schema-website.d.ts +60 -5
- package/dist/types/components/general/schema-website.d.ts.map +1 -1
- package/dist/types/components/general/splitscroll.d.ts +51 -0
- package/dist/types/components/general/splitscroll.d.ts.map +1 -0
- package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts +30 -0
- package/dist/types/components/sitebuilder/config/ConfigBuilder.d.ts.map +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/types/stories/callout/callout.stories.d.ts +7 -0
- package/dist/types/stories/callout/callout.stories.d.ts.map +1 -1
- package/dist/types/stories/general/splitscroll.stories.d.ts +19 -0
- package/dist/types/stories/general/splitscroll.stories.d.ts.map +1 -0
- package/dist/types/stories/lookbook/lookbook.stories.d.ts +19 -0
- package/dist/types/stories/lookbook/lookbook.stories.d.ts.map +1 -0
- package/dist/types/tests/splitscroll.test.d.ts +2 -0
- package/dist/types/tests/splitscroll.test.d.ts.map +1 -0
- package/package.json +5 -5
|
@@ -841,6 +841,17 @@ async function analyzeSinglePage(url) {
|
|
|
841
841
|
console.warn(`Failed to load page: ${url} - no response received`);
|
|
842
842
|
// Continue with analysis using available data, but mark header-dependent metrics as unavailable
|
|
843
843
|
}
|
|
844
|
+
// Check Content-Type header - only analyze HTML pages
|
|
845
|
+
const contentType = response?.headers()['content-type'] || '';
|
|
846
|
+
if (!contentType.startsWith('text/html')) {
|
|
847
|
+
return {
|
|
848
|
+
url,
|
|
849
|
+
title: '',
|
|
850
|
+
statusCode: response ? response.status() : 200,
|
|
851
|
+
audits: [],
|
|
852
|
+
crawledAt: new Date().toISOString()
|
|
853
|
+
};
|
|
854
|
+
}
|
|
844
855
|
// Wait for H1 elements to be rendered (if any) with a short timeout
|
|
845
856
|
try {
|
|
846
857
|
await page.waitForSelector('h1', { timeout: 1000 }); // Reduced from 2000 to 1000
|
|
@@ -1186,6 +1197,10 @@ export async function performOnSiteSEOAnalysis(baseUrl) {
|
|
|
1186
1197
|
for (const pageUrl of pagesToAnalyze) {
|
|
1187
1198
|
try {
|
|
1188
1199
|
const pageAnalysis = await analyzeSinglePage(pageUrl);
|
|
1200
|
+
// Skip pages that aren't HTML (no audits)
|
|
1201
|
+
if (pageAnalysis.audits.length === 0) {
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1189
1204
|
// Extract and remove schema audits from page-level audits to avoid duplication
|
|
1190
1205
|
const schemaIds = Object.keys(schemaResults);
|
|
1191
1206
|
pageAnalysis.audits = pageAnalysis.audits.filter(audit => {
|
|
@@ -38,7 +38,8 @@ Callout.propTypes = {
|
|
|
38
38
|
imgClick: PropTypes.func,
|
|
39
39
|
title: PropTypes.string,
|
|
40
40
|
subtitle: PropTypes.string,
|
|
41
|
-
content: PropTypes.string,
|
|
41
|
+
content: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
|
|
42
|
+
children: PropTypes.node,
|
|
42
43
|
buttonText: PropTypes.string,
|
|
43
44
|
// SmartImage props
|
|
44
45
|
aboveFold: PropTypes.bool,
|
|
@@ -46,13 +47,13 @@ Callout.propTypes = {
|
|
|
46
47
|
cloudinaryDomain: PropTypes.string,
|
|
47
48
|
cloudinaryTransforms: PropTypes.string, */
|
|
48
49
|
};
|
|
49
|
-
export function Callout({ variant = 'default', boxShape = "squircle", layout = "horizontal", direction = 'left', gridColumns = { left: 1, right: 2 }, url, img, imgAlt, imgShape = 'square', imgClick, title, subtitle, content, buttonText, aboveFold,
|
|
50
|
+
export function Callout({ variant = 'default', boxShape = "squircle", layout = "horizontal", direction = 'left', gridColumns = { left: 1, right: 2 }, url, img, imgAlt, imgShape = 'square', imgClick, title, subtitle, content, children, buttonText, aboveFold,
|
|
50
51
|
/* cloudinaryEnv,
|
|
51
52
|
cloudinaryDomain,
|
|
52
53
|
cloudinaryTransforms */ }) {
|
|
53
54
|
const target = url && url.substring(0, 4).toLowerCase() === 'http' ? '_blank' : '_self';
|
|
54
55
|
const friendlyTitle = title ? title.toLowerCase().replace(/\s+/g, '-') : undefined;
|
|
55
|
-
const body = _jsxs("div", { className: "callout-body", children: [(title) ? _jsx(CalloutHeader, { title: title, url: url, target: target }) : null, (subtitle) ? _jsx("div", { className: "callout-subtitle", children: _jsx("h3", { children: subtitle }) }) : null, content ? _jsx("div", { className: "callout-content", children: _jsx(_Fragment, { children: content }) }) : null, url && buttonText
|
|
56
|
+
const body = _jsxs("div", { className: "callout-body", children: [(title) ? _jsx(CalloutHeader, { title: title, url: url, target: target }) : null, (subtitle) ? _jsx("div", { className: "callout-subtitle", children: _jsx("h3", { children: subtitle }) }) : null, children ? _jsx("div", { className: "callout-content", children: children }) : content ? _jsx("div", { className: "callout-content", children: _jsx(_Fragment, { children: content }) }) : null, url && buttonText
|
|
56
57
|
? _jsx(CalloutButton, { title: buttonText, url: url, target: target })
|
|
57
58
|
: url && title
|
|
58
59
|
? _jsx(CalloutButton, { title: title || "", url: url, target: target })
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* Custom hook for IntersectionObserver
|
|
4
|
+
*
|
|
5
|
+
* @param callback - Function to call when intersection changes
|
|
6
|
+
* @param options - IntersectionObserver options
|
|
7
|
+
* @returns Ref to attach to the element to observe
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const elementRef = useIntersectionObserver((entry) => {
|
|
12
|
+
* if (entry.isIntersecting) {
|
|
13
|
+
* console.log('Element is visible!');
|
|
14
|
+
* }
|
|
15
|
+
* }, { threshold: 0.5 });
|
|
16
|
+
*
|
|
17
|
+
* return <div ref={elementRef}>Observed content</div>
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function useIntersectionObserver(callback, options = {}) {
|
|
21
|
+
const elementRef = useRef(null);
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const element = elementRef.current;
|
|
24
|
+
if (!element)
|
|
25
|
+
return;
|
|
26
|
+
const { root = null, rootMargin = '0px', threshold = 0, disconnectAfterIntersection = false } = options;
|
|
27
|
+
const observer = new IntersectionObserver((entries) => {
|
|
28
|
+
entries.forEach((entry) => {
|
|
29
|
+
callback(entry, observer);
|
|
30
|
+
if (disconnectAfterIntersection && entry.isIntersecting) {
|
|
31
|
+
observer.unobserve(entry.target);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}, {
|
|
35
|
+
root,
|
|
36
|
+
rootMargin,
|
|
37
|
+
threshold
|
|
38
|
+
});
|
|
39
|
+
observer.observe(element);
|
|
40
|
+
return () => {
|
|
41
|
+
observer.disconnect();
|
|
42
|
+
};
|
|
43
|
+
}, [callback, options.root, options.rootMargin, options.threshold, options.disconnectAfterIntersection]);
|
|
44
|
+
return elementRef;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Utility function to observe multiple elements with the same configuration
|
|
48
|
+
* Useful for observing a list of elements or when you need more control than the hook provides
|
|
49
|
+
*
|
|
50
|
+
* @param selector - CSS selector for elements to observe
|
|
51
|
+
* @param callback - Function to call when intersection changes
|
|
52
|
+
* @param options - IntersectionObserver options
|
|
53
|
+
* @returns Cleanup function to disconnect the observer
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* ```tsx
|
|
57
|
+
* useEffect(() => {
|
|
58
|
+
* const cleanup = observeIntersection('.fade-in', (entry) => {
|
|
59
|
+
* if (entry.isIntersecting) {
|
|
60
|
+
* entry.target.classList.add('visible');
|
|
61
|
+
* }
|
|
62
|
+
* }, { threshold: 0.1 });
|
|
63
|
+
*
|
|
64
|
+
* return cleanup;
|
|
65
|
+
* }, []);
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export function observeIntersection(selector, callback, options = {}) {
|
|
69
|
+
const { root = null, rootMargin = '0px', threshold = 0, disconnectAfterIntersection = false } = options;
|
|
70
|
+
const elements = document.querySelectorAll(selector);
|
|
71
|
+
if (elements.length === 0) {
|
|
72
|
+
return () => { }; // Return no-op cleanup if no elements found
|
|
73
|
+
}
|
|
74
|
+
const observer = new IntersectionObserver((entries) => {
|
|
75
|
+
entries.forEach((entry) => {
|
|
76
|
+
callback(entry, observer);
|
|
77
|
+
if (disconnectAfterIntersection && entry.isIntersecting) {
|
|
78
|
+
observer.unobserve(entry.target);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}, {
|
|
82
|
+
root,
|
|
83
|
+
rootMargin,
|
|
84
|
+
threshold
|
|
85
|
+
});
|
|
86
|
+
elements.forEach((element) => {
|
|
87
|
+
observer.observe(element);
|
|
88
|
+
});
|
|
89
|
+
// Return cleanup function
|
|
90
|
+
return () => {
|
|
91
|
+
observer.disconnect();
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Utility functions for viewport detection
|
|
96
|
+
* These are useful for initial checks before setting up observers
|
|
97
|
+
*/
|
|
98
|
+
/**
|
|
99
|
+
* Check if an element is fully in the viewport
|
|
100
|
+
*/
|
|
101
|
+
export function isElementInViewport(element) {
|
|
102
|
+
const rect = element.getBoundingClientRect();
|
|
103
|
+
return (rect.top >= 0 &&
|
|
104
|
+
rect.left >= 0 &&
|
|
105
|
+
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
106
|
+
rect.right <= (window.innerWidth || document.documentElement.clientWidth));
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Check if an element is partially in the viewport
|
|
110
|
+
*/
|
|
111
|
+
export function isElementPartiallyInViewport(element) {
|
|
112
|
+
const rect = element.getBoundingClientRect();
|
|
113
|
+
return (rect.top < (window.innerHeight || document.documentElement.clientHeight) &&
|
|
114
|
+
rect.left < (window.innerWidth || document.documentElement.clientWidth) &&
|
|
115
|
+
rect.bottom > 0 &&
|
|
116
|
+
rect.right > 0);
|
|
117
|
+
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
// import React, { useState, useEffect } from 'react';
|
|
2
1
|
import PropTypes from "prop-types";
|
|
2
|
+
import { observeIntersection, isElementPartiallyInViewport } from './intersection-observer';
|
|
3
3
|
import './microinteractions.css';
|
|
4
4
|
/* ========== MICRO ANIMATIONS ========== */
|
|
5
|
+
/**
|
|
6
|
+
* MicroInteractions handles global site animations and interactions.
|
|
7
|
+
* It is typically called once in a top-level component or effect.
|
|
8
|
+
*
|
|
9
|
+
* @param props - Configuration props for enabling/disabling interactions
|
|
10
|
+
* @returns A cleanup function if scrollfadeElements is used
|
|
11
|
+
*/
|
|
5
12
|
MicroInteractions.propTypes = {
|
|
6
13
|
buttonring: PropTypes.bool,
|
|
7
14
|
cartpulse: PropTypes.bool,
|
|
@@ -14,10 +21,9 @@ MicroInteractions.propTypes = {
|
|
|
14
21
|
scrollfadeElements: PropTypes.string,
|
|
15
22
|
};
|
|
16
23
|
export function MicroInteractions(props) {
|
|
17
|
-
// const debug = true ;
|
|
18
24
|
const body = document.body;
|
|
19
25
|
for (const propName in props) {
|
|
20
|
-
if (Object.prototype.hasOwnProperty.call(props, propName)) {
|
|
26
|
+
if (Object.prototype.hasOwnProperty.call(props, propName) && propName !== 'scrollfadeElements') {
|
|
21
27
|
if (props[propName] === true) {
|
|
22
28
|
body.classList.add(propName);
|
|
23
29
|
}
|
|
@@ -26,61 +32,44 @@ export function MicroInteractions(props) {
|
|
|
26
32
|
}
|
|
27
33
|
}
|
|
28
34
|
}
|
|
29
|
-
if (props.scrollfadeElements)
|
|
30
|
-
ScrollFade(props.scrollfadeElements);
|
|
31
|
-
}
|
|
32
|
-
function isElementInViewport(el) {
|
|
33
|
-
const rect = el.getBoundingClientRect();
|
|
34
|
-
return (rect.top >= 0 &&
|
|
35
|
-
rect.left >= 0 &&
|
|
36
|
-
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
|
|
37
|
-
rect.right <= (window.innerWidth || document.documentElement.clientWidth));
|
|
38
|
-
}
|
|
39
|
-
function isElementPartiallyInViewport(el) {
|
|
40
|
-
const rect = el.getBoundingClientRect();
|
|
41
|
-
return (rect.top < (window.innerHeight || document.documentElement.clientHeight) &&
|
|
42
|
-
rect.left < (window.innerWidth || document.documentElement.clientWidth) &&
|
|
43
|
-
rect.bottom > 0 &&
|
|
44
|
-
rect.right > 0);
|
|
35
|
+
if (props.scrollfadeElements) {
|
|
36
|
+
return ScrollFade(props.scrollfadeElements);
|
|
37
|
+
}
|
|
45
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Applies a fade-in animation to elements as they enter the viewport
|
|
41
|
+
* @param elements - CSS selector for elements to animate
|
|
42
|
+
* @returns Cleanup function for the intersection observer
|
|
43
|
+
*/
|
|
46
44
|
function ScrollFade(elements) {
|
|
47
|
-
const options = {
|
|
48
|
-
root: null, // Observes intersection with the viewport
|
|
49
|
-
rootMargin: "0px 0px -100px 0px", // Shrinks the top of the root by 200px
|
|
50
|
-
threshold: 0 // Triggers when any part of the element intersects the adjusted root
|
|
51
|
-
};
|
|
52
|
-
const observer = new IntersectionObserver((entries) => {
|
|
53
|
-
entries.forEach((entry) => {
|
|
54
|
-
if (entry.isIntersecting) {
|
|
55
|
-
// if (entry.intersectionRatio > 0.5) {
|
|
56
|
-
// Add the animation class when the element enters the viewport
|
|
57
|
-
entry.target.classList.add('scrollfade');
|
|
58
|
-
entry.target.classList.remove('hidden');
|
|
59
|
-
// Optionally, stop observing once animated if you only want it to animate once
|
|
60
|
-
observer.unobserve(entry.target);
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
// Optionally, remove the animation class if you want it to re-animate on re-entry
|
|
64
|
-
// entry.target.classList.remove('hidden');
|
|
65
|
-
// entry.target.classList.remove('scrollfade');
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
}, options);
|
|
69
|
-
// Select the elements you want to observe and initially hide them
|
|
70
45
|
const elementsToAnimate = document.querySelectorAll(elements);
|
|
46
|
+
// Initial state setup
|
|
71
47
|
elementsToAnimate.forEach((element) => {
|
|
72
48
|
if (isElementPartiallyInViewport(element)) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (element.classList.contains('scrollfade')) {
|
|
77
|
-
element.classList.remove('scrollfade');
|
|
78
|
-
}
|
|
49
|
+
// If already in viewport, make sure it's visible without animation
|
|
50
|
+
element.classList.remove('hidden');
|
|
51
|
+
element.classList.remove('scrollfade');
|
|
79
52
|
}
|
|
80
53
|
else {
|
|
81
54
|
// Apply initial hidden state to elements NOT on the screen
|
|
82
|
-
element.classList.add('hidden');
|
|
83
|
-
|
|
55
|
+
element.classList.add('hidden');
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
// Setup observer for elements not yet visible
|
|
59
|
+
const cleanup = observeIntersection(elements, (entry, observer) => {
|
|
60
|
+
if (entry.isIntersecting) {
|
|
61
|
+
const element = entry.target;
|
|
62
|
+
// Only animate if it was hidden
|
|
63
|
+
if (element.classList.contains('hidden')) {
|
|
64
|
+
element.classList.add('scrollfade');
|
|
65
|
+
element.classList.remove('hidden');
|
|
66
|
+
// Stop observing after animation triggers
|
|
67
|
+
observer.unobserve(element);
|
|
68
|
+
}
|
|
84
69
|
}
|
|
70
|
+
}, {
|
|
71
|
+
rootMargin: "0px 0px -100px 0px",
|
|
72
|
+
threshold: 0
|
|
85
73
|
});
|
|
74
|
+
return cleanup;
|
|
86
75
|
}
|
|
@@ -1,23 +1,35 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import PropTypes from 'prop-types';
|
|
3
|
-
|
|
3
|
+
const servicesSchemaPropTypes = {
|
|
4
|
+
siteInfo: PropTypes.object,
|
|
4
5
|
provider: PropTypes.shape({
|
|
5
6
|
name: PropTypes.string.isRequired,
|
|
6
7
|
url: PropTypes.string.isRequired,
|
|
7
8
|
logo: PropTypes.string,
|
|
8
9
|
telephone: PropTypes.string,
|
|
9
10
|
email: PropTypes.string,
|
|
10
|
-
})
|
|
11
|
+
}),
|
|
11
12
|
services: PropTypes.arrayOf(PropTypes.shape({
|
|
12
13
|
name: PropTypes.string.isRequired,
|
|
13
14
|
description: PropTypes.string.isRequired,
|
|
14
15
|
url: PropTypes.string,
|
|
15
16
|
image: PropTypes.string,
|
|
16
17
|
areaServed: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.string)]),
|
|
17
|
-
}))
|
|
18
|
+
})),
|
|
18
19
|
};
|
|
19
20
|
export function ServicesSchema(props) {
|
|
20
|
-
const
|
|
21
|
+
const siteInfo = props.siteInfo;
|
|
22
|
+
const services = (siteInfo?.services || props.services || []);
|
|
23
|
+
const provider = props.provider || {
|
|
24
|
+
name: siteInfo?.name || '',
|
|
25
|
+
url: siteInfo?.url || '',
|
|
26
|
+
logo: siteInfo?.image,
|
|
27
|
+
telephone: siteInfo?.telephone,
|
|
28
|
+
email: siteInfo?.email
|
|
29
|
+
};
|
|
30
|
+
if (!services.length || !provider.name) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
21
33
|
const serviceObjects = services.map((service) => ({
|
|
22
34
|
'@type': 'Service',
|
|
23
35
|
name: service.name,
|
|
@@ -39,4 +51,5 @@ export function ServicesSchema(props) {
|
|
|
39
51
|
...service
|
|
40
52
|
}) } }, idx))) }));
|
|
41
53
|
}
|
|
54
|
+
ServicesSchema.propTypes = servicesSchemaPropTypes;
|
|
42
55
|
export default ServicesSchema;
|
|
@@ -12,31 +12,124 @@ WebsiteSchema.propTypes = {
|
|
|
12
12
|
name: PropTypes.string,
|
|
13
13
|
url: PropTypes.string,
|
|
14
14
|
description: PropTypes.string,
|
|
15
|
+
keywords: PropTypes.string,
|
|
16
|
+
inLanguage: PropTypes.string,
|
|
17
|
+
sameAs: PropTypes.arrayOf(PropTypes.string),
|
|
15
18
|
potentialAction: PropTypes.shape({
|
|
16
19
|
'@type': PropTypes.string,
|
|
17
20
|
target: PropTypes.shape({
|
|
18
21
|
'@type': PropTypes.string,
|
|
19
22
|
urlTemplate: PropTypes.string
|
|
20
|
-
}),
|
|
21
|
-
query: PropTypes.string
|
|
23
|
+
}).isRequired,
|
|
24
|
+
'query-input': PropTypes.string
|
|
22
25
|
}),
|
|
23
|
-
|
|
26
|
+
publisher: PropTypes.shape({
|
|
27
|
+
'@type': PropTypes.string,
|
|
28
|
+
name: PropTypes.string.isRequired,
|
|
29
|
+
url: PropTypes.string,
|
|
30
|
+
logo: PropTypes.shape({
|
|
31
|
+
'@type': PropTypes.string,
|
|
32
|
+
url: PropTypes.string.isRequired,
|
|
33
|
+
width: PropTypes.number,
|
|
34
|
+
height: PropTypes.number
|
|
35
|
+
})
|
|
36
|
+
}),
|
|
37
|
+
copyrightYear: PropTypes.number,
|
|
38
|
+
copyrightHolder: PropTypes.shape({
|
|
39
|
+
'@type': PropTypes.string,
|
|
40
|
+
name: PropTypes.string.isRequired,
|
|
41
|
+
url: PropTypes.string
|
|
42
|
+
}),
|
|
43
|
+
siteInfo: PropTypes.object
|
|
24
44
|
};
|
|
25
45
|
export function WebsiteSchema(props) {
|
|
26
|
-
// const config = usePixelatedConfig();
|
|
27
46
|
const siteInfo = props.siteInfo;
|
|
28
|
-
// Use props if provided, otherwise fall back to siteInfo
|
|
29
47
|
const name = props.name || siteInfo?.name;
|
|
30
48
|
const url = props.url || siteInfo?.url;
|
|
49
|
+
if (!name || !url) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
31
52
|
const description = props.description || siteInfo?.description;
|
|
32
|
-
const
|
|
53
|
+
const keywords = props.keywords || siteInfo?.keywords;
|
|
54
|
+
const inLanguage = props.inLanguage || siteInfo?.default_locale;
|
|
55
|
+
const sameAs = props.sameAs || siteInfo?.sameAs;
|
|
56
|
+
const publisher = props.publisher || buildPublisher(siteInfo);
|
|
57
|
+
const potentialAction = props.potentialAction || buildPotentialAction(siteInfo?.potentialAction);
|
|
58
|
+
const copyrightYear = props.copyrightYear ?? siteInfo?.copyrightYear;
|
|
59
|
+
const copyrightHolder = props.copyrightHolder || buildCopyrightHolder(siteInfo);
|
|
33
60
|
const schemaData = {
|
|
34
61
|
'@context': 'https://schema.org',
|
|
35
62
|
'@type': 'WebSite',
|
|
36
63
|
name,
|
|
37
64
|
url,
|
|
38
65
|
...(description && { description }),
|
|
39
|
-
...(
|
|
66
|
+
...(keywords && { keywords }),
|
|
67
|
+
...(inLanguage && { inLanguage }),
|
|
68
|
+
...(sameAs && sameAs.length ? { sameAs } : {}),
|
|
69
|
+
...(publisher && { publisher }),
|
|
70
|
+
...(potentialAction && { potentialAction }),
|
|
71
|
+
...(copyrightYear != null && { copyrightYear }),
|
|
72
|
+
...(copyrightHolder && { copyrightHolder })
|
|
40
73
|
};
|
|
41
74
|
return (_jsx("script", { type: "application/ld+json", dangerouslySetInnerHTML: { __html: JSON.stringify(schemaData) } }));
|
|
42
75
|
}
|
|
76
|
+
function buildPublisher(siteInfo) {
|
|
77
|
+
if (!siteInfo) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
if (!siteInfo.name) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const logoUrl = siteInfo.image;
|
|
84
|
+
const logoWidth = parseDimension(siteInfo.image_width);
|
|
85
|
+
const logoHeight = parseDimension(siteInfo.image_height);
|
|
86
|
+
const logo = logoUrl
|
|
87
|
+
? {
|
|
88
|
+
'@type': 'ImageObject',
|
|
89
|
+
url: logoUrl,
|
|
90
|
+
...(logoWidth !== undefined && { width: logoWidth }),
|
|
91
|
+
...(logoHeight !== undefined && { height: logoHeight })
|
|
92
|
+
}
|
|
93
|
+
: undefined;
|
|
94
|
+
return {
|
|
95
|
+
'@type': siteInfo.publisherType || 'Organization',
|
|
96
|
+
name: siteInfo.name,
|
|
97
|
+
...(siteInfo.url && { url: siteInfo.url }),
|
|
98
|
+
...(logo && { logo })
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function buildCopyrightHolder(siteInfo) {
|
|
102
|
+
if (!siteInfo?.name) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const holderType = siteInfo.publisherType || 'Organization';
|
|
106
|
+
return {
|
|
107
|
+
'@type': holderType,
|
|
108
|
+
name: siteInfo.name,
|
|
109
|
+
...(siteInfo.url && { url: siteInfo.url })
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function buildPotentialAction(action) {
|
|
113
|
+
if (!action || !action.target) {
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
const queryInput = action['query-input'] ?? action.queryInput;
|
|
117
|
+
return {
|
|
118
|
+
'@type': action['@type'] ?? 'SearchAction',
|
|
119
|
+
target: {
|
|
120
|
+
'@type': 'EntryPoint',
|
|
121
|
+
urlTemplate: action.target
|
|
122
|
+
},
|
|
123
|
+
...(queryInput && { 'query-input': queryInput })
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function parseDimension(value) {
|
|
127
|
+
if (typeof value === 'number') {
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
if (!value) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
const parsed = Number(value);
|
|
134
|
+
return Number.isNaN(parsed) ? undefined : parsed;
|
|
135
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SplitScroll Styles
|
|
3
|
+
*
|
|
4
|
+
* Provides the sticky image behavior and layered parallax effect
|
|
5
|
+
* for the SplitScroll component
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
.splitscroll-container {
|
|
9
|
+
position: relative;
|
|
10
|
+
width: 100%;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.splitscroll-section {
|
|
14
|
+
position: relative;
|
|
15
|
+
min-height: 100vh;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Split variant callout adjustments for splitscroll */
|
|
19
|
+
.splitscroll-section .callout.split {
|
|
20
|
+
min-height: 100vh;
|
|
21
|
+
display: grid;
|
|
22
|
+
grid-template-columns: 1fr 1fr;
|
|
23
|
+
gap: 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* Left side - sticky image */
|
|
27
|
+
.splitscroll-section .callout.split .callout-image {
|
|
28
|
+
position: sticky;
|
|
29
|
+
top: 0;
|
|
30
|
+
height: 100vh;
|
|
31
|
+
overflow: hidden;
|
|
32
|
+
z-index: 1;
|
|
33
|
+
transition: opacity 0.3s ease;
|
|
34
|
+
width: 50vw;
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/* Layer images on top of each other */
|
|
39
|
+
.splitscroll-section .callout-image {
|
|
40
|
+
z-index: calc(var(--section-index, 0) + 1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Active section has highest z-index */
|
|
44
|
+
.splitscroll-section.active .callout-image {
|
|
45
|
+
z-index: 100;
|
|
46
|
+
opacity: 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* Non-active sections fade slightly */
|
|
50
|
+
.splitscroll-section:not(.active) .callout-image {
|
|
51
|
+
opacity: 0.95;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/* Right side - scrolling content */
|
|
55
|
+
.splitscroll-section .callout.split .callout-body {
|
|
56
|
+
position: relative;
|
|
57
|
+
padding: 4rem 2rem;
|
|
58
|
+
min-height: 100vh;
|
|
59
|
+
display: flex;
|
|
60
|
+
flex-direction: column;
|
|
61
|
+
justify-content: center;
|
|
62
|
+
z-index: 2;
|
|
63
|
+
width: 50vw;
|
|
64
|
+
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/* Image fills container and is centered */
|
|
68
|
+
.splitscroll-section .callout-image img {
|
|
69
|
+
width: 100%;
|
|
70
|
+
height: 100%;
|
|
71
|
+
object-fit: cover;
|
|
72
|
+
object-position: center;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Smooth transitions for section changes */
|
|
76
|
+
.splitscroll-section {
|
|
77
|
+
transition: transform 0.3s ease;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Responsive behavior */
|
|
81
|
+
@media (max-width: 768px) {
|
|
82
|
+
.splitscroll-section .callout.split {
|
|
83
|
+
grid-template-columns: 1fr;
|
|
84
|
+
grid-template-rows: 50vh auto;
|
|
85
|
+
min-height: auto;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.splitscroll-section .callout.split .callout-image {
|
|
89
|
+
position: relative;
|
|
90
|
+
height: 50vh;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.splitscroll-section .callout.split .callout-body {
|
|
94
|
+
min-height: 50vh;
|
|
95
|
+
padding: 2rem 1rem;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* Optional: Add subtle parallax effect to content */
|
|
100
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
101
|
+
.splitscroll-section.active .callout-body {
|
|
102
|
+
animation: fadeInUp 0.6s ease forwards;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@keyframes fadeInUp {
|
|
106
|
+
from {
|
|
107
|
+
opacity: 0.8;
|
|
108
|
+
transform: translateY(20px);
|
|
109
|
+
}
|
|
110
|
+
to {
|
|
111
|
+
opacity: 1;
|
|
112
|
+
transform: translateY(0);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* Respect reduced motion preferences */
|
|
118
|
+
@media (prefers-reduced-motion: reduce) {
|
|
119
|
+
.splitscroll-section,
|
|
120
|
+
.splitscroll-section .callout-image,
|
|
121
|
+
.splitscroll-section .callout-body {
|
|
122
|
+
transition: none;
|
|
123
|
+
animation: none;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import React, { Children, useState, useEffect } from 'react';
|
|
4
|
+
import PropTypes from 'prop-types';
|
|
5
|
+
import { Callout } from './callout';
|
|
6
|
+
import { observeIntersection } from './intersection-observer';
|
|
7
|
+
import './splitscroll.css';
|
|
8
|
+
/**
|
|
9
|
+
* SplitScroll - A scrolling split-page layout with sticky images
|
|
10
|
+
*
|
|
11
|
+
* Creates a splitscroll-style layout where the left side shows sticky images
|
|
12
|
+
* that layer over each other as you scroll, while the right side contains
|
|
13
|
+
* scrolling content sections.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```tsx
|
|
17
|
+
* <SplitScroll>
|
|
18
|
+
* <SplitScroll.Section img="/image1.jpg" title="Section 1">
|
|
19
|
+
* <YourContent />
|
|
20
|
+
* </SplitScroll.Section>
|
|
21
|
+
* <SplitScroll.Section img="/image2.jpg" title="Section 2">
|
|
22
|
+
* <MoreContent />
|
|
23
|
+
* </SplitScroll.Section>
|
|
24
|
+
* </SplitScroll>
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
SplitScroll.propTypes = {
|
|
28
|
+
children: PropTypes.node.isRequired,
|
|
29
|
+
};
|
|
30
|
+
export function SplitScroll({ children }) {
|
|
31
|
+
const [activeSectionIndex, setActiveSectionIndex] = useState(0);
|
|
32
|
+
const childArray = Children.toArray(children);
|
|
33
|
+
const sectionCount = childArray.length;
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
// Set up intersection observers for each section
|
|
36
|
+
const cleanup = observeIntersection('.splitscroll-section', (entry) => {
|
|
37
|
+
if (entry.isIntersecting) {
|
|
38
|
+
const index = parseInt(entry.target.getAttribute('data-section-index') || '0', 10);
|
|
39
|
+
setActiveSectionIndex(index);
|
|
40
|
+
}
|
|
41
|
+
}, {
|
|
42
|
+
rootMargin: '-20% 0px -60% 0px', // Trigger when section is 20% from top
|
|
43
|
+
threshold: 0
|
|
44
|
+
});
|
|
45
|
+
return cleanup;
|
|
46
|
+
}, [sectionCount]);
|
|
47
|
+
// Clone children and add props for active state and index
|
|
48
|
+
const enhancedChildren = Children.map(children, (child, index) => {
|
|
49
|
+
if (React.isValidElement(child)) {
|
|
50
|
+
const additionalProps = {
|
|
51
|
+
isActive: index === activeSectionIndex,
|
|
52
|
+
sectionIndex: index,
|
|
53
|
+
totalSections: sectionCount
|
|
54
|
+
};
|
|
55
|
+
return React.cloneElement(child, additionalProps);
|
|
56
|
+
}
|
|
57
|
+
return child;
|
|
58
|
+
});
|
|
59
|
+
return (_jsx("div", { className: "splitscroll-container", children: enhancedChildren }));
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* SplitScroll.Section - Individual section within a SplitScroll
|
|
63
|
+
*
|
|
64
|
+
* A facade for the Callout component with variant="split" preset.
|
|
65
|
+
* Automatically configured for the splitscroll layout.
|
|
66
|
+
*/
|
|
67
|
+
const splitscrollSectionPropTypes = {
|
|
68
|
+
img: PropTypes.string.isRequired,
|
|
69
|
+
imgAlt: PropTypes.string,
|
|
70
|
+
imgShape: PropTypes.oneOf(['square', 'bevel', 'squircle', 'round']),
|
|
71
|
+
title: PropTypes.string,
|
|
72
|
+
subtitle: PropTypes.string,
|
|
73
|
+
url: PropTypes.string,
|
|
74
|
+
buttonText: PropTypes.string,
|
|
75
|
+
children: PropTypes.node,
|
|
76
|
+
aboveFold: PropTypes.bool,
|
|
77
|
+
// Internal props added by SplitScroll parent
|
|
78
|
+
isActive: PropTypes.bool,
|
|
79
|
+
sectionIndex: PropTypes.number,
|
|
80
|
+
totalSections: PropTypes.number,
|
|
81
|
+
};
|
|
82
|
+
const SplitScrollSectionComponent = function SplitScrollSection({ img, imgAlt, imgShape = 'square', title, subtitle, url, buttonText, children, aboveFold, isActive, sectionIndex, totalSections, }) {
|
|
83
|
+
return (_jsx("div", { className: `splitscroll-section ${isActive ? 'active' : ''}`, "data-section-index": sectionIndex, style: {
|
|
84
|
+
'--section-index': sectionIndex,
|
|
85
|
+
'--total-sections': totalSections
|
|
86
|
+
}, children: _jsx(Callout, { variant: "split", img: img, imgAlt: imgAlt, imgShape: imgShape, title: title, subtitle: subtitle, url: url, buttonText: buttonText, aboveFold: aboveFold ?? (sectionIndex === 0), children: children }) }));
|
|
87
|
+
};
|
|
88
|
+
SplitScrollSectionComponent.propTypes = splitscrollSectionPropTypes;
|
|
89
|
+
SplitScroll.Section = SplitScrollSectionComponent;
|