feat: add rStack AppSwitcher dropdown to header

Adds the unified rStack app switcher as pure HTML/CSS/JS with pastel
badges, emoji icons, and categorized navigation across all 17 rStack apps.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-02-24 19:15:55 -08:00
parent 28aafb73fe
commit 61add7bc56
1 changed files with 317 additions and 1 deletions

View File

@ -124,6 +124,210 @@
.mt-2 { margin-top: 1rem; }
.mb-1 { margin-bottom: 0.5rem; }
.mb-2 { margin-bottom: 1rem; }
/* ── AppSwitcher ── */
.app-switcher {
position: relative;
}
.app-switcher-trigger {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 10px;
border-radius: 8px;
font-size: 0.875rem;
font-weight: 600;
background: rgba(255,255,255,0.08);
color: #cbd5e1;
border: none;
cursor: pointer;
transition: background 0.15s;
line-height: 1;
}
.app-switcher-trigger:hover {
background: rgba(255,255,255,0.12);
}
.app-switcher-badge {
width: 24px;
height: 24px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 10px;
font-weight: 900;
color: #0f172a;
line-height: 1;
flex-shrink: 0;
}
.app-switcher-arrow {
font-size: 0.7em;
opacity: 0.6;
}
.app-switcher-dropdown {
display: none;
position: absolute;
top: 100%;
left: 0;
margin-top: 6px;
width: 300px;
max-height: 70vh;
overflow-y: auto;
border-radius: 12px;
background: #1e293b;
border: 1px solid rgba(255,255,255,0.1);
box-shadow: 0 20px 40px rgba(0,0,0,0.3);
z-index: 9999;
}
.app-switcher-dropdown.open {
display: block;
}
.app-switcher-header {
padding: 12px 14px;
border-bottom: 1px solid rgba(255,255,255,0.08);
display: flex;
align-items: center;
gap: 10px;
}
.app-switcher-header-badge {
width: 28px;
height: 28px;
border-radius: 8px;
background: linear-gradient(135deg, #67e8f9, #c4b5fd, #fda4af);
display: flex;
align-items: center;
justify-content: center;
font-size: 11px;
font-weight: 900;
color: #0f172a;
line-height: 1;
flex-shrink: 0;
}
.app-switcher-header-title {
font-size: 0.875rem;
font-weight: 700;
color: #fff;
}
.app-switcher-header-sub {
font-size: 10px;
color: #94a3b8;
}
.app-switcher-cat {
padding: 12px 14px 4px;
font-size: 0.6rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #64748b;
user-select: none;
}
.app-switcher-item {
display: flex;
align-items: center;
transition: background 0.12s;
}
.app-switcher-item:hover {
background: rgba(255,255,255,0.04);
}
.app-switcher-item.current {
background: rgba(255,255,255,0.07);
}
.app-switcher-item a.app-switcher-link {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
padding: 8px 14px;
color: #cbd5e1;
text-decoration: none;
min-width: 0;
}
.app-switcher-item-info {
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
}
.app-switcher-item-name {
display: flex;
align-items: center;
gap: 6px;
}
.app-switcher-item-name span:first-child {
font-size: 0.875rem;
font-weight: 600;
}
.app-switcher-item-name span:last-child {
font-size: 0.875rem;
flex-shrink: 0;
}
.app-switcher-item-desc {
font-size: 11px;
color: #94a3b8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.app-switcher-item .app-switcher-ext {
width: 32px;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
color: #22d3ee;
opacity: 0;
text-decoration: none;
flex-shrink: 0;
transition: opacity 0.12s;
}
.app-switcher-item:hover .app-switcher-ext {
opacity: 0.5;
}
.app-switcher-item .app-switcher-ext:hover {
opacity: 1 !important;
}
.app-switcher-footer {
padding: 10px 14px;
border-top: 1px solid rgba(255,255,255,0.08);
text-align: center;
}
.app-switcher-footer a {
font-size: 11px;
color: #64748b;
text-decoration: none;
transition: color 0.15s;
}
.app-switcher-footer a:hover {
color: #22d3ee;
}
/* badge colors */
.badge-teal { background: #5eead4; }
.badge-amber { background: #fcd34d; }
.badge-rose { background: #fda4af; }
.badge-sky { background: #7dd3fc; }
.badge-emerald { background: #6ee7b7; }
.badge-green { background: #86efac; }
.badge-indigo { background: #a5b4fc; }
.badge-fuchsia { background: #f0abfc; }
.badge-violet { background: #c4b5fd; }
.badge-lime { background: #bef264; }
.badge-yellow { background: #fde047; }
.badge-orange { background: #fdba74; }
.badge-red { background: #fca5a5; }
.badge-blue { background: #93c5fd; }
.badge-cyan { background: #67e8f9; }
.badge-pink { background: #f9a8d4; }
.badge-purple { background: #d8b4fe; }
.app-switcher-dropdown::-webkit-scrollbar {
width: 6px;
}
.app-switcher-dropdown::-webkit-scrollbar-track {
background: transparent;
}
.app-switcher-dropdown::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.1);
border-radius: 3px;
}
</style>
{% block extra_css %}{% endblock %}
<script defer src="https://rdata.online/collect.js" data-website-id="4a68a014-2f1e-46b8-9e56-ac8d5c12b3bf"></script>
@ -131,7 +335,18 @@
<body>
<div class="container">
<header>
<h1>rfiles.online</h1>
<div style="display:flex;align-items:center;gap:12px;">
<!-- AppSwitcher dropdown -->
<div class="app-switcher" id="appSwitcher">
<button class="app-switcher-trigger" id="appSwitcherTrigger" type="button" aria-expanded="false" aria-haspopup="true">
<span class="app-switcher-badge badge-cyan">rFi</span>
<span>rFiles</span>
<span class="app-switcher-arrow">&#9662;</span>
</button>
<div class="app-switcher-dropdown" id="appSwitcherDropdown" role="menu"></div>
</div>
<h1>rfiles.online</h1>
</div>
<nav>
<a href="/" class="{% if request.resolver_match.url_name == 'landing' %}active{% endif %}">Demo</a>
<a href="{% url 'portal:upload' %}" class="{% if request.resolver_match.url_name == 'upload' %}active{% endif %}">Create Topic</a>
@ -240,5 +455,106 @@
to { transform: translateX(0); opacity: 1; }
}
</style>
<script>
(function() {
var CURRENT = 'files';
var MODULES = [
{ id:'space', name:'rSpace', badge:'rS', badgeClass:'badge-teal', emoji:'\u{1F3A8}', desc:'Real-time collaborative canvas', domain:'rspace.online' },
{ id:'notes', name:'rNotes', badge:'rN', badgeClass:'badge-amber', emoji:'\u{1F4DD}', desc:'Group note-taking & knowledge capture', domain:'rnotes.online' },
{ id:'pubs', name:'rPubs', badge:'rP', badgeClass:'badge-rose', emoji:'\u{1F4F0}', desc:'Collaborative publishing platform', domain:'rpubs.online' },
{ id:'cal', name:'rCal', badge:'rC', badgeClass:'badge-sky', emoji:'\u{1F4C5}', desc:'Collaborative scheduling & events', domain:'rcal.online' },
{ id:'trips', name:'rTrips', badge:'rT', badgeClass:'badge-emerald', emoji:'\u2708\uFE0F', desc:'Group travel planning in real time', domain:'rtrips.online' },
{ id:'maps', name:'rMaps', badge:'rM', badgeClass:'badge-green', emoji:'\u{1F5FA}\uFE0F', desc:'Collaborative real-time mapping', domain:'rmaps.online' },
{ id:'inbox', name:'rInbox', badge:'rI', badgeClass:'badge-indigo', emoji:'\u{1F4EC}', desc:'Private group messaging', domain:'rinbox.online' },
{ id:'choices', name:'rChoices', badge:'rCh', badgeClass:'badge-fuchsia', emoji:'\u{1F500}', desc:'Collaborative decision making', domain:'rchoices.online' },
{ id:'vote', name:'rVote', badge:'rV', badgeClass:'badge-violet', emoji:'\u{1F5F3}\uFE0F', desc:'Real-time polls & governance', domain:'rvote.online' },
{ id:'funds', name:'rFunds', badge:'rF', badgeClass:'badge-lime', emoji:'\u{1F4B8}', desc:'Collaborative fundraising & grants', domain:'rfunds.online' },
{ id:'wallet', name:'rWallet', badge:'rW', badgeClass:'badge-yellow', emoji:'\u{1F4B0}', desc:'Multi-chain crypto wallet', domain:'rwallet.online' },
{ id:'cart', name:'rCart', badge:'rCt', badgeClass:'badge-orange', emoji:'\u{1F6D2}', desc:'Group commerce & shared shopping', domain:'rcart.online' },
{ id:'auctions',name:'rAuctions', badge:'rA', badgeClass:'badge-red', emoji:'\u{1F528}', desc:'Live auction platform', domain:'rauctions.online' },
{ id:'network', name:'rNetwork', badge:'rNe', badgeClass:'badge-blue', emoji:'\u{1F310}', desc:'Community network & social graph', domain:'rnetwork.online' },
{ id:'files', name:'rFiles', badge:'rFi', badgeClass:'badge-cyan', emoji:'\u{1F4C1}', desc:'Collaborative file storage', domain:'rfiles.online' },
{ id:'tube', name:'rTube', badge:'rTu', badgeClass:'badge-pink', emoji:'\u{1F3AC}', desc:'Group video platform', domain:'rtube.online' },
{ id:'data', name:'rData', badge:'rD', badgeClass:'badge-purple', emoji:'\u{1F4CA}', desc:'Analytics & insights dashboard', domain:'rdata.online' }
];
var CATEGORIES = [
{ name:'Creating', ids:['space','notes','pubs'] },
{ name:'Planning', ids:['cal','trips','maps'] },
{ name:'Discussing & Deciding', ids:['inbox','choices','vote'] },
{ name:'Funding & Commerce', ids:['funds','wallet','cart','auctions'] },
{ name:'Social & Sharing', ids:['network','files','tube','data'] }
];
var dropdown = document.getElementById('appSwitcherDropdown');
var trigger = document.getElementById('appSwitcherTrigger');
var wrapper = document.getElementById('appSwitcher');
// Build index
var byId = {};
MODULES.forEach(function(m){ byId[m.id] = m; });
// Render dropdown HTML
var html = '';
// Header
html += '<div class="app-switcher-header">';
html += ' <span class="app-switcher-header-badge">r*</span>';
html += ' <div>';
html += ' <div class="app-switcher-header-title">rStack</div>';
html += ' <div class="app-switcher-header-sub">Self-hosted community app suite</div>';
html += ' </div>';
html += '</div>';
// Categories
CATEGORIES.forEach(function(cat) {
html += '<div class="app-switcher-cat">' + cat.name + '</div>';
cat.ids.forEach(function(id) {
var m = byId[id];
if (!m) return;
var isCurrent = (id === CURRENT);
html += '<div class="app-switcher-item' + (isCurrent ? ' current' : '') + '">';
html += ' <a class="app-switcher-link" href="https://' + m.domain + '">';
html += ' <span class="app-switcher-badge ' + m.badgeClass + '">' + m.badge + '</span>';
html += ' <div class="app-switcher-item-info">';
html += ' <div class="app-switcher-item-name"><span>' + m.name + '</span><span>' + m.emoji + '</span></div>';
html += ' <div class="app-switcher-item-desc">' + m.desc + '</div>';
html += ' </div>';
html += ' </a>';
html += ' <a class="app-switcher-ext" href="https://' + m.domain + '" target="_blank" rel="noopener noreferrer" title="' + m.domain + '">&#8599;</a>';
html += '</div>';
});
});
// Footer
html += '<div class="app-switcher-footer">';
html += ' <a href="https://rstack.online">rstack.online &mdash; self-hosted, community-run</a>';
html += '</div>';
dropdown.innerHTML = html;
// Toggle
trigger.addEventListener('click', function(e) {
e.stopPropagation();
var isOpen = dropdown.classList.toggle('open');
trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
});
// Close on outside click
document.addEventListener('click', function(e) {
if (!wrapper.contains(e.target)) {
dropdown.classList.remove('open');
trigger.setAttribute('aria-expanded', 'false');
}
});
// Close on Escape
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
dropdown.classList.remove('open');
trigger.setAttribute('aria-expanded', 'false');
}
});
})();
</script>
</body>
</html>