stagent 0.7.0 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stagent",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "AI Business Operating System — run your business with AI agents. Local-first, multi-provider, governed.",
5
5
  "keywords": [
6
6
  "ai",
@@ -810,6 +810,12 @@ body {
810
810
  .book-callout-lesson .book-callout-icon { color: oklch(0.55 0.20 300); }
811
811
  .book-callout-lesson .book-callout-title { color: oklch(0.45 0.16 300); }
812
812
 
813
+ .book-callout-case-study {
814
+ border-left-color: oklch(0.62 0.16 55);
815
+ background: oklch(0.62 0.16 55 / 0.05);
816
+ }
817
+ .book-callout-case-study .book-callout-icon { color: oklch(0.62 0.16 55); }
818
+ .book-callout-case-study .book-callout-title { color: oklch(0.50 0.12 55); }
813
819
 
814
820
  /* callout body inline code */
815
821
  .book-callout-body code {
@@ -821,6 +827,7 @@ body {
821
827
  [data-book-theme="sepia"] .book-callout-warning { background: oklch(0.65 0.18 75 / 0.08); }
822
828
  [data-book-theme="sepia"] .book-callout-info { background: oklch(0.55 0.20 260 / 0.08); }
823
829
  [data-book-theme="sepia"] .book-callout-lesson { background: oklch(0.55 0.20 300 / 0.08); }
830
+ [data-book-theme="sepia"] .book-callout-case-study { background: oklch(0.62 0.16 55 / 0.08); }
824
831
  [data-book-theme="sepia"] .book-callout-body code { background: oklch(0 0 0 / 0.06); }
825
832
 
826
833
  /* dark callout overrides */
@@ -848,6 +855,11 @@ body {
848
855
  [data-book-theme="dark"] .book-callout-lesson .book-callout-icon { color: oklch(0.70 0.18 300); }
849
856
  [data-book-theme="dark"] .book-callout-lesson .book-callout-title { color: oklch(0.75 0.14 300); }
850
857
 
858
+ [data-book-theme="dark"] .book-callout-case-study {
859
+ background: oklch(0.62 0.16 55 / 0.10);
860
+ }
861
+ [data-book-theme="dark"] .book-callout-case-study .book-callout-icon { color: oklch(0.75 0.16 55); }
862
+ [data-book-theme="dark"] .book-callout-case-study .book-callout-title { color: oklch(0.80 0.12 55); }
851
863
 
852
864
  [data-book-theme="dark"] .book-callout-body code { background: oklch(1 0 0 / 0.10); }
853
865
 
@@ -860,6 +872,8 @@ body {
860
872
  .dark .book-callout-info .book-callout-title { color: oklch(0.75 0.14 260); }
861
873
  .dark .book-callout-lesson .book-callout-icon { color: oklch(0.70 0.18 300); }
862
874
  .dark .book-callout-lesson .book-callout-title { color: oklch(0.75 0.14 300); }
875
+ .dark .book-callout-case-study .book-callout-icon { color: oklch(0.75 0.16 55); }
876
+ .dark .book-callout-case-study .book-callout-title { color: oklch(0.80 0.12 55); }
863
877
  .dark .book-callout-body code { background: oklch(1 0 0 / 0.10); }
864
878
 
865
879
  /* ─── Book Interactive Blocks ─── */
@@ -5,6 +5,7 @@ import {
5
5
  BookOpen,
6
6
  BookmarkPlus,
7
7
  BookmarkMinus,
8
+ ChevronDown,
8
9
  ChevronLeft,
9
10
  ChevronRight,
10
11
  List,
@@ -87,6 +88,7 @@ export function BookReader({ chapters: CHAPTERS }: { chapters: BookChapter[] })
87
88
  const [tocOpen, setTocOpen] = useState(false);
88
89
  const [settingsOpen, setSettingsOpen] = useState(false);
89
90
  const [tocTab, setTocTab] = useState<"chapters" | "bookmarks">("chapters");
91
+ const [collapsedParts, setCollapsedParts] = useState<Set<number>>(new Set());
90
92
  const [activePath, setActivePath] = useState<string | null>(null);
91
93
  const [recommendedPath, setRecommendedPath] = useState<string | null>(null);
92
94
  const contentRef = useRef<HTMLDivElement>(null);
@@ -160,6 +162,31 @@ export function BookReader({ chapters: CHAPTERS }: { chapters: BookChapter[] })
160
162
  }
161
163
  }, []);
162
164
 
165
+ const togglePart = useCallback((partNumber: number) => {
166
+ setCollapsedParts((prev) => {
167
+ const next = new Set(prev);
168
+ if (next.has(partNumber)) {
169
+ next.delete(partNumber);
170
+ } else {
171
+ next.add(partNumber);
172
+ }
173
+ return next;
174
+ });
175
+ }, []);
176
+
177
+ // Auto-expand the current chapter's part so it's always visible in TOC
178
+ useEffect(() => {
179
+ const currentPart = currentChapter.part.number;
180
+ setCollapsedParts((prev) => {
181
+ if (prev.has(currentPart)) {
182
+ const next = new Set(prev);
183
+ next.delete(currentPart);
184
+ return next;
185
+ }
186
+ return prev;
187
+ });
188
+ }, [currentChapter]);
189
+
163
190
  // Track scroll progress
164
191
  useEffect(() => {
165
192
  const el = contentRef.current;
@@ -431,15 +458,38 @@ export function BookReader({ chapters: CHAPTERS }: { chapters: BookChapter[] })
431
458
  onSelectPath={handlePathChange}
432
459
  />
433
460
 
434
- {PARTS.map((part) => (
461
+ {PARTS.map((part) => {
462
+ const isPartOpen = !collapsedParts.has(part.number);
463
+ return (
435
464
  <div key={part.number}>
436
- <p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground mb-2">
437
- Part {part.number}: {part.title}
438
- </p>
439
- <p className="text-xs text-muted-foreground mb-3">
440
- {part.description}
441
- </p>
442
- <div className="space-y-1">
465
+ <button
466
+ onClick={() => togglePart(part.number)}
467
+ className="flex w-full items-center justify-between gap-2 text-left cursor-pointer group"
468
+ aria-expanded={isPartOpen}
469
+ >
470
+ <div>
471
+ <p className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
472
+ Part {part.number}: {part.title}
473
+ </p>
474
+ <p className="text-xs text-muted-foreground mt-0.5">
475
+ {part.description}
476
+ </p>
477
+ </div>
478
+ <ChevronDown
479
+ className={cn(
480
+ "h-3.5 w-3.5 shrink-0 text-muted-foreground transition-transform duration-300 ease-in-out",
481
+ isPartOpen && "rotate-180"
482
+ )}
483
+ />
484
+ </button>
485
+ <div
486
+ className={cn(
487
+ "grid transition-[grid-template-rows] duration-300 ease-in-out",
488
+ isPartOpen ? "grid-rows-[1fr]" : "grid-rows-[0fr]"
489
+ )}
490
+ >
491
+ <div className="overflow-hidden">
492
+ <div className="space-y-1 pt-2">
443
493
  {(chaptersByPart.get(part.number) ?? []).map((ch) => {
444
494
  const chProgress = progress[ch.id]?.progress ?? 0;
445
495
  const chPct = Math.round(chProgress * 100);
@@ -488,8 +538,11 @@ export function BookReader({ chapters: CHAPTERS }: { chapters: BookChapter[] })
488
538
  );
489
539
  })}
490
540
  </div>
541
+ </div>
542
+ </div>
491
543
  </div>
492
- ))}
544
+ );
545
+ })}
493
546
  </div>
494
547
  ) : (
495
548
  <div className="space-y-1">
@@ -8,6 +8,7 @@ import {
8
8
  AlertTriangle,
9
9
  BookOpen,
10
10
  PenLine,
11
+ Building2,
11
12
  ArrowRight,
12
13
  ChevronDown,
13
14
  Copy,
@@ -203,6 +204,10 @@ const calloutConfig = {
203
204
  icon: PenLine,
204
205
  className: "book-callout-authors-note",
205
206
  },
207
+ "case-study": {
208
+ icon: Building2,
209
+ className: "book-callout-case-study",
210
+ },
206
211
  };
207
212
 
208
213
  function CalloutBlockView({
@@ -213,7 +218,7 @@ function CalloutBlockView({
213
218
  imageAlt,
214
219
  defaultCollapsed,
215
220
  }: {
216
- variant: "tip" | "warning" | "info" | "lesson" | "authors-note";
221
+ variant: "tip" | "warning" | "info" | "lesson" | "authors-note" | "case-study";
217
222
  title?: string;
218
223
  markdown: string;
219
224
  imageSrc?: string;
@@ -206,7 +206,7 @@ export function CellDisplay({ column, value, onToggleBoolean }: CellDisplayProps
206
206
  return (
207
207
  <a
208
208
  href={`mailto:${value}`}
209
- className="text-sm text-primary underline underline-offset-2 hover:text-primary/80"
209
+ className="text-sm text-primary underline underline-offset-2 hover:text-primary/80 truncate max-w-[200px] block"
210
210
  onClick={(e) => e.stopPropagation()}
211
211
  >
212
212
  {String(value)}
@@ -215,12 +215,12 @@ export function CellDisplay({ column, value, onToggleBoolean }: CellDisplayProps
215
215
 
216
216
  case "computed":
217
217
  return (
218
- <span className="text-sm text-muted-foreground italic">
218
+ <span className="text-sm text-muted-foreground italic truncate max-w-[200px] block">
219
219
  {String(value)}
220
220
  </span>
221
221
  );
222
222
 
223
223
  default:
224
- return <span className="text-sm">{String(value)}</span>;
224
+ return <span className="text-sm truncate max-w-[200px] block">{String(value)}</span>;
225
225
  }
226
226
  }
@@ -0,0 +1,271 @@
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 { Switch } from "@/components/ui/switch";
8
+ import { Textarea } from "@/components/ui/textarea";
9
+ import {
10
+ Select,
11
+ SelectContent,
12
+ SelectItem,
13
+ SelectTrigger,
14
+ SelectValue,
15
+ } from "@/components/ui/select";
16
+ import {
17
+ Sheet,
18
+ SheetContent,
19
+ SheetHeader,
20
+ SheetTitle,
21
+ SheetFooter,
22
+ } from "@/components/ui/sheet";
23
+ import { toast } from "sonner";
24
+ import { TableRelationCombobox } from "./table-relation-combobox";
25
+ import type { ColumnDef } from "@/lib/tables/types";
26
+
27
+ interface ParsedRow {
28
+ id: string;
29
+ data: Record<string, unknown>;
30
+ position: number;
31
+ createdBy: string | null;
32
+ }
33
+
34
+ interface TableRowSheetProps {
35
+ tableId: string;
36
+ columns: ColumnDef[];
37
+ row: ParsedRow;
38
+ open: boolean;
39
+ onOpenChange: (open: boolean) => void;
40
+ onRowUpdated: (rowId: string, data: Record<string, unknown>) => void;
41
+ }
42
+
43
+ export function TableRowSheet({
44
+ tableId,
45
+ columns,
46
+ row,
47
+ open,
48
+ onOpenChange,
49
+ onRowUpdated,
50
+ }: TableRowSheetProps) {
51
+ const [formData, setFormData] = useState<Record<string, unknown>>({});
52
+ const [saving, setSaving] = useState(false);
53
+
54
+ // Reset form data when sheet opens or row changes
55
+ useEffect(() => {
56
+ if (open) {
57
+ setFormData({ ...row.data });
58
+ }
59
+ }, [open, row.id, row.data]);
60
+
61
+ const setField = (name: string, value: unknown) => {
62
+ setFormData((prev) => ({ ...prev, [name]: value }));
63
+ };
64
+
65
+ const handleSave = async () => {
66
+ setSaving(true);
67
+ try {
68
+ // Exclude computed columns from the payload
69
+ const payload: Record<string, unknown> = {};
70
+ for (const col of columns) {
71
+ if (col.dataType !== "computed") {
72
+ payload[col.name] = formData[col.name] ?? null;
73
+ }
74
+ }
75
+
76
+ const res = await fetch(`/api/tables/${tableId}/rows/${row.id}`, {
77
+ method: "PATCH",
78
+ headers: { "Content-Type": "application/json" },
79
+ body: JSON.stringify({ data: payload }),
80
+ });
81
+
82
+ if (!res.ok) {
83
+ toast.error("Failed to save row");
84
+ return;
85
+ }
86
+
87
+ onRowUpdated(row.id, { ...formData, ...payload });
88
+ onOpenChange(false);
89
+ toast.success("Row updated");
90
+ } catch {
91
+ toast.error("Failed to save row");
92
+ } finally {
93
+ setSaving(false);
94
+ }
95
+ };
96
+
97
+ const sortedColumns = [...columns].sort((a, b) => a.position - b.position);
98
+
99
+ return (
100
+ <Sheet open={open} onOpenChange={onOpenChange}>
101
+ <SheetContent side="right" className="w-[480px] sm:max-w-[480px] flex flex-col">
102
+ <SheetHeader>
103
+ <SheetTitle>Edit Row</SheetTitle>
104
+ </SheetHeader>
105
+ <div className="px-6 pb-6 space-y-4 overflow-y-auto flex-1">
106
+ {sortedColumns.map((col) => (
107
+ <RowField
108
+ key={col.name}
109
+ column={col}
110
+ value={formData[col.name]}
111
+ onChange={(v) => setField(col.name, v)}
112
+ />
113
+ ))}
114
+ </div>
115
+ <SheetFooter className="px-6">
116
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
117
+ Cancel
118
+ </Button>
119
+ <Button onClick={handleSave} disabled={saving}>
120
+ {saving ? "Saving..." : "Save"}
121
+ </Button>
122
+ </SheetFooter>
123
+ </SheetContent>
124
+ </Sheet>
125
+ );
126
+ }
127
+
128
+ // ── Per-field renderer ──────────────────────────────────────────────────
129
+
130
+ interface RowFieldProps {
131
+ column: ColumnDef;
132
+ value: unknown;
133
+ onChange: (value: unknown) => void;
134
+ }
135
+
136
+ function RowField({ column, value, onChange }: RowFieldProps) {
137
+ const strValue = value == null ? "" : String(value);
138
+
139
+ return (
140
+ <div className="space-y-2">
141
+ <Label className="flex items-center gap-1.5">
142
+ {column.displayName}
143
+ {column.required && <span className="text-destructive">*</span>}
144
+ </Label>
145
+ {renderInput(column, value, strValue, onChange)}
146
+ </div>
147
+ );
148
+ }
149
+
150
+ function renderInput(
151
+ column: ColumnDef,
152
+ value: unknown,
153
+ strValue: string,
154
+ onChange: (value: unknown) => void,
155
+ ) {
156
+ switch (column.dataType) {
157
+ case "text":
158
+ return (
159
+ <Textarea
160
+ rows={3}
161
+ value={strValue}
162
+ onChange={(e) => onChange(e.target.value)}
163
+ placeholder={`Enter ${column.displayName.toLowerCase()}...`}
164
+ />
165
+ );
166
+
167
+ case "number":
168
+ return (
169
+ <Input
170
+ type="number"
171
+ value={strValue}
172
+ onChange={(e) =>
173
+ onChange(e.target.value === "" ? null : Number(e.target.value))
174
+ }
175
+ placeholder="0"
176
+ />
177
+ );
178
+
179
+ case "date":
180
+ return (
181
+ <Input
182
+ type="date"
183
+ value={strValue}
184
+ onChange={(e) => onChange(e.target.value)}
185
+ />
186
+ );
187
+
188
+ case "email":
189
+ return (
190
+ <Input
191
+ type="email"
192
+ value={strValue}
193
+ onChange={(e) => onChange(e.target.value)}
194
+ placeholder="name@example.com"
195
+ />
196
+ );
197
+
198
+ case "url":
199
+ return (
200
+ <Input
201
+ type="url"
202
+ value={strValue}
203
+ onChange={(e) => onChange(e.target.value)}
204
+ placeholder="https://..."
205
+ />
206
+ );
207
+
208
+ case "boolean":
209
+ return (
210
+ <div className="flex items-center gap-2 pt-1">
211
+ <Switch
212
+ checked={!!value}
213
+ onCheckedChange={(checked) => onChange(checked)}
214
+ />
215
+ <span className="text-sm text-muted-foreground">
216
+ {value ? "Yes" : "No"}
217
+ </span>
218
+ </div>
219
+ );
220
+
221
+ case "select": {
222
+ const options = column.config?.options ?? [];
223
+ return (
224
+ <Select value={strValue} onValueChange={onChange}>
225
+ <SelectTrigger>
226
+ <SelectValue placeholder={`Select ${column.displayName.toLowerCase()}...`} />
227
+ </SelectTrigger>
228
+ <SelectContent>
229
+ {options.map((opt) => (
230
+ <SelectItem key={opt} value={opt}>
231
+ {opt}
232
+ </SelectItem>
233
+ ))}
234
+ </SelectContent>
235
+ </Select>
236
+ );
237
+ }
238
+
239
+ case "relation": {
240
+ const targetId = column.config?.targetTableId;
241
+ const dispCol = column.config?.displayColumn ?? "name";
242
+ if (!targetId) return <Input disabled value="No target table configured" />;
243
+ return (
244
+ <TableRelationCombobox
245
+ targetTableId={targetId}
246
+ displayColumn={dispCol}
247
+ value={strValue || null}
248
+ onChange={(v) => onChange(v)}
249
+ />
250
+ );
251
+ }
252
+
253
+ case "computed":
254
+ return (
255
+ <Input
256
+ disabled
257
+ value={strValue}
258
+ className="text-muted-foreground italic"
259
+ />
260
+ );
261
+
262
+ default:
263
+ return (
264
+ <Input
265
+ value={strValue}
266
+ onChange={(e) => onChange(e.target.value)}
267
+ placeholder={`Enter ${column.displayName.toLowerCase()}...`}
268
+ />
269
+ );
270
+ }
271
+ }