@jdcpuwiz/homelab-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 JdCpuWiz
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,228 @@
1
+ # @jdcpuwiz/homelab-ui
2
+
3
+ Shared dark-theme UI primitives for the JdCpuWiz homelab. Doghouse is the canonical reference; this package is the extracted version every other project consumes.
4
+
5
+ **What you get**
6
+
7
+ - **Components** — `Sidebar`, `SidebarNavItem`, `Button`, `Modal`, `ConfirmDialog`, `PageHeader`, `EmptyState`, `Spinner`, `TagChips`
8
+ - **Tailwind preset** — full token namespace (`bg-sidebar`, `bg-card`, `text-brand`, `bg-status-success`, `rounded-widget`, `w-sidebar`, …)
9
+ - **Global CSS variables** — `--hl-brand`, `--hl-sidebar`, `--hl-card`, etc. Override at `:root` to re-skin a project.
10
+ - **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)
11
+
12
+ ## Why this exists
13
+
14
+ 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.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @jdcpuwiz/homelab-ui
20
+ ```
21
+
22
+ Peer deps: `react >= 18`, `next >= 15` (only for font wiring — non-Next consumers wire their own), `tailwindcss ^3`.
23
+
24
+ ## Setup (3 steps — copy-paste)
25
+
26
+ ### 1. Wire fonts in `app/layout.tsx`
27
+
28
+ `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:
29
+
30
+ ```tsx
31
+ import type { Metadata } from "next";
32
+ import { Geist, Geist_Mono, Orbitron } from "next/font/google";
33
+ import "@jdcpuwiz/homelab-ui/globals.css";
34
+ import "./globals.css"; // optional, your own project styles
35
+
36
+ const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
37
+ const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });
38
+ const orbitron = Orbitron({ variable: "--font-orbitron", subsets: ["latin"] });
39
+
40
+ export const metadata: Metadata = { title: "My App" };
41
+
42
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
43
+ return (
44
+ <html lang="en" className="dark">
45
+ <body className={`${geistSans.variable} ${geistMono.variable} ${orbitron.variable} antialiased`}>
46
+ {children}
47
+ </body>
48
+ </html>
49
+ );
50
+ }
51
+ ```
52
+
53
+ 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:
54
+
55
+ ```ts
56
+ import { FONT_CSS_VARIABLES } from "@jdcpuwiz/homelab-ui";
57
+ // → { sans: "--font-geist-sans", mono: "--font-geist-mono", display: "--font-orbitron" }
58
+ ```
59
+
60
+ > **Sanity check after deploy:** open DevTools and run
61
+ > `getComputedStyle(document.body).fontFamily` — it MUST start with `"Geist"`.
62
+ > If it starts with `"ui-sans-serif"`, your font wiring is broken. See
63
+ > "Troubleshooting" below.
64
+
65
+ ### 2. Apply the Tailwind preset in `tailwind.config.{js,ts}`
66
+
67
+ ```js
68
+ module.exports = {
69
+ presets: [require("@jdcpuwiz/homelab-ui/tailwind-preset")],
70
+ content: [
71
+ "./app/**/*.{ts,tsx}",
72
+ "./components/**/*.{ts,tsx}",
73
+ // REQUIRED: pull classes from the package's compiled JS
74
+ "./node_modules/@jdcpuwiz/homelab-ui/dist/**/*.{js,mjs,cjs}",
75
+ ],
76
+ };
77
+ ```
78
+
79
+ 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.
80
+
81
+ ### 3. Render the sidebar
82
+
83
+ ```tsx
84
+ import { Sidebar, SidebarNavItem } from "@jdcpuwiz/homelab-ui";
85
+ import { LayoutDashboard, Server } from "lucide-react";
86
+ import Link from "next/link";
87
+
88
+ export default function AppShell({ children }) {
89
+ return (
90
+ <div className="flex h-screen overflow-hidden">
91
+ <Sidebar
92
+ logoSrc="/logo.png"
93
+ logoAlt="My App"
94
+ nav={
95
+ <>
96
+ <SidebarNavItem
97
+ label="Home"
98
+ icon={<LayoutDashboard size={16} />}
99
+ href="/"
100
+ render={({ href, className }, c) => (
101
+ <Link href={href} className={className}>
102
+ {c}
103
+ </Link>
104
+ )}
105
+ />
106
+ <SidebarNavItem
107
+ label="Servers"
108
+ icon={<Server size={16} />}
109
+ href="/servers"
110
+ render={({ href, className }, c) => (
111
+ <Link href={href} className={className}>
112
+ {c}
113
+ </Link>
114
+ )}
115
+ />
116
+ </>
117
+ }
118
+ footer="v0.1.0"
119
+ />
120
+ <main className="flex-1 overflow-y-auto bg-[var(--hl-content)]">
121
+ {children}
122
+ </main>
123
+ </div>
124
+ );
125
+ }
126
+ ```
127
+
128
+ Done. The app boots with the right fonts, sidebar, and theme.
129
+
130
+ ## Components
131
+
132
+ | Component | Purpose |
133
+ |-------------------|---------|
134
+ | `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). |
135
+ | `SidebarNavItem` | Active = `bg-brand` + white text; inactive = `text-white/45` + `bg-white/5` hover. Supports `href` (auto `<a>`) or `render` (next/link wrapper). |
136
+ | `Button` | `variant` × `size`. Variants: `primary` (brand orange), `secondary` (card), `tertiary` (ghost), `danger` (red). Sizes: `sm`, `md`. |
137
+ | `Modal` | Overlay + click-outside + Esc + role=dialog + aria-* wiring + footer slot. `density: "form" \| "media"`. |
138
+ | `ConfirmDialog` | Modal + Button×2, Enter triggers confirm. `tone: "danger" \| "primary"`. |
139
+ | `PageHeader` | Title + description + back link + actions slot + optional header image (drop a PNG in `/public/` to brand). Supports breadcrumbs. |
140
+ | `EmptyState` | `panel` (warm card body) or `inline` (text-only) variant. |
141
+ | `Spinner` | `<Loader2 />` + label, sized for the dashboard's tertiary-text grey. |
142
+ | `TagChips` | Solid-color pills with auto white/black text contrast on yellow. Read-only `<span>` or interactive `<button>`. |
143
+
144
+ ### Helpers
145
+
146
+ ```ts
147
+ import { cn, tagTextColor } from "@jdcpuwiz/homelab-ui";
148
+ ```
149
+
150
+ - `cn(...inputs)` — clsx + tailwind-merge.
151
+ - `tagTextColor(hex)` — returns `#ffffff` or `#000000` depending on luminance. Use on any user-picked chip background so yellow doesn't render unreadable white text.
152
+
153
+ ## Tokens
154
+
155
+ The Tailwind preset exposes the canonical homelab palette:
156
+
157
+ | Token | Use |
158
+ |---------------------------------------------------------|-----|
159
+ | `bg-sidebar` `bg-content` `bg-card` `bg-card-hover` `bg-card-warm` | Surfaces (darkest → lightest) |
160
+ | `bg-brand` `text-brand` `text-brand-ink` `bg-brand-hover` `ring-brand-glow` | Brand orange — **identity only, never status** |
161
+ | `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). |
162
+ | `border-border` `border-input` `border-input-hover` | Borders |
163
+ | `bg-overlay` (`rgba(0,0,0,0.9)`) `bg-overlay-chip` (`rgba(0,0,0,0.6)`) | Modal backdrops / hover chips over images |
164
+ | `text-title` (`#aa89b7`) | h1 purple (doghouse convention) |
165
+ | `text-ink-primary` `text-ink-secondary` `text-ink-tertiary` `text-ink-disabled` | When `text-white/60` semantics aren't enough |
166
+ | `rounded-widget` (xl), `rounded-row` (lg), `rounded-chip`, `rounded-panel` (2xl) | Radius aliases |
167
+ | `w-sidebar` (15rem), `w-logo` `h-logo` (9rem) | Layout sizes |
168
+ | `font-sans` `font-mono` `font-display` | Geist / Geist Mono / Orbitron |
169
+ | `text-2xs` (10px / 14px) | Sidebar footers, grid captions |
170
+
171
+ ### Overriding tokens per app
172
+
173
+ 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.
174
+
175
+ ```css
176
+ /* app/globals.css — re-skin your project */
177
+ :root {
178
+ --hl-brand: #00aaff; /* swap brand orange for blue */
179
+ --hl-sidebar: #0a1929;
180
+ --hl-card: #112844;
181
+ }
182
+ ```
183
+
184
+ ## Without Next.js
185
+
186
+ 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`:
187
+
188
+ ```css
189
+ :root {
190
+ --font-geist-sans: "Geist", system-ui, sans-serif;
191
+ --font-geist-mono: "Geist Mono", ui-monospace, monospace;
192
+ --font-orbitron: "Orbitron", sans-serif;
193
+ }
194
+ ```
195
+
196
+ ## Troubleshooting
197
+
198
+ ### Fonts render as system sans-serif
199
+
200
+ Run `getComputedStyle(document.body).fontFamily` in DevTools. Symptoms:
201
+
202
+ - 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".
203
+ - Returns `"Geist", ...` → working as intended.
204
+
205
+ ### Sidebar looks unstyled
206
+
207
+ You're missing the `node_modules/@jdcpuwiz/homelab-ui/dist/**` entry in your `tailwind.config` `content` array. Add it.
208
+
209
+ ### Modal renders but Tailwind classes don't apply
210
+
211
+ Same as above — Tailwind isn't scanning the package's dist files.
212
+
213
+ ### `bg-brand` is undefined
214
+
215
+ 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)" }}`.
216
+
217
+ ## Local development
218
+
219
+ ```bash
220
+ npm install
221
+ npm run build # tsup → dist/
222
+ npm run typecheck # tsc --noEmit
223
+ npm run ladle # reference renders at http://localhost:61000
224
+ ```
225
+
226
+ ## License
227
+
228
+ MIT