@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.
@@ -1,45 +1,92 @@
1
1
  ---
2
2
  name: monty-design
3
- description: Design Monty app pages — Lyra shadcn styling (borderless, sharp-cornered, 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.
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 looks native to Monty: quiet, rectilinear, data-forward.
10
- The design system is already installed — your job is to NOT fight it.
11
-
12
- ## The one law: stock Lyra, nothing invented
13
-
14
- Every app ships shadcn preset `radix-lyra` (see `components.json`). Lyra
15
- surfaces are **borderless and sharp-cornered**: `Card` is `rounded-none`,
16
- no border, a `bg-card` fill with a hairline `ring-1 ring-foreground/10`.
17
-
18
- - Use the components as they come: `Card`/`CardHeader`/`CardTitle`/
19
- `CardDescription`/`CardContent`, `Table`, `Badge`, Never rebuild a
20
- surface as a styled `div` a hand-rolled `rounded-xl border bg-…` card is
21
- the canonical mistake.
22
- - Never invent tokens or raw colors. Semantic tokens only (`bg-background`,
23
- `text-muted-foreground`, `border-border`, `var(--chart-2)`, …).
24
- - Sanctioned overrides are content-level only: e.g. `text-2xl tabular-nums`
25
- on a stat value, a width on a label column. If an override styles 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
- The Monty shell is dark-only; a light page inside it reads as broken. The
31
- app theme's `.dark` tokens mirror the shell palette (#101112 ground,
32
- #17181A cards, #266DF0 primary) so embedded pages are seamless never
33
- retheme or hand-pick your own dark colors. Until
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, so a custom page
42
- is indistinguishable from a record page. Never hand-roll the header bar.
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-6 pt-4">
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-2xl tabular-nums">{value}</CardTitle>
76
- <div className="text-xs text-muted-foreground">{delta or context}</div>
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** (matches the
84
- Lyra rectilinear look). The three failure modes to avoid: pill "track+fill"
85
- bars (read as progress bars), rounded caps (misstate where a value ends),
86
- and floating bars with no axis (nothing anchors the eye).
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 tabular-nums`,
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 tiny tabular tick labels (10px, muted,
97
- right-aligned in a left gutter). Round the axis top to a "nice" integer
98
- (≤4 exact; else the next multiple of 5/10/50) so ticks stay honest.
99
- Direct value labels above non-zero columns.
100
- - **Color**: one accent ramp from the Monty palette — `var(--chart-2)` for
101
- the emphasized series (today, the selection), `var(--chart-5)` for
102
- context. The values are the platform's (Attio-blue family, matching the
103
- shell); never restate them as hex, never a hue per category, and text
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: `ColumnChart` and `BarRow` in
113
- `demos/crm/src/routes/stats.tsx` (day-selector columns, status bars) and
114
- `demos/crm-v2/src/routes/stats.tsx` (money detail on bars, top-deals table).
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-*` on any surface you authored surfaces are stock
119
- Lyra components.
120
- 2. No rounded corners on any chart mark; every bar/column sits on an axis.
121
- 3. Dark: the page blends into the shell with no light seams.
122
- 4. Values in tabular figures; labels in text tokens; one accent hue.
123
- 5. Screenshot it inside the shell (`/apps/<slug>/…`), not just the dev port.
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.
@@ -10,7 +10,7 @@ logic.** Everything below is the complete contract.
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 [Tabler](https://tabler.io/icons) icon name, e.g. `"receipt"`, `"users"`) Monty renders your app's logo tile from it. |
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` theme tokens.
35
- The look is Lyra: surfaces are stock components borderless,
36
- sharp-cornered never hand-styled divs; chart marks are sharp
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 = edit `monty.config.ts` and save.** Types update
39
- immediately. Prefer additive changes; give new fields `.optional()` or
40
- `.default(...)` so existing records stay readable. Describe fields for
41
- the next agent that fills them in: `.describe("what this holds")` on any
42
- field, and `montySelect`/`montyMultiSelect` instead of bare `z.enum` so
43
- every option says WHEN it applies, not what the word means.
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.config.ts
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 "../../monty.config";
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-6">…</main>
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 `monty.config.ts` `tables`. |
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, public endpoints, schedules
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/schedule/none), and `ctx.track()`
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) PUBLIC endpointdeclared in monty.config.ts `publicFns: ["stripe"]`,
271
- // then ANYONE on the internet can call GET/POST /__monty/public/stripe.
284
+ // 2) SHARED OUTWARDafter `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) SCHEDULEDdeclared in monty.config.ts `schedule: { digest: "0 9 * * *" }`
284
- // (5-field cron, UTC; max 3). Runs as digest({}, ctx) with
285
- // ctx.viewer = { lane: "schedule", cron }. Make it idempotent re-runs happen.
286
- export async function digest(_args: Record<string, unknown>, ctx: MontyFnContext) { /* */ }
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
- Public functions are NOT callable through `useServerFn` — only at their
295
- `/__monty/public/<name>` URL (and vice versa: undeclared functions are never
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 your schedules for real (a `cron:` line
303
- prints per firing, UTC) and serves public functions at
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
@@ -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. If you theme the app dark-only, change
13
- the background below to match. -->
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: oklch(1 0 0)">
18
- <div style="width: 10px; height: 10px; border-radius: 999px; background: #999; animation: monty-pulse 1.2s ease-in-out infinite"></div>
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>
@@ -9,8 +9,8 @@
9
9
  "typecheck": "tsc --noEmit"
10
10
  },
11
11
  "dependencies": {
12
- "@fontsource-variable/roboto": "^5.2.10",
13
- "@montytools/sdk": "^0.2.5",
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-xs 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!",
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-none border border-transparent bg-clip-padding text-xs 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",
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/80",
12
+ default: "bg-primary text-primary-foreground shadow-btn-primary hover:bg-primary/90",
13
13
  outline:
14
- "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
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-none px-2 text-xs 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-none 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",
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-none [&_svg:not([class*='size-'])]:size-3",
31
- "icon-sm": "size-7 rounded-none",
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-xs/relaxed 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",
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-sm font-medium group-data-[size=sm]/card:text-sm",
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-xs/relaxed text-muted-foreground", className)}
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-sm font-medium", className)}
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-xs/relaxed text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
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-xs text-balance",
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-xs data-[variant=legend]:text-sm",
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-xs/relaxed group-data-[disabled=true]/field:opacity-50",
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-xs/relaxed leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
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-xs group-data-[variant=outline]/field-group:-mb-2",
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-xs font-normal text-destructive", className)}
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-none border border-input bg-transparent px-2.5 py-1 text-xs transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs 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-xs dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
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-xs 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",
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}