npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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.

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

  • ComponentsSidebar, 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 :root to re-skin a project.
  • Font wiring snippet — Geist + Geist Mono + Orbitron via next/font/google, with the exact 6-line snippet to paste into app/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-ui

Peer 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 #ffffff or #000000 depending 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, ... → the next/font snippet isn't pasted in app/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:61000

License

MIT