Frontend Performance Optimization #21
@@ -1,110 +1,114 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { memo } from "react";
|
||||||
import ContentLockup from "./ContentLockup";
|
import ContentLockup from "./ContentLockup";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
|
|
||||||
const AskOrganizer = ({
|
const AskOrganizer = memo(
|
||||||
title,
|
({
|
||||||
subtitle,
|
title,
|
||||||
description,
|
subtitle,
|
||||||
buttonText = "Ask an organizer",
|
description,
|
||||||
buttonHref = "#",
|
buttonText = "Ask an organizer",
|
||||||
className = "",
|
buttonHref = "#",
|
||||||
variant = "centered", // centered, left-aligned, compact
|
className = "",
|
||||||
onContactClick, // Analytics callback
|
variant = "centered", // centered, left-aligned, compact
|
||||||
}) => {
|
onContactClick, // Analytics callback
|
||||||
// Analytics tracking for contact button clicks
|
}) => {
|
||||||
const handleContactClick = (event) => {
|
// Analytics tracking for contact button clicks
|
||||||
// Track contact button interaction
|
const handleContactClick = (event) => {
|
||||||
if (onContactClick) {
|
// Track contact button interaction
|
||||||
onContactClick({
|
if (onContactClick) {
|
||||||
event: "contact_button_click",
|
onContactClick({
|
||||||
component: "AskOrganizer",
|
event: "contact_button_click",
|
||||||
variant,
|
component: "AskOrganizer",
|
||||||
buttonText,
|
variant,
|
||||||
buttonHref,
|
buttonText,
|
||||||
timestamp: new Date().toISOString(),
|
buttonHref,
|
||||||
});
|
timestamp: new Date().toISOString(),
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Additional analytics tracking (can be expanded)
|
// Additional analytics tracking (can be expanded)
|
||||||
if (typeof window !== "undefined" && window.gtag) {
|
if (typeof window !== "undefined" && window.gtag) {
|
||||||
window.gtag("event", "contact_button_click", {
|
window.gtag("event", "contact_button_click", {
|
||||||
event_category: "engagement",
|
event_category: "engagement",
|
||||||
event_label: "ask_organizer",
|
event_label: "ask_organizer",
|
||||||
value: 1,
|
value: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Variant-specific styling
|
// Variant-specific styling
|
||||||
const variantStyles = {
|
const variantStyles = {
|
||||||
centered: {
|
centered: {
|
||||||
container: "text-center",
|
container: "text-center",
|
||||||
buttonContainer: "flex justify-center",
|
buttonContainer: "flex justify-center",
|
||||||
},
|
},
|
||||||
"left-aligned": {
|
"left-aligned": {
|
||||||
container: "text-left",
|
container: "text-left",
|
||||||
buttonContainer: "flex justify-start",
|
buttonContainer: "flex justify-start",
|
||||||
},
|
},
|
||||||
compact: {
|
compact: {
|
||||||
container: "text-center",
|
container: "text-center",
|
||||||
buttonContainer: "flex justify-center",
|
buttonContainer: "flex justify-center",
|
||||||
},
|
},
|
||||||
inverse: {
|
inverse: {
|
||||||
container: "text-center",
|
container: "text-center",
|
||||||
buttonContainer: "flex justify-center",
|
buttonContainer: "flex justify-center",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = variantStyles[variant] || variantStyles.centered;
|
const styles = variantStyles[variant] || variantStyles.centered;
|
||||||
|
|
||||||
// Section padding based on variant
|
// Section padding based on variant
|
||||||
const sectionPadding =
|
const sectionPadding =
|
||||||
variant === "compact"
|
variant === "compact"
|
||||||
? "py-[var(--spacing-scale-016)] px-[var(--spacing-scale-016)] md:py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-032)]"
|
? "py-[var(--spacing-scale-016)] px-[var(--spacing-scale-016)] md:py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-032)]"
|
||||||
: "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-032)] md:py-[var(--spacing-scale-096)] md:px-[var(--spacing-scale-064)]";
|
: "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-032)] md:py-[var(--spacing-scale-096)] md:px-[var(--spacing-scale-064)]";
|
||||||
|
|
||||||
// Gap between content and button based on variant
|
// Gap between content and button based on variant
|
||||||
const contentGap =
|
const contentGap =
|
||||||
variant === "compact"
|
variant === "compact"
|
||||||
? "gap-[var(--spacing-scale-020)]"
|
? "gap-[var(--spacing-scale-020)]"
|
||||||
: "gap-[var(--spacing-scale-040)]";
|
: "gap-[var(--spacing-scale-040)]";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={`${sectionPadding} ${className}`}
|
className={`${sectionPadding} ${className}`}
|
||||||
aria-labelledby="ask-organizer-headline"
|
aria-labelledby="ask-organizer-headline"
|
||||||
role="region"
|
role="region"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<div className={`flex flex-col ${contentGap} ${styles.container}`}>
|
<div className={`flex flex-col ${contentGap} ${styles.container}`}>
|
||||||
{/* Content Lockup */}
|
{/* Content Lockup */}
|
||||||
<ContentLockup
|
<ContentLockup
|
||||||
title={title}
|
title={title}
|
||||||
subtitle={subtitle}
|
subtitle={subtitle}
|
||||||
description={description}
|
description={description}
|
||||||
variant={variant === "inverse" ? "ask-inverse" : "ask"}
|
variant={variant === "inverse" ? "ask-inverse" : "ask"}
|
||||||
alignment={variant === "left-aligned" ? "left" : "center"}
|
alignment={variant === "left-aligned" ? "left" : "center"}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Button */}
|
{/* Button */}
|
||||||
<div className={styles.buttonContainer}>
|
<div className={styles.buttonContainer}>
|
||||||
<Button
|
<Button
|
||||||
href={buttonHref}
|
href={buttonHref}
|
||||||
size="large"
|
size="large"
|
||||||
variant={variant === "inverse" ? "primary" : "default"}
|
variant={variant === "inverse" ? "primary" : "default"}
|
||||||
className="xl:!px-[var(--spacing-scale-020)] xl:!py-[var(--spacing-scale-012)] xl:!text-[24px] xl:!leading-[28px]"
|
className="xl:!px-[var(--spacing-scale-020)] xl:!py-[var(--spacing-scale-012)] xl:!text-[24px] xl:!leading-[28px]"
|
||||||
onClick={handleContactClick}
|
onClick={handleContactClick}
|
||||||
aria-label={`${buttonText} - Contact an organizer for help`}
|
aria-label={`${buttonText} - Contact an organizer for help`}
|
||||||
>
|
>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
);
|
||||||
);
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
|
AskOrganizer.displayName = "AskOrganizer";
|
||||||
|
|
||||||
export default AskOrganizer;
|
export default AskOrganizer;
|
||||||
|
|||||||
+18
-16
@@ -1,18 +1,20 @@
|
|||||||
export default function Avatar({
|
import React, { memo } from "react";
|
||||||
src,
|
|
||||||
alt,
|
|
||||||
size = "small",
|
|
||||||
className = "",
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
const sizeStyles = {
|
|
||||||
small: "w-[var(--spacing-scale-016)] h-[var(--spacing-scale-016)]",
|
|
||||||
medium: "w-[18px] h-[18px]",
|
|
||||||
large: "w-[var(--spacing-scale-024)] h-[var(--spacing-scale-024)]",
|
|
||||||
xlarge: "w-[var(--spacing-scale-032)] h-[var(--spacing-scale-032)]",
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseStyles = `rounded-[var(--radius-measures-radius-full)] object-cover ${sizeStyles[size]} ${className}`;
|
const Avatar = memo(
|
||||||
|
({ src, alt, size = "small", className = "", ...props }) => {
|
||||||
|
const sizeStyles = {
|
||||||
|
small: "w-[var(--spacing-scale-016)] h-[var(--spacing-scale-016)]",
|
||||||
|
medium: "w-[18px] h-[18px]",
|
||||||
|
large: "w-[var(--spacing-scale-024)] h-[var(--spacing-scale-024)]",
|
||||||
|
xlarge: "w-[var(--spacing-scale-032)] h-[var(--spacing-scale-032)]",
|
||||||
|
};
|
||||||
|
|
||||||
return <img src={src} alt={alt} className={baseStyles} {...props} />;
|
const baseStyles = `rounded-[var(--radius-measures-radius-full)] object-cover ${sizeStyles[size]} ${className}`;
|
||||||
}
|
|
||||||
|
return <img src={src} alt={alt} className={baseStyles} {...props} />;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Avatar.displayName = "Avatar";
|
||||||
|
|
||||||
|
export default Avatar;
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
export default function AvatarContainer({
|
import React, { memo } from "react";
|
||||||
children,
|
|
||||||
size = "small",
|
|
||||||
className = "",
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
const sizeStyles = {
|
|
||||||
small: "flex -space-x-[var(--spacing-scale-008)]",
|
|
||||||
medium: "flex -space-x-[9px]",
|
|
||||||
large: "flex -space-x-[var(--spacing-scale-010)]",
|
|
||||||
xlarge: "flex -space-x-[13px]",
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseStyles = `items-center ${sizeStyles[size]} ${className}`;
|
const AvatarContainer = memo(
|
||||||
|
({ children, size = "small", className = "", ...props }) => {
|
||||||
|
const sizeStyles = {
|
||||||
|
small: "flex -space-x-[var(--spacing-scale-008)]",
|
||||||
|
medium: "flex -space-x-[9px]",
|
||||||
|
large: "flex -space-x-[var(--spacing-scale-010)]",
|
||||||
|
xlarge: "flex -space-x-[13px]",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const baseStyles = `items-center ${sizeStyles[size]} ${className}`;
|
||||||
<div className={baseStyles} {...props}>
|
|
||||||
{children}
|
return (
|
||||||
</div>
|
<div className={baseStyles} {...props}>
|
||||||
);
|
{children}
|
||||||
}
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
AvatarContainer.displayName = "AvatarContainer";
|
||||||
|
|
||||||
|
export default AvatarContainer;
|
||||||
|
|||||||
+98
-90
@@ -1,108 +1,116 @@
|
|||||||
export default function Button({
|
import React, { memo } from "react";
|
||||||
children,
|
|
||||||
variant = "default",
|
|
||||||
size = "xsmall",
|
|
||||||
className = "",
|
|
||||||
disabled = false,
|
|
||||||
type = "button",
|
|
||||||
onClick,
|
|
||||||
href,
|
|
||||||
target,
|
|
||||||
rel,
|
|
||||||
ariaLabel,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
const sizeStyles = {
|
|
||||||
xsmall:
|
|
||||||
"px-[var(--spacing-scale-006)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)]",
|
|
||||||
small:
|
|
||||||
"px-[var(--spacing-measures-spacing-008)] py-[var(--spacing-measures-spacing-008)] gap-[var(--spacing-scale-004)]",
|
|
||||||
medium: "p-[var(--spacing-scale-010)] gap-[var(--spacing-scale-004)]",
|
|
||||||
large:
|
|
||||||
"px-[var(--spacing-scale-012)] py-[var(--spacing-scale-010)] gap-[var(--spacing-scale-004)]",
|
|
||||||
xlarge:
|
|
||||||
"px-[var(--spacing-scale-020)] py-[var(--spacing-scale-012)] gap-[var(--spacing-scale-008)]",
|
|
||||||
};
|
|
||||||
|
|
||||||
const fontStyles = {
|
const Button = memo(
|
||||||
xsmall: "font-inter text-[10px] leading-[12px] font-medium tracking-[0%]",
|
({
|
||||||
small: "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
|
children,
|
||||||
medium: "font-inter text-[14px] leading-[16px] font-medium tracking-[0%]",
|
variant = "default",
|
||||||
large: "font-inter text-[16px] leading-[20px] font-medium tracking-[0%]",
|
size = "xsmall",
|
||||||
xlarge: "font-inter text-[24px] leading-[28px] font-normal tracking-[0%]",
|
className = "",
|
||||||
};
|
disabled = false,
|
||||||
|
type = "button",
|
||||||
|
onClick,
|
||||||
|
href,
|
||||||
|
target,
|
||||||
|
rel,
|
||||||
|
ariaLabel,
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const sizeStyles = {
|
||||||
|
xsmall:
|
||||||
|
"px-[var(--spacing-scale-006)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)]",
|
||||||
|
small:
|
||||||
|
"px-[var(--spacing-measures-spacing-008)] py-[var(--spacing-measures-spacing-008)] gap-[var(--spacing-scale-004)]",
|
||||||
|
medium: "p-[var(--spacing-scale-010)] gap-[var(--spacing-scale-004)]",
|
||||||
|
large:
|
||||||
|
"px-[var(--spacing-scale-012)] py-[var(--spacing-scale-010)] gap-[var(--spacing-scale-004)]",
|
||||||
|
xlarge:
|
||||||
|
"px-[var(--spacing-scale-020)] py-[var(--spacing-scale-012)] gap-[var(--spacing-scale-008)]",
|
||||||
|
};
|
||||||
|
|
||||||
const variantStyles = {
|
const fontStyles = {
|
||||||
default:
|
xsmall: "font-inter text-[10px] leading-[12px] font-medium tracking-[0%]",
|
||||||
"bg-[var(--color-surface-inverse-primary)] text-[var(--color-content-inverse-primary)] hover:bg-[var(--color-surface-inverse-primary)] hover:text-[var(--color-content-inverse-brand-primary)] hover:outline-[var(--border-color-default-brandprimary)] hover:outline-inset hover:scale-[1.02] hover:shadow-lg focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-content-default-brand-primary)] focus:ring-offset-1 focus:scale-[1.02] active:bg-[var(--color-surface-inverse-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:outline-[var(--border-color-default-brandprimary)] active:outline-offset-1 active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100 disabled:hover:shadow-none disabled:hover:outline-none",
|
small: "font-inter text-[12px] leading-[14px] font-medium tracking-[0%]",
|
||||||
secondary:
|
medium: "font-inter text-[14px] leading-[16px] font-medium tracking-[0%]",
|
||||||
"bg-transparent text-[var(--color-content-default-brand-primary)] hover:text-[var(--color-content-default-primary)] hover:scale-[1.02] hover:bg-transparent hover:outline-none focus:outline-1 focus:outline-inset focus:outline-[var(--border-color-default-tertiary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
|
large: "font-inter text-[16px] leading-[20px] font-medium tracking-[0%]",
|
||||||
primary:
|
xlarge: "font-inter text-[24px] leading-[28px] font-normal tracking-[0%]",
|
||||||
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] hover:bg-[var(--color-surface-default-primary)] hover:text-[var(--color-content-default-brand-primary)] hover:scale-[1.02] focus:bg-[var(--color-surface-default-primary)] focus:text-[var(--color-content-default-brand-primary)] focus:outline-none focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] focus:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-primary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
|
};
|
||||||
outlined:
|
|
||||||
"bg-transparent text-[var(--color-content-default-primary)] border-[1.5px] border-[var(--color-content-default-primary)] hover:bg-transparent hover:text-[var(--color-content-default-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-content-default-brand-primary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-default-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-content-default-primary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] focus:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:border-transparent active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:border-[1.5px] disabled:border-[var(--color-surface-default-secondary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
|
|
||||||
dark: "bg-transparent text-[var(--color-content-inverse-primary)] border border-[var(--border-color-default-primary)] hover:bg-transparent hover:text-[var(--color-content-inverse-brand-primary)] hover:border hover:border-[var(--border-color-inverse-brandprimary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-inverse-primary)] focus:outline-none focus:border focus:border-[var(--border-color-default-primary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] focus:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:border-transparent active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-primary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
|
|
||||||
inverse:
|
|
||||||
"bg-transparent text-[var(--color-content-inverse-primary)] hover:text-[var(--color-content-inverse-brand-primary)] hover:scale-[1.02] hover:bg-transparent hover:outline-none focus:outline-1 focus:outline-inset focus:outline-[var(--border-color-default-tertiary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-tertiary)] focus:blur-[0px] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-primary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
|
|
||||||
};
|
|
||||||
|
|
||||||
const hoverOutlineStyles = {
|
const variantStyles = {
|
||||||
xsmall: "hover:outline-1",
|
default:
|
||||||
small: "hover:outline-1",
|
"bg-[var(--color-surface-inverse-primary)] text-[var(--color-content-inverse-primary)] hover:bg-[var(--color-surface-inverse-primary)] hover:text-[var(--color-content-inverse-brand-primary)] hover:outline-[var(--border-color-default-brandprimary)] hover:outline-inset hover:scale-[1.02] hover:shadow-lg focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:outline-none focus:ring-1 focus:ring-[var(--color-content-default-brand-primary)] focus:ring-offset-1 focus:scale-[1.02] active:bg-[var(--color-surface-inverse-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:outline-[var(--border-color-default-brandprimary)] active:outline-offset-1 active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100 disabled:hover:shadow-none disabled:hover:outline-none",
|
||||||
medium: "hover:outline-1",
|
secondary:
|
||||||
large: "hover:outline-2",
|
"bg-transparent text-[var(--color-content-default-brand-primary)] hover:text-[var(--color-content-default-primary)] hover:scale-[1.02] hover:bg-transparent hover:outline-none focus:outline-1 focus:outline-inset focus:outline-[var(--border-color-default-tertiary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
|
||||||
xlarge: "hover:outline-[2.5px]",
|
primary:
|
||||||
};
|
"bg-[var(--color-surface-default-primary)] text-[var(--color-content-default-primary)] hover:bg-[var(--color-surface-default-primary)] hover:text-[var(--color-content-default-brand-primary)] hover:scale-[1.02] focus:bg-[var(--color-surface-default-primary)] focus:text-[var(--color-content-default-brand-primary)] focus:outline-none focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] focus:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-primary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
|
||||||
|
outlined:
|
||||||
|
"bg-transparent text-[var(--color-content-default-primary)] border-[1.5px] border-[var(--color-content-default-primary)] hover:bg-transparent hover:text-[var(--color-content-default-brand-primary)] hover:border-[1.5px] hover:border-[var(--color-content-default-brand-primary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-default-primary)] focus:outline-none focus:border-[1.5px] focus:border-[var(--color-content-default-primary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] focus:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:border-transparent active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-default-secondary)] disabled:text-[var(--color-content-inverse-tertiary)] disabled:border-[1.5px] disabled:border-[var(--color-surface-default-secondary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
|
||||||
|
dark: "bg-transparent text-[var(--color-content-inverse-primary)] border border-[var(--border-color-default-primary)] hover:bg-transparent hover:text-[var(--color-content-inverse-brand-primary)] hover:border hover:border-[var(--border-color-inverse-brandprimary)] hover:scale-[1.02] focus:bg-transparent focus:text-[var(--color-content-inverse-primary)] focus:outline-none focus:border focus:border-[var(--border-color-default-primary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-brand-primary)] focus:blur-[0px] focus:scale-[1.02] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:border-transparent active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-primary)] disabled:border-transparent disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
|
||||||
|
inverse:
|
||||||
|
"bg-transparent text-[var(--color-content-inverse-primary)] hover:text-[var(--color-content-inverse-brand-primary)] hover:scale-[1.02] hover:bg-transparent hover:outline-none focus:outline-1 focus:outline-inset focus:outline-[var(--border-color-default-tertiary)] focus:shadow-[0_0_10px_1px_var(--color-surface-default-tertiary)] focus:blur-[0px] active:bg-[var(--color-surface-default-brand-primary)] active:text-[var(--color-content-inverse-primary)] active:shadow-none active:scale-[0.98] disabled:bg-[var(--color-surface-inverse-secondary)] disabled:text-[var(--color-content-default-primary)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:scale-100 disabled:active:scale-100",
|
||||||
|
};
|
||||||
|
|
||||||
// Only apply outline styles to default and secondary variants, not primary, outlined, dark, or inverse
|
const hoverOutlineStyles = {
|
||||||
const outlineStyles =
|
xsmall: "hover:outline-1",
|
||||||
variant === "primary" ||
|
small: "hover:outline-1",
|
||||||
variant === "outlined" ||
|
medium: "hover:outline-1",
|
||||||
variant === "dark" ||
|
large: "hover:outline-2",
|
||||||
variant === "inverse"
|
xlarge: "hover:outline-[2.5px]",
|
||||||
? ""
|
};
|
||||||
: hoverOutlineStyles[size];
|
|
||||||
|
|
||||||
const baseStyles = `inline-flex items-center justify-start box-border ${sizeStyles[size]} rounded-[var(--radius-measures-radius-full)] ${fontStyles[size]} transition-all duration-500 ease-in-out cursor-pointer ${variantStyles[variant]} ${outlineStyles}`;
|
// Only apply outline styles to default and secondary variants, not primary, outlined, dark, or inverse
|
||||||
|
const outlineStyles =
|
||||||
|
variant === "primary" ||
|
||||||
|
variant === "outlined" ||
|
||||||
|
variant === "dark" ||
|
||||||
|
variant === "inverse"
|
||||||
|
? ""
|
||||||
|
: hoverOutlineStyles[size];
|
||||||
|
|
||||||
let finalVariant = variant;
|
const baseStyles = `inline-flex items-center justify-start box-border ${sizeStyles[size]} rounded-[var(--radius-measures-radius-full)] ${fontStyles[size]} transition-all duration-500 ease-in-out cursor-pointer ${variantStyles[variant]} ${outlineStyles}`;
|
||||||
if (disabled) {
|
|
||||||
finalVariant = "default";
|
|
||||||
}
|
|
||||||
|
|
||||||
const combinedStyles = `${baseStyles} ${className}`;
|
let finalVariant = variant;
|
||||||
|
if (disabled) {
|
||||||
|
finalVariant = "default";
|
||||||
|
}
|
||||||
|
|
||||||
const accessibilityProps = {
|
const combinedStyles = `${baseStyles} ${className}`;
|
||||||
...(ariaLabel && { "aria-label": ariaLabel }),
|
|
||||||
...(disabled && { "aria-disabled": "true" }),
|
const accessibilityProps = {
|
||||||
...(target && { target }),
|
...(ariaLabel && { "aria-label": ariaLabel }),
|
||||||
...(rel && { rel }),
|
...(disabled && { "aria-disabled": "true" }),
|
||||||
tabIndex: disabled ? -1 : 0,
|
...(target && { target }),
|
||||||
...props,
|
...(rel && { rel }),
|
||||||
};
|
tabIndex: disabled ? -1 : 0,
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (href && !disabled) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={href}
|
||||||
|
className={combinedStyles}
|
||||||
|
onClick={onClick}
|
||||||
|
{...accessibilityProps}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (href && !disabled) {
|
|
||||||
return (
|
return (
|
||||||
<a
|
<button
|
||||||
href={href}
|
type={type}
|
||||||
className={combinedStyles}
|
className={combinedStyles}
|
||||||
|
disabled={disabled}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
{...accessibilityProps}
|
{...accessibilityProps}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
Button.displayName = "Button";
|
||||||
<button
|
|
||||||
type={type}
|
export default Button;
|
||||||
className={combinedStyles}
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={onClick}
|
|
||||||
{...accessibilityProps}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import React, { memo } from "react";
|
||||||
import { getAssetPath } from "../../lib/assetUtils";
|
import { getAssetPath } from "../../lib/assetUtils";
|
||||||
import ContentContainer from "./ContentContainer";
|
import ContentContainer from "./ContentContainer";
|
||||||
|
|
||||||
export default function ContentBanner({ post }) {
|
const ContentBanner = memo(({ post }) => {
|
||||||
// Get article-specific horizontal thumbnail (small) and banner (md+)
|
// Get article-specific horizontal thumbnail (small) and banner (md+)
|
||||||
const getBackgroundImage = (post) => {
|
const getBackgroundImage = (post) => {
|
||||||
if (post.frontmatter?.thumbnail?.horizontal) {
|
if (post.frontmatter?.thumbnail?.horizontal) {
|
||||||
@@ -71,4 +72,8 @@ export default function ContentBanner({ post }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
ContentBanner.displayName = "ContentBanner";
|
||||||
|
|
||||||
|
export default ContentBanner;
|
||||||
|
|||||||
+105
-101
@@ -1,127 +1,131 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { memo } from "react";
|
||||||
import { getAssetPath, ASSETS } from "../../lib/assetUtils";
|
import { getAssetPath, ASSETS } from "../../lib/assetUtils";
|
||||||
|
|
||||||
const ContentContainer = ({ post, width = "200px", size = "responsive" }) => {
|
const ContentContainer = memo(
|
||||||
// Get the corresponding icon based on the same logic as background images
|
({ post, width = "200px", size = "responsive" }) => {
|
||||||
const getIconImage = (slug) => {
|
// Get the corresponding icon based on the same logic as background images
|
||||||
const icons = [
|
const getIconImage = (slug) => {
|
||||||
getAssetPath(ASSETS.ICON_1),
|
const icons = [
|
||||||
getAssetPath(ASSETS.ICON_2),
|
getAssetPath(ASSETS.ICON_1),
|
||||||
getAssetPath(ASSETS.ICON_3),
|
getAssetPath(ASSETS.ICON_2),
|
||||||
];
|
getAssetPath(ASSETS.ICON_3),
|
||||||
|
];
|
||||||
|
|
||||||
if (!slug) return icons[0];
|
if (!slug) return icons[0];
|
||||||
|
|
||||||
// Use the same cycling logic as background images to ensure matching
|
// Use the same cycling logic as background images to ensure matching
|
||||||
const slugOrder = [
|
const slugOrder = [
|
||||||
"building-community-trust",
|
"building-community-trust",
|
||||||
"operational-security-mutual-aid",
|
"operational-security-mutual-aid",
|
||||||
"making-decisions-without-hierarchy",
|
"making-decisions-without-hierarchy",
|
||||||
"resolving-active-conflicts",
|
"resolving-active-conflicts",
|
||||||
];
|
];
|
||||||
const index = slugOrder.indexOf(slug);
|
const index = slugOrder.indexOf(slug);
|
||||||
const finalIndex = index >= 0 ? index % icons.length : 0;
|
const finalIndex = index >= 0 ? index % icons.length : 0;
|
||||||
return icons[finalIndex];
|
return icons[finalIndex];
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconImage = getIconImage(post.slug);
|
const iconImage = getIconImage(post.slug);
|
||||||
|
|
||||||
// Choose styling based on size prop
|
// Choose styling based on size prop
|
||||||
const containerClasses =
|
const containerClasses =
|
||||||
size === "xs"
|
size === "xs"
|
||||||
? "relative z-20 h-full flex flex-col gap-[var(--measures-spacing-012)]"
|
? "relative z-20 h-full flex flex-col gap-[var(--measures-spacing-012)]"
|
||||||
: "relative z-20 h-full flex flex-col gap-[var(--measures-spacing-012)] sm:gap-[var(--measures-spacing-016)] md:gap-[18px] lg:gap-[var(--measures-spacing-024)]";
|
: "relative z-20 h-full flex flex-col gap-[var(--measures-spacing-012)] sm:gap-[var(--measures-spacing-016)] md:gap-[18px] lg:gap-[var(--measures-spacing-024)]";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
|
||||||
className={`${containerClasses} ${
|
|
||||||
size === "responsive"
|
|
||||||
? "max-w-[298px] sm:max-w-[479px] lg:max-w-[365px] xl:max-w-[623px]"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
style={size === "responsive" ? {} : { width }}
|
|
||||||
>
|
|
||||||
{/* Content Container - gap between icon and text */}
|
|
||||||
<div
|
<div
|
||||||
className={
|
className={`${containerClasses} ${
|
||||||
size === "xs"
|
size === "responsive"
|
||||||
? "flex flex-col gap-[var(--measures-spacing-008)]"
|
? "max-w-[298px] sm:max-w-[479px] lg:max-w-[365px] xl:max-w-[623px]"
|
||||||
: "flex flex-col gap-[var(--measures-spacing-008)] sm:gap-[var(--measures-spacing-012)] md:gap-[var(--measures-spacing-008)] lg:gap-[var(--measures-spacing-016)] xl:gap-[var(--measures-spacing-004)]"
|
: ""
|
||||||
}
|
}`}
|
||||||
|
style={size === "responsive" ? {} : { width }}
|
||||||
>
|
>
|
||||||
{/* Icon */}
|
{/* Content Container - gap between icon and text */}
|
||||||
<div className="w-[60px] h-[30px] flex items-center justify-center">
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img
|
|
||||||
src={iconImage}
|
|
||||||
alt={`Icon for ${post.frontmatter.title}`}
|
|
||||||
className="w-[60px] h-[30px] object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Text Container */}
|
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
size === "xs"
|
size === "xs"
|
||||||
? "flex flex-col gap-[var(--measures-spacing-004)]"
|
? "flex flex-col gap-[var(--measures-spacing-008)]"
|
||||||
: "flex flex-col gap-[var(--measures-spacing-004)] md:gap-[var(--measures-spacing-002)] lg:gap-[var(--measures-spacing-004)]"
|
: "flex flex-col gap-[var(--measures-spacing-008)] sm:gap-[var(--measures-spacing-012)] md:gap-[var(--measures-spacing-008)] lg:gap-[var(--measures-spacing-016)] xl:gap-[var(--measures-spacing-004)]"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* Title */}
|
{/* Icon */}
|
||||||
<h3
|
<div className="w-[60px] h-[30px] flex items-center justify-center">
|
||||||
className={
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
size === "xs"
|
<img
|
||||||
? "font-bricolage font-medium text-[18px] leading-[120%] text-[var(--color-content-inverse-brand-royal)] group-hover:text-blue-200 transition-colors"
|
src={iconImage}
|
||||||
: "font-bricolage font-medium text-[18px] leading-[120%] sm:text-[24px] sm:leading-[24px] md:text-[32px] md:leading-[110%] lg:text-[44px] lg:leading-[110%] xl:text-[64px] xl:leading-[110%] text-[var(--color-content-inverse-brand-royal)] group-hover:text-blue-200 transition-colors"
|
alt={`Icon for ${post.frontmatter.title}`}
|
||||||
}
|
className="w-[60px] h-[30px] object-contain"
|
||||||
>
|
/>
|
||||||
{post.frontmatter.title}
|
</div>
|
||||||
</h3>
|
|
||||||
|
|
||||||
{/* Description */}
|
{/* Text Container */}
|
||||||
<p
|
<div
|
||||||
className={
|
className={
|
||||||
size === "xs"
|
size === "xs"
|
||||||
? "font-inter font-normal text-[12px] leading-[16px] text-[var(--color-content-inverse-brand-royal)] max-w-md"
|
? "flex flex-col gap-[var(--measures-spacing-004)]"
|
||||||
: "font-inter font-normal text-[12px] leading-[16px] sm:text-[14px] sm:leading-[20px] md:text-[14px] md:leading-[20px] lg:text-[18px] lg:leading-[130%] xl:text-[24px] xl:leading-[32px] text-[var(--color-content-inverse-brand-royal)]"
|
: "flex flex-col gap-[var(--measures-spacing-004)] md:gap-[var(--measures-spacing-002)] lg:gap-[var(--measures-spacing-004)]"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{post.frontmatter.description}
|
{/* Title */}
|
||||||
</p>
|
<h3
|
||||||
|
className={
|
||||||
|
size === "xs"
|
||||||
|
? "font-bricolage font-medium text-[18px] leading-[120%] text-[var(--color-content-inverse-brand-royal)] group-hover:text-blue-200 transition-colors"
|
||||||
|
: "font-bricolage font-medium text-[18px] leading-[120%] sm:text-[24px] sm:leading-[24px] md:text-[32px] md:leading-[110%] lg:text-[44px] lg:leading-[110%] xl:text-[64px] xl:leading-[110%] text-[var(--color-content-inverse-brand-royal)] group-hover:text-blue-200 transition-colors"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{post.frontmatter.title}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<p
|
||||||
|
className={
|
||||||
|
size === "xs"
|
||||||
|
? "font-inter font-normal text-[12px] leading-[16px] text-[var(--color-content-inverse-brand-royal)] max-w-md"
|
||||||
|
: "font-inter font-normal text-[12px] leading-[16px] sm:text-[14px] sm:leading-[20px] md:text-[14px] md:leading-[20px] lg:text-[18px] lg:leading-[130%] xl:text-[24px] xl:leading-[32px] text-[var(--color-content-inverse-brand-royal)]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{post.frontmatter.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Metadata Container - horizontal with 8px gap */}
|
||||||
|
<div className="flex items-center gap-[var(--measures-spacing-008)]">
|
||||||
|
{/* Author Name */}
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
size === "xs"
|
||||||
|
? "font-inter font-normal text-[10px] leading-[14px] text-[var(--color-content-inverse-brand-royal)]"
|
||||||
|
: "font-inter font-normal text-[10px] leading-[14px] md:text-[12px] md:leading-[16px] lg:text-[14px] lg:leading-[20px] xl:text-[18px] xl:leading-[130%] text-[var(--color-content-inverse-brand-royal)]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{post.frontmatter.author}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{/* Date */}
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
size === "xs"
|
||||||
|
? "font-inter font-normal text-[10px] leading-[14px] text-[var(--color-content-inverse-brand-royal)]"
|
||||||
|
: "font-inter font-normal text-[10px] leading-[14px] md:text-[12px] md:leading-[16px] lg:text-[14px] lg:leading-[20px] xl:text-[18px] xl:leading-[130%] text-[var(--color-content-inverse-brand-royal)]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{new Date(post.frontmatter.date).toLocaleDateString("en-US", {
|
||||||
|
year: "numeric",
|
||||||
|
month: "long",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
{/* Metadata Container - horizontal with 8px gap */}
|
ContentContainer.displayName = "ContentContainer";
|
||||||
<div className="flex items-center gap-[var(--measures-spacing-008)]">
|
|
||||||
{/* Author Name */}
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
size === "xs"
|
|
||||||
? "font-inter font-normal text-[10px] leading-[14px] text-[var(--color-content-inverse-brand-royal)]"
|
|
||||||
: "font-inter font-normal text-[10px] leading-[14px] md:text-[12px] md:leading-[16px] lg:text-[14px] lg:leading-[20px] xl:text-[18px] xl:leading-[130%] text-[var(--color-content-inverse-brand-royal)]"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{post.frontmatter.author}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{/* Date */}
|
|
||||||
<span
|
|
||||||
className={
|
|
||||||
size === "xs"
|
|
||||||
? "font-inter font-normal text-[10px] leading-[14px] text-[var(--color-content-inverse-brand-royal)]"
|
|
||||||
: "font-inter font-normal text-[10px] leading-[14px] md:text-[12px] md:leading-[16px] lg:text-[14px] lg:leading-[20px] xl:text-[18px] xl:leading-[130%] text-[var(--color-content-inverse-brand-royal)]"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{new Date(post.frontmatter.date).toLocaleDateString("en-US", {
|
|
||||||
year: "numeric",
|
|
||||||
month: "long",
|
|
||||||
})}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ContentContainer;
|
export default ContentContainer;
|
||||||
|
|||||||
+167
-158
@@ -1,178 +1,187 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import React, { memo } from "react";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
import { getAssetPath } from "../../lib/assetUtils";
|
import { getAssetPath } from "../../lib/assetUtils";
|
||||||
|
|
||||||
const ContentLockup = ({
|
const ContentLockup = memo(
|
||||||
title,
|
({
|
||||||
subtitle,
|
title,
|
||||||
description,
|
subtitle,
|
||||||
ctaText,
|
description,
|
||||||
ctaHref,
|
ctaText,
|
||||||
buttonClassName = "",
|
ctaHref,
|
||||||
variant = "hero",
|
buttonClassName = "",
|
||||||
linkText,
|
variant = "hero",
|
||||||
linkHref,
|
linkText,
|
||||||
alignment = "center", // center, left
|
linkHref,
|
||||||
}) => {
|
alignment = "center", // center, left
|
||||||
// Variant-specific styling
|
}) => {
|
||||||
const variantStyles = {
|
// Variant-specific styling
|
||||||
hero: {
|
const variantStyles = {
|
||||||
container:
|
hero: {
|
||||||
"flex flex-col gap-[var(--spacing-scale-006)] sm:gap-[var(--spacing-scale-012)] md:gap-[var(--spacing-scale-020)] lg:gap-[var(--spacing-scale-020)] relative z-10",
|
container:
|
||||||
textContainer:
|
"flex flex-col gap-[var(--spacing-scale-006)] sm:gap-[var(--spacing-scale-012)] md:gap-[var(--spacing-scale-020)] lg:gap-[var(--spacing-scale-020)] relative z-10",
|
||||||
"flex flex-col md:gap-[var(--spacing-scale-004)] lg:gap-[var(--spacing-scale-008)] xl:gap-[var(--spacing-scale-020)]",
|
textContainer:
|
||||||
titleGroup: "flex flex-col xl:gap-0",
|
"flex flex-col md:gap-[var(--spacing-scale-004)] lg:gap-[var(--spacing-scale-008)] xl:gap-[var(--spacing-scale-020)]",
|
||||||
titleContainer:
|
titleGroup: "flex flex-col xl:gap-0",
|
||||||
"flex gap-[var(--spacing-scale-008)] xl:gap-[var(--spacing-scale-010)] items-center",
|
titleContainer:
|
||||||
title:
|
"flex gap-[var(--spacing-scale-008)] xl:gap-[var(--spacing-scale-010)] items-center",
|
||||||
"font-bricolage-grotesque font-medium text-[32px] leading-[32px] sm:text-[52px] sm:leading-[52px] md:text-[44px] md:leading-[44px] lg:text-[64px] lg:leading-[64px] xl:text-[96px] xl:leading-[110%] text-[var(--color-content-inverse-primary)]",
|
title:
|
||||||
subtitle:
|
"font-bricolage-grotesque font-medium text-[32px] leading-[32px] sm:text-[52px] sm:leading-[52px] md:text-[44px] md:leading-[44px] lg:text-[64px] lg:leading-[64px] xl:text-[96px] xl:leading-[110%] text-[var(--color-content-inverse-primary)]",
|
||||||
"font-bricolage-grotesque font-medium text-[32px] leading-[32px] sm:text-[52px] sm:leading-[52px] md:text-[44px] md:leading-[44px] lg:text-[64px] lg:leading-[64px] xl:text-[96px] xl:leading-[110%] text-[var(--color-content-inverse-primary)]",
|
subtitle:
|
||||||
description:
|
"font-bricolage-grotesque font-medium text-[32px] leading-[32px] sm:text-[52px] sm:leading-[52px] md:text-[44px] md:leading-[44px] lg:text-[64px] lg:leading-[64px] xl:text-[96px] xl:leading-[110%] text-[var(--color-content-inverse-primary)]",
|
||||||
"font-inter font-normal text-[18px] leading-[130%] lg:text-[24px] lg:leading-[32px] xl:text-[32px] xl:leading-[40px] text-[var(--color-content-inverse-primary)]",
|
description:
|
||||||
shape:
|
"font-inter font-normal text-[18px] leading-[130%] lg:text-[24px] lg:leading-[32px] xl:text-[32px] xl:leading-[40px] text-[var(--color-content-inverse-primary)]",
|
||||||
"w-[27.2px] h-[27.2px] md:w-[34px] md:h-[34px] lg:w-[50px] lg:h-[50px]",
|
shape:
|
||||||
},
|
"w-[27.2px] h-[27.2px] md:w-[34px] md:h-[34px] lg:w-[50px] lg:h-[50px]",
|
||||||
feature: {
|
},
|
||||||
container: "flex flex-col gap-[var(--spacing-scale-012)] relative z-10",
|
feature: {
|
||||||
textContainer: "flex flex-col gap-[var(--spacing-scale-012)]",
|
container: "flex flex-col gap-[var(--spacing-scale-012)] relative z-10",
|
||||||
titleGroup: "flex flex-col gap-[var(--spacing-scale-012)]",
|
textContainer: "flex flex-col gap-[var(--spacing-scale-012)]",
|
||||||
titleContainer: "flex gap-[var(--spacing-scale-008)] items-center",
|
titleGroup: "flex flex-col gap-[var(--spacing-scale-012)]",
|
||||||
title:
|
titleContainer: "flex gap-[var(--spacing-scale-008)] items-center",
|
||||||
"font-bricolage-grotesque font-medium text-[32px] leading-[130%] tracking-[0] text-[var(--color-content-default-primary)]",
|
title:
|
||||||
subtitle:
|
"font-bricolage-grotesque font-medium text-[32px] leading-[130%] tracking-[0] text-[var(--color-content-default-primary)]",
|
||||||
"font-space-grotesk font-normal text-[20px] leading-[130%] tracking-[0] text-[var(--color-content-default-primary)]",
|
subtitle:
|
||||||
description:
|
"font-space-grotesk font-normal text-[20px] leading-[130%] tracking-[0] text-[var(--color-content-default-primary)]",
|
||||||
"font-inter font-normal text-[16px] leading-[140%] lg:text-[18px] lg:leading-[150%] xl:text-[20px] xl:leading-[160%] text-[var(--color-content-secondary)]",
|
description:
|
||||||
shape:
|
"font-inter font-normal text-[16px] leading-[140%] lg:text-[18px] lg:leading-[150%] xl:text-[20px] xl:leading-[160%] text-[var(--color-content-secondary)]",
|
||||||
"w-[20px] h-[20px] md:w-[24px] md:h-[24px] lg:w-[28px] lg:h-[28px]",
|
shape:
|
||||||
},
|
"w-[20px] h-[20px] md:w-[24px] md:h-[24px] lg:w-[28px] lg:h-[28px]",
|
||||||
learn: {
|
},
|
||||||
container:
|
learn: {
|
||||||
"flex flex-col gap-[var(--spacing-scale-012)] relative z-10 pt-[var(--spacing-scale-016)] pb-[var(--spacing-scale-016)] px-[var(--spacing-scale-020)] sm:pt-[var(--spacing-scale-040)] sm:pb-0 md:pt-[var(--spacing-scale-056)] md:px-[var(--spacing-scale-032)] lg:pt-[var(--spacing-scale-056)] lg:px-[var(--spacing-scale-064)]",
|
container:
|
||||||
textContainer:
|
"flex flex-col gap-[var(--spacing-scale-012)] relative z-10 pt-[var(--spacing-scale-016)] pb-[var(--spacing-scale-016)] px-[var(--spacing-scale-020)] sm:pt-[var(--spacing-scale-040)] sm:pb-0 md:pt-[var(--spacing-scale-056)] md:px-[var(--spacing-scale-032)] lg:pt-[var(--spacing-scale-056)] lg:px-[var(--spacing-scale-064)]",
|
||||||
"flex flex-col gap-[var(--spacing-scale-012)] md:gap-[var(--spacing-scale-016)]",
|
textContainer:
|
||||||
titleGroup:
|
"flex flex-col gap-[var(--spacing-scale-012)] md:gap-[var(--spacing-scale-016)]",
|
||||||
"flex flex-col gap-[var(--spacing-scale-012)] md:gap-[var(--spacing-scale-016)] lg:gap-[var(--spacing-scale-008)]",
|
titleGroup:
|
||||||
titleContainer: "flex gap-[var(--spacing-scale-008)] items-center",
|
"flex flex-col gap-[var(--spacing-scale-012)] md:gap-[var(--spacing-scale-016)] lg:gap-[var(--spacing-scale-008)]",
|
||||||
title:
|
titleContainer: "flex gap-[var(--spacing-scale-008)] items-center",
|
||||||
"font-bricolage-grotesque font-medium text-[28px] leading-[36px] tracking-[0] md:text-[44px] md:leading-[110%] lg:text-[52px] text-[var(--color-content-default-primary)]",
|
title:
|
||||||
subtitle:
|
"font-bricolage-grotesque font-medium text-[28px] leading-[36px] tracking-[0] md:text-[44px] md:leading-[110%] lg:text-[52px] text-[var(--color-content-default-primary)]",
|
||||||
"font-space-grotesk font-normal text-[16px] leading-[24px] tracking-[0] lg:text-[24px] lg:leading-[28px] text-[var(--color-content-default-primary)]",
|
subtitle:
|
||||||
description:
|
"font-space-grotesk font-normal text-[16px] leading-[24px] tracking-[0] lg:text-[24px] lg:leading-[28px] text-[var(--color-content-default-primary)]",
|
||||||
"font-inter font-normal text-[16px] leading-[140%] lg:text-[18px] lg:leading-[150%] xl:text-[20px] xl:leading-[160%] text-[var(--color-content-secondary)]",
|
description:
|
||||||
shape:
|
"font-inter font-normal text-[16px] leading-[140%] lg:text-[18px] lg:leading-[150%] xl:text-[20px] xl:leading-[160%] text-[var(--color-content-secondary)]",
|
||||||
"w-[20px] h-[20px] md:w-[24px] md:h-[24px] lg:w-[28px] lg:h-[28px]",
|
shape:
|
||||||
},
|
"w-[20px] h-[20px] md:w-[24px] md:h-[24px] lg:w-[28px] lg:h-[28px]",
|
||||||
ask: {
|
},
|
||||||
container: "flex flex-col gap-[var(--spacing-scale-008)] relative z-10",
|
ask: {
|
||||||
textContainer: "flex flex-col gap-[var(--spacing-scale-008)]",
|
container: "flex flex-col gap-[var(--spacing-scale-008)] relative z-10",
|
||||||
titleGroup: "flex flex-col gap-[var(--spacing-scale-008)]",
|
textContainer: "flex flex-col gap-[var(--spacing-scale-008)]",
|
||||||
titleContainer: "flex gap-[var(--spacing-scale-008)] items-center",
|
titleGroup: "flex flex-col gap-[var(--spacing-scale-008)]",
|
||||||
title:
|
titleContainer: "flex gap-[var(--spacing-scale-008)] items-center",
|
||||||
"font-bricolage-grotesque font-medium text-[36px] leading-[110%] tracking-[0] md:text-[44px] md:leading-[110%] xl:text-[52px] xl:leading-[110%] text-[var(--color-content-default-brand-primary)]",
|
title:
|
||||||
subtitle:
|
"font-bricolage-grotesque font-medium text-[36px] leading-[110%] tracking-[0] md:text-[44px] md:leading-[110%] xl:text-[52px] xl:leading-[110%] text-[var(--color-content-default-brand-primary)]",
|
||||||
"font-inter font-normal text-[18px] leading-[130%] tracking-[0] md:text-[24px] md:leading-[32px] text-[var(--color-content-default-primary)]",
|
subtitle:
|
||||||
shape:
|
"font-inter font-normal text-[18px] leading-[130%] tracking-[0] md:text-[24px] md:leading-[32px] text-[var(--color-content-default-primary)]",
|
||||||
"w-[16px] h-[16px] md:w-[20px] md:h-[20px] lg:w-[24px] lg:h-[24px]",
|
shape:
|
||||||
},
|
"w-[16px] h-[16px] md:w-[20px] md:h-[20px] lg:w-[24px] lg:h-[24px]",
|
||||||
"ask-inverse": {
|
},
|
||||||
container: "flex flex-col gap-[var(--spacing-scale-008)] relative z-10",
|
"ask-inverse": {
|
||||||
textContainer: "flex flex-col gap-[var(--spacing-scale-008)]",
|
container: "flex flex-col gap-[var(--spacing-scale-008)] relative z-10",
|
||||||
titleGroup: "flex flex-col gap-[var(--spacing-scale-008)]",
|
textContainer: "flex flex-col gap-[var(--spacing-scale-008)]",
|
||||||
titleContainer: "flex gap-[var(--spacing-scale-008)] items-center",
|
titleGroup: "flex flex-col gap-[var(--spacing-scale-008)]",
|
||||||
title:
|
titleContainer: "flex gap-[var(--spacing-scale-008)] items-center",
|
||||||
"font-bricolage-grotesque font-medium text-[36px] leading-[110%] tracking-[0] md:text-[44px] md:leading-[110%] xl:text-[52px] xl:leading-[110%] text-[var(--color-content-inverse-primary)]",
|
title:
|
||||||
subtitle:
|
"font-bricolage-grotesque font-medium text-[36px] leading-[110%] tracking-[0] md:text-[44px] md:leading-[110%] xl:text-[52px] xl:leading-[110%] text-[var(--color-content-inverse-primary)]",
|
||||||
"font-inter font-normal text-[18px] leading-[130%] tracking-[0] md:text-[24px] md:leading-[32px] text-[var(--color-content-inverse-primary)]",
|
subtitle:
|
||||||
shape:
|
"font-inter font-normal text-[18px] leading-[130%] tracking-[0] md:text-[24px] md:leading-[32px] text-[var(--color-content-inverse-primary)]",
|
||||||
"w-[16px] h-[16px] md:w-[20px] md:h-[20px] lg:w-[24px] lg:h-[24px]",
|
shape:
|
||||||
},
|
"w-[16px] h-[16px] md:w-[20px] md:h-[20px] lg:w-[24px] lg:h-[24px]",
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const styles = variantStyles[variant] || variantStyles.hero;
|
const styles = variantStyles[variant] || variantStyles.hero;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{variant === "ask" || variant === "ask-inverse" ? (
|
{variant === "ask" || variant === "ask-inverse" ? (
|
||||||
/* Simplified structure for ask variant */
|
/* Simplified structure for ask variant */
|
||||||
<div
|
|
||||||
className={`${styles.titleGroup} ${
|
|
||||||
alignment === "left" ? "text-left" : "text-center"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
className={`${styles.titleContainer} ${
|
className={`${styles.titleGroup} ${
|
||||||
alignment === "left" ? "justify-start" : "justify-center"
|
alignment === "left" ? "text-left" : "text-center"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<h1 className={styles.title}>{title}</h1>
|
<div
|
||||||
</div>
|
className={`${styles.titleContainer} ${
|
||||||
<h2 className={styles.subtitle}>{subtitle}</h2>
|
alignment === "left" ? "justify-start" : "justify-center"
|
||||||
</div>
|
}`}
|
||||||
) : (
|
>
|
||||||
/* Full structure for other variants */
|
|
||||||
<div className={styles.textContainer}>
|
|
||||||
{/* Title and subtitle group */}
|
|
||||||
<div className={styles.titleGroup}>
|
|
||||||
{/* Title container */}
|
|
||||||
<div className={styles.titleContainer}>
|
|
||||||
<h1 className={styles.title}>{title}</h1>
|
<h1 className={styles.title}>{title}</h1>
|
||||||
{variant === "hero" && (
|
|
||||||
<img
|
|
||||||
src={getAssetPath("assets/Shapes_1.svg")}
|
|
||||||
alt=""
|
|
||||||
className={styles.shape}
|
|
||||||
role="presentation"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Subtitle */}
|
|
||||||
<h2 className={styles.subtitle}>{subtitle}</h2>
|
<h2 className={styles.subtitle}>{subtitle}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Full structure for other variants */
|
||||||
|
<div className={styles.textContainer}>
|
||||||
|
{/* Title and subtitle group */}
|
||||||
|
<div className={styles.titleGroup}>
|
||||||
|
{/* Title container */}
|
||||||
|
<div className={styles.titleContainer}>
|
||||||
|
<h1 className={styles.title}>{title}</h1>
|
||||||
|
{variant === "hero" && (
|
||||||
|
<img
|
||||||
|
src={getAssetPath("assets/Shapes_1.svg")}
|
||||||
|
alt=""
|
||||||
|
className={styles.shape}
|
||||||
|
role="presentation"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
{/* Subtitle */}
|
||||||
{description && <p className={styles.description}>{description}</p>}
|
<h2 className={styles.subtitle}>{subtitle}</h2>
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Link for feature variant */}
|
{/* Description */}
|
||||||
{variant === "feature" && linkText && (
|
{description && <p className={styles.description}>{description}</p>}
|
||||||
<a
|
</div>
|
||||||
href={linkHref || "#"}
|
)}
|
||||||
className="font-inter font-medium text-[16px] leading-[20px] underline text-[var(--color-content-default-primary)] hover:text-gray-300 transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 focus:ring-offset-[#171717] rounded-sm px-1 py-0.5"
|
|
||||||
>
|
|
||||||
{linkText}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* CTA Button */}
|
{/* Link for feature variant */}
|
||||||
{ctaText && (
|
{variant === "feature" && linkText && (
|
||||||
<div className="flex justify-start">
|
<a
|
||||||
{/* Small button for xsm and sm breakpoints */}
|
href={linkHref || "#"}
|
||||||
<div className="block md:hidden">
|
className="font-inter font-medium text-[16px] leading-[20px] underline text-[var(--color-content-default-primary)] hover:text-gray-300 transition-colors focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 focus:ring-offset-[#171717] rounded-sm px-1 py-0.5"
|
||||||
<Button variant="primary" size="small">
|
>
|
||||||
{ctaText}
|
{linkText}
|
||||||
</Button>
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* CTA Button */}
|
||||||
|
{ctaText && (
|
||||||
|
<div className="flex justify-start">
|
||||||
|
{/* Small button for xsm and sm breakpoints */}
|
||||||
|
<div className="block md:hidden">
|
||||||
|
<Button variant="primary" size="small">
|
||||||
|
{ctaText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/* Large button for md and lg breakpoints */}
|
||||||
|
<div className="hidden md:block xl:hidden">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="large"
|
||||||
|
className={buttonClassName}
|
||||||
|
>
|
||||||
|
{ctaText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/* XLarge button for xl breakpoint */}
|
||||||
|
<div className="hidden xl:block">
|
||||||
|
<Button variant="primary" size="xlarge">
|
||||||
|
{ctaText}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Large button for md and lg breakpoints */}
|
)}
|
||||||
<div className="hidden md:block xl:hidden">
|
</div>
|
||||||
<Button variant="primary" size="large" className={buttonClassName}>
|
);
|
||||||
{ctaText}
|
}
|
||||||
</Button>
|
);
|
||||||
</div>
|
|
||||||
{/* XLarge button for xl breakpoint */}
|
ContentLockup.displayName = "ContentLockup";
|
||||||
<div className="hidden xl:block">
|
|
||||||
<Button variant="primary" size="xlarge">
|
|
||||||
{ctaText}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ContentLockup;
|
export default ContentLockup;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { memo } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import ContentContainer from "./ContentContainer";
|
import ContentContainer from "./ContentContainer";
|
||||||
import { getAssetPath, ASSETS } from "../../lib/assetUtils";
|
import { getAssetPath, ASSETS } from "../../lib/assetUtils";
|
||||||
@@ -9,87 +9,91 @@ import { getAssetPath, ASSETS } from "../../lib/assetUtils";
|
|||||||
* ContentThumbnailTemplate component for displaying blog post previews
|
* ContentThumbnailTemplate component for displaying blog post previews
|
||||||
* Simplified version to debug infinite loop
|
* Simplified version to debug infinite loop
|
||||||
*/
|
*/
|
||||||
const ContentThumbnailTemplate = ({
|
const ContentThumbnailTemplate = memo(
|
||||||
post,
|
({
|
||||||
className = "",
|
post,
|
||||||
variant = "vertical", // Internal prop for testing/development
|
className = "",
|
||||||
}) => {
|
variant = "vertical", // Internal prop for testing/development
|
||||||
// Get article-specific background image from frontmatter
|
}) => {
|
||||||
const getBackgroundImage = (post, variant) => {
|
// Get article-specific background image from frontmatter
|
||||||
// Check if post has thumbnail images defined in frontmatter
|
const getBackgroundImage = (post, variant) => {
|
||||||
if (post.frontmatter?.thumbnail) {
|
// Check if post has thumbnail images defined in frontmatter
|
||||||
const imageName =
|
if (post.frontmatter?.thumbnail) {
|
||||||
variant === "vertical"
|
const imageName =
|
||||||
? post.frontmatter.thumbnail.vertical
|
variant === "vertical"
|
||||||
: post.frontmatter.thumbnail.horizontal;
|
? post.frontmatter.thumbnail.vertical
|
||||||
|
: post.frontmatter.thumbnail.horizontal;
|
||||||
|
|
||||||
if (imageName) {
|
if (imageName) {
|
||||||
// Return path to image in public/content/blog directory
|
// Return path to image in public/content/blog directory
|
||||||
return `/content/blog/${imageName}`;
|
return `/content/blog/${imageName}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback to default images if no thumbnail specified
|
// Fallback to default images if no thumbnail specified
|
||||||
const fallbackImages = {
|
const fallbackImages = {
|
||||||
vertical: getAssetPath(ASSETS.VERTICAL_1),
|
vertical: getAssetPath(ASSETS.VERTICAL_1),
|
||||||
horizontal: getAssetPath(ASSETS.HORIZONTAL_1),
|
horizontal: getAssetPath(ASSETS.HORIZONTAL_1),
|
||||||
|
};
|
||||||
|
|
||||||
|
return fallbackImages[variant] || fallbackImages.vertical;
|
||||||
};
|
};
|
||||||
|
|
||||||
return fallbackImages[variant] || fallbackImages.vertical;
|
const backgroundImage = getBackgroundImage(post, variant);
|
||||||
};
|
|
||||||
|
|
||||||
const backgroundImage = getBackgroundImage(post, variant);
|
if (variant === "vertical") {
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
href={`/blog/${post.slug}`}
|
||||||
|
className={`block transition-transform duration-200 hover:scale-[1.02] ${className}`}
|
||||||
|
>
|
||||||
|
<div className="relative w-full aspect-[2/3] overflow-hidden pt-[18px] pl-[18px] pr-[42px] pb-[212px]">
|
||||||
|
{/* Background SVG - fills container with maintained aspect */}
|
||||||
|
<div className="absolute inset-0 z-0">
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={backgroundImage}
|
||||||
|
alt={`Background for ${post.frontmatter.title}`}
|
||||||
|
className="w-full h-full object-cover"
|
||||||
|
/>
|
||||||
|
{/* Gradient overlay for better text readability */}
|
||||||
|
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-black/60 z-10" />
|
||||||
|
</div>
|
||||||
|
|
||||||
if (variant === "vertical") {
|
{/* Content Section - positioned within the padding constraints */}
|
||||||
|
<ContentContainer post={post} width="200px" size="xs" />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Horizontal variant
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
href={`/blog/${post.slug}`}
|
href={`/blog/${post.slug}`}
|
||||||
className={`block transition-transform duration-200 hover:scale-[1.02] ${className}`}
|
className={`block transition-transform duration-200 hover:scale-[1.02] ${className}`}
|
||||||
>
|
>
|
||||||
<div className="relative w-full aspect-[2/3] overflow-hidden pt-[18px] pl-[18px] pr-[42px] pb-[212px]">
|
<div className="relative min-w-[320px] max-w-[800px] h-[225.5px] overflow-hidden pt-[13.75px] pr-[76px] pb-[73.75px] pl-[14px]">
|
||||||
{/* Background SVG - fills container with maintained aspect */}
|
{/* Background SVG - sized to fit the 320x225.5 container exactly */}
|
||||||
<div className="absolute inset-0 z-0">
|
<div className="absolute inset-0 z-0">
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
<img
|
<img
|
||||||
src={backgroundImage}
|
src={backgroundImage}
|
||||||
alt={`Background for ${post.frontmatter.title}`}
|
alt={`Background for ${post.frontmatter.title}`}
|
||||||
className="w-full h-full object-cover"
|
className="w-full h-[225.5px] object-cover"
|
||||||
/>
|
/>
|
||||||
{/* Gradient overlay for better text readability */}
|
{/* Gradient overlay */}
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-transparent to-black/60 z-10" />
|
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-transparent to-black/70 z-10" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content Section - positioned within the padding constraints */}
|
{/* Content - positioned within the padding constraints */}
|
||||||
<ContentContainer post={post} width="200px" size="xs" />
|
<ContentContainer post={post} width="230px" size="xs" />
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Horizontal variant
|
ContentThumbnailTemplate.displayName = "ContentThumbnailTemplate";
|
||||||
return (
|
|
||||||
<Link
|
|
||||||
href={`/blog/${post.slug}`}
|
|
||||||
className={`block transition-transform duration-200 hover:scale-[1.02] ${className}`}
|
|
||||||
>
|
|
||||||
<div className="relative min-w-[320px] max-w-[800px] h-[225.5px] overflow-hidden pt-[13.75px] pr-[76px] pb-[73.75px] pl-[14px]">
|
|
||||||
{/* Background SVG - sized to fit the 320x225.5 container exactly */}
|
|
||||||
<div className="absolute inset-0 z-0">
|
|
||||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
|
||||||
<img
|
|
||||||
src={backgroundImage}
|
|
||||||
alt={`Background for ${post.frontmatter.title}`}
|
|
||||||
className="w-full h-[225.5px] object-cover"
|
|
||||||
/>
|
|
||||||
{/* Gradient overlay */}
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-transparent to-black/70 z-10" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Content - positioned within the padding constraints */}
|
|
||||||
<ContentContainer post={post} width="230px" size="xs" />
|
|
||||||
</div>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ContentThumbnailTemplate;
|
export default ContentThumbnailTemplate;
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { Component } from "react";
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false, error: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(error) {
|
||||||
|
// Update state so the next render will show the fallback UI
|
||||||
|
return { hasError: true, error };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error, errorInfo) {
|
||||||
|
// Log the error to an error reporting service
|
||||||
|
console.error("ErrorBoundary caught an error:", error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
// Fallback UI using design tokens
|
||||||
|
return (
|
||||||
|
<div className="min-h-[200px] flex items-center justify-center p-[var(--spacing-scale-016)]">
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-xl font-semibold text-[var(--color-content-default-primary)] mb-[var(--spacing-scale-008)]">
|
||||||
|
Something went wrong
|
||||||
|
</h2>
|
||||||
|
<p className="text-[var(--color-content-default-secondary)] mb-[var(--spacing-scale-016)]">
|
||||||
|
We're sorry, but something unexpected happened.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => this.setState({ hasError: false, error: null })}
|
||||||
|
className="px-[var(--spacing-scale-016)] py-[var(--spacing-scale-008)] bg-[var(--color-surface-default-brand-royal)] text-[var(--color-content-inverse-primary)] rounded-[var(--radius-measures-radius-small)] hover:bg-[var(--color-surface-hover-brand-royal)] transition-colors"
|
||||||
|
>
|
||||||
|
Try again
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
|
|
||||||
@@ -1,11 +1,49 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { memo, useMemo } from "react";
|
||||||
import ContentLockup from "./ContentLockup";
|
import ContentLockup from "./ContentLockup";
|
||||||
import MiniCard from "./MiniCard";
|
import MiniCard from "./MiniCard";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
const FeatureGrid = ({ title, subtitle, className = "" }) => {
|
const FeatureGrid = memo(({ title, subtitle, className = "" }) => {
|
||||||
|
// Memoize the feature data to prevent unnecessary re-renders
|
||||||
|
const features = useMemo(
|
||||||
|
() => [
|
||||||
|
{
|
||||||
|
backgroundColor: "bg-[var(--color-surface-default-brand-royal)]",
|
||||||
|
labelLine1: "Decision-making",
|
||||||
|
labelLine2: "support",
|
||||||
|
panelContent: "/assets/Feature_Support.png",
|
||||||
|
ariaLabel: "Decision-making support tools",
|
||||||
|
href: "#decision-making",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
backgroundColor: "bg-[#D1FFE2]",
|
||||||
|
labelLine1: "Values alignment",
|
||||||
|
labelLine2: "exercises",
|
||||||
|
panelContent: "/assets/Feature_Exercises.png",
|
||||||
|
ariaLabel: "Values alignment exercises",
|
||||||
|
href: "#values-alignment",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
backgroundColor: "bg-[#F4CAFF]",
|
||||||
|
labelLine1: "Membership",
|
||||||
|
labelLine2: "guidance",
|
||||||
|
panelContent: "/assets/Feature_Guidance.png",
|
||||||
|
ariaLabel: "Membership guidance resources",
|
||||||
|
href: "#membership-guidance",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
backgroundColor: "bg-[#CBDDFF]",
|
||||||
|
labelLine1: "Conflict resolution",
|
||||||
|
labelLine2: "tools",
|
||||||
|
panelContent: "/assets/Feature_Tools.png",
|
||||||
|
ariaLabel: "Conflict resolution tools",
|
||||||
|
href: "#conflict-resolution",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[]
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
className={`p-0 lg:p-[var(--spacing-scale-064)] ${className}`}
|
className={`p-0 lg:p-[var(--spacing-scale-064)] ${className}`}
|
||||||
@@ -32,43 +70,24 @@ const FeatureGrid = ({ title, subtitle, className = "" }) => {
|
|||||||
role="grid"
|
role="grid"
|
||||||
aria-label="Feature tools and services"
|
aria-label="Feature tools and services"
|
||||||
>
|
>
|
||||||
<MiniCard
|
{features.map((feature, index) => (
|
||||||
backgroundColor="bg-[var(--color-surface-default-brand-royal)]"
|
<MiniCard
|
||||||
labelLine1="Decision-making"
|
key={index}
|
||||||
labelLine2="support"
|
backgroundColor={feature.backgroundColor}
|
||||||
panelContent="assets/Feature_Support.png"
|
labelLine1={feature.labelLine1}
|
||||||
ariaLabel="Decision-making support tools"
|
labelLine2={feature.labelLine2}
|
||||||
href="#decision-making"
|
panelContent={feature.panelContent}
|
||||||
/>
|
ariaLabel={feature.ariaLabel}
|
||||||
<MiniCard
|
href={feature.href}
|
||||||
backgroundColor="bg-[#D1FFE2]"
|
/>
|
||||||
labelLine1="Values alignment"
|
))}
|
||||||
labelLine2="exercises"
|
|
||||||
panelContent="assets/Feature_Exercises.png"
|
|
||||||
ariaLabel="Values alignment exercises"
|
|
||||||
href="#values-alignment"
|
|
||||||
/>
|
|
||||||
<MiniCard
|
|
||||||
backgroundColor="bg-[#F4CAFF]"
|
|
||||||
labelLine1="Membership"
|
|
||||||
labelLine2="guidance"
|
|
||||||
panelContent="assets/Feature_Guidance.png"
|
|
||||||
ariaLabel="Membership guidance resources"
|
|
||||||
href="#membership-guidance"
|
|
||||||
/>
|
|
||||||
<MiniCard
|
|
||||||
backgroundColor="bg-[#CBDDFF]"
|
|
||||||
labelLine1="Conflict resolution"
|
|
||||||
labelLine2="tools"
|
|
||||||
panelContent="assets/Feature_Tools.png"
|
|
||||||
ariaLabel="Conflict resolution tools"
|
|
||||||
href="#conflict-resolution"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
FeatureGrid.displayName = "FeatureGrid";
|
||||||
|
|
||||||
export default FeatureGrid;
|
export default FeatureGrid;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import React, { memo } from "react";
|
||||||
import Logo from "./Logo";
|
import Logo from "./Logo";
|
||||||
import Separator from "./Separator";
|
import Separator from "./Separator";
|
||||||
import { getAssetPath, ASSETS } from "../../lib/assetUtils";
|
import { getAssetPath, ASSETS } from "../../lib/assetUtils";
|
||||||
|
|
||||||
export default function Footer() {
|
const Footer = memo(() => {
|
||||||
// Schema markup for organization information
|
// Schema markup for organization information
|
||||||
const schemaData = {
|
const schemaData = {
|
||||||
"@context": "https://schema.org",
|
"@context": "https://schema.org",
|
||||||
@@ -155,4 +156,8 @@ export default function Footer() {
|
|||||||
</footer>
|
</footer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
Footer.displayName = "Footer";
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import React, { memo } from "react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import Logo from "./Logo";
|
import Logo from "./Logo";
|
||||||
import MenuBar from "./MenuBar";
|
import MenuBar from "./MenuBar";
|
||||||
@@ -38,7 +39,7 @@ export const logoConfig = [
|
|||||||
{ breakpoint: "hidden xl:block", size: "headerXl", showText: true },
|
{ breakpoint: "hidden xl:block", size: "headerXl", showText: true },
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Header() {
|
const Header = memo(() => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
// Schema markup for site navigation
|
// Schema markup for site navigation
|
||||||
@@ -214,4 +215,8 @@ export default function Header() {
|
|||||||
</header>
|
</header>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
Header.displayName = "Header";
|
||||||
|
|
||||||
|
export default Header;
|
||||||
|
|||||||
+38
-36
@@ -1,39 +1,41 @@
|
|||||||
|
import React, { memo } from "react";
|
||||||
import { getAssetPath } from "../../lib/assetUtils";
|
import { getAssetPath } from "../../lib/assetUtils";
|
||||||
|
|
||||||
export default function HeaderTab({
|
const HeaderTab = memo(
|
||||||
children,
|
({ children, className = "", stretch = false, ...props }) => {
|
||||||
className = "",
|
const stretchClasses = stretch
|
||||||
stretch = false,
|
? "flex-1 sm:mr-[var(--spacing-scale-008)] md:mr-[185px] lg:mr-[var(--spacing-scale-024)] xl:mr-[var(--spacing-scale-032)]"
|
||||||
...props
|
: "";
|
||||||
}) {
|
|
||||||
const stretchClasses = stretch
|
|
||||||
? "flex-1 sm:mr-[var(--spacing-scale-008)] md:mr-[185px] lg:mr-[var(--spacing-scale-024)] xl:mr-[var(--spacing-scale-032)]"
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`HeaderTab header-breakpoint-transition relative bg-[var(--color-surface-inverse-brand-primary)] rounded-t-[32px] sm:rounded-t-[32px] md:rounded-t-[32px] lg:rounded-t-[32px] xl:rounded-t-[32px] pl-[var(--spacing-scale-012)] h-[40px] sm:h-[52px] md:h-[52px] lg:h-[52px] xl:h-[64px] sm:pr-[var(--spacing-scale-006)] md:pl-[var(--spacing-scale-024)] lg:pl-[var(--spacing-scale-024)] xl:pl-[var(--spacing-scale-032)] md:pr-[var(--spacing-scale-012)] lg:pr-[var(--spacing-scale-048)] xl:pr-[var(--spacing-scale-120)] md:gap-[var(--spacing-scale-032)] ${stretchClasses} ${className}`}
|
className={`HeaderTab header-breakpoint-transition relative bg-[var(--color-surface-inverse-brand-primary)] rounded-t-[32px] sm:rounded-t-[32px] md:rounded-t-[32px] lg:rounded-t-[32px] xl:rounded-t-[32px] pl-[var(--spacing-scale-012)] h-[40px] sm:h-[52px] md:h-[52px] lg:h-[52px] xl:h-[64px] sm:pr-[var(--spacing-scale-006)] md:pl-[var(--spacing-scale-024)] lg:pl-[var(--spacing-scale-024)] xl:pl-[var(--spacing-scale-032)] md:pr-[var(--spacing-scale-012)] lg:pr-[var(--spacing-scale-048)] xl:pr-[var(--spacing-scale-120)] md:gap-[var(--spacing-scale-032)] ${stretchClasses} ${className}`}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<img
|
<img
|
||||||
src={getAssetPath("assets/Union_xsm.svg")}
|
src={getAssetPath("assets/Union_xsm.svg")}
|
||||||
alt=""
|
alt=""
|
||||||
role="presentation"
|
role="presentation"
|
||||||
className="absolute -bottom-[3px] -right-[52px] w-[61px] h-[24px] sm:w-[61px] sm:h-[31.5px] sm:hidden -z-10"
|
className="absolute -bottom-[3px] -right-[52px] w-[61px] h-[24px] sm:w-[61px] sm:h-[31.5px] sm:hidden -z-10"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src={getAssetPath("assets/Union_sm_md_lg.svg")}
|
src={getAssetPath("assets/Union_sm_md_lg.svg")}
|
||||||
alt=""
|
alt=""
|
||||||
role="presentation"
|
role="presentation"
|
||||||
className="absolute -bottom-[3.7px] -right-[53px] w-[61px] h-[24px] sm:w-[61px] sm:h-[31.5px] hidden sm:block xl:hidden -z-10"
|
className="absolute -bottom-[3.7px] -right-[53px] w-[61px] h-[24px] sm:w-[61px] sm:h-[31.5px] hidden sm:block xl:hidden -z-10"
|
||||||
/>
|
/>
|
||||||
<img
|
<img
|
||||||
src={getAssetPath("assets/Union_xlg.svg")}
|
src={getAssetPath("assets/Union_xlg.svg")}
|
||||||
alt=""
|
alt=""
|
||||||
role="presentation"
|
role="presentation"
|
||||||
className="absolute -bottom-[6px] -right-[94px] w-[105px] h-[53px] hidden xl:block -z-10"
|
className="absolute -bottom-[6px] -right-[94px] w-[105px] h-[53px] hidden xl:block -z-10"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
HeaderTab.displayName = "HeaderTab";
|
||||||
|
|
||||||
|
export default HeaderTab;
|
||||||
|
|||||||
@@ -1,47 +1,54 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import React, { memo } from "react";
|
||||||
import ContentLockup from "./ContentLockup";
|
import ContentLockup from "./ContentLockup";
|
||||||
import HeroDecor from "./HeroDecor";
|
import HeroDecor from "./HeroDecor";
|
||||||
import { getAssetPath } from "../../lib/assetUtils";
|
import { getAssetPath } from "../../lib/assetUtils";
|
||||||
|
|
||||||
const HeroBanner = ({ title, subtitle, description, ctaText, ctaHref }) => {
|
const HeroBanner = memo(
|
||||||
return (
|
({ title, subtitle, description, ctaText, ctaHref }) => {
|
||||||
<section className="bg-transparent px-[var(--spacing-scale-008)] sm:px-[var(--spacing-scale-010)] md:px-[var(--spacing-scale-016)] lg:px-[var(--spacing-scale-024)] xl:px-[var(--spacing-scale-048)]">
|
return (
|
||||||
<div className="flex flex-col gap-[var(--spacing-scale-010)]">
|
<section className="bg-transparent px-[var(--spacing-scale-008)] sm:px-[var(--spacing-scale-010)] md:px-[var(--spacing-scale-016)] lg:px-[var(--spacing-scale-024)] xl:px-[var(--spacing-scale-048)]">
|
||||||
{/* Frame container for content */}
|
<div className="flex flex-col gap-[var(--spacing-scale-010)]">
|
||||||
<div className="bg-[var(--color-surface-inverse-brand-primary)] p-[var(--spacing-scale-012)] sm:p-[var(--spacing-scale-016)] md:p-[var(--spacing-scale-064)] lg:py-[var(--spacing-scale-096)] lg:px-[var(--spacing-scale-064)] rounded-tl-none rounded-tr-[var(--radius-measures-radius-medium)] rounded-br-[var(--radius-measures-radius-medium)] rounded-bl-[var(--radius-measures-radius-medium)] flex flex-col gap-[var(--spacing-scale-024)] sm:gap-[var(--spacing-scale-024)] md:flex-row md:gap-[var(--spacing-scale-048)] relative overflow-hidden">
|
{/* Frame container for content */}
|
||||||
{/* DECORATIONS (behind content) */}
|
<div className="bg-[var(--color-surface-inverse-brand-primary)] p-[var(--spacing-scale-012)] sm:p-[var(--spacing-scale-016)] md:p-[var(--spacing-scale-064)] lg:py-[var(--spacing-scale-096)] lg:px-[var(--spacing-scale-064)] rounded-tl-none rounded-tr-[var(--radius-measures-radius-medium)] rounded-br-[var(--radius-measures-radius-medium)] rounded-bl-[var(--radius-measures-radius-medium)] flex flex-col gap-[var(--spacing-scale-024)] sm:gap-[var(--spacing-scale-024)] md:flex-row md:gap-[var(--spacing-scale-048)] relative overflow-hidden">
|
||||||
<HeroDecor
|
{/* DECORATIONS (behind content) */}
|
||||||
className="pointer-events-none absolute z-0
|
<HeroDecor
|
||||||
|
className="pointer-events-none absolute z-0
|
||||||
left-0 top-0
|
left-0 top-0
|
||||||
translate-x-[-72px] translate-y-[26px] sm:translate-x-[-78px] sm:translate-y-[24px] md:translate-x-[-86px] md:translate-y-[16px] lg:translate-x-[-88px] lg:translate-y-[16px]
|
translate-x-[-72px] translate-y-[26px] sm:translate-x-[-78px] sm:translate-y-[24px] md:translate-x-[-86px] md:translate-y-[16px] lg:translate-x-[-88px] lg:translate-y-[16px]
|
||||||
w-[1540px] h-[645px] scale-[1.04]"
|
w-[1540px] h-[645px] scale-[1.04]"
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Content lockup - Large variant */}
|
|
||||||
<div className="md:flex-1">
|
|
||||||
<ContentLockup
|
|
||||||
title={title}
|
|
||||||
subtitle={subtitle}
|
|
||||||
description={description}
|
|
||||||
ctaText={ctaText}
|
|
||||||
ctaHref={ctaHref}
|
|
||||||
buttonClassName="shrink-0 whitespace-nowrap min-w-[280px]"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Hero Image Container */}
|
{/* Content lockup - Large variant */}
|
||||||
<div className="w-full h-full md:flex-1 rounded-[8px] overflow-hidden relative z-10 flex items-center justify-center">
|
<div className="md:flex-1">
|
||||||
<img
|
<ContentLockup
|
||||||
src={getAssetPath("assets/HeroImage.png")}
|
title={title}
|
||||||
alt="Hero illustration"
|
subtitle={subtitle}
|
||||||
className="w-full h-auto"
|
description={description}
|
||||||
/>
|
ctaText={ctaText}
|
||||||
|
ctaHref={ctaHref}
|
||||||
|
buttonClassName="shrink-0 whitespace-nowrap min-w-[280px]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Hero Image Container */}
|
||||||
|
<div className="w-full h-full md:flex-1 rounded-[8px] overflow-hidden relative z-10 flex items-center justify-center">
|
||||||
|
<img
|
||||||
|
src={getAssetPath("assets/HeroImage.png")}
|
||||||
|
alt="Hero illustration"
|
||||||
|
className="w-full h-auto"
|
||||||
|
loading="eager"
|
||||||
|
fetchPriority="high"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
);
|
||||||
);
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
|
HeroBanner.displayName = "HeroBanner";
|
||||||
|
|
||||||
export default HeroBanner;
|
export default HeroBanner;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
const HeroDecor = ({ className = "" }) => {
|
import React, { memo } from "react";
|
||||||
|
|
||||||
|
const HeroDecor = memo(({ className = "" }) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className={`text-[var(--color-surface-default-brand-lighter-accent)] opacity-50 ${className}`}
|
className={`text-[var(--color-surface-default-brand-lighter-accent)] opacity-50 ${className}`}
|
||||||
@@ -65,6 +67,8 @@ const HeroDecor = ({ className = "" }) => {
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
HeroDecor.displayName = "HeroDecor";
|
||||||
|
|
||||||
export default HeroDecor;
|
export default HeroDecor;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import React, { memo } from "react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import Logo from "./Logo";
|
import Logo from "./Logo";
|
||||||
import MenuBar from "./MenuBar";
|
import MenuBar from "./MenuBar";
|
||||||
@@ -9,7 +10,7 @@ import AvatarContainer from "./AvatarContainer";
|
|||||||
import Avatar from "./Avatar";
|
import Avatar from "./Avatar";
|
||||||
import HeaderTab from "./HeaderTab";
|
import HeaderTab from "./HeaderTab";
|
||||||
|
|
||||||
export default function HomeHeader() {
|
const HomeHeader = memo(() => {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
// Schema markup for site navigation (home page specific)
|
// Schema markup for site navigation (home page specific)
|
||||||
@@ -33,9 +34,9 @@ export default function HomeHeader() {
|
|||||||
];
|
];
|
||||||
|
|
||||||
const avatarImages = [
|
const avatarImages = [
|
||||||
{ src: "assets/Avatar_1.png", alt: "Avatar 1" },
|
{ src: "/assets/Avatar_1.png", alt: "Avatar 1" },
|
||||||
{ src: "assets/Avatar_2.png", alt: "Avatar 2" },
|
{ src: "/assets/Avatar_2.png", alt: "Avatar 2" },
|
||||||
{ src: "assets/Avatar_3.png", alt: "Avatar 3" },
|
{ src: "/assets/Avatar_3.png", alt: "Avatar 3" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const logoConfig = [
|
const logoConfig = [
|
||||||
@@ -78,10 +79,10 @@ export default function HomeHeader() {
|
|||||||
? size === "home" || size === "homeMd"
|
? size === "home" || size === "homeMd"
|
||||||
? "homeMd"
|
? "homeMd"
|
||||||
: size === "large"
|
: size === "large"
|
||||||
? "large"
|
? "large"
|
||||||
: size === "homeXlarge"
|
: size === "homeXlarge"
|
||||||
? "homeXlarge"
|
? "homeXlarge"
|
||||||
: "xsmallUseCases"
|
: "xsmallUseCases"
|
||||||
: size
|
: size
|
||||||
}
|
}
|
||||||
variant={
|
variant={
|
||||||
@@ -241,4 +242,8 @@ export default function HomeHeader() {
|
|||||||
</header>
|
</header>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
HomeHeader.displayName = "HomeHeader";
|
||||||
|
|
||||||
|
export default HomeHeader;
|
||||||
|
|||||||
@@ -1,37 +1,41 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { memo } from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple image placeholder component for testing
|
* Simple image placeholder component for testing
|
||||||
* Generates colored backgrounds with text overlays
|
* Generates colored backgrounds with text overlays
|
||||||
*/
|
*/
|
||||||
const ImagePlaceholder = ({
|
const ImagePlaceholder = memo(
|
||||||
width = 260,
|
({
|
||||||
height = 390,
|
width = 260,
|
||||||
text = "Blog Image",
|
height = 390,
|
||||||
color = "blue",
|
text = "Blog Image",
|
||||||
className = "",
|
color = "blue",
|
||||||
}) => {
|
className = "",
|
||||||
const colors = {
|
}) => {
|
||||||
blue: "bg-blue-500",
|
const colors = {
|
||||||
green: "bg-green-500",
|
blue: "bg-blue-500",
|
||||||
purple: "bg-purple-500",
|
green: "bg-green-500",
|
||||||
red: "bg-red-500",
|
purple: "bg-purple-500",
|
||||||
orange: "bg-orange-500",
|
red: "bg-red-500",
|
||||||
teal: "bg-teal-500",
|
orange: "bg-orange-500",
|
||||||
};
|
teal: "bg-teal-500",
|
||||||
|
};
|
||||||
|
|
||||||
const bgColor = colors[color] || colors.blue;
|
const bgColor = colors[color] || colors.blue;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`${bgColor} flex items-center justify-center text-white font-bold text-lg ${className}`}
|
className={`${bgColor} flex items-center justify-center text-white font-bold text-lg ${className}`}
|
||||||
style={{ width: `${width}px`, height: `${height}px` }}
|
style={{ width: `${width}px`, height: `${height}px` }}
|
||||||
>
|
>
|
||||||
{text}
|
{text}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
ImagePlaceholder.displayName = "ImagePlaceholder";
|
||||||
|
|
||||||
export default ImagePlaceholder;
|
export default ImagePlaceholder;
|
||||||
|
|||||||
+27
-22
@@ -1,7 +1,8 @@
|
|||||||
|
import React, { memo } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getAssetPath, ASSETS } from "../../lib/assetUtils";
|
import { getAssetPath, ASSETS } from "../../lib/assetUtils";
|
||||||
|
|
||||||
export default function Logo({ size = "default", showText = true }) {
|
const Logo = memo(({ size = "default", showText = true }) => {
|
||||||
// Size configurations
|
// Size configurations
|
||||||
const sizes = {
|
const sizes = {
|
||||||
default: {
|
default: {
|
||||||
@@ -94,26 +95,26 @@ export default function Logo({ size = "default", showText = true }) {
|
|||||||
size === "homeHeaderXsmall"
|
size === "homeHeaderXsmall"
|
||||||
? sizes.homeHeaderXsmall
|
? sizes.homeHeaderXsmall
|
||||||
: size === "homeHeaderSm"
|
: size === "homeHeaderSm"
|
||||||
? sizes.homeHeaderSm
|
? sizes.homeHeaderSm
|
||||||
: size === "homeHeaderMd"
|
: size === "homeHeaderMd"
|
||||||
? sizes.homeHeaderMd
|
? sizes.homeHeaderMd
|
||||||
: size === "homeHeaderLg"
|
: size === "homeHeaderLg"
|
||||||
? sizes.homeHeaderLg
|
? sizes.homeHeaderLg
|
||||||
: size === "homeHeaderXl"
|
: size === "homeHeaderXl"
|
||||||
? sizes.homeHeaderXl
|
? sizes.homeHeaderXl
|
||||||
: size === "header"
|
: size === "header"
|
||||||
? sizes.header
|
? sizes.header
|
||||||
: size === "headerMd"
|
: size === "headerMd"
|
||||||
? sizes.headerMd
|
? sizes.headerMd
|
||||||
: size === "headerLg"
|
: size === "headerLg"
|
||||||
? sizes.headerLg
|
? sizes.headerLg
|
||||||
: size === "headerXl"
|
: size === "headerXl"
|
||||||
? sizes.headerXl
|
? sizes.headerXl
|
||||||
: size === "footer"
|
: size === "footer"
|
||||||
? sizes.footer
|
? sizes.footer
|
||||||
: size === "footerLg"
|
: size === "footerLg"
|
||||||
? sizes.footerLg
|
? sizes.footerLg
|
||||||
: sizes.default;
|
: sizes.default;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Link href="/" className="block" aria-label="CommunityRule Logo">
|
<Link href="/" className="block" aria-label="CommunityRule Logo">
|
||||||
@@ -165,4 +166,8 @@ export default function Logo({ size = "default", showText = true }) {
|
|||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
Logo.displayName = "Logo";
|
||||||
|
|
||||||
|
export default Logo;
|
||||||
|
|||||||
@@ -1,45 +1,45 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import React, { useState, useEffect, memo } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
const LogoWall = ({ logos = [] }) => {
|
const LogoWall = memo(({ logos = [] }) => {
|
||||||
const [isVisible, setIsVisible] = useState(false);
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
|
||||||
// Default logos if none provided - ordered for mobile (3 rows × 2 columns)
|
// Default logos if none provided - ordered for mobile (3 rows × 2 columns)
|
||||||
const defaultLogos = [
|
const defaultLogos = [
|
||||||
{
|
{
|
||||||
src: "assets/Section/Logo_FoodNotBombs.png",
|
src: "/assets/Section/Logo_FoodNotBombs.png",
|
||||||
alt: "Food Not Bombs",
|
alt: "Food Not Bombs",
|
||||||
size: "h-11 lg:h-14 xl:h-[70px]",
|
size: "h-11 lg:h-14 xl:h-[70px]",
|
||||||
order: "order-1 sm:order-4", // Mobile: row 1 col 1, SM: row 2 col 1 (bottom left)
|
order: "order-1 sm:order-4", // Mobile: row 1 col 1, SM: row 2 col 1 (bottom left)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "assets/Section/Logo_StartCOOP.png",
|
src: "/assets/Section/Logo_StartCOOP.png",
|
||||||
alt: "Start COOP",
|
alt: "Start COOP",
|
||||||
size: "h-[42px] lg:h-[53px] xl:h-[66px]",
|
size: "h-[42px] lg:h-[53px] xl:h-[66px]",
|
||||||
order: "order-2 sm:order-2", // Mobile: row 1 col 2, SM: row 1 col 2 (top middle)
|
order: "order-2 sm:order-2", // Mobile: row 1 col 2, SM: row 1 col 2 (top middle)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "assets/Section/Logo_Metagov.png",
|
src: "/assets/Section/Logo_Metagov.png",
|
||||||
alt: "Metagov",
|
alt: "Metagov",
|
||||||
size: "h-6 lg:h-8 xl:h-[41px]",
|
size: "h-6 lg:h-8 xl:h-[41px]",
|
||||||
order: "order-3 sm:order-1", // Mobile: row 2 col 1, SM: row 1 col 1 (top left)
|
order: "order-3 sm:order-1", // Mobile: row 2 col 1, SM: row 1 col 1 (top left)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "assets/Section/Logo_OpenCivics.png",
|
src: "/assets/Section/Logo_OpenCivics.png",
|
||||||
alt: "Open Civics",
|
alt: "Open Civics",
|
||||||
size: "h-8 lg:h-10 xl:h-[50px]",
|
size: "h-8 lg:h-10 xl:h-[50px]",
|
||||||
order: "order-4 sm:order-5 md:order-6", // Mobile: row 2 col 2, SM: row 2 col 2, MD: swapped with Mutual Aid CO
|
order: "order-4 sm:order-5 md:order-6", // Mobile: row 2 col 2, SM: row 2 col 2, MD: swapped with Mutual Aid CO
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "assets/Section/Logo_MutualAidCO.png",
|
src: "/assets/Section/Logo_MutualAidCO.png",
|
||||||
alt: "Mutual Aid CO",
|
alt: "Mutual Aid CO",
|
||||||
size: "h-11 lg:h-14 xl:h-[70px]",
|
size: "h-11 lg:h-14 xl:h-[70px]",
|
||||||
order: "order-5 sm:order-6 md:order-5", // Mobile: row 3 col 1, SM: row 2 col 3, MD: swapped with OpenCivics
|
order: "order-5 sm:order-6 md:order-5", // Mobile: row 3 col 1, SM: row 2 col 3, MD: swapped with OpenCivics
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
src: "assets/Section/Logo_CUBoulder.png",
|
src: "/assets/Section/Logo_CUBoulder.png",
|
||||||
alt: "CU Boulder",
|
alt: "CU Boulder",
|
||||||
size: "h-10 lg:h-12 xl:h-[60px]",
|
size: "h-10 lg:h-12 xl:h-[60px]",
|
||||||
order: "order-6 sm:order-3", // Mobile: row 3 col 2, SM: row 1 col 3 (top right)
|
order: "order-6 sm:order-3", // Mobile: row 3 col 2, SM: row 1 col 3 (top right)
|
||||||
@@ -98,6 +98,8 @@ const LogoWall = ({ logos = [] }) => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
LogoWall.displayName = "LogoWall";
|
||||||
|
|
||||||
export default LogoWall;
|
export default LogoWall;
|
||||||
|
|||||||
+31
-28
@@ -1,30 +1,33 @@
|
|||||||
export default function MenuBar({
|
import React, { memo } from "react";
|
||||||
children,
|
|
||||||
className = "",
|
|
||||||
size = "default",
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
const sizeStyles = {
|
|
||||||
xsmall:
|
|
||||||
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)] rounded-[4px]",
|
|
||||||
default:
|
|
||||||
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)]",
|
|
||||||
medium:
|
|
||||||
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-004)]",
|
|
||||||
large:
|
|
||||||
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-012)]",
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseStyles = `flex items-center ${sizeStyles[size]} ${className}`;
|
const MenuBar = memo(
|
||||||
|
({ children, className = "", size = "default", ...props }) => {
|
||||||
|
const sizeStyles = {
|
||||||
|
xsmall:
|
||||||
|
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)] rounded-[4px]",
|
||||||
|
default:
|
||||||
|
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-001)]",
|
||||||
|
medium:
|
||||||
|
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-004)]",
|
||||||
|
large:
|
||||||
|
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-004)] gap-[var(--spacing-scale-012)]",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const baseStyles = `flex items-center ${sizeStyles[size]} ${className}`;
|
||||||
<nav
|
|
||||||
className={baseStyles}
|
return (
|
||||||
role="menubar"
|
<nav
|
||||||
aria-label="Main navigation menu"
|
className={baseStyles}
|
||||||
{...props}
|
role="menubar"
|
||||||
>
|
aria-label="Main navigation menu"
|
||||||
{children}
|
{...props}
|
||||||
</nav>
|
>
|
||||||
);
|
{children}
|
||||||
}
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
MenuBar.displayName = "MenuBar";
|
||||||
|
|
||||||
|
export default MenuBar;
|
||||||
|
|||||||
+150
-142
@@ -1,158 +1,166 @@
|
|||||||
export default function MenuBarItem({
|
import React, { memo } from "react";
|
||||||
href = "#",
|
|
||||||
children,
|
|
||||||
variant = "default",
|
|
||||||
size = "default",
|
|
||||||
className = "",
|
|
||||||
disabled = false,
|
|
||||||
isActive = false,
|
|
||||||
ariaLabel,
|
|
||||||
...props
|
|
||||||
}) {
|
|
||||||
const variantStyles = {
|
|
||||||
default:
|
|
||||||
"bg-transparent text-[var(--color-content-default-brand-primary)] hover:bg-[var(--color-surface-default-tertiary)] hover:text-[var(--color-content-default-brand-primary)] hover:scale-[1.02] active:bg-transparent active:text-[var(--color-content-default-brand-primary)] active:scale-[0.98] disabled:bg-[var(--color-surface-default-tertiary)] disabled:text-[var(--color-content-default-tertiary)] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 disabled:active:scale-100",
|
|
||||||
home: "bg-transparent text-[var(--color-content-inverse-primary)] hover:bg-[var(--color-content-default-brand-accent)] hover:text-[var(--color-content-inverse-primary)] hover:scale-[1.02] active:bg-transparent active:text-[var(--color-content-inverse-primary)] active:scale-[0.98] disabled:bg-[var(--color-surface-default-tertiary)] disabled:text-[var(--color-content-default-tertiary)] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 disabled:active:scale-100",
|
|
||||||
};
|
|
||||||
|
|
||||||
const activeOutlineStyles = {
|
const MenuBarItem = memo(
|
||||||
xsmall:
|
({
|
||||||
"active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]",
|
href = "#",
|
||||||
xsmallUseCases:
|
children,
|
||||||
"active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]",
|
variant = "default",
|
||||||
default:
|
size = "default",
|
||||||
"active:outline-1 active:outline-[var(--color-content-default-brand-primary)] focus:outline-1 focus:outline-[var(--color-content-default-brand-primary)]",
|
className = "",
|
||||||
homeMd:
|
disabled = false,
|
||||||
"active:outline-[1.5px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-brand-primary)]",
|
isActive = false,
|
||||||
homeUseCases:
|
ariaLabel,
|
||||||
"active:outline-[1.5px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-brand-primary)]",
|
...props
|
||||||
large:
|
}) => {
|
||||||
"active:outline-[1.75px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-brand-primary)]",
|
const variantStyles = {
|
||||||
largeUseCases:
|
default:
|
||||||
"active:outline-[1.75px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-brand-primary)]",
|
"bg-transparent text-[var(--color-content-default-brand-primary)] hover:bg-[var(--color-surface-default-tertiary)] hover:text-[var(--color-content-default-brand-primary)] hover:scale-[1.02] active:bg-transparent active:text-[var(--color-content-default-brand-primary)] active:scale-[0.98] disabled:bg-[var(--color-surface-default-tertiary)] disabled:text-[var(--color-content-default-tertiary)] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 disabled:active:scale-100",
|
||||||
homeXlarge:
|
home: "bg-transparent text-[var(--color-content-inverse-primary)] hover:bg-[var(--color-content-default-brand-accent)] hover:text-[var(--color-content-inverse-primary)] hover:scale-[1.02] active:bg-transparent active:text-[var(--color-content-inverse-primary)] active:scale-[0.98] disabled:bg-[var(--color-surface-default-tertiary)] disabled:text-[var(--color-content-default-tertiary)] disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100 disabled:active:scale-100",
|
||||||
"active:outline-[2px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[2px] focus:outline-[var(--color-content-default-brand-primary)]",
|
};
|
||||||
xlarge:
|
|
||||||
"active:outline-2 active:outline-[var(--color-content-default-brand-primary)] focus:outline-2 focus:outline-[var(--color-content-default-brand-primary)]",
|
|
||||||
};
|
|
||||||
|
|
||||||
const homeOutlineStyles = {
|
const activeOutlineStyles = {
|
||||||
xsmall:
|
xsmall:
|
||||||
"active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]",
|
"active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]",
|
||||||
xsmallUseCases:
|
xsmallUseCases:
|
||||||
"active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]",
|
"active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]",
|
||||||
default:
|
default:
|
||||||
"active:outline-[1.5px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-primary)]",
|
"active:outline-1 active:outline-[var(--color-content-default-brand-primary)] focus:outline-1 focus:outline-[var(--color-content-default-brand-primary)]",
|
||||||
homeMd:
|
homeMd:
|
||||||
"active:outline-[1.5px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-primary)]",
|
"active:outline-[1.5px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-brand-primary)]",
|
||||||
homeUseCases:
|
homeUseCases:
|
||||||
"active:outline-[1.5px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-primary)]",
|
"active:outline-[1.5px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-brand-primary)]",
|
||||||
largeUseCases:
|
large:
|
||||||
"active:outline-[1.75px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-primary)]",
|
"active:outline-[1.75px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-brand-primary)]",
|
||||||
large:
|
largeUseCases:
|
||||||
"active:outline-[1.75px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-primary)]",
|
"active:outline-[1.75px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-brand-primary)]",
|
||||||
homeXlarge:
|
homeXlarge:
|
||||||
"active:outline-[2px] active:outline-[var(--color-content-default-primary)] focus:outline-[2px] focus:outline-[var(--color-content-default-primary)]",
|
"active:outline-[2px] active:outline-[var(--color-content-default-brand-primary)] focus:outline-[2px] focus:outline-[var(--color-content-default-brand-primary)]",
|
||||||
xlarge:
|
xlarge:
|
||||||
"active:outline-2 active:outline-[var(--color-content-default-primary)] focus:outline-2 focus:outline-[var(--color-content-default-primary)]",
|
"active:outline-2 active:outline-[var(--color-content-default-brand-primary)] focus:outline-2 focus:outline-[var(--color-content-default-brand-primary)]",
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeStateStyles = {
|
const homeOutlineStyles = {
|
||||||
xsmall:
|
xsmall:
|
||||||
"!outline-1 !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-1 focus:!outline-[var(--color-content-default-brand-primary)]",
|
"active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]",
|
||||||
xsmallUseCases:
|
xsmallUseCases:
|
||||||
"!outline-1 !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-1 focus:!outline-[var(--color-content-default-brand-primary)]",
|
"active:outline-1 active:outline-[var(--color-content-default-primary)] focus:outline-1 focus:outline-[var(--color-content-default-primary)]",
|
||||||
default:
|
default:
|
||||||
"!outline-[1.5px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.5px] focus:!outline-[var(--color-content-default-brand-primary)]",
|
"active:outline-[1.5px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-primary)]",
|
||||||
homeMd:
|
homeMd:
|
||||||
"!outline-[1.5px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.5px] focus:!outline-[var(--color-content-default-brand-primary)]",
|
"active:outline-[1.5px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-primary)]",
|
||||||
homeUseCases:
|
homeUseCases:
|
||||||
"!outline-[1.5px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.5px] focus:!outline-[var(--color-content-default-brand-primary)]",
|
"active:outline-[1.5px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.5px] focus:outline-[var(--color-content-default-primary)]",
|
||||||
large:
|
largeUseCases:
|
||||||
"!outline-[1.75px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.75px] focus:!outline-[var(--color-content-default-brand-primary)]",
|
"active:outline-[1.75px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-primary)]",
|
||||||
largeUseCases:
|
large:
|
||||||
"!outline-[1.75px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.75px] focus:!outline-[var(--color-content-default-brand-primary)]",
|
"active:outline-[1.75px] active:outline-[var(--color-content-default-primary)] focus:outline-[1.75px] focus:outline-[var(--color-content-default-primary)]",
|
||||||
homeXlarge:
|
homeXlarge:
|
||||||
"!outline-[2px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[2px] focus:!outline-[var(--color-content-default-brand-primary)]",
|
"active:outline-[2px] active:outline-[var(--color-content-default-primary)] focus:outline-[2px] focus:outline-[var(--color-content-default-primary)]",
|
||||||
xlarge:
|
xlarge:
|
||||||
"!outline-2 !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-2 focus:!outline-[var(--color-content-default-brand-primary)]",
|
"active:outline-2 active:outline-[var(--color-content-default-primary)] focus:outline-2 focus:outline-[var(--color-content-default-primary)]",
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeStyles = {
|
const activeStateStyles = {
|
||||||
default:
|
xsmall:
|
||||||
"px-[var(--spacing-measures-spacing-016)] py-[var(--spacing-measures-spacing-016)] gap-[var(--spacing-scale-004)]",
|
"!outline-1 !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-1 focus:!outline-[var(--color-content-default-brand-primary)]",
|
||||||
xsmall:
|
xsmallUseCases:
|
||||||
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)] gap-[var(--spacing-scale-004)]",
|
"!outline-1 !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-1 focus:!outline-[var(--color-content-default-brand-primary)]",
|
||||||
xsmallUseCases:
|
default:
|
||||||
"px-[var(--spacing-scale-002)] py-[var(--spacing-scale-002)] gap-[var(--spacing-scale-004)]",
|
"!outline-[1.5px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.5px] focus:!outline-[var(--color-content-default-brand-primary)]",
|
||||||
homeMd:
|
homeMd:
|
||||||
"px-[var(--spacing-scale-008)] py-[var(--spacing-scale-008)] gap-[var(--spacing-scale-004)]",
|
"!outline-[1.5px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.5px] focus:!outline-[var(--color-content-default-brand-primary)]",
|
||||||
homeUseCases:
|
homeUseCases:
|
||||||
"px-[var(--spacing-scale-002)] py-[var(--spacing-scale-008)] gap-[var(--spacing-scale-004)]",
|
"!outline-[1.5px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.5px] focus:!outline-[var(--color-content-default-brand-primary)]",
|
||||||
large:
|
large:
|
||||||
"px-[var(--spacing-scale-012)] py-[var(--spacing-scale-012)] gap-[var(--spacing-scale-004)] h-[44px]",
|
"!outline-[1.75px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.75px] focus:!outline-[var(--color-content-default-brand-primary)]",
|
||||||
largeUseCases:
|
largeUseCases:
|
||||||
"px-[var(--spacing-scale-012)] py-[var(--spacing-scale-012)] gap-[var(--spacing-scale-004)] h-[44px]",
|
"!outline-[1.75px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[1.75px] focus:!outline-[var(--color-content-default-brand-primary)]",
|
||||||
homeXlarge:
|
homeXlarge:
|
||||||
"px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] gap-[var(--spacing-scale-004)] h-[44px]",
|
"!outline-[2px] !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-[2px] focus:!outline-[var(--color-content-default-brand-primary)]",
|
||||||
xlarge:
|
xlarge:
|
||||||
"px-[var(--spacing-scale-016)] py-[var(--spacing-scale-008)] gap-[var(--spacing-scale-004)] h-[44px]",
|
"!outline-2 !outline-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)] focus:!outline-2 focus:!outline-[var(--color-content-default-brand-primary)]",
|
||||||
};
|
};
|
||||||
|
|
||||||
const smallTextStyle =
|
const sizeStyles = {
|
||||||
"font-inter text-[10px] leading-[12px] font-medium tracking-[0%]";
|
default:
|
||||||
const mediumTextStyle =
|
"px-[var(--spacing-measures-spacing-016)] py-[var(--spacing-measures-spacing-016)] gap-[var(--spacing-scale-004)]",
|
||||||
"font-inter text-[12px] leading-[14px] font-medium tracking-[0%]";
|
xsmall:
|
||||||
const largeTextStyle =
|
"px-[var(--spacing-scale-004)] py-[var(--spacing-scale-002)] gap-[var(--spacing-scale-004)]",
|
||||||
"font-inter text-[16px] leading-[20px] font-medium tracking-[0%]";
|
xsmallUseCases:
|
||||||
const xlargeTextStyle =
|
"px-[var(--spacing-scale-002)] py-[var(--spacing-scale-002)] gap-[var(--spacing-scale-004)]",
|
||||||
"font-inter text-[24px] leading-[28px] font-normal tracking-[0%]";
|
homeMd:
|
||||||
|
"px-[var(--spacing-scale-008)] py-[var(--spacing-scale-008)] gap-[var(--spacing-scale-004)]",
|
||||||
|
homeUseCases:
|
||||||
|
"px-[var(--spacing-scale-002)] py-[var(--spacing-scale-008)] gap-[var(--spacing-scale-004)]",
|
||||||
|
large:
|
||||||
|
"px-[var(--spacing-scale-012)] py-[var(--spacing-scale-012)] gap-[var(--spacing-scale-004)] h-[44px]",
|
||||||
|
largeUseCases:
|
||||||
|
"px-[var(--spacing-scale-012)] py-[var(--spacing-scale-012)] gap-[var(--spacing-scale-004)] h-[44px]",
|
||||||
|
homeXlarge:
|
||||||
|
"px-[var(--spacing-scale-016)] py-[var(--spacing-scale-016)] gap-[var(--spacing-scale-004)] h-[44px]",
|
||||||
|
xlarge:
|
||||||
|
"px-[var(--spacing-scale-016)] py-[var(--spacing-scale-008)] gap-[var(--spacing-scale-004)] h-[44px]",
|
||||||
|
};
|
||||||
|
|
||||||
const textStyles = {
|
const smallTextStyle =
|
||||||
default: smallTextStyle,
|
"font-inter text-[10px] leading-[12px] font-medium tracking-[0%]";
|
||||||
xsmall: smallTextStyle,
|
const mediumTextStyle =
|
||||||
xsmallUseCases: smallTextStyle,
|
"font-inter text-[12px] leading-[14px] font-medium tracking-[0%]";
|
||||||
home: smallTextStyle,
|
const largeTextStyle =
|
||||||
homeMd: mediumTextStyle,
|
"font-inter text-[16px] leading-[20px] font-medium tracking-[0%]";
|
||||||
homeUseCases: mediumTextStyle,
|
const xlargeTextStyle =
|
||||||
large: largeTextStyle,
|
"font-inter text-[24px] leading-[28px] font-normal tracking-[0%]";
|
||||||
largeUseCases: largeTextStyle,
|
|
||||||
homeXlarge: xlargeTextStyle,
|
|
||||||
xlarge: xlargeTextStyle,
|
|
||||||
};
|
|
||||||
|
|
||||||
const baseStyles = `inline-flex items-center ${sizeStyles[size]} rounded-[var(--radius-measures-radius-full)] ${textStyles[size]} transition-all duration-200 ease-in-out cursor-pointer focus:scale-[1.02]`;
|
const textStyles = {
|
||||||
|
default: smallTextStyle,
|
||||||
|
xsmall: smallTextStyle,
|
||||||
|
xsmallUseCases: smallTextStyle,
|
||||||
|
home: smallTextStyle,
|
||||||
|
homeMd: mediumTextStyle,
|
||||||
|
homeUseCases: mediumTextStyle,
|
||||||
|
large: largeTextStyle,
|
||||||
|
largeUseCases: largeTextStyle,
|
||||||
|
homeXlarge: xlargeTextStyle,
|
||||||
|
xlarge: xlargeTextStyle,
|
||||||
|
};
|
||||||
|
|
||||||
let finalVariant = variant;
|
const baseStyles = `inline-flex items-center ${sizeStyles[size]} rounded-[var(--radius-measures-radius-full)] ${textStyles[size]} transition-all duration-200 ease-in-out cursor-pointer focus:scale-[1.02]`;
|
||||||
if (disabled) {
|
|
||||||
finalVariant = "default";
|
|
||||||
}
|
|
||||||
|
|
||||||
const combinedStyles = `${baseStyles} ${variantStyles[finalVariant]} ${
|
let finalVariant = variant;
|
||||||
finalVariant === "home"
|
if (disabled) {
|
||||||
? homeOutlineStyles[size]
|
finalVariant = "default";
|
||||||
: activeOutlineStyles[size]
|
}
|
||||||
} ${isActive ? activeStateStyles[size] : ""} ${className}`;
|
|
||||||
|
|
||||||
const accessibilityProps = {
|
const combinedStyles = `${baseStyles} ${variantStyles[finalVariant]} ${
|
||||||
...(ariaLabel && { "aria-label": ariaLabel }),
|
finalVariant === "home"
|
||||||
...(disabled && { "aria-disabled": "true" }),
|
? homeOutlineStyles[size]
|
||||||
role: "menuitem",
|
: activeOutlineStyles[size]
|
||||||
tabIndex: disabled ? -1 : 0,
|
} ${isActive ? activeStateStyles[size] : ""} ${className}`;
|
||||||
...props,
|
|
||||||
};
|
const accessibilityProps = {
|
||||||
|
...(ariaLabel && { "aria-label": ariaLabel }),
|
||||||
|
...(disabled && { "aria-disabled": "true" }),
|
||||||
|
role: "menuitem",
|
||||||
|
tabIndex: disabled ? -1 : 0,
|
||||||
|
...props,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (disabled) {
|
||||||
|
return (
|
||||||
|
<span className={combinedStyles} {...accessibilityProps}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (disabled) {
|
|
||||||
return (
|
return (
|
||||||
<span className={combinedStyles} {...accessibilityProps}>
|
<a href={href} className={combinedStyles} {...accessibilityProps}>
|
||||||
{children}
|
{children}
|
||||||
</span>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
MenuBarItem.displayName = "MenuBarItem";
|
||||||
<a href={href} className={combinedStyles} {...accessibilityProps}>
|
|
||||||
{children}
|
export default MenuBarItem;
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
+108
-96
@@ -1,112 +1,124 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { memo } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
|
|
||||||
const MiniCard = ({
|
const MiniCard = memo(
|
||||||
children,
|
({
|
||||||
className = "",
|
children,
|
||||||
backgroundColor = "bg-[var(--color-surface-default-brand-royal)]",
|
className = "",
|
||||||
panelContent,
|
backgroundColor = "bg-[var(--color-surface-default-brand-royal)]",
|
||||||
label,
|
panelContent,
|
||||||
labelLine1,
|
label,
|
||||||
labelLine2,
|
labelLine1,
|
||||||
onClick,
|
labelLine2,
|
||||||
href,
|
onClick,
|
||||||
ariaLabel,
|
href,
|
||||||
}) => {
|
ariaLabel,
|
||||||
const cardContent = (
|
}) => {
|
||||||
<div className={`h-[186px] flex flex-col gap-[7px] ${className}`}>
|
const cardContent = (
|
||||||
{/* Top part - Inner panel */}
|
<div className={`h-[186px] flex flex-col gap-[7px] ${className}`}>
|
||||||
<div
|
{/* Top part - Inner panel */}
|
||||||
className={`flex-1 rounded-[var(--radius-measures-radius-xlarge)] border border-[1px] py-[var(--spacing-scale-032)] px-[var(--spacing-scale-024)] ${backgroundColor} flex items-center justify-center transition-all duration-200 hover:scale-[1.02] hover:shadow-lg`}
|
<div
|
||||||
>
|
className={`flex-1 rounded-[var(--radius-measures-radius-xlarge)] border border-[1px] py-[var(--spacing-scale-032)] px-[var(--spacing-scale-024)] ${backgroundColor} flex items-center justify-center transition-all duration-200 hover:scale-[1.02] hover:shadow-lg`}
|
||||||
{/* Content for the inner panel */}
|
>
|
||||||
{panelContent && (
|
{/* Content for the inner panel */}
|
||||||
<div className="flex items-center justify-center w-full h-full">
|
{panelContent && (
|
||||||
<Image
|
<div className="flex items-center justify-center w-full h-full">
|
||||||
src={panelContent}
|
<Image
|
||||||
alt={
|
src={panelContent}
|
||||||
ariaLabel ||
|
alt={
|
||||||
`${labelLine1} ${labelLine2}` ||
|
ariaLabel ||
|
||||||
label ||
|
`${labelLine1} ${labelLine2}` ||
|
||||||
"Feature icon"
|
label ||
|
||||||
}
|
"Feature icon"
|
||||||
className="max-w-[58px] max-h-[58px] w-auto h-auto object-contain"
|
}
|
||||||
unoptimized
|
className="max-w-[58px] max-h-[58px] w-auto h-auto object-contain"
|
||||||
width={0}
|
width={58}
|
||||||
height={0}
|
height={58}
|
||||||
sizes="100vw"
|
sizes="(max-width: 768px) 50vw, 25vw"
|
||||||
/>
|
loading="lazy"
|
||||||
</div>
|
placeholder="blur"
|
||||||
)}
|
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAYEBQYFBAYGBQYHBwYIChAKCgkJChQODwwQFxQYGBcUFhYaHSUfGhsjHBYWICwgIyYnKSopGR8tMC0oMCUoKSj/2wBDAQcHBwoIChMKChMoGhYaKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCj/wAARCAABAAEDASIAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAv/xAAUEAEAAAAAAAAAAAAAAAAAAAAA/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAX/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwCdABmX/9k="
|
||||||
{children}
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Bottom part - Text container */}
|
{/* Bottom part - Text container */}
|
||||||
<div className="font-inter font-medium text-[12px] leading-[14px] text-center text-[var(--color-content-default-primary)]">
|
<div className="font-inter font-medium text-[12px] leading-[14px] text-center text-[var(--color-content-default-primary)]">
|
||||||
{labelLine1 && labelLine2 ? (
|
{labelLine1 && labelLine2 ? (
|
||||||
<>
|
<>
|
||||||
<div>{labelLine1}</div>
|
<div>{labelLine1}</div>
|
||||||
<div>{labelLine2}</div>
|
<div>{labelLine2}</div>
|
||||||
<div> </div>
|
<div> </div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
label
|
label
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
// If href is provided, render as a link
|
|
||||||
if (href) {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href={href}
|
|
||||||
className="block focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 rounded-[var(--radius-measures-radius-xlarge)] transition-all duration-200 hover:scale-[1.02]"
|
|
||||||
aria-label={
|
|
||||||
ariaLabel || `${labelLine1} ${labelLine2}` || label || "Feature card"
|
|
||||||
}
|
|
||||||
tabIndex={0}
|
|
||||||
>
|
|
||||||
{cardContent}
|
|
||||||
</a>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// If onClick is provided, render as a button
|
// If href is provided, render as a link
|
||||||
if (onClick) {
|
if (href) {
|
||||||
return (
|
return (
|
||||||
<button
|
<a
|
||||||
onClick={onClick}
|
href={href}
|
||||||
className="block w-full text-left focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 rounded-[var(--radius-measures-radius-xlarge)] transition-all duration-200 hover:scale-[1.02]"
|
className="block focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 rounded-[var(--radius-measures-radius-xlarge)] transition-all duration-200 hover:scale-[1.02]"
|
||||||
aria-label={
|
aria-label={
|
||||||
ariaLabel || `${labelLine1} ${labelLine2}` || label || "Feature card"
|
ariaLabel ||
|
||||||
}
|
`${labelLine1} ${labelLine2}` ||
|
||||||
tabIndex={0}
|
label ||
|
||||||
onKeyDown={(e) => {
|
"Feature card"
|
||||||
if (e.key === "Enter" || e.key === " ") {
|
|
||||||
e.preventDefault();
|
|
||||||
onClick();
|
|
||||||
}
|
}
|
||||||
}}
|
tabIndex={0}
|
||||||
|
>
|
||||||
|
{cardContent}
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If onClick is provided, render as a button
|
||||||
|
if (onClick) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className="block w-full text-left focus:outline-none focus:ring-2 focus:ring-[var(--color-surface-default-brand-royal)] focus:ring-offset-2 rounded-[var(--radius-measures-radius-xlarge)] transition-all duration-200 hover:scale-[1.02]"
|
||||||
|
aria-label={
|
||||||
|
ariaLabel ||
|
||||||
|
`${labelLine1} ${labelLine2}` ||
|
||||||
|
label ||
|
||||||
|
"Feature card"
|
||||||
|
}
|
||||||
|
tabIndex={0}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === "Enter" || e.key === " ") {
|
||||||
|
e.preventDefault();
|
||||||
|
onClick();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{cardContent}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default render as a div
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="block"
|
||||||
|
aria-label={
|
||||||
|
ariaLabel || `${labelLine1} ${labelLine2}` || label || "Feature card"
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{cardContent}
|
{cardContent}
|
||||||
</button>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// Default render as a div
|
MiniCard.displayName = "MiniCard";
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="block"
|
|
||||||
aria-label={
|
|
||||||
ariaLabel || `${labelLine1} ${labelLine2}` || label || "Feature card"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{cardContent}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MiniCard;
|
export default MiniCard;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export default function NavigationItem({
|
import React, { memo } from "react";
|
||||||
|
|
||||||
|
const NavigationItem = memo(({
|
||||||
href = "#",
|
href = "#",
|
||||||
children,
|
children,
|
||||||
variant = "default",
|
variant = "default",
|
||||||
@@ -50,4 +52,8 @@ export default function NavigationItem({
|
|||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
NavigationItem.displayName = "NavigationItem";
|
||||||
|
|
||||||
|
export default NavigationItem;
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import React, { memo } from "react";
|
||||||
import SectionNumber from "./SectionNumber";
|
import SectionNumber from "./SectionNumber";
|
||||||
|
|
||||||
const NumberedCard = ({ number, text, iconShape, iconColor }) => {
|
const NumberedCard = memo(({ number, text, iconShape, iconColor }) => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-[var(--color-surface-inverse-primary)] rounded-[12px] p-5 shadow-lg flex flex-col gap-4 sm:p-8 sm:gap-8 sm:flex-row sm:items-center lg:p-8 lg:gap-0 lg:flex-row lg:items-stretch lg:relative lg:h-[238px]">
|
<div className="bg-[var(--color-surface-inverse-primary)] rounded-[12px] p-5 shadow-lg flex flex-col gap-4 sm:p-8 sm:gap-8 sm:flex-row sm:items-center lg:p-8 lg:gap-0 lg:flex-row lg:items-stretch lg:relative lg:h-[238px]">
|
||||||
{/* Section Number - Top right (lg breakpoint) */}
|
{/* Section Number - Top right (lg breakpoint) */}
|
||||||
@@ -18,6 +19,8 @@ const NumberedCard = ({ number, text, iconShape, iconColor }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
NumberedCard.displayName = "NumberedCard";
|
||||||
|
|
||||||
export default NumberedCard;
|
export default NumberedCard;
|
||||||
|
|||||||
@@ -1,23 +1,27 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import React, { memo, useMemo } from "react";
|
||||||
import NumberedCard from "./NumberedCard";
|
import NumberedCard from "./NumberedCard";
|
||||||
import SectionHeader from "./SectionHeader";
|
import SectionHeader from "./SectionHeader";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
|
|
||||||
const NumberedCards = ({ title, subtitle, cards }) => {
|
const NumberedCards = memo(({ title, subtitle, cards }) => {
|
||||||
// Schema markup for SEO
|
// Memoize schema data to prevent unnecessary re-computations
|
||||||
const schemaData = {
|
const schemaData = useMemo(
|
||||||
"@context": "https://schema.org",
|
() => ({
|
||||||
"@type": "HowTo",
|
"@context": "https://schema.org",
|
||||||
name: title,
|
"@type": "HowTo",
|
||||||
description: subtitle,
|
name: title,
|
||||||
step: cards.map((card, index) => ({
|
description: subtitle,
|
||||||
"@type": "HowToStep",
|
step: cards.map((card, index) => ({
|
||||||
position: index + 1,
|
"@type": "HowToStep",
|
||||||
name: card.text,
|
position: index + 1,
|
||||||
text: card.text,
|
name: card.text,
|
||||||
})),
|
text: card.text,
|
||||||
};
|
})),
|
||||||
|
}),
|
||||||
|
[title, subtitle, cards]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -70,6 +74,8 @@ const NumberedCards = ({ title, subtitle, cards }) => {
|
|||||||
</section>
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
NumberedCards.displayName = "NumberedCards";
|
||||||
|
|
||||||
export default NumberedCards;
|
export default NumberedCards;
|
||||||
|
|||||||
+224
-219
@@ -1,247 +1,252 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState, memo } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import QuoteDecor from "./QuoteDecor";
|
import QuoteDecor from "./QuoteDecor";
|
||||||
|
|
||||||
const QuoteBlock = ({
|
const QuoteBlock = memo(
|
||||||
variant = "standard",
|
({
|
||||||
className = "",
|
variant = "standard",
|
||||||
quote = "The rules of decision-making must be open and available to everyone, and this can happen only if they are formalized.",
|
className = "",
|
||||||
author = "Jo Freeman",
|
quote = "The rules of decision-making must be open and available to everyone, and this can happen only if they are formalized.",
|
||||||
source = "The Tyranny of Structurelessness",
|
author = "Jo Freeman",
|
||||||
avatarSrc = "assets/Quote_Avatar.svg",
|
source = "The Tyranny of Structurelessness",
|
||||||
id,
|
avatarSrc = "/assets/Quote_Avatar.svg",
|
||||||
fallbackAvatarSrc = "assets/Quote_Avatar.svg", // Fallback avatar
|
id,
|
||||||
onError, // Error callback
|
fallbackAvatarSrc = "/assets/Quote_Avatar.svg", // Fallback avatar
|
||||||
}) => {
|
onError, // Error callback
|
||||||
const [imageError, setImageError] = useState(false);
|
}) => {
|
||||||
const [imageLoading, setImageLoading] = useState(true);
|
const [imageError, setImageError] = useState(false);
|
||||||
|
const [imageLoading, setImageLoading] = useState(true);
|
||||||
|
|
||||||
// Variant configurations
|
// Variant configurations
|
||||||
const variants = {
|
const variants = {
|
||||||
compact: {
|
compact: {
|
||||||
container: "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-016)]",
|
container:
|
||||||
card: "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-016)] md:py-[var(--spacing-scale-040)] md:px-[var(--spacing-scale-024)] rounded-[var(--radius-measures-radius-small)]",
|
"py-[var(--spacing-scale-032)] px-[var(--spacing-scale-016)]",
|
||||||
gap: "gap-[var(--spacing-scale-016)] md:gap-[var(--spacing-scale-024)]",
|
card: "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-016)] md:py-[var(--spacing-scale-040)] md:px-[var(--spacing-scale-024)] rounded-[var(--radius-measures-radius-small)]",
|
||||||
avatarGap: "gap-[var(--spacing-scale-012)]",
|
gap: "gap-[var(--spacing-scale-016)] md:gap-[var(--spacing-scale-024)]",
|
||||||
avatar: "w-[48px] h-[48px] md:w-[64px] md:h-[64px]",
|
avatarGap: "gap-[var(--spacing-scale-012)]",
|
||||||
quote: "text-[16px] leading-[120%] md:text-[20px] md:leading-[110%]",
|
avatar: "w-[48px] h-[48px] md:w-[64px] md:h-[64px]",
|
||||||
author: "text-[10px] leading-[120%] md:text-[12px]",
|
quote: "text-[16px] leading-[120%] md:text-[20px] md:leading-[110%]",
|
||||||
source: "text-[10px] leading-[120%] md:text-[12px]",
|
author: "text-[10px] leading-[120%] md:text-[12px]",
|
||||||
showDecor: false,
|
source: "text-[10px] leading-[120%] md:text-[12px]",
|
||||||
},
|
showDecor: false,
|
||||||
standard: {
|
},
|
||||||
container:
|
standard: {
|
||||||
"md:py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-016)] lg:p-[var(--spacing-scale-064)]",
|
container:
|
||||||
card: "py-[var(--spacing-scale-064)] px-[var(--spacing-scale-020)] md:py-[var(--spacing-scale-064)] md:px-[var(--spacing-scale-048)] md:rounded-[var(--radius-measures-radius-medium)] lg:py-[var(--spacing-scale-064)] lg:pl-[120px] lg:pr-[320px]",
|
"md:py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-016)] lg:p-[var(--spacing-scale-064)]",
|
||||||
gap: "gap-[var(--spacing-scale-024)] md:gap-[var(--spacing-scale-048)] lg:gap-[var(--spacing-scale-064)] xl:gap-[105px]",
|
card: "py-[var(--spacing-scale-064)] px-[var(--spacing-scale-020)] md:py-[var(--spacing-scale-064)] md:px-[var(--spacing-scale-048)] md:rounded-[var(--radius-measures-radius-medium)] lg:py-[var(--spacing-scale-064)] lg:pl-[120px] lg:pr-[320px]",
|
||||||
avatarGap:
|
gap: "gap-[var(--spacing-scale-024)] md:gap-[var(--spacing-scale-048)] lg:gap-[var(--spacing-scale-064)] xl:gap-[105px]",
|
||||||
"gap-[var(--spacing-scale-020)] lg:gap-[var(--spacing-scale-018)] xl:gap-[var(--spacing-scale-032)]",
|
avatarGap:
|
||||||
avatar:
|
"gap-[var(--spacing-scale-020)] lg:gap-[var(--spacing-scale-018)] xl:gap-[var(--spacing-scale-032)]",
|
||||||
"md:w-[120px] md:h-[120px] lg:w-[150px] lg:h-[150px] xl:w-[200px] xl:h-[200px]",
|
avatar:
|
||||||
quote:
|
"md:w-[120px] md:h-[120px] lg:w-[150px] lg:h-[150px] xl:w-[200px] xl:h-[200px]",
|
||||||
"text-[18px] leading-[120%] md:text-[36px] md:leading-[110%] md:tracking-[0px] lg:text-[52px] xl:text-[64px]",
|
quote:
|
||||||
author:
|
"text-[18px] leading-[120%] md:text-[36px] md:leading-[110%] md:tracking-[0px] lg:text-[52px] xl:text-[64px]",
|
||||||
"text-[12px] leading-[120%] md:text-[18px] md:leading-[120%] md:tracking-[0.24px] lg:text-[24px] xl:text-[32px]",
|
author:
|
||||||
source:
|
"text-[12px] leading-[120%] md:text-[18px] md:leading-[120%] md:tracking-[0.24px] lg:text-[24px] xl:text-[32px]",
|
||||||
"text-[12px] leading-[120%] md:text-[18px] md:leading-[120%] md:tracking-[0.24px] lg:text-[24px] xl:text-[32px]",
|
source:
|
||||||
showDecor: true,
|
"text-[12px] leading-[120%] md:text-[18px] md:leading-[120%] md:tracking-[0.24px] lg:text-[24px] xl:text-[32px]",
|
||||||
},
|
showDecor: true,
|
||||||
extended: {
|
},
|
||||||
container:
|
extended: {
|
||||||
"py-[var(--spacing-scale-048)] px-[var(--spacing-scale-024)] md:py-[var(--spacing-scale-064)] md:px-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-080)] lg:px-[var(--spacing-scale-048)]",
|
container:
|
||||||
card: "py-[var(--spacing-scale-080)] px-[var(--spacing-scale-032)] md:py-[var(--spacing-scale-096)] md:px-[var(--spacing-scale-064)] md:rounded-[var(--radius-measures-radius-large)] lg:py-[var(--spacing-scale-112)] lg:pl-[160px] lg:pr-[400px]",
|
"py-[var(--spacing-scale-048)] px-[var(--spacing-scale-024)] md:py-[var(--spacing-scale-064)] md:px-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-080)] lg:px-[var(--spacing-scale-048)]",
|
||||||
gap: "gap-[var(--spacing-scale-032)] md:gap-[var(--spacing-scale-064)] lg:gap-[var(--spacing-scale-080)] xl:gap-[140px]",
|
card: "py-[var(--spacing-scale-080)] px-[var(--spacing-scale-032)] md:py-[var(--spacing-scale-096)] md:px-[var(--spacing-scale-064)] md:rounded-[var(--radius-measures-radius-large)] lg:py-[var(--spacing-scale-112)] lg:pl-[160px] lg:pr-[400px]",
|
||||||
avatarGap:
|
gap: "gap-[var(--spacing-scale-032)] md:gap-[var(--spacing-scale-064)] lg:gap-[var(--spacing-scale-080)] xl:gap-[140px]",
|
||||||
"gap-[var(--spacing-scale-032)] lg:gap-[var(--spacing-scale-040)] xl:gap-[var(--spacing-scale-048)]",
|
avatarGap:
|
||||||
avatar:
|
"gap-[var(--spacing-scale-032)] lg:gap-[var(--spacing-scale-040)] xl:gap-[var(--spacing-scale-048)]",
|
||||||
"w-[80px] h-[80px] md:w-[140px] md:h-[140px] lg:w-[180px] lg:h-[180px] xl:w-[240px] xl:h-[240px]",
|
avatar:
|
||||||
quote:
|
"w-[80px] h-[80px] md:w-[140px] md:h-[140px] lg:w-[180px] lg:h-[180px] xl:w-[240px] xl:h-[240px]",
|
||||||
"text-[20px] leading-[120%] md:text-[40px] md:leading-[110%] md:tracking-[0px] lg:text-[60px] xl:text-[72px]",
|
quote:
|
||||||
author:
|
"text-[20px] leading-[120%] md:text-[40px] md:leading-[110%] md:tracking-[0px] lg:text-[60px] xl:text-[72px]",
|
||||||
"text-[14px] leading-[120%] md:text-[20px] md:leading-[120%] md:tracking-[0.24px] lg:text-[28px] xl:text-[36px]",
|
author:
|
||||||
source:
|
"text-[14px] leading-[120%] md:text-[20px] md:leading-[120%] md:tracking-[0.24px] lg:text-[28px] xl:text-[36px]",
|
||||||
"text-[14px] leading-[120%] md:text-[20px] md:leading-[120%] md:tracking-[0.24px] lg:text-[28px] xl:text-[36px]",
|
source:
|
||||||
showDecor: true,
|
"text-[14px] leading-[120%] md:text-[20px] md:leading-[120%] md:tracking-[0.24px] lg:text-[28px] xl:text-[36px]",
|
||||||
},
|
showDecor: true,
|
||||||
};
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const config = variants[variant] || variants.standard;
|
const config = variants[variant] || variants.standard;
|
||||||
|
|
||||||
// Use provided ID or generate a stable one based on content
|
// Use provided ID or generate a stable one based on content
|
||||||
const baseId = id || `quote-${author.toLowerCase().replace(/\s+/g, "-")}`;
|
const baseId = id || `quote-${author.toLowerCase().replace(/\s+/g, "-")}`;
|
||||||
const quoteId = `${baseId}-content`;
|
const quoteId = `${baseId}-content`;
|
||||||
const authorId = `${baseId}-author`;
|
const authorId = `${baseId}-author`;
|
||||||
|
|
||||||
// Error handling functions
|
// Error handling functions
|
||||||
const handleImageError = (error) => {
|
const handleImageError = (error) => {
|
||||||
console.warn(
|
console.warn(
|
||||||
`QuoteBlock: Failed to load avatar image for ${author}:`,
|
`QuoteBlock: Failed to load avatar image for ${author}:`,
|
||||||
error,
|
error
|
||||||
);
|
);
|
||||||
setImageError(true);
|
setImageError(true);
|
||||||
setImageLoading(false);
|
setImageLoading(false);
|
||||||
|
|
||||||
// Call error callback if provided
|
// Call error callback if provided
|
||||||
if (onError) {
|
if (onError) {
|
||||||
onError({
|
onError({
|
||||||
type: "image_load_error",
|
type: "image_load_error",
|
||||||
message: `Failed to load avatar for ${author}`,
|
message: `Failed to load avatar for ${author}`,
|
||||||
author,
|
author,
|
||||||
avatarSrc,
|
avatarSrc,
|
||||||
error,
|
error,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleImageLoad = () => {
|
||||||
|
setImageLoading(false);
|
||||||
|
setImageError(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Validate required props
|
||||||
|
if (!quote || !author) {
|
||||||
|
console.error("QuoteBlock: Missing required props (quote or author)");
|
||||||
|
if (onError) {
|
||||||
|
onError({
|
||||||
|
type: "missing_props",
|
||||||
|
message: "QuoteBlock requires quote and author props",
|
||||||
|
quote: !!quote,
|
||||||
|
author: !!author,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return null; // Don't render if missing required props
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageLoad = () => {
|
// Determine which avatar to use
|
||||||
setImageLoading(false);
|
const currentAvatarSrc = imageError ? fallbackAvatarSrc : avatarSrc;
|
||||||
setImageError(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Validate required props
|
return (
|
||||||
if (!quote || !author) {
|
<section
|
||||||
console.error("QuoteBlock: Missing required props (quote or author)");
|
className={`${config.container} ${className}`}
|
||||||
if (onError) {
|
aria-labelledby={quoteId}
|
||||||
onError({
|
role="region"
|
||||||
type: "missing_props",
|
|
||||||
message: "QuoteBlock requires quote and author props",
|
|
||||||
quote: !!quote,
|
|
||||||
author: !!author,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return null; // Don't render if missing required props
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine which avatar to use
|
|
||||||
const currentAvatarSrc = imageError ? fallbackAvatarSrc : avatarSrc;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section
|
|
||||||
className={`${config.container} ${className}`}
|
|
||||||
aria-labelledby={quoteId}
|
|
||||||
role="region"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
className={`${config.card} bg-[var(--color-surface-default-brand-darker-accent)] relative overflow-hidden`}
|
|
||||||
>
|
>
|
||||||
{/* Background with noise texture */}
|
|
||||||
<div
|
<div
|
||||||
className="absolute inset-0 bg-[var(--color-surface-default-brand-darker-accent)]"
|
className={`${config.card} bg-[var(--color-surface-default-brand-darker-accent)] relative overflow-hidden`}
|
||||||
style={{
|
>
|
||||||
filter:
|
{/* Background with noise texture */}
|
||||||
'url(\'data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg"><defs><filter id="grain" filterUnits="objectBoundingBox" x="0" y="0" width="1" height="1" colorInterpolationFilters="sRGB"><feTurbulence type="fractalNoise" baseFrequency="0.4" numOctaves="3" seed="7" stitchTiles="stitch" result="noise"/><feColorMatrix in="noise" result="softNoise" type="matrix" values="0.8 0 0 0 0.3 0 0.6 0 0 0.2 0 0 1.0 0 0.4 0 0 0 0.25 0"/><feComposite in="softNoise" in2="SourceAlpha" operator="in" result="maskedNoise"/><feBlend in="SourceGraphic" in2="maskedNoise" mode="multiply"/></filter></defs></svg>#grain\')',
|
<div
|
||||||
}}
|
className="absolute inset-0 bg-[var(--color-surface-default-brand-darker-accent)]"
|
||||||
/>
|
style={{
|
||||||
|
filter:
|
||||||
|
'url(\'data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg"><defs><filter id="grain" filterUnits="objectBoundingBox" x="0" y="0" width="1" height="1" colorInterpolationFilters="sRGB"><feTurbulence type="fractalNoise" baseFrequency="0.4" numOctaves="3" seed="7" stitchTiles="stitch" result="noise"/><feColorMatrix in="noise" result="softNoise" type="matrix" values="0.8 0 0 0 0.3 0 0.6 0 0 0.2 0 0 1.0 0 0.4 0 0 0 0.25 0"/><feComposite in="softNoise" in2="SourceAlpha" operator="in" result="maskedNoise"/><feBlend in="SourceGraphic" in2="maskedNoise" mode="multiply"/></filter></defs></svg>#grain\')',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* DECORATIONS (behind content) */}
|
{/* DECORATIONS (behind content) */}
|
||||||
{config.showDecor && (
|
{config.showDecor && (
|
||||||
<QuoteDecor
|
<QuoteDecor
|
||||||
className="pointer-events-none absolute z-0
|
className="pointer-events-none absolute z-0
|
||||||
left-0 top-0
|
left-0 top-0
|
||||||
w-full h-full"
|
w-full h-full"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`flex flex-col ${config.gap} relative z-10`}>
|
<div className={`flex flex-col ${config.gap} relative z-10`}>
|
||||||
<div className={`flex flex-col ${config.avatarGap}`}>
|
<div className={`flex flex-col ${config.avatarGap}`}>
|
||||||
{/* Avatar with error handling */}
|
{/* Avatar with error handling */}
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
{!imageError ? (
|
{!imageError ? (
|
||||||
<Image
|
<Image
|
||||||
src={avatarSrc}
|
src={avatarSrc}
|
||||||
alt={`Portrait of ${author}`}
|
alt={`Portrait of ${author}`}
|
||||||
width={64}
|
width={64}
|
||||||
height={64}
|
height={64}
|
||||||
className={`filter sepia ${
|
className={`filter sepia ${
|
||||||
config.avatar
|
config.avatar
|
||||||
} transition-opacity duration-300 ${
|
} transition-opacity duration-300 ${
|
||||||
imageLoading ? "opacity-0" : "opacity-100"
|
imageLoading ? "opacity-0" : "opacity-100"
|
||||||
}`}
|
}`}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
onError={handleImageError}
|
onError={handleImageError}
|
||||||
onLoad={handleImageLoad}
|
onLoad={handleImageLoad}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{/* Loading state */}
|
{/* Loading state */}
|
||||||
{imageLoading && !imageError && (
|
{imageLoading && !imageError && (
|
||||||
<div
|
<div
|
||||||
className={`absolute inset-0 bg-gray-200 animate-pulse rounded-full ${config.avatar}`}
|
className={`absolute inset-0 bg-gray-200 animate-pulse rounded-full ${config.avatar}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Error state - show initials */}
|
{/* Error state - show initials */}
|
||||||
{imageError && (
|
{imageError && (
|
||||||
<div
|
<div
|
||||||
className={`flex items-center justify-center bg-gray-300 rounded-full ${config.avatar} text-gray-600 font-bold`}
|
className={`flex items-center justify-center bg-gray-300 rounded-full ${config.avatar} text-gray-600 font-bold`}
|
||||||
|
>
|
||||||
|
<span className="text-sm md:text-base lg:text-lg xl:text-xl">
|
||||||
|
{author
|
||||||
|
.split(" ")
|
||||||
|
.map((n) => n[0])
|
||||||
|
.join("")
|
||||||
|
.toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<blockquote
|
||||||
|
id={quoteId}
|
||||||
|
aria-labelledby={authorId}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
data-qopen="“"
|
||||||
|
data-qclose="”"
|
||||||
|
className={[
|
||||||
|
"font-bricolage-grotesque font-normal",
|
||||||
|
config.quote,
|
||||||
|
"text-[var(--color-content-inverse-primary)]",
|
||||||
|
// give space for the hanging open-quote so it's not clipped:
|
||||||
|
"pl-[0.6em] -indent-[0.6em]",
|
||||||
|
// inject quotes
|
||||||
|
"relative before:content-[attr(data-qopen)] after:content-[attr(data-qclose)]",
|
||||||
|
// lock quote glyphs to your display face
|
||||||
|
"before:[font-family:var(--font-bricolage-grotesque)]",
|
||||||
|
"after:[font-family:var(--font-bricolage-grotesque)]",
|
||||||
|
].join(" ")}
|
||||||
>
|
>
|
||||||
<span className="text-sm md:text-base lg:text-lg xl:text-xl">
|
{quote}
|
||||||
{author
|
</p>
|
||||||
.split(" ")
|
</blockquote>
|
||||||
.map((n) => n[0])
|
|
||||||
.join("")
|
|
||||||
.toUpperCase()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
<footer className="flex flex-col gap-[var(--spacing-scale-008)] md:gap-[var(--spacing-scale-012)] xl:gap-[var(--spacing-scale-020)]">
|
||||||
<blockquote
|
<cite
|
||||||
id={quoteId}
|
id={authorId}
|
||||||
aria-labelledby={authorId}
|
className={`font-inter font-normal ${config.author} text-[var(--color-content-inverse-primary)] uppercase not-italic`}
|
||||||
className="relative"
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
data-qopen="“"
|
|
||||||
data-qclose="”"
|
|
||||||
className={[
|
|
||||||
"font-bricolage-grotesque font-normal",
|
|
||||||
config.quote,
|
|
||||||
"text-[var(--color-content-inverse-primary)]",
|
|
||||||
// give space for the hanging open-quote so it's not clipped:
|
|
||||||
"pl-[0.6em] -indent-[0.6em]",
|
|
||||||
// inject quotes
|
|
||||||
"relative before:content-[attr(data-qopen)] after:content-[attr(data-qclose)]",
|
|
||||||
// lock quote glyphs to your display face
|
|
||||||
"before:[font-family:var(--font-bricolage-grotesque)]",
|
|
||||||
"after:[font-family:var(--font-bricolage-grotesque)]",
|
|
||||||
].join(" ")}
|
|
||||||
>
|
>
|
||||||
{quote}
|
{author}
|
||||||
</p>
|
</cite>
|
||||||
</blockquote>
|
{source && (
|
||||||
|
<p
|
||||||
|
data-qopen="“"
|
||||||
|
data-qclose="”"
|
||||||
|
className={[
|
||||||
|
"font-inter font-normal",
|
||||||
|
config.source,
|
||||||
|
"text-[var(--color-content-inverse-primary)] uppercase",
|
||||||
|
"pl-[0.6em] -indent-[0.6em]",
|
||||||
|
"relative before:content-[attr(data-qopen)] after:content-[attr(data-qclose)]",
|
||||||
|
"before:[font-family:var(--font-inter)] after:[font-family:var(--font-inter)]",
|
||||||
|
].join(" ")}
|
||||||
|
>
|
||||||
|
{source}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<footer className="flex flex-col gap-[var(--spacing-scale-008)] md:gap-[var(--spacing-scale-012)] xl:gap-[var(--spacing-scale-020)]">
|
|
||||||
<cite
|
|
||||||
id={authorId}
|
|
||||||
className={`font-inter font-normal ${config.author} text-[var(--color-content-inverse-primary)] uppercase not-italic`}
|
|
||||||
>
|
|
||||||
{author}
|
|
||||||
</cite>
|
|
||||||
{source && (
|
|
||||||
<p
|
|
||||||
data-qopen="“"
|
|
||||||
data-qclose="”"
|
|
||||||
className={[
|
|
||||||
"font-inter font-normal",
|
|
||||||
config.source,
|
|
||||||
"text-[var(--color-content-inverse-primary)] uppercase",
|
|
||||||
"pl-[0.6em] -indent-[0.6em]",
|
|
||||||
"relative before:content-[attr(data-qopen)] after:content-[attr(data-qclose)]",
|
|
||||||
"before:[font-family:var(--font-inter)] after:[font-family:var(--font-inter)]",
|
|
||||||
].join(" ")}
|
|
||||||
>
|
|
||||||
{source}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</footer>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
);
|
||||||
);
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
|
QuoteBlock.displayName = "QuoteBlock";
|
||||||
|
|
||||||
export default QuoteBlock;
|
export default QuoteBlock;
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
const QuoteDecor = ({ className = "" }) => {
|
import React, { memo } from "react";
|
||||||
|
|
||||||
|
const QuoteDecor = memo(({ className = "" }) => {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
className={`text-[var(--color-surface-inverse-brand-primary)] opacity-100 w-full h-full md:max-w-[640px] lg:max-w-[850px] xl:max-w-[1100px] ${className}`}
|
className={`text-[var(--color-surface-inverse-brand-primary)] opacity-100 w-full h-full md:max-w-[640px] lg:max-w-[850px] xl:max-w-[1100px] ${className}`}
|
||||||
@@ -68,6 +70,8 @@ const QuoteDecor = ({ className = "" }) => {
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
QuoteDecor.displayName = "QuoteDecor";
|
||||||
|
|
||||||
export default QuoteDecor;
|
export default QuoteDecor;
|
||||||
|
|||||||
+141
-130
@@ -1,152 +1,163 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import React, { useState, useEffect, memo, useMemo, useCallback } from "react";
|
||||||
import ContentThumbnailTemplate from "./ContentThumbnailTemplate";
|
import ContentThumbnailTemplate from "./ContentThumbnailTemplate";
|
||||||
|
|
||||||
export default function RelatedArticles({
|
const RelatedArticles = memo(
|
||||||
relatedPosts,
|
({ relatedPosts, currentPostSlug, slugOrder = [] }) => {
|
||||||
currentPostSlug,
|
// Memoize filtered posts to prevent unnecessary re-computations
|
||||||
slugOrder = [],
|
const filteredPosts = useMemo(
|
||||||
}) {
|
() => relatedPosts.filter((post) => post.slug !== currentPostSlug),
|
||||||
// Filter out the current post from related posts
|
[relatedPosts, currentPostSlug]
|
||||||
const filteredPosts = relatedPosts.filter(
|
);
|
||||||
(post) => post.slug !== currentPostSlug,
|
|
||||||
);
|
|
||||||
|
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [isMobile, setIsMobile] = useState(true);
|
const [isMobile, setIsMobile] = useState(true);
|
||||||
|
|
||||||
// Check if we're on mobile (below lg breakpoint)
|
// Memoize the mouse down handler to prevent unnecessary re-renders
|
||||||
useEffect(() => {
|
const handleMouseDown = useCallback((e) => {
|
||||||
const checkScreenSize = () => {
|
const slider = e.currentTarget;
|
||||||
setIsMobile(window.innerWidth < 1024); // lg breakpoint is 1024px
|
const startX = e.pageX - slider.offsetLeft;
|
||||||
};
|
const scrollLeft = slider.scrollLeft;
|
||||||
|
|
||||||
checkScreenSize();
|
const handleMouseMove = (e) => {
|
||||||
window.addEventListener("resize", checkScreenSize);
|
const x = e.pageX - slider.offsetLeft;
|
||||||
return () => window.removeEventListener("resize", checkScreenSize);
|
const walk = (x - startX) * 2;
|
||||||
}, []);
|
slider.scrollLeft = scrollLeft - walk;
|
||||||
|
};
|
||||||
|
|
||||||
// Auto-advance every 3 seconds (only on mobile)
|
const handleMouseUp = () => {
|
||||||
useEffect(() => {
|
document.removeEventListener("mousemove", handleMouseMove);
|
||||||
if (filteredPosts.length <= 1 || !isMobile) return;
|
document.removeEventListener("mouseup", handleMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
document.addEventListener("mousemove", handleMouseMove);
|
||||||
setProgress(0);
|
document.addEventListener("mouseup", handleMouseUp);
|
||||||
setCurrentIndex((prev) => (prev + 1) % filteredPosts.length);
|
}, []);
|
||||||
}, 3000);
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
// Memoize transform style to prevent unnecessary recalculations
|
||||||
}, [filteredPosts.length, isMobile]);
|
const transformStyle = useMemo(
|
||||||
|
() => ({
|
||||||
|
transform: isMobile
|
||||||
|
? `translateX(calc(50% - 130px - ${currentIndex * 260}px))`
|
||||||
|
: "none",
|
||||||
|
scrollBehavior: !isMobile ? "smooth" : "auto",
|
||||||
|
}),
|
||||||
|
[isMobile, currentIndex]
|
||||||
|
);
|
||||||
|
|
||||||
// Progress animation (only on mobile)
|
// Memoize progress bar style calculation
|
||||||
useEffect(() => {
|
const getProgressStyle = useCallback(
|
||||||
if (filteredPosts.length <= 1 || !isMobile) return;
|
(index) => ({
|
||||||
|
width:
|
||||||
|
index === currentIndex
|
||||||
|
? `${progress}%`
|
||||||
|
: index < currentIndex
|
||||||
|
? "100%"
|
||||||
|
: "0%",
|
||||||
|
}),
|
||||||
|
[currentIndex, progress]
|
||||||
|
);
|
||||||
|
|
||||||
const progressInterval = setInterval(() => {
|
// Check if we're on mobile (below lg breakpoint)
|
||||||
setProgress((prev) => {
|
useEffect(() => {
|
||||||
if (prev >= 100) {
|
const checkScreenSize = () => {
|
||||||
return 0;
|
setIsMobile(window.innerWidth < 1024); // lg breakpoint is 1024px
|
||||||
}
|
};
|
||||||
return prev + 1;
|
|
||||||
});
|
|
||||||
}, 30); // 30ms intervals for smooth animation
|
|
||||||
|
|
||||||
return () => clearInterval(progressInterval);
|
checkScreenSize();
|
||||||
}, [currentIndex, filteredPosts.length, isMobile]);
|
window.addEventListener("resize", checkScreenSize);
|
||||||
|
return () => window.removeEventListener("resize", checkScreenSize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (filteredPosts.length === 0) {
|
// Auto-advance every 3 seconds (only on mobile)
|
||||||
return null;
|
useEffect(() => {
|
||||||
}
|
if (filteredPosts.length <= 1 || !isMobile) return;
|
||||||
|
|
||||||
return (
|
const interval = setInterval(() => {
|
||||||
<section className="py-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-064)]">
|
setProgress(0);
|
||||||
<div className="flex flex-col gap-[var(--spacing-scale-032)] lg:gap-[51px]">
|
setCurrentIndex((prev) => (prev + 1) % filteredPosts.length);
|
||||||
<h2 className="text-[32px] lg:text-[44px] leading-[110%] font-medium text-[var(--color-content-inverse-primary)] text-center">
|
}, 3000);
|
||||||
Related Articles
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{/* Horizontal Articles Row - Carousel on mobile, Scrollable slider on desktop */}
|
return () => clearInterval(interval);
|
||||||
<div className="flex justify-center overflow-hidden">
|
}, [filteredPosts.length, isMobile]);
|
||||||
<div
|
|
||||||
className={`flex gap-0 transition-transform duration-500 ease-in-out ${
|
|
||||||
!isMobile
|
|
||||||
? "overflow-x-auto scrollbar-hide cursor-grab active:cursor-grabbing"
|
|
||||||
: ""
|
|
||||||
}`}
|
|
||||||
style={{
|
|
||||||
transform: isMobile
|
|
||||||
? `translateX(calc(50% - 130px - ${currentIndex * 260}px))`
|
|
||||||
: "none",
|
|
||||||
scrollBehavior: !isMobile ? "smooth" : "auto",
|
|
||||||
}}
|
|
||||||
onMouseDown={
|
|
||||||
!isMobile
|
|
||||||
? (e) => {
|
|
||||||
const slider = e.currentTarget;
|
|
||||||
const startX = e.pageX - slider.offsetLeft;
|
|
||||||
const scrollLeft = slider.scrollLeft;
|
|
||||||
|
|
||||||
const handleMouseMove = (e) => {
|
// Progress animation (only on mobile)
|
||||||
const x = e.pageX - slider.offsetLeft;
|
useEffect(() => {
|
||||||
const walk = (x - startX) * 2;
|
if (filteredPosts.length <= 1 || !isMobile) return;
|
||||||
slider.scrollLeft = scrollLeft - walk;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseUp = () => {
|
const progressInterval = setInterval(() => {
|
||||||
document.removeEventListener(
|
setProgress((prev) => {
|
||||||
"mousemove",
|
if (prev >= 100) {
|
||||||
handleMouseMove,
|
return 0;
|
||||||
);
|
}
|
||||||
document.removeEventListener("mouseup", handleMouseUp);
|
return prev + 1;
|
||||||
};
|
});
|
||||||
|
}, 30); // 30ms intervals for smooth animation
|
||||||
|
|
||||||
document.addEventListener("mousemove", handleMouseMove);
|
return () => clearInterval(progressInterval);
|
||||||
document.addEventListener("mouseup", handleMouseUp);
|
}, [currentIndex, filteredPosts.length, isMobile]);
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{filteredPosts.map((relatedPost, index) => (
|
|
||||||
<div
|
|
||||||
key={relatedPost.slug}
|
|
||||||
className="flex flex-col items-center flex-shrink-0"
|
|
||||||
>
|
|
||||||
<ContentThumbnailTemplate
|
|
||||||
post={relatedPost}
|
|
||||||
variant="vertical"
|
|
||||||
slugOrder={slugOrder}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Progress bars - only show on mobile */}
|
if (filteredPosts.length === 0) {
|
||||||
{isMobile && (
|
return null;
|
||||||
<div className="flex justify-center gap-[var(--measures-spacing-008)] px-[var(--measures-spacing-064)]">
|
}
|
||||||
{filteredPosts.map((relatedPost, index) => (
|
|
||||||
<div
|
return (
|
||||||
key={relatedPost.slug}
|
<section className="py-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-064)]">
|
||||||
className="max-w-[var(--measures-spacing-056)] w-full h-[var(--measures-spacing-004)] bg-gray-200 rounded-full overflow-hidden"
|
<div className="flex flex-col gap-[var(--spacing-scale-032)] lg:gap-[51px]">
|
||||||
>
|
<h2 className="text-[32px] lg:text-[44px] leading-[110%] font-medium text-[var(--color-content-inverse-primary)] text-center">
|
||||||
|
Related Articles
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{/* Horizontal Articles Row - Carousel on mobile, Scrollable slider on desktop */}
|
||||||
|
<div className="flex justify-center overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`flex gap-0 transition-transform duration-500 ease-in-out ${
|
||||||
|
!isMobile
|
||||||
|
? "overflow-x-auto scrollbar-hide cursor-grab active:cursor-grabbing"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
style={transformStyle}
|
||||||
|
onMouseDown={!isMobile ? handleMouseDown : undefined}
|
||||||
|
>
|
||||||
|
{filteredPosts.map((relatedPost, index) => (
|
||||||
<div
|
<div
|
||||||
className="h-full bg-gray-600 rounded-full transition-all duration-75 ease-linear"
|
key={relatedPost.slug}
|
||||||
style={{
|
className="flex flex-col items-center flex-shrink-0"
|
||||||
width:
|
>
|
||||||
index === currentIndex
|
<ContentThumbnailTemplate
|
||||||
? `${progress}%`
|
post={relatedPost}
|
||||||
: index < currentIndex
|
variant="vertical"
|
||||||
? "100%"
|
slugOrder={slugOrder}
|
||||||
: "0%",
|
/>
|
||||||
}}
|
</div>
|
||||||
/>
|
))}
|
||||||
</div>
|
</div>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
{/* Progress bars - only show on mobile */}
|
||||||
</section>
|
{isMobile && (
|
||||||
);
|
<div className="flex justify-center gap-[var(--measures-spacing-008)] px-[var(--measures-spacing-064)]">
|
||||||
}
|
{filteredPosts.map((relatedPost, index) => (
|
||||||
|
<div
|
||||||
|
key={relatedPost.slug}
|
||||||
|
className="max-w-[var(--measures-spacing-056)] w-full h-[var(--measures-spacing-004)] bg-gray-200 rounded-full overflow-hidden"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="h-full bg-gray-600 rounded-full transition-all duration-75 ease-linear"
|
||||||
|
style={getProgressStyle(index)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
RelatedArticles.displayName = "RelatedArticles";
|
||||||
|
|
||||||
|
export default RelatedArticles;
|
||||||
|
|||||||
+69
-63
@@ -1,73 +1,79 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
const RuleCard = ({
|
import React, { memo } from "react";
|
||||||
title,
|
|
||||||
description,
|
|
||||||
icon,
|
|
||||||
backgroundColor = "bg-[var(--color-community-teal-100)]",
|
|
||||||
className = "",
|
|
||||||
onClick,
|
|
||||||
}) => {
|
|
||||||
const handleClick = () => {
|
|
||||||
// Basic analytics event tracking
|
|
||||||
if (typeof window !== "undefined" && window.gtag) {
|
|
||||||
window.gtag("event", "template_selected", {
|
|
||||||
template_name: title,
|
|
||||||
template_type: "governance_pattern",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom analytics event for other tracking systems
|
const RuleCard = memo(
|
||||||
if (typeof window !== "undefined" && window.analytics) {
|
({
|
||||||
window.analytics.track("Template Selected", {
|
title,
|
||||||
templateName: title,
|
description,
|
||||||
templateType: "governance_pattern",
|
icon,
|
||||||
});
|
backgroundColor = "bg-[var(--color-community-teal-100)]",
|
||||||
}
|
className = "",
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
const handleClick = () => {
|
||||||
|
// Basic analytics event tracking
|
||||||
|
if (typeof window !== "undefined" && window.gtag) {
|
||||||
|
window.gtag("event", "template_selected", {
|
||||||
|
template_name: title,
|
||||||
|
template_type: "governance_pattern",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (onClick) onClick();
|
// Custom analytics event for other tracking systems
|
||||||
};
|
if (typeof window !== "undefined" && window.analytics) {
|
||||||
|
window.analytics.track("Template Selected", {
|
||||||
|
templateName: title,
|
||||||
|
templateType: "governance_pattern",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeyDown = (event) => {
|
if (onClick) onClick();
|
||||||
if (event.key === "Enter" || event.key === " ") {
|
};
|
||||||
event.preventDefault();
|
|
||||||
handleClick();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
const handleKeyDown = (event) => {
|
||||||
<div
|
if (event.key === "Enter" || event.key === " ") {
|
||||||
className={`${backgroundColor} rounded-[var(--radius-measures-radius-small)] pt-[var(--spacing-scale-012)] pr-[var(--spacing-scale-012)] pl-[var(--spacing-scale-012)] pb-[var(--spacing-scale-024)] md:p-[var(--spacing-scale-024)] md:h-[210px] lg:h-[277px] flex flex-col gap-[18px] shadow-lg backdrop-blur-sm transition-all duration-500 ease-in-out hover:shadow-xl hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-[var(--color-community-teal-500)] focus:ring-offset-2 cursor-pointer min-h-[44px] min-w-[44px] ${className}`}
|
event.preventDefault();
|
||||||
tabIndex={0}
|
handleClick();
|
||||||
role="button"
|
}
|
||||||
aria-label={`Learn more about ${title} governance pattern`}
|
};
|
||||||
onClick={handleClick}
|
|
||||||
onKeyDown={handleKeyDown}
|
return (
|
||||||
>
|
<div
|
||||||
{/* Header Container */}
|
className={`${backgroundColor} rounded-[var(--radius-measures-radius-small)] pt-[var(--spacing-scale-012)] pr-[var(--spacing-scale-012)] pl-[var(--spacing-scale-012)] pb-[var(--spacing-scale-024)] md:p-[var(--spacing-scale-024)] md:h-[210px] lg:h-[277px] flex flex-col gap-[18px] shadow-lg backdrop-blur-sm transition-all duration-500 ease-in-out hover:shadow-xl hover:scale-[1.02] focus:outline-none focus:ring-2 focus:ring-[var(--color-community-teal-500)] focus:ring-offset-2 cursor-pointer min-h-[44px] min-w-[44px] ${className}`}
|
||||||
<div className="grid grid-cols-[auto_1fr] h-[72px] md:h-[80px] lg:h-[138px] border-b border-[var(--color-surface-default-primary)]">
|
tabIndex={0}
|
||||||
{/* Icon Container */}
|
role="button"
|
||||||
{icon && (
|
aria-label={`Learn more about ${title} governance pattern`}
|
||||||
<div className="p-[var(--spacing-scale-016)] md:p-[var(--spacing-scale-012)] lg:p-[var(--spacing-scale-024)] border-r border-[var(--color-surface-default-primary)] w-fit flex items-center justify-center">
|
onClick={handleClick}
|
||||||
{icon}
|
onKeyDown={handleKeyDown}
|
||||||
</div>
|
>
|
||||||
)}
|
{/* Header Container */}
|
||||||
{/* Title Container */}
|
<div className="grid grid-cols-[auto_1fr] h-[72px] md:h-[80px] lg:h-[138px] border-b border-[var(--color-surface-default-primary)]">
|
||||||
{title && (
|
{/* Icon Container */}
|
||||||
<div className="pl-[var(--spacing-scale-008)] md:pl-[var(--spacing-scale-012)] lg:pl-[var(--spacing-scale-024)] flex items-center gap-[var(--spacing-scale-004)]">
|
{icon && (
|
||||||
<h3 className="font-space-grotesk font-bold text-[20px] md:text-[28px] lg:text-[36px] leading-[28px] md:leading-[36px] lg:leading-[44px] text-[--color-content-inverse-primary]">
|
<div className="p-[var(--spacing-scale-016)] md:p-[var(--spacing-scale-012)] lg:p-[var(--spacing-scale-024)] border-r border-[var(--color-surface-default-primary)] w-fit flex items-center justify-center">
|
||||||
{title}
|
{icon}
|
||||||
</h3>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
{/* Title Container */}
|
||||||
|
{title && (
|
||||||
|
<div className="pl-[var(--spacing-scale-008)] md:pl-[var(--spacing-scale-012)] lg:pl-[var(--spacing-scale-024)] flex items-center gap-[var(--spacing-scale-004)]">
|
||||||
|
<h3 className="font-space-grotesk font-bold text-[20px] md:text-[28px] lg:text-[36px] leading-[28px] md:leading-[36px] lg:leading-[44px] text-[--color-content-inverse-primary]">
|
||||||
|
{title}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{description && (
|
||||||
|
<p className="font-inter font-medium text-[12px] md:text-[14px] lg:text-[18px] leading-[14px] md:leading-[16px] lg:leading-[24px] text-[var(--color-content-inverse-primary)]">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{description && (
|
);
|
||||||
<p className="font-inter font-medium text-[12px] md:text-[14px] lg:text-[18px] leading-[14px] md:leading-[16px] lg:leading-[24px] text-[var(--color-content-inverse-primary)]">
|
}
|
||||||
{description}
|
);
|
||||||
</p>
|
|
||||||
)}
|
RuleCard.displayName = "RuleCard";
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RuleCard;
|
export default RuleCard;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import React from "react";
|
import React, { memo } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import RuleCard from "./RuleCard";
|
import RuleCard from "./RuleCard";
|
||||||
import Button from "./Button";
|
import Button from "./Button";
|
||||||
import { getAssetPath } from "../../lib/assetUtils";
|
import { getAssetPath } from "../../lib/assetUtils";
|
||||||
|
|
||||||
const RuleStack = ({ className = "" }) => {
|
const RuleStack = memo(({ className = "" }) => {
|
||||||
const handleTemplateClick = (templateName) => {
|
const handleTemplateClick = (templateName) => {
|
||||||
// Basic analytics tracking
|
// Basic analytics tracking
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
@@ -99,6 +99,8 @@ const RuleStack = ({ className = "" }) => {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
RuleStack.displayName = "RuleStack";
|
||||||
|
|
||||||
export default RuleStack;
|
export default RuleStack;
|
||||||
|
|||||||
@@ -1,54 +1,60 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
const SectionHeader = ({ title, subtitle, titleLg, variant = "default" }) => {
|
import React, { memo } from "react";
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
variant === "multi-line"
|
|
||||||
? "flex flex-col gap-[var(--spacing-scale-004)] w-full lg:flex-row lg:justify-between lg:items-start xl:gap-[var(--spacing-scale-024)]"
|
|
||||||
: "flex flex-col gap-[var(--spacing-scale-004)] w-full lg:flex-row lg:justify-between lg:items-start xl:gap-[var(--spacing-scale-024)]"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{/* Title Container - Left side (lg breakpoint) */}
|
|
||||||
<div
|
|
||||||
className={
|
|
||||||
variant === "multi-line"
|
|
||||||
? "lg:w-[50%] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center xl:w-[50%] xl:h-[156px] xl:flex xl:items-center"
|
|
||||||
: "lg:w-[369px] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center xl:w-[452px] xl:h-[156px] xl:flex xl:items-center"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<h2
|
|
||||||
className={
|
|
||||||
variant === "multi-line"
|
|
||||||
? "font-bricolage-grotesque font-bold text-[28px] leading-[36px] md:font-bold md:text-[32px] md:leading-[40px] lg:w-[410px] lg:text-left xl:text-[40px] xl:leading-[52px] text-[var(--color-content-default-primary)]"
|
|
||||||
: "font-bricolage-grotesque font-bold text-[28px] leading-[36px] sm:text-[32px] sm:leading-[40px] lg:text-[32px] lg:leading-[40px] lg:w-[369px] lg:pr-[var(--spacing-scale-096)] xl:text-[40px] xl:leading-[52px] xl:w-[452px] xl:pr-[var(--spacing-scale-096)] text-[var(--color-content-default-primary)]"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="block lg:hidden">{title}</span>
|
|
||||||
<span className="hidden lg:block">{titleLg || title}</span>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Subtitle Container */}
|
const SectionHeader = memo(
|
||||||
|
({ title, subtitle, titleLg, variant = "default" }) => {
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
variant === "multi-line"
|
variant === "multi-line"
|
||||||
? "lg:w-[50%] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center lg:justify-end lg:ml-[var(--spacing-scale-016)] xl:ml-[0px] xl:w-[50%] xl:h-[156px] xl:flex xl:items-center xl:justify-end"
|
? "flex flex-col gap-[var(--spacing-scale-004)] w-full lg:flex-row lg:justify-between lg:items-start xl:gap-[var(--spacing-scale-024)]"
|
||||||
: "lg:w-[928px] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center lg:justify-end xl:h-[156px] xl:flex xl:items-center xl:justify-end"
|
: "flex flex-col gap-[var(--spacing-scale-004)] w-full lg:flex-row lg:justify-between lg:items-start xl:gap-[var(--spacing-scale-024)]"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<p
|
{/* Title Container - Left side (lg breakpoint) */}
|
||||||
|
<div
|
||||||
className={
|
className={
|
||||||
variant === "multi-line"
|
variant === "multi-line"
|
||||||
? "font-inter font-normal text-[14px] leading-[20px] md:font-normal md:text-[18px] md:leading-[130%] xl:text-[24px] xl:leading-[32px] text-[var(--color-content-default-tertiary)]"
|
? "lg:w-[50%] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center xl:w-[50%] xl:h-[156px] xl:flex xl:items-center"
|
||||||
: "font-inter font-normal text-[18px] leading-[130%] sm:text-[18px] sm:leading-[32px] lg:text-[24px] lg:leading-[32px] xl:text-[32px] xl:leading-[40px] xl:text-right text-[#484848] sm:text-[var(--color-content-default-tertiary)] lg:text-[var(--color-content-default-tertiary)] xl:text-[var(--color-content-default-tertiary)] tracking-[0px]"
|
: "lg:w-[369px] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center xl:w-[452px] xl:h-[156px] xl:flex xl:items-center"
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{subtitle}
|
<h2
|
||||||
</p>
|
className={
|
||||||
|
variant === "multi-line"
|
||||||
|
? "font-bricolage-grotesque font-bold text-[28px] leading-[36px] md:font-bold md:text-[32px] md:leading-[40px] lg:w-[410px] lg:text-left xl:text-[40px] xl:leading-[52px] text-[var(--color-content-default-primary)]"
|
||||||
|
: "font-bricolage-grotesque font-bold text-[28px] leading-[36px] sm:text-[32px] sm:leading-[40px] lg:text-[32px] lg:leading-[40px] lg:w-[369px] lg:pr-[var(--spacing-scale-096)] xl:text-[40px] xl:leading-[52px] xl:w-[452px] xl:pr-[var(--spacing-scale-096)] text-[var(--color-content-default-primary)]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="block lg:hidden">{title}</span>
|
||||||
|
<span className="hidden lg:block">{titleLg || title}</span>
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subtitle Container */}
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
variant === "multi-line"
|
||||||
|
? "lg:w-[50%] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center lg:justify-end lg:ml-[var(--spacing-scale-016)] xl:ml-[0px] xl:w-[50%] xl:h-[156px] xl:flex xl:items-center xl:justify-end"
|
||||||
|
: "lg:w-[928px] lg:h-[var(--spacing-scale-120)] lg:flex lg:items-center lg:justify-end xl:h-[156px] xl:flex xl:items-center xl:justify-end"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
className={
|
||||||
|
variant === "multi-line"
|
||||||
|
? "font-inter font-normal text-[14px] leading-[20px] md:font-normal md:text-[18px] md:leading-[130%] xl:text-[24px] xl:leading-[32px] text-[var(--color-content-default-tertiary)]"
|
||||||
|
: "font-inter font-normal text-[18px] leading-[130%] sm:text-[18px] sm:leading-[32px] lg:text-[24px] lg:leading-[32px] xl:text-[32px] xl:leading-[40px] xl:text-right text-[#484848] sm:text-[var(--color-content-default-tertiary)] lg:text-[var(--color-content-default-tertiary)] xl:text-[var(--color-content-default-tertiary)] tracking-[0px]"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{subtitle}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
}
|
||||||
};
|
);
|
||||||
|
|
||||||
|
SectionHeader.displayName = "SectionHeader";
|
||||||
|
|
||||||
export default SectionHeader;
|
export default SectionHeader;
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
const SectionNumber = ({ number }) => {
|
import React, { memo } from "react";
|
||||||
|
|
||||||
|
const SectionNumber = memo(({ number }) => {
|
||||||
const getImageSrc = (num) => {
|
const getImageSrc = (num) => {
|
||||||
switch (num) {
|
switch (num) {
|
||||||
case 1:
|
case 1:
|
||||||
return "assets/SectionNumber_1.png";
|
return "/assets/SectionNumber_1.png";
|
||||||
case 2:
|
case 2:
|
||||||
return "assets/SectionNumber_2.png";
|
return "/assets/SectionNumber_2.png";
|
||||||
case 3:
|
case 3:
|
||||||
return "assets/SectionNumber_3.png";
|
return "/assets/SectionNumber_3.png";
|
||||||
default:
|
default:
|
||||||
return "assets/SectionNumber_1.png";
|
return "/assets/SectionNumber_1.png";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -28,6 +30,8 @@ const SectionNumber = ({ number }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
SectionNumber.displayName = "SectionNumber";
|
||||||
|
|
||||||
export default SectionNumber;
|
export default SectionNumber;
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
export default function Separator() {
|
import React, { memo } from "react";
|
||||||
|
|
||||||
|
const Separator = memo(() => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center self-stretch">
|
<div className="flex flex-col items-center self-stretch">
|
||||||
<div className="flex items-start self-stretch h-px w-full bg-[var(--border-color-default-secondary)]" />
|
<div className="flex items-start self-stretch h-px w-full bg-[var(--border-color-default-secondary)]" />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
Separator.displayName = "Separator";
|
||||||
|
|
||||||
|
export default Separator;
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const inter = Inter({
|
|||||||
weight: ["400", "500", "600", "700"],
|
weight: ["400", "500", "600", "700"],
|
||||||
variable: "--font-inter",
|
variable: "--font-inter",
|
||||||
display: "swap",
|
display: "swap",
|
||||||
|
preload: true,
|
||||||
|
fallback: ["system-ui", "arial"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const bricolageGrotesque = Bricolage_Grotesque({
|
const bricolageGrotesque = Bricolage_Grotesque({
|
||||||
@@ -17,6 +19,8 @@ const bricolageGrotesque = Bricolage_Grotesque({
|
|||||||
weight: ["400", "500", "700", "800"],
|
weight: ["400", "500", "700", "800"],
|
||||||
variable: "--font-bricolage-grotesque",
|
variable: "--font-bricolage-grotesque",
|
||||||
display: "swap",
|
display: "swap",
|
||||||
|
preload: true,
|
||||||
|
fallback: ["system-ui", "arial"],
|
||||||
});
|
});
|
||||||
|
|
||||||
const spaceGrotesk = Space_Grotesk({
|
const spaceGrotesk = Space_Grotesk({
|
||||||
@@ -24,6 +28,8 @@ const spaceGrotesk = Space_Grotesk({
|
|||||||
weight: ["400", "500", "700"],
|
weight: ["400", "500", "700"],
|
||||||
variable: "--font-space-grotesk",
|
variable: "--font-space-grotesk",
|
||||||
display: "swap",
|
display: "swap",
|
||||||
|
preload: true,
|
||||||
|
fallback: ["system-ui", "arial"],
|
||||||
});
|
});
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
|
|||||||
+58
-1
@@ -5,12 +5,69 @@ const nextConfig = {
|
|||||||
eslint: {
|
eslint: {
|
||||||
ignoreDuringBuilds: true,
|
ignoreDuringBuilds: true,
|
||||||
},
|
},
|
||||||
webpack(config) {
|
// Performance optimizations
|
||||||
|
experimental: {
|
||||||
|
optimizeCss: true,
|
||||||
|
optimizePackageImports: ["react", "react-dom"],
|
||||||
|
},
|
||||||
|
// Compression
|
||||||
|
compress: true,
|
||||||
|
// Image optimization
|
||||||
|
images: {
|
||||||
|
formats: ["image/webp", "image/avif"],
|
||||||
|
minimumCacheTTL: 60,
|
||||||
|
dangerouslyAllowSVG: true,
|
||||||
|
contentSecurityPolicy: "default-src 'self'; script-src 'none'; sandbox;",
|
||||||
|
},
|
||||||
|
// Headers for caching
|
||||||
|
async headers() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
source: "/(.*)",
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: "X-Content-Type-Options",
|
||||||
|
value: "nosniff",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "X-Frame-Options",
|
||||||
|
value: "DENY",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "X-XSS-Protection",
|
||||||
|
value: "1; mode=block",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: "/static/(.*)",
|
||||||
|
headers: [
|
||||||
|
{
|
||||||
|
key: "Cache-Control",
|
||||||
|
value: "public, max-age=31536000, immutable",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
webpack(config, { dev, isServer }) {
|
||||||
|
// SVG handling
|
||||||
config.module.rules.push({
|
config.module.rules.push({
|
||||||
test: /\.svg$/,
|
test: /\.svg$/,
|
||||||
issuer: /\.[jt]sx?$/,
|
issuer: /\.[jt]sx?$/,
|
||||||
use: ["@svgr/webpack"],
|
use: ["@svgr/webpack"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Production optimizations
|
||||||
|
if (!dev && !isServer) {
|
||||||
|
// Tree shaking optimization
|
||||||
|
config.optimization = {
|
||||||
|
...config.optimization,
|
||||||
|
usedExports: true,
|
||||||
|
sideEffects: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+96
-16
@@ -12,6 +12,7 @@
|
|||||||
"@mdx-js/loader": "^3.1.1",
|
"@mdx-js/loader": "^3.1.1",
|
||||||
"@mdx-js/react": "^3.1.1",
|
"@mdx-js/react": "^3.1.1",
|
||||||
"@next/mdx": "^15.5.2",
|
"@next/mdx": "^15.5.2",
|
||||||
|
"critters": "^0.0.23",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -8097,7 +8098,6 @@
|
|||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-convert": "^2.0.1"
|
"color-convert": "^2.0.1"
|
||||||
@@ -8890,7 +8890,6 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
|
||||||
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==",
|
||||||
"dev": true,
|
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
@@ -9185,7 +9184,6 @@
|
|||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ansi-styles": "^4.1.0",
|
"ansi-styles": "^4.1.0",
|
||||||
@@ -9491,7 +9489,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"color-name": "~1.1.4"
|
"color-name": "~1.1.4"
|
||||||
@@ -9504,7 +9501,6 @@
|
|||||||
"version": "1.1.4",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||||
"devOptional": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/color-string": {
|
"node_modules/color-string": {
|
||||||
@@ -9778,6 +9774,95 @@
|
|||||||
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/critters": {
|
||||||
|
"version": "0.0.23",
|
||||||
|
"resolved": "https://registry.npmjs.org/critters/-/critters-0.0.23.tgz",
|
||||||
|
"integrity": "sha512-/MCsQbuzTPA/ZTOjjyr2Na5o3lRpr8vd0MZE8tMP0OBNg/VrLxWHteVKalQ8KR+fBmUadbJLdoyEz9sT+q84qg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"chalk": "^4.1.0",
|
||||||
|
"css-select": "^5.1.0",
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"htmlparser2": "^8.0.2",
|
||||||
|
"postcss": "^8.4.23",
|
||||||
|
"postcss-media-query-parser": "^0.2.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/critters/node_modules/dom-serializer": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.2",
|
||||||
|
"entities": "^4.2.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/cheeriojs/dom-serializer?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/critters/node_modules/domelementtype": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "BSD-2-Clause"
|
||||||
|
},
|
||||||
|
"node_modules/critters/node_modules/domhandler": {
|
||||||
|
"version": "5.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 4"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domhandler?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/critters/node_modules/domutils": {
|
||||||
|
"version": "3.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"dom-serializer": "^2.0.0",
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/critters/node_modules/htmlparser2": {
|
||||||
|
"version": "8.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||||
|
"integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==",
|
||||||
|
"funding": [
|
||||||
|
"https://github.com/fb55/htmlparser2?sponsor=1",
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/fb55"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"domelementtype": "^2.3.0",
|
||||||
|
"domhandler": "^5.0.3",
|
||||||
|
"domutils": "^3.0.1",
|
||||||
|
"entities": "^4.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@@ -9814,7 +9899,6 @@
|
|||||||
"version": "5.2.2",
|
"version": "5.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz",
|
||||||
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
"integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"boolbase": "^1.0.0",
|
"boolbase": "^1.0.0",
|
||||||
@@ -9831,7 +9915,6 @@
|
|||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz",
|
||||||
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
"integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"domelementtype": "^2.3.0",
|
"domelementtype": "^2.3.0",
|
||||||
@@ -9846,7 +9929,6 @@
|
|||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz",
|
||||||
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
"integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -9859,7 +9941,6 @@
|
|||||||
"version": "5.0.3",
|
"version": "5.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz",
|
||||||
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
"integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"domelementtype": "^2.3.0"
|
"domelementtype": "^2.3.0"
|
||||||
@@ -9875,7 +9956,6 @@
|
|||||||
"version": "3.2.2",
|
"version": "3.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz",
|
||||||
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
"integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dom-serializer": "^2.0.0",
|
"dom-serializer": "^2.0.0",
|
||||||
@@ -9904,7 +9984,6 @@
|
|||||||
"version": "6.2.2",
|
"version": "6.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz",
|
||||||
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
"integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
@@ -10576,7 +10655,6 @@
|
|||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.12"
|
"node": ">=0.12"
|
||||||
@@ -12782,7 +12860,6 @@
|
|||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
@@ -18108,7 +18185,6 @@
|
|||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz",
|
||||||
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
"integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-2-Clause",
|
"license": "BSD-2-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"boolbase": "^1.0.0"
|
"boolbase": "^1.0.0"
|
||||||
@@ -19163,7 +19239,6 @@
|
|||||||
"version": "8.5.6",
|
"version": "8.5.6",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -19188,6 +19263,12 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/postcss-media-query-parser": {
|
||||||
|
"version": "0.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz",
|
||||||
|
"integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/prelude-ls": {
|
"node_modules/prelude-ls": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||||
@@ -21534,7 +21615,6 @@
|
|||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"has-flag": "^4.0.0"
|
"has-flag": "^4.0.0"
|
||||||
|
|||||||
@@ -39,6 +39,7 @@
|
|||||||
"@mdx-js/loader": "^3.1.1",
|
"@mdx-js/loader": "^3.1.1",
|
||||||
"@mdx-js/react": "^3.1.1",
|
"@mdx-js/react": "^3.1.1",
|
||||||
"@next/mdx": "^15.5.2",
|
"@next/mdx": "^15.5.2",
|
||||||
|
"critters": "^0.0.23",
|
||||||
"gray-matter": "^4.0.3",
|
"gray-matter": "^4.0.3",
|
||||||
"next": "15.2.4",
|
"next": "15.2.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user