tablo_ui/components/primitives/
select.rs1use topcoat::{
3 Result,
4 context::Cx,
5 icon::{IconData, icon, iconify::iconify_icon},
6 view::{Attributes, Child, StaticClass, View, attributes, class, component, view},
7};
8
9const SELECT: StaticClass = class!(
11 "h-9 w-full appearance-none items-center rounded-lg border border-border \
12 bg-transparent pr-8 pl-3 text-left text-sm transition-colors outline-none \
13 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 \
14 aria-invalid:border-destructive aria-invalid:focus-visible:ring-destructive \
15 focus-visible:ring-offset-background disabled:pointer-events-none",
16);
17
18const PICKER: StaticClass = class!(
21 "[&::picker(select)]:[appearance:base-select] \
22 [&::picker(select)]:mt-1 [&::picker(select)]:rounded-lg \
23 [&::picker(select)]:border [&::picker(select)]:border-border \
24 [&::picker(select)]:bg-popover [&::picker(select)]:p-1 \
25 [&::picker(select)]:text-popover-foreground [&::picker(select)]:shadow-sm \
26 [&::picker-icon]:hidden \
27 [&_optgroup>legend]:px-2 [&_optgroup>legend]:py-1.5 \
28 [&_optgroup>legend]:text-xs [&_optgroup>legend]:font-medium \
29 [&_optgroup>legend]:text-muted-foreground [&_optgroup>legend]:cursor-default \
30 [&_optgroup>legend]:select-none \
31 [&_option]:flex [&_option]:items-center [&_option]:gap-2 [&_option]:rounded-md \
32 [&_option]:px-2 [&_option]:py-1.5 [&_option]:text-sm [&_option]:outline-none \
33 [&_option:hover]:bg-foreground/5 [&_option:focus]:bg-foreground/5 \
34 [&_option:checked]:font-medium \
35 [&_option::checkmark]:order-1 [&_option::checkmark]:ml-auto \
36 [&_option::checkmark]:size-4 [&_option::checkmark]:shrink-0 \
37 [&_option::checkmark]:content-[''] [&_option::checkmark]:bg-muted-foreground \
38 [&_option::checkmark]:[mask-size:100%_100%] \
39 [&_option::checkmark]:[mask-image:var(--select-checkmark)]",
40);
41
42const CHECKMARK: IconData = iconify_icon!("lucide:check");
44
45fn checkmark_style(cx: &Cx) -> String {
48 let svg = format!(
49 r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="{}">{}</svg>"#,
50 CHECKMARK.view_box(),
51 CHECKMARK.into_body().render(cx),
52 );
53 let mut style = String::from(r#"--select-checkmark: url("data:image/svg+xml,"#);
54 for char in svg.chars() {
57 match char {
58 '%' => style.push_str("%25"),
59 '"' => style.push_str("%22"),
60 '#' => style.push_str("%23"),
61 _ => style.push(char),
62 }
63 }
64 style.push_str(r#"")"#);
65 style
66}
67
68#[component]
89pub async fn select(
90 cx: &Cx,
91 #[default] mut attrs: Attributes,
92 #[default] child: Child<'_>,
93) -> Result<impl View> {
94 Ok(view! {
100 <span
101 class=(class!(
102 "relative block has-[:disabled]:opacity-50 \
103 [&>select]:[appearance:base-select] \
104 [&:has(select:open)>svg]:rotate-180",
105 attrs.remove("class"),
106 ))
107 style=(checkmark_style(cx))
108 >
109 <select class=(class!(SELECT, PICKER)) (attrs)>(child)</select>
110 icon(
111 data: iconify_icon!("lucide:chevron-down"),
112 attrs: attributes! {
113 class="pointer-events-none absolute top-1/2 right-3 size-4 \
114 -translate-y-1/2 text-muted-foreground transition-transform"
115 }
116 )
117 </span>
118 })
119}