This repository has been archived on 2026-05-05. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
el-ui-retired/examples/profile-card/src/main.rs
T

351 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Profile card example — demonstrates el-ui styling, layout, i18n, config, and secrets.
//!
//! This example shows what building with el-ui looks like:
//!
//! - Styling via semantic tokens and the StyleModifier trait
//! - Responsive layout: VStack/HStack that wrap automatically
//! - Localization via LocaleContext and t()/t_plural()
//! - Configuration from el.toml / env vars
//! - Secrets that never appear in logs
use std::collections::HashMap;
use el_config::prelude::*;
use el_i18n::prelude::*;
use el_layout::prelude::*;
use el_secrets::prelude::*;
use el_style::prelude::*;
// --- Domain model ---
struct UserProfile {
handle: String,
display_name: String,
bio: String,
follower_count: i64,
following_count: i64,
is_verified: bool,
is_following: bool,
}
// --- Profile card component ---
/// A profile card component.
///
/// In a real el-ui app, this would be a .el component file compiled by
/// el-ui-compiler. Here we show the same patterns in pure Rust so the
/// example is self-contained and runnable.
struct ProfileCard {
profile: UserProfile,
theme: Theme,
style: StyleSet,
locale: LocaleContext,
}
impl StyleModifier for ProfileCard {
fn style_mut(&mut self) -> &mut StyleSet {
&mut self.style
}
}
impl ProfileCard {
fn new(profile: UserProfile, theme: Theme, locale: LocaleContext) -> Self {
Self {
profile,
theme,
style: StyleSet::default(),
locale,
}
}
/// "Render" the card — in a real app the backend converts this to
/// native views. Here we produce a human-readable description.
fn render(&self) -> String {
let t = &self.locale;
let theme = &self.theme;
// --- Header HStack (avatar + name + verified badge) ---
// HStack wraps automatically if the container is narrow (mobile-first)
let header = HStack::new()
.spacing(12)
.alignment(VAlign::Center)
.wrap(true);
// --- Name text: Title style ---
let name_style = theme.typography.resolve(&TextStyle::Title);
// --- Body text: Body style ---
let body_style = theme.typography.resolve(&TextStyle::Body);
// --- Stats HStack (followers / following) ---
let _stats_layout = HStack::new().spacing(24).wrap(true);
// --- Follow button ---
// VStack wraps children that don't fit, so this works on any screen width
let _card_layout = VStack::new()
.spacing(16)
.alignment(HAlign::Leading)
.wrap(true);
// --- Localized strings ---
let follow_label = if self.profile.is_following {
t.t("profile.following")
} else {
t.t("profile.follow")
};
let followers_label =
t.t_plural("profile.followers", self.profile.follower_count);
let following_label =
t.t_plural("profile.following_count", self.profile.following_count);
// --- Color resolution ---
let (bg_r, bg_g, bg_b, _) = theme.colors.resolve(&Color::Surface);
let (text_r, text_g, text_b, _) = theme.colors.resolve(&Color::OnSurface);
let (primary_r, primary_g, primary_b, _) = theme.colors.resolve(&Color::Primary);
// --- Shadow ---
let shadow_css = theme
.shadows
.resolve(&Shadow::Md)
.map(|s| s.to_css())
.unwrap_or_default();
// --- Formatted stats (locale-aware numbers) ---
let follower_count_fmt = format_integer(self.profile.follower_count, &self.locale.locale);
let following_count_fmt = format_integer(self.profile.following_count, &self.locale.locale);
// --- RTL layout signal ---
let layout_direction = if self.locale.is_rtl() { "rtl" } else { "ltr" };
// --- Build the output ---
format!(
r#"
┌─ ProfileCard ─────────────────────────────────────────────
│ Layout direction: {}
│ Theme mode: {:?}
│ [Card background: rgb({},{},{}), shadow: {}]
│ {} {} [Header HStack, spacing=12, wrap=true]
│ Avatar [44×44pt, radius=Full — meets touch target]
│ {} [font: {}pt weight={}, color: rgb({},{},{})]
│ {} [verified badge]
│ {} [Body style, {}pt, color: rgb({},{},{})]
│ [Stats HStack, spacing=24, wrap=true]
│ {} {} — follower count (locale-formatted)
│ {} {} — following count
│ [Button: Primary variant]
│ {} [color: rgb({},{},{})]
│ [Card ends]
└────────────────────────────────────────────────────────────
"#,
layout_direction,
theme.mode,
bg_r, bg_g, bg_b,
shadow_css,
header.spacing,
if header.wrap { "wrap=true" } else { "wrap=false" },
self.profile.display_name,
name_style.size,
name_style.weight.value(),
text_r, text_g, text_b,
if self.profile.is_verified { "" } else { "" },
self.profile.bio,
body_style.size,
text_r, text_g, text_b,
follower_count_fmt,
followers_label,
following_count_fmt,
following_label,
follow_label,
primary_r, primary_g, primary_b,
)
}
}
// --- Application entry point ---
fn main() {
// 1. Configuration — layered, typed
let el_toml = r#"
[config]
app.name = "ProfileCard Example"
app.version = "1.0.0"
profile.max_bio_length = "160"
[env.development]
app.debug = "true"
"#;
let toml_source = load_from_toml(el_toml, &Environment::Development)
.expect("el.toml should be valid");
let mut config = Config::new(Environment::Development);
config.push_source(Box::new(toml_source));
let app_name = config.get::<String>("app.name").unwrap_or_default();
let app_version = config.get::<String>("app.version").unwrap_or_default();
let max_bio: u32 = config.get_or("profile.max_bio_length", 160u32);
let debug: bool = config.get_or("app.debug", false);
println!("=== {} v{} ===", app_name, app_version);
println!("Environment: {}", config.environment);
println!("Debug mode: {}", debug);
println!("Max bio: {} chars", max_bio);
// 2. Secrets — loaded at startup, never logged
let mut secret_src = InMemorySource::new();
secret_src.insert("analytics.key", "ana_abc123xyz");
let secrets = SecretsResolver::new()
.source(Box::new(secret_src))
.require("analytics.key")
.resolve()
.expect("required secrets must be present at startup");
let analytics_key = secrets.require("analytics.key");
// This will always print [REDACTED] — never the actual key
println!("Analytics key: {} (safely [REDACTED] in logs)", analytics_key);
// To actually use it:
let _actual_key: &str = analytics_key.expose();
// 3. Localization — English
let mut en_bundle = LocaleBundle::new(Locale::en_us());
en_bundle.insert("profile.follow", "Follow");
en_bundle.insert("profile.following", "Following");
let mut forms = HashMap::new();
forms.insert("one".to_string(), "{n} Follower".to_string());
forms.insert("other".to_string(), "{n} Followers".to_string());
en_bundle.insert_plural("profile.followers", forms);
let mut following_forms = HashMap::new();
following_forms.insert("one".to_string(), "{n} Following".to_string());
following_forms.insert("other".to_string(), "{n} Following".to_string());
en_bundle.insert_plural("profile.following_count", following_forms);
let en_ctx = LocaleContext::new(Locale::en_us(), en_bundle);
// 4. Theme — light, system colors
let light_theme = Theme::default_light();
let dark_theme = Theme::default_dark();
// 5. Profile data
let profile = UserProfile {
handle: "alice".to_string(),
display_name: "Alice Chen".to_string(),
bio: "Building beautiful things with el-ui. Rust enthusiast.".to_string(),
follower_count: 12_483,
following_count: 342,
is_verified: true,
is_following: false,
};
// 6. Render the card (light theme, English)
println!("\n=== Light Theme, English ===");
let card = ProfileCard::new(
UserProfile {
handle: profile.handle.clone(),
display_name: profile.display_name.clone(),
bio: profile.bio.clone(),
follower_count: profile.follower_count,
following_count: profile.following_count,
is_verified: profile.is_verified,
is_following: profile.is_following,
},
light_theme,
en_ctx.clone(),
);
// Apply style modifiers — fluent, zero-cost at compile time
let styled_card = card
.padding(16)
.background(Color::Surface)
.radius(Radius::Lg)
.shadow(Shadow::Md)
.max_width(Dimension::Fixed(480));
println!("{}", styled_card.render());
// 7. Render with dark theme
println!("=== Dark Theme, English ===");
let card_dark = ProfileCard::new(
UserProfile {
handle: profile.handle.clone(),
display_name: profile.display_name.clone(),
bio: profile.bio.clone(),
follower_count: 1, // test singular
following_count: profile.following_count,
is_verified: profile.is_verified,
is_following: true,
},
dark_theme,
en_ctx,
);
let styled_dark = card_dark
.padding(16)
.background(Color::Surface)
.radius(Radius::Lg)
.shadow(Shadow::Lg);
println!("{}", styled_dark.render());
// 8. Layout demonstration
println!("=== Layout Engine Demo ===");
// Grid: auto columns — picks 1, 2, 3... based on container width
let grid = GridLayout::new().columns_auto(200.0).gap(16);
for width in [300.0f32, 600.0, 900.0, 1200.0] {
println!(
" Container {}px → {} columns",
width,
grid.active_columns(width)
);
}
// Responsive value
let cols: Responsive<u32> = Responsive::fixed(1).md(2).lg(3);
println!("\n Responsive columns:");
for bp in [
Breakpoint::Base,
Breakpoint::Sm,
Breakpoint::Md,
Breakpoint::Lg,
Breakpoint::Xl,
] {
println!(" {:?}: {} col(s)", bp, cols.resolve(bp));
}
// Platform sizing
let ios_sizing = PlatformSizing::for_platform(PlatformFamily::Ios);
let android_sizing = PlatformSizing::for_platform(PlatformFamily::Android);
println!("\n Min touch targets:");
println!(" iOS: {}pt", ios_sizing.min_touch_target);
println!(" Android: {}dp", android_sizing.min_touch_target);
// 9. Locale formatting
println!("\n=== Locale-Aware Formatting ===");
let number = 1_234_567.89;
for (tag, locale) in [
("en-US", Locale::en_us()),
("de-DE", Locale::new("de-DE")),
("fr-FR", Locale::fr_fr()),
("ja-JP", Locale::ja()),
] {
let formatted = format_number(number, &locale, 2);
let currency = format_currency(1234.56, &locale, "USD");
println!(" {}: {} | {}", tag, formatted, currency);
}
// 10. RTL detection
println!("\n=== RTL Detection ===");
for tag in ["en-US", "ar-SA", "he", "fa", "zh-TW"] {
let locale = Locale::new(tag);
println!(" {}: {}", tag, if locale.is_rtl() { "RTL" } else { "LTR" });
}
println!("\nAll systems operational. el-ui is ready.");
}