stagent 0.6.2 → 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.
Files changed (176) hide show
  1. package/README.md +21 -2
  2. package/dist/cli.js +272 -1
  3. package/docs/.coverage-gaps.json +66 -16
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/dashboard-kanban.md +13 -7
  6. package/docs/features/settings.md +15 -3
  7. package/docs/features/tables.md +122 -0
  8. package/docs/index.md +3 -2
  9. package/docs/journeys/developer.md +26 -16
  10. package/docs/journeys/personal-use.md +23 -9
  11. package/docs/journeys/power-user.md +40 -14
  12. package/docs/journeys/work-use.md +43 -15
  13. package/docs/manifest.json +27 -17
  14. package/package.json +3 -2
  15. package/src/app/api/chat/entities/search/route.ts +12 -3
  16. package/src/app/api/documents/[id]/route.ts +5 -1
  17. package/src/app/api/documents/[id]/versions/route.ts +53 -0
  18. package/src/app/api/documents/route.ts +5 -1
  19. package/src/app/api/projects/[id]/documents/route.ts +124 -0
  20. package/src/app/api/projects/[id]/route.ts +72 -3
  21. package/src/app/api/projects/__tests__/delete-project.test.ts +13 -0
  22. package/src/app/api/schedules/route.ts +19 -1
  23. package/src/app/api/snapshots/[id]/restore/route.ts +62 -0
  24. package/src/app/api/snapshots/[id]/route.ts +44 -0
  25. package/src/app/api/snapshots/route.ts +54 -0
  26. package/src/app/api/snapshots/settings/route.ts +67 -0
  27. package/src/app/api/tables/[id]/charts/[chartId]/route.ts +89 -0
  28. package/src/app/api/tables/[id]/charts/route.ts +72 -0
  29. package/src/app/api/tables/[id]/columns/route.ts +70 -0
  30. package/src/app/api/tables/[id]/export/route.ts +94 -0
  31. package/src/app/api/tables/[id]/history/route.ts +15 -0
  32. package/src/app/api/tables/[id]/import/route.ts +111 -0
  33. package/src/app/api/tables/[id]/route.ts +86 -0
  34. package/src/app/api/tables/[id]/rows/[rowId]/history/route.ts +32 -0
  35. package/src/app/api/tables/[id]/rows/[rowId]/route.ts +51 -0
  36. package/src/app/api/tables/[id]/rows/route.ts +101 -0
  37. package/src/app/api/tables/[id]/triggers/[triggerId]/route.ts +65 -0
  38. package/src/app/api/tables/[id]/triggers/route.ts +122 -0
  39. package/src/app/api/tables/route.ts +65 -0
  40. package/src/app/api/tables/templates/route.ts +92 -0
  41. package/src/app/api/tasks/[id]/route.ts +37 -2
  42. package/src/app/api/tasks/[id]/siblings/route.ts +48 -0
  43. package/src/app/api/tasks/route.ts +8 -9
  44. package/src/app/api/workflows/[id]/documents/route.ts +209 -0
  45. package/src/app/api/workflows/[id]/execute/route.ts +6 -2
  46. package/src/app/api/workflows/[id]/route.ts +16 -3
  47. package/src/app/api/workflows/[id]/status/route.ts +18 -2
  48. package/src/app/api/workflows/route.ts +13 -2
  49. package/src/app/documents/page.tsx +5 -1
  50. package/src/app/layout.tsx +0 -1
  51. package/src/app/manifest.ts +3 -3
  52. package/src/app/projects/[id]/page.tsx +62 -2
  53. package/src/app/settings/page.tsx +2 -0
  54. package/src/app/tables/[id]/page.tsx +67 -0
  55. package/src/app/tables/page.tsx +21 -0
  56. package/src/app/tables/templates/page.tsx +19 -0
  57. package/src/components/chat/chat-table-result.tsx +139 -0
  58. package/src/components/documents/document-browser.tsx +1 -1
  59. package/src/components/documents/document-chip-bar.tsx +17 -1
  60. package/src/components/documents/document-detail-view.tsx +51 -0
  61. package/src/components/documents/document-grid.tsx +5 -0
  62. package/src/components/documents/document-table.tsx +4 -0
  63. package/src/components/documents/types.ts +3 -0
  64. package/src/components/projects/project-form-sheet.tsx +109 -2
  65. package/src/components/schedules/schedule-form.tsx +91 -1
  66. package/src/components/settings/data-management-section.tsx +17 -12
  67. package/src/components/settings/database-snapshots-section.tsx +469 -0
  68. package/src/components/shared/app-sidebar.tsx +2 -0
  69. package/src/components/shared/document-picker-sheet.tsx +486 -0
  70. package/src/components/tables/table-browser.tsx +234 -0
  71. package/src/components/tables/table-cell-editor.tsx +226 -0
  72. package/src/components/tables/table-chart-builder.tsx +288 -0
  73. package/src/components/tables/table-chart-view.tsx +146 -0
  74. package/src/components/tables/table-column-header.tsx +103 -0
  75. package/src/components/tables/table-column-sheet.tsx +331 -0
  76. package/src/components/tables/table-create-sheet.tsx +240 -0
  77. package/src/components/tables/table-detail-sheet.tsx +144 -0
  78. package/src/components/tables/table-detail-tabs.tsx +278 -0
  79. package/src/components/tables/table-grid.tsx +61 -0
  80. package/src/components/tables/table-history-tab.tsx +148 -0
  81. package/src/components/tables/table-import-wizard.tsx +542 -0
  82. package/src/components/tables/table-list-table.tsx +95 -0
  83. package/src/components/tables/table-relation-combobox.tsx +217 -0
  84. package/src/components/tables/table-spreadsheet.tsx +499 -0
  85. package/src/components/tables/table-template-gallery.tsx +162 -0
  86. package/src/components/tables/table-template-preview.tsx +219 -0
  87. package/src/components/tables/table-toolbar.tsx +79 -0
  88. package/src/components/tables/table-triggers-tab.tsx +446 -0
  89. package/src/components/tables/types.ts +6 -0
  90. package/src/components/tables/use-spreadsheet-keys.ts +171 -0
  91. package/src/components/tables/utils.ts +29 -0
  92. package/src/components/tasks/task-card.tsx +8 -1
  93. package/src/components/tasks/task-create-panel.tsx +111 -14
  94. package/src/components/tasks/task-detail-view.tsx +47 -0
  95. package/src/components/tasks/task-edit-dialog.tsx +103 -2
  96. package/src/components/workflows/workflow-form-view.tsx +207 -7
  97. package/src/components/workflows/workflow-kanban-card.tsx +8 -1
  98. package/src/components/workflows/workflow-list.tsx +90 -45
  99. package/src/components/workflows/workflow-status-view.tsx +168 -23
  100. package/src/instrumentation.ts +3 -0
  101. package/src/lib/__tests__/npx-process-cwd.test.ts +17 -2
  102. package/src/lib/agents/__tests__/claude-agent.test.ts +5 -1
  103. package/src/lib/agents/claude-agent.ts +3 -1
  104. package/src/lib/agents/profiles/registry.ts +6 -3
  105. package/src/lib/agents/runtime/anthropic-direct.ts +29 -0
  106. package/src/lib/agents/runtime/openai-direct.ts +29 -0
  107. package/src/lib/book/__tests__/chapter-slugs.test.ts +80 -0
  108. package/src/lib/book/chapter-generator.ts +4 -19
  109. package/src/lib/book/chapter-mapping.ts +17 -0
  110. package/src/lib/book/content.ts +5 -16
  111. package/src/lib/book/update-detector.ts +3 -16
  112. package/src/lib/chat/engine.ts +1 -0
  113. package/src/lib/chat/stagent-tools.ts +2 -0
  114. package/src/lib/chat/system-prompt.ts +9 -1
  115. package/src/lib/chat/tool-catalog.ts +35 -0
  116. package/src/lib/chat/tools/settings-tools.ts +109 -0
  117. package/src/lib/chat/tools/table-tools.ts +955 -0
  118. package/src/lib/chat/tools/workflow-tools.ts +145 -2
  119. package/src/lib/constants/table-status.ts +68 -0
  120. package/src/lib/data/__tests__/clear.test.ts +1 -1
  121. package/src/lib/data/clear.ts +57 -0
  122. package/src/lib/data/seed-data/__tests__/profiles.test.ts +28 -23
  123. package/src/lib/data/seed-data/conversations.ts +350 -42
  124. package/src/lib/data/seed-data/documents.ts +564 -591
  125. package/src/lib/data/seed-data/learned-context.ts +101 -22
  126. package/src/lib/data/seed-data/notifications.ts +344 -70
  127. package/src/lib/data/seed-data/profile-test-results.ts +92 -11
  128. package/src/lib/data/seed-data/profiles.ts +144 -46
  129. package/src/lib/data/seed-data/projects.ts +50 -18
  130. package/src/lib/data/seed-data/repo-imports.ts +28 -13
  131. package/src/lib/data/seed-data/schedules.ts +208 -41
  132. package/src/lib/data/seed-data/table-templates.ts +234 -0
  133. package/src/lib/data/seed-data/tasks.ts +614 -116
  134. package/src/lib/data/seed-data/usage-ledger.ts +182 -103
  135. package/src/lib/data/seed-data/user-tables.ts +203 -0
  136. package/src/lib/data/seed-data/views.ts +52 -7
  137. package/src/lib/data/seed-data/workflows.ts +231 -84
  138. package/src/lib/data/seed.ts +55 -14
  139. package/src/lib/data/tables.ts +417 -0
  140. package/src/lib/db/bootstrap.ts +275 -0
  141. package/src/lib/db/index.ts +9 -0
  142. package/src/lib/db/migrations/0016_add_workflow_document_inputs.sql +13 -0
  143. package/src/lib/db/migrations/0017_add_document_picker_tables.sql +25 -0
  144. package/src/lib/db/migrations/0018_add_workflow_run_number.sql +2 -0
  145. package/src/lib/db/migrations/0019_add_tables_feature.sql +160 -0
  146. package/src/lib/db/migrations/0020_add_table_triggers.sql +19 -0
  147. package/src/lib/db/migrations/0021_add_row_history.sql +15 -0
  148. package/src/lib/db/schema.ts +445 -0
  149. package/src/lib/docs/reader.ts +2 -3
  150. package/src/lib/documents/context-builder.ts +75 -2
  151. package/src/lib/documents/document-resolver.ts +119 -0
  152. package/src/lib/documents/processors/spreadsheet.ts +2 -1
  153. package/src/lib/schedules/scheduler.ts +31 -1
  154. package/src/lib/snapshots/auto-backup.ts +132 -0
  155. package/src/lib/snapshots/retention.ts +64 -0
  156. package/src/lib/snapshots/snapshot-manager.ts +429 -0
  157. package/src/lib/tables/computed.ts +61 -0
  158. package/src/lib/tables/context-builder.ts +139 -0
  159. package/src/lib/tables/formula-engine.ts +415 -0
  160. package/src/lib/tables/history.ts +115 -0
  161. package/src/lib/tables/import.ts +343 -0
  162. package/src/lib/tables/query-builder.ts +152 -0
  163. package/src/lib/tables/trigger-evaluator.ts +146 -0
  164. package/src/lib/tables/types.ts +141 -0
  165. package/src/lib/tables/validation.ts +119 -0
  166. package/src/lib/utils/app-root.ts +20 -0
  167. package/src/lib/utils/stagent-paths.ts +20 -0
  168. package/src/lib/validators/__tests__/task.test.ts +43 -10
  169. package/src/lib/validators/task.ts +7 -1
  170. package/src/lib/workflows/blueprints/registry.ts +3 -3
  171. package/src/lib/workflows/engine.ts +24 -8
  172. package/src/lib/workflows/types.ts +14 -0
  173. package/tsconfig.json +3 -1
  174. package/public/icon.svg +0 -13
  175. package/src/components/tasks/file-upload.tsx +0 -120
  176. /package/docs/features/{playbook.md → user-guide.md} +0 -0
@@ -0,0 +1,288 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Input } from "@/components/ui/input";
6
+ import { Label } from "@/components/ui/label";
7
+ import {
8
+ Select,
9
+ SelectContent,
10
+ SelectItem,
11
+ SelectTrigger,
12
+ SelectValue,
13
+ } from "@/components/ui/select";
14
+ import {
15
+ Sheet,
16
+ SheetContent,
17
+ SheetHeader,
18
+ SheetTitle,
19
+ SheetFooter,
20
+ } from "@/components/ui/sheet";
21
+ import { BarChart3, LineChart, PieChart, ScatterChart } from "lucide-react";
22
+ import { toast } from "sonner";
23
+ import { cn } from "@/lib/utils";
24
+
25
+ type ChartType = "bar" | "line" | "pie" | "scatter";
26
+ type Aggregation = "sum" | "avg" | "count" | "min" | "max";
27
+
28
+ export interface EditChartData {
29
+ id: string;
30
+ name: string;
31
+ config: {
32
+ type: ChartType;
33
+ xColumn: string;
34
+ yColumn?: string;
35
+ aggregation?: Aggregation;
36
+ };
37
+ }
38
+
39
+ interface TableChartBuilderProps {
40
+ tableId: string;
41
+ columns: Array<{ name: string; displayName: string; dataType: string }>;
42
+ open: boolean;
43
+ onOpenChange: (open: boolean) => void;
44
+ onChartSaved: () => void;
45
+ editChart?: EditChartData | null;
46
+ }
47
+
48
+ const CHART_TYPES: Array<{
49
+ value: ChartType;
50
+ label: string;
51
+ icon: typeof BarChart3;
52
+ }> = [
53
+ { value: "bar", label: "Bar", icon: BarChart3 },
54
+ { value: "line", label: "Line", icon: LineChart },
55
+ { value: "pie", label: "Pie", icon: PieChart },
56
+ { value: "scatter", label: "Scatter", icon: ScatterChart },
57
+ ];
58
+
59
+ const AGGREGATIONS: Array<{ value: Aggregation; label: string }> = [
60
+ { value: "count", label: "Count" },
61
+ { value: "sum", label: "Sum" },
62
+ { value: "avg", label: "Average" },
63
+ { value: "min", label: "Min" },
64
+ { value: "max", label: "Max" },
65
+ ];
66
+
67
+ const NUMERIC_TYPES = new Set(["number", "currency", "percent", "integer", "float"]);
68
+
69
+ export function TableChartBuilder({
70
+ tableId,
71
+ columns,
72
+ open,
73
+ onOpenChange,
74
+ onChartSaved,
75
+ editChart,
76
+ }: TableChartBuilderProps) {
77
+ const [chartType, setChartType] = useState<ChartType>("bar");
78
+ const [title, setTitle] = useState("");
79
+ const [xColumn, setXColumn] = useState("");
80
+ const [yColumn, setYColumn] = useState("");
81
+ const [aggregation, setAggregation] = useState<Aggregation>("count");
82
+ const [saving, setSaving] = useState(false);
83
+
84
+ const isEditing = !!editChart;
85
+ const numericColumns = columns.filter((c) => NUMERIC_TYPES.has(c.dataType));
86
+
87
+ // Populate form when editing
88
+ useEffect(() => {
89
+ if (editChart) {
90
+ setChartType(editChart.config.type);
91
+ setTitle(editChart.name);
92
+ setXColumn(editChart.config.xColumn);
93
+ setYColumn(editChart.config.yColumn ?? "");
94
+ setAggregation(editChart.config.aggregation ?? "count");
95
+ } else {
96
+ resetForm();
97
+ }
98
+ }, [editChart]);
99
+
100
+ function resetForm() {
101
+ setChartType("bar");
102
+ setTitle("");
103
+ setXColumn("");
104
+ setYColumn("");
105
+ setAggregation("count");
106
+ }
107
+
108
+ async function handleSave() {
109
+ if (!title.trim()) {
110
+ toast.error("Chart title is required");
111
+ return;
112
+ }
113
+ if (!xColumn) {
114
+ toast.error("X-axis column is required");
115
+ return;
116
+ }
117
+ if (aggregation !== "count" && !yColumn) {
118
+ toast.error("Y-axis column is required for this aggregation");
119
+ return;
120
+ }
121
+
122
+ setSaving(true);
123
+ try {
124
+ const payload = {
125
+ type: chartType,
126
+ title: title.trim(),
127
+ xColumn,
128
+ yColumn: yColumn || null,
129
+ aggregation,
130
+ };
131
+
132
+ const url = isEditing
133
+ ? `/api/tables/${tableId}/charts/${editChart.id}`
134
+ : `/api/tables/${tableId}/charts`;
135
+
136
+ const res = await fetch(url, {
137
+ method: isEditing ? "PATCH" : "POST",
138
+ headers: { "Content-Type": "application/json" },
139
+ body: JSON.stringify(payload),
140
+ });
141
+
142
+ if (res.ok) {
143
+ toast.success(isEditing ? `Chart "${title}" updated` : `Chart "${title}" created`);
144
+ } else {
145
+ const data = await res.json().catch(() => null);
146
+ toast.error(data?.error ?? `Failed to ${isEditing ? "update" : "create"} chart`);
147
+ return;
148
+ }
149
+
150
+ onOpenChange(false);
151
+ onChartSaved();
152
+ if (!isEditing) resetForm();
153
+ } catch {
154
+ toast.error(`Failed to ${isEditing ? "update" : "create"} chart`);
155
+ } finally {
156
+ setSaving(false);
157
+ }
158
+ }
159
+
160
+ return (
161
+ <Sheet open={open} onOpenChange={onOpenChange}>
162
+ <SheetContent side="right" className="w-[420px] sm:max-w-[420px]">
163
+ <SheetHeader>
164
+ <SheetTitle>{isEditing ? "Edit Chart" : "Create Chart"}</SheetTitle>
165
+ </SheetHeader>
166
+
167
+ <div className="px-6 pb-6 space-y-5 overflow-y-auto">
168
+ {/* Chart type selector */}
169
+ <div className="space-y-2">
170
+ <Label>Chart Type</Label>
171
+ <div className="grid grid-cols-4 gap-2">
172
+ {CHART_TYPES.map((ct) => {
173
+ const Icon = ct.icon;
174
+ return (
175
+ <button
176
+ key={ct.value}
177
+ type="button"
178
+ onClick={() => setChartType(ct.value)}
179
+ className={cn(
180
+ "flex flex-col items-center gap-1.5 rounded-lg border p-3 text-xs transition-colors",
181
+ "hover:bg-accent hover:text-accent-foreground",
182
+ "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
183
+ chartType === ct.value
184
+ ? "border-primary bg-primary/5 text-primary"
185
+ : "border-border text-muted-foreground"
186
+ )}
187
+ >
188
+ <Icon className="h-5 w-5" />
189
+ {ct.label}
190
+ </button>
191
+ );
192
+ })}
193
+ </div>
194
+ </div>
195
+
196
+ {/* Title */}
197
+ <div className="space-y-2">
198
+ <Label htmlFor="chart-title">Title</Label>
199
+ <Input
200
+ id="chart-title"
201
+ placeholder="e.g. Revenue by Month"
202
+ value={title}
203
+ onChange={(e) => setTitle(e.target.value)}
204
+ />
205
+ </div>
206
+
207
+ {/* X-axis column */}
208
+ <div className="space-y-2">
209
+ <Label>X-Axis Column</Label>
210
+ <Select value={xColumn} onValueChange={setXColumn}>
211
+ <SelectTrigger>
212
+ <SelectValue placeholder="Select column" />
213
+ </SelectTrigger>
214
+ <SelectContent>
215
+ {columns.map((col) => (
216
+ <SelectItem key={col.name} value={col.name}>
217
+ {col.displayName}
218
+ </SelectItem>
219
+ ))}
220
+ </SelectContent>
221
+ </Select>
222
+ </div>
223
+
224
+ {/* Aggregation */}
225
+ <div className="space-y-2">
226
+ <Label>Aggregation</Label>
227
+ <Select
228
+ value={aggregation}
229
+ onValueChange={(v) => setAggregation(v as Aggregation)}
230
+ >
231
+ <SelectTrigger>
232
+ <SelectValue />
233
+ </SelectTrigger>
234
+ <SelectContent>
235
+ {AGGREGATIONS.map((agg) => (
236
+ <SelectItem key={agg.value} value={agg.value}>
237
+ {agg.label}
238
+ </SelectItem>
239
+ ))}
240
+ </SelectContent>
241
+ </Select>
242
+ </div>
243
+
244
+ {/* Y-axis column (only for non-count aggregations) */}
245
+ {aggregation !== "count" && (
246
+ <div className="space-y-2">
247
+ <Label>Y-Axis Column (numeric)</Label>
248
+ <Select value={yColumn} onValueChange={setYColumn}>
249
+ <SelectTrigger>
250
+ <SelectValue placeholder="Select numeric column" />
251
+ </SelectTrigger>
252
+ <SelectContent>
253
+ {numericColumns.length === 0 ? (
254
+ <SelectItem value="_none" disabled>
255
+ No numeric columns available
256
+ </SelectItem>
257
+ ) : (
258
+ numericColumns.map((col) => (
259
+ <SelectItem key={col.name} value={col.name}>
260
+ {col.displayName}
261
+ </SelectItem>
262
+ ))
263
+ )}
264
+ </SelectContent>
265
+ </Select>
266
+ {numericColumns.length === 0 && (
267
+ <p className="text-xs text-muted-foreground">
268
+ Add a numeric column to use sum, average, min, or max aggregations.
269
+ </p>
270
+ )}
271
+ </div>
272
+ )}
273
+ </div>
274
+
275
+ <SheetFooter className="px-6">
276
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
277
+ Cancel
278
+ </Button>
279
+ <Button onClick={handleSave} disabled={saving}>
280
+ {saving
281
+ ? isEditing ? "Updating..." : "Creating..."
282
+ : isEditing ? "Update Chart" : "Create Chart"}
283
+ </Button>
284
+ </SheetFooter>
285
+ </SheetContent>
286
+ </Sheet>
287
+ );
288
+ }
@@ -0,0 +1,146 @@
1
+ "use client";
2
+
3
+ import { useMemo } from "react";
4
+ import {
5
+ BarChart,
6
+ Bar,
7
+ LineChart,
8
+ Line,
9
+ PieChart,
10
+ Pie,
11
+ ScatterChart,
12
+ Scatter,
13
+ XAxis,
14
+ YAxis,
15
+ CartesianGrid,
16
+ Tooltip,
17
+ Legend,
18
+ ResponsiveContainer,
19
+ Cell,
20
+ } from "recharts";
21
+
22
+ interface ChartConfig {
23
+ type: "bar" | "line" | "pie" | "scatter";
24
+ xColumn: string;
25
+ yColumn?: string;
26
+ aggregation?: "sum" | "avg" | "count" | "min" | "max";
27
+ }
28
+
29
+ interface TableChartViewProps {
30
+ config: ChartConfig;
31
+ title: string;
32
+ rows: Array<{ data: Record<string, unknown> }>;
33
+ }
34
+
35
+ const CHART_COLORS = [
36
+ "oklch(0.65 0.18 250)",
37
+ "oklch(0.70 0.16 180)",
38
+ "oklch(0.68 0.17 320)",
39
+ "oklch(0.72 0.14 80)",
40
+ "oklch(0.60 0.20 30)",
41
+ "oklch(0.75 0.12 140)",
42
+ ];
43
+
44
+ export function TableChartView({ config, title, rows }: TableChartViewProps) {
45
+ const chartData = useMemo(() => {
46
+ if (config.type === "pie" || config.aggregation === "count") {
47
+ // Aggregate by xColumn values
48
+ const counts = new Map<string, number>();
49
+ for (const row of rows) {
50
+ const key = String(row.data[config.xColumn] ?? "Unknown");
51
+ counts.set(key, (counts.get(key) ?? 0) + 1);
52
+ }
53
+ return Array.from(counts.entries()).map(([name, value]) => ({ name, value }));
54
+ }
55
+
56
+ if (config.aggregation && config.yColumn) {
57
+ // Group by xColumn, aggregate yColumn
58
+ const groups = new Map<string, number[]>();
59
+ for (const row of rows) {
60
+ const key = String(row.data[config.xColumn] ?? "Unknown");
61
+ const val = Number(row.data[config.yColumn]);
62
+ if (!isNaN(val)) {
63
+ if (!groups.has(key)) groups.set(key, []);
64
+ groups.get(key)!.push(val);
65
+ }
66
+ }
67
+
68
+ return Array.from(groups.entries()).map(([name, values]) => {
69
+ let value: number;
70
+ switch (config.aggregation) {
71
+ case "sum": value = values.reduce((a, b) => a + b, 0); break;
72
+ case "avg": value = values.reduce((a, b) => a + b, 0) / values.length; break;
73
+ case "min": value = Math.min(...values); break;
74
+ case "max": value = Math.max(...values); break;
75
+ default: value = values.length;
76
+ }
77
+ return { name, value: Math.round(value * 100) / 100 };
78
+ });
79
+ }
80
+
81
+ // Raw x/y mapping
82
+ return rows.map((row) => ({
83
+ name: String(row.data[config.xColumn] ?? ""),
84
+ value: config.yColumn ? Number(row.data[config.yColumn]) || 0 : 1,
85
+ }));
86
+ }, [config, rows]);
87
+
88
+ if (chartData.length === 0) {
89
+ return <p className="text-sm text-muted-foreground p-4">No data to chart.</p>;
90
+ }
91
+
92
+ return (
93
+ <div className="space-y-2">
94
+ <h3 className="text-sm font-medium">{title}</h3>
95
+ <div className="h-[300px] w-full">
96
+ <ResponsiveContainer width="100%" height="100%">
97
+ {config.type === "bar" ? (
98
+ <BarChart data={chartData}>
99
+ <CartesianGrid strokeDasharray="3 3" className="opacity-30" />
100
+ <XAxis dataKey="name" tick={{ fontSize: 12 }} />
101
+ <YAxis tick={{ fontSize: 12 }} />
102
+ <Tooltip />
103
+ <Legend />
104
+ <Bar dataKey="value" name={config.yColumn ?? config.aggregation ?? "value"} fill="oklch(0.65 0.18 250)" radius={[4, 4, 0, 0]} />
105
+ </BarChart>
106
+ ) : config.type === "line" ? (
107
+ <LineChart data={chartData}>
108
+ <CartesianGrid strokeDasharray="3 3" className="opacity-30" />
109
+ <XAxis dataKey="name" tick={{ fontSize: 12 }} />
110
+ <YAxis tick={{ fontSize: 12 }} />
111
+ <Tooltip />
112
+ <Legend />
113
+ <Line type="monotone" dataKey="value" name={config.yColumn ?? config.aggregation ?? "value"} stroke="oklch(0.65 0.18 250)" strokeWidth={2} dot={{ r: 3 }} />
114
+ </LineChart>
115
+ ) : config.type === "pie" ? (
116
+ <PieChart>
117
+ <Tooltip />
118
+ <Legend />
119
+ <Pie
120
+ data={chartData}
121
+ cx="50%"
122
+ cy="50%"
123
+ outerRadius={100}
124
+ dataKey="value"
125
+ nameKey="name"
126
+ label={({ name, percent }: { name?: string; percent?: number }) => `${name ?? ""} ${((percent ?? 0) * 100).toFixed(0)}%`}
127
+ >
128
+ {chartData.map((_, i) => (
129
+ <Cell key={i} fill={CHART_COLORS[i % CHART_COLORS.length]} />
130
+ ))}
131
+ </Pie>
132
+ </PieChart>
133
+ ) : (
134
+ <ScatterChart>
135
+ <CartesianGrid strokeDasharray="3 3" className="opacity-30" />
136
+ <XAxis dataKey="name" tick={{ fontSize: 12 }} name={config.xColumn} />
137
+ <YAxis dataKey="value" tick={{ fontSize: 12 }} name={config.yColumn ?? "value"} />
138
+ <Tooltip />
139
+ <Scatter data={chartData} fill="oklch(0.65 0.18 250)" />
140
+ </ScatterChart>
141
+ )}
142
+ </ResponsiveContainer>
143
+ </div>
144
+ </div>
145
+ );
146
+ }
@@ -0,0 +1,103 @@
1
+ "use client";
2
+
3
+ import { Button } from "@/components/ui/button";
4
+ import {
5
+ DropdownMenu,
6
+ DropdownMenuContent,
7
+ DropdownMenuItem,
8
+ DropdownMenuSeparator,
9
+ DropdownMenuTrigger,
10
+ } from "@/components/ui/dropdown-menu";
11
+ import {
12
+ Type,
13
+ Hash,
14
+ Calendar,
15
+ CheckSquare,
16
+ List,
17
+ Link,
18
+ Mail,
19
+ GitBranch,
20
+ Sparkles,
21
+ ArrowUp,
22
+ ArrowDown,
23
+ ChevronDown,
24
+ Trash2,
25
+ Pencil,
26
+ } from "lucide-react";
27
+ import type { ColumnDef } from "@/lib/tables/types";
28
+ import type { ColumnDataType } from "@/lib/constants/table-status";
29
+ import type { LucideIcon } from "lucide-react";
30
+
31
+ const typeIcons: Record<ColumnDataType, LucideIcon> = {
32
+ text: Type,
33
+ number: Hash,
34
+ date: Calendar,
35
+ boolean: CheckSquare,
36
+ select: List,
37
+ url: Link,
38
+ email: Mail,
39
+ relation: GitBranch,
40
+ computed: Sparkles,
41
+ };
42
+
43
+ interface ColumnHeaderProps {
44
+ column: ColumnDef;
45
+ sortDirection: "asc" | "desc" | null;
46
+ onSort: (direction: "asc" | "desc") => void;
47
+ onRename: () => void;
48
+ onDelete: () => void;
49
+ }
50
+
51
+ export function SpreadsheetColumnHeader({
52
+ column,
53
+ sortDirection,
54
+ onSort,
55
+ onRename,
56
+ onDelete,
57
+ }: ColumnHeaderProps) {
58
+ const Icon = typeIcons[column.dataType] ?? Type;
59
+
60
+ return (
61
+ <div className="flex items-center gap-1 w-full">
62
+ <Icon className="h-3.5 w-3.5 text-muted-foreground shrink-0" />
63
+ <span className="truncate text-xs font-medium">{column.displayName}</span>
64
+ {sortDirection === "asc" && <ArrowUp className="h-3 w-3 text-muted-foreground" />}
65
+ {sortDirection === "desc" && <ArrowDown className="h-3 w-3 text-muted-foreground" />}
66
+
67
+ <DropdownMenu>
68
+ <DropdownMenuTrigger asChild>
69
+ <Button
70
+ variant="ghost"
71
+ size="icon"
72
+ className="h-5 w-5 ml-auto shrink-0 opacity-0 group-hover:opacity-100 focus:opacity-100"
73
+ >
74
+ <ChevronDown className="h-3 w-3" />
75
+ </Button>
76
+ </DropdownMenuTrigger>
77
+ <DropdownMenuContent align="start">
78
+ <DropdownMenuItem onClick={() => onSort("asc")}>
79
+ <ArrowUp className="h-4 w-4 mr-2" />
80
+ Sort Ascending
81
+ </DropdownMenuItem>
82
+ <DropdownMenuItem onClick={() => onSort("desc")}>
83
+ <ArrowDown className="h-4 w-4 mr-2" />
84
+ Sort Descending
85
+ </DropdownMenuItem>
86
+ <DropdownMenuSeparator />
87
+ <DropdownMenuItem onClick={onRename}>
88
+ <Pencil className="h-4 w-4 mr-2" />
89
+ Rename
90
+ </DropdownMenuItem>
91
+ <DropdownMenuSeparator />
92
+ <DropdownMenuItem
93
+ onClick={onDelete}
94
+ className="text-destructive focus:text-destructive"
95
+ >
96
+ <Trash2 className="h-4 w-4 mr-2" />
97
+ Delete Column
98
+ </DropdownMenuItem>
99
+ </DropdownMenuContent>
100
+ </DropdownMenu>
101
+ </div>
102
+ );
103
+ }