use leptos::{prelude::*, tachys::view::any_view::IntoAny};
use orbital_base_components::ThemeColor;
use orbital_style::inject_style;
use turf::inline_style_sheet_values;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextTag {
B,
Code,
Div,
Em,
H1,
H2,
H3,
H4,
H5,
H6,
I,
Label,
P,
Pre,
#[default]
Span,
Strong,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextAlign {
Center,
End,
Justify,
#[default]
Start,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextFont {
#[default]
Base,
Monospace,
Numeric,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextWeight {
Bold,
Medium,
#[default]
Regular,
Semibold,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum TextSize {
S100,
S200,
#[default]
S300,
S400,
S500,
S600,
S700,
S800,
S900,
S1000,
}
#[component]
pub fn Text(
#[prop(optional)]
tag: TextTag,
#[prop(optional)]
align: TextAlign,
#[prop(optional)]
block: bool,
#[prop(optional)]
font: TextFont,
#[prop(optional)]
italic: bool,
#[prop(optional)]
size: TextSize,
#[prop(optional)]
strikethrough: bool,
#[prop(optional)]
truncate: bool,
#[prop(optional)]
underline: bool,
#[prop(optional)]
weight: TextWeight,
#[prop(default = true)]
wrap: bool,
#[prop(optional, into)]
color: MaybeProp<ThemeColor>,
#[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional, into)] style: MaybeProp<String>,
#[prop(optional, into)]
test_id: MaybeProp<String>,
children: Children,
) -> impl IntoView {
let (style_sheet, class_names) = inline_style_sheet_values! {
.Root {
display: inline;
font-family: inherit;
margin: 0;
color: var(--orb-color-text-primary);
}
.Block {
display: block;
}
.AlignStart {
text-align: start;
}
.AlignCenter {
text-align: center;
}
.AlignEnd {
text-align: end;
}
.AlignJustify {
text-align: justify;
}
.FontBase {
font-family: var(--orb-type-family-sans);
}
.FontNumeric {
font-family: var(--orb-type-family-numeric);
font-variant-numeric: tabular-nums;
}
.FontMonospace {
font-family: var(--orb-type-family-mono);
}
.Italic {
font-style: italic;
}
.Underline {
text-decoration-line: underline;
}
.Strikethrough {
text-decoration-line: line-through;
}
.Underline.Strikethrough {
text-decoration-line: underline line-through;
}
.Wrap {
white-space: normal;
overflow-wrap: break-word;
}
.NoWrap {
white-space: nowrap;
}
.Truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 100%;
display: inline-block;
}
.Block.Truncate {
display: block;
}
.Size100 { font-size: var(--orb-type-size-2xs); line-height: var(--orb-type-line-sm); }
.Size200 { font-size: var(--orb-type-size-xs); line-height: var(--orb-type-line-sm); }
.Size300 { font-size: var(--orb-type-size-sm); line-height: var(--orb-type-line-md); }
.Size400 { font-size: var(--orb-type-size-md); line-height: var(--orb-type-line-lg); }
.Size500 { font-size: var(--orb-type-size-lg); line-height: var(--orb-type-line-xl); }
.Size600 { font-size: var(--orb-type-size-xl); line-height: 1.333; }
.Size700 { font-size: var(--orb-type-size-2xl); line-height: 1.286; }
.Size800 { font-size: var(--orb-type-size-3xl); line-height: 1.25; }
.Size900 { font-size: var(--orb-type-size-4xl); line-height: 1.3; }
.Size1000 { font-size: var(--orb-type-size-5xl); line-height: 1.35; }
.WeightRegular { font-weight: var(--orb-type-weight-regular); }
.WeightMedium { font-weight: 500; }
.WeightSemibold { font-weight: var(--orb-type-weight-semibold); }
.WeightBold { font-weight: var(--orb-type-weight-bold); }
};
inject_style("orbital-text", style_sheet);
let class = Signal::derive(move || {
let mut classes: Vec<String> = vec![class_names.root.to_string()];
if block {
classes.push(class_names.block.to_string());
}
classes.push(
match align {
TextAlign::Start => class_names.align_start,
TextAlign::Center => class_names.align_center,
TextAlign::End => class_names.align_end,
TextAlign::Justify => class_names.align_justify,
}
.to_string(),
);
classes.push(
match font {
TextFont::Base => class_names.font_base,
TextFont::Numeric => class_names.font_numeric,
TextFont::Monospace => class_names.font_monospace,
}
.to_string(),
);
if italic {
classes.push(class_names.italic.to_string());
}
if underline {
classes.push(class_names.underline.to_string());
}
if strikethrough {
classes.push(class_names.strikethrough.to_string());
}
classes.push(
match size {
TextSize::S100 => class_names.size_100,
TextSize::S200 => class_names.size_200,
TextSize::S300 => class_names.size_300,
TextSize::S400 => class_names.size_400,
TextSize::S500 => class_names.size_500,
TextSize::S600 => class_names.size_600,
TextSize::S700 => class_names.size_700,
TextSize::S800 => class_names.size_800,
TextSize::S900 => class_names.size_900,
TextSize::S1000 => class_names.size_1000,
}
.to_string(),
);
classes.push(
match weight {
TextWeight::Regular => class_names.weight_regular,
TextWeight::Medium => class_names.weight_medium,
TextWeight::Semibold => class_names.weight_semibold,
TextWeight::Bold => class_names.weight_bold,
}
.to_string(),
);
if truncate {
classes.push(class_names.truncate.to_string());
}
classes.push(if wrap {
class_names.wrap.to_string()
} else {
class_names.no_wrap.to_string()
});
if let Some(extra) = class.get() {
if !extra.trim().is_empty() {
classes.push(extra);
}
}
classes.join(" ")
});
let merged_style = move || {
let mut parts = Vec::new();
if let Some(c) = color.get() {
parts.push(format!("color: {}", c.css_var()));
}
if let Some(extra) = style.get() {
if !extra.is_empty() {
parts.push(extra);
}
}
if parts.is_empty() {
None
} else {
Some(parts.join("; "))
}
};
let rendered = match tag {
TextTag::B => {
view! { <b class=class style=merged_style data-testid=test_id>{children()}</b> }.into_any()
}
TextTag::Code => {
view! { <code class=class style=merged_style data-testid=test_id>{children()}</code> }
.into_any()
}
TextTag::Div => {
view! { <div class=class style=merged_style data-testid=test_id>{children()}</div> }.into_any()
}
TextTag::Em => {
view! { <em class=class style=merged_style data-testid=test_id>{children()}</em> }.into_any()
}
TextTag::H1 => {
view! { <h1 class=class style=merged_style data-testid=test_id>{children()}</h1> }.into_any()
}
TextTag::H2 => {
view! { <h2 class=class style=merged_style data-testid=test_id>{children()}</h2> }.into_any()
}
TextTag::H3 => {
view! { <h3 class=class style=merged_style data-testid=test_id>{children()}</h3> }.into_any()
}
TextTag::H4 => {
view! { <h4 class=class style=merged_style data-testid=test_id>{children()}</h4> }.into_any()
}
TextTag::H5 => {
view! { <h5 class=class style=merged_style data-testid=test_id>{children()}</h5> }.into_any()
}
TextTag::H6 => {
view! { <h6 class=class style=merged_style data-testid=test_id>{children()}</h6> }.into_any()
}
TextTag::I => {
view! { <i class=class style=merged_style data-testid=test_id>{children()}</i> }.into_any()
}
TextTag::Label => {
view! { <label class=class style=merged_style data-testid=test_id>{children()}</label> }
.into_any()
}
TextTag::P => {
view! { <p class=class style=merged_style data-testid=test_id>{children()}</p> }.into_any()
}
TextTag::Pre => {
view! { <pre class=class style=merged_style data-testid=test_id>{children()}</pre> }.into_any()
}
TextTag::Span => {
view! { <span class=class style=merged_style data-testid=test_id>{children()}</span> }
.into_any()
}
TextTag::Strong => {
view! { <strong class=class style=merged_style data-testid=test_id>{children()}</strong> }
.into_any()
}
};
view! {
{rendered}
}
}
macro_rules! text_preset {
($name:ident, size: $size:expr, weight: $weight:expr) => {
#[component]
pub fn $name(
#[prop(optional)] tag: TextTag,
#[prop(optional)] align: TextAlign,
#[prop(optional)] block: bool,
#[prop(default = true)] wrap: bool,
#[prop(optional)] italic: bool,
#[prop(optional)] underline: bool,
#[prop(optional)] strikethrough: bool,
#[prop(optional)] truncate: bool,
#[prop(optional, into)] color: MaybeProp<ThemeColor>,
#[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional, into)] style: MaybeProp<String>,
#[prop(optional, into)] test_id: MaybeProp<String>,
children: Children,
) -> impl IntoView {
view! {
<Text
tag=tag
align=align
block=block
wrap=wrap
italic=italic
underline=underline
strikethrough=strikethrough
truncate=truncate
color=color
class=class
style=style
test_id=test_id
size=$size
weight=$weight
>
{children()}
</Text>
}
}
};
}
text_preset!(Caption2, size: TextSize::S200, weight: TextWeight::Regular);
text_preset!(Caption2Strong, size: TextSize::S200, weight: TextWeight::Semibold);
text_preset!(Caption1, size: TextSize::S100, weight: TextWeight::Regular);
text_preset!(Caption1Strong, size: TextSize::S100, weight: TextWeight::Semibold);
text_preset!(Caption1Stronger, size: TextSize::S100, weight: TextWeight::Bold);
text_preset!(Body1, size: TextSize::S300, weight: TextWeight::Regular);
text_preset!(Body1Strong, size: TextSize::S300, weight: TextWeight::Semibold);
text_preset!(Body1Stronger, size: TextSize::S300, weight: TextWeight::Bold);
text_preset!(Body2, size: TextSize::S400, weight: TextWeight::Regular);
text_preset!(Subtitle2, size: TextSize::S400, weight: TextWeight::Semibold);
text_preset!(Subtitle2Stronger, size: TextSize::S400, weight: TextWeight::Bold);
text_preset!(Subtitle1, size: TextSize::S400, weight: TextWeight::Semibold);
text_preset!(Title3, size: TextSize::S500, weight: TextWeight::Semibold);
text_preset!(Title2, size: TextSize::S600, weight: TextWeight::Semibold);
text_preset!(Title1, size: TextSize::S700, weight: TextWeight::Semibold);
text_preset!(LargeTitle, size: TextSize::S800, weight: TextWeight::Semibold);
text_preset!(Display, size: TextSize::S1000, weight: TextWeight::Semibold);
#[component]
pub fn FormLabel(
#[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional, into)] style: MaybeProp<String>,
#[prop(optional, into)] test_id: MaybeProp<String>,
children: Children,
) -> impl IntoView {
view! {
<Caption2 tag=TextTag::Label block=true class=class style=style test_id=test_id>
{children()}
</Caption2>
}
}
#[component]
pub fn FormHint(
#[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional, into)] style: MaybeProp<String>,
#[prop(optional, into)] test_id: MaybeProp<String>,
children: Children,
) -> impl IntoView {
let (style_sheet, class_names) = inline_style_sheet_values! {
.FormHint {
color: var(--orb-color-text-tertiary);
margin-top: 4px;
}
};
inject_style("orbital-form-hint", style_sheet);
let base = class_names.form_hint.to_string();
let class_val = match class.get() {
None => base.clone(),
Some(ref s) if s.trim().is_empty() => base.clone(),
Some(ref s) => format!("{} {}", base, s),
};
view! {
<Caption2 block=true class=class_val style=style test_id=test_id>
{children()}
</Caption2>
}
}
#[component]
pub fn SectionTitle(
#[prop(optional, into)] class: MaybeProp<String>,
#[prop(optional, into)] style: MaybeProp<String>,
#[prop(optional, into)] test_id: MaybeProp<String>,
children: Children,
) -> impl IntoView {
let (style_sheet, class_names) = inline_style_sheet_values! {
.SectionTitle {
color: var(--orb-color-text-primary);
}
};
inject_style("orbital-section-title", style_sheet);
let base = class_names.section_title.to_string();
let class_val = match class.get() {
None => base.clone(),
Some(ref s) if s.trim().is_empty() => base.clone(),
Some(ref s) => format!("{} {}", base, s),
};
view! {
<Caption1Strong block=true class=class_val style=style test_id=test_id>
{children()}
</Caption1Strong>
}
}