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