use leptos::prelude::*;
use orbital_macros::component_doc;
use turf::inline_style_sheet_values;
use crate::components::{Body1, Caption1, Title3};
use crate::primitives::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HeightUnit {
Px,
Vh,
Svh,
Em,
Rem,
Percent,
}
impl HeightUnit {
fn as_str(self) -> &'static str {
match self {
HeightUnit::Px => "px",
HeightUnit::Vh => "vh",
HeightUnit::Svh => "svh",
HeightUnit::Em => "em",
HeightUnit::Rem => "rem",
HeightUnit::Percent => "%",
}
}
}
#[derive(Clone, PartialEq)]
pub struct HeroCta {
pub label: String,
pub href: String,
pub appearance: ButtonAppearance,
}
impl HeroCta {
pub fn new(
label: impl Into<String>,
href: impl Into<String>,
appearance: ButtonAppearance,
) -> Self {
Self {
label: label.into(),
href: href.into(),
appearance,
}
}
}
#[component_doc(
category = "Patterns",
preview_slug = "components/patterns/hero-section",
preview_label = "Hero Section",
preview_icon = icondata::AiRocketOutlined,
)]
#[component]
pub fn HeroSection(
#[prop(optional, default = "home".to_string())]
id: String,
title: String,
subtitle: String,
#[prop(optional, default = Vec::new())]
ctas: Vec<HeroCta>,
#[prop(optional)]
fine_print: Option<String>,
#[prop(optional, default = true)]
full_height: bool,
#[prop(optional)]
height: Option<f64>,
#[prop(optional, default = HeightUnit::Svh)]
height_unit: HeightUnit,
) -> impl IntoView {
let height_css = Signal::derive(move || {
if full_height {
"100svh".to_string()
} else if let Some(h) = height {
format!("{}{}", h, height_unit.as_str())
} else {
"100svh".to_string() }
});
let (style_sheet, class_names) = inline_style_sheet_values! {
.Layer {
position: relative;
padding: 80px clamp(16px, 4vw, 40px);
display: grid;
align-items: center;
overflow: clip;
isolation: isolate;
transform: none;
}
.Layer::before {
content: "";
position: absolute;
inset: 0;
transform: translateZ(-0.6px) scale(1.6);
z-index: 0;
pointer-events: none;
background:
radial-gradient(60% 40% at 50% 18%, color-mix(in oklab, var(--orb-color-brand-bg) 22%, transparent), transparent 60%),
radial-gradient(50% 50% at 18% 45%, color-mix(in oklab, var(--orb-color-brand-bg-subtle) 18%, transparent), transparent 55%),
radial-gradient(45% 45% at 82% 55%, color-mix(in oklab, var(--orb-color-brand-fg) 12%, transparent), transparent 55%),
linear-gradient(180deg, var(--orb-color-surface-canvas) 0%, var(--orb-color-surface-shell) 100%);
}
.LayerNoMotion::before {
transform: none;
}
.Layer > * {
position: relative;
z-index: 1;
}
.HeroInner {
max-width: 1000px;
margin-inline: auto;
text-align: center;
transform: translateZ(0);
color: var(--orb-color-text-primary);
}
.HeroFlex {
align-items: center;
}
.HeroTitle {
font-size: clamp(28px, 5vw, 56px);
line-height: 1.1;
}
.HeroSub {
margin: 12px 0 24px;
font-size: clamp(16px, 2vw, 20px);
}
.CtaRow {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 8px;
}
@media (prefers-reduced-motion: reduce) {
.Layer::before {
transform: none;
}
}
};
let prefers_reduced = crate::components::motions::use_reduced_motion();
let layer_class = Signal::derive(move || {
let mut classes = vec![class_names.layer.to_string()];
if prefers_reduced.get() {
classes.push(class_names.layer_no_motion.to_string());
}
classes.join(" ")
});
view! {
<style>{style_sheet}</style>
<section
class=layer_class
id=id.clone()
aria-label="Hero"
style=move || format!("min-height: {}", height_css.get())
>
<div class=class_names.hero_inner>
<Flex vertical=true class=class_names.hero_flex>
<Title3 tag=crate::components::TextTag::H1 class=class_names.hero_title>
{title.clone()}
</Title3>
<Body1 class=class_names.hero_sub>{subtitle.clone()}</Body1>
{if !ctas.is_empty() {
view! {
<div class=class_names.cta_row>
{ctas.into_iter().map(|cta| {
view! {
<Button appearance=cta.appearance size=ButtonSize::Large>
<a href=cta.href.clone() style="text-decoration: none; color: inherit; display: block;">
{cta.label}
</a>
</Button>
}
}).collect::<Vec<_>>()}
</div>
}.into_any()
} else {
().into_any()
}}
{if let Some(fp) = fine_print.clone() {
view! {
<Caption1>{fp}</Caption1>
}.into_any()
} else {
().into_any()
}}
</Flex>
</div>
</section>
}
}