harebear-ui 0.5.0 → 0.7.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/AGENTS.md +19 -2
- package/README.md +108 -36
- package/bin/harebear-ui.mjs +66 -3
- package/dist/components/admin/status.d.ts +3 -1
- package/dist/components/admin/status.js +5 -2
- package/dist/components/ai/approval.d.ts +3 -1
- package/dist/components/ai/approval.js +6 -3
- package/dist/components/ai/chat.d.ts +5 -2
- package/dist/components/ai/chat.js +1 -1
- package/dist/components/ai/code-block.d.ts +5 -1
- package/dist/components/ai/code-block.js +5 -3
- package/dist/components/ai/conversation-list.d.ts +16 -2
- package/dist/components/ai/conversation-list.js +88 -37
- package/dist/components/ai/model-picker.d.ts +6 -1
- package/dist/components/ai/model-picker.js +43 -23
- package/dist/components/layout/media-item.d.ts +13 -0
- package/dist/components/layout/media-item.js +54 -0
- package/dist/components/layout/metric.d.ts +15 -0
- package/dist/components/layout/metric.js +51 -0
- package/dist/components/layout/show-more.d.ts +17 -0
- package/dist/components/layout/show-more.js +30 -0
- package/dist/components/layout/stack.d.ts +39 -0
- package/dist/components/layout/stack.js +114 -0
- package/dist/components/layout/text.d.ts +37 -0
- package/dist/components/layout/text.js +52 -0
- package/dist/components/theme/theme-toggle.d.ts +4 -1
- package/dist/components/theme/theme-toggle.js +11 -6
- package/dist/components/ui/badge.d.ts +4 -1
- package/dist/components/ui/badge.js +10 -1
- package/dist/components/ui/button.d.ts +3 -1
- package/dist/components/ui/button.js +13 -3
- package/dist/components/ui/empty.d.ts +4 -1
- package/dist/components/ui/empty.js +3 -2
- package/dist/components/ui/item.d.ts +4 -1
- package/dist/components/ui/item.js +7 -2
- package/dist/index.d.ts +8 -3
- package/dist/index.js +7 -2
- package/docs/COMPONENTS.md +96 -71
- package/docs/guides/agents.md +75 -0
- package/docs/guides/getting-started.md +217 -0
- package/docs/guides/theming.md +98 -0
- package/llms.txt +6 -1
- package/package.json +2 -1
- package/skills/harebear-ui/SKILL.md +1 -1
|
@@ -1,19 +1,35 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { ConfirmDialog } from "../admin/confirm-dialog.js";
|
|
2
3
|
import { StatusDot } from "../admin/status.js";
|
|
3
4
|
import { Button } from "../ui/button.js";
|
|
4
5
|
import { Empty, EmptyDescription } from "../ui/empty.js";
|
|
5
6
|
import { useSidebarOptional } from "../ui/sidebar.js";
|
|
6
7
|
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip.js";
|
|
7
8
|
import { cn } from "../../lib/utils.js";
|
|
9
|
+
import * as React from "react";
|
|
8
10
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
-
import { PlusIcon } from "lucide-react";
|
|
11
|
+
import { PlusIcon, Trash2Icon } from "lucide-react";
|
|
10
12
|
// src/components/ai/conversation-list.tsx
|
|
11
13
|
/**
|
|
12
14
|
* Grouped list of chat threads for a sidebar. Inside a Sidebar it closes the mobile sheet on select,
|
|
13
15
|
* and hides itself when the sidebar is collapsed to its icon rail.
|
|
14
16
|
*/
|
|
15
|
-
function ConversationList({ groups, activeId, onSelect, renderAction, empty = "No conversations yet", className, ...props }) {
|
|
17
|
+
function ConversationList({ groups, activeId, onSelect, renderAction, onDelete, deleteDescription = "This cannot be undone.", empty = "No conversations yet", className, ...props }) {
|
|
16
18
|
const sidebar = useSidebarOptional();
|
|
19
|
+
const [confirm, setConfirm] = React.useState(null);
|
|
20
|
+
const [confirmOpen, setConfirmOpen] = React.useState(false);
|
|
21
|
+
const deleteButton = (c) => /* @__PURE__ */ jsx(Button, {
|
|
22
|
+
variant: "ghost",
|
|
23
|
+
size: "icon-sm",
|
|
24
|
+
"aria-label": `Delete ${c.title}`,
|
|
25
|
+
title: typeof c.deleteDisabled === "string" ? c.deleteDisabled : "Delete conversation",
|
|
26
|
+
disabled: !!c.deleteDisabled,
|
|
27
|
+
onClick: () => {
|
|
28
|
+
setConfirm(c);
|
|
29
|
+
setConfirmOpen(true);
|
|
30
|
+
},
|
|
31
|
+
children: /* @__PURE__ */ jsx(Trash2Icon, {})
|
|
32
|
+
});
|
|
17
33
|
const select = (id) => {
|
|
18
34
|
onSelect?.(id);
|
|
19
35
|
sidebar?.setOpenMobile(false);
|
|
@@ -24,43 +40,57 @@ function ConversationList({ groups, activeId, onSelect, renderAction, empty = "N
|
|
|
24
40
|
"aria-label": "Conversations",
|
|
25
41
|
className: cn("flex flex-col gap-4.5 group-data-[collapsible=icon]:hidden", className),
|
|
26
42
|
...props,
|
|
27
|
-
children: [
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
children: c.title
|
|
47
|
-
}), c.description !== void 0 && /* @__PURE__ */ jsxs("span", {
|
|
48
|
-
className: "flex min-w-0 items-center gap-1.5 text-xs font-normal text-muted-foreground tabular-nums",
|
|
49
|
-
children: [c.tone && /* @__PURE__ */ jsx(StatusDot, {
|
|
50
|
-
tone: c.tone,
|
|
51
|
-
pulse: c.pulse
|
|
52
|
-
}), /* @__PURE__ */ jsx("span", {
|
|
43
|
+
children: [
|
|
44
|
+
visible.map((g) => /* @__PURE__ */ jsxs("div", {
|
|
45
|
+
className: "flex flex-col gap-0.5",
|
|
46
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
47
|
+
className: "px-3 pb-1.5 text-xs font-medium text-muted-foreground",
|
|
48
|
+
children: g.label
|
|
49
|
+
}), g.items.map((c) => {
|
|
50
|
+
const active = c.id === activeId;
|
|
51
|
+
const action = renderAction ? renderAction(c) : onDelete ? deleteButton(c) : null;
|
|
52
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
53
|
+
"data-slot": "conversation-item",
|
|
54
|
+
"data-active": active || void 0,
|
|
55
|
+
className: cn("group/conversation flex min-w-0 items-center rounded-md text-muted-foreground transition-colors hover:bg-muted hover:text-foreground", active && "bg-card text-foreground shadow-card hover:bg-card"),
|
|
56
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
57
|
+
type: "button",
|
|
58
|
+
"aria-current": active ? "page" : void 0,
|
|
59
|
+
onClick: () => select(c.id),
|
|
60
|
+
className: cn("flex min-w-0 flex-1 flex-col rounded-md py-2.25 pl-3 text-left font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring/40", action ? "pr-1" : "pr-3"),
|
|
61
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
53
62
|
className: "truncate",
|
|
54
|
-
children: c.
|
|
63
|
+
children: c.title
|
|
64
|
+
}), c.description !== void 0 && /* @__PURE__ */ jsxs("span", {
|
|
65
|
+
className: "flex min-w-0 items-center gap-1.5 text-xs font-normal text-muted-foreground tabular-nums",
|
|
66
|
+
children: [c.tone && /* @__PURE__ */ jsx(StatusDot, {
|
|
67
|
+
tone: c.tone,
|
|
68
|
+
pulse: c.pulse
|
|
69
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
70
|
+
className: "truncate",
|
|
71
|
+
children: c.description
|
|
72
|
+
})]
|
|
55
73
|
})]
|
|
74
|
+
}), action && /* @__PURE__ */ jsx("div", {
|
|
75
|
+
className: "mr-1 flex shrink-0 items-center [@media(hover:hover)]:opacity-0 [@media(hover:hover)]:group-hover/conversation:opacity-100 [@media(hover:hover)]:group-focus-within/conversation:opacity-100",
|
|
76
|
+
children: action
|
|
56
77
|
})]
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
78
|
+
}, c.id);
|
|
79
|
+
})]
|
|
80
|
+
}, g.label)),
|
|
81
|
+
!visible.length && empty && /* @__PURE__ */ jsx(Empty, { children: /* @__PURE__ */ jsx(EmptyDescription, { children: empty }) }),
|
|
82
|
+
onDelete && /* @__PURE__ */ jsx(ConfirmDialog, {
|
|
83
|
+
open: confirmOpen,
|
|
84
|
+
onOpenChange: setConfirmOpen,
|
|
85
|
+
title: `Delete “${confirm?.title ?? ""}”?`,
|
|
86
|
+
description: deleteDescription,
|
|
87
|
+
confirmLabel: "Delete",
|
|
88
|
+
destructive: true,
|
|
89
|
+
onConfirm: () => {
|
|
90
|
+
if (confirm) onDelete(confirm);
|
|
91
|
+
}
|
|
92
|
+
})
|
|
93
|
+
]
|
|
64
94
|
});
|
|
65
95
|
}
|
|
66
96
|
/**
|
|
@@ -101,6 +131,27 @@ function groupByDate(items, getDate, toConversation, now = Date.now()) {
|
|
|
101
131
|
return groups.filter((g) => g.items.length);
|
|
102
132
|
}
|
|
103
133
|
/**
|
|
134
|
+
* Short timestamp for lists and headers: the time today ("9:41 AM"), the date this year ("Nov 3"), else with the
|
|
135
|
+
* year ("Nov 3, 2025"). Uses the browser locale unless `locale` is given. Pass `now` for deterministic tests.
|
|
136
|
+
*/
|
|
137
|
+
function formatWhen(date, now = Date.now(), locale) {
|
|
138
|
+
const d = new Date(date);
|
|
139
|
+
const n = new Date(now);
|
|
140
|
+
if (d.toDateString() === n.toDateString()) return d.toLocaleTimeString(locale, {
|
|
141
|
+
hour: "numeric",
|
|
142
|
+
minute: "2-digit"
|
|
143
|
+
});
|
|
144
|
+
if (d.getFullYear() === n.getFullYear()) return d.toLocaleDateString(locale, {
|
|
145
|
+
month: "short",
|
|
146
|
+
day: "numeric"
|
|
147
|
+
});
|
|
148
|
+
return d.toLocaleDateString(locale, {
|
|
149
|
+
month: "short",
|
|
150
|
+
day: "numeric",
|
|
151
|
+
year: "numeric"
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
104
155
|
* Full-width "New conversation" button for a sidebar header. In the icon rail it shrinks to an icon with a
|
|
105
156
|
* tooltip; on mobile it closes the sidebar sheet after the click.
|
|
106
157
|
*/
|
|
@@ -136,4 +187,4 @@ function NewConversationButton({ label = "New conversation", icon = /* @__PURE__
|
|
|
136
187
|
children: label
|
|
137
188
|
})] });
|
|
138
189
|
}
|
|
139
|
-
export { ConversationList, NewConversationButton, groupByDate };
|
|
190
|
+
export { ConversationList, NewConversationButton, formatWhen, groupByDate };
|
|
@@ -7,6 +7,8 @@ export interface ModelOption {
|
|
|
7
7
|
description?: React.ReactNode;
|
|
8
8
|
/** Availability dot. Default "success". */
|
|
9
9
|
status?: StatusTone;
|
|
10
|
+
/** Section heading in the menu, e.g. the provider. Models with the same group are listed together. */
|
|
11
|
+
group?: string;
|
|
10
12
|
}
|
|
11
13
|
export interface ModelPickerProps {
|
|
12
14
|
models: ModelOption[];
|
|
@@ -16,6 +18,9 @@ export interface ModelPickerProps {
|
|
|
16
18
|
className?: string;
|
|
17
19
|
disabled?: boolean;
|
|
18
20
|
}
|
|
19
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Pill button showing the active model with a status dot; opens a menu of models, sectioned by `group`.
|
|
23
|
+
* A `value` missing from `models` is still listed (and checked) at the top.
|
|
24
|
+
*/
|
|
20
25
|
declare function ModelPicker({ models, value, onValueChange, label, className, disabled }: ModelPickerProps): React.JSX.Element;
|
|
21
26
|
export { ModelPicker };
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { StatusDot } from "../admin/status.js";
|
|
3
3
|
import { Button } from "../ui/button.js";
|
|
4
|
-
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "../ui/dropdown-menu.js";
|
|
4
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu.js";
|
|
5
5
|
import { cn } from "../../lib/utils.js";
|
|
6
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
7
|
import { CheckIcon, ChevronDownIcon } from "lucide-react";
|
|
8
8
|
// src/components/ai/model-picker.tsx
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Pill button showing the active model with a status dot; opens a menu of models, sectioned by `group`.
|
|
11
|
+
* A `value` missing from `models` is still listed (and checked) at the top.
|
|
12
|
+
*/
|
|
10
13
|
function ModelPicker({ models, value, onValueChange, label = "Model", className, disabled }) {
|
|
11
14
|
const current = models.find((m) => m.id === value);
|
|
15
|
+
const all = current || !value ? models : [{ id: value }, ...models];
|
|
16
|
+
const groups = [];
|
|
17
|
+
for (const m of all) {
|
|
18
|
+
const g = groups.find((x) => x.name === m.group);
|
|
19
|
+
if (g) g.items.push(m);
|
|
20
|
+
else groups.push({
|
|
21
|
+
name: m.group,
|
|
22
|
+
items: [m]
|
|
23
|
+
});
|
|
24
|
+
}
|
|
12
25
|
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
13
26
|
asChild: true,
|
|
14
27
|
disabled,
|
|
@@ -29,27 +42,34 @@ function ModelPicker({ models, value, onValueChange, label = "Model", className,
|
|
|
29
42
|
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
30
43
|
align: "end",
|
|
31
44
|
className: "min-w-56",
|
|
32
|
-
children: [/* @__PURE__ */ jsx(DropdownMenuLabel, { children: label }),
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}),
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
children: [/* @__PURE__ */ jsx(DropdownMenuLabel, { children: label }), groups.map((g, i) => /* @__PURE__ */ jsxs(DropdownMenuGroup, { children: [
|
|
46
|
+
i > 0 && /* @__PURE__ */ jsx(DropdownMenuSeparator, {}),
|
|
47
|
+
g.name && /* @__PURE__ */ jsx(DropdownMenuLabel, {
|
|
48
|
+
className: "text-xs font-medium text-muted-foreground",
|
|
49
|
+
children: g.name
|
|
50
|
+
}),
|
|
51
|
+
g.items.map((m) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
52
|
+
onSelect: () => onValueChange(m.id),
|
|
53
|
+
className: "items-start gap-2.5",
|
|
54
|
+
children: [
|
|
55
|
+
/* @__PURE__ */ jsx(StatusDot, {
|
|
56
|
+
tone: m.status ?? "success",
|
|
57
|
+
className: "mt-1.5"
|
|
58
|
+
}),
|
|
59
|
+
/* @__PURE__ */ jsxs("div", {
|
|
60
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
61
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
62
|
+
className: "font-mono text-xs",
|
|
63
|
+
children: m.label ?? m.id
|
|
64
|
+
}), m.description && /* @__PURE__ */ jsx("span", {
|
|
65
|
+
className: "text-xs text-muted-foreground",
|
|
66
|
+
children: m.description
|
|
67
|
+
})]
|
|
68
|
+
}),
|
|
69
|
+
m.id === value && /* @__PURE__ */ jsx(CheckIcon, { className: "mt-0.5" })
|
|
70
|
+
]
|
|
71
|
+
}, m.id))
|
|
72
|
+
] }, g.name ?? `group-${i}`))]
|
|
53
73
|
})] });
|
|
54
74
|
}
|
|
55
75
|
export { ModelPicker };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface MediaItemProps extends Omit<React.ComponentProps<"div">, "title"> {
|
|
3
|
+
title: React.ReactNode;
|
|
4
|
+
/** Opens in a new tab. */
|
|
5
|
+
href?: string;
|
|
6
|
+
image?: string;
|
|
7
|
+
imageShape?: "poster" | "square" | "wide";
|
|
8
|
+
meta?: React.ReactNode;
|
|
9
|
+
description?: React.ReactNode;
|
|
10
|
+
}
|
|
11
|
+
/** Thumbnail, title, a meta line and a two-line description. For media, articles, releases. */
|
|
12
|
+
declare function MediaItem({ title, href, image, imageShape, meta, description, className, ...props }: MediaItemProps): React.JSX.Element;
|
|
13
|
+
export { MediaItem };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
// src/components/layout/media-item.tsx
|
|
5
|
+
const shapeClass = {
|
|
6
|
+
poster: "aspect-[2/3] w-14",
|
|
7
|
+
square: "aspect-square w-12",
|
|
8
|
+
wide: "aspect-video w-24"
|
|
9
|
+
};
|
|
10
|
+
/** Thumbnail, title, a meta line and a two-line description. For media, articles, releases. */
|
|
11
|
+
function MediaItem({ title, href, image, imageShape = "poster", meta, description, className, ...props }) {
|
|
12
|
+
const img = image ? /* @__PURE__ */ jsx("img", {
|
|
13
|
+
src: image,
|
|
14
|
+
alt: "",
|
|
15
|
+
loading: "lazy",
|
|
16
|
+
className: cn("shrink-0 rounded-md bg-muted object-cover", shapeClass[imageShape])
|
|
17
|
+
}) : null;
|
|
18
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
19
|
+
"data-slot": "media-item",
|
|
20
|
+
className: cn("flex min-w-0 items-start gap-3", className),
|
|
21
|
+
...props,
|
|
22
|
+
children: [img && href ? /* @__PURE__ */ jsx("a", {
|
|
23
|
+
href,
|
|
24
|
+
target: "_blank",
|
|
25
|
+
rel: "noreferrer",
|
|
26
|
+
tabIndex: -1,
|
|
27
|
+
className: "shrink-0",
|
|
28
|
+
children: img
|
|
29
|
+
}) : img, /* @__PURE__ */ jsxs("div", {
|
|
30
|
+
className: "flex min-w-0 flex-col gap-0.5",
|
|
31
|
+
children: [
|
|
32
|
+
/* @__PURE__ */ jsx("span", {
|
|
33
|
+
className: "line-clamp-2 font-medium",
|
|
34
|
+
children: href ? /* @__PURE__ */ jsx("a", {
|
|
35
|
+
href,
|
|
36
|
+
target: "_blank",
|
|
37
|
+
rel: "noreferrer",
|
|
38
|
+
className: "outline-none hover:underline focus-visible:underline",
|
|
39
|
+
children: title
|
|
40
|
+
}) : title
|
|
41
|
+
}),
|
|
42
|
+
meta && /* @__PURE__ */ jsx("span", {
|
|
43
|
+
className: "text-xs text-muted-foreground",
|
|
44
|
+
children: meta
|
|
45
|
+
}),
|
|
46
|
+
description && /* @__PURE__ */ jsx("span", {
|
|
47
|
+
className: "line-clamp-2 text-[13px] text-muted-foreground",
|
|
48
|
+
children: description
|
|
49
|
+
})
|
|
50
|
+
]
|
|
51
|
+
})]
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
export { MediaItem };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface MetricProps extends Omit<React.ComponentProps<"div">, "children"> {
|
|
3
|
+
label: React.ReactNode;
|
|
4
|
+
value: React.ReactNode;
|
|
5
|
+
unit?: React.ReactNode;
|
|
6
|
+
/** Percent change. Positive reads as good unless `lowerIsBetter`. */
|
|
7
|
+
delta?: number;
|
|
8
|
+
lowerIsBetter?: boolean;
|
|
9
|
+
align?: "start" | "center";
|
|
10
|
+
}
|
|
11
|
+
/** A number with a unit, a label under it, and an optional percent change over it. */
|
|
12
|
+
declare function Metric({ label, value, unit, delta, lowerIsBetter, align, className, ...props }: MetricProps): React.JSX.Element;
|
|
13
|
+
/** Metrics side by side, evenly spaced. */
|
|
14
|
+
declare function MetricGroup({ className, ...props }: React.ComponentProps<"div">): React.JSX.Element;
|
|
15
|
+
export { Metric, MetricGroup };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { ArrowDownRightIcon, ArrowUpRightIcon } from "lucide-react";
|
|
5
|
+
// src/components/layout/metric.tsx
|
|
6
|
+
/** A number with a unit, a label under it, and an optional percent change over it. */
|
|
7
|
+
function Metric({ label, value, unit, delta, lowerIsBetter, align = "center", className, ...props }) {
|
|
8
|
+
const good = delta === void 0 || delta === 0 ? void 0 : lowerIsBetter ? delta < 0 : delta > 0;
|
|
9
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
10
|
+
"data-slot": "metric",
|
|
11
|
+
className: cn("flex min-w-0 flex-col gap-0.5", align === "center" ? "items-center text-center" : "items-start", className),
|
|
12
|
+
...props,
|
|
13
|
+
children: [
|
|
14
|
+
delta !== void 0 && Number.isFinite(delta) && /* @__PURE__ */ jsxs("span", {
|
|
15
|
+
className: cn("inline-flex items-center gap-0.5 text-xs font-medium tabular-nums", good === void 0 ? "text-muted-foreground" : good ? "text-success" : "text-destructive"),
|
|
16
|
+
children: [
|
|
17
|
+
delta > 0 ? "+" : "",
|
|
18
|
+
delta.toFixed(1),
|
|
19
|
+
"%",
|
|
20
|
+
delta > 0 ? /* @__PURE__ */ jsx(ArrowUpRightIcon, {
|
|
21
|
+
"aria-hidden": true,
|
|
22
|
+
className: "size-3.5"
|
|
23
|
+
}) : delta < 0 ? /* @__PURE__ */ jsx(ArrowDownRightIcon, {
|
|
24
|
+
"aria-hidden": true,
|
|
25
|
+
className: "size-3.5"
|
|
26
|
+
}) : null
|
|
27
|
+
]
|
|
28
|
+
}),
|
|
29
|
+
/* @__PURE__ */ jsxs("span", {
|
|
30
|
+
className: "text-xl font-semibold tracking-tight whitespace-nowrap tabular-nums @sm:text-2xl",
|
|
31
|
+
children: [value, unit && /* @__PURE__ */ jsx("span", {
|
|
32
|
+
className: "ml-0.5 text-xs font-medium text-muted-foreground",
|
|
33
|
+
children: unit
|
|
34
|
+
})]
|
|
35
|
+
}),
|
|
36
|
+
/* @__PURE__ */ jsx("span", {
|
|
37
|
+
className: "text-xs font-medium tracking-wide text-muted-foreground uppercase",
|
|
38
|
+
children: label
|
|
39
|
+
})
|
|
40
|
+
]
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/** Metrics side by side, evenly spaced. */
|
|
44
|
+
function MetricGroup({ className, ...props }) {
|
|
45
|
+
return /* @__PURE__ */ jsx("div", {
|
|
46
|
+
"data-slot": "metric-group",
|
|
47
|
+
className: cn("grid auto-cols-fr grid-flow-col gap-2", className),
|
|
48
|
+
...props
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export { Metric, MetricGroup };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
declare const gapClass: {
|
|
3
|
+
readonly none: "gap-0";
|
|
4
|
+
readonly xs: "gap-1";
|
|
5
|
+
readonly sm: "gap-2";
|
|
6
|
+
readonly md: "gap-3";
|
|
7
|
+
readonly lg: "gap-4";
|
|
8
|
+
readonly xl: "gap-6";
|
|
9
|
+
};
|
|
10
|
+
export interface ShowMoreProps extends React.ComponentProps<"div"> {
|
|
11
|
+
/** Children shown before the "Show N more" button. Default 5. */
|
|
12
|
+
initial?: number;
|
|
13
|
+
gap?: keyof typeof gapClass;
|
|
14
|
+
}
|
|
15
|
+
/** A list that collapses after `initial` children, with a button to show the rest. */
|
|
16
|
+
declare function ShowMore({ initial, gap, className, children, ...props }: ShowMoreProps): React.JSX.Element;
|
|
17
|
+
export { ShowMore };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
// src/components/layout/show-more.tsx
|
|
6
|
+
const gapClass = {
|
|
7
|
+
none: "gap-0",
|
|
8
|
+
xs: "gap-1",
|
|
9
|
+
sm: "gap-2",
|
|
10
|
+
md: "gap-3",
|
|
11
|
+
lg: "gap-4",
|
|
12
|
+
xl: "gap-6"
|
|
13
|
+
};
|
|
14
|
+
/** A list that collapses after `initial` children, with a button to show the rest. */
|
|
15
|
+
function ShowMore({ initial = 5, gap = "md", className, children, ...props }) {
|
|
16
|
+
const [open, setOpen] = React.useState(false);
|
|
17
|
+
const all = React.Children.toArray(children);
|
|
18
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
19
|
+
"data-slot": "show-more",
|
|
20
|
+
className: cn("flex min-w-0 flex-col", gapClass[gap], className),
|
|
21
|
+
...props,
|
|
22
|
+
children: [open ? all : all.slice(0, initial), all.length > initial && /* @__PURE__ */ jsx("button", {
|
|
23
|
+
type: "button",
|
|
24
|
+
onClick: () => setOpen(!open),
|
|
25
|
+
className: "self-start rounded-md text-xs font-medium text-muted-foreground outline-none hover:text-foreground focus-visible:ring-4 focus-visible:ring-ring/20",
|
|
26
|
+
children: open ? "Show less" : `Show ${all.length - initial} more`
|
|
27
|
+
})]
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export { ShowMore };
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type StackSpace = "none" | "xs" | "sm" | "md" | "lg" | "xl";
|
|
3
|
+
export type StackAlign = "start" | "center" | "end" | "stretch" | "baseline";
|
|
4
|
+
export type StackJustify = "start" | "center" | "end" | "between";
|
|
5
|
+
export interface StackProps extends React.ComponentProps<"div"> {
|
|
6
|
+
gap?: StackSpace;
|
|
7
|
+
align?: StackAlign;
|
|
8
|
+
}
|
|
9
|
+
/** Vertical stack with a named gap. */
|
|
10
|
+
declare function Stack({ gap, align, className, ...props }: StackProps): React.JSX.Element;
|
|
11
|
+
export interface RowProps extends React.ComponentProps<"div"> {
|
|
12
|
+
gap?: StackSpace;
|
|
13
|
+
align?: StackAlign;
|
|
14
|
+
justify?: StackJustify;
|
|
15
|
+
wrap?: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Horizontal row with a named gap. */
|
|
18
|
+
declare function Row({ gap, align, justify, wrap, className, ...props }: RowProps): React.JSX.Element;
|
|
19
|
+
export type GridCols = 1 | 2 | 3 | 4 | 5 | 6;
|
|
20
|
+
export type ResponsiveGridCols = GridCols | {
|
|
21
|
+
base?: GridCols;
|
|
22
|
+
sm?: GridCols;
|
|
23
|
+
md?: GridCols;
|
|
24
|
+
lg?: GridCols;
|
|
25
|
+
};
|
|
26
|
+
export interface GridProps extends React.ComponentProps<"div"> {
|
|
27
|
+
/** Equal columns: a number, or per breakpoint `{ base: 1, sm: 2, md: 3 }`. */
|
|
28
|
+
cols?: ResponsiveGridCols;
|
|
29
|
+
gap?: StackSpace;
|
|
30
|
+
/**
|
|
31
|
+
* What sm/md/lg measure. "viewport" (default): the screen, Tailwind's sm/md/lg.
|
|
32
|
+
* "container": the nearest `@container` ancestor (sm >= 28rem, md >= 42rem, lg >= 56rem),
|
|
33
|
+
* for blocks that sit in cards of any width.
|
|
34
|
+
*/
|
|
35
|
+
query?: "viewport" | "container";
|
|
36
|
+
}
|
|
37
|
+
/** Grid of equal columns with named gaps and per-breakpoint column counts. */
|
|
38
|
+
declare function Grid({ cols: c, gap, query, className, ...props }: GridProps): React.JSX.Element;
|
|
39
|
+
export { Grid, Row, Stack };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
// src/components/layout/stack.tsx
|
|
5
|
+
const gapClass = {
|
|
6
|
+
none: "gap-0",
|
|
7
|
+
xs: "gap-1",
|
|
8
|
+
sm: "gap-2",
|
|
9
|
+
md: "gap-3",
|
|
10
|
+
lg: "gap-4",
|
|
11
|
+
xl: "gap-6"
|
|
12
|
+
};
|
|
13
|
+
const alignClass = {
|
|
14
|
+
start: "items-start",
|
|
15
|
+
center: "items-center",
|
|
16
|
+
end: "items-end",
|
|
17
|
+
stretch: "items-stretch",
|
|
18
|
+
baseline: "items-baseline"
|
|
19
|
+
};
|
|
20
|
+
const justifyClass = {
|
|
21
|
+
start: "justify-start",
|
|
22
|
+
center: "justify-center",
|
|
23
|
+
end: "justify-end",
|
|
24
|
+
between: "justify-between"
|
|
25
|
+
};
|
|
26
|
+
/** Vertical stack with a named gap. */
|
|
27
|
+
function Stack({ gap = "md", align = "stretch", className, ...props }) {
|
|
28
|
+
return /* @__PURE__ */ jsx("div", {
|
|
29
|
+
"data-slot": "stack",
|
|
30
|
+
className: cn("flex min-w-0 flex-col", gapClass[gap], alignClass[align], className),
|
|
31
|
+
...props
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** Horizontal row with a named gap. */
|
|
35
|
+
function Row({ gap = "sm", align = "center", justify = "start", wrap, className, ...props }) {
|
|
36
|
+
return /* @__PURE__ */ jsx("div", {
|
|
37
|
+
"data-slot": "row",
|
|
38
|
+
className: cn("flex min-w-0", gapClass[gap], alignClass[align], justifyClass[justify], wrap && "flex-wrap", className),
|
|
39
|
+
...props
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
const cols = {
|
|
43
|
+
base: {
|
|
44
|
+
1: "grid-cols-1",
|
|
45
|
+
2: "grid-cols-2",
|
|
46
|
+
3: "grid-cols-3",
|
|
47
|
+
4: "grid-cols-4",
|
|
48
|
+
5: "grid-cols-5",
|
|
49
|
+
6: "grid-cols-6"
|
|
50
|
+
},
|
|
51
|
+
viewport: {
|
|
52
|
+
sm: {
|
|
53
|
+
1: "sm:grid-cols-1",
|
|
54
|
+
2: "sm:grid-cols-2",
|
|
55
|
+
3: "sm:grid-cols-3",
|
|
56
|
+
4: "sm:grid-cols-4",
|
|
57
|
+
5: "sm:grid-cols-5",
|
|
58
|
+
6: "sm:grid-cols-6"
|
|
59
|
+
},
|
|
60
|
+
md: {
|
|
61
|
+
1: "md:grid-cols-1",
|
|
62
|
+
2: "md:grid-cols-2",
|
|
63
|
+
3: "md:grid-cols-3",
|
|
64
|
+
4: "md:grid-cols-4",
|
|
65
|
+
5: "md:grid-cols-5",
|
|
66
|
+
6: "md:grid-cols-6"
|
|
67
|
+
},
|
|
68
|
+
lg: {
|
|
69
|
+
1: "lg:grid-cols-1",
|
|
70
|
+
2: "lg:grid-cols-2",
|
|
71
|
+
3: "lg:grid-cols-3",
|
|
72
|
+
4: "lg:grid-cols-4",
|
|
73
|
+
5: "lg:grid-cols-5",
|
|
74
|
+
6: "lg:grid-cols-6"
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
container: {
|
|
78
|
+
sm: {
|
|
79
|
+
1: "@md:grid-cols-1",
|
|
80
|
+
2: "@md:grid-cols-2",
|
|
81
|
+
3: "@md:grid-cols-3",
|
|
82
|
+
4: "@md:grid-cols-4",
|
|
83
|
+
5: "@md:grid-cols-5",
|
|
84
|
+
6: "@md:grid-cols-6"
|
|
85
|
+
},
|
|
86
|
+
md: {
|
|
87
|
+
1: "@2xl:grid-cols-1",
|
|
88
|
+
2: "@2xl:grid-cols-2",
|
|
89
|
+
3: "@2xl:grid-cols-3",
|
|
90
|
+
4: "@2xl:grid-cols-4",
|
|
91
|
+
5: "@2xl:grid-cols-5",
|
|
92
|
+
6: "@2xl:grid-cols-6"
|
|
93
|
+
},
|
|
94
|
+
lg: {
|
|
95
|
+
1: "@4xl:grid-cols-1",
|
|
96
|
+
2: "@4xl:grid-cols-2",
|
|
97
|
+
3: "@4xl:grid-cols-3",
|
|
98
|
+
4: "@4xl:grid-cols-4",
|
|
99
|
+
5: "@4xl:grid-cols-5",
|
|
100
|
+
6: "@4xl:grid-cols-6"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
/** Grid of equal columns with named gaps and per-breakpoint column counts. */
|
|
105
|
+
function Grid({ cols: c = 2, gap = "md", query = "viewport", className, ...props }) {
|
|
106
|
+
const r = typeof c === "number" ? { base: c } : c;
|
|
107
|
+
const q = cols[query];
|
|
108
|
+
return /* @__PURE__ */ jsx("div", {
|
|
109
|
+
"data-slot": "grid",
|
|
110
|
+
className: cn("grid min-w-0", gapClass[gap], cols.base[r.base ?? 1], r.sm && q.sm[r.sm], r.md && q.md[r.md], r.lg && q.lg[r.lg], className),
|
|
111
|
+
...props
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export { Grid, Row, Stack };
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export type TextTone = "default" | "muted" | "success" | "warning" | "danger" | "info";
|
|
3
|
+
declare const sizeClass: {
|
|
4
|
+
readonly xs: "text-xs";
|
|
5
|
+
readonly sm: "text-[13px]";
|
|
6
|
+
readonly base: "text-sm";
|
|
7
|
+
readonly lg: "text-base";
|
|
8
|
+
readonly xl: "text-xl";
|
|
9
|
+
readonly "2xl": "text-2xl";
|
|
10
|
+
};
|
|
11
|
+
declare const weightClass: {
|
|
12
|
+
readonly normal: "font-normal";
|
|
13
|
+
readonly medium: "font-medium";
|
|
14
|
+
readonly semibold: "font-semibold";
|
|
15
|
+
};
|
|
16
|
+
export interface TextProps extends Omit<React.ComponentProps<"span">, "ref"> {
|
|
17
|
+
size?: keyof typeof sizeClass;
|
|
18
|
+
tone?: TextTone;
|
|
19
|
+
weight?: keyof typeof weightClass;
|
|
20
|
+
mono?: boolean;
|
|
21
|
+
/** One line with an ellipsis. */
|
|
22
|
+
truncate?: boolean;
|
|
23
|
+
/** Clamp to this many lines. */
|
|
24
|
+
lines?: 1 | 2 | 3;
|
|
25
|
+
/** Fixed-width digits for numbers that update. */
|
|
26
|
+
tabular?: boolean;
|
|
27
|
+
as?: "span" | "p" | "div";
|
|
28
|
+
}
|
|
29
|
+
/** Text with props instead of classes: size, tone, weight, mono, truncate, lines. */
|
|
30
|
+
declare function Text({ size, tone, weight, mono, truncate, lines, tabular, as: As, className, ...props }: TextProps): React.JSX.Element;
|
|
31
|
+
export interface HeadingProps extends React.ComponentProps<"h3"> {
|
|
32
|
+
/** 3: a section title in a card. 4: a small uppercase label over a group. */
|
|
33
|
+
level?: 3 | 4;
|
|
34
|
+
}
|
|
35
|
+
/** Section heading inside a card or panel. */
|
|
36
|
+
declare function Heading({ level, className, ...props }: HeadingProps): React.JSX.Element;
|
|
37
|
+
export { Heading, Text };
|