1use crate::assets;
2use askama::Template;
3use std::fmt::{self, Write as _};
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6pub struct HtmlAttr<'a> {
7 pub name: &'a str,
8 pub value: &'a str,
9}
10
11impl<'a> HtmlAttr<'a> {
12 pub const fn new(name: &'a str, value: &'a str) -> Self {
13 Self { name, value }
14 }
15
16 pub const fn hx_get(value: &'a str) -> Self {
17 Self::new("hx-get", value)
18 }
19
20 pub const fn hx_post(value: &'a str) -> Self {
21 Self::new("hx-post", value)
22 }
23
24 pub const fn hx_put(value: &'a str) -> Self {
25 Self::new("hx-put", value)
26 }
27
28 pub const fn hx_patch(value: &'a str) -> Self {
29 Self::new("hx-patch", value)
30 }
31
32 pub const fn hx_delete(value: &'a str) -> Self {
33 Self::new("hx-delete", value)
34 }
35
36 pub const fn hx_target(value: &'a str) -> Self {
37 Self::new("hx-target", value)
38 }
39
40 pub const fn hx_swap(value: &'a str) -> Self {
41 Self::new("hx-swap", value)
42 }
43
44 pub const fn hx_trigger(value: &'a str) -> Self {
45 Self::new("hx-trigger", value)
46 }
47
48 pub const fn hx_confirm(value: &'a str) -> Self {
49 Self::new("hx-confirm", value)
50 }
51}
52
53#[derive(Clone, Copy, Debug, Eq, PartialEq)]
54pub struct TrustedHtml<'a> {
55 html: &'a str,
56}
57
58impl<'a> TrustedHtml<'a> {
59 pub const fn new(html: &'a str) -> Self {
60 Self { html }
61 }
62
63 pub const fn as_str(self) -> &'a str {
64 self.html
65 }
66}
67
68impl fmt::Display for TrustedHtml<'_> {
69 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
70 f.write_str(self.html)
71 }
72}
73
74impl askama::FastWritable for TrustedHtml<'_> {
75 #[inline]
76 fn write_into(&self, dest: &mut dyn fmt::Write, _: &dyn askama::Values) -> askama::Result<()> {
77 Ok(dest.write_str(self.html)?)
78 }
79}
80
81impl askama::filters::HtmlSafe for TrustedHtml<'_> {}
82
83#[derive(Clone, Copy, Debug, Eq, PartialEq)]
84pub enum ButtonVariant {
85 Default,
86 Primary,
87 Ghost,
88 Danger,
89}
90
91impl ButtonVariant {
92 fn class(self) -> &'static str {
93 match self {
94 Self::Default => "",
95 Self::Primary => " primary",
96 Self::Ghost => " ghost",
97 Self::Danger => " danger",
98 }
99 }
100}
101
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
103pub enum ButtonSize {
104 Default,
105 Small,
106 Large,
107}
108
109impl ButtonSize {
110 fn class(self) -> &'static str {
111 match self {
112 Self::Default => "",
113 Self::Small => " sm",
114 Self::Large => " lg",
115 }
116 }
117}
118
119#[derive(Debug, Template)]
120#[non_exhaustive]
121#[template(path = "components/button.html")]
122pub struct Button<'a> {
123 pub label: &'a str,
124 pub href: Option<&'a str>,
125 pub variant: ButtonVariant,
126 pub size: ButtonSize,
127 pub attrs: &'a [HtmlAttr<'a>],
128 pub disabled: bool,
129 pub button_type: &'a str,
130}
131
132impl<'a> Button<'a> {
133 pub const fn new(label: &'a str) -> Self {
134 Self {
135 label,
136 href: None,
137 variant: ButtonVariant::Default,
138 size: ButtonSize::Default,
139 attrs: &[],
140 disabled: false,
141 button_type: "button",
142 }
143 }
144
145 pub const fn primary(label: &'a str) -> Self {
146 Self {
147 variant: ButtonVariant::Primary,
148 ..Self::new(label)
149 }
150 }
151
152 pub const fn link(label: &'a str, href: &'a str) -> Self {
153 Self {
154 href: Some(href),
155 ..Self::new(label)
156 }
157 }
158
159 pub const fn with_href(mut self, href: &'a str) -> Self {
160 self.href = Some(href);
161 self
162 }
163
164 pub const fn with_variant(mut self, variant: ButtonVariant) -> Self {
165 self.variant = variant;
166 self
167 }
168
169 pub const fn with_size(mut self, size: ButtonSize) -> Self {
170 self.size = size;
171 self
172 }
173
174 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
175 self.attrs = attrs;
176 self
177 }
178
179 pub const fn disabled(mut self) -> Self {
180 self.disabled = true;
181 self
182 }
183
184 pub const fn with_button_type(mut self, button_type: &'a str) -> Self {
185 self.button_type = button_type;
186 self
187 }
188
189 pub fn class_name(&self) -> String {
190 format!("wf-btn{}{}", self.variant.class(), self.size.class())
191 }
192}
193
194impl<'a> askama::filters::HtmlSafe for Button<'a> {}
195
196#[derive(Clone, Copy, Debug, Eq, PartialEq)]
197pub enum FeedbackKind {
198 Info,
199 Ok,
200 Warn,
201 Error,
202}
203
204impl FeedbackKind {
205 fn class(self) -> &'static str {
206 match self {
207 Self::Info => "info",
208 Self::Ok => "ok",
209 Self::Warn => "warn",
210 Self::Error => "err",
211 }
212 }
213}
214
215#[derive(Debug, Template)]
216#[non_exhaustive]
217#[template(path = "components/alert.html")]
218pub struct Alert<'a> {
219 pub kind: FeedbackKind,
220 pub title: Option<&'a str>,
221 pub message: &'a str,
222}
223
224impl<'a> Alert<'a> {
225 pub const fn new(kind: FeedbackKind, message: &'a str) -> Self {
226 Self {
227 kind,
228 title: None,
229 message,
230 }
231 }
232
233 pub const fn with_title(mut self, title: &'a str) -> Self {
234 self.title = Some(title);
235 self
236 }
237
238 pub fn class_name(&self) -> String {
239 format!("wf-alert {}", self.kind.class())
240 }
241}
242
243impl<'a> askama::filters::HtmlSafe for Alert<'a> {}
244
245#[derive(Debug, Template)]
246#[non_exhaustive]
247#[template(path = "components/tag.html")]
248pub struct Tag<'a> {
249 pub kind: Option<FeedbackKind>,
250 pub label: &'a str,
251 pub dot: bool,
252}
253
254impl<'a> Tag<'a> {
255 pub const fn new(label: &'a str) -> Self {
256 Self {
257 kind: None,
258 label,
259 dot: false,
260 }
261 }
262
263 pub const fn status(kind: FeedbackKind, label: &'a str) -> Self {
264 Self {
265 kind: Some(kind),
266 label,
267 dot: true,
268 }
269 }
270
271 pub const fn with_kind(mut self, kind: FeedbackKind) -> Self {
272 self.kind = Some(kind);
273 self
274 }
275
276 pub const fn with_dot(mut self) -> Self {
277 self.dot = true;
278 self
279 }
280
281 pub fn class_name(&self) -> String {
282 match self.kind {
283 Some(kind) => format!("wf-tag {}", kind.class()),
284 None => "wf-tag".to_owned(),
285 }
286 }
287}
288
289impl<'a> askama::filters::HtmlSafe for Tag<'a> {}
290
291#[derive(Clone, Copy, Debug, Eq, PartialEq)]
292pub enum FieldState {
293 Default,
294 Error,
295 Success,
296}
297
298impl FieldState {
299 fn class(self) -> &'static str {
300 match self {
301 Self::Default => "",
302 Self::Error => " is-error",
303 Self::Success => " is-success",
304 }
305 }
306}
307
308#[derive(Debug, Template)]
309#[non_exhaustive]
310#[template(path = "components/field.html")]
311pub struct Field<'a> {
312 pub label: &'a str,
313 pub control_html: TrustedHtml<'a>,
314 pub hint: Option<&'a str>,
315 pub state: FieldState,
316}
317
318impl<'a> Field<'a> {
319 pub const fn new(label: &'a str, control_html: TrustedHtml<'a>) -> Self {
320 Self {
321 label,
322 control_html,
323 hint: None,
324 state: FieldState::Default,
325 }
326 }
327
328 pub const fn with_hint(mut self, hint: &'a str) -> Self {
329 self.hint = Some(hint);
330 self
331 }
332
333 pub const fn with_state(mut self, state: FieldState) -> Self {
334 self.state = state;
335 self
336 }
337
338 pub fn class_name(&self) -> String {
339 format!("wf-field{}", self.state.class())
340 }
341}
342
343impl<'a> askama::filters::HtmlSafe for Field<'a> {}
344
345#[derive(Debug, Template)]
346#[non_exhaustive]
347#[template(path = "components/form.html")]
348pub struct Form<'a> {
349 pub body_html: TrustedHtml<'a>,
350 pub action: Option<&'a str>,
351 pub method: &'a str,
352 pub enctype: Option<&'a str>,
353 pub attrs: &'a [HtmlAttr<'a>],
354}
355
356impl<'a> Form<'a> {
357 pub const fn new(body_html: TrustedHtml<'a>) -> Self {
358 Self {
359 body_html,
360 action: None,
361 method: "post",
362 enctype: None,
363 attrs: &[],
364 }
365 }
366
367 pub const fn with_action(mut self, action: &'a str) -> Self {
368 self.action = Some(action);
369 self
370 }
371
372 pub const fn with_method(mut self, method: &'a str) -> Self {
373 self.method = method;
374 self
375 }
376
377 pub const fn with_enctype(mut self, enctype: &'a str) -> Self {
378 self.enctype = Some(enctype);
379 self
380 }
381
382 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
383 self.attrs = attrs;
384 self
385 }
386}
387
388impl<'a> askama::filters::HtmlSafe for Form<'a> {}
389
390#[derive(Debug, Template)]
391#[non_exhaustive]
392#[template(path = "components/form_section.html")]
393pub struct FormSection<'a> {
394 pub title: &'a str,
395 pub body_html: TrustedHtml<'a>,
396 pub description: Option<&'a str>,
397 pub actions_html: Option<TrustedHtml<'a>>,
398}
399
400impl<'a> FormSection<'a> {
401 pub const fn new(title: &'a str, body_html: TrustedHtml<'a>) -> Self {
402 Self {
403 title,
404 body_html,
405 description: None,
406 actions_html: None,
407 }
408 }
409
410 pub const fn with_description(mut self, description: &'a str) -> Self {
411 self.description = Some(description);
412 self
413 }
414
415 pub const fn with_actions(mut self, actions_html: TrustedHtml<'a>) -> Self {
416 self.actions_html = Some(actions_html);
417 self
418 }
419}
420
421impl<'a> askama::filters::HtmlSafe for FormSection<'a> {}
422
423#[derive(Debug, Template)]
424#[non_exhaustive]
425#[template(path = "components/form_actions.html")]
426pub struct FormActions<'a> {
427 pub primary_html: TrustedHtml<'a>,
428 pub secondary_html: Option<TrustedHtml<'a>>,
429}
430
431impl<'a> FormActions<'a> {
432 pub const fn new(primary_html: TrustedHtml<'a>) -> Self {
433 Self {
434 primary_html,
435 secondary_html: None,
436 }
437 }
438
439 pub const fn with_secondary(mut self, secondary_html: TrustedHtml<'a>) -> Self {
440 self.secondary_html = Some(secondary_html);
441 self
442 }
443}
444
445impl<'a> askama::filters::HtmlSafe for FormActions<'a> {}
446
447#[derive(Debug, Template)]
448#[non_exhaustive]
449#[template(path = "components/object_fieldset.html")]
450pub struct ObjectFieldset<'a> {
451 pub legend: &'a str,
452 pub body_html: TrustedHtml<'a>,
453 pub description: Option<&'a str>,
454 pub actions_html: Option<TrustedHtml<'a>>,
455}
456
457impl<'a> ObjectFieldset<'a> {
458 pub const fn new(legend: &'a str, body_html: TrustedHtml<'a>) -> Self {
459 Self {
460 legend,
461 body_html,
462 description: None,
463 actions_html: None,
464 }
465 }
466
467 pub const fn with_description(mut self, description: &'a str) -> Self {
468 self.description = Some(description);
469 self
470 }
471
472 pub const fn with_actions(mut self, actions_html: TrustedHtml<'a>) -> Self {
473 self.actions_html = Some(actions_html);
474 self
475 }
476}
477
478impl<'a> askama::filters::HtmlSafe for ObjectFieldset<'a> {}
479
480#[derive(Debug, Template)]
481#[non_exhaustive]
482#[template(path = "components/repeatable_array.html")]
483pub struct RepeatableArray<'a> {
484 pub label: &'a str,
485 pub items_html: TrustedHtml<'a>,
486 pub description: Option<&'a str>,
487 pub action_html: Option<TrustedHtml<'a>>,
488}
489
490impl<'a> RepeatableArray<'a> {
491 pub const fn new(label: &'a str, items_html: TrustedHtml<'a>) -> Self {
492 Self {
493 label,
494 items_html,
495 description: None,
496 action_html: None,
497 }
498 }
499
500 pub const fn with_description(mut self, description: &'a str) -> Self {
501 self.description = Some(description);
502 self
503 }
504
505 pub const fn with_action(mut self, action_html: TrustedHtml<'a>) -> Self {
506 self.action_html = Some(action_html);
507 self
508 }
509}
510
511impl<'a> askama::filters::HtmlSafe for RepeatableArray<'a> {}
512
513#[derive(Debug, Template)]
514#[non_exhaustive]
515#[template(path = "components/repeatable_item.html")]
516pub struct RepeatableItem<'a> {
517 pub label: &'a str,
518 pub body_html: TrustedHtml<'a>,
519 pub actions_html: Option<TrustedHtml<'a>>,
520}
521
522impl<'a> RepeatableItem<'a> {
523 pub const fn new(label: &'a str, body_html: TrustedHtml<'a>) -> Self {
524 Self {
525 label,
526 body_html,
527 actions_html: None,
528 }
529 }
530
531 pub const fn with_actions(mut self, actions_html: TrustedHtml<'a>) -> Self {
532 self.actions_html = Some(actions_html);
533 self
534 }
535}
536
537impl<'a> askama::filters::HtmlSafe for RepeatableItem<'a> {}
538
539#[derive(Debug, Template)]
540#[non_exhaustive]
541#[template(path = "components/current_upload.html")]
542pub struct CurrentUpload<'a> {
543 pub label: &'a str,
544 pub href: &'a str,
545 pub filename: &'a str,
546 pub meta: Option<&'a str>,
547 pub thumbnail_html: Option<TrustedHtml<'a>>,
548 pub actions_html: Option<TrustedHtml<'a>>,
549}
550
551impl<'a> CurrentUpload<'a> {
552 pub const fn new(label: &'a str, href: &'a str, filename: &'a str) -> Self {
553 Self {
554 label,
555 href,
556 filename,
557 meta: None,
558 thumbnail_html: None,
559 actions_html: None,
560 }
561 }
562
563 pub const fn with_meta(mut self, meta: &'a str) -> Self {
564 self.meta = Some(meta);
565 self
566 }
567
568 pub const fn with_thumbnail(mut self, thumbnail_html: TrustedHtml<'a>) -> Self {
569 self.thumbnail_html = Some(thumbnail_html);
570 self
571 }
572
573 pub const fn with_actions(mut self, actions_html: TrustedHtml<'a>) -> Self {
574 self.actions_html = Some(actions_html);
575 self
576 }
577}
578
579impl<'a> askama::filters::HtmlSafe for CurrentUpload<'a> {}
580
581#[derive(Debug, Template)]
582#[non_exhaustive]
583#[template(path = "components/reference_select.html")]
584pub struct ReferenceSelect<'a> {
585 pub label: &'a str,
586 pub select_html: TrustedHtml<'a>,
587 pub hint: Option<&'a str>,
588}
589
590impl<'a> ReferenceSelect<'a> {
591 pub const fn new(label: &'a str, select_html: TrustedHtml<'a>) -> Self {
592 Self {
593 label,
594 select_html,
595 hint: None,
596 }
597 }
598
599 pub const fn with_hint(mut self, hint: &'a str) -> Self {
600 self.hint = Some(hint);
601 self
602 }
603}
604
605impl<'a> askama::filters::HtmlSafe for ReferenceSelect<'a> {}
606
607#[derive(Debug, Template)]
608#[non_exhaustive]
609#[template(path = "components/markdown_textarea.html")]
610pub struct MarkdownTextarea<'a> {
611 pub name: &'a str,
612 pub value: Option<&'a str>,
613 pub placeholder: Option<&'a str>,
614 pub rows: u16,
615 pub attrs: &'a [HtmlAttr<'a>],
616}
617
618impl<'a> MarkdownTextarea<'a> {
619 pub const fn new(name: &'a str) -> Self {
620 Self {
621 name,
622 value: None,
623 placeholder: None,
624 rows: 6,
625 attrs: &[],
626 }
627 }
628
629 pub const fn with_value(mut self, value: &'a str) -> Self {
630 self.value = Some(value);
631 self
632 }
633
634 pub const fn with_placeholder(mut self, placeholder: &'a str) -> Self {
635 self.placeholder = Some(placeholder);
636 self
637 }
638
639 pub const fn with_rows(mut self, rows: u16) -> Self {
640 self.rows = rows;
641 self
642 }
643
644 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
645 self.attrs = attrs;
646 self
647 }
648}
649
650impl<'a> askama::filters::HtmlSafe for MarkdownTextarea<'a> {}
651
652#[derive(Debug, Template)]
653#[non_exhaustive]
654#[template(path = "components/rich_text_host.html")]
655pub struct RichTextHost<'a> {
656 pub id: &'a str,
657 pub name: &'a str,
658 pub value: Option<&'a str>,
659 pub toolbar_html: Option<TrustedHtml<'a>>,
660 pub body_html: Option<TrustedHtml<'a>>,
661}
662
663impl<'a> RichTextHost<'a> {
664 pub const fn new(id: &'a str, name: &'a str) -> Self {
665 Self {
666 id,
667 name,
668 value: None,
669 toolbar_html: None,
670 body_html: None,
671 }
672 }
673
674 pub const fn with_value(mut self, value: &'a str) -> Self {
675 self.value = Some(value);
676 self
677 }
678
679 pub const fn with_toolbar(mut self, toolbar_html: TrustedHtml<'a>) -> Self {
680 self.toolbar_html = Some(toolbar_html);
681 self
682 }
683
684 pub const fn with_body(mut self, body_html: TrustedHtml<'a>) -> Self {
685 self.body_html = Some(body_html);
686 self
687 }
688}
689
690impl<'a> askama::filters::HtmlSafe for RichTextHost<'a> {}
691
692#[derive(Debug, Template)]
693#[non_exhaustive]
694#[template(path = "components/button_group.html")]
695pub struct ButtonGroup<'a> {
696 pub buttons: &'a [Button<'a>],
697 pub attrs: &'a [HtmlAttr<'a>],
698}
699
700impl<'a> ButtonGroup<'a> {
701 pub const fn new(buttons: &'a [Button<'a>]) -> Self {
702 Self {
703 buttons,
704 attrs: &[],
705 }
706 }
707
708 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
709 self.attrs = attrs;
710 self
711 }
712}
713
714impl<'a> askama::filters::HtmlSafe for ButtonGroup<'a> {}
715
716#[derive(Debug, Template)]
717#[non_exhaustive]
718#[template(path = "components/split_button.html")]
719pub struct SplitButton<'a> {
720 pub action: Button<'a>,
721 pub menu: Button<'a>,
722 pub attrs: &'a [HtmlAttr<'a>],
723}
724
725impl<'a> SplitButton<'a> {
726 pub const fn new(action: Button<'a>, menu: Button<'a>) -> Self {
727 Self {
728 action,
729 menu,
730 attrs: &[],
731 }
732 }
733
734 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
735 self.attrs = attrs;
736 self
737 }
738}
739
740impl<'a> askama::filters::HtmlSafe for SplitButton<'a> {}
741
742#[derive(Debug, Template)]
743#[non_exhaustive]
744#[template(path = "components/icon_button.html")]
745pub struct IconButton<'a> {
746 pub icon: TrustedHtml<'a>,
747 pub label: &'a str,
748 pub href: Option<&'a str>,
749 pub variant: ButtonVariant,
750 pub attrs: &'a [HtmlAttr<'a>],
751 pub disabled: bool,
752 pub button_type: &'a str,
753}
754
755impl<'a> IconButton<'a> {
756 pub const fn new(icon: TrustedHtml<'a>, label: &'a str) -> Self {
757 Self {
758 icon,
759 label,
760 href: None,
761 variant: ButtonVariant::Default,
762 attrs: &[],
763 disabled: false,
764 button_type: "button",
765 }
766 }
767
768 pub const fn with_href(mut self, href: &'a str) -> Self {
769 self.href = Some(href);
770 self
771 }
772
773 pub const fn with_variant(mut self, variant: ButtonVariant) -> Self {
774 self.variant = variant;
775 self
776 }
777
778 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
779 self.attrs = attrs;
780 self
781 }
782
783 pub const fn disabled(mut self) -> Self {
784 self.disabled = true;
785 self
786 }
787
788 pub const fn with_button_type(mut self, button_type: &'a str) -> Self {
789 self.button_type = button_type;
790 self
791 }
792
793 pub fn class_name(&self) -> String {
794 format!("wf-icon-btn{}", self.variant.class())
795 }
796}
797
798impl<'a> askama::filters::HtmlSafe for IconButton<'a> {}
799
800#[derive(Clone, Copy, Debug, Eq, PartialEq)]
801pub enum ControlSize {
802 Default,
803 Small,
804}
805
806impl ControlSize {
807 fn class(self) -> &'static str {
808 match self {
809 Self::Default => "",
810 Self::Small => " sm",
811 }
812 }
813}
814
815#[derive(Debug, Template)]
816#[non_exhaustive]
817#[template(path = "components/input.html")]
818pub struct Input<'a> {
819 pub name: &'a str,
820 pub input_type: &'a str,
821 pub value: Option<&'a str>,
822 pub placeholder: Option<&'a str>,
823 pub size: ControlSize,
824 pub attrs: &'a [HtmlAttr<'a>],
825 pub disabled: bool,
826 pub required: bool,
827}
828
829impl<'a> Input<'a> {
830 pub const fn new(name: &'a str) -> Self {
831 Self {
832 name,
833 input_type: "text",
834 value: None,
835 placeholder: None,
836 size: ControlSize::Default,
837 attrs: &[],
838 disabled: false,
839 required: false,
840 }
841 }
842
843 pub const fn email(name: &'a str) -> Self {
844 Self {
845 input_type: "email",
846 ..Self::new(name)
847 }
848 }
849
850 pub const fn url(name: &'a str) -> Self {
851 Self {
852 input_type: "url",
853 ..Self::new(name)
854 }
855 }
856
857 pub const fn search(name: &'a str) -> Self {
858 Self {
859 input_type: "search",
860 ..Self::new(name)
861 }
862 }
863
864 pub const fn with_type(mut self, input_type: &'a str) -> Self {
865 self.input_type = input_type;
866 self
867 }
868
869 pub const fn with_value(mut self, value: &'a str) -> Self {
870 self.value = Some(value);
871 self
872 }
873
874 pub const fn with_placeholder(mut self, placeholder: &'a str) -> Self {
875 self.placeholder = Some(placeholder);
876 self
877 }
878
879 pub const fn with_size(mut self, size: ControlSize) -> Self {
880 self.size = size;
881 self
882 }
883
884 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
885 self.attrs = attrs;
886 self
887 }
888
889 pub const fn disabled(mut self) -> Self {
890 self.disabled = true;
891 self
892 }
893
894 pub const fn required(mut self) -> Self {
895 self.required = true;
896 self
897 }
898
899 pub fn class_name(&self) -> String {
900 format!("wf-input{}", self.size.class())
901 }
902}
903
904impl<'a> askama::filters::HtmlSafe for Input<'a> {}
905
906#[derive(Debug, Template)]
907#[non_exhaustive]
908#[template(path = "components/textarea.html")]
909pub struct Textarea<'a> {
910 pub name: &'a str,
911 pub value: Option<&'a str>,
912 pub placeholder: Option<&'a str>,
913 pub rows: Option<u16>,
914 pub attrs: &'a [HtmlAttr<'a>],
915 pub disabled: bool,
916 pub required: bool,
917}
918
919impl<'a> Textarea<'a> {
920 pub const fn new(name: &'a str) -> Self {
921 Self {
922 name,
923 value: None,
924 placeholder: None,
925 rows: None,
926 attrs: &[],
927 disabled: false,
928 required: false,
929 }
930 }
931
932 pub const fn with_value(mut self, value: &'a str) -> Self {
933 self.value = Some(value);
934 self
935 }
936
937 pub const fn with_placeholder(mut self, placeholder: &'a str) -> Self {
938 self.placeholder = Some(placeholder);
939 self
940 }
941
942 pub const fn with_rows(mut self, rows: u16) -> Self {
943 self.rows = Some(rows);
944 self
945 }
946
947 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
948 self.attrs = attrs;
949 self
950 }
951
952 pub const fn disabled(mut self) -> Self {
953 self.disabled = true;
954 self
955 }
956
957 pub const fn required(mut self) -> Self {
958 self.required = true;
959 self
960 }
961}
962
963impl<'a> askama::filters::HtmlSafe for Textarea<'a> {}
964
965#[derive(Clone, Copy, Debug, Eq, PartialEq)]
966pub struct SelectOption<'a> {
967 pub value: &'a str,
968 pub label: &'a str,
969 pub selected: bool,
970 pub disabled: bool,
971}
972
973impl<'a> SelectOption<'a> {
974 pub const fn new(value: &'a str, label: &'a str) -> Self {
975 Self {
976 value,
977 label,
978 selected: false,
979 disabled: false,
980 }
981 }
982
983 pub const fn selected(mut self) -> Self {
984 self.selected = true;
985 self
986 }
987
988 pub const fn disabled(mut self) -> Self {
989 self.disabled = true;
990 self
991 }
992}
993
994#[derive(Debug, Template)]
995#[non_exhaustive]
996#[template(path = "components/select.html")]
997pub struct Select<'a> {
998 pub name: &'a str,
999 pub options: &'a [SelectOption<'a>],
1000 pub size: ControlSize,
1001 pub attrs: &'a [HtmlAttr<'a>],
1002 pub disabled: bool,
1003 pub required: bool,
1004}
1005
1006impl<'a> Select<'a> {
1007 pub const fn new(name: &'a str, options: &'a [SelectOption<'a>]) -> Self {
1008 Self {
1009 name,
1010 options,
1011 size: ControlSize::Default,
1012 attrs: &[],
1013 disabled: false,
1014 required: false,
1015 }
1016 }
1017
1018 pub const fn with_size(mut self, size: ControlSize) -> Self {
1019 self.size = size;
1020 self
1021 }
1022
1023 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
1024 self.attrs = attrs;
1025 self
1026 }
1027
1028 pub const fn disabled(mut self) -> Self {
1029 self.disabled = true;
1030 self
1031 }
1032
1033 pub const fn required(mut self) -> Self {
1034 self.required = true;
1035 self
1036 }
1037
1038 pub fn class_name(&self) -> String {
1039 format!("wf-select{}", self.size.class())
1040 }
1041}
1042
1043impl<'a> askama::filters::HtmlSafe for Select<'a> {}
1044
1045#[derive(Debug, Template)]
1046#[non_exhaustive]
1047#[template(path = "components/input_group.html")]
1048pub struct InputGroup<'a> {
1049 pub control_html: TrustedHtml<'a>,
1050 pub prefix: Option<&'a str>,
1051 pub suffix: Option<&'a str>,
1052 pub attrs: &'a [HtmlAttr<'a>],
1053}
1054
1055impl<'a> InputGroup<'a> {
1056 pub const fn new(control_html: TrustedHtml<'a>) -> Self {
1057 Self {
1058 control_html,
1059 prefix: None,
1060 suffix: None,
1061 attrs: &[],
1062 }
1063 }
1064
1065 pub const fn with_prefix(mut self, prefix: &'a str) -> Self {
1066 self.prefix = Some(prefix);
1067 self
1068 }
1069
1070 pub const fn with_suffix(mut self, suffix: &'a str) -> Self {
1071 self.suffix = Some(suffix);
1072 self
1073 }
1074
1075 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
1076 self.attrs = attrs;
1077 self
1078 }
1079}
1080
1081impl<'a> askama::filters::HtmlSafe for InputGroup<'a> {}
1082
1083#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1084pub enum CheckKind {
1085 Checkbox,
1086 Radio,
1087}
1088
1089impl CheckKind {
1090 fn input_type(self) -> &'static str {
1091 match self {
1092 Self::Checkbox => "checkbox",
1093 Self::Radio => "radio",
1094 }
1095 }
1096}
1097
1098#[derive(Debug, Template)]
1099#[non_exhaustive]
1100#[template(path = "components/check_row.html")]
1101pub struct CheckRow<'a> {
1102 pub kind: CheckKind,
1103 pub name: &'a str,
1104 pub value: &'a str,
1105 pub label: &'a str,
1106 pub attrs: &'a [HtmlAttr<'a>],
1107 pub checked: bool,
1108 pub disabled: bool,
1109}
1110
1111impl<'a> CheckRow<'a> {
1112 pub const fn checkbox(name: &'a str, value: &'a str, label: &'a str) -> Self {
1113 Self {
1114 kind: CheckKind::Checkbox,
1115 name,
1116 value,
1117 label,
1118 attrs: &[],
1119 checked: false,
1120 disabled: false,
1121 }
1122 }
1123
1124 pub const fn radio(name: &'a str, value: &'a str, label: &'a str) -> Self {
1125 Self {
1126 kind: CheckKind::Radio,
1127 ..Self::checkbox(name, value, label)
1128 }
1129 }
1130
1131 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
1132 self.attrs = attrs;
1133 self
1134 }
1135
1136 pub const fn checked(mut self) -> Self {
1137 self.checked = true;
1138 self
1139 }
1140
1141 pub const fn disabled(mut self) -> Self {
1142 self.disabled = true;
1143 self
1144 }
1145
1146 pub fn input_type(&self) -> &'static str {
1147 self.kind.input_type()
1148 }
1149}
1150
1151impl<'a> askama::filters::HtmlSafe for CheckRow<'a> {}
1152
1153#[derive(Debug, Template)]
1154#[non_exhaustive]
1155#[template(path = "components/switch.html")]
1156pub struct Switch<'a> {
1157 pub name: &'a str,
1158 pub value: &'a str,
1159 pub attrs: &'a [HtmlAttr<'a>],
1160 pub checked: bool,
1161 pub disabled: bool,
1162}
1163
1164impl<'a> Switch<'a> {
1165 pub const fn new(name: &'a str) -> Self {
1166 Self {
1167 name,
1168 value: "on",
1169 attrs: &[],
1170 checked: false,
1171 disabled: false,
1172 }
1173 }
1174
1175 pub const fn with_value(mut self, value: &'a str) -> Self {
1176 self.value = value;
1177 self
1178 }
1179
1180 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
1181 self.attrs = attrs;
1182 self
1183 }
1184
1185 pub const fn checked(mut self) -> Self {
1186 self.checked = true;
1187 self
1188 }
1189
1190 pub const fn disabled(mut self) -> Self {
1191 self.disabled = true;
1192 self
1193 }
1194}
1195
1196impl<'a> askama::filters::HtmlSafe for Switch<'a> {}
1197
1198#[derive(Debug, Template)]
1199#[non_exhaustive]
1200#[template(path = "components/range.html")]
1201pub struct Range<'a> {
1202 pub name: &'a str,
1203 pub value: Option<&'a str>,
1204 pub min: Option<&'a str>,
1205 pub max: Option<&'a str>,
1206 pub step: Option<&'a str>,
1207 pub attrs: &'a [HtmlAttr<'a>],
1208 pub disabled: bool,
1209}
1210
1211impl<'a> Range<'a> {
1212 pub const fn new(name: &'a str) -> Self {
1213 Self {
1214 name,
1215 value: None,
1216 min: None,
1217 max: None,
1218 step: None,
1219 attrs: &[],
1220 disabled: false,
1221 }
1222 }
1223
1224 pub const fn with_value(mut self, value: &'a str) -> Self {
1225 self.value = Some(value);
1226 self
1227 }
1228
1229 pub const fn with_bounds(mut self, min: &'a str, max: &'a str) -> Self {
1230 self.min = Some(min);
1231 self.max = Some(max);
1232 self
1233 }
1234
1235 pub const fn with_step(mut self, step: &'a str) -> Self {
1236 self.step = Some(step);
1237 self
1238 }
1239
1240 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
1241 self.attrs = attrs;
1242 self
1243 }
1244
1245 pub const fn disabled(mut self) -> Self {
1246 self.disabled = true;
1247 self
1248 }
1249}
1250
1251impl<'a> askama::filters::HtmlSafe for Range<'a> {}
1252
1253#[derive(Debug, Template)]
1254#[non_exhaustive]
1255#[template(path = "components/dropzone.html")]
1256pub struct Dropzone<'a> {
1257 pub name: &'a str,
1258 pub title: &'a str,
1259 pub hint: Option<&'a str>,
1260 pub accept: Option<&'a str>,
1261 pub attrs: &'a [HtmlAttr<'a>],
1262 pub multiple: bool,
1263 pub disabled: bool,
1264 pub dragover: bool,
1265}
1266
1267impl<'a> Dropzone<'a> {
1268 pub const fn new(name: &'a str) -> Self {
1269 Self {
1270 name,
1271 title: "Drop files or click",
1272 hint: None,
1273 accept: None,
1274 attrs: &[],
1275 multiple: false,
1276 disabled: false,
1277 dragover: false,
1278 }
1279 }
1280
1281 pub const fn with_title(mut self, title: &'a str) -> Self {
1282 self.title = title;
1283 self
1284 }
1285
1286 pub const fn with_hint(mut self, hint: &'a str) -> Self {
1287 self.hint = Some(hint);
1288 self
1289 }
1290
1291 pub const fn with_accept(mut self, accept: &'a str) -> Self {
1292 self.accept = Some(accept);
1293 self
1294 }
1295
1296 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
1297 self.attrs = attrs;
1298 self
1299 }
1300
1301 pub const fn multiple(mut self) -> Self {
1302 self.multiple = true;
1303 self
1304 }
1305
1306 pub const fn disabled(mut self) -> Self {
1307 self.disabled = true;
1308 self
1309 }
1310
1311 pub const fn dragover(mut self) -> Self {
1312 self.dragover = true;
1313 self
1314 }
1315
1316 pub fn class_name(&self) -> String {
1317 let dragover = if self.dragover { " is-dragover" } else { "" };
1318 let disabled = if self.disabled { " is-disabled" } else { "" };
1319 format!("wf-dropzone{dragover}{disabled}")
1320 }
1321}
1322
1323impl<'a> askama::filters::HtmlSafe for Dropzone<'a> {}
1324
1325#[derive(Debug, Template)]
1326#[non_exhaustive]
1327#[template(path = "components/panel.html")]
1328pub struct Panel<'a> {
1329 pub title: &'a str,
1330 pub body_html: TrustedHtml<'a>,
1331 pub action_html: Option<TrustedHtml<'a>>,
1332 pub danger: bool,
1333 pub attrs: &'a [HtmlAttr<'a>],
1334}
1335
1336impl<'a> Panel<'a> {
1337 pub const fn new(title: &'a str, body_html: TrustedHtml<'a>) -> Self {
1338 Self {
1339 title,
1340 body_html,
1341 action_html: None,
1342 danger: false,
1343 attrs: &[],
1344 }
1345 }
1346
1347 pub const fn with_action(mut self, action_html: TrustedHtml<'a>) -> Self {
1348 self.action_html = Some(action_html);
1349 self
1350 }
1351
1352 pub const fn danger(mut self) -> Self {
1353 self.danger = true;
1354 self
1355 }
1356
1357 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
1358 self.attrs = attrs;
1359 self
1360 }
1361
1362 pub fn class_name(&self) -> &'static str {
1363 if self.danger {
1364 "wf-panel is-danger"
1365 } else {
1366 "wf-panel"
1367 }
1368 }
1369}
1370
1371impl<'a> askama::filters::HtmlSafe for Panel<'a> {}
1372
1373#[derive(Debug, Template)]
1374#[non_exhaustive]
1375#[template(path = "components/form_panel.html")]
1376pub struct FormPanel<'a> {
1377 pub title: &'a str,
1378 pub body_html: TrustedHtml<'a>,
1379 pub subtitle: Option<&'a str>,
1380 pub actions_html: Option<TrustedHtml<'a>>,
1381 pub meta_html: Option<TrustedHtml<'a>>,
1382 pub attrs: &'a [HtmlAttr<'a>],
1383}
1384
1385impl<'a> FormPanel<'a> {
1386 pub const fn new(title: &'a str, body_html: TrustedHtml<'a>) -> Self {
1387 Self {
1388 title,
1389 body_html,
1390 subtitle: None,
1391 actions_html: None,
1392 meta_html: None,
1393 attrs: &[],
1394 }
1395 }
1396
1397 pub const fn with_subtitle(mut self, subtitle: &'a str) -> Self {
1398 self.subtitle = Some(subtitle);
1399 self
1400 }
1401
1402 pub const fn with_actions(mut self, actions_html: TrustedHtml<'a>) -> Self {
1403 self.actions_html = Some(actions_html);
1404 self
1405 }
1406
1407 pub const fn with_meta(mut self, meta_html: TrustedHtml<'a>) -> Self {
1408 self.meta_html = Some(meta_html);
1409 self
1410 }
1411
1412 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
1413 self.attrs = attrs;
1414 self
1415 }
1416}
1417
1418impl<'a> askama::filters::HtmlSafe for FormPanel<'a> {}
1419
1420#[derive(Debug, Template)]
1421#[non_exhaustive]
1422#[template(path = "components/split_shell.html")]
1423pub struct SplitShell<'a> {
1424 pub content_html: TrustedHtml<'a>,
1425 pub visual_html: Option<TrustedHtml<'a>>,
1426 pub top_html: Option<TrustedHtml<'a>>,
1427 pub footer_html: Option<TrustedHtml<'a>>,
1428 pub mode: Option<&'a str>,
1429 pub mode_locked: bool,
1430 pub asset_base_path: &'a str,
1431 pub attrs: &'a [HtmlAttr<'a>],
1432}
1433
1434impl<'a> SplitShell<'a> {
1435 pub const fn new(content_html: TrustedHtml<'a>) -> Self {
1436 Self {
1437 content_html,
1438 visual_html: None,
1439 top_html: None,
1440 footer_html: None,
1441 mode: None,
1442 mode_locked: false,
1443 asset_base_path: assets::DEFAULT_BASE_PATH,
1444 attrs: &[],
1445 }
1446 }
1447
1448 pub const fn with_visual(mut self, visual_html: TrustedHtml<'a>) -> Self {
1449 self.visual_html = Some(visual_html);
1450 self
1451 }
1452
1453 pub const fn with_top(mut self, top_html: TrustedHtml<'a>) -> Self {
1454 self.top_html = Some(top_html);
1455 self
1456 }
1457
1458 pub const fn with_footer(mut self, footer_html: TrustedHtml<'a>) -> Self {
1459 self.footer_html = Some(footer_html);
1460 self
1461 }
1462
1463 pub const fn with_mode(mut self, mode: &'a str) -> Self {
1464 self.mode = Some(mode);
1465 self
1466 }
1467
1468 pub const fn mode_locked(mut self) -> Self {
1469 self.mode_locked = true;
1470 self
1471 }
1472
1473 pub const fn with_asset_base_path(mut self, asset_base_path: &'a str) -> Self {
1474 self.asset_base_path = asset_base_path;
1475 self
1476 }
1477
1478 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
1479 self.attrs = attrs;
1480 self
1481 }
1482}
1483
1484impl<'a> askama::filters::HtmlSafe for SplitShell<'a> {}
1485
1486#[derive(Debug, Template)]
1487#[non_exhaustive]
1488#[template(path = "components/settings_section.html")]
1489pub struct SettingsSection<'a> {
1490 pub title: &'a str,
1491 pub body_html: TrustedHtml<'a>,
1492 pub description: Option<&'a str>,
1493 pub action_html: Option<TrustedHtml<'a>>,
1494 pub danger: bool,
1495 pub attrs: &'a [HtmlAttr<'a>],
1496}
1497
1498impl<'a> SettingsSection<'a> {
1499 pub const fn new(title: &'a str, body_html: TrustedHtml<'a>) -> Self {
1500 Self {
1501 title,
1502 body_html,
1503 description: None,
1504 action_html: None,
1505 danger: false,
1506 attrs: &[],
1507 }
1508 }
1509
1510 pub const fn with_description(mut self, description: &'a str) -> Self {
1511 self.description = Some(description);
1512 self
1513 }
1514
1515 pub const fn with_action(mut self, action_html: TrustedHtml<'a>) -> Self {
1516 self.action_html = Some(action_html);
1517 self
1518 }
1519
1520 pub const fn danger(mut self) -> Self {
1521 self.danger = true;
1522 self
1523 }
1524
1525 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
1526 self.attrs = attrs;
1527 self
1528 }
1529
1530 pub fn class_name(&self) -> &'static str {
1531 if self.danger {
1532 "wf-panel wf-settings-section is-danger"
1533 } else {
1534 "wf-panel wf-settings-section"
1535 }
1536 }
1537}
1538
1539impl<'a> askama::filters::HtmlSafe for SettingsSection<'a> {}
1540
1541#[derive(Debug, Template)]
1542#[non_exhaustive]
1543#[template(path = "components/inline_form_row.html")]
1544pub struct InlineFormRow<'a> {
1545 pub label: &'a str,
1546 pub control_html: TrustedHtml<'a>,
1547 pub hint: Option<&'a str>,
1548 pub action_html: Option<TrustedHtml<'a>>,
1549}
1550
1551impl<'a> InlineFormRow<'a> {
1552 pub const fn new(label: &'a str, control_html: TrustedHtml<'a>) -> Self {
1553 Self {
1554 label,
1555 control_html,
1556 hint: None,
1557 action_html: None,
1558 }
1559 }
1560
1561 pub const fn with_hint(mut self, hint: &'a str) -> Self {
1562 self.hint = Some(hint);
1563 self
1564 }
1565
1566 pub const fn with_action(mut self, action_html: TrustedHtml<'a>) -> Self {
1567 self.action_html = Some(action_html);
1568 self
1569 }
1570}
1571
1572impl<'a> askama::filters::HtmlSafe for InlineFormRow<'a> {}
1573
1574#[derive(Debug, Template)]
1575#[non_exhaustive]
1576#[template(path = "components/copyable_value.html")]
1577pub struct CopyableValue<'a> {
1578 pub label: &'a str,
1579 pub id: &'a str,
1580 pub value: &'a str,
1581 pub button_label: &'a str,
1582 pub secret: bool,
1583}
1584
1585impl<'a> CopyableValue<'a> {
1586 pub const fn new(label: &'a str, id: &'a str, value: &'a str) -> Self {
1587 Self {
1588 label,
1589 id,
1590 value,
1591 button_label: "Copy",
1592 secret: false,
1593 }
1594 }
1595
1596 pub const fn with_button_label(mut self, button_label: &'a str) -> Self {
1597 self.button_label = button_label;
1598 self
1599 }
1600
1601 pub const fn secret(mut self) -> Self {
1602 self.secret = true;
1603 self
1604 }
1605
1606 pub fn value_class(&self) -> &'static str {
1607 if self.secret {
1608 "wf-copyable-value is-secret"
1609 } else {
1610 "wf-copyable-value"
1611 }
1612 }
1613}
1614
1615impl<'a> askama::filters::HtmlSafe for CopyableValue<'a> {}
1616
1617#[derive(Debug, Template)]
1618#[non_exhaustive]
1619#[template(path = "components/secret_value.html")]
1620pub struct SecretValue<'a> {
1621 pub label: &'a str,
1622 pub id: &'a str,
1623 pub value: &'a str,
1624 pub button_label: &'a str,
1625 pub revealed: bool,
1626 pub copy_raw_value: bool,
1627 pub warning: Option<&'a str>,
1628 pub help_html: Option<TrustedHtml<'a>>,
1629 pub attrs: &'a [HtmlAttr<'a>],
1630}
1631
1632impl<'a> SecretValue<'a> {
1633 pub const fn new(label: &'a str, id: &'a str, value: &'a str) -> Self {
1634 Self {
1635 label,
1636 id,
1637 value,
1638 button_label: "Copy",
1639 revealed: false,
1640 copy_raw_value: false,
1641 warning: None,
1642 help_html: None,
1643 attrs: &[],
1644 }
1645 }
1646
1647 pub const fn revealed(mut self) -> Self {
1648 self.revealed = true;
1649 self
1650 }
1651
1652 pub const fn copy_raw_value(mut self) -> Self {
1653 self.copy_raw_value = true;
1654 self
1655 }
1656
1657 pub const fn with_button_label(mut self, button_label: &'a str) -> Self {
1658 self.button_label = button_label;
1659 self
1660 }
1661
1662 pub const fn with_warning(mut self, warning: &'a str) -> Self {
1663 self.warning = Some(warning);
1664 self
1665 }
1666
1667 pub const fn with_help(mut self, help_html: TrustedHtml<'a>) -> Self {
1668 self.help_html = Some(help_html);
1669 self
1670 }
1671
1672 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
1673 self.attrs = attrs;
1674 self
1675 }
1676
1677 pub const fn display_value(&self) -> &str {
1678 if self.revealed {
1679 self.value
1680 } else {
1681 "********"
1682 }
1683 }
1684
1685 pub fn value_class(&self) -> &'static str {
1686 if self.revealed {
1687 "wf-secret-code is-revealed"
1688 } else {
1689 "wf-secret-code is-masked"
1690 }
1691 }
1692}
1693
1694impl<'a> askama::filters::HtmlSafe for SecretValue<'a> {}
1695
1696#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1697pub struct ChecklistItem<'a> {
1698 pub label: &'a str,
1699 pub description: Option<&'a str>,
1700 pub kind: FeedbackKind,
1701 pub status_label: Option<&'a str>,
1702 pub icon_html: Option<TrustedHtml<'a>>,
1703}
1704
1705impl<'a> ChecklistItem<'a> {
1706 pub const fn new(label: &'a str, kind: FeedbackKind) -> Self {
1707 Self {
1708 label,
1709 description: None,
1710 kind,
1711 status_label: None,
1712 icon_html: None,
1713 }
1714 }
1715
1716 pub const fn info(label: &'a str) -> Self {
1717 Self::new(label, FeedbackKind::Info)
1718 }
1719
1720 pub const fn ok(label: &'a str) -> Self {
1721 Self::new(label, FeedbackKind::Ok)
1722 }
1723
1724 pub const fn warn(label: &'a str) -> Self {
1725 Self::new(label, FeedbackKind::Warn)
1726 }
1727
1728 pub const fn error(label: &'a str) -> Self {
1729 Self::new(label, FeedbackKind::Error)
1730 }
1731
1732 pub const fn with_description(mut self, description: &'a str) -> Self {
1733 self.description = Some(description);
1734 self
1735 }
1736
1737 pub const fn with_status_label(mut self, status_label: &'a str) -> Self {
1738 self.status_label = Some(status_label);
1739 self
1740 }
1741
1742 pub const fn with_icon(mut self, icon_html: TrustedHtml<'a>) -> Self {
1743 self.icon_html = Some(icon_html);
1744 self
1745 }
1746
1747 pub fn class_name(&self) -> &'static str {
1748 match self.kind {
1749 FeedbackKind::Info => "wf-checklist-item is-info",
1750 FeedbackKind::Ok => "wf-checklist-item is-ok",
1751 FeedbackKind::Warn => "wf-checklist-item is-warn",
1752 FeedbackKind::Error => "wf-checklist-item is-err",
1753 }
1754 }
1755
1756 pub fn status_text(&self) -> &'a str {
1757 self.status_label.unwrap_or(match self.kind {
1758 FeedbackKind::Info => "info",
1759 FeedbackKind::Ok => "ok",
1760 FeedbackKind::Warn => "warn",
1761 FeedbackKind::Error => "error",
1762 })
1763 }
1764}
1765
1766#[derive(Debug, Template)]
1767#[non_exhaustive]
1768#[template(path = "components/checklist.html")]
1769pub struct Checklist<'a> {
1770 pub items: &'a [ChecklistItem<'a>],
1771 pub attrs: &'a [HtmlAttr<'a>],
1772}
1773
1774impl<'a> Checklist<'a> {
1775 pub const fn new(items: &'a [ChecklistItem<'a>]) -> Self {
1776 Self { items, attrs: &[] }
1777 }
1778
1779 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
1780 self.attrs = attrs;
1781 self
1782 }
1783}
1784
1785impl<'a> askama::filters::HtmlSafe for Checklist<'a> {}
1786
1787#[derive(Debug, Template)]
1788#[non_exhaustive]
1789#[template(path = "components/code_grid.html")]
1790pub struct CodeGrid<'a> {
1791 pub codes: &'a [&'a str],
1792 pub label: Option<&'a str>,
1793 pub attrs: &'a [HtmlAttr<'a>],
1794}
1795
1796impl<'a> CodeGrid<'a> {
1797 pub const fn new(codes: &'a [&'a str]) -> Self {
1798 Self {
1799 codes,
1800 label: None,
1801 attrs: &[],
1802 }
1803 }
1804
1805 pub const fn with_label(mut self, label: &'a str) -> Self {
1806 self.label = Some(label);
1807 self
1808 }
1809
1810 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
1811 self.attrs = attrs;
1812 self
1813 }
1814}
1815
1816impl<'a> askama::filters::HtmlSafe for CodeGrid<'a> {}
1817
1818#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1819pub struct CredentialStatusItem<'a> {
1820 pub label: &'a str,
1821 pub value: &'a str,
1822 pub kind: FeedbackKind,
1823 pub status_label: &'a str,
1824}
1825
1826impl<'a> CredentialStatusItem<'a> {
1827 pub const fn new(
1828 label: &'a str,
1829 value: &'a str,
1830 kind: FeedbackKind,
1831 status_label: &'a str,
1832 ) -> Self {
1833 Self {
1834 label,
1835 value,
1836 kind,
1837 status_label,
1838 }
1839 }
1840
1841 pub const fn ok(label: &'a str, value: &'a str) -> Self {
1842 Self::new(label, value, FeedbackKind::Ok, "ok")
1843 }
1844
1845 pub const fn warn(label: &'a str, value: &'a str) -> Self {
1846 Self::new(label, value, FeedbackKind::Warn, "warn")
1847 }
1848
1849 pub const fn error(label: &'a str, value: &'a str) -> Self {
1850 Self::new(label, value, FeedbackKind::Error, "error")
1851 }
1852
1853 pub const fn info(label: &'a str, value: &'a str) -> Self {
1854 Self::new(label, value, FeedbackKind::Info, "info")
1855 }
1856
1857 pub fn kind_class(&self) -> String {
1858 format!("wf-tag {}", self.kind.class())
1859 }
1860}
1861
1862#[derive(Debug, Template)]
1863#[non_exhaustive]
1864#[template(path = "components/credential_status_list.html")]
1865pub struct CredentialStatusList<'a> {
1866 pub items: &'a [CredentialStatusItem<'a>],
1867}
1868
1869impl<'a> CredentialStatusList<'a> {
1870 pub const fn new(items: &'a [CredentialStatusItem<'a>]) -> Self {
1871 Self { items }
1872 }
1873}
1874
1875impl<'a> askama::filters::HtmlSafe for CredentialStatusList<'a> {}
1876
1877#[derive(Debug, Template)]
1878#[non_exhaustive]
1879#[template(path = "components/confirm_action.html")]
1880pub struct ConfirmAction<'a> {
1881 pub label: &'a str,
1882 pub action: &'a str,
1883 pub method: &'a str,
1884 pub message: Option<&'a str>,
1885 pub confirm: Option<&'a str>,
1886 pub attrs: &'a [HtmlAttr<'a>],
1887}
1888
1889impl<'a> ConfirmAction<'a> {
1890 pub const fn new(label: &'a str, action: &'a str) -> Self {
1891 Self {
1892 label,
1893 action,
1894 method: "post",
1895 message: None,
1896 confirm: None,
1897 attrs: &[],
1898 }
1899 }
1900
1901 pub const fn with_method(mut self, method: &'a str) -> Self {
1902 self.method = method;
1903 self
1904 }
1905
1906 pub const fn with_message(mut self, message: &'a str) -> Self {
1907 self.message = Some(message);
1908 self
1909 }
1910
1911 pub const fn with_confirm(mut self, confirm: &'a str) -> Self {
1912 self.confirm = Some(confirm);
1913 self
1914 }
1915
1916 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
1917 self.attrs = attrs;
1918 self
1919 }
1920}
1921
1922impl<'a> askama::filters::HtmlSafe for ConfirmAction<'a> {}
1923
1924#[derive(Debug, Template)]
1925#[non_exhaustive]
1926#[template(path = "components/card.html")]
1927pub struct Card<'a> {
1928 pub title: &'a str,
1929 pub body_html: TrustedHtml<'a>,
1930 pub kicker: Option<&'a str>,
1931 pub foot_html: Option<TrustedHtml<'a>>,
1932 pub raised: bool,
1933 pub attrs: &'a [HtmlAttr<'a>],
1934}
1935
1936impl<'a> Card<'a> {
1937 pub const fn new(title: &'a str, body_html: TrustedHtml<'a>) -> Self {
1938 Self {
1939 title,
1940 body_html,
1941 kicker: None,
1942 foot_html: None,
1943 raised: false,
1944 attrs: &[],
1945 }
1946 }
1947
1948 pub const fn with_kicker(mut self, kicker: &'a str) -> Self {
1949 self.kicker = Some(kicker);
1950 self
1951 }
1952
1953 pub const fn with_foot(mut self, foot_html: TrustedHtml<'a>) -> Self {
1954 self.foot_html = Some(foot_html);
1955 self
1956 }
1957
1958 pub const fn raised(mut self) -> Self {
1959 self.raised = true;
1960 self
1961 }
1962
1963 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
1964 self.attrs = attrs;
1965 self
1966 }
1967
1968 pub fn class_name(&self) -> &'static str {
1969 if self.raised {
1970 "wf-card is-raised"
1971 } else {
1972 "wf-card"
1973 }
1974 }
1975}
1976
1977impl<'a> askama::filters::HtmlSafe for Card<'a> {}
1978
1979#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1980pub enum BadgeKind {
1981 Default,
1982 Muted,
1983 Error,
1984}
1985
1986impl BadgeKind {
1987 fn class(self) -> &'static str {
1988 match self {
1989 Self::Default => "",
1990 Self::Muted => " muted",
1991 Self::Error => " err",
1992 }
1993 }
1994}
1995
1996#[derive(Debug, Template)]
1997#[non_exhaustive]
1998#[template(path = "components/badge.html")]
1999pub struct Badge<'a> {
2000 pub label: &'a str,
2001 pub kind: BadgeKind,
2002}
2003
2004impl<'a> Badge<'a> {
2005 pub const fn new(label: &'a str) -> Self {
2006 Self {
2007 label,
2008 kind: BadgeKind::Default,
2009 }
2010 }
2011
2012 pub const fn muted(label: &'a str) -> Self {
2013 Self {
2014 kind: BadgeKind::Muted,
2015 ..Self::new(label)
2016 }
2017 }
2018
2019 pub const fn error(label: &'a str) -> Self {
2020 Self {
2021 kind: BadgeKind::Error,
2022 ..Self::new(label)
2023 }
2024 }
2025
2026 pub fn class_name(&self) -> String {
2027 format!("wf-badge{}", self.kind.class())
2028 }
2029}
2030
2031impl<'a> askama::filters::HtmlSafe for Badge<'a> {}
2032
2033#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2034pub enum AvatarSize {
2035 Default,
2036 Small,
2037 Large,
2038 ExtraLarge,
2039}
2040
2041impl AvatarSize {
2042 fn class(self) -> &'static str {
2043 match self {
2044 Self::Default => "",
2045 Self::Small => " sm",
2046 Self::Large => " lg",
2047 Self::ExtraLarge => " xl",
2048 }
2049 }
2050}
2051
2052#[derive(Debug, Template)]
2053#[non_exhaustive]
2054#[template(path = "components/avatar.html")]
2055pub struct Avatar<'a> {
2056 pub initials: &'a str,
2057 pub image_src: Option<&'a str>,
2058 pub size: AvatarSize,
2059 pub accent: bool,
2060}
2061
2062impl<'a> Avatar<'a> {
2063 pub const fn new(initials: &'a str) -> Self {
2064 Self {
2065 initials,
2066 image_src: None,
2067 size: AvatarSize::Default,
2068 accent: false,
2069 }
2070 }
2071
2072 pub const fn with_image(mut self, image_src: &'a str) -> Self {
2073 self.image_src = Some(image_src);
2074 self
2075 }
2076
2077 pub const fn with_size(mut self, size: AvatarSize) -> Self {
2078 self.size = size;
2079 self
2080 }
2081
2082 pub const fn accent(mut self) -> Self {
2083 self.accent = true;
2084 self
2085 }
2086
2087 pub fn class_name(&self) -> String {
2088 let accent = if self.accent { " accent" } else { "" };
2089 format!("wf-avatar{}{}", self.size.class(), accent)
2090 }
2091}
2092
2093impl<'a> askama::filters::HtmlSafe for Avatar<'a> {}
2094
2095#[derive(Debug, Template)]
2096#[non_exhaustive]
2097#[template(path = "components/avatar_group.html")]
2098pub struct AvatarGroup<'a> {
2099 pub avatars: &'a [Avatar<'a>],
2100}
2101
2102impl<'a> AvatarGroup<'a> {
2103 pub const fn new(avatars: &'a [Avatar<'a>]) -> Self {
2104 Self { avatars }
2105 }
2106}
2107
2108impl<'a> askama::filters::HtmlSafe for AvatarGroup<'a> {}
2109
2110#[derive(Debug, Template)]
2111#[non_exhaustive]
2112#[template(path = "components/user_button.html")]
2113pub struct UserButton<'a> {
2114 pub name: &'a str,
2115 pub email: &'a str,
2116 pub avatar: Avatar<'a>,
2117 pub compact: bool,
2118 pub attrs: &'a [HtmlAttr<'a>],
2119}
2120
2121impl<'a> UserButton<'a> {
2122 pub const fn new(name: &'a str, email: &'a str, avatar: Avatar<'a>) -> Self {
2123 Self {
2124 name,
2125 email,
2126 avatar,
2127 compact: false,
2128 attrs: &[],
2129 }
2130 }
2131
2132 pub const fn compact(mut self) -> Self {
2133 self.compact = true;
2134 self
2135 }
2136
2137 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
2138 self.attrs = attrs;
2139 self
2140 }
2141
2142 pub fn class_name(&self) -> &'static str {
2143 if self.compact {
2144 "wf-user compact"
2145 } else {
2146 "wf-user"
2147 }
2148 }
2149}
2150
2151impl<'a> askama::filters::HtmlSafe for UserButton<'a> {}
2152
2153#[derive(Debug, Template)]
2154#[non_exhaustive]
2155#[template(path = "components/wordmark.html")]
2156pub struct Wordmark<'a> {
2157 pub name: &'a str,
2158 pub mark_html: Option<TrustedHtml<'a>>,
2159}
2160
2161impl<'a> Wordmark<'a> {
2162 pub const fn new(name: &'a str) -> Self {
2163 Self {
2164 name,
2165 mark_html: None,
2166 }
2167 }
2168
2169 pub const fn with_mark(mut self, mark_html: TrustedHtml<'a>) -> Self {
2170 self.mark_html = Some(mark_html);
2171 self
2172 }
2173}
2174
2175impl<'a> askama::filters::HtmlSafe for Wordmark<'a> {}
2176
2177#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2178pub enum DeltaKind {
2179 Neutral,
2180 Up,
2181 Down,
2182}
2183
2184impl DeltaKind {
2185 fn class(self) -> &'static str {
2186 match self {
2187 Self::Neutral => "",
2188 Self::Up => " up",
2189 Self::Down => " down",
2190 }
2191 }
2192}
2193
2194#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2195pub struct Stat<'a> {
2196 pub label: &'a str,
2197 pub value: &'a str,
2198 pub unit: Option<&'a str>,
2199 pub delta: Option<&'a str>,
2200 pub delta_kind: DeltaKind,
2201 pub foot: Option<&'a str>,
2202}
2203
2204impl<'a> Stat<'a> {
2205 pub const fn new(label: &'a str, value: &'a str) -> Self {
2206 Self {
2207 label,
2208 value,
2209 unit: None,
2210 delta: None,
2211 delta_kind: DeltaKind::Neutral,
2212 foot: None,
2213 }
2214 }
2215
2216 pub const fn with_unit(mut self, unit: &'a str) -> Self {
2217 self.unit = Some(unit);
2218 self
2219 }
2220
2221 pub const fn with_delta(mut self, delta: &'a str, kind: DeltaKind) -> Self {
2222 self.delta = Some(delta);
2223 self.delta_kind = kind;
2224 self
2225 }
2226
2227 pub const fn with_foot(mut self, foot: &'a str) -> Self {
2228 self.foot = Some(foot);
2229 self
2230 }
2231
2232 pub fn delta_class(&self) -> String {
2233 format!("wf-stat-delta{}", self.delta_kind.class())
2234 }
2235}
2236
2237#[derive(Debug, Template)]
2238#[non_exhaustive]
2239#[template(path = "components/stat_row.html")]
2240pub struct StatRow<'a> {
2241 pub stats: &'a [Stat<'a>],
2242}
2243
2244impl<'a> StatRow<'a> {
2245 pub const fn new(stats: &'a [Stat<'a>]) -> Self {
2246 Self { stats }
2247 }
2248}
2249
2250impl<'a> askama::filters::HtmlSafe for StatRow<'a> {}
2251
2252#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2253pub struct BreadcrumbItem<'a> {
2254 pub label: &'a str,
2255 pub href: Option<&'a str>,
2256 pub current: bool,
2257}
2258
2259impl<'a> BreadcrumbItem<'a> {
2260 pub const fn link(label: &'a str, href: &'a str) -> Self {
2261 Self {
2262 label,
2263 href: Some(href),
2264 current: false,
2265 }
2266 }
2267
2268 pub const fn current(label: &'a str) -> Self {
2269 Self {
2270 label,
2271 href: None,
2272 current: true,
2273 }
2274 }
2275}
2276
2277#[derive(Debug, Template)]
2278#[non_exhaustive]
2279#[template(path = "components/breadcrumbs.html")]
2280pub struct Breadcrumbs<'a> {
2281 pub items: &'a [BreadcrumbItem<'a>],
2282}
2283
2284impl<'a> Breadcrumbs<'a> {
2285 pub const fn new(items: &'a [BreadcrumbItem<'a>]) -> Self {
2286 Self { items }
2287 }
2288}
2289
2290impl<'a> askama::filters::HtmlSafe for Breadcrumbs<'a> {}
2291
2292#[derive(Debug, Template)]
2293#[non_exhaustive]
2294#[template(path = "components/page_header.html")]
2295pub struct PageHeader<'a> {
2296 pub title: &'a str,
2297 pub subtitle: Option<&'a str>,
2298 pub back_href: Option<&'a str>,
2299 pub back_label: &'a str,
2300 pub meta_html: Option<TrustedHtml<'a>>,
2301 pub primary_html: Option<TrustedHtml<'a>>,
2302 pub secondary_html: Option<TrustedHtml<'a>>,
2303}
2304
2305impl<'a> PageHeader<'a> {
2306 pub const fn new(title: &'a str) -> Self {
2307 Self {
2308 title,
2309 subtitle: None,
2310 back_href: None,
2311 back_label: "Back",
2312 meta_html: None,
2313 primary_html: None,
2314 secondary_html: None,
2315 }
2316 }
2317
2318 pub const fn with_subtitle(mut self, subtitle: &'a str) -> Self {
2319 self.subtitle = Some(subtitle);
2320 self
2321 }
2322
2323 pub const fn with_back(mut self, href: &'a str, label: &'a str) -> Self {
2324 self.back_href = Some(href);
2325 self.back_label = label;
2326 self
2327 }
2328
2329 pub const fn with_meta(mut self, meta_html: TrustedHtml<'a>) -> Self {
2330 self.meta_html = Some(meta_html);
2331 self
2332 }
2333
2334 pub const fn with_primary(mut self, primary_html: TrustedHtml<'a>) -> Self {
2335 self.primary_html = Some(primary_html);
2336 self
2337 }
2338
2339 pub const fn with_secondary(mut self, secondary_html: TrustedHtml<'a>) -> Self {
2340 self.secondary_html = Some(secondary_html);
2341 self
2342 }
2343
2344 pub const fn has_actions(&self) -> bool {
2345 self.primary_html.is_some() || self.secondary_html.is_some()
2346 }
2347}
2348
2349impl<'a> askama::filters::HtmlSafe for PageHeader<'a> {}
2350
2351#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2352pub struct TabItem<'a> {
2353 pub label: &'a str,
2354 pub href: &'a str,
2355 pub active: bool,
2356}
2357
2358impl<'a> TabItem<'a> {
2359 pub const fn link(label: &'a str, href: &'a str) -> Self {
2360 Self {
2361 label,
2362 href,
2363 active: false,
2364 }
2365 }
2366
2367 pub const fn active(mut self) -> Self {
2368 self.active = true;
2369 self
2370 }
2371}
2372
2373#[derive(Debug, Template)]
2374#[non_exhaustive]
2375#[template(path = "components/tabs.html")]
2376pub struct Tabs<'a> {
2377 pub items: &'a [TabItem<'a>],
2378}
2379
2380impl<'a> Tabs<'a> {
2381 pub const fn new(items: &'a [TabItem<'a>]) -> Self {
2382 Self { items }
2383 }
2384}
2385
2386impl<'a> askama::filters::HtmlSafe for Tabs<'a> {}
2387
2388#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2389pub struct SegmentOption<'a> {
2390 pub label: &'a str,
2391 pub value: &'a str,
2392 pub active: bool,
2393}
2394
2395impl<'a> SegmentOption<'a> {
2396 pub const fn new(label: &'a str, value: &'a str) -> Self {
2397 Self {
2398 label,
2399 value,
2400 active: false,
2401 }
2402 }
2403
2404 pub const fn active(mut self) -> Self {
2405 self.active = true;
2406 self
2407 }
2408}
2409
2410#[derive(Debug, Template)]
2411#[non_exhaustive]
2412#[template(path = "components/segmented_control.html")]
2413pub struct SegmentedControl<'a> {
2414 pub options: &'a [SegmentOption<'a>],
2415 pub small: bool,
2416}
2417
2418impl<'a> SegmentedControl<'a> {
2419 pub const fn new(options: &'a [SegmentOption<'a>]) -> Self {
2420 Self {
2421 options,
2422 small: false,
2423 }
2424 }
2425
2426 pub const fn small(mut self) -> Self {
2427 self.small = true;
2428 self
2429 }
2430
2431 pub fn class_name(&self) -> &'static str {
2432 if self.small { "wf-seg sm" } else { "wf-seg" }
2433 }
2434}
2435
2436impl<'a> askama::filters::HtmlSafe for SegmentedControl<'a> {}
2437
2438#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2439pub struct PageLink<'a> {
2440 pub label: &'a str,
2441 pub href: Option<&'a str>,
2442 pub active: bool,
2443 pub disabled: bool,
2444 pub ellipsis: bool,
2445}
2446
2447impl<'a> PageLink<'a> {
2448 pub const fn link(label: &'a str, href: &'a str) -> Self {
2449 Self {
2450 label,
2451 href: Some(href),
2452 active: false,
2453 disabled: false,
2454 ellipsis: false,
2455 }
2456 }
2457
2458 pub const fn disabled(label: &'a str) -> Self {
2459 Self {
2460 label,
2461 href: None,
2462 active: false,
2463 disabled: true,
2464 ellipsis: false,
2465 }
2466 }
2467
2468 pub const fn ellipsis() -> Self {
2469 Self {
2470 label: "...",
2471 href: None,
2472 active: false,
2473 disabled: false,
2474 ellipsis: true,
2475 }
2476 }
2477
2478 pub const fn active(mut self) -> Self {
2479 self.active = true;
2480 self
2481 }
2482}
2483
2484#[derive(Debug, Template)]
2485#[non_exhaustive]
2486#[template(path = "components/pagination.html")]
2487pub struct Pagination<'a> {
2488 pub pages: &'a [PageLink<'a>],
2489}
2490
2491impl<'a> Pagination<'a> {
2492 pub const fn new(pages: &'a [PageLink<'a>]) -> Self {
2493 Self { pages }
2494 }
2495}
2496
2497impl<'a> askama::filters::HtmlSafe for Pagination<'a> {}
2498
2499#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2500pub enum StepState {
2501 Upcoming,
2502 Active,
2503 Done,
2504}
2505
2506#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2507pub struct StepItem<'a> {
2508 pub label: &'a str,
2509 pub href: Option<&'a str>,
2510 pub state: StepState,
2511}
2512
2513impl<'a> StepItem<'a> {
2514 pub const fn new(label: &'a str) -> Self {
2515 Self {
2516 label,
2517 href: None,
2518 state: StepState::Upcoming,
2519 }
2520 }
2521
2522 pub const fn with_href(mut self, href: &'a str) -> Self {
2523 self.href = Some(href);
2524 self
2525 }
2526
2527 pub const fn active(mut self) -> Self {
2528 self.state = StepState::Active;
2529 self
2530 }
2531
2532 pub const fn done(mut self) -> Self {
2533 self.state = StepState::Done;
2534 self
2535 }
2536
2537 pub fn class_name(&self) -> &'static str {
2538 match self.state {
2539 StepState::Upcoming => "wf-step",
2540 StepState::Active => "wf-step is-active",
2541 StepState::Done => "wf-step is-done",
2542 }
2543 }
2544
2545 pub fn is_active(&self) -> bool {
2546 self.state == StepState::Active
2547 }
2548}
2549
2550#[derive(Debug, Template)]
2551#[non_exhaustive]
2552#[template(path = "components/stepper.html")]
2553pub struct Stepper<'a> {
2554 pub steps: &'a [StepItem<'a>],
2555}
2556
2557impl<'a> Stepper<'a> {
2558 pub const fn new(steps: &'a [StepItem<'a>]) -> Self {
2559 Self { steps }
2560 }
2561}
2562
2563impl<'a> askama::filters::HtmlSafe for Stepper<'a> {}
2564
2565#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2566pub struct AccordionItem<'a> {
2567 pub title: &'a str,
2568 pub body_html: TrustedHtml<'a>,
2569 pub open: bool,
2570}
2571
2572impl<'a> AccordionItem<'a> {
2573 pub const fn new(title: &'a str, body_html: TrustedHtml<'a>) -> Self {
2574 Self {
2575 title,
2576 body_html,
2577 open: false,
2578 }
2579 }
2580
2581 pub const fn open(mut self) -> Self {
2582 self.open = true;
2583 self
2584 }
2585}
2586
2587#[derive(Debug, Template)]
2588#[non_exhaustive]
2589#[template(path = "components/accordion.html")]
2590pub struct Accordion<'a> {
2591 pub items: &'a [AccordionItem<'a>],
2592}
2593
2594impl<'a> Accordion<'a> {
2595 pub const fn new(items: &'a [AccordionItem<'a>]) -> Self {
2596 Self { items }
2597 }
2598}
2599
2600impl<'a> askama::filters::HtmlSafe for Accordion<'a> {}
2601
2602#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2603pub struct FaqItem<'a> {
2604 pub question: &'a str,
2605 pub answer_html: TrustedHtml<'a>,
2606}
2607
2608impl<'a> FaqItem<'a> {
2609 pub const fn new(question: &'a str, answer_html: TrustedHtml<'a>) -> Self {
2610 Self {
2611 question,
2612 answer_html,
2613 }
2614 }
2615}
2616
2617#[derive(Debug, Template)]
2618#[non_exhaustive]
2619#[template(path = "components/faq.html")]
2620pub struct Faq<'a> {
2621 pub items: &'a [FaqItem<'a>],
2622}
2623
2624impl<'a> Faq<'a> {
2625 pub const fn new(items: &'a [FaqItem<'a>]) -> Self {
2626 Self { items }
2627 }
2628}
2629
2630impl<'a> askama::filters::HtmlSafe for Faq<'a> {}
2631
2632#[derive(Debug, Template)]
2633#[non_exhaustive]
2634#[template(path = "components/nav_section.html")]
2635pub struct NavSection<'a> {
2636 pub label: &'a str,
2637}
2638
2639impl<'a> NavSection<'a> {
2640 pub const fn new(label: &'a str) -> Self {
2641 Self { label }
2642 }
2643}
2644
2645impl<'a> askama::filters::HtmlSafe for NavSection<'a> {}
2646
2647#[derive(Debug, Template)]
2648#[non_exhaustive]
2649#[template(path = "components/nav_item.html")]
2650pub struct NavItem<'a> {
2651 pub label: &'a str,
2652 pub href: &'a str,
2653 pub count: Option<&'a str>,
2654 pub active: bool,
2655}
2656
2657impl<'a> NavItem<'a> {
2658 pub const fn new(label: &'a str, href: &'a str) -> Self {
2659 Self {
2660 label,
2661 href,
2662 count: None,
2663 active: false,
2664 }
2665 }
2666
2667 pub const fn active(mut self) -> Self {
2668 self.active = true;
2669 self
2670 }
2671
2672 pub const fn with_count(mut self, count: &'a str) -> Self {
2673 self.count = Some(count);
2674 self
2675 }
2676
2677 pub fn class_name(&self) -> &'static str {
2678 if self.active {
2679 "wf-nav-item is-active"
2680 } else {
2681 "wf-nav-item"
2682 }
2683 }
2684}
2685
2686impl<'a> askama::filters::HtmlSafe for NavItem<'a> {}
2687
2688#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2689pub struct ContextSwitcherItem<'a> {
2690 pub label: &'a str,
2691 pub href: &'a str,
2692 pub meta: Option<&'a str>,
2693 pub badge_html: Option<TrustedHtml<'a>>,
2694 pub active: bool,
2695 pub disabled: bool,
2696}
2697
2698impl<'a> ContextSwitcherItem<'a> {
2699 pub const fn link(label: &'a str, href: &'a str) -> Self {
2700 Self {
2701 label,
2702 href,
2703 meta: None,
2704 badge_html: None,
2705 active: false,
2706 disabled: false,
2707 }
2708 }
2709
2710 pub const fn with_meta(mut self, meta: &'a str) -> Self {
2711 self.meta = Some(meta);
2712 self
2713 }
2714
2715 pub const fn with_badge(mut self, badge_html: TrustedHtml<'a>) -> Self {
2716 self.badge_html = Some(badge_html);
2717 self
2718 }
2719
2720 pub const fn active(mut self) -> Self {
2721 self.active = true;
2722 self
2723 }
2724
2725 pub const fn disabled(mut self) -> Self {
2726 self.disabled = true;
2727 self
2728 }
2729
2730 pub fn class_name(&self) -> &'static str {
2731 match (self.active, self.disabled) {
2732 (true, true) => "wf-context-switcher-item is-active is-disabled",
2733 (true, false) => "wf-context-switcher-item is-active",
2734 (false, true) => "wf-context-switcher-item is-disabled",
2735 (false, false) => "wf-context-switcher-item",
2736 }
2737 }
2738}
2739
2740#[derive(Debug, Template)]
2741#[non_exhaustive]
2742#[template(path = "components/context_switcher.html")]
2743pub struct ContextSwitcher<'a> {
2744 pub label: &'a str,
2745 pub current: &'a str,
2746 pub items: &'a [ContextSwitcherItem<'a>],
2747 pub meta_html: Option<TrustedHtml<'a>>,
2748 pub open: bool,
2749 pub attrs: &'a [HtmlAttr<'a>],
2750}
2751
2752impl<'a> ContextSwitcher<'a> {
2753 pub const fn new(
2754 label: &'a str,
2755 current: &'a str,
2756 items: &'a [ContextSwitcherItem<'a>],
2757 ) -> Self {
2758 Self {
2759 label,
2760 current,
2761 items,
2762 meta_html: None,
2763 open: false,
2764 attrs: &[],
2765 }
2766 }
2767
2768 pub const fn with_meta(mut self, meta_html: TrustedHtml<'a>) -> Self {
2769 self.meta_html = Some(meta_html);
2770 self
2771 }
2772
2773 pub const fn open(mut self) -> Self {
2774 self.open = true;
2775 self
2776 }
2777
2778 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
2779 self.attrs = attrs;
2780 self
2781 }
2782}
2783
2784impl<'a> askama::filters::HtmlSafe for ContextSwitcher<'a> {}
2785
2786#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2787pub struct SidenavItem<'a> {
2788 pub label: &'a str,
2789 pub href: &'a str,
2790 pub badge: Option<&'a str>,
2791 pub coming_soon: Option<&'a str>,
2792 pub active: bool,
2793 pub muted: bool,
2794 pub disabled: bool,
2795 pub attrs: &'a [HtmlAttr<'a>],
2796}
2797
2798impl<'a> SidenavItem<'a> {
2799 pub const fn link(label: &'a str, href: &'a str) -> Self {
2800 Self {
2801 label,
2802 href,
2803 badge: None,
2804 coming_soon: None,
2805 active: false,
2806 muted: false,
2807 disabled: false,
2808 attrs: &[],
2809 }
2810 }
2811
2812 pub const fn active(mut self) -> Self {
2813 self.active = true;
2814 self
2815 }
2816
2817 pub const fn muted(mut self) -> Self {
2818 self.muted = true;
2819 self
2820 }
2821
2822 pub const fn disabled(mut self) -> Self {
2823 self.disabled = true;
2824 self
2825 }
2826
2827 pub const fn with_badge(mut self, badge: &'a str) -> Self {
2828 self.badge = Some(badge);
2829 self
2830 }
2831
2832 pub const fn with_coming_soon(mut self, coming_soon: &'a str) -> Self {
2833 self.coming_soon = Some(coming_soon);
2834 self
2835 }
2836
2837 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
2838 self.attrs = attrs;
2839 self
2840 }
2841
2842 pub fn class_name(&self) -> &'static str {
2843 match (self.active, self.muted, self.disabled) {
2844 (true, true, true) => "wf-sidenav-item is-active is-muted is-disabled",
2845 (true, true, false) => "wf-sidenav-item is-active is-muted",
2846 (true, false, true) => "wf-sidenav-item is-active is-disabled",
2847 (true, false, false) => "wf-sidenav-item is-active",
2848 (false, true, true) => "wf-sidenav-item is-muted is-disabled",
2849 (false, true, false) => "wf-sidenav-item is-muted",
2850 (false, false, true) => "wf-sidenav-item is-disabled",
2851 (false, false, false) => "wf-sidenav-item",
2852 }
2853 }
2854}
2855
2856#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2857pub struct SidenavSection<'a> {
2858 pub label: &'a str,
2859 pub items: &'a [SidenavItem<'a>],
2860}
2861
2862impl<'a> SidenavSection<'a> {
2863 pub const fn new(label: &'a str, items: &'a [SidenavItem<'a>]) -> Self {
2864 Self { label, items }
2865 }
2866}
2867
2868#[derive(Debug, Template)]
2869#[non_exhaustive]
2870#[template(path = "components/sidenav.html")]
2871pub struct Sidenav<'a> {
2872 pub sections: &'a [SidenavSection<'a>],
2873 pub attrs: &'a [HtmlAttr<'a>],
2874 pub landmark: bool,
2875}
2876
2877impl<'a> Sidenav<'a> {
2878 pub const fn new(sections: &'a [SidenavSection<'a>]) -> Self {
2879 Self {
2880 sections,
2881 attrs: &[],
2882 landmark: true,
2883 }
2884 }
2885
2886 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
2887 self.attrs = attrs;
2888 self
2889 }
2890
2891 pub const fn embedded(mut self) -> Self {
2892 self.landmark = false;
2893 self
2894 }
2895}
2896
2897impl<'a> askama::filters::HtmlSafe for Sidenav<'a> {}
2898
2899#[derive(Debug, Template)]
2900#[non_exhaustive]
2901#[template(path = "components/topbar.html")]
2902pub struct Topbar<'a> {
2903 pub breadcrumbs_html: TrustedHtml<'a>,
2904 pub actions_html: TrustedHtml<'a>,
2905}
2906
2907impl<'a> Topbar<'a> {
2908 pub const fn new(breadcrumbs_html: TrustedHtml<'a>, actions_html: TrustedHtml<'a>) -> Self {
2909 Self {
2910 breadcrumbs_html,
2911 actions_html,
2912 }
2913 }
2914}
2915
2916impl<'a> askama::filters::HtmlSafe for Topbar<'a> {}
2917
2918#[derive(Debug, Template)]
2919#[non_exhaustive]
2920#[template(path = "components/statusbar.html")]
2921pub struct Statusbar<'a> {
2922 pub left: &'a str,
2923 pub right: &'a str,
2924}
2925
2926impl<'a> Statusbar<'a> {
2927 pub const fn new(left: &'a str, right: &'a str) -> Self {
2928 Self { left, right }
2929 }
2930}
2931
2932impl<'a> askama::filters::HtmlSafe for Statusbar<'a> {}
2933
2934#[derive(Debug, Template)]
2935#[non_exhaustive]
2936#[template(path = "components/empty_state.html")]
2937pub struct EmptyState<'a> {
2938 pub title: &'a str,
2939 pub body: &'a str,
2940 pub glyph_html: Option<TrustedHtml<'a>>,
2941 pub actions_html: Option<TrustedHtml<'a>>,
2942 pub bordered: bool,
2943 pub dense: bool,
2944}
2945
2946impl<'a> EmptyState<'a> {
2947 pub const fn new(title: &'a str, body: &'a str) -> Self {
2948 Self {
2949 title,
2950 body,
2951 glyph_html: None,
2952 actions_html: None,
2953 bordered: false,
2954 dense: false,
2955 }
2956 }
2957
2958 pub const fn with_glyph(mut self, glyph_html: TrustedHtml<'a>) -> Self {
2959 self.glyph_html = Some(glyph_html);
2960 self
2961 }
2962
2963 pub const fn with_actions(mut self, actions_html: TrustedHtml<'a>) -> Self {
2964 self.actions_html = Some(actions_html);
2965 self
2966 }
2967
2968 pub const fn bordered(mut self) -> Self {
2969 self.bordered = true;
2970 self
2971 }
2972
2973 pub const fn dense(mut self) -> Self {
2974 self.dense = true;
2975 self
2976 }
2977
2978 pub fn class_name(&self) -> String {
2979 let bordered = if self.bordered { " bordered" } else { "" };
2980 let dense = if self.dense { " dense" } else { "" };
2981 format!("wf-empty{bordered}{dense}")
2982 }
2983}
2984
2985impl<'a> askama::filters::HtmlSafe for EmptyState<'a> {}
2986
2987#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2988pub enum SortDirection {
2989 Ascending,
2990 Descending,
2991}
2992
2993impl SortDirection {
2994 fn arrow(self) -> &'static str {
2995 match self {
2996 Self::Ascending => "^",
2997 Self::Descending => "v",
2998 }
2999 }
3000}
3001
3002#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3003pub enum TableColumnWidth {
3004 Auto,
3005 ExtraSmall,
3006 Small,
3007 Medium,
3008 Large,
3009 ExtraLarge,
3010 Id,
3011 Checkbox,
3012 Action,
3013}
3014
3015#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3016pub struct TableHeader<'a> {
3017 pub label: &'a str,
3018 pub numeric: bool,
3019}
3020
3021impl<'a> TableHeader<'a> {
3022 pub const fn new(label: &'a str) -> Self {
3023 Self {
3024 label,
3025 numeric: false,
3026 }
3027 }
3028
3029 pub const fn numeric(label: &'a str) -> Self {
3030 Self {
3031 label,
3032 numeric: true,
3033 }
3034 }
3035
3036 pub fn class_name(&self) -> &'static str {
3037 if self.numeric { "num" } else { "" }
3038 }
3039}
3040
3041#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3042pub struct TableCell<'a> {
3043 pub text: &'a str,
3044 pub numeric: bool,
3045 pub strong: bool,
3046 pub muted: bool,
3047}
3048
3049impl<'a> TableCell<'a> {
3050 pub const fn new(text: &'a str) -> Self {
3051 Self {
3052 text,
3053 numeric: false,
3054 strong: false,
3055 muted: false,
3056 }
3057 }
3058
3059 pub const fn numeric(text: &'a str) -> Self {
3060 Self {
3061 numeric: true,
3062 ..Self::new(text)
3063 }
3064 }
3065
3066 pub const fn strong(text: &'a str) -> Self {
3067 Self {
3068 strong: true,
3069 ..Self::new(text)
3070 }
3071 }
3072
3073 pub const fn muted(text: &'a str) -> Self {
3074 Self {
3075 muted: true,
3076 ..Self::new(text)
3077 }
3078 }
3079
3080 pub fn class_name(&self) -> &'static str {
3081 match (self.numeric, self.strong, self.muted) {
3082 (false, false, false) => "",
3083 (true, false, false) => "num",
3084 (false, true, false) => "strong",
3085 (false, false, true) => "muted",
3086 (true, true, false) => "num strong",
3087 (true, false, true) => "num muted",
3088 (false, true, true) => "strong muted",
3089 (true, true, true) => "num strong muted",
3090 }
3091 }
3092}
3093
3094#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3095pub struct TableRow<'a> {
3096 pub cells: &'a [TableCell<'a>],
3097 pub selected: bool,
3098}
3099
3100impl<'a> TableRow<'a> {
3101 pub const fn new(cells: &'a [TableCell<'a>]) -> Self {
3102 Self {
3103 cells,
3104 selected: false,
3105 }
3106 }
3107
3108 pub const fn selected(mut self) -> Self {
3109 self.selected = true;
3110 self
3111 }
3112}
3113
3114#[derive(Debug, Template)]
3115#[non_exhaustive]
3116#[template(path = "components/table.html")]
3117pub struct Table<'a> {
3118 pub headers: &'a [TableHeader<'a>],
3119 pub rows: &'a [TableRow<'a>],
3120 pub flush: bool,
3121 pub interactive: bool,
3122 pub sticky: bool,
3123 pub pin_last: bool,
3124}
3125
3126impl<'a> Table<'a> {
3127 pub const fn new(headers: &'a [TableHeader<'a>], rows: &'a [TableRow<'a>]) -> Self {
3128 Self {
3129 headers,
3130 rows,
3131 flush: false,
3132 interactive: false,
3133 sticky: false,
3134 pin_last: false,
3135 }
3136 }
3137
3138 pub const fn flush(mut self) -> Self {
3139 self.flush = true;
3140 self
3141 }
3142
3143 pub const fn interactive(mut self) -> Self {
3144 self.interactive = true;
3145 self
3146 }
3147
3148 pub const fn sticky(mut self) -> Self {
3149 self.sticky = true;
3150 self
3151 }
3152
3153 pub const fn pin_last(mut self) -> Self {
3154 self.pin_last = true;
3155 self
3156 }
3157
3158 pub fn class_name(&self) -> String {
3159 let flush = if self.flush { " flush" } else { "" };
3160 let interactive = if self.interactive {
3161 " is-interactive"
3162 } else {
3163 ""
3164 };
3165 let sticky = if self.sticky { " sticky" } else { "" };
3166 let pin_last = if self.pin_last { " pin-last" } else { "" };
3167 format!("wf-table{flush}{interactive}{sticky}{pin_last}")
3168 }
3169}
3170
3171impl<'a> askama::filters::HtmlSafe for Table<'a> {}
3172
3173#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3174pub struct DataTableHeader<'a> {
3175 pub label: &'a str,
3176 pub numeric: bool,
3177 pub sort_key: Option<&'a str>,
3178 pub sort_direction: Option<SortDirection>,
3179 pub width: TableColumnWidth,
3180}
3181
3182impl<'a> DataTableHeader<'a> {
3183 pub const fn new(label: &'a str) -> Self {
3184 Self {
3185 label,
3186 numeric: false,
3187 sort_key: None,
3188 sort_direction: None,
3189 width: TableColumnWidth::Auto,
3190 }
3191 }
3192
3193 pub const fn numeric(label: &'a str) -> Self {
3194 Self {
3195 numeric: true,
3196 ..Self::new(label)
3197 }
3198 }
3199
3200 pub const fn sort(label: &'a str, sort_key: &'a str) -> Self {
3201 Self {
3202 sort_key: Some(sort_key),
3203 ..Self::new(label)
3204 }
3205 }
3206
3207 pub const fn sorted(label: &'a str, sort_key: &'a str, direction: SortDirection) -> Self {
3208 Self {
3209 sort_direction: Some(direction),
3210 ..Self::sort(label, sort_key)
3211 }
3212 }
3213
3214 pub const fn sortable(mut self, sort_key: &'a str, direction: SortDirection) -> Self {
3215 self.sort_key = Some(sort_key);
3216 self.sort_direction = Some(direction);
3217 self
3218 }
3219
3220 pub const fn with_width(mut self, width: TableColumnWidth) -> Self {
3221 self.width = width;
3222 self
3223 }
3224
3225 pub const fn action_column(mut self) -> Self {
3226 self.width = TableColumnWidth::Action;
3227 self
3228 }
3229
3230 pub fn class_name(&self) -> &'static str {
3231 match (self.width, self.numeric) {
3232 (TableColumnWidth::Auto, false) => "",
3233 (TableColumnWidth::Auto, true) => "num",
3234 (TableColumnWidth::ExtraSmall, false) => "wf-col-xs",
3235 (TableColumnWidth::ExtraSmall, true) => "wf-col-xs num",
3236 (TableColumnWidth::Small, false) => "wf-col-sm",
3237 (TableColumnWidth::Small, true) => "wf-col-sm num",
3238 (TableColumnWidth::Medium, false) => "wf-col-md",
3239 (TableColumnWidth::Medium, true) => "wf-col-md num",
3240 (TableColumnWidth::Large, false) => "wf-col-lg",
3241 (TableColumnWidth::Large, true) => "wf-col-lg num",
3242 (TableColumnWidth::ExtraLarge, false) => "wf-col-xl",
3243 (TableColumnWidth::ExtraLarge, true) => "wf-col-xl num",
3244 (TableColumnWidth::Id, false) => "wf-col-id",
3245 (TableColumnWidth::Id, true) => "wf-col-id num",
3246 (TableColumnWidth::Checkbox, false) => "wf-col-chk",
3247 (TableColumnWidth::Checkbox, true) => "wf-col-chk num",
3248 (TableColumnWidth::Action, false) => "wf-col-act",
3249 (TableColumnWidth::Action, true) => "wf-col-act num",
3250 }
3251 }
3252
3253 pub fn sort_arrow(&self) -> &'static str {
3254 self.sort_direction.map(SortDirection::arrow).unwrap_or("-")
3255 }
3256}
3257
3258#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3259pub struct DataTableCell<'a> {
3260 pub text: &'a str,
3261 pub html: Option<TrustedHtml<'a>>,
3262 pub numeric: bool,
3263 pub strong: bool,
3264 pub muted: bool,
3265}
3266
3267impl<'a> DataTableCell<'a> {
3268 pub const fn new(text: &'a str) -> Self {
3269 Self {
3270 text,
3271 html: None,
3272 numeric: false,
3273 strong: false,
3274 muted: false,
3275 }
3276 }
3277
3278 pub const fn numeric(text: &'a str) -> Self {
3279 Self {
3280 numeric: true,
3281 ..Self::new(text)
3282 }
3283 }
3284
3285 pub const fn strong(text: &'a str) -> Self {
3286 Self {
3287 strong: true,
3288 ..Self::new(text)
3289 }
3290 }
3291
3292 pub const fn muted(text: &'a str) -> Self {
3293 Self {
3294 muted: true,
3295 ..Self::new(text)
3296 }
3297 }
3298
3299 pub const fn html(html: TrustedHtml<'a>) -> Self {
3300 Self {
3301 text: "",
3302 html: Some(html),
3303 numeric: false,
3304 strong: false,
3305 muted: false,
3306 }
3307 }
3308
3309 pub fn class_name(&self) -> &'static str {
3310 match (self.numeric, self.strong, self.muted) {
3311 (false, false, false) => "",
3312 (true, false, false) => "num",
3313 (false, true, false) => "strong",
3314 (false, false, true) => "muted",
3315 (true, true, false) => "num strong",
3316 (true, false, true) => "num muted",
3317 (false, true, true) => "strong muted",
3318 (true, true, true) => "num strong muted",
3319 }
3320 }
3321}
3322
3323#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3324pub struct DataTableRow<'a> {
3325 pub cells: &'a [DataTableCell<'a>],
3326 pub selected: bool,
3327}
3328
3329impl<'a> DataTableRow<'a> {
3330 pub const fn new(cells: &'a [DataTableCell<'a>]) -> Self {
3331 Self {
3332 cells,
3333 selected: false,
3334 }
3335 }
3336
3337 pub const fn selected(mut self) -> Self {
3338 self.selected = true;
3339 self
3340 }
3341}
3342
3343#[derive(Debug, Template)]
3344#[non_exhaustive]
3345#[template(path = "components/data_table.html")]
3346pub struct DataTable<'a> {
3347 pub headers: &'a [DataTableHeader<'a>],
3348 pub rows: &'a [DataTableRow<'a>],
3349 pub flush: bool,
3350 pub interactive: bool,
3351 pub sticky: bool,
3352 pub pin_last: bool,
3353}
3354
3355impl<'a> DataTable<'a> {
3356 pub const fn new(headers: &'a [DataTableHeader<'a>], rows: &'a [DataTableRow<'a>]) -> Self {
3357 Self {
3358 headers,
3359 rows,
3360 flush: false,
3361 interactive: false,
3362 sticky: false,
3363 pin_last: false,
3364 }
3365 }
3366
3367 pub const fn flush(mut self) -> Self {
3368 self.flush = true;
3369 self
3370 }
3371
3372 pub const fn interactive(mut self) -> Self {
3373 self.interactive = true;
3374 self
3375 }
3376
3377 pub const fn sticky(mut self) -> Self {
3378 self.sticky = true;
3379 self
3380 }
3381
3382 pub const fn pin_last(mut self) -> Self {
3383 self.pin_last = true;
3384 self
3385 }
3386
3387 pub fn class_name(&self) -> String {
3388 let flush = if self.flush { " flush" } else { "" };
3389 let interactive = if self.interactive {
3390 " is-interactive"
3391 } else {
3392 ""
3393 };
3394 let sticky = if self.sticky { " sticky" } else { "" };
3395 let pin_last = if self.pin_last { " pin-last" } else { "" };
3396 format!("wf-table{flush}{interactive}{sticky}{pin_last}")
3397 }
3398}
3399
3400impl<'a> askama::filters::HtmlSafe for DataTable<'a> {}
3401
3402#[derive(Debug, Template)]
3403#[non_exhaustive]
3404#[template(path = "components/filter_bar.html")]
3405pub struct FilterBar<'a> {
3406 pub controls_html: TrustedHtml<'a>,
3407 pub actions_html: Option<TrustedHtml<'a>>,
3408 pub attrs: &'a [HtmlAttr<'a>],
3409}
3410
3411impl<'a> FilterBar<'a> {
3412 pub const fn new(controls_html: TrustedHtml<'a>) -> Self {
3413 Self {
3414 controls_html,
3415 actions_html: None,
3416 attrs: &[],
3417 }
3418 }
3419
3420 pub const fn with_actions(mut self, actions_html: TrustedHtml<'a>) -> Self {
3421 self.actions_html = Some(actions_html);
3422 self
3423 }
3424
3425 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
3426 self.attrs = attrs;
3427 self
3428 }
3429}
3430
3431impl<'a> askama::filters::HtmlSafe for FilterBar<'a> {}
3432
3433#[derive(Debug, Template)]
3434#[non_exhaustive]
3435#[template(path = "components/bulk_action_bar.html")]
3436pub struct BulkActionBar<'a> {
3437 pub count_label: &'a str,
3438 pub actions_html: TrustedHtml<'a>,
3439 pub attrs: &'a [HtmlAttr<'a>],
3440}
3441
3442impl<'a> BulkActionBar<'a> {
3443 pub const fn new(count_label: &'a str, actions_html: TrustedHtml<'a>) -> Self {
3444 Self {
3445 count_label,
3446 actions_html,
3447 attrs: &[],
3448 }
3449 }
3450
3451 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
3452 self.attrs = attrs;
3453 self
3454 }
3455}
3456
3457impl<'a> askama::filters::HtmlSafe for BulkActionBar<'a> {}
3458
3459#[derive(Debug, Template)]
3460#[non_exhaustive]
3461#[template(path = "components/table_footer.html")]
3462pub struct TableFooter<'a> {
3463 pub content_html: TrustedHtml<'a>,
3464 pub actions_html: Option<TrustedHtml<'a>>,
3465 pub attrs: &'a [HtmlAttr<'a>],
3466}
3467
3468impl<'a> TableFooter<'a> {
3469 pub const fn new(content_html: TrustedHtml<'a>) -> Self {
3470 Self {
3471 content_html,
3472 actions_html: None,
3473 attrs: &[],
3474 }
3475 }
3476
3477 pub const fn with_actions(mut self, actions_html: TrustedHtml<'a>) -> Self {
3478 self.actions_html = Some(actions_html);
3479 self
3480 }
3481
3482 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
3483 self.attrs = attrs;
3484 self
3485 }
3486}
3487
3488impl<'a> askama::filters::HtmlSafe for TableFooter<'a> {}
3489
3490#[derive(Debug, Template)]
3491#[non_exhaustive]
3492#[template(path = "components/row_select.html")]
3493pub struct RowSelect<'a> {
3494 pub name: &'a str,
3495 pub value: &'a str,
3496 pub label: &'a str,
3497 pub checked: bool,
3498 pub disabled: bool,
3499 pub attrs: &'a [HtmlAttr<'a>],
3500}
3501
3502impl<'a> RowSelect<'a> {
3503 pub const fn new(name: &'a str, value: &'a str, label: &'a str) -> Self {
3504 Self {
3505 name,
3506 value,
3507 label,
3508 checked: false,
3509 disabled: false,
3510 attrs: &[],
3511 }
3512 }
3513
3514 pub const fn checked(mut self) -> Self {
3515 self.checked = true;
3516 self
3517 }
3518
3519 pub const fn disabled(mut self) -> Self {
3520 self.disabled = true;
3521 self
3522 }
3523
3524 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
3525 self.attrs = attrs;
3526 self
3527 }
3528}
3529
3530impl<'a> askama::filters::HtmlSafe for RowSelect<'a> {}
3531
3532#[derive(Debug, Template)]
3533#[non_exhaustive]
3534#[template(path = "components/table_wrap.html")]
3535pub struct TableWrap<'a> {
3536 pub table_html: TrustedHtml<'a>,
3537 pub filterbar_html: Option<TrustedHtml<'a>>,
3538 pub filterbar_component_html: Option<TrustedHtml<'a>>,
3539 pub bulk_count: Option<&'a str>,
3540 pub bulk_actions_html: Option<TrustedHtml<'a>>,
3541 pub bulkbar_component_html: Option<TrustedHtml<'a>>,
3542 pub footer_html: Option<TrustedHtml<'a>>,
3543 pub footer_component_html: Option<TrustedHtml<'a>>,
3544}
3545
3546impl<'a> TableWrap<'a> {
3547 pub const fn new(table_html: TrustedHtml<'a>) -> Self {
3548 Self {
3549 table_html,
3550 filterbar_html: None,
3551 filterbar_component_html: None,
3552 bulk_count: None,
3553 bulk_actions_html: None,
3554 bulkbar_component_html: None,
3555 footer_html: None,
3556 footer_component_html: None,
3557 }
3558 }
3559
3560 pub const fn with_filterbar(mut self, filterbar_html: TrustedHtml<'a>) -> Self {
3561 self.filterbar_html = Some(filterbar_html);
3562 self
3563 }
3564
3565 pub const fn with_filterbar_component(mut self, filterbar_html: TrustedHtml<'a>) -> Self {
3566 self.filterbar_component_html = Some(filterbar_html);
3567 self
3568 }
3569
3570 pub const fn with_bulkbar(
3571 mut self,
3572 bulk_count: &'a str,
3573 bulk_actions_html: TrustedHtml<'a>,
3574 ) -> Self {
3575 self.bulk_count = Some(bulk_count);
3576 self.bulk_actions_html = Some(bulk_actions_html);
3577 self
3578 }
3579
3580 pub const fn with_bulkbar_component(mut self, bulkbar_html: TrustedHtml<'a>) -> Self {
3581 self.bulkbar_component_html = Some(bulkbar_html);
3582 self
3583 }
3584
3585 pub const fn with_footer(mut self, footer_html: TrustedHtml<'a>) -> Self {
3586 self.footer_html = Some(footer_html);
3587 self
3588 }
3589
3590 pub const fn with_footer_component(mut self, footer_html: TrustedHtml<'a>) -> Self {
3591 self.footer_component_html = Some(footer_html);
3592 self
3593 }
3594}
3595
3596impl<'a> askama::filters::HtmlSafe for TableWrap<'a> {}
3597
3598#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3599pub struct DefinitionItem<'a> {
3600 pub term: &'a str,
3601 pub description: &'a str,
3602}
3603
3604impl<'a> DefinitionItem<'a> {
3605 pub const fn new(term: &'a str, description: &'a str) -> Self {
3606 Self { term, description }
3607 }
3608}
3609
3610#[derive(Debug, Template)]
3611#[non_exhaustive]
3612#[template(path = "components/definition_list.html")]
3613pub struct DefinitionList<'a> {
3614 pub items: &'a [DefinitionItem<'a>],
3615 pub flush: bool,
3616}
3617
3618impl<'a> DefinitionList<'a> {
3619 pub const fn new(items: &'a [DefinitionItem<'a>]) -> Self {
3620 Self {
3621 items,
3622 flush: false,
3623 }
3624 }
3625
3626 pub const fn flush(mut self) -> Self {
3627 self.flush = true;
3628 self
3629 }
3630
3631 pub fn class_name(&self) -> &'static str {
3632 if self.flush { "wf-dl flush" } else { "wf-dl" }
3633 }
3634}
3635
3636impl<'a> askama::filters::HtmlSafe for DefinitionList<'a> {}
3637
3638#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3639pub struct RankRow<'a> {
3640 pub label: &'a str,
3641 pub value: &'a str,
3642 pub percent: u8,
3643}
3644
3645impl<'a> RankRow<'a> {
3646 pub const fn new(label: &'a str, value: &'a str, percent: u8) -> Self {
3647 Self {
3648 label,
3649 value,
3650 percent,
3651 }
3652 }
3653
3654 pub fn bounded_percent(&self) -> u8 {
3655 self.percent.min(100)
3656 }
3657}
3658
3659#[derive(Debug, Template)]
3660#[non_exhaustive]
3661#[template(path = "components/rank_list.html")]
3662pub struct RankList<'a> {
3663 pub rows: &'a [RankRow<'a>],
3664}
3665
3666impl<'a> RankList<'a> {
3667 pub const fn new(rows: &'a [RankRow<'a>]) -> Self {
3668 Self { rows }
3669 }
3670}
3671
3672impl<'a> askama::filters::HtmlSafe for RankList<'a> {}
3673
3674#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3675pub struct FeedRow<'a> {
3676 pub time: &'a str,
3677 pub kicker: &'a str,
3678 pub text: &'a str,
3679}
3680
3681impl<'a> FeedRow<'a> {
3682 pub const fn new(time: &'a str, kicker: &'a str, text: &'a str) -> Self {
3683 Self { time, kicker, text }
3684 }
3685}
3686
3687#[derive(Debug, Template)]
3688#[non_exhaustive]
3689#[template(path = "components/feed.html")]
3690pub struct Feed<'a> {
3691 pub rows: &'a [FeedRow<'a>],
3692}
3693
3694impl<'a> Feed<'a> {
3695 pub const fn new(rows: &'a [FeedRow<'a>]) -> Self {
3696 Self { rows }
3697 }
3698}
3699
3700impl<'a> askama::filters::HtmlSafe for Feed<'a> {}
3701
3702#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3703pub struct TimelineItem<'a> {
3704 pub time: &'a str,
3705 pub title: &'a str,
3706 pub body_html: TrustedHtml<'a>,
3707 pub active: bool,
3708}
3709
3710impl<'a> TimelineItem<'a> {
3711 pub const fn new(time: &'a str, title: &'a str, body_html: TrustedHtml<'a>) -> Self {
3712 Self {
3713 time,
3714 title,
3715 body_html,
3716 active: false,
3717 }
3718 }
3719
3720 pub const fn active(mut self) -> Self {
3721 self.active = true;
3722 self
3723 }
3724
3725 pub fn class_name(&self) -> &'static str {
3726 if self.active {
3727 "wf-timeline-item is-active"
3728 } else {
3729 "wf-timeline-item"
3730 }
3731 }
3732}
3733
3734#[derive(Debug, Template)]
3735#[non_exhaustive]
3736#[template(path = "components/timeline.html")]
3737pub struct Timeline<'a> {
3738 pub items: &'a [TimelineItem<'a>],
3739}
3740
3741impl<'a> Timeline<'a> {
3742 pub const fn new(items: &'a [TimelineItem<'a>]) -> Self {
3743 Self { items }
3744 }
3745}
3746
3747impl<'a> askama::filters::HtmlSafe for Timeline<'a> {}
3748
3749#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3750pub enum TreeItemKind {
3751 Folder,
3752 File,
3753}
3754
3755#[derive(Clone, Copy, Debug, Eq, PartialEq)]
3756pub struct TreeItem<'a> {
3757 pub kind: TreeItemKind,
3758 pub label: &'a str,
3759 pub active: bool,
3760 pub collapsed: bool,
3761 pub children_html: Option<TrustedHtml<'a>>,
3762}
3763
3764impl<'a> TreeItem<'a> {
3765 pub const fn folder(label: &'a str) -> Self {
3766 Self {
3767 kind: TreeItemKind::Folder,
3768 label,
3769 active: false,
3770 collapsed: false,
3771 children_html: None,
3772 }
3773 }
3774
3775 pub const fn file(label: &'a str) -> Self {
3776 Self {
3777 kind: TreeItemKind::File,
3778 label,
3779 active: false,
3780 collapsed: false,
3781 children_html: None,
3782 }
3783 }
3784
3785 pub const fn active(mut self) -> Self {
3786 self.active = true;
3787 self
3788 }
3789
3790 pub const fn collapsed(mut self) -> Self {
3791 self.collapsed = true;
3792 self
3793 }
3794
3795 pub const fn with_children(mut self, children_html: TrustedHtml<'a>) -> Self {
3796 self.children_html = Some(children_html);
3797 self
3798 }
3799
3800 pub fn item_class(&self) -> &'static str {
3801 if self.collapsed { "is-collapsed" } else { "" }
3802 }
3803
3804 pub fn label_class(&self) -> &'static str {
3805 match (self.kind, self.active) {
3806 (TreeItemKind::Folder, _) => "tree-folder",
3807 (TreeItemKind::File, true) => "tree-file is-active",
3808 (TreeItemKind::File, false) => "tree-file",
3809 }
3810 }
3811}
3812
3813#[derive(Debug, Template)]
3814#[non_exhaustive]
3815#[template(path = "components/tree_view.html")]
3816pub struct TreeView<'a> {
3817 pub items: &'a [TreeItem<'a>],
3818 pub nested: bool,
3819}
3820
3821impl<'a> TreeView<'a> {
3822 pub const fn new(items: &'a [TreeItem<'a>]) -> Self {
3823 Self {
3824 items,
3825 nested: false,
3826 }
3827 }
3828
3829 pub const fn nested(mut self) -> Self {
3830 self.nested = true;
3831 self
3832 }
3833
3834 pub fn class_name(&self) -> &'static str {
3835 if self.nested { "" } else { "wf-tree" }
3836 }
3837}
3838
3839impl<'a> askama::filters::HtmlSafe for TreeView<'a> {}
3840
3841#[derive(Debug, Template)]
3842#[non_exhaustive]
3843#[template(path = "components/framed.html")]
3844pub struct Framed<'a> {
3845 pub content_html: TrustedHtml<'a>,
3846 pub dense: bool,
3847 pub dashed: bool,
3848}
3849
3850impl<'a> Framed<'a> {
3851 pub const fn new(content_html: TrustedHtml<'a>) -> Self {
3852 Self {
3853 content_html,
3854 dense: false,
3855 dashed: false,
3856 }
3857 }
3858
3859 pub const fn dense(mut self) -> Self {
3860 self.dense = true;
3861 self
3862 }
3863
3864 pub const fn dashed(mut self) -> Self {
3865 self.dashed = true;
3866 self
3867 }
3868
3869 pub fn class_name(&self) -> String {
3870 let dense = if self.dense { " dense" } else { "" };
3871 let dashed = if self.dashed { " dashed" } else { "" };
3872 format!("wf-framed{dense}{dashed}")
3873 }
3874}
3875
3876impl<'a> askama::filters::HtmlSafe for Framed<'a> {}
3877
3878#[derive(Debug, Template)]
3879#[non_exhaustive]
3880#[template(path = "components/grid.html")]
3881pub struct Grid<'a> {
3882 pub content_html: TrustedHtml<'a>,
3883 pub columns: u8,
3884}
3885
3886impl<'a> Grid<'a> {
3887 pub const fn new(content_html: TrustedHtml<'a>) -> Self {
3888 Self {
3889 content_html,
3890 columns: 2,
3891 }
3892 }
3893
3894 pub const fn with_columns(mut self, columns: u8) -> Self {
3895 self.columns = columns;
3896 self
3897 }
3898
3899 pub fn class_name(&self) -> String {
3900 format!("wf-grid cols-{}", self.columns)
3901 }
3902}
3903
3904impl<'a> askama::filters::HtmlSafe for Grid<'a> {}
3905
3906#[derive(Debug, Template)]
3907#[non_exhaustive]
3908#[template(path = "components/split.html")]
3909pub struct Split<'a> {
3910 pub content_html: TrustedHtml<'a>,
3911 pub vertical: bool,
3912}
3913
3914impl<'a> Split<'a> {
3915 pub const fn new(content_html: TrustedHtml<'a>) -> Self {
3916 Self {
3917 content_html,
3918 vertical: false,
3919 }
3920 }
3921
3922 pub const fn vertical(mut self) -> Self {
3923 self.vertical = true;
3924 self
3925 }
3926
3927 pub fn class_name(&self) -> &'static str {
3928 if self.vertical {
3929 "wf-split vertical"
3930 } else {
3931 "wf-split"
3932 }
3933 }
3934}
3935
3936impl<'a> askama::filters::HtmlSafe for Split<'a> {}
3937
3938#[derive(Debug, Template)]
3939#[non_exhaustive]
3940#[template(path = "components/callout.html")]
3941pub struct Callout<'a> {
3942 pub kind: FeedbackKind,
3943 pub title: Option<&'a str>,
3944 pub body_html: TrustedHtml<'a>,
3945}
3946
3947impl<'a> Callout<'a> {
3948 pub const fn new(kind: FeedbackKind, body_html: TrustedHtml<'a>) -> Self {
3949 Self {
3950 kind,
3951 title: None,
3952 body_html,
3953 }
3954 }
3955
3956 pub const fn with_title(mut self, title: &'a str) -> Self {
3957 self.title = Some(title);
3958 self
3959 }
3960
3961 pub fn class_name(&self) -> String {
3962 format!("wf-callout {}", self.kind.class())
3963 }
3964}
3965
3966impl<'a> askama::filters::HtmlSafe for Callout<'a> {}
3967
3968#[derive(Debug, Template)]
3969#[non_exhaustive]
3970#[template(path = "components/toast.html")]
3971pub struct Toast<'a> {
3972 pub kind: FeedbackKind,
3973 pub message: &'a str,
3974}
3975
3976impl<'a> Toast<'a> {
3977 pub const fn new(kind: FeedbackKind, message: &'a str) -> Self {
3978 Self { kind, message }
3979 }
3980
3981 pub fn class_name(&self) -> String {
3982 format!("wf-toast {}", self.kind.class())
3983 }
3984}
3985
3986impl<'a> askama::filters::HtmlSafe for Toast<'a> {}
3987
3988#[derive(Debug, Template)]
3989#[non_exhaustive]
3990#[template(path = "components/toast_host.html")]
3991pub struct ToastHost<'a> {
3992 pub id: &'a str,
3993}
3994
3995impl<'a> ToastHost<'a> {
3996 pub const fn new() -> Self {
3997 Self { id: "toast-host" }
3998 }
3999
4000 pub const fn with_id(mut self, id: &'a str) -> Self {
4001 self.id = id;
4002 self
4003 }
4004}
4005
4006impl<'a> Default for ToastHost<'a> {
4007 fn default() -> Self {
4008 Self::new()
4009 }
4010}
4011
4012impl<'a> askama::filters::HtmlSafe for ToastHost<'a> {}
4013
4014#[derive(Debug, Template)]
4015#[non_exhaustive]
4016#[template(path = "components/tooltip.html")]
4017pub struct Tooltip<'a> {
4018 pub tip: &'a str,
4019 pub content_html: TrustedHtml<'a>,
4020}
4021
4022impl<'a> Tooltip<'a> {
4023 pub const fn new(tip: &'a str, content_html: TrustedHtml<'a>) -> Self {
4024 Self { tip, content_html }
4025 }
4026}
4027
4028impl<'a> askama::filters::HtmlSafe for Tooltip<'a> {}
4029
4030#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4031pub enum MenuItemKind {
4032 Button,
4033 Link,
4034 Separator,
4035}
4036
4037#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4038pub struct MenuItem<'a> {
4039 pub kind: MenuItemKind,
4040 pub label: &'a str,
4041 pub href: Option<&'a str>,
4042 pub danger: bool,
4043 pub disabled: bool,
4044 pub kbd: Option<&'a str>,
4045 pub attrs: &'a [HtmlAttr<'a>],
4046}
4047
4048impl<'a> MenuItem<'a> {
4049 pub const fn button(label: &'a str) -> Self {
4050 Self {
4051 kind: MenuItemKind::Button,
4052 label,
4053 href: None,
4054 danger: false,
4055 disabled: false,
4056 kbd: None,
4057 attrs: &[],
4058 }
4059 }
4060
4061 pub const fn link(label: &'a str, href: &'a str) -> Self {
4062 Self {
4063 kind: MenuItemKind::Link,
4064 href: Some(href),
4065 ..Self::button(label)
4066 }
4067 }
4068
4069 pub const fn separator() -> Self {
4070 Self {
4071 kind: MenuItemKind::Separator,
4072 label: "",
4073 href: None,
4074 danger: false,
4075 disabled: false,
4076 kbd: None,
4077 attrs: &[],
4078 }
4079 }
4080
4081 pub const fn danger(mut self) -> Self {
4082 self.danger = true;
4083 self
4084 }
4085
4086 pub const fn disabled(mut self) -> Self {
4087 self.disabled = true;
4088 self
4089 }
4090
4091 pub const fn with_kbd(mut self, kbd: &'a str) -> Self {
4092 self.kbd = Some(kbd);
4093 self
4094 }
4095
4096 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
4097 self.attrs = attrs;
4098 self
4099 }
4100
4101 pub fn class_name(&self) -> &'static str {
4102 if self.danger {
4103 "wf-menu-item danger"
4104 } else {
4105 "wf-menu-item"
4106 }
4107 }
4108}
4109
4110#[derive(Debug, Template)]
4111#[non_exhaustive]
4112#[template(path = "components/menu.html")]
4113pub struct Menu<'a> {
4114 pub items: &'a [MenuItem<'a>],
4115}
4116
4117impl<'a> Menu<'a> {
4118 pub const fn new(items: &'a [MenuItem<'a>]) -> Self {
4119 Self { items }
4120 }
4121}
4122
4123impl<'a> askama::filters::HtmlSafe for Menu<'a> {}
4124
4125#[derive(Debug, Template)]
4126#[non_exhaustive]
4127#[template(path = "components/popover.html")]
4128pub struct Popover<'a> {
4129 pub trigger_html: TrustedHtml<'a>,
4130 pub body_html: TrustedHtml<'a>,
4131 pub heading: Option<&'a str>,
4132 pub side: &'a str,
4133 pub open: bool,
4134}
4135
4136impl<'a> Popover<'a> {
4137 pub const fn new(trigger_html: TrustedHtml<'a>, body_html: TrustedHtml<'a>) -> Self {
4138 Self {
4139 trigger_html,
4140 body_html,
4141 heading: None,
4142 side: "bottom",
4143 open: false,
4144 }
4145 }
4146
4147 pub const fn with_heading(mut self, heading: &'a str) -> Self {
4148 self.heading = Some(heading);
4149 self
4150 }
4151
4152 pub const fn with_side(mut self, side: &'a str) -> Self {
4153 self.side = side;
4154 self
4155 }
4156
4157 pub const fn open(mut self) -> Self {
4158 self.open = true;
4159 self
4160 }
4161
4162 pub fn popover_class(&self) -> &'static str {
4163 if self.open {
4164 "wf-popover is-open"
4165 } else {
4166 "wf-popover"
4167 }
4168 }
4169}
4170
4171impl<'a> askama::filters::HtmlSafe for Popover<'a> {}
4172
4173#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
4174pub enum ModalSize {
4175 #[default]
4176 Default,
4177 Large,
4178}
4179
4180impl ModalSize {
4181 fn class(self) -> &'static str {
4182 match self {
4183 Self::Default => "",
4184 Self::Large => " wf-modal--lg",
4185 }
4186 }
4187}
4188
4189#[derive(Debug, Template)]
4190#[non_exhaustive]
4191#[template(path = "components/modal.html")]
4192pub struct Modal<'a> {
4193 pub title: &'a str,
4194 pub body_html: TrustedHtml<'a>,
4195 pub footer_html: Option<TrustedHtml<'a>>,
4196 pub open: bool,
4197 pub size: ModalSize,
4198}
4199
4200impl<'a> Modal<'a> {
4201 pub const fn new(title: &'a str, body_html: TrustedHtml<'a>) -> Self {
4202 Self {
4203 title,
4204 body_html,
4205 footer_html: None,
4206 open: false,
4207 size: ModalSize::Default,
4208 }
4209 }
4210
4211 pub const fn with_footer(mut self, footer_html: TrustedHtml<'a>) -> Self {
4212 self.footer_html = Some(footer_html);
4213 self
4214 }
4215
4216 pub const fn open(mut self) -> Self {
4217 self.open = true;
4218 self
4219 }
4220
4221 pub const fn with_size(mut self, size: ModalSize) -> Self {
4222 self.size = size;
4223 self
4224 }
4225
4226 pub const fn large(self) -> Self {
4227 self.with_size(ModalSize::Large)
4228 }
4229
4230 pub fn overlay_class(&self) -> &'static str {
4231 if self.open {
4232 "wf-overlay is-open"
4233 } else {
4234 "wf-overlay"
4235 }
4236 }
4237
4238 pub fn modal_class(&self) -> String {
4239 let open = if self.open { " is-open" } else { "" };
4240 let size = self.size.class();
4241 format!("wf-modal{open}{size}")
4242 }
4243}
4244
4245impl<'a> Default for Modal<'a> {
4246 fn default() -> Self {
4247 Self::new("", TrustedHtml::new(""))
4248 }
4249}
4250
4251impl<'a> askama::filters::HtmlSafe for Modal<'a> {}
4252
4253#[derive(Debug, Template)]
4254#[non_exhaustive]
4255#[template(path = "components/drawer.html")]
4256pub struct Drawer<'a> {
4257 pub title: &'a str,
4258 pub body_html: TrustedHtml<'a>,
4259 pub footer_html: Option<TrustedHtml<'a>>,
4260 pub open: bool,
4261 pub left: bool,
4262}
4263
4264impl<'a> Drawer<'a> {
4265 pub const fn new(title: &'a str, body_html: TrustedHtml<'a>) -> Self {
4266 Self {
4267 title,
4268 body_html,
4269 footer_html: None,
4270 open: false,
4271 left: false,
4272 }
4273 }
4274
4275 pub const fn with_footer(mut self, footer_html: TrustedHtml<'a>) -> Self {
4276 self.footer_html = Some(footer_html);
4277 self
4278 }
4279
4280 pub const fn open(mut self) -> Self {
4281 self.open = true;
4282 self
4283 }
4284
4285 pub const fn left(mut self) -> Self {
4286 self.left = true;
4287 self
4288 }
4289
4290 pub fn overlay_class(&self) -> &'static str {
4291 if self.open {
4292 "wf-overlay is-open"
4293 } else {
4294 "wf-overlay"
4295 }
4296 }
4297
4298 pub fn drawer_class(&self) -> String {
4299 let open = if self.open { " is-open" } else { "" };
4300 let left = if self.left { " left" } else { "" };
4301 format!("wf-drawer{open}{left}")
4302 }
4303}
4304
4305impl<'a> askama::filters::HtmlSafe for Drawer<'a> {}
4306
4307#[derive(Debug, Template)]
4308#[non_exhaustive]
4309#[template(path = "components/progress.html")]
4310pub struct Progress {
4311 pub value: Option<u8>,
4312}
4313
4314impl Progress {
4315 pub const fn new(value: u8) -> Self {
4316 Self { value: Some(value) }
4317 }
4318
4319 pub const fn indeterminate() -> Self {
4320 Self { value: None }
4321 }
4322
4323 pub fn class_name(&self) -> &'static str {
4324 if self.value.is_some() {
4325 "wf-progress"
4326 } else {
4327 "wf-progress indeterminate"
4328 }
4329 }
4330
4331 pub fn bounded_value(&self) -> u8 {
4332 self.value.unwrap_or(0).min(100)
4333 }
4334}
4335
4336impl askama::filters::HtmlSafe for Progress {}
4337
4338#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4339pub enum MeterColor {
4340 Accent,
4341 Ok,
4342 Warn,
4343 Error,
4344 Info,
4345}
4346
4347impl MeterColor {
4348 fn css_var(self) -> &'static str {
4349 match self {
4350 Self::Accent => "var(--accent)",
4351 Self::Ok => "var(--ok)",
4352 Self::Warn => "var(--warn)",
4353 Self::Error => "var(--err)",
4354 Self::Info => "var(--info)",
4355 }
4356 }
4357}
4358
4359#[derive(Debug, Template)]
4360#[non_exhaustive]
4361#[template(path = "components/meter.html")]
4362pub struct Meter {
4363 pub value: u8,
4364 pub width_px: Option<u16>,
4365 pub height_px: Option<u16>,
4366 pub color: Option<MeterColor>,
4367}
4368
4369impl Meter {
4370 pub const fn new(value: u8) -> Self {
4371 Self {
4372 value,
4373 width_px: None,
4374 height_px: None,
4375 color: None,
4376 }
4377 }
4378
4379 pub const fn with_size_px(mut self, width_px: u16, height_px: u16) -> Self {
4380 self.width_px = Some(width_px);
4381 self.height_px = Some(height_px);
4382 self
4383 }
4384
4385 pub const fn with_color(mut self, color: MeterColor) -> Self {
4386 self.color = Some(color);
4387 self
4388 }
4389
4390 pub fn style(&self) -> String {
4391 let mut style = String::with_capacity(72);
4392 let _ = write!(&mut style, "--meter: {}%", self.value.min(100));
4393 if let Some(width) = self.width_px {
4394 let _ = write!(&mut style, "; --meter-w: {width}px");
4395 }
4396 if let Some(height) = self.height_px {
4397 let _ = write!(&mut style, "; --meter-h: {height}px");
4398 }
4399 if let Some(color) = self.color {
4400 style.push_str("; --meter-c: ");
4401 style.push_str(color.css_var());
4402 }
4403 style
4404 }
4405}
4406
4407impl askama::filters::HtmlSafe for Meter {}
4408
4409#[derive(Debug, Template)]
4410#[non_exhaustive]
4411#[template(path = "components/code_block.html")]
4412pub struct CodeBlock<'a> {
4413 pub code: &'a str,
4414 pub language: Option<&'a str>,
4415 pub label: Option<&'a str>,
4416 pub copy_target_id: Option<&'a str>,
4417 pub attrs: &'a [HtmlAttr<'a>],
4418}
4419
4420impl<'a> CodeBlock<'a> {
4421 pub const fn new(code: &'a str) -> Self {
4422 Self {
4423 code,
4424 language: None,
4425 label: None,
4426 copy_target_id: None,
4427 attrs: &[],
4428 }
4429 }
4430
4431 pub const fn with_language(mut self, language: &'a str) -> Self {
4432 self.language = Some(language);
4433 self
4434 }
4435
4436 pub const fn with_label(mut self, label: &'a str) -> Self {
4437 self.label = Some(label);
4438 self
4439 }
4440
4441 pub const fn with_copy_target(mut self, copy_target_id: &'a str) -> Self {
4442 self.copy_target_id = Some(copy_target_id);
4443 self
4444 }
4445
4446 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
4447 self.attrs = attrs;
4448 self
4449 }
4450}
4451
4452impl<'a> askama::filters::HtmlSafe for CodeBlock<'a> {}
4453
4454#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4455pub struct SnippetTab<'a> {
4456 pub label: &'a str,
4457 pub code: &'a str,
4458 pub language: Option<&'a str>,
4459 pub active: bool,
4460}
4461
4462impl<'a> SnippetTab<'a> {
4463 pub const fn new(label: &'a str, code: &'a str) -> Self {
4464 Self {
4465 label,
4466 code,
4467 language: None,
4468 active: false,
4469 }
4470 }
4471
4472 pub const fn with_language(mut self, language: &'a str) -> Self {
4473 self.language = Some(language);
4474 self
4475 }
4476
4477 pub const fn active(mut self) -> Self {
4478 self.active = true;
4479 self
4480 }
4481
4482 pub fn tab_class(&self) -> &'static str {
4483 if self.active {
4484 "wf-snippet-tab is-active"
4485 } else {
4486 "wf-snippet-tab"
4487 }
4488 }
4489
4490 pub fn panel_class(&self) -> &'static str {
4491 if self.active {
4492 "wf-snippet-panel is-active"
4493 } else {
4494 "wf-snippet-panel"
4495 }
4496 }
4497}
4498
4499#[derive(Debug, Template)]
4500#[non_exhaustive]
4501#[template(path = "components/snippet_tabs.html")]
4502pub struct SnippetTabs<'a> {
4503 pub id: &'a str,
4504 pub tabs: &'a [SnippetTab<'a>],
4505 pub attrs: &'a [HtmlAttr<'a>],
4506}
4507
4508impl<'a> SnippetTabs<'a> {
4509 pub const fn new(id: &'a str, tabs: &'a [SnippetTab<'a>]) -> Self {
4510 Self {
4511 id,
4512 tabs,
4513 attrs: &[],
4514 }
4515 }
4516
4517 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
4518 self.attrs = attrs;
4519 self
4520 }
4521}
4522
4523impl<'a> askama::filters::HtmlSafe for SnippetTabs<'a> {}
4524
4525#[derive(Debug, Template)]
4526#[non_exhaustive]
4527#[template(path = "components/strength_meter.html")]
4528pub struct StrengthMeter<'a> {
4529 pub value: u8,
4530 pub max: u8,
4531 pub text: &'a str,
4532 pub label: Option<&'a str>,
4533 pub kind: Option<FeedbackKind>,
4534 pub live: bool,
4535}
4536
4537impl<'a> StrengthMeter<'a> {
4538 pub const fn new(value: u8, max: u8, text: &'a str) -> Self {
4539 Self {
4540 value,
4541 max,
4542 text,
4543 label: None,
4544 kind: None,
4545 live: false,
4546 }
4547 }
4548
4549 pub const fn with_label(mut self, label: &'a str) -> Self {
4550 self.label = Some(label);
4551 self
4552 }
4553
4554 pub const fn with_feedback(mut self, feedback: FeedbackKind) -> Self {
4555 self.kind = Some(feedback);
4556 self
4557 }
4558
4559 pub const fn live(mut self) -> Self {
4560 self.live = true;
4561 self
4562 }
4563
4564 pub fn class_name(&self) -> &'static str {
4565 match self.kind {
4566 Some(FeedbackKind::Info) => "wf-strength-meter is-info",
4567 Some(FeedbackKind::Ok) => "wf-strength-meter is-ok",
4568 Some(FeedbackKind::Warn) => "wf-strength-meter is-warn",
4569 Some(FeedbackKind::Error) => "wf-strength-meter is-err",
4570 None => "wf-strength-meter",
4571 }
4572 }
4573
4574 pub fn bounded_value(&self) -> u8 {
4575 self.value.min(self.max)
4576 }
4577
4578 pub fn percentage(&self) -> u8 {
4579 if self.max == 0 {
4580 0
4581 } else {
4582 ((u16::from(self.bounded_value()) * 100) / u16::from(self.max)) as u8
4583 }
4584 }
4585
4586 pub fn style(&self) -> String {
4587 format!("--strength: {}%", self.percentage())
4588 }
4589}
4590
4591impl<'a> askama::filters::HtmlSafe for StrengthMeter<'a> {}
4592
4593#[derive(Debug, Template)]
4594#[non_exhaustive]
4595#[template(path = "components/kbd.html")]
4596pub struct Kbd<'a> {
4597 pub label: &'a str,
4598}
4599
4600impl<'a> Kbd<'a> {
4601 pub const fn new(label: &'a str) -> Self {
4602 Self { label }
4603 }
4604}
4605
4606impl<'a> askama::filters::HtmlSafe for Kbd<'a> {}
4607
4608#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4609pub enum SkeletonKind {
4610 Line,
4611 Title,
4612 Block,
4613}
4614
4615impl SkeletonKind {
4616 fn class(self) -> &'static str {
4617 match self {
4618 Self::Line => "line",
4619 Self::Title => "title",
4620 Self::Block => "block",
4621 }
4622 }
4623}
4624
4625#[derive(Debug, Template)]
4626#[non_exhaustive]
4627#[template(path = "components/skeleton.html")]
4628pub struct Skeleton {
4629 pub kind: SkeletonKind,
4630}
4631
4632impl Skeleton {
4633 pub const fn line() -> Self {
4634 Self {
4635 kind: SkeletonKind::Line,
4636 }
4637 }
4638
4639 pub const fn title() -> Self {
4640 Self {
4641 kind: SkeletonKind::Title,
4642 }
4643 }
4644
4645 pub const fn block() -> Self {
4646 Self {
4647 kind: SkeletonKind::Block,
4648 }
4649 }
4650
4651 pub fn class_name(&self) -> String {
4652 format!("wf-skeleton {}", self.kind.class())
4653 }
4654}
4655
4656impl askama::filters::HtmlSafe for Skeleton {}
4657
4658#[derive(Debug, Template)]
4659#[non_exhaustive]
4660#[template(path = "components/spinner.html")]
4661pub struct Spinner {
4662 pub large: bool,
4663}
4664
4665impl Spinner {
4666 pub const fn new() -> Self {
4667 Self { large: false }
4668 }
4669
4670 pub const fn large() -> Self {
4671 Self { large: true }
4672 }
4673
4674 pub fn class_name(&self) -> &'static str {
4675 if self.large {
4676 "wf-spinner lg"
4677 } else {
4678 "wf-spinner"
4679 }
4680 }
4681}
4682
4683impl Default for Spinner {
4684 fn default() -> Self {
4685 Self::new()
4686 }
4687}
4688
4689impl askama::filters::HtmlSafe for Spinner {}
4690
4691#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4692pub enum ModelineSegmentKind {
4693 Default,
4694 Chevron,
4695 Flag,
4696 Buffer,
4697 Mode,
4698 Position,
4699 Progress,
4700}
4701
4702impl ModelineSegmentKind {
4703 fn class(self) -> &'static str {
4704 match self {
4705 Self::Default => "wf-ml-seg",
4706 Self::Chevron => "wf-ml-seg wf-ml-chevron",
4707 Self::Flag => "wf-ml-seg wf-ml-flag",
4708 Self::Buffer => "wf-ml-seg wf-ml-buffer",
4709 Self::Mode => "wf-ml-seg wf-ml-mode",
4710 Self::Position => "wf-ml-seg wf-ml-pos",
4711 Self::Progress => "wf-ml-seg wf-ml-progress",
4712 }
4713 }
4714}
4715
4716#[derive(Debug, Template)]
4717#[non_exhaustive]
4718#[template(path = "components/modeline_segment.html")]
4719pub struct ModelineSegment<'a> {
4720 pub label: &'a str,
4721 pub kind: ModelineSegmentKind,
4722 pub state: Option<FeedbackKind>,
4723 pub href: Option<&'a str>,
4724 pub button: bool,
4725 pub button_type: &'a str,
4726 pub active: bool,
4727 pub kbd: Option<&'a str>,
4728 pub html: Option<TrustedHtml<'a>>,
4729 pub attrs: &'a [HtmlAttr<'a>],
4730}
4731
4732impl<'a> ModelineSegment<'a> {
4733 pub const fn text(label: &'a str) -> Self {
4734 Self {
4735 label,
4736 kind: ModelineSegmentKind::Default,
4737 state: None,
4738 href: None,
4739 button: false,
4740 button_type: "button",
4741 active: false,
4742 kbd: None,
4743 html: None,
4744 attrs: &[],
4745 }
4746 }
4747
4748 pub const fn chevron(label: &'a str) -> Self {
4749 Self {
4750 kind: ModelineSegmentKind::Chevron,
4751 ..Self::text(label)
4752 }
4753 }
4754
4755 pub const fn flag(label: &'a str) -> Self {
4756 Self {
4757 kind: ModelineSegmentKind::Flag,
4758 ..Self::text(label)
4759 }
4760 }
4761
4762 pub const fn buffer(label: &'a str) -> Self {
4763 Self {
4764 kind: ModelineSegmentKind::Buffer,
4765 ..Self::text(label)
4766 }
4767 }
4768
4769 pub const fn mode(label: &'a str) -> Self {
4770 Self {
4771 kind: ModelineSegmentKind::Mode,
4772 ..Self::text(label)
4773 }
4774 }
4775
4776 pub const fn position(label: &'a str) -> Self {
4777 Self {
4778 kind: ModelineSegmentKind::Position,
4779 ..Self::text(label)
4780 }
4781 }
4782
4783 pub const fn progress(label: &'a str) -> Self {
4784 Self {
4785 kind: ModelineSegmentKind::Progress,
4786 ..Self::text(label)
4787 }
4788 }
4789
4790 pub const fn link(label: &'a str, href: &'a str) -> Self {
4791 Self {
4792 href: Some(href),
4793 ..Self::text(label)
4794 }
4795 }
4796
4797 pub const fn button(label: &'a str) -> Self {
4798 Self {
4799 button: true,
4800 ..Self::text(label)
4801 }
4802 }
4803
4804 pub const fn with_feedback(mut self, feedback: FeedbackKind) -> Self {
4805 self.state = Some(feedback);
4806 self
4807 }
4808
4809 pub const fn active(mut self) -> Self {
4810 self.active = true;
4811 self
4812 }
4813
4814 pub const fn with_kbd(mut self, kbd: &'a str) -> Self {
4815 self.kbd = Some(kbd);
4816 self
4817 }
4818
4819 pub const fn with_html(mut self, html: TrustedHtml<'a>) -> Self {
4820 self.html = Some(html);
4821 self
4822 }
4823
4824 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
4825 self.attrs = attrs;
4826 self
4827 }
4828
4829 pub const fn with_button_type(mut self, button_type: &'a str) -> Self {
4830 self.button_type = button_type;
4831 self
4832 }
4833
4834 pub fn class_name(&self) -> String {
4835 let mut class = String::from(self.kind.class());
4836 if self.href.is_some() || self.button || !self.attrs.is_empty() {
4837 class.push_str(" is-interactive");
4838 }
4839 if self.active {
4840 class.push_str(" is-active");
4841 }
4842 if let Some(kind) = self.state {
4843 class.push_str(" is-");
4844 class.push_str(kind.class());
4845 }
4846 class
4847 }
4848}
4849
4850impl<'a> askama::filters::HtmlSafe for ModelineSegment<'a> {}
4851
4852#[derive(Debug, Template)]
4853#[non_exhaustive]
4854#[template(path = "components/modeline.html")]
4855pub struct Modeline<'a> {
4856 pub left_segments: &'a [ModelineSegment<'a>],
4857 pub right_segments: &'a [ModelineSegment<'a>],
4858 pub fill: bool,
4859 pub attrs: &'a [HtmlAttr<'a>],
4860}
4861
4862impl<'a> Modeline<'a> {
4863 pub const fn new(left_segments: &'a [ModelineSegment<'a>]) -> Self {
4864 Self {
4865 left_segments,
4866 right_segments: &[],
4867 fill: true,
4868 attrs: &[],
4869 }
4870 }
4871
4872 pub const fn with_right(mut self, right_segments: &'a [ModelineSegment<'a>]) -> Self {
4873 self.right_segments = right_segments;
4874 self
4875 }
4876
4877 pub const fn without_fill(mut self) -> Self {
4878 self.fill = false;
4879 self
4880 }
4881
4882 pub const fn with_attrs(mut self, attrs: &'a [HtmlAttr<'a>]) -> Self {
4883 self.attrs = attrs;
4884 self
4885 }
4886}
4887
4888impl<'a> askama::filters::HtmlSafe for Modeline<'a> {}
4889
4890#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4891pub struct MinibufferHistoryRow<'a> {
4892 pub time: &'a str,
4893 pub message: &'a str,
4894 pub kind: Option<FeedbackKind>,
4895}
4896
4897impl<'a> MinibufferHistoryRow<'a> {
4898 pub const fn new(time: &'a str, message: &'a str) -> Self {
4899 Self {
4900 time,
4901 message,
4902 kind: None,
4903 }
4904 }
4905
4906 pub const fn with_feedback(mut self, feedback: FeedbackKind) -> Self {
4907 self.kind = Some(feedback);
4908 self
4909 }
4910
4911 pub fn class_name(&self) -> &'static str {
4912 match self.kind {
4913 Some(FeedbackKind::Info) => "row is-info",
4914 Some(FeedbackKind::Ok) => "row is-ok",
4915 Some(FeedbackKind::Warn) => "row is-warn",
4916 Some(FeedbackKind::Error) => "row is-err",
4917 None => "row",
4918 }
4919 }
4920}
4921
4922#[derive(Debug, Template)]
4923#[non_exhaustive]
4924#[template(path = "components/minibuffer.html")]
4925pub struct Minibuffer<'a> {
4926 pub prompt: &'a str,
4927 pub message: Option<&'a str>,
4928 pub kind: Option<FeedbackKind>,
4929 pub time: Option<&'a str>,
4930 pub history: &'a [MinibufferHistoryRow<'a>],
4931}
4932
4933impl<'a> Minibuffer<'a> {
4934 pub const fn new() -> Self {
4935 Self {
4936 prompt: ">",
4937 message: None,
4938 kind: None,
4939 time: None,
4940 history: &[],
4941 }
4942 }
4943
4944 pub const fn with_prompt(mut self, prompt: &'a str) -> Self {
4945 self.prompt = prompt;
4946 self
4947 }
4948
4949 pub const fn with_message(mut self, kind: FeedbackKind, message: &'a str) -> Self {
4950 self.kind = Some(kind);
4951 self.message = Some(message);
4952 self
4953 }
4954
4955 pub const fn with_time(mut self, time: &'a str) -> Self {
4956 self.time = Some(time);
4957 self
4958 }
4959
4960 pub const fn with_history(mut self, history: &'a [MinibufferHistoryRow<'a>]) -> Self {
4961 self.history = history;
4962 self
4963 }
4964
4965 pub const fn has_history(&self) -> bool {
4966 !self.history.is_empty()
4967 }
4968
4969 pub fn message_class(&self) -> String {
4970 match self.kind {
4971 Some(kind) if self.message.is_some() => {
4972 format!("wf-minibuffer-msg is-visible is-{}", kind.class())
4973 }
4974 _ => "wf-minibuffer-msg".to_owned(),
4975 }
4976 }
4977}
4978
4979impl<'a> Default for Minibuffer<'a> {
4980 fn default() -> Self {
4981 Self::new()
4982 }
4983}
4984
4985impl<'a> askama::filters::HtmlSafe for Minibuffer<'a> {}
4986
4987#[derive(Debug, Template)]
4988#[non_exhaustive]
4989#[template(path = "components/minibuffer_echo.html")]
4990pub struct MinibufferEcho<'a> {
4991 pub kind: FeedbackKind,
4992 pub message: &'a str,
4993}
4994
4995impl<'a> MinibufferEcho<'a> {
4996 pub const fn new(kind: FeedbackKind, message: &'a str) -> Self {
4997 Self { kind, message }
4998 }
4999
5000 pub const fn info(message: &'a str) -> Self {
5001 Self::new(FeedbackKind::Info, message)
5002 }
5003
5004 pub const fn ok(message: &'a str) -> Self {
5005 Self::new(FeedbackKind::Ok, message)
5006 }
5007
5008 pub const fn warn(message: &'a str) -> Self {
5009 Self::new(FeedbackKind::Warn, message)
5010 }
5011
5012 pub const fn error(message: &'a str) -> Self {
5013 Self::new(FeedbackKind::Error, message)
5014 }
5015
5016 pub fn kind_class(&self) -> &'static str {
5017 self.kind.class()
5018 }
5019}
5020
5021impl<'a> askama::filters::HtmlSafe for MinibufferEcho<'a> {}
5022
5023#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5024pub struct FeatureItem<'a> {
5025 pub title: &'a str,
5026 pub body: &'a str,
5027}
5028
5029impl<'a> FeatureItem<'a> {
5030 pub const fn new(title: &'a str, body: &'a str) -> Self {
5031 Self { title, body }
5032 }
5033}
5034
5035#[derive(Debug, Template)]
5036#[non_exhaustive]
5037#[template(path = "components/feature_grid.html")]
5038pub struct FeatureGrid<'a> {
5039 pub items: &'a [FeatureItem<'a>],
5040}
5041
5042impl<'a> FeatureGrid<'a> {
5043 pub const fn new(items: &'a [FeatureItem<'a>]) -> Self {
5044 Self { items }
5045 }
5046}
5047
5048impl<'a> askama::filters::HtmlSafe for FeatureGrid<'a> {}
5049
5050#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5051pub struct MarketingStep<'a> {
5052 pub title: &'a str,
5053 pub body: &'a str,
5054}
5055
5056impl<'a> MarketingStep<'a> {
5057 pub const fn new(title: &'a str, body: &'a str) -> Self {
5058 Self { title, body }
5059 }
5060}
5061
5062#[derive(Debug, Template)]
5063#[non_exhaustive]
5064#[template(path = "components/marketing_step_grid.html")]
5065pub struct MarketingStepGrid<'a> {
5066 pub steps: &'a [MarketingStep<'a>],
5067}
5068
5069impl<'a> MarketingStepGrid<'a> {
5070 pub const fn new(steps: &'a [MarketingStep<'a>]) -> Self {
5071 Self { steps }
5072 }
5073}
5074
5075impl<'a> askama::filters::HtmlSafe for MarketingStepGrid<'a> {}
5076
5077#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5078pub struct PricingPlan<'a> {
5079 pub name: &'a str,
5080 pub price: &'a str,
5081 pub unit: Option<&'a str>,
5082 pub blurb: Option<&'a str>,
5083 pub featured: bool,
5084}
5085
5086impl<'a> PricingPlan<'a> {
5087 pub const fn new(name: &'a str, price: &'a str) -> Self {
5088 Self {
5089 name,
5090 price,
5091 unit: None,
5092 blurb: None,
5093 featured: false,
5094 }
5095 }
5096
5097 pub const fn with_unit(mut self, unit: &'a str) -> Self {
5098 self.unit = Some(unit);
5099 self
5100 }
5101
5102 pub const fn with_blurb(mut self, blurb: &'a str) -> Self {
5103 self.blurb = Some(blurb);
5104 self
5105 }
5106
5107 pub const fn featured(mut self) -> Self {
5108 self.featured = true;
5109 self
5110 }
5111
5112 pub fn class_name(&self) -> &'static str {
5113 if self.featured {
5114 "wf-plan is-featured"
5115 } else {
5116 "wf-plan"
5117 }
5118 }
5119}
5120
5121#[derive(Debug, Template)]
5122#[non_exhaustive]
5123#[template(path = "components/pricing_plans.html")]
5124pub struct PricingPlans<'a> {
5125 pub plans: &'a [PricingPlan<'a>],
5126}
5127
5128impl<'a> PricingPlans<'a> {
5129 pub const fn new(plans: &'a [PricingPlan<'a>]) -> Self {
5130 Self { plans }
5131 }
5132}
5133
5134impl<'a> askama::filters::HtmlSafe for PricingPlans<'a> {}
5135
5136#[derive(Debug, Template)]
5137#[non_exhaustive]
5138#[template(path = "components/testimonial.html")]
5139pub struct Testimonial<'a> {
5140 pub quote_html: TrustedHtml<'a>,
5141 pub name: &'a str,
5142 pub role: &'a str,
5143}
5144
5145impl<'a> Testimonial<'a> {
5146 pub const fn new(quote_html: TrustedHtml<'a>, name: &'a str, role: &'a str) -> Self {
5147 Self {
5148 quote_html,
5149 name,
5150 role,
5151 }
5152 }
5153}
5154
5155impl<'a> askama::filters::HtmlSafe for Testimonial<'a> {}
5156
5157#[derive(Debug, Template)]
5158#[non_exhaustive]
5159#[template(path = "components/marketing_section.html")]
5160pub struct MarketingSection<'a> {
5161 pub title: &'a str,
5162 pub content_html: TrustedHtml<'a>,
5163 pub kicker: Option<&'a str>,
5164 pub subtitle: Option<&'a str>,
5165}
5166
5167impl<'a> MarketingSection<'a> {
5168 pub const fn new(title: &'a str, content_html: TrustedHtml<'a>) -> Self {
5169 Self {
5170 title,
5171 content_html,
5172 kicker: None,
5173 subtitle: None,
5174 }
5175 }
5176
5177 pub const fn with_kicker(mut self, kicker: &'a str) -> Self {
5178 self.kicker = Some(kicker);
5179 self
5180 }
5181
5182 pub const fn with_subtitle(mut self, subtitle: &'a str) -> Self {
5183 self.subtitle = Some(subtitle);
5184 self
5185 }
5186}
5187
5188impl<'a> askama::filters::HtmlSafe for MarketingSection<'a> {}
5189
5190#[cfg(test)]
5191mod tests {
5192 use super::*;
5193
5194 #[test]
5195 fn renders_button_with_htmx_attrs() {
5196 let attrs = [HtmlAttr::hx_post("/save?next=<home>")];
5197 let html = Button::primary("Save").with_attrs(&attrs).render().unwrap();
5198
5199 assert!(html.contains(r#"class="wf-btn primary""#));
5200 assert!(html.contains(r#"hx-post="/save?next="#));
5201 assert!(!html.contains(r#"hx-post="/save?next=<home>""#));
5202 }
5203
5204 #[test]
5205 fn field_escapes_copy_and_renders_trusted_control_html() {
5206 let html = Field::new(
5207 "Email <required>",
5208 TrustedHtml::new(r#"<input class="wf-input" name="email">"#),
5209 )
5210 .with_hint("Use <work> address")
5211 .render()
5212 .unwrap();
5213
5214 assert!(html.contains("Email"));
5215 assert!(!html.contains("Email <required>"));
5216 assert!(html.contains(r#"<input class="wf-input" name="email">"#));
5217 assert!(html.contains("Use"));
5218 assert!(!html.contains("Use <work> address"));
5219 }
5220
5221 #[test]
5222 fn trusted_html_writes_without_formatter_allocation() {
5223 let mut html = String::new();
5224
5225 askama::FastWritable::write_into(
5226 &TrustedHtml::new("<strong>Ready</strong>"),
5227 &mut html,
5228 askama::NO_VALUES,
5229 )
5230 .unwrap();
5231
5232 assert_eq!(html, "<strong>Ready</strong>");
5233 }
5234
5235 #[derive(Template)]
5236 #[template(source = "{{ button }}", ext = "html")]
5237 struct NestedButton<'a> {
5238 button: Button<'a>,
5239 }
5240
5241 #[test]
5242 fn nested_components_render_as_html() {
5243 let html = NestedButton {
5244 button: Button::primary("Save"),
5245 }
5246 .render()
5247 .unwrap();
5248
5249 assert!(html.contains("<button"));
5250 assert!(!html.contains("<button"));
5251 }
5252
5253 #[test]
5254 fn action_primitives_render_wave_funk_markup() {
5255 let attrs = [HtmlAttr::hx_post("/actions/archive")];
5256 let buttons = [
5257 Button::new("Left"),
5258 Button::primary("Archive").with_attrs(&attrs),
5259 ];
5260
5261 let group_html = ButtonGroup::new(&buttons).render().unwrap();
5262 let split_html = SplitButton::new(Button::primary("Run"), Button::new("More"))
5263 .render()
5264 .unwrap();
5265 let icon_html = IconButton::new(TrustedHtml::new("×"), "Close")
5266 .with_variant(ButtonVariant::Ghost)
5267 .render()
5268 .unwrap();
5269
5270 assert!(group_html.contains(r#"class="wf-btn-group""#));
5271 assert!(group_html.contains(r#"hx-post="/actions/archive""#));
5272 assert!(split_html.contains(r#"class="wf-btn-split""#));
5273 assert!(split_html.contains(r#"class="wf-btn caret""#));
5274 assert!(icon_html.contains(r#"class="wf-icon-btn ghost""#));
5275 assert!(icon_html.contains(r#"aria-label="Close""#));
5276 assert!(icon_html.contains("×"));
5277 }
5278
5279 #[test]
5280 fn text_form_primitives_escape_copy_and_attrs() {
5281 let attrs = [HtmlAttr::hx_get("/validate/email")];
5282 let input_html = Input::email("email")
5283 .with_value("sandeep<wavefunk>")
5284 .with_placeholder("Email <address>")
5285 .with_attrs(&attrs)
5286 .render()
5287 .unwrap();
5288 let textarea_html = Textarea::new("notes")
5289 .with_value("Hello <team>")
5290 .with_placeholder("Notes <optional>")
5291 .render()
5292 .unwrap();
5293 let options = [
5294 SelectOption::new("starter", "Starter"),
5295 SelectOption::new("pro", "Pro <team>").selected(),
5296 ];
5297 let select_html = Select::new("plan", &options).render().unwrap();
5298
5299 assert!(input_html.contains(r#"class="wf-input""#));
5300 assert!(input_html.contains(r#"type="email""#));
5301 assert!(input_html.contains(r#"hx-get="/validate/email""#));
5302 assert!(!input_html.contains("sandeep<wavefunk>"));
5303 assert!(!input_html.contains("Email <address>"));
5304 assert!(textarea_html.contains(r#"class="wf-textarea""#));
5305 assert!(!textarea_html.contains("Hello <team>"));
5306 assert!(select_html.contains(r#"class="wf-select""#));
5307 assert!(select_html.contains(r#"value="pro" selected"#));
5308 assert!(!select_html.contains("Pro <team>"));
5309 }
5310
5311 #[test]
5312 fn grouped_choice_and_range_primitives_render_expected_classes() {
5313 let input_html = Input::url("site_url").render().unwrap();
5314 let group_html = InputGroup::new(TrustedHtml::new(&input_html))
5315 .with_prefix("https://")
5316 .with_suffix(".wavefunk.test")
5317 .render()
5318 .unwrap();
5319 let checkbox_html = CheckRow::checkbox("terms", "yes", "Accept <terms>")
5320 .checked()
5321 .render()
5322 .unwrap();
5323 let radio_html = CheckRow::radio("plan", "pro", "Pro").render().unwrap();
5324 let switch_html = Switch::new("enabled").checked().render().unwrap();
5325 let range_html = Range::new("volume")
5326 .with_bounds("0", "100")
5327 .with_value("50")
5328 .render()
5329 .unwrap();
5330 let field_html = Field::new("URL", TrustedHtml::new(&group_html))
5331 .with_state(FieldState::Success)
5332 .render()
5333 .unwrap();
5334
5335 assert!(group_html.contains(r#"class="wf-input-group""#));
5336 assert!(group_html.contains(r#"class="wf-input-addon">https://"#));
5337 assert!(checkbox_html.contains(r#"class="wf-check-row""#));
5338 assert!(checkbox_html.contains(r#"type="checkbox""#));
5339 assert!(checkbox_html.contains("checked"));
5340 assert!(!checkbox_html.contains("Accept <terms>"));
5341 assert!(radio_html.contains(r#"type="radio""#));
5342 assert!(switch_html.contains(r#"class="wf-switch""#));
5343 assert!(switch_html.contains("checked"));
5344 assert!(range_html.contains(r#"class="wf-range""#));
5345 assert!(range_html.contains(r#"min="0""#));
5346 assert!(field_html.contains(r#"class="wf-field is-success""#));
5347 }
5348
5349 #[test]
5350 fn layout_navigation_and_data_primitives_render_expected_markup() {
5351 let panel = Panel::new("Deployments", TrustedHtml::new("<p>Ready</p>"))
5352 .with_action(TrustedHtml::new(
5353 r#"<a class="wf-panel-link" href="/all">All</a>"#,
5354 ))
5355 .render()
5356 .unwrap();
5357 let card = Card::new("Project <alpha>", TrustedHtml::new("<p>Live</p>"))
5358 .with_kicker("Status")
5359 .raised()
5360 .render()
5361 .unwrap();
5362 let stats = [Stat::new("Requests", "42").with_unit("rpm")];
5363 let stat_row = StatRow::new(&stats).render().unwrap();
5364 let badge = Badge::muted("beta").render().unwrap();
5365 let avatar = Avatar::new("SN").accent().render().unwrap();
5366 let crumbs = [
5367 BreadcrumbItem::link("Projects", "/projects"),
5368 BreadcrumbItem::current("Wavefunk <UI>"),
5369 ];
5370 let breadcrumbs = Breadcrumbs::new(&crumbs).render().unwrap();
5371 let tabs = [
5372 TabItem::link("Overview", "/").active(),
5373 TabItem::link("Settings", "/settings"),
5374 ];
5375 let tab_html = Tabs::new(&tabs).render().unwrap();
5376 let segments = [
5377 SegmentOption::new("List", "list").active(),
5378 SegmentOption::new("Grid", "grid"),
5379 ];
5380 let seg_html = SegmentedControl::new(&segments).render().unwrap();
5381 let pages = [
5382 PageLink::link("1", "/page/1").active(),
5383 PageLink::ellipsis(),
5384 PageLink::disabled("Next"),
5385 ];
5386 let pagination = Pagination::new(&pages).render().unwrap();
5387 let nav_section = NavSection::new("Workspace").render().unwrap();
5388 let nav_item = NavItem::new("Dashboard", "/").active().with_count("3");
5389 let topbar = Topbar::new(TrustedHtml::new(&breadcrumbs), TrustedHtml::new(&badge))
5390 .render()
5391 .unwrap();
5392 let statusbar = Statusbar::new("Connected", "v0.1").render().unwrap();
5393 let empty = EmptyState::new("No hooks", "Create a hook to start.")
5394 .with_glyph(TrustedHtml::new("∅"))
5395 .bordered()
5396 .render()
5397 .unwrap();
5398 let table_headers = [TableHeader::new("Name"), TableHeader::numeric("Runs")];
5399 let table_cells = [TableCell::strong("Build <main>"), TableCell::numeric("12")];
5400 let table_rows = [TableRow::new(&table_cells).selected()];
5401 let table = Table::new(&table_headers, &table_rows)
5402 .interactive()
5403 .render()
5404 .unwrap();
5405 let dl_items = [DefinitionItem::new("Runtime", "Rust <stable>")];
5406 let dl = DefinitionList::new(&dl_items).render().unwrap();
5407 let grid = Grid::new(TrustedHtml::new(&card))
5408 .with_columns(2)
5409 .render()
5410 .unwrap();
5411 let split = Split::new(TrustedHtml::new(&panel))
5412 .vertical()
5413 .render()
5414 .unwrap();
5415
5416 assert!(panel.contains(r#"class="wf-panel""#));
5417 assert!(card.contains(r#"class="wf-card is-raised""#));
5418 assert!(!card.contains("Project <alpha>"));
5419 assert!(stat_row.contains(r#"class="wf-stat-row""#));
5420 assert!(badge.contains(r#"class="wf-badge muted""#));
5421 assert!(avatar.contains(r#"class="wf-avatar accent""#));
5422 assert!(breadcrumbs.contains(r#"class="wf-crumbs""#));
5423 assert!(!breadcrumbs.contains("Wavefunk <UI>"));
5424 assert!(tab_html.contains(r#"class="wf-tabs""#));
5425 assert!(seg_html.contains(r#"class="wf-seg""#));
5426 assert!(pagination.contains(r#"class="wf-pagination""#));
5427 assert!(nav_section.contains(r#"class="wf-nav-section""#));
5428 assert!(
5429 nav_item
5430 .render()
5431 .unwrap()
5432 .contains(r#"class="wf-nav-item is-active""#)
5433 );
5434 assert!(topbar.contains(r#"class="wf-topbar""#));
5435 assert!(statusbar.contains(r#"class="wf-statusbar wf-hair""#));
5436 assert!(empty.contains(r#"class="wf-empty bordered""#));
5437 assert!(table.contains(r#"class="wf-table is-interactive""#));
5438 assert!(!table.contains("Build <main>"));
5439 assert!(dl.contains(r#"class="wf-dl""#));
5440 assert!(!dl.contains("Rust <stable>"));
5441 assert!(grid.contains(r#"class="wf-grid cols-2""#));
5442 assert!(split.contains(r#"class="wf-split vertical""#));
5443 }
5444
5445 #[test]
5446 fn page_header_supports_title_meta_back_and_action_slots() {
5447 let primary = Button::primary("Create").render().unwrap();
5448 let secondary = Button::new("Export").render().unwrap();
5449 let header = PageHeader::new("Deployments <prod>")
5450 .with_subtitle("Filtered by team <ops>")
5451 .with_back("/settings", "Settings")
5452 .with_meta(TrustedHtml::new(
5453 r#"<span class="wf-badge muted">12</span>"#,
5454 ))
5455 .with_primary(TrustedHtml::new(&primary))
5456 .with_secondary(TrustedHtml::new(&secondary))
5457 .render()
5458 .unwrap();
5459
5460 assert!(header.contains(r#"class="wf-pageheader""#));
5461 assert!(header.contains(r#"class="wf-pageheader-main""#));
5462 assert!(header.contains(r#"<a class="wf-backlink" href="/settings">"#));
5463 assert!(header.contains(">Settings<"));
5464 assert!(header.contains(r#"class="wf-pagetitle""#));
5465 assert!(!header.contains("Deployments <prod>"));
5466 assert!(header.contains(r#"class="wf-pageheader-subtitle""#));
5467 assert!(!header.contains("Filtered by team <ops>"));
5468 assert!(header.contains(r#"<span class="wf-badge muted">12</span>"#));
5469 assert!(header.contains(r#"class="wf-pageheader-actions""#));
5470 assert!(header.contains(">Create<"));
5471 assert!(header.contains(">Export<"));
5472 }
5473
5474 #[test]
5475 fn feedback_overlay_and_loading_primitives_render_expected_markup() {
5476 let callout = Callout::new(FeedbackKind::Warn, TrustedHtml::new("<p>Heads up</p>"))
5477 .with_title("Warning")
5478 .render()
5479 .unwrap();
5480 let toast = Toast::new(FeedbackKind::Ok, "Saved <now>")
5481 .render()
5482 .unwrap();
5483 let toast_host = ToastHost::new().render().unwrap();
5484 let tooltip = Tooltip::new("Copy id", TrustedHtml::new(r#"<button>copy</button>"#))
5485 .render()
5486 .unwrap();
5487 let menu_items = [
5488 MenuItem::button("Open"),
5489 MenuItem::link("Settings", "/settings"),
5490 MenuItem::separator(),
5491 MenuItem::button("Delete").danger(),
5492 ];
5493 let menu = Menu::new(&menu_items).render().unwrap();
5494 let popover = Popover::new(
5495 TrustedHtml::new(r#"<button data-popover-toggle>Open</button>"#),
5496 TrustedHtml::new(&menu),
5497 )
5498 .with_heading("Menu")
5499 .open()
5500 .render()
5501 .unwrap();
5502 let modal = Modal::new("Confirm", TrustedHtml::new("<p>Continue?</p>"))
5503 .with_footer(TrustedHtml::new(
5504 r#"<button class="wf-btn primary">Confirm</button>"#,
5505 ))
5506 .open()
5507 .render()
5508 .unwrap();
5509 let drawer = Drawer::new("Details", TrustedHtml::new("<p>Side sheet</p>"))
5510 .left()
5511 .open()
5512 .render()
5513 .unwrap();
5514 let skeleton = Skeleton::title().render().unwrap();
5515 let spinner = Spinner::large().render().unwrap();
5516 let minibuffer = Minibuffer::new()
5517 .with_message(FeedbackKind::Info, "Queued <job>")
5518 .with_time("09:41")
5519 .render()
5520 .unwrap();
5521 let minibuffer_echo = MinibufferEcho::warn("Queued <job>").render().unwrap();
5522
5523 assert!(callout.contains(r#"class="wf-callout warn""#));
5524 assert!(toast.contains(r#"class="wf-toast ok""#));
5525 assert!(!toast.contains("Saved <now>"));
5526 assert!(toast_host.contains(r#"class="wf-toast-host""#));
5527 assert!(tooltip.contains(r#"class="wf-tooltip""#));
5528 assert!(tooltip.contains(r#"data-tip="Copy id""#));
5529 assert!(menu.contains(r#"class="wf-menu""#));
5530 assert!(menu.contains(r#"class="wf-menu-sep""#));
5531 assert!(popover.contains(r#"class="wf-popover is-open""#));
5532 assert!(modal.contains(r#"class="wf-modal is-open""#));
5533 assert!(modal.contains(r#"class="wf-overlay is-open""#));
5534 assert!(modal.contains(r#"data-wf-dismiss="overlay""#));
5535 assert!(drawer.contains(r#"class="wf-drawer is-open left""#));
5536 assert!(drawer.contains(r#"data-wf-dismiss="overlay""#));
5537 assert!(skeleton.contains(r#"class="wf-skeleton title""#));
5538 assert!(spinner.contains(r#"class="wf-spinner lg""#));
5539 assert!(minibuffer.contains(r#"class="wf-minibuffer""#));
5540 assert!(minibuffer.contains("data-wf-echo"));
5541 assert!(!minibuffer.contains("Queued <job>"));
5542 assert!(minibuffer_echo.contains(r#"hidden"#));
5543 assert!(minibuffer_echo.contains(r#"data-wf-echo-kind="warn""#));
5544 assert!(minibuffer_echo.contains(r#"data-wf-echo-message="Queued "#));
5545 assert!(!minibuffer_echo.contains("Queued <job>"));
5546 }
5547
5548 #[test]
5549 fn modal_size_and_spacing_utilities_cover_large_overlay_layouts() {
5550 let modal = Modal::new("Edit record", TrustedHtml::new("<p>Large form</p>"))
5551 .large()
5552 .open()
5553 .render()
5554 .unwrap();
5555 let components_css = include_str!("../static/wavefunk/css/04-components.css");
5556 let utilities_css = include_str!("../static/wavefunk/css/05-utilities.css");
5557
5558 assert!(modal.contains(r#"class="wf-modal is-open wf-modal--lg""#));
5559 assert!(components_css.contains(".wf-modal--lg"));
5560 assert!(utilities_css.contains(".wf-mb-1 { margin-bottom: 4px; }"));
5561 assert!(utilities_css.contains(".wf-mb-8 { margin-bottom: 32px; }"));
5562 assert!(utilities_css.contains(".wf-ml-2 { margin-left: 8px; }"));
5563 }
5564
5565 #[test]
5566 fn form_composition_and_dropzone_components_render_expected_markup() {
5567 let input_html = Input::email("email")
5568 .with_placeholder("you@example.test")
5569 .render()
5570 .unwrap();
5571 let field_html = Field::new("Email", TrustedHtml::new(&input_html))
5572 .with_hint("Use <work> address")
5573 .render()
5574 .unwrap();
5575 let actions_html = FormActions::new(TrustedHtml::new(
5576 r#"<button class="wf-btn primary">Save</button>"#,
5577 ))
5578 .with_secondary(TrustedHtml::new(
5579 r#"<button class="wf-btn">Cancel</button>"#,
5580 ))
5581 .render()
5582 .unwrap();
5583 let section_html = FormSection::new("Profile <setup>", TrustedHtml::new(&field_html))
5584 .with_description("Shown to teammates <public>")
5585 .render()
5586 .unwrap();
5587 let attrs = [HtmlAttr::hx_post("/profile")];
5588 let form_html = Form::new(TrustedHtml::new(§ion_html))
5589 .with_action("/profile/save?next=<home>")
5590 .with_method("post")
5591 .with_attrs(&attrs)
5592 .render()
5593 .unwrap();
5594 let dropzone_attrs = [HtmlAttr::new("data-intent", "avatar <upload>")];
5595 let dropzone_html = Dropzone::new("avatar")
5596 .with_title("Drop avatar <image>")
5597 .with_hint("PNG or JPG <2MB>")
5598 .with_accept("image/png,image/jpeg")
5599 .with_attrs(&dropzone_attrs)
5600 .multiple()
5601 .disabled()
5602 .dragover()
5603 .render()
5604 .unwrap();
5605
5606 assert!(actions_html.contains(r#"class="wf-form-actions""#));
5607 assert!(section_html.contains(r#"class="wf-form-section""#));
5608 assert!(!section_html.contains("Profile <setup>"));
5609 assert!(!section_html.contains("Shown to teammates <public>"));
5610 assert!(form_html.contains(r#"<form class="wf-form""#));
5611 assert!(form_html.contains(r#"method="post""#));
5612 assert!(form_html.contains(r#"hx-post="/profile""#));
5613 assert!(!form_html.contains(r#"action="/profile/save?next=<home>""#));
5614 assert!(dropzone_html.contains(r#"class="wf-dropzone is-dragover is-disabled""#));
5615 assert!(dropzone_html.contains("data-upload-zone"));
5616 assert!(dropzone_html.contains(r#"type="file""#));
5617 assert!(dropzone_html.contains(r#"multiple"#));
5618 assert!(dropzone_html.contains(r#"disabled"#));
5619 assert!(dropzone_html.contains(r#"accept="image/png,image/jpeg""#));
5620 assert!(dropzone_html.contains(r#"data-intent="avatar "#));
5621 assert!(!dropzone_html.contains(r#"data-intent="avatar <upload>""#));
5622 assert!(!dropzone_html.contains("Drop avatar <image>"));
5623 assert!(!dropzone_html.contains("PNG or JPG <2MB>"));
5624 }
5625
5626 #[test]
5627 fn generated_form_building_blocks_render_generic_schema_shapes() {
5628 let title = Input::new("title").render().unwrap();
5629 let title_field = Field::new("Title", TrustedHtml::new(&title))
5630 .render()
5631 .unwrap();
5632 let object = ObjectFieldset::new("Metadata", TrustedHtml::new(&title_field))
5633 .with_description("Nested object fields")
5634 .render()
5635 .unwrap();
5636 let item = RepeatableItem::new("Link 1", TrustedHtml::new(&title_field))
5637 .with_actions(TrustedHtml::new(
5638 r#"<button class="wf-btn sm">Remove</button>"#,
5639 ))
5640 .render()
5641 .unwrap();
5642 let array = RepeatableArray::new("Links", TrustedHtml::new(&item))
5643 .with_description("Zero or more external links.")
5644 .with_action(TrustedHtml::new(
5645 r#"<button class="wf-btn sm">Add link</button>"#,
5646 ))
5647 .render()
5648 .unwrap();
5649 let upload = CurrentUpload::new("Hero image", "/media/hero.jpg", "hero.jpg")
5650 .with_meta("1200x630 JPG")
5651 .with_thumbnail(TrustedHtml::new(r#"<img src="/media/hero.jpg" alt="">"#))
5652 .render()
5653 .unwrap();
5654 let options = [
5655 SelectOption::new("home", "Home"),
5656 SelectOption::new("about", "About").selected(),
5657 ];
5658 let select = Select::new("related_page", &options).render().unwrap();
5659 let reference = ReferenceSelect::new("Related page", TrustedHtml::new(&select))
5660 .with_hint("Search and choose another record.")
5661 .render()
5662 .unwrap();
5663 let markdown = MarkdownTextarea::new("body")
5664 .with_value("# Hello")
5665 .with_rows(8)
5666 .render()
5667 .unwrap();
5668 let richtext = RichTextHost::new("body-editor", "body_html")
5669 .with_value("<p>Hello</p>")
5670 .with_toolbar(TrustedHtml::new(
5671 r#"<button class="wf-btn sm">Bold</button>"#,
5672 ))
5673 .render()
5674 .unwrap();
5675
5676 assert!(object.contains(r#"<fieldset class="wf-object-fieldset">"#));
5677 assert!(object.contains(r#"<legend class="wf-object-legend">Metadata</legend>"#));
5678 assert!(array.contains(r#"class="wf-repeatable-array""#));
5679 assert!(array.contains(r#"class="wf-repeatable-item""#));
5680 assert!(upload.contains(r#"class="wf-current-upload""#));
5681 assert!(upload.contains(r#"<a href="/media/hero.jpg">hero.jpg</a>"#));
5682 assert!(reference.contains(r#"class="wf-reference-select""#));
5683 assert!(markdown.contains(r#"class="wf-textarea wf-markdown-textarea""#));
5684 assert!(markdown.contains("data-wf-markdown"));
5685 assert!(richtext.contains(r#"class="wf-richtext""#));
5686 assert!(richtext.contains("data-wf-richtext"));
5687 assert!(richtext.contains(r#"data-wf-richtext-modal-host"#));
5688 }
5689
5690 #[test]
5691 fn table_workflow_components_support_sorting_actions_and_chrome() {
5692 let _source_compatible_header = TableHeader {
5693 label: "Legacy",
5694 numeric: false,
5695 };
5696 let _source_compatible_cell = TableCell {
5697 text: "Legacy",
5698 numeric: false,
5699 strong: false,
5700 muted: false,
5701 };
5702 let headers = [
5703 DataTableHeader::new("Name").sortable("name", SortDirection::Ascending),
5704 DataTableHeader::numeric("Runs").with_width(TableColumnWidth::Small),
5705 DataTableHeader::new("Actions").action_column(),
5706 ];
5707 let actions = IconButton::new(TrustedHtml::new("×"), "Stop")
5708 .with_variant(ButtonVariant::Danger)
5709 .render()
5710 .unwrap();
5711 let row_cells = [
5712 DataTableCell::strong("Build <main>"),
5713 DataTableCell::numeric("12"),
5714 DataTableCell::html(TrustedHtml::new(&actions)),
5715 ];
5716 let rows = [DataTableRow::new(&row_cells).selected()];
5717 let filter_html = Input::new("q")
5718 .with_size(ControlSize::Small)
5719 .with_placeholder("Search")
5720 .render()
5721 .unwrap();
5722 let bulk_html = Button::new("Delete").render().unwrap();
5723 let table_html = DataTable::new(&headers, &rows)
5724 .interactive()
5725 .sticky()
5726 .pin_last()
5727 .render()
5728 .unwrap();
5729 let wrap_html = TableWrap::new(TrustedHtml::new(&table_html))
5730 .with_filterbar(TrustedHtml::new(&filter_html))
5731 .with_bulkbar("1 selected", TrustedHtml::new(&bulk_html))
5732 .with_footer(TrustedHtml::new("Showing 1-1 of 1"))
5733 .render()
5734 .unwrap();
5735
5736 assert!(table_html.contains(r#"class="wf-sort-h is-active""#));
5737 assert!(table_html.contains(r#"data-sort-key="name""#));
5738 assert!(table_html.contains(r#"class="wf-sort-arrow">^"#));
5739 assert!(table_html.contains(r#"class="wf-col-sm num""#));
5740 assert!(table_html.contains(r#"class="wf-col-act""#));
5741 assert!(table_html.contains("×"));
5742 assert!(!table_html.contains("Build <main>"));
5743 assert!(wrap_html.contains(r#"class="wf-tablewrap""#));
5744 assert!(wrap_html.contains(r#"class="wf-filterbar""#));
5745 assert!(wrap_html.contains(r#"class="wf-bulkbar""#));
5746 assert!(wrap_html.contains(r#"class="wf-tablefoot""#));
5747 }
5748
5749 #[test]
5750 fn resource_table_chrome_components_compose_filter_bulk_footer_and_selection() {
5751 let filter_input = Input::search("q")
5752 .with_placeholder("Search resources")
5753 .with_size(ControlSize::Small)
5754 .render()
5755 .unwrap();
5756 let filter_action = Button::new("Refresh").render().unwrap();
5757 let filterbar = FilterBar::new(TrustedHtml::new(&filter_input))
5758 .with_actions(TrustedHtml::new(&filter_action))
5759 .render()
5760 .unwrap();
5761 let bulk_action = Button::new("Delete")
5762 .with_variant(ButtonVariant::Danger)
5763 .render()
5764 .unwrap();
5765 let bulkbar = BulkActionBar::new("2 selected", TrustedHtml::new(&bulk_action))
5766 .render()
5767 .unwrap();
5768 let footer_action = Pagination::new(&[
5769 PageLink::link("1", "/page/1").active(),
5770 PageLink::link("2", "/page/2"),
5771 ])
5772 .render()
5773 .unwrap();
5774 let footer = TableFooter::new(TrustedHtml::new("Showing 1-2 of 8"))
5775 .with_actions(TrustedHtml::new(&footer_action))
5776 .render()
5777 .unwrap();
5778 let selector = RowSelect::new("selected", "build", "Select Build")
5779 .checked()
5780 .render()
5781 .unwrap();
5782 let headers = [
5783 DataTableHeader::new("").with_width(TableColumnWidth::Checkbox),
5784 DataTableHeader::sorted("Name", "name", SortDirection::Ascending),
5785 ];
5786 let cells = [
5787 DataTableCell::html(TrustedHtml::new(&selector)),
5788 DataTableCell::strong("Build"),
5789 ];
5790 let rows = [DataTableRow::new(&cells).selected()];
5791 let table = DataTable::new(&headers, &rows)
5792 .interactive()
5793 .render()
5794 .unwrap();
5795 let wrap = TableWrap::new(TrustedHtml::new(&table))
5796 .with_filterbar_component(TrustedHtml::new(&filterbar))
5797 .with_bulkbar_component(TrustedHtml::new(&bulkbar))
5798 .with_footer_component(TrustedHtml::new(&footer))
5799 .render()
5800 .unwrap();
5801
5802 assert!(filterbar.contains(r#"class="wf-filterbar""#));
5803 assert!(filterbar.contains(r#"class="wf-filterbar-actions""#));
5804 assert!(bulkbar.contains(r#"class="wf-bulkbar""#));
5805 assert!(bulkbar.contains(r#"class="wf-sel-count">2 selected"#));
5806 assert!(footer.contains(r#"class="wf-tablefoot""#));
5807 assert!(footer.contains(r#"class="wf-tablefoot-actions""#));
5808 assert!(selector.contains(r#"class="wf-check wf-rowselect""#));
5809 assert!(selector.contains(r#"aria-label="Select Build""#));
5810 assert!(selector.contains("checked"));
5811 assert!(table.contains(r#"data-sort-key="name""#));
5812 assert!(wrap.matches(r#"class="wf-filterbar""#).count() == 1);
5813 assert!(wrap.matches(r#"class="wf-bulkbar""#).count() == 1);
5814 assert!(wrap.matches(r#"class="wf-tablefoot""#).count() == 1);
5815 }
5816
5817 #[test]
5818 fn progress_stepper_and_disclosure_components_render_expected_markup() {
5819 let progress = Progress::new(60).render().unwrap();
5820 let indeterminate = Progress::indeterminate().render().unwrap();
5821 let meter = Meter::new(75)
5822 .with_size_px(96, 6)
5823 .with_color(MeterColor::Ok)
5824 .render()
5825 .unwrap();
5826 let kbd = Kbd::new("Ctrl <K>").render().unwrap();
5827 let steps = [
5828 StepItem::new("Account").done(),
5829 StepItem::new("Profile <public>")
5830 .active()
5831 .with_href("/profile"),
5832 StepItem::new("Invite"),
5833 ];
5834 let stepper = Stepper::new(&steps).render().unwrap();
5835 let accordion_items = [
5836 AccordionItem::new("What is <UI>?", TrustedHtml::new("<p>Typed</p>")).open(),
5837 AccordionItem::new("Can it htmx?", TrustedHtml::new("<p>Yes</p>")),
5838 ];
5839 let accordion = Accordion::new(&accordion_items).render().unwrap();
5840 let faq_items = [FaqItem::new(
5841 "Why typed?",
5842 TrustedHtml::new("<p>To preserve semver.</p>"),
5843 )];
5844 let faq = Faq::new(&faq_items).render().unwrap();
5845
5846 assert!(progress.contains(r#"class="wf-progress""#));
5847 assert!(progress.contains(r#"style="--progress: 60%""#));
5848 assert!(indeterminate.contains(r#"class="wf-progress indeterminate""#));
5849 assert!(meter.contains(
5850 r#"style="--meter: 75%; --meter-w: 96px; --meter-h: 6px; --meter-c: var(--ok)""#
5851 ));
5852 assert!(kbd.contains(r#"class="wf-kbd""#));
5853 assert!(!kbd.contains("Ctrl <K>"));
5854 assert!(stepper.contains(r#"class="wf-step is-done""#));
5855 assert!(stepper.contains(r#"aria-current="step""#));
5856 assert!(!stepper.contains("Profile <public>"));
5857 assert!(accordion.contains(r#"class="wf-accordion""#));
5858 assert!(accordion.contains(r#"<details class="wf-accordion-item" open>"#));
5859 assert!(!accordion.contains("What is <UI>?"));
5860 assert!(faq.contains(r#"class="wf-faq""#));
5861 assert!(faq.contains("<p>To preserve semver.</p>"));
5862 }
5863
5864 #[test]
5865 fn identity_brand_and_operational_components_render_expected_markup() {
5866 let avatars = [
5867 Avatar::new("SN").with_image("/avatar.png").accent(),
5868 Avatar::new("WF").with_size(AvatarSize::Small),
5869 ];
5870 let avatar_group = AvatarGroup::new(&avatars).render().unwrap();
5871 let full_user = UserButton::new("Wave Funk", "team@example.test", Avatar::new("WF"))
5872 .render()
5873 .unwrap();
5874 let user = UserButton::new(
5875 "Sandeep <Nambiar>",
5876 "sandeep@example.test",
5877 Avatar::new("SN"),
5878 )
5879 .compact()
5880 .render()
5881 .unwrap();
5882 let wordmark = Wordmark::new("Wave <Funk>")
5883 .with_mark(TrustedHtml::new(r#"<svg class="wf-mark"></svg>"#))
5884 .render()
5885 .unwrap();
5886 let ranks = [RankRow::new("Builds <main>", "42", 72)];
5887 let rank_list = RankList::new(&ranks).render().unwrap();
5888 let feed_rows = [FeedRow::new("09:41", "Deploy <prod>", "Released <v1>")];
5889 let feed = Feed::new(&feed_rows).render().unwrap();
5890 let timeline_items =
5891 [
5892 TimelineItem::new("09:42", "Queued <job>", TrustedHtml::new("<p>Pending</p>"))
5893 .active(),
5894 ];
5895 let timeline = Timeline::new(&timeline_items).render().unwrap();
5896 let tree_children = [TreeItem::file("components.rs").active()];
5897 let tree_child_html = TreeView::new(&tree_children).nested().render().unwrap();
5898 let tree_items = [TreeItem::folder("src <root>")
5899 .collapsed()
5900 .with_children(TrustedHtml::new(&tree_child_html))];
5901 let tree = TreeView::new(&tree_items).render().unwrap();
5902 let framed = Framed::new(TrustedHtml::new("<code>cargo test</code>"))
5903 .dense()
5904 .dashed()
5905 .render()
5906 .unwrap();
5907
5908 assert!(avatar_group.contains(r#"class="wf-avatar-group""#));
5909 assert!(avatar_group.contains(r#"<img src="/avatar.png" alt="SN">"#));
5910 assert!(full_user.contains(r#"class="wf-user""#));
5911 assert!(!full_user.contains(r#"class="wf-user compact""#));
5912 assert!(user.contains(r#"class="wf-user compact""#));
5913 assert!(!user.contains("Sandeep <Nambiar>"));
5914 assert!(wordmark.contains(r#"class="wf-wordmark""#));
5915 assert!(wordmark.contains(r#"<svg class="wf-mark"></svg>"#));
5916 assert!(!wordmark.contains("Wave <Funk>"));
5917 assert!(rank_list.contains(r#"class="wf-rank""#));
5918 assert!(rank_list.contains(r#"style="width: 72%""#));
5919 assert!(!rank_list.contains("Builds <main>"));
5920 assert!(feed.contains(r#"class="wf-feed""#));
5921 assert!(!feed.contains("Deploy <prod>"));
5922 assert!(!feed.contains("Released <v1>"));
5923 assert!(timeline.contains(r#"class="wf-timeline-item is-active""#));
5924 assert!(!timeline.contains("Queued <job>"));
5925 assert!(tree.contains(r#"class="wf-tree""#));
5926 assert!(tree.contains(r#"class="is-collapsed""#));
5927 assert!(!tree.contains("src <root>"));
5928 assert!(framed.contains(r#"class="wf-framed dense dashed""#));
5929 }
5930
5931 #[test]
5932 fn settings_and_admin_workflow_primitives_render_generic_markup() {
5933 let input = Input::email("email").render().unwrap();
5934 let save = Button::primary("Save")
5935 .with_button_type("submit")
5936 .render()
5937 .unwrap();
5938 let row = InlineFormRow::new("Notification email", TrustedHtml::new(&input))
5939 .with_hint("Used for account notices <private>")
5940 .with_action(TrustedHtml::new(&save))
5941 .render()
5942 .unwrap();
5943 let copy = CopyableValue::new("Webhook URL", "webhook-url", "https://example.test/hook")
5944 .with_button_label("Copy URL")
5945 .render()
5946 .unwrap();
5947 let statuses = [
5948 CredentialStatusItem::ok("Mail", "Configured"),
5949 CredentialStatusItem::warn("Backups", "Rotation due"),
5950 ];
5951 let status_list = CredentialStatusList::new(&statuses).render().unwrap();
5952 let confirm = ConfirmAction::new("Delete workspace", "/settings/delete")
5953 .with_message("This cannot be undone.")
5954 .with_confirm("Delete this workspace?")
5955 .render()
5956 .unwrap();
5957 let section_body = format!("{row}{copy}{status_list}{confirm}");
5958 let section = SettingsSection::new("Workspace settings", TrustedHtml::new(§ion_body))
5959 .with_description("Operational settings for this app.")
5960 .danger()
5961 .render()
5962 .unwrap();
5963
5964 assert!(row.contains(r#"class="wf-inline-form-row""#));
5965 assert!(!row.contains("account notices <private>"));
5966 assert!(copy.contains(r#"class="wf-copyable""#));
5967 assert!(copy.contains(r#"id="webhook-url""#));
5968 assert!(copy.contains(r##"data-wf-copy="#webhook-url""##));
5969 assert!(copy.contains(">Copy URL<"));
5970 assert!(status_list.contains(r#"class="wf-credential-list""#));
5971 assert!(status_list.contains(r#"class="wf-tag ok""#));
5972 assert!(status_list.contains(r#"class="wf-tag warn""#));
5973 assert!(confirm.contains(
5974 r#"<form class="wf-confirm-action" action="/settings/delete" method="post">"#
5975 ));
5976 assert!(confirm.contains(r#"hx-confirm="Delete this workspace?""#));
5977 assert!(confirm.contains(r#"class="wf-btn danger""#));
5978 assert!(section.contains(r#"class="wf-panel wf-settings-section is-danger""#));
5979 assert!(section.contains("Operational settings for this app."));
5980 }
5981
5982 #[test]
5983 fn split_shell_and_form_panel_render_generic_setup_surfaces() {
5984 let actions = Button::primary("Continue").render().unwrap();
5985 let panel = FormPanel::new(
5986 "Setup <workspace>",
5987 TrustedHtml::new(r#"<form class="wf-form">Fields</form>"#),
5988 )
5989 .with_subtitle("Use generic product copy <only>")
5990 .with_actions(TrustedHtml::new(&actions))
5991 .render()
5992 .unwrap();
5993 let attrs = [HtmlAttr::new("data-surface", "setup <flow>")];
5994 let shell = SplitShell::new(TrustedHtml::new(&panel))
5995 .with_top(TrustedHtml::new(
5996 r#"<a class="wf-btn ghost" href="/">Back</a>"#,
5997 ))
5998 .with_visual(TrustedHtml::new(r#"<pre aria-label="preview">wave</pre>"#))
5999 .with_footer(TrustedHtml::new(r#"<div class="wf-statusbar">Ready</div>"#))
6000 .with_mode("light")
6001 .mode_locked()
6002 .with_asset_base_path("/assets/wavefunk")
6003 .with_attrs(&attrs)
6004 .render()
6005 .unwrap();
6006
6007 assert!(panel.contains(r#"class="wf-form-panel""#));
6008 assert!(!panel.contains("Setup <workspace>"));
6009 assert!(!panel.contains("generic product copy <only>"));
6010 assert!(panel.contains(r#"<form class="wf-form">Fields</form>"#));
6011 assert!(shell.contains(r#"class="wf-split-shell""#));
6012 assert!(shell.contains(r#"data-mode="light""#));
6013 assert!(shell.contains(r#"data-mode-locked"#));
6014 assert!(shell.contains(r#"data-wf-asset-base="/assets/wavefunk""#));
6015 assert!(shell.contains(r#"data-surface="setup "#));
6016 assert!(!shell.contains(r#"data-surface="setup <flow>""#));
6017 assert!(shell.contains(r#"<pre aria-label="preview">wave</pre>"#));
6018 }
6019
6020 #[test]
6021 fn modeline_minibuffer_history_context_switcher_and_sidenav_are_generic() {
6022 let toggle_attrs = [HtmlAttr::new("data-mode-toggle", "")];
6023 let left = [
6024 ModelineSegment::chevron("WF"),
6025 ModelineSegment::buffer("workspace.rs"),
6026 ModelineSegment::button("Mode").with_attrs(&toggle_attrs),
6027 ];
6028 let right = [
6029 ModelineSegment::position("L12:C4"),
6030 ModelineSegment::text("Ready").with_feedback(FeedbackKind::Ok),
6031 ];
6032 let modeline = Modeline::new(&left).with_right(&right).render().unwrap();
6033 let history =
6034 [MinibufferHistoryRow::new("09:41", "Saved <draft>").with_feedback(FeedbackKind::Ok)];
6035 let minibuffer = Minibuffer::new()
6036 .with_prompt("wf")
6037 .with_message(FeedbackKind::Info, "Queued <job>")
6038 .with_history(&history)
6039 .render()
6040 .unwrap();
6041 let switcher_items = [
6042 ContextSwitcherItem::link("Production <east>", "/contexts/prod")
6043 .with_meta("3 apps")
6044 .active(),
6045 ContextSwitcherItem::link("Sandbox", "/contexts/sandbox")
6046 .with_badge(TrustedHtml::new(r#"<span class="wf-tag">test</span>"#)),
6047 ];
6048 let switcher = ContextSwitcher::new("Workspace", "Production", &switcher_items)
6049 .with_meta(TrustedHtml::new(r#"<span class="wf-tag ok">live</span>"#))
6050 .open()
6051 .render()
6052 .unwrap();
6053 let side_items = [
6054 SidenavItem::link("Overview", "/overview").active(),
6055 SidenavItem::link("Reports <beta>", "/reports")
6056 .muted()
6057 .with_badge("Soon"),
6058 SidenavItem::link("Billing", "/billing")
6059 .disabled()
6060 .with_coming_soon("coming soon"),
6061 ];
6062 let side_sections = [SidenavSection::new("Manage <workspace>", &side_items)];
6063 let sidenav = Sidenav::new(&side_sections).render().unwrap();
6064 let embedded_sidenav = Sidenav::new(&side_sections).embedded().render().unwrap();
6065
6066 assert!(modeline.contains(r#"class="wf-modeline""#));
6067 assert!(modeline.contains(r#"class="wf-ml-seg wf-ml-chevron""#));
6068 assert!(modeline.contains(r#"data-mode-toggle="""#));
6069 assert!(modeline.contains(r#"class="wf-ml-seg wf-ml-pos""#));
6070 assert!(modeline.contains(r#"class="wf-ml-seg is-ok""#));
6071 assert!(modeline.contains(r#"class="wf-ml-fill""#));
6072 assert!(minibuffer.contains(r#"class="wf-minibuffer-history""#));
6073 assert!(!minibuffer.contains("Queued <job>"));
6074 assert!(!minibuffer.contains("Saved <draft>"));
6075 assert!(switcher.contains(r#"class="wf-context-switcher""#));
6076 assert!(switcher.contains(r#"<details class="wf-context-switcher" open>"#));
6077 assert!(!switcher.contains("Production <east>"));
6078 assert!(switcher.contains(r#"<span class="wf-tag">test</span>"#));
6079 assert!(sidenav.contains(r#"class="wf-sidenav""#));
6080 assert!(sidenav.contains(r#"class="wf-sidenav-item is-active""#));
6081 assert!(sidenav.contains(r#"class="wf-sidenav-item is-muted""#));
6082 assert!(sidenav.contains(r#"aria-disabled="true""#));
6083 assert!(!sidenav.contains("Manage <workspace>"));
6084 assert!(!sidenav.contains("Reports <beta>"));
6085 assert!(sidenav.contains(r#"<nav class="wf-sidenav""#));
6086 assert!(embedded_sidenav.contains(r#"<div class="wf-sidenav""#));
6087 assert!(!embedded_sidenav.contains(r#"<nav class="wf-sidenav""#));
6088 }
6089
6090 #[test]
6091 fn secret_checklist_code_grid_snippets_and_strength_meter_are_product_neutral() {
6092 let secret = SecretValue::new("Recovery token", "recovery-token", "tok_<secret>")
6093 .with_warning("Shown once <store it>")
6094 .with_help(TrustedHtml::new(
6095 "<strong>Store this value securely.</strong>",
6096 ))
6097 .render()
6098 .unwrap();
6099 let checklist_items = [
6100 ChecklistItem::ok("DNS configured <edge>").with_description("Records verified."),
6101 ChecklistItem::warn("Webhook retry").with_status_label("review"),
6102 ];
6103 let checklist = Checklist::new(&checklist_items).render().unwrap();
6104 let codes = ["ABCD-EFGH", "IJKL<MNOP>"];
6105 let code_grid = CodeGrid::new(&codes)
6106 .with_label("One-time codes <backup>")
6107 .render()
6108 .unwrap();
6109 let block = CodeBlock::new("cargo add wavefunk-ui <latest>")
6110 .with_label("Install")
6111 .with_language("shell")
6112 .with_copy_target("install-command")
6113 .render()
6114 .unwrap();
6115 let tabs = [
6116 SnippetTab::new("Rust", r#"let value = "<typed>";"#)
6117 .with_language("rust")
6118 .active(),
6119 SnippetTab::new("Shell", "cargo test").with_language("shell"),
6120 ];
6121 let snippets = SnippetTabs::new("quickstart", &tabs).render().unwrap();
6122 let strength = StrengthMeter::new(3, 4, "Strong <enough>")
6123 .with_label("Key strength")
6124 .with_feedback(FeedbackKind::Ok)
6125 .live()
6126 .render()
6127 .unwrap();
6128
6129 assert!(secret.contains(r#"class="wf-secret-value""#));
6130 assert!(secret.contains(r##"data-wf-copy="#recovery-token""##));
6131 assert!(!secret.contains("data-wf-copy-value"));
6132 assert!(secret.contains("********"));
6133 assert!(!secret.contains("tok_<secret>"));
6134 assert!(!secret.contains("Shown once <store it>"));
6135 assert!(secret.contains("<strong>Store this value securely.</strong>"));
6136 let copyable_masked = SecretValue::new("Raw token", "raw-token", "raw-secret")
6137 .copy_raw_value()
6138 .render()
6139 .unwrap();
6140 assert!(copyable_masked.contains(r#"data-wf-copy-value="raw-secret""#));
6141 assert!(checklist.contains(r#"class="wf-checklist""#));
6142 assert!(checklist.contains(r#"class="wf-checklist-item is-ok""#));
6143 assert!(checklist.contains(r#"class="wf-checklist-item is-warn""#));
6144 assert!(!checklist.contains("DNS configured <edge>"));
6145 assert!(code_grid.contains(r#"class="wf-code-grid""#));
6146 assert!(!code_grid.contains("IJKL<MNOP>"));
6147 assert!(!code_grid.contains("One-time codes <backup>"));
6148 assert!(block.contains(r#"class="wf-code-block""#));
6149 assert!(block.contains(r#"data-language="shell""#));
6150 assert!(block.contains(r##"data-wf-copy="#install-command""##));
6151 assert!(!block.contains("wavefunk-ui <latest>"));
6152 assert!(snippets.contains(r#"class="wf-snippet-tabs""#));
6153 assert!(snippets.contains(r#"role="tablist""#));
6154 assert!(snippets.contains(r##"data-wf-snippet-tab="#quickstart-panel-1""##));
6155 assert!(snippets.contains(r#"aria-controls="quickstart-panel-1""#));
6156 assert!(snippets.contains(r#"id="quickstart-panel-2""#));
6157 assert!(snippets.contains(r#"hidden"#));
6158 assert!(!snippets.contains(r#"let value = "<typed>";"#));
6159 assert!(strength.contains(r#"class="wf-strength-meter is-ok""#));
6160 assert!(strength.contains(r#"role="progressbar""#));
6161 assert!(strength.contains(r#"aria-valuenow="3""#));
6162 assert!(strength.contains(r#"aria-valuemax="4""#));
6163 assert!(strength.contains(r#"style="--strength: 75%""#));
6164 assert!(!strength.contains("Strong <enough>"));
6165 }
6166
6167 #[test]
6168 fn marketing_primitives_render_stable_typed_sections() {
6169 let features = [
6170 FeatureItem::new("Typed <APIs>", "No struct literal churn."),
6171 FeatureItem::new("Embedded assets", "Self-contained binaries."),
6172 ];
6173 let feature_grid = FeatureGrid::new(&features).render().unwrap();
6174 let steps = [
6175 MarketingStep::new("Install", "Add the crate."),
6176 MarketingStep::new("Render", "Use Askama templates."),
6177 ];
6178 let step_grid = MarketingStepGrid::new(&steps).render().unwrap();
6179 let plans = [
6180 PricingPlan::new("Starter", "$9")
6181 .with_blurb("For small teams.")
6182 .featured(),
6183 PricingPlan::new("Scale", "$29"),
6184 ];
6185 let pricing = PricingPlans::new(&plans).render().unwrap();
6186 let testimonial = Testimonial::new(
6187 TrustedHtml::new("<p>Fast to wire.</p>"),
6188 "Operator <one>",
6189 "Founder",
6190 )
6191 .render()
6192 .unwrap();
6193 let section = MarketingSection::new("Component <system>", TrustedHtml::new(&feature_grid))
6194 .with_kicker("Wave Funk")
6195 .with_subtitle("Typed primitives for Rust apps.")
6196 .render()
6197 .unwrap();
6198
6199 assert!(feature_grid.contains(r#"class="mk-features""#));
6200 assert!(!feature_grid.contains("Typed <APIs>"));
6201 assert!(step_grid.contains(r#"class="mk-steps""#));
6202 assert!(pricing.contains(r#"class="wf-plans""#));
6203 assert!(pricing.contains(r#"class="wf-plan is-featured""#));
6204 assert!(testimonial.contains(r#"class="wf-testimonial""#));
6205 assert!(!testimonial.contains("Operator <one>"));
6206 assert!(section.contains(r#"class="mk-sect""#));
6207 assert!(!section.contains("Component <system>"));
6208 }
6209}