commit 3a8ced323884c51477637b4a256ea26e1d0e7c0d
parent a5d74574ba56d110512f0b3dd72a64c29e53f966
Author: triesap <triesap@radroots.dev>
Date: Thu, 22 Jan 2026 04:38:08 +0000
app: add lucide icon foundation
- add icondata lucide dependency to ui-components
- introduce RadrootsAppUiIconKey and key parsing helper
- render lucide svg via RadrootsAppUiIcon
- cover icon mapping with unit tests
Diffstat:
5 files changed, 150 insertions(+), 0 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
@@ -885,6 +885,31 @@ dependencies = [
]
[[package]]
+name = "icondata"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "18db9eec46b6c870bf84281dd8065fb207b23a9c1c0cb367c49a919a61a38dec"
+dependencies = [
+ "icondata_core",
+ "icondata_lu",
+]
+
+[[package]]
+name = "icondata_core"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c97be924215abd5e630d84e95a47c710138a6559b4c55039f4f33aa897fa859"
+
+[[package]]
+name = "icondata_lu"
+version = "0.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d552c45cc3ab1d1bf88cc0201004eb92418141e5454e9e0e46c4b4a4faf66248"
+dependencies = [
+ "icondata_core",
+]
+
+[[package]]
name = "icu_collections"
version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1714,6 +1739,7 @@ dependencies = [
name = "radroots-app-ui-components"
version = "0.1.0"
dependencies = [
+ "icondata",
"leptos",
"radroots-app-ui-core",
"radroots-app-ui-primitives",
diff --git a/Cargo.toml b/Cargo.toml
@@ -24,6 +24,7 @@ license = "AGPL-3.0"
[workspace.dependencies]
leptos = { version = "0.8.5", default-features = false }
leptos_router = { version = "0.8.5", default-features = false }
+icondata = { version = "0.4", default-features = false, features = ["lucide"] }
wasm-bindgen = "=0.2.100"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
diff --git a/crates/ui-components/Cargo.toml b/crates/ui-components/Cargo.toml
@@ -13,3 +13,4 @@ crate-type = ["rlib"]
radroots-app-ui-core = { path = "../ui-core" }
radroots-app-ui-primitives = { path = "../ui-primitives" }
leptos = { workspace = true, features = ["csr"] }
+icondata.workspace = true
diff --git a/crates/ui-components/src/icon.rs b/crates/ui-components/src/icon.rs
@@ -0,0 +1,115 @@
+#![forbid(unsafe_code)]
+
+use icondata::{Icon, LuChevronRight, LuChevronsUpDown, LuPlus};
+use leptos::prelude::*;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
+pub enum RadrootsAppUiIconKey {
+ CaretRight,
+ CaretUpDown,
+ Plus,
+}
+
+impl RadrootsAppUiIconKey {
+ pub const fn as_str(self) -> &'static str {
+ match self {
+ RadrootsAppUiIconKey::CaretRight => "caret-right",
+ RadrootsAppUiIconKey::CaretUpDown => "caret-up-down",
+ RadrootsAppUiIconKey::Plus => "plus",
+ }
+ }
+}
+
+pub fn radroots_app_ui_icon_key_from_name(name: &str) -> Option<RadrootsAppUiIconKey> {
+ match name {
+ "caret-right" | "chevron-right" => Some(RadrootsAppUiIconKey::CaretRight),
+ "caret-up-down" | "chevrons-up-down" => Some(RadrootsAppUiIconKey::CaretUpDown),
+ "plus" => Some(RadrootsAppUiIconKey::Plus),
+ _ => None,
+ }
+}
+
+pub fn radroots_app_ui_icon_data(key: RadrootsAppUiIconKey) -> Icon {
+ match key {
+ RadrootsAppUiIconKey::CaretRight => LuChevronRight,
+ RadrootsAppUiIconKey::CaretUpDown => LuChevronsUpDown,
+ RadrootsAppUiIconKey::Plus => LuPlus,
+ }
+}
+
+#[component]
+pub fn RadrootsAppUiIcon(
+ key: RadrootsAppUiIconKey,
+ #[prop(optional)] class: Option<String>,
+ #[prop(optional)] size: Option<u32>,
+) -> impl IntoView {
+ let icon = radroots_app_ui_icon_data(key);
+ let class_value = class.unwrap_or_default();
+ let size_value = size.unwrap_or(20).to_string();
+ let view_box = icon.view_box.unwrap_or("0 0 24 24");
+ let stroke = icon.stroke.unwrap_or("currentColor");
+ let fill = icon.fill.unwrap_or("none");
+ let stroke_width = icon.stroke_width.unwrap_or("2");
+ let stroke_linecap = icon.stroke_linecap.unwrap_or("round");
+ let stroke_linejoin = icon.stroke_linejoin.unwrap_or("round");
+ view! {
+ <svg
+ class=class_value
+ width=size_value.clone()
+ height=size_value
+ viewBox=view_box
+ fill=fill
+ stroke=stroke
+ stroke-width=stroke_width
+ stroke-linecap=stroke_linecap
+ stroke-linejoin=stroke_linejoin
+ xmlns="http://www.w3.org/2000/svg"
+ focusable="false"
+ aria-hidden="true"
+ attr:style=icon.style
+ attr:x=icon.x
+ attr:y=icon.y
+ inner_html=icon.data
+ />
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{
+ radroots_app_ui_icon_key_from_name,
+ radroots_app_ui_icon_data,
+ RadrootsAppUiIconKey,
+ };
+
+ #[test]
+ fn icon_key_parses_names() {
+ assert_eq!(
+ radroots_app_ui_icon_key_from_name("caret-right"),
+ Some(RadrootsAppUiIconKey::CaretRight)
+ );
+ assert_eq!(
+ radroots_app_ui_icon_key_from_name("chevron-right"),
+ Some(RadrootsAppUiIconKey::CaretRight)
+ );
+ assert_eq!(
+ radroots_app_ui_icon_key_from_name("caret-up-down"),
+ Some(RadrootsAppUiIconKey::CaretUpDown)
+ );
+ assert_eq!(
+ radroots_app_ui_icon_key_from_name("chevrons-up-down"),
+ Some(RadrootsAppUiIconKey::CaretUpDown)
+ );
+ assert_eq!(
+ radroots_app_ui_icon_key_from_name("plus"),
+ Some(RadrootsAppUiIconKey::Plus)
+ );
+ assert_eq!(radroots_app_ui_icon_key_from_name("unknown"), None);
+ }
+
+ #[test]
+ fn icon_data_resolves() {
+ let icon = radroots_app_ui_icon_data(RadrootsAppUiIconKey::Plus);
+ assert!(!icon.data.is_empty());
+ }
+}
diff --git a/crates/ui-components/src/lib.rs b/crates/ui-components/src/lib.rs
@@ -1,12 +1,19 @@
#![forbid(unsafe_code)]
mod button;
+mod icon;
mod label;
mod separator;
mod dialog;
mod sheet;
pub use button::RadrootsAppUiButton;
+pub use icon::{
+ radroots_app_ui_icon_data,
+ radroots_app_ui_icon_key_from_name,
+ RadrootsAppUiIcon,
+ RadrootsAppUiIconKey,
+};
pub use label::RadrootsAppUiLabel;
pub use separator::{
radroots_app_ui_separator_orientation_value,