@agent-native/toolkit 0.8.2 → 0.9.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.
Files changed (60) hide show
  1. package/README.md +30 -0
  2. package/agent-native.eject.json +33 -0
  3. package/dist/dashboard/DataTable.d.ts +20 -0
  4. package/dist/dashboard/DataTable.d.ts.map +1 -0
  5. package/dist/dashboard/DataTable.js +87 -0
  6. package/dist/dashboard/DataTable.js.map +1 -0
  7. package/dist/dashboard/DateRangePicker.d.ts +15 -0
  8. package/dist/dashboard/DateRangePicker.d.ts.map +1 -0
  9. package/dist/dashboard/DateRangePicker.js +15 -0
  10. package/dist/dashboard/DateRangePicker.js.map +1 -0
  11. package/dist/dashboard/GenericChartPanel.d.ts +57 -0
  12. package/dist/dashboard/GenericChartPanel.d.ts.map +1 -0
  13. package/dist/dashboard/GenericChartPanel.js +119 -0
  14. package/dist/dashboard/GenericChartPanel.js.map +1 -0
  15. package/dist/dashboard/GenericChartPanel.spec.d.ts +2 -0
  16. package/dist/dashboard/GenericChartPanel.spec.d.ts.map +1 -0
  17. package/dist/dashboard/GenericChartPanel.spec.js +41 -0
  18. package/dist/dashboard/GenericChartPanel.spec.js.map +1 -0
  19. package/dist/dashboard/MetricCard.d.ts +14 -0
  20. package/dist/dashboard/MetricCard.d.ts.map +1 -0
  21. package/dist/dashboard/MetricCard.js +10 -0
  22. package/dist/dashboard/MetricCard.js.map +1 -0
  23. package/dist/dashboard/StatsCard.d.ts +16 -0
  24. package/dist/dashboard/StatsCard.d.ts.map +1 -0
  25. package/dist/dashboard/StatsCard.js +8 -0
  26. package/dist/dashboard/StatsCard.js.map +1 -0
  27. package/dist/dashboard/dashboard-layout.d.ts +49 -0
  28. package/dist/dashboard/dashboard-layout.d.ts.map +1 -0
  29. package/dist/dashboard/dashboard-layout.js +231 -0
  30. package/dist/dashboard/dashboard-layout.js.map +1 -0
  31. package/dist/dashboard/dashboard-layout.spec.d.ts +2 -0
  32. package/dist/dashboard/dashboard-layout.spec.d.ts.map +1 -0
  33. package/dist/dashboard/dashboard-layout.spec.js +52 -0
  34. package/dist/dashboard/dashboard-layout.spec.js.map +1 -0
  35. package/dist/dashboard/index.d.ts +7 -0
  36. package/dist/dashboard/index.d.ts.map +1 -0
  37. package/dist/dashboard/index.js +7 -0
  38. package/dist/dashboard/index.js.map +1 -0
  39. package/dist/index.d.ts +1 -0
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -0
  42. package/dist/index.js.map +1 -1
  43. package/dist/ui/calendar.js +8 -8
  44. package/dist/ui/calendar.js.map +1 -1
  45. package/dist/ui/date-picker.d.ts.map +1 -1
  46. package/dist/ui/date-picker.js +1 -1
  47. package/dist/ui/date-picker.js.map +1 -1
  48. package/package.json +15 -1
  49. package/src/dashboard/DataTable.tsx +273 -0
  50. package/src/dashboard/DateRangePicker.tsx +57 -0
  51. package/src/dashboard/GenericChartPanel.spec.tsx +53 -0
  52. package/src/dashboard/GenericChartPanel.tsx +461 -0
  53. package/src/dashboard/MetricCard.tsx +54 -0
  54. package/src/dashboard/StatsCard.tsx +49 -0
  55. package/src/dashboard/dashboard-layout.spec.ts +79 -0
  56. package/src/dashboard/dashboard-layout.ts +329 -0
  57. package/src/dashboard/index.ts +6 -0
  58. package/src/index.ts +1 -0
  59. package/src/ui/calendar.tsx +8 -8
  60. package/src/ui/date-picker.tsx +4 -1
@@ -0,0 +1,273 @@
1
+ import {
2
+ IconArrowsUpDown,
3
+ IconChevronLeft,
4
+ IconChevronRight,
5
+ } from "@tabler/icons-react";
6
+ import { useMemo, useState } from "react";
7
+
8
+ import { Button } from "../ui/button.js";
9
+ import { Card, CardContent, CardHeader, CardTitle } from "../ui/card.js";
10
+ import {
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue,
16
+ } from "../ui/select.js";
17
+ import { Skeleton } from "../ui/skeleton.js";
18
+ import {
19
+ Table,
20
+ TableBody,
21
+ TableCell,
22
+ TableHead,
23
+ TableHeader,
24
+ TableRow,
25
+ } from "../ui/table.js";
26
+ import { cn } from "../utils.js";
27
+
28
+ const DEFAULT_PAGE_SIZE_OPTIONS = [10, 25, 50, 100];
29
+ const TABLE_MIN_HEIGHT_CLASS = "min-h-[386px]";
30
+
31
+ export interface DataTableLabels {
32
+ noData: string;
33
+ rowsPerPage: string;
34
+ of: string;
35
+ previousPage: string;
36
+ nextPage: string;
37
+ }
38
+
39
+ export interface DataTableProps {
40
+ title?: string;
41
+ data: Record<string, unknown>[];
42
+ columns?: string[];
43
+ loading?: boolean;
44
+ error?: string;
45
+ maxRows?: number;
46
+ pageSizeOptions?: number[];
47
+ labels?: Partial<DataTableLabels>;
48
+ }
49
+
50
+ const DEFAULT_LABELS: DataTableLabels = {
51
+ noData: "No data available.",
52
+ rowsPerPage: "Rows per page",
53
+ of: "of",
54
+ previousPage: "Previous page",
55
+ nextPage: "Next page",
56
+ };
57
+
58
+ /** A source-agnostic, sortable and paginated dashboard table. */
59
+ export function DataTable({
60
+ title,
61
+ data,
62
+ columns: columnsProp,
63
+ loading,
64
+ error,
65
+ maxRows,
66
+ pageSizeOptions = DEFAULT_PAGE_SIZE_OPTIONS,
67
+ labels: labelOverrides,
68
+ }: DataTableProps) {
69
+ const labels = { ...DEFAULT_LABELS, ...labelOverrides };
70
+ const [sortColumn, setSortColumn] = useState<string | null>(null);
71
+ const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
72
+ const [page, setPage] = useState(0);
73
+ const [pageSize, setPageSize] = useState(pageSizeOptions[0] ?? 10);
74
+
75
+ const columns = useMemo(() => {
76
+ if (columnsProp) return columnsProp;
77
+ return data.length === 0 ? [] : Object.keys(data[0]);
78
+ }, [columnsProp, data]);
79
+ const rows = useMemo(
80
+ () =>
81
+ maxRows != null && data.length > maxRows ? data.slice(0, maxRows) : data,
82
+ [data, maxRows],
83
+ );
84
+ const sortedRows = useMemo(() => {
85
+ if (!sortColumn) return rows;
86
+ return [...rows].sort((a, b) => {
87
+ const aValue = a[sortColumn];
88
+ const bValue = b[sortColumn];
89
+ if (aValue == null && bValue == null) return 0;
90
+ if (aValue == null) return 1;
91
+ if (bValue == null) return -1;
92
+ const comparison =
93
+ typeof aValue === "number" && typeof bValue === "number"
94
+ ? aValue - bValue
95
+ : String(aValue).localeCompare(String(bValue));
96
+ return sortDirection === "asc" ? comparison : -comparison;
97
+ });
98
+ }, [rows, sortColumn, sortDirection]);
99
+ const pageCount = Math.ceil(sortedRows.length / pageSize);
100
+ const pagedRows = sortedRows.slice(page * pageSize, (page + 1) * pageSize);
101
+
102
+ const handleSort = (column: string) => {
103
+ if (sortColumn === column) {
104
+ setSortDirection((direction) => (direction === "asc" ? "desc" : "asc"));
105
+ } else {
106
+ setSortColumn(column);
107
+ setSortDirection("desc");
108
+ }
109
+ setPage(0);
110
+ };
111
+
112
+ const content = loading ? (
113
+ <DataTableLoadingSkeleton />
114
+ ) : error ? (
115
+ <p className="py-4 text-sm text-destructive">{error}</p>
116
+ ) : data.length === 0 ? (
117
+ <p className="py-4 text-center text-sm text-muted-foreground">
118
+ {labels.noData}
119
+ </p>
120
+ ) : (
121
+ <div className={cn("overflow-x-auto", TABLE_MIN_HEIGHT_CLASS)}>
122
+ <Table>
123
+ <TableHeader>
124
+ <TableRow>
125
+ {columns.map((column) => (
126
+ <TableHead
127
+ key={column}
128
+ className="cursor-pointer select-none whitespace-nowrap hover:text-foreground"
129
+ onClick={() => handleSort(column)}
130
+ >
131
+ <span className="flex items-center gap-1">
132
+ {column}
133
+ <IconArrowsUpDown
134
+ className={cn(
135
+ "h-3 w-3",
136
+ sortColumn === column
137
+ ? "text-foreground"
138
+ : "text-muted-foreground/50",
139
+ )}
140
+ />
141
+ </span>
142
+ </TableHead>
143
+ ))}
144
+ </TableRow>
145
+ </TableHeader>
146
+ <TableBody>
147
+ {pagedRows.map((row, index) => (
148
+ <TableRow key={index}>
149
+ {columns.map((column) => (
150
+ <TableCell
151
+ key={column}
152
+ className="max-w-[300px] truncate whitespace-nowrap"
153
+ >
154
+ {formatValue(row[column])}
155
+ </TableCell>
156
+ ))}
157
+ </TableRow>
158
+ ))}
159
+ </TableBody>
160
+ </Table>
161
+ {sortedRows.length > (pageSizeOptions[0] ?? 10) && (
162
+ <div className="flex items-center justify-between border-t border-border px-2 py-2 text-xs text-muted-foreground">
163
+ <div className="flex items-center gap-2">
164
+ <span>{labels.rowsPerPage}</span>
165
+ <Select
166
+ value={String(pageSize)}
167
+ onValueChange={(value) => {
168
+ setPageSize(Number(value));
169
+ setPage(0);
170
+ }}
171
+ >
172
+ <SelectTrigger className="h-6 w-16 border-border px-2 py-0 text-xs">
173
+ <SelectValue />
174
+ </SelectTrigger>
175
+ <SelectContent>
176
+ {pageSizeOptions.map((size) => (
177
+ <SelectItem key={size} value={String(size)}>
178
+ {size}
179
+ </SelectItem>
180
+ ))}
181
+ </SelectContent>
182
+ </Select>
183
+ </div>
184
+ <div className="flex items-center gap-2">
185
+ <span>
186
+ {page * pageSize + 1}–
187
+ {Math.min((page + 1) * pageSize, sortedRows.length)} {labels.of}{" "}
188
+ {sortedRows.length}
189
+ </span>
190
+ <Button
191
+ aria-label={labels.previousPage}
192
+ variant="ghost"
193
+ size="icon"
194
+ className="h-6 w-6"
195
+ onClick={() => setPage((current) => current - 1)}
196
+ disabled={page === 0}
197
+ >
198
+ <IconChevronLeft className="h-3.5 w-3.5" />
199
+ </Button>
200
+ <Button
201
+ aria-label={labels.nextPage}
202
+ variant="ghost"
203
+ size="icon"
204
+ className="h-6 w-6"
205
+ onClick={() => setPage((current) => current + 1)}
206
+ disabled={page >= pageCount - 1}
207
+ >
208
+ <IconChevronRight className="h-3.5 w-3.5" />
209
+ </Button>
210
+ </div>
211
+ </div>
212
+ )}
213
+ </div>
214
+ );
215
+
216
+ if (!title) return content;
217
+ return (
218
+ <Card className="border-border/50 bg-card">
219
+ <CardHeader>
220
+ <CardTitle className="text-base">{title}</CardTitle>
221
+ </CardHeader>
222
+ <CardContent>{content}</CardContent>
223
+ </Card>
224
+ );
225
+ }
226
+
227
+ function formatValue(value: unknown): string {
228
+ if (value == null) return "-";
229
+ if (typeof value === "number")
230
+ return Number.isInteger(value) ? value.toLocaleString() : value.toFixed(4);
231
+ return typeof value === "object" ? JSON.stringify(value) : String(value);
232
+ }
233
+
234
+ function DataTableLoadingSkeleton() {
235
+ const columnWidths = ["w-24", "w-32", "w-20", "w-28"];
236
+ return (
237
+ <div className={cn("space-y-1", TABLE_MIN_HEIGHT_CLASS)}>
238
+ <div className="overflow-x-auto">
239
+ <div className="min-w-[480px]">
240
+ <div className="grid h-8 grid-cols-4 items-center border-b border-border px-2">
241
+ {columnWidths.map((width, index) => (
242
+ <Skeleton key={index} className={cn("h-3", width)} />
243
+ ))}
244
+ </div>
245
+ {Array.from({ length: 10 }).map((_, row) => (
246
+ <div
247
+ key={row}
248
+ className="grid h-8 grid-cols-4 items-center border-b border-border/50 px-2"
249
+ >
250
+ {columnWidths.map((width, column) => (
251
+ <Skeleton
252
+ key={column}
253
+ className={cn(
254
+ "h-3",
255
+ column === 0
256
+ ? "w-36"
257
+ : column === 2
258
+ ? "ml-auto w-16"
259
+ : width,
260
+ )}
261
+ />
262
+ ))}
263
+ </div>
264
+ ))}
265
+ </div>
266
+ </div>
267
+ <div className="flex h-8 items-center justify-between border-t border-border px-2 text-xs">
268
+ <Skeleton className="h-3 w-28" />
269
+ <Skeleton className="h-3 w-24" />
270
+ </div>
271
+ </div>
272
+ );
273
+ }
@@ -0,0 +1,57 @@
1
+ import { Button } from "../ui/button.js";
2
+ import { cn } from "../utils.js";
3
+
4
+ export type DateRange = "7d" | "30d" | "90d";
5
+
6
+ export interface DateRangeOption {
7
+ value: DateRange;
8
+ label: string;
9
+ }
10
+
11
+ export interface DateRangePickerProps {
12
+ value: DateRange;
13
+ onChange: (range: DateRange) => void;
14
+ options?: DateRangeOption[];
15
+ className?: string;
16
+ }
17
+
18
+ export const DEFAULT_DATE_RANGE_OPTIONS: DateRangeOption[] = [
19
+ { value: "7d", label: "7 days" },
20
+ { value: "30d", label: "30 days" },
21
+ { value: "90d", label: "90 days" },
22
+ ];
23
+
24
+ export function dateRangeToInterval(range: DateRange): number {
25
+ return Number.parseInt(range, 10);
26
+ }
27
+
28
+ export function DateRangePicker({
29
+ value,
30
+ onChange,
31
+ options = DEFAULT_DATE_RANGE_OPTIONS,
32
+ className,
33
+ }: DateRangePickerProps) {
34
+ return (
35
+ <div
36
+ className={cn(
37
+ "flex items-center gap-1 rounded-lg border border-border p-1",
38
+ className,
39
+ )}
40
+ >
41
+ {options.map((option) => (
42
+ <Button
43
+ key={option.value}
44
+ variant="ghost"
45
+ size="sm"
46
+ className={cn(
47
+ "h-7 px-3 text-xs",
48
+ value === option.value && "bg-secondary text-secondary-foreground",
49
+ )}
50
+ onClick={() => onChange(option.value)}
51
+ >
52
+ {option.label}
53
+ </Button>
54
+ ))}
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,53 @@
1
+ import { AreaChart, BarChart, LineChart, PieChart } from "recharts";
2
+ import { describe, expect, it } from "vitest";
3
+
4
+ import {
5
+ GenericChartRenderer,
6
+ formatGenericChartValue,
7
+ resolveGenericChartKeys,
8
+ } from "./GenericChartPanel.js";
9
+
10
+ const rows = [
11
+ { day: "Mon", revenue: 12, costs: 4 },
12
+ { day: "Tue", revenue: 18, costs: 7 },
13
+ ];
14
+
15
+ function render(
16
+ chartType: Parameters<typeof GenericChartRenderer>[0]["config"]["chartType"],
17
+ ) {
18
+ return GenericChartRenderer({ rows, config: { chartType } });
19
+ }
20
+
21
+ describe("GenericChartPanel renderers", () => {
22
+ it("derives portable x/y keys and formats provider values", () => {
23
+ expect(resolveGenericChartKeys(rows, {})).toEqual({
24
+ xKey: "day",
25
+ yKeys: ["revenue", "costs"],
26
+ });
27
+ expect(formatGenericChartValue("1200")).toBe("1,200");
28
+ });
29
+
30
+ it("renders KPI metrics from an inferred numeric value", () => {
31
+ const element = render("metric");
32
+ expect(element.props.children[0].props.children).toBe("30");
33
+ });
34
+
35
+ it.each([
36
+ ["line", LineChart],
37
+ ["area", AreaChart],
38
+ ["stacked-area", AreaChart],
39
+ ["bar", BarChart],
40
+ ["stacked-bar", BarChart],
41
+ ["pie", PieChart],
42
+ ["donut", PieChart],
43
+ ] as const)("uses Recharts for %s", (chartType, Component) => {
44
+ const element = render(chartType);
45
+ expect(element.props.children.type).toBe(Component);
46
+ });
47
+
48
+ it("uses the shared table renderer for tabular data", () => {
49
+ const element = render("table");
50
+ expect(element.type.name).toBe("DataTable");
51
+ expect(element.props.data).toBe(rows);
52
+ });
53
+ });