@cntyclub/ui-react 0.13.0 → 0.15.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.d.ts +69 -2
- package/dist/index.js +327 -170
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ui/avatar-group.tsx +63 -10
- package/src/components/ui/chat-divider.tsx +67 -0
- package/src/components/ui/context-meter.tsx +134 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cntyclub/ui-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "React component library for the Country Club UI Kit — Base UI primitives styled with the Country Club design system (Tailwind CSS v4)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -5,6 +5,12 @@ import type * as React from "react";
|
|
|
5
5
|
|
|
6
6
|
import { cn } from "../../lib/utils/css";
|
|
7
7
|
import { Avatar, avatarVariants } from "./avatar";
|
|
8
|
+
import {
|
|
9
|
+
Tooltip,
|
|
10
|
+
TooltipPopup,
|
|
11
|
+
TooltipProvider,
|
|
12
|
+
TooltipTrigger,
|
|
13
|
+
} from "./tooltip";
|
|
8
14
|
|
|
9
15
|
type AvatarSize = NonNullable<VariantProps<typeof avatarVariants>["size"]>;
|
|
10
16
|
|
|
@@ -29,6 +35,11 @@ interface AvatarGroupItem {
|
|
|
29
35
|
/** Initials shown when there is no image. */
|
|
30
36
|
initials?: React.ReactNode;
|
|
31
37
|
alt?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Label revealed on hover when `showNameOnHover` is set (e.g. the person's
|
|
40
|
+
* name). Falls back to `alt` when omitted.
|
|
41
|
+
*/
|
|
42
|
+
name?: React.ReactNode;
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
interface AvatarGroupProps
|
|
@@ -39,11 +50,19 @@ interface AvatarGroupProps
|
|
|
39
50
|
/** Cap the visible avatars; the rest collapse into a "+N" chip. */
|
|
40
51
|
max?: number;
|
|
41
52
|
size?: AvatarSize;
|
|
53
|
+
/**
|
|
54
|
+
* Reveal each avatar's `name` (or `alt`) in a tooltip above it on hover /
|
|
55
|
+
* focus. Only applies to the data-driven `items` path.
|
|
56
|
+
*/
|
|
57
|
+
showNameOnHover?: boolean;
|
|
42
58
|
}
|
|
43
59
|
|
|
44
60
|
/**
|
|
45
61
|
* A row of overlapping avatars with an overflow "+N" chip. Pass `items` for the
|
|
46
62
|
* data-driven path, or compose `<Avatar>` children directly for full control.
|
|
63
|
+
*
|
|
64
|
+
* With `showNameOnHover`, hovering (or focusing) an avatar reveals its `name`
|
|
65
|
+
* in a tooltip — e.g. hovering Amir's photo shows "Amir".
|
|
47
66
|
*/
|
|
48
67
|
function AvatarGroup({
|
|
49
68
|
className,
|
|
@@ -51,13 +70,14 @@ function AvatarGroup({
|
|
|
51
70
|
max,
|
|
52
71
|
size = "sm",
|
|
53
72
|
overlap,
|
|
73
|
+
showNameOnHover = false,
|
|
54
74
|
children,
|
|
55
75
|
...props
|
|
56
76
|
}: AvatarGroupProps) {
|
|
57
77
|
const shown = items && max ? items.slice(0, max) : items;
|
|
58
78
|
const overflow = items && max ? items.length - max : 0;
|
|
59
79
|
|
|
60
|
-
|
|
80
|
+
const group = (
|
|
61
81
|
<div
|
|
62
82
|
className={cn(avatarGroupVariants({ overlap }), className)}
|
|
63
83
|
data-slot="avatar-group"
|
|
@@ -65,16 +85,36 @@ function AvatarGroup({
|
|
|
65
85
|
{...props}
|
|
66
86
|
>
|
|
67
87
|
{items
|
|
68
|
-
? shown?.map((item, i) =>
|
|
69
|
-
|
|
70
|
-
|
|
88
|
+
? shown?.map((item, i) => {
|
|
89
|
+
const label = item.name ?? item.alt;
|
|
90
|
+
const avatar = (
|
|
91
|
+
<Avatar
|
|
92
|
+
alt={item.alt}
|
|
93
|
+
initials={item.initials}
|
|
94
|
+
size={size}
|
|
95
|
+
src={item.src}
|
|
96
|
+
/>
|
|
97
|
+
);
|
|
98
|
+
if (!(showNameOnHover && label)) {
|
|
71
99
|
// biome-ignore lint/suspicious/noArrayIndexKey: order is stable
|
|
72
|
-
key={i}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
100
|
+
return <Avatar alt={item.alt} initials={item.initials} key={i} size={size} src={item.src} />;
|
|
101
|
+
}
|
|
102
|
+
return (
|
|
103
|
+
// The span wrapper is the group's direct child, so it carries the
|
|
104
|
+
// ring/overlap styling; the tooltip anchors to it (Base UI needs a
|
|
105
|
+
// ref-able host, which the Avatar fn component doesn't forward).
|
|
106
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: order is stable
|
|
107
|
+
<Tooltip key={i}>
|
|
108
|
+
<TooltipTrigger
|
|
109
|
+
className="inline-flex cursor-default"
|
|
110
|
+
render={<span />}
|
|
111
|
+
>
|
|
112
|
+
{avatar}
|
|
113
|
+
</TooltipTrigger>
|
|
114
|
+
<TooltipPopup>{label}</TooltipPopup>
|
|
115
|
+
</Tooltip>
|
|
116
|
+
);
|
|
117
|
+
})
|
|
78
118
|
: children}
|
|
79
119
|
{overflow > 0 ? (
|
|
80
120
|
<span
|
|
@@ -92,6 +132,19 @@ function AvatarGroup({
|
|
|
92
132
|
) : null}
|
|
93
133
|
</div>
|
|
94
134
|
);
|
|
135
|
+
|
|
136
|
+
// Self-contained: provide a tooltip context so the hover mode works without
|
|
137
|
+
// the consumer having to mount a TooltipProvider. A short delay keeps names
|
|
138
|
+
// from flickering as the pointer sweeps across the row.
|
|
139
|
+
if (showNameOnHover) {
|
|
140
|
+
return (
|
|
141
|
+
<TooltipProvider delay={150} closeDelay={0}>
|
|
142
|
+
{group}
|
|
143
|
+
</TooltipProvider>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return group;
|
|
95
148
|
}
|
|
96
149
|
|
|
97
150
|
export { AvatarGroup, avatarGroupVariants };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils/css";
|
|
6
|
+
|
|
7
|
+
type ChatDividerVariant = "default" | "summary" | "unread";
|
|
8
|
+
|
|
9
|
+
interface ChatDividerProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
10
|
+
/** Centered label, e.g. "New messages", "You're caught up", "Earlier messages summarized". */
|
|
11
|
+
label?: React.ReactNode;
|
|
12
|
+
/** Optional leading icon (e.g. a summary or sparkle glyph). */
|
|
13
|
+
icon?: React.ReactNode;
|
|
14
|
+
/**
|
|
15
|
+
* - `default` — neutral hairline with a muted label.
|
|
16
|
+
* - `summary` — marks where a rolling summary folds older history.
|
|
17
|
+
* - `unread` — the "new messages" baseline; accented so it stands out.
|
|
18
|
+
*/
|
|
19
|
+
variant?: ChatDividerVariant;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const LABEL_TONE: Record<ChatDividerVariant, string> = {
|
|
23
|
+
default: "text-muted-foreground",
|
|
24
|
+
summary: "text-muted-foreground",
|
|
25
|
+
unread: "text-primary",
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const LINE_TONE: Record<ChatDividerVariant, string> = {
|
|
29
|
+
default: "bg-border",
|
|
30
|
+
summary: "bg-border",
|
|
31
|
+
unread: "bg-primary/40",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A centered, labeled divider for the message stream: the point a rolling summary
|
|
36
|
+
* folds history (`summary`), the unread baseline (`unread`), or a plain date/section
|
|
37
|
+
* break (`default`). Pairs with the backend `AgentSummaryCheckpoint` and each
|
|
38
|
+
* participant's `joined_seq`/`last_read_seq`.
|
|
39
|
+
*/
|
|
40
|
+
function ChatDivider({ label, icon, variant = "default", className, ...props }: ChatDividerProps) {
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
className={cn("flex w-full items-center gap-3 py-1 select-none", className)}
|
|
44
|
+
data-slot="chat-divider"
|
|
45
|
+
data-variant={variant}
|
|
46
|
+
role="separator"
|
|
47
|
+
{...props}
|
|
48
|
+
>
|
|
49
|
+
<span className={cn("h-px flex-1", LINE_TONE[variant])} />
|
|
50
|
+
{label != null ? (
|
|
51
|
+
<span
|
|
52
|
+
className={cn(
|
|
53
|
+
"inline-flex items-center gap-1.5 font-medium text-xs whitespace-nowrap",
|
|
54
|
+
LABEL_TONE[variant],
|
|
55
|
+
)}
|
|
56
|
+
>
|
|
57
|
+
{icon ? <span className="inline-flex shrink-0 items-center">{icon}</span> : null}
|
|
58
|
+
{label}
|
|
59
|
+
</span>
|
|
60
|
+
) : null}
|
|
61
|
+
<span className={cn("h-px flex-1", LINE_TONE[variant])} />
|
|
62
|
+
</div>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { ChatDivider };
|
|
67
|
+
export type { ChatDividerProps, ChatDividerVariant };
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../lib/utils/css";
|
|
6
|
+
|
|
7
|
+
type ContextMeterState = "ok" | "warn" | "summarizing" | "full";
|
|
8
|
+
|
|
9
|
+
interface ContextMeterProps extends Omit<React.SVGProps<SVGSVGElement>, "children"> {
|
|
10
|
+
/** Tokens currently in context. Used with `budget` when `pct` is not given. */
|
|
11
|
+
used?: number;
|
|
12
|
+
/** The model's context budget in tokens. */
|
|
13
|
+
budget?: number;
|
|
14
|
+
/** Fill fraction 0..1. Takes precedence over `used`/`budget` when provided. */
|
|
15
|
+
pct?: number;
|
|
16
|
+
/** Visual state; derived from the thresholds below when omitted. */
|
|
17
|
+
state?: ContextMeterState;
|
|
18
|
+
/** Threshold fractions — mirror the backend `config.context` values. */
|
|
19
|
+
warnAt?: number;
|
|
20
|
+
summarizeAt?: number;
|
|
21
|
+
lockAt?: number;
|
|
22
|
+
/** Diameter in px. */
|
|
23
|
+
size?: number;
|
|
24
|
+
/** Ring thickness in px. */
|
|
25
|
+
strokeWidth?: number;
|
|
26
|
+
/** Render the rounded percentage in the ring's center. */
|
|
27
|
+
showLabel?: boolean;
|
|
28
|
+
className?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const STATE_COLOR: Record<ContextMeterState, string> = {
|
|
32
|
+
ok: "text-muted-foreground",
|
|
33
|
+
warn: "text-amber-500",
|
|
34
|
+
summarizing: "text-sky-500",
|
|
35
|
+
full: "text-destructive",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function deriveState(
|
|
39
|
+
pct: number,
|
|
40
|
+
warnAt: number,
|
|
41
|
+
summarizeAt: number,
|
|
42
|
+
lockAt: number,
|
|
43
|
+
): ContextMeterState {
|
|
44
|
+
if (pct >= lockAt) return "full";
|
|
45
|
+
if (pct >= summarizeAt) return "summarizing";
|
|
46
|
+
if (pct >= warnAt) return "warn";
|
|
47
|
+
return "ok";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* A compact radial gauge for how full an agent conversation's context window is.
|
|
52
|
+
*
|
|
53
|
+
* Feed it the backend `context_meter` payload (`pct`/`state` + thresholds) or a
|
|
54
|
+
* raw `used`/`budget` pair. The ring colors shift as the conversation approaches
|
|
55
|
+
* the summarize (folds older messages) and lock (must start a new chat) points,
|
|
56
|
+
* mirroring Claude Code's context indicator. Theme-aware and accessible
|
|
57
|
+
* (`role="meter"`).
|
|
58
|
+
*/
|
|
59
|
+
function ContextMeter({
|
|
60
|
+
used,
|
|
61
|
+
budget,
|
|
62
|
+
pct,
|
|
63
|
+
state,
|
|
64
|
+
warnAt = 0.75,
|
|
65
|
+
summarizeAt = 0.85,
|
|
66
|
+
lockAt = 0.98,
|
|
67
|
+
size = 24,
|
|
68
|
+
strokeWidth = 2.5,
|
|
69
|
+
showLabel = false,
|
|
70
|
+
className,
|
|
71
|
+
...props
|
|
72
|
+
}: ContextMeterProps) {
|
|
73
|
+
const fraction = Math.max(
|
|
74
|
+
0,
|
|
75
|
+
Math.min(1, pct ?? (budget ? (used ?? 0) / budget : 0)),
|
|
76
|
+
);
|
|
77
|
+
const resolvedState = state ?? deriveState(fraction, warnAt, summarizeAt, lockAt);
|
|
78
|
+
const percent = Math.round(fraction * 100);
|
|
79
|
+
|
|
80
|
+
const radius = (size - strokeWidth) / 2;
|
|
81
|
+
const circumference = 2 * Math.PI * radius;
|
|
82
|
+
const dash = circumference * fraction;
|
|
83
|
+
const center = size / 2;
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<span
|
|
87
|
+
className={cn("relative inline-flex items-center justify-center", STATE_COLOR[resolvedState])}
|
|
88
|
+
data-slot="context-meter"
|
|
89
|
+
data-state={resolvedState}
|
|
90
|
+
style={{ width: size, height: size }}
|
|
91
|
+
>
|
|
92
|
+
<svg
|
|
93
|
+
width={size}
|
|
94
|
+
height={size}
|
|
95
|
+
viewBox={`0 0 ${size} ${size}`}
|
|
96
|
+
role="meter"
|
|
97
|
+
aria-valuemin={0}
|
|
98
|
+
aria-valuemax={100}
|
|
99
|
+
aria-valuenow={percent}
|
|
100
|
+
aria-label={`Context ${percent}% used`}
|
|
101
|
+
className={cn("-rotate-90", className)}
|
|
102
|
+
{...props}
|
|
103
|
+
>
|
|
104
|
+
<circle
|
|
105
|
+
cx={center}
|
|
106
|
+
cy={center}
|
|
107
|
+
r={radius}
|
|
108
|
+
fill="none"
|
|
109
|
+
strokeWidth={strokeWidth}
|
|
110
|
+
className="stroke-muted"
|
|
111
|
+
/>
|
|
112
|
+
<circle
|
|
113
|
+
cx={center}
|
|
114
|
+
cy={center}
|
|
115
|
+
r={radius}
|
|
116
|
+
fill="none"
|
|
117
|
+
strokeWidth={strokeWidth}
|
|
118
|
+
strokeLinecap="round"
|
|
119
|
+
stroke="currentColor"
|
|
120
|
+
strokeDasharray={`${dash} ${circumference}`}
|
|
121
|
+
className="transition-[stroke-dasharray] duration-500 ease-out"
|
|
122
|
+
/>
|
|
123
|
+
</svg>
|
|
124
|
+
{showLabel ? (
|
|
125
|
+
<span className="absolute font-medium text-[0.5rem] text-foreground tabular-nums">
|
|
126
|
+
{percent}
|
|
127
|
+
</span>
|
|
128
|
+
) : null}
|
|
129
|
+
</span>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export { ContextMeter };
|
|
134
|
+
export type { ContextMeterProps, ContextMeterState };
|
package/src/index.ts
CHANGED
|
@@ -24,12 +24,14 @@ export * from "./components/ui/card";
|
|
|
24
24
|
export * from "./components/ui/carousel";
|
|
25
25
|
export * from "./components/ui/chart";
|
|
26
26
|
export * from "./components/ui/chat";
|
|
27
|
+
export * from "./components/ui/chat-divider";
|
|
27
28
|
export * from "./components/ui/checkbox";
|
|
28
29
|
export * from "./components/ui/checkbox-group";
|
|
29
30
|
export * from "./components/ui/collapsible";
|
|
30
31
|
export * from "./components/ui/combobox";
|
|
31
32
|
export * from "./components/ui/command";
|
|
32
33
|
export * from "./components/ui/confetti";
|
|
34
|
+
export * from "./components/ui/context-meter";
|
|
33
35
|
export * from "./components/ui/credit-card";
|
|
34
36
|
export * from "./components/ui/data-table-paged";
|
|
35
37
|
export * from "./components/ui/drawer";
|