harebear-ui 0.0.0-stage → 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/AGENTS.md +63 -0
- package/LICENSE +21 -0
- package/README.md +92 -2
- package/bin/harebear-ui.mjs +125 -0
- package/dist/components/admin/activity-feed.d.ts +15 -0
- package/dist/components/admin/activity-feed.js +50 -0
- package/dist/components/admin/app-shell.d.ts +47 -0
- package/dist/components/admin/app-shell.js +108 -0
- package/dist/components/admin/callout.d.ts +7 -0
- package/dist/components/admin/callout.js +18 -0
- package/dist/components/admin/key-value-grid.d.ts +11 -0
- package/dist/components/admin/key-value-grid.js +23 -0
- package/dist/components/admin/link-tile.d.ts +17 -0
- package/dist/components/admin/link-tile.js +36 -0
- package/dist/components/admin/page-header.d.ts +14 -0
- package/dist/components/admin/page-header.js +41 -0
- package/dist/components/admin/search-input.d.ts +12 -0
- package/dist/components/admin/search-input.js +32 -0
- package/dist/components/admin/stat-card.d.ts +20 -0
- package/dist/components/admin/stat-card.js +53 -0
- package/dist/components/admin/status.d.ts +12 -0
- package/dist/components/admin/status.js +32 -0
- package/dist/components/admin/timeline.d.ts +13 -0
- package/dist/components/admin/timeline.js +36 -0
- package/dist/components/admin/usage-meter.d.ts +17 -0
- package/dist/components/admin/usage-meter.js +43 -0
- package/dist/components/admin/user-chip.d.ts +14 -0
- package/dist/components/admin/user-chip.js +38 -0
- package/dist/components/ai/chat.d.ts +44 -0
- package/dist/components/ai/chat.js +147 -0
- package/dist/components/ai/code-block.d.ts +21 -0
- package/dist/components/ai/code-block.js +53 -0
- package/dist/components/ai/conversation-list.d.ts +18 -0
- package/dist/components/ai/conversation-list.js +30 -0
- package/dist/components/ai/model-picker.d.ts +21 -0
- package/dist/components/ai/model-picker.js +55 -0
- package/dist/components/ai/prompt-input.d.ts +27 -0
- package/dist/components/ai/prompt-input.js +99 -0
- package/dist/components/data/bar-list.d.ts +14 -0
- package/dist/components/data/bar-list.js +27 -0
- package/dist/components/data/data-table.d.ts +70 -0
- package/dist/components/data/data-table.js +208 -0
- package/dist/components/data/sparkline.d.ts +16 -0
- package/dist/components/data/sparkline.js +72 -0
- package/dist/components/data/status-bars.d.ts +17 -0
- package/dist/components/data/status-bars.js +37 -0
- package/dist/components/data/trend-chart.d.ts +31 -0
- package/dist/components/data/trend-chart.js +136 -0
- package/dist/components/theme/theme-provider.d.ts +42 -0
- package/dist/components/theme/theme-provider.js +110 -0
- package/dist/components/theme/theme-toggle.d.ts +5 -0
- package/dist/components/theme/theme-toggle.js +20 -0
- package/dist/components/ui/accordion.d.ts +7 -0
- package/dist/components/ui/accordion.js +53 -0
- package/dist/components/ui/alert-dialog.d.ts +18 -0
- package/dist/components/ui/alert-dialog.js +99 -0
- package/dist/components/ui/alert.d.ts +10 -0
- package/dist/components/ui/alert.js +42 -0
- package/dist/components/ui/aspect-ratio.d.ts +3 -0
- package/dist/components/ui/aspect-ratio.js +11 -0
- package/dist/components/ui/avatar.d.ts +11 -0
- package/dist/components/ui/avatar.js +49 -0
- package/dist/components/ui/badge.d.ts +9 -0
- package/dist/components/ui/badge.js +30 -0
- package/dist/components/ui/breadcrumb.d.ts +11 -0
- package/dist/components/ui/breadcrumb.js +70 -0
- package/dist/components/ui/button-group.d.ts +11 -0
- package/dist/components/ui/button-group.js +39 -0
- package/dist/components/ui/button.d.ts +11 -0
- package/dist/components/ui/button.js +55 -0
- package/dist/components/ui/calendar.d.ts +10 -0
- package/dist/components/ui/calendar.js +108 -0
- package/dist/components/ui/card.d.ts +11 -0
- package/dist/components/ui/card.js +55 -0
- package/dist/components/ui/carousel.d.ts +28 -0
- package/dist/components/ui/carousel.js +131 -0
- package/dist/components/ui/chart.d.ts +44 -0
- package/dist/components/ui/chart.js +148 -0
- package/dist/components/ui/checkbox.d.ts +4 -0
- package/dist/components/ui/checkbox.js +19 -0
- package/dist/components/ui/collapsible.d.ts +5 -0
- package/dist/components/ui/collapsible.js +23 -0
- package/dist/components/ui/command.d.ts +18 -0
- package/dist/components/ui/command.js +86 -0
- package/dist/components/ui/context-menu.d.ts +31 -0
- package/dist/components/ui/context-menu.js +123 -0
- package/dist/components/ui/dialog.d.ts +17 -0
- package/dist/components/ui/dialog.js +94 -0
- package/dist/components/ui/drawer.d.ts +13 -0
- package/dist/components/ui/drawer.js +76 -0
- package/dist/components/ui/dropdown-menu.d.ts +29 -0
- package/dist/components/ui/dropdown-menu.js +126 -0
- package/dist/components/ui/empty.d.ts +11 -0
- package/dist/components/ui/empty.js +59 -0
- package/dist/components/ui/field.d.ts +24 -0
- package/dist/components/ui/field.js +109 -0
- package/dist/components/ui/hover-card.d.ts +6 -0
- package/dist/components/ui/hover-card.js +30 -0
- package/dist/components/ui/input-group.d.ts +16 -0
- package/dist/components/ui/input-group.js +77 -0
- package/dist/components/ui/input-otp.d.ts +11 -0
- package/dist/components/ui/input-otp.js +46 -0
- package/dist/components/ui/input.d.ts +3 -0
- package/dist/components/ui/input.js +13 -0
- package/dist/components/ui/item.d.ts +23 -0
- package/dist/components/ui/item.js +117 -0
- package/dist/components/ui/kbd.d.ts +3 -0
- package/dist/components/ui/kbd.js +19 -0
- package/dist/components/ui/label.d.ts +4 -0
- package/dist/components/ui/label.js +13 -0
- package/dist/components/ui/menubar.d.ts +30 -0
- package/dist/components/ui/menubar.js +133 -0
- package/dist/components/ui/navigation-menu.d.ts +14 -0
- package/dist/components/ui/navigation-menu.js +79 -0
- package/dist/components/ui/pagination.d.ts +17 -0
- package/dist/components/ui/pagination.js +79 -0
- package/dist/components/ui/popover.d.ts +10 -0
- package/dist/components/ui/popover.js +54 -0
- package/dist/components/ui/progress.d.ts +4 -0
- package/dist/components/ui/progress.js +18 -0
- package/dist/components/ui/radio-group.d.ts +5 -0
- package/dist/components/ui/radio-group.js +25 -0
- package/dist/components/ui/resizable.d.ts +7 -0
- package/dist/components/ui/resizable.js +27 -0
- package/dist/components/ui/scroll-area.d.ts +5 -0
- package/dist/components/ui/scroll-area.js +35 -0
- package/dist/components/ui/select.d.ts +15 -0
- package/dist/components/ui/select.js +98 -0
- package/dist/components/ui/separator.d.ts +4 -0
- package/dist/components/ui/separator.js +15 -0
- package/dist/components/ui/sheet.d.ts +14 -0
- package/dist/components/ui/sheet.js +88 -0
- package/dist/components/ui/sidebar.d.ts +69 -0
- package/dist/components/ui/sidebar.js +377 -0
- package/dist/components/ui/skeleton.d.ts +2 -0
- package/dist/components/ui/skeleton.js +12 -0
- package/dist/components/ui/slider.d.ts +4 -0
- package/dist/components/ui/slider.js +35 -0
- package/dist/components/ui/sonner.d.ts +3 -0
- package/dist/components/ui/sonner.js +29 -0
- package/dist/components/ui/spinner.d.ts +2 -0
- package/dist/components/ui/spinner.js +15 -0
- package/dist/components/ui/switch.d.ts +6 -0
- package/dist/components/ui/switch.js +18 -0
- package/dist/components/ui/table.d.ts +10 -0
- package/dist/components/ui/table.js +65 -0
- package/dist/components/ui/tabs.d.ts +11 -0
- package/dist/components/ui/tabs.js +45 -0
- package/dist/components/ui/textarea.d.ts +3 -0
- package/dist/components/ui/textarea.js +12 -0
- package/dist/components/ui/toggle-group.d.ts +10 -0
- package/dist/components/ui/toggle-group.js +50 -0
- package/dist/components/ui/toggle.d.ts +9 -0
- package/dist/components/ui/toggle.js +35 -0
- package/dist/components/ui/tooltip.d.ts +7 -0
- package/dist/components/ui/tooltip.js +34 -0
- package/dist/hooks/use-copy-to-clipboard.d.ts +5 -0
- package/dist/hooks/use-copy-to-clipboard.js +24 -0
- package/dist/hooks/use-mobile.d.ts +1 -0
- package/dist/hooks/use-mobile.js +17 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +80 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.js +8 -0
- package/dist/shadcn-base.css +638 -0
- package/dist/styles.css +188 -0
- package/docs/COMPONENTS.md +352 -0
- package/llms.txt +34 -0
- package/package.json +116 -4
- package/skills/harebear-ui/SKILL.md +33 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface Conversation {
|
|
3
|
+
id: string;
|
|
4
|
+
title: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ConversationGroup {
|
|
7
|
+
/** e.g. "Today", "Yesterday", "Previous 7 days". */
|
|
8
|
+
label: string;
|
|
9
|
+
items: Conversation[];
|
|
10
|
+
}
|
|
11
|
+
export interface ConversationListProps extends Omit<React.ComponentProps<"nav">, "onSelect"> {
|
|
12
|
+
groups: ConversationGroup[];
|
|
13
|
+
activeId?: string;
|
|
14
|
+
onSelect?: (id: string) => void;
|
|
15
|
+
}
|
|
16
|
+
/** Grouped list of chat threads for a sidebar. */
|
|
17
|
+
declare function ConversationList({ groups, activeId, onSelect, className, ...props }: ConversationListProps): React.JSX.Element;
|
|
18
|
+
export { ConversationList };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../../lib/utils.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
// src/components/ai/conversation-list.tsx
|
|
5
|
+
/** Grouped list of chat threads for a sidebar. */
|
|
6
|
+
function ConversationList({ groups, activeId, onSelect, className, ...props }) {
|
|
7
|
+
return /* @__PURE__ */ jsx("nav", {
|
|
8
|
+
"data-slot": "conversation-list",
|
|
9
|
+
"aria-label": "Conversations",
|
|
10
|
+
className: cn("flex flex-col gap-4.5", className),
|
|
11
|
+
...props,
|
|
12
|
+
children: groups.map((g) => /* @__PURE__ */ jsxs("div", {
|
|
13
|
+
className: "flex flex-col gap-0.5",
|
|
14
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
15
|
+
className: "px-3 pb-1.5 text-xs font-medium text-muted-foreground",
|
|
16
|
+
children: g.label
|
|
17
|
+
}), g.items.map((c) => {
|
|
18
|
+
const active = c.id === activeId;
|
|
19
|
+
return /* @__PURE__ */ jsx("button", {
|
|
20
|
+
type: "button",
|
|
21
|
+
"aria-current": active ? "page" : void 0,
|
|
22
|
+
onClick: () => onSelect?.(c.id),
|
|
23
|
+
className: cn("w-full truncate rounded-md px-3 py-2.25 text-left font-medium text-muted-foreground transition-colors outline-none hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/40", active && "bg-card text-foreground shadow-card hover:bg-card"),
|
|
24
|
+
children: c.title
|
|
25
|
+
}, c.id);
|
|
26
|
+
})]
|
|
27
|
+
}, g.label))
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export { ConversationList };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { StatusTone } from "../admin/status.js";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export interface ModelOption {
|
|
4
|
+
id: string;
|
|
5
|
+
/** Display name. Defaults to id. Rendered monospace. */
|
|
6
|
+
label?: string;
|
|
7
|
+
description?: React.ReactNode;
|
|
8
|
+
/** Availability dot. Default "success". */
|
|
9
|
+
status?: StatusTone;
|
|
10
|
+
}
|
|
11
|
+
export interface ModelPickerProps {
|
|
12
|
+
models: ModelOption[];
|
|
13
|
+
value: string;
|
|
14
|
+
onValueChange: (id: string) => void;
|
|
15
|
+
label?: React.ReactNode;
|
|
16
|
+
className?: string;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/** Pill button showing the active model with a status dot; opens a menu of models. */
|
|
20
|
+
declare function ModelPicker({ models, value, onValueChange, label, className, disabled }: ModelPickerProps): React.JSX.Element;
|
|
21
|
+
export { ModelPicker };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { StatusDot } from "../admin/status.js";
|
|
3
|
+
import { Button } from "../ui/button.js";
|
|
4
|
+
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "../ui/dropdown-menu.js";
|
|
5
|
+
import { cn } from "../../lib/utils.js";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { CheckIcon, ChevronDownIcon } from "lucide-react";
|
|
8
|
+
// src/components/ai/model-picker.tsx
|
|
9
|
+
/** Pill button showing the active model with a status dot; opens a menu of models. */
|
|
10
|
+
function ModelPicker({ models, value, onValueChange, label = "Model", className, disabled }) {
|
|
11
|
+
const current = models.find((m) => m.id === value);
|
|
12
|
+
return /* @__PURE__ */ jsxs(DropdownMenu, { children: [/* @__PURE__ */ jsx(DropdownMenuTrigger, {
|
|
13
|
+
asChild: true,
|
|
14
|
+
disabled,
|
|
15
|
+
children: /* @__PURE__ */ jsxs(Button, {
|
|
16
|
+
variant: "outline",
|
|
17
|
+
size: "sm",
|
|
18
|
+
shape: "pill",
|
|
19
|
+
className: cn("gap-2 px-3", className),
|
|
20
|
+
children: [
|
|
21
|
+
/* @__PURE__ */ jsx(StatusDot, { tone: current?.status ?? "success" }),
|
|
22
|
+
/* @__PURE__ */ jsx("span", {
|
|
23
|
+
className: "font-mono text-xs",
|
|
24
|
+
children: current?.label ?? value
|
|
25
|
+
}),
|
|
26
|
+
/* @__PURE__ */ jsx(ChevronDownIcon, { className: "text-muted-foreground" })
|
|
27
|
+
]
|
|
28
|
+
})
|
|
29
|
+
}), /* @__PURE__ */ jsxs(DropdownMenuContent, {
|
|
30
|
+
align: "end",
|
|
31
|
+
className: "min-w-56",
|
|
32
|
+
children: [/* @__PURE__ */ jsx(DropdownMenuLabel, { children: label }), models.map((m) => /* @__PURE__ */ jsxs(DropdownMenuItem, {
|
|
33
|
+
onSelect: () => onValueChange(m.id),
|
|
34
|
+
className: "items-start gap-2.5",
|
|
35
|
+
children: [
|
|
36
|
+
/* @__PURE__ */ jsx(StatusDot, {
|
|
37
|
+
tone: m.status ?? "success",
|
|
38
|
+
className: "mt-1.5"
|
|
39
|
+
}),
|
|
40
|
+
/* @__PURE__ */ jsxs("div", {
|
|
41
|
+
className: "flex min-w-0 flex-1 flex-col",
|
|
42
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
43
|
+
className: "font-mono text-xs",
|
|
44
|
+
children: m.label ?? m.id
|
|
45
|
+
}), m.description && /* @__PURE__ */ jsx("span", {
|
|
46
|
+
className: "text-xs text-muted-foreground",
|
|
47
|
+
children: m.description
|
|
48
|
+
})]
|
|
49
|
+
}),
|
|
50
|
+
m.id === value && /* @__PURE__ */ jsx(CheckIcon, { className: "mt-0.5" })
|
|
51
|
+
]
|
|
52
|
+
}, m.id))]
|
|
53
|
+
})] });
|
|
54
|
+
}
|
|
55
|
+
export { ModelPicker };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { Button } from "../ui/button.js";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
export interface PromptInputProps extends Omit<React.ComponentProps<"form">, "onSubmit" | "onChange"> {
|
|
4
|
+
value: string;
|
|
5
|
+
onValueChange: (value: string) => void;
|
|
6
|
+
/** Called with the trimmed text on Enter or the send button. Shift+Enter inserts a newline. */
|
|
7
|
+
onSubmit: (value: string) => void;
|
|
8
|
+
placeholder?: string;
|
|
9
|
+
/** While true, the send button becomes a stop button. */
|
|
10
|
+
loading?: boolean;
|
|
11
|
+
onStop?: () => void;
|
|
12
|
+
/** Shows a paperclip button. */
|
|
13
|
+
onAttach?: () => void;
|
|
14
|
+
/** Chips/toggles between the attach and send buttons, e.g. <PromptInputChip>. */
|
|
15
|
+
tools?: React.ReactNode;
|
|
16
|
+
/** Small text under the box, e.g. a disclaimer. */
|
|
17
|
+
footer?: React.ReactNode;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
autoFocus?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/** Chat composer: auto-growing textarea with attach, tool chips, and send/stop. */
|
|
22
|
+
declare function PromptInput({ value, onValueChange, onSubmit, placeholder, loading, onStop, onAttach, tools, footer, disabled, autoFocus, className, ...props }: PromptInputProps): React.JSX.Element;
|
|
23
|
+
/** Toggleable pill for the composer toolbar (web search, a knowledge source…). */
|
|
24
|
+
declare function PromptInputChip({ active, className, ...props }: React.ComponentProps<typeof Button> & {
|
|
25
|
+
active?: boolean;
|
|
26
|
+
}): React.JSX.Element;
|
|
27
|
+
export { PromptInput, PromptInputChip };
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Button } from "../ui/button.js";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import { ArrowUpIcon, PaperclipIcon, SquareIcon } from "lucide-react";
|
|
7
|
+
// src/components/ai/prompt-input.tsx
|
|
8
|
+
/** Chat composer: auto-growing textarea with attach, tool chips, and send/stop. */
|
|
9
|
+
function PromptInput({ value, onValueChange, onSubmit, placeholder = "Send a message…", loading, onStop, onAttach, tools, footer, disabled, autoFocus, className, ...props }) {
|
|
10
|
+
const id = React.useId();
|
|
11
|
+
const canSend = value.trim().length > 0 && !disabled;
|
|
12
|
+
const submit = () => {
|
|
13
|
+
if (canSend && !loading) onSubmit(value.trim());
|
|
14
|
+
};
|
|
15
|
+
return /* @__PURE__ */ jsxs("form", {
|
|
16
|
+
"data-slot": "prompt-input",
|
|
17
|
+
className: cn("mx-auto flex w-full max-w-190 flex-col gap-2.5", className),
|
|
18
|
+
onSubmit: (e) => {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
submit();
|
|
21
|
+
},
|
|
22
|
+
...props,
|
|
23
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
24
|
+
className: "flex flex-col gap-2 rounded-[calc(var(--radius-xl)*1.2)] bg-muted pt-3 pr-3 pb-2.5 pl-3.5 transition-shadow focus-within:ring-4 focus-within:ring-primary/12",
|
|
25
|
+
children: [
|
|
26
|
+
/* @__PURE__ */ jsx("label", {
|
|
27
|
+
htmlFor: id,
|
|
28
|
+
className: "sr-only",
|
|
29
|
+
children: "Message"
|
|
30
|
+
}),
|
|
31
|
+
/* @__PURE__ */ jsx("textarea", {
|
|
32
|
+
id,
|
|
33
|
+
rows: 2,
|
|
34
|
+
value,
|
|
35
|
+
disabled,
|
|
36
|
+
autoFocus,
|
|
37
|
+
placeholder,
|
|
38
|
+
onChange: (e) => onValueChange(e.target.value),
|
|
39
|
+
onKeyDown: (e) => {
|
|
40
|
+
if (e.key === "Enter" && !e.shiftKey && !e.nativeEvent.isComposing) {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
submit();
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
className: "field-sizing-content max-h-60 min-h-13 w-full resize-none border-0 bg-transparent px-1.5 py-1 text-[15px] leading-normal text-foreground outline-none placeholder:text-muted-foreground"
|
|
46
|
+
}),
|
|
47
|
+
/* @__PURE__ */ jsxs("div", {
|
|
48
|
+
className: "flex items-center gap-1.5",
|
|
49
|
+
children: [
|
|
50
|
+
onAttach && /* @__PURE__ */ jsx(Button, {
|
|
51
|
+
type: "button",
|
|
52
|
+
variant: "ghost",
|
|
53
|
+
size: "icon-sm",
|
|
54
|
+
shape: "pill",
|
|
55
|
+
"aria-label": "Attach file",
|
|
56
|
+
onClick: onAttach,
|
|
57
|
+
children: /* @__PURE__ */ jsx(PaperclipIcon, {})
|
|
58
|
+
}),
|
|
59
|
+
tools,
|
|
60
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1" }),
|
|
61
|
+
loading && onStop ? /* @__PURE__ */ jsx(Button, {
|
|
62
|
+
type: "button",
|
|
63
|
+
size: "icon",
|
|
64
|
+
shape: "pill",
|
|
65
|
+
className: "size-9",
|
|
66
|
+
"aria-label": "Stop generating",
|
|
67
|
+
onClick: onStop,
|
|
68
|
+
children: /* @__PURE__ */ jsx(SquareIcon, { className: "fill-current" })
|
|
69
|
+
}) : /* @__PURE__ */ jsx(Button, {
|
|
70
|
+
type: "submit",
|
|
71
|
+
size: "icon",
|
|
72
|
+
shape: "pill",
|
|
73
|
+
className: "size-9",
|
|
74
|
+
"aria-label": "Send message",
|
|
75
|
+
disabled: !canSend || loading,
|
|
76
|
+
children: /* @__PURE__ */ jsx(ArrowUpIcon, {})
|
|
77
|
+
})
|
|
78
|
+
]
|
|
79
|
+
})
|
|
80
|
+
]
|
|
81
|
+
}), footer && /* @__PURE__ */ jsx("p", {
|
|
82
|
+
className: "text-center text-xs text-muted-foreground",
|
|
83
|
+
children: footer
|
|
84
|
+
})]
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
/** Toggleable pill for the composer toolbar (web search, a knowledge source…). */
|
|
88
|
+
function PromptInputChip({ active, className, ...props }) {
|
|
89
|
+
return /* @__PURE__ */ jsx(Button, {
|
|
90
|
+
type: "button",
|
|
91
|
+
size: "sm",
|
|
92
|
+
shape: "pill",
|
|
93
|
+
variant: active ? "soft" : "outline",
|
|
94
|
+
"aria-pressed": active,
|
|
95
|
+
className: cn("h-8", className),
|
|
96
|
+
...props
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
export { PromptInput, PromptInputChip };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
export interface BarListItem {
|
|
3
|
+
label: React.ReactNode;
|
|
4
|
+
/** Share 0-100 used for the bar width and the bold percent. */
|
|
5
|
+
percent: number;
|
|
6
|
+
/** Optional absolute value shown muted before the percent, e.g. "32,068". */
|
|
7
|
+
value?: React.ReactNode;
|
|
8
|
+
color?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Ranked list of labeled bars, e.g. traffic sources or top pages. */
|
|
11
|
+
declare function BarList({ items, className, ...props }: Omit<React.ComponentProps<"div">, "children"> & {
|
|
12
|
+
items: BarListItem[];
|
|
13
|
+
}): React.JSX.Element;
|
|
14
|
+
export { BarList };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { UsageMeter } from "../admin/usage-meter.js";
|
|
3
|
+
import { cn } from "../../lib/utils.js";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
// src/components/data/bar-list.tsx
|
|
6
|
+
/** Ranked list of labeled bars, e.g. traffic sources or top pages. */
|
|
7
|
+
function BarList({ items, className, ...props }) {
|
|
8
|
+
return /* @__PURE__ */ jsx("div", {
|
|
9
|
+
"data-slot": "bar-list",
|
|
10
|
+
className: cn("flex flex-col gap-4.5", className),
|
|
11
|
+
...props,
|
|
12
|
+
children: items.map((item, i) => /* @__PURE__ */ jsx(UsageMeter, {
|
|
13
|
+
label: item.label,
|
|
14
|
+
percent: item.percent,
|
|
15
|
+
color: item.color,
|
|
16
|
+
value: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
17
|
+
item.value !== void 0 && /* @__PURE__ */ jsxs("span", {
|
|
18
|
+
className: "font-normal text-muted-foreground",
|
|
19
|
+
children: [item.value, " · "]
|
|
20
|
+
}),
|
|
21
|
+
item.percent,
|
|
22
|
+
"%"
|
|
23
|
+
] })
|
|
24
|
+
}, i))
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export { BarList };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { ColumnDef, RowData } from "@tanstack/react-table";
|
|
3
|
+
/** Feature set used by DataTable. Use with createDataTableColumnHelper for typed columns. */
|
|
4
|
+
export declare const dataTableFeatures: {
|
|
5
|
+
columnFilteringFeature: import("@tanstack/react-table").TableFeature;
|
|
6
|
+
globalFilteringFeature: import("@tanstack/react-table").TableFeature;
|
|
7
|
+
rowSortingFeature: import("@tanstack/react-table").TableFeature;
|
|
8
|
+
rowSelectionFeature: import("@tanstack/react-table").TableFeature;
|
|
9
|
+
rowPaginationFeature: import("@tanstack/react-table").TableFeature;
|
|
10
|
+
filteredRowModel: (table: import("@tanstack/react-table").Table<any, any>) => () => import("@tanstack/react-table").RowModel<any, any>;
|
|
11
|
+
sortedRowModel: (table: import("@tanstack/react-table").Table<any, any>) => () => import("@tanstack/react-table").RowModel<any, any>;
|
|
12
|
+
paginatedRowModel: (table: import("@tanstack/react-table").Table<any, any>) => () => import("@tanstack/react-table").RowModel<any, any>;
|
|
13
|
+
filterFns: {
|
|
14
|
+
includesString: import("@tanstack/react-table").CreatedFilterFn<any, any>;
|
|
15
|
+
};
|
|
16
|
+
sortFns: {
|
|
17
|
+
alphanumeric: import("@tanstack/react-table").CreatedSortFn<any, any>;
|
|
18
|
+
text: import("@tanstack/react-table").CreatedSortFn<any, any>;
|
|
19
|
+
basic: import("@tanstack/react-table").CreatedSortFn<any, any>;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
export type DataTableFeatures = typeof dataTableFeatures;
|
|
23
|
+
export type DataTableColumnDef<TData extends RowData> = ColumnDef<DataTableFeatures, TData, any>;
|
|
24
|
+
/** Typed column helper for DataTable: `const col = createDataTableColumnHelper<Customer>()`. */
|
|
25
|
+
export declare function createDataTableColumnHelper<TData extends RowData>(): import("@tanstack/react-table").ColumnHelper<{
|
|
26
|
+
columnFilteringFeature: import("@tanstack/react-table").TableFeature;
|
|
27
|
+
globalFilteringFeature: import("@tanstack/react-table").TableFeature;
|
|
28
|
+
rowSortingFeature: import("@tanstack/react-table").TableFeature;
|
|
29
|
+
rowSelectionFeature: import("@tanstack/react-table").TableFeature;
|
|
30
|
+
rowPaginationFeature: import("@tanstack/react-table").TableFeature;
|
|
31
|
+
filteredRowModel: (table: import("@tanstack/react-table").Table<any, any>) => () => import("@tanstack/react-table").RowModel<any, any>;
|
|
32
|
+
sortedRowModel: (table: import("@tanstack/react-table").Table<any, any>) => () => import("@tanstack/react-table").RowModel<any, any>;
|
|
33
|
+
paginatedRowModel: (table: import("@tanstack/react-table").Table<any, any>) => () => import("@tanstack/react-table").RowModel<any, any>;
|
|
34
|
+
filterFns: {
|
|
35
|
+
includesString: import("@tanstack/react-table").CreatedFilterFn<any, any>;
|
|
36
|
+
};
|
|
37
|
+
sortFns: {
|
|
38
|
+
alphanumeric: import("@tanstack/react-table").CreatedSortFn<any, any>;
|
|
39
|
+
text: import("@tanstack/react-table").CreatedSortFn<any, any>;
|
|
40
|
+
basic: import("@tanstack/react-table").CreatedSortFn<any, any>;
|
|
41
|
+
};
|
|
42
|
+
}, TData>;
|
|
43
|
+
export interface DataTableProps<TData extends RowData> {
|
|
44
|
+
columns: DataTableColumnDef<TData>[];
|
|
45
|
+
data: TData[];
|
|
46
|
+
/** Stable row id. Defaults to the row index. Needed for selection and activeRowId. */
|
|
47
|
+
getRowId?: (row: TData, index: number) => string;
|
|
48
|
+
/** Show a search box that filters across all string/number columns. */
|
|
49
|
+
searchable?: boolean;
|
|
50
|
+
searchPlaceholder?: string;
|
|
51
|
+
/** Extra controls rendered after the search box (filters, sort, actions). */
|
|
52
|
+
toolbar?: React.ReactNode;
|
|
53
|
+
/** Adds a checkbox column. */
|
|
54
|
+
selectable?: boolean;
|
|
55
|
+
onSelectionChange?: (rows: TData[]) => void;
|
|
56
|
+
onRowClick?: (row: TData) => void;
|
|
57
|
+
/** Highlights this row (e.g. the one open in a detail panel). */
|
|
58
|
+
activeRowId?: string;
|
|
59
|
+
/** Rows per page. 0 disables pagination. Default 10. */
|
|
60
|
+
pageSize?: number;
|
|
61
|
+
/** Shown when there are no rows. */
|
|
62
|
+
emptyMessage?: React.ReactNode;
|
|
63
|
+
className?: string;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Sortable, searchable, selectable, paginated table. Client-side processing via
|
|
67
|
+
* TanStack Table v9. For server-side data, compose Table primitives with useTable directly.
|
|
68
|
+
*/
|
|
69
|
+
declare function DataTable<TData extends RowData>({ columns: columnsProp, data, getRowId, searchable, searchPlaceholder, toolbar, selectable, onSelectionChange, onRowClick, activeRowId, pageSize, emptyMessage, className }: DataTableProps<TData>): React.JSX.Element;
|
|
70
|
+
export { DataTable };
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { SearchInput } from "../admin/search-input.js";
|
|
3
|
+
import { Button } from "../ui/button.js";
|
|
4
|
+
import { Checkbox } from "../ui/checkbox.js";
|
|
5
|
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../ui/table.js";
|
|
6
|
+
import { cn } from "../../lib/utils.js";
|
|
7
|
+
import * as React from "react";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { ArrowDownIcon, ArrowUpDownIcon, ArrowUpIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
|
10
|
+
import { columnFilteringFeature, createColumnHelper, createFilteredRowModel, createPaginatedRowModel, createSortedRowModel, filterFn_includesString, globalFilteringFeature, rowPaginationFeature, rowSelectionFeature, rowSortingFeature, sortFn_alphanumeric, sortFn_basic, sortFn_text, tableFeatures, useTable } from "@tanstack/react-table";
|
|
11
|
+
// src/components/data/data-table.tsx
|
|
12
|
+
/** Feature set used by DataTable. Use with createDataTableColumnHelper for typed columns. */
|
|
13
|
+
const dataTableFeatures = tableFeatures({
|
|
14
|
+
columnFilteringFeature,
|
|
15
|
+
globalFilteringFeature,
|
|
16
|
+
rowSortingFeature,
|
|
17
|
+
rowSelectionFeature,
|
|
18
|
+
rowPaginationFeature,
|
|
19
|
+
filteredRowModel: createFilteredRowModel(),
|
|
20
|
+
sortedRowModel: createSortedRowModel(),
|
|
21
|
+
paginatedRowModel: createPaginatedRowModel(),
|
|
22
|
+
filterFns: { includesString: filterFn_includesString },
|
|
23
|
+
sortFns: {
|
|
24
|
+
alphanumeric: sortFn_alphanumeric,
|
|
25
|
+
text: sortFn_text,
|
|
26
|
+
basic: sortFn_basic
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
/** Typed column helper for DataTable: `const col = createDataTableColumnHelper<Customer>()`. */
|
|
30
|
+
function createDataTableColumnHelper() {
|
|
31
|
+
return createColumnHelper();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Sortable, searchable, selectable, paginated table. Client-side processing via
|
|
35
|
+
* TanStack Table v9. For server-side data, compose Table primitives with useTable directly.
|
|
36
|
+
*/
|
|
37
|
+
function DataTable({ columns: columnsProp, data, getRowId, searchable, searchPlaceholder = "Search", toolbar, selectable, onSelectionChange, onRowClick, activeRowId, pageSize = 10, emptyMessage = "No results.", className }) {
|
|
38
|
+
const columns = React.useMemo(() => {
|
|
39
|
+
if (!selectable) return columnsProp;
|
|
40
|
+
return [{
|
|
41
|
+
id: "__select",
|
|
42
|
+
enableSorting: false,
|
|
43
|
+
enableGlobalFilter: false,
|
|
44
|
+
header: ({ table }) => /* @__PURE__ */ jsx(Checkbox, {
|
|
45
|
+
"aria-label": "Select all rows",
|
|
46
|
+
checked: table.getIsAllPageRowsSelected() ? true : table.getIsSomePageRowsSelected() ? "indeterminate" : false,
|
|
47
|
+
onCheckedChange: (v) => table.toggleAllPageRowsSelected(!!v)
|
|
48
|
+
}),
|
|
49
|
+
cell: ({ row }) => /* @__PURE__ */ jsx(Checkbox, {
|
|
50
|
+
"aria-label": "Select row",
|
|
51
|
+
checked: row.getIsSelected(),
|
|
52
|
+
onClick: (e) => e.stopPropagation(),
|
|
53
|
+
onCheckedChange: (v) => row.toggleSelected(!!v)
|
|
54
|
+
})
|
|
55
|
+
}, ...columnsProp];
|
|
56
|
+
}, [columnsProp, selectable]);
|
|
57
|
+
const paginate = pageSize > 0;
|
|
58
|
+
const table = useTable({
|
|
59
|
+
features: dataTableFeatures,
|
|
60
|
+
columns,
|
|
61
|
+
data,
|
|
62
|
+
getRowId,
|
|
63
|
+
globalFilterFn: "includesString",
|
|
64
|
+
initialState: { pagination: {
|
|
65
|
+
pageIndex: 0,
|
|
66
|
+
pageSize: paginate ? pageSize : Number.MAX_SAFE_INTEGER
|
|
67
|
+
} }
|
|
68
|
+
});
|
|
69
|
+
const rowSelection = table.state.rowSelection;
|
|
70
|
+
const onSelectionChangeRef = React.useRef(onSelectionChange);
|
|
71
|
+
onSelectionChangeRef.current = onSelectionChange;
|
|
72
|
+
React.useEffect(() => {
|
|
73
|
+
onSelectionChangeRef.current?.(table.getSelectedRowModel().rows.map((r) => r.original));
|
|
74
|
+
}, [rowSelection]);
|
|
75
|
+
const filteredCount = table.getFilteredRowModel().rows.length;
|
|
76
|
+
const { pageIndex, pageSize: size } = table.state.pagination;
|
|
77
|
+
const pageCount = table.getPageCount();
|
|
78
|
+
const rows = table.getRowModel().rows;
|
|
79
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
80
|
+
"data-slot": "data-table",
|
|
81
|
+
className: cn("flex min-w-0 flex-col gap-4", className),
|
|
82
|
+
children: [
|
|
83
|
+
(searchable || toolbar) && /* @__PURE__ */ jsxs("div", {
|
|
84
|
+
className: "flex flex-wrap items-center gap-2.5",
|
|
85
|
+
children: [searchable && /* @__PURE__ */ jsx(SearchInput, {
|
|
86
|
+
wrapperClassName: "min-w-50 flex-[0_1_300px]",
|
|
87
|
+
placeholder: searchPlaceholder,
|
|
88
|
+
value: String(table.state.globalFilter ?? ""),
|
|
89
|
+
onChange: (e) => table.setGlobalFilter(e.target.value)
|
|
90
|
+
}), toolbar]
|
|
91
|
+
}),
|
|
92
|
+
/* @__PURE__ */ jsxs(Table, {
|
|
93
|
+
className: "border-spacing-y-0.5",
|
|
94
|
+
children: [/* @__PURE__ */ jsx(TableHeader, { children: table.getHeaderGroups().map((group) => /* @__PURE__ */ jsx(TableRow, { children: group.headers.map((header) => {
|
|
95
|
+
const canSort = header.column.getCanSort();
|
|
96
|
+
const sorted = header.column.getIsSorted();
|
|
97
|
+
const SortIcon = sorted === "asc" ? ArrowUpIcon : sorted === "desc" ? ArrowDownIcon : ArrowUpDownIcon;
|
|
98
|
+
const meta = header.column.columnDef.meta;
|
|
99
|
+
return /* @__PURE__ */ jsx(TableHead, {
|
|
100
|
+
className: cn(header.column.id === "__select" && "w-11", meta?.align === "right" && "text-right"),
|
|
101
|
+
"aria-sort": sorted === "asc" ? "ascending" : sorted === "desc" ? "descending" : void 0,
|
|
102
|
+
children: header.isPlaceholder ? null : canSort ? /* @__PURE__ */ jsxs("button", {
|
|
103
|
+
type: "button",
|
|
104
|
+
onClick: header.column.getToggleSortingHandler(),
|
|
105
|
+
className: cn("-mx-1 inline-flex items-center gap-1 rounded-sm px-1 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:outline-none", sorted && "text-foreground"),
|
|
106
|
+
children: [/* @__PURE__ */ jsx(table.FlexRender, { header }), /* @__PURE__ */ jsx(SortIcon, { className: cn("size-3.5", !sorted && "opacity-40") })]
|
|
107
|
+
}) : /* @__PURE__ */ jsx(table.FlexRender, { header })
|
|
108
|
+
}, header.id);
|
|
109
|
+
}) }, group.id)) }), /* @__PURE__ */ jsx(TableBody, { children: rows.length === 0 ? /* @__PURE__ */ jsx(TableRow, {
|
|
110
|
+
className: "hover:bg-transparent",
|
|
111
|
+
children: /* @__PURE__ */ jsx(TableCell, {
|
|
112
|
+
colSpan: columns.length,
|
|
113
|
+
className: "h-24 text-center text-muted-foreground",
|
|
114
|
+
children: emptyMessage
|
|
115
|
+
})
|
|
116
|
+
}) : rows.map((row) => /* @__PURE__ */ jsx(TableRow, {
|
|
117
|
+
"data-state": row.getIsSelected() || row.id === activeRowId ? "selected" : void 0,
|
|
118
|
+
onClick: onRowClick ? () => onRowClick(row.original) : void 0,
|
|
119
|
+
className: cn(onRowClick && "cursor-pointer"),
|
|
120
|
+
children: row.getAllCells().map((cell) => {
|
|
121
|
+
const meta = cell.column.columnDef.meta;
|
|
122
|
+
return /* @__PURE__ */ jsx(TableCell, {
|
|
123
|
+
className: cn("py-3", meta?.align === "right" && "text-right"),
|
|
124
|
+
children: /* @__PURE__ */ jsx(table.FlexRender, { cell })
|
|
125
|
+
}, cell.id);
|
|
126
|
+
})
|
|
127
|
+
}, row.id)) })]
|
|
128
|
+
}),
|
|
129
|
+
paginate && filteredCount > 0 && /* @__PURE__ */ jsxs("div", {
|
|
130
|
+
className: "flex flex-wrap items-center justify-between gap-3 px-1.5",
|
|
131
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
132
|
+
className: "text-[13px] text-muted-foreground tabular-nums",
|
|
133
|
+
children: [
|
|
134
|
+
"Showing ",
|
|
135
|
+
Math.min(filteredCount, pageIndex * size + 1),
|
|
136
|
+
"-",
|
|
137
|
+
Math.min(filteredCount, (pageIndex + 1) * size),
|
|
138
|
+
" of",
|
|
139
|
+
" ",
|
|
140
|
+
filteredCount
|
|
141
|
+
]
|
|
142
|
+
}), pageCount > 1 && /* @__PURE__ */ jsxs("nav", {
|
|
143
|
+
"aria-label": "Pagination",
|
|
144
|
+
className: "flex items-center gap-1",
|
|
145
|
+
children: [
|
|
146
|
+
/* @__PURE__ */ jsx(Button, {
|
|
147
|
+
variant: "ghost",
|
|
148
|
+
size: "icon-sm",
|
|
149
|
+
"aria-label": "Previous page",
|
|
150
|
+
disabled: !table.getCanPreviousPage(),
|
|
151
|
+
onClick: () => table.previousPage(),
|
|
152
|
+
children: /* @__PURE__ */ jsx(ChevronLeftIcon, {})
|
|
153
|
+
}),
|
|
154
|
+
pageList(pageIndex, pageCount).map((p, i) => p === "…" ? /* @__PURE__ */ jsx("span", {
|
|
155
|
+
className: "px-1 text-muted-foreground",
|
|
156
|
+
children: "…"
|
|
157
|
+
}, `gap-${i}`) : /* @__PURE__ */ jsx(Button, {
|
|
158
|
+
variant: p === pageIndex ? "secondary" : "ghost",
|
|
159
|
+
size: "icon-sm",
|
|
160
|
+
className: "tabular-nums",
|
|
161
|
+
"aria-current": p === pageIndex ? "page" : void 0,
|
|
162
|
+
onClick: () => table.setPageIndex(p),
|
|
163
|
+
children: p + 1
|
|
164
|
+
}, p)),
|
|
165
|
+
/* @__PURE__ */ jsx(Button, {
|
|
166
|
+
variant: "ghost",
|
|
167
|
+
size: "icon-sm",
|
|
168
|
+
"aria-label": "Next page",
|
|
169
|
+
disabled: !table.getCanNextPage(),
|
|
170
|
+
onClick: () => table.nextPage(),
|
|
171
|
+
children: /* @__PURE__ */ jsx(ChevronRightIcon, {})
|
|
172
|
+
})
|
|
173
|
+
]
|
|
174
|
+
})]
|
|
175
|
+
})
|
|
176
|
+
]
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/** Zero-based page indexes with ellipses: 0 1 2 … 27 */
|
|
180
|
+
function pageList(current, count) {
|
|
181
|
+
if (count <= 7) return Array.from({ length: count }, (_, i) => i);
|
|
182
|
+
const set = new Set([
|
|
183
|
+
0,
|
|
184
|
+
count - 1,
|
|
185
|
+
current - 1,
|
|
186
|
+
current,
|
|
187
|
+
current + 1
|
|
188
|
+
].filter((p) => p >= 0 && p < count));
|
|
189
|
+
if (current <= 2) for (const p of [
|
|
190
|
+
1,
|
|
191
|
+
2,
|
|
192
|
+
3
|
|
193
|
+
]) set.add(p);
|
|
194
|
+
if (current >= count - 3) for (const p of [
|
|
195
|
+
count - 4,
|
|
196
|
+
count - 3,
|
|
197
|
+
count - 2
|
|
198
|
+
]) set.add(p);
|
|
199
|
+
const sorted = [...set].sort((a, b) => a - b);
|
|
200
|
+
const out = [];
|
|
201
|
+
sorted.forEach((p, i) => {
|
|
202
|
+
const prev = sorted[i - 1];
|
|
203
|
+
if (i > 0 && prev !== void 0 && p - prev > 1) out.push("…");
|
|
204
|
+
out.push(p);
|
|
205
|
+
});
|
|
206
|
+
return out;
|
|
207
|
+
}
|
|
208
|
+
export { DataTable, createDataTableColumnHelper, dataTableFeatures };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
/** Catmull-Rom smoothed SVG path through points. */
|
|
3
|
+
export declare function smoothPath(points: ReadonlyArray<readonly [number, number]>): string;
|
|
4
|
+
export interface SparklineProps extends Omit<React.ComponentProps<"svg">, "children" | "fill"> {
|
|
5
|
+
data: number[];
|
|
6
|
+
/** Stroke/fill color. Any CSS color; default the primary token. */
|
|
7
|
+
color?: string;
|
|
8
|
+
/** Show the soft area fill under the line. Default true. */
|
|
9
|
+
area?: boolean;
|
|
10
|
+
/** Fixed y-range; defaults to the data's min/max. */
|
|
11
|
+
min?: number;
|
|
12
|
+
max?: number;
|
|
13
|
+
}
|
|
14
|
+
/** Tiny smoothed trend line. Stretches to its container width; set height via className. */
|
|
15
|
+
declare function Sparkline({ data, color, area: showArea, min, max, className, ...props }: SparklineProps): React.JSX.Element;
|
|
16
|
+
export { Sparkline };
|
|
@@ -0,0 +1,72 @@
|
|
|
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/data/sparkline.tsx
|
|
6
|
+
/** Catmull-Rom smoothed SVG path through points. */
|
|
7
|
+
function smoothPath(points) {
|
|
8
|
+
if (points.length === 0) return "";
|
|
9
|
+
const f = (n) => n.toFixed(1);
|
|
10
|
+
const first = points[0];
|
|
11
|
+
let d = `M${f(first[0])},${f(first[1])}`;
|
|
12
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
13
|
+
const p0 = points[i - 1] ?? points[i];
|
|
14
|
+
const p1 = points[i];
|
|
15
|
+
const p2 = points[i + 1];
|
|
16
|
+
const p3 = points[i + 2] ?? p2;
|
|
17
|
+
const c1x = p1[0] + (p2[0] - p0[0]) / 6;
|
|
18
|
+
const c1y = p1[1] + (p2[1] - p0[1]) / 6;
|
|
19
|
+
const c2x = p2[0] - (p3[0] - p1[0]) / 6;
|
|
20
|
+
const c2y = p2[1] - (p3[1] - p1[1]) / 6;
|
|
21
|
+
d += ` C${f(c1x)},${f(c1y)} ${f(c2x)},${f(c2y)} ${f(p2[0])},${f(p2[1])}`;
|
|
22
|
+
}
|
|
23
|
+
return d;
|
|
24
|
+
}
|
|
25
|
+
/** Tiny smoothed trend line. Stretches to its container width; set height via className. */
|
|
26
|
+
function Sparkline({ data, color = "var(--primary)", area: showArea = true, min, max, className, ...props }) {
|
|
27
|
+
const W = 120;
|
|
28
|
+
const H = 36;
|
|
29
|
+
const paths = React.useMemo(() => {
|
|
30
|
+
if (data.length < 2) return {
|
|
31
|
+
line: "",
|
|
32
|
+
fill: ""
|
|
33
|
+
};
|
|
34
|
+
const lo = min ?? Math.min(...data);
|
|
35
|
+
const hi = max ?? Math.max(...data);
|
|
36
|
+
const l = smoothPath(data.map((v, i) => [i / (data.length - 1) * W, 32 - (v - lo) / (hi - lo || 1) * 28]));
|
|
37
|
+
return {
|
|
38
|
+
line: l,
|
|
39
|
+
fill: `${l} L${W},${H} L0,${H} Z`
|
|
40
|
+
};
|
|
41
|
+
}, [
|
|
42
|
+
data,
|
|
43
|
+
min,
|
|
44
|
+
max
|
|
45
|
+
]);
|
|
46
|
+
return /* @__PURE__ */ jsxs("svg", {
|
|
47
|
+
"data-slot": "sparkline",
|
|
48
|
+
viewBox: `0 0 ${W} ${H}`,
|
|
49
|
+
preserveAspectRatio: "none",
|
|
50
|
+
"aria-hidden": "true",
|
|
51
|
+
className: cn("block h-9 w-full overflow-visible", className),
|
|
52
|
+
...props,
|
|
53
|
+
children: [showArea && /* @__PURE__ */ jsx("path", {
|
|
54
|
+
d: paths.fill,
|
|
55
|
+
style: {
|
|
56
|
+
fill: color,
|
|
57
|
+
fillOpacity: .08
|
|
58
|
+
}
|
|
59
|
+
}), /* @__PURE__ */ jsx("path", {
|
|
60
|
+
d: paths.line,
|
|
61
|
+
vectorEffect: "non-scaling-stroke",
|
|
62
|
+
style: {
|
|
63
|
+
fill: "none",
|
|
64
|
+
stroke: color,
|
|
65
|
+
strokeWidth: 2,
|
|
66
|
+
strokeLinecap: "round",
|
|
67
|
+
strokeLinejoin: "round"
|
|
68
|
+
}
|
|
69
|
+
})]
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
export { Sparkline, smoothPath };
|