flagon-io/g1t

public

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t/apps/sudo/app/components/shell.tsx

240 lines8,682 bytesCodeBlame
1/**
2 * sudo's frame. On wide screens, a sidebar: Overview and Reach out as
3 * links, then sections that fold open to their pages, as GitLab and
4 * Vercel do theirs (apps/web's project sidebar is the same pattern). On a
5 * phone, a top bar whose menu holds the same. sudo ships no JavaScript,
6 * so every fold is a <details>, drawn open on the server for the section
7 * holding the current page. All of it comes from lib/nav.ts.
8 */
9import {
10 Bot,
11 Building2,
12 Boxes,
13 ChevronRight,
14 CircleDollarSign,
15 Cloud,
16 CreditCard,
17 Eye,
18 FileText,
19 Gauge,
20 HandCoins,
21 Inbox,
22 LayoutDashboard,
23 LifeBuoy,
24 type LucideIcon,
25 Megaphone,
26 Menu,
27 ScrollText,
28 Server,
29 ShieldAlert,
30 ShieldCheck,
31 Tags,
32 UserCog,
33 Users,
34 UsersRound,
35 X,
36 Zap,
37} from "lucide-react";
38import { Link, useLocation } from "react-router";
39
40import { Logo } from "~/components/logo";
41import { NAV, type NavGroup, type NavIcon, type NavItem, holdsCurrent, isCurrent } from "~/lib/nav";
42
43const ICONS: Record<NavIcon, LucideIcon> = {
44 overview: LayoutDashboard,
45 workspaces: Boxes,
46 enterprises: Building2,
47 "reach-out": Zap,
48 people: Users,
49 invoices: FileText,
50 prices: Tags,
51 credits: HandCoins,
52 usage: Gauge,
53 stripe: CreditCard,
54 costs: Cloud,
55 agents: Bot,
56 abuse: ShieldAlert,
57 inbox: Inbox,
58 "view-as": Eye,
59 announcements: Megaphone,
60 staff: UserCog,
61 audit: ScrollText,
62 customers: UsersRound,
63 revenue: CircleDollarSign,
64 platform: Server,
65 support: LifeBuoy,
66 team: UserCog,
67};
68
69/** A page's or section's icon, as the sidebar shows it. */
70export function NavGlyph({ icon, size = 15, className }: { icon: NavIcon; size?: number; className?: string }) {
71 const Icon = ICONS[icon];
72 return <Icon size={size} className={className} />;
73}
74
75function SoonPill() {
76 return (
77 <span className="shrink-0 rounded-full px-1.5 py-px text-[0.625rem] font-medium tracking-wide text-muted uppercase ring-1 ring-line">
78 Soon
79 </span>
80 );
81}
82
83/** A top-level link, with its icon. */
84function TopLink({ item, pathname }: { item: NavItem; pathname: string }) {
85 const current = isCurrent(item, pathname);
86 return (
87 <Link
88 to={item.to}
89 title={item.about}
90 aria-current={current ? "page" : undefined}
91 className={`group flex h-8 items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors ${
92 current ? "bg-raised font-medium text-fg" : "text-muted hover:bg-raised/60 hover:text-fg"
93 }`}
94 >
95 <NavGlyph icon={item.icon} className={`shrink-0 ${current ? "text-merged" : "text-faint group-hover:text-muted"}`} />
96 <span className="min-w-0 grow truncate">{item.label}</span>
97 {item.soon && <SoonPill />}
98 </Link>
99 );
100}
101
102/** A page inside a section: indented, no icon, the current one marked in lavender. */
103function SubLink({ item, pathname }: { item: NavItem; pathname: string }) {
104 const current = isCurrent(item, pathname);
105 return (
106 <Link
107 to={item.to}
108 title={item.about}
109 aria-current={current ? "page" : undefined}
110 className={`relative flex h-8 items-center gap-2 rounded-md pr-2 pl-[2.375rem] text-[0.8125rem] transition-colors ${
111 current
112 ? "bg-raised font-medium text-fg before:absolute before:top-1.5 before:bottom-1.5 before:left-[1.1875rem] before:z-10 before:w-px before:bg-merged"
113 : item.soon
114 ? "text-faint hover:bg-raised/60 hover:text-muted"
115 : "text-muted hover:bg-raised/60 hover:text-fg"
116 }`}
117 >
118 <span className="min-w-0 grow truncate">{item.label}</span>
119 {item.soon && <SoonPill />}
120 </Link>
121 );
122}
123
124/** A section: one row that folds open to its pages. Open when it holds the current page. */
125function Section({ group, pathname }: { group: NavGroup & { title: string }; pathname: string }) {
126 const current = holdsCurrent(group, pathname);
127 const allSoon = group.items.every((item) => item.soon);
128 return (
129 <details open={current} className="group/section">
130 <summary
131 className={`group flex h-8 cursor-pointer list-none items-center gap-2.5 rounded-md px-2 text-[0.8125rem] transition-colors select-none hover:bg-raised/60 [&::-webkit-details-marker]:hidden ${
132 current ? "text-fg" : allSoon ? "text-faint hover:text-muted" : "text-muted hover:text-fg"
133 }`}
134 >
135 {group.icon && <NavGlyph icon={group.icon} className={`shrink-0 ${current ? "text-muted" : "text-faint group-hover:text-muted"}`} />}
136 <span className={`min-w-0 grow truncate ${current ? "font-medium" : ""}`}>{group.title}</span>
137 {allSoon && (
138 <span className="group-open/section:hidden">
139 <SoonPill />
140 </span>
141 )}
142 <ChevronRight size={14} aria-hidden="true" className="shrink-0 text-faint transition-transform duration-150 group-open/section:rotate-90" />
143 </summary>
144 <ul className="relative mt-px mb-1 space-y-px before:absolute before:top-1 before:bottom-1 before:left-[1.1875rem] before:w-px before:bg-line">
145 {group.items.map((item) => (
146 <li key={item.to}>
147 <SubLink item={item} pathname={pathname} />
148 </li>
149 ))}
150 </ul>
151 </details>
152 );
153}
154
155function NavTree({ pathname }: { pathname: string }) {
156 return (
157 <div className="space-y-px">
158 {NAV.map((group, index) =>
159 group.title ? (
160 <Section key={group.title} group={group as NavGroup & { title: string }} pathname={pathname} />
161 ) : (
162 <ul key={index} className={`space-y-px ${index === 0 ? "" : "pt-2"} ${index < NAV.length - 1 ? "pb-2" : ""}`}>
163 {group.items.map((item) => (
164 <li key={item.to}>
165 <TopLink item={item} pathname={pathname} />
166 </li>
167 ))}
168 </ul>
169 ),
170 )}
171 </div>
172 );
173}
174
175/** Who is signed in: a panel of its own at the foot of the menu. */
176function SignedIn({ email }: { email: string | null | undefined }) {
177 return (
178 <div className="border-t border-line bg-raised/40 px-4 py-3">
179 {email && (
180 <p title="Signed in through Cloudflare Access" className="flex items-center gap-2 text-[0.8125rem]">
181 <ShieldCheck size={15} className="shrink-0 text-merged" />
182 <span className="min-w-0 truncate font-mono text-xs text-fg-soft">{email}</span>
183 </p>
184 )}
185 <p className="mt-1.5 text-[0.6875rem] leading-snug text-faint">g1t staff only. Every change is recorded with who made it.</p>
186 </div>
187 );
188}
189
190/** The sidebar, from `lg` up. */
191export function Sidebar({ email }: { email: string | null | undefined }) {
192 const { pathname } = useLocation();
193 return (
194 <aside className="fixed inset-y-0 left-0 z-30 hidden w-60 flex-col border-r border-line bg-surface/50 lg:flex">
195 <div className="flex h-14 shrink-0 items-center px-4">
196 <Link to="/" aria-label="sudo overview">
197 <Logo />
198 </Link>
199 </div>
200 <nav aria-label="sudo" className="grow overflow-y-auto px-2 pt-2 pb-6">
201 <NavTree pathname={pathname} />
202 </nav>
203 <div className="shrink-0">
204 <SignedIn email={email} />
205 </div>
206 </aside>
207 );
208}
209
210/** The top bar and its menu, below `lg`. */
211export function MobileBar({ email }: { email: string | null | undefined }) {
212 const { pathname } = useLocation();
213 return (
214 <header className="sticky top-0 z-40 border-b border-line bg-surface/95 backdrop-blur lg:hidden">
215 <div className="flex h-14 items-center justify-between gap-2 px-4">
216 <Link to="/" aria-label="sudo overview">
217 <Logo />
218 </Link>
219 {/* A details element: it opens and closes with no script. A link
220 loads a new page, which arrives with the menu closed. */}
221 <details className="group/menu">
222 <summary
223 aria-label="Menu"
224 className="flex h-9 cursor-pointer list-none items-center gap-1.5 rounded-md border border-line px-2.5 text-sm text-muted transition-colors select-none hover:border-line-strong hover:text-fg group-open/menu:border-line-strong group-open/menu:text-fg [&::-webkit-details-marker]:hidden"
225 >
226 <Menu size={16} className="group-open/menu:hidden" />
227 <X size={16} className="hidden group-open/menu:block" />
228 Menu
229 </summary>
230 <div className="absolute inset-x-0 top-14 max-h-[calc(100dvh-3.5rem)] overflow-y-auto border-b border-line bg-bg shadow-2xl shadow-black/50">
231 <nav aria-label="sudo" className="px-2 pt-3 pb-4">
232 <NavTree pathname={pathname} />
233 </nav>
234 <SignedIn email={email} />
235 </div>
236 </details>
237 </div>
238 </header>
239 );
240}