1use crate::{
9 core::{
10 assets,
11 attrs::{component_attributes, FcData, Kind},
12 },
13 prelude::*,
14 theme::use_stylesheets,
15};
16const STATUS: Bem = Bem::new("fc-agent-status");
17const METRIC: Bem = Bem::new("fc-metric");
18const TRANSFER: Bem = Bem::new("fc-transfer");
19const APPROVAL: Bem = Bem::new("fc-approval");
20const PROMPT: Bem = Bem::new("fc-user-prompt");
21const DEVICE: Bem = Bem::new("fc-device-auth");
22
23#[derive(Debug, Clone, Default, PartialEq, Eq)]
25pub struct Record {
26 pub id: Option<Id>,
28 pub seq: Option<u64>,
30 pub state: Option<Lifecycle>,
32 pub ts: Option<Timestamp>,
34}
35impl Record {
36 fn attributes(&self, kind: Kind) -> Spread {
37 FcData {
38 kind: Some(kind),
39 id: self.id.clone(),
40 seq: self.seq,
41 state: self.state,
42 ts: self.ts.clone(),
43 ..FcData::default()
44 }
45 .attributes()
46 }
47}
48
49fn shell(
50 bem: Bem,
51 kind: Kind,
52 label: &AccessibleName,
53 record: &Record,
54 class: Option<&str>,
55 attributes: Vec<Attribute>,
56) -> Spread {
57 let mut attributes = component_attributes(
58 attributes,
59 ClassList::new(bem.block()).extend(class),
60 &["aria-label"],
61 );
62 attributes.push(Attribute::new("aria-label", label.to_string(), None, false));
63 attributes.extend(record.attributes(kind));
64 attributes
65}
66
67#[component]
69pub fn AgentStatus(
70 state: Lifecycle,
71 label: Element,
73 #[props(default)] meta: Option<Element>,
74 #[props(default = SpinnerKind::Sparkle)] spinner: SpinnerKind,
75 #[props(default)] class: Option<String>,
76 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
77) -> Element {
78 let mut attributes = component_attributes(
79 attributes,
80 ClassList::new(STATUS.block()).extend(class.as_deref()),
81 &[],
82 );
83 attributes.extend(
84 FcData {
85 kind: Some(Kind::AgentStatus),
86 state: Some(state),
87 ..FcData::default()
88 }
89 .attributes(),
90 );
91 rsx! {
92 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_AGENT_STATUS_CSS } }
93 div { ..attributes,
94 Spinner { kind: spinner, size: SpinnerSize::Sm, label: SpinnerLabel::Decorative }
95 span { class: STATUS.element("label"), role: "status", {label} }
96 if let Some(meta) = meta { span { class: STATUS.element("meta"), {meta} } }
97 }
98 }
99}
100
101#[component]
103pub fn Metric(
104 label: AccessibleName,
105 title: Element,
106 value: Option<f64>,
109 display: String,
111 #[props(default)] meta: Option<Element>,
112 #[props(default)] actions: Option<Element>,
113 #[props(default)]
115 status: Option<Element>,
116 #[props(default)]
118 region: bool,
119 #[props(default)] record: Record,
120 #[props(default)] class: Option<String>,
121 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
122) -> Element {
123 let attributes = shell(
124 METRIC,
125 Kind::Metric,
126 &label,
127 &record,
128 class.as_deref(),
129 attributes,
130 );
131 let body = rsx! {
132 {status}
133 dl {
134 dt { class: METRIC.element("title"), {title} }
135 dd { class: METRIC.element("value"),
136 if let Some(value) = value { data { value: "{value}", "{display}" } } else { "{display}" }
137 }
138 if let Some(meta) = meta { dd { class: METRIC.element("meta"), {meta} } }
139 }
140 if let Some(actions) = actions { div { class: METRIC.element("actions"), {actions} } }
141 };
142 rsx! {
143 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_METRIC_CSS } }
144 {card(region, attributes, body)}
145 }
146}
147
148#[component]
150pub fn Transfer(
151 label: AccessibleName,
152 title: Element,
153 #[props(default = HeadingLevel::H3)] heading: HeadingLevel,
154 #[props(default)]
156 meta: Option<Element>,
157 #[props(default)]
159 progress: Option<ProgressValue>,
160 #[props(default)] actions: Option<Element>,
161 #[props(default)] status: Option<Element>,
162 #[props(default)] region: bool,
163 #[props(default)] record: Record,
164 #[props(default)] class: Option<String>,
165 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
166 children: Element,
167) -> Element {
168 let attributes = shell(
169 TRANSFER,
170 Kind::Transfer,
171 &label,
172 &record,
173 class.as_deref(),
174 attributes,
175 );
176 let body = rsx! {
177 {status}
178 {super::heading(heading, TRANSFER.element("title"), title)}
179 if let Some(meta) = meta { p { class: TRANSFER.element("meta"), {meta} } }
180 {children}
181 if let Some(progress) = progress { Progress { label: label.clone(), value: progress } }
182 if let Some(actions) = actions { div { class: TRANSFER.element("actions"), {actions} } }
183 };
184 rsx! {
185 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_TRANSFER_CSS } }
186 {card(region, attributes, body)}
187 }
188}
189
190#[component]
192pub fn Approval(
193 label: AccessibleName,
194 title: Element,
195 #[props(default = HeadingLevel::H3)] heading: HeadingLevel,
196 #[props(default)] meta: Option<Element>,
197 #[props(default)]
199 actions: Option<Element>,
200 #[props(default)] status: Option<Element>,
201 #[props(default)] region: bool,
202 #[props(default)] record: Record,
203 #[props(default)] class: Option<String>,
204 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
205 children: Element,
206) -> Element {
207 let attributes = shell(
208 APPROVAL,
209 Kind::Approval,
210 &label,
211 &record,
212 class.as_deref(),
213 attributes,
214 );
215 let body = rsx! {
216 {status}
217 {super::heading(heading, APPROVAL.element("title"), title)}
218 {children}
219 if let Some(meta) = meta { p { class: APPROVAL.element("meta"), {meta} } }
220 if let Some(actions) = actions { div { class: APPROVAL.element("actions"), {actions} } }
221 };
222 rsx! {
223 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_APPROVAL_CSS } }
224 {card(region, attributes, body)}
225 }
226}
227
228#[component]
232pub fn UserPrompt(
233 label: AccessibleName,
234 title: Element,
235 #[props(default = HeadingLevel::H3)] heading: HeadingLevel,
236 #[props(default)] meta: Option<Element>,
237 #[props(default)] actions: Option<Element>,
238 #[props(default)] onsubmit: Option<EventHandler<()>>,
239 #[props(default)] status: Option<Element>,
240 #[props(default)] region: bool,
241 #[props(default)] record: Record,
242 #[props(default)] class: Option<String>,
243 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
244 children: Element,
245) -> Element {
246 let attributes = shell(
247 PROMPT,
248 Kind::UserPrompt,
249 &label,
250 &record,
251 class.as_deref(),
252 attributes,
253 );
254 let body = rsx! {
255 {status}
256 {super::heading(heading, PROMPT.element("title"), title)}
257 {children}
258 if let Some(meta) = meta { p { class: PROMPT.element("meta"), {meta} } }
259 if let Some(actions) = actions { div { class: PROMPT.element("actions"), {actions} } }
260 };
261 rsx! {
262 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_USER_PROMPT_CSS } }
263 if region {
264 section { ..attributes, {body} }
265 } else {
266 form { onsubmit: move |event| { event.prevent_default(); if let Some(onsubmit) = onsubmit { onsubmit.call(()); } },
267 ..attributes, {body}
268 }
269 }
270 }
271}
272
273#[component]
278pub fn DeviceAuth(
279 label: AccessibleName,
280 title: Element,
281 #[props(default = HeadingLevel::H3)] heading: HeadingLevel,
282 #[props(default)]
284 code: Option<String>,
285 #[props(default)] meta: Option<Element>,
286 #[props(default)] actions: Option<Element>,
287 #[props(default)] status: Option<Element>,
288 #[props(default)] region: bool,
289 #[props(default)] record: Record,
290 #[props(default)] class: Option<String>,
291 #[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
292 children: Element,
293) -> Element {
294 let attributes = shell(
295 DEVICE,
296 Kind::DeviceAuth,
297 &label,
298 &record,
299 class.as_deref(),
300 attributes,
301 );
302 let body = rsx! {
303 {status}
304 {super::heading(heading, DEVICE.element("title"), title)}
305 {children}
306 if let Some(code) = code { code { class: DEVICE.element("code"), "{code}" } }
307 if let Some(meta) = meta { p { class: DEVICE.element("meta"), {meta} } }
308 if let Some(actions) = actions { div { class: DEVICE.element("actions"), {actions} } }
309 };
310 rsx! {
311 if use_stylesheets() { document::Stylesheet { href: assets::COMPONENTS_DEVICE_AUTH_CSS } }
312 {card(region, attributes, body)}
313 }
314}
315
316fn card(region: bool, attributes: Spread, body: Element) -> Element {
319 if region {
320 rsx! { section { ..attributes, {body} } }
321 } else {
322 rsx! { article { ..attributes, {body} } }
323 }
324}