Import december-2020-12-18 from private repo (scrubbed 2026-05-27)

Targets Rec Room 2020.12.18 client.

Re-import after adding:
- DorkNet.ClientMod/dorknet-clientmod.json.template — config schema
  the launcher's manifest-driven patcher renders into UserData/.
- tools/build-patcher-zip.ps1 — produces the dorknet-clientpatch-{key}.zip
  Release asset the launcher fetches.
- .github/workflows/release.yml — auto-publishes both Release assets
  on v1-december-* tag push.

Scrubbed via maintainer-side scrub-for-public.ps1: same drop rules as
the prior import, plus build-patcher-zip.ps1 added to the tools/
allow-list so it survives the scrub.
This commit is contained in:
Alexa
2026-05-27 01:04:12 +03:00
commit 4edf25b2d5
375 changed files with 201291 additions and 0 deletions
@@ -0,0 +1,146 @@
import { useEffect } from 'react';
import { NavLink } from 'react-router-dom';
import {
LayoutDashboard, Users, Building2, Ban, Flag, ScrollText, Activity,
Megaphone, Upload, ShoppingBag, Gift, KeyRound, Wrench, MessageSquare, Settings, X,
} from './Icons';
const groups: Array<{ title: string; items: Array<{ to: string; label: string; icon: React.ReactNode; new?: boolean }>}> = [
{
title: 'Overview',
items: [
{ to: '/', label: 'Dashboard', icon: <LayoutDashboard /> },
],
},
{
title: 'People',
items: [
{ to: '/players', label: 'Players', icon: <Users /> },
{ to: '/bans', label: 'Bans', icon: <Ban /> },
{ to: '/reports', label: 'Reports', icon: <Flag /> },
{ to: '/audit', label: 'Audit log', icon: <ScrollText /> },
{ to: '/logs', label: 'Player logs', icon: <Activity /> },
],
},
{
title: 'Content',
items: [
// Rooms is the unified view — clicking a row opens /rooms/:id
// which carries the old RR-Originals edit modal, live Photon
// instance list, recent visitors, ownership management, and
// per-room leaderboard channels in one tabbed page.
{ to: '/rooms', label: 'Rooms', icon: <Building2 /> },
{ to: '/import-room', label: 'Import room', icon: <Upload /> },
{ to: '/store', label: 'Store catalog', icon: <ShoppingBag /> },
{ to: '/community', label: 'Community board', icon: <MessageSquare /> },
{ to: '/loading-tips', label: 'Loading screen tips', icon: <MessageSquare /> },
],
},
{
title: 'Tools',
items: [
{ to: '/broadcast', label: 'Broadcast', icon: <Megaphone /> },
{ to: '/gift', label: 'Gift items', icon: <Gift />, new: true },
{ to: '/passwords', label: 'Reset passwords', icon: <KeyRound />, new: true },
{ to: '/grants', label: 'Grant maker pen', icon: <Wrench />, new: true },
{ to: '/settings', label: 'Server settings', icon: <Settings />, new: true },
],
},
];
// Two-mode rendering:
// * Desktop (md+): always-visible static rail to the left of <main>.
// * Mobile: hidden by default; opens as a slide-in drawer with a
// backdrop scrim, controlled by `open` from <Layout>. Tapping any
// nav link calls `onNavigate()` so the drawer closes automatically
// after the user picks a destination. Escape also closes.
export function Sidebar({ open, onClose, onNavigate }: { open: boolean; onClose: () => void; onNavigate: () => void }) {
// Lock body scroll while the mobile drawer is up so the page behind
// doesn't scroll through the backdrop. Skip on desktop where `open`
// is irrelevant (the sidebar is always there).
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') onClose(); };
document.addEventListener('keydown', onKey);
const prev = document.body.style.overflow;
document.body.style.overflow = 'hidden';
return () => {
document.removeEventListener('keydown', onKey);
document.body.style.overflow = prev;
};
}, [open, onClose]);
const inner = (
<>
<div className="px-2 pb-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="size-8 rounded-md bg-gradient-to-br from-brand-400 to-brand-600 flex items-center justify-center font-bold text-white">D</div>
<div>
<div className="text-sm font-semibold tracking-tight text-ink-50">DorkNet</div>
<div className="text-[10px] uppercase tracking-widest text-ink-400">admin</div>
</div>
</div>
<button
onClick={onClose}
className="md:hidden rounded-md p-1 text-ink-400 hover:bg-ink-800 hover:text-ink-200"
aria-label="Close menu"
>
<X />
</button>
</div>
<nav className="flex flex-col gap-3 overflow-y-auto">
{groups.map(group => (
<div key={group.title}>
<div className="px-2 pb-1 text-[10px] font-semibold uppercase tracking-widest text-ink-400">{group.title}</div>
<div className="flex flex-col gap-0.5">
{group.items.map(item => (
<NavLink
key={item.to}
to={item.to}
end={item.to === '/'}
onClick={onNavigate}
className={({ isActive }) =>
`group flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm transition-colors ${
isActive
? 'bg-brand-500/15 text-brand-100 ring-1 ring-inset ring-brand-500/30'
: 'text-ink-200 hover:bg-ink-800 hover:text-ink-50'
}`
}
>
<span className="text-ink-300 group-[.active]:text-brand-200">{item.icon}</span>
<span className="flex-1">{item.label}</span>
{item.new && <span className="badge badge-neutral !bg-brand-500/20 !text-brand-200">new</span>}
</NavLink>
))}
</div>
</div>
))}
</nav>
</>
);
return (
<>
{/* Mobile drawer + backdrop */}
<div
className={`md:hidden fixed inset-0 z-40 transition-opacity ${open ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}`}
onClick={onClose}
aria-hidden={!open}
>
<div className="absolute inset-0 bg-ink-950/70 backdrop-blur-sm" />
</div>
<aside
className={`md:hidden fixed inset-y-0 left-0 z-50 w-72 max-w-[85vw] flex flex-col gap-1 border-r border-ink-800 bg-ink-900 px-3 py-4 transition-transform duration-200 ${open ? 'translate-x-0' : '-translate-x-full'}`}
aria-hidden={!open}
role="dialog"
aria-label="Navigation"
>
{inner}
</aside>
{/* Desktop static rail */}
<aside className="hidden md:flex md:w-60 lg:w-64 flex-col gap-1 border-r border-ink-800 bg-ink-900/60 px-3 py-4">
{inner}
</aside>
</>
);
}