@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/dist/index.cjs ADDED
@@ -0,0 +1,602 @@
1
+ "use client";
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/index.ts
22
+ var src_exports = {};
23
+ __export(src_exports, {
24
+ Button: () => Button,
25
+ ConfirmDialog: () => ConfirmDialog,
26
+ EmptyState: () => EmptyState,
27
+ FONT_CSS_VARIABLES: () => FONT_CSS_VARIABLES,
28
+ Modal: () => Modal,
29
+ PageHeader: () => PageHeader,
30
+ Sidebar: () => Sidebar,
31
+ SidebarNavItem: () => SidebarNavItem,
32
+ Spinner: () => Spinner,
33
+ TagChips: () => TagChips,
34
+ cn: () => cn,
35
+ tagTextColor: () => tagTextColor
36
+ });
37
+ module.exports = __toCommonJS(src_exports);
38
+
39
+ // src/components/Button.tsx
40
+ var import_react = require("react");
41
+
42
+ // src/lib/utils.ts
43
+ var import_clsx = require("clsx");
44
+ var import_tailwind_merge = require("tailwind-merge");
45
+ function cn(...inputs) {
46
+ return (0, import_tailwind_merge.twMerge)((0, import_clsx.clsx)(inputs));
47
+ }
48
+
49
+ // src/components/Button.tsx
50
+ var import_jsx_runtime = require("react/jsx-runtime");
51
+ 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]";
52
+ var VARIANT = {
53
+ primary: "bg-[#ff9900] text-[#0a0a0a] hover:bg-[#cc6a1a]",
54
+ secondary: "bg-[#1e1e1e] text-white hover:bg-[#252525]",
55
+ tertiary: "text-white/60 hover:bg-[#252525] hover:text-white",
56
+ danger: "bg-[#b91c1c] text-white hover:brightness-110"
57
+ };
58
+ var SIZE = {
59
+ sm: "px-3 py-1 text-xs",
60
+ md: "px-4 py-1.5 text-sm"
61
+ };
62
+ var Button = (0, import_react.forwardRef)(
63
+ function Button2({ variant = "secondary", size = "md", className, ...rest }, ref) {
64
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
65
+ "button",
66
+ {
67
+ ref,
68
+ ...rest,
69
+ className: cn(BASE, VARIANT[variant], SIZE[size], className)
70
+ }
71
+ );
72
+ }
73
+ );
74
+
75
+ // src/components/ConfirmDialog.tsx
76
+ var import_react3 = require("react");
77
+
78
+ // src/components/Modal.tsx
79
+ var import_react2 = require("react");
80
+ var import_lucide_react = require("lucide-react");
81
+ var import_jsx_runtime2 = require("react/jsx-runtime");
82
+ function Modal({
83
+ open,
84
+ onClose,
85
+ title,
86
+ showClose,
87
+ density = "form",
88
+ maxWidth = "max-w-md",
89
+ footer,
90
+ children
91
+ }) {
92
+ const titleIdRef = (0, import_react2.useRef)(
93
+ `hl-modal-${Math.random().toString(36).slice(2, 8)}`
94
+ );
95
+ (0, import_react2.useEffect)(() => {
96
+ if (!open) return;
97
+ const onKey = (e) => {
98
+ if (e.key === "Escape") onClose();
99
+ };
100
+ window.addEventListener("keydown", onKey);
101
+ return () => window.removeEventListener("keydown", onKey);
102
+ }, [open, onClose]);
103
+ if (!open) return null;
104
+ const showCloseBtn = showClose ?? Boolean(title);
105
+ const overlayClass = density === "media" ? "bg-black/90" : "bg-black/70";
106
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
107
+ "div",
108
+ {
109
+ className: cn(
110
+ "fixed inset-0 z-50 flex items-center justify-center p-4",
111
+ overlayClass
112
+ ),
113
+ onClick: onClose,
114
+ role: "presentation",
115
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
116
+ "div",
117
+ {
118
+ role: "dialog",
119
+ "aria-modal": "true",
120
+ "aria-labelledby": title ? titleIdRef.current : void 0,
121
+ className: cn(
122
+ "w-full rounded-[1rem] border-2 border-[#1e1e1e] bg-[#111111] p-5",
123
+ maxWidth
124
+ ),
125
+ onClick: (e) => e.stopPropagation(),
126
+ children: [
127
+ (title || showCloseBtn) && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { className: "mb-4 flex items-start justify-between gap-3", children: [
128
+ title && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
129
+ "h2",
130
+ {
131
+ id: titleIdRef.current,
132
+ className: "text-sm font-semibold uppercase tracking-widest text-white",
133
+ children: title
134
+ }
135
+ ),
136
+ showCloseBtn && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
137
+ "button",
138
+ {
139
+ type: "button",
140
+ onClick: onClose,
141
+ className: "ml-auto rounded-[0.25rem] p-1 text-white/40 transition hover:bg-[#252525] hover:text-white active:scale-90",
142
+ "aria-label": "Close",
143
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_lucide_react.X, { className: "h-4 w-4" })
144
+ }
145
+ )
146
+ ] }),
147
+ children,
148
+ footer && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className: "mt-5 flex flex-wrap items-center justify-end gap-2", children: footer })
149
+ ]
150
+ }
151
+ )
152
+ }
153
+ );
154
+ }
155
+
156
+ // src/components/ConfirmDialog.tsx
157
+ var import_jsx_runtime3 = require("react/jsx-runtime");
158
+ function ConfirmDialog({
159
+ open,
160
+ title,
161
+ message,
162
+ confirmLabel = "Confirm",
163
+ cancelLabel = "Cancel",
164
+ tone = "danger",
165
+ onConfirm,
166
+ onClose
167
+ }) {
168
+ (0, import_react3.useEffect)(() => {
169
+ if (!open) return;
170
+ const onKey = (e) => {
171
+ if (e.key === "Enter") onConfirm();
172
+ };
173
+ window.addEventListener("keydown", onKey);
174
+ return () => window.removeEventListener("keydown", onKey);
175
+ }, [open, onConfirm]);
176
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
177
+ Modal,
178
+ {
179
+ open,
180
+ onClose,
181
+ title,
182
+ footer: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_jsx_runtime3.Fragment, { children: [
183
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Button, { variant: "tertiary", size: "md", onClick: onClose, children: cancelLabel }),
184
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
185
+ Button,
186
+ {
187
+ variant: tone === "danger" ? "danger" : "primary",
188
+ size: "md",
189
+ onClick: onConfirm,
190
+ children: confirmLabel
191
+ }
192
+ )
193
+ ] }),
194
+ children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("p", { className: "whitespace-pre-line text-sm text-white/70", children: message })
195
+ }
196
+ );
197
+ }
198
+
199
+ // src/components/EmptyState.tsx
200
+ var import_jsx_runtime4 = require("react/jsx-runtime");
201
+ function EmptyState({
202
+ icon,
203
+ title,
204
+ message,
205
+ action,
206
+ variant = "panel",
207
+ className
208
+ }) {
209
+ if (variant === "inline") {
210
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: cn("px-3 py-2 text-xs text-white/40", className), children: message });
211
+ }
212
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
213
+ "div",
214
+ {
215
+ className: cn(
216
+ "flex flex-col items-center justify-center gap-3 rounded-[1rem] bg-[#2a2620] p-12 text-center",
217
+ className
218
+ ),
219
+ children: [
220
+ icon && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { className: "text-white/30", children: icon }),
221
+ title && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("h3", { className: "text-sm font-semibold text-white", children: title }),
222
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("p", { className: "text-sm text-white/60", children: message }),
223
+ action
224
+ ]
225
+ }
226
+ );
227
+ }
228
+
229
+ // src/components/PageHeader.tsx
230
+ var import_react4 = require("react");
231
+ var import_lucide_react2 = require("lucide-react");
232
+ var import_jsx_runtime5 = require("react/jsx-runtime");
233
+ function PageHeader({
234
+ title,
235
+ description,
236
+ back,
237
+ actions,
238
+ imageSrc,
239
+ imageAlt,
240
+ breadcrumb,
241
+ className
242
+ }) {
243
+ const [imageErrored, setImageErrored] = (0, import_react4.useState)(false);
244
+ (0, import_react4.useEffect)(() => {
245
+ setImageErrored(false);
246
+ }, [imageSrc]);
247
+ const hasImageSlot = !!imageSrc;
248
+ const imageRenders = hasImageSlot && !imageErrored;
249
+ const hasBreadcrumb = Array.isArray(breadcrumb) && breadcrumb.length > 0;
250
+ const textIsSrOnly = hasImageSlot;
251
+ const backInner = back ? /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(import_jsx_runtime5.Fragment, { children: [
252
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react2.ArrowLeft, { className: "h-3 w-3" }),
253
+ back.label
254
+ ] }) : null;
255
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
256
+ "header",
257
+ {
258
+ className: cn(
259
+ "flex flex-wrap items-end justify-between gap-3",
260
+ className
261
+ ),
262
+ children: [
263
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "flex min-w-0 items-end gap-4", children: [
264
+ hasImageSlot && (imageRenders ? (
265
+ // eslint-disable-next-line @next/next/no-img-element
266
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
267
+ "img",
268
+ {
269
+ src: imageSrc,
270
+ alt: imageAlt ?? title,
271
+ onError: () => setImageErrored(true),
272
+ className: "h-28 w-28 shrink-0 rounded-[1rem] object-contain md:h-32 md:w-32"
273
+ }
274
+ )
275
+ ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(PlaceholderHeader, { title, imageSrc })),
276
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { className: "min-w-0", children: [
277
+ back && (back.render ? back.render(
278
+ back.href,
279
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "inline-flex items-center gap-1 text-xs text-white/40 transition hover:text-white", children: backInner })
280
+ ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
281
+ "a",
282
+ {
283
+ href: back.href,
284
+ className: "inline-flex items-center gap-1 text-xs text-white/40 transition hover:text-white",
285
+ children: backInner
286
+ }
287
+ )),
288
+ hasBreadcrumb ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
289
+ Breadcrumb,
290
+ {
291
+ crumbs: breadcrumb,
292
+ srTitle: title,
293
+ className: back ? "mt-1" : ""
294
+ }
295
+ ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
296
+ "h1",
297
+ {
298
+ className: cn(
299
+ "text-xl font-semibold text-[#aa89b7]",
300
+ back && "mt-1",
301
+ textIsSrOnly && "sr-only"
302
+ ),
303
+ children: title
304
+ }
305
+ ),
306
+ description && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { className: "mt-1 text-xs text-white/40", children: description })
307
+ ] })
308
+ ] }),
309
+ actions && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { className: "flex items-center gap-2", children: actions })
310
+ ]
311
+ }
312
+ );
313
+ }
314
+ function PlaceholderHeader({
315
+ title,
316
+ imageSrc
317
+ }) {
318
+ const file = imageSrc.replace(/^\//, "");
319
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
320
+ "div",
321
+ {
322
+ title: `Drop ${file} in /public to brand this header`,
323
+ 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",
324
+ children: [
325
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(import_lucide_react2.ImageOff, { className: "h-6 w-6" }),
326
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { className: "px-2 text-center text-[10px] font-semibold uppercase tracking-widest", children: title })
327
+ ]
328
+ }
329
+ );
330
+ }
331
+ function Breadcrumb({
332
+ crumbs,
333
+ srTitle,
334
+ className
335
+ }) {
336
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
337
+ "nav",
338
+ {
339
+ "aria-label": "Breadcrumb",
340
+ className: cn(
341
+ "flex flex-wrap items-center gap-1.5 text-xl font-semibold",
342
+ className
343
+ ),
344
+ children: [
345
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("h1", { className: "sr-only", children: srTitle }),
346
+ crumbs.map((crumb, idx) => {
347
+ const isLast = idx === crumbs.length - 1;
348
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
349
+ "span",
350
+ {
351
+ className: "flex items-center gap-1.5",
352
+ children: [
353
+ idx > 0 && /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
354
+ import_lucide_react2.ChevronRight,
355
+ {
356
+ className: "h-4 w-4 shrink-0 text-white/30",
357
+ "aria-hidden": "true"
358
+ }
359
+ ),
360
+ crumb.onClick && !isLast ? /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
361
+ "button",
362
+ {
363
+ type: "button",
364
+ onClick: crumb.onClick,
365
+ className: "rounded px-1 text-white/45 transition hover:text-white",
366
+ children: crumb.label
367
+ }
368
+ ) : /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
369
+ "span",
370
+ {
371
+ className: cn(
372
+ isLast ? "text-white" : "text-white/45",
373
+ isLast && "px-1"
374
+ ),
375
+ "aria-current": isLast ? "page" : void 0,
376
+ children: crumb.label
377
+ }
378
+ )
379
+ ]
380
+ },
381
+ `${crumb.label}-${idx}`
382
+ );
383
+ })
384
+ ]
385
+ }
386
+ );
387
+ }
388
+
389
+ // src/components/Sidebar.tsx
390
+ var import_react5 = require("react");
391
+ var import_lucide_react3 = require("lucide-react");
392
+ var import_jsx_runtime6 = require("react/jsx-runtime");
393
+ function Sidebar({
394
+ logoSrc,
395
+ logoAlt,
396
+ widgets,
397
+ nav,
398
+ middle,
399
+ lower,
400
+ admin,
401
+ footer,
402
+ mobileOpen: controlledOpen,
403
+ onMobileOpenChange,
404
+ className,
405
+ showMobileTopBar = true
406
+ }) {
407
+ const [internalOpen, setInternalOpen] = (0, import_react5.useState)(false);
408
+ const isControlled = controlledOpen !== void 0;
409
+ const open = isControlled ? controlledOpen : internalOpen;
410
+ const setOpen = (next) => {
411
+ if (!isControlled) setInternalOpen(next);
412
+ onMobileOpenChange?.(next);
413
+ };
414
+ (0, import_react5.useEffect)(() => {
415
+ if (typeof document === "undefined") return;
416
+ if (!open) return;
417
+ const prev = document.body.style.overflow;
418
+ document.body.style.overflow = "hidden";
419
+ return () => {
420
+ document.body.style.overflow = prev;
421
+ };
422
+ }, [open]);
423
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
424
+ showMobileTopBar && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("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: [
425
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
426
+ "button",
427
+ {
428
+ type: "button",
429
+ onClick: () => setOpen(true),
430
+ className: "rounded-[0.25rem] p-1.5 text-white transition hover:bg-[#252525] active:scale-90",
431
+ "aria-label": "Open menu",
432
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.Menu, { className: "h-5 w-5" })
433
+ }
434
+ ),
435
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("img", { src: logoSrc, alt: "", width: 24, height: 24 }),
436
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("p", { className: "text-sm font-semibold text-white", children: logoAlt })
437
+ ] }),
438
+ open && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
439
+ "div",
440
+ {
441
+ className: "fixed inset-0 z-40 bg-black/70 md:hidden",
442
+ onClick: () => setOpen(false),
443
+ role: "presentation"
444
+ }
445
+ ),
446
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
447
+ "aside",
448
+ {
449
+ className: cn(
450
+ "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",
451
+ open ? "translate-x-0" : "-translate-x-full md:translate-x-0",
452
+ className
453
+ ),
454
+ children: [
455
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { className: "relative flex flex-col items-center border-b-2 border-[#1e1e1e] px-4 pt-6 pb-5", children: [
456
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
457
+ "img",
458
+ {
459
+ src: logoSrc,
460
+ alt: logoAlt,
461
+ className: "h-36 w-36 rounded-[1rem] object-contain"
462
+ }
463
+ ),
464
+ showMobileTopBar && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
465
+ "button",
466
+ {
467
+ type: "button",
468
+ onClick: () => setOpen(false),
469
+ className: "absolute right-3 top-3 rounded-[0.25rem] p-1 text-white/40 transition hover:bg-[#252525] hover:text-white md:hidden",
470
+ "aria-label": "Close menu",
471
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(import_lucide_react3.X, { className: "h-4 w-4" })
472
+ }
473
+ )
474
+ ] }),
475
+ widgets && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-3 mt-3", children: widgets }),
476
+ nav && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("nav", { className: "mx-3 mt-3 space-y-0.5 rounded-[0.75rem] bg-[#1e1e1e] p-2", children: nav }),
477
+ middle && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "mx-3 mt-3 flex-1 overflow-y-auto rounded-[0.75rem] bg-[#1e1e1e] p-2", children: middle }),
478
+ lower && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("nav", { className: "mx-3 mt-3 space-y-0.5 rounded-[0.75rem] bg-[#1e1e1e] p-2", children: lower }),
479
+ !middle && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "flex-1" }),
480
+ admin && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { children: admin }),
481
+ footer && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { className: "border-t-2 border-[#1e1e1e] px-4 py-3 text-center text-xs text-white/20", children: footer })
482
+ ]
483
+ }
484
+ )
485
+ ] });
486
+ }
487
+
488
+ // src/components/SidebarNavItem.tsx
489
+ var import_jsx_runtime7 = require("react/jsx-runtime");
490
+ var ITEM_CLASS_BASE = "flex items-center gap-2 rounded-[0.5rem] px-3 py-2.5 text-base font-medium transition-all";
491
+ var itemClass = (active) => cn(
492
+ ITEM_CLASS_BASE,
493
+ active ? "bg-[#ff9900] text-white" : "text-white/45 hover:bg-white/5 hover:text-white"
494
+ );
495
+ function SidebarNavItem({
496
+ label,
497
+ icon,
498
+ active = false,
499
+ onClick,
500
+ href,
501
+ render
502
+ }) {
503
+ const className = itemClass(active);
504
+ const inner = /* @__PURE__ */ (0, import_jsx_runtime7.jsxs)(import_jsx_runtime7.Fragment, { children: [
505
+ icon,
506
+ /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("span", { children: label })
507
+ ] });
508
+ if (href && render) return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(import_jsx_runtime7.Fragment, { children: render({ href, className }, inner) });
509
+ if (href) {
510
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)("a", { href, className, children: inner });
511
+ }
512
+ return /* @__PURE__ */ (0, import_jsx_runtime7.jsx)(
513
+ "button",
514
+ {
515
+ type: "button",
516
+ onClick,
517
+ "aria-pressed": active,
518
+ className: `${className} w-full`,
519
+ children: inner
520
+ }
521
+ );
522
+ }
523
+
524
+ // src/components/Spinner.tsx
525
+ var import_lucide_react4 = require("lucide-react");
526
+ var import_jsx_runtime8 = require("react/jsx-runtime");
527
+ function Spinner({ label = "Loading\u2026", className }) {
528
+ return /* @__PURE__ */ (0, import_jsx_runtime8.jsxs)(
529
+ "p",
530
+ {
531
+ className: cn(
532
+ "flex items-center gap-2 px-3 py-2 text-xs text-white/40",
533
+ className
534
+ ),
535
+ children: [
536
+ /* @__PURE__ */ (0, import_jsx_runtime8.jsx)(import_lucide_react4.Loader2, { className: "h-3 w-3 animate-spin" }),
537
+ label
538
+ ]
539
+ }
540
+ );
541
+ }
542
+
543
+ // src/lib/colors.ts
544
+ function tagTextColor(hex) {
545
+ const normalized = hex.trim().replace(/^#/, "");
546
+ if (normalized.length !== 3 && normalized.length !== 6) return "#ffffff";
547
+ const expand = normalized.length === 3 ? normalized.split("").map((c) => c + c).join("") : normalized;
548
+ const r = parseInt(expand.slice(0, 2), 16);
549
+ const g = parseInt(expand.slice(2, 4), 16);
550
+ const b = parseInt(expand.slice(4, 6), 16);
551
+ if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return "#ffffff";
552
+ const luma = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
553
+ return luma > 0.65 ? "#000000" : "#ffffff";
554
+ }
555
+
556
+ // src/components/TagChips.tsx
557
+ var import_jsx_runtime9 = require("react/jsx-runtime");
558
+ function TagChips({ tags, size = "xs", onClick }) {
559
+ if (tags.length === 0) return null;
560
+ const sizeClasses = size === "xs" ? "px-1.5 py-0.5 text-[10px]" : "px-2 py-0.5 text-xs";
561
+ return /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("div", { className: "flex flex-wrap gap-1", children: tags.map((t) => {
562
+ const style = {
563
+ backgroundColor: t.color,
564
+ color: tagTextColor(t.color)
565
+ };
566
+ const className = `inline-block rounded-[0.25rem] font-semibold ${sizeClasses}`;
567
+ return onClick ? /* @__PURE__ */ (0, import_jsx_runtime9.jsx)(
568
+ "button",
569
+ {
570
+ type: "button",
571
+ onClick: () => onClick(t),
572
+ className: `${className} transition active:scale-95`,
573
+ style,
574
+ children: t.name
575
+ },
576
+ t.id
577
+ ) : /* @__PURE__ */ (0, import_jsx_runtime9.jsx)("span", { className, style, children: t.name }, t.id);
578
+ }) });
579
+ }
580
+
581
+ // src/lib/fonts.ts
582
+ var FONT_CSS_VARIABLES = {
583
+ sans: "--font-geist-sans",
584
+ mono: "--font-geist-mono",
585
+ display: "--font-orbitron"
586
+ };
587
+ // Annotate the CommonJS export names for ESM import in node:
588
+ 0 && (module.exports = {
589
+ Button,
590
+ ConfirmDialog,
591
+ EmptyState,
592
+ FONT_CSS_VARIABLES,
593
+ Modal,
594
+ PageHeader,
595
+ Sidebar,
596
+ SidebarNavItem,
597
+ Spinner,
598
+ TagChips,
599
+ cn,
600
+ tagTextColor
601
+ });
602
+ //# sourceMappingURL=index.cjs.map