@dreamtree-org/twreact-ui 2.1.1 → 2.2.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 +48 -4
- package/ai-skills/dreamtree-ui.md +20 -3
- package/dist/_shared/{cn-DPPQUJu1.js → cn-TMJ-_w1a.js} +14 -1
- package/dist/_shared/useLatestRef-CdtlnPUz.js +21 -0
- package/dist/_shared/useReducedMotion-B_PMM5ER.js +77 -0
- package/dist/ai-schema.json +513 -0
- package/dist/charts/index.js +6 -76
- package/dist/components/core/Table/Table.d.ts.map +1 -1
- package/dist/components/motion/BentoExpandGrid.d.ts +10 -0
- package/dist/components/motion/BentoExpandGrid.d.ts.map +1 -0
- package/dist/components/motion/CountUp.d.ts +9 -0
- package/dist/components/motion/CountUp.d.ts.map +1 -0
- package/dist/components/motion/DeviceFrame.d.ts +8 -0
- package/dist/components/motion/DeviceFrame.d.ts.map +1 -0
- package/dist/components/motion/DeviceMorphHero.d.ts +11 -0
- package/dist/components/motion/DeviceMorphHero.d.ts.map +1 -0
- package/dist/components/motion/LogoMarquee.d.ts +9 -0
- package/dist/components/motion/LogoMarquee.d.ts.map +1 -0
- package/dist/components/motion/PhotoCollage.d.ts +9 -0
- package/dist/components/motion/PhotoCollage.d.ts.map +1 -0
- package/dist/components/motion/Reveal.d.ts +9 -0
- package/dist/components/motion/Reveal.d.ts.map +1 -0
- package/dist/components/motion/RevealGroup.d.ts +5 -0
- package/dist/components/motion/RevealGroup.d.ts.map +1 -0
- package/dist/components/motion/RotatingBadge.d.ts +9 -0
- package/dist/components/motion/RotatingBadge.d.ts.map +1 -0
- package/dist/components/motion/ScrollStage.d.ts +11 -0
- package/dist/components/motion/ScrollStage.d.ts.map +1 -0
- package/dist/components/motion/SmoothScrollProvider.d.ts +18 -0
- package/dist/components/motion/SmoothScrollProvider.d.ts.map +1 -0
- package/dist/components/motion/StackedCards.d.ts +10 -0
- package/dist/components/motion/StackedCards.d.ts.map +1 -0
- package/dist/components/motion/StatStrip.d.ts +8 -0
- package/dist/components/motion/StatStrip.d.ts.map +1 -0
- package/dist/components/motion/TestimonialSpotlight.d.ts +10 -0
- package/dist/components/motion/TestimonialSpotlight.d.ts.map +1 -0
- package/dist/components/motion/storyData.d.ts +52 -0
- package/dist/components/motion/storyData.d.ts.map +1 -0
- package/dist/components/navigation/Navbar.d.ts.map +1 -1
- package/dist/esm/components/core/Table/MobileCard.js +2 -2
- package/dist/esm/components/core/Table/MobileFilters.js +1 -1
- package/dist/esm/components/core/Table/Table.js +28 -26
- package/dist/esm/components/motion/BentoExpandGrid.js +144 -0
- package/dist/esm/components/motion/CountUp.js +97 -0
- package/dist/esm/components/motion/DeviceFrame.js +102 -0
- package/dist/esm/components/motion/DeviceMorphHero.js +228 -0
- package/dist/esm/components/motion/LogoMarquee.js +144 -0
- package/dist/esm/components/motion/PhotoCollage.js +92 -0
- package/dist/esm/components/motion/Reveal.js +115 -0
- package/dist/esm/components/motion/RevealGroup.js +43 -0
- package/dist/esm/components/motion/RotatingBadge.js +66 -0
- package/dist/esm/components/motion/ScrollStage.js +62 -0
- package/dist/esm/components/motion/SmoothScrollProvider.js +63 -0
- package/dist/esm/components/motion/StackedCards.js +166 -0
- package/dist/esm/components/motion/StatStrip.js +58 -0
- package/dist/esm/components/motion/TestimonialSpotlight.js +209 -0
- package/dist/esm/components/navigation/Navbar.js +5 -2
- package/dist/esm/hooks/useScrollProgress.js +107 -0
- package/dist/esm/hooks/useSmoothScroll.js +33 -0
- package/dist/esm/motion/engine/inView.js +54 -0
- package/dist/esm/motion/engine/progress.js +45 -0
- package/dist/esm/motion/engine/scheduler.js +67 -0
- package/dist/esm/motion/engine/useHydrated.js +17 -0
- package/dist/esm/motion/engine/useIsomorphicLayoutEffect.js +7 -0
- package/dist/esm/motion/engine/useMergedRef.js +12 -0
- package/dist/esm/motion/index.js +16 -0
- package/dist/esm/motion/tokens.js +10 -0
- package/dist/hooks/useScrollProgress.d.ts +17 -0
- package/dist/hooks/useScrollProgress.d.ts.map +1 -0
- package/dist/hooks/useSmoothScroll.d.ts +11 -0
- package/dist/hooks/useSmoothScroll.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.esm.css +1 -1
- package/dist/index.esm.js +36 -31
- package/dist/index.js +36 -31
- package/dist/motion/engine/inView.d.ts +5 -0
- package/dist/motion/engine/inView.d.ts.map +1 -0
- package/dist/motion/engine/progress.d.ts +18 -0
- package/dist/motion/engine/progress.d.ts.map +1 -0
- package/dist/motion/engine/scheduler.d.ts +10 -0
- package/dist/motion/engine/scheduler.d.ts.map +1 -0
- package/dist/motion/engine/useHydrated.d.ts +3 -0
- package/dist/motion/engine/useHydrated.d.ts.map +1 -0
- package/dist/motion/engine/useIsomorphicLayoutEffect.d.ts +4 -0
- package/dist/motion/engine/useIsomorphicLayoutEffect.d.ts.map +1 -0
- package/dist/motion/engine/useMergedRef.d.ts +3 -0
- package/dist/motion/engine/useMergedRef.d.ts.map +1 -0
- package/dist/motion/index.d.ts +20 -0
- package/dist/motion/index.d.ts.map +1 -0
- package/dist/motion/index.js +1813 -0
- package/dist/motion/tokens.d.ts +6 -0
- package/dist/motion/tokens.d.ts.map +1 -0
- package/dist/rich-text/index.js +3 -18
- package/dist/tokens/tokens.json +21 -0
- package/dist/types/motion.d.ts +290 -0
- package/dist/types/motion.d.ts.map +1 -0
- package/doc/README.md +17 -1
- package/mcp/catalog.snapshot.json +959 -12
- package/motion/package.json +5 -0
- package/package.json +14 -3
- package/tokens/tokens.json +6 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { objectWithoutProperties as _objectWithoutProperties, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import Reveal from './Reveal.js';
|
|
6
|
+
|
|
7
|
+
var _excluded = ["effect", "delay", "stagger", "once", "as", "itemAs", "children"];
|
|
8
|
+
/** Reveals each child in turn: child `i` waits `delay + i * stagger` ms. */
|
|
9
|
+
var RevealGroup = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
|
|
10
|
+
var _ref$effect = _ref.effect,
|
|
11
|
+
effect = _ref$effect === void 0 ? "fade-up" : _ref$effect,
|
|
12
|
+
_ref$delay = _ref.delay,
|
|
13
|
+
delay = _ref$delay === void 0 ? 0 : _ref$delay,
|
|
14
|
+
_ref$stagger = _ref.stagger,
|
|
15
|
+
stagger = _ref$stagger === void 0 ? 80 : _ref$stagger,
|
|
16
|
+
_ref$once = _ref.once,
|
|
17
|
+
once = _ref$once === void 0 ? true : _ref$once,
|
|
18
|
+
_ref$as = _ref.as,
|
|
19
|
+
Tag = _ref$as === void 0 ? "div" : _ref$as,
|
|
20
|
+
_ref$itemAs = _ref.itemAs,
|
|
21
|
+
itemAs = _ref$itemAs === void 0 ? "div" : _ref$itemAs,
|
|
22
|
+
children = _ref.children,
|
|
23
|
+
rest = _objectWithoutProperties(_ref, _excluded);
|
|
24
|
+
var index = 0;
|
|
25
|
+
return jsx(Tag, _objectSpread2(_objectSpread2({
|
|
26
|
+
ref: ref
|
|
27
|
+
}, rest), {}, {
|
|
28
|
+
children: React.Children.map(children, function (child) {
|
|
29
|
+
if (child == null || child === false) return child;
|
|
30
|
+
var i = index++;
|
|
31
|
+
return jsx(Reveal, {
|
|
32
|
+
as: itemAs,
|
|
33
|
+
effect: effect === "split-words" ? "fade-up" : effect,
|
|
34
|
+
delay: delay + i * stagger,
|
|
35
|
+
once: once,
|
|
36
|
+
children: child
|
|
37
|
+
});
|
|
38
|
+
})
|
|
39
|
+
}));
|
|
40
|
+
});
|
|
41
|
+
RevealGroup.displayName = "RevealGroup";
|
|
42
|
+
|
|
43
|
+
export { RevealGroup as default };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { objectWithoutProperties as _objectWithoutProperties, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
|
|
3
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
|
+
import React, { useId } from 'react';
|
|
5
|
+
import { cn } from '../../utils/cn.js';
|
|
6
|
+
|
|
7
|
+
var _excluded = ["text", "speed", "size", "icon", "className", "style"];
|
|
8
|
+
var RADIUS = 38;
|
|
9
|
+
var CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
|
10
|
+
/**
|
|
11
|
+
* A circular badge with text set around its edge, slowly turning (SVG `textPath`). The
|
|
12
|
+
* text is spread to fill the circle exactly. Exposed as one image named by `text`
|
|
13
|
+
* (override with `aria-label`); it stops turning under reduced motion.
|
|
14
|
+
*/
|
|
15
|
+
var RotatingBadge = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
|
|
16
|
+
var text = _ref.text,
|
|
17
|
+
_ref$speed = _ref.speed,
|
|
18
|
+
speed = _ref$speed === void 0 ? 14 : _ref$speed,
|
|
19
|
+
_ref$size = _ref.size,
|
|
20
|
+
size = _ref$size === void 0 ? 120 : _ref$size,
|
|
21
|
+
icon = _ref.icon,
|
|
22
|
+
className = _ref.className,
|
|
23
|
+
style = _ref.style,
|
|
24
|
+
rest = _objectWithoutProperties(_ref, _excluded);
|
|
25
|
+
var pathId = "dt-badge-".concat(useId().replace(/:/g, ""));
|
|
26
|
+
return jsxs("div", _objectSpread2(_objectSpread2({
|
|
27
|
+
ref: ref,
|
|
28
|
+
role: "img",
|
|
29
|
+
"aria-label": text,
|
|
30
|
+
className: cn("relative inline-flex shrink-0 items-center justify-center rounded-full text-foreground", className),
|
|
31
|
+
style: _objectSpread2({
|
|
32
|
+
width: size,
|
|
33
|
+
height: size
|
|
34
|
+
}, style)
|
|
35
|
+
}, rest), {}, {
|
|
36
|
+
children: [jsxs("svg", {
|
|
37
|
+
viewBox: "0 0 100 100",
|
|
38
|
+
"aria-hidden": "true",
|
|
39
|
+
className: "absolute inset-0 h-full w-full animate-spin motion-reduce:animate-none",
|
|
40
|
+
style: {
|
|
41
|
+
animationDuration: "".concat(speed, "s")
|
|
42
|
+
},
|
|
43
|
+
children: [jsx("defs", {
|
|
44
|
+
children: jsx("path", {
|
|
45
|
+
id: pathId,
|
|
46
|
+
d: "M 50,50 m -".concat(RADIUS, ",0 a ").concat(RADIUS, ",").concat(RADIUS, " 0 1,1 ").concat(RADIUS * 2, ",0 a ").concat(RADIUS, ",").concat(RADIUS, " 0 1,1 -").concat(RADIUS * 2, ",0")
|
|
47
|
+
})
|
|
48
|
+
}), jsx("text", {
|
|
49
|
+
className: "fill-current text-[8.5px] font-semibold uppercase",
|
|
50
|
+
children: jsx("textPath", {
|
|
51
|
+
href: "#".concat(pathId),
|
|
52
|
+
textLength: CIRCUMFERENCE - 2,
|
|
53
|
+
lengthAdjust: "spacing",
|
|
54
|
+
children: text
|
|
55
|
+
})
|
|
56
|
+
})]
|
|
57
|
+
}), icon ? jsx("span", {
|
|
58
|
+
"aria-hidden": "true",
|
|
59
|
+
className: "relative",
|
|
60
|
+
children: icon
|
|
61
|
+
}) : null]
|
|
62
|
+
}));
|
|
63
|
+
});
|
|
64
|
+
RotatingBadge.displayName = "RotatingBadge";
|
|
65
|
+
|
|
66
|
+
export { RotatingBadge as default };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { objectWithoutProperties as _objectWithoutProperties, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
import React, { useRef } from 'react';
|
|
5
|
+
import { cn } from '../../utils/cn.js';
|
|
6
|
+
import { useReducedMotion } from '../../hooks/useReducedMotion.js';
|
|
7
|
+
import { useMediaQuery } from '../../hooks/useMediaQuery.js';
|
|
8
|
+
import { useScrollProgress } from '../../hooks/useScrollProgress.js';
|
|
9
|
+
import { useMergedRef } from '../../motion/engine/useMergedRef.js';
|
|
10
|
+
import { MOBILE_QUERY, SCENE_LENGTH_MOBILE, SCENE_LENGTH } from '../../motion/tokens.js';
|
|
11
|
+
|
|
12
|
+
var _excluded = ["length", "mobileLength", "pin", "children", "onProgress", "className", "contentClassName", "style", "as"];
|
|
13
|
+
/**
|
|
14
|
+
* A pinned scroll scene: a tall section whose content sticks to the viewport while the
|
|
15
|
+
* user scrolls through `length` vh. Progress (0–1) is written to `--dt-p` on the root, so
|
|
16
|
+
* children animate with CSS (`opacity: var(--dt-p, 1)`), and passed to `children(progress)`
|
|
17
|
+
* and `onProgress`. Under reduced motion, or with `pin={false}`, it renders in normal flow
|
|
18
|
+
* at its final state.
|
|
19
|
+
*/
|
|
20
|
+
var ScrollStage = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
|
|
21
|
+
var _ref$length = _ref.length,
|
|
22
|
+
length = _ref$length === void 0 ? SCENE_LENGTH : _ref$length,
|
|
23
|
+
_ref$mobileLength = _ref.mobileLength,
|
|
24
|
+
mobileLength = _ref$mobileLength === void 0 ? SCENE_LENGTH_MOBILE : _ref$mobileLength,
|
|
25
|
+
_ref$pin = _ref.pin,
|
|
26
|
+
pin = _ref$pin === void 0 ? true : _ref$pin,
|
|
27
|
+
children = _ref.children,
|
|
28
|
+
onProgress = _ref.onProgress,
|
|
29
|
+
className = _ref.className,
|
|
30
|
+
contentClassName = _ref.contentClassName,
|
|
31
|
+
style = _ref.style,
|
|
32
|
+
_ref$as = _ref.as,
|
|
33
|
+
Tag = _ref$as === void 0 ? "section" : _ref$as,
|
|
34
|
+
rest = _objectWithoutProperties(_ref, _excluded);
|
|
35
|
+
var local = useRef(null);
|
|
36
|
+
var setRef = useMergedRef(ref, local);
|
|
37
|
+
var reduced = useReducedMotion();
|
|
38
|
+
var mobile = useMediaQuery(MOBILE_QUERY);
|
|
39
|
+
var pinned = pin && !reduced;
|
|
40
|
+
var progress = useScrollProgress(local, {
|
|
41
|
+
mode: "pin",
|
|
42
|
+
enabled: pinned,
|
|
43
|
+
onChange: onProgress
|
|
44
|
+
});
|
|
45
|
+
var vh = mobile ? mobileLength : length;
|
|
46
|
+
return jsx(Tag, _objectSpread2(_objectSpread2({
|
|
47
|
+
ref: setRef,
|
|
48
|
+
"data-dt-stage": pinned ? "pinned" : "static",
|
|
49
|
+
className: cn("relative", className),
|
|
50
|
+
style: pinned ? _objectSpread2({
|
|
51
|
+
height: "".concat(Math.max(vh, 100), "vh")
|
|
52
|
+
}, style) : style
|
|
53
|
+
}, rest), {}, {
|
|
54
|
+
children: jsx("div", {
|
|
55
|
+
className: cn(pinned && "sticky top-0 h-screen overflow-hidden", contentClassName),
|
|
56
|
+
children: typeof children === "function" ? children(progress) : children
|
|
57
|
+
})
|
|
58
|
+
}));
|
|
59
|
+
});
|
|
60
|
+
ScrollStage.displayName = "ScrollStage";
|
|
61
|
+
|
|
62
|
+
export { ScrollStage as default };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
|
|
3
|
+
import { jsx } from 'react/jsx-runtime';
|
|
4
|
+
import { useState, useEffect, useMemo } from 'react';
|
|
5
|
+
import { useReducedMotion } from '../../hooks/useReducedMotion.js';
|
|
6
|
+
import { NATIVE_SCROLL, SmoothScrollContext } from '../../hooks/useSmoothScroll.js';
|
|
7
|
+
import { subscribe } from '../../motion/engine/scheduler.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Opt-in smooth (inertial) scrolling with Lenis. Lenis is an optional peer dependency
|
|
11
|
+
* and is never imported by this package: install it and pass the class —
|
|
12
|
+
* `<SmoothScrollProvider lenis={Lenis}>`. Without `lenis`, with `enabled={false}` or when
|
|
13
|
+
* the user prefers reduced motion, the page keeps native scrolling. Lenis runs on the same
|
|
14
|
+
* frame loop as the other motion components.
|
|
15
|
+
*/
|
|
16
|
+
function SmoothScrollProvider(_ref) {
|
|
17
|
+
var LenisClass = _ref.lenis,
|
|
18
|
+
options = _ref.options,
|
|
19
|
+
_ref$enabled = _ref.enabled,
|
|
20
|
+
enabled = _ref$enabled === void 0 ? true : _ref$enabled,
|
|
21
|
+
children = _ref.children;
|
|
22
|
+
var reduced = useReducedMotion();
|
|
23
|
+
var _useState = useState(null),
|
|
24
|
+
_useState2 = _slicedToArray(_useState, 2),
|
|
25
|
+
instance = _useState2[0],
|
|
26
|
+
setInstance = _useState2[1];
|
|
27
|
+
useEffect(function () {
|
|
28
|
+
if (!enabled || reduced || typeof LenisClass !== "function") return undefined;
|
|
29
|
+
var lenis = new LenisClass(_objectSpread2({
|
|
30
|
+
autoRaf: false
|
|
31
|
+
}, options));
|
|
32
|
+
var unsubscribe = subscribe({
|
|
33
|
+
loop: true,
|
|
34
|
+
write: function write(_, now) {
|
|
35
|
+
return lenis.raf(now);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
setInstance(lenis);
|
|
39
|
+
return function () {
|
|
40
|
+
unsubscribe();
|
|
41
|
+
lenis.destroy();
|
|
42
|
+
setInstance(null);
|
|
43
|
+
};
|
|
44
|
+
// `options` is read once per Lenis instance; change `enabled` or `lenis` to rebuild it.
|
|
45
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
46
|
+
}, [LenisClass, enabled, reduced]);
|
|
47
|
+
var value = useMemo(function () {
|
|
48
|
+
return instance ? {
|
|
49
|
+
lenis: instance,
|
|
50
|
+
scrollTo: function scrollTo(target) {
|
|
51
|
+
var opts = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
52
|
+
return instance.scrollTo(target, opts);
|
|
53
|
+
}
|
|
54
|
+
} : NATIVE_SCROLL;
|
|
55
|
+
}, [instance]);
|
|
56
|
+
return jsx(SmoothScrollContext.Provider, {
|
|
57
|
+
value: value,
|
|
58
|
+
children: children
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
SmoothScrollProvider.displayName = "SmoothScrollProvider";
|
|
62
|
+
|
|
63
|
+
export { SmoothScrollProvider as default };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { objectWithoutProperties as _objectWithoutProperties, objectSpread2 as _objectSpread2, typeof as _typeof } from '../../_virtual/_rollupPluginBabelHelpers.js';
|
|
3
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
|
+
import React, { useRef, useEffect } from 'react';
|
|
5
|
+
import { cn } from '../../utils/cn.js';
|
|
6
|
+
import { useReducedMotion } from '../../hooks/useReducedMotion.js';
|
|
7
|
+
import { observe } from '../../motion/engine/inView.js';
|
|
8
|
+
import { subscribe } from '../../motion/engine/scheduler.js';
|
|
9
|
+
import { clamp01 } from '../../motion/engine/progress.js';
|
|
10
|
+
import { useMergedRef } from '../../motion/engine/useMergedRef.js';
|
|
11
|
+
import { SCROLL_STICKY_TOP } from '../../motion/tokens.js';
|
|
12
|
+
|
|
13
|
+
var _excluded = ["items", "stickyTop", "tilt", "scaleBack", "dimBack", "mediaSide", "renderItem", "className", "style"];
|
|
14
|
+
var isImage = function isImage(media) {
|
|
15
|
+
return media && _typeof(media) === "object" && ! /*#__PURE__*/React.isValidElement(media) && "src" in media;
|
|
16
|
+
};
|
|
17
|
+
function CardBody(_ref) {
|
|
18
|
+
var item = _ref.item,
|
|
19
|
+
flip = _ref.flip;
|
|
20
|
+
return jsxs("div", {
|
|
21
|
+
className: "grid gap-8 md:grid-cols-2 md:items-center",
|
|
22
|
+
children: [jsxs("div", {
|
|
23
|
+
className: cn("flex flex-col gap-4", flip && "md:order-2"),
|
|
24
|
+
children: [item.kicker ? jsx("p", {
|
|
25
|
+
className: "text-micro uppercase tracking-widest text-primary-600 dark:text-primary-400",
|
|
26
|
+
children: item.kicker
|
|
27
|
+
}) : null, jsx("h3", {
|
|
28
|
+
className: "text-section-title font-semibold tracking-tight text-foreground",
|
|
29
|
+
children: item.title
|
|
30
|
+
}), item.body ? jsx("div", {
|
|
31
|
+
className: "text-body text-muted",
|
|
32
|
+
children: item.body
|
|
33
|
+
}) : null, item.bullets && item.bullets.length ? jsx("ul", {
|
|
34
|
+
className: "grid gap-2 sm:grid-cols-2",
|
|
35
|
+
children: item.bullets.map(function (b, i) {
|
|
36
|
+
return jsxs("li", {
|
|
37
|
+
className: "flex items-start gap-2 text-caption text-foreground",
|
|
38
|
+
children: [jsx("span", {
|
|
39
|
+
"aria-hidden": "true",
|
|
40
|
+
className: "mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-primary-500"
|
|
41
|
+
}), b]
|
|
42
|
+
}, i);
|
|
43
|
+
})
|
|
44
|
+
}) : null, item.metric || item.cta ? jsxs("div", {
|
|
45
|
+
className: "mt-2 flex flex-wrap items-end justify-between gap-4",
|
|
46
|
+
children: [item.metric ? jsxs("p", {
|
|
47
|
+
className: "flex flex-col",
|
|
48
|
+
children: [jsx("span", {
|
|
49
|
+
className: "text-kpi font-semibold text-foreground",
|
|
50
|
+
children: item.metric.value
|
|
51
|
+
}), jsx("span", {
|
|
52
|
+
className: "text-caption text-muted",
|
|
53
|
+
children: item.metric.label
|
|
54
|
+
})]
|
|
55
|
+
}) : null, item.cta]
|
|
56
|
+
}) : null]
|
|
57
|
+
}), item.media ? jsx("div", {
|
|
58
|
+
className: cn("overflow-hidden rounded-card", flip && "md:order-1"),
|
|
59
|
+
children: isImage(item.media) ? jsx("img", {
|
|
60
|
+
src: item.media.src,
|
|
61
|
+
alt: item.media.alt,
|
|
62
|
+
loading: "lazy",
|
|
63
|
+
className: "aspect-[4/3] h-full w-full object-cover"
|
|
64
|
+
}) : item.media
|
|
65
|
+
}) : null]
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Cards that stick one over another as the page scrolls (sticky stacking). Cards
|
|
70
|
+
* underneath shrink and dim as the next one covers them, and each has a slight
|
|
71
|
+
* alternating tilt so the stack looks hand-placed. Tilt is off below `md` and under
|
|
72
|
+
* reduced motion; the scale/dim effect is off under reduced motion.
|
|
73
|
+
*/
|
|
74
|
+
var StackedCards = /*#__PURE__*/React.forwardRef(function (_ref2, ref) {
|
|
75
|
+
var _ref2$items = _ref2.items,
|
|
76
|
+
items = _ref2$items === void 0 ? [] : _ref2$items,
|
|
77
|
+
_ref2$stickyTop = _ref2.stickyTop,
|
|
78
|
+
stickyTop = _ref2$stickyTop === void 0 ? SCROLL_STICKY_TOP : _ref2$stickyTop,
|
|
79
|
+
_ref2$tilt = _ref2.tilt,
|
|
80
|
+
tilt = _ref2$tilt === void 0 ? 1 : _ref2$tilt,
|
|
81
|
+
_ref2$scaleBack = _ref2.scaleBack,
|
|
82
|
+
scaleBack = _ref2$scaleBack === void 0 ? 0.94 : _ref2$scaleBack,
|
|
83
|
+
_ref2$dimBack = _ref2.dimBack,
|
|
84
|
+
dimBack = _ref2$dimBack === void 0 ? 0.15 : _ref2$dimBack,
|
|
85
|
+
_ref2$mediaSide = _ref2.mediaSide,
|
|
86
|
+
mediaSide = _ref2$mediaSide === void 0 ? "alternate" : _ref2$mediaSide,
|
|
87
|
+
renderItem = _ref2.renderItem,
|
|
88
|
+
className = _ref2.className,
|
|
89
|
+
style = _ref2.style,
|
|
90
|
+
rest = _objectWithoutProperties(_ref2, _excluded);
|
|
91
|
+
var local = useRef(null);
|
|
92
|
+
var setRef = useMergedRef(ref, local);
|
|
93
|
+
var reduced = useReducedMotion();
|
|
94
|
+
useEffect(function () {
|
|
95
|
+
var list = local.current;
|
|
96
|
+
if (!list || reduced || scaleBack >= 1 && dimBack <= 0) return undefined;
|
|
97
|
+
var slots = Array.from(list.children);
|
|
98
|
+
var cards = slots.map(function (li) {
|
|
99
|
+
return li.firstElementChild;
|
|
100
|
+
});
|
|
101
|
+
var measure = function measure() {
|
|
102
|
+
return slots.map(function (li) {
|
|
103
|
+
return li.getBoundingClientRect();
|
|
104
|
+
});
|
|
105
|
+
};
|
|
106
|
+
var write = function write(rects) {
|
|
107
|
+
cards.forEach(function (card, i) {
|
|
108
|
+
var next = rects[i + 1];
|
|
109
|
+
// 0 while the next card is a full card-height away, 1 once it has reached the sticky line.
|
|
110
|
+
var cover = next ? clamp01(1 - (next.top - stickyTop) / Math.max(rects[i].height, 1)) : 0;
|
|
111
|
+
card.style.setProperty("--dt-scale", String(1 - (1 - scaleBack) * cover));
|
|
112
|
+
card.style.setProperty("--dt-dim", String(1 - dimBack * cover));
|
|
113
|
+
});
|
|
114
|
+
};
|
|
115
|
+
var unsubscribe = null;
|
|
116
|
+
var stop = observe(list, function (inView) {
|
|
117
|
+
if (inView && !unsubscribe) unsubscribe = subscribe({
|
|
118
|
+
measure: measure,
|
|
119
|
+
write: write
|
|
120
|
+
});else if (!inView && unsubscribe) {
|
|
121
|
+
unsubscribe();
|
|
122
|
+
unsubscribe = null;
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
return function () {
|
|
126
|
+
stop();
|
|
127
|
+
if (unsubscribe) unsubscribe();
|
|
128
|
+
cards.forEach(function (card) {
|
|
129
|
+
card.style.removeProperty("--dt-scale");
|
|
130
|
+
card.style.removeProperty("--dt-dim");
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
}, [items, stickyTop, scaleBack, dimBack, reduced]);
|
|
134
|
+
return jsx("ol", _objectSpread2(_objectSpread2({
|
|
135
|
+
ref: setRef,
|
|
136
|
+
className: cn("relative flex flex-col gap-8", className),
|
|
137
|
+
style: style
|
|
138
|
+
}, rest), {}, {
|
|
139
|
+
children: items.map(function (item, i) {
|
|
140
|
+
var _item$id;
|
|
141
|
+
var deg = tilt ? (i % 2 === 0 ? -1 : 1) * tilt * (1 + i % 3 * 0.5) : 0;
|
|
142
|
+
var flip = mediaSide === "alternate" && i % 2 === 1;
|
|
143
|
+
return jsx("li", {
|
|
144
|
+
className: "sticky",
|
|
145
|
+
style: {
|
|
146
|
+
top: "".concat(stickyTop, "px")
|
|
147
|
+
},
|
|
148
|
+
children: jsx("article", {
|
|
149
|
+
className: cn("origin-top rounded-card border border-line bg-surface p-6 shadow-lg dark:bg-background md:p-10",
|
|
150
|
+
// motion-safe: none of this applies when the OS asks for reduced motion (even before hydration).
|
|
151
|
+
"motion-safe:[filter:brightness(var(--dt-dim,1))] motion-safe:[transform:scale(var(--dt-scale,1))]", "motion-safe:md:[transform:rotate(var(--dt-tilt,0deg))_scale(var(--dt-scale,1))]"),
|
|
152
|
+
style: deg ? {
|
|
153
|
+
"--dt-tilt": "".concat(deg, "deg")
|
|
154
|
+
} : undefined,
|
|
155
|
+
children: renderItem ? renderItem(item, i) : jsx(CardBody, {
|
|
156
|
+
item: item,
|
|
157
|
+
flip: flip
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
}, (_item$id = item.id) !== null && _item$id !== void 0 ? _item$id : i);
|
|
161
|
+
})
|
|
162
|
+
}));
|
|
163
|
+
});
|
|
164
|
+
StackedCards.displayName = "StackedCards";
|
|
165
|
+
|
|
166
|
+
export { StackedCards as default };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { objectWithoutProperties as _objectWithoutProperties, objectSpread2 as _objectSpread2 } from '../../_virtual/_rollupPluginBabelHelpers.js';
|
|
3
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
import { cn } from '../../utils/cn.js';
|
|
6
|
+
import CountUp from './CountUp.js';
|
|
7
|
+
|
|
8
|
+
var _excluded = ["items", "variant", "duration", "locale", "className"];
|
|
9
|
+
var COLS = {
|
|
10
|
+
1: "sm:grid-cols-1",
|
|
11
|
+
2: "sm:grid-cols-2",
|
|
12
|
+
3: "sm:grid-cols-3",
|
|
13
|
+
4: "sm:grid-cols-2 lg:grid-cols-4"
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A row of headline numbers that count up on view — as bordered cards or one band with
|
|
17
|
+
* dividers. A description list: each label is a `<dt>`, each number its `<dd>`.
|
|
18
|
+
*/
|
|
19
|
+
var StatStrip = /*#__PURE__*/React.forwardRef(function (_ref, ref) {
|
|
20
|
+
var _ref$items = _ref.items,
|
|
21
|
+
items = _ref$items === void 0 ? [] : _ref$items,
|
|
22
|
+
_ref$variant = _ref.variant,
|
|
23
|
+
variant = _ref$variant === void 0 ? "cards" : _ref$variant,
|
|
24
|
+
duration = _ref.duration,
|
|
25
|
+
locale = _ref.locale,
|
|
26
|
+
className = _ref.className,
|
|
27
|
+
rest = _objectWithoutProperties(_ref, _excluded);
|
|
28
|
+
var cards = variant !== "band";
|
|
29
|
+
var cols = COLS[Math.min(items.length, 4)] || COLS[4];
|
|
30
|
+
return jsx("dl", _objectSpread2(_objectSpread2({
|
|
31
|
+
ref: ref,
|
|
32
|
+
className: cn("grid grid-cols-2", cols, cards ? "gap-4" : "gap-y-6 rounded-card border border-line bg-surface py-6 dark:bg-background sm:divide-x sm:divide-line", className)
|
|
33
|
+
}, rest), {}, {
|
|
34
|
+
children: items.map(function (item, i) {
|
|
35
|
+
return jsxs("div", {
|
|
36
|
+
className: cn("flex flex-col-reverse gap-1", cards ? "rounded-card border border-line bg-surface p-6 shadow-xs dark:bg-background" : "px-6 text-center"),
|
|
37
|
+
children: [jsx("dt", {
|
|
38
|
+
className: "text-caption text-muted",
|
|
39
|
+
children: item.label
|
|
40
|
+
}), jsx("dd", {
|
|
41
|
+
className: "text-kpi font-semibold tracking-tight text-foreground",
|
|
42
|
+
children: jsx(CountUp, {
|
|
43
|
+
value: item.value,
|
|
44
|
+
prefix: item.prefix,
|
|
45
|
+
suffix: item.suffix,
|
|
46
|
+
decimals: item.decimals,
|
|
47
|
+
format: item.format,
|
|
48
|
+
duration: duration,
|
|
49
|
+
locale: locale
|
|
50
|
+
})
|
|
51
|
+
})]
|
|
52
|
+
}, i);
|
|
53
|
+
})
|
|
54
|
+
}));
|
|
55
|
+
});
|
|
56
|
+
StatStrip.displayName = "StatStrip";
|
|
57
|
+
|
|
58
|
+
export { StatStrip as default };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { objectWithoutProperties as _objectWithoutProperties, slicedToArray as _slicedToArray, objectSpread2 as _objectSpread2, typeof as _typeof } from '../../_virtual/_rollupPluginBabelHelpers.js';
|
|
3
|
+
import { jsxs, jsx } from 'react/jsx-runtime';
|
|
4
|
+
import React, { useRef, useId, useState, useEffect } from 'react';
|
|
5
|
+
import { cn } from '../../utils/cn.js';
|
|
6
|
+
import { useReducedMotion } from '../../hooks/useReducedMotion.js';
|
|
7
|
+
import { observe } from '../../motion/engine/inView.js';
|
|
8
|
+
import { useMergedRef } from '../../motion/engine/useMergedRef.js';
|
|
9
|
+
|
|
10
|
+
var _excluded = ["items", "autoplay", "grid", "label", "className"];
|
|
11
|
+
var isImage = function isImage(logo) {
|
|
12
|
+
return logo && _typeof(logo) === "object" && ! /*#__PURE__*/React.isValidElement(logo) && "src" in logo;
|
|
13
|
+
};
|
|
14
|
+
function Logo(_ref) {
|
|
15
|
+
var logo = _ref.logo,
|
|
16
|
+
decorative = _ref.decorative;
|
|
17
|
+
if (!logo) return null;
|
|
18
|
+
if (isImage(logo)) {
|
|
19
|
+
return jsx("img", {
|
|
20
|
+
src: logo.src,
|
|
21
|
+
alt: decorative ? "" : logo.alt,
|
|
22
|
+
className: "max-h-10 max-w-[70%] object-contain"
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
return decorative ? jsx("span", {
|
|
26
|
+
"aria-hidden": "true",
|
|
27
|
+
children: logo
|
|
28
|
+
}) : logo;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Client quotes with a 3×3 tile grid of logos: the active client's logo sits in the centre
|
|
32
|
+
* tile and the quote beside it. Dots are a tab list (arrow keys, Home/End). Autoplay pauses
|
|
33
|
+
* on hover, while focus is inside, while off screen and under reduced motion, and has a
|
|
34
|
+
* visible pause button (WCAG 2.2.2).
|
|
35
|
+
*/
|
|
36
|
+
var TestimonialSpotlight = /*#__PURE__*/React.forwardRef(function (_ref2, ref) {
|
|
37
|
+
var _ref2$items = _ref2.items,
|
|
38
|
+
items = _ref2$items === void 0 ? [] : _ref2$items,
|
|
39
|
+
_ref2$autoplay = _ref2.autoplay,
|
|
40
|
+
autoplay = _ref2$autoplay === void 0 ? 6000 : _ref2$autoplay,
|
|
41
|
+
_ref2$grid = _ref2.grid,
|
|
42
|
+
grid = _ref2$grid === void 0 ? true : _ref2$grid,
|
|
43
|
+
_ref2$label = _ref2.label,
|
|
44
|
+
label = _ref2$label === void 0 ? "Testimonials" : _ref2$label,
|
|
45
|
+
className = _ref2.className,
|
|
46
|
+
rest = _objectWithoutProperties(_ref2, _excluded);
|
|
47
|
+
var local = useRef(null);
|
|
48
|
+
var setRef = useMergedRef(ref, local);
|
|
49
|
+
var reduced = useReducedMotion();
|
|
50
|
+
var id = useId();
|
|
51
|
+
var _useState = useState(0),
|
|
52
|
+
_useState2 = _slicedToArray(_useState, 2),
|
|
53
|
+
index = _useState2[0],
|
|
54
|
+
setIndex = _useState2[1];
|
|
55
|
+
var _useState3 = useState(false),
|
|
56
|
+
_useState4 = _slicedToArray(_useState3, 2),
|
|
57
|
+
paused = _useState4[0],
|
|
58
|
+
setPaused = _useState4[1];
|
|
59
|
+
var _useState5 = useState(false),
|
|
60
|
+
_useState6 = _slicedToArray(_useState5, 2),
|
|
61
|
+
hovered = _useState6[0],
|
|
62
|
+
setHovered = _useState6[1];
|
|
63
|
+
var _useState7 = useState(false),
|
|
64
|
+
_useState8 = _slicedToArray(_useState7, 2),
|
|
65
|
+
focused = _useState8[0],
|
|
66
|
+
setFocused = _useState8[1];
|
|
67
|
+
var _useState9 = useState(false),
|
|
68
|
+
_useState0 = _slicedToArray(_useState9, 2),
|
|
69
|
+
inView = _useState0[0],
|
|
70
|
+
setInView = _useState0[1];
|
|
71
|
+
var count = items.length;
|
|
72
|
+
var playing = autoplay > 0 && count > 1 && !reduced && !paused && !hovered && !focused && inView;
|
|
73
|
+
useEffect(function () {
|
|
74
|
+
return observe(local.current, setInView);
|
|
75
|
+
}, []);
|
|
76
|
+
useEffect(function () {
|
|
77
|
+
if (!playing) return undefined;
|
|
78
|
+
var timer = setTimeout(function () {
|
|
79
|
+
return setIndex(function (i) {
|
|
80
|
+
return (i + 1) % count;
|
|
81
|
+
});
|
|
82
|
+
}, autoplay);
|
|
83
|
+
return function () {
|
|
84
|
+
return clearTimeout(timer);
|
|
85
|
+
};
|
|
86
|
+
}, [playing, index, autoplay, count]);
|
|
87
|
+
if (!count) return null;
|
|
88
|
+
var current = items[Math.min(index, count - 1)];
|
|
89
|
+
var tabs = items.map(function (_, i) {
|
|
90
|
+
return "".concat(id, "-tab-").concat(i);
|
|
91
|
+
});
|
|
92
|
+
var onTabKey = function onTabKey(event) {
|
|
93
|
+
var map = {
|
|
94
|
+
ArrowRight: index + 1,
|
|
95
|
+
ArrowDown: index + 1,
|
|
96
|
+
ArrowLeft: index - 1,
|
|
97
|
+
ArrowUp: index - 1,
|
|
98
|
+
Home: 0,
|
|
99
|
+
End: count - 1
|
|
100
|
+
};
|
|
101
|
+
if (!(event.key in map)) return;
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
var next = (map[event.key] + count) % count;
|
|
104
|
+
setIndex(next);
|
|
105
|
+
var el = document.getElementById(tabs[next]);
|
|
106
|
+
if (el) el.focus();
|
|
107
|
+
};
|
|
108
|
+
return jsxs("section", _objectSpread2(_objectSpread2({
|
|
109
|
+
ref: setRef,
|
|
110
|
+
"aria-roledescription": "carousel",
|
|
111
|
+
"aria-label": label,
|
|
112
|
+
className: cn("grid items-center gap-10 md:grid-cols-2", className),
|
|
113
|
+
onMouseEnter: function onMouseEnter() {
|
|
114
|
+
return setHovered(true);
|
|
115
|
+
},
|
|
116
|
+
onMouseLeave: function onMouseLeave() {
|
|
117
|
+
return setHovered(false);
|
|
118
|
+
},
|
|
119
|
+
onFocus: function onFocus() {
|
|
120
|
+
return setFocused(true);
|
|
121
|
+
},
|
|
122
|
+
onBlur: function onBlur(e) {
|
|
123
|
+
if (!e.currentTarget.contains(e.relatedTarget)) setFocused(false);
|
|
124
|
+
}
|
|
125
|
+
}, rest), {}, {
|
|
126
|
+
children: [grid ? jsx("div", {
|
|
127
|
+
className: "grid aspect-square grid-cols-3 gap-3",
|
|
128
|
+
children: Array.from({
|
|
129
|
+
length: 9
|
|
130
|
+
}, function (_, tile) {
|
|
131
|
+
var centre = tile === 4;
|
|
132
|
+
var other = items[(index + tile + 1) % count];
|
|
133
|
+
return jsx("div", {
|
|
134
|
+
className: cn("flex items-center justify-center rounded-card border border-line", centre ? "bg-surface shadow-md dark:bg-background" : "bg-background"),
|
|
135
|
+
children: centre ? jsx("span", {
|
|
136
|
+
className: "flex w-full justify-center animate-fade-in motion-reduce:animate-none",
|
|
137
|
+
children: jsx(Logo, {
|
|
138
|
+
logo: current.logo
|
|
139
|
+
})
|
|
140
|
+
}, index) : tile % 2 === 0 ? jsx("span", {
|
|
141
|
+
className: "flex w-full justify-center opacity-30 grayscale",
|
|
142
|
+
"aria-hidden": "true",
|
|
143
|
+
children: jsx(Logo, {
|
|
144
|
+
logo: other.logo,
|
|
145
|
+
decorative: true
|
|
146
|
+
})
|
|
147
|
+
}) : null
|
|
148
|
+
}, tile);
|
|
149
|
+
})
|
|
150
|
+
}) : null, jsxs("div", {
|
|
151
|
+
className: "flex flex-col gap-6",
|
|
152
|
+
children: [jsxs("figure", {
|
|
153
|
+
id: "".concat(id, "-panel"),
|
|
154
|
+
role: "tabpanel",
|
|
155
|
+
"aria-labelledby": tabs[index],
|
|
156
|
+
"aria-live": playing ? "off" : "polite",
|
|
157
|
+
className: "flex flex-col gap-4 animate-fade-in motion-reduce:animate-none",
|
|
158
|
+
children: [jsx("blockquote", {
|
|
159
|
+
className: "text-section-title font-medium leading-snug text-foreground",
|
|
160
|
+
children: current.quote
|
|
161
|
+
}), jsxs("figcaption", {
|
|
162
|
+
className: "text-body text-muted",
|
|
163
|
+
children: [jsx("span", {
|
|
164
|
+
className: "font-semibold text-foreground",
|
|
165
|
+
children: current.name
|
|
166
|
+
}), current.role ? jsxs("span", {
|
|
167
|
+
children: [", ", current.role]
|
|
168
|
+
}) : null]
|
|
169
|
+
})]
|
|
170
|
+
}, index), count > 1 ? jsxs("div", {
|
|
171
|
+
className: "flex items-center gap-4",
|
|
172
|
+
children: [jsx("div", {
|
|
173
|
+
role: "tablist",
|
|
174
|
+
"aria-label": "".concat(label, ": choose a quote"),
|
|
175
|
+
className: "flex gap-2",
|
|
176
|
+
onKeyDown: onTabKey,
|
|
177
|
+
children: items.map(function (item, i) {
|
|
178
|
+
var _item$id;
|
|
179
|
+
return jsx("button", {
|
|
180
|
+
id: tabs[i],
|
|
181
|
+
type: "button",
|
|
182
|
+
role: "tab",
|
|
183
|
+
"aria-selected": i === index,
|
|
184
|
+
"aria-controls": "".concat(id, "-panel"),
|
|
185
|
+
tabIndex: i === index ? 0 : -1,
|
|
186
|
+
"aria-label": "Quote ".concat(i + 1, " of ").concat(count),
|
|
187
|
+
onClick: function onClick() {
|
|
188
|
+
return setIndex(i);
|
|
189
|
+
},
|
|
190
|
+
className: cn("h-2.5 rounded-pill transition-[width,background-color] duration-base motion-reduce:transition-none focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500 focus-visible:ring-offset-2", i === index ? "w-6 bg-primary-600" : "w-2.5 bg-line hover:bg-muted/60")
|
|
191
|
+
}, (_item$id = item.id) !== null && _item$id !== void 0 ? _item$id : i);
|
|
192
|
+
})
|
|
193
|
+
}), autoplay > 0 && !reduced ? jsx("button", {
|
|
194
|
+
type: "button",
|
|
195
|
+
onClick: function onClick() {
|
|
196
|
+
return setPaused(function (p) {
|
|
197
|
+
return !p;
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
className: "rounded-pill px-2 py-1 text-caption text-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500",
|
|
201
|
+
children: paused ? "Play" : "Pause"
|
|
202
|
+
}) : null]
|
|
203
|
+
}) : null]
|
|
204
|
+
})]
|
|
205
|
+
}));
|
|
206
|
+
});
|
|
207
|
+
TestimonialSpotlight.displayName = "TestimonialSpotlight";
|
|
208
|
+
|
|
209
|
+
export { TestimonialSpotlight as default };
|