@@ -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 = '×';
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