From 1d8e19c88f4043bc0fdacb7ebfd2b5400aae2eae Mon Sep 17 00:00:00 2001 From: Per Stark Date: Thu, 12 Dec 2024 20:59:27 +0100 Subject: [PATCH] tailwindcss + wip auth --- .gitignore | 1 + Cargo.lock | 248 ++- Cargo.toml | 4 + package-lock.json | 171 ++ package.json | 5 + src/auth.rs | 152 ++ src/bin/server.rs | 66 +- src/error.rs | 9 + src/ingress/analysis/ingress_analyser.rs | 6 +- src/lib.rs | 1 + src/retrieval/graph.rs | 8 +- src/retrieval/mod.rs | 4 +- src/retrieval/vector.rs | 4 +- src/server/assets/style.css | 1810 ++++++++++++++++--- src/server/routes/auth.rs | 36 + src/server/routes/index.rs | 12 +- src/server/routes/mod.rs | 1 + src/server/templates/auth/signup.html | 62 + src/server/templates/base.html | 43 +- src/server/templates/index.html | 17 +- src/server/templates/search_result.html | 12 +- src/storage/db.rs | 14 +- src/storage/types/knowledge_relationship.rs | 4 +- src/storage/types/mod.rs | 1 + src/storage/types/user.rs | 100 + tailwind.config.js | 5 +- 26 files changed, 2496 insertions(+), 300 deletions(-) create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/auth.rs create mode 100644 src/server/routes/auth.rs create mode 100644 src/server/templates/auth/signup.html create mode 100644 src/storage/types/user.rs diff --git a/.gitignore b/.gitignore index a128199..1929fba 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .direnv .devenv database +node_modules diff --git a/Cargo.lock b/Cargo.lock index 6885342..bc7a283 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,16 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aes" version = "0.8.4" @@ -47,6 +57,20 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.8" @@ -176,9 +200,9 @@ checksum = "ea50b14b7a4b9343f8c627a7a53c52076482bd4bdad0a24fd3ec533ed616cc2c" [[package]] name = "anyhow" -version = "1.0.89" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6" +checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7" [[package]] name = "approx" @@ -516,6 +540,17 @@ dependencies = [ "reactor-trait", ] +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -546,9 +581,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" [[package]] name = "async-trait" -version = "0.1.82" +version = "0.1.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1" +checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd" dependencies = [ "proc-macro2", "quote", @@ -592,9 +627,9 @@ checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" [[package]] name = "axum" -version = "0.7.5" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" dependencies = [ "async-trait", "axum-core", @@ -618,9 +653,9 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper", "tokio", - "tower", + "tower 0.5.2", "tower-layer", "tower-service", "tracing", @@ -628,9 +663,9 @@ dependencies = [ [[package]] name = "axum-core" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" dependencies = [ "async-trait", "bytes", @@ -641,7 +676,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 0.1.2", + "sync_wrapper", "tower-layer", "tower-service", "tracing", @@ -658,6 +693,73 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "axum_session" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219e632039c0e9c7f53f135d7f0aebbfccc39c3a18af2034f789cd1cf0f0a463" +dependencies = [ + "aes-gcm", + "async-trait", + "axum", + "base64 0.22.1", + "bytes", + "chrono", + "cookie", + "dashmap 6.1.0", + "forwarded-header-value", + "futures", + "hmac", + "http", + "http-body", + "rand", + "serde", + "serde_json", + "sha2", + "thiserror", + "tokio", + "tower-layer", + "tower-service", + "tracing", + "uuid", +] + +[[package]] +name = "axum_session_auth" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "129764f6aecc0e73490db71d50695d9b9f09160b25bc219521210ed605315137" +dependencies = [ + "anyhow", + "async-recursion", + "async-trait", + "axum-core", + "axum_session", + "bytes", + "chrono", + "dashmap 6.1.0", + "futures", + "http", + "http-body", + "serde", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum_session_surreal" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f1fd0c3084f929dd0a3351b7879b8b59f5b3ecd1cac25e93a812e76980dcfb" +dependencies = [ + "async-trait", + "axum_session", + "chrono", + "surrealdb", +] + [[package]] name = "axum_typed_multipart" version = "0.12.1" @@ -1148,6 +1250,21 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "aes-gcm", + "base64 0.22.1", + "percent-encoding", + "rand", + "subtle", + "time", + "version_check", +] + [[package]] name = "cookie-factory" version = "0.3.3" @@ -1217,9 +1334,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "darling" version = "0.20.10" @@ -1268,6 +1395,20 @@ dependencies = [ "parking_lot_core", ] +[[package]] +name = "dashmap" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" +dependencies = [ + "cfg-if", + "crossbeam-utils", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", +] + [[package]] name = "data-encoding" version = "2.6.0" @@ -1605,6 +1746,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "forwarded-header-value" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835f84f38484cc86f110a805655697908257fb9a7af005234060891557198e9" +dependencies = [ + "nonempty", + "thiserror", +] + [[package]] name = "fragile" version = "2.0.0" @@ -1828,6 +1979,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.31.0" @@ -2086,7 +2247,7 @@ dependencies = [ "pin-project-lite", "socket2 0.5.7", "tokio", - "tower", + "tower 0.4.13", "tower-service", "tracing", ] @@ -2640,6 +2801,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonempty" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e591e719385e6ebaeb5ce5d3887f7d5676fceca6411d1925ccc95745f3d6f7" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -2749,6 +2916,12 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.1.5" @@ -3141,6 +3314,18 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3538,7 +3723,7 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper", "tokio", "tokio-rustls", "tokio-util", @@ -4412,7 +4597,7 @@ dependencies = [ "cedar-policy", "chrono", "ciborium", - "dashmap", + "dashmap 5.5.3", "deunicode", "dmp", "fst", @@ -4515,12 +4700,6 @@ dependencies = [ "syn 2.0.87", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.1" @@ -4835,6 +5014,21 @@ dependencies = [ "tokio", "tower-layer", "tower-service", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", "tracing", ] @@ -5119,6 +5313,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -5639,8 +5843,12 @@ checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" name = "zettle_db" version = "0.1.0" dependencies = [ + "anyhow", "async-openai", "axum", + "axum_session", + "axum_session_auth", + "axum_session_surreal", "axum_typed_multipart", "futures", "lapin", diff --git a/Cargo.toml b/Cargo.toml index 21a549b..b405e5e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,8 +4,12 @@ version = "0.1.0" edition = "2021" [dependencies] +anyhow = "1.0.94" async-openai = "0.24.1" axum = { version = "0.7.5", features = ["multipart", "macros"] } +axum_session = "0.14.4" +axum_session_auth = "0.14.1" +axum_session_surreal = "0.2.1" axum_typed_multipart = "0.12.1" futures = "0.3.31" lapin = { version = "2.5.0", features = ["serde_json"] } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a982a47 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,171 @@ +{ + "name": "zettle_db", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "daisyui": "^4.12.20" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/css-selector-tokenizer": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/css-selector-tokenizer/-/css-selector-tokenizer-0.8.0.tgz", + "integrity": "sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "fastparse": "^1.1.2" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/culori": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/culori/-/culori-3.3.0.tgz", + "integrity": "sha512-pHJg+jbuFsCjz9iclQBqyL3B2HLCBF71BwVNujUYEvCeQMvV97R59MNK3R2+jgJ3a1fcZgI9B3vYgz8lzr/BFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/daisyui": { + "version": "4.12.20", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-4.12.20.tgz", + "integrity": "sha512-uHr3SQsd2yTjRdVuswTiqGFvZTxX0sGSBRa8JJdbKgmZCk/kRFh4B7Z2jg9vLIdwsHTHPyPgCkZadQo1ce0tAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-selector-tokenizer": "^0.8", + "culori": "^3", + "picocolors": "^1", + "postcss-js": "^4" + }, + "engines": { + "node": ">=16.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/daisyui" + } + }, + "node_modules/fastparse": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", + "integrity": "sha512-483XLLxTVIwWK3QTrMGRqUfUpoOs/0hbQrl2oz4J0pAcm3A3bu84wxTFqGqkJzewCLdME38xJLJAxBABfQT8sQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..eefb4a0 --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "daisyui": "^4.12.20" + } +} diff --git a/src/auth.rs b/src/auth.rs new file mode 100644 index 0000000..536bc67 --- /dev/null +++ b/src/auth.rs @@ -0,0 +1,152 @@ +// use crate::{error::ApiError, server::routes::auth::SignupParams, storage::db::SurrealDbClient}; +// use axum::async_trait; +// use axum_session_auth::Authentication; +// use serde::{Deserialize, Serialize}; +// use surrealdb::{ +// engine::any::Any, +// opt::auth::{Database, Namespace, Record}, +// Object, Surreal, +// }; +// use tracing::info; +// use uuid::Uuid; + +// #[derive(Deserialize, Serialize)] +// pub struct AuthParams { +// email: String, +// password: String, +// user_id: String, +// } + +// #[derive(Debug, Clone, Serialize, Deserialize)] +// pub struct User { +// pub user_id: String, +// pub email: String, +// #[serde(default)] +// pub anonymous: bool, +// } + +// impl Default for User { +// fn default() -> Self { +// Self { +// user_id: "user:guest".into(), +// email: "guest@example.com".into(), +// anonymous: true, +// } +// } +// } + +// #[async_trait] +// impl Authentication> for User { +// async fn load_user(userid: i64, pool: Option<&Surreal>) -> Result { +// let pool = pool.unwrap(); +// User::get_user(userid, pool) +// .await +// .ok_or_else(|| anyhow::anyhow!("Could not load user")) +// } + +// fn is_authenticated(&self) -> bool { +// !self.anonymous +// } + +// fn is_active(&self) -> bool { +// !self.anonymous +// } + +// fn is_anonymous(&self) -> bool { +// self.anonymous +// } +// } + +// impl User { +// // pub async fn get_user_by_email( +// // email: &str, +// // db: &SurrealDbClient, +// // ) -> Result, ApiError> { +// // info!("First, let's see what records exist"); +// // let debug_query: Vec = db.select("users").await?; +// // // let debug_query: Vec = db.client.query("SELECT * FROM user").await?.take(0)?; +// // info!("All users in database: {:?}", debug_query); + +// // // let tables: Vec = db.client.query("INFO FOR DB").await?.take(0)?; +// // // info!("Available tables: {:?}", tables); + +// // // Modified query to match exactly how the record is stored +// // let user: Option = db +// // .client +// // .query("SELECT * FROM user WHERE email = $email LIMIT 1") +// // .bind(("email", email.to_string())) +// // .await? +// // .take(0)?; + +// // info!("Found user: {:?}", user); + +// // Ok(user) +// // } + +// pub async fn get_user(id: i64, pool: &Surreal) -> Option { +// let user: Option = pool +// .query("SELECT * FROM user WHERE user_id = $user_id") +// .bind(("user_id", format!("user:{}", id))) +// .await +// .ok()? +// .take(0) +// .ok()?; + +// user +// } + +// pub async fn signin(params: SignupParams, db: &SurrealDbClient) -> Result<(), ApiError> { +// info!("Trying to sign in"); +// let result = db +// .client +// .signin(Record { +// access: "account", +// namespace: "test", + +// database: "test", +// params: SignupParams { +// email: params.email, +// password: params.password, +// }, +// }) +// .await?; + +// info!("{:?}", result.into_insecure_token()); +// Ok(()) +// } + +// pub async fn signup(params: SignupParams, db: &SurrealDbClient) -> Result { +// // First check if user already exists +// if let Some(_) = Self::get_user_by_email(¶ms.email, db).await? { +// return Err(ApiError::UserAlreadyExists); +// } + +// // Use SurrealDB's built-in signup +// let signup_response = db +// .client +// .signup(Record { +// access: "account", +// namespace: "test", +// database: "test", +// params: AuthParams { +// email: params.email.clone(), +// password: params.password.clone(), +// user_id: Uuid::new_v4().to_string(), +// }, +// }) +// .await?; + +// info!("Signup response: {:?}", signup_response); + +// // Wait a moment to ensure the record is created +// tokio::time::sleep(tokio::time::Duration::from_millis(300)).await; + +// Self::signin(params, db).await?; +// // Fetch the created user +// // let user = Self::get_user_by_email(¶ms.email, db) +// // .await? +// // .ok_or(ApiError::UserNotFound)?; + +// Ok(User::default()) +// } +// } diff --git a/src/bin/server.rs b/src/bin/server.rs index 85cdc0f..ee6cb0b 100644 --- a/src/bin/server.rs +++ b/src/bin/server.rs @@ -1,23 +1,33 @@ use axum::{ extract::DefaultBodyLimit, + http::Method, routing::{get, post}, Router, }; +use axum_session::{SessionConfig, SessionLayer, SessionStore}; +use axum_session_auth::{Auth, AuthConfig, AuthSession, AuthSessionLayer, Rights}; +use axum_session_surreal::SessionSurrealPool; use std::sync::Arc; +use surrealdb::{engine::any::Any, Surreal}; use tera::Tera; use tower_http::services::ServeDir; +use tracing::info; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; use zettle_db::{ rabbitmq::{consumer::RabbitMQConsumer, publisher::RabbitMQProducer, RabbitMQConfig}, server::{ routes::{ - file::upload_handler, index::index_handler, ingress::ingress_handler, - query::query_handler, queue_length::queue_length_handler, + auth::{show_signup_form, signup_handler}, + file::upload_handler, + index::index_handler, + ingress::ingress_handler, + query::query_handler, + queue_length::queue_length_handler, search_result::search_result_handler, }, AppState, }, - storage::db::SurrealDbClient, + storage::{db::SurrealDbClient, types::user::User}, }; #[tokio::main(flavor = "multi_thread", worker_threads = 2)] @@ -44,11 +54,32 @@ async fn main() -> Result<(), Box> { tera: Arc::new(Tera::new("src/server/templates/**/*.html").unwrap()), openai_client: Arc::new(async_openai::Client::new()), }; + // app_state.surreal_db_client.query("DELETE user").await?; + + // setup_auth(&app_state.surreal_db_client).await?; + + let session_config = SessionConfig::default() + .with_table_name("test_session_table") + .with_secure(false); + let auth_config = AuthConfig::::default(); + + let session_store: SessionStore> = SessionStore::new( + Some(app_state.surreal_db_client.client.clone().into()), + session_config, + ) + .await?; // Create Axum router let app = Router::new() .nest("/api/v1", api_routes_v1()) - .nest("", html_routes()) + .nest( + "/", + html_routes( + session_store, + auth_config, + app_state.surreal_db_client.client.clone(), + ), + ) .with_state(app_state); tracing::info!("Listening on 0.0.0.0:3000"); @@ -72,9 +103,34 @@ fn api_routes_v1() -> Router { } /// Router for HTML endpoints -fn html_routes() -> Router { +/// +fn html_routes( + session_store: SessionStore>, + auth_config: AuthConfig, + db_client: Surreal, +) -> Router { Router::new() .route("/", get(index_handler)) .route("/search", get(search_result_handler)) + .route("/signup", get(show_signup_form).post(signup_handler)) .nest_service("/assets", ServeDir::new("src/server/assets")) + .layer( + AuthSessionLayer::, Surreal>::new(Some( + db_client, + )) + .with_config(auth_config), + ) + .layer(SessionLayer::new(session_store)) } + +// async fn setup_auth(db: &SurrealDbClient) -> Result<(), Box> { +// db.query( +// "DEFINE TABLE user SCHEMALESS; +// DEFINE INDEX unique_name ON TABLE user FIELDS email UNIQUE; +// DEFINE ACCESS account ON DATABASE TYPE RECORD +// SIGNUP ( CREATE user SET email = $email, password = crypto::argon2::generate($password), anonymous = false, user_id = $user_id) +// SIGNIN ( SELECT * FROM user WHERE email = $email AND crypto::argon2::compare(password, $password) );", +// ) +// .await?; +// Ok(()) +// } diff --git a/src/error.rs b/src/error.rs index e0b9160..5c5b9b2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -60,16 +60,25 @@ pub enum ApiError { OpenAIerror(#[from] OpenAIError), #[error("File error: {0}")] FileError(#[from] FileError), + #[error("SurrealDb error: {0}")] + SurrealDbError(#[from] surrealdb::Error), + #[error("User already exists")] + UserAlreadyExists, + #[error("User was not found")] + UserNotFound, } impl IntoResponse for ApiError { fn into_response(self) -> axum::response::Response { let (status, error_message) = match &self { ApiError::ProcessingError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), + ApiError::SurrealDbError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), ApiError::PublishingError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), ApiError::DatabaseError(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), ApiError::OpenAIerror(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()), ApiError::QueryError(_) => (StatusCode::BAD_REQUEST, self.to_string()), + ApiError::UserAlreadyExists => (StatusCode::BAD_REQUEST, self.to_string()), + ApiError::UserNotFound => (StatusCode::BAD_REQUEST, self.to_string()), ApiError::IngressContentError(_) => { (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()) } diff --git a/src/ingress/analysis/ingress_analyser.rs b/src/ingress/analysis/ingress_analyser.rs index 26bcc2f..6d47f69 100644 --- a/src/ingress/analysis/ingress_analyser.rs +++ b/src/ingress/analysis/ingress_analyser.rs @@ -10,20 +10,20 @@ use async_openai::types::{ ResponseFormatJsonSchema, }; use serde_json::json; -use surrealdb::engine::remote::ws::Client; +use surrealdb::engine::any::Any; use surrealdb::Surreal; use tracing::debug; use super::types::llm_analysis_result::LLMGraphAnalysisResult; pub struct IngressAnalyzer<'a> { - db_client: &'a Surreal, + db_client: &'a Surreal, openai_client: &'a async_openai::Client, } impl<'a> IngressAnalyzer<'a> { pub fn new( - db_client: &'a Surreal, + db_client: &'a Surreal, openai_client: &'a async_openai::Client, ) -> Self { Self { diff --git a/src/lib.rs b/src/lib.rs index 8fc7ad7..c679302 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ +pub mod auth; pub mod error; pub mod ingress; pub mod rabbitmq; diff --git a/src/retrieval/graph.rs b/src/retrieval/graph.rs index ee7f1a7..4a61bc3 100644 --- a/src/retrieval/graph.rs +++ b/src/retrieval/graph.rs @@ -1,4 +1,4 @@ -use surrealdb::{engine::remote::ws::Client, Error, Surreal}; +use surrealdb::{engine::any::Any, Error, Surreal}; use tracing::debug; use crate::storage::types::{knowledge_entity::KnowledgeEntity, StoredObject}; @@ -33,7 +33,7 @@ use crate::storage::types::{knowledge_entity::KnowledgeEntity, StoredObject}; pub async fn find_entities_by_source_ids( source_id: Vec, table_name: String, - db_client: &Surreal, + db_client: &Surreal, ) -> Result, Error> where T: for<'de> serde::Deserialize<'de>, @@ -50,7 +50,7 @@ where /// Find entities by their relationship to the id pub async fn find_entities_by_relationship_by_id( - db_client: &Surreal, + db_client: &Surreal, entity_id: String, ) -> Result, Error> { let query = format!( @@ -65,7 +65,7 @@ pub async fn find_entities_by_relationship_by_id( /// Get a specific KnowledgeEntity by its id pub async fn get_entity_by_id( - db_client: &Surreal, + db_client: &Surreal, entity_id: &str, ) -> Result, Error> { db_client diff --git a/src/retrieval/mod.rs b/src/retrieval/mod.rs index b4a9788..8f9ba91 100644 --- a/src/retrieval/mod.rs +++ b/src/retrieval/mod.rs @@ -11,7 +11,7 @@ use crate::{ }; use futures::future::{try_join, try_join_all}; use std::collections::HashMap; -use surrealdb::{engine::remote::ws::Client, Surreal}; +use surrealdb::{engine::any::Any, Surreal}; /// Performs a comprehensive knowledge entity retrieval using multiple search strategies /// to find the most relevant entities for a given query. @@ -34,7 +34,7 @@ use surrealdb::{engine::remote::ws::Client, Surreal}; /// * `Result, ProcessingError>` - A deduplicated vector of relevant /// knowledge entities, or an error if the retrieval process fails pub async fn combined_knowledge_entity_retrieval( - db_client: &Surreal, + db_client: &Surreal, openai_client: &async_openai::Client, query: &str, ) -> Result, ProcessingError> { diff --git a/src/retrieval/vector.rs b/src/retrieval/vector.rs index 53b7dbf..7a131f9 100644 --- a/src/retrieval/vector.rs +++ b/src/retrieval/vector.rs @@ -1,4 +1,4 @@ -use surrealdb::{engine::remote::ws::Client, Surreal}; +use surrealdb::{engine::any::Any, Surreal}; use crate::{error::ProcessingError, utils::embedding::generate_embedding}; @@ -25,7 +25,7 @@ use crate::{error::ProcessingError, utils::embedding::generate_embedding}; pub async fn find_items_by_vector_similarity( take: u8, input_text: &str, - db_client: &Surreal, + db_client: &Surreal, table: String, openai_client: &async_openai::Client, ) -> Result, ProcessingError> diff --git a/src/server/assets/style.css b/src/server/assets/style.css index d5a548e..7aae006 100644 --- a/src/server/assets/style.css +++ b/src/server/assets/style.css @@ -554,6 +554,109 @@ video { display: none; } +:root, +[data-theme] { + background-color: var(--fallback-b1,oklch(var(--b1)/1)); + color: var(--fallback-bc,oklch(var(--bc)/1)); +} + +@supports not (color: oklch(0% 0 0)) { + :root { + color-scheme: light; + --fallback-p: #491eff; + --fallback-pc: #d4dbff; + --fallback-s: #ff41c7; + --fallback-sc: #fff9fc; + --fallback-a: #00cfbd; + --fallback-ac: #00100d; + --fallback-n: #2b3440; + --fallback-nc: #d7dde4; + --fallback-b1: #ffffff; + --fallback-b2: #e5e6e6; + --fallback-b3: #e5e6e6; + --fallback-bc: #1f2937; + --fallback-in: #00b3f0; + --fallback-inc: #000000; + --fallback-su: #00ca92; + --fallback-suc: #000000; + --fallback-wa: #ffc22d; + --fallback-wac: #000000; + --fallback-er: #ff6f70; + --fallback-erc: #000000; + } + + @media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --fallback-p: #7582ff; + --fallback-pc: #050617; + --fallback-s: #ff71cf; + --fallback-sc: #190211; + --fallback-a: #00c7b5; + --fallback-ac: #000e0c; + --fallback-n: #2a323c; + --fallback-nc: #a6adbb; + --fallback-b1: #1d232a; + --fallback-b2: #191e24; + --fallback-b3: #15191e; + --fallback-bc: #a6adbb; + --fallback-in: #00b3f0; + --fallback-inc: #000000; + --fallback-su: #00ca92; + --fallback-suc: #000000; + --fallback-wa: #ffc22d; + --fallback-wac: #000000; + --fallback-er: #ff6f70; + --fallback-erc: #000000; + } + } +} + +html { + -webkit-tap-highlight-color: transparent; +} + +* { + scrollbar-color: color-mix(in oklch, currentColor 35%, transparent) transparent; +} + +*:hover { + scrollbar-color: color-mix(in oklch, currentColor 60%, transparent) transparent; +} + +:root { + color-scheme: dark; + --in: 72.06% 0.191 231.6; + --su: 64.8% 0.150 160; + --wa: 84.71% 0.199 83.87; + --er: 71.76% 0.221 22.18; + --pc: 13.138% 0.0392 275.75; + --sc: 14.96% 0.052 342.55; + --ac: 14.902% 0.0334 183.61; + --inc: 0% 0 0; + --suc: 0% 0 0; + --wac: 0% 0 0; + --erc: 0% 0 0; + --rounded-box: 1rem; + --rounded-btn: 0.5rem; + --rounded-badge: 1.9rem; + --animation-btn: 0.25s; + --animation-input: .2s; + --btn-focus-scale: 0.95; + --border-btn: 1px; + --tab-border: 1px; + --tab-radius: 0.5rem; + --p: 65.69% 0.196 275.75; + --s: 74.8% 0.26 342.55; + --a: 74.51% 0.167 183.61; + --n: 31.3815% 0.021108 254.139175; + --nc: 74.6477% 0.0216 264.435964; + --b1: 25.3267% 0.015896 252.417568; + --b2: 23.2607% 0.013807 253.100675; + --b3: 21.1484% 0.01165 254.087939; + --bc: 74.6477% 0.0216 264.435964; +} + .container { width: 100%; } @@ -588,8 +691,1326 @@ video { } } -.absolute { +.avatar { + position: relative; + display: inline-flex; +} + +.avatar > div { + display: block; + aspect-ratio: 1 / 1; + overflow: hidden; +} + +.avatar img { + height: 100%; + width: 100%; + -o-object-fit: cover; + object-fit: cover; +} + +.avatar.placeholder > div { + display: flex; + align-items: center; + justify-content: center; +} + +@media (hover:hover) { + .label a:hover { + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + } + + .menu li > *:not(ul, .menu-title, details, .btn):active, +.menu li > *:not(ul, .menu-title, details, .btn).active, +.menu li > details > summary:active { + --tw-bg-opacity: 1; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-text-opacity: 1; + color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); + } +} + +.btn { + display: inline-flex; + height: 3rem; + min-height: 3rem; + flex-shrink: 0; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + flex-wrap: wrap; + align-items: center; + justify-content: center; + border-radius: var(--rounded-btn, 0.5rem); + border-color: transparent; + border-color: oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity)); + padding-left: 1rem; + padding-right: 1rem; + text-align: center; + font-size: 0.875rem; + line-height: 1em; + gap: 0.5rem; + font-weight: 600; + text-decoration-line: none; + transition-duration: 200ms; + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + border-width: var(--border-btn, 1px); + transition-property: color, background-color, border-color, opacity, box-shadow, transform; + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + outline-color: var(--fallback-bc,oklch(var(--bc)/1)); + background-color: oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity)); + --tw-bg-opacity: 1; + --tw-border-opacity: 1; +} + +.btn-disabled, + .btn[disabled], + .btn:disabled { + pointer-events: none; +} + +:where(.btn:is(input[type="checkbox"])), +:where(.btn:is(input[type="radio"])) { + width: auto; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; +} + +.btn:is(input[type="checkbox"]):after, +.btn:is(input[type="radio"]):after { + --tw-content: attr(aria-label); + content: var(--tw-content); +} + +.card { + position: relative; + display: flex; + flex-direction: column; + border-radius: var(--rounded-box, 1rem); +} + +.card:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.card-body { + display: flex; + flex: 1 1 auto; + flex-direction: column; + padding: var(--padding-card, 2rem); + gap: 0.5rem; +} + +.card-body :where(p) { + flex-grow: 1; +} + +.card figure { + display: flex; + align-items: center; + justify-content: center; +} + +.card.image-full { + display: grid; +} + +.card.image-full:before { + position: relative; + content: ""; + z-index: 10; + border-radius: var(--rounded-box, 1rem); + --tw-bg-opacity: 1; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + opacity: 0.75; +} + +.card.image-full:before, + .card.image-full > * { + grid-column-start: 1; + grid-row-start: 1; +} + +.card.image-full > figure img { + height: 100%; + -o-object-fit: cover; + object-fit: cover; +} + +.card.image-full > .card-body { + position: relative; + z-index: 20; + --tw-text-opacity: 1; + color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); +} + +.chat { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + -moz-column-gap: 0.75rem; + column-gap: 0.75rem; + padding-top: 0.25rem; + padding-bottom: 0.25rem; +} + +.chat-image { + grid-row: span 2 / span 2; + align-self: flex-end; +} + +.chat-bubble { + position: relative; + display: block; + width: -moz-fit-content; + width: fit-content; + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + max-width: 90%; + border-radius: var(--rounded-box, 1rem); + min-height: 2.75rem; + min-width: 2.75rem; + --tw-bg-opacity: 1; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-text-opacity: 1; + color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); +} + +.chat-bubble:before { position: absolute; + bottom: 0px; + height: 0.75rem; + width: 0.75rem; + background-color: inherit; + content: ""; + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; +} + +.chat-start { + place-items: start; + grid-template-columns: auto 1fr; +} + +.chat-start .chat-header { + grid-column-start: 2; +} + +.chat-start .chat-footer { + grid-column-start: 2; +} + +.chat-start .chat-image { + grid-column-start: 1; +} + +.chat-start .chat-bubble { + grid-column-start: 2; + border-end-start-radius: 0px; +} + +.chat-start .chat-bubble:before { + -webkit-mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 3 3 L 3 0 C 3 1 1 3 0 3'/%3e%3c/svg%3e"); + mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 3 3 L 3 0 C 3 1 1 3 0 3'/%3e%3c/svg%3e"); + inset-inline-start: -0.749rem; +} + +[dir="rtl"] .chat-start .chat-bubble:before { + -webkit-mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 1 3 L 3 3 C 2 3 0 1 0 0'/%3e%3c/svg%3e"); + mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 1 3 L 3 3 C 2 3 0 1 0 0'/%3e%3c/svg%3e"); +} + +.chat-end .chat-image { + grid-column-start: 2; +} + +.chat-end .chat-bubble { + grid-column-start: 1; + border-end-end-radius: 0px; +} + +.chat-end .chat-bubble:before { + -webkit-mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 1 3 L 3 3 C 2 3 0 1 0 0'/%3e%3c/svg%3e"); + mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 1 3 L 3 3 C 2 3 0 1 0 0'/%3e%3c/svg%3e"); + inset-inline-start: 99.9%; +} + +[dir="rtl"] .chat-end .chat-bubble:before { + -webkit-mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 3 3 L 3 0 C 3 1 1 3 0 3'/%3e%3c/svg%3e"); + mask-image: url("data:image/svg+xml,%3csvg width='3' height='3' xmlns='http://www.w3.org/2000/svg'%3e%3cpath fill='black' d='m 0 3 L 3 3 L 3 0 C 3 1 1 3 0 3'/%3e%3c/svg%3e"); +} + +.divider { + display: flex; + flex-direction: row; + align-items: center; + align-self: stretch; + margin-top: 1rem; + margin-bottom: 1rem; + height: 1rem; + white-space: nowrap; +} + +.divider:before, + .divider:after { + height: 0.125rem; + width: 100%; + flex-grow: 1; + --tw-content: ''; + content: var(--tw-content); + background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); +} + +@media (hover: hover) { + .btm-nav > *.disabled:hover, + .btm-nav > *[disabled]:hover { + pointer-events: none; + --tw-border-opacity: 0; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-bg-opacity: 0.1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-text-opacity: 0.2; + } + + .btn:hover { + --tw-border-opacity: 1; + border-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn:hover { + background-color: color-mix( + in oklab, + oklch(var(--btn-color, var(--b2)) / var(--tw-bg-opacity, 1)) 90%, + black + ); + border-color: color-mix( + in oklab, + oklch(var(--btn-color, var(--b2)) / var(--tw-border-opacity, 1)) 90%, + black + ); + } + } + + @supports not (color: oklch(0% 0 0)) { + .btn:hover { + background-color: var(--btn-color, var(--fallback-b2)); + border-color: var(--btn-color, var(--fallback-b2)); + } + } + + .btn.glass:hover { + --glass-opacity: 25%; + --glass-border-opacity: 15%; + } + + .btn-ghost:hover { + border-color: transparent; + } + + @supports (color: oklch(0% 0 0)) { + .btn-ghost:hover { + background-color: var(--fallback-bc,oklch(var(--bc)/0.2)); + } + } + + .btn-outline.btn-primary:hover { + --tw-text-opacity: 1; + color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-primary:hover { + background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); + } + } + + .btn-disabled:hover, + .btn[disabled]:hover, + .btn:disabled:hover { + --tw-border-opacity: 0; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-bg-opacity: 0.2; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-text-opacity: 0.2; + } + + @supports (color: color-mix(in oklab, black, black)) { + .btn:is(input[type="checkbox"]:checked):hover, .btn:is(input[type="radio"]:checked):hover { + background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); + } + } + + :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { + cursor: pointer; + outline: 2px solid transparent; + outline-offset: 2px; + } + + @supports (color: oklch(0% 0 0)) { + :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(.active, .btn):hover, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(.active, .btn):hover { + background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); + } + } +} + +.form-control { + display: flex; + flex-direction: column; +} + +.label { + display: flex; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + align-items: center; + justify-content: space-between; + padding-left: 0.25rem; + padding-right: 0.25rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + +.indicator { + position: relative; + display: inline-flex; + width: -moz-max-content; + width: max-content; +} + +.indicator :where(.indicator-item) { + z-index: 1; + position: absolute; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + white-space: nowrap; +} + +.input { + flex-shrink: 1; + -webkit-appearance: none; + -moz-appearance: none; + appearance: none; + height: 3rem; + padding-left: 1rem; + padding-right: 1rem; + font-size: 1rem; + line-height: 2; + line-height: 1.5rem; + border-radius: var(--rounded-btn, 0.5rem); + border-width: 1px; + border-color: transparent; + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); +} + +.input[type="number"]::-webkit-inner-spin-button, +.input-md[type="number"]::-webkit-inner-spin-button { + margin-top: -1rem; + margin-bottom: -1rem; + margin-inline-end: -1rem; +} + +.join { + display: inline-flex; + align-items: stretch; + border-radius: var(--rounded-btn, 0.5rem); +} + +.join :where(.join-item) { + border-start-end-radius: 0; + border-end-end-radius: 0; + border-end-start-radius: 0; + border-start-start-radius: 0; +} + +.join .join-item:not(:first-child):not(:last-child), + .join *:not(:first-child):not(:last-child) .join-item { + border-start-end-radius: 0; + border-end-end-radius: 0; + border-end-start-radius: 0; + border-start-start-radius: 0; +} + +.join .join-item:first-child:not(:last-child), + .join *:first-child:not(:last-child) .join-item { + border-start-end-radius: 0; + border-end-end-radius: 0; +} + +.join .dropdown .join-item:first-child:not(:last-child), + .join *:first-child:not(:last-child) .dropdown .join-item { + border-start-end-radius: inherit; + border-end-end-radius: inherit; +} + +.join :where(.join-item:first-child:not(:last-child)), + .join :where(*:first-child:not(:last-child) .join-item) { + border-end-start-radius: inherit; + border-start-start-radius: inherit; +} + +.join .join-item:last-child:not(:first-child), + .join *:last-child:not(:first-child) .join-item { + border-end-start-radius: 0; + border-start-start-radius: 0; +} + +.join :where(.join-item:last-child:not(:first-child)), + .join :where(*:last-child:not(:first-child) .join-item) { + border-start-end-radius: inherit; + border-end-end-radius: inherit; +} + +@supports not selector(:has(*)) { + :where(.join *) { + border-radius: inherit; + } +} + +@supports selector(:has(*)) { + :where(.join *:has(.join-item)) { + border-radius: inherit; + } +} + +.link { + cursor: pointer; + text-decoration-line: underline; +} + +.menu { + display: flex; + flex-direction: column; + flex-wrap: wrap; + font-size: 0.875rem; + line-height: 1.25rem; + padding: 0.5rem; +} + +.menu :where(li ul) { + position: relative; + white-space: nowrap; + margin-inline-start: 1rem; + padding-inline-start: 0.5rem; +} + +.menu :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), .menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) { + display: grid; + grid-auto-flow: column; + align-content: flex-start; + align-items: center; + gap: 0.5rem; + grid-auto-columns: minmax(auto, max-content) auto max-content; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; +} + +.menu li.disabled { + cursor: not-allowed; + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + color: var(--fallback-bc,oklch(var(--bc)/0.3)); +} + +.menu :where(li > .menu-dropdown:not(.menu-dropdown-show)) { + display: none; +} + +:where(.menu li) { + position: relative; + display: flex; + flex-shrink: 0; + flex-direction: column; + flex-wrap: wrap; + align-items: stretch; +} + +:where(.menu li) .badge { + justify-self: end; +} + +.navbar { + display: flex; + align-items: center; + padding: var(--navbar-padding, 0.5rem); + min-height: 4rem; + width: 100%; +} + +:where(.navbar > *:not(script, style)) { + display: inline-flex; + align-items: center; +} + +.avatar-group :where(.avatar) { + overflow: hidden; + border-radius: 9999px; + border-width: 4px; + --tw-border-opacity: 1; + border-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-border-opacity))); +} + +.btm-nav > *.disabled, + .btm-nav > *[disabled] { + pointer-events: none; + --tw-border-opacity: 0; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-bg-opacity: 0.1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-text-opacity: 0.2; +} + +.btm-nav > * .label { + font-size: 1rem; + line-height: 1.5rem; +} + +@media (prefers-reduced-motion: no-preference) { + .btn { + animation: button-pop var(--animation-btn, 0.25s) ease-out; + } +} + +.btn:active:hover, + .btn:active:focus { + animation: button-pop 0s ease-out; + transform: scale(var(--btn-focus-scale, 0.97)); +} + +@supports not (color: oklch(0% 0 0)) { + .btn { + background-color: var(--btn-color, var(--fallback-b2)); + border-color: var(--btn-color, var(--fallback-b2)); + } + + .btn-primary { + --btn-color: var(--fallback-p); + } +} + +@supports (color: color-mix(in oklab, black, black)) { + .btn-outline.btn-primary.btn-active { + background-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); + border-color: color-mix(in oklab, var(--fallback-p,oklch(var(--p)/1)) 90%, black); + } +} + +.btn:focus-visible { + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; +} + +.btn-primary { + --tw-text-opacity: 1; + color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); + outline-color: var(--fallback-p,oklch(var(--p)/1)); +} + +@supports (color: oklch(0% 0 0)) { + .btn-primary { + --btn-color: var(--p); + } +} + +.btn.glass { + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + outline-color: currentColor; +} + +.btn.glass.btn-active { + --glass-opacity: 25%; + --glass-border-opacity: 15%; +} + +.btn-ghost { + border-width: 1px; + border-color: transparent; + background-color: transparent; + color: currentColor; + --tw-shadow: 0 0 #0000; + --tw-shadow-colored: 0 0 #0000; + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); + outline-color: currentColor; +} + +.btn-ghost.btn-active { + border-color: transparent; + background-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + +.btn-outline.btn-primary { + --tw-text-opacity: 1; + color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity))); +} + +.btn-outline.btn-primary.btn-active { + --tw-text-opacity: 1; + color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); +} + +.btn.btn-disabled, + .btn[disabled], + .btn:disabled { + --tw-border-opacity: 0; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-bg-opacity: 0.2; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + --tw-text-opacity: 0.2; +} + +.btn:is(input[type="checkbox"]:checked), +.btn:is(input[type="radio"]:checked) { + --tw-border-opacity: 1; + border-color: var(--fallback-p,oklch(var(--p)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); + --tw-text-opacity: 1; + color: var(--fallback-pc,oklch(var(--pc)/var(--tw-text-opacity))); +} + +.btn:is(input[type="checkbox"]:checked):focus-visible, .btn:is(input[type="radio"]:checked):focus-visible { + outline-color: var(--fallback-p,oklch(var(--p)/1)); +} + +@keyframes button-pop { + 0% { + transform: scale(var(--btn-focus-scale, 0.98)); + } + + 40% { + transform: scale(1.02); + } + + 100% { + transform: scale(1); + } +} + +.card :where(figure:first-child) { + overflow: hidden; + border-start-start-radius: inherit; + border-start-end-radius: inherit; + border-end-start-radius: unset; + border-end-end-radius: unset; +} + +.card :where(figure:last-child) { + overflow: hidden; + border-start-start-radius: unset; + border-start-end-radius: unset; + border-end-start-radius: inherit; + border-end-end-radius: inherit; +} + +.card:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; +} + +.card.bordered { + border-width: 1px; + --tw-border-opacity: 1; + border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); +} + +.card.compact .card-body { + padding: 1rem; + font-size: 0.875rem; + line-height: 1.25rem; +} + +.card-title { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: 1.25rem; + line-height: 1.75rem; + font-weight: 600; +} + +.card.image-full :where(figure) { + overflow: hidden; + border-radius: inherit; +} + +@keyframes checkmark { + 0% { + background-position-y: 5px; + } + + 50% { + background-position-y: -2px; + } + + 100% { + background-position-y: 0; + } +} + +.divider:not(:empty) { + gap: 1rem; +} + +.divider-primary:before, + .divider-primary:after { + --tw-bg-opacity: 1; + background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); +} + +.label-text { + font-size: 0.875rem; + line-height: 1.25rem; + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); +} + +.input input { + --tw-bg-opacity: 1; + background-color: var(--fallback-p,oklch(var(--p)/var(--tw-bg-opacity))); + background-color: transparent; +} + +.input input:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.input[list]::-webkit-calendar-picker-indicator { + line-height: 1em; +} + +.input-bordered { + border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + +.input:focus, + .input:focus-within { + box-shadow: none; + border-color: var(--fallback-bc,oklch(var(--bc)/0.2)); + outline-style: solid; + outline-width: 2px; + outline-offset: 2px; + outline-color: var(--fallback-bc,oklch(var(--bc)/0.2)); +} + +.input:has(> input[disabled]), + .input-disabled, + .input:disabled, + .input[disabled] { + cursor: not-allowed; + --tw-border-opacity: 1; + border-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-border-opacity))); + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); + color: var(--fallback-bc,oklch(var(--bc)/0.4)); +} + +.input:has(> input[disabled])::-moz-placeholder, .input-disabled::-moz-placeholder, .input:disabled::-moz-placeholder, .input[disabled]::-moz-placeholder { + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +.input:has(> input[disabled])::placeholder, + .input-disabled::placeholder, + .input:disabled::placeholder, + .input[disabled]::placeholder { + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-placeholder-opacity))); + --tw-placeholder-opacity: 0.2; +} + +.input:has(> input[disabled]) > input[disabled] { + cursor: not-allowed; +} + +.input::-webkit-date-and-time-value { + text-align: inherit; +} + +.join > :where(*:not(:first-child)) { + margin-top: 0px; + margin-bottom: 0px; + margin-inline-start: -1px; +} + +.join > :where(*:not(:first-child)):is(.btn) { + margin-inline-start: calc(var(--border-btn) * -1); +} + +.link-primary { + --tw-text-opacity: 1; + color: var(--fallback-p,oklch(var(--p)/var(--tw-text-opacity))); +} + +@supports (color:color-mix(in oklab,black,black)) { + @media (hover:hover) { + .link-primary:hover { + color: color-mix(in oklab,var(--fallback-p,oklch(var(--p)/1)) 80%,black); + } + } +} + +.link:focus { + outline: 2px solid transparent; + outline-offset: 2px; +} + +.link:focus-visible { + outline: 2px solid currentColor; + outline-offset: 2px; +} + +.loading { + pointer-events: none; + display: inline-block; + aspect-ratio: 1 / 1; + width: 1.5rem; + background-color: currentColor; + -webkit-mask-size: 100%; + mask-size: 100%; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + -webkit-mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E"); +} + +.loading-spinner { + -webkit-mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E"); + mask-image: url("data:image/svg+xml,%3Csvg width='24' height='24' stroke='black' viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cg transform-origin='center'%3E%3Ccircle cx='12' cy='12' r='9.5' fill='none' stroke-width='3' stroke-linecap='round'%3E%3CanimateTransform attributeName='transform' type='rotate' from='0 12 12' to='360 12 12' dur='2s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dasharray' values='0,150;42,150;42,150' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3Canimate attributeName='stroke-dashoffset' values='0;-16;-59' keyTimes='0;0.475;1' dur='1.5s' repeatCount='indefinite'/%3E%3C/circle%3E%3C/g%3E%3C/svg%3E"); +} + +:where(.menu li:empty) { + --tw-bg-opacity: 1; + background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); + opacity: 0.1; + margin: 0.5rem 1rem; + height: 1px; +} + +.menu :where(li ul):before { + position: absolute; + bottom: 0.75rem; + inset-inline-start: 0px; + top: 0.75rem; + width: 1px; + --tw-bg-opacity: 1; + background-color: var(--fallback-bc,oklch(var(--bc)/var(--tw-bg-opacity))); + opacity: 0.1; + content: ""; +} + +.menu :where(li:not(.menu-title) > *:not(ul, details, .menu-title, .btn)), +.menu :where(li:not(.menu-title) > details > summary:not(.menu-title)) { + border-radius: var(--rounded-btn, 0.5rem); + padding-left: 1rem; + padding-right: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + text-align: start; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, -webkit-backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter, -webkit-backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-timing-function: cubic-bezier(0, 0, 0.2, 1); + transition-duration: 200ms; + text-wrap: balance; +} + +:where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(summary, .active, .btn).focus, :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):not(summary, .active, .btn):focus, :where(.menu li:not(.menu-title, .disabled) > *:not(ul, details, .menu-title)):is(summary):not(.active, .btn):focus-visible, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(summary, .active, .btn).focus, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):not(summary, .active, .btn):focus, :where(.menu li:not(.menu-title, .disabled) > details > summary:not(.menu-title)):is(summary):not(.active, .btn):focus-visible { + cursor: pointer; + background-color: var(--fallback-bc,oklch(var(--bc)/0.1)); + --tw-text-opacity: 1; + color: var(--fallback-bc,oklch(var(--bc)/var(--tw-text-opacity))); + outline: 2px solid transparent; + outline-offset: 2px; +} + +.menu li > *:not(ul, .menu-title, details, .btn):active, +.menu li > *:not(ul, .menu-title, details, .btn).active, +.menu li > details > summary:active { + --tw-bg-opacity: 1; + background-color: var(--fallback-n,oklch(var(--n)/var(--tw-bg-opacity))); + --tw-text-opacity: 1; + color: var(--fallback-nc,oklch(var(--nc)/var(--tw-text-opacity))); +} + +.menu :where(li > details > summary)::-webkit-details-marker { + display: none; +} + +.menu :where(li > details > summary):after, +.menu :where(li > .menu-dropdown-toggle):after { + justify-self: end; + display: block; + margin-top: -0.5rem; + height: 0.5rem; + width: 0.5rem; + transform: rotate(45deg); + transition-property: transform, margin-top; + transition-duration: 0.3s; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + content: ""; + transform-origin: 75% 75%; + box-shadow: 2px 2px; + pointer-events: none; +} + +.menu :where(li > details[open] > summary):after, +.menu :where(li > .menu-dropdown-toggle.menu-dropdown-show):after { + transform: rotate(225deg); + margin-top: 0; +} + +.mockup-browser .mockup-browser-toolbar .input { + position: relative; + margin-left: auto; + margin-right: auto; + display: block; + height: 1.75rem; + width: 24rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + --tw-bg-opacity: 1; + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity))); + padding-left: 2rem; + direction: ltr; +} + +.mockup-browser .mockup-browser-toolbar .input:before { + content: ""; + position: absolute; + left: 0.5rem; + top: 50%; + aspect-ratio: 1 / 1; + height: 0.75rem; + --tw-translate-y: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + border-radius: 9999px; + border-width: 2px; + border-color: currentColor; + opacity: 0.6; +} + +.mockup-browser .mockup-browser-toolbar .input:after { + content: ""; + position: absolute; + left: 1.25rem; + top: 50%; + height: 0.5rem; + --tw-translate-y: 25%; + --tw-rotate: -45deg; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); + border-radius: 9999px; + border-width: 1px; + border-color: currentColor; + opacity: 0.6; +} + +@keyframes modal-pop { + 0% { + opacity: 0; + } +} + +@keyframes progress-loading { + 50% { + background-position-x: -115%; + } +} + +@keyframes radiomark { + 0% { + box-shadow: 0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset, + 0 0 0 12px var(--fallback-b1,oklch(var(--b1)/1)) inset; + } + + 50% { + box-shadow: 0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset, + 0 0 0 3px var(--fallback-b1,oklch(var(--b1)/1)) inset; + } + + 100% { + box-shadow: 0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset, + 0 0 0 4px var(--fallback-b1,oklch(var(--b1)/1)) inset; + } +} + +@keyframes rating-pop { + 0% { + transform: translateY(-0.125em); + } + + 40% { + transform: translateY(-0.125em); + } + + 100% { + transform: translateY(0); + } +} + +@keyframes skeleton { + from { + background-position: 150%; + } + + to { + background-position: -50%; + } +} + +@keyframes toast-pop { + 0% { + transform: scale(0.9); + opacity: 0; + } + + 100% { + transform: scale(1); + opacity: 1; + } +} + +.indicator :where(.indicator-item) { + bottom: auto; + inset-inline-end: 0px; + inset-inline-start: auto; + top: 0px; + --tw-translate-y: -50%; + --tw-translate-x: 50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.indicator :where(.indicator-item):where([dir="rtl"], [dir="rtl"] *) { + --tw-translate-x: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.indicator :where(.indicator-item.indicator-start) { + inset-inline-end: auto; + inset-inline-start: 0px; + --tw-translate-x: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.indicator :where(.indicator-item.indicator-start):where([dir="rtl"], [dir="rtl"] *) { + --tw-translate-x: 50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.indicator :where(.indicator-item.indicator-center) { + inset-inline-end: 50%; + inset-inline-start: 50%; + --tw-translate-x: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.indicator :where(.indicator-item.indicator-center):where([dir="rtl"], [dir="rtl"] *) { + --tw-translate-x: 50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.indicator :where(.indicator-item.indicator-end) { + inset-inline-end: 0px; + inset-inline-start: auto; + --tw-translate-x: 50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.indicator :where(.indicator-item.indicator-end):where([dir="rtl"], [dir="rtl"] *) { + --tw-translate-x: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.indicator :where(.indicator-item.indicator-bottom) { + bottom: 0px; + top: auto; + --tw-translate-y: 50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.indicator :where(.indicator-item.indicator-middle) { + bottom: 50%; + top: 50%; + --tw-translate-y: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.indicator :where(.indicator-item.indicator-top) { + bottom: auto; + top: 0px; + --tw-translate-y: -50%; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} + +.join.join-vertical { + flex-direction: column; +} + +.join.join-vertical .join-item:first-child:not(:last-child), + .join.join-vertical *:first-child:not(:last-child) .join-item { + border-end-start-radius: 0; + border-end-end-radius: 0; + border-start-start-radius: inherit; + border-start-end-radius: inherit; +} + +.join.join-vertical .join-item:last-child:not(:first-child), + .join.join-vertical *:last-child:not(:first-child) .join-item { + border-start-start-radius: 0; + border-start-end-radius: 0; + border-end-start-radius: inherit; + border-end-end-radius: inherit; +} + +.join.join-horizontal { + flex-direction: row; +} + +.join.join-horizontal .join-item:first-child:not(:last-child), + .join.join-horizontal *:first-child:not(:last-child) .join-item { + border-end-end-radius: 0; + border-start-end-radius: 0; + border-end-start-radius: inherit; + border-start-start-radius: inherit; +} + +.join.join-horizontal .join-item:last-child:not(:first-child), + .join.join-horizontal *:last-child:not(:first-child) .join-item { + border-end-start-radius: 0; + border-start-start-radius: 0; + border-end-end-radius: inherit; + border-start-end-radius: inherit; +} + +.menu-horizontal { + display: inline-flex; + flex-direction: row; +} + +.menu-horizontal > li:not(.menu-title) > details > ul { + position: absolute; +} + +.avatar.online:before { + content: ""; + position: absolute; + z-index: 10; + display: block; + border-radius: 9999px; + --tw-bg-opacity: 1; + background-color: var(--fallback-su,oklch(var(--su)/var(--tw-bg-opacity))); + outline-style: solid; + outline-width: 2px; + outline-color: var(--fallback-b1,oklch(var(--b1)/1)); + width: 15%; + height: 15%; + top: 7%; + right: 7%; +} + +.avatar.offline:before { + content: ""; + position: absolute; + z-index: 10; + display: block; + border-radius: 9999px; + --tw-bg-opacity: 1; + background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity))); + outline-style: solid; + outline-width: 2px; + outline-color: var(--fallback-b1,oklch(var(--b1)/1)); + width: 15%; + height: 15%; + top: 7%; + right: 7%; +} + +.card-compact .card-body { + padding: 1rem; + font-size: 0.875rem; + line-height: 1.25rem; +} + +.card-compact .card-title { + margin-bottom: 0.25rem; +} + +.card-normal .card-body { + padding: var(--padding-card, 2rem); + font-size: 1rem; + line-height: 1.5rem; +} + +.card-normal .card-title { + margin-bottom: 0.75rem; +} + +.join.join-vertical > :where(*:not(:first-child)) { + margin-left: 0px; + margin-right: 0px; + margin-top: -1px; +} + +.join.join-vertical > :where(*:not(:first-child)):is(.btn) { + margin-top: calc(var(--border-btn) * -1); +} + +.join.join-horizontal > :where(*:not(:first-child)) { + margin-top: 0px; + margin-bottom: 0px; + margin-inline-start: -1px; +} + +.join.join-horizontal > :where(*:not(:first-child)):is(.btn) { + margin-inline-start: calc(var(--border-btn) * -1); + margin-top: 0px; +} + +.menu-horizontal > li:not(.menu-title) > details > ul { + margin-inline-start: 0px; + margin-top: 1rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-inline-end: 0.5rem; +} + +.menu-horizontal > li > details > ul:before { + content: none; +} + +:where(.menu-horizontal > li:not(.menu-title) > details > ul) { + border-radius: var(--rounded-box, 1rem); + --tw-bg-opacity: 1; + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity))); + --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); } .relative { @@ -613,6 +2034,14 @@ video { margin-top: 2rem; } +.mb-4 { + margin-bottom: 1rem; +} + +.mt-6 { + margin-top: 1.5rem; +} + .block { display: block; } @@ -621,54 +2050,86 @@ video { display: flex; } -.h-full { - height: 100%; +.hidden { + display: none; } .h-16 { height: 4rem; } -.h-36 { - height: 9rem; -} - .h-auto { height: auto; } -.min-h-screen { - min-height: 100vh; -} - -.min-h-\[80vh\] { - min-height: 80vh; -} - -.min-h-16 { - min-height: 4rem; +.h-full { + height: 100%; } .min-h-36 { min-height: 9rem; } +.min-h-\[80vh\] { + min-height: 80vh; +} + +.min-h-screen { + min-height: 100vh; +} + +.min-h-\[6rem\] { + min-height: 6rem; +} + .w-full { width: 100%; } -.max-w-7xl { - max-width: 80rem; +.w-10 { + width: 2.5rem; +} + +.w-96 { + width: 24rem; +} + +.min-w-\[18rem\] { + min-width: 18rem; } .max-w-2xl { max-width: 42rem; } +.max-w-7xl { + max-width: 80rem; +} + +.max-w-xs { + max-width: 20rem; +} + +.max-w-4xl { + max-width: 56rem; +} + +.flex-1 { + flex: 1 1 0%; +} + +.flex-none { + flex: none; +} + .flex-col { flex-direction: column; } +.flex-wrap { + flex-wrap: wrap; +} + .items-center { align-items: center; } @@ -685,12 +2146,22 @@ video { gap: 1rem; } +.gap-2 { + gap: 0.5rem; +} + .space-x-4 > :not([hidden]) ~ :not([hidden]) { --tw-space-x-reverse: 0; margin-right: calc(1rem * var(--tw-space-x-reverse)); margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse))); } +.space-y-3 > :not([hidden]) ~ :not([hidden]) { + --tw-space-y-reverse: 0; + margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse))); + margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)); +} + .space-y-4 > :not([hidden]) ~ :not([hidden]) { --tw-space-y-reverse: 0; margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse))); @@ -703,40 +2174,46 @@ video { margin-bottom: calc(2rem * var(--tw-space-y-reverse)); } -.space-y-3 > :not([hidden]) ~ :not([hidden]) { - --tw-space-y-reverse: 0; - margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse))); - margin-bottom: calc(0.75rem * var(--tw-space-y-reverse)); +.overflow-x-auto { + overflow-x: auto; +} + +.overflow-x-hidden { + overflow-x: hidden; } .break-words { overflow-wrap: break-word; } -.rounded { - border-radius: 0.25rem; +.rounded-lg { + border-radius: 0.5rem; } .rounded-md { border-radius: 0.375rem; } -.rounded-lg { - border-radius: 0.5rem; -} - .rounded-xl { border-radius: 0.75rem; } -.rounded-s { - border-start-start-radius: 0.25rem; - border-end-start-radius: 0.25rem; +.rounded-full { + border-radius: 9999px; } -.rounded-b-md { - border-bottom-right-radius: 0.375rem; - border-bottom-left-radius: 0.375rem; +.rounded-t-none { + border-top-left-radius: 0px; + border-top-right-radius: 0px; +} + +.rounded-b-box { + border-bottom-right-radius: var(--rounded-box, 1rem); + border-bottom-left-radius: var(--rounded-box, 1rem); +} + +.rounded-se-box { + border-start-end-radius: var(--rounded-box, 1rem); } .border { @@ -747,56 +2224,26 @@ video { border-bottom-width: 1px; } -.border-white\/10 { - border-color: rgb(255 255 255 / 0.1); -} - .border-blue-500\/30 { border-color: rgb(59 130 246 / 0.3); } -.border-purple-500\/30 { - border-color: rgb(168 85 247 / 0.3); -} - .border-gray-600 { --tw-border-opacity: 1; border-color: rgb(75 85 99 / var(--tw-border-opacity, 1)); } -.bg-blue-300 { - --tw-bg-opacity: 1; - background-color: rgb(147 197 253 / var(--tw-bg-opacity, 1)); +.border-purple-500\/30 { + border-color: rgb(168 85 247 / 0.3); } -.bg-red-500 { - --tw-bg-opacity: 1; - background-color: rgb(239 68 68 / var(--tw-bg-opacity, 1)); +.border-white\/10 { + border-color: rgb(255 255 255 / 0.1); } -.bg-slate-50 { - --tw-bg-opacity: 1; - background-color: rgb(248 250 252 / var(--tw-bg-opacity, 1)); -} - -.bg-slate-100 { - --tw-bg-opacity: 1; - background-color: rgb(241 245 249 / var(--tw-bg-opacity, 1)); -} - -.bg-blue-200 { - --tw-bg-opacity: 1; - background-color: rgb(191 219 254 / var(--tw-bg-opacity, 1)); -} - -.bg-blue-400 { - --tw-bg-opacity: 1; - background-color: rgb(96 165 250 / var(--tw-bg-opacity, 1)); -} - -.bg-blue-600 { - --tw-bg-opacity: 1; - background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1)); +.border-base-300 { + --tw-border-opacity: 1; + border-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-border-opacity, 1))); } .bg-black\/30 { @@ -807,18 +2254,28 @@ video { background-color: rgb(37 99 235 / 0.2); } +.bg-gray-800 { + --tw-bg-opacity: 1; + background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1)); +} + .bg-purple-600\/20 { background-color: rgb(147 51 234 / 0.2); } -.bg-white { +.bg-base-100 { --tw-bg-opacity: 1; - background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1)); + background-color: var(--fallback-b1,oklch(var(--b1)/var(--tw-bg-opacity, 1))); } -.bg-gray-800 { +.bg-base-200 { --tw-bg-opacity: 1; - background-color: rgb(31 41 55 / var(--tw-bg-opacity, 1)); + background-color: var(--fallback-b2,oklch(var(--b2)/var(--tw-bg-opacity, 1))); +} + +.bg-base-300 { + --tw-bg-opacity: 1; + background-color: var(--fallback-b3,oklch(var(--b3)/var(--tw-bg-opacity, 1))); } .bg-gradient-to-br { @@ -835,15 +2292,9 @@ video { --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); } -.from-slate-900 { - --tw-gradient-from: #0f172a var(--tw-gradient-from-position); - --tw-gradient-to: rgb(15 23 42 / 0) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.from-slate-100 { - --tw-gradient-from: #f1f5f9 var(--tw-gradient-from-position); - --tw-gradient-to: rgb(241 245 249 / 0) var(--tw-gradient-to-position); +.from-pink-500 { + --tw-gradient-from: #ec4899 var(--tw-gradient-from-position); + --tw-gradient-to: rgb(236 72 153 / 0) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); } @@ -853,93 +2304,39 @@ video { --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); } -.from-pink-500 { - --tw-gradient-from: #ec4899 var(--tw-gradient-from-position); - --tw-gradient-to: rgb(236 72 153 / 0) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to); -} - -.via-purple-900 { - --tw-gradient-to: rgb(88 28 135 / 0) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-from), #581c87 var(--tw-gradient-via-position), var(--tw-gradient-to); -} - .via-purple-500 { --tw-gradient-to: rgb(168 85 247 / 0) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-from), #a855f7 var(--tw-gradient-via-position), var(--tw-gradient-to); } -.via-purple-300 { - --tw-gradient-to: rgb(216 180 254 / 0) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-from), #d8b4fe var(--tw-gradient-via-position), var(--tw-gradient-to); -} - -.via-purple-200 { - --tw-gradient-to: rgb(233 213 255 / 0) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-from), #e9d5ff var(--tw-gradient-via-position), var(--tw-gradient-to); -} - -.via-purple-100 { - --tw-gradient-to: rgb(243 232 255 / 0) var(--tw-gradient-to-position); - --tw-gradient-stops: var(--tw-gradient-from), #f3e8ff var(--tw-gradient-via-position), var(--tw-gradient-to); -} - .via-red-500 { --tw-gradient-to: rgb(239 68 68 / 0) var(--tw-gradient-to-position); --tw-gradient-stops: var(--tw-gradient-from), #ef4444 var(--tw-gradient-via-position), var(--tw-gradient-to); } -.to-purple-500 { - --tw-gradient-to: #a855f7 var(--tw-gradient-to-position); -} - -.to-slate-900 { - --tw-gradient-to: #0f172a var(--tw-gradient-to-position); -} - .to-pink-500 { --tw-gradient-to: #ec4899 var(--tw-gradient-to-position); } -.to-slate-200 { - --tw-gradient-to: #e2e8f0 var(--tw-gradient-to-position); -} - .to-purple-200 { --tw-gradient-to: #e9d5ff var(--tw-gradient-to-position); } -.to-purple-300 { - --tw-gradient-to: #d8b4fe var(--tw-gradient-to-position); -} - -.to-purple-400 { - --tw-gradient-to: #c084fc var(--tw-gradient-to-position); -} - -.to-pink-600 { - --tw-gradient-to: #db2777 var(--tw-gradient-to-position); -} - .to-yellow-500 { --tw-gradient-to: #eab308 var(--tw-gradient-to-position); } +.bg-cover { + background-size: cover; +} + .bg-clip-text { -webkit-background-clip: text; background-clip: text; } -.p-10 { - padding: 2.5rem; -} - -.p-2 { - padding: 0.5rem; -} - -.p-1 { - padding: 0.25rem; +.bg-top { + background-position: top; } .p-0\.5 { @@ -950,9 +2347,8 @@ video { padding: 1rem; } -.py-4 { - padding-top: 1rem; - padding-bottom: 1rem; +.p-2 { + padding: 0.5rem; } .px-3 { @@ -965,77 +2361,69 @@ video { padding-right: 1rem; } -.py-2 { - padding-top: 0.5rem; - padding-bottom: 0.5rem; -} - -.py-8 { - padding-top: 2rem; - padding-bottom: 2rem; -} - .px-6 { padding-left: 1.5rem; padding-right: 1.5rem; } +.py-2 { + padding-top: 0.5rem; + padding-bottom: 0.5rem; +} + .py-3 { padding-top: 0.75rem; padding-bottom: 0.75rem; } -.py-10 { - padding-top: 2.5rem; - padding-bottom: 2.5rem; -} - -.pb-10 { - padding-bottom: 2.5rem; -} - -.pt-4 { +.py-4 { padding-top: 1rem; + padding-bottom: 1rem; +} + +.py-8 { + padding-top: 2rem; + padding-bottom: 2rem; +} + +.px-1 { + padding-left: 0.25rem; + padding-right: 0.25rem; } .text-center { text-align: center; } -.font-mono { - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; -} - -.text-4xl { - font-size: 2.25rem; - line-height: 2.5rem; -} - -.text-lg { - font-size: 1.125rem; - line-height: 1.75rem; -} - .text-2xl { font-size: 1.5rem; line-height: 2rem; } -.text-sm { - font-size: 0.875rem; - line-height: 1.25rem; -} - .text-5xl { font-size: 3rem; line-height: 1; } +.text-lg { + font-size: 1.125rem; + line-height: 1.75rem; +} + +.text-sm { + font-size: 0.875rem; + line-height: 1.25rem; +} + .text-xl { font-size: 1.25rem; line-height: 1.75rem; } +.font-black { + font-weight: 900; +} + .font-bold { font-weight: 700; } @@ -1044,8 +2432,9 @@ video { font-weight: 500; } -.font-black { - font-weight: 900; +.text-blue-400 { + --tw-text-opacity: 1; + color: rgb(96 165 250 / var(--tw-text-opacity, 1)); } .text-gray-300 { @@ -1053,15 +2442,6 @@ video { color: rgb(209 213 219 / var(--tw-text-opacity, 1)); } -.text-transparent { - color: transparent; -} - -.text-blue-400 { - --tw-text-opacity: 1; - color: rgb(96 165 250 / var(--tw-text-opacity, 1)); -} - .text-gray-400 { --tw-text-opacity: 1; color: rgb(156 163 175 / var(--tw-text-opacity, 1)); @@ -1072,6 +2452,10 @@ video { color: rgb(192 132 252 / var(--tw-text-opacity, 1)); } +.text-transparent { + color: transparent; +} + .text-white { --tw-text-opacity: 1; color: rgb(255 255 255 / var(--tw-text-opacity, 1)); @@ -1087,27 +2471,27 @@ video { color: rgb(156 163 175 / var(--tw-placeholder-opacity, 1)); } +.shadow-xl { + --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); + --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color); + box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow); +} + .outline-none { outline: 2px solid transparent; outline-offset: 2px; } -.backdrop-blur-sm { - --tw-backdrop-blur: blur(4px); - -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); - backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); -} - .backdrop-blur-md { --tw-backdrop-blur: blur(12px); -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); } -.transition-colors { - transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; +.backdrop-blur-sm { + --tw-backdrop-blur: blur(4px); + -webkit-backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); + backdrop-filter: var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia); } .transition-all { @@ -1116,6 +2500,16 @@ video { transition-duration: 150ms; } +.transition-colors { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +.\[border-width\:var\(--tab-border\)\] { + border-width: var(--tab-border); +} + .hover\:scale-105:hover { --tw-scale-x: 1.05; --tw-scale-y: 1.05; diff --git a/src/server/routes/auth.rs b/src/server/routes/auth.rs new file mode 100644 index 0000000..e0b61f3 --- /dev/null +++ b/src/server/routes/auth.rs @@ -0,0 +1,36 @@ +use axum::{ + extract::State, + response::{Html, IntoResponse}, + Form, +}; +use axum_session_auth::AuthSession; +use axum_session_surreal::SessionSurrealPool; +use serde::{Deserialize, Serialize}; +use surrealdb::{engine::any::Any, Surreal}; + +use crate::{error::ApiError, server::AppState, storage::types::user::User}; + +#[derive(Deserialize, Serialize)] +pub struct SignupParams { + pub email: String, + pub password: String, +} + +pub async fn show_signup_form(State(state): State) -> Html { + let context = tera::Context::new(); + let html = state + .tera + .render("auth/signup.html", &context) + .unwrap_or_else(|_| "

Error rendering template

".to_string()); + Html(html) +} + +pub async fn signup_handler( + State(state): State, + auth: AuthSession, Surreal>, + Form(form): Form, +) -> Result { + let user = User::create_new(form.email, form.password, &state.surreal_db_client).await?; + auth.login_user(user.id); + Ok(()) +} diff --git a/src/server/routes/index.rs b/src/server/routes/index.rs index 7706221..c544ece 100644 --- a/src/server/routes/index.rs +++ b/src/server/routes/index.rs @@ -1,13 +1,21 @@ use axum::{extract::State, response::Html}; +use axum_session_auth::AuthSession; +use axum_session_surreal::SessionSurrealPool; use serde_json::json; +use surrealdb::{engine::any::Any, Surreal}; use tera::Context; use tracing::info; -use crate::{error::ApiError, server::AppState}; +use crate::{error::ApiError, server::AppState, storage::types::user::User}; -pub async fn index_handler(State(state): State) -> Result, ApiError> { +pub async fn index_handler( + State(state): State, + auth: AuthSession, Surreal>, +) -> Result, ApiError> { info!("Displaying index page"); + info!("{:?}", auth.current_user); + let queue_length = state.rabbitmq_consumer.get_queue_length().await?; let output = state diff --git a/src/server/routes/mod.rs b/src/server/routes/mod.rs index 724e6f7..1443dcd 100644 --- a/src/server/routes/mod.rs +++ b/src/server/routes/mod.rs @@ -1,3 +1,4 @@ +pub mod auth; pub mod file; pub mod index; pub mod ingress; diff --git a/src/server/templates/auth/signup.html b/src/server/templates/auth/signup.html new file mode 100644 index 0000000..4761c57 --- /dev/null +++ b/src/server/templates/auth/signup.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} + +{% block content %} +
+
+
+

Sign Up

+ +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+ +
OR
+ +
+ Already have an account? + Login +
+
+
+
+
+ + + +{% endblock %} \ No newline at end of file diff --git a/src/server/templates/base.html b/src/server/templates/base.html index 64af638..ec8fa87 100644 --- a/src/server/templates/base.html +++ b/src/server/templates/base.html @@ -1,5 +1,5 @@ - + @@ -9,30 +9,25 @@ - + -