add el-style, el-layout, el-i18n, el-config, el-secrets: responsive by default, theme-driven, zero breakpoints
This commit is contained in:
@@ -0,0 +1,350 @@
|
||||
//! 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.");
|
||||
}
|
||||
Reference in New Issue
Block a user