finish ui

This commit is contained in:
JMARyA 2025-01-17 17:52:28 +01:00
parent 36128864aa
commit 6c1ca1e8c0
Signed by: jmarya
GPG key ID: 901B2ADDF27C2263
4 changed files with 421 additions and 262 deletions

10
Cargo.lock generated
View file

@ -202,7 +202,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]] [[package]]
name = "based" name = "based"
version = "0.1.0" version = "0.1.0"
source = "git+https://git.hydrar.de/jmarya/based?branch=ui#78e3d6b798eb32d85ab58dd8891457f7ab6f75aa" source = "git+https://git.hydrar.de/jmarya/based?branch=ui#79f08fd202abcfbc52cbab09be7dccc02f4c7c01"
dependencies = [ dependencies = [
"bcrypt", "bcrypt",
"chrono", "chrono",
@ -313,9 +313,9 @@ checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.9" version = "1.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8293772165d9345bdaaa39b45b2109591e63fe5e6fbc23c6ff930a048aa310b" checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@ -3364,9 +3364,9 @@ dependencies = [
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "vcpkg" name = "vcpkg"

View file

@ -2,7 +2,7 @@ use based::auth::MaybeUser;
use based::request::{RawResponse, RequestContext, StringResponse, respond_with}; use based::request::{RawResponse, RequestContext, StringResponse, respond_with};
use based::ui::components::Shell; use based::ui::components::Shell;
use based::ui::{prelude::*, render_page}; use based::ui::{prelude::*, render_page};
use maud::{PreEscaped, html}; use maud::{PreEscaped, Render, html};
use pacco::pkg::mirror::MirrorRepository; use pacco::pkg::mirror::MirrorRepository;
use rocket::http::{ContentType, Status}; use rocket::http::{ContentType, Status};
use rocket::{State, get}; use rocket::{State, get};
@ -24,35 +24,39 @@ pub async fn index_page(
) -> StringResponse { ) -> StringResponse {
let repos: Vec<String> = Repository::list(); let repos: Vec<String> = Repository::list();
let content = html!( let content = Div().vanish()
(Flex(html! { .push(
h1 class="text-4xl font-bold pb-6" { "Repositories" }; Flex(
@if let Some(user) = user.take_user() { Div().vanish()
a class="text-lg" href="/account" { (user.username) }; .push(Padding(Text("Repositories")._4xl().bold()).bottom(ScreenValue::_6))
}; .push_some(user.take_user().as_ref(), |user: &based::auth::User| Link("/account", Text(&user.username).large()))
}).justify(Justify::Between)) ).justify(Justify::Between)
).push(
html!(
div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6" { div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6" {
@for repo in repos { @for repo in repos {
// TODO : Implement class transition (Animated(
(Link(&format!("/{repo}"),
Rounded( Rounded(
Padding( Padding(
Shadow::medium( Shadow::medium(
Flex(Hover( Flex(Hover(
Background(Gray::_600, Nothing()), Background(Gray::_600, Nothing())).on(
Background(Gray::_700, Background(Gray::_700,
Link(
&format!("/{repo}"),
Div().vanish().push(Text(&repo).medium().color(&Gray::_100)) Div().vanish().push(Text(&repo).medium().color(&Gray::_100))
.push(html! { .push_some(
@if config.is_mirrored_repo(&repo) { if config.is_mirrored_repo(&repo) {
(Padding(Rounded(Background(Blue::_500, Text("Mirrored").white().medium().sm())).size(Size::Full)).x(ScreenValue::_3).y(ScreenValue::_1)) Some(&())
}; } else { None },
}) |_: &()| Context(Padding(Rounded(Background(Blue::_500, Text("Mirrored").white().medium().sm())).size(Size::Full)).x(ScreenValue::_3).y(ScreenValue::_1)))).use_htmx()
))).items_center().gap(4))).all(ScreenValue::_4)) ))).items_center().gap(ScreenValue::_4)
)).all(ScreenValue::_4))
.size(Size::Large))) .size(Size::Large)))
} }
} }
); )
).render();
render(content, "Repositories", ctx).await render(content, "Repositories", ctx).await
} }

View file

@ -1,5 +1,8 @@
use based::request::{RequestContext, StringResponse}; use based::request::{RequestContext, StringResponse};
use based::ui::prelude::*; use based::ui::primitives::flex::Strategy;
use based::ui::primitives::space::SpaceBetweenWidget;
use based::ui::primitives::text::{Code, TextWidget};
use based::ui::{UIWidget, prelude::*};
use maud::{PreEscaped, Render, html}; use maud::{PreEscaped, Render, html};
use rocket::{State, get}; use rocket::{State, get};
@ -35,150 +38,212 @@ pub async fn pkg_ui(
let binaries = pkg.binaries(); let binaries = pkg.binaries();
let mut pkginfo = pkg.pkginfo(); let mut pkginfo = pkg.pkginfo();
let content = html! { let content = Div().vanish()
.push(
Flex(
// Package Name // Package Name
div class="flex flex-wrap gap-2 justify-center items-center" { Div().vanish()
(Link(&format!("/{}", repo.name), .push(
Link(&format!("/{}", repo.name),
Text(&repo.name).bold().color(&Gray::_100)._3xl() Text(&repo.name).bold().color(&Gray::_100)._3xl()
).use_htmx()) ).use_htmx()
)
p class="font-bold p-2 text-gray-400" { "/" }; .push(Padding(Text("/").bold().color(&Gray::_400)).all(ScreenValue::_2))
h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100 pr-2" { .push(
(pkg.name) Padding(Text(&pkg.name)._3xl().bold().color(&Gray::_100)).right(ScreenValue::_2)
}; )
.push_if(pkg.is_signed(), || {
@if pkg.is_signed() { Flex(
div class="flex items-center gap-2 text-slate-300 pr-4" { Padding(
(Span("")._2xl().bold()) Div().vanish()
(Span("Signed").sm().medium()) .push(Span("")._2xl().bold().color(&Slate::_300))
.push(Span("Signed").sm().medium().color(&Slate::_300))
// TODO : Add more info: Who signed? + Public Key recv // TODO : Add more info: Who signed? + Public Key recv
} ).right(ScreenValue::_4)
} ).items_center().gap(ScreenValue::_2)
})
@for arch in arch { .push_for_each(&arch, |arch: &Architecture| {
(Rounded( Rounded(
Padding( Padding(
Background(Gray::_700, Background(Gray::_700,
Span(&arch.to_string()).medium().sm()) Span(&arch.to_string()).medium().sm())
).x(ScreenValue::_3).y(ScreenValue::_1) ).x(ScreenValue::_3).y(ScreenValue::_1)
).size(Size::Full) ).size(Size::Full)
) })
} .push(Margin(
Flex(
a href=(format!("/pkg/{}/{}/{}", pkg.repo, pkg.arch.to_string(), pkg.file_name())) class="ml-4 inline-flex items-center px-2 py-2 bg-gray-200 text-black font-xs rounded-lg shadow-md hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50" { Padding(
Hover(Background(Gray::_300, Nothing())).on(Background(Gray::_200,
Rounded(Shadow::medium(
Link(&format!("/pkg/{}/{}/{}", pkg.repo, pkg.arch.to_string(), pkg.file_name()),
Paragraph(
html! {
svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" { svg class="w-5 h-5" xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2" {
path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14m7-7l-7 7-7-7" {}; path stroke-linecap="round" stroke-linejoin="round" d="M12 5v14m7-7l-7 7-7-7" {};
}; };
p class="ml-2" { "Download" }; p class="ml-2 text-black" { "Download" };
};
};
div class="flex flex-wrap pt-6" {
div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition flex-1 m-2 grow" {
h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300 underline" { "Info" };
@if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "pkgdesc" }).1 {
(build_info(desc.0, desc.1))
} }
).black().xs()))).size(Size::Large)
@if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "packager" }).1 { ))
(build_info(desc.0, desc.1)) ).x(ScreenValue::_2).y(ScreenValue::_2)
} ).items_center()
).left(ScreenValue::_4)
@if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "url" }).1 { )
(build_info(desc.0, desc.1)) ).wrap(Wrap::Wrap).gap(ScreenValue::_2).full_center().group()
} )
.push(
@if let Some(desc) = take_out(&mut pkginfo, |x| { x.0 == "size" }).1 { Padding(
(build_info(desc.0, desc.1)) Flex(
} Div().vanish().push(
Context(
@for (key, val) in pkginfo { FlexGrow(Strategy::Grow,
(build_info(key, val)) Animated(Background(Gray::_800, Shadow::large(Rounded(Margin(Padding(SpaceBetween(
}; Div()
}; .push(
Context(Text("Info").xl().semibold().color(&Gray::_300).underlined())
div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition m-2 grow md:grow-0 md:shrink" { )
h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300" { "Versions" } .push_some(
ul class="space-y-1" { take_out(&mut pkginfo, |x| { x.0 == "pkgdesc" }).1,
@for version in versions { |x: (String, String)| build_info(x.0, x.1)
li class="text-gray-800 dark:text-gray-100 hover:text-blue-500 dark:hover:text-blue-400 transition" { )
(Link(&format!("/{}/{}?ver={version}", repo.name, &pkg.name), .push_some(
take_out(&mut pkginfo, |x| { x.0 == "packager" }).1,
|x: (String, String)| build_info(x.0, x.1)
)
.push_some(
take_out(&mut pkginfo, |x| { x.0 == "url" }).1,
|x: (String, String)| build_info(x.0, x.1)
)
.push_some(
take_out(&mut pkginfo, |x| { x.0 == "size" }).1,
|x: (String, String)| build_info(x.0, x.1)
)
.push_for_each(&pkginfo, |(key, val)| build_info(key.clone(), val.clone()))
).y(ScreenValue::_2)).all(ScreenValue::_4)).all(ScreenValue::_2)).size(Size::Large)))))
)).push(
SpaceBetween(
Padding(
Background(Gray::_800,
Shadow::large(
Rounded(
Margin(
Screen::medium(FlexGrow(Strategy::NoGrow, Nothing())).on(
FlexGrow(Strategy::Grow,
Div().vanish()
.push(
Text("Versions").xl().semibold().color(&Gray::_300)
)
.push(
SpaceBetween(
Div().vanish()
.push_for_each(&versions, |version: &String| {
Animated(Link(&format!("/{}/{}?ver={version}", repo.name, &pkg.name),
if pkg.version.as_ref() if pkg.version.as_ref()
.map(|x| *x == Package::version(&version).0).unwrap_or_default() .map(|x| *x == Package::version(&version).0).unwrap_or_default()
{ Text(&version).color(&Blue::_500) } else { Text(&version) } { Text(&version).color(&Blue::_500).render() } else {
Hover(Text("").color(&Blue::_400)).on(
Text(&version).color(&Gray::_100)).render()
}
).use_htmx()) ).use_htmx())
}; })
} ).y(ScreenValue::_1)
} )
} ))
}; ).all(ScreenValue::_2)
).size(Size::Large)
div class="flex flex-wrap pt-6" { )
div class="space-y-2" { )
h2 class="text-xl font-bold text-gray-700 dark:text-gray-300" { "Content" } ).all(ScreenValue::_4)
).y(ScreenValue::_2)
div class="flex flex-wrap" { )
@if !systemd_units.is_empty() { ).wrap(Wrap::Wrap).group()
div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition m-2 grow" { ).top(ScreenValue::_6)
h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Systemd Units" } ).push(
ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" { Padding(Flex(
@for unit in systemd_units { SpaceBetween(
Div().vanish()
.push(
Text("Content").xl().bold().color(&Gray::_300)
)
.push(
Flex(
Div().vanish()
.push_if(!systemd_units.is_empty(), || {
InfoCard(
Div().vanish()
.push(
CardTitle("Systemd Units")
)
.push(
ListElements(&systemd_units)
)
)
})
.push_if(!pacman_hooks.is_empty(), || {
InfoCard(
Div().vanish()
.push(CardTitle("Pacman Hooks"))
.push(
Paragraph(
SpaceBetween(
Div().vanish()
.push_for_each(&pacman_hooks, |hook: &(String, String)| {
Div().vanish()
.push(
Text(&hook.0).xl().semibold().color(&Gray::_300)
)
.push(
Padding(Rounded(Background(Gray::_700, Code(&hook.1).sm().color(&Gray::_100))).size(Size::Large)).all(ScreenValue::_4)
)
})
.push(
html! {
@for unit in &systemd_units {
li { (unit) } li { (unit) }
} }
} })
} ).y(ScreenValue::_1)
} ).list_style(ListStyle::Disc).color(&Gray::_300)
)
@if !pacman_hooks.is_empty() { )
div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition m-2 grow" { })
h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Pacman Hooks" } .push_if(!binaries.is_empty(),
ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" { || InfoCard(
@for hook in pacman_hooks { Div().vanish()
h2 class="text-xl font-semibold text-gray-700 dark:text-gray-300" { (hook.0) } .push(
CardTitle("Binaries")
pre class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg text-gray-800 dark:text-gray-100 overflow-x-auto text-sm" { ).push(
(hook.1) ListElements(&binaries)
} )
} )
} )
} .push(
} InfoCard(
Div().vanish().push(
@if !binaries.is_empty() { CardTitle("Package Files")
div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition m-2 grow" { )
h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Binaries" } .push(
ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" { ListElements(&pkg.file_list())
@for binary in binaries { )
li { (binary) } )
} )
} ).group().wrap(Wrap::Wrap)
} )
} ).y(ScreenValue::_2)
).wrap(Wrap::Wrap).group()).top(ScreenValue::_6)
div class="space-y-2 p-4 bg-gray-50 dark:bg-gray-800 shadow-lg rounded-lg transition m-2 grow" { ).push_some(install_script.as_ref(), |install_script: &String| {
h3 class="text-lg font-medium text-gray-800 dark:text-gray-100 underline" { "Package Files" } SpaceBetween(
ul class="list-disc list-inside text-gray-700 dark:text-gray-300 space-y-1" { Margin(
@for file in pkg.file_list() { Div()
li { (file) } .push(Text("Install Script")._3xl().semibold().color(&Gray::_300).indentation(ScreenValue::_2))
} .push(Rounded(Padding(Background(Gray::_700,
} Code(install_script.trim())
} .color(&Gray::_100).sm()
} .indentation(ScreenValue::_0)
}; )).all(ScreenValue::_2)).size(Size::Large))
}; ).top(ScreenValue::_6)
).y(ScreenValue::_4)
// Install Script }).render();
@if let Some(install_script) = install_script {
div class="space-y-4 pt-6" {
h2 class="text-3xl font-semibold text-gray-700 dark:text-gray-300" { "Install Script" }
pre class="bg-gray-100 dark:bg-gray-700 p-4 rounded-lg text-gray-800 dark:text-gray-100 overflow-x-auto text-sm" {
(install_script)
}
};
}
};
Some(render(content, pkg_name, ctx).await) Some(render(content, pkg_name, ctx).await)
} }
@ -222,21 +287,32 @@ pub async fn repo_ui(
repo.list_pkg() repo.list_pkg()
}; };
let content = html! { let repo_info = Margin(
// Repository name and architectures // Repository name and architectures
div class="flex flex-wrap items-center justify-center mb-6" { Flex(
h1 class="text-3xl font-bold text-gray-800 dark:text-gray-100" { Div()
(repo.name) .vanish()
}; .push(Text(&repo.name)._3xl().bold().color(&Gray::_100))
.push_if(config.is_mirrored_repo(&repo.name), || {
@if config.is_mirrored_repo(&repo.name) { Background(
div class="inline-block px-3 py-1 text-sm font-medium text-white bg-blue-500 rounded-full ml-4" { "Mirrored" }; Blue::_500,
}; Rounded(
Margin(
div class="flex gap-2 mt-2 md:mt-0 ml-4" { Padding(Text("Mirrored").sm().medium().white())
@for a in architectures { .x(ScreenValue::_3)
.y(ScreenValue::_1),
)
.left(ScreenValue::_4),
)
.size(Size::Full),
)
})
.push(
Screen::medium(Margin(Nothing()).top(ScreenValue::_0)).on(Margin(
Flex(Div().vanish().push_for_each(&architectures, |a: &String| {
html! {
@if let Some(arch) = arch.as_ref() { @if let Some(arch) = arch.as_ref() {
@if arch.to_string() == a { @if arch.to_string() == *a {
(arch_card(&a, &repo.name, true)) (arch_card(&a, &repo.name, true))
} @else { } @else {
(arch_card(&a, &repo.name, false)) (arch_card(&a, &repo.name, false))
@ -245,50 +321,92 @@ pub async fn repo_ui(
(arch_card(&a, &repo.name, false)) (arch_card(&a, &repo.name, false))
} }
} }
} }))
}; .group()
.gap(ScreenValue::_2),
)
.left(ScreenValue::_4)
.top(ScreenValue::_2)),
),
)
.wrap(Wrap::Wrap)
.full_center()
.group(),
)
.bottom(ScreenValue::_6);
// Package list let package_list = SpaceBetween(Div().vanish().push_for_each(&packages, |pkg| {
ul class="space-y-4" { Animated(
@for pkg in packages { Flex(
( Padding(
Hover(Background(Gray::_600, Nothing())).on(Background(
Gray::_700,
Rounded(Shadow::medium(
Link( Link(
&format!("/{}/{pkg}", repo.name), &format!("/{}/{pkg}", repo.name),
// TODO : Implement transition class Div()
.vanish()
.push(Context(Sized(
ScreenValue::_10,
ScreenValue::_10,
Rounded(Background(
Blue::_500,
Flex( Flex(
Text(
&pkg.chars()
.next()
.unwrap_or_default()
.to_uppercase()
.to_string(),
)
.white()
.semibold(),
)
.full_center()
.group(),
))
.size(Size::Full),
)))
.push(Context(Width(
ScreenValue::fit,
Text(&pkg).medium().color(&Gray::_100),
)))
.push(Context(
Padding( Padding(
Hover( Flex(
Background(Gray::_600, Nothing()), Div()
Background(Gray::_700, .vanish()
Rounded(
Shadow::medium(
Div().vanish()
.push( .push(
html! { (Sized(ScreenValue::_10, ScreenValue::_10, Rounded( Span("")._2xl().bold().color(&Slate::_300),
Background(Blue::_500, Flex(Text(
&pkg.chars().next().unwrap_or_default().to_uppercase().to_string()
).white().semibold()).full_center().group()
)).size(Size::Full))) }
) )
.push(html! { (Width(ScreenValue::fit, Text(&pkg).medium().color(&Gray::_100))) }) .push(
.push(html! { Span("Signed")
( .sm()
Flex( .medium()
Padding( .color(&Slate::_300),
Div().vanish() ),
.push(Span("")._2xl().bold().color(&Slate::_300))
.push(Span("Signed").sm().medium().color(&Slate::_300))
).right(ScreenValue::_4)
).items_center().gap(2).group()
) )
}) .items_center()
.gap(ScreenValue::_2)
.group(),
) )
).size(Size::Large)))).all(ScreenValue::_4)).items_center().gap(4) .right(ScreenValue::_4),
).use_htmx() )),
) )
} .use_htmx(),
} ))
}; .size(Size::Large),
)),
)
.all(ScreenValue::_4),
)
.items_center()
.gap(ScreenValue::_4),
)
}))
.y(ScreenValue::_4);
let content = Div().vanish().push(repo_info).push(package_list).render();
render(content, &repo.name, ctx).await render(content, &repo.name, ctx).await
} }
@ -307,12 +425,16 @@ pub fn build_info(key: String, value: String) -> PreEscaped<String> {
return key_value("Packager".to_string(), value); return key_value("Packager".to_string(), value);
} }
"url" => { "url" => {
return html! { return Flex(
div class="flex" { Div()
span class="font-bold w-32" { (format!("Website: ")) }; .vanish()
a class="ml-2 text-blue-400" href=(value) { (value) }; .push(Width(ScreenValue::_32, Span("Website: ").bold()))
}; .push(
}; Margin(Link(&value, Span(&value).color(&Blue::_400))).left(ScreenValue::_6),
),
)
.group()
.render();
} }
"builddate" => { "builddate" => {
let date = chrono::DateTime::from_timestamp(value.parse().unwrap(), 0).unwrap(); let date = chrono::DateTime::from_timestamp(value.parse().unwrap(), 0).unwrap();
@ -349,7 +471,7 @@ pub fn key_value(key: String, value: String) -> PreEscaped<String> {
Div() Div()
.vanish() .vanish()
.push(Width(ScreenValue::_32, Span(&format!("{key}: ")).bold())) .push(Width(ScreenValue::_32, Span(&format!("{key}: ")).bold()))
.push(Margin(Span(&value)).left(ScreenValue::_2)), .push(Margin(Span(&value)).left(ScreenValue::_6)),
) )
.items_center() .items_center()
.group() .group()
@ -384,24 +506,57 @@ pub fn find_pkg_url(pkg: &str) -> Option<String> {
pub fn pkg_list_info(key: &str, value: &str) -> PreEscaped<String> { pub fn pkg_list_info(key: &str, value: &str) -> PreEscaped<String> {
let pkgs = value.split_whitespace().map(|pkg| { let pkgs = value.split_whitespace().map(|pkg| {
if let Some(pkg_url) = find_pkg_url(pkg) { if let Some(pkg_url) = find_pkg_url(pkg) {
html! { Margin(Link(&pkg_url, Text(&pkg).color(&Blue::_400)).use_htmx()).left(ScreenValue::_6)
a href=(pkg_url) class="ml-2 text-blue-400" { (pkg) };
}
} else { } else {
html! { Margin(Span(&pkg)).left(ScreenValue::_6)
span class="ml-2" { (pkg) };
}
} }
}); });
html! { html! {
(Flex(html! { (Flex(
span class="font-bold w-32" { (format!("{key}: ")) }; Div().vanish()
div class="flex flex-wrap" { .push(Width(ScreenValue::_32, Span(&format!("{key}: ")).bold()))
.push(Flex(
html! {
@for pkg in pkgs { @for pkg in pkgs {
(pkg) (pkg)
}; }
}; }
}).items_center().group()) ).group().wrap(Wrap::Wrap))
).items_center().group())
} }
} }
#[allow(non_snake_case)]
pub fn ListElements(el: &[String]) -> TextWidget {
Paragraph(
SpaceBetween(html! {
@for unit in el {
li { (unit) }
}
})
.y(ScreenValue::_1),
)
.list_style(ListStyle::Disc)
.color(&Gray::_300)
}
#[allow(non_snake_case)]
pub fn CardTitle(title: &str) -> PreEscaped<String> {
Context(Text(title).large().medium().color(&Gray::_100).underlined())
}
#[allow(non_snake_case)]
pub fn InfoCard<T: UIWidget + 'static>(inner: T) -> SpaceBetweenWidget {
SpaceBetween(
Padding(Background(
Gray::_800,
Shadow::large(
Rounded(Margin(FlexGrow(Strategy::Grow, inner)).all(ScreenValue::_2))
.size(Size::Large),
),
))
.all(ScreenValue::_4),
)
.y(ScreenValue::_2)
}

View file

@ -128,7 +128,7 @@ pub async fn account_page(user: User, ctx: RequestContext) -> StringResponse {
(Link("/passwd", Margin( (Link("/passwd", Margin(
Rounded(Padding( Rounded(Padding(
Hover( Hover(
Background(Green::_600, Nothing()), Background(Green::_600, Nothing())).on(
Background(Green::_500, Text("Change Password").white()) Background(Green::_500, Text("Change Password").white())
) )
).x(ScreenValue::_6).y(ScreenValue::_2)) ).x(ScreenValue::_6).y(ScreenValue::_2))