@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 +21 -0
- package/README.md +228 -0
- package/dist/index.cjs +602 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +302 -0
- package/dist/index.d.ts +302 -0
- package/dist/index.js +565 -0
- package/dist/index.js.map +1 -0
- package/globals.css +56 -0
- package/package.json +86 -0
- package/tailwind-preset.cjs +135 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { ClassValue } from 'clsx';
|
|
3
|
+
|
|
4
|
+
type Variant = "primary" | "secondary" | "tertiary" | "danger";
|
|
5
|
+
type Size = "sm" | "md";
|
|
6
|
+
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
7
|
+
variant?: Variant;
|
|
8
|
+
size?: Size;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Homelab Button. Use the variant + size props; never pass raw color
|
|
12
|
+
* classes through className. className is for layout extras only
|
|
13
|
+
* (e.g. `w-full`).
|
|
14
|
+
*/
|
|
15
|
+
declare const Button: react.ForwardRefExoticComponent<react.ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
16
|
+
variant?: Variant;
|
|
17
|
+
size?: Size;
|
|
18
|
+
} & react.RefAttributes<HTMLButtonElement>>;
|
|
19
|
+
|
|
20
|
+
type ConfirmDialogProps = {
|
|
21
|
+
open: boolean;
|
|
22
|
+
title: string;
|
|
23
|
+
message: string;
|
|
24
|
+
confirmLabel?: string;
|
|
25
|
+
cancelLabel?: string;
|
|
26
|
+
tone?: "danger" | "primary";
|
|
27
|
+
onConfirm: () => void;
|
|
28
|
+
onClose: () => void;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Confirm/cancel dialog built on Modal + Button. Enter triggers confirm,
|
|
32
|
+
* Esc cancels (via Modal's built-in handling).
|
|
33
|
+
*/
|
|
34
|
+
declare function ConfirmDialog({ open, title, message, confirmLabel, cancelLabel, tone, onConfirm, onClose, }: ConfirmDialogProps): react.JSX.Element;
|
|
35
|
+
|
|
36
|
+
type EmptyStateProps = {
|
|
37
|
+
icon?: React.ReactNode;
|
|
38
|
+
title?: string;
|
|
39
|
+
message: string;
|
|
40
|
+
action?: React.ReactNode;
|
|
41
|
+
/**
|
|
42
|
+
* `panel` — large empty state with warm card background (page bodies).
|
|
43
|
+
* `inline` — compact text-only line for sidebar tree, popovers, etc.
|
|
44
|
+
*/
|
|
45
|
+
variant?: "panel" | "inline";
|
|
46
|
+
className?: string;
|
|
47
|
+
};
|
|
48
|
+
declare function EmptyState({ icon, title, message, action, variant, className, }: EmptyStateProps): react.JSX.Element;
|
|
49
|
+
|
|
50
|
+
type Density = "form" | "media";
|
|
51
|
+
type ModalProps = {
|
|
52
|
+
open: boolean;
|
|
53
|
+
onClose: () => void;
|
|
54
|
+
title?: string;
|
|
55
|
+
/** Show the close X in the top right. Default: true when title is set. */
|
|
56
|
+
showClose?: boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Overlay weight:
|
|
59
|
+
* `form` — dialogs / pickers / confirms (`bg-black/70`)
|
|
60
|
+
* `media` — lightbox / asset preview (`bg-black/90`)
|
|
61
|
+
* Defaults to `form`.
|
|
62
|
+
*/
|
|
63
|
+
density?: Density;
|
|
64
|
+
/** Max width — default `max-w-md`. */
|
|
65
|
+
maxWidth?: string;
|
|
66
|
+
/** Footer slot — render buttons here. Always right-aligned. */
|
|
67
|
+
footer?: React.ReactNode;
|
|
68
|
+
children: React.ReactNode;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Single modal frame. Every dialog routes through this:
|
|
72
|
+
* - overlay + click-outside dismiss
|
|
73
|
+
* - Esc-to-close
|
|
74
|
+
* - role=dialog + aria-modal + aria-labelledby wiring
|
|
75
|
+
* - DESIGN.md header treatment (uppercase tracking-widest)
|
|
76
|
+
* - shared right-aligned footer row
|
|
77
|
+
*/
|
|
78
|
+
declare function Modal({ open, onClose, title, showClose, density, maxWidth, footer, children, }: ModalProps): react.JSX.Element | null;
|
|
79
|
+
|
|
80
|
+
type Crumb = {
|
|
81
|
+
label: string;
|
|
82
|
+
onClick?: () => void;
|
|
83
|
+
};
|
|
84
|
+
type PageHeaderProps = {
|
|
85
|
+
title: string;
|
|
86
|
+
description?: string;
|
|
87
|
+
/** Back link rendered above the title. */
|
|
88
|
+
back?: {
|
|
89
|
+
href: string;
|
|
90
|
+
label: string;
|
|
91
|
+
render?: (href: string, children: React.ReactNode) => React.ReactNode;
|
|
92
|
+
};
|
|
93
|
+
/** Right-aligned actions slot. */
|
|
94
|
+
actions?: React.ReactNode;
|
|
95
|
+
/**
|
|
96
|
+
* Header image URL (e.g. `/logos.png`). When the URL resolves, the
|
|
97
|
+
* image is the visual title. When it 404s, a styled placeholder at
|
|
98
|
+
* the same dimensions takes its place — the layout stays stable so
|
|
99
|
+
* creating a new themed page "just works" the moment you drop a
|
|
100
|
+
* matching PNG in /public/.
|
|
101
|
+
*
|
|
102
|
+
* When `imageSrc` is undefined, no image block renders and the
|
|
103
|
+
* purple text-title becomes the visual title.
|
|
104
|
+
*/
|
|
105
|
+
imageSrc?: string;
|
|
106
|
+
imageAlt?: string;
|
|
107
|
+
/**
|
|
108
|
+
* Breadcrumb trail. When provided, replaces the text title. Last
|
|
109
|
+
* crumb is current (white + bold); earlier crumbs are clickable.
|
|
110
|
+
*/
|
|
111
|
+
breadcrumb?: Crumb[];
|
|
112
|
+
className?: string;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* Page-header block. The image block reserves space whenever `imageSrc`
|
|
116
|
+
* is set — drop the PNG in /public/ and the layout just fills in.
|
|
117
|
+
*
|
|
118
|
+
* Text title rules:
|
|
119
|
+
* - imageSrc set + image present → text becomes sr-only
|
|
120
|
+
* - imageSrc set + image 404 → placeholder block + text becomes sr-only
|
|
121
|
+
* - imageSrc unset → text title is visible
|
|
122
|
+
* - breadcrumb provided → breadcrumb replaces text title regardless of image
|
|
123
|
+
*
|
|
124
|
+
* The back link uses a plain <a> by default. In Next.js, pass
|
|
125
|
+
* `back.render` to use next/link:
|
|
126
|
+
*
|
|
127
|
+
* import Link from "next/link";
|
|
128
|
+
* <PageHeader back={{ href: "/foo", label: "Back", render: (href, c) => <Link href={href}>{c}</Link> }} />
|
|
129
|
+
*/
|
|
130
|
+
declare function PageHeader({ title, description, back, actions, imageSrc, imageAlt, breadcrumb, className, }: PageHeaderProps): react.JSX.Element;
|
|
131
|
+
|
|
132
|
+
type SidebarProps = {
|
|
133
|
+
/**
|
|
134
|
+
* Logo image src — typically `/logo.png` or `/<project>-logo-256.png`.
|
|
135
|
+
* Rendered at w-36 h-36 inside a centered block with px-4 pt-6 pb-5
|
|
136
|
+
* + border-b-2 (doghouse-canonical spec).
|
|
137
|
+
*/
|
|
138
|
+
logoSrc: string;
|
|
139
|
+
/** Alt text for the logo + label shown in the mobile top bar. */
|
|
140
|
+
logoAlt: string;
|
|
141
|
+
/**
|
|
142
|
+
* Slots — every section is optional. Render them in this order:
|
|
143
|
+
* 1. Logo block (always)
|
|
144
|
+
* 2. widgets — clock / weather / homepage rotator / etc
|
|
145
|
+
* 3. nav — primary navigation
|
|
146
|
+
* 4. middle — long-running content (folder tree, etc) — flex-1
|
|
147
|
+
* 5. lower — secondary nav (manage-tags, settings link, etc)
|
|
148
|
+
* 6. (spacer)
|
|
149
|
+
* 7. admin — admin section
|
|
150
|
+
* 8. footer — version stamp, etc
|
|
151
|
+
*/
|
|
152
|
+
widgets?: React.ReactNode;
|
|
153
|
+
nav?: React.ReactNode;
|
|
154
|
+
middle?: React.ReactNode;
|
|
155
|
+
lower?: React.ReactNode;
|
|
156
|
+
admin?: React.ReactNode;
|
|
157
|
+
footer?: React.ReactNode;
|
|
158
|
+
/**
|
|
159
|
+
* Mobile-drawer behavior. Controlled — the consumer owns the open
|
|
160
|
+
* state so they can wire it from anywhere (top-bar hamburger,
|
|
161
|
+
* keyboard shortcut, route change, etc.).
|
|
162
|
+
*
|
|
163
|
+
* When omitted, the Sidebar manages its own drawer state using the
|
|
164
|
+
* built-in mobile top bar (hamburger renders top-left on screens < md).
|
|
165
|
+
*/
|
|
166
|
+
mobileOpen?: boolean;
|
|
167
|
+
onMobileOpenChange?: (open: boolean) => void;
|
|
168
|
+
/** Extra classes on the outer <aside>. */
|
|
169
|
+
className?: string;
|
|
170
|
+
/**
|
|
171
|
+
* When true, render a built-in mobile top bar (logo + hamburger) on
|
|
172
|
+
* screens < md. Defaults to true. Disable when the consumer renders
|
|
173
|
+
* its own top chrome.
|
|
174
|
+
*/
|
|
175
|
+
showMobileTopBar?: boolean;
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Canonical homelab sidebar shell. Doghouse is the reference impl;
|
|
179
|
+
* this is the extracted version every other project consumes.
|
|
180
|
+
*
|
|
181
|
+
* Dimensions are non-negotiable:
|
|
182
|
+
* - md+ width: 15rem (`w-60` / `w-sidebar`)
|
|
183
|
+
* - md+ position: fixed-flex in a `display:flex` parent
|
|
184
|
+
* - mobile: off-canvas drawer at full sidebar width
|
|
185
|
+
* - logo block: w-36 h-36 centered, px-4 pt-6 pb-5, border-b-2
|
|
186
|
+
*
|
|
187
|
+
* The component renders raw hex values (#111111, #1e1e1e) so it works
|
|
188
|
+
* even when the consumer skips the Tailwind preset. Apply the preset
|
|
189
|
+
* + globals.css for token-based overrides.
|
|
190
|
+
*/
|
|
191
|
+
declare function Sidebar({ logoSrc, logoAlt, widgets, nav, middle, lower, admin, footer, mobileOpen: controlledOpen, onMobileOpenChange, className, showMobileTopBar, }: SidebarProps): react.JSX.Element;
|
|
192
|
+
|
|
193
|
+
type SidebarNavItemProps = {
|
|
194
|
+
label: string;
|
|
195
|
+
icon?: React.ReactNode;
|
|
196
|
+
active?: boolean;
|
|
197
|
+
onClick?: () => void;
|
|
198
|
+
/**
|
|
199
|
+
* When set, renders as <a href>. Otherwise renders as <button>. In
|
|
200
|
+
* Next.js, prefer passing `render` so you can wrap in next/link
|
|
201
|
+
* without losing classes:
|
|
202
|
+
*
|
|
203
|
+
* <SidebarNavItem
|
|
204
|
+
* label="Home"
|
|
205
|
+
* href="/"
|
|
206
|
+
* render={(props, children) => <Link {...props}>{children}</Link>}
|
|
207
|
+
* />
|
|
208
|
+
*/
|
|
209
|
+
href?: string;
|
|
210
|
+
render?: (props: {
|
|
211
|
+
href: string;
|
|
212
|
+
className: string;
|
|
213
|
+
}, children: React.ReactNode) => React.ReactNode;
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Single nav row for the Sidebar's `nav` / `lower` slots. Matches the
|
|
217
|
+
* doghouse-canonical style: active = orange bg + white text, inactive =
|
|
218
|
+
* white/45 text + white/5 hover bg.
|
|
219
|
+
*/
|
|
220
|
+
declare function SidebarNavItem({ label, icon, active, onClick, href, render, }: SidebarNavItemProps): react.JSX.Element;
|
|
221
|
+
|
|
222
|
+
type SpinnerProps = {
|
|
223
|
+
label?: string;
|
|
224
|
+
className?: string;
|
|
225
|
+
};
|
|
226
|
+
/**
|
|
227
|
+
* Consistent inline loading affordance — used wherever the app would
|
|
228
|
+
* otherwise say "Loading…" in plain text. Compact spinner + label, color
|
|
229
|
+
* is the same tertiary-text grey across the app.
|
|
230
|
+
*/
|
|
231
|
+
declare function Spinner({ label, className }: SpinnerProps): react.JSX.Element;
|
|
232
|
+
|
|
233
|
+
type TagRef = {
|
|
234
|
+
id: string | number;
|
|
235
|
+
name: string;
|
|
236
|
+
color: string;
|
|
237
|
+
};
|
|
238
|
+
type TagChipsProps = {
|
|
239
|
+
tags: TagRef[];
|
|
240
|
+
size?: "xs" | "sm";
|
|
241
|
+
onClick?: (tag: TagRef) => void;
|
|
242
|
+
};
|
|
243
|
+
/**
|
|
244
|
+
* Render a row of solid-color tag chips. Yellow auto-flips text to
|
|
245
|
+
* black per the homelab status-palette contrast rule (see tagTextColor).
|
|
246
|
+
*
|
|
247
|
+
* Renders `<button>` when interactive (`onClick` set), `<span>` when
|
|
248
|
+
* read-only — keeps screen-reader semantics honest.
|
|
249
|
+
*/
|
|
250
|
+
declare function TagChips({ tags, size, onClick }: TagChipsProps): react.JSX.Element | null;
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Conditional Tailwind class joiner. Merges variants and resolves
|
|
254
|
+
* conflicts (e.g. `cn("p-2", "p-4")` → `"p-4"`).
|
|
255
|
+
*/
|
|
256
|
+
declare function cn(...inputs: ClassValue[]): string;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Pick black or white text for a solid color chip.
|
|
260
|
+
*
|
|
261
|
+
* The homelab status palette forces white text on every status color
|
|
262
|
+
* EXCEPT yellow (warning: #eab308), which uses black for contrast. This
|
|
263
|
+
* helper enforces that rule wherever a user-picked color drives a chip
|
|
264
|
+
* background (TagChips, status badges, etc).
|
|
265
|
+
*
|
|
266
|
+
* The yellow rule generalizes — anything in the yellow band (high
|
|
267
|
+
* luminance + warm hue) gets black ink.
|
|
268
|
+
*/
|
|
269
|
+
declare function tagTextColor(hex: string): "#ffffff" | "#000000";
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Font CSS variable names used by `@jdcpuwiz/homelab-ui`'s Tailwind preset.
|
|
273
|
+
*
|
|
274
|
+
* Why constants and not re-exported loaders: Next.js's `next/font/google`
|
|
275
|
+
* is a build-time directive scanned out of your *app* source by the SWC
|
|
276
|
+
* plugin. The loader call MUST literally appear as `const X = Font(...)`
|
|
277
|
+
* in your `app/layout.tsx`; pre-bundling it through a library turns
|
|
278
|
+
* `const` into `var` and Next refuses to compile it. So the canonical
|
|
279
|
+
* setup is "paste this snippet into layout.tsx" (see README) — and these
|
|
280
|
+
* constants document the expected variable names so you wire `--font-*`
|
|
281
|
+
* correctly without guessing.
|
|
282
|
+
*
|
|
283
|
+
* Snippet to paste into `app/layout.tsx`:
|
|
284
|
+
*
|
|
285
|
+
* import { Geist, Geist_Mono, Orbitron } from "next/font/google";
|
|
286
|
+
*
|
|
287
|
+
* const geistSans = Geist({ variable: "--font-geist-sans", subsets: ["latin"] });
|
|
288
|
+
* const geistMono = Geist_Mono({ variable: "--font-geist-mono", subsets: ["latin"] });
|
|
289
|
+
* const orbitron = Orbitron({ variable: "--font-orbitron", subsets: ["latin"] });
|
|
290
|
+
*
|
|
291
|
+
* <body className={`${geistSans.variable} ${geistMono.variable} ${orbitron.variable} antialiased`}>
|
|
292
|
+
*
|
|
293
|
+
* Sanity check after deploy:
|
|
294
|
+
* getComputedStyle(document.body).fontFamily.startsWith('"Geist"')
|
|
295
|
+
*/
|
|
296
|
+
declare const FONT_CSS_VARIABLES: {
|
|
297
|
+
readonly sans: "--font-geist-sans";
|
|
298
|
+
readonly mono: "--font-geist-mono";
|
|
299
|
+
readonly display: "--font-orbitron";
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
export { Button, type ButtonProps, ConfirmDialog, type ConfirmDialogProps, type Crumb, EmptyState, type EmptyStateProps, FONT_CSS_VARIABLES, Modal, type ModalProps, PageHeader, type PageHeaderProps, Sidebar, SidebarNavItem, type SidebarNavItemProps, type SidebarProps, Spinner, type SpinnerProps, TagChips, type TagChipsProps, type TagRef, cn, tagTextColor };
|