feat: alive UI wave, galaxy presence, spread and fleet enhancements
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Dashboard ambient layer, comrade presence, Mission Deck and War Room, Emberwake supply chain, spread/docs publishing, fleet policy and modules API, CI docker mining, and refreshed USB pack.
This commit is contained in:
@@ -137,6 +137,24 @@
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-item--docs {
|
||||
margin-top: 0.35rem;
|
||||
border: 1px solid rgba(0, 232, 245, 0.22);
|
||||
background: linear-gradient(90deg, rgba(0, 232, 245, 0.06), transparent);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav-item--docs:hover {
|
||||
background: linear-gradient(90deg, rgba(0, 232, 245, 0.14), rgba(168, 62, 240, 0.06));
|
||||
border-color: rgba(0, 232, 245, 0.45);
|
||||
box-shadow: 0 0 16px rgba(0, 232, 245, 0.12);
|
||||
}
|
||||
|
||||
.mobile-more-link--docs {
|
||||
border: 1px solid rgba(0, 232, 245, 0.25);
|
||||
background: rgba(0, 232, 245, 0.06);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: linear-gradient(90deg, rgba(0, 245, 255, 0.12), transparent);
|
||||
color: var(--neon-cyan);
|
||||
@@ -169,6 +187,23 @@
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
|
||||
.nav-item--mission.active {
|
||||
background: linear-gradient(90deg, rgba(255, 140, 58, 0.18), transparent);
|
||||
color: #ff8c3a;
|
||||
border-color: rgba(255, 140, 58, 0.45);
|
||||
box-shadow: inset 0 0 24px rgba(255, 140, 58, 0.12);
|
||||
}
|
||||
|
||||
.nav-item--mission:hover {
|
||||
color: #ffb366;
|
||||
border-color: rgba(255, 140, 58, 0.35);
|
||||
}
|
||||
|
||||
.nav-glow--mission {
|
||||
background: #ff8c3a;
|
||||
box-shadow: 0 0 14px #ff8c3a, 0 0 28px rgba(255, 140, 58, 0.45);
|
||||
}
|
||||
|
||||
/* ── Matrix rain ──────────────────────────────── */
|
||||
.matrix-rain-wrap {
|
||||
/* Flex-grow to fill all space between nav and footer */
|
||||
|
||||
@@ -11,30 +11,50 @@ import SacredGeometryLayer from '../Visual/sacredGeometry/SacredGeometryLayer';
|
||||
import { SacredMotif } from '../Visual/sacredGeometry/motifs';
|
||||
import SetupBanner from '../SetupBanner';
|
||||
import { getSetupStatus } from '../../help/setupStatus';
|
||||
import { resolvePageWeather } from '../../help/pageWeather';
|
||||
import { api } from '../../api/client';
|
||||
import { usePresence } from '../../context/PresenceContext';
|
||||
import ComradeAvatar from '../Presence/ComradeAvatar';
|
||||
import type { ServerConfig } from '../../types';
|
||||
import '../Presence/Presence.css';
|
||||
import './Layout.css';
|
||||
import './MobileNav.css';
|
||||
import '../../styles/operatorDeck.css';
|
||||
|
||||
interface LayoutProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
function operatorDeckId(pathname: string): string {
|
||||
const path = pathname.split('?')[0].replace(/\/$/, '') || '/';
|
||||
if (path.startsWith('/mission-deck')) return 'mission-deck';
|
||||
if (path.startsWith('/forge') || path.startsWith('/builder')) return 'forge';
|
||||
if (path.startsWith('/crucible')) return 'crucible';
|
||||
if (path.startsWith('/emberwake') || path.startsWith('/spread')) return 'emberwake';
|
||||
if (path.startsWith('/agents')) return 'fleet';
|
||||
if (path.startsWith('/builds')) return 'builds';
|
||||
if (path.startsWith('/settings')) return 'settings';
|
||||
if (path.startsWith('/pathtracer')) return 'pathtracer';
|
||||
return 'dashboard';
|
||||
}
|
||||
|
||||
const NAV = [
|
||||
{ to: '/dashboard', label: 'Command Deck', icon: 'deck' },
|
||||
{ to: '/agents', label: 'Fleet Roster', icon: 'fleet' },
|
||||
{ to: '/crucible', label: 'Crucible', icon: 'crucible' },
|
||||
{ to: '/forge', label: 'Forge', icon: 'forge' },
|
||||
{ to: '/mission-deck', label: 'Mission Deck', icon: 'mission', glow: true },
|
||||
{ to: '/builds', label: 'Builds', icon: 'builds' },
|
||||
{ to: '/emberwake', label: 'Emberwake', icon: 'ember' },
|
||||
{ to: '/guide', label: 'Field Guide', icon: 'guide' },
|
||||
{ to: '/settings', label: 'Calibrate', icon: 'gear' },
|
||||
{ to: '/pathtracer', label: 'Path Tracer', icon: 'trace' },
|
||||
] as const;
|
||||
|
||||
const DOCS_HREF = '/docs/';
|
||||
|
||||
/** Primary tabs on iPhone bottom bar */
|
||||
const MOBILE_PRIMARY = NAV.slice(0, 4);
|
||||
/** Builds, Guide, Calibrate, Path Tracer — “More” sheet */
|
||||
/** Builds, Calibrate, Path Tracer — “More” sheet */
|
||||
const MOBILE_MORE = NAV.slice(4);
|
||||
|
||||
function NavIcon({ type }: { type: string }) {
|
||||
@@ -60,6 +80,12 @@ function NavIcon({ type }: { type: string }) {
|
||||
<path d="M8 16l-2 4 4-2" />
|
||||
</svg>
|
||||
);
|
||||
case 'mission':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M13 2L3 14h8l-1 8 10-12h-8l1-8z" />
|
||||
</svg>
|
||||
);
|
||||
case 'builds':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
@@ -69,14 +95,6 @@ function NavIcon({ type }: { type: string }) {
|
||||
<path d="M7 4.5h10M7 10.5h10M7 16.5h10" strokeOpacity="0.35" />
|
||||
</svg>
|
||||
);
|
||||
case 'guide':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M4 19.5A2.5 2.5 0 016.5 17H20" />
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z" />
|
||||
<path d="M8 7h8M8 11h6" />
|
||||
</svg>
|
||||
);
|
||||
case 'crucible':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
@@ -103,6 +121,14 @@ function NavIcon({ type }: { type: string }) {
|
||||
<path d="M12 8v4" />
|
||||
</svg>
|
||||
);
|
||||
case 'docs':
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
<path d="M4 19.5A2.5 2.5 0 016.5 17H20" />
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z" />
|
||||
<path d="M8 7h8M8 11h6" strokeOpacity="0.55" />
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5">
|
||||
@@ -185,6 +211,7 @@ function MobileTopStats() {
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
const location = useLocation();
|
||||
const isMobile = useIsMobileLayout();
|
||||
const { othersOnline, comrades } = usePresence();
|
||||
const [serverConfig, setServerConfig] = useState<ServerConfig | null>(null);
|
||||
const [moreOpen, setMoreOpen] = useState(false);
|
||||
|
||||
@@ -206,6 +233,7 @@ export default function Layout({ children }: LayoutProps) {
|
||||
}, [moreOpen]);
|
||||
|
||||
const setupStatus = getSetupStatus(serverConfig);
|
||||
const pageWeather = resolvePageWeather(location.pathname);
|
||||
const moreActive = MOBILE_MORE.some((item) => location.pathname === item.to);
|
||||
const mobileShortLabel: Record<string, string> = {
|
||||
'/dashboard': 'Deck',
|
||||
@@ -215,9 +243,12 @@ export default function Layout({ children }: LayoutProps) {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`layout${isMobile ? ' layout--mobile' : ''}`}>
|
||||
<div
|
||||
className={`layout${isMobile ? ' layout--mobile' : ''}${othersOnline ? ' layout--comrades-online' : ''}`}
|
||||
data-operator-deck={operatorDeckId(location.pathname)}
|
||||
>
|
||||
{!isMobile && <CursorFire />}
|
||||
<AmbientBackground />
|
||||
<AmbientBackground weather={pageWeather} />
|
||||
<SacredGeometryLayer />
|
||||
<nav className="sidebar sidebar--desktop desktop-only">
|
||||
<div className="sidebar-header">
|
||||
@@ -238,15 +269,30 @@ export default function Layout({ children }: LayoutProps) {
|
||||
<NavLink
|
||||
key={item.to}
|
||||
to={item.to}
|
||||
className={({ isActive }) => `nav-item ${isActive ? 'active' : ''}`}
|
||||
className={({ isActive }) =>
|
||||
`nav-item${'glow' in item && item.glow ? ' nav-item--mission' : ''} ${isActive ? 'active' : ''}`
|
||||
}
|
||||
>
|
||||
<span className="nav-icon">
|
||||
<NavIcon type={item.icon} />
|
||||
</span>
|
||||
<span className="nav-label">{item.label}</span>
|
||||
{location.pathname === item.to && <span className="nav-glow" />}
|
||||
{location.pathname === item.to && (
|
||||
<span className={`nav-glow${'glow' in item && item.glow ? ' nav-glow--mission' : ''}`} />
|
||||
)}
|
||||
</NavLink>
|
||||
))}
|
||||
<a
|
||||
href={DOCS_HREF}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="nav-item nav-item--docs"
|
||||
>
|
||||
<span className="nav-icon">
|
||||
<NavIcon type="docs" />
|
||||
</span>
|
||||
<span className="nav-label">Documentation</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="sidebar-sacred-sigil" aria-hidden>
|
||||
@@ -258,6 +304,18 @@ export default function Layout({ children }: LayoutProps) {
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<FleetReadout />
|
||||
{othersOnline && (
|
||||
<div className="sidebar-comrades">
|
||||
<div className="sidebar-comrades-avatars">
|
||||
{comrades.slice(0, 4).map((c) => (
|
||||
<ComradeAvatar key={c.user} user={c.user} size="sm" />
|
||||
))}
|
||||
</div>
|
||||
<span className="sidebar-comrades-label">
|
||||
{comrades.length} comrade{comrades.length === 1 ? '' : 's'} online
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="sidebar-sig font-tech">
|
||||
<span className="sig-love">made with <span className="sig-heart">♥</span> drjones</span>
|
||||
<span className="sig-ver">v0.0.1</span>
|
||||
@@ -300,6 +358,16 @@ export default function Layout({ children }: LayoutProps) {
|
||||
{item.label}
|
||||
</NavLink>
|
||||
))}
|
||||
<a
|
||||
href={DOCS_HREF}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mobile-more-link mobile-more-link--docs"
|
||||
onClick={() => setMoreOpen(false)}
|
||||
>
|
||||
<NavIcon type="docs" />
|
||||
Documentation
|
||||
</a>
|
||||
</div>
|
||||
<nav className="mobile-bottom-nav" aria-label="Main navigation">
|
||||
{MOBILE_PRIMARY.map((item) => (
|
||||
|
||||
Reference in New Issue
Block a user