Compare commits
3 commits
6568f9fbc8
...
4f611b9544
| Author | SHA1 | Date | |
|---|---|---|---|
| 4f611b9544 | |||
| d925573629 | |||
| a45bf45015 |
14 changed files with 237 additions and 1480 deletions
6
.forgejo/workflows/test.yaml
Normal file
6
.forgejo/workflows/test.yaml
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
on: [push, workflow_dispatch]
|
||||||
|
jobs:
|
||||||
|
test:
|
||||||
|
runs-on: playwright-latest
|
||||||
|
steps:
|
||||||
|
- run: echo All good!
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -2,5 +2,8 @@
|
||||||
e2e/node_modules
|
e2e/node_modules
|
||||||
e2e/playwright-report
|
e2e/playwright-report
|
||||||
e2e/test-results
|
e2e/test-results
|
||||||
some_user.db
|
/some_user.db
|
||||||
dbs
|
/dbs
|
||||||
|
/hashed_static
|
||||||
|
/users.db
|
||||||
|
/.sqlx
|
||||||
|
|
|
||||||
1461
Cargo.lock
generated
1461
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -3,15 +3,13 @@ name = "mascarpone"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[build]
|
|
||||||
rustflags = ["--cfg=sqlx_macros_unstable"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.100"
|
anyhow = "1.0.100"
|
||||||
axum = { version = "0.8.6", features = ["macros", "form"] }
|
axum = { version = "0.8.6", features = ["macros", "form"] }
|
||||||
axum-extra = { version = "0.10.3", features = ["form"] }
|
axum-extra = { version = "0.10.3", features = ["form"] }
|
||||||
axum-htmx = "0.8.1"
|
axum-htmx = "0.8.1"
|
||||||
axum-login = "0.18.0"
|
axum-login = "0.18.0"
|
||||||
|
cache_bust = { version = "0.2.0", features = ["macro"] }
|
||||||
chrono = { version = "0.4.42", features = ["clock", "alloc"] }
|
chrono = { version = "0.4.42", features = ["clock", "alloc"] }
|
||||||
clap = { version = "4.5.53", features = ["derive"] }
|
clap = { version = "4.5.53", features = ["derive"] }
|
||||||
http = "1.3.1"
|
http = "1.3.1"
|
||||||
|
|
@ -38,6 +36,5 @@ tracing = "0.1.41"
|
||||||
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3.20", features = ["env-filter"] }
|
||||||
vcard = "0.4.13"
|
vcard = "0.4.13"
|
||||||
|
|
||||||
[dev-dependencies]
|
[build-dependencies]
|
||||||
cargo-watch = "8.5.3"
|
cache_bust = "0.2.0"
|
||||||
systemfd = "0.4.6"
|
|
||||||
|
|
|
||||||
26
Taskfile
26
Taskfile
|
|
@ -16,13 +16,31 @@ refresh_sqlx_db() {
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_env() {
|
||||||
|
refresh_sqlx_db > /dev/null
|
||||||
|
DATABASE_URL=sqlite:some_user.db \
|
||||||
|
CACHE_BUST_ASSETS_DIR=static \
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
_cargo() {
|
||||||
|
_env cargo "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
edit() {
|
||||||
|
_env nvim
|
||||||
|
}
|
||||||
|
|
||||||
deploy_to_server() {
|
deploy_to_server() {
|
||||||
where="$1"
|
where="$1"
|
||||||
refresh_sqlx_db
|
_cargo build --release
|
||||||
env DATABASE_URL=sqlite:some_user.db cargo build --release
|
|
||||||
rsync -v -essh ./target/release/mascarpone "$where:~" \
|
rsync -v -essh ./target/release/mascarpone "$where:~" \
|
||||||
&& rsync -rav -essh ./static "$where:~" \
|
&& rsync -rav -essh ./hashed_static "$where:~/" \
|
||||||
&& ssh -t "$where" "sudo mv -f mascarpone /usr/bin/ && sudo rm -rf /var/local/mascarpone/static && sudo mv -f static /var/local/mascarpone/ && sudo systemctl restart mascarpone"
|
&& ssh -t "$where" "sudo mv -f mascarpone /usr/bin/ && sudo rm -rf /var/local/mascarpone/hashed_static && sudo mv -f hashed_static /var/local/mascarpone/ && sudo systemctl restart mascarpone"
|
||||||
|
}
|
||||||
|
|
||||||
|
dev() {
|
||||||
|
_cargo run -- serve
|
||||||
}
|
}
|
||||||
|
|
||||||
"$@"
|
"$@"
|
||||||
|
|
|
||||||
12
build.rs
Normal file
12
build.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
use cache_bust::CacheBust;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
println!("cargo:rerun-if-changed=migrations");
|
||||||
|
|
||||||
|
let cache_bust = CacheBust::builder()
|
||||||
|
.in_dir("static".to_owned())
|
||||||
|
.out_dir("hashed_static".to_owned())
|
||||||
|
.build();
|
||||||
|
|
||||||
|
cache_bust.hash_dir().expect("Cache busting failed");
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
[tools]
|
[tools]
|
||||||
"cargo:systemfd" = "latest"
|
|
||||||
"watchexec" = "latest"
|
|
||||||
"rust-analyzer" = "latest"
|
"rust-analyzer" = "latest"
|
||||||
"jj" = "latest"
|
"jj" = "latest"
|
||||||
"pnpm" = "latest"
|
"pnpm" = "latest"
|
||||||
|
|
|
||||||
13
src/main.rs
13
src/main.rs
|
|
@ -182,19 +182,12 @@ async fn serve(port: &u32) -> Result<(), anyhow::Error> {
|
||||||
.route_layer(login_required!(Backend, login_url = "/login"))
|
.route_layer(login_required!(Backend, login_url = "/login"))
|
||||||
.merge(auth::router())
|
.merge(auth::router())
|
||||||
.merge(ics::router())
|
.merge(ics::router())
|
||||||
.nest_service("/static", ServeDir::new("./static"))
|
.nest_service("/static", ServeDir::new("./hashed_static"))
|
||||||
.layer(auth_layer)
|
.layer(auth_layer)
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
let mut listenfd = listenfd::ListenFd::from_env();
|
let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
|
||||||
let listener = match listenfd.take_tcp_listener(0)? {
|
tracing::debug!("Starting axum on 0.0.0.0:{}...", port);
|
||||||
Some(listener) => {
|
|
||||||
listener.set_nonblocking(true)?;
|
|
||||||
TcpListener::from_std(listener)
|
|
||||||
}
|
|
||||||
None => TcpListener::bind(format!("0.0.0.0:{}", port)).await,
|
|
||||||
}?;
|
|
||||||
tracing::debug!("Starting axum on 0.0.0.0:3000...");
|
|
||||||
axum::serve(listener, app)
|
axum::serve(listener, app)
|
||||||
.with_graceful_shutdown(shutdown_signal(deletion_task.abort_handle()))
|
.with_graceful_shutdown(shutdown_signal(deletion_task.abort_handle()))
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use axum::{
|
||||||
response::{IntoResponse, Redirect},
|
response::{IntoResponse, Redirect},
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
};
|
};
|
||||||
|
use cache_bust::asset;
|
||||||
use maud::{DOCTYPE, html};
|
use maud::{DOCTYPE, html};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
|
@ -78,8 +79,8 @@ mod get {
|
||||||
script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js" {}
|
script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js" {}
|
||||||
script src="https://cdn.jsdelivr.net/npm/htmx-ext-response-targets@2.0.4" integrity="sha384-T41oglUPvXLGBVyRdZsVRxNWnOOqCynaPubjUVjxhsjFTKrFJGEMm3/0KGmNQ+Pg" crossorigin="anonymous" {}
|
script src="https://cdn.jsdelivr.net/npm/htmx-ext-response-targets@2.0.4" integrity="sha384-T41oglUPvXLGBVyRdZsVRxNWnOOqCynaPubjUVjxhsjFTKrFJGEMm3/0KGmNQ+Pg" crossorigin="anonymous" {}
|
||||||
script src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/dist/cdn.min.js" defer {}
|
script src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/dist/cdn.min.js" defer {}
|
||||||
link rel="stylesheet" type="text/css" href="/static/index.css";
|
link rel="stylesheet" type="text/css" href=(format!("/static/{}", asset!("index.css")));
|
||||||
link rel="stylesheet" type="text/css" href="/static/login.css";
|
link rel="stylesheet" type="text/css" href=(format!("/static/{}", asset!("login.css")));
|
||||||
title { "Mascarpone" }
|
title { "Mascarpone" }
|
||||||
}
|
}
|
||||||
body hx-ext="response-targets" {
|
body hx-ext="response-targets" {
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use axum::{
|
||||||
routing::{delete, get, post, put},
|
routing::{delete, get, post, put},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::Form;
|
use axum_extra::extract::Form;
|
||||||
|
use cache_bust::asset;
|
||||||
use chrono::DateTime;
|
use chrono::DateTime;
|
||||||
use maud::{Markup, PreEscaped, html};
|
use maud::{Markup, PreEscaped, html};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
@ -19,7 +20,7 @@ use crate::models::user::AuthSession;
|
||||||
use crate::models::{HydratedContact, JournalEntry};
|
use crate::models::{HydratedContact, JournalEntry};
|
||||||
use crate::{AppError, AppState};
|
use crate::{AppError, AppState};
|
||||||
|
|
||||||
#[derive(serde::Serialize)]
|
#[derive(serde::Serialize, Debug)]
|
||||||
pub struct Address {
|
pub struct Address {
|
||||||
pub id: DbId,
|
pub id: DbId,
|
||||||
pub contact_id: DbId,
|
pub contact_id: DbId,
|
||||||
|
|
@ -100,7 +101,7 @@ mod get {
|
||||||
.text_body;
|
.text_body;
|
||||||
|
|
||||||
Ok(layout.render(
|
Ok(layout.render(
|
||||||
Some(vec!["/static/contact.css", "/static/journal.css"]),
|
Some(vec![asset!("contact.css"), asset!("journal.css")]),
|
||||||
html! {
|
html! {
|
||||||
a href=(format!("/contact/{}/edit", contact_id)) { "Edit" }
|
a href=(format!("/contact/{}/edit", contact_id)) { "Edit" }
|
||||||
|
|
||||||
|
|
@ -210,7 +211,7 @@ mod get {
|
||||||
.text_body
|
.text_body
|
||||||
.unwrap_or(String::new());
|
.unwrap_or(String::new());
|
||||||
|
|
||||||
Ok(layout.render(Some(vec!["/static/contact.css"]), html! {
|
Ok(layout.render(Some(vec![asset!("contact.css")]), html! {
|
||||||
form hx-ext="response-targets" {
|
form hx-ext="response-targets" {
|
||||||
div {
|
div {
|
||||||
input type="button" value="Save" hx-put=(cid_url) hx-target-error="#error";
|
input type="button" value="Save" hx-put=(cid_url) hx-target-error="#error";
|
||||||
|
|
@ -248,24 +249,31 @@ mod get {
|
||||||
label { "addresses" }
|
label { "addresses" }
|
||||||
div x-data=(json!({ "addresses": addresses, "new_label": "", "new_address": "" })) {
|
div x-data=(json!({ "addresses": addresses, "new_label": "", "new_address": "" })) {
|
||||||
template x-for="(address, index) in addresses" x-bind:key="index" {
|
template x-for="(address, index) in addresses" x-bind:key="index" {
|
||||||
div {
|
.address-input {
|
||||||
input name="address_label" x-show="addresses.length > 1" x-model="address.label" placeholder="label";
|
input name="address_label" x-show="addresses.length" x-model="address.label" placeholder="label";
|
||||||
input name="address_value" x-model="address.value" placeholder="address";
|
.grow-wrap x-bind:data-replicated-value="address.value" {
|
||||||
|
textarea name="address_value" x-model="address.value" placeholder="address" {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
div {
|
}
|
||||||
input x-show="addresses.length > 1" name="address_label" x-model="new_label" placeholder="label";
|
.address-input {
|
||||||
input name="address_value" x-model="new_address" placeholder="new address";
|
input x-show="addresses.length" name="address_label" x-model="new_label" placeholder="label";
|
||||||
|
.grow-wrap x-bind:data-replicated-value="new_address" {
|
||||||
|
textarea name="address_value" x-model="new_address" placeholder="new address" {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
input type="button" value="Add" x-on:click="addresses.push({ label: new_label, value: new_address }); new_label = ''; new_address = ''";
|
input type="button" value="Add" x-on:click="addresses.push({ label: new_label, value: new_address }); new_label = ''; new_address = ''";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
div #text_body {
|
||||||
|
div { "Free text (supports markdown)" }
|
||||||
.grow-wrap data-replicated-value=(text_body) {
|
.grow-wrap data-replicated-value=(text_body) {
|
||||||
textarea name="text_body"
|
textarea name="text_body"
|
||||||
onInput="this.parentNode.dataset.replicatedValue = this.value"
|
onInput="this.parentNode.dataset.replicatedValue = this.value"
|
||||||
{ (text_body) }
|
{ (text_body) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
|
use cache_bust::asset;
|
||||||
use chrono::{Local, NaiveDate, TimeDelta};
|
use chrono::{Local, NaiveDate, TimeDelta};
|
||||||
use maud::{Markup, html};
|
use maud::{Markup, html};
|
||||||
use sqlx::sqlite::SqlitePool;
|
use sqlx::sqlite::SqlitePool;
|
||||||
|
|
@ -230,7 +231,7 @@ pub mod get {
|
||||||
.fetch_all(pool)
|
.fetch_all(pool)
|
||||||
.await?;
|
.await?;
|
||||||
Ok(layout.render(
|
Ok(layout.render(
|
||||||
Some(vec!["/static/home.css", "/static/journal.css"]),
|
Some(vec![asset!("home.css"), asset!("journal.css")]),
|
||||||
html! {
|
html! {
|
||||||
(freshness_section(&freshens)?)
|
(freshness_section(&freshens)?)
|
||||||
(birthdays_section(&prev_birthdays, &upcoming_birthdays)?)
|
(birthdays_section(&prev_birthdays, &upcoming_birthdays)?)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use axum::RequestPartsExt;
|
use axum::RequestPartsExt;
|
||||||
use axum::extract::FromRequestParts;
|
use axum::extract::FromRequestParts;
|
||||||
// use axum::response::{IntoResponse, Redirect};
|
use cache_bust::asset;
|
||||||
use http::request::Parts;
|
use http::request::Parts;
|
||||||
use maud::{DOCTYPE, Markup, html};
|
use maud::{DOCTYPE, Markup, html};
|
||||||
use sqlx::FromRow;
|
use sqlx::FromRow;
|
||||||
|
|
@ -62,14 +62,14 @@ impl Layout {
|
||||||
(DOCTYPE)
|
(DOCTYPE)
|
||||||
html {
|
html {
|
||||||
head {
|
head {
|
||||||
link rel="stylesheet" type="text/css" href="/static/index.css";
|
link rel="stylesheet" type="text/css" href=(format!("/static/{}", asset!("index.css")));
|
||||||
meta name="viewport" content="width=device-width";
|
meta name="viewport" content="width=device-width";
|
||||||
script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js" {}
|
script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js" {}
|
||||||
script src="https://cdn.jsdelivr.net/npm/htmx-ext-response-targets@2.0.4" integrity="sha384-T41oglUPvXLGBVyRdZsVRxNWnOOqCynaPubjUVjxhsjFTKrFJGEMm3/0KGmNQ+Pg" crossorigin="anonymous" {}
|
script src="https://cdn.jsdelivr.net/npm/htmx-ext-response-targets@2.0.4" integrity="sha384-T41oglUPvXLGBVyRdZsVRxNWnOOqCynaPubjUVjxhsjFTKrFJGEMm3/0KGmNQ+Pg" crossorigin="anonymous" {}
|
||||||
script src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/dist/cdn.min.js" defer {}
|
script src="https://cdn.jsdelivr.net/npm/alpinejs@3.15.0/dist/cdn.min.js" defer {}
|
||||||
@if let Some(hrefs) = css {
|
@if let Some(hrefs) = css {
|
||||||
@for href in hrefs {
|
@for href in hrefs {
|
||||||
link rel="stylesheet" type="text/css" href=(href);
|
link rel="stylesheet" type="text/css" href=(format!("/static/{}", href));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use axum::{
|
||||||
routing::{delete, get, post, put},
|
routing::{delete, get, post, put},
|
||||||
};
|
};
|
||||||
use axum_extra::extract::Form;
|
use axum_extra::extract::Form;
|
||||||
|
use cache_bust::asset;
|
||||||
use maud::{Markup, html};
|
use maud::{Markup, html};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
@ -61,7 +62,7 @@ mod get {
|
||||||
let ics_path: Option<String> = ics_path.0;
|
let ics_path: Option<String> = ics_path.0;
|
||||||
|
|
||||||
Ok(layout.render(
|
Ok(layout.render(
|
||||||
Some(vec!["static/settings.css"]),
|
Some(vec![asset!("settings.css")]),
|
||||||
html! {
|
html! {
|
||||||
h2 { "Birthdays Calendar URL" }
|
h2 { "Birthdays Calendar URL" }
|
||||||
(calendar_link(ics_path))
|
(calendar_link(ics_path))
|
||||||
|
|
|
||||||
|
|
@ -33,19 +33,28 @@ main {
|
||||||
grid-template-columns: min-content auto;
|
grid-template-columns: min-content auto;
|
||||||
row-gap: 0.5em;
|
row-gap: 0.5em;
|
||||||
|
|
||||||
.label {
|
.label[data-is-empty="false"] {
|
||||||
color: var(--line-color);
|
color: var(--line-color);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
margin-right: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value {
|
||||||
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#text_body {
|
#text_body {
|
||||||
margin-top: 1em;
|
margin-top: 1em;
|
||||||
|
|
||||||
p+p {
|
p {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p:first-child {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
em {
|
em {
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
}
|
}
|
||||||
|
|
@ -53,6 +62,40 @@ main {
|
||||||
strong {
|
strong {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
list-style: disc inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-block: 0.83em;
|
||||||
|
font-size: 1.50em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-block: 1.00em;
|
||||||
|
font-size: 1.17em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin-block: 1.33em;
|
||||||
|
font-size: 1.00em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
margin-block: 1.67em;
|
||||||
|
font-size: 0.83em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
margin-block: 2.33em;
|
||||||
|
font-size: 0.67em;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
padding: 0.1em 0 0.1em 0.5em;
|
||||||
|
border-left: 2px solid var(--line-color);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.grow-wrap {
|
.grow-wrap {
|
||||||
|
|
@ -72,14 +115,35 @@ main {
|
||||||
&>textarea,
|
&>textarea,
|
||||||
&::after {
|
&::after {
|
||||||
/* Identical styling required!! */
|
/* Identical styling required!! */
|
||||||
margin-top: 1em;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 12em;
|
||||||
|
font: inherit;
|
||||||
|
border: 1px solid gray;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 2px 0;
|
||||||
|
padding: 0.25em;
|
||||||
|
|
||||||
/* Place on top of each other */
|
/* Place on top of each other */
|
||||||
grid-area: 1 / 1 / 2 / 2;
|
grid-area: 1 / 1 / 2 / 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media only screen and (max-width: 650px) {
|
||||||
|
.address-input+.address-input {
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#text_body {
|
||||||
|
margin-top: 1em;
|
||||||
|
|
||||||
|
.grow-wrap>textarea,
|
||||||
|
.grow-wrap::after {
|
||||||
|
/* Identical styling required!! */
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
font-size: small;
|
font-size: small;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue