@montytools/cli 0.2.10 → 0.4.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/bin/monty.mjs +1156 -54
- package/package.json +1 -1
- package/skills/monty-build/SKILL.md +32 -18
- package/template/.claude/settings.json +5 -0
- package/template/AGENTS.md +59 -2
- package/template/index.html +1 -1
- package/template/monty.config.ts +14 -13
- package/template/package.json +1 -1
- package/template/src/routes/index.tsx +20 -245
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: monty-build
|
|
3
|
-
description: Build, run, and deploy Monty workspace apps. Use whenever the task involves a Monty app, monty.config.ts, the monty CLI (create/dev/deploy/add), the @montytools/sdk, or a prompt mentioning usemonty.dev. Covers folder discipline, the build loop, data/auth rules, and error handling.
|
|
3
|
+
description: Build, run, and deploy Monty workspace apps. Use whenever the task involves a Monty app, monty.config.ts, the monty CLI (create/dev/logs/deploy/add), the @montytools/sdk, or a prompt mentioning usemonty.dev. Covers folder discipline, the build loop, data/auth rules, and error handling.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Building Monty apps
|
|
@@ -12,29 +12,42 @@ platform's job. The complete contract lives in the app's own `AGENTS.md`
|
|
|
12
12
|
the territory.
|
|
13
13
|
|
|
14
14
|
Every app has two states: **Live** (published — what members and visitors
|
|
15
|
-
see) and **Studio** (development —
|
|
16
|
-
|
|
17
|
-
mean something else on this platform.
|
|
15
|
+
see) and **Studio** (development — a running dev shell on `#dev`-sandboxed
|
|
16
|
+
data, often already started for you by the Monty desktop). Those are the only
|
|
17
|
+
names for them; "dev"/"prod" mean something else on this platform.
|
|
18
18
|
|
|
19
19
|
## Rules
|
|
20
20
|
|
|
21
|
-
1. **Folders are managed.** Apps live in
|
|
22
|
-
|
|
23
|
-
`monty
|
|
24
|
-
|
|
21
|
+
1. **Folders are managed.** Apps live in `~/.monty/apps/<id>` (a server-issued
|
|
22
|
+
id, minted when `monty create` registers the app — so create needs
|
|
23
|
+
`monty login` first). `monty current` tells you where you are;
|
|
24
|
+
`cd "$(monty select <slug>)"` jumps to an app; `monty apps` lists local
|
|
25
|
+
ones. Never mkdir app folders by hand, and never edit the `id:` line in
|
|
26
|
+
`monty.config.ts`.
|
|
27
|
+
2. **The loop:** `monty create <slug> --name "Name" --icon <tabler-icon>` →
|
|
25
28
|
(if the prompt includes a `build id`, pass it: `--build <id>` — the
|
|
26
29
|
workspace's New app screen tracks your progress live) →
|
|
27
30
|
`monty install` → edit `monty.config.ts` (zod tables) + `src/routes/` →
|
|
28
|
-
verify
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
running
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
verify in Studio: run `monty dev` once — if the dev shell is already
|
|
32
|
+
running (the Monty desktop usually runs it for you) it prints the status,
|
|
33
|
+
Studio URL, and recent log lines, then **exits immediately**; if nothing
|
|
34
|
+
is running it starts the shell (start it in the background and move on).
|
|
35
|
+
Then iterate: edit code → vite hot-reloads → `monty logs -n 50` shows
|
|
36
|
+
whether it compiled and any browser errors. **You are done when
|
|
37
|
+
`monty dev` reports `state: online` (or prints the Studio URL on a fresh
|
|
38
|
+
start) and the app works.** Re-running `monty dev` is always safe — it
|
|
39
|
+
attaches, prints status, and exits. Never try to run a second dev
|
|
40
|
+
*server* for the same app (attach handles this for you) and never kill a
|
|
41
|
+
dev shell you didn't start; `monty dev --takeover` is the only sanctioned
|
|
42
|
+
restart when a session is wedged. Taking the app Live is the OWNER'S
|
|
43
|
+
click (Publish in the workspace menu bar); **never run `monty deploy`
|
|
44
|
+
yourself** unless the user explicitly asks for a direct Live deploy.
|
|
34
45
|
3. **Everything through the CLI.** `monty install`, `monty build`,
|
|
35
46
|
`monty typecheck`, `monty dev`, `monty deploy` — never run vite, tsc,
|
|
36
47
|
pnpm, or npm scripts directly. `monty dev` auto-picks a free port and
|
|
37
|
-
prints it; `monty typecheck` builds first when needed.
|
|
48
|
+
prints it; `monty typecheck` builds first when needed. `monty logs`
|
|
49
|
+
(add `-f` to follow) is how you read the dev shell's output — vite build
|
|
50
|
+
errors, browser errors, and publish results all land there.
|
|
38
51
|
4. **One import surface:** `@montytools/sdk` (`defineApp`, zod) and
|
|
39
52
|
`@montytools/sdk/react` (hooks: `useList`, `useInsert`, …). Never import
|
|
40
53
|
Clerk or Convex directly; never fetch external APIs from app code — the
|
|
@@ -46,7 +59,7 @@ mean something else on this platform.
|
|
|
46
59
|
7. **Errors are instructions.** Every failure prints
|
|
47
60
|
`[MontyError CODE] Fix: …` — do exactly what the Fix says; don't guess.
|
|
48
61
|
Typecheck failures block deploy by design.
|
|
49
|
-
8. **Verify before publish.**
|
|
62
|
+
8. **Verify before publish.** The dev shell writes to the `#dev` Studio
|
|
50
63
|
sandbox — Live team records are never touched, so exercise the app for
|
|
51
64
|
real.
|
|
52
65
|
|
|
@@ -55,10 +68,11 @@ mean something else on this platform.
|
|
|
55
68
|
| command | purpose |
|
|
56
69
|
|---|---|
|
|
57
70
|
| `monty login` | browser sign-in (loopback authorize), once per machine |
|
|
58
|
-
| `monty create <slug>` |
|
|
71
|
+
| `monty create <slug>` | register the app in the workspace + stamp it into `~/.monty/apps/<id>` (needs login) |
|
|
59
72
|
| `monty current` / `select` / `apps` | where am I / jump to app / list local |
|
|
60
73
|
| `monty install` / `build` / `typecheck` | full lifecycle via the CLI — no raw pnpm/vite/tsc |
|
|
61
|
-
| `monty dev` | run the app in Studio (auto-
|
|
74
|
+
| `monty dev` | run the app in Studio, or attach to an already-running session (auto-port, sandboxed data, auto-auth) |
|
|
75
|
+
| `monty logs [-n N] [-f]` | read/follow the dev shell log — the debugging window after every edit |
|
|
62
76
|
| `monty add <name…>` | install curated shadcn components |
|
|
63
77
|
| `monty deploy` | build + typecheck + upload straight to Live (owner escape hatch) |
|
|
64
78
|
| `monty skills` | (re)install this skill for your agent |
|
package/template/AGENTS.md
CHANGED
|
@@ -10,8 +10,8 @@ Folders are managed for you: this app lives in `~/Monty/<slug>`. `monty current`
|
|
|
10
10
|
|
|
11
11
|
| File | What it is |
|
|
12
12
|
|---|---|
|
|
13
|
-
| `monty.config.ts` | Your data schema — plain zod. The ONLY place data shapes are defined. Also `name` + `icon` (any [
|
|
14
|
-
| `src/routes/` | Your UI — TanStack Router file routes (`index.tsx` = `/`). |
|
|
13
|
+
| `monty.config.ts` | Your data schema — plain zod. The ONLY place data shapes are defined. Also `name` + `icon` (any [Tabler](https://tabler.io/icons) icon name, e.g. `"receipt"`, `"users"`) — Monty renders your app's logo tile from it. |
|
|
14
|
+
| `src/routes/` | Your UI — TanStack Router file routes (`index.tsx` = `/`). The starter `index.tsx` is a blank-canvas placeholder — replace it with the real app. |
|
|
15
15
|
| `src/main.tsx` | Wiring. Do not edit. |
|
|
16
16
|
|
|
17
17
|
## Rules (violations break the app)
|
|
@@ -110,6 +110,63 @@ Every platform error is one line shaped like:
|
|
|
110
110
|
| `UNAUTHENTICATED` / `NO_ACTIVE_WORKSPACE` | App isn't running through the Monty host/dev shell. |
|
|
111
111
|
| `MISSING_ENV` / `NO_PROVIDER` | `.env.local` or the `<MontyProvider>` in `main.tsx` was removed. |
|
|
112
112
|
|
|
113
|
+
## Server code (optional): functions, public endpoints, schedules
|
|
114
|
+
|
|
115
|
+
When logic must not run in the browser (private tables, third-party APIs with
|
|
116
|
+
secret keys, webhooks, clocks), create `server/index.ts` with named async
|
|
117
|
+
exports. Web-standard APIs only (`fetch`, `crypto`, `URL`, …) — no `node:`
|
|
118
|
+
imports; it runs on Cloudflare Workers when Live and inside `monty dev` in
|
|
119
|
+
Studio. Every export gets `ctx`: `ctx.records` (full CRUD on all your
|
|
120
|
+
tables, including ones the UI never exposes), `ctx.secrets` (see below),
|
|
121
|
+
`ctx.viewer` (who called: member session/visitor/schedule/none), and
|
|
122
|
+
`ctx.track()` (emit an event).
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// server/index.ts
|
|
126
|
+
import type { MontyFnContext, MontyPublicRequest } from "@montytools/sdk/server";
|
|
127
|
+
|
|
128
|
+
// 1) App-called: invoke from the UI with useServerFn.
|
|
129
|
+
export async function score(args: { sessionId: string }, ctx: MontyFnContext) {
|
|
130
|
+
const weights = await ctx.records.list("scoring"); // hidden table — fine here
|
|
131
|
+
return { verdict: weights.length > 0 ? "ok" : "empty" };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 2) PUBLIC endpoint — declared in monty.config.ts `publicFns: ["stripe"]`,
|
|
135
|
+
// then ANYONE on the internet can call GET/POST /__monty/public/stripe.
|
|
136
|
+
// Verify a signature from ctx.secrets against the RAW req.body before
|
|
137
|
+
// trusting anything; return { status, body?, contentType? } to control the
|
|
138
|
+
// response (or return data for 200 JSON, or nothing for {"ok":true}).
|
|
139
|
+
export async function stripe(req: MontyPublicRequest, ctx: MontyFnContext) {
|
|
140
|
+
if (!verify(req.headers["stripe-signature"], req.body, ctx.secrets.STRIPE_WEBHOOK_SECRET)) {
|
|
141
|
+
return { status: 401, body: "bad signature" };
|
|
142
|
+
}
|
|
143
|
+
await ctx.records.insert("payments", JSON.parse(req.body));
|
|
144
|
+
return { status: 200, body: "ok" };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 3) SCHEDULED — declared in monty.config.ts `schedule: { digest: "0 9 * * *" }`
|
|
148
|
+
// (5-field cron, UTC; max 3). Runs as digest({}, ctx) with
|
|
149
|
+
// ctx.viewer = { lane: "schedule", cron }. Make it idempotent — re-runs happen.
|
|
150
|
+
export async function digest(_args: Record<string, unknown>, ctx: MontyFnContext) { /* … */ }
|
|
151
|
+
|
|
152
|
+
// 4) onEvent — reserved name: the platform calls it after every accepted
|
|
153
|
+
// track() event. Forward to Slack/Meta/PostHog/your CRM here, with your keys.
|
|
154
|
+
export async function onEvent(event: { name: string }, ctx: MontyFnContext) { /* … */ }
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
From the UI: `const score = useServerFn<Result>(app, "score"); await score({ sessionId })`.
|
|
158
|
+
Public functions are NOT callable through `useServerFn` — only at their
|
|
159
|
+
`/__monty/public/<name>` URL (and vice versa: undeclared functions are never
|
|
160
|
+
public).
|
|
161
|
+
|
|
162
|
+
**Secrets:** `monty secret set STRIPE_KEY` stores a key for Live (write-only,
|
|
163
|
+
never in code or config). In Studio, put the same names in the gitignored
|
|
164
|
+
`.monty/secrets.json`. Both arrive as `ctx.secrets.STRIPE_KEY`.
|
|
165
|
+
|
|
166
|
+
**Studio behavior:** `monty dev` runs your schedules for real (a `cron:` line
|
|
167
|
+
prints per firing, UTC) and serves public functions at
|
|
168
|
+
`http://localhost:<port>/__monty/public/<name>` — curl them to test.
|
|
169
|
+
|
|
113
170
|
## Dev loop
|
|
114
171
|
|
|
115
172
|
Everything goes through the `monty` CLI — never run vite, tsc, pnpm, or npm
|
package/template/index.html
CHANGED
package/template/monty.config.ts
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
1
|
import { defineApp } from "@montytools/sdk";
|
|
3
2
|
|
|
4
3
|
// This file is the ONLY place data shapes are defined. Edit it, save, and the
|
|
5
|
-
// typed hooks in your components update immediately.
|
|
4
|
+
// typed hooks in your components update immediately. The starter ships with no
|
|
5
|
+
// tables — declare each one as a zod object as the app needs it, e.g.:
|
|
6
|
+
//
|
|
7
|
+
// import { z } from "zod";
|
|
8
|
+
// tables: {
|
|
9
|
+
// tasks: z.object({
|
|
10
|
+
// title: z.string().min(1),
|
|
11
|
+
// done: z.boolean().default(false),
|
|
12
|
+
// }),
|
|
13
|
+
// },
|
|
6
14
|
export const app = defineApp({
|
|
7
|
-
slug: "
|
|
8
|
-
name: "
|
|
9
|
-
icon: "
|
|
10
|
-
tables: {
|
|
11
|
-
expenses: z.object({
|
|
12
|
-
title: z.string().min(1),
|
|
13
|
-
amount: z.number().positive(),
|
|
14
|
-
status: z.enum(["draft", "submitted", "approved"]).default("draft"),
|
|
15
|
-
assigneeId: z.string().optional(), // a workspace member's userId (from useMembers)
|
|
16
|
-
}),
|
|
17
|
-
},
|
|
15
|
+
slug: "new-app",
|
|
16
|
+
name: "New app",
|
|
17
|
+
icon: "layout-grid",
|
|
18
|
+
tables: {},
|
|
18
19
|
});
|
|
19
20
|
|
|
20
21
|
export type App = typeof app;
|
package/template/package.json
CHANGED
|
@@ -1,263 +1,38 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
2
1
|
import { createFileRoute } from "@tanstack/react-router";
|
|
3
|
-
import {
|
|
2
|
+
import { Sparkles } from "lucide-react";
|
|
4
3
|
|
|
5
|
-
import {
|
|
6
|
-
useInsert,
|
|
7
|
-
useList,
|
|
8
|
-
useMembers,
|
|
9
|
-
useRemove,
|
|
10
|
-
useUpdate,
|
|
11
|
-
} from "@montytools/sdk/react";
|
|
12
|
-
import { Badge } from "@/components/ui/badge";
|
|
13
|
-
import { Button } from "@/components/ui/button";
|
|
14
|
-
import {
|
|
15
|
-
Card,
|
|
16
|
-
CardContent,
|
|
17
|
-
CardHeader,
|
|
18
|
-
CardTitle,
|
|
19
|
-
} from "@/components/ui/card";
|
|
20
4
|
import {
|
|
21
5
|
Empty,
|
|
22
6
|
EmptyDescription,
|
|
23
7
|
EmptyHeader,
|
|
8
|
+
EmptyMedia,
|
|
24
9
|
EmptyTitle,
|
|
25
10
|
} from "@/components/ui/empty";
|
|
26
|
-
import {
|
|
27
|
-
Field,
|
|
28
|
-
FieldError,
|
|
29
|
-
FieldGroup,
|
|
30
|
-
FieldLabel,
|
|
31
|
-
} from "@/components/ui/field";
|
|
32
|
-
import { Input } from "@/components/ui/input";
|
|
33
|
-
import {
|
|
34
|
-
Select,
|
|
35
|
-
SelectContent,
|
|
36
|
-
SelectGroup,
|
|
37
|
-
SelectItem,
|
|
38
|
-
SelectTrigger,
|
|
39
|
-
SelectValue,
|
|
40
|
-
} from "@/components/ui/select";
|
|
41
|
-
import { Spinner } from "@/components/ui/spinner";
|
|
42
|
-
import {
|
|
43
|
-
Table,
|
|
44
|
-
TableBody,
|
|
45
|
-
TableCell,
|
|
46
|
-
TableHead,
|
|
47
|
-
TableHeader,
|
|
48
|
-
TableRow,
|
|
49
|
-
} from "@/components/ui/table";
|
|
50
11
|
|
|
51
12
|
import { app } from "../../monty.config";
|
|
52
13
|
|
|
53
14
|
export const Route = createFileRoute("/")({
|
|
54
|
-
component:
|
|
15
|
+
component: BlankCanvas,
|
|
55
16
|
});
|
|
56
17
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
approved: null,
|
|
62
|
-
};
|
|
63
|
-
const STATUS_VARIANT = {
|
|
64
|
-
draft: "secondary",
|
|
65
|
-
submitted: "default",
|
|
66
|
-
approved: "outline",
|
|
67
|
-
} as const;
|
|
68
|
-
|
|
69
|
-
function ExpenseBoard() {
|
|
70
|
-
const [statusFilter, setStatusFilter] = useState<Status | "all">("all");
|
|
71
|
-
|
|
72
|
-
// Live, typed, reactive — a teammate's write re-renders this list.
|
|
73
|
-
const { data: expenses, status, loadMore } = useList(app, "expenses", {
|
|
74
|
-
filter: statusFilter === "all" ? undefined : { status: statusFilter },
|
|
75
|
-
order: "desc",
|
|
76
|
-
limit: 50,
|
|
77
|
-
});
|
|
78
|
-
const members = useMembers();
|
|
79
|
-
const insert = useInsert(app, "expenses");
|
|
80
|
-
const update = useUpdate(app, "expenses");
|
|
81
|
-
const remove = useRemove(app, "expenses");
|
|
82
|
-
|
|
83
|
-
const [title, setTitle] = useState("");
|
|
84
|
-
const [amount, setAmount] = useState("");
|
|
85
|
-
// "__none" sentinel: a controlled Radix Select must never flip to undefined
|
|
86
|
-
// (it goes uncontrolled and keeps displaying the stale selection).
|
|
87
|
-
const [assigneeId, setAssigneeId] = useState("__none");
|
|
88
|
-
const [formError, setFormError] = useState<string | null>(null);
|
|
89
|
-
|
|
90
|
-
async function addExpense() {
|
|
91
|
-
setFormError(null);
|
|
92
|
-
try {
|
|
93
|
-
await insert({
|
|
94
|
-
title,
|
|
95
|
-
amount: Number(amount),
|
|
96
|
-
...(assigneeId !== "__none" ? { assigneeId } : {}),
|
|
97
|
-
});
|
|
98
|
-
setTitle("");
|
|
99
|
-
setAmount("");
|
|
100
|
-
setAssigneeId("__none");
|
|
101
|
-
} catch (error) {
|
|
102
|
-
setFormError(error instanceof Error ? error.message : String(error));
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const memberName = (userId?: string) =>
|
|
107
|
-
members?.find((m) => m.userId === userId)?.name ?? "—";
|
|
108
|
-
|
|
18
|
+
// The starter ships empty on purpose: this page is what the person watching
|
|
19
|
+
// the preview sees while their agent builds the first version. Replace this
|
|
20
|
+
// whole route with the real app — don't build around it.
|
|
21
|
+
function BlankCanvas() {
|
|
109
22
|
return (
|
|
110
|
-
<main className="
|
|
111
|
-
<
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
<
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
<SelectItem value="draft">Draft</SelectItem>
|
|
124
|
-
<SelectItem value="submitted">Submitted</SelectItem>
|
|
125
|
-
<SelectItem value="approved">Approved</SelectItem>
|
|
126
|
-
</SelectGroup>
|
|
127
|
-
</SelectContent>
|
|
128
|
-
</Select>
|
|
129
|
-
</header>
|
|
130
|
-
|
|
131
|
-
<Card>
|
|
132
|
-
<CardHeader>
|
|
133
|
-
<CardTitle>Add expense</CardTitle>
|
|
134
|
-
</CardHeader>
|
|
135
|
-
<CardContent>
|
|
136
|
-
<FieldGroup>
|
|
137
|
-
<div className="flex flex-wrap items-end gap-4">
|
|
138
|
-
<Field className="min-w-48 flex-1">
|
|
139
|
-
<FieldLabel htmlFor="title">Title</FieldLabel>
|
|
140
|
-
<Input
|
|
141
|
-
id="title"
|
|
142
|
-
value={title}
|
|
143
|
-
onChange={(e) => setTitle(e.target.value)}
|
|
144
|
-
placeholder="Team lunch"
|
|
145
|
-
/>
|
|
146
|
-
</Field>
|
|
147
|
-
<Field className="w-32">
|
|
148
|
-
<FieldLabel htmlFor="amount">Amount</FieldLabel>
|
|
149
|
-
<Input
|
|
150
|
-
id="amount"
|
|
151
|
-
type="number"
|
|
152
|
-
value={amount}
|
|
153
|
-
onChange={(e) => setAmount(e.target.value)}
|
|
154
|
-
placeholder="42.50"
|
|
155
|
-
/>
|
|
156
|
-
</Field>
|
|
157
|
-
<Field className="w-48">
|
|
158
|
-
<FieldLabel>Assignee</FieldLabel>
|
|
159
|
-
<Select value={assigneeId} onValueChange={setAssigneeId}>
|
|
160
|
-
<SelectTrigger>
|
|
161
|
-
<SelectValue placeholder="Unassigned" />
|
|
162
|
-
</SelectTrigger>
|
|
163
|
-
<SelectContent>
|
|
164
|
-
<SelectGroup>
|
|
165
|
-
<SelectItem value="__none">Unassigned</SelectItem>
|
|
166
|
-
{(members ?? []).map((m) => (
|
|
167
|
-
<SelectItem key={m.userId} value={m.userId}>
|
|
168
|
-
{m.name}
|
|
169
|
-
</SelectItem>
|
|
170
|
-
))}
|
|
171
|
-
</SelectGroup>
|
|
172
|
-
</SelectContent>
|
|
173
|
-
</Select>
|
|
174
|
-
</Field>
|
|
175
|
-
<Button onClick={addExpense}>
|
|
176
|
-
<PlusIcon data-icon="inline-start" />
|
|
177
|
-
Add
|
|
178
|
-
</Button>
|
|
179
|
-
</div>
|
|
180
|
-
{formError && <FieldError>{formError}</FieldError>}
|
|
181
|
-
</FieldGroup>
|
|
182
|
-
</CardContent>
|
|
183
|
-
</Card>
|
|
184
|
-
|
|
185
|
-
{status === "LoadingFirstPage" && expenses.length === 0 ? (
|
|
186
|
-
<div className="flex justify-center p-12">
|
|
187
|
-
<Spinner />
|
|
188
|
-
</div>
|
|
189
|
-
) : expenses.length === 0 ? (
|
|
190
|
-
<Empty>
|
|
191
|
-
<EmptyHeader>
|
|
192
|
-
<EmptyTitle>No expenses yet</EmptyTitle>
|
|
193
|
-
<EmptyDescription>
|
|
194
|
-
Add the first one above — it appears for every teammate in real
|
|
195
|
-
time.
|
|
196
|
-
</EmptyDescription>
|
|
197
|
-
</EmptyHeader>
|
|
198
|
-
</Empty>
|
|
199
|
-
) : (
|
|
200
|
-
<Card>
|
|
201
|
-
<CardContent>
|
|
202
|
-
<Table>
|
|
203
|
-
<TableHeader>
|
|
204
|
-
<TableRow>
|
|
205
|
-
<TableHead>Title</TableHead>
|
|
206
|
-
<TableHead>Amount</TableHead>
|
|
207
|
-
<TableHead>Status</TableHead>
|
|
208
|
-
<TableHead>Assignee</TableHead>
|
|
209
|
-
<TableHead className="w-40" />
|
|
210
|
-
</TableRow>
|
|
211
|
-
</TableHeader>
|
|
212
|
-
<TableBody>
|
|
213
|
-
{expenses.map((e) => (
|
|
214
|
-
<TableRow key={e._id}>
|
|
215
|
-
<TableCell className="font-medium">{e.title}</TableCell>
|
|
216
|
-
<TableCell>${e.amount.toFixed(2)}</TableCell>
|
|
217
|
-
<TableCell>
|
|
218
|
-
<Badge variant={STATUS_VARIANT[e.status]}>
|
|
219
|
-
{e.status}
|
|
220
|
-
</Badge>
|
|
221
|
-
</TableCell>
|
|
222
|
-
<TableCell>{memberName(e.assigneeId)}</TableCell>
|
|
223
|
-
<TableCell>
|
|
224
|
-
<div className="flex justify-end gap-2">
|
|
225
|
-
{NEXT_STATUS[e.status] && (
|
|
226
|
-
<Button
|
|
227
|
-
variant="outline"
|
|
228
|
-
size="sm"
|
|
229
|
-
onClick={() =>
|
|
230
|
-
update(e._id, { status: NEXT_STATUS[e.status]! })
|
|
231
|
-
}
|
|
232
|
-
>
|
|
233
|
-
<CheckIcon data-icon="inline-start" />
|
|
234
|
-
{NEXT_STATUS[e.status]}
|
|
235
|
-
</Button>
|
|
236
|
-
)}
|
|
237
|
-
<Button
|
|
238
|
-
variant="ghost"
|
|
239
|
-
size="sm"
|
|
240
|
-
onClick={() => remove(e._id)}
|
|
241
|
-
>
|
|
242
|
-
<TrashIcon data-icon="inline-start" />
|
|
243
|
-
Delete
|
|
244
|
-
</Button>
|
|
245
|
-
</div>
|
|
246
|
-
</TableCell>
|
|
247
|
-
</TableRow>
|
|
248
|
-
))}
|
|
249
|
-
</TableBody>
|
|
250
|
-
</Table>
|
|
251
|
-
{status === "CanLoadMore" && (
|
|
252
|
-
<div className="flex justify-center pt-4">
|
|
253
|
-
<Button variant="ghost" onClick={() => loadMore()}>
|
|
254
|
-
Load more
|
|
255
|
-
</Button>
|
|
256
|
-
</div>
|
|
257
|
-
)}
|
|
258
|
-
</CardContent>
|
|
259
|
-
</Card>
|
|
260
|
-
)}
|
|
23
|
+
<main className="grid min-h-dvh place-items-center p-6">
|
|
24
|
+
<Empty>
|
|
25
|
+
<EmptyHeader>
|
|
26
|
+
<EmptyMedia variant="icon">
|
|
27
|
+
<Sparkles />
|
|
28
|
+
</EmptyMedia>
|
|
29
|
+
<EmptyTitle>{app.name} is a blank canvas</EmptyTitle>
|
|
30
|
+
<EmptyDescription>
|
|
31
|
+
Your agent builds the first version right on this page — changes
|
|
32
|
+
appear live as it works.
|
|
33
|
+
</EmptyDescription>
|
|
34
|
+
</EmptyHeader>
|
|
35
|
+
</Empty>
|
|
261
36
|
</main>
|
|
262
37
|
);
|
|
263
38
|
}
|