From d89ff40d33bf26dcc5e3752e283806f293a8ecdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gabor=20K=C3=B6rber?= Date: Tue, 19 Dec 2023 21:53:35 +0100 Subject: [PATCH] making admin work with a state, however it probably should go into a middleware instead. --- Cargo.lock | 1 + Cargo.toml | 6 +- Justfile | 2 +- src/admin/domain.rs | 35 + src/admin/mod.rs | 13 + src/admin/state.rs | 158 ++ src/admin/views.rs | 171 ++ src/lib.rs | 1 + src/main.rs | 12 +- src/service/templates.rs | 26 + src/state.rs | 10 + static/django_admin/css/combined.css | 2633 ++++++++++++++++++++++++++ templates/admin/app_list.html | 38 + templates/admin/base.html | 127 ++ templates/admin/index.html | 1 + templates/admin/nav_sidebar.html | 9 + 16 files changed, 3240 insertions(+), 3 deletions(-) create mode 100644 src/admin/domain.rs create mode 100644 src/admin/mod.rs create mode 100644 src/admin/state.rs create mode 100644 src/admin/views.rs create mode 100644 static/django_admin/css/combined.css create mode 100644 templates/admin/app_list.html create mode 100644 templates/admin/base.html create mode 100644 templates/admin/index.html create mode 100644 templates/admin/nav_sidebar.html diff --git a/Cargo.lock b/Cargo.lock index 06d0e9c..8fad05c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -541,6 +541,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "80084fa3099f58b7afab51e5f92e24c2c2c68dcad26e96ad104bd6011570461d" dependencies = [ "memo-map", + "percent-encoding", "self_cell", "serde", ] diff --git a/Cargo.toml b/Cargo.toml index bbf8934..02b6481 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,11 @@ barrel = { version = "0.7.0", optional = true, features = ["pg"] } diesel = { version = "2.1.3", features = ["serde_json", "postgres"] } dotenvy = "0.15.7" mime_guess = "2.0.4" -minijinja = { version = "1.0.8", features = ["loader"] } +minijinja = { version = "1.0.8", features = [ + "loader", + "builtins", + "urlencode", +] } minijinja-autoreload = "1.0.8" once_cell = "1.18.0" pulldown-cmark = "0.9.3" diff --git a/Justfile b/Justfile index f3a8f66..b87c256 100644 --- a/Justfile +++ b/Justfile @@ -2,7 +2,7 @@ default: @just hello run: - @cargo run main + @cargo run --bin miniweb bin args='': @cargo run --bin {{args}} diff --git a/src/admin/domain.rs b/src/admin/domain.rs new file mode 100644 index 0000000..30d8bc3 --- /dev/null +++ b/src/admin/domain.rs @@ -0,0 +1,35 @@ +pub use dto::AdminApp; +pub use dto::AdminModel; + +mod auth { + + struct AdminUser {} + + struct AdminRole {} + + struct AdminGroup {} + + struct AdminActionLog {} +} + +mod dto { + use serde::{Deserialize, Serialize}; + + #[derive(Deserialize, Serialize)] + pub struct AdminModel { + pub name: String, + pub object_name: String, + pub admin_url: String, + pub view_only: bool, + + pub add_url: Option, + } + + #[derive(Deserialize, Serialize)] + pub struct AdminApp { + pub name: String, + pub app_label: String, + pub app_url: String, + pub models: Vec, + } +} diff --git a/src/admin/mod.rs b/src/admin/mod.rs new file mode 100644 index 0000000..e025d5e --- /dev/null +++ b/src/admin/mod.rs @@ -0,0 +1,13 @@ +use axum::{routing::get, Router}; + +use crate::state::AppState; + +pub mod domain; +pub mod state; +pub mod views; + +/* +pub fn routes() -> Router { + Router::new().route("/", get(views::index).post(views::index_action)) +} +*/ diff --git a/src/admin/state.rs b/src/admin/state.rs new file mode 100644 index 0000000..7d79231 --- /dev/null +++ b/src/admin/state.rs @@ -0,0 +1,158 @@ +use crate::admin::domain::{AdminApp, AdminModel}; +use axum::routing::MethodRouter; +use axum::{async_trait, response::IntoResponse}; +use axum::{routing::get, Router}; +use core::future::Future; +use std::collections::HashMap; +use std::sync::Arc; + +pub type AdminState = Arc; + +// main registry. +#[derive(Clone)] +pub struct AdminRegistry { + base_path: String, + apps: HashMap, + models: HashMap, +} + +impl AdminRegistry { + pub fn new(base_path: &str) -> Self { + AdminRegistry { + base_path: base_path.to_owned(), + apps: HashMap::new(), + models: HashMap::new(), + } + } + + pub fn generate_router(&self) -> Router + where + T: Clone, + T: Send, + T: Sync, + T: 'static, + crate::service::templates::Templates: axum::extract::FromRef, + AdminState: axum::extract::FromRef, + { + routing::generate_routes::(&self) + } + + pub fn get_apps(&self) -> Vec { + self.apps + .iter() + .map(|(key, node)| self.get_app(key, node)) + .collect() + } + + fn get_app(&self, name: &str, node: &internal::DataNode) -> AdminApp { + let label = node.label.as_ref().unwrap_or(&String::new()).clone(); + AdminApp { + name: name.to_owned(), + app_label: label, + app_url: "".to_owned(), + models: vec![], + } + } + + pub fn register_app(&mut self, name: &str, app_label: &str, app_url: &str) { + self.apps.insert( + name.to_owned(), + internal::DataNode { + object_name: name.to_owned(), + label: Some(app_label.to_owned()), + }, + ); + } + + pub fn get_models(&self) -> Vec { + vec![] + } + + pub fn get_models_for_app(&self, name: &str) -> Vec { + vec![] + } + + pub fn register_model(&mut self, name: &str, config: AdminModelConfig) {} +} + +// user uses this configuration object to register another model. +pub struct AdminModelConfig { + pub app: Arc, + pub custom_index_handler: Option, +} + +mod internal { + #[derive(Clone)] + pub struct DataNode { + pub object_name: String, + pub label: Option, + } +} + +mod routing { + use axum::{routing::get, Router}; + + pub fn generate_routes(registry: &super::AdminRegistry) -> Router + where + T: Clone, + T: Send, + T: Sync, + T: 'static, + crate::service::templates::Templates: axum::extract::FromRef, + super::AdminState: axum::extract::FromRef, + { + let mut router: Router = Router::new(); + let apps = registry.get_apps(); + router = router.route( + &format!("/{}", registry.base_path), + get(crate::admin::views::index), + ); + for app in apps { + let app_route = format!("/{}/{}", registry.base_path, app.app_url); + + // Add a route for the app + router = router.route( + &app_route, + get(move || async move { format!("Admin panel for {}", app.app_label) }), + ); + + // Add routes for each model in the app + /*for model in data_node.models.iter() { + let model_route = format!("{}/{}", app_route, model.name); + router = router.route( + &model_route, + get(move || async move { format!("Model page for {}", model.name) }), + ); + }*/ + } + + router + } + + fn define_example_data(registry: &mut super::AdminRegistry) { + /*let models = vec![ + AdminModel { + name: "User".to_owned(), + object_name: "User".to_owned(), + admin_url: "/admin/users/user".to_owned(), + view_only: false, + add_url: Some("/admin/users/user/add".to_owned()), + }, + AdminModel { + name: "Group".to_owned(), + object_name: "Group".to_owned(), + admin_url: "/admin/users/group".to_owned(), + view_only: false, + add_url: None, + }, + AdminModel { + name: "Permission".to_owned(), + object_name: "Permission".to_owned(), + admin_url: "/admin/users/permission".to_owned(), + view_only: true, + add_url: None, + }, + ];*/ + registry.register_app("auth", "Authorities", "auth"); + } +} diff --git a/src/admin/views.rs b/src/admin/views.rs new file mode 100644 index 0000000..261a537 --- /dev/null +++ b/src/admin/views.rs @@ -0,0 +1,171 @@ +use std::sync::Arc; + +use axum::{extract::State, response::IntoResponse, Form}; + +use crate::admin::domain::{AdminApp, AdminModel}; +use crate::admin::state; +use crate::service::templates; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize)] +pub struct Question { + question: String, +} + +#[derive(Deserialize)] +pub struct ExampleData { + title: String, + content: String, +} + +#[derive(Serialize, Deserialize)] +pub struct AdminRequest { + pub path: String, +} + +#[derive(Deserialize, Serialize)] +pub struct AdminContext { + pub language_code: Option, + pub language_bidi: Option, + pub user: Option, // Todo: user type + pub site_url: Option, + pub docsroot: Option, + pub messages: Vec, // Todo: message type + pub title: Option, + pub subtitle: Option, + pub content: String, + + pub request: AdminRequest, + pub available_apps: Vec, +} + +impl Default for AdminContext { + fn default() -> Self { + AdminContext { + language_code: Some("en-us".to_string()), // Default language code + language_bidi: Some(false), // Default language bidi + user: None, //UserType::default(), // Assuming UserType has a Default impl + site_url: None, + docsroot: None, + messages: Vec::new(), // Empty vector for messages + title: None, + subtitle: None, + content: String::new(), // Empty string for content + available_apps: Vec::new(), + request: AdminRequest { + path: "".to_owned(), + }, + } + } +} + +// Index is the entry point of Admin. It should display the Dashboard to a logged in user. +pub async fn index_example(templates: State) -> impl IntoResponse { + let models = vec![ + AdminModel { + name: "User".to_owned(), + object_name: "User".to_owned(), + admin_url: "/admin/users/user".to_owned(), + view_only: false, + add_url: Some("/admin/users/user/add".to_owned()), + }, + AdminModel { + name: "Group".to_owned(), + object_name: "Group".to_owned(), + admin_url: "/admin/users/group".to_owned(), + view_only: false, + add_url: None, + }, + AdminModel { + name: "Permission".to_owned(), + object_name: "Permission".to_owned(), + admin_url: "/admin/users/permission".to_owned(), + view_only: true, + add_url: None, + }, + ]; + let core_app = AdminApp { + name: "Admin".to_owned(), + app_label: "admin".to_owned(), + app_url: "/admin/users/".to_owned(), + models: models, + }; + templates.render_html( + "admin/index.html", + AdminContext { + available_apps: vec![core_app], + ..Default::default() + }, + ) +} + +pub async fn index( + templates: State, + administration: State>, +) -> impl IntoResponse { + let models = vec![ + AdminModel { + name: "User".to_owned(), + object_name: "User".to_owned(), + admin_url: "/admin/users/user".to_owned(), + view_only: false, + add_url: Some("/admin/users/user/add".to_owned()), + }, + AdminModel { + name: "Group".to_owned(), + object_name: "Group".to_owned(), + admin_url: "/admin/users/group".to_owned(), + view_only: false, + add_url: None, + }, + AdminModel { + name: "Permission".to_owned(), + object_name: "Permission".to_owned(), + admin_url: "/admin/users/permission".to_owned(), + view_only: true, + add_url: None, + }, + ]; + let core_app = AdminApp { + name: "Admin".to_owned(), + app_label: "admin".to_owned(), + app_url: "/admin/users/".to_owned(), + models: models, + }; + + let mut available = vec![core_app]; + available.extend(administration.get_apps()); + + templates.render_html( + "admin/index.html", + AdminContext { + available_apps: available, + ..Default::default() + }, + ) +} + +// Index Action is POST to the index site. We can anchor some general business code here. +pub async fn index_action(Form(example_data): Form) -> impl IntoResponse { + "There is your answer!".to_owned() +} + +// List Items renders the entire list item page. +pub async fn list_item_collection(templates: State) -> impl IntoResponse { + templates.render_html("admin/list_items.html", ()) +} + +// Items Action is a POST to an item list. By default these are actions, that work on a list of items as input. +pub async fn item_collection_action(Form(question): Form) -> impl IntoResponse { + "There is your answer!".to_owned() +} + +// Item Details shows one single dataset. +pub async fn item_details(templates: State) -> impl IntoResponse { + templates.render_html("admin/item_detail.html", ()) +} + +// Item Action allows running an action on one single dataset. +pub async fn item_action(Form(question): Form) -> impl IntoResponse { + "There is your answer!".to_owned() +} diff --git a/src/lib.rs b/src/lib.rs index 60473e7..daf967a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod admin; pub mod schema; pub mod service; pub mod state; diff --git a/src/main.rs b/src/main.rs index 80272ad..7a08afa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +mod admin; mod howto; mod service; mod state; @@ -8,6 +9,7 @@ use axum::{ extract::State, handler::HandlerWithoutStateExt, response::IntoResponse, routing::get, Router, }; use std::net::SocketAddr; +use std::sync::Arc; async fn home(templates: State) -> impl IntoResponse { templates.render_html("index.html", ()) @@ -21,12 +23,20 @@ async fn hello_world(templates: State) -> impl IntoRespons async fn main() { // Prepare App State let tmpl = templates::Templates::initialize().expect("Template Engine could not be loaded."); - let state: AppState = AppState { templates: tmpl }; + let mut admin = admin::state::AdminRegistry::new("admin"); + admin.register_app("auth", "Authorities", "auth"); + let admin_router = admin.generate_router(); + + let state: AppState = AppState { + templates: tmpl, + admin: Arc::new(admin), + }; // Application Route let app = Router::new() .route("/", get(home)) .route("/hello", get(hello_world)) + .merge(admin_router) .nest("/howto", howto::routes()) .route_service("/static/*file", handlers::static_handler.into_service()) .fallback(handlers::not_found_handler) diff --git a/src/service/templates.rs b/src/service/templates.rs index e5e4299..7994b11 100644 --- a/src/service/templates.rs +++ b/src/service/templates.rs @@ -17,6 +17,12 @@ impl Templates { let template_path = "templates"; environment.set_loader(path_loader(&template_path)); environment.add_filter("markdown", markdown); + environment.add_filter("yesno", filter_yesno); + environment.add_function("static", tpl_static); + environment.add_function("url", tpl_url); + environment.add_function("csrf_token", tpl_to_be_implemented); + environment.add_function("translate", tpl_translate); + notifier.watch_path(template_path, true); Ok(environment) }); @@ -58,3 +64,23 @@ fn markdown(value: String) -> Value { pulldown_cmark::html::push_html(&mut html, parser); Value::from_safe_string(html) } + +fn filter_yesno(value: String, yesno: String) -> String { + "".into() +} + +fn tpl_url(value: String) -> Value { + Value::from_safe_string(value.into()) +} + +fn tpl_translate(value: String) -> Value { + Value::from_safe_string(value.into()) +} + +fn tpl_to_be_implemented(value: String) -> Value { + Value::from_safe_string("".into()) +} + +fn tpl_static(value: String) -> Value { + Value::from_safe_string("".into()) +} diff --git a/src/state.rs b/src/state.rs index 3c640e7..234f123 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,10 +1,14 @@ +use std::sync::Arc; + use axum::extract::FromRef; +use crate::admin::state::AdminRegistry; use crate::service::templates; #[derive(Clone)] pub struct AppState { pub templates: templates::Templates, + pub admin: Arc, } impl FromRef for templates::Templates { @@ -12,3 +16,9 @@ impl FromRef for templates::Templates { app_state.templates.clone() } } + +impl FromRef for Arc { + fn from_ref(app_state: &AppState) -> Arc { + app_state.admin.clone() + } +} diff --git a/static/django_admin/css/combined.css b/static/django_admin/css/combined.css new file mode 100644 index 0000000..5165d92 --- /dev/null +++ b/static/django_admin/css/combined.css @@ -0,0 +1,2633 @@ +/* + DJANGO Admin styles +*/ + +/* VARIABLE DEFINITIONS */ +html[data-theme="light"], +:root { + --primary: #79aec8; + --secondary: #417690; + --accent: #f5dd5d; + --primary-fg: #fff; + + --body-fg: #333; + --body-bg: #fff; + --body-quiet-color: #666; + --body-loud-color: #000; + + --header-color: #ffc; + --header-branding-color: var(--accent); + --header-bg: var(--secondary); + --header-link-color: var(--primary-fg); + + --breadcrumbs-fg: #c4dce8; + --breadcrumbs-link-fg: var(--body-bg); + --breadcrumbs-bg: #264b5d; + + --link-fg: #417893; + --link-hover-color: #036; + --link-selected-fg: var(--secondary); + + --hairline-color: #e8e8e8; + --border-color: #ccc; + + --error-fg: #ba2121; + + --message-success-bg: #dfd; + --message-warning-bg: #ffc; + --message-error-bg: #ffefef; + + --darkened-bg: #f8f8f8; + /* A bit darker than --body-bg */ + --selected-bg: #e4e4e4; + /* E.g. selected table cells */ + --selected-row: #ffc; + + --button-fg: #fff; + --button-bg: var(--secondary); + --button-hover-bg: #205067; + --default-button-bg: #205067; + --default-button-hover-bg: var(--secondary); + --close-button-bg: #747474; + --close-button-hover-bg: #333; + --delete-button-bg: #ba2121; + --delete-button-hover-bg: #a41515; + + --object-tools-fg: var(--button-fg); + --object-tools-bg: var(--close-button-bg); + --object-tools-hover-bg: var(--close-button-hover-bg); + + --font-family-primary: + "Segoe UI", + system-ui, + Roboto, + "Helvetica Neue", + Arial, + sans-serif, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji"; + --font-family-monospace: + ui-monospace, + Menlo, + Monaco, + "Cascadia Mono", + "Segoe UI Mono", + "Roboto Mono", + "Oxygen Mono", + "Ubuntu Monospace", + "Source Code Pro", + "Fira Mono", + "Droid Sans Mono", + "Courier New", + monospace, + "Apple Color Emoji", + "Segoe UI Emoji", + "Segoe UI Symbol", + "Noto Color Emoji"; +} + +html, +body { + height: 100%; +} + +body { + margin: 0; + padding: 0; + font-size: 0.875rem; + font-family: var(--font-family-primary); + color: var(--body-fg); + background: var(--body-bg); +} + +/* LINKS */ + +a:link, +a:visited { + color: var(--body-fg); + text-decoration: none; + transition: color 0.15s, background 0.15s; +} + +a:focus, +a:hover { + color: var(--link-hover-color); +} + +a:focus { + text-decoration: underline; +} + +a img { + border: none; +} + +a.section:link, +a.section:visited { + color: var(--header-link-color); + text-decoration: none; +} + +a.section:focus, +a.section:hover { + text-decoration: underline; +} + +/* GLOBAL DEFAULTS */ + +p, +ol, +ul, +dl { + margin: .2em 0 .8em 0; +} + +p { + padding: 0; + line-height: 140%; +} + +h1, +h2, +h3, +h4, +h5 { + font-weight: bold; +} + +h1 { + margin: 0 0 20px; + font-weight: 300; + font-size: 1.25rem; + color: var(--body-quiet-color); +} + +h2 { + font-size: 1rem; + margin: 1em 0 .5em 0; +} + +h2.subhead { + font-weight: normal; + margin-top: 0; +} + +h3 { + font-size: 0.875rem; + margin: .8em 0 .3em 0; + color: var(--body-quiet-color); + font-weight: bold; +} + +h4 { + font-size: 0.75rem; + margin: 1em 0 .8em 0; + padding-bottom: 3px; +} + +h5 { + font-size: 0.625rem; + margin: 1.5em 0 .5em 0; + color: var(--body-quiet-color); + text-transform: uppercase; + letter-spacing: 1px; +} + +ul>li { + list-style-type: square; + padding: 1px 0; +} + +li ul { + margin-bottom: 0; +} + +li, +dt, +dd { + font-size: 0.8125rem; + line-height: 1.25rem; +} + +dt { + font-weight: bold; + margin-top: 4px; +} + +dd { + margin-left: 0; +} + +form { + margin: 0; + padding: 0; +} + +fieldset { + margin: 0; + min-width: 0; + padding: 0; + border: none; + border-top: 1px solid var(--hairline-color); +} + +blockquote { + font-size: 0.6875rem; + color: #777; + margin-left: 2px; + padding-left: 10px; + border-left: 5px solid #ddd; +} + +code, +pre { + font-family: var(--font-family-monospace); + color: var(--body-quiet-color); + font-size: 0.75rem; + overflow-x: auto; +} + +pre.literal-block { + margin: 10px; + background: var(--darkened-bg); + padding: 6px 8px; +} + +code strong { + color: #930; +} + +hr { + clear: both; + color: var(--hairline-color); + background-color: var(--hairline-color); + height: 1px; + border: none; + margin: 0; + padding: 0; + line-height: 1px; +} + +/* TEXT STYLES & MODIFIERS */ + +.small { + font-size: 0.6875rem; +} + +.mini { + font-size: 0.625rem; +} + +.help, +p.help, +form p.help, +div.help, +form div.help, +div.help li { + font-size: 0.6875rem; + color: var(--body-quiet-color); +} + +div.help ul { + margin-bottom: 0; +} + +.help-tooltip { + cursor: help; +} + +p img, +h1 img, +h2 img, +h3 img, +h4 img, +td img { + vertical-align: middle; +} + +.quiet, +a.quiet:link, +a.quiet:visited { + color: var(--body-quiet-color); + font-weight: normal; +} + +.clear { + clear: both; +} + +.nowrap { + white-space: nowrap; +} + +.hidden { + display: none !important; +} + +/* TABLES */ + +table { + border-collapse: collapse; + border-color: var(--border-color); +} + +td, +th { + font-size: 0.8125rem; + line-height: 1rem; + border-bottom: 1px solid var(--hairline-color); + vertical-align: top; + padding: 8px; +} + +th { + font-weight: 600; + text-align: left; +} + +thead th, +tfoot td { + color: var(--body-quiet-color); + padding: 5px 10px; + font-size: 0.6875rem; + background: var(--body-bg); + border: none; + border-top: 1px solid var(--hairline-color); + border-bottom: 1px solid var(--hairline-color); +} + +tfoot td { + border-bottom: none; + border-top: 1px solid var(--hairline-color); +} + +thead th.required { + color: var(--body-loud-color); +} + +tr.alt { + background: var(--darkened-bg); +} + +tr:nth-child(odd), +.row-form-errors { + background: var(--body-bg); +} + +tr:nth-child(even), +tr:nth-child(even) .errorlist, +tr:nth-child(odd)+.row-form-errors, +tr:nth-child(odd)+.row-form-errors .errorlist { + background: var(--darkened-bg); +} + +/* SORTABLE TABLES */ + +thead th { + padding: 5px 10px; + line-height: normal; + text-transform: uppercase; + background: var(--darkened-bg); +} + +thead th a:link, +thead th a:visited { + color: var(--body-quiet-color); +} + +thead th.sorted { + background: var(--selected-bg); +} + +thead th.sorted .text { + padding-right: 42px; +} + +table thead th .text span { + padding: 8px 10px; + display: block; +} + +table thead th .text a { + display: block; + cursor: pointer; + padding: 8px 10px; +} + +table thead th .text a:focus, +table thead th .text a:hover { + background: var(--selected-bg); +} + +thead th.sorted a.sortremove { + visibility: hidden; +} + +table thead th.sorted:hover a.sortremove { + visibility: visible; +} + +table thead th.sorted .sortoptions { + display: block; + padding: 9px 5px 0 5px; + float: right; + text-align: right; +} + +table thead th.sorted .sortpriority { + font-size: .8em; + min-width: 12px; + text-align: center; + vertical-align: 3px; + margin-left: 2px; + margin-right: 2px; +} + +table thead th.sorted .sortoptions a { + position: relative; + width: 14px; + height: 14px; + display: inline-block; + background: url(../img/sorting-icons.svg) 0 0 no-repeat; + background-size: 14px auto; +} + +table thead th.sorted .sortoptions a.sortremove { + background-position: 0 0; +} + +table thead th.sorted .sortoptions a.sortremove:after { + content: '\\'; + position: absolute; + top: -6px; + left: 3px; + font-weight: 200; + font-size: 1.125rem; + color: var(--body-quiet-color); +} + +table thead th.sorted .sortoptions a.sortremove:focus:after, +table thead th.sorted .sortoptions a.sortremove:hover:after { + color: var(--link-fg); +} + +table thead th.sorted .sortoptions a.sortremove:focus, +table thead th.sorted .sortoptions a.sortremove:hover { + background-position: 0 -14px; +} + +table thead th.sorted .sortoptions a.ascending { + background-position: 0 -28px; +} + +table thead th.sorted .sortoptions a.ascending:focus, +table thead th.sorted .sortoptions a.ascending:hover { + background-position: 0 -42px; +} + +table thead th.sorted .sortoptions a.descending { + top: 1px; + background-position: 0 -56px; +} + +table thead th.sorted .sortoptions a.descending:focus, +table thead th.sorted .sortoptions a.descending:hover { + background-position: 0 -70px; +} + +/* FORM DEFAULTS */ + +input, +textarea, +select, +.form-row p, +form .button { + margin: 2px 0; + padding: 2px 3px; + vertical-align: middle; + font-family: var(--font-family-primary); + font-weight: normal; + font-size: 0.8125rem; +} + +.form-row div.help { + padding: 2px 3px; +} + +textarea { + vertical-align: top; +} + +input[type=text], +input[type=password], +input[type=email], +input[type=url], +input[type=number], +input[type=tel], +textarea, +select, +.vTextField { + border: 1px solid var(--border-color); + border-radius: 4px; + padding: 5px 6px; + margin-top: 0; + color: var(--body-fg); + background-color: var(--body-bg); +} + +input[type=text]:focus, +input[type=password]:focus, +input[type=email]:focus, +input[type=url]:focus, +input[type=number]:focus, +input[type=tel]:focus, +textarea:focus, +select:focus, +.vTextField:focus { + border-color: var(--body-quiet-color); +} + +select { + height: 1.875rem; +} + +select[multiple] { + /* Allow HTML size attribute to override the height in the rule above. */ + height: auto; + min-height: 150px; +} + +/* FORM BUTTONS */ + +.button, +input[type=submit], +input[type=button], +.submit-row input, +a.button { + background: var(--button-bg); + padding: 10px 15px; + border: none; + border-radius: 4px; + color: var(--button-fg); + cursor: pointer; + transition: background 0.15s; +} + +a.button { + padding: 4px 5px; +} + +.button:active, +input[type=submit]:active, +input[type=button]:active, +.button:focus, +input[type=submit]:focus, +input[type=button]:focus, +.button:hover, +input[type=submit]:hover, +input[type=button]:hover { + background: var(--button-hover-bg); +} + +.button[disabled], +input[type=submit][disabled], +input[type=button][disabled] { + opacity: 0.4; +} + +.button.default, +input[type=submit].default, +.submit-row input.default { + border: none; + font-weight: 400; + background: var(--default-button-bg); +} + +.button.default:active, +input[type=submit].default:active, +.button.default:focus, +input[type=submit].default:focus, +.button.default:hover, +input[type=submit].default:hover { + background: var(--default-button-hover-bg); +} + +.button[disabled].default, +input[type=submit][disabled].default, +input[type=button][disabled].default { + opacity: 0.4; +} + + +/* MODULES */ + +.module { + border: none; + margin-bottom: 30px; + background: var(--body-bg); +} + +.module p, +.module ul, +.module h3, +.module h4, +.module dl, +.module pre { + padding-left: 10px; + padding-right: 10px; +} + +.module blockquote { + margin-left: 12px; +} + +.module ul, +.module ol { + margin-left: 1.5em; +} + +.module h3 { + margin-top: .6em; +} + +.module h2, +.module caption, +.inline-group h2 { + margin: 0; + padding: 8px; + font-weight: 400; + font-size: 0.8125rem; + text-align: left; + background: var(--header-bg); + color: var(--header-link-color); +} + +.module caption, +.inline-group h2 { + font-size: 0.75rem; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +.module table { + border-collapse: collapse; +} + +/* MESSAGES & ERRORS */ + +ul.messagelist { + padding: 0; + margin: 0; +} + +ul.messagelist li { + display: block; + font-weight: 400; + font-size: 0.8125rem; + padding: 10px 10px 10px 65px; + margin: 0 0 10px 0; + background: var(--message-success-bg) url(../img/icon-yes.svg) 40px 12px no-repeat; + background-size: 16px auto; + color: var(--body-fg); + word-break: break-word; +} + +ul.messagelist li.warning { + background: var(--message-warning-bg) url(../img/icon-alert.svg) 40px 14px no-repeat; + background-size: 14px auto; +} + +ul.messagelist li.error { + background: var(--message-error-bg) url(../img/icon-no.svg) 40px 12px no-repeat; + background-size: 16px auto; +} + +.errornote { + font-size: 0.875rem; + font-weight: 700; + display: block; + padding: 10px 12px; + margin: 0 0 10px 0; + color: var(--error-fg); + border: 1px solid var(--error-fg); + border-radius: 4px; + background-color: var(--body-bg); + background-position: 5px 12px; + overflow-wrap: break-word; +} + +ul.errorlist { + margin: 0 0 4px; + padding: 0; + color: var(--error-fg); + background: var(--body-bg); +} + +ul.errorlist li { + font-size: 0.8125rem; + display: block; + margin-bottom: 4px; + overflow-wrap: break-word; +} + +ul.errorlist li:first-child { + margin-top: 0; +} + +ul.errorlist li a { + color: inherit; + text-decoration: underline; +} + +td ul.errorlist { + margin: 0; + padding: 0; +} + +td ul.errorlist li { + margin: 0; +} + +.form-row.errors { + margin: 0; + border: none; + border-bottom: 1px solid var(--hairline-color); + background: none; +} + +.form-row.errors ul.errorlist li { + padding-left: 0; +} + +.errors input, +.errors select, +.errors textarea, +td ul.errorlist+input, +td ul.errorlist+select, +td ul.errorlist+textarea { + border: 1px solid var(--error-fg); +} + +.description { + font-size: 0.75rem; + padding: 5px 0 0 12px; +} + +/* BREADCRUMBS */ + +div.breadcrumbs { + background: var(--breadcrumbs-bg); + padding: 10px 40px; + border: none; + color: var(--breadcrumbs-fg); + text-align: left; +} + +div.breadcrumbs a { + color: var(--breadcrumbs-link-fg); +} + +div.breadcrumbs a:focus, +div.breadcrumbs a:hover { + color: var(--breadcrumbs-fg); +} + +/* ACTION ICONS */ + +.viewlink, +.inlineviewlink { + padding-left: 16px; + background: url(../img/icon-viewlink.svg) 0 1px no-repeat; +} + +.hidelink { + padding-left: 16px; + background: url(../img/icon-hidelink.svg) 0 1px no-repeat; +} + +.addlink { + padding-left: 16px; + background: url(../img/icon-addlink.svg) 0 1px no-repeat; +} + +.changelink, +.inlinechangelink { + padding-left: 16px; + background: url(../img/icon-changelink.svg) 0 1px no-repeat; +} + +.deletelink { + padding-left: 16px; + background: url(../img/icon-deletelink.svg) 0 1px no-repeat; +} + +a.deletelink:link, +a.deletelink:visited { + color: #CC3434; + /* XXX Probably unused? */ +} + +a.deletelink:focus, +a.deletelink:hover { + color: #993333; + /* XXX Probably unused? */ + text-decoration: none; +} + +/* OBJECT TOOLS */ + +.object-tools { + font-size: 0.625rem; + font-weight: bold; + padding-left: 0; + float: right; + position: relative; + margin-top: -48px; +} + +.object-tools li { + display: block; + float: left; + margin-left: 5px; + height: 1rem; +} + +.object-tools a { + border-radius: 15px; +} + +.object-tools a:link, +.object-tools a:visited { + display: block; + float: left; + padding: 3px 12px; + background: var(--object-tools-bg); + color: var(--object-tools-fg); + font-weight: 400; + font-size: 0.6875rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.object-tools a:focus, +.object-tools a:hover { + background-color: var(--object-tools-hover-bg); +} + +.object-tools a:focus { + text-decoration: none; +} + +.object-tools a.viewsitelink, +.object-tools a.addlink { + background-repeat: no-repeat; + background-position: right 7px center; + padding-right: 26px; +} + +.object-tools a.viewsitelink { + background-image: url(../img/tooltag-arrowright.svg); +} + +.object-tools a.addlink { + background-image: url(../img/tooltag-add.svg); +} + +/* OBJECT HISTORY */ + +#change-history table { + width: 100%; +} + +#change-history table tbody th { + width: 16em; +} + +#change-history .paginator { + color: var(--body-quiet-color); + border-bottom: 1px solid var(--hairline-color); + background: var(--body-bg); + overflow: hidden; +} + +/* PAGE STRUCTURE */ + +#container { + position: relative; + width: 100%; + min-width: 980px; + padding: 0; + display: flex; + flex-direction: column; + height: 100%; +} + +#container>.main { + display: flex; + flex: 1 0 auto; +} + +.main>.content { + flex: 1 0; + max-width: 100%; +} + +.skip-to-content-link { + position: absolute; + top: -999px; + margin: 5px; + padding: 5px; + background: var(--body-bg); + z-index: 1; +} + +.skip-to-content-link:focus { + left: 0px; + top: 0px; +} + +#content { + padding: 20px 40px; +} + +.dashboard #content { + width: 600px; +} + +#content-main { + float: left; + width: 100%; +} + +#content-related { + float: right; + width: 260px; + position: relative; + margin-right: -300px; +} + +@media (forced-colors: active) { + #content-related { + border: 1px solid; + } +} + +#footer { + clear: both; + padding: 10px; +} + +/* COLUMN TYPES */ + +.colMS { + margin-right: 300px; +} + +.colSM { + margin-left: 300px; +} + +.colSM #content-related { + float: left; + margin-right: 0; + margin-left: -300px; +} + +.colSM #content-main { + float: right; +} + +.popup .colM { + width: auto; +} + +/* HEADER */ + +#header { + width: auto; + height: auto; + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 40px; + background: var(--header-bg); + color: var(--header-color); +} + +#header a:link, +#header a:visited, +#logout-form button { + color: var(--header-link-color); +} + +#header a:focus, +#header a:hover { + text-decoration: underline; +} + +@media (forced-colors: active) { + #header { + border-bottom: 1px solid; + } +} + +#branding { + display: flex; +} + +#site-name { + padding: 0; + margin: 0; + margin-inline-end: 20px; + font-weight: 300; + font-size: 1.5rem; + color: var(--header-branding-color); +} + +#site-name a:link, +#site-name a:visited { + color: var(--accent); +} + +#branding h2 { + padding: 0 10px; + font-size: 0.875rem; + margin: -8px 0 8px 0; + font-weight: normal; + color: var(--header-color); +} + +#branding a:hover { + text-decoration: none; +} + +#logout-form { + display: inline; +} + +#logout-form button { + background: none; + border: 0; + cursor: pointer; + font-family: var(--font-family-primary); +} + +#user-tools { + float: right; + margin: 0 0 0 20px; + text-align: right; +} + +#user-tools, +#logout-form button { + padding: 0; + font-weight: 300; + font-size: 0.6875rem; + letter-spacing: 0.5px; + text-transform: uppercase; +} + +#user-tools a, +#logout-form button { + border-bottom: 1px solid rgba(255, 255, 255, 0.25); +} + +#user-tools a:focus, +#user-tools a:hover, +#logout-form button:active, +#logout-form button:hover { + text-decoration: none; + border-bottom: 0; +} + +#logout-form button:active, +#logout-form button:hover { + margin-bottom: 1px; +} + +/* SIDEBAR */ + +#content-related { + background: var(--darkened-bg); +} + +#content-related .module { + background: none; +} + +#content-related h3 { + color: var(--body-quiet-color); + padding: 0 16px; + margin: 0 0 16px; +} + +#content-related h4 { + font-size: 0.8125rem; +} + +#content-related p { + padding-left: 16px; + padding-right: 16px; +} + +#content-related .actionlist { + padding: 0; + margin: 16px; +} + +#content-related .actionlist li { + line-height: 1.2; + margin-bottom: 10px; + padding-left: 18px; +} + +#content-related .module h2 { + background: none; + padding: 16px; + margin-bottom: 16px; + border-bottom: 1px solid var(--hairline-color); + font-size: 1.125rem; + color: var(--body-fg); +} + +.delete-confirmation form input[type="submit"] { + background: var(--delete-button-bg); + border-radius: 4px; + padding: 10px 15px; + color: var(--button-fg); +} + +.delete-confirmation form input[type="submit"]:active, +.delete-confirmation form input[type="submit"]:focus, +.delete-confirmation form input[type="submit"]:hover { + background: var(--delete-button-hover-bg); +} + +.delete-confirmation form .cancel-link { + display: inline-block; + vertical-align: middle; + height: 0.9375rem; + line-height: 0.9375rem; + border-radius: 4px; + padding: 10px 15px; + color: var(--button-fg); + background: var(--close-button-bg); + margin: 0 0 0 10px; +} + +.delete-confirmation form .cancel-link:active, +.delete-confirmation form .cancel-link:focus, +.delete-confirmation form .cancel-link:hover { + background: var(--close-button-hover-bg); +} + +/* POPUP */ +.popup #content { + padding: 20px; +} + +.popup #container { + min-width: 0; +} + +.popup #header { + padding: 10px 20px; +} + +/* PAGINATOR */ + +.paginator { + display: flex; + align-items: center; + gap: 4px; + font-size: 0.8125rem; + padding-top: 10px; + padding-bottom: 10px; + line-height: 22px; + margin: 0; + border-top: 1px solid var(--hairline-color); + width: 100%; +} + +.paginator a:link, +.paginator a:visited { + padding: 2px 6px; + background: var(--button-bg); + text-decoration: none; + color: var(--button-fg); +} + +.paginator a.showall { + border: none; + background: none; + color: var(--link-fg); +} + +.paginator a.showall:focus, +.paginator a.showall:hover { + background: none; + color: var(--link-hover-color); +} + +.paginator .end { + margin-right: 6px; +} + +.paginator .this-page { + padding: 2px 6px; + font-weight: bold; + font-size: 0.8125rem; + vertical-align: top; +} + +.paginator a:focus, +.paginator a:hover { + color: white; + background: var(--link-hover-color); +} + +.paginator input { + margin-left: auto; +} + +.base-svgs { + display: none; +} + +.visually-hidden { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; + color: var(--body-fg); + background-color: var(--body-bg); +} + +.sticky { + position: sticky; + top: 0; + max-height: 100vh; +} + +.toggle-nav-sidebar { + z-index: 20; + left: 0; + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 23px; + width: 23px; + border: 0; + border-right: 1px solid var(--hairline-color); + background-color: var(--body-bg); + cursor: pointer; + font-size: 1.25rem; + color: var(--link-fg); + padding: 0; +} + +[dir="rtl"] .toggle-nav-sidebar { + border-left: 1px solid var(--hairline-color); + border-right: 0; +} + +.toggle-nav-sidebar:hover, +.toggle-nav-sidebar:focus { + background-color: var(--darkened-bg); +} + +#nav-sidebar { + z-index: 15; + flex: 0 0 275px; + left: -276px; + margin-left: -276px; + border-top: 1px solid transparent; + border-right: 1px solid var(--hairline-color); + background-color: var(--body-bg); + overflow: auto; +} + +[dir="rtl"] #nav-sidebar { + border-left: 1px solid var(--hairline-color); + border-right: 0; + left: 0; + margin-left: 0; + right: -276px; + margin-right: -276px; +} + +.toggle-nav-sidebar::before { + content: '\00BB'; +} + +.main.shifted .toggle-nav-sidebar::before { + content: '\00AB'; +} + +.main>#nav-sidebar { + visibility: hidden; +} + +.main.shifted>#nav-sidebar { + margin-left: 0; + visibility: visible; +} + +[dir="rtl"] .main.shifted>#nav-sidebar { + margin-right: 0; +} + +#nav-sidebar .module th { + width: 100%; + overflow-wrap: anywhere; +} + +#nav-sidebar .module th, +#nav-sidebar .module caption { + padding-left: 16px; +} + +#nav-sidebar .module td { + white-space: nowrap; +} + +[dir="rtl"] #nav-sidebar .module th, +[dir="rtl"] #nav-sidebar .module caption { + padding-left: 8px; + padding-right: 16px; +} + +#nav-sidebar .current-app .section:link, +#nav-sidebar .current-app .section:visited { + color: var(--header-color); + font-weight: bold; +} + +#nav-sidebar .current-model { + background: var(--selected-row); +} + +@media (forced-colors: active) { + #nav-sidebar .current-model { + background-color: SelectedItem; + } +} + +.main>#nav-sidebar+.content { + max-width: calc(100% - 23px); +} + +.main.shifted>#nav-sidebar+.content { + max-width: calc(100% - 299px); +} + +@media (max-width: 767px) { + + #nav-sidebar, + #toggle-nav-sidebar { + display: none; + } + + .main>#nav-sidebar+.content, + .main.shifted>#nav-sidebar+.content { + max-width: 100%; + } +} + +#nav-filter { + width: 100%; + box-sizing: border-box; + padding: 2px 5px; + margin: 5px 0; + border: 1px solid var(--border-color); + background-color: var(--darkened-bg); + color: var(--body-fg); +} + +#nav-filter:focus { + border-color: var(--body-quiet-color); +} + +#nav-filter.no-results { + background: var(--message-error-bg); +} + +#nav-sidebar table { + width: 100%; +} + +/* DASHBOARD */ +.dashboard td, +.dashboard th { + word-break: break-word; +} + +.dashboard .module table th { + width: 100%; +} + +.dashboard .module table td { + white-space: nowrap; +} + +.dashboard .module table td a { + display: block; + padding-right: .6em; +} + +/* RECENT ACTIONS MODULE */ + +.module ul.actionlist { + margin-left: 0; +} + +ul.actionlist li { + list-style-type: none; + overflow: hidden; + text-overflow: ellipsis; +} + +/* SELECTOR (FILTER INTERFACE) */ + +.selector { + width: 800px; + float: left; + display: flex; +} + +.selector select { + width: 380px; + height: 17.2em; + flex: 1 0 auto; +} + +.selector-available, +.selector-chosen { + width: 380px; + text-align: center; + margin-bottom: 5px; + display: flex; + flex-direction: column; +} + +.selector-available h2, +.selector-chosen h2 { + border: 1px solid var(--border-color); + border-radius: 4px 4px 0 0; +} + +.selector-chosen .list-footer-display { + border: 1px solid var(--border-color); + border-top: none; + border-radius: 0 0 4px 4px; + margin: 0 0 10px; + padding: 8px; + text-align: center; + background: var(--primary); + color: var(--header-link-color); + cursor: pointer; +} + +.selector-chosen .list-footer-display__clear { + color: var(--breadcrumbs-fg); +} + +.selector-chosen h2 { + background: var(--secondary); + color: var(--header-link-color); +} + +.selector .selector-available h2 { + background: var(--darkened-bg); + color: var(--body-quiet-color); +} + +.selector .selector-filter { + border: 1px solid var(--border-color); + border-width: 0 1px; + padding: 8px; + color: var(--body-quiet-color); + font-size: 0.625rem; + margin: 0; + text-align: left; +} + +.selector .selector-filter label, +.inline-group .aligned .selector .selector-filter label { + float: left; + margin: 7px 0 0; + width: 18px; + height: 18px; + padding: 0; + overflow: hidden; + line-height: 1; +} + +.selector .selector-available input, +.selector .selector-chosen input { + width: 320px; + margin-left: 8px; +} + +.selector ul.selector-chooser { + align-self: center; + width: 22px; + background-color: var(--selected-bg); + border-radius: 10px; + margin: 0 5px; + padding: 0; + transform: translateY(-17px); +} + +.selector-chooser li { + margin: 0; + padding: 3px; + list-style-type: none; +} + +.selector select { + padding: 0 10px; + margin: 0 0 10px; + border-radius: 0 0 4px 4px; +} + +.selector .selector-chosen--with-filtered select { + margin: 0; + border-radius: 0; + height: 14em; +} + +.selector .selector-chosen:not(.selector-chosen--with-filtered) .list-footer-display { + display: none; +} + +.selector-add, +.selector-remove { + width: 16px; + height: 16px; + display: block; + text-indent: -3000px; + overflow: hidden; + cursor: default; + opacity: 0.55; +} + +.active.selector-add, +.active.selector-remove { + opacity: 1; +} + +.active.selector-add:hover, +.active.selector-remove:hover { + cursor: pointer; +} + +.selector-add { + background: url(../img/selector-icons.svg) 0 -96px no-repeat; +} + +.active.selector-add:focus, +.active.selector-add:hover { + background-position: 0 -112px; +} + +.selector-remove { + background: url(../img/selector-icons.svg) 0 -64px no-repeat; +} + +.active.selector-remove:focus, +.active.selector-remove:hover { + background-position: 0 -80px; +} + +a.selector-chooseall, +a.selector-clearall { + display: inline-block; + height: 16px; + text-align: left; + margin: 1px auto 3px; + overflow: hidden; + font-weight: bold; + line-height: 16px; + color: var(--body-quiet-color); + text-decoration: none; + opacity: 0.55; +} + +a.active.selector-chooseall:focus, +a.active.selector-clearall:focus, +a.active.selector-chooseall:hover, +a.active.selector-clearall:hover { + color: var(--link-fg); +} + +a.active.selector-chooseall, +a.active.selector-clearall { + opacity: 1; +} + +a.active.selector-chooseall:hover, +a.active.selector-clearall:hover { + cursor: pointer; +} + +a.selector-chooseall { + padding: 0 18px 0 0; + background: url(../img/selector-icons.svg) right -160px no-repeat; + cursor: default; +} + +a.active.selector-chooseall:focus, +a.active.selector-chooseall:hover { + background-position: 100% -176px; +} + +a.selector-clearall { + padding: 0 0 0 18px; + background: url(../img/selector-icons.svg) 0 -128px no-repeat; + cursor: default; +} + +a.active.selector-clearall:focus, +a.active.selector-clearall:hover { + background-position: 0 -144px; +} + +/* STACKED SELECTORS */ + +.stacked { + float: left; + width: 490px; + display: block; +} + +.stacked select { + width: 480px; + height: 10.1em; +} + +.stacked .selector-available, +.stacked .selector-chosen { + width: 480px; +} + +.stacked .selector-available { + margin-bottom: 0; +} + +.stacked .selector-available input { + width: 422px; +} + +.stacked ul.selector-chooser { + height: 22px; + width: 50px; + margin: 0 0 10px 40%; + background-color: #eee; + border-radius: 10px; + transform: none; +} + +.stacked .selector-chooser li { + float: left; + padding: 3px 3px 3px 5px; +} + +.stacked .selector-chooseall, +.stacked .selector-clearall { + display: none; +} + +.stacked .selector-add { + background: url(../img/selector-icons.svg) 0 -32px no-repeat; + cursor: default; +} + +.stacked .active.selector-add { + background-position: 0 -32px; + cursor: pointer; +} + +.stacked .active.selector-add:focus, +.stacked .active.selector-add:hover { + background-position: 0 -48px; + cursor: pointer; +} + +.stacked .selector-remove { + background: url(../img/selector-icons.svg) 0 0 no-repeat; + cursor: default; +} + +.stacked .active.selector-remove { + background-position: 0 0px; + cursor: pointer; +} + +.stacked .active.selector-remove:focus, +.stacked .active.selector-remove:hover { + background-position: 0 -16px; + cursor: pointer; +} + +.selector .help-icon { + background: url(../img/icon-unknown.svg) 0 0 no-repeat; + display: inline-block; + vertical-align: middle; + margin: -2px 0 0 2px; + width: 13px; + height: 13px; +} + +.selector .selector-chosen .help-icon { + background: url(../img/icon-unknown-alt.svg) 0 0 no-repeat; +} + +.selector .search-label-icon { + background: url(../img/search.svg) 0 0 no-repeat; + display: inline-block; + height: 1.125rem; + width: 1.125rem; +} + +/* DATE AND TIME */ + +p.datetime { + line-height: 20px; + margin: 0; + padding: 0; + color: var(--body-quiet-color); + font-weight: bold; +} + +.datetime span { + white-space: nowrap; + font-weight: normal; + font-size: 0.6875rem; + color: var(--body-quiet-color); +} + +.datetime input, +.form-row .datetime input.vDateField, +.form-row .datetime input.vTimeField { + margin-left: 5px; + margin-bottom: 4px; +} + +table p.datetime { + font-size: 0.6875rem; + margin-left: 0; + padding-left: 0; +} + +.datetimeshortcuts .clock-icon, +.datetimeshortcuts .date-icon { + position: relative; + display: inline-block; + vertical-align: middle; + height: 16px; + width: 16px; + overflow: hidden; +} + +.datetimeshortcuts .clock-icon { + background: url(../img/icon-clock.svg) 0 0 no-repeat; +} + +.datetimeshortcuts a:focus .clock-icon, +.datetimeshortcuts a:hover .clock-icon { + background-position: 0 -16px; +} + +.datetimeshortcuts .date-icon { + background: url(../img/icon-calendar.svg) 0 0 no-repeat; + top: -1px; +} + +.datetimeshortcuts a:focus .date-icon, +.datetimeshortcuts a:hover .date-icon { + background-position: 0 -16px; +} + +.timezonewarning { + font-size: 0.6875rem; + color: var(--body-quiet-color); +} + +/* URL */ + +p.url { + line-height: 20px; + margin: 0; + padding: 0; + color: var(--body-quiet-color); + font-size: 0.6875rem; + font-weight: bold; +} + +.url a { + font-weight: normal; +} + +/* FILE UPLOADS */ + +p.file-upload { + line-height: 20px; + margin: 0; + padding: 0; + color: var(--body-quiet-color); + font-size: 0.6875rem; + font-weight: bold; +} + +.file-upload a { + font-weight: normal; +} + +.file-upload .deletelink { + margin-left: 5px; +} + +span.clearable-file-input label { + color: var(--body-fg); + font-size: 0.6875rem; + display: inline; + float: none; +} + +/* CALENDARS & CLOCKS */ + +.calendarbox, +.clockbox { + margin: 5px auto; + font-size: 0.75rem; + width: 19em; + text-align: center; + background: var(--body-bg); + color: var(--body-fg); + border: 1px solid var(--hairline-color); + border-radius: 4px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15); + overflow: hidden; + position: relative; +} + +.clockbox { + width: auto; +} + +.calendar { + margin: 0; + padding: 0; +} + +.calendar table { + margin: 0; + padding: 0; + border-collapse: collapse; + background: white; + width: 100%; +} + +.calendar caption, +.calendarbox h2 { + margin: 0; + text-align: center; + border-top: none; + font-weight: 700; + font-size: 0.75rem; + color: #333; + background: var(--accent); +} + +.calendar th { + padding: 8px 5px; + background: var(--darkened-bg); + border-bottom: 1px solid var(--border-color); + font-weight: 400; + font-size: 0.75rem; + text-align: center; + color: var(--body-quiet-color); +} + +.calendar td { + font-weight: 400; + font-size: 0.75rem; + text-align: center; + padding: 0; + border-top: 1px solid var(--hairline-color); + border-bottom: none; +} + +.calendar td.selected a { + background: var(--secondary); + color: var(--button-fg); +} + +.calendar td.nonday { + background: var(--darkened-bg); +} + +.calendar td.today a { + font-weight: 700; +} + +.calendar td a, +.timelist a { + display: block; + font-weight: 400; + padding: 6px; + text-decoration: none; + color: var(--body-quiet-color); +} + +.calendar td a:focus, +.timelist a:focus, +.calendar td a:hover, +.timelist a:hover { + background: var(--primary); + color: white; +} + +.calendar td a:active, +.timelist a:active { + background: var(--header-bg); + color: white; +} + +.calendarnav { + font-size: 0.625rem; + text-align: center; + color: #ccc; + margin: 0; + padding: 1px 3px; +} + +.calendarnav a:link, +#calendarnav a:visited, +#calendarnav a:focus, +#calendarnav a:hover { + color: var(--body-quiet-color); +} + +.calendar-shortcuts { + background: var(--body-bg); + color: var(--body-quiet-color); + font-size: 0.6875rem; + line-height: 0.6875rem; + border-top: 1px solid var(--hairline-color); + padding: 8px 0; +} + +.calendarbox .calendarnav-previous, +.calendarbox .calendarnav-next { + display: block; + position: absolute; + top: 8px; + width: 15px; + height: 15px; + text-indent: -9999px; + padding: 0; +} + +.calendarnav-previous { + left: 10px; + background: url(../img/calendar-icons.svg) 0 0 no-repeat; +} + +.calendarbox .calendarnav-previous:focus, +.calendarbox .calendarnav-previous:hover { + background-position: 0 -15px; +} + +.calendarnav-next { + right: 10px; + background: url(../img/calendar-icons.svg) 0 -30px no-repeat; +} + +.calendarbox .calendarnav-next:focus, +.calendarbox .calendarnav-next:hover { + background-position: 0 -45px; +} + +.calendar-cancel { + margin: 0; + padding: 4px 0; + font-size: 0.75rem; + background: var(--close-button-bg); + border-top: 1px solid var(--border-color); + color: var(--button-fg); +} + +.calendar-cancel:focus, +.calendar-cancel:hover { + background: var(--close-button-hover-bg); +} + +.calendar-cancel a { + color: var(--button-fg); + display: block; +} + +ul.timelist, +.timelist li { + list-style-type: none; + margin: 0; + padding: 0; +} + +.timelist a { + padding: 2px; +} + +/* EDIT INLINE */ + +.inline-deletelink { + float: right; + text-indent: -9999px; + background: url(../img/inline-delete.svg) 0 0 no-repeat; + width: 16px; + height: 16px; + border: 0px none; +} + +.inline-deletelink:focus, +.inline-deletelink:hover { + cursor: pointer; +} + +/* RELATED WIDGET WRAPPER */ +.related-widget-wrapper { + float: left; + /* display properly in form rows with multiple fields */ + overflow: hidden; + /* clear floated contents */ +} + +.related-widget-wrapper-link { + opacity: .6; + filter: grayscale(1); +} + +.related-widget-wrapper-link:link { + opacity: 1; + filter: grayscale(0); +} + +select+.related-widget-wrapper-link, +.related-widget-wrapper-link+.related-widget-wrapper-link { + margin-left: 7px; +} + +/* GIS MAPS */ +.dj_map { + width: 600px; + height: 400px; +} + +/* FORM ROWS */ + +.form-row { + overflow: hidden; + padding: 10px; + font-size: 0.8125rem; + border-bottom: 1px solid var(--hairline-color); +} + +.form-row img, +.form-row input { + vertical-align: middle; +} + +.form-row label input[type="checkbox"] { + margin-top: 0; + vertical-align: 0; +} + +form .form-row p { + padding-left: 0; +} + +.flex-container { + display: flex; + flex-wrap: wrap; +} + +.form-multiline>div { + padding-bottom: 10px; +} + +/* FORM LABELS */ + +label { + font-weight: normal; + color: var(--body-quiet-color); + font-size: 0.8125rem; +} + +.required label, +label.required { + font-weight: bold; + color: var(--body-fg); +} + +/* RADIO BUTTONS */ + +form div.radiolist div { + padding-right: 7px; +} + +form div.radiolist.inline div { + display: inline-block; +} + +form div.radiolist label { + width: auto; +} + +form div.radiolist input[type="radio"] { + margin: -2px 4px 0 0; + padding: 0; +} + +form ul.inline { + margin-left: 0; + padding: 0; +} + +form ul.inline li { + float: left; + padding-right: 7px; +} + +/* ALIGNED FIELDSETS */ + +.aligned label { + display: block; + padding: 4px 10px 0 0; + width: 160px; + word-wrap: break-word; + line-height: 1; +} + +.aligned label:not(.vCheckboxLabel):after { + content: ''; + display: inline-block; + vertical-align: middle; + height: 1.625rem; +} + +.aligned label+p, +.aligned .checkbox-row+div.help, +.aligned label+div.readonly { + padding: 6px 0; + margin-top: 0; + margin-bottom: 0; + margin-left: 0; + overflow-wrap: break-word; +} + +.aligned ul label { + display: inline; + float: none; + width: auto; +} + +.aligned .form-row input { + margin-bottom: 0; +} + +.colMS .aligned .vLargeTextField, +.colMS .aligned .vXMLLargeTextField { + width: 350px; +} + +form .aligned ul { + margin-left: 160px; + padding-left: 10px; +} + +form .aligned div.radiolist { + display: inline-block; + margin: 0; + padding: 0; +} + +form .aligned p.help, +form .aligned div.help { + margin-top: 0; + margin-left: 160px; + padding-left: 10px; +} + +form .aligned p.date div.help.timezonewarning, +form .aligned p.datetime div.help.timezonewarning, +form .aligned p.time div.help.timezonewarning { + margin-left: 0; + padding-left: 0; + font-weight: normal; +} + +form .aligned p.help:last-child, +form .aligned div.help:last-child { + margin-bottom: 0; + padding-bottom: 0; +} + +form .aligned input+p.help, +form .aligned textarea+p.help, +form .aligned select+p.help, +form .aligned input+div.help, +form .aligned textarea+div.help, +form .aligned select+div.help { + margin-left: 160px; + padding-left: 10px; +} + +form .aligned ul li { + list-style: none; +} + +form .aligned table p { + margin-left: 0; + padding-left: 0; +} + +.aligned .vCheckboxLabel { + float: none; + width: auto; + display: inline-block; + vertical-align: -3px; + padding: 0 0 5px 5px; +} + +.aligned .vCheckboxLabel+p.help, +.aligned .vCheckboxLabel+div.help { + margin-top: -4px; +} + +.colM .aligned .vLargeTextField, +.colM .aligned .vXMLLargeTextField { + width: 610px; +} + +fieldset .fieldBox { + margin-right: 20px; +} + +/* WIDE FIELDSETS */ + +.wide label { + width: 200px; +} + +form .wide p, +form .wide ul.errorlist, +form .wide input+p.help, +form .wide input+div.help { + margin-left: 200px; +} + +form .wide p.help, +form .wide div.help { + padding-left: 50px; +} + +form div.help ul { + padding-left: 0; + margin-left: 0; +} + +.colM fieldset.wide .vLargeTextField, +.colM fieldset.wide .vXMLLargeTextField { + width: 450px; +} + +/* COLLAPSED FIELDSETS */ + +fieldset.collapsed * { + display: none; +} + +fieldset.collapsed h2, +fieldset.collapsed { + display: block; +} + +fieldset.collapsed { + border: 1px solid var(--hairline-color); + border-radius: 4px; + overflow: hidden; +} + +fieldset.collapsed h2 { + background: var(--darkened-bg); + color: var(--body-quiet-color); +} + +fieldset .collapse-toggle { + color: var(--header-link-color); +} + +fieldset.collapsed .collapse-toggle { + background: transparent; + display: inline; + color: var(--link-fg); +} + +/* MONOSPACE TEXTAREAS */ + +fieldset.monospace textarea { + font-family: var(--font-family-monospace); +} + +/* SUBMIT ROW */ + +.submit-row { + padding: 12px 14px 12px; + margin: 0 0 20px; + background: var(--darkened-bg); + border: 1px solid var(--hairline-color); + border-radius: 4px; + overflow: hidden; + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +body.popup .submit-row { + overflow: auto; +} + +.submit-row input { + height: 2.1875rem; + line-height: 0.9375rem; +} + +.submit-row input, +.submit-row a { + margin: 0; +} + +.submit-row input.default { + text-transform: uppercase; +} + +.submit-row a.deletelink { + margin-left: auto; +} + +.submit-row a.deletelink { + display: block; + background: var(--delete-button-bg); + border-radius: 4px; + padding: 0.625rem 0.9375rem; + height: 0.9375rem; + line-height: 0.9375rem; + color: var(--button-fg); +} + +.submit-row a.closelink { + display: inline-block; + background: var(--close-button-bg); + border-radius: 4px; + padding: 10px 15px; + height: 0.9375rem; + line-height: 0.9375rem; + color: var(--button-fg); +} + +.submit-row a.deletelink:focus, +.submit-row a.deletelink:hover, +.submit-row a.deletelink:active { + background: var(--delete-button-hover-bg); + text-decoration: none; +} + +.submit-row a.closelink:focus, +.submit-row a.closelink:hover, +.submit-row a.closelink:active { + background: var(--close-button-hover-bg); + text-decoration: none; +} + +/* CUSTOM FORM FIELDS */ + +.vSelectMultipleField { + vertical-align: top; +} + +.vCheckboxField { + border: none; +} + +.vDateField, +.vTimeField { + margin-right: 2px; + margin-bottom: 4px; +} + +.vDateField { + min-width: 6.85em; +} + +.vTimeField { + min-width: 4.7em; +} + +.vURLField { + width: 30em; +} + +.vLargeTextField, +.vXMLLargeTextField { + width: 48em; +} + +.flatpages-flatpage #id_content { + height: 40.2em; +} + +.module table .vPositiveSmallIntegerField { + width: 2.2em; +} + +.vIntegerField { + width: 5em; +} + +.vBigIntegerField { + width: 10em; +} + +.vForeignKeyRawIdAdminField { + width: 5em; +} + +.vTextField, +.vUUIDField { + width: 20em; +} + +/* INLINES */ + +.inline-group { + padding: 0; + margin: 0 0 30px; +} + +.inline-group thead th { + padding: 8px 10px; +} + +.inline-group .aligned label { + width: 160px; +} + +.inline-related { + position: relative; +} + +.inline-related h3 { + margin: 0; + color: var(--body-quiet-color); + padding: 5px; + font-size: 0.8125rem; + background: var(--darkened-bg); + border-top: 1px solid var(--hairline-color); + border-bottom: 1px solid var(--hairline-color); +} + +.inline-related h3 span.delete { + float: right; +} + +.inline-related h3 span.delete label { + margin-left: 2px; + font-size: 0.6875rem; +} + +.inline-related fieldset { + margin: 0; + background: var(--body-bg); + border: none; + width: 100%; +} + +.inline-related fieldset.module h3 { + margin: 0; + padding: 2px 5px 3px 5px; + font-size: 0.6875rem; + text-align: left; + font-weight: bold; + background: #bcd; + color: var(--body-bg); +} + +.inline-group .tabular fieldset.module { + border: none; +} + +.inline-related.tabular fieldset.module table { + width: 100%; + overflow-x: scroll; +} + +.last-related fieldset { + border: none; +} + +.inline-group .tabular tr.has_original td { + padding-top: 2em; +} + +.inline-group .tabular tr td.original { + padding: 2px 0 0 0; + width: 0; + _position: relative; +} + +.inline-group .tabular th.original { + width: 0px; + padding: 0; +} + +.inline-group .tabular td.original p { + position: absolute; + left: 0; + height: 1.1em; + padding: 2px 9px; + overflow: hidden; + font-size: 0.5625rem; + font-weight: bold; + color: var(--body-quiet-color); + _width: 700px; +} + +.inline-group ul.tools { + padding: 0; + margin: 0; + list-style: none; +} + +.inline-group ul.tools li { + display: inline; + padding: 0 5px; +} + +.inline-group div.add-row, +.inline-group .tabular tr.add-row td { + color: var(--body-quiet-color); + background: var(--darkened-bg); + padding: 8px 10px; + border-bottom: 1px solid var(--hairline-color); +} + +.inline-group .tabular tr.add-row td { + padding: 8px 10px; + border-bottom: 1px solid var(--hairline-color); +} + +.inline-group ul.tools a.add, +.inline-group div.add-row a, +.inline-group .tabular tr.add-row td a { + background: url(../img/icon-addlink.svg) 0 1px no-repeat; + padding-left: 16px; + font-size: 0.75rem; +} + +.empty-form { + display: none; +} + +/* RELATED FIELD ADD ONE / LOOKUP */ + +.related-lookup { + margin-left: 5px; + display: inline-block; + vertical-align: middle; + background-repeat: no-repeat; + background-size: 14px; +} + +.related-lookup { + width: 1rem; + height: 1rem; + background-image: url(../img/search.svg); +} + +form .related-widget-wrapper ul { + display: inline-block; + margin-left: 0; + padding-left: 0; +} + +.clearable-file-input input { + margin-top: 0; +} \ No newline at end of file diff --git a/templates/admin/app_list.html b/templates/admin/app_list.html new file mode 100644 index 0000000..e7b92f2 --- /dev/null +++ b/templates/admin/app_list.html @@ -0,0 +1,38 @@ +{% if app_list %} + {% for app in app_list %} +
+ + + {% for model in app.models %} + + {% if model.admin_url %} + + {% else %} + + {% endif %} + + {% if model.add_url %} + + {% else %} + + {% endif %} + + {% if model.admin_url and show_changelinks %} + {% if model.view_only %} + + {% else %} + + {% endif %} + {% elif show_changelinks %} + + {% endif %} + + {% endfor %} +
+ {{ app.name }} +
{{ model.name }}{{ model.name }}{{ translate( 'Add') }}{{ translate( 'View') }}{{ translate( 'Change') }}
+
+ {% endfor %} +{% else %} +

{{ translate( 'You don’t have permission to view or edit anything.') }}

+{% endif %} diff --git a/templates/admin/base.html b/templates/admin/base.html new file mode 100644 index 0000000..06b67ef --- /dev/null +++ b/templates/admin/base.html @@ -0,0 +1,127 @@ + +{% set is_nav_sidebar_enabled = true %} + + +{% block title %}{% endblock %} + + +{% block dark_mode_vars %} + + +{% endblock %} +{% if not is_popup and is_nav_sidebar_enabled %} + + +{% endif %} +{% block extrastyle %}{% endblock %} +{% if LANGUAGE_BIDI %}{% endif %} +{% block extrahead %}{% endblock %} +{% block responsive %} + + + {% if LANGUAGE_BIDI %}{% endif %} +{% endblock %} +{% block blockbots %}{% endblock %} + + + +{{ translate('Skip to main content') }} + +
+ + {% if not is_popup %} + + {% block header %} + + {% endblock %} + + {% block nav_breadcrumbs %} + + {% endblock %} + {% endif %} + +
+ {% if not is_popup and is_nav_sidebar_enabled %} + {% block nav_sidebar %} + {% include "admin/nav_sidebar.html" %} + {% endblock %} + {% endif %} +
+ {% block messages %} + {% if messages %} +
    {% for message in messages %} + {{ message|capfirst }} + {% endfor %}
+ {% endif %} + {% endblock messages %} + +
+ {% block pretitle %}{% endblock %} + {% block content_title %}{% if title %}

{{ title }}

{% endif %}{% endblock %} + {% block content_subtitle %}{% if subtitle %}

{{ subtitle }}

{% endif %}{% endblock %} + {% block content %} + {% block object_tools %}{% endblock %} + {{ content }} + {% endblock %} + {% block sidebar %}{% endblock %} +
+
+ + {% block footer %}{% endblock %} +
+
+
+ + + + + + + + + + + \ No newline at end of file diff --git a/templates/admin/index.html b/templates/admin/index.html new file mode 100644 index 0000000..edc0044 --- /dev/null +++ b/templates/admin/index.html @@ -0,0 +1 @@ +{% extends "admin/base.html" %} diff --git a/templates/admin/nav_sidebar.html b/templates/admin/nav_sidebar.html new file mode 100644 index 0000000..ab8641b --- /dev/null +++ b/templates/admin/nav_sidebar.html @@ -0,0 +1,9 @@ + + \ No newline at end of file