@montytools/cli 0.5.4 → 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.
@@ -4,13 +4,13 @@ Monty is a work OS: your app runs inside a team's workspace, on shared reactive
4
4
  data, with auth and deployment handled by the platform. **You only write product
5
5
  logic.** Everything below is the complete contract.
6
6
 
7
- Folders are managed for you: this app lives in `~/Monty/<slug>`. `monty current` confirms where you are; `cd "$(monty select <slug>)"` jumps to any app; never create app folders by hand.
7
+ `monty current` confirms which app folder you are in. Never create app folders by hand `monty create` and `monty connect` do it.
8
8
 
9
9
  ## The three files that matter
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
 
@@ -24,22 +24,33 @@ Folders are managed for you: this app lives in `~/Monty/<slug>`. `monty current`
24
24
  3. **UI is shadcn/ui, preconfigured — never build components from scratch.**
25
25
  Before building any UI piece, run `monty components`. If the capability is
26
26
  listed (data tables, kanban, calendar, combobox, file upload, rich text,
27
- charts, …) install the curated implementation with `monty add <name>`;
28
- core shadcn components install by bare name (`monty add dialog tabs`).
29
- Everything lands in `src/components/ui/*` already carrying the Monty
30
- theme. `monty docs <name>` shows a component's source before installing.
27
+ charts, …) install the curated implementation with
28
+ `monty components add <name>`; core shadcn components install by bare
29
+ name (`monty components add dialog tabs`). Everything lands in
30
+ `src/components/ui/*` already carrying the Monty theme.
31
+ `monty components docs <name>` shows a component's source before installing.
31
32
  Icons from `lucide-react`. Don't install other component libraries or
32
33
  write raw-color CSS — use semantic tokens (`bg-background`,
33
- `text-muted-foreground`, …). Don't edit `src/index.css` theme tokens.
34
- The look is Lyra: surfaces are stock components borderless,
35
- 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
36
44
  rectangles on real axes. The `monty-design` skill is the full spec.
37
- 4. **Schema changes = edit `monty.config.ts` and save.** Types update
38
- immediately. Prefer additive changes; give new fields `.optional()` or
39
- `.default(...)` so existing records stay readable. Describe fields for
40
- the next agent that fills them in: `.describe("what this holds")` on any
41
- field, and `montySelect`/`montyMultiSelect` instead of bare `z.enum` so
42
- 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).
43
54
  5. **You are not done until the work is saved.** After every meaningful
44
55
  change verified in dev, run `monty save "<what changed>"` — it builds,
45
56
  typechecks, and pushes the working copy to the cloud copy, like
@@ -47,8 +58,12 @@ Folders are managed for you: this app lives in `~/Monty/<slug>`. `monty current`
47
58
 
48
59
  ## Data: define, then use
49
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
+
50
65
  ```ts
51
- // monty.config.ts
66
+ // src/monty.gen.ts (GENERATED — read, never edit)
52
67
  import { defineApp, montyFileSchema, montySelect } from "@montytools/sdk";
53
68
  import { z } from "zod";
54
69
 
@@ -72,7 +87,7 @@ export const app = defineApp({
72
87
 
73
88
  ```tsx
74
89
  import { useList, useRecord, useInsert, useUpdate, useRemove, useMembers, useUploadFile, useFileUrl } from "@montytools/sdk/react";
75
- import { app } from "../../monty.config";
90
+ import { app } from "../monty.gen";
76
91
 
77
92
  const { data, status, loadMore } = useList(app, "expenses", {
78
93
  filter: { status: "submitted" }, // equality on schema fields only
@@ -180,14 +195,14 @@ Rules that matter:
180
195
  ```tsx
181
196
  import { PageHeader, PageHeaderButton } from "@montytools/sdk/ui";
182
197
 
183
- <div className="flex h-full min-h-dvh flex-col">
198
+ <div className="flex h-full min-h-dvh flex-col bg-background">
184
199
  <PageHeader icon={ChartColumn} title="Reports" meta="42 rows">
185
200
  <PageHeaderButton onClick={exportCsv}>Export</PageHeaderButton>
186
201
  <PageHeaderButton primary onClick={openNew}>
187
202
  <Plus className="size-3.5" /> New
188
203
  </PageHeaderButton>
189
204
  </PageHeader>
190
- <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>
191
206
  </div>
192
207
  ```
193
208
 
@@ -234,7 +249,7 @@ Every platform error is one line shaped like:
234
249
  | Code | Meaning |
235
250
  |---|---|
236
251
  | `VALIDATION` | Payload doesn't match your zod schema (unknown fields are also an error) — fix the payload or the schema. |
237
- | `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). |
238
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. |
239
254
  | `INVALID_SCHEMA` | A table field uses a reserved name (`_*`, `updatedAt`, `createdBy`) — rename it. |
240
255
  | `SCHEMA_DRIFT` (warning) | Stored rows predate your latest schema change; nothing crashes, but make changed fields `.optional()`/`.default(...)`. |
@@ -242,7 +257,7 @@ Every platform error is one line shaped like:
242
257
  | `UNAUTHENTICATED` / `NO_ACTIVE_WORKSPACE` | App isn't running through the Monty host/dev shell. |
243
258
  | `MISSING_ENV` / `NO_PROVIDER` | `.env.local` or the `<MontyProvider>` in `main.tsx` was removed. |
244
259
 
245
- ## Server code (optional): functions, public endpoints, schedules
260
+ ## Server code (optional): functions, shared addresses, clock rules
246
261
 
247
262
  When logic must not run in the browser (private tables, third-party APIs with
248
263
  secret keys, webhooks, clocks), create `server/index.ts` with named async
@@ -253,7 +268,7 @@ tables, including ones the UI never exposes), `ctx.files` (platform file
253
268
  storage: `upload(data, {name?, contentType?}) → MontyFile`,
254
269
  `download(fileOrId) → Blob`, `remove(fileOrId)` — same descriptors as the
255
270
  `useUploadFile` hook, 10MB cap), `ctx.secrets` (see below), `ctx.viewer`
256
- (who called: member session/visitor/schedule/none), and `ctx.track()`
271
+ (who called: member session/visitor/rule/none), and `ctx.track()`
257
272
  (emit an event).
258
273
 
259
274
  ```ts
@@ -266,8 +281,10 @@ export async function score(args: { sessionId: string }, ctx: MontyFnContext) {
266
281
  return { verdict: weights.length > 0 ? "ok" : "empty" };
267
282
  }
268
283
 
269
- // 2) PUBLIC endpointdeclared in monty.config.ts `publicFns: ["stripe"]`,
270
- // 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.
271
288
  // Verify a signature from ctx.secrets against the RAW req.body before
272
289
  // trusting anything; return { status, body?, contentType? } to control the
273
290
  // response (or return data for 200 JSON, or nothing for {"ok":true}).
@@ -279,10 +296,12 @@ export async function stripe(req: MontyPublicRequest, ctx: MontyFnContext) {
279
296
  return { status: 200, body: "ok" };
280
297
  }
281
298
 
282
- // 3) SCHEDULEDdeclared in monty.config.ts `schedule: { digest: "0 9 * * *" }`
283
- // (5-field cron, UTC; max 3). Runs as digest({}, ctx) with
284
- // ctx.viewer = { lane: "schedule", cron }. Make it idempotent re-runs happen.
285
- 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) { /* … */ }
286
305
 
287
306
  // 4) onEvent — reserved name: the platform calls it after every accepted
288
307
  // track() event. Forward to Slack/Meta/PostHog/your CRM here, with your keys.
@@ -290,16 +309,17 @@ export async function onEvent(event: { name: string }, ctx: MontyFnContext) { /*
290
309
  ```
291
310
 
292
311
  From the UI: `const score = useServerFn<Result>(app, "score"); await score({ sessionId })`.
293
- Public functions are NOT callable through `useServerFn` — only at their
294
- `/__monty/public/<name>` URL (and vice versa: undeclared functions are never
295
- 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.
296
315
 
297
316
  **Secrets:** `monty secret set STRIPE_KEY` stores a key for Live (write-only,
298
317
  never in code or config). In a dev session, put the same names in the
299
318
  gitignored `.monty/secrets.json`. Both arrive as `ctx.secrets.STRIPE_KEY`.
300
319
 
301
- **Session behavior:** `monty dev` runs your schedules for real (a `cron:` line
302
- 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
303
323
  `http://localhost:<port>/__monty/public/<name>` — curl them to test.
304
324
 
305
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.4",
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}
@@ -44,7 +44,7 @@ function SelectTrigger({
44
44
  data-slot="select-trigger"
45
45
  data-size={size}
46
46
  className={cn(
47
- "flex w-fit items-center justify-between gap-1.5 rounded-none border border-input bg-transparent py-2 pr-2 pl-2.5 text-xs whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-none *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 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",
47
+ "flex w-fit items-center justify-between gap-1.5 rounded-control border border-input bg-transparent py-2 pr-2 pl-2.5 text-body whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-1 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-1 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-control *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 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",
48
48
  className
49
49
  )}
50
50
  {...props}
@@ -69,7 +69,7 @@ function SelectContent({
69
69
  <SelectPrimitive.Content
70
70
  data-slot="select-content"
71
71
  data-align-trigger={position === "item-aligned"}
72
- className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-none bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
72
+ className={cn("relative z-50 max-h-(--radix-select-content-available-height) min-w-36 origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-overlay bg-popover text-popover-foreground shadow-menu duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", position ==="popper"&&"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1", className )}
73
73
  position={position}
74
74
  align={align}
75
75
  {...props}
@@ -97,7 +97,7 @@ function SelectLabel({
97
97
  return (
98
98
  <SelectPrimitive.Label
99
99
  data-slot="select-label"
100
- className={cn("px-2 py-2 text-xs text-muted-foreground", className)}
100
+ className={cn("px-2 py-2 text-meta text-muted-foreground", className)}
101
101
  {...props}
102
102
  />
103
103
  )
@@ -112,7 +112,7 @@ function SelectItem({
112
112
  <SelectPrimitive.Item
113
113
  data-slot="select-item"
114
114
  className={cn(
115
- "relative flex w-full cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-xs outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
115
+ "relative flex w-full cursor-default items-center gap-2 rounded-none py-2 pr-8 pl-2 text-body outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
116
116
  className
117
117
  )}
118
118
  {...props}
@@ -10,7 +10,7 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
10
10
  >
11
11
  <table
12
12
  data-slot="table"
13
- className={cn("w-full caption-bottom text-xs", className)}
13
+ className={cn("w-full caption-bottom text-body", className)}
14
14
  {...props}
15
15
  />
16
16
  </div>
@@ -68,7 +68,7 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
68
68
  <th
69
69
  data-slot="table-head"
70
70
  className={cn(
71
- "h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
71
+ "h-10 px-2 text-left align-middle text-meta font-medium whitespace-nowrap text-muted-foreground [&:has([role=checkbox])]:pr-0",
72
72
  className
73
73
  )}
74
74
  {...props}
@@ -96,7 +96,7 @@ function TableCaption({
96
96
  return (
97
97
  <caption
98
98
  data-slot="table-caption"
99
- className={cn("mt-4 text-xs text-muted-foreground", className)}
99
+ className={cn("mt-4 text-meta text-muted-foreground", className)}
100
100
  {...props}
101
101
  />
102
102
  )
@@ -1,138 +1,27 @@
1
1
  @import "tailwindcss";
2
2
  @import "tw-animate-css";
3
3
  @import "shadcn/tailwind.css";
4
- @import "@fontsource-variable/roboto";
4
+ @import "@fontsource-variable/inter";
5
+ /* The Monty design tokens — colors, type ladder, radius policy, elevation.
6
+ The ONE place style values live; nothing in an app restates them. */
7
+ @import "@montytools/sdk/tokens.css";
5
8
 
6
9
  /* Shared platform chrome (@montytools/sdk/ui) ships Tailwind classes in its
7
10
  dist — scan it so they compile. */
8
- @source "../node_modules/@montytools/sdk/dist/ui.js";
11
+ @source "../node_modules/@montytools/sdk/dist";
9
12
 
10
13
 
11
14
  @custom-variant dark (&:is(.dark *));
12
15
 
13
- @theme inline {
14
- --font-heading: var(--font-sans);
15
- --font-sans: 'Roboto Variable', sans-serif;
16
- --color-sidebar-ring: var(--sidebar-ring);
17
- --color-sidebar-border: var(--sidebar-border);
18
- --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
19
- --color-sidebar-accent: var(--sidebar-accent);
20
- --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
21
- --color-sidebar-primary: var(--sidebar-primary);
22
- --color-sidebar-foreground: var(--sidebar-foreground);
23
- --color-sidebar: var(--sidebar);
24
- --color-chart-5: var(--chart-5);
25
- --color-chart-4: var(--chart-4);
26
- --color-chart-3: var(--chart-3);
27
- --color-chart-2: var(--chart-2);
28
- --color-chart-1: var(--chart-1);
29
- --color-ring: var(--ring);
30
- --color-input: var(--input);
31
- --color-border: var(--border);
32
- --color-destructive: var(--destructive);
33
- --color-accent-foreground: var(--accent-foreground);
34
- --color-accent: var(--accent);
35
- --color-muted-foreground: var(--muted-foreground);
36
- --color-muted: var(--muted);
37
- --color-secondary-foreground: var(--secondary-foreground);
38
- --color-secondary: var(--secondary);
39
- --color-primary-foreground: var(--primary-foreground);
40
- --color-primary: var(--primary);
41
- --color-popover-foreground: var(--popover-foreground);
42
- --color-popover: var(--popover);
43
- --color-card-foreground: var(--card-foreground);
44
- --color-card: var(--card);
45
- --color-foreground: var(--foreground);
46
- --color-background: var(--background);
47
- --radius-sm: calc(var(--radius) * 0.6);
48
- --radius-md: calc(var(--radius) * 0.8);
49
- --radius-lg: var(--radius);
50
- --radius-xl: calc(var(--radius) * 1.4);
51
- --radius-2xl: calc(var(--radius) * 1.8);
52
- --radius-3xl: calc(var(--radius) * 2.2);
53
- --radius-4xl: calc(var(--radius) * 2.6);
54
- }
55
-
56
- :root {
57
- --background: oklch(1 0 0);
58
- --foreground: oklch(0.153 0.006 107.1);
59
- --card: oklch(1 0 0);
60
- --card-foreground: oklch(0.153 0.006 107.1);
61
- --popover: oklch(1 0 0);
62
- --popover-foreground: oklch(0.153 0.006 107.1);
63
- --primary: oklch(0.511 0.096 186.391);
64
- --primary-foreground: oklch(0.984 0.014 180.72);
65
- --secondary: oklch(0.967 0.001 286.375);
66
- --secondary-foreground: oklch(0.21 0.006 285.885);
67
- --muted: oklch(0.966 0.005 106.5);
68
- --muted-foreground: oklch(0.58 0.031 107.3);
69
- --accent: oklch(0.966 0.005 106.5);
70
- --accent-foreground: oklch(0.228 0.013 107.4);
71
- --destructive: oklch(0.577 0.245 27.325);
72
- --border: oklch(0.93 0.007 106.5);
73
- --input: oklch(0.93 0.007 106.5);
74
- --ring: oklch(0.737 0.021 106.9);
75
- --chart-1: oklch(0.855 0.138 181.071);
76
- --chart-2: oklch(0.704 0.14 182.503);
77
- --chart-3: oklch(0.6 0.118 184.704);
78
- --chart-4: oklch(0.511 0.096 186.391);
79
- --chart-5: oklch(0.437 0.078 188.216);
80
- --radius: 0.625rem;
81
- --sidebar: oklch(0.988 0.003 106.5);
82
- --sidebar-foreground: oklch(0.153 0.006 107.1);
83
- --sidebar-primary: oklch(0.6 0.118 184.704);
84
- --sidebar-primary-foreground: oklch(0.984 0.014 180.72);
85
- --sidebar-accent: oklch(0.966 0.005 106.5);
86
- --sidebar-accent-foreground: oklch(0.228 0.013 107.4);
87
- --sidebar-border: oklch(0.93 0.007 106.5);
88
- --sidebar-ring: oklch(0.737 0.021 106.9);
89
- }
90
-
91
- .dark {
92
- --background: #101112;
93
- --foreground: #F1F2F3;
94
- --card: #17181A;
95
- --card-foreground: #F1F2F3;
96
- --popover: #17181A;
97
- --popover-foreground: #F1F2F3;
98
- --primary: #266DF0;
99
- --primary-foreground: #FFFFFF;
100
- --secondary: rgb(255 255 255 / 0.05);
101
- --secondary-foreground: #F1F2F3;
102
- --muted: rgb(255 255 255 / 0.05);
103
- --muted-foreground: rgb(255 255 255 / 0.6);
104
- --accent: rgb(255 255 255 / 0.05);
105
- --accent-foreground: #F1F2F3;
106
- --destructive: oklch(0.68 0.19 25);
107
- --border: #1B1C1F;
108
- --input: #212224;
109
- --ring: #266DF0;
110
- --chart-1: #266DF0;
111
- --chart-2: #4E8CFC;
112
- --chart-3: #9B69FF;
113
- --chart-4: #6EA5F7;
114
- --chart-5: #2D3A55;
115
- --sidebar: #101112;
116
- --sidebar-foreground: #F1F2F3;
117
- --sidebar-primary: #266DF0;
118
- --sidebar-primary-foreground: #FFFFFF;
119
- --sidebar-accent: rgb(255 255 255 / 0.05);
120
- --sidebar-accent-foreground: #F1F2F3;
121
- --sidebar-border: #1B1C1F;
122
- --sidebar-ring: #266DF0;
123
- }
124
-
125
16
  @layer base {
126
17
  * {
127
18
  @apply border-border outline-ring/50;
128
19
  }
129
20
  body {
130
- @apply bg-background text-foreground;
21
+ @apply bg-background font-sans text-body text-foreground;
22
+ -webkit-font-smoothing: antialiased;
131
23
  }
132
24
  button:not(:disabled), [role="button"]:not(:disabled) {
133
25
  cursor: pointer;
134
26
  }
135
- html {
136
- @apply font-sans;
137
- }
138
27
  }
@@ -4,7 +4,7 @@ import { RouterProvider, createRouter } from "@tanstack/react-router";
4
4
  import { MontyProvider, MontySplash } from "@montytools/sdk/react";
5
5
 
6
6
  import "./index.css";
7
- import { app } from "../monty.config";
7
+ import { app } from "./monty.gen";
8
8
  import { routeTree } from "./routeTree.gen";
9
9
 
10
10
  // defaultPendingComponent: lazy route chunks show the same splash the rest
@@ -0,0 +1,16 @@
1
+ import { defineApp } from "@montytools/sdk";
2
+
3
+ // GENERATED by Monty from the app's config stored in the workspace.
4
+ // DO NOT EDIT: edits here never land anywhere, and this file is
5
+ // overwritten on every sync (a running `monty dev` regenerates it within
6
+ // a heartbeat of a remote change; `monty save` and `monty schema pull`
7
+ // refresh it too). Change the app through the doors instead:
8
+ // `monty schema set`, the MCP schema_update tool, or Configuration.
9
+ // Import { app } from it for typed SDK hooks — that part is yours.
10
+ export const app = defineApp({
11
+ slug: "new-app",
12
+ name: "New app",
13
+ icon: "layout-grid",
14
+ tables: {},
15
+ });
16
+ export type App = typeof app;