commit 95a59b3f228c94121d5bd242f738d32fd75051b3
parent 66bf69b8e7b8635d18e2309d80edb584a3dc74fb
Author: triesap <tyson@radroots.org>
Date: Fri, 6 Feb 2026 16:18:17 +0000
ui: add form field primitives
- add form field, chip, and chips components
- add form css for labels, hints, and chip states
- export form ui helpers from ui-components
- import form styles into app css
Diffstat:
4 files changed, 106 insertions(+), 0 deletions(-)
diff --git a/app/app.css b/app/app.css
@@ -10,6 +10,7 @@
@import "./stylesheets/styles-maplibre-gl.css";
@import "./stylesheets/styles-superellipse.css";
@import "../crates/ui-components/assets/list.css";
+@import "../crates/ui-components/assets/form.css";
@import "../crates/ui-components/assets/nav.css";
@import "../crates/ui-components/assets/nav_tabs.css";
diff --git a/crates/ui-components/assets/form.css b/crates/ui-components/assets/form.css
@@ -0,0 +1,29 @@
+@layer components {
+ .form-field {
+ @apply flex flex-col gap-2;
+ }
+
+ .form-field__label {
+ @apply text-[0.72rem] font-semibold uppercase tracking-[0.08em] text-ly1-gl-label/80;
+ }
+
+ .form-field__control {
+ @apply flex flex-col gap-2;
+ }
+
+ .form-field__hint {
+ @apply text-xs text-ly1-gl-label/70;
+ }
+
+ .form-chips {
+ @apply flex flex-wrap gap-2;
+ }
+
+ .form-chip {
+ @apply inline-flex items-center justify-center rounded-full border border-ly1-edge/60 bg-ly1 px-3 py-1 text-sm text-ly1-gl transition-colors;
+ }
+
+ .form-chip[data-active="true"] {
+ @apply border-ly1-edge/80 bg-ly1-focus text-ly1-gl;
+ }
+}
diff --git a/crates/ui-components/src/form.rs b/crates/ui-components/src/form.rs
@@ -0,0 +1,74 @@
+#![forbid(unsafe_code)]
+
+use leptos::ev::MouseEvent;
+use leptos::prelude::*;
+
+use crate::RadrootsAppUiLabel;
+
+#[component]
+pub fn RadrootsAppUiFormField(
+ label: String,
+ #[prop(optional)] hint: Option<String>,
+ #[prop(optional)] id: Option<String>,
+ #[prop(optional)] class: Option<String>,
+ children: Children,
+) -> impl IntoView {
+ view! {
+ <section id=id class=class.unwrap_or_else(|| "form-field".to_string())>
+ <RadrootsAppUiLabel class="form-field__label".to_string()>
+ {label}
+ </RadrootsAppUiLabel>
+ <div class="form-field__control">{children()}</div>
+ {move || {
+ hint.clone()
+ .map(|value| view! { <p class="form-field__hint">{value}</p> }.into_any())
+ .unwrap_or_else(|| view! { <></> }.into_any())
+ }}
+ </section>
+ }
+}
+
+#[component]
+pub fn RadrootsAppUiChips(
+ #[prop(optional)] id: Option<String>,
+ #[prop(optional)] class: Option<String>,
+ children: Children,
+) -> impl IntoView {
+ let class_value = match class {
+ Some(value) => format!("form-chips {value}"),
+ None => "form-chips".to_string(),
+ };
+ view! {
+ <div id=id class=class_value>
+ {children()}
+ </div>
+ }
+}
+
+#[component]
+pub fn RadrootsAppUiChip(
+ label: String,
+ active: bool,
+ #[prop(optional)] class: Option<String>,
+ #[prop(optional)] on_click: Option<Callback<MouseEvent>>,
+) -> impl IntoView {
+ let class_value = match class {
+ Some(value) => format!("form-chip {value}"),
+ None => "form-chip".to_string(),
+ };
+ let on_click = move |ev: MouseEvent| {
+ if let Some(handler) = on_click {
+ handler.run(ev);
+ }
+ };
+ view! {
+ <button
+ type="button"
+ class=class_value
+ attr:data-active=move || if active { "true" } else { "false" }
+ on:click=on_click
+ >
+ {label}
+ </button>
+ }
+}
diff --git a/crates/ui-components/src/lib.rs b/crates/ui-components/src/lib.rs
@@ -9,6 +9,7 @@ mod list_types;
mod separator;
mod spinner;
mod dialog;
+mod form;
mod sheet;
mod scroll;
mod nav_header;
@@ -22,6 +23,7 @@ pub use button_layout::{
RadrootsAppUiButtonLayoutBottom,
RadrootsAppUiButtonLayoutPair,
};
+pub use form::{RadrootsAppUiChip, RadrootsAppUiChips, RadrootsAppUiFormField};
pub use icon::{
radroots_app_ui_icon_data,
radroots_app_ui_icon_key_from_name,