Skip to content

Commit 7369482

Browse files
committed
Expose showcase features in a responsive sidebar
1 parent d7a2af4 commit 7369482

5 files changed

Lines changed: 116 additions & 211 deletions

File tree

src/main.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
5656
id: 0,
5757
tenant_id: "community".to_string(),
5858
title: "Welcome to The Sovereign SaaS Blog & Publisher".to_string(),
59-
body: "Welcome to The Sovereign SaaS Blog & Publisher. Explore the top navigation bar to test the available front-end foundations, the Hybrid ORM, local Rullst Studio (http://127.0.0.1:5555), Nexus CMS (/nexus), Capital Billing, and Security RASP demonstrations.\n\nThe example uses task-local tenant scoping as a development fixture. Production applications must derive membership from authenticated identity and keep cross-tenant negative tests in their own authorization model.".to_string(),
59+
body: "Welcome to The Sovereign SaaS Blog & Publisher. Explore the feature sidebar to test the available front-end foundations, the Hybrid ORM, local Rullst Studio (http://127.0.0.1:5555), Nexus CMS (/nexus), Capital Billing, and Security RASP demonstrations.\n\nThe example uses task-local tenant scoping as a development fixture. Production applications must derive membership from authenticated identity and keep cross-tenant negative tests in their own authorization model.".to_string(),
6060
};
6161
let _ = post1.save().await;
6262
}

src/showcase_nav.rs

Lines changed: 29 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -103,17 +103,16 @@ pub fn render_showcase_nav(active_route: &str) -> String {
103103
routes
104104
.iter()
105105
.filter(|(_, _, _, _, cat)| *cat == category)
106-
.map(|(path, _, full_label, desc, _)| {
106+
.map(|(path, label, _, desc, _)| {
107107
let current = if *path == active_route {
108108
"page"
109109
} else {
110110
"false"
111111
};
112112
html! {
113113
<li>
114-
<a href={path} class="showcase-demo-link" aria-current={current}>
115-
<span class="showcase-demo-title">{full_label}</span>
116-
<span class="showcase-demo-description">{desc}</span>
114+
<a href={path} class="showcase-demo-link" aria-current={current} title={desc}>
115+
<span class="showcase-demo-title">{label}</span>
117116
</a>
118117
</li>
119118
}
@@ -130,50 +129,41 @@ pub fn render_showcase_nav(active_route: &str) -> String {
130129
<header class="showcase-banner">
131130
<div class="showcase-banner-inner">
132131
<a href="/" class="showcase-brand" aria-label="Rullst Showcase home">
133-
<img src="https://raw.githubusercontent.com/Rullst/Rullst/main/Rullst.png" alt="" class="showcase-brand-img" />
132+
<img src="/static/rullst.png" alt="" class="showcase-brand-img" />
134133
<span class="showcase-logo">"RULLST"</span>
135134
<span class="showcase-badge">"v12"</span>
136135
</a>
137136

138-
<nav class="showcase-navigation" aria-label="Main navigation">
139-
<details class="showcase-menu" name="showcase-navigation">
140-
<summary>"Demos"</summary>
141-
<div class="showcase-menu-panel showcase-demos-panel">
142-
<div>
143-
<h2 class="showcase-menu-heading">"Web paradigms"</h2>
144-
<ul class="showcase-link-list">{ rullst::html::RawHtml(paradigms) }</ul>
145-
</div>
146-
<div>
147-
<h2 class="showcase-menu-heading">"Architecture & SaaS"</h2>
148-
<ul class="showcase-link-list">{ rullst::html::RawHtml(features) }</ul>
149-
</div>
150-
</div>
151-
</details>
152-
<details class="showcase-menu" name="showcase-navigation">
153-
<summary>"Showcases"</summary>
154-
<div class="showcase-menu-panel showcase-compact-panel">
155-
<h2 class="showcase-menu-heading">"Explore Rullst in action"</h2>
156-
<ul class="showcase-link-list">{ rullst::html::RawHtml(showcases) }</ul>
157-
</div>
158-
</details>
159-
<details class="showcase-menu" name="showcase-navigation">
160-
<summary>"Tools"</summary>
161-
<div class="showcase-menu-panel showcase-compact-panel">
162-
<h2 class="showcase-menu-heading">"Try the sandbox"</h2>
163-
<ul class="showcase-link-list">
164-
<li><a href="/studio" target="_blank" rel="noopener noreferrer">"🚀 Studio Developer Cockpit"</a></li>
165-
<li><a href="/nexus" target="_blank" rel="noopener noreferrer">"🛡️ Nexus Admin CMS"</a></li>
166-
</ul>
167-
<p class="showcase-tenant">"Active tenant: " <strong>{&tenant_id}</strong></p>
168-
</div>
169-
</details>
170-
</nav>
137+
<button type="button" id="showcase-nav-toggle" class="showcase-nav-toggle" aria-controls="showcase-sidebar" aria-expanded="false">
138+
<span aria-hidden="true">"☰"</span> "All features"
139+
</button>
140+
<span class="showcase-header-label">"Explore the Rullst framework"</span>
171141

172142
<a href={DISCORD_URL} class="showcase-discord-btn" target="_blank" rel="noopener noreferrer">
173-
"Join Discord" <span aria-hidden="true">"↗"</span>
143+
"Join the Rullst community on Discord" <span aria-hidden="true">"↗"</span>
174144
</a>
175145
</div>
176146
</header>
147+
<aside id="showcase-sidebar" class="showcase-sidebar" aria-label="Rullst features">
148+
<div class="showcase-sidebar-header">
149+
<a href="/" class="showcase-brand" aria-label="Rullst Showcase home">
150+
<img src="/static/rullst.png" alt="" class="showcase-brand-img" />
151+
<span class="showcase-logo">"RULLST"</span><span class="showcase-badge">"v12"</span>
152+
</a>
153+
<button type="button" id="showcase-nav-close" aria-label="Close feature navigation">"×"</button>
154+
</div>
155+
<nav class="showcase-navigation" aria-label="All framework features">
156+
<div class="showcase-sidebar-group"><h2 class="showcase-menu-heading">"Web paradigms"</h2><ul class="showcase-link-list">{ rullst::html::RawHtml(paradigms) }</ul></div>
157+
<div class="showcase-sidebar-group"><h2 class="showcase-menu-heading">"Architecture & SaaS"</h2><ul class="showcase-link-list">{ rullst::html::RawHtml(features) }</ul></div>
158+
<div class="showcase-sidebar-group"><h2 class="showcase-menu-heading">"Developer tools"</h2><ul class="showcase-link-list">
159+
<li><a href="/studio" target="_blank" rel="noopener noreferrer">"🚀 Studio Cockpit ↗"</a></li>
160+
<li><a href="/nexus" target="_blank" rel="noopener noreferrer">"🛡️ Nexus Admin CMS ↗"</a></li>
161+
</ul></div>
162+
<div class="showcase-sidebar-group"><h2 class="showcase-menu-heading">"Explore the showcases"</h2><ul class="showcase-link-list">{ rullst::html::RawHtml(showcases) }</ul></div>
163+
<p class="showcase-tenant">"Active tenant: "<strong>{&tenant_id}</strong></p>
164+
</nav>
165+
</aside>
166+
<button type="button" id="showcase-nav-backdrop" class="showcase-nav-backdrop" aria-label="Close feature navigation" tabindex="-1" hidden="true"></button>
177167
<script>{ rullst::html::RawHtml(include_str!("../static/showcase-nav.js").to_string()) }</script>
178168

179169
<div class="showcase-framework-banner">

static/rullst.png

438 KB
Loading

static/showcase-nav.js

Lines changed: 56 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,62 @@
11
(() => {
22
try { localStorage.removeItem('htmx-history-cache'); } catch (_) { /* Storage can be disabled. */ }
3-
const menus = [...document.querySelectorAll('.showcase-menu')];
4-
const closeMenus = (except) => menus.forEach((menu) => {
5-
if (menu !== except) menu.open = false;
6-
});
7-
8-
menus.forEach((menu) => {
9-
menu.addEventListener('toggle', () => {
10-
if (menu.open) closeMenus(menu);
11-
});
12-
menu.addEventListener('click', (event) => {
13-
if (event.target.closest('a')) menu.open = false;
14-
});
15-
});
16-
document.addEventListener('click', (event) => {
17-
if (!event.target.closest('.showcase-menu')) closeMenus();
18-
});
19-
document.addEventListener('keydown', (event) => {
20-
if (event.key !== 'Escape') return;
21-
const openMenu = menus.find((menu) => menu.open);
22-
if (openMenu) {
23-
openMenu.open = false;
24-
openMenu.querySelector('summary').focus();
3+
const sidebar = document.getElementById('showcase-sidebar');
4+
const toggle = document.getElementById('showcase-nav-toggle');
5+
const close = document.getElementById('showcase-nav-close');
6+
const backdrop = document.getElementById('showcase-nav-backdrop');
7+
if (!sidebar || !toggle || !close || !backdrop) return;
8+
const mobile = matchMedia('(max-width: 1000px)');
9+
let previousOverflow = '';
10+
const inertState = new Map();
11+
function setOpen(open, returnFocus = false) {
12+
open = open && mobile.matches;
13+
const wasOpen = sidebar.classList.contains('is-open');
14+
sidebar.classList.toggle('is-open', open);
15+
sidebar.inert = mobile.matches && !open;
16+
toggle.setAttribute('aria-expanded', String(open));
17+
backdrop.hidden = !open;
18+
if (open) {
19+
sidebar.setAttribute('role', 'dialog');
20+
sidebar.setAttribute('aria-modal', 'true');
21+
if (!wasOpen) {
22+
previousOverflow = document.body.style.overflow;
23+
for (const child of document.body.children) {
24+
if (child === sidebar || child === backdrop || child.tagName === 'SCRIPT') continue;
25+
inertState.set(child, child.inert);
26+
child.inert = true;
27+
}
28+
document.body.style.overflow = 'hidden';
29+
close.focus();
30+
}
31+
} else {
32+
sidebar.removeAttribute('role');
33+
sidebar.removeAttribute('aria-modal');
34+
for (const [element, wasInert] of inertState) element.inert = wasInert;
35+
inertState.clear();
36+
if (wasOpen) document.body.style.overflow = previousOverflow;
37+
if (returnFocus) toggle.focus();
2538
}
39+
}
40+
document.documentElement.setAttribute('data-showcase-nav-ready', 'true');
41+
setOpen(false);
42+
toggle.addEventListener('click', () => setOpen(true));
43+
close.addEventListener('click', () => setOpen(false, true));
44+
backdrop.addEventListener('click', () => setOpen(false, true));
45+
sidebar.addEventListener('click', event => {
46+
if (event.target.closest('a') && mobile.matches) setOpen(false, true);
2647
});
27-
document.addEventListener('focusin', (event) => {
28-
closeMenus(event.target.closest('.showcase-menu'));
48+
document.addEventListener('keydown', event => {
49+
if (!sidebar.classList.contains('is-open')) return;
50+
if (event.key === 'Escape') setOpen(false, true);
51+
if (event.key === 'Tab') {
52+
const items = [...sidebar.querySelectorAll('a,button')].filter(el => el.getClientRects().length);
53+
const first = items[0], last = items[items.length - 1];
54+
if (event.shiftKey && document.activeElement === first) {
55+
event.preventDefault(); last.focus();
56+
} else if (!event.shiftKey && document.activeElement === last) {
57+
event.preventDefault(); first.focus();
58+
}
59+
}
2960
});
61+
mobile.addEventListener('change', () => setOpen(false));
3062
})();

0 commit comments

Comments
 (0)