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}