@jdcpuwiz/homelab-ui
v0.4.1
Published
Shared dark-theme UI primitives for the JdCpuWiz homelab — Sidebar, Button, Modal, PageHeader, TagChips, plus the canonical Tailwind preset, CSS vars, and next/font wiring.
Maintainers
Readme
@jdcpuwiz/homelab-ui
Shared dark-theme UI primitives for the JdCpuWiz homelab. Doghouse is the canonical reference; this package is the extracted version every other project consumes.
What you get
- Components —
Sidebar,SidebarNavItem,Button,Modal,ConfirmDialog,PageHeader,EmptyState,Spinner,TagChips - Tailwind preset — full token namespace (
bg-sidebar,bg-card,text-brand,bg-status-success,rounded-widget,w-sidebar, …) - Global CSS variables —
--hl-brand,--hl-sidebar,--hl-card, etc. Override at:rootto re-skin a project. - Font wiring snippet — Geist + Geist Mono + Orbitron via
next/font/google, with the exact 6-line snippet to paste intoapp/layout.tsx(next/font is a build-time API that can't be re-exported — see below)
Why this exists
Asset Den shipped 9 phases of "design-expert approved" code that turned out to have the wrong fonts because next/font was never imported — the CSS variable resolved to nothing and the whole app silently fell back to ui-sans-serif. Every homelab project was re-deriving the same primitives, sidebar shell, color palette, and font wiring; every project drifted. This package is the single source of truth so updates land everywhere.
Install
npm install @jdcpuwiz/homelab-uiPeer deps: react >= 18, next >= 15 (only for font wiring — non-Next consumers wire their own), tailwindcss ^3.
Setup (3 steps — copy-paste)
1. Wire fonts in app/layout.tsx
next/font/google is a build-time directive — Next's SWC plugin scans your app source for literal const Foo = Font(...) calls and a library can't re-export the loaders (bundling converts const to var and Next refuses). So the canonical setup is to paste this snippet directly into your layout:
import type { Metadata } from "next";
import { Geist, Geist_Mono, Orbitron } from "next/font/google";
import "@jdcpuwiz/homelab-ui/globals.css";
import "./globals.css"; // optional, your own project styles
const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });
const orbitron = Orbitron({ variable: "--font-orbitron", subsets: ["latin"] });
export const metadata: Metadata = { title: "My App" };
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className="dark">
<body className={`${geistSans.variable} ${geistMono.variable} ${orbitron.variable} antialiased`}>
{children}
</body>
</html>
);
}The variable names (--font-geist-sans, --font-geist-mono, --font-orbitron) match exactly what the Tailwind preset references for font-sans / font-mono / font-display. If you'd rather not memorize them, import them as constants:
import { FONT_CSS_VARIABLES } from "@jdcpuwiz/homelab-ui";
// → { sans: "--font-geist-sans", mono: "--font-geist-mono", display: "--font-orbitron" }Sanity check after deploy: open DevTools and run
getComputedStyle(document.body).fontFamily— it MUST start with"Geist". If it starts with"ui-sans-serif", your font wiring is broken. See "Troubleshooting" below.
2. Apply the Tailwind preset in tailwind.config.{js,ts}
module.exports = {
presets: [require("@jdcpuwiz/homelab-ui/tailwind-preset")],
content: [
"./app/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
// REQUIRED: pull classes from the package's compiled JS
"./node_modules/@jdcpuwiz/homelab-ui/dist/**/*.{js,mjs,cjs}",
],
};Skipping the node_modules/@jdcpuwiz/... entry means Tailwind won't see the classes the package uses internally → broken styles. This is the single most common setup mistake.
3. Render the sidebar
import { Sidebar, SidebarNavItem } from "@jdcpuwiz/homelab-ui";
import { LayoutDashboard, Server } from "lucide-react";
import Link from "next/link";
export default function AppShell({ children }) {
return (
<div className="flex h-screen overflow-hidden">
<Sidebar
logoSrc="/logo.png"
logoAlt="My App"
nav={
<>
<SidebarNavItem
label="Home"
icon={<LayoutDashboard size={16} />}
href="/"
render={({ href, className }, c) => (
<Link href={href} className={className}>
{c}
</Link>
)}
/>
<SidebarNavItem
label="Servers"
icon={<Server size={16} />}
href="/servers"
render={({ href, className }, c) => (
<Link href={href} className={className}>
{c}
</Link>
)}
/>
</>
}
footer="v0.1.0"
/>
<main className="flex-1 overflow-y-auto bg-[var(--hl-content)]">
{children}
</main>
</div>
);
}Done. The app boots with the right fonts, sidebar, and theme.
Components
| Component | Purpose |
|-------------------|---------|
| Sidebar | Doghouse-canonical shell. Slots: widgets, nav, middle, lower, admin, footer. w-60 fixed md+, off-canvas drawer on mobile (built-in hamburger + close X). |
| SidebarNavItem | Active = bg-brand + white text; inactive = text-white/45 + bg-white/5 hover. Supports href (auto <a>) or render (next/link wrapper). |
| Button | variant × size. Variants: primary (brand orange), secondary (card), tertiary (ghost), danger (red). Sizes: sm, md. |
| Modal | Overlay + click-outside + Esc + role=dialog + aria-* wiring + footer slot. density: "form" \| "media". |
| ConfirmDialog | Modal + Button×2, Enter triggers confirm. tone: "danger" \| "primary". |
| PageHeader | Title + description + back link + actions slot + optional header image (drop a PNG in /public/ to brand). Supports breadcrumbs. |
| EmptyState | panel (warm card body) or inline (text-only) variant. |
| Spinner | <Loader2 /> + label, sized for the dashboard's tertiary-text grey. |
| TagChips | Solid-color pills with auto white/black text contrast on yellow. Read-only <span> or interactive <button>. |
Helpers
import { cn, tagTextColor } from "@jdcpuwiz/homelab-ui";cn(...inputs)— clsx + tailwind-merge.tagTextColor(hex)— returns#ffffffor#000000depending on luminance. Use on any user-picked chip background so yellow doesn't render unreadable white text.
Tokens
The Tailwind preset exposes the canonical homelab palette:
| Token | Use |
|---------------------------------------------------------|-----|
| bg-sidebar bg-content bg-card bg-card-hover bg-card-warm | Surfaces (darkest → lightest) |
| bg-brand text-brand text-brand-ink bg-brand-hover ring-brand-glow | Brand orange — identity only, never status |
| bg-status-success (#15803d), bg-status-info (#1d4ed8), bg-status-warning (#eab308, black text), bg-status-danger (#b91c1c), bg-status-special (#6d28d9), bg-status-neutral (#6b7280), bg-status-empty (#4b5563), bg-status-primary (= brand, black text) | Status pills — solid bg + white text per the global rule (yellow + primary take black). |
| border-border border-input border-input-hover | Borders |
| bg-overlay (rgba(0,0,0,0.9)) bg-overlay-chip (rgba(0,0,0,0.6)) | Modal backdrops / hover chips over images |
| text-title (#ffffff) | h1 title color — override --hl-title at :root to re-theme |
| text-ink-primary text-ink-secondary text-ink-tertiary text-ink-disabled | When text-white/60 semantics aren't enough |
| rounded-widget (xl), rounded-row (lg), rounded-chip, rounded-panel (2xl) | Radius aliases |
| w-sidebar (default 15rem), w-logo h-logo (9rem) | Layout sizes (sidebar width is var(--hl-sidebar-width) — override at :root) |
| font-sans font-mono font-display | Geist / Geist Mono / Orbitron |
| text-2xs (10px / 14px) | Sidebar footers, grid captions |
Overriding tokens per app
The CSS variables are namespaced (--hl-*). Redefine them at :root in your own CSS — the preset's color tokens reference the vars, so your override wins everywhere automatically.
/* app/globals.css — re-skin your project */
:root {
--hl-brand: #00aaff; /* swap brand orange for blue */
--hl-sidebar: #0a1929;
--hl-card: #112844;
}Without Next.js
Every component works standalone. Just wire fonts yourself — load Geist + Geist Mono + Orbitron however your framework prefers (CSS @import url(...), <link> tag, etc.) and set the same three CSS variables on :root:
:root {
--font-geist-sans: "Geist", system-ui, sans-serif;
--font-geist-mono: "Geist Mono", ui-monospace, monospace;
--font-orbitron: "Orbitron", sans-serif;
}Troubleshooting
Fonts render as system sans-serif
Run getComputedStyle(document.body).fontFamily in DevTools. Symptoms:
- Returns
"ui-sans-serif", system-ui, ...→ thenext/fontsnippet isn't pasted inapp/layout.tsx, or${geistSans.variable}isn't applied to<body>'s className. Re-paste the snippet from "Setup → 1". - Returns
"Geist", ...→ working as intended.
Sidebar looks unstyled
You're missing the node_modules/@jdcpuwiz/homelab-ui/dist/** entry in your tailwind.config content array. Add it.
Modal renders but Tailwind classes don't apply
Same as above — Tailwind isn't scanning the package's dist files.
bg-brand is undefined
You skipped the Tailwind preset. Add presets: [require("@jdcpuwiz/homelab-ui/tailwind-preset")] to your tailwind.config. Alternatively, use the raw CSS variables: style={{ backgroundColor: "var(--hl-brand)" }}.
Local development
npm install
npm run build # tsup → dist/
npm run typecheck # tsc --noEmit
npm run ladle # reference renders at http://localhost:61000License
MIT
