@montytools/cli 0.5.5 → 0.5.6
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 +289 -324
- package/lib/schemaCodegen.mjs +7 -3
- package/lib/schemaPull.mjs +36 -50
- package/lib/styleLint.mjs +175 -0
- package/lib/tokens-manifest.mjs +90 -0
- package/package.json +2 -2
- package/skills/monty-build/SKILL.md +7 -5
- package/skills/monty-design/SKILL.md +104 -55
- package/template/AGENTS.md +47 -28
- package/template/index.html +5 -5
- package/template/package.json +2 -2
- package/template/src/components/ui/badge.tsx +1 -1
- package/template/src/components/ui/button.tsx +7 -7
- package/template/src/components/ui/card.tsx +3 -3
- package/template/src/components/ui/empty.tsx +3 -3
- package/template/src/components/ui/field.tsx +5 -5
- package/template/src/components/ui/input.tsx +1 -1
- package/template/src/components/ui/label.tsx +1 -1
- package/template/src/components/ui/select.tsx +4 -4
- package/template/src/components/ui/table.tsx +3 -3
- package/template/src/index.css +7 -118
- package/template/src/main.tsx +1 -1
- package/template/src/monty.gen.ts +16 -0
- package/template/src/routes/index.tsx +1 -1
- package/template/tsconfig.json +1 -1
- package/template/monty.config.ts +0 -21
|
@@ -1,45 +1,92 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: monty-design
|
|
3
|
-
description: Design Monty app pages — Lyra shadcn styling (
|
|
3
|
+
description: Design Monty app pages — the Monty design tokens (type ladder, radius policy, elevation), Lyra shadcn styling (sharp surfaces, stock components only), dark-only theming, and the Monty chart language (square marks on real axes). Use whenever building or restyling UI in a Monty app; pages, dashboards, charts, stat tiles, KPI rows, tables, or any prompt about how a Monty app should look.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Designing Monty pages
|
|
7
7
|
|
|
8
8
|
Monty is a B2B work OS. Apps render inside the platform shell, so a page is
|
|
9
|
-
"designed" when it
|
|
10
|
-
The design system is already installed —
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
`
|
|
24
|
-
- Sanctioned overrides are content-level only:
|
|
25
|
-
on a
|
|
9
|
+
"designed" when it is indistinguishable from the shell around it: quiet,
|
|
10
|
+
rectilinear, data-forward. The design system is already installed — the shell
|
|
11
|
+
and every app share ONE token sheet (`@montytools/sdk/tokens.css`, imported
|
|
12
|
+
by `src/index.css`) and one component surface. Your job is to NOT fight it.
|
|
13
|
+
|
|
14
|
+
## The one law: tokens and stock components, nothing invented
|
|
15
|
+
|
|
16
|
+
- Use components as they come: `Card`/`CardHeader`/`CardTitle`/
|
|
17
|
+
`CardDescription`/`CardContent`, `Table`, `Badge`, `Button`, … Never
|
|
18
|
+
rebuild a surface as a styled `div` — a hand-rolled `rounded-xl border`
|
|
19
|
+
card is the canonical mistake.
|
|
20
|
+
- Never write a raw value the tokens already name: no hex/oklch colors, no
|
|
21
|
+
`text-[13px]`, no `rounded-[10px]`, no hand-written `shadow-[…]`.
|
|
22
|
+
Semantic utilities only (`bg-background`, `text-muted-foreground`,
|
|
23
|
+
`var(--chart-2)`, `shadow-control`, …).
|
|
24
|
+
- Sanctioned overrides are content-level only: `tabular-nums` on values, a
|
|
25
|
+
width on a label column, `capitalize` on an enum. If an override styles a
|
|
26
26
|
SURFACE, you are off the system.
|
|
27
27
|
|
|
28
|
+
## The type ladder — six styles, and fewer is better
|
|
29
|
+
|
|
30
|
+
One class per role; each carries its size, line-height, weight, and
|
|
31
|
+
tracking. **Hierarchy comes from weight and ink, never from inventing
|
|
32
|
+
sizes.**
|
|
33
|
+
|
|
34
|
+
| Class | Size | Use |
|
|
35
|
+
|---|---|---|
|
|
36
|
+
| `text-tick` | 10/12 | chart axis ticks ONLY |
|
|
37
|
+
| `text-meta` | 12/16 | column headers, chips, captions, timestamps, secondary values |
|
|
38
|
+
| `text-body` | 13/20 | THE DEFAULT — content, controls, table cells (already on `body`) |
|
|
39
|
+
| `text-title` | 14/20 · 500 | card titles, page-header titles, nav rows |
|
|
40
|
+
| `text-heading` | 18/24 · 600 | page-level headings (rare — `PageHeader` owns page tops) |
|
|
41
|
+
| `text-stat` | 24/32 · 600 | stat-tile values, always with `tabular-nums` |
|
|
42
|
+
|
|
43
|
+
A typical page uses three: `text-body` (inherited — write nothing),
|
|
44
|
+
`text-meta` for secondary lines, `text-stat` on tile values. Reach for the
|
|
45
|
+
others only when the role in the table matches. Weights: default 400,
|
|
46
|
+
`font-medium` for titled/interactive text; 600 arrives only via
|
|
47
|
+
`text-heading`/`text-stat`. Never `font-bold`, never `text-xs/sm/lg/2xl` —
|
|
48
|
+
those sizes exist for legacy chrome, not for pages.
|
|
49
|
+
|
|
50
|
+
## Radius policy — sharp, everywhere
|
|
51
|
+
|
|
52
|
+
Lyra is rectilinear: surfaces, controls, and overlays are ALL square-
|
|
53
|
+
cornered. Cards/tables/tiles/chips are `rounded-none` with `bg-card ring-1
|
|
54
|
+
ring-foreground/10` hairlines, never borders; buttons, inputs, dialogs, and
|
|
55
|
+
popovers come sharp from the stock components. Elevation and hierarchy come
|
|
56
|
+
from hairlines and the three shadows, never from softened corners.
|
|
57
|
+
`rounded-full` (avatars, status dots) is the ONE exception — write no other
|
|
58
|
+
`rounded-*` class yourself; the semantic names the chrome uses
|
|
59
|
+
(`rounded-control`, `rounded-overlay`) resolve to 0 by policy.
|
|
60
|
+
|
|
61
|
+
## Color and status
|
|
62
|
+
|
|
63
|
+
The palette is the platform's; you never pick colors. Ground `bg-background`,
|
|
64
|
+
surface `bg-card`, ink `text-foreground`/`text-muted-foreground`, quiet fill
|
|
65
|
+
`bg-accent`, the one accent `bg-primary`, links `text-link`. Status is a
|
|
66
|
+
**dot + muted text**, never a colored pill: `text-success`, `text-warning`,
|
|
67
|
+
`text-destructive` for the dot/figure, label in `text-muted-foreground`.
|
|
68
|
+
Elevation: `shadow-control` on rest-state controls, `shadow-btn-primary` on
|
|
69
|
+
the one primary action, `shadow-menu` on overlays — never `shadow-md/lg/xl`.
|
|
70
|
+
|
|
71
|
+
## Spacing — the 4px grid, three names
|
|
72
|
+
|
|
73
|
+
Page bodies are `p-page` (24px); cards pad themselves; card grids are
|
|
74
|
+
`gap-3` (12px); control rows `gap-2`. Stay on the default Tailwind steps
|
|
75
|
+
(`p-1`…`p-6`); if you're typing an arbitrary value like `p-[18px]`, the
|
|
76
|
+
layout is wrong, not the scale.
|
|
77
|
+
|
|
28
78
|
## Dark-only, in the shell's palette
|
|
29
79
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
the template ships dark by default: `class="dark"` on `<html>` in
|
|
35
|
-
`index.html`, and any boot-splash background set to the dark `--background`
|
|
36
|
-
value. Verify your page against the shell, not in isolation.
|
|
80
|
+
Monty is dark-only; the tokens ship the shell palette (#101112 ground) and
|
|
81
|
+
`index.html` carries `class="dark"` plus the dark boot splash. Never
|
|
82
|
+
retheme, never hand-pick a dark color, and verify your page against the
|
|
83
|
+
shell (`/apps/<slug>/…`), not the bare dev port.
|
|
37
84
|
|
|
38
85
|
## Page anatomy
|
|
39
86
|
|
|
40
87
|
Every page opens with the platform chrome from `@montytools/sdk/ui` — the
|
|
41
|
-
SAME components the shell renders system table views with
|
|
42
|
-
|
|
88
|
+
SAME components the shell renders system table views with. Never hand-roll
|
|
89
|
+
the header bar.
|
|
43
90
|
|
|
44
91
|
```tsx
|
|
45
92
|
import { PageHeader, PageHeaderButton } from "@montytools/sdk/ui";
|
|
@@ -50,7 +97,7 @@ import { PageHeader, PageHeaderButton } from "@montytools/sdk/ui";
|
|
|
50
97
|
<PageHeaderButton primary onClick={main}><Plus className="size-3.5" /> New</PageHeaderButton>
|
|
51
98
|
</PageHeader>
|
|
52
99
|
<div className="min-h-0 flex-1 overflow-auto">
|
|
53
|
-
<div className="flex flex-col gap-3 p-
|
|
100
|
+
<div className="flex flex-col gap-3 p-page pt-4">
|
|
54
101
|
{/* KPI row */}
|
|
55
102
|
<div className="grid grid-cols-[repeat(auto-fill,minmax(14rem,1fr))] gap-3">…</div>
|
|
56
103
|
{/* section cards */}
|
|
@@ -72,36 +119,35 @@ A stat tile is a stock Card, nothing more:
|
|
|
72
119
|
<Card size="sm">
|
|
73
120
|
<CardHeader>
|
|
74
121
|
<CardDescription>Leads added</CardDescription>
|
|
75
|
-
<CardTitle className="text-
|
|
76
|
-
<div className="text-
|
|
122
|
+
<CardTitle className="text-stat tabular-nums">{value}</CardTitle>
|
|
123
|
+
<div className="text-meta text-muted-foreground">{delta or context}</div>
|
|
77
124
|
</CardHeader>
|
|
78
125
|
</Card>
|
|
79
126
|
```
|
|
80
127
|
|
|
81
128
|
## Charts: sharp marks on real axes
|
|
82
129
|
|
|
83
|
-
Chart marks are **sharp rectangles — no rounded corners, ever
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
130
|
+
Chart marks are **sharp rectangles — no rounded corners, ever**. The three
|
|
131
|
+
failure modes to avoid: pill "track+fill" bars (read as progress bars),
|
|
132
|
+
rounded caps (misstate where a value ends), and floating bars with no axis
|
|
133
|
+
(nothing anchors the eye).
|
|
87
134
|
|
|
88
135
|
- **Horizontal bars**: grow from a left hairline baseline
|
|
89
136
|
(`border-l border-foreground/25`) over quarter gridlines
|
|
90
137
|
(`absolute left-1/4|1/2|3/4 border-l border-foreground/10` —
|
|
91
138
|
foreground-alpha so hairlines read on any surface), bar `h-4`,
|
|
92
|
-
fill `var(--chart-2)`. Label left in `text-muted-foreground`
|
|
93
|
-
(fixed-width, truncate); count right in `font-medium
|
|
94
|
-
zero values muted with no fill.
|
|
139
|
+
fill `var(--chart-2)`. Label left in `text-body text-muted-foreground`
|
|
140
|
+
(fixed-width, truncate); count right in `text-body font-medium
|
|
141
|
+
tabular-nums`, zero values muted with no fill.
|
|
95
142
|
- **Columns**: a value axis with a baseline (`border-foreground/25`) plus
|
|
96
|
-
hairline gridlines (`border-foreground/10`) and
|
|
97
|
-
right-aligned in a left gutter). Round the axis top to a
|
|
98
|
-
(≤4 exact; else the next multiple of 5/10/50) so ticks
|
|
99
|
-
Direct value labels above non-zero columns.
|
|
100
|
-
- **Color**: one accent ramp
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
never wears the data color.
|
|
143
|
+
hairline gridlines (`border-foreground/10`) and `text-tick` tabular tick
|
|
144
|
+
labels (muted, right-aligned in a left gutter). Round the axis top to a
|
|
145
|
+
"nice" integer (≤4 exact; else the next multiple of 5/10/50) so ticks
|
|
146
|
+
stay honest. Direct `text-tick` value labels above non-zero columns.
|
|
147
|
+
- **Color**: one accent ramp — `var(--chart-2)` for the emphasized series
|
|
148
|
+
(today, the selection), `var(--chart-5)` for context. Never restate the
|
|
149
|
+
values as hex, never a hue per category, and text never wears the data
|
|
150
|
+
color.
|
|
105
151
|
- **Numbers**: `tabular-nums` everywhere values align; money via
|
|
106
152
|
`toLocaleString(undefined, { style: "currency", currency: "USD" })`
|
|
107
153
|
(compact notation on tiles, full in tables).
|
|
@@ -109,15 +155,18 @@ and floating bars with no axis (nothing anchors the eye).
|
|
|
109
155
|
muted line ("No activity logged on this day.") — structure stays, zeros
|
|
110
156
|
carry meaning.
|
|
111
157
|
|
|
112
|
-
Reference implementations: `
|
|
113
|
-
|
|
114
|
-
`demos/crm-v2/src/routes/
|
|
158
|
+
Reference implementations: `demos/crm-v2/src/routes/stats.tsx` (KPI tiles,
|
|
159
|
+
day-selector columns, status bars, top-deals table) and
|
|
160
|
+
`demos/crm-v2/src/routes/reports.tsx` (stat tiles over live records).
|
|
115
161
|
|
|
116
162
|
## Checklist before you call a page done
|
|
117
163
|
|
|
118
|
-
1. No `border`/`rounded-*`
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
164
|
+
1. No `border`/`rounded-*` anywhere you authored (`rounded-full` on dots
|
|
165
|
+
and avatars is the one exception) — surfaces and controls are stock
|
|
166
|
+
components, sharp by policy.
|
|
167
|
+
2. No text class outside the six ladder styles; no arbitrary values
|
|
168
|
+
(`text-[…]`, `bg-[#…]`, `rounded-[…]`, `shadow-[…]`).
|
|
169
|
+
3. No rounded corners on any chart mark; every bar/column sits on an axis.
|
|
170
|
+
4. Dark: the page blends into the shell with no light seams.
|
|
171
|
+
5. Values in tabular figures; status as dot + muted text, never a pill.
|
|
172
|
+
6. Screenshot it inside the shell (`/apps/<slug>/…`), not just the dev port.
|
package/template/AGENTS.md
CHANGED
|
@@ -10,7 +10,7 @@ logic.** Everything below is the complete contract.
|
|
|
10
10
|
|
|
11
11
|
| File | What it is |
|
|
12
12
|
|---|---|
|
|
13
|
-
| `monty.
|
|
13
|
+
| `src/monty.gen.ts` | The app's schema, GENERATED from the workspace config — never edit it (it is overwritten on every sync). Import `{ app }` from it for typed hooks. Change the schema with `monty schema set` (read it first with `monty schema`); a running `monty dev` regenerates this file within a heartbeat. |
|
|
14
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
|
|
|
@@ -31,16 +31,26 @@ logic.** Everything below is the complete contract.
|
|
|
31
31
|
`monty components docs <name>` shows a component's source before installing.
|
|
32
32
|
Icons from `lucide-react`. Don't install other component libraries or
|
|
33
33
|
write raw-color CSS — use semantic tokens (`bg-background`,
|
|
34
|
-
`text-muted-foreground`, …). Don't edit `src/index.css
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
`text-muted-foreground`, …). Don't edit `src/index.css`; the design
|
|
35
|
+
tokens come from `@montytools/sdk/tokens.css` — the same sheet the Monty
|
|
36
|
+
shell runs on, so styling with tokens IS what makes the app look native.
|
|
37
|
+
Text is the six-step ladder (`text-tick/meta/body/title/heading/stat`);
|
|
38
|
+
`text-body` is the default and already on `body`, so most text needs NO
|
|
39
|
+
size class at all — the fewer styles a page states, the more native it
|
|
40
|
+
looks. Never arbitrary values (`text-[15px]`, `bg-[#…]`, `rounded-[…]`).
|
|
41
|
+
The look is Lyra: rectilinear — surfaces AND controls are stock
|
|
42
|
+
components, sharp-cornered by policy (`rounded-full` on dots/avatars is
|
|
43
|
+
the one exception), never hand-styled divs; chart marks are sharp
|
|
37
44
|
rectangles on real axes. The `monty-design` skill is the full spec.
|
|
38
|
-
4. **Schema changes
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
45
|
+
4. **Schema changes go through the door, never a file.** Read the config
|
|
46
|
+
with `monty schema` (pure JSON on stdout), edit it, write it back whole
|
|
47
|
+
with `monty schema set '<json>'` (or pipe: `monty schema set -`) —
|
|
48
|
+
validated, CAS-guarded, live in the workspace within seconds, and
|
|
49
|
+
`src/monty.gen.ts` (your typed view) regenerates automatically while
|
|
50
|
+
`monty dev` runs. Prefer additive changes; give new fields defaults or
|
|
51
|
+
mark them optional so existing records stay readable. Describe fields
|
|
52
|
+
for the next agent that fills them in (`description` on the field spec;
|
|
53
|
+
enum options say WHEN they apply, not what the word means).
|
|
44
54
|
5. **You are not done until the work is saved.** After every meaningful
|
|
45
55
|
change verified in dev, run `monty save "<what changed>"` — it builds,
|
|
46
56
|
typechecks, and pushes the working copy to the cloud copy, like
|
|
@@ -48,8 +58,12 @@ logic.** Everything below is the complete contract.
|
|
|
48
58
|
|
|
49
59
|
## Data: define, then use
|
|
50
60
|
|
|
61
|
+
`src/monty.gen.ts` mirrors the workspace config as exactly this shape —
|
|
62
|
+
zod tables wrapped in `defineApp` — so this is what your typed `app`
|
|
63
|
+
object looks like (generated; declare the real thing via `monty schema set`):
|
|
64
|
+
|
|
51
65
|
```ts
|
|
52
|
-
// monty.
|
|
66
|
+
// src/monty.gen.ts (GENERATED — read, never edit)
|
|
53
67
|
import { defineApp, montyFileSchema, montySelect } from "@montytools/sdk";
|
|
54
68
|
import { z } from "zod";
|
|
55
69
|
|
|
@@ -73,7 +87,7 @@ export const app = defineApp({
|
|
|
73
87
|
|
|
74
88
|
```tsx
|
|
75
89
|
import { useList, useRecord, useInsert, useUpdate, useRemove, useMembers, useUploadFile, useFileUrl } from "@montytools/sdk/react";
|
|
76
|
-
import { app } from "
|
|
90
|
+
import { app } from "../monty.gen";
|
|
77
91
|
|
|
78
92
|
const { data, status, loadMore } = useList(app, "expenses", {
|
|
79
93
|
filter: { status: "submitted" }, // equality on schema fields only
|
|
@@ -181,14 +195,14 @@ Rules that matter:
|
|
|
181
195
|
```tsx
|
|
182
196
|
import { PageHeader, PageHeaderButton } from "@montytools/sdk/ui";
|
|
183
197
|
|
|
184
|
-
<div className="flex h-full min-h-dvh flex-col">
|
|
198
|
+
<div className="flex h-full min-h-dvh flex-col bg-background">
|
|
185
199
|
<PageHeader icon={ChartColumn} title="Reports" meta="42 rows">
|
|
186
200
|
<PageHeaderButton onClick={exportCsv}>Export</PageHeaderButton>
|
|
187
201
|
<PageHeaderButton primary onClick={openNew}>
|
|
188
202
|
<Plus className="size-3.5" /> New
|
|
189
203
|
</PageHeaderButton>
|
|
190
204
|
</PageHeader>
|
|
191
|
-
<main className="min-h-0 flex-1 overflow-auto p-
|
|
205
|
+
<main className="min-h-0 flex-1 overflow-auto p-page">…</main>
|
|
192
206
|
</div>
|
|
193
207
|
```
|
|
194
208
|
|
|
@@ -235,7 +249,7 @@ Every platform error is one line shaped like:
|
|
|
235
249
|
| Code | Meaning |
|
|
236
250
|
|---|---|
|
|
237
251
|
| `VALIDATION` | Payload doesn't match your zod schema (unknown fields are also an error) — fix the payload or the schema. |
|
|
238
|
-
| `UNKNOWN_TABLE` | Table name not in
|
|
252
|
+
| `UNKNOWN_TABLE` | Table name not in the workspace config's `tables` (`monty schema` shows it). |
|
|
239
253
|
| `NOT_FOUND` | Stale, foreign, or WRONG-TABLE record id — ids come from `useList`/`useRecord` on the same table; never hard-code or mix them. |
|
|
240
254
|
| `INVALID_SCHEMA` | A table field uses a reserved name (`_*`, `updatedAt`, `createdBy`) — rename it. |
|
|
241
255
|
| `SCHEMA_DRIFT` (warning) | Stored rows predate your latest schema change; nothing crashes, but make changed fields `.optional()`/`.default(...)`. |
|
|
@@ -243,7 +257,7 @@ Every platform error is one line shaped like:
|
|
|
243
257
|
| `UNAUTHENTICATED` / `NO_ACTIVE_WORKSPACE` | App isn't running through the Monty host/dev shell. |
|
|
244
258
|
| `MISSING_ENV` / `NO_PROVIDER` | `.env.local` or the `<MontyProvider>` in `main.tsx` was removed. |
|
|
245
259
|
|
|
246
|
-
## Server code (optional): functions,
|
|
260
|
+
## Server code (optional): functions, shared addresses, clock rules
|
|
247
261
|
|
|
248
262
|
When logic must not run in the browser (private tables, third-party APIs with
|
|
249
263
|
secret keys, webhooks, clocks), create `server/index.ts` with named async
|
|
@@ -254,7 +268,7 @@ tables, including ones the UI never exposes), `ctx.files` (platform file
|
|
|
254
268
|
storage: `upload(data, {name?, contentType?}) → MontyFile`,
|
|
255
269
|
`download(fileOrId) → Blob`, `remove(fileOrId)` — same descriptors as the
|
|
256
270
|
`useUploadFile` hook, 10MB cap), `ctx.secrets` (see below), `ctx.viewer`
|
|
257
|
-
(who called: member session/visitor/
|
|
271
|
+
(who called: member session/visitor/rule/none), and `ctx.track()`
|
|
258
272
|
(emit an event).
|
|
259
273
|
|
|
260
274
|
```ts
|
|
@@ -267,8 +281,10 @@ export async function score(args: { sessionId: string }, ctx: MontyFnContext) {
|
|
|
267
281
|
return { verdict: weights.length > 0 ? "ok" : "empty" };
|
|
268
282
|
}
|
|
269
283
|
|
|
270
|
-
// 2)
|
|
271
|
-
//
|
|
284
|
+
// 2) SHARED OUTWARD — after `monty save` ships it, run
|
|
285
|
+
// `monty public set stripe` (the whole list; it replaces) and ANYONE on
|
|
286
|
+
// the internet can call GET/POST /__monty/public/stripe — instantly, no
|
|
287
|
+
// second save.
|
|
272
288
|
// Verify a signature from ctx.secrets against the RAW req.body before
|
|
273
289
|
// trusting anything; return { status, body?, contentType? } to control the
|
|
274
290
|
// response (or return data for 200 JSON, or nothing for {"ok":true}).
|
|
@@ -280,10 +296,12 @@ export async function stripe(req: MontyPublicRequest, ctx: MontyFnContext) {
|
|
|
280
296
|
return { status: 200, body: "ok" };
|
|
281
297
|
}
|
|
282
298
|
|
|
283
|
-
// 3)
|
|
284
|
-
// (
|
|
285
|
-
//
|
|
286
|
-
|
|
299
|
+
// 3) ON THE CLOCK — wire an `every` rule after the save ships the function
|
|
300
|
+
// (rules door / MCP rules_update): { name: "daily-digest", on: "every",
|
|
301
|
+
// cron: "0 9 * * *", run: "digest" } (5-field cron, UTC; max 3 every
|
|
302
|
+
// rules). The platform fires it as digest({ event }, ctx) with
|
|
303
|
+
// event = { type: "every", cron }. Make it idempotent — re-runs happen.
|
|
304
|
+
export async function digest(args: { event?: { cron?: string } }, ctx: MontyFnContext) { /* … */ }
|
|
287
305
|
|
|
288
306
|
// 4) onEvent — reserved name: the platform calls it after every accepted
|
|
289
307
|
// track() event. Forward to Slack/Meta/PostHog/your CRM here, with your keys.
|
|
@@ -291,16 +309,17 @@ export async function onEvent(event: { name: string }, ctx: MontyFnContext) { /*
|
|
|
291
309
|
```
|
|
292
310
|
|
|
293
311
|
From the UI: `const score = useServerFn<Result>(app, "score"); await score({ sessionId })`.
|
|
294
|
-
|
|
295
|
-
`/__monty/public/<name>` URL (and vice versa:
|
|
296
|
-
public).
|
|
312
|
+
Shared (public) functions are NOT callable through `useServerFn` — only at
|
|
313
|
+
their `/__monty/public/<name>` URL (and vice versa: unshared functions are
|
|
314
|
+
never public). `monty public` lists what is shared.
|
|
297
315
|
|
|
298
316
|
**Secrets:** `monty secret set STRIPE_KEY` stores a key for Live (write-only,
|
|
299
317
|
never in code or config). In a dev session, put the same names in the
|
|
300
318
|
gitignored `.monty/secrets.json`. Both arrive as `ctx.secrets.STRIPE_KEY`.
|
|
301
319
|
|
|
302
|
-
**Session behavior:** `monty dev` runs
|
|
303
|
-
|
|
320
|
+
**Session behavior:** while `monty dev` runs, the platform dispatches your
|
|
321
|
+
`every` rules to this session (each firing journals in the app's rule runs)
|
|
322
|
+
and serves shared functions at
|
|
304
323
|
`http://localhost:<port>/__monty/public/<name>` — curl them to test.
|
|
305
324
|
|
|
306
325
|
## Dev loop
|
package/template/index.html
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
|
-
<html lang="en">
|
|
2
|
+
<html lang="en" class="dark">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
<div id="root">
|
|
10
10
|
<!-- Boot splash: the first paint, shown while the JS loads. React
|
|
11
11
|
replaces it with the SDK's identical MontySplash, so a cold open
|
|
12
|
-
is one continuous visual.
|
|
13
|
-
|
|
12
|
+
is one continuous visual. The background matches the platform's
|
|
13
|
+
dark --background (raw hex is unavoidable pre-CSS). -->
|
|
14
14
|
<style>
|
|
15
15
|
@keyframes monty-pulse { 0%, 100% { opacity: 1 } 50% { opacity: .25 } }
|
|
16
16
|
</style>
|
|
17
|
-
<div style="display: grid; place-items: center; min-height: 100vh; background:
|
|
18
|
-
<div style="width: 10px; height: 10px; border-radius: 999px; background: #
|
|
17
|
+
<div style="display: grid; place-items: center; min-height: 100vh; background: #101112">
|
|
18
|
+
<div style="width: 10px; height: 10px; border-radius: 999px; background: #666; animation: monty-pulse 1.2s ease-in-out infinite"></div>
|
|
19
19
|
</div>
|
|
20
20
|
</div>
|
|
21
21
|
<script type="module" src="/src/main.tsx"></script>
|
package/template/package.json
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"typecheck": "tsc --noEmit"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@fontsource-variable/
|
|
13
|
-
"@montytools/sdk": "^0.2.
|
|
12
|
+
"@fontsource-variable/inter": "^5.3.0",
|
|
13
|
+
"@montytools/sdk": "^0.2.6",
|
|
14
14
|
"@tanstack/react-router": "1.170.17",
|
|
15
15
|
"class-variance-authority": "^0.7.1",
|
|
16
16
|
"clsx": "^2.1.1",
|
|
@@ -5,7 +5,7 @@ import { Slot } from "radix-ui"
|
|
|
5
5
|
import { cn } from "@/lib/utils"
|
|
6
6
|
|
|
7
7
|
const badgeVariants = cva(
|
|
8
|
-
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-none border border-transparent px-2 py-0.5 text-
|
|
8
|
+
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-none border border-transparent px-2 py-0.5 text-meta font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
|
@@ -5,13 +5,13 @@ import { Slot } from "radix-ui"
|
|
|
5
5
|
import { cn } from "@/lib/utils"
|
|
6
6
|
|
|
7
7
|
const buttonVariants = cva(
|
|
8
|
-
"group/button inline-flex shrink-0 items-center justify-center rounded-
|
|
8
|
+
"group/button inline-flex shrink-0 items-center justify-center rounded-control border border-transparent bg-clip-padding text-body font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
9
9
|
{
|
|
10
10
|
variants: {
|
|
11
11
|
variant: {
|
|
12
|
-
default: "bg-primary text-primary-foreground hover:bg-primary/
|
|
12
|
+
default: "bg-primary text-primary-foreground shadow-btn-primary hover:bg-primary/90",
|
|
13
13
|
outline:
|
|
14
|
-
"border-
|
|
14
|
+
"border-transparent bg-background shadow-control hover:bg-accent hover:text-foreground aria-expanded:bg-accent aria-expanded:text-foreground",
|
|
15
15
|
secondary:
|
|
16
16
|
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
|
17
17
|
ghost:
|
|
@@ -23,12 +23,12 @@ const buttonVariants = cva(
|
|
|
23
23
|
size: {
|
|
24
24
|
default:
|
|
25
25
|
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
26
|
-
xs: "h-6 gap-1 rounded-
|
|
27
|
-
sm: "h-7 gap-1 rounded-
|
|
26
|
+
xs: "h-6 gap-1 rounded-control-sm px-2 text-meta has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
27
|
+
sm: "h-7 gap-1 rounded-control px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
28
28
|
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
|
29
29
|
icon: "size-8",
|
|
30
|
-
"icon-xs": "size-6 rounded-
|
|
31
|
-
"icon-sm": "size-7 rounded-
|
|
30
|
+
"icon-xs": "size-6 rounded-control-sm [&_svg:not([class*='size-'])]:size-3",
|
|
31
|
+
"icon-sm": "size-7 rounded-control",
|
|
32
32
|
"icon-lg": "size-9",
|
|
33
33
|
},
|
|
34
34
|
},
|
|
@@ -12,7 +12,7 @@ function Card({
|
|
|
12
12
|
data-slot="card"
|
|
13
13
|
data-size={size}
|
|
14
14
|
className={cn(
|
|
15
|
-
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-none bg-card py-(--card-spacing) text-
|
|
15
|
+
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-none bg-card py-(--card-spacing) text-body text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none",
|
|
16
16
|
className
|
|
17
17
|
)}
|
|
18
18
|
{...props}
|
|
@@ -38,7 +38,7 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
38
38
|
<div
|
|
39
39
|
data-slot="card-title"
|
|
40
40
|
className={cn(
|
|
41
|
-
"font-heading text-
|
|
41
|
+
"font-heading text-title group-data-[size=sm]/card:text-title",
|
|
42
42
|
className
|
|
43
43
|
)}
|
|
44
44
|
{...props}
|
|
@@ -50,7 +50,7 @@ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
50
50
|
return (
|
|
51
51
|
<div
|
|
52
52
|
data-slot="card-description"
|
|
53
|
-
className={cn("text-
|
|
53
|
+
className={cn("text-meta text-muted-foreground", className)}
|
|
54
54
|
{...props}
|
|
55
55
|
/>
|
|
56
56
|
)
|
|
@@ -59,7 +59,7 @@ function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
59
59
|
return (
|
|
60
60
|
<div
|
|
61
61
|
data-slot="empty-title"
|
|
62
|
-
className={cn("font-heading text-
|
|
62
|
+
className={cn("font-heading text-title", className)}
|
|
63
63
|
{...props}
|
|
64
64
|
/>
|
|
65
65
|
)
|
|
@@ -70,7 +70,7 @@ function EmptyDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
|
70
70
|
<div
|
|
71
71
|
data-slot="empty-description"
|
|
72
72
|
className={cn(
|
|
73
|
-
"text-
|
|
73
|
+
"text-meta text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
|
74
74
|
className
|
|
75
75
|
)}
|
|
76
76
|
{...props}
|
|
@@ -83,7 +83,7 @@ function EmptyContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
83
83
|
<div
|
|
84
84
|
data-slot="empty-content"
|
|
85
85
|
className={cn(
|
|
86
|
-
"flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-
|
|
86
|
+
"flex w-full max-w-sm min-w-0 flex-col items-center gap-2.5 text-body text-balance",
|
|
87
87
|
className
|
|
88
88
|
)}
|
|
89
89
|
{...props}
|
|
@@ -28,7 +28,7 @@ function FieldLegend({
|
|
|
28
28
|
data-slot="field-legend"
|
|
29
29
|
data-variant={variant}
|
|
30
30
|
className={cn(
|
|
31
|
-
"mb-2.5 font-medium data-[variant=label]:text-
|
|
31
|
+
"mb-2.5 font-medium data-[variant=label]:text-meta data-[variant=legend]:text-title",
|
|
32
32
|
className
|
|
33
33
|
)}
|
|
34
34
|
{...props}
|
|
@@ -118,7 +118,7 @@ function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
|
|
|
118
118
|
<div
|
|
119
119
|
data-slot="field-label"
|
|
120
120
|
className={cn(
|
|
121
|
-
"flex w-fit items-center gap-2 text-
|
|
121
|
+
"flex w-fit items-center gap-2 text-body group-data-[disabled=true]/field:opacity-50",
|
|
122
122
|
className
|
|
123
123
|
)}
|
|
124
124
|
{...props}
|
|
@@ -131,7 +131,7 @@ function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
|
|
|
131
131
|
<p
|
|
132
132
|
data-slot="field-description"
|
|
133
133
|
className={cn(
|
|
134
|
-
"text-left text-
|
|
134
|
+
"text-left text-meta leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
|
|
135
135
|
"last:mt-0 nth-last-2:-mt-1",
|
|
136
136
|
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
|
|
137
137
|
className
|
|
@@ -153,7 +153,7 @@ function FieldSeparator({
|
|
|
153
153
|
data-slot="field-separator"
|
|
154
154
|
data-content={!!children}
|
|
155
155
|
className={cn(
|
|
156
|
-
"relative -my-2 h-5 text-
|
|
156
|
+
"relative -my-2 h-5 text-meta group-data-[variant=outline]/field-group:-mb-2",
|
|
157
157
|
className
|
|
158
158
|
)}
|
|
159
159
|
{...props}
|
|
@@ -214,7 +214,7 @@ function FieldError({
|
|
|
214
214
|
<div
|
|
215
215
|
role="alert"
|
|
216
216
|
data-slot="field-error"
|
|
217
|
-
className={cn("text-
|
|
217
|
+
className={cn("text-meta font-normal text-destructive", className)}
|
|
218
218
|
{...props}
|
|
219
219
|
>
|
|
220
220
|
{content}
|
|
@@ -8,7 +8,7 @@ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
|
8
8
|
type={type}
|
|
9
9
|
data-slot="input"
|
|
10
10
|
className={cn(
|
|
11
|
-
"h-8 w-full min-w-0 rounded-
|
|
11
|
+
"h-8 w-full min-w-0 rounded-control border border-input bg-transparent px-2.5 py-1 text-body transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-body file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 md:text-body dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
|
12
12
|
className
|
|
13
13
|
)}
|
|
14
14
|
{...props}
|
|
@@ -11,7 +11,7 @@ function Label({
|
|
|
11
11
|
<LabelPrimitive.Root
|
|
12
12
|
data-slot="label"
|
|
13
13
|
className={cn(
|
|
14
|
-
"flex items-center gap-2 text-
|
|
14
|
+
"flex items-center gap-2 text-meta leading-none select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
|
15
15
|
className
|
|
16
16
|
)}
|
|
17
17
|
{...props}
|