switch to scss asset & remove sir

This commit is contained in:
2025-10-27 18:54:34 -06:00
parent 987cfd57d2
commit 4e30be3ebd
4 changed files with 339 additions and 498 deletions
Generated
-66
View File
@@ -107,12 +107,6 @@ dependencies = [
"pkg-config",
]
[[package]]
name = "arc-swap"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457"
[[package]]
name = "ashpd"
version = "0.8.1"
@@ -3256,15 +3250,6 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
[[package]]
name = "litrs"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5"
dependencies = [
"proc-macro2",
]
[[package]]
name = "lock_api"
version = "0.4.12"
@@ -3651,7 +3636,6 @@ dependencies = [
"serde",
"serde-wasm-bindgen 0.6.5",
"serde_json",
"sir",
"tokio",
"tokio-rustls",
"tokio-util",
@@ -3860,17 +3844,6 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -5022,23 +4995,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rsass"
version = "0.28.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "212e14dfa9e48df42c0125c80a213a9a0269103130c8c154080fdbbb79ce7d52"
dependencies = [
"arc-swap",
"fastrand",
"lazy_static",
"nom",
"num-bigint",
"num-integer",
"num-rational",
"num-traits",
"tracing",
]
[[package]]
name = "rust-embed"
version = "8.5.0"
@@ -5782,28 +5738,6 @@ version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
[[package]]
name = "sir"
version = "0.6.0-alpha.4"
source = "git+https://gitlab.com/samsartor/sir#99ac4db5c0a02debf15c145c998fe90d854269b2"
dependencies = [
"dioxus",
"once_cell",
"sir-macro",
]
[[package]]
name = "sir-macro"
version = "0.5.0"
source = "git+https://gitlab.com/samsartor/sir#99ac4db5c0a02debf15c145c998fe90d854269b2"
dependencies = [
"litrs",
"proc-macro2",
"quote",
"rand 0.8.5",
"rsass",
]
[[package]]
name = "slab"
version = "0.4.9"
-3
View File
@@ -82,9 +82,6 @@ ordermap = "0.5.3"
html-purifier = "0.3.0"
markdown = "0.3.0"
futures-channel = "0.3.30"
sir = { git = "https://gitlab.com/samsartor/sir", features = [
"dioxus",
] } # dioxus 0.6
mumble-web2-common = { workspace = true }
serde = { workspace = true }
tracing-subscriber = { version = "0.3.18", features = ["ansi"] }
+293
View File
@@ -0,0 +1,293 @@
:root {
--txt-color: oklch(0.9 0 99);
--bg-color: oklch(0.15 0.01 338.64);
--light-bg-color: oklch(0.25 0.01 338.64);
--login-bg-color: #5d7680;
--primary-btn-color: #7bad9f;
--accent-normal: #7bad9f;
--accent-muted: #ff746c;
--accent-deafened: #464459;
--line-width: 2px;
--line-color: white;
}
body {
margin: 0;
}
#main {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-around;
background-color: var(--bg-color);
overflow: auto;
color: var(--txt-color);
font-family: Nunito;
font-size: 15pt;
font-weight: 600;
}
button {
font-weight: bold;
font-size: medium;
border: none;
border-radius: 4px;
color: var(--txt-color);
background-color: var(--primary-btn-color);
cursor: pointer;
}
input {
border: none;
border-radius: 4px;
background-color: white;
color: black;
}
input:focus,
input:focus-visible {
border: none;
outline: solid var(--line-width) var(--accent-normal);
outline-offset: -3px;
}
a:link {
color: var(--accent-normal);
}
a:visited {
color: var(--accent-muted);
}
.userpil {
border-radius: 100px;
padding: 4px 8px;
width: fit-content;
img {
height: 1em;
vertical-align: text-bottom;
}
&.is_self {
font-weight: bolder;
}
}
.channel {
&_details {
flex: 0 0 100%;
summary {
cursor: pointer;
}
summary:focus-visible {
outline: none;
}
}
&_children {
border-left: solid var(--line-color) var(--line-width);
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
margin-left: 5px;
padding-left: 11px;
padding-top: 4px;
}
}
.chat {
&_panel {
display: flex;
flex-direction: column;
}
&_history {
overflow-y: auto;
flex: 1 0 0;
}
&_message {
display: flex;
flex-direction: row;
margin: 16px;
gap: 8px;
align-items: center;
}
&_box_wrapper {
padding: 16px;
border-top: solid var(--line-color) var(--line-width);
}
&_box {
display: flex;
flex-direction: row;
gap: 16px;
background-color: var(--light-bg-color);
padding-top: 16px;
padding-bottom: 16px;
padding-left: 8px;
padding-right: 16px;
border-radius: 8px;
input {
color: white;
background-color: var(--light-bg-color);
font-size: larger;
flex-grow: 1;
border: none;
}
input:focus {
outline: none;
}
}
}
.user_edit_button {
background-color: oklch(0.53 0.1431 264.18);
border-radius: 50%;
aspect-ratio: 1 / 1;
}
.button_row {
display: flex;
gap: 10px;
.spacer {
flex-grow: 1;
}
}
.toggle_button {
padding: 8px;
height: 100%;
aspect-ratio: 1 / 1;
background-color: unset;
border: solid rgb(255 255 255 / 0.1) 3px;
border-radius: 10px;
color: rgb(255 255 255 / 50%);
transition: all 0.5s ease-in-out;
&.is_on {
background-color: oklch(0.5 0.1381 21.71 / 20.12%);
color: oklch(0.53 0.1505 21.71 / 89.38%);
}
.material-symbols-outlined {
font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;
vertical-align: middle;
font-size: 35px;
}
}
.server {
&_grid {
display: grid;
height: 100%;
background-color: var(--bg-color);
grid-template-rows: 1fr auto;
grid-template-columns: 1fr 2fr;
grid-template-areas:
"tree chat"
"control chat";
@media screen and (max-width: 720px) {
grid-template-rows: auto 1fr 1fr;
grid-template-columns: 1fr;
grid-template-areas:
"tree"
"control"
"chat";
}
}
&_channel_box {
padding: 16px;
overflow: auto;
grid-area: tree;
}
&_chat_box {
display: flex;
flex-direction: row;
grid-area: chat;
border-left: solid var(--line-color) var(--line-width);
@media screen and (max-width: 720px) {
border-left: unset;
border-top: solid var(--line-color) var(--line-width);
}
}
&_control_box {
padding: 16px;
margin: 16px;
background-color: var(--light-bg-color);
border-radius: 10px;
overflow: hidden;
grid-area: control;
display: flex;
gap: 10px;
flex-direction: column;
}
}
.login {
max-width: 50vw;
align-self: center;
padding: 32px;
border-radius: 16px;
background-color: var(--login-bg-color);
display: flex;
flex-direction: column;
gap: 16px;
input,
button {
padding: 8px;
}
h1 {
margin: 0;
color: #b3c6b4;
}
&_bttn {
font-weight: bold;
font-size: large;
}
&_error {
background-color: white;
border-radius: 4px;
overflow: auto;
padding: 4px;
color: red;
pre {
color: black;
}
}
}
+46 -429
View File
@@ -4,7 +4,6 @@ use dioxus::prelude::*;
use mime_guess::Mime;
use mumble_web2_common::{ClientConfig, ServerStatus};
use ordermap::OrderSet;
use sir::{css, global_css};
use std::collections::HashMap;
use crate::imp;
@@ -138,25 +137,6 @@ impl UserIcon {
#[component]
pub fn UserPill(name: String, icon: UserIcon, isself: bool) -> Element {
let pill = css!(
"
border-radius: 100px;
padding: 4px 8px;
width: fit-content;
img {
height: 1em;
vertical-align: text-bottom;
}
"
);
let pill_self = css!(
"
font-weight: bolder;
"
);
let color = match icon {
UserIcon::Normal => "var(--accent-normal)",
UserIcon::Muted => "var(--accent-muted)",
@@ -166,7 +146,7 @@ pub fn UserPill(name: String, icon: UserIcon, isself: bool) -> Element {
rsx!(
div {
class: match isself { true => format!("{pill} {pill_self}"), false => format!("{pill}") },
class: match isself { true => "userpil is_self", false => "userpil" },
style: "background-color: {color}",
{ icon.url().map(|url| rsx!(img { src: url })) }
"\u{00A0}{name}\u{00A0}"
@@ -200,34 +180,9 @@ pub fn Channel(id: ChannelId) -> Element {
return rsx!("missing channel {id}");
};
let channel_details = css!(
"
flex: 0 0 100%;
summary {
cursor: pointer;
}
summary:focus-visible {
outline: none;
}
"
);
let channel_children = css!(
"
border-left: solid var(--line-color) var(--line-width);
display: flex;
flex-direction: row;
flex-wrap: wrap;
gap: 8px;
margin-left: 5px;
padding-left: 11px;
padding-top: 4px; "
);
rsx!(
details {
class: "{channel_details}",
class: "channel_details",
open: true,
summary {
span {
@@ -242,7 +197,7 @@ pub fn Channel(id: ChannelId) -> Element {
}
if state.users.len() + state.children.len() > 0 {
div {
class: "{channel_children}",
class: "channel_children",
for id in state.users.iter() {
User { id: *id }
}
@@ -283,71 +238,6 @@ pub fn ChatView() -> Element {
let server = STATE.server.read();
let mut draft = use_signal(|| "".to_string());
let chat_panel = css!(
"
display: flex;
flex-direction: column;
"
);
let chat_history = css!(
"
overflow-y: auto;
flex: 1 0 0;
"
);
let chat_message = css!(
"
display: flex;
flex-direction: row;
margin: 16px;
gap: 8px;
align-items: center;
"
);
let chat_box_wrapper = css!(
"
padding: 16px;
border-top: solid var(--line-color) var(--line-width);
"
);
let chat_box = css!(
"
display: flex;
flex-direction: row;
gap: 16px;
background-color: var(--light-bg-color);
padding-top: 16px;
padding-bottom: 16px;
padding-left: 8px;
padding-right: 16px;
border-radius: 8px;
input {
color: white;
background-color: var(--light-bg-color);
font-size: larger;
flex-grow: 1;
border: none;
}
input:focus {
outline: none;
}
"
);
let mut do_send = move || {
if let Some(user) = STATE.server.read().this_user() {
net.send(SendChat {
@@ -359,12 +249,12 @@ pub fn ChatView() -> Element {
rsx!(
div {
class: "{chat_panel}",
class: "chat_panel",
div {
class: "{chat_history}",
class: "chat_history",
for chat in server.chat.iter() {
div {
class: "{chat_message}",
class: "chat_message",
if let Some(sender) = chat.sender.and_then(|u| server.users.get(&u)) {
UserPill {
name: sender.name.clone(),
@@ -379,9 +269,9 @@ pub fn ChatView() -> Element {
}
}
div {
class: "{chat_box_wrapper}",
class: "chat_box_wrapper",
div {
class: "{chat_box}",
class: "chat_box",
input {
placeholder: "say something",
value: "{draft.read()}",
@@ -418,25 +308,6 @@ pub fn ChatView() -> Element {
)
}
// true => rsx!(span { class: "material-symbols-outlined", style: "{button_style}", "mic_off"}),
//Connecting => rsx! {
// div {
// class: "{connecting_status}",
// span {
// class: "material-symbols-outlined",
// style: "vertical-align: middle; font-size: 30px;",
// "signal_cellular_alt_2_bar"
// }
// span {
// style: "width: 5px; display: inline-block;"
// }
// span {
// style: "vertical-align: middle; font-size: 30px;",
// "Connecting"
// }
// }
//},
#[component]
pub fn ControlView(config: Resource<ClientConfig>) -> Element {
let net: Coroutine<Command> = use_coroutine_handle();
@@ -462,99 +333,15 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
.as_ref()
.and_then(|gui_config| gui_config.proxy_url.clone());
let button_row = css!(
r#"
display: flex;
gap: 10px;
"#
);
let spacer = css!(
r#"
flex-grow: 1;
"#
);
let toggle_button = css!(
r#"
padding: 8px;
height: 100%;
aspect-ratio: 1 / 1;
background-color: unset;
border: solid rgb(255 255 255 / 0.1) 3px;
border-radius: 10px;
color: rgb(255 255 255 / 50%);
transition: all 0.5s ease-in-out;
"#
);
let toggle_button_on = css!(
r#"
padding: 8px;
height: 100%;
aspect-ratio: 1 / 1;
background-color: oklch(0.5 0.1381 21.71 / 20.12%);
border: solid rgb(255 255 255 / 0) 3px;
border-radius: 10px;
color: oklch(0.53 0.1505 21.71 / 89.38%);
transition: all 0.25s ease-in-out;
"#
);
let button_style = r#"
font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;
vertical-align: middle;
font-size: 35px;
"#;
let connecting_status = css!(
r#"
color: yellow;
"#
);
let connected_status = css!(
r#"
color: oklch(0.55 0.1184 141.35);
"#
);
let disconnected_status = css!(
r#"
color: gray;
"#
);
let failed_status = css!(
r#"
color: red;
"#
);
let connection_info = css!(
r#"
color: gray;
"#
);
let user_edit_button = css!(
r#"
background-color: oklch(0.53 0.1431 264.18);
border-radius: 50%;
aspect-ratio: 1 / 1;
"#
);
let connecting_color = "yellow";
let connected_color = "oklch(0.55 0.1184 141.35)";
let disconnected_color = "gray";
let failed_color = "red";
let connection_status = match &*status.read() {
Connecting => rsx! {
div {
class: "{connecting_status}",
style: "color: \"{connecting_color}\";",
span {
class: "material-symbols-outlined",
style: "vertical-align: middle; font-size: 30px;",
@@ -572,7 +359,7 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
Connected => rsx! {
div {
div {
class: "{connected_status}",
style: "color: \"{connected_color}\";",
span {
class: "material-symbols-outlined",
style: "vertical-align: middle; font-size: 30px;",
@@ -587,7 +374,6 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
}
}
div {
class: "{connection_info}",
span { style: "width: 3px; display: inline-block;"}
span { "{current_channel_name}" }
if let Some(proxy_url) = proxy_url {
@@ -599,7 +385,7 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
},
Disconnected => rsx! {
div {
class: "{disconnected_status}",
style: "color: \"{disconnected_color}\";",
span {
class: "material-symbols-outlined",
style: "vertical-align: middle;",
@@ -616,7 +402,7 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
},
Failed(_) => rsx! {
div {
class: "{failed_status}",
style: "color: \"{failed_color}\";",
span {
class: "material-symbols-outlined",
style: "vertical-align: middle;",
@@ -637,17 +423,16 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
rsx!(
// Server control
div {
class: "{button_row}",
class: "button_row",
div {
{connection_status}
}
span { class: "{spacer}" }
span { class: "spacer" }
button {
class: "{toggle_button}",
class: "toggle_button",
onclick: move |_| net.send(Disconnect),
span {
class: "material-symbols-outlined",
style: "{button_style}",
"signal_disconnected"
}
}
@@ -655,9 +440,9 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
hr { style: "width: 100%;" }
// User control
div {
class: "{button_row}",
class: "button_row",
button {
class: "{user_edit_button}",
class: "user_edit_button",
span {
class: "material-symbols-outlined",
style: "color: oklch(0.65 0.2245 28.06); font-size: 45px; font-variation-settings: 'FILL' 1, 'wght' 700, 'GRAD' 0, 'opsz' 48;",
@@ -672,11 +457,11 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
span { style: "font-size: 20px; color: gray;", "some data" }
}
}
span { class: "{spacer}" }
span { class: "spacer" }
button {
class: match denoise() {
true => toggle_button_on,
false => toggle_button,
true => "toggle_button is_on",
false => "toggle_button",
},
role: "switch",
aria_checked: denoise(),
@@ -686,36 +471,36 @@ pub fn ControlView(config: Resource<ClientConfig>) -> Element {
net.send(UpdateMicEffects { denoise: new_denoise })
},
match denoise() {
true => rsx!(span { class: "material-symbols-outlined", style: "{button_style}", "cadence"}),
false => rsx!(span { class: "material-symbols-outlined", style: "{button_style}", "graphic_eq"}),
true => rsx!(span { class: "material-symbols-outlined", "cadence"}),
false => rsx!(span { class: "material-symbols-outlined", "graphic_eq"}),
}
}
button {
class: match mute || self_mute {
true => toggle_button_on,
false => toggle_button,
true => "toggle_button is_on",
false => "toggle_button",
},
role: "switch",
aria_checked: mute || self_mute,
disabled: mute,
onclick: move |_| net.send(SetMute { mute: !self_mute }),
match mute || self_mute {
true => rsx!(span { class: "material-symbols-outlined", style: "{button_style}", "mic_off"}),
false => rsx!(span { class: "material-symbols-outlined", style: "{button_style}", "mic"}),
true => rsx!(span { class: "material-symbols-outlined", "mic_off"}),
false => rsx!(span { class: "material-symbols-outlined", "mic"}),
}
}
button {
class: match deaf || self_deaf {
true => toggle_button_on,
false => toggle_button,
true => "toggle_button in_on",
false => "toggle_button",
},
role: "switch",
aria_checked: deaf || self_deaf,
disabled: deaf,
onclick: move |_| net.send(SetDeaf { deaf: !self_deaf }),
match deaf || self_deaf {
true => rsx!(span { class: "material-symbols-outlined", style: "{button_style}", "volume_off"}),
false => rsx!(span { class: "material-symbols-outlined", style: "{button_style}", "volume_up"}),
true => rsx!(span { class: "material-symbols-outlined", "volume_off"}),
false => rsx!(span { class: "material-symbols-outlined", "volume_up"}),
}
}
}
@@ -737,71 +522,11 @@ pub fn ServerView(config: Resource<ClientConfig>) -> Element {
return rsx!();
};
let grid = css!(
r#"
display: grid;
height: 100%;
background-color: var(--bg-color);
grid-template-rows: 1fr auto;
grid-template-columns: 1fr 2fr;
grid-template-areas:
"tree chat"
"control chat";
@media screen and (max-width: 720px) {
grid-template-rows: auto 1fr 1fr;
grid-template-columns: 1fr;
grid-template-areas:
"tree"
"control"
"chat";
}
"#
);
let channel_box = css!(
"
padding: 16px;
overflow: auto;
grid-area: tree;
"
);
let chat_box = css!(
"
display: flex;
flex-direction: row;
grid-area: chat;
border-left: solid var(--line-color) var(--line-width);
@media screen and (max-width: 720px) {
border-left:unset;
border-top: solid var(--line-color) var(--line-width);
}
"
);
let control_box = css!(
"
padding: 16px;
margin: 16px;
background-color: var(--light-bg-color);
border-radius: 10px;
overflow: hidden;
grid-area: control;
display: flex;
gap: 10px;
flex-direction: column;
"
);
rsx!(
div {
class: "{grid}",
class: "server_grid",
div {
class: "{channel_box}",
class: "server_channel_box",
for (id, state) in server.channels.iter() {
if state.parent.is_none() {
Channel { id: *id }
@@ -809,11 +534,11 @@ pub fn ServerView(config: Resource<ClientConfig>) -> Element {
}
}
div {
class: "{chat_box}",
class: "server_chat_box",
ChatView {}
}
div {
class: "{control_box}",
class: "server_control_box",
ControlView { config }
}
}
@@ -865,49 +590,6 @@ pub fn LoginView(config: Resource<ClientConfig>) -> Element {
let previous_username = imp::load_username();
let mut username = use_signal(|| previous_username.unwrap_or(String::new()));
let error = css!(
"
background-color: white;
border-radius: 4px;
overflow: auto;
padding: 4px;
color: red;
pre {
color: black;
}
"
);
let login_box = css!(
"
max-width: 50vw;
align-self: center;
padding: 32px;
border-radius: 16px;
background-color: var(--login-bg-color);
display: flex;
flex-direction: column;
gap: 16px;
input,button {
padding: 8px;
}
h1 {
margin: 0;
color: #b3c6b4;
}
"
);
let bttn = css!(
"
font-weight: bold;
font-size: large;
"
);
let do_connect = move |_| {
//let _ = set_default_username(&username.read());
let _ = imp::set_default_username(&username.read());
@@ -921,25 +603,25 @@ pub fn LoginView(config: Resource<ClientConfig>) -> Element {
let bottom = match &*status.read() {
Disconnected => rsx! {
button {
class: "{bttn}",
class: "login_bttn",
onclick: do_connect.clone(),
"Connect"
}
},
Connecting => rsx! {
div {
class: "{bttn}",
class: "login_bttn",
"Connecting..."
}
},
Failed(msg) => rsx!(
button {
class: "{bttn}",
class: "login_bttn",
onclick: do_connect.clone(),
"Reconnect"
}
div {
class: "{error}",
class: "login_error",
"Failed to connect:"
pre {
"{msg}"
@@ -950,7 +632,7 @@ pub fn LoginView(config: Resource<ClientConfig>) -> Element {
};
rsx!(
div {
class: "{login_box}",
class: "login",
h1 {
"Mumble Web"
}
@@ -1025,6 +707,8 @@ pub fn LoginView(config: Resource<ClientConfig>) -> Element {
}
pub fn app() -> Element {
static STYLE: Asset = asset!("/assets/main.scss");
use_coroutine(|rx: UnboundedReceiver<Command>| super::network_entrypoint(rx));
let config = use_resource(|| async move {
match imp::load_config().await {
@@ -1033,78 +717,11 @@ pub fn app() -> Element {
}
});
global_css!(
"
:root {
--txt-color: oklch(0.9 0 99);
--bg-color: oklch(0.15 0.01 338.64);
--light-bg-color: oklch(0.25 0.01 338.64);
--login-bg-color: #5d7680;
--primary-btn-color: #7bad9f;
--accent-normal: #7bad9f;
--accent-muted: #ff746c;
--accent-deafened: #464459;
--line-width: 2px;
--line-color: white;
}
body {
margin: 0;
}
#main {
height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-around;
background-color: var(--bg-color);
overflow: auto;
color: var(--txt-color);
font-family: Nunito;
font-size: 15pt;
font-weight: 600;
}
button {
font-weight: bold;
font-size: medium;
border: none;
border-radius: 4px;
color: var(--txt-color);
background-color: var(--primary-btn-color);
cursor: pointer;
}
input {
border: none;
border-radius: 4px;
background-color: white;
color: black;
}
input:focus,input:focus-visible {
border: none;
outline: solid var(--line-width) var(--accent-normal);
outline-offset: -3px;
}
a:link {
color: var(--accent-normal);
}
a:visited {
color: var(--accent-muted);
}
"
);
rsx!(
document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Nunito:ital,wght@0,200..1000;1,200..1000&display=swap" }
document::Link{ rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" }
document::Link{ rel: "stylesheet", href: STYLE }
sir::AppStyle { }
match *STATE.status.read() {
Connected => rsx!(ServerView { config }),
_ => rsx!(LoginView { config }),