create-muten 0.0.16 → 0.0.17

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/README.md CHANGED
@@ -89,7 +89,7 @@ whether a framework's island plugin is pre-wired:
89
89
  | **muten + Svelte** | same, plus `@sveltejs/vite-plugin-svelte` + Svelte, for **Svelte islands** (a lighter runtime) |
90
90
 
91
91
  An *island* is a real framework component used as a node - `use X from "react:./X.jsx"` →
92
- `X(value: @s, onChange: act) client:visible` (props ↓ + events ↑, lazy + code-split). Default to `.muten`;
92
+ `X(value: s, onChange: act) client:visible` (props ↓ + events ↑, lazy + code-split). Default to `.muten`;
93
93
  reach for an island only for a widget muten can't express.
94
94
 
95
95
  When **Tailwind or DaisyUI** is added, `theme.muten` is centralized to **match Tailwind's scale** (so
package/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // create-muten — scaffold a new Muten app, with modern interactive prompts (@clack/prompts).
3
3
  //
4
4
  // npm create muten@latest [name] (or: npx create-muten)
5
- // create-muten [name] [--template muten|react|svelte] [--css|--scss] [--tailwind] [--daisyui] [--vercel] [--tauri] [--pm npm|pnpm|yarn|bun] [--no-install]
5
+ // create-muten [name] [--css|--scss] [--tailwind] [--daisyui] [--vercel] [--tauri] [--pm npm|pnpm|yarn|bun] [--no-install]
6
6
  //
7
7
  // Stylesheet (CSS or SCSS) is the base; Tailwind is an optional add-on ON TOP of CSS (it's a styling
8
8
  // library, not a stylesheet replacement). Interactive in a TTY; flags / non-TTY make it scriptable.
@@ -16,7 +16,6 @@ import color from 'picocolors';
16
16
  const SELF = dirname(fileURLToPath(import.meta.url));
17
17
  const TEMPLATE = join(SELF, 'template');
18
18
  const TAURI_TEMPLATE = join(SELF, 'template-tauri'); // src-tauri/ overlay, copied only when --tauri
19
- const TEMPLATES = ['muten', 'react', 'svelte']; // the "template" IS the flavor: pure muten, or muten + a framework for islands
20
19
  const PKG = JSON.parse(readFileSync(join(SELF, 'package.json'), 'utf8'));
21
20
  const PMS = ['npm', 'pnpm', 'yarn', 'bun'];
22
21
 
@@ -61,30 +60,13 @@ const WELCOME_CSS = `
61
60
  .card-text { color: #71717a; font-size: 13px; line-height: 1.5; }
62
61
  @media (max-width: 560px) { .stats, .cards { grid-template-columns: 1fr; } }
63
62
  `;
64
- // vite.config composed from the chosen options — muten always; svelte/react add island plugins; tailwind last.
65
- const viteConfig = ({ tailwind, svelte, react }) => {
63
+ // vite.config composed from the chosen options — muten always; tailwind last.
64
+ const viteConfig = ({ tailwind }) => {
66
65
  const imports = [`import muten from '@muten/core/vite-plugin-muten.js';`];
67
66
  const plugins = ['muten()'];
68
- if (svelte) { imports.push(`import { svelte } from '@sveltejs/vite-plugin-svelte';`); plugins.push('svelte()'); }
69
- if (react) { imports.push(`import react from '@vitejs/plugin-react';`); plugins.push('react()'); }
70
67
  if (tailwind) { imports.push(`import tailwindcss from '@tailwindcss/vite';`); plugins.push('tailwindcss()'); }
71
68
  return `${imports.join('\n')}\n\nexport default {\n plugins: [${plugins.join(', ')}],\n};\n`;
72
69
  };
73
- // Tell the AI the island plugin is wired so it can drop in a real Svelte/React component (incl. shadcn/Radix).
74
- const ISLANDS_NOTE = ({ svelte, react }) => {
75
- const techs = [svelte && 'Svelte', react && 'React'].filter(Boolean).join(' + ');
76
- const ex = react
77
- ? `use Widget from "react:./Widget.jsx"` + ' → ' + `Widget(value: @sel, onChange: pick) client:visible`
78
- : `use Widget from "svelte:./Widget.svelte"` + ' → ' + `Widget(value: @sel, onChange: pick) client:visible`;
79
- return `
80
- ## Framework islands (${techs} — wired)
81
- The ${techs} Vite plugin is installed. For a genuinely-interactive widget Muten can't express (date-picker,
82
- combobox, command palette, rich editor — including React/Svelte libs like shadcn/Radix), write the component
83
- in its own \`.${react ? 'jsx' : 'svelte'}\` file and mount it as a node: \`${ex}\`. props ↓ (\`@state\`) + events ↑
84
- (an \`onX: action\` calls a Muten action), lazy + code-split. Default to \`.muten\` for the UI; reach for an
85
- island only for the foreign piece. Full details: SKILL §14.
86
- `;
87
- };
88
70
  // When Tailwind/DaisyUI is chosen, the Muten token scale is centralized to MATCH Tailwind's defaults, so
89
71
  // style() tokens and Tailwind utilities share one scale (e.g. style(gap.md) == gap-4 == 1rem). Plain
90
72
  // css/scss keeps the default theme.muten. (DaisyUI builds on Tailwind's scale; its colors come via @plugin.)
@@ -151,10 +133,9 @@ async function main() {
151
133
  const has = (f) => argv.includes(f);
152
134
  const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; };
153
135
  if (has('-v') || has('--version')) { console.log(PKG.version); return; }
154
- if (has('-h') || has('--help')) { console.log('Usage: create-muten [name] [--template muten|react|svelte] [--css|--scss] [--tailwind] [--daisyui] [--vercel] [--tauri] [--pm npm|pnpm|yarn|bun] [--no-install]'); return; }
136
+ if (has('-h') || has('--help')) { console.log('Usage: create-muten [name] [--css|--scss] [--tailwind] [--daisyui] [--vercel] [--tauri] [--pm npm|pnpm|yarn|bun] [--no-install]'); return; }
155
137
 
156
- let name = argv.filter((a, i) => !a.startsWith('-') && argv[i - 1] !== '--pm' && argv[i - 1] !== '--template')[0];
157
- let template = val('--template') || (has('--react') ? 'react' : has('--svelte') ? 'svelte' : has('--muten') ? 'muten' : undefined); // flavor: muten | react | svelte
138
+ let name = argv.filter((a, i) => !a.startsWith('-') && argv[i - 1] !== '--pm')[0];
158
139
  let style = has('--scss') ? 'scss' : has('--css') ? 'css' : undefined; // the base stylesheet
159
140
  let tailwind = has('--tailwind') ? true : undefined; // optional add-on (CSS only)
160
141
  let daisyui = has('--daisyui') ? true : undefined; // component classes on Tailwind
@@ -163,7 +144,6 @@ async function main() {
163
144
  let pm = val('--pm');
164
145
  let install = has('--no-install') ? false : undefined;
165
146
  if (name && !validName(name)) { console.error(`Invalid name: "${name}" (letters, digits, . _ -)`); process.exit(1); }
166
- if (template && !TEMPLATES.includes(template)) { console.error(`Unknown template: "${template}" (${TEMPLATES.join(', ')})`); process.exit(1); }
167
147
  if (pm && !PMS.includes(pm)) { console.error(`Unknown package manager: "${pm}" (${PMS.join(', ')})`); process.exit(1); }
168
148
  const dpm = detectPM();
169
149
 
@@ -172,11 +152,6 @@ async function main() {
172
152
  logo();
173
153
  intro(color.dim(`create-muten v${PKG.version}`));
174
154
  if (!name) name = keep(await text({ message: 'Project name', placeholder: 'muten-app', defaultValue: 'muten-app', validate: (v) => (v && !validName(v)) ? 'Use letters, digits, . _ - (start alphanumeric).' : undefined }));
175
- if (!template) template = keep(await select({ message: 'Template', options: [
176
- { value: 'muten', label: 'muten', hint: 'pure — the AI-first DSL, zero framework runtime' },
177
- { value: 'react', label: 'muten + React', hint: 'React islands: shadcn, Radix, any React lib' },
178
- { value: 'svelte', label: 'muten + Svelte', hint: 'Svelte islands: a lighter runtime' },
179
- ] }));
180
155
  if (!style && tailwind === undefined) { // ONE explicit styling choice — each is opt-in, "CSS" = nothing extra
181
156
  const styling = keep(await select({ message: 'Styling', options: [
182
157
  { value: 'css', label: 'CSS', hint: 'plain — no framework, zero deps' },
@@ -192,7 +167,6 @@ async function main() {
192
167
  if (tauri === undefined) tauri = keep(await confirm({ message: 'Desktop app? (Tauri — native window, ships the OS webview, needs Rust)', initialValue: false }));
193
168
  }
194
169
  name = name || 'muten-app';
195
- template = template || 'muten';
196
170
  style = style || 'css';
197
171
  if (daisyui) tailwind = true; // DaisyUI is a Tailwind plugin
198
172
  if (tailwind === undefined) tailwind = false;
@@ -200,16 +174,13 @@ async function main() {
200
174
  if (vercel === undefined) vercel = false;
201
175
  if (tauri === undefined) tauri = false;
202
176
  if (tailwind) style = 'css'; // Tailwind v4 is CSS-native (not SCSS)
203
- const svelte = template === 'svelte'; // the flavor IS the islands choice
204
- const react = template === 'react';
205
177
  pm = pm || dpm;
206
178
  if (install === undefined) install = false;
207
179
 
208
180
  const target = resolve(name);
209
181
  if (existsSync(target)) { (process.stdin.isTTY ? cancel : console.error)(`"${name}" already exists.`); process.exit(1); }
210
182
 
211
- // EVERY flavor scaffolds the SAME base template (identical welcome page); react/svelte only add the
212
- // island plugin + deps below, and tailwind/daisyui only swap the stylesheet.
183
+ // The base template is the pure "muten" app (the welcome page); tailwind/daisyui only swap the stylesheet.
213
184
  cpSync(TEMPLATE, target, { recursive: true });
214
185
  const ignore = join(target, '_gitignore');
215
186
  if (existsSync(ignore)) renameSync(ignore, join(target, '.gitignore'));
@@ -232,9 +203,6 @@ async function main() {
232
203
  writeFileSync(join(target, 'src', style === 'scss' ? 'styles.scss' : 'styles.css'), RESET + WELCOME_CSS);
233
204
  }
234
205
  if (style === 'scss') addDev({ sass: '^1.101.0' });
235
- if (svelte) { addDep({ svelte: '^5.0.0' }); addDev({ '@sveltejs/vite-plugin-svelte': '^7.0.0' }); }
236
- if (react) { addDep({ react: '^19.0.0', 'react-dom': '^19.0.0' }); addDev({ '@vitejs/plugin-react': '^6.0.0' }); }
237
- if (svelte || react) appendAgents(ISLANDS_NOTE({ svelte, react }));
238
206
  if (tauri) { // native desktop wrapper around the same web build (dist)
239
207
  cpSync(join(TAURI_TEMPLATE, 'src-tauri'), join(target, 'src-tauri'), { recursive: true });
240
208
  writeFileSync(join(target, 'src-tauri', '.gitignore'), '/target\n/gen/schemas\n'); // npm strips real .gitignore from packages
@@ -251,10 +219,10 @@ async function main() {
251
219
  pkg.scripts = { ...pkg.scripts, tauri: 'tauri', 'tauri:dev': 'tauri dev', 'tauri:build': 'tauri build' };
252
220
  appendAgents(TAURI_NOTE(pm));
253
221
  }
254
- writeFileSync(join(target, 'vite.config.mjs'), viteConfig({ tailwind, svelte, react })); // composed: muten + chosen plugins
222
+ writeFileSync(join(target, 'vite.config.mjs'), viteConfig({ tailwind })); // composed: muten + chosen plugins
255
223
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
256
224
 
257
- const desc = `${template}, ${style}${tailwind ? ' + Tailwind' : ''}${daisyui ? ' + DaisyUI' : ''}${vercel ? ' + Vercel' : ''}${tauri ? ' + Tauri' : ''}`;
225
+ const desc = `muten, ${style}${tailwind ? ' + Tailwind' : ''}${daisyui ? ' + DaisyUI' : ''}${vercel ? ' + Vercel' : ''}${tauri ? ' + Tauri' : ''}`;
258
226
  if (!install) {
259
227
  if (process.stdin.isTTY) { note(`cd ${name}\n${pm} install\n${pm} run dev`, 'Next steps'); outro(color.green(`Created ${name} (${desc})`)); }
260
228
  else console.log(`\n Created ${name} (${desc}, ${pm})\n cd ${name} && ${pm} install && ${pm} run dev\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-muten",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "description": "Scaffold a new Muten app.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -3,7 +3,7 @@
3
3
  This project uses **Muten**, an AI-first frontend framework. The UI is written in **`.muten` files**
4
4
  (a small declarative DSL) — **not** React, JSX, Vue, Svelte or hand-written HTML/JS. No model is
5
5
  trained on Muten yet, so **follow this guide instead of guessing**. Foreign code enters ONLY through explicit
6
- escapes — `use` for JS functions, **islands** for a Svelte/React widget — never as the page UI itself; never add a JS bootstrap.
6
+ escapes — `use` for JS functions, `Custom` for a vanilla-JS widget — never as the page UI itself; never add a JS bootstrap.
7
7
 
8
8
  > Full language reference (every primitive, prop, token, pattern): the **`muten` skill** at
9
9
  > [`skills/muten/SKILL.md`](skills/muten/SKILL.md).
@@ -14,7 +14,7 @@ escapes — `use` for JS functions, **islands** for a Svelte/React widget — ne
14
14
  - Primitives are **PascalCase** (`Stack`, `Text`, `Button`); control flow is lowercase (`when`, `each`).
15
15
  - **`style(...)`** = layout/typography tokens (Muten builds STRUCTURE). **`class("...")`** = your look
16
16
  (your CSS / Tailwind). Muten ships no skin — appearance is yours.
17
- - State references use `@name`; interpolate in any string with `{expr}`: `Text "Hi, {user.name}"`.
17
+ - A state reference is a **bare name** (no sigil); interpolate in any string with `{expr}`: `Text "Hi, {user.name}"`.
18
18
 
19
19
  ## File map
20
20
  ```
@@ -40,8 +40,8 @@ Page style(padding.lg, gap.md) {
40
40
 
41
41
  ## Cheat-sheet
42
42
  - **Layout:** `Stack` (vertical), `Page` (`<main>`), `Header`/`Nav`/`Sidebar`/`Footer` (landmarks). Horizontal = `style(row)`.
43
- - **Content:** `Text`, `Title "x" h2`, `Span`, `Image "{src}" alt "…"` (alt required), `Link "x" -> /route`, `Button "x" -> action(arg)`.
44
- - **Data:** `DataTable @list columns(a, b)`, `Form bind @draft submit create`, `SearchField bind @q`.
43
+ - **Content:** `Text`, `Title "x" h2`, `Span`, `Image "{src}" alt("…")` (alt required), `Link "x" -> /route`, `Button "x" -> action(arg)`.
44
+ - **Data:** `DataTable list columns(a, b)`, `Form bind(draft) submit(create)`, `SearchField bind(q)`.
45
45
  - **Control:** `when <expr> { … }`, `each <list> as item { … }`.
46
46
  - **Interactivity:** reactive class `class(active when isOpen)`; events on any element `on(keydown: act, mouseenter: act)`; a `/404` route catches unmatched paths.
47
47
  - **State:** `state { q = "" : text users = query listUsers : list<User> }` — query states expose `.loading/.error/.data`.
@@ -49,17 +49,17 @@ Page style(padding.lg, gap.md) {
49
49
  - **Writes:** a source-backed list gets `create`/`update`/`delete` in an action (`orders.create(draft)` → POST/PUT/DELETE the resource, optimistic + updates the list). The action is async with reactive `name.pending`/`name.error` for UX. Local-only mutations stay `push`/`set`/`reset`/`remove`.
50
50
  - **Refetch:** re-run a query with N params (search / paginate / filter): `products.refetch(q: term, page: n)` in an action → builds `?q=&page=` and reloads the list.
51
51
  - **Escape hatch:** non-RESTful API? `post`/`put`/`delete` a `"client:/path"` (interpolated) with optional `body` in an action: `post "shop:/orders" body item`. Uses the client's base+headers; `mutates` is optional for pure commands.
52
- - **JS & framework escapes (`use`):** call JS functions — `use fmt from "./lib.ts"` → `Text "{fmt(x)}"`. Mount a Svelte/React widget as an **island** `use Box from "react:./Box.jsx"``Box(value: @s, onPick: act) client:visible` (props↓ + events↑, lazy, code-split). Add the framework's Vite plugin. Full details: SKILL §14.
53
- - **Actions:** `action add mutates users <- item { users.push(item) }` — ops: `push/set/reset/remove`; branch with `if/else`.
52
+ - **JS escape (`use`):** call named JS functions behind a typed, synchronous border — `use fmt from "./lib.ts"` → `Text "{fmt(x)}"`. A visual widget Muten can't express vanilla-JS `Custom` (no framework-component escape). Full details: SKILL §14.
53
+ - **Actions:** `action add(item: User) mutates users { users.push(item) }` — typed params in `(…)`; ops: `push/set/reset/remove/toggle/patch`; branch with `if/else`.
54
54
  - **Tokens:** `gap.md padding.lg cols.3 text.lg row center between` — responsive prefix: `md:cols.2`.
55
55
 
56
56
  ## Dependencies & limits
57
57
  - **CSS / Tailwind / SCSS: YES** — it's a Vite app; install them and use `class("…")` + your CSS.
58
- - **React / Vue / Svelte as the page UI: NO** pages are `.muten` (vanilla DOM, no framework runtime). But a
59
- single interactive widget or framework lib CAN enter as an **island** (`use X from "react:…"`, SKILL §14) or a
60
- vanilla `Custom` component — for the foreign piece, not the whole UI.
58
+ - **React / Vue / Svelte: NO at all.** Muten ships ZERO framework runtime; pages are `.muten` (vanilla DOM).
59
+ A widget Muten can't express enters as a vanilla `Custom` component (SKILL §13); JS logic via `use` (§14)
60
+ for the foreign piece, never the whole UI.
61
61
  - Routing uses **real paths** (`/path`, History API; deploy serves `index.html` for any path); route params work (`/product/:id` → `param id`). SEO: `meta { title "…" description "…" }` per page → `<head>` tags (og auto-derived). Shell has no local state → use a
62
- `.store`. No `toggle` op `set(not x)`. `style()` is layout tokens only; visuals go in `class()`.
62
+ `.store`. Flip a bool with `x.toggle()`. `style()` is layout tokens only; visuals go in `class()`.
63
63
  - The full reference (stores, routing, theme, every primitive) is in [`skills/muten/SKILL.md`](skills/muten/SKILL.md).
64
64
 
65
65
  ## Commands
@@ -6,16 +6,17 @@ description: Read and write Muten — the AI-first frontend DSL this app is buil
6
6
  # Muten — complete language reference
7
7
 
8
8
  Muten compiles `.muten` files to vanilla JS + fine-grained signals (no virtual DOM). The `@muten/core`
9
- Vite plugin does the compiling. You write a small declarative DSL for the UI — **not** React/JSX/Vue/Svelte/HTML;
10
- foreign code comes in only through explicit escapes (`use` for JS functions, **islands** for Svelte/React widgets — §14).
11
- A page with no reactivity compiles to plain zero-runtime HTML; a reactive one ships ~1KB of signals.
9
+ Vite plugin does the compiling. You write a small declarative DSL for the UI — **not** React/JSX/Vue/Svelte/HTML.
10
+ Muten ships ZERO framework runtime; foreign code comes in only through explicit escapes (`use` for JS logic
11
+ functions §14, `Custom` for a vanilla-JS widget §13). A page with no reactivity compiles to plain
12
+ zero-runtime HTML; a reactive one ships ~1KB of signals.
12
13
 
13
14
  ## Mental model & golden rules
14
15
  - **UI** → `.muten` files (pages, parts, the app root, the theme). **App-global state** → `.store` files.
15
16
  - **`src/app.muten` is the entry.** `index.html` loads it; the plugin boots it. **Never create `main.js`** or a `<script>` bootstrap.
16
17
  - Primitives are **PascalCase** (`Stack`, `Text`); keywords/control flow are **lowercase** (`when`, `each`, `state`).
17
18
  - `style(...)` = layout/typography **tokens** (Muten builds STRUCTURE). `class("...")` = **look** (your CSS / Tailwind); toggle reactively with `class(active when isOpen)`. Muten ships no skin.
18
- - `@name` = a state reference. `{expr}` = interpolation inside a Text/label/path string: `Text "Hi, {user.name}"`. (NOT inside `class("…")` — for a dynamic class use `class(name when cond)`.)
19
+ - A reference is a **bare name** (no sigil) everywhere — `count`, `user.name`, `cart.total`. `{expr}` = interpolation inside a Text/label/path string: `Text "Hi, {user.name}"`. (NOT inside `class("…")` — for a dynamic class use `class(name when cond)`.)
19
20
  - Each page has **one root node**. Reactivity is automatic: reading a state in interpolation / `when` / `each` re-renders just that spot.
20
21
 
21
22
  ## 1. What you CAN install / use
@@ -29,17 +30,13 @@ This is a normal **Vite** project, so the whole Vite/npm ecosystem for **styling
29
30
  **`use` logic imports** (date libs, fetch wrappers, zod, etc.).
30
31
  - **JS logic via `use … from "./lib.ts"` — YES.** Import named functions and call them in any expression
31
32
  (`use fmt from "./lib.ts"` → `Text "{fmt(x)}"`). The `.ts` is a facade over any npm. See §14.
32
- - **Svelte / React components via ISLANDS — YES.** A genuinely-interactive widget or a framework UI lib
33
- Muten can't express → mount a real `.svelte`/`.jsx` with `use X from "svelte:…"`. See §14.
34
33
  - **Host UI via the `Custom` primitive** — write vanilla JS in `src/components/<Name>.js` (charts,
35
34
  maps, a third-party widget) and mount it with `Custom`. See §Custom.
36
35
 
37
36
  ## 2. What you CANNOT do
38
- - **Don't build the page UI out of React / Vue / Svelte.** Pages are `.muten` → vanilla DOM, no framework
39
- runtime; you don't compose the app from MUI/Chakra/shadcn. BUT a *specific* interactive widget or framework
40
- lib CAN enter as an **island** (`use X from "react:…"`, §14) or a vanilla-JS `Custom` — for the foreign piece,
41
- not the whole UI. Default to `.muten`; reach for an island only when Muten genuinely can't express it.
42
- - **No JSX / hooks / `className` inside `.muten`.** Those live in the island's own `.svelte`/`.jsx` file, never in a page.
37
+ - **No React / Vue / Svelte — at all.** Muten ships ZERO framework runtime. Pages are `.muten` → vanilla DOM;
38
+ you don't compose the app from MUI/Chakra/shadcn. For a widget Muten can't express, drop to a vanilla-JS
39
+ `Custom` (§13); for JS logic, `use` a function (§14). There is no JSX/hooks/`className` anywhere.
43
40
  - **No arbitrary inline CSS via `style()`** — `style()` only takes the layout/typography tokens below.
44
41
  Visual styling (colors, borders, shadows) goes through `class("…")` + your CSS.
45
42
 
@@ -49,7 +46,7 @@ This is a normal **Vite** project, so the whole Vite/npm ecosystem for **styling
49
46
  static pages ship zero JS; reactive pages get their content (lists, `each`, interpolation from mock data)
50
47
  baked into the HTML, then the runtime boots for interactivity. No special syntax needed.
51
48
  - **Shell has no local state** — put shell/cross-page state in a `.store` (see the mobile-menu pattern).
52
- - **No `toggle` op** — flip a bool with `set(not x)`.
49
+ - **Flip a bool** with `x.toggle()` (or `x.set(not x)`).
53
50
  - **Forms**: `Form` (auto-generated from an entity) and `SearchField` (single text input) are the
54
51
  built-ins; richer custom inputs need a `Custom` component for now.
55
52
  - **Pages are single-root** (one top node per page).
@@ -78,13 +75,13 @@ entity User { # data shape + validation (implicit `id uuid`)
78
75
 
79
76
  state { # page-LOCAL reactive state
80
77
  q = "" : text
81
- users = query listUsers : list<User> # query → async; exposes @users.loading/.error/.data
78
+ users = query listUsers : list<User> # query → async; exposes users.loading/.error/.data
82
79
  }
83
80
 
84
81
  const TAX = 0.21 # compile-time immutable scalar (inlined, never reactive)
85
82
 
86
- action add mutates users <- item { # mutation; `mutates` lists what it may change (enforced)
87
- users.push(item) # ops: push | set | reset | remove
83
+ action add(item: User) mutates users { # mutation; typed params in (…); `mutates` lists what it may change (enforced)
84
+ users.push(item) # local ops: push | set | reset | remove | toggle | patch
88
85
  users.push({ name: item.name, role: "admin" }) # inline object literal — build a record inline
89
86
  if item.vip { rating.set(5) } else { rating.set(1) } # if/else = the only branching in actions
90
87
  }
@@ -140,9 +137,9 @@ No `api` field → the client named `default`. The flat `api { base, headers }`
140
137
  state { orders = query orders : list<Order> }
141
138
  sources { orders: { api: "shop", url: "/orders", at: "data" } }
142
139
 
143
- action buy mutates orders <- item { orders.create(item) } # POST /orders → append the result
144
- action edit mutates orders <- item { orders.update(item) } # PUT /orders/{id} → replace by id
145
- action drop mutates orders <- item { orders.delete(item) } # DELETE /orders/{id} → remove by id
140
+ action buy(item: Order) mutates orders { orders.create(item) } # POST /orders → append the result
141
+ action edit(item: Order) mutates orders { orders.update(item) } # PUT /orders/{id} → replace by id
142
+ action drop(item: Order) mutates orders { orders.delete(item) } # DELETE /orders/{id} → remove by id
146
143
 
147
144
  Button "Buy" -> buy(product)
148
145
  ```
@@ -158,27 +155,27 @@ when buy.error { Text "Could not save: {buy.error}" }
158
155
  state { q = "" : text page = 1 : number products = query products : list<Product> }
159
156
  sources { products: { url: "/products", at: "data" } }
160
157
 
161
- action search mutates products <- term { products.refetch(q: term, page: 1) }
158
+ action search(term: text) mutates products { products.refetch(q: term, page: 1) }
162
159
  action next mutates products { page.set(page + 1) products.refetch(q: q, page: page) }
163
160
 
164
- SearchField bind q
161
+ SearchField bind(q)
165
162
  Button "Search" -> search(q)
166
163
  Button "Next" -> next
167
164
  ```
168
165
  Pass as many params as you need (`q`, `page`, `sort`, `category`, …). The query's `.loading`/`.error` reflect the refetch.
169
166
 
170
- **Live data — `every Ns` (auto-refresh)** — append `every <duration>` to a query to poll it on a timer (`5s`, `500ms`, `2m`). The background refetch is **silent** (no loading flash) and rendering is **keyed by id**, so only the rows whose data actually changed re-render fine for live dashboards / stats / prices, even with large lists:
167
+ **Live data — `query x live` (WebSocket)** — append `live` to a query to subscribe to a **WebSocket** instead of fetching: the server PUSHES, muten reacts (event-driven, NOT polling). Each message replaces the data; the **keyed reconciliation** updates only the rows whose fields changed (focus/scroll survive) and writes **batch** into one render per frame:
171
168
  ```
172
- state { prices = query prices every 5s : list<Price> }
173
- sources { prices: { url: "/prices", at: "data" } }
174
- # each prices.data as p { Text "{p.symbol} {p.value}" } — only changed rows update, focus/scroll survive
169
+ state { prices = query prices live : list<Price> }
170
+ sources { prices: { url: "ws://feed.example.com/prices", at: "data" } }
171
+ # each prices.data as p { Text "{p.symbol} {p.value}" } — only changed rows touch the DOM
175
172
  ```
176
- Client-side only (deploy via `vite build`; static `muten build` pages don't poll). Use `refetch` for user-driven refresh, `every` for background refresh.
173
+ Plain `WebSocket` under the hood, exposed as one keyword; the socket closes automatically when the page unmounts. Client-side only (deploy via `vite build`). Use `refetch` for user-driven refresh, `live` for server-pushed real-time. (Polling via a timer was intentionally NOT added — it isn't reactive. For *huge* live lists you still virtualize + send server-side deltas, as in any framework.)
177
174
 
178
175
  **Escape hatch — explicit request** (when the API isn't RESTful): `post`/`put`/`delete` a `"client:/path"` (interpolated) with an optional `body`, in an action:
179
176
  ```
180
- action buy <- item { post "shop:/orders" body item } # any method, any path
181
- action cancel <- o { delete "shop:/orders/{o.id}/cancel" } # custom path, interpolated
177
+ action buy(item: Order) { post "shop:/orders" body item } # any method, any path
178
+ action cancel(o: Order) { delete "shop:/orders/{o.id}/cancel" } # custom path, interpolated
182
179
  action ping { post "shop:/health" } # no body, no `mutates` needed
183
180
  ```
184
181
  It uses the named client's base + headers; the action is async with `.pending`/`.error`. Prefer `create`/`update`/`delete` when the API is RESTful (those also update the list); reach for `post`/`put`/`delete` only when the convention doesn't fit.
@@ -194,21 +191,21 @@ A bare string is the node's main prop. `{ }` = children. Lay out with `style()`,
194
191
  | `Text` | paragraph, interpolates | `Text "Hi, {user.name}"` |
195
192
  | `Title` | heading; level keyword | `Title "Dashboard" h2` |
196
193
  | `Span` | inline text | `Span "{cart.total}"` |
197
- | `Image` | `<img>`, **alt required** | `Image "{p.image}" alt "{p.title}"` |
194
+ | `Image` | `<img>`, **alt required** | `Image "{p.image}" alt("{p.title}")` |
198
195
  | `Link` | client-side nav | `Link "Catalog" -> /catalog` |
199
196
  | `Button` | runs an action | `Button "Save" -> save(draft)` |
200
- | `SearchField` | text input bound to state | `SearchField bind @q "Search…"` |
201
- | `Form` | auto-form from an entity draft | `Form bind @draft submit create "Save"` |
202
- | `DataTable` | reactive table over a list/query | `DataTable @users columns(name, email)` |
197
+ | `SearchField` | text input bound to state | `SearchField bind(q) "Search…"` |
198
+ | `Form` | auto-form from an entity draft | `Form bind(draft) submit(create) "Save"` |
199
+ | `DataTable` | reactive table over a list/query | `DataTable users columns(name, email)` |
203
200
  | `RowAction` | a button inside each table row | `RowAction "Delete" -> remove(row.id)` |
204
201
  | `slot` | outlet inside `shell` | `slot` |
205
- | `Custom` | host-JS escape hatch | `Custom Chart inputs(data: @sales) on(pick: select)` |
202
+ | `Custom` | host-JS escape hatch | `Custom Chart inputs(data: sales) on(pick: select)` |
206
203
 
207
204
  Horizontal layout = a region with `style(row)` (there is no `Row` primitive). Clickable card =
208
205
  `Button { … }` or `Link "" -> /x { … }` with children instead of a label.
209
206
 
210
- Modifiers (after a primitive): `style(tokens)` · `class("css")` · `bind @state` · `submit action` ·
211
- `where(clauses)` · `columns(a, b)` · `alt "…"` · `inputs(k: v)` · `on(event: action)`.
207
+ Modifiers (after a primitive): `style(tokens)` · `class("css")` · `bind(state)` · `submit(action)` ·
208
+ `where(clauses)` · `columns(a, b)` · `alt("…")` · `inputs(k: v)` · `on(event: action)`.
212
209
  `class()` also toggles reactively (`class(active when isOpen)`); `on(event: action)` works on **any** element
213
210
  (keydown, mouseenter, change, blur, …) and calls the action — use `Button -> action(arg)` when you need an arg.
214
211
 
@@ -239,20 +236,22 @@ Responsive: prefix any token with a breakpoint → `md:cols.2`, `lg:cols.4` (`sm
239
236
 
240
237
  ## 8. State, actions & reactivity
241
238
  - `state` cells are signals; reading them in interpolation / `when` / `each` auto-updates that spot.
242
- - `query` state is async → render with `when @x.loading { … }`, then use `@x.data`.
239
+ - `query` state is async → render with `when x.loading { … }`, then use `x.data`.
243
240
  - Mutate **only** through `action`s, and only the state in `mutates` (the linter enforces it):
244
- - `list.push(x)` (append; auto-fills uuid fields) · `s.set(v)` · `s.reset()` · `list.remove(x => x.id == id)`
241
+ - `list.push(x)` (append; auto-fills uuid fields) · `s.set(v)` · `s.reset()` · `s.toggle()` (flip a bool) · `list.remove where id == itemId`
245
242
  - **Inline object literal** (build a record without leaving Muten): `posts.push({ title: draft.title, body: draft.body })`, `draft.set({ name: c.name })`. Keys must be real fields of the entity.
246
- - **Edit / move / toggle an item in place**: `list.patch(x => x.id == c.id, { done: not x.done })` — position-preserving, list ONLY the changed fields. This is the right tool for toggle/update/move (NOT remove+push, which reorders the item to the end).
243
+ - **Edit / move / toggle an item in place**: `list.patch where id == c.id with { done: not done }` — position-preserving, list ONLY the changed fields. This is the right tool for toggle/update/move (NOT remove+push, which reorders the item to the end).
244
+ - **Item fields are bare inside `where`/`with`** (item-implicit, like a `where`-filter). So a param must be named DIFFERENTLY from any field: `remove where id == id` is an error (both mean the field) — write `remove where id == itemId` with the param named `itemId`. The oracle flags the clash and tells you to rename.
247
245
  - There is no `toggle`: `flag.set(not flag)`.
248
246
  - Control flow in the tree: `when <expr> { … }` (mount/unmount), `each <list> as item { … }` (item is a scope var). Filter a list with `where`: `each posts as p where p.published { … }` renders only matching items.
249
247
  - Expressions: `== != < > <= >=`, `and or not`, `contains` (case-insensitive substring / list membership),
250
248
  `+ - * /`, ternary `c ? a : b`, parentheses, refs (`user.name`, `cart.total`, `$item.x`).
251
- - **List aggregates** (method + lambda, like `remove`) for a cart total / KPI count / "N active", NO JS needed:
252
- - `lines.sum(l => l.price * l.qty)` · `todos.count(t => not t.done)` · `reviews.avg(r => r.score)` · `min/max(x => …)`.
253
- - `.length` is the count-all; `count(x => cond)` is the filtered count. Works in interpolation, `when`, and store `get`.
254
- - **Sort a list** (same method+lambda shape; returns a sorted COPY): `each contacts.sort(c => c.name) as c { … }` (ascending) ·
255
- `each scores.sortDesc(s => s.points) as s { }` (descending). Use in `each` or a store `get`.
249
+ - **List aggregates** `by` projects a value per item, `where` is a predicate; item fields are bare (item-implicit). For a cart total / KPI count / "N active", NO JS needed:
250
+ - `lines.sum by price * qty` · `todos.count where not done` · `reviews.avg by score` · `prices.min by amount` · `prices.max by amount`.
251
+ - `.length` is the count-all; `count where cond` is the filtered count. Works in interpolation, `when`, and a `get`.
252
+ - **Embedding in a bigger expression needs grouping `()`** (the `by`/`where` body runs to the end): `when (todos.count where not done) > 0 { … }`. Standalone (in a `get`) needs none: `get openCount = todos.count where not done`.
253
+ - **Sort a list** (`sort by` ascending / `sortDesc by` descending; returns a sorted COPY): `each contacts.sort by name as c { … }` ·
254
+ `each scores.sortDesc by points as s { … }`. Use in `each` or a `get`.
256
255
 
257
256
  ## 9. Stores — app-global state
258
257
  A `.store` file = state shared across pages, **no prop drilling**. The file name is the domain.
@@ -260,13 +259,13 @@ A `.store` file = state shared across pages, **no prop drilling**. The file name
260
259
  # src/ui.store → referenced everywhere as ui.<member>
261
260
  state { menuOpen = false : bool }
262
261
  get isOpen = menuOpen # derived/memoized value (read as ui.isOpen)
263
- action toggleMenu mutates menuOpen <- x { menuOpen.set(not menuOpen) }
262
+ action toggleMenu mutates menuOpen { menuOpen.toggle() }
264
263
  effect { /* runs whenever the store state it reads changes */ }
265
264
  ```
266
265
  Use it from any page/shell by name: `when ui.menuOpen { … }`, `Button "☰" -> ui.toggleMenu`. The Vite
267
266
  plugin auto-detects every `.store` file. `get` = memoized; `effect` = reactive side-effect (Angular-style).
268
- **A page action can CALL a store action** (composition) — `action add <- d { cart.add(d) draft.reset() }` does
269
- store work AND local work in one handler (e.g. add to the store, then clear the form). Wire it with `Form submit add`.
267
+ **A page action can CALL a store action** (composition) — `action add(d: Item) mutates draft { cart.add(d) draft.reset() }` does
268
+ store work AND local work in one handler (e.g. add to the store, then clear the form). Wire it with `Form submit(add)`.
270
269
 
271
270
  ## 10. Routing — how it works
272
271
  `src/app.muten` maps URLs to pages. It uses **real paths** (`/about`, History API — client-side nav, no
@@ -316,13 +315,13 @@ routes { / -> home }
316
315
  ```
317
316
 
318
317
  ## 11. Entities, forms & validation
319
- `entity` defines a shape + constraints. `Form bind @draft submit create` auto-renders one input per
318
+ `entity` defines a shape + constraints. `Form bind(draft) submit(create)` auto-renders one input per
320
319
  field and validates on submit (per-field `.field-error`), blocking the action if invalid.
321
320
  ```
322
321
  entity Task { title text required notes text done bool }
323
322
  state { draft = {} : Task tasks = [] : list<Task> }
324
- action create mutates tasks, draft <- t { tasks.push(draft) draft.reset() }
325
- # in the page: Form bind @draft submit create "Add task"
323
+ action create(t: Task) mutates tasks, draft { tasks.push(t) draft.reset() }
324
+ # in the page: Form bind(draft) submit(create) "Add task"
326
325
  ```
327
326
 
328
327
  ## 12. Parts — reusable composition
@@ -345,47 +344,30 @@ For anything Muten can't express (a chart, a 3rd-party widget), write vanilla JS
345
344
  `src/components/<Name>.js` and mount it with `Custom`. It receives `inputs` (values/state) and wires
346
345
  DOM events to your actions via `on`. This is the ONLY way to use non-Muten UI code.
347
346
  ```
348
- Custom Chart inputs(data: @sales) on(pointSelect: select)
347
+ Custom Chart inputs(data: sales) on(pointSelect: select)
349
348
  # → src/components/Chart.js exports a mount(el, { inputs, on }) that builds vanilla DOM.
350
349
  ```
351
350
 
352
- ## 14. `use` — JS logic functions & framework islands
353
- Two escapes that pull in real JS/npm behind a typed border. Both reuse `use from`; the prefix decides which.
354
-
355
- **Logic functions** — `use` named exports from a `.ts`/`.js` file and call them in any expression:
351
+ ## 14. `use` — JS logic functions
352
+ One escape that pulls in real JS/npm behind a typed, **synchronous** border. `use` named exports from a
353
+ `.ts`/`.js` file and call them in any expression:
356
354
  ```
357
355
  use fmt, slug from "./lib/format.ts" # named exports ONLY (the .ts is a facade over any npm)
358
356
  Text "{fmt(order.total)}" # called like any expression
359
357
  Link "{slug(post.title)}" -> /blog/{post.id}
360
358
  ```
361
359
  Import zod/date-fns/nanoid/whatever *inside* `format.ts` and expose tidy named functions; Muten sees only the
362
- names, so the oracle still checks your calls. Keep the border **synchronous** (no async functions).
363
-
364
- **Islands** mount a real **Svelte or React** component for an interactive widget or framework UI lib Muten
365
- can't express (a date-picker, rich editor, a React charting component):
366
- ```
367
- use Counter from "svelte:./Counter.svelte" # `svelte:` / `react:` prefix = an ISLAND (not a logic fn)
368
- use Likes from "react:./Likes.jsx"
369
- Page {
370
- Counter(start: @total, onChange: setTotal) # props ↓ (@state) + events ↑ (a muten action)
371
- Likes(start: @total, onLike: setTotal) client:visible # lazy: hydrate when scrolled into view
372
- }
373
- ```
374
- - `prop: @state` sends a value **down** (snapshot; a React island re-renders when the signal changes). `onX: action`
375
- sends a callback that fires a **muten action** — that's how the island writes **back** to muten state.
376
- - `client:visible` / `client:idle` = **lazy** hydration (load the island's JS only when visible / idle). No
377
- directive = on load. Every island is code-split, so it never bloats the main bundle.
378
- - **Install the framework's Vite plugin** (`@sveltejs/vite-plugin-svelte` or `@vitejs/plugin-react`) next to
379
- `muten()` in `vite.config.mjs`. The component file is normal Svelte/React — it owns its own tooling; Muten
380
- only validates the node + its args. This is how a **React/Svelte component lib** comes in: wrap it in an island.
360
+ names, so the oracle still checks your calls. Keep the border **synchronous** (no async functions). For a
361
+ visual widget Muten can't express (a chart, a map, a date-picker), drop to a vanilla-JS `Custom` (§13) — there
362
+ is no framework-component escape; Muten owns the whole UI.
381
363
 
382
364
  ## 15. Gotchas
383
- - It's NOT React: PascalCase primitives + `{ }` children; no JSX/hooks/`className` (those live in island files, §14).
365
+ - It is NOT JSX PascalCase primitives + `{ }` children; no JSX/hooks/`className` anywhere.
384
366
  - No `main.js`/`<script>` — `app.muten` is the entry.
385
367
  - `style()` (layout tokens) ≠ `class()` (look). No colors/borders in `style()`.
386
- - `Image` without `alt` fails validation (`alt ""` for decorative).
368
+ - `Image` without `alt` fails validation (`alt("")` for decorative).
387
369
  - Actions may only touch their declared `mutates`.
388
- - Want a library? CSS → `class()`. JS function → `use` (§14). A widget → `Custom`. A React/Svelte component an **island** (§14).
370
+ - Want a library? CSS → `class()`. JS function → `use` (§14). A widget → `Custom` (§13). There is no framework-component escape.
389
371
 
390
372
  ## 16. Minimal full app
391
373
  ```
@@ -395,11 +377,11 @@ routes { / -> home }
395
377
  # src/pages/home/home.muten
396
378
  screen home
397
379
  state { name = "" : text }
398
- action greet mutates name <- v { name.set(v) }
380
+ action greet(v: text) mutates name { name.set(v) }
399
381
 
400
382
  Page style(padding.lg, gap.md) {
401
383
  Title "Hello"
402
- SearchField bind @name "Your name"
384
+ SearchField bind(name) "Your name"
403
385
  when name { Text "Hi, {name}!" }
404
386
  }
405
387
  ```
@@ -8,7 +8,7 @@
8
8
  "lint": "muten lint"
9
9
  },
10
10
  "dependencies": {
11
- "@muten/core": "^0.0.13"
11
+ "@muten/core": "^0.0.14"
12
12
  },
13
13
  "devDependencies": {
14
14
  "vite": "^8.0.16"
@@ -6,7 +6,7 @@ Page class("welcome") {
6
6
  Stack class("wrap") {
7
7
 
8
8
  Stack class("hero") {
9
- Image "/muten.svg" alt "muten logo" class("logo")
9
+ Image "/muten.svg" alt("muten logo") class("logo")
10
10
  Title "muten" class("brand")
11
11
  Text "An AI-first frontend framework." class("tagline")
12
12
  Text "You write small .muten files. muten compiles them to vanilla JS plus fine-grained signals — no virtual DOM, no runtime to ship. The language stays small and analyzable, so an AI can locate and mutate your app cheaply." class("lead")