@brightweblabs/ui 0.4.0 → 1.0.1
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/package.json +43 -7
- package/src/components/action.tsx +12 -0
- package/src/components/activity-message.tsx +1 -1
- package/src/components/alert-dialog.tsx +4 -4
- package/src/components/avatar.tsx +80 -0
- package/src/components/badge.tsx +20 -30
- package/src/components/button-variants.ts +47 -0
- package/src/components/button.tsx +102 -53
- package/src/components/calendar.tsx +5 -4
- package/src/components/chart.tsx +4 -4
- package/src/components/dropdown-menu.tsx +5 -5
- package/src/components/empty-state.tsx +26 -0
- package/src/components/field.tsx +2 -2
- package/src/components/initials-avatar.tsx +30 -0
- package/src/components/input.tsx +1 -1
- package/src/components/kpi-breakdown-bar.tsx +71 -0
- package/src/components/label.tsx +1 -1
- package/src/components/pagination.tsx +2 -3
- package/src/components/password-input.tsx +10 -43
- package/src/components/password-strength.tsx +9 -10
- package/src/components/phone-input.tsx +114 -0
- package/src/components/popover.tsx +1 -1
- package/src/components/role-badge.tsx +34 -0
- package/src/components/search-field.tsx +63 -0
- package/src/components/section-heading.tsx +28 -0
- package/src/components/sheet.tsx +34 -5
- package/src/components/skeleton-table.tsx +27 -0
- package/src/components/skeleton.tsx +45 -0
- package/src/components/sonner.tsx +2 -1
- package/src/components/stat-tile.tsx +30 -0
- package/src/components/status-pill.tsx +37 -0
- package/src/components/surface-card.tsx +11 -0
- package/src/components/table-pagination.tsx +107 -0
- package/src/components/table.tsx +7 -4
- package/src/components/theme-provider.tsx +130 -0
- package/src/components/tooltip.tsx +2 -2
- package/src/index.ts +22 -1
- package/src/lib/patterns.ts +52 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CSSProperties } from "react";
|
|
2
|
+
|
|
3
|
+
export function Skeleton({ className = "", rounded = "var(--radius)", style }: {
|
|
4
|
+
className?: string;
|
|
5
|
+
rounded?: string;
|
|
6
|
+
style?: CSSProperties;
|
|
7
|
+
}) {
|
|
8
|
+
return <span aria-hidden className={`skeleton-ghost block ${className}`} style={{ borderRadius: rounded, ...style }} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function SkeletonLine({ w = "100%", className = "" }: { w?: string; className?: string }) {
|
|
12
|
+
return <Skeleton rounded="999px" className={`h-[0.6rem] ${className}`} style={{ width: w }} />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function SkeletonText({ lines = 3, className = "" }: { lines?: number; className?: string }) {
|
|
16
|
+
const widths = ["100%", "92%", "84%", "96%", "70%"];
|
|
17
|
+
return (
|
|
18
|
+
<div className={`flex flex-col gap-2 ${className}`}>
|
|
19
|
+
{Array.from({ length: lines }, (_, index) => (
|
|
20
|
+
<SkeletonLine key={index} w={index === lines - 1 ? "55%" : widths[index % widths.length]} />
|
|
21
|
+
))}
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function SkeletonCard({ lines = 3, meta = true, className = "", bodyClassName = "" }: {
|
|
27
|
+
lines?: number;
|
|
28
|
+
meta?: boolean;
|
|
29
|
+
className?: string;
|
|
30
|
+
bodyClassName?: string;
|
|
31
|
+
}) {
|
|
32
|
+
return (
|
|
33
|
+
<div className={`flex flex-col gap-4 rounded-[var(--radius-card)] border border-border bg-card p-5 shadow-[0_1px_2px_var(--hairline)] ${className}`}>
|
|
34
|
+
<div className="flex flex-col gap-2">
|
|
35
|
+
<SkeletonLine w="45%" className="h-[0.7rem]" />
|
|
36
|
+
{meta ? <SkeletonLine w="28%" /> : null}
|
|
37
|
+
</div>
|
|
38
|
+
<SkeletonText lines={lines} className={bodyClassName} />
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function SkeletonCircle({ size = "2.25rem", className = "" }: { size?: string; className?: string }) {
|
|
44
|
+
return <Skeleton rounded="50%" className={`shrink-0 ${className}`} style={{ width: size, height: size }} />;
|
|
45
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { CircleCheckIcon, InfoIcon, Loader2Icon, OctagonXIcon, TriangleAlertIcon } from "lucide-react";
|
|
4
|
-
import { useTheme } from "next-themes";
|
|
5
4
|
import { Toaster as Sonner, type ToasterProps } from "sonner";
|
|
6
5
|
|
|
6
|
+
import { useTheme } from "./theme-provider";
|
|
7
|
+
|
|
7
8
|
const Toaster = ({ ...props }: ToasterProps) => {
|
|
8
9
|
const { theme = "system" } = useTheme();
|
|
9
10
|
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { HTMLAttributes, ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
export type StatTileProps = HTMLAttributes<HTMLDivElement> & {
|
|
6
|
+
label: ReactNode;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function StatTile({ label, children, className, ...props }: StatTileProps) {
|
|
10
|
+
return (
|
|
11
|
+
<div className={cn("stat-cell", className)} {...props}>
|
|
12
|
+
<p className="text-ui-label">{label}</p>
|
|
13
|
+
<div className="mt-3">{children}</div>
|
|
14
|
+
</div>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type StatValueProps = HTMLAttributes<HTMLParagraphElement> & {
|
|
19
|
+
size?: "normal" | "large" | "display";
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const statValueSizeClasses: Record<NonNullable<StatValueProps["size"]>, string> = {
|
|
23
|
+
normal: "text-ui-metric",
|
|
24
|
+
large: "text-ui-metric-xl",
|
|
25
|
+
display: "text-ui-metric-display",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export function StatValue({ size = "normal", className, ...props }: StatValueProps) {
|
|
29
|
+
return <p className={cn(statValueSizeClasses[size], className)} {...props} />;
|
|
30
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
2
|
+
import { tintPill } from "@brightweblabs/theme/tint";
|
|
3
|
+
import type { ComponentProps, CSSProperties, ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../lib/utils";
|
|
6
|
+
|
|
7
|
+
export type StatusPillSize = "small" | "normal";
|
|
8
|
+
|
|
9
|
+
export const STATUS_PILL_BASE_CLASS =
|
|
10
|
+
"inline-flex shrink-0 items-center justify-center gap-1.5 whitespace-nowrap rounded-full border font-semibold leading-none";
|
|
11
|
+
|
|
12
|
+
export const STATUS_PILL_SIZE_CLASSES: Record<StatusPillSize, string> = {
|
|
13
|
+
small: "h-5 px-2 text-ui-micro",
|
|
14
|
+
normal: "h-7 px-3 text-ui-meta",
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type StatusPillProps = Omit<ComponentProps<"span">, "children"> & {
|
|
18
|
+
children: ReactNode;
|
|
19
|
+
token: string;
|
|
20
|
+
size?: StatusPillSize;
|
|
21
|
+
asChild?: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function StatusPill({ children, token, size = "small", asChild = false, className, style, ...props }: StatusPillProps) {
|
|
25
|
+
const Comp = asChild ? Slot : "span";
|
|
26
|
+
const tint = tintPill(token);
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<Comp
|
|
30
|
+
className={cn(STATUS_PILL_BASE_CLASS, STATUS_PILL_SIZE_CLASSES[size], tint.className, className)}
|
|
31
|
+
style={{ ...tint.style, ...style } as CSSProperties}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
{children}
|
|
35
|
+
</Comp>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { HTMLAttributes } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
export type SurfaceCardProps = HTMLAttributes<HTMLElement> & {
|
|
6
|
+
isLight?: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export function SurfaceCard({ isLight = false, className, ...props }: SurfaceCardProps) {
|
|
10
|
+
return <article className={cn("surface-card", isLight && "is-light", className)} {...props} />;
|
|
11
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ChevronLeft, ChevronRight } from "lucide-react";
|
|
4
|
+
|
|
5
|
+
import { getPaginationWindow } from "../lib/patterns";
|
|
6
|
+
import { cn } from "../lib/utils";
|
|
7
|
+
import { Pagination, PaginationContent, PaginationEllipsis, PaginationItem, PaginationLink } from "./pagination";
|
|
8
|
+
|
|
9
|
+
export type TablePaginationProps = {
|
|
10
|
+
page: number;
|
|
11
|
+
totalPages: number;
|
|
12
|
+
onPageChange: (page: number) => void;
|
|
13
|
+
summary?: string;
|
|
14
|
+
className?: string;
|
|
15
|
+
previousLabel?: string;
|
|
16
|
+
nextLabel?: string;
|
|
17
|
+
pageLabel?: (page: number, totalPages: number) => string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function TablePagination({
|
|
21
|
+
page,
|
|
22
|
+
totalPages,
|
|
23
|
+
onPageChange,
|
|
24
|
+
summary,
|
|
25
|
+
className,
|
|
26
|
+
previousLabel = "Go to the previous page",
|
|
27
|
+
nextLabel = "Go to the next page",
|
|
28
|
+
pageLabel = (currentPage, pageCount) => `Page ${currentPage} of ${pageCount}`,
|
|
29
|
+
}: TablePaginationProps) {
|
|
30
|
+
const safeTotalPages = Math.max(1, Math.trunc(totalPages));
|
|
31
|
+
const safePage = Math.min(Math.max(Math.trunc(page), 1), safeTotalPages);
|
|
32
|
+
const items = getPaginationWindow(safePage, safeTotalPages);
|
|
33
|
+
|
|
34
|
+
const changePage = (nextPage: number) => {
|
|
35
|
+
const clampedPage = Math.min(Math.max(nextPage, 1), safeTotalPages);
|
|
36
|
+
if (clampedPage !== safePage) onPageChange(clampedPage);
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className={cn("flex min-w-0 flex-col gap-2 border-t border-hairline px-4 py-2 md:flex-row md:items-center md:justify-between", className)}>
|
|
41
|
+
{summary ? (
|
|
42
|
+
<p className="min-w-0 truncate text-ui-meta">
|
|
43
|
+
<span className="font-semibold text-foreground">{pageLabel(safePage, safeTotalPages)}</span>
|
|
44
|
+
<span className="px-1.5 text-border">·</span>
|
|
45
|
+
{summary}
|
|
46
|
+
</p>
|
|
47
|
+
) : null}
|
|
48
|
+
<Pagination className="mx-0 w-auto min-w-0 shrink-0 justify-start md:ml-auto md:justify-end">
|
|
49
|
+
<PaginationContent className="gap-1.5">
|
|
50
|
+
<PaginationItem>
|
|
51
|
+
<PaginationLink
|
|
52
|
+
size="sm"
|
|
53
|
+
href="#"
|
|
54
|
+
aria-label={previousLabel}
|
|
55
|
+
aria-disabled={safePage === 1}
|
|
56
|
+
onClick={(event) => {
|
|
57
|
+
event.preventDefault();
|
|
58
|
+
changePage(safePage - 1);
|
|
59
|
+
}}
|
|
60
|
+
className={cn("size-8 rounded-full border border-transparent text-muted-foreground hover:border-hairline-strong hover:bg-surface-hover hover:text-foreground", safePage === 1 && "pointer-events-none opacity-45")}
|
|
61
|
+
>
|
|
62
|
+
<ChevronLeft className="size-4" />
|
|
63
|
+
</PaginationLink>
|
|
64
|
+
</PaginationItem>
|
|
65
|
+
{items.map((item) => typeof item === "number" ? (
|
|
66
|
+
<PaginationItem key={item}>
|
|
67
|
+
<PaginationLink
|
|
68
|
+
size="sm"
|
|
69
|
+
href="#"
|
|
70
|
+
isActive={item === safePage}
|
|
71
|
+
onClick={(event) => {
|
|
72
|
+
event.preventDefault();
|
|
73
|
+
changePage(item);
|
|
74
|
+
}}
|
|
75
|
+
className={cn(
|
|
76
|
+
"size-8 rounded-full text-ui-label normal-case tracking-normal",
|
|
77
|
+
item === safePage ? "border-hairline-strong bg-elevate-2 text-foreground" : "text-muted-foreground hover:bg-surface-hover hover:text-foreground",
|
|
78
|
+
)}
|
|
79
|
+
>
|
|
80
|
+
{item}
|
|
81
|
+
</PaginationLink>
|
|
82
|
+
</PaginationItem>
|
|
83
|
+
) : (
|
|
84
|
+
<PaginationItem key={item}>
|
|
85
|
+
<PaginationEllipsis />
|
|
86
|
+
</PaginationItem>
|
|
87
|
+
))}
|
|
88
|
+
<PaginationItem>
|
|
89
|
+
<PaginationLink
|
|
90
|
+
size="sm"
|
|
91
|
+
href="#"
|
|
92
|
+
aria-label={nextLabel}
|
|
93
|
+
aria-disabled={safePage === safeTotalPages}
|
|
94
|
+
onClick={(event) => {
|
|
95
|
+
event.preventDefault();
|
|
96
|
+
changePage(safePage + 1);
|
|
97
|
+
}}
|
|
98
|
+
className={cn("size-8 rounded-full border border-transparent text-muted-foreground hover:border-hairline-strong hover:bg-surface-hover hover:text-foreground", safePage === safeTotalPages && "pointer-events-none opacity-45")}
|
|
99
|
+
>
|
|
100
|
+
<ChevronRight className="size-4" />
|
|
101
|
+
</PaginationLink>
|
|
102
|
+
</PaginationItem>
|
|
103
|
+
</PaginationContent>
|
|
104
|
+
</Pagination>
|
|
105
|
+
</div>
|
|
106
|
+
);
|
|
107
|
+
}
|
package/src/components/table.tsx
CHANGED
|
@@ -2,9 +2,12 @@ import * as React from "react";
|
|
|
2
2
|
|
|
3
3
|
import { cn } from "../lib/utils";
|
|
4
4
|
|
|
5
|
-
const Table = React.forwardRef<
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const Table = React.forwardRef<
|
|
6
|
+
HTMLTableElement,
|
|
7
|
+
React.HTMLAttributes<HTMLTableElement> & { containerClassName?: string }
|
|
8
|
+
>(
|
|
9
|
+
({ className, containerClassName, ...props }, ref) => (
|
|
10
|
+
<div className={cn("w-full flex-1 overflow-auto", containerClassName)}>
|
|
8
11
|
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
|
|
9
12
|
</div>
|
|
10
13
|
),
|
|
@@ -25,7 +28,7 @@ TableBody.displayName = "TableBody";
|
|
|
25
28
|
|
|
26
29
|
const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
|
|
27
30
|
({ className, ...props }, ref) => (
|
|
28
|
-
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-
|
|
31
|
+
<tfoot ref={ref} className={cn("border-t bg-muted/50 font-semibold [&>tr]:last:border-b-0", className)} {...props} />
|
|
29
32
|
),
|
|
30
33
|
);
|
|
31
34
|
TableFooter.displayName = "TableFooter";
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useState } from "react";
|
|
4
|
+
import type { ReactNode } from "react";
|
|
5
|
+
|
|
6
|
+
type Theme = "light" | "dark" | "system";
|
|
7
|
+
|
|
8
|
+
type ThemeProviderProps = {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
defaultTheme?: Theme;
|
|
11
|
+
enableSystem?: boolean;
|
|
12
|
+
disableTransitionOnChange?: boolean;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type ThemeContextValue = {
|
|
16
|
+
theme: Theme;
|
|
17
|
+
setTheme: (theme: string) => void;
|
|
18
|
+
resolvedTheme: "light" | "dark";
|
|
19
|
+
systemTheme: "light" | "dark";
|
|
20
|
+
themes: Theme[];
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const STORAGE_KEY = "theme";
|
|
24
|
+
const THEME_VALUES: Theme[] = ["light", "dark", "system"];
|
|
25
|
+
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
|
26
|
+
|
|
27
|
+
function getSystemTheme(): "light" | "dark" {
|
|
28
|
+
if (typeof window === "undefined") return "light";
|
|
29
|
+
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function readStoredTheme(defaultTheme: Theme): Theme {
|
|
33
|
+
if (typeof window === "undefined") return defaultTheme;
|
|
34
|
+
try {
|
|
35
|
+
const stored = window.localStorage.getItem(STORAGE_KEY);
|
|
36
|
+
return stored === "light" || stored === "dark" || stored === "system" ? stored : defaultTheme;
|
|
37
|
+
} catch {
|
|
38
|
+
return defaultTheme;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function applyResolvedTheme(resolvedTheme: "light" | "dark", disableTransitionOnChange: boolean) {
|
|
43
|
+
const root = document.documentElement;
|
|
44
|
+
let cleanup: (() => void) | null = null;
|
|
45
|
+
|
|
46
|
+
if (disableTransitionOnChange) {
|
|
47
|
+
const style = document.createElement("style");
|
|
48
|
+
style.appendChild(document.createTextNode("*,*::before,*::after{transition:none!important;animation-duration:0s!important}"));
|
|
49
|
+
document.head.appendChild(style);
|
|
50
|
+
cleanup = () => {
|
|
51
|
+
window.getComputedStyle(document.body);
|
|
52
|
+
window.setTimeout(() => style.remove(), 1);
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
root.classList.remove("light", "dark");
|
|
57
|
+
root.classList.add(resolvedTheme);
|
|
58
|
+
root.dataset.theme = resolvedTheme;
|
|
59
|
+
root.style.colorScheme = resolvedTheme;
|
|
60
|
+
cleanup?.();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function ThemeProvider({
|
|
64
|
+
children,
|
|
65
|
+
defaultTheme = "light",
|
|
66
|
+
enableSystem = true,
|
|
67
|
+
disableTransitionOnChange = false,
|
|
68
|
+
}: ThemeProviderProps) {
|
|
69
|
+
const [theme, setThemeState] = useState<Theme>(defaultTheme);
|
|
70
|
+
const [systemTheme, setSystemTheme] = useState<"light" | "dark">("light");
|
|
71
|
+
const resolvedTheme = theme === "system" && enableSystem ? systemTheme : theme === "dark" ? "dark" : "light";
|
|
72
|
+
|
|
73
|
+
useLayoutEffect(() => {
|
|
74
|
+
applyResolvedTheme(resolvedTheme, disableTransitionOnChange);
|
|
75
|
+
}, [disableTransitionOnChange, resolvedTheme]);
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
setThemeState(readStoredTheme(defaultTheme));
|
|
79
|
+
setSystemTheme(getSystemTheme());
|
|
80
|
+
}, [defaultTheme]);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (!enableSystem) return;
|
|
84
|
+
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
|
85
|
+
const handleChange = () => setSystemTheme(media.matches ? "dark" : "light");
|
|
86
|
+
handleChange();
|
|
87
|
+
media.addEventListener("change", handleChange);
|
|
88
|
+
return () => media.removeEventListener("change", handleChange);
|
|
89
|
+
}, [enableSystem]);
|
|
90
|
+
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
const handleStorage = (event: StorageEvent) => {
|
|
93
|
+
if (event.key === STORAGE_KEY) setThemeState(readStoredTheme(defaultTheme));
|
|
94
|
+
};
|
|
95
|
+
window.addEventListener("storage", handleStorage);
|
|
96
|
+
return () => window.removeEventListener("storage", handleStorage);
|
|
97
|
+
}, [defaultTheme]);
|
|
98
|
+
|
|
99
|
+
const setTheme = useCallback((nextTheme: string) => {
|
|
100
|
+
const normalizedTheme: Theme = nextTheme === "dark" || nextTheme === "light" || nextTheme === "system" ? nextTheme : defaultTheme;
|
|
101
|
+
setThemeState(normalizedTheme);
|
|
102
|
+
try {
|
|
103
|
+
window.localStorage.setItem(STORAGE_KEY, normalizedTheme);
|
|
104
|
+
} catch {
|
|
105
|
+
// In-memory theme state remains usable when storage is unavailable.
|
|
106
|
+
}
|
|
107
|
+
}, [defaultTheme]);
|
|
108
|
+
|
|
109
|
+
const value = useMemo<ThemeContextValue>(() => ({
|
|
110
|
+
theme,
|
|
111
|
+
setTheme,
|
|
112
|
+
resolvedTheme,
|
|
113
|
+
systemTheme,
|
|
114
|
+
themes: enableSystem ? THEME_VALUES : ["light", "dark"],
|
|
115
|
+
}), [enableSystem, resolvedTheme, setTheme, systemTheme, theme]);
|
|
116
|
+
|
|
117
|
+
return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function useTheme(): ThemeContextValue {
|
|
121
|
+
return useContext(ThemeContext) ?? {
|
|
122
|
+
theme: "light",
|
|
123
|
+
setTheme: () => undefined,
|
|
124
|
+
resolvedTheme: "light",
|
|
125
|
+
systemTheme: "light",
|
|
126
|
+
themes: THEME_VALUES,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type { Theme, ThemeContextValue, ThemeProviderProps };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
|
-
import
|
|
4
|
+
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
|
|
5
5
|
|
|
6
6
|
import { cn } from "../lib/utils"
|
|
7
7
|
|
|
@@ -41,7 +41,7 @@ function TooltipContent({
|
|
|
41
41
|
data-slot="tooltip-content"
|
|
42
42
|
sideOffset={sideOffset}
|
|
43
43
|
className={cn(
|
|
44
|
-
"z-[1300] overflow-hidden rounded-
|
|
44
|
+
"z-[1300] overflow-hidden rounded-[var(--radius-card)] border border-[color:var(--hairline-strong)] bg-[color:color-mix(in_srgb,var(--popover)_95%,transparent)] px-2.5 py-1.5 text-ui-meta !font-semibold text-popover-foreground shadow-[0_14px_34px_var(--elevate-3)] backdrop-blur-xl data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0",
|
|
45
45
|
className
|
|
46
46
|
)}
|
|
47
47
|
{...props}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { Button, buttonVariants } from "./components/button";
|
|
2
|
-
export {
|
|
2
|
+
export type { ButtonProps, ButtonVariantProps } from "./components/button";
|
|
3
|
+
export { Badge, badgeVariants } from "./components/badge";
|
|
4
|
+
export * from "./components/avatar";
|
|
3
5
|
export * from "./components/dropdown-menu";
|
|
4
6
|
export {
|
|
5
7
|
Field,
|
|
@@ -13,7 +15,14 @@ export {
|
|
|
13
15
|
} from "./components/field";
|
|
14
16
|
export { Input } from "./components/input";
|
|
15
17
|
export { PasswordInput } from "./components/password-input";
|
|
18
|
+
export type { PasswordInputProps } from "./components/password-input";
|
|
16
19
|
export { PasswordStrength } from "./components/password-strength";
|
|
20
|
+
export { PhoneInput } from "./components/phone-input";
|
|
21
|
+
export type { PhoneInputProps } from "./components/phone-input";
|
|
22
|
+
export { SearchField } from "./components/search-field";
|
|
23
|
+
export type { SearchFieldProps } from "./components/search-field";
|
|
24
|
+
export * from "./components/skeleton";
|
|
25
|
+
export { TableRowsSkeleton } from "./components/skeleton-table";
|
|
17
26
|
export { Separator } from "./components/separator";
|
|
18
27
|
export * from "./components/alert-dialog";
|
|
19
28
|
export * from "./components/card";
|
|
@@ -26,8 +35,20 @@ export * from "./components/popover";
|
|
|
26
35
|
export * from "./components/sheet";
|
|
27
36
|
export * from "./components/table";
|
|
28
37
|
export { Toaster } from "./components/sonner";
|
|
38
|
+
export { ThemeProvider, useTheme } from "./components/theme-provider";
|
|
39
|
+
export type { Theme, ThemeContextValue, ThemeProviderProps } from "./components/theme-provider";
|
|
29
40
|
export { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./components/tooltip";
|
|
30
41
|
export { ActivityMessage } from "./components/activity-message";
|
|
42
|
+
export * from "./components/action";
|
|
43
|
+
export * from "./components/empty-state";
|
|
44
|
+
export * from "./components/initials-avatar";
|
|
45
|
+
export * from "./components/kpi-breakdown-bar";
|
|
46
|
+
export * from "./components/role-badge";
|
|
47
|
+
export * from "./components/section-heading";
|
|
48
|
+
export * from "./components/stat-tile";
|
|
49
|
+
export * from "./components/status-pill";
|
|
50
|
+
export * from "./components/surface-card";
|
|
51
|
+
export * from "./components/table-pagination";
|
|
31
52
|
export {
|
|
32
53
|
formatActivityValue,
|
|
33
54
|
toActivityChanges,
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export type PaginationWindowItem = number | "start-ellipsis" | "end-ellipsis";
|
|
2
|
+
|
|
3
|
+
export function getPaginationWindow(page: number, totalPages: number): PaginationWindowItem[] {
|
|
4
|
+
const safeTotal = Math.max(1, Math.trunc(totalPages));
|
|
5
|
+
const safePage = Math.min(Math.max(Math.trunc(page), 1), safeTotal);
|
|
6
|
+
|
|
7
|
+
if (safeTotal <= 5) {
|
|
8
|
+
return Array.from({ length: safeTotal }, (_, index) => index + 1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const pages = new Set([1, safeTotal, safePage - 1, safePage, safePage + 1]);
|
|
12
|
+
const visible = Array.from(pages).filter((value) => value >= 1 && value <= safeTotal).sort((a, b) => a - b);
|
|
13
|
+
const result: PaginationWindowItem[] = [];
|
|
14
|
+
|
|
15
|
+
visible.forEach((value, index) => {
|
|
16
|
+
const previous = visible[index - 1];
|
|
17
|
+
if (previous && value - previous > 1) {
|
|
18
|
+
result.push(previous === 1 ? "start-ellipsis" : "end-ellipsis");
|
|
19
|
+
}
|
|
20
|
+
result.push(value);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
return result;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getInitials(label?: string | null, fallback?: string | null): string {
|
|
27
|
+
const source = (label || fallback || "?").trim();
|
|
28
|
+
if (!source) return "?";
|
|
29
|
+
|
|
30
|
+
return source
|
|
31
|
+
.split(/\s+/)
|
|
32
|
+
.map((token) => token.charAt(0))
|
|
33
|
+
.filter(Boolean)
|
|
34
|
+
.slice(0, 2)
|
|
35
|
+
.join("")
|
|
36
|
+
.toUpperCase();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getRoleLabel(role: string): string {
|
|
40
|
+
return role
|
|
41
|
+
.trim()
|
|
42
|
+
.replace(/[_-]+/g, " ")
|
|
43
|
+
.replace(/\b\w/g, (character) => character.toUpperCase());
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveRoleToken(
|
|
47
|
+
role: string,
|
|
48
|
+
tokenMap: Readonly<Record<string, string>>,
|
|
49
|
+
fallbackToken = "--semantic-neutral",
|
|
50
|
+
): string {
|
|
51
|
+
return tokenMap[role] ?? fallbackToken;
|
|
52
|
+
}
|