Skip to content

Commit 0182464

Browse files
committed
fix(portfolio,lms): improve mobile responsiveness, resolve nexus drawer trapping and document issues 3 and 4
1 parent 19622e7 commit 0182464

5 files changed

Lines changed: 439 additions & 33 deletions

File tree

blueprints/lms/src/main.rs

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,108 @@ async fn studio_tailwind_patch(
156156
rullst::server::Response::from_parts(parts, axum::body::Body::from(bytes))
157157
}
158158

159+
async fn nexus_mobile_patch(
160+
req: rullst::server::Request,
161+
next: rullst::server::Next,
162+
) -> rullst::server::Response {
163+
use rullst::server::IntoResponse;
164+
let res = next.run(req).await;
165+
let (mut parts, body) = res.into_parts();
166+
let is_html = parts
167+
.headers
168+
.get(rullst::server::header::CONTENT_TYPE)
169+
.and_then(|v| v.to_str().ok())
170+
.map(|ct| ct.contains("text/html"))
171+
.unwrap_or(false);
172+
if !is_html {
173+
return rullst::server::Response::from_parts(parts, body);
174+
}
175+
let Ok(bytes) = axum::body::to_bytes(body, 2 * 1024 * 1024).await else {
176+
return (rullst::server::StatusCode::INTERNAL_SERVER_ERROR, "Failed to buffer nexus body").into_response();
177+
};
178+
let html = String::from_utf8_lossy(&bytes);
179+
if html.contains("nexus-sidebar") {
180+
let patch = r#"
181+
<style>
182+
@media (max-width: 900px) {
183+
#nexus-sidebar-backdrop {
184+
display: none;
185+
position: fixed;
186+
inset: 0;
187+
background: rgba(0, 0, 0, 0.65);
188+
backdrop-filter: blur(4px);
189+
-webkit-backdrop-filter: blur(4px);
190+
z-index: 95;
191+
}
192+
.nexus-sidebar.nexus-sidebar-open ~ #nexus-sidebar-backdrop,
193+
body:has(.nexus-sidebar-open) #nexus-sidebar-backdrop {
194+
display: block;
195+
}
196+
.nexus-sidebar {
197+
z-index: 100 !important;
198+
max-width: 85vw !important;
199+
box-shadow: 12px 0 35px rgba(0, 0, 0, 0.6);
200+
}
201+
.nexus-sidebar-close-btn {
202+
display: inline-flex;
203+
align-items: center;
204+
justify-content: center;
205+
width: 32px;
206+
height: 32px;
207+
border-radius: 8px;
208+
background: rgba(255, 255, 255, 0.08);
209+
border: 1px solid var(--border);
210+
color: #fff;
211+
font-size: 22px;
212+
line-height: 1;
213+
cursor: pointer;
214+
margin-left: auto;
215+
transition: background 0.15s ease;
216+
}
217+
.nexus-sidebar-close-btn:hover {
218+
background: rgba(255, 255, 255, 0.2);
219+
}
220+
}
221+
</style>
222+
<script>
223+
document.addEventListener('DOMContentLoaded', () => {
224+
const sidebar = document.getElementById('nexus-sidebar');
225+
if (!sidebar) return;
226+
const brand = sidebar.querySelector('.nexus-brand');
227+
if (brand && !document.getElementById('nexus-sidebar-close')) {
228+
const btn = document.createElement('button');
229+
btn.id = 'nexus-sidebar-close';
230+
btn.className = 'nexus-sidebar-close-btn';
231+
btn.innerHTML = '&times;';
232+
btn.setAttribute('aria-label', 'Close menu');
233+
btn.onclick = (e) => {
234+
e.preventDefault();
235+
sidebar.classList.remove('nexus-sidebar-open');
236+
};
237+
brand.appendChild(btn);
238+
}
239+
if (!document.getElementById('nexus-sidebar-backdrop')) {
240+
const backdrop = document.createElement('div');
241+
backdrop.id = 'nexus-sidebar-backdrop';
242+
backdrop.onclick = () => sidebar.classList.remove('nexus-sidebar-open');
243+
document.body.appendChild(backdrop);
244+
}
245+
sidebar.querySelectorAll('a').forEach(a => {
246+
a.addEventListener('click', () => sidebar.classList.remove('nexus-sidebar-open'));
247+
});
248+
document.addEventListener('keydown', (e) => {
249+
if (e.key === 'Escape') sidebar.classList.remove('nexus-sidebar-open');
250+
});
251+
});
252+
</script>
253+
"#;
254+
let patched = html.replace("</body>", &format!("{patch}</body>"));
255+
parts.headers.remove(rullst::server::header::CONTENT_LENGTH);
256+
return rullst::server::Response::from_parts(parts, axum::body::Body::from(patched));
257+
}
258+
rullst::server::Response::from_parts(parts, axum::body::Body::from(bytes))
259+
}
260+
159261
async fn studio_auth_guard(
160262
req: rullst::server::Request,
161263
next: rullst::server::Next,
@@ -268,7 +370,8 @@ let nexus = rullst::nexus::Nexus::new()
268370
.register::<models::user_achievement::UserAchievement>()
269371
.register::<models::score_event::ScoreEvent>()
270372
.register::<models::score_correction::ScoreCorrection>()
271-
.try_build()?;
373+
.try_build()?
374+
.layer(rullst::server::from_fn(nexus_mobile_patch));
272375

273376
let public = routes![
274377
get("/" => controllers::lms_controller::index),

blueprints/portfolio/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ rust-version = "1.96.0"
66

77
[dependencies]
88
rullst = { version = "12.0.0", default-features = false, features = ["orm", "strict-sqlite", "studio", "nexus"] }
9+
axum = "0.8"
910
serde = { version = "1.0", features = ["derive"] }
1011
serde_json = "1.0"
1112
tokio = { version = "1.0", features = ["full"] }

blueprints/portfolio/src/main.rs

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,108 @@ async fn studio_cache_handler(
176176
rullst::response::Html(full_html).into_response()
177177
}
178178

179+
async fn nexus_mobile_patch(
180+
req: rullst::server::Request,
181+
next: rullst::server::Next,
182+
) -> rullst::server::Response {
183+
use rullst::server::IntoResponse;
184+
let res = next.run(req).await;
185+
let (mut parts, body) = res.into_parts();
186+
let is_html = parts
187+
.headers
188+
.get(rullst::server::header::CONTENT_TYPE)
189+
.and_then(|v| v.to_str().ok())
190+
.map(|ct| ct.contains("text/html"))
191+
.unwrap_or(false);
192+
if !is_html {
193+
return rullst::server::Response::from_parts(parts, body);
194+
}
195+
let Ok(bytes) = axum::body::to_bytes(body, 2 * 1024 * 1024).await else {
196+
return (rullst::server::StatusCode::INTERNAL_SERVER_ERROR, "Failed to buffer nexus body").into_response();
197+
};
198+
let html = String::from_utf8_lossy(&bytes);
199+
if html.contains("nexus-sidebar") {
200+
let patch = r#"
201+
<style>
202+
@media (max-width: 900px) {
203+
#nexus-sidebar-backdrop {
204+
display: none;
205+
position: fixed;
206+
inset: 0;
207+
background: rgba(0, 0, 0, 0.65);
208+
backdrop-filter: blur(4px);
209+
-webkit-backdrop-filter: blur(4px);
210+
z-index: 95;
211+
}
212+
.nexus-sidebar.nexus-sidebar-open ~ #nexus-sidebar-backdrop,
213+
body:has(.nexus-sidebar-open) #nexus-sidebar-backdrop {
214+
display: block;
215+
}
216+
.nexus-sidebar {
217+
z-index: 100 !important;
218+
max-width: 85vw !important;
219+
box-shadow: 12px 0 35px rgba(0, 0, 0, 0.6);
220+
}
221+
.nexus-sidebar-close-btn {
222+
display: inline-flex;
223+
align-items: center;
224+
justify-content: center;
225+
width: 32px;
226+
height: 32px;
227+
border-radius: 8px;
228+
background: rgba(255, 255, 255, 0.08);
229+
border: 1px solid var(--border);
230+
color: #fff;
231+
font-size: 22px;
232+
line-height: 1;
233+
cursor: pointer;
234+
margin-left: auto;
235+
transition: background 0.15s ease;
236+
}
237+
.nexus-sidebar-close-btn:hover {
238+
background: rgba(255, 255, 255, 0.2);
239+
}
240+
}
241+
</style>
242+
<script>
243+
document.addEventListener('DOMContentLoaded', () => {
244+
const sidebar = document.getElementById('nexus-sidebar');
245+
if (!sidebar) return;
246+
const brand = sidebar.querySelector('.nexus-brand');
247+
if (brand && !document.getElementById('nexus-sidebar-close')) {
248+
const btn = document.createElement('button');
249+
btn.id = 'nexus-sidebar-close';
250+
btn.className = 'nexus-sidebar-close-btn';
251+
btn.innerHTML = '&times;';
252+
btn.setAttribute('aria-label', 'Close menu');
253+
btn.onclick = (e) => {
254+
e.preventDefault();
255+
sidebar.classList.remove('nexus-sidebar-open');
256+
};
257+
brand.appendChild(btn);
258+
}
259+
if (!document.getElementById('nexus-sidebar-backdrop')) {
260+
const backdrop = document.createElement('div');
261+
backdrop.id = 'nexus-sidebar-backdrop';
262+
backdrop.onclick = () => sidebar.classList.remove('nexus-sidebar-open');
263+
document.body.appendChild(backdrop);
264+
}
265+
sidebar.querySelectorAll('a').forEach(a => {
266+
a.addEventListener('click', () => sidebar.classList.remove('nexus-sidebar-open'));
267+
});
268+
document.addEventListener('keydown', (e) => {
269+
if (e.key === 'Escape') sidebar.classList.remove('nexus-sidebar-open');
270+
});
271+
});
272+
</script>
273+
"#;
274+
let patched = html.replace("</body>", &format!("{patch}</body>"));
275+
parts.headers.remove(rullst::server::header::CONTENT_LENGTH);
276+
return rullst::server::Response::from_parts(parts, axum::body::Body::from(patched));
277+
}
278+
rullst::server::Response::from_parts(parts, axum::body::Body::from(bytes))
279+
}
280+
179281
// 1. Resilient Nexus Auth Policy (defaults to public sandbox credentials for demonstration)
180282
let nexus_user = std::env::var("NEXUS_ADMIN_USERNAME").unwrap_or_else(|_| "admin".to_string());
181283
let raw_pass = std::env::var("NEXUS_ADMIN_PASSWORD").unwrap_or_else(|_| "SovereignPortfolio2026!".to_string());
@@ -193,7 +295,8 @@ async fn studio_cache_handler(
193295
.register::<models::project::Project>()
194296
.register::<models::experience::Experience>()
195297
.register::<models::skill::Skill>()
196-
.try_build()?;
298+
.try_build()?
299+
.layer(rullst::server::from_fn(nexus_mobile_patch));
197300

198301
let studio_router = rullst::studio::data_browser::router()
199302
.route("/cache", rullst::server::get(studio_cache_handler))

0 commit comments

Comments
 (0)