@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.js
ADDED
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/components/Button.tsx
|
|
4
|
+
import { forwardRef } from "react";
|
|
5
|
+
|
|
6
|
+
// src/lib/utils.ts
|
|
7
|
+
import { clsx } from "clsx";
|
|
8
|
+
import { twMerge } from "tailwind-merge";
|
|
9
|
+
function cn(...inputs) {
|
|
10
|
+
return twMerge(clsx(inputs));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// src/components/Button.tsx
|
|
14
|
+
import { jsx } from "react/jsx-runtime";
|
|
15
|
+
var BASE = "inline-flex items-center justify-center gap-1.5 rounded-[0.5rem] font-semibold transition active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb84d]";
|
|
16
|
+
var VARIANT = {
|
|
17
|
+
primary: "bg-[#ff9900] text-[#0a0a0a] hover:bg-[#cc6a1a]",
|
|
18
|
+
secondary: "bg-[#1e1e1e] text-white hover:bg-[#252525]",
|
|
19
|
+
tertiary: "text-white/60 hover:bg-[#252525] hover:text-white",
|
|
20
|
+
danger: "bg-[#b91c1c] text-white hover:brightness-110"
|
|
21
|
+
};
|
|
22
|
+
var SIZE = {
|
|
23
|
+
sm: "px-3 py-1 text-xs",
|
|
24
|
+
md: "px-4 py-1.5 text-sm"
|
|
25
|
+
};
|
|
26
|
+
var Button = forwardRef(
|
|
27
|
+
function Button2({ variant = "secondary", size = "md", className, ...rest }, ref) {
|
|
28
|
+
return /* @__PURE__ */ jsx(
|
|
29
|
+
"button",
|
|
30
|
+
{
|
|
31
|
+
ref,
|
|
32
|
+
...rest,
|
|
33
|
+
className: cn(BASE, VARIANT[variant], SIZE[size], className)
|
|
34
|
+
}
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
// src/components/ConfirmDialog.tsx
|
|
40
|
+
import { useEffect as useEffect2 } from "react";
|
|
41
|
+
|
|
42
|
+
// src/components/Modal.tsx
|
|
43
|
+
import { useEffect, useRef } from "react";
|
|
44
|
+
import { X } from "lucide-react";
|
|
45
|
+
import { jsx as jsx2, jsxs } from "react/jsx-runtime";
|
|
46
|
+
function Modal({
|
|
47
|
+
open,
|
|
48
|
+
onClose,
|
|
49
|
+
title,
|
|
50
|
+
showClose,
|
|
51
|
+
density = "form",
|
|
52
|
+
maxWidth = "max-w-md",
|
|
53
|
+
footer,
|
|
54
|
+
children
|
|
55
|
+
}) {
|
|
56
|
+
const titleIdRef = useRef(
|
|
57
|
+
`hl-modal-${Math.random().toString(36).slice(2, 8)}`
|
|
58
|
+
);
|
|
59
|
+
useEffect(() => {
|
|
60
|
+
if (!open) return;
|
|
61
|
+
const onKey = (e) => {
|
|
62
|
+
if (e.key === "Escape") onClose();
|
|
63
|
+
};
|
|
64
|
+
window.addEventListener("keydown", onKey);
|
|
65
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
66
|
+
}, [open, onClose]);
|
|
67
|
+
if (!open) return null;
|
|
68
|
+
const showCloseBtn = showClose ?? Boolean(title);
|
|
69
|
+
const overlayClass = density === "media" ? "bg-black/90" : "bg-black/70";
|
|
70
|
+
return /* @__PURE__ */ jsx2(
|
|
71
|
+
"div",
|
|
72
|
+
{
|
|
73
|
+
className: cn(
|
|
74
|
+
"fixed inset-0 z-50 flex items-center justify-center p-4",
|
|
75
|
+
overlayClass
|
|
76
|
+
),
|
|
77
|
+
onClick: onClose,
|
|
78
|
+
role: "presentation",
|
|
79
|
+
children: /* @__PURE__ */ jsxs(
|
|
80
|
+
"div",
|
|
81
|
+
{
|
|
82
|
+
role: "dialog",
|
|
83
|
+
"aria-modal": "true",
|
|
84
|
+
"aria-labelledby": title ? titleIdRef.current : void 0,
|
|
85
|
+
className: cn(
|
|
86
|
+
"w-full rounded-[1rem] border-2 border-[#1e1e1e] bg-[#111111] p-5",
|
|
87
|
+
maxWidth
|
|
88
|
+
),
|
|
89
|
+
onClick: (e) => e.stopPropagation(),
|
|
90
|
+
children: [
|
|
91
|
+
(title || showCloseBtn) && /* @__PURE__ */ jsxs("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
|
|
92
|
+
title && /* @__PURE__ */ jsx2(
|
|
93
|
+
"h2",
|
|
94
|
+
{
|
|
95
|
+
id: titleIdRef.current,
|
|
96
|
+
className: "text-sm font-semibold uppercase tracking-widest text-white",
|
|
97
|
+
children: title
|
|
98
|
+
}
|
|
99
|
+
),
|
|
100
|
+
showCloseBtn && /* @__PURE__ */ jsx2(
|
|
101
|
+
"button",
|
|
102
|
+
{
|
|
103
|
+
type: "button",
|
|
104
|
+
onClick: onClose,
|
|
105
|
+
className: "ml-auto rounded-[0.25rem] p-1 text-white/40 transition hover:bg-[#252525] hover:text-white active:scale-90",
|
|
106
|
+
"aria-label": "Close",
|
|
107
|
+
children: /* @__PURE__ */ jsx2(X, { className: "h-4 w-4" })
|
|
108
|
+
}
|
|
109
|
+
)
|
|
110
|
+
] }),
|
|
111
|
+
children,
|
|
112
|
+
footer && /* @__PURE__ */ jsx2("div", { className: "mt-5 flex flex-wrap items-center justify-end gap-2", children: footer })
|
|
113
|
+
]
|
|
114
|
+
}
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// src/components/ConfirmDialog.tsx
|
|
121
|
+
import { Fragment, jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
122
|
+
function ConfirmDialog({
|
|
123
|
+
open,
|
|
124
|
+
title,
|
|
125
|
+
message,
|
|
126
|
+
confirmLabel = "Confirm",
|
|
127
|
+
cancelLabel = "Cancel",
|
|
128
|
+
tone = "danger",
|
|
129
|
+
onConfirm,
|
|
130
|
+
onClose
|
|
131
|
+
}) {
|
|
132
|
+
useEffect2(() => {
|
|
133
|
+
if (!open) return;
|
|
134
|
+
const onKey = (e) => {
|
|
135
|
+
if (e.key === "Enter") onConfirm();
|
|
136
|
+
};
|
|
137
|
+
window.addEventListener("keydown", onKey);
|
|
138
|
+
return () => window.removeEventListener("keydown", onKey);
|
|
139
|
+
}, [open, onConfirm]);
|
|
140
|
+
return /* @__PURE__ */ jsx3(
|
|
141
|
+
Modal,
|
|
142
|
+
{
|
|
143
|
+
open,
|
|
144
|
+
onClose,
|
|
145
|
+
title,
|
|
146
|
+
footer: /* @__PURE__ */ jsxs2(Fragment, { children: [
|
|
147
|
+
/* @__PURE__ */ jsx3(Button, { variant: "tertiary", size: "md", onClick: onClose, children: cancelLabel }),
|
|
148
|
+
/* @__PURE__ */ jsx3(
|
|
149
|
+
Button,
|
|
150
|
+
{
|
|
151
|
+
variant: tone === "danger" ? "danger" : "primary",
|
|
152
|
+
size: "md",
|
|
153
|
+
onClick: onConfirm,
|
|
154
|
+
children: confirmLabel
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
] }),
|
|
158
|
+
children: /* @__PURE__ */ jsx3("p", { className: "whitespace-pre-line text-sm text-white/70", children: message })
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// src/components/EmptyState.tsx
|
|
164
|
+
import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
165
|
+
function EmptyState({
|
|
166
|
+
icon,
|
|
167
|
+
title,
|
|
168
|
+
message,
|
|
169
|
+
action,
|
|
170
|
+
variant = "panel",
|
|
171
|
+
className
|
|
172
|
+
}) {
|
|
173
|
+
if (variant === "inline") {
|
|
174
|
+
return /* @__PURE__ */ jsx4("p", { className: cn("px-3 py-2 text-xs text-white/40", className), children: message });
|
|
175
|
+
}
|
|
176
|
+
return /* @__PURE__ */ jsxs3(
|
|
177
|
+
"div",
|
|
178
|
+
{
|
|
179
|
+
className: cn(
|
|
180
|
+
"flex flex-col items-center justify-center gap-3 rounded-[1rem] bg-[#2a2620] p-12 text-center",
|
|
181
|
+
className
|
|
182
|
+
),
|
|
183
|
+
children: [
|
|
184
|
+
icon && /* @__PURE__ */ jsx4("div", { className: "text-white/30", children: icon }),
|
|
185
|
+
title && /* @__PURE__ */ jsx4("h3", { className: "text-sm font-semibold text-white", children: title }),
|
|
186
|
+
/* @__PURE__ */ jsx4("p", { className: "text-sm text-white/60", children: message }),
|
|
187
|
+
action
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// src/components/PageHeader.tsx
|
|
194
|
+
import { useEffect as useEffect3, useState } from "react";
|
|
195
|
+
import { ArrowLeft, ChevronRight, ImageOff } from "lucide-react";
|
|
196
|
+
import { Fragment as Fragment2, jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
197
|
+
function PageHeader({
|
|
198
|
+
title,
|
|
199
|
+
description,
|
|
200
|
+
back,
|
|
201
|
+
actions,
|
|
202
|
+
imageSrc,
|
|
203
|
+
imageAlt,
|
|
204
|
+
breadcrumb,
|
|
205
|
+
className
|
|
206
|
+
}) {
|
|
207
|
+
const [imageErrored, setImageErrored] = useState(false);
|
|
208
|
+
useEffect3(() => {
|
|
209
|
+
setImageErrored(false);
|
|
210
|
+
}, [imageSrc]);
|
|
211
|
+
const hasImageSlot = !!imageSrc;
|
|
212
|
+
const imageRenders = hasImageSlot && !imageErrored;
|
|
213
|
+
const hasBreadcrumb = Array.isArray(breadcrumb) && breadcrumb.length > 0;
|
|
214
|
+
const textIsSrOnly = hasImageSlot;
|
|
215
|
+
const backInner = back ? /* @__PURE__ */ jsxs4(Fragment2, { children: [
|
|
216
|
+
/* @__PURE__ */ jsx5(ArrowLeft, { className: "h-3 w-3" }),
|
|
217
|
+
back.label
|
|
218
|
+
] }) : null;
|
|
219
|
+
return /* @__PURE__ */ jsxs4(
|
|
220
|
+
"header",
|
|
221
|
+
{
|
|
222
|
+
className: cn(
|
|
223
|
+
"flex flex-wrap items-end justify-between gap-3",
|
|
224
|
+
className
|
|
225
|
+
),
|
|
226
|
+
children: [
|
|
227
|
+
/* @__PURE__ */ jsxs4("div", { className: "flex min-w-0 items-end gap-4", children: [
|
|
228
|
+
hasImageSlot && (imageRenders ? (
|
|
229
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
230
|
+
/* @__PURE__ */ jsx5(
|
|
231
|
+
"img",
|
|
232
|
+
{
|
|
233
|
+
src: imageSrc,
|
|
234
|
+
alt: imageAlt ?? title,
|
|
235
|
+
onError: () => setImageErrored(true),
|
|
236
|
+
className: "h-28 w-28 shrink-0 rounded-[1rem] object-contain md:h-32 md:w-32"
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
|
+
) : /* @__PURE__ */ jsx5(PlaceholderHeader, { title, imageSrc })),
|
|
240
|
+
/* @__PURE__ */ jsxs4("div", { className: "min-w-0", children: [
|
|
241
|
+
back && (back.render ? back.render(
|
|
242
|
+
back.href,
|
|
243
|
+
/* @__PURE__ */ jsx5("span", { className: "inline-flex items-center gap-1 text-xs text-white/40 transition hover:text-white", children: backInner })
|
|
244
|
+
) : /* @__PURE__ */ jsx5(
|
|
245
|
+
"a",
|
|
246
|
+
{
|
|
247
|
+
href: back.href,
|
|
248
|
+
className: "inline-flex items-center gap-1 text-xs text-white/40 transition hover:text-white",
|
|
249
|
+
children: backInner
|
|
250
|
+
}
|
|
251
|
+
)),
|
|
252
|
+
hasBreadcrumb ? /* @__PURE__ */ jsx5(
|
|
253
|
+
Breadcrumb,
|
|
254
|
+
{
|
|
255
|
+
crumbs: breadcrumb,
|
|
256
|
+
srTitle: title,
|
|
257
|
+
className: back ? "mt-1" : ""
|
|
258
|
+
}
|
|
259
|
+
) : /* @__PURE__ */ jsx5(
|
|
260
|
+
"h1",
|
|
261
|
+
{
|
|
262
|
+
className: cn(
|
|
263
|
+
"text-xl font-semibold text-[#aa89b7]",
|
|
264
|
+
back && "mt-1",
|
|
265
|
+
textIsSrOnly && "sr-only"
|
|
266
|
+
),
|
|
267
|
+
children: title
|
|
268
|
+
}
|
|
269
|
+
),
|
|
270
|
+
description && /* @__PURE__ */ jsx5("p", { className: "mt-1 text-xs text-white/40", children: description })
|
|
271
|
+
] })
|
|
272
|
+
] }),
|
|
273
|
+
actions && /* @__PURE__ */ jsx5("div", { className: "flex items-center gap-2", children: actions })
|
|
274
|
+
]
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
function PlaceholderHeader({
|
|
279
|
+
title,
|
|
280
|
+
imageSrc
|
|
281
|
+
}) {
|
|
282
|
+
const file = imageSrc.replace(/^\//, "");
|
|
283
|
+
return /* @__PURE__ */ jsxs4(
|
|
284
|
+
"div",
|
|
285
|
+
{
|
|
286
|
+
title: `Drop ${file} in /public to brand this header`,
|
|
287
|
+
className: "flex h-28 w-28 shrink-0 flex-col items-center justify-center gap-1 rounded-[1rem] border-2 border-dashed border-[#1e1e1e] bg-[#2a2620] text-white/30 md:h-32 md:w-32",
|
|
288
|
+
children: [
|
|
289
|
+
/* @__PURE__ */ jsx5(ImageOff, { className: "h-6 w-6" }),
|
|
290
|
+
/* @__PURE__ */ jsx5("span", { className: "px-2 text-center text-[10px] font-semibold uppercase tracking-widest", children: title })
|
|
291
|
+
]
|
|
292
|
+
}
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
function Breadcrumb({
|
|
296
|
+
crumbs,
|
|
297
|
+
srTitle,
|
|
298
|
+
className
|
|
299
|
+
}) {
|
|
300
|
+
return /* @__PURE__ */ jsxs4(
|
|
301
|
+
"nav",
|
|
302
|
+
{
|
|
303
|
+
"aria-label": "Breadcrumb",
|
|
304
|
+
className: cn(
|
|
305
|
+
"flex flex-wrap items-center gap-1.5 text-xl font-semibold",
|
|
306
|
+
className
|
|
307
|
+
),
|
|
308
|
+
children: [
|
|
309
|
+
/* @__PURE__ */ jsx5("h1", { className: "sr-only", children: srTitle }),
|
|
310
|
+
crumbs.map((crumb, idx) => {
|
|
311
|
+
const isLast = idx === crumbs.length - 1;
|
|
312
|
+
return /* @__PURE__ */ jsxs4(
|
|
313
|
+
"span",
|
|
314
|
+
{
|
|
315
|
+
className: "flex items-center gap-1.5",
|
|
316
|
+
children: [
|
|
317
|
+
idx > 0 && /* @__PURE__ */ jsx5(
|
|
318
|
+
ChevronRight,
|
|
319
|
+
{
|
|
320
|
+
className: "h-4 w-4 shrink-0 text-white/30",
|
|
321
|
+
"aria-hidden": "true"
|
|
322
|
+
}
|
|
323
|
+
),
|
|
324
|
+
crumb.onClick && !isLast ? /* @__PURE__ */ jsx5(
|
|
325
|
+
"button",
|
|
326
|
+
{
|
|
327
|
+
type: "button",
|
|
328
|
+
onClick: crumb.onClick,
|
|
329
|
+
className: "rounded px-1 text-white/45 transition hover:text-white",
|
|
330
|
+
children: crumb.label
|
|
331
|
+
}
|
|
332
|
+
) : /* @__PURE__ */ jsx5(
|
|
333
|
+
"span",
|
|
334
|
+
{
|
|
335
|
+
className: cn(
|
|
336
|
+
isLast ? "text-white" : "text-white/45",
|
|
337
|
+
isLast && "px-1"
|
|
338
|
+
),
|
|
339
|
+
"aria-current": isLast ? "page" : void 0,
|
|
340
|
+
children: crumb.label
|
|
341
|
+
}
|
|
342
|
+
)
|
|
343
|
+
]
|
|
344
|
+
},
|
|
345
|
+
`${crumb.label}-${idx}`
|
|
346
|
+
);
|
|
347
|
+
})
|
|
348
|
+
]
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// src/components/Sidebar.tsx
|
|
354
|
+
import { useEffect as useEffect4, useState as useState2 } from "react";
|
|
355
|
+
import { Menu, X as X2 } from "lucide-react";
|
|
356
|
+
import { Fragment as Fragment3, jsx as jsx6, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
357
|
+
function Sidebar({
|
|
358
|
+
logoSrc,
|
|
359
|
+
logoAlt,
|
|
360
|
+
widgets,
|
|
361
|
+
nav,
|
|
362
|
+
middle,
|
|
363
|
+
lower,
|
|
364
|
+
admin,
|
|
365
|
+
footer,
|
|
366
|
+
mobileOpen: controlledOpen,
|
|
367
|
+
onMobileOpenChange,
|
|
368
|
+
className,
|
|
369
|
+
showMobileTopBar = true
|
|
370
|
+
}) {
|
|
371
|
+
const [internalOpen, setInternalOpen] = useState2(false);
|
|
372
|
+
const isControlled = controlledOpen !== void 0;
|
|
373
|
+
const open = isControlled ? controlledOpen : internalOpen;
|
|
374
|
+
const setOpen = (next) => {
|
|
375
|
+
if (!isControlled) setInternalOpen(next);
|
|
376
|
+
onMobileOpenChange?.(next);
|
|
377
|
+
};
|
|
378
|
+
useEffect4(() => {
|
|
379
|
+
if (typeof document === "undefined") return;
|
|
380
|
+
if (!open) return;
|
|
381
|
+
const prev = document.body.style.overflow;
|
|
382
|
+
document.body.style.overflow = "hidden";
|
|
383
|
+
return () => {
|
|
384
|
+
document.body.style.overflow = prev;
|
|
385
|
+
};
|
|
386
|
+
}, [open]);
|
|
387
|
+
return /* @__PURE__ */ jsxs5(Fragment3, { children: [
|
|
388
|
+
showMobileTopBar && /* @__PURE__ */ jsxs5("div", { className: "fixed inset-x-0 top-0 z-30 flex items-center gap-2 border-b-2 border-[#1e1e1e] bg-[#111111] px-3 py-2 md:hidden", children: [
|
|
389
|
+
/* @__PURE__ */ jsx6(
|
|
390
|
+
"button",
|
|
391
|
+
{
|
|
392
|
+
type: "button",
|
|
393
|
+
onClick: () => setOpen(true),
|
|
394
|
+
className: "rounded-[0.25rem] p-1.5 text-white transition hover:bg-[#252525] active:scale-90",
|
|
395
|
+
"aria-label": "Open menu",
|
|
396
|
+
children: /* @__PURE__ */ jsx6(Menu, { className: "h-5 w-5" })
|
|
397
|
+
}
|
|
398
|
+
),
|
|
399
|
+
/* @__PURE__ */ jsx6("img", { src: logoSrc, alt: "", width: 24, height: 24 }),
|
|
400
|
+
/* @__PURE__ */ jsx6("p", { className: "text-sm font-semibold text-white", children: logoAlt })
|
|
401
|
+
] }),
|
|
402
|
+
open && /* @__PURE__ */ jsx6(
|
|
403
|
+
"div",
|
|
404
|
+
{
|
|
405
|
+
className: "fixed inset-0 z-40 bg-black/70 md:hidden",
|
|
406
|
+
onClick: () => setOpen(false),
|
|
407
|
+
role: "presentation"
|
|
408
|
+
}
|
|
409
|
+
),
|
|
410
|
+
/* @__PURE__ */ jsxs5(
|
|
411
|
+
"aside",
|
|
412
|
+
{
|
|
413
|
+
className: cn(
|
|
414
|
+
"fixed inset-y-0 left-0 z-50 flex w-60 shrink-0 flex-col border-r-2 border-[#1e1e1e] bg-[#111111] transition-transform md:static md:translate-x-0 md:h-screen",
|
|
415
|
+
open ? "translate-x-0" : "-translate-x-full md:translate-x-0",
|
|
416
|
+
className
|
|
417
|
+
),
|
|
418
|
+
children: [
|
|
419
|
+
/* @__PURE__ */ jsxs5("div", { className: "relative flex flex-col items-center border-b-2 border-[#1e1e1e] px-4 pt-6 pb-5", children: [
|
|
420
|
+
/* @__PURE__ */ jsx6(
|
|
421
|
+
"img",
|
|
422
|
+
{
|
|
423
|
+
src: logoSrc,
|
|
424
|
+
alt: logoAlt,
|
|
425
|
+
className: "h-36 w-36 rounded-[1rem] object-contain"
|
|
426
|
+
}
|
|
427
|
+
),
|
|
428
|
+
showMobileTopBar && /* @__PURE__ */ jsx6(
|
|
429
|
+
"button",
|
|
430
|
+
{
|
|
431
|
+
type: "button",
|
|
432
|
+
onClick: () => setOpen(false),
|
|
433
|
+
className: "absolute right-3 top-3 rounded-[0.25rem] p-1 text-white/40 transition hover:bg-[#252525] hover:text-white md:hidden",
|
|
434
|
+
"aria-label": "Close menu",
|
|
435
|
+
children: /* @__PURE__ */ jsx6(X2, { className: "h-4 w-4" })
|
|
436
|
+
}
|
|
437
|
+
)
|
|
438
|
+
] }),
|
|
439
|
+
widgets && /* @__PURE__ */ jsx6("div", { className: "mx-3 mt-3", children: widgets }),
|
|
440
|
+
nav && /* @__PURE__ */ jsx6("nav", { className: "mx-3 mt-3 space-y-0.5 rounded-[0.75rem] bg-[#1e1e1e] p-2", children: nav }),
|
|
441
|
+
middle && /* @__PURE__ */ jsx6("div", { className: "mx-3 mt-3 flex-1 overflow-y-auto rounded-[0.75rem] bg-[#1e1e1e] p-2", children: middle }),
|
|
442
|
+
lower && /* @__PURE__ */ jsx6("nav", { className: "mx-3 mt-3 space-y-0.5 rounded-[0.75rem] bg-[#1e1e1e] p-2", children: lower }),
|
|
443
|
+
!middle && /* @__PURE__ */ jsx6("div", { className: "flex-1" }),
|
|
444
|
+
admin && /* @__PURE__ */ jsx6("div", { children: admin }),
|
|
445
|
+
footer && /* @__PURE__ */ jsx6("div", { className: "border-t-2 border-[#1e1e1e] px-4 py-3 text-center text-xs text-white/20", children: footer })
|
|
446
|
+
]
|
|
447
|
+
}
|
|
448
|
+
)
|
|
449
|
+
] });
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// src/components/SidebarNavItem.tsx
|
|
453
|
+
import { Fragment as Fragment4, jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
454
|
+
var ITEM_CLASS_BASE = "flex items-center gap-2 rounded-[0.5rem] px-3 py-2.5 text-base font-medium transition-all";
|
|
455
|
+
var itemClass = (active) => cn(
|
|
456
|
+
ITEM_CLASS_BASE,
|
|
457
|
+
active ? "bg-[#ff9900] text-white" : "text-white/45 hover:bg-white/5 hover:text-white"
|
|
458
|
+
);
|
|
459
|
+
function SidebarNavItem({
|
|
460
|
+
label,
|
|
461
|
+
icon,
|
|
462
|
+
active = false,
|
|
463
|
+
onClick,
|
|
464
|
+
href,
|
|
465
|
+
render
|
|
466
|
+
}) {
|
|
467
|
+
const className = itemClass(active);
|
|
468
|
+
const inner = /* @__PURE__ */ jsxs6(Fragment4, { children: [
|
|
469
|
+
icon,
|
|
470
|
+
/* @__PURE__ */ jsx7("span", { children: label })
|
|
471
|
+
] });
|
|
472
|
+
if (href && render) return /* @__PURE__ */ jsx7(Fragment4, { children: render({ href, className }, inner) });
|
|
473
|
+
if (href) {
|
|
474
|
+
return /* @__PURE__ */ jsx7("a", { href, className, children: inner });
|
|
475
|
+
}
|
|
476
|
+
return /* @__PURE__ */ jsx7(
|
|
477
|
+
"button",
|
|
478
|
+
{
|
|
479
|
+
type: "button",
|
|
480
|
+
onClick,
|
|
481
|
+
"aria-pressed": active,
|
|
482
|
+
className: `${className} w-full`,
|
|
483
|
+
children: inner
|
|
484
|
+
}
|
|
485
|
+
);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// src/components/Spinner.tsx
|
|
489
|
+
import { Loader2 } from "lucide-react";
|
|
490
|
+
import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
491
|
+
function Spinner({ label = "Loading\u2026", className }) {
|
|
492
|
+
return /* @__PURE__ */ jsxs7(
|
|
493
|
+
"p",
|
|
494
|
+
{
|
|
495
|
+
className: cn(
|
|
496
|
+
"flex items-center gap-2 px-3 py-2 text-xs text-white/40",
|
|
497
|
+
className
|
|
498
|
+
),
|
|
499
|
+
children: [
|
|
500
|
+
/* @__PURE__ */ jsx8(Loader2, { className: "h-3 w-3 animate-spin" }),
|
|
501
|
+
label
|
|
502
|
+
]
|
|
503
|
+
}
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// src/lib/colors.ts
|
|
508
|
+
function tagTextColor(hex) {
|
|
509
|
+
const normalized = hex.trim().replace(/^#/, "");
|
|
510
|
+
if (normalized.length !== 3 && normalized.length !== 6) return "#ffffff";
|
|
511
|
+
const expand = normalized.length === 3 ? normalized.split("").map((c) => c + c).join("") : normalized;
|
|
512
|
+
const r = parseInt(expand.slice(0, 2), 16);
|
|
513
|
+
const g = parseInt(expand.slice(2, 4), 16);
|
|
514
|
+
const b = parseInt(expand.slice(4, 6), 16);
|
|
515
|
+
if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return "#ffffff";
|
|
516
|
+
const luma = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
|
|
517
|
+
return luma > 0.65 ? "#000000" : "#ffffff";
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// src/components/TagChips.tsx
|
|
521
|
+
import { jsx as jsx9 } from "react/jsx-runtime";
|
|
522
|
+
function TagChips({ tags, size = "xs", onClick }) {
|
|
523
|
+
if (tags.length === 0) return null;
|
|
524
|
+
const sizeClasses = size === "xs" ? "px-1.5 py-0.5 text-[10px]" : "px-2 py-0.5 text-xs";
|
|
525
|
+
return /* @__PURE__ */ jsx9("div", { className: "flex flex-wrap gap-1", children: tags.map((t) => {
|
|
526
|
+
const style = {
|
|
527
|
+
backgroundColor: t.color,
|
|
528
|
+
color: tagTextColor(t.color)
|
|
529
|
+
};
|
|
530
|
+
const className = `inline-block rounded-[0.25rem] font-semibold ${sizeClasses}`;
|
|
531
|
+
return onClick ? /* @__PURE__ */ jsx9(
|
|
532
|
+
"button",
|
|
533
|
+
{
|
|
534
|
+
type: "button",
|
|
535
|
+
onClick: () => onClick(t),
|
|
536
|
+
className: `${className} transition active:scale-95`,
|
|
537
|
+
style,
|
|
538
|
+
children: t.name
|
|
539
|
+
},
|
|
540
|
+
t.id
|
|
541
|
+
) : /* @__PURE__ */ jsx9("span", { className, style, children: t.name }, t.id);
|
|
542
|
+
}) });
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// src/lib/fonts.ts
|
|
546
|
+
var FONT_CSS_VARIABLES = {
|
|
547
|
+
sans: "--font-geist-sans",
|
|
548
|
+
mono: "--font-geist-mono",
|
|
549
|
+
display: "--font-orbitron"
|
|
550
|
+
};
|
|
551
|
+
export {
|
|
552
|
+
Button,
|
|
553
|
+
ConfirmDialog,
|
|
554
|
+
EmptyState,
|
|
555
|
+
FONT_CSS_VARIABLES,
|
|
556
|
+
Modal,
|
|
557
|
+
PageHeader,
|
|
558
|
+
Sidebar,
|
|
559
|
+
SidebarNavItem,
|
|
560
|
+
Spinner,
|
|
561
|
+
TagChips,
|
|
562
|
+
cn,
|
|
563
|
+
tagTextColor
|
|
564
|
+
};
|
|
565
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/Button.tsx","../src/lib/utils.ts","../src/components/ConfirmDialog.tsx","../src/components/Modal.tsx","../src/components/EmptyState.tsx","../src/components/PageHeader.tsx","../src/components/Sidebar.tsx","../src/components/SidebarNavItem.tsx","../src/components/Spinner.tsx","../src/lib/colors.ts","../src/components/TagChips.tsx","../src/lib/fonts.ts"],"sourcesContent":["\"use client\";\n\nimport { forwardRef } from \"react\";\nimport { cn } from \"../lib/utils\";\n\ntype Variant = \"primary\" | \"secondary\" | \"tertiary\" | \"danger\";\ntype Size = \"sm\" | \"md\";\n\nexport type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {\n variant?: Variant;\n size?: Size;\n};\n\nconst BASE =\n \"inline-flex items-center justify-center gap-1.5 rounded-[0.5rem] font-semibold transition active:scale-95 disabled:cursor-not-allowed disabled:opacity-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-[#ffb84d]\";\n\nconst VARIANT: Record<Variant, string> = {\n primary: \"bg-[#ff9900] text-[#0a0a0a] hover:bg-[#cc6a1a]\",\n secondary: \"bg-[#1e1e1e] text-white hover:bg-[#252525]\",\n tertiary: \"text-white/60 hover:bg-[#252525] hover:text-white\",\n danger: \"bg-[#b91c1c] text-white hover:brightness-110\",\n};\n\nconst SIZE: Record<Size, string> = {\n sm: \"px-3 py-1 text-xs\",\n md: \"px-4 py-1.5 text-sm\",\n};\n\n/**\n * Homelab Button. Use the variant + size props; never pass raw color\n * classes through className. className is for layout extras only\n * (e.g. `w-full`).\n */\nexport const Button = forwardRef<HTMLButtonElement, ButtonProps>(\n function Button(\n { variant = \"secondary\", size = \"md\", className, ...rest },\n ref,\n ) {\n return (\n <button\n ref={ref}\n {...rest}\n className={cn(BASE, VARIANT[variant], SIZE[size], className)}\n />\n );\n },\n);\n","import { clsx, type ClassValue } from \"clsx\";\nimport { twMerge } from \"tailwind-merge\";\n\n/**\n * Conditional Tailwind class joiner. Merges variants and resolves\n * conflicts (e.g. `cn(\"p-2\", \"p-4\")` → `\"p-4\"`).\n */\nexport function cn(...inputs: ClassValue[]): string {\n return twMerge(clsx(inputs));\n}\n","\"use client\";\n\nimport { useEffect } from \"react\";\nimport { Modal } from \"./Modal\";\nimport { Button } from \"./Button\";\n\nexport type ConfirmDialogProps = {\n open: boolean;\n title: string;\n message: string;\n confirmLabel?: string;\n cancelLabel?: string;\n tone?: \"danger\" | \"primary\";\n onConfirm: () => void;\n onClose: () => void;\n};\n\n/**\n * Confirm/cancel dialog built on Modal + Button. Enter triggers confirm,\n * Esc cancels (via Modal's built-in handling).\n */\nexport function ConfirmDialog({\n open,\n title,\n message,\n confirmLabel = \"Confirm\",\n cancelLabel = \"Cancel\",\n tone = \"danger\",\n onConfirm,\n onClose,\n}: ConfirmDialogProps) {\n useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Enter\") onConfirm();\n };\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [open, onConfirm]);\n\n return (\n <Modal\n open={open}\n onClose={onClose}\n title={title}\n footer={\n <>\n <Button variant=\"tertiary\" size=\"md\" onClick={onClose}>\n {cancelLabel}\n </Button>\n <Button\n variant={tone === \"danger\" ? \"danger\" : \"primary\"}\n size=\"md\"\n onClick={onConfirm}\n >\n {confirmLabel}\n </Button>\n </>\n }\n >\n <p className=\"whitespace-pre-line text-sm text-white/70\">{message}</p>\n </Modal>\n );\n}\n","\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { X } from \"lucide-react\";\nimport { cn } from \"../lib/utils\";\n\ntype Density = \"form\" | \"media\";\n\nexport type ModalProps = {\n open: boolean;\n onClose: () => void;\n title?: string;\n /** Show the close X in the top right. Default: true when title is set. */\n showClose?: boolean;\n /**\n * Overlay weight:\n * `form` — dialogs / pickers / confirms (`bg-black/70`)\n * `media` — lightbox / asset preview (`bg-black/90`)\n * Defaults to `form`.\n */\n density?: Density;\n /** Max width — default `max-w-md`. */\n maxWidth?: string;\n /** Footer slot — render buttons here. Always right-aligned. */\n footer?: React.ReactNode;\n children: React.ReactNode;\n};\n\n/**\n * Single modal frame. Every dialog routes through this:\n * - overlay + click-outside dismiss\n * - Esc-to-close\n * - role=dialog + aria-modal + aria-labelledby wiring\n * - DESIGN.md header treatment (uppercase tracking-widest)\n * - shared right-aligned footer row\n */\nexport function Modal({\n open,\n onClose,\n title,\n showClose,\n density = \"form\",\n maxWidth = \"max-w-md\",\n footer,\n children,\n}: ModalProps) {\n const titleIdRef = useRef(\n `hl-modal-${Math.random().toString(36).slice(2, 8)}`,\n );\n\n useEffect(() => {\n if (!open) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n window.addEventListener(\"keydown\", onKey);\n return () => window.removeEventListener(\"keydown\", onKey);\n }, [open, onClose]);\n\n if (!open) return null;\n\n const showCloseBtn = showClose ?? Boolean(title);\n const overlayClass = density === \"media\" ? \"bg-black/90\" : \"bg-black/70\";\n\n return (\n <div\n className={cn(\n \"fixed inset-0 z-50 flex items-center justify-center p-4\",\n overlayClass,\n )}\n onClick={onClose}\n role=\"presentation\"\n >\n <div\n role=\"dialog\"\n aria-modal=\"true\"\n aria-labelledby={title ? titleIdRef.current : undefined}\n className={cn(\n \"w-full rounded-[1rem] border-2 border-[#1e1e1e] bg-[#111111] p-5\",\n maxWidth,\n )}\n onClick={(e) => e.stopPropagation()}\n >\n {(title || showCloseBtn) && (\n <div className=\"mb-4 flex items-start justify-between gap-3\">\n {title && (\n <h2\n id={titleIdRef.current}\n className=\"text-sm font-semibold uppercase tracking-widest text-white\"\n >\n {title}\n </h2>\n )}\n {showCloseBtn && (\n <button\n type=\"button\"\n onClick={onClose}\n className=\"ml-auto rounded-[0.25rem] p-1 text-white/40 transition hover:bg-[#252525] hover:text-white active:scale-90\"\n aria-label=\"Close\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n )}\n {children}\n {footer && (\n <div className=\"mt-5 flex flex-wrap items-center justify-end gap-2\">\n {footer}\n </div>\n )}\n </div>\n </div>\n );\n}\n","\"use client\";\n\nimport { cn } from \"../lib/utils\";\n\nexport type EmptyStateProps = {\n icon?: React.ReactNode;\n title?: string;\n message: string;\n action?: React.ReactNode;\n /**\n * `panel` — large empty state with warm card background (page bodies).\n * `inline` — compact text-only line for sidebar tree, popovers, etc.\n */\n variant?: \"panel\" | \"inline\";\n className?: string;\n};\n\nexport function EmptyState({\n icon,\n title,\n message,\n action,\n variant = \"panel\",\n className,\n}: EmptyStateProps) {\n if (variant === \"inline\") {\n return (\n <p className={cn(\"px-3 py-2 text-xs text-white/40\", className)}>\n {message}\n </p>\n );\n }\n return (\n <div\n className={cn(\n \"flex flex-col items-center justify-center gap-3 rounded-[1rem] bg-[#2a2620] p-12 text-center\",\n className,\n )}\n >\n {icon && <div className=\"text-white/30\">{icon}</div>}\n {title && (\n <h3 className=\"text-sm font-semibold text-white\">{title}</h3>\n )}\n <p className=\"text-sm text-white/60\">{message}</p>\n {action}\n </div>\n );\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { ArrowLeft, ChevronRight, ImageOff } from \"lucide-react\";\nimport { cn } from \"../lib/utils\";\n\nexport type Crumb = {\n label: string;\n onClick?: () => void;\n};\n\nexport type PageHeaderProps = {\n title: string;\n description?: string;\n /** Back link rendered above the title. */\n back?: { href: string; label: string; render?: (href: string, children: React.ReactNode) => React.ReactNode };\n /** Right-aligned actions slot. */\n actions?: React.ReactNode;\n /**\n * Header image URL (e.g. `/logos.png`). When the URL resolves, the\n * image is the visual title. When it 404s, a styled placeholder at\n * the same dimensions takes its place — the layout stays stable so\n * creating a new themed page \"just works\" the moment you drop a\n * matching PNG in /public/.\n *\n * When `imageSrc` is undefined, no image block renders and the\n * purple text-title becomes the visual title.\n */\n imageSrc?: string;\n imageAlt?: string;\n /**\n * Breadcrumb trail. When provided, replaces the text title. Last\n * crumb is current (white + bold); earlier crumbs are clickable.\n */\n breadcrumb?: Crumb[];\n className?: string;\n};\n\n/**\n * Page-header block. The image block reserves space whenever `imageSrc`\n * is set — drop the PNG in /public/ and the layout just fills in.\n *\n * Text title rules:\n * - imageSrc set + image present → text becomes sr-only\n * - imageSrc set + image 404 → placeholder block + text becomes sr-only\n * - imageSrc unset → text title is visible\n * - breadcrumb provided → breadcrumb replaces text title regardless of image\n *\n * The back link uses a plain <a> by default. In Next.js, pass\n * `back.render` to use next/link:\n *\n * import Link from \"next/link\";\n * <PageHeader back={{ href: \"/foo\", label: \"Back\", render: (href, c) => <Link href={href}>{c}</Link> }} />\n */\nexport function PageHeader({\n title,\n description,\n back,\n actions,\n imageSrc,\n imageAlt,\n breadcrumb,\n className,\n}: PageHeaderProps) {\n const [imageErrored, setImageErrored] = useState(false);\n useEffect(() => {\n setImageErrored(false);\n }, [imageSrc]);\n\n const hasImageSlot = !!imageSrc;\n const imageRenders = hasImageSlot && !imageErrored;\n const hasBreadcrumb = Array.isArray(breadcrumb) && breadcrumb.length > 0;\n const textIsSrOnly = hasImageSlot;\n\n const backInner = back ? (\n <>\n <ArrowLeft className=\"h-3 w-3\" />\n {back.label}\n </>\n ) : null;\n\n return (\n <header\n className={cn(\n \"flex flex-wrap items-end justify-between gap-3\",\n className,\n )}\n >\n <div className=\"flex min-w-0 items-end gap-4\">\n {hasImageSlot &&\n (imageRenders ? (\n // eslint-disable-next-line @next/next/no-img-element\n <img\n src={imageSrc}\n alt={imageAlt ?? title}\n onError={() => setImageErrored(true)}\n className=\"h-28 w-28 shrink-0 rounded-[1rem] object-contain md:h-32 md:w-32\"\n />\n ) : (\n <PlaceholderHeader title={title} imageSrc={imageSrc!} />\n ))}\n <div className=\"min-w-0\">\n {back &&\n (back.render ? (\n back.render(\n back.href,\n <span className=\"inline-flex items-center gap-1 text-xs text-white/40 transition hover:text-white\">\n {backInner}\n </span>,\n )\n ) : (\n <a\n href={back.href}\n className=\"inline-flex items-center gap-1 text-xs text-white/40 transition hover:text-white\"\n >\n {backInner}\n </a>\n ))}\n {hasBreadcrumb ? (\n <Breadcrumb\n crumbs={breadcrumb!}\n srTitle={title}\n className={back ? \"mt-1\" : \"\"}\n />\n ) : (\n <h1\n className={cn(\n \"text-xl font-semibold text-[#aa89b7]\",\n back && \"mt-1\",\n textIsSrOnly && \"sr-only\",\n )}\n >\n {title}\n </h1>\n )}\n {description && (\n <p className=\"mt-1 text-xs text-white/40\">{description}</p>\n )}\n </div>\n </div>\n {actions && <div className=\"flex items-center gap-2\">{actions}</div>}\n </header>\n );\n}\n\nfunction PlaceholderHeader({\n title,\n imageSrc,\n}: {\n title: string;\n imageSrc: string;\n}) {\n const file = imageSrc.replace(/^\\//, \"\");\n return (\n <div\n title={`Drop ${file} in /public to brand this header`}\n className=\"flex h-28 w-28 shrink-0 flex-col items-center justify-center gap-1 rounded-[1rem] border-2 border-dashed border-[#1e1e1e] bg-[#2a2620] text-white/30 md:h-32 md:w-32\"\n >\n <ImageOff className=\"h-6 w-6\" />\n <span className=\"px-2 text-center text-[10px] font-semibold uppercase tracking-widest\">\n {title}\n </span>\n </div>\n );\n}\n\nfunction Breadcrumb({\n crumbs,\n srTitle,\n className,\n}: {\n crumbs: Crumb[];\n srTitle: string;\n className?: string;\n}) {\n return (\n <nav\n aria-label=\"Breadcrumb\"\n className={cn(\n \"flex flex-wrap items-center gap-1.5 text-xl font-semibold\",\n className,\n )}\n >\n <h1 className=\"sr-only\">{srTitle}</h1>\n {crumbs.map((crumb, idx) => {\n const isLast = idx === crumbs.length - 1;\n return (\n <span\n key={`${crumb.label}-${idx}`}\n className=\"flex items-center gap-1.5\"\n >\n {idx > 0 && (\n <ChevronRight\n className=\"h-4 w-4 shrink-0 text-white/30\"\n aria-hidden=\"true\"\n />\n )}\n {crumb.onClick && !isLast ? (\n <button\n type=\"button\"\n onClick={crumb.onClick}\n className=\"rounded px-1 text-white/45 transition hover:text-white\"\n >\n {crumb.label}\n </button>\n ) : (\n <span\n className={cn(\n isLast ? \"text-white\" : \"text-white/45\",\n isLast && \"px-1\",\n )}\n aria-current={isLast ? \"page\" : undefined}\n >\n {crumb.label}\n </span>\n )}\n </span>\n );\n })}\n </nav>\n );\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { Menu, X } from \"lucide-react\";\nimport { cn } from \"../lib/utils\";\n\nexport type SidebarProps = {\n /**\n * Logo image src — typically `/logo.png` or `/<project>-logo-256.png`.\n * Rendered at w-36 h-36 inside a centered block with px-4 pt-6 pb-5\n * + border-b-2 (doghouse-canonical spec).\n */\n logoSrc: string;\n /** Alt text for the logo + label shown in the mobile top bar. */\n logoAlt: string;\n /**\n * Slots — every section is optional. Render them in this order:\n * 1. Logo block (always)\n * 2. widgets — clock / weather / homepage rotator / etc\n * 3. nav — primary navigation\n * 4. middle — long-running content (folder tree, etc) — flex-1\n * 5. lower — secondary nav (manage-tags, settings link, etc)\n * 6. (spacer)\n * 7. admin — admin section\n * 8. footer — version stamp, etc\n */\n widgets?: React.ReactNode;\n nav?: React.ReactNode;\n middle?: React.ReactNode;\n lower?: React.ReactNode;\n admin?: React.ReactNode;\n footer?: React.ReactNode;\n /**\n * Mobile-drawer behavior. Controlled — the consumer owns the open\n * state so they can wire it from anywhere (top-bar hamburger,\n * keyboard shortcut, route change, etc.).\n *\n * When omitted, the Sidebar manages its own drawer state using the\n * built-in mobile top bar (hamburger renders top-left on screens < md).\n */\n mobileOpen?: boolean;\n onMobileOpenChange?: (open: boolean) => void;\n /** Extra classes on the outer <aside>. */\n className?: string;\n /**\n * When true, render a built-in mobile top bar (logo + hamburger) on\n * screens < md. Defaults to true. Disable when the consumer renders\n * its own top chrome.\n */\n showMobileTopBar?: boolean;\n};\n\n/**\n * Canonical homelab sidebar shell. Doghouse is the reference impl;\n * this is the extracted version every other project consumes.\n *\n * Dimensions are non-negotiable:\n * - md+ width: 15rem (`w-60` / `w-sidebar`)\n * - md+ position: fixed-flex in a `display:flex` parent\n * - mobile: off-canvas drawer at full sidebar width\n * - logo block: w-36 h-36 centered, px-4 pt-6 pb-5, border-b-2\n *\n * The component renders raw hex values (#111111, #1e1e1e) so it works\n * even when the consumer skips the Tailwind preset. Apply the preset\n * + globals.css for token-based overrides.\n */\nexport function Sidebar({\n logoSrc,\n logoAlt,\n widgets,\n nav,\n middle,\n lower,\n admin,\n footer,\n mobileOpen: controlledOpen,\n onMobileOpenChange,\n className,\n showMobileTopBar = true,\n}: SidebarProps) {\n const [internalOpen, setInternalOpen] = useState(false);\n const isControlled = controlledOpen !== undefined;\n const open = isControlled ? controlledOpen : internalOpen;\n\n const setOpen = (next: boolean) => {\n if (!isControlled) setInternalOpen(next);\n onMobileOpenChange?.(next);\n };\n\n // Lock body scroll while the mobile drawer is open.\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n if (!open) return;\n const prev = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n return () => {\n document.body.style.overflow = prev;\n };\n }, [open]);\n\n return (\n <>\n {showMobileTopBar && (\n <div className=\"fixed inset-x-0 top-0 z-30 flex items-center gap-2 border-b-2 border-[#1e1e1e] bg-[#111111] px-3 py-2 md:hidden\">\n <button\n type=\"button\"\n onClick={() => setOpen(true)}\n className=\"rounded-[0.25rem] p-1.5 text-white transition hover:bg-[#252525] active:scale-90\"\n aria-label=\"Open menu\"\n >\n <Menu className=\"h-5 w-5\" />\n </button>\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img src={logoSrc} alt=\"\" width={24} height={24} />\n <p className=\"text-sm font-semibold text-white\">{logoAlt}</p>\n </div>\n )}\n\n {open && (\n <div\n className=\"fixed inset-0 z-40 bg-black/70 md:hidden\"\n onClick={() => setOpen(false)}\n role=\"presentation\"\n />\n )}\n\n <aside\n className={cn(\n \"fixed inset-y-0 left-0 z-50 flex w-60 shrink-0 flex-col border-r-2 border-[#1e1e1e] bg-[#111111] transition-transform md:static md:translate-x-0 md:h-screen\",\n open ? \"translate-x-0\" : \"-translate-x-full md:translate-x-0\",\n className,\n )}\n >\n <div className=\"relative flex flex-col items-center border-b-2 border-[#1e1e1e] px-4 pt-6 pb-5\">\n {/* eslint-disable-next-line @next/next/no-img-element */}\n <img\n src={logoSrc}\n alt={logoAlt}\n className=\"h-36 w-36 rounded-[1rem] object-contain\"\n />\n {showMobileTopBar && (\n <button\n type=\"button\"\n onClick={() => setOpen(false)}\n className=\"absolute right-3 top-3 rounded-[0.25rem] p-1 text-white/40 transition hover:bg-[#252525] hover:text-white md:hidden\"\n aria-label=\"Close menu\"\n >\n <X className=\"h-4 w-4\" />\n </button>\n )}\n </div>\n\n {widgets && <div className=\"mx-3 mt-3\">{widgets}</div>}\n\n {nav && (\n <nav className=\"mx-3 mt-3 space-y-0.5 rounded-[0.75rem] bg-[#1e1e1e] p-2\">\n {nav}\n </nav>\n )}\n\n {middle && (\n <div className=\"mx-3 mt-3 flex-1 overflow-y-auto rounded-[0.75rem] bg-[#1e1e1e] p-2\">\n {middle}\n </div>\n )}\n\n {lower && (\n <nav className=\"mx-3 mt-3 space-y-0.5 rounded-[0.75rem] bg-[#1e1e1e] p-2\">\n {lower}\n </nav>\n )}\n\n {!middle && <div className=\"flex-1\" />}\n\n {admin && <div>{admin}</div>}\n\n {footer && (\n <div className=\"border-t-2 border-[#1e1e1e] px-4 py-3 text-center text-xs text-white/20\">\n {footer}\n </div>\n )}\n </aside>\n </>\n );\n}\n","\"use client\";\n\nimport { cn } from \"../lib/utils\";\n\nexport type SidebarNavItemProps = {\n label: string;\n icon?: React.ReactNode;\n active?: boolean;\n onClick?: () => void;\n /**\n * When set, renders as <a href>. Otherwise renders as <button>. In\n * Next.js, prefer passing `render` so you can wrap in next/link\n * without losing classes:\n *\n * <SidebarNavItem\n * label=\"Home\"\n * href=\"/\"\n * render={(props, children) => <Link {...props}>{children}</Link>}\n * />\n */\n href?: string;\n render?: (\n props: { href: string; className: string },\n children: React.ReactNode,\n ) => React.ReactNode;\n};\n\nconst ITEM_CLASS_BASE =\n \"flex items-center gap-2 rounded-[0.5rem] px-3 py-2.5 text-base font-medium transition-all\";\n\nconst itemClass = (active: boolean) =>\n cn(\n ITEM_CLASS_BASE,\n active\n ? \"bg-[#ff9900] text-white\"\n : \"text-white/45 hover:bg-white/5 hover:text-white\",\n );\n\n/**\n * Single nav row for the Sidebar's `nav` / `lower` slots. Matches the\n * doghouse-canonical style: active = orange bg + white text, inactive =\n * white/45 text + white/5 hover bg.\n */\nexport function SidebarNavItem({\n label,\n icon,\n active = false,\n onClick,\n href,\n render,\n}: SidebarNavItemProps) {\n const className = itemClass(active);\n const inner = (\n <>\n {icon}\n <span>{label}</span>\n </>\n );\n\n if (href && render) return <>{render({ href, className }, inner)}</>;\n if (href) {\n return (\n <a href={href} className={className}>\n {inner}\n </a>\n );\n }\n return (\n <button\n type=\"button\"\n onClick={onClick}\n aria-pressed={active}\n className={`${className} w-full`}\n >\n {inner}\n </button>\n );\n}\n","\"use client\";\n\nimport { Loader2 } from \"lucide-react\";\nimport { cn } from \"../lib/utils\";\n\nexport type SpinnerProps = {\n label?: string;\n className?: string;\n};\n\n/**\n * Consistent inline loading affordance — used wherever the app would\n * otherwise say \"Loading…\" in plain text. Compact spinner + label, color\n * is the same tertiary-text grey across the app.\n */\nexport function Spinner({ label = \"Loading…\", className }: SpinnerProps) {\n return (\n <p\n className={cn(\n \"flex items-center gap-2 px-3 py-2 text-xs text-white/40\",\n className,\n )}\n >\n <Loader2 className=\"h-3 w-3 animate-spin\" />\n {label}\n </p>\n );\n}\n","/**\n * Pick black or white text for a solid color chip.\n *\n * The homelab status palette forces white text on every status color\n * EXCEPT yellow (warning: #eab308), which uses black for contrast. This\n * helper enforces that rule wherever a user-picked color drives a chip\n * background (TagChips, status badges, etc).\n *\n * The yellow rule generalizes — anything in the yellow band (high\n * luminance + warm hue) gets black ink.\n */\nexport function tagTextColor(hex: string): \"#ffffff\" | \"#000000\" {\n const normalized = hex.trim().replace(/^#/, \"\");\n if (normalized.length !== 3 && normalized.length !== 6) return \"#ffffff\";\n const expand =\n normalized.length === 3\n ? normalized\n .split(\"\")\n .map((c) => c + c)\n .join(\"\")\n : normalized;\n const r = parseInt(expand.slice(0, 2), 16);\n const g = parseInt(expand.slice(2, 4), 16);\n const b = parseInt(expand.slice(4, 6), 16);\n if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return \"#ffffff\";\n // Perceived luminance per ITU BT.601.\n const luma = (0.299 * r + 0.587 * g + 0.114 * b) / 255;\n return luma > 0.65 ? \"#000000\" : \"#ffffff\";\n}\n","\"use client\";\n\nimport { tagTextColor } from \"../lib/colors\";\n\nexport type TagRef = {\n id: string | number;\n name: string;\n color: string;\n};\n\nexport type TagChipsProps = {\n tags: TagRef[];\n size?: \"xs\" | \"sm\";\n onClick?: (tag: TagRef) => void;\n};\n\n/**\n * Render a row of solid-color tag chips. Yellow auto-flips text to\n * black per the homelab status-palette contrast rule (see tagTextColor).\n *\n * Renders `<button>` when interactive (`onClick` set), `<span>` when\n * read-only — keeps screen-reader semantics honest.\n */\nexport function TagChips({ tags, size = \"xs\", onClick }: TagChipsProps) {\n if (tags.length === 0) return null;\n const sizeClasses =\n size === \"xs\" ? \"px-1.5 py-0.5 text-[10px]\" : \"px-2 py-0.5 text-xs\";\n\n return (\n <div className=\"flex flex-wrap gap-1\">\n {tags.map((t) => {\n const style = {\n backgroundColor: t.color,\n color: tagTextColor(t.color),\n };\n const className = `inline-block rounded-[0.25rem] font-semibold ${sizeClasses}`;\n return onClick ? (\n <button\n key={t.id}\n type=\"button\"\n onClick={() => onClick(t)}\n className={`${className} transition active:scale-95`}\n style={style}\n >\n {t.name}\n </button>\n ) : (\n <span key={t.id} className={className} style={style}>\n {t.name}\n </span>\n );\n })}\n </div>\n );\n}\n","/**\n * Font CSS variable names used by `@jdcpuwiz/homelab-ui`'s Tailwind preset.\n *\n * Why constants and not re-exported loaders: Next.js's `next/font/google`\n * is a build-time directive scanned out of your *app* source by the SWC\n * plugin. The loader call MUST literally appear as `const X = Font(...)`\n * in your `app/layout.tsx`; pre-bundling it through a library turns\n * `const` into `var` and Next refuses to compile it. So the canonical\n * setup is \"paste this snippet into layout.tsx\" (see README) — and these\n * constants document the expected variable names so you wire `--font-*`\n * correctly without guessing.\n *\n * Snippet to paste into `app/layout.tsx`:\n *\n * import { Geist, Geist_Mono, Orbitron } from \"next/font/google\";\n *\n * const geistSans = Geist({ variable: \"--font-geist-sans\", subsets: [\"latin\"] });\n * const geistMono = Geist_Mono({ variable: \"--font-geist-mono\", subsets: [\"latin\"] });\n * const orbitron = Orbitron({ variable: \"--font-orbitron\", subsets: [\"latin\"] });\n *\n * <body className={`${geistSans.variable} ${geistMono.variable} ${orbitron.variable} antialiased`}>\n *\n * Sanity check after deploy:\n * getComputedStyle(document.body).fontFamily.startsWith('\"Geist\"')\n */\nexport const FONT_CSS_VARIABLES = {\n sans: \"--font-geist-sans\",\n mono: \"--font-geist-mono\",\n display: \"--font-orbitron\",\n} as const;\n"],"mappings":"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","names":["Button","useEffect","jsx","jsx","jsxs","useEffect","jsx","jsxs","useEffect","Fragment","jsx","jsxs","useEffect","useEffect","useState","X","Fragment","jsx","jsxs","useState","useEffect","X","Fragment","jsx","jsxs","jsx","jsxs","jsx"]}
|