commit df887321623b251e9518966dc6a8e48a764cb658 Author: debbster Date: Wed Sep 2 16:09:34 2026 +0000 Initial commit diff --git a/.agentsignore b/.agentsignore new file mode 100644 index 0000000..8a4bb11 --- /dev/null +++ b/.agentsignore @@ -0,0 +1,7 @@ +package-lock.json +Cargo.lock +/node_modules +/target +/dist +/.idea +/.git \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c50b5d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,277 @@ + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# AWS User-specific +.idea/**/aws.xml + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# SonarLint plugin +.idea/sonarlint/ + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Rust template +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ +dist/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +# RustRover +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +### Linux template +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Node template +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..30cf57e --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Ignored default folder with query files +/queries/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..4b151ab --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..71401a8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,64 @@ +# AGENTS.md + +Iframe Leptos CSR aka "Mont Fort App" craft. Data from generated Matterhorn `*Api`. Theme is `mountainridge`. You implement pages. You do not own codegen, hosting, or the toolchain. + +Use the injected workspace tools for git, check, build, issues, and Matterhorn deps. Do not shell out to `git` / `cargo` / `npm` / `trunk`. Commit only if asked (`feat:` / `fix:` / `docs:` / `refactor:`). + +## Frozen + +`src/codegen/**`, `schemas/`, `read_app_config`, `index.html` config script + `` + asset `copy-dir`, `Router base`, `data-theme="mountainridge"`, `src/styles.css` theme block, `Cargo.toml`. + +Missing entity → Matterhorn-search tool → Matterhorn-dep tool, then generate. Still missing → issue, stop. New crate or new CSS tokens → stop. Field not on the struct → migration, not a UI hack. + +## Layout + +```text +src/main.rs mount +src/app.rs App + Shell + routes + nav ← register here +src/context.rs AppCtx +src/components/ pages + PageHeader, EmptyView, ErrorView +src/codegen/ read-only types + *Api +``` + +```rust +let ctx = expect_context::(); +let api = ThingApi::new(ctx.matterhorn_config.clone()); +``` + +Pages never read `window.__embeddedAppConfig`. Config failure is handled in `App`. + +## Page + +`src/components/_list.rs` → export in `mod.rs` → one relative `` and one `href="./…"` `` in `src/app.rs`. Do not touch `main.rs`. + +Flat siblings under `/`. Nest a `ParentRoute` only for a second ``. `path!("features")`, never `path!("/features")`. + +```rust +let data = LocalResource::new(move || { + let api = api.clone(); + async move { api.fetch_thing(vec![], vec![], HashMap::new(), 50, 0).await } +}); + +view! { + + {move || Suspend::new(async move { + match data.await { + Ok(rows) if rows.is_empty() => view! { }.into_any(), + Ok(rows) => view! { /* Option fields only */ }.into_any(), + Err(err) => view! { }.into_any(), + } + })} +} +``` + +Do not add ``. Do not call `.get()` on the resource. `store_*` / forms only if the user asked. Projections = schema names (`createdAt`, not `created_at`). + +`view!` text is quoted. `leptos::prelude::*`, not `use leptos::*`. `` takes `attr:class` (or `{..} class`). Brand/nav is not a `.btn`. + +## UI + +DaisyUI on `mountainridge`: `card`, `table`, `badge`, `menu`. Real `.btn` for submit/delete only. No `tailwind.config.*`. No page CSS. Scroll, if needed, is `min-h-0 flex-1 overflow-y-auto` on the page body — not `h-dvh`, not `html` overflow. + +## Done + +`check` + `build` clean. Diff = new page + `mod.rs` + `app.rs`. Theme, styles, codegen, Cargo untouched. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..36d28bc --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "r_bbdf_49dc_d744_4108_8b4d_7feee4b2411f" +repository = "http://mridge-gitea:3000/debbster/bbdf49dc-d744-4108-8b4d-7feee4b2411f.git" +version = "0.1.0" +edition = "2021" + +[dependencies] +leptos = { version = "0.8", features = ["csr"] } +leptos_router = "0.8" +console_error_panic_hook = "0.1" +wasm-bindgen = "0.2" +serde = { version = "1.0", features = ["derive"] } +matterhorn-client = { version = "0.1" } +uuid = { version = "1", features = ["serde"] } +chrono = { version = "0.4", features = ["serde"] } +anyhow = "1.0" +serde_json = "1.0.149" +reqwest = { version = "0.13", features = ["json"] } +thiserror = "2.0" +once_cell = "1.21" +tokio = { version = "1.53", features = ["sync"] } +gloo-timers = { version = "0.4", features = ["futures"] } +base64 = "0.23" +regress = "0.12" +web-sys = "0.3" +wasm-bindgen-futures = "0.4" +js-sys = "0.3" diff --git a/Trunk.toml b/Trunk.toml new file mode 100644 index 0000000..423abdb --- /dev/null +++ b/Trunk.toml @@ -0,0 +1,6 @@ +[build] +filehash = false +public_url = "/crafts/releases/debbster/bbdf49dc-d744-4108-8b4d-7feee4b2411f/" + +[serve] +addresses = ["0.0.0.0"] diff --git a/assets/mridge-craft-icon.webp b/assets/mridge-craft-icon.webp new file mode 100644 index 0000000..815acc2 Binary files /dev/null and b/assets/mridge-craft-icon.webp differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..5d55eb7 --- /dev/null +++ b/index.html @@ -0,0 +1,23 @@ + + + + + + + MountainRidge.xyz - Mont Fort App + + + + + + + + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..62e2950 --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "dependencies": { + "@fontsource/geist": "^5.3.0", + "daisyui": "^5.5.18", + "tailwindcss": "^4.2.1" + }, + "scripts": { + "build-craft": "npm install && tailwindcss -i src/styles.css -o assets/styles.css --minify && trunk build --config Trunk.toml", + "fmt": "cargo fmt", + "check": "cargo fmt && cargo check --all-targets --all-features -q --message-format short --color never", + "lint": "cargo clippy --all-targets --all-features -- -D warnings", + "test": "cargo test --all-targets --all-features" + } +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..016e80e --- /dev/null +++ b/src/app.rs @@ -0,0 +1,97 @@ +use crate::components::{ + ConfigError, ErrorView, Home, LoadingView, SoftwareFeatureList, +}; +use crate::context::{read_app_config, AppCtx}; +use leptos::prelude::*; +use leptos_router::components::{A, Outlet, ParentRoute, Route, Router, Routes}; +use leptos_router::path; + +/// Register new pages here: add a `` and a matching nav ``. +#[component] +pub fn App() -> impl IntoView { + match read_app_config() { + None => view! { }.into_any(), + Some(config) => { + provide_context(config.clone()); + view! { + + "Not found"

}> + + + +
+
+ } + .into_any() + } + } +} + +#[component] +fn Shell() -> impl IntoView { + let ctx = expect_context::(); + let user = ctx.user.clone().unwrap_or_else(|| "Guest".to_string()); + let logo = ctx.asset_path("mridge-craft-icon.webp"); + + view! { +
+ + +
+ + +
+
+ + } + }> + }> + + + +
+
+
+ +
+ + +
+
+ } +} diff --git a/src/components/config_error.rs b/src/components/config_error.rs new file mode 100644 index 0000000..43b6dc9 --- /dev/null +++ b/src/components/config_error.rs @@ -0,0 +1,21 @@ +use leptos::prelude::*; + +#[component] +pub fn ConfigError() -> impl IntoView { + view! { +
+
+
+

"Configuration unavailable"

+

+ "This craft did not receive APP_CONFIG from the host. " + "token and matterhorn_base_url are required." +

+

+ "Reload the preview after the host has posted the iframe message. Do not continue without config." +

+
+
+
+ } +} diff --git a/src/components/empty_view.rs b/src/components/empty_view.rs new file mode 100644 index 0000000..d606037 --- /dev/null +++ b/src/components/empty_view.rs @@ -0,0 +1,14 @@ +use leptos::prelude::*; + +#[component] +pub fn EmptyView( + #[prop(default = "Nothing to show")] title: &'static str, + #[prop(default = "No records were returned.")] message: &'static str, +) -> impl IntoView { + view! { +
+

{title}

+

{message}

+
+ } +} diff --git a/src/components/error_view.rs b/src/components/error_view.rs new file mode 100644 index 0000000..1468fed --- /dev/null +++ b/src/components/error_view.rs @@ -0,0 +1,16 @@ +use leptos::prelude::*; + +#[component] +pub fn ErrorView( + #[prop(default = "Something went wrong")] title: &'static str, + message: String, +) -> impl IntoView { + view! { + + } +} diff --git a/src/components/home.rs b/src/components/home.rs new file mode 100644 index 0000000..31f4af7 --- /dev/null +++ b/src/components/home.rs @@ -0,0 +1,14 @@ +use crate::components::PageHeader; +use leptos::prelude::*; + +#[component] +pub fn Home() -> impl IntoView { + view! { + +
+
+

"Use the navigation to open generated entity pages."

+
+
+ } +} diff --git a/src/components/loading_view.rs b/src/components/loading_view.rs new file mode 100644 index 0000000..5509d1b --- /dev/null +++ b/src/components/loading_view.rs @@ -0,0 +1,11 @@ +use leptos::prelude::*; + +#[component] +pub fn LoadingView(#[prop(default = "Loading…")] label: &'static str) -> impl IntoView { + view! { +
+ + {label} +
+ } +} diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..bdb2ec2 --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1,13 @@ +mod config_error; +mod empty_view; +mod error_view; +mod home; +mod loading_view; +mod page_header; + +pub use config_error::ConfigError; +pub use empty_view::EmptyView; +pub use error_view::ErrorView; +pub use home::Home; +pub use loading_view::LoadingView; +pub use page_header::PageHeader; \ No newline at end of file diff --git a/src/components/page_header.rs b/src/components/page_header.rs new file mode 100644 index 0000000..92a1ac5 --- /dev/null +++ b/src/components/page_header.rs @@ -0,0 +1,20 @@ +use leptos::prelude::*; + +#[component] +pub fn PageHeader( + title: &'static str, + #[prop(optional)] subtitle: Option<&'static str>, + #[prop(optional)] children: Option, +) -> impl IntoView { + view! { +
+
+

{title}

+ {subtitle.map(|text| view! {

{text}

})} +
+
+ {children.map(|c| c())} +
+
+ } +} diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..ecd06a1 --- /dev/null +++ b/src/context.rs @@ -0,0 +1,100 @@ +use matterhorn_client::ClientConfig; +use wasm_bindgen::JsValue; + +#[derive(Clone, Debug)] +pub struct AppCtx { + pub user: Option, + pub base_path: String, + pub matterhorn_config: ClientConfig, +} + +impl AppCtx { + pub fn asset_path(&self, file: &str) -> String { + format!("{}assets/{}", self.base_path, file.trim_start_matches('/')) + } +} + +fn clog(msg: impl AsRef) { + web_sys::console::log_1(&JsValue::from_str(&format!( + "[craft config] {}", + msg.as_ref() + ))); +} + +fn js_type_name(value: &JsValue) -> String { + if value.is_undefined() { + "undefined".into() + } else if value.is_null() { + "null".into() + } else if let Some(s) = value.as_string() { + format!("string(len={})", s.len()) + } else { + js_sys::Object::from(value.clone()).to_string().into() + } +} + +fn field_string(raw: &JsValue, key: &str) -> Option { + let value = js_sys::Reflect::get(raw, &JsValue::from_str(key)).ok()?; + clog(format!("{key}: {}", js_type_name(&value))); + value.as_string() +} + +/// Reads iframe-injected `window.__embeddedAppConfig`. +/// Expected payload: +/// `{ type: "APP_CONFIG", token, user, matterhorn_base_url }` +pub fn read_app_config() -> Option { + clog("read_app_config()"); + + let base_path = web_sys::window() + .and_then(|w| w.document()) + .and_then(|d| d.query_selector("base").ok().flatten()) + .and_then(|el| el.get_attribute("href")) + .unwrap_or_else(|| "/".to_string()); + + let window = match web_sys::window() { + Some(w) => w, + None => { + clog("window is missing"); + return None; + } + }; + + let embedded = js_sys::Reflect::get(&window, &"__embeddedAppConfig".into()).ok(); + let raw = match embedded { + Some(value) if !value.is_undefined() && !value.is_null() => value, + _ => { + clog("no usable __embeddedAppConfig"); + return None; + } + }; + + let token = match field_string(&raw, "token") { + Some(value) if !value.is_empty() => value, + Some(_) => { + clog("token is empty — host message not applied yet?"); + return None; + } + None => { + clog("token is not a string"); + return None; + } + }; + let base_url = match field_string(&raw, "matterhorn_base_url") { + Some(value) if !value.is_empty() => value, + Some(_) => { + clog("matterhorn_base_url is empty"); + return None; + } + None => { + clog("matterhorn_base_url is not a string"); + return None; + } + }; + let user = field_string(&raw, "username"); + + Some(AppCtx { + user, + base_path, + matterhorn_config: ClientConfig { base_url, token }, + }) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..d3ce72e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,12 @@ +mod app; +mod context; +mod components; +mod codegen; + +use app::App; +use leptos::mount::mount_to_body; + +fn main() { + console_error_panic_hook::set_once(); + mount_to_body(App); +} diff --git a/src/styles.css b/src/styles.css new file mode 100644 index 0000000..da01db8 --- /dev/null +++ b/src/styles.css @@ -0,0 +1,315 @@ +@import "@fontsource/geist/200.css"; +@import "@fontsource/geist/300.css"; +@import "@fontsource/geist/400.css"; +@import "@fontsource/geist/500.css"; +@import "@fontsource/geist/600.css"; +@import "tailwindcss"; + +@plugin "daisyui" { + themes: mountainridge --default; + darkTheme: mountainridge; + base: true; + styled: true; + utils: true; + logs: false; +} + +@plugin "daisyui/theme" { + name: "mountainridge"; + default: true; + prefersdark: true; + color-scheme: "dark"; + font-family: "Geist", system-ui, sans-serif; + /* Backgrounds — charcoal scale */ + --color-base-100: oklch(16% 0.02 214); + --color-base-200: oklch(13% 0.02 214); + --color-base-300: oklch(10% 0.02 214); + --color-base-content: oklch(92% 0.005 214); + + /* Primary — sea-grass green rgb(88, 191, 177) */ + --color-primary: oklch(72% 0.097 183); + --color-primary-content: oklch(13% 0.02 214); + + /* Secondary — glacier light blue rgb(94, 150, 159) */ + --color-secondary: oklch(60% 0.055 205); + --color-secondary-content: oklch(13% 0.02 214); + + /* Accent — flamingo pink rgba(253, 117, 127) */ + --color-accent: oklch(72% 0.155 14); /* flamingo pink */ + --color-accent-content: oklch(13% 0.02 214); + + + /* Neutral — charcoal slate */ + --color-neutral: oklch(28% 0.02 214); + --color-neutral-content: oklch(80% 0.01 214); + + /* Semantic */ + --color-info: oklch(60% 0.055 205); + --color-info-content: oklch(13% 0.02 214); + --color-success: oklch(72% 0.097 183); + --color-success-content: oklch(13% 0.02 214); + --color-warning: oklch(68% 0.16 42); /* alpenglow orange */ + --color-warning-content: oklch(13% 0.02 214); + --color-error: oklch(72% 0.155 14); /* flamingo pink */ + --color-error-content: oklch(13% 0.02 214); + + /* Shape */ + --radius-selector: 1.5rem; + --radius-field: 0.48rem; + --radius-box: 0.48rem; + --size-selector: 0.25rem; + --size-field: 0.25rem; + --border: 1px; + --depth: 1; + --noise: 0; +} + +:root { + all: inherit; + color: var(--color-base-content); /* DaisyUI's base text color */ + /* font-family: "Geist", system-ui, sans-serif;*/ + /* creating the perfect shadow according to twitter/X + - https://codepen.io/jh3y/pen/yLWgjpd + - https://x.com/jh3yy/status/1796208073830232574 + */ + --tint: 214; + --alpha: 4; + --base: hsl(var(--tint, 214) 80% 27% / calc(var(--alpha, 4) * 1%)); + --shade: hsl(from var(--base) calc(h + 8) 25 calc(l - 5)); + + --perfect-shadow: 0 0 0 1px var(--base), + 0 1px 1px -0.5px var(--shade), + 0 3px 3px -1.5px var(--shade), + 0 6px 6px -3px var(--shade), + 0 12px 12px -6px var(--base), + 0 24px 24px -12px var(--base); + + + --shade-active: hsla(173, 41%, 53%, 0.66); + --base-active: hsl(var(--tint, 214) 80% 27% / calc(var(--alpha, 4) * 1%)); + + --perfect-shadow-active: 0 0 0 1px var(--base-active), + 0 1px 1px -0.5px var(--shade-active), + 0 3px 3px -1.5px var(--shade-active), + 0 6px 6px -3px var(--shade-active), + 0 12px 12px -6px var(--base-active), + 0 24px 24px -12px var(--base-active); + + --perfect-shadow-thin-active: 0 0 0 1px var(--base-active), + 0 1px 2px -0.5px var(--shade-active), + 0 2px 4px -1px var(--shade-active), + 0 4px 8px -2px var(--shade-active); + + --shade-active-warn: rgba(253, 117, 127, 0.66); + --perfect-shadow-warn: 0 0 0 1px var(--shade-active-warn), + 0 1px 1px -0.5px var(--shade-active-warn), + 0 3px 3px -1.5px var(--shade-active-warn), + 0 6px 6px -3px var(--shade-active-warn), + 0 12px 12px -6px var(--base-active), + 0 24px 24px -12px var(--base-active); + + + --base-thin: hsl(var(--tint, 214) 80% 27% / calc(var(--alpha, 4) * 1%)); + --shade-thin: hsl(from var(--base-thin) calc(h + 8) 25 calc(l - 5)); + + --perfect-shadow-thin: 0 0 0 1px var(--base-thin), + 0 1px 2px -0.5px var(--shade-thin), + 0 3px 6px -1px var(--shade-thin); + --perfect-shadow-light: 0px 6px 20px -4px rgba(0, 0, 0, 0.40), 0px 0px 30px -8px rgba(123, 94, 248, 0.10); + /* === DARK BACKGROUND SHADOWS === */ + + /* Tinted with glacier blue for atmosphere */ + --tint-dark: 205; + --alpha-dark: 30; + + --base-dark: hsl(var(--tint-dark) 60% 60% / calc(var(--alpha-dark) * 1%)); + --shade-dark: hsl(from var(--base-dark) calc(h + 8) 70 calc(l + 10)); + + /* Full shadow — for cards, modals, elevated surfaces */ + --perfect-shadow-dark: 0 0 0 1px hsl(205 40% 50% / 15%), + 0 1px 1px -0.5px var(--base-dark), + 0 3px 3px -1.5px var(--base-dark), + 0 6px 6px -3px var(--shade-dark), + 0 12px 12px -6px var(--shade-dark), + 0 24px 24px -12px var(--base-dark); + + /* Thin shadow — for buttons, inputs, small elements */ + --perfect-shadow-thin-dark: 0 0 0 1px hsl(205 40% 50% / 12%), + 0 1px 2px -0.5px var(--base-dark), + 0 3px 6px -1px var(--shade-dark); + + /* Active/focus state — sea-grass green glow */ + --shade-active-dark: hsla(183, 35%, 55%, 0.45); /* sea-grass green */ + + --perfect-shadow-thin-active-dark: 0 0 0 1px hsla(183, 35%, 55%, 0.3), + 0 1px 2px -0.5px var(--shade-active-dark), + 0 2px 4px -1px var(--shade-active-dark), + 0 4px 8px -2px var(--shade-active-dark); + + /* Warning/error active state — flamingo pink glow */ + --shade-active-warn-dark: rgba(253, 117, 127, 0.45); + + --perfect-shadow-warn-dark: 0 0 0 1px rgba(253, 117, 127, 0.3), + 0 1px 1px -0.5px var(--shade-active-warn-dark), + 0 3px 3px -1.5px var(--shade-active-warn-dark), + 0 6px 6px -3px var(--shade-active-warn-dark), + 0 12px 12px -6px var(--shade-active-warn-dark); + + --mountain-white-dark: rgb(245, 242, 242); /* ≈ 80% – popular "dark mode off-white" */ + --mountain-white: rgb(255, 252, 252, 1); + --ghost-white: rgb(248, 248, 255, 1); + + /* Very subtle darkening – still very bright */ + --mountain-white-darker-1: rgb(245, 242, 242); /* ≈ 96% brightness */ + --mountain-white-darker-2: rgb(235, 232, 232); /* ≈ 92% */ + + /* Noticeably darker but still clearly a very light / "off-white" */ + --mountain-white-darker-3: rgb(230, 227, 227); /* ≈ 90% */ + --mountain-white-darker-4: rgb(220, 217, 217); /* ≈ 86% */ + --mountain-white-darker-5: rgb(204, 201, 201); /* ≈ 80% – popular "dark mode off-white" */ + + /* Deeper / moodier off-whites – start feeling more greyish */ + --mountain-white-darker-6: rgb(189, 186, 186); /* ≈ 74% */ + --mountain-white-darker-7: rgb(170, 167, 167); /* ≈ 67% – quite smoky now */ + --mountain-white-darker-8: rgb(150, 147, 147); /* ≈ 59% – very muted/dark off-white */ + + --charcoal-deep: rgb(15, 20, 25); + --charcoal-dark: rgb(25, 30, 35); /* +10 */ + --charcoal-medium: rgb(35, 40, 48); /* +20ish */ + --charcoal-soft: rgb(45, 50, 58); /* noticeably lighter but still moody */ + --charcoal-slate: rgb(55, 60, 68); /* good background / surface color */ + --charcoal-lighter: rgb(70, 75, 85); /* readable dark gray, modern UI feel */ + --deep-night-black: rgb(8, 18, 28); + --abyss-black: rgb(6, 15, 22); + + --bs-dark: var(--charcoal-medium); /* changes the base dark color */ + --bs-dark-rgb: 35, 40, 48; + --bs-blue: var(--glacier-blue); + + /* custom colors */ + --flamingo-pink: rgba(253, 117, 127, 1); + --sea-grass-green: rgba(88, 191, 177, 1); + + --mountain-darkgray: rgb(12, 31, 49, 1); + --mountain-gray: rgb(102, 133, 139, 1); + --mountain-lightgray: rgb(121, 152, 155, 1); + + --glacier-blue: rgb(28, 80, 101); + --glacier-darkblue: rgb(12, 39, 49); + --glacier-lightblue: rgb(94, 150, 159); + --glacier-blue-rgb: 94, 150, 159; + + /* borders, corners, and shadows */ + --perfect-border-radius: 0.48em; + --backdrop-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 1px 3px 0 rgba(0, 0, 0, 0.19); + --backdrop-transition: all 0.3s ease; + + /* global overwrites of bootstrap vars */ + --text-bg-primary-badge-color: var(--glacier-blue); + + /* White to Darker Blue diagonal gradient */ + --gradient-white-dark-blue: linear-gradient( + 142deg, + var(--ghost-white) 0%, + var(--ghost-white) 55%, + var(--glacier-lightblue) 67%, + var(--glacier-lightblue) 75%, + var(--glacier-blue) 100% + ); + --gradient-dark: linear-gradient(135deg, var(--mountain-white) 0%, var(--mountain-darkgray) 100%); +} + +* { + scrollbar-width: thin; + scrollbar-color: color-mix(in oklch, var(--color-base-content) 28%, transparent) var(--color-base-200); +} + +*::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +*::-webkit-scrollbar-track { + background: var(--color-base-200); +} + +*::-webkit-scrollbar-thumb { + background-color: color-mix(in oklch, var(--color-base-content) 28%, transparent); + border-radius: var(--radius-field); + border: 2px solid var(--color-base-200); +} + +*::-webkit-scrollbar-thumb:hover { + background-color: color-mix(in oklch, var(--color-primary) 65%, transparent); +} + +*::-webkit-scrollbar-corner { + background: var(--color-base-200); +} + +html, body { + height: 100%; + width: 100%; + margin: 0; + overflow: hidden; + background-color: var(--color-base-200); + color: var(--color-base-content); +} + +body > * { + height: 100%; + max-height: 100%; + min-height: 0; +} + +.drawer, +.drawer-content, +.drawer-side { + height: 100%; + max-height: 100%; + min-height: 0 !important; +} + +.drawer-content { + overflow: hidden; +} + +html, :root, body { + font-size: 14px; +} + +html, body { + font-family: "Geist Variable", "Geist", system-ui, sans-serif; + font-weight: 300; + font-synthesis: none; /* no fake 700 from 400 */ +} + +h1, h2, h3 { + font-weight: 300; +} + +h4, h5, h6, p { + font-weight: 400; +} + +.btn { + box-shadow: var(--perfect-shadow-thin-dark); + transition: transform 0.3s ease-out, box-shadow 0.3s ease-out; +} + +.btn:hover { + transform: scale(1.1) translateY(-2px); + box-shadow: var(--perfect-shadow-thin-active-dark); +} + +.btn:not(.btn-ghost):not(.btn-outline):hover { + transform: scale(1.1) translateY(-2px); + box-shadow: var(--perfect-shadow-thin-active-dark); +} + +.btn, +.menu a, +.tab, +.table :where(th) { + font-weight: 400; +} \ No newline at end of file