@montytools/cli 0.5.5 → 0.5.7
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 +181 -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 +105 -55
- package/template/AGENTS.md +54 -30
- 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,19 @@ 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-[…]`). `monty style`
|
|
169
|
+
verifies this — `monty save` refuses what it flags.
|
|
170
|
+
3. No rounded corners on any chart mark; every bar/column sits on an axis.
|
|
171
|
+
4. Dark: the page blends into the shell with no light seams.
|
|
172
|
+
5. Values in tabular figures; status as dot + muted text, never a pill.
|
|
173
|
+
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,25 +31,40 @@ 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
|
-
change verified in dev, run `monty save "<what changed>"` — it
|
|
55
|
+
change verified in dev, run `monty save "<what changed>"` — it style-
|
|
56
|
+
lints (token vocabulary only — `monty style` checks on demand), builds,
|
|
46
57
|
typechecks, and pushes the working copy to the cloud copy, like
|
|
47
58
|
`git push main`. Unsaved work exists only on this machine.
|
|
48
59
|
|
|
49
60
|
## Data: define, then use
|
|
50
61
|
|
|
62
|
+
`src/monty.gen.ts` mirrors the workspace config as exactly this shape —
|
|
63
|
+
zod tables wrapped in `defineApp` — so this is what your typed `app`
|
|
64
|
+
object looks like (generated; declare the real thing via `monty schema set`):
|
|
65
|
+
|
|
51
66
|
```ts
|
|
52
|
-
// monty.
|
|
67
|
+
// src/monty.gen.ts (GENERATED — read, never edit)
|
|
53
68
|
import { defineApp, montyFileSchema, montySelect } from "@montytools/sdk";
|
|
54
69
|
import { z } from "zod";
|
|
55
70
|
|
|
@@ -73,7 +88,7 @@ export const app = defineApp({
|
|
|
73
88
|
|
|
74
89
|
```tsx
|
|
75
90
|
import { useList, useRecord, useInsert, useUpdate, useRemove, useMembers, useUploadFile, useFileUrl } from "@montytools/sdk/react";
|
|
76
|
-
import { app } from "
|
|
91
|
+
import { app } from "../monty.gen";
|
|
77
92
|
|
|
78
93
|
const { data, status, loadMore } = useList(app, "expenses", {
|
|
79
94
|
filter: { status: "submitted" }, // equality on schema fields only
|
|
@@ -181,14 +196,14 @@ Rules that matter:
|
|
|
181
196
|
```tsx
|
|
182
197
|
import { PageHeader, PageHeaderButton } from "@montytools/sdk/ui";
|
|
183
198
|
|
|
184
|
-
<div className="flex h-full min-h-dvh flex-col">
|
|
199
|
+
<div className="flex h-full min-h-dvh flex-col bg-background">
|
|
185
200
|
<PageHeader icon={ChartColumn} title="Reports" meta="42 rows">
|
|
186
201
|
<PageHeaderButton onClick={exportCsv}>Export</PageHeaderButton>
|
|
187
202
|
<PageHeaderButton primary onClick={openNew}>
|
|
188
203
|
<Plus className="size-3.5" /> New
|
|
189
204
|
</PageHeaderButton>
|
|
190
205
|
</PageHeader>
|
|
191
|
-
<main className="min-h-0 flex-1 overflow-auto p-
|
|
206
|
+
<main className="min-h-0 flex-1 overflow-auto p-page">…</main>
|
|
192
207
|
</div>
|
|
193
208
|
```
|
|
194
209
|
|
|
@@ -200,7 +215,11 @@ Rules that matter:
|
|
|
200
215
|
for record-like tables (`<div className={SURFACE}><table>…`), identical to
|
|
201
216
|
the app's Configuration surfaces.
|
|
202
217
|
- **Extend records before rebuilding them.** For a custom page that is still
|
|
203
|
-
one table, use `RecordPage` from `@montytools/sdk/react`.
|
|
218
|
+
one table, use `RecordPage` from `@montytools/sdk/react`. To show ONE
|
|
219
|
+
record — from a board card, a report row, anywhere — use `RecordProfile`
|
|
220
|
+
(same module): every field plus one related tab per table that refs this
|
|
221
|
+
one, all derived from the schema with zero configuration. Never hand-build
|
|
222
|
+
a record dialog. Add behavior with
|
|
204
223
|
its typed `headerActions`, `rowActions`, `selectionActions`, and
|
|
205
224
|
`recordActions` callbacks. Use `RecordActionButton` and the lower-level
|
|
206
225
|
`RecordPage*`/`RecordTable*` components from `@montytools/sdk/ui` when you
|
|
@@ -235,7 +254,7 @@ Every platform error is one line shaped like:
|
|
|
235
254
|
| Code | Meaning |
|
|
236
255
|
|---|---|
|
|
237
256
|
| `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
|
|
257
|
+
| `UNKNOWN_TABLE` | Table name not in the workspace config's `tables` (`monty schema` shows it). |
|
|
239
258
|
| `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
259
|
| `INVALID_SCHEMA` | A table field uses a reserved name (`_*`, `updatedAt`, `createdBy`) — rename it. |
|
|
241
260
|
| `SCHEMA_DRIFT` (warning) | Stored rows predate your latest schema change; nothing crashes, but make changed fields `.optional()`/`.default(...)`. |
|
|
@@ -243,7 +262,7 @@ Every platform error is one line shaped like:
|
|
|
243
262
|
| `UNAUTHENTICATED` / `NO_ACTIVE_WORKSPACE` | App isn't running through the Monty host/dev shell. |
|
|
244
263
|
| `MISSING_ENV` / `NO_PROVIDER` | `.env.local` or the `<MontyProvider>` in `main.tsx` was removed. |
|
|
245
264
|
|
|
246
|
-
## Server code (optional): functions,
|
|
265
|
+
## Server code (optional): functions, shared addresses, clock rules
|
|
247
266
|
|
|
248
267
|
When logic must not run in the browser (private tables, third-party APIs with
|
|
249
268
|
secret keys, webhooks, clocks), create `server/index.ts` with named async
|
|
@@ -254,7 +273,7 @@ tables, including ones the UI never exposes), `ctx.files` (platform file
|
|
|
254
273
|
storage: `upload(data, {name?, contentType?}) → MontyFile`,
|
|
255
274
|
`download(fileOrId) → Blob`, `remove(fileOrId)` — same descriptors as the
|
|
256
275
|
`useUploadFile` hook, 10MB cap), `ctx.secrets` (see below), `ctx.viewer`
|
|
257
|
-
(who called: member session/visitor/
|
|
276
|
+
(who called: member session/visitor/rule/none), and `ctx.track()`
|
|
258
277
|
(emit an event).
|
|
259
278
|
|
|
260
279
|
```ts
|
|
@@ -267,8 +286,10 @@ export async function score(args: { sessionId: string }, ctx: MontyFnContext) {
|
|
|
267
286
|
return { verdict: weights.length > 0 ? "ok" : "empty" };
|
|
268
287
|
}
|
|
269
288
|
|
|
270
|
-
// 2)
|
|
271
|
-
//
|
|
289
|
+
// 2) SHARED OUTWARD — after `monty save` ships it, run
|
|
290
|
+
// `monty public set stripe` (the whole list; it replaces) and ANYONE on
|
|
291
|
+
// the internet can call GET/POST /__monty/public/stripe — instantly, no
|
|
292
|
+
// second save.
|
|
272
293
|
// Verify a signature from ctx.secrets against the RAW req.body before
|
|
273
294
|
// trusting anything; return { status, body?, contentType? } to control the
|
|
274
295
|
// response (or return data for 200 JSON, or nothing for {"ok":true}).
|
|
@@ -280,10 +301,12 @@ export async function stripe(req: MontyPublicRequest, ctx: MontyFnContext) {
|
|
|
280
301
|
return { status: 200, body: "ok" };
|
|
281
302
|
}
|
|
282
303
|
|
|
283
|
-
// 3)
|
|
284
|
-
// (
|
|
285
|
-
//
|
|
286
|
-
|
|
304
|
+
// 3) ON THE CLOCK — wire an `every` rule after the save ships the function
|
|
305
|
+
// (rules door / MCP rules_update): { name: "daily-digest", on: "every",
|
|
306
|
+
// cron: "0 9 * * *", run: "digest" } (5-field cron, UTC; max 3 every
|
|
307
|
+
// rules). The platform fires it as digest({ event }, ctx) with
|
|
308
|
+
// event = { type: "every", cron }. Make it idempotent — re-runs happen.
|
|
309
|
+
export async function digest(args: { event?: { cron?: string } }, ctx: MontyFnContext) { /* … */ }
|
|
287
310
|
|
|
288
311
|
// 4) onEvent — reserved name: the platform calls it after every accepted
|
|
289
312
|
// track() event. Forward to Slack/Meta/PostHog/your CRM here, with your keys.
|
|
@@ -291,16 +314,17 @@ export async function onEvent(event: { name: string }, ctx: MontyFnContext) { /*
|
|
|
291
314
|
```
|
|
292
315
|
|
|
293
316
|
From the UI: `const score = useServerFn<Result>(app, "score"); await score({ sessionId })`.
|
|
294
|
-
|
|
295
|
-
`/__monty/public/<name>` URL (and vice versa:
|
|
296
|
-
public).
|
|
317
|
+
Shared (public) functions are NOT callable through `useServerFn` — only at
|
|
318
|
+
their `/__monty/public/<name>` URL (and vice versa: unshared functions are
|
|
319
|
+
never public). `monty public` lists what is shared.
|
|
297
320
|
|
|
298
321
|
**Secrets:** `monty secret set STRIPE_KEY` stores a key for Live (write-only,
|
|
299
322
|
never in code or config). In a dev session, put the same names in the
|
|
300
323
|
gitignored `.monty/secrets.json`. Both arrive as `ctx.secrets.STRIPE_KEY`.
|
|
301
324
|
|
|
302
|
-
**Session behavior:** `monty dev` runs
|
|
303
|
-
|
|
325
|
+
**Session behavior:** while `monty dev` runs, the platform dispatches your
|
|
326
|
+
`every` rules to this session (each firing journals in the app's rule runs)
|
|
327
|
+
and serves shared functions at
|
|
304
328
|
`http://localhost:<port>/__monty/public/<name>` — curl them to test.
|
|
305
329
|
|
|
306
330
|
## 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.7",
|
|
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}
|