create-muten 0.0.7 → 0.0.9

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
@@ -60,8 +60,8 @@ In an interactive terminal it prompts for a few things (defaults in parentheses)
60
60
  | Prompt | Options | Default |
61
61
  |---|---|---|
62
62
  | **Project name** | any valid folder name | `muten-app` |
63
- | **Template** | `basic` / `routing` / `full` | `basic` |
64
- | **Stylesheet** | `css` / `scss` | `css` |
63
+ | **Template** | `muten` / `muten + React` / `muten + Svelte` | `muten` |
64
+ | **Styling** | `css` / `scss` | `css` |
65
65
  | **Add Tailwind CSS?** | `Y` / `n` (CSS only) | `n` |
66
66
  | **Add DaisyUI?** | `Y` / `n` (needs Tailwind) | `n` |
67
67
  | **Package manager** | `npm` / `pnpm` / `yarn` / `bun` | the one that launched it |
@@ -70,18 +70,25 @@ In an interactive terminal it prompts for a few things (defaults in parentheses)
70
70
  Tailwind is an optional add-on **on top of** CSS (the look layer; you still style via `class("…")`) — it
71
71
  wires `@tailwindcss/vite` + an `@import "tailwindcss"` and notes the setup in the app's `.claude/` guide.
72
72
 
73
- ## Templates
73
+ ## Templates (flavors)
74
+
75
+ Every flavor scaffolds the **same** welcome page and the same `.muten` workflow — the only difference is
76
+ whether a framework's island plugin is pre-wired:
74
77
 
75
78
  | Template | What you get |
76
79
  |---|---|
77
- | **basic** | one page — the minimal starter |
78
- | **routing** | a persistent shell (navbar + footer) + multiple real-path routes + a static `about` page |
79
- | **full** | routing + a `.store` + an `api` block + a source-backed products page + Tailwind — a real data app |
80
+ | **muten** | pure muten — the AI-first DSL, zero framework runtime |
81
+ | **muten + React** | same, plus `@vitejs/plugin-react` + React, so you can drop in a **React island** (shadcn/Radix, any React lib) |
82
+ | **muten + Svelte** | same, plus `@sveltejs/vite-plugin-svelte` + Svelte, for **Svelte islands** (a lighter runtime) |
83
+
84
+ An *island* is a real framework component used as a node — `use X from "react:./X.jsx"` →
85
+ `X(value: @s, onChange: act) client:visible` (props ↓ + events ↑, lazy + code-split). Default to `.muten`;
86
+ reach for an island only for a widget muten can't express.
80
87
 
81
- When **Tailwind or DaisyUI** is selected, `theme.muten` is centralized to **match Tailwind's scale** (so
88
+ When **Tailwind or DaisyUI** is added, `theme.muten` is centralized to **match Tailwind's scale** (so
82
89
  `style()` tokens and Tailwind utilities share one scale, e.g. `style(gap.md)` == `gap-4`); plain CSS/SCSS
83
90
  keeps the default scale. **DaisyUI** adds component classes (`btn`, `card`, `modal`) usable in `class("…")` —
84
- the closest fit to shadcn that works in Muten (pure classes, no React; behavior is Muten state + `on()`).
91
+ pure classes, no React; behavior is Muten state + `on()`.
85
92
 
86
93
  If you accept the last prompt it runs `<pm> install` followed by `<pm> run dev` — your app is live in a
87
94
  single step. Choosing SCSS also adds `sass` and switches the stylesheet to `.scss` automatically.
@@ -99,7 +106,7 @@ create-muten my-app --css --no-install # just scaffold, decide later
99
106
  | Flag | Effect |
100
107
  |---|---|
101
108
  | `<name>` | the project folder (positional argument) |
102
- | `--template <basic\|routing\|full>` | which starter (default: `basic`; `full` implies Tailwind) |
109
+ | `--template <muten\|react\|svelte>` | flavor (default: `muten`); `--react` / `--svelte` are shortcuts |
103
110
  | `--css` / `--scss` | pick the stylesheet (default: `css`) |
104
111
  | `--tailwind` | add Tailwind CSS v4 on top of CSS (forces `--css`) |
105
112
  | `--daisyui` | add DaisyUI component classes (implies `--tailwind`) |
@@ -128,6 +135,23 @@ my-app/
128
135
  There is **no hand-written `main.js`**: the Vite plugin compiles `src/app.muten` into the app's entry,
129
136
  so the whole app is `.muten` from the first line.
130
137
 
138
+ ## What you can build
139
+
140
+ A muten app reaches the whole web platform through bounded escapes — reach for the **lowest tier that works**:
141
+
142
+ - **Pure muten** — CRUD / SaaS / catalog / dashboard / content: pages, routing, `state`/`store`, `query` over
143
+ REST, `Form` + validation, `DataTable`, `when`/`each`, SSG + SEO. The declarative 80%, zero extra deps.
144
+ - **muten + the platform** *(no framework runtime)* — native HTML (`<input type="date">`, `<dialog>`) + `class()`,
145
+ CSS libs (Tailwind / DaisyUI), **vanilla JS via `Custom`** (charts, maps, date-pickers, rich-text, grids),
146
+ `use fmt from "./lib.ts"` for any JS logic. Almost every "hard widget" lands here, *without React*.
147
+ - **Svelte / React island** (`--svelte` / `--react`) — only when the component *is* a framework component
148
+ (shadcn/ui, a React-only lib). Ships that runtime, lazy + code-split. The narrow last resort.
149
+
150
+ Full reference (every primitive, the three tiers, the roadmap): [`@muten/core`](https://www.npmjs.com/package/@muten/core).
151
+
152
+ > **Status: pre-1.0.** The core (language, compiler, CLI, Vite plugin, extension, islands) is solid; the
153
+ > ecosystem is young and full island SSR is experimental. Great for real apps, not yet for critical production.
154
+
131
155
  ## Requirements
132
156
 
133
157
  - **Node.js 18+**
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 basic|routing|full] [--css|--scss] [--tailwind] [--daisyui] [--pm npm|pnpm|yarn|bun] [--no-install]
5
+ // create-muten [name] [--template basic|routing|full] [--css|--scss] [--tailwind] [--daisyui] [--svelte] [--react] [--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.
@@ -15,8 +15,7 @@ import color from 'picocolors';
15
15
 
16
16
  const SELF = dirname(fileURLToPath(import.meta.url));
17
17
  const TEMPLATE = join(SELF, 'template');
18
- const OVERLAYS = join(SELF, 'overlays'); // additive layers per template variant (routing, full)
19
- const TEMPLATES = ['basic', 'routing', 'full'];
18
+ 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
 
@@ -37,13 +36,55 @@ const tailwindStyles = (daisyui) => `@import "tailwindcss";${daisyui ? '\n@plugi
37
36
  /* Muten layout primitive Tailwind doesn't define */
38
37
  .stack { display: flex; flex-direction: column; }
39
38
  `;
40
- const TAILWIND_VITE = `import muten from '@muten/core/vite-plugin-muten.js';
41
- import tailwindcss from '@tailwindcss/vite';
42
-
43
- export default {
44
- plugins: [muten(), tailwindcss()],
39
+ // Starter welcome page styles (used by the scaffolded home.muten). Self-contained plain CSS — looks good
40
+ // with or without Tailwind; delete it (and the page) when you build your own.
41
+ const WELCOME_CSS = `
42
+ /* starter welcome page (src/pages/home/home.muten) — delete when you build your own — */
43
+ .welcome { background: #fafafa; color: #18181b; padding: 64px 24px; }
44
+ .wrap { max-width: 720px; margin: 0 auto; display: flex; flex-direction: column; gap: 52px; }
45
+ .hero { text-align: center; }
46
+ .logo { width: 64px; height: 64px; border-radius: 16px; margin: 0 auto; box-shadow: 0 6px 20px rgba(255,94,0,.28); }
47
+ .brand { font-size: clamp(40px, 8vw, 58px); font-weight: 800; letter-spacing: -.04em; line-height: 1; margin-top: 22px; background: linear-gradient(135deg, #ff5e00, #ff9a00); -webkit-background-clip: text; background-clip: text; color: transparent; }
48
+ .tagline { font-size: 18px; font-weight: 600; color: #27272a; margin-top: 10px; }
49
+ .lead { max-width: 580px; margin: 14px auto 0; color: #52525b; font-size: 16px; line-height: 1.65; }
50
+ .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
51
+ .stat { border: 1px solid #e4e4e7; border-radius: 14px; padding: 22px 16px; text-align: center; background: #fff; }
52
+ .stat-n { font-size: 26px; font-weight: 800; letter-spacing: -.02em; color: #ff5e00; }
53
+ .stat-l { color: #71717a; font-size: 12px; line-height: 1.45; margin-top: 6px; }
54
+ .section { display: flex; flex-direction: column; gap: 14px; }
55
+ .h2 { font-size: 22px; font-weight: 700; letter-spacing: -.02em; }
56
+ .snippet { background: #18181b; color: #e4e4e7; border-radius: 14px; padding: 20px 22px; margin: 0; overflow-x: auto; white-space: pre; font: 13px/1.7 ui-monospace, 'SF Mono', Menlo, Consolas, monospace; }
57
+ .note { color: #71717a; font-size: 13px; line-height: 1.55; }
58
+ .cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }
59
+ .card { border: 1px solid #e4e4e7; border-radius: 14px; padding: 18px; background: #fff; }
60
+ .card-title { font-size: 14px; font-weight: 600; margin-bottom: 5px; }
61
+ .card-text { color: #71717a; font-size: 13px; line-height: 1.5; }
62
+ @media (max-width: 560px) { .stats, .cards { grid-template-columns: 1fr; } }
63
+ `;
64
+ // vite.config composed from the chosen options — muten always; svelte/react add island plugins; tailwind last.
65
+ const viteConfig = ({ tailwind, svelte, react }) => {
66
+ const imports = [`import muten from '@muten/core/vite-plugin-muten.js';`];
67
+ 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
+ if (tailwind) { imports.push(`import tailwindcss from '@tailwindcss/vite';`); plugins.push('tailwindcss()'); }
71
+ return `${imports.join('\n')}\n\nexport default {\n plugins: [${plugins.join(', ')}],\n};\n`;
45
72
  };
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.
46
86
  `;
87
+ };
47
88
  // When Tailwind/DaisyUI is chosen, the Muten token scale is centralized to MATCH Tailwind's defaults, so
48
89
  // style() tokens and Tailwind utilities share one scale (e.g. style(gap.md) == gap-4 == 1rem). Plain
49
90
  // css/scss keeps the default theme.muten. (DaisyUI builds on Tailwind's scale; its colors come via @plugin.)
@@ -78,15 +119,23 @@ const detectPM = () => { const ua = process.env.npm_config_user_agent || ''; ret
78
119
  const validName = (n) => /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(n);
79
120
  const keep = (v) => { if (isCancel(v)) { cancel('Cancelled.'); process.exit(0); } return v; };
80
121
 
122
+ // The muten mark + wordmark, in the brand orange (#FF5E00) — shown before the prompts. Truecolor ANSI
123
+ // (modern terminals); degrades to plain text where unsupported.
124
+ const logo = () => {
125
+ const o = '\x1b[38;2;255;94;0m', tile = '\x1b[48;2;255;94;0m\x1b[1m\x1b[97m', b = '\x1b[1m', d = '\x1b[2m', r = '\x1b[0m';
126
+ console.log(`\n ${tile} M ${r} ${b}${o}muten${r}`);
127
+ console.log(` ${d} the AI-first frontend framework${r}\n`);
128
+ };
129
+
81
130
  async function main() {
82
131
  const argv = process.argv.slice(2);
83
132
  const has = (f) => argv.includes(f);
84
133
  const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; };
85
134
  if (has('-v') || has('--version')) { console.log(PKG.version); return; }
86
- if (has('-h') || has('--help')) { console.log('Usage: create-muten [name] [--template basic|routing|full] [--css|--scss] [--tailwind] [--daisyui] [--pm npm|pnpm|yarn|bun] [--no-install]'); return; }
135
+ if (has('-h') || has('--help')) { console.log('Usage: create-muten [name] [--template muten|react|svelte] [--css|--scss] [--tailwind] [--daisyui] [--pm npm|pnpm|yarn|bun] [--no-install]'); return; }
87
136
 
88
137
  let name = argv.filter((a, i) => !a.startsWith('-') && argv[i - 1] !== '--pm' && argv[i - 1] !== '--template')[0];
89
- let template = val('--template') || (has('--full') ? 'full' : has('--routing') ? 'routing' : has('--basic') ? 'basic' : undefined);
138
+ let template = val('--template') || (has('--react') ? 'react' : has('--svelte') ? 'svelte' : has('--muten') ? 'muten' : undefined); // flavor: muten | react | svelte
90
139
  let style = has('--scss') ? 'scss' : has('--css') ? 'css' : undefined; // the base stylesheet
91
140
  let tailwind = has('--tailwind') ? true : undefined; // optional add-on (CSS only)
92
141
  let daisyui = has('--daisyui') ? true : undefined; // component classes on Tailwind
@@ -99,39 +148,39 @@ async function main() {
99
148
 
100
149
  // Styled prompts only with a real TTY (piped/CI input would hang); otherwise use flags + defaults.
101
150
  if (process.stdin.isTTY) {
102
- intro(color.bgCyan(color.black(' create-muten ')) + color.dim(' the AI-first frontend framework'));
151
+ logo();
152
+ intro(color.dim(`create-muten v${PKG.version}`));
103
153
  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 }));
104
154
  if (!template) template = keep(await select({ message: 'Template', options: [
105
- { value: 'basic', label: 'Basic', hint: 'one page' },
106
- { value: 'routing', label: 'Routing', hint: 'shell + multiple pages' },
107
- { value: 'full', label: 'Routing + store + API + Tailwind', hint: 'a real data app' },
155
+ { value: 'muten', label: 'muten', hint: 'pure — the AI-first DSL, zero framework runtime' },
156
+ { value: 'react', label: 'muten + React', hint: 'React islands: shadcn, Radix, any React lib' },
157
+ { value: 'svelte', label: 'muten + Svelte', hint: 'Svelte islands: a lighter runtime' },
108
158
  ] }));
109
- if (template === 'full') tailwind = true; // the full template implies Tailwind
110
- // Tailwind is an add-on on top of CSS (Tailwind v4 is CSS-native; Sass isn't recommended).
111
- if (!style && !tailwind) style = keep(await select({ message: 'Stylesheet', options: [
159
+ if (!style) style = keep(await select({ message: 'Styling', options: [
112
160
  { value: 'css', label: 'CSS', hint: 'plain, zero deps' },
113
161
  { value: 'scss', label: 'SCSS', hint: 'adds sass' },
114
162
  ] }));
115
- if (tailwind === undefined) tailwind = style === 'css' ? keep(await confirm({ message: 'Add Tailwind CSS? (utility classes via class("…"))', initialValue: false })) : false;
116
- if (tailwind && daisyui === undefined) daisyui = keep(await confirm({ message: 'Add DaisyUI? (component classes: btn, card, modal…)', initialValue: template === 'full' }));
163
+ if (tailwind === undefined) tailwind = style === 'css' ? keep(await confirm({ message: 'Add Tailwind CSS?', initialValue: false })) : false;
164
+ if (tailwind && daisyui === undefined) daisyui = keep(await confirm({ message: 'Add DaisyUI? (component classes: btn, card, modal…)', initialValue: false }));
117
165
  }
118
166
  name = name || 'muten-app';
119
- template = template || 'basic';
120
- if (template === 'full') tailwind = true; // full implies Tailwind
121
- if (daisyui) tailwind = true; // DaisyUI is a Tailwind plugin
167
+ template = template || 'muten';
122
168
  style = style || 'css';
169
+ if (daisyui) tailwind = true; // DaisyUI is a Tailwind plugin
123
170
  if (tailwind === undefined) tailwind = false;
124
171
  if (daisyui === undefined) daisyui = false;
125
- if (tailwind) style = 'css'; // Tailwind implies a CSS base (not SCSS)
172
+ if (tailwind) style = 'css'; // Tailwind v4 is CSS-native (not SCSS)
173
+ const svelte = template === 'svelte'; // the flavor IS the islands choice
174
+ const react = template === 'react';
126
175
  pm = pm || dpm;
127
176
  if (install === undefined) install = false;
128
177
 
129
178
  const target = resolve(name);
130
179
  if (existsSync(target)) { (process.stdin.isTTY ? cancel : console.error)(`"${name}" already exists.`); process.exit(1); }
131
180
 
132
- // scaffold from ./template (the basic base) + layer the chosen variant's overlay + the stylesheet/add-ons
181
+ // EVERY flavor scaffolds the SAME base template (identical welcome page); react/svelte only add the
182
+ // island plugin + deps below, and tailwind/daisyui only swap the stylesheet.
133
183
  cpSync(TEMPLATE, target, { recursive: true });
134
- if (template !== 'basic') cpSync(join(OVERLAYS, template), target, { recursive: true }); // routing / full overlay
135
184
  const ignore = join(target, '_gitignore');
136
185
  if (existsSync(ignore)) renameSync(ignore, join(target, '.gitignore'));
137
186
 
@@ -139,19 +188,23 @@ async function main() {
139
188
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
140
189
  pkg.name = name;
141
190
  const addDev = (deps) => { pkg.devDependencies = { ...(pkg.devDependencies || {}), ...deps }; };
191
+ const addDep = (deps) => { pkg.dependencies = { ...(pkg.dependencies || {}), ...deps }; };
192
+ const appendAgents = (text) => { const f = join(target, '.claude', 'AGENTS.md'); if (existsSync(f)) writeFileSync(f, readFileSync(f, 'utf8') + text); };
142
193
 
143
194
  if (tailwind) {
144
- writeFileSync(join(target, 'src', 'styles.css'), tailwindStyles(daisyui));
145
- writeFileSync(join(target, 'vite.config.mjs'), TAILWIND_VITE);
195
+ writeFileSync(join(target, 'src', 'styles.css'), tailwindStyles(daisyui) + WELCOME_CSS);
146
196
  writeFileSync(join(target, 'theme.muten'), TAILWIND_THEME); // scale centralized to Tailwind's
147
197
  addDev({ tailwindcss: '^4.0.0', '@tailwindcss/vite': '^4.0.0' });
148
198
  if (daisyui) addDev({ daisyui: '^5.0.0' });
149
- const agents = join(target, '.claude', 'AGENTS.md'); // tell the AI what styling is available
150
- if (existsSync(agents)) writeFileSync(agents, readFileSync(agents, 'utf8') + TAILWIND_NOTE + (daisyui ? DAISY_NOTE : ''));
199
+ appendAgents(TAILWIND_NOTE + (daisyui ? DAISY_NOTE : '')); // tell the AI what styling is available
151
200
  } else {
152
- writeFileSync(join(target, 'src', style === 'scss' ? 'styles.scss' : 'styles.css'), RESET);
201
+ writeFileSync(join(target, 'src', style === 'scss' ? 'styles.scss' : 'styles.css'), RESET + WELCOME_CSS);
153
202
  }
154
203
  if (style === 'scss') addDev({ sass: '^1.101.0' });
204
+ if (svelte) { addDep({ svelte: '^5.0.0' }); addDev({ '@sveltejs/vite-plugin-svelte': '^7.0.0' }); }
205
+ if (react) { addDep({ react: '^19.0.0', 'react-dom': '^19.0.0' }); addDev({ '@vitejs/plugin-react': '^6.0.0' }); }
206
+ if (svelte || react) appendAgents(ISLANDS_NOTE({ svelte, react }));
207
+ writeFileSync(join(target, 'vite.config.mjs'), viteConfig({ tailwind, svelte, react })); // composed: muten + chosen plugins
155
208
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
156
209
 
157
210
  const desc = `${template}, ${style}${tailwind ? ' + Tailwind' : ''}${daisyui ? ' + DaisyUI' : ''}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-muten",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "Scaffold a new Muten app.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -2,8 +2,8 @@
2
2
 
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
- trained on Muten yet, so **follow this guide instead of guessing**: never import React/Vue, never write
6
- `.jsx`/`.vue`, never add a JS bootstrap.
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.
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).
@@ -49,13 +49,15 @@ 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.
52
53
  - **Actions:** `action add mutates users <- item { users.push(item) }` — ops: `push/set/reset/remove`; branch with `if/else`.
53
54
  - **Tokens:** `gap.md padding.lg cols.3 text.lg row center between` — responsive prefix: `md:cols.2`.
54
55
 
55
56
  ## Dependencies & limits
56
57
  - **CSS / Tailwind / SCSS: YES** — it's a Vite app; install them and use `class("…")` + your CSS.
57
- - **React / Vue / Svelte UI libraries: NO** — the UI is `.muten` (vanilla DOM, no JS framework runtime).
58
- Need a JS widget? Wrap it in a `Custom` host component (`src/components/<Name>.js`).
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.
59
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
60
62
  `.store`. No `toggle` op → `set(not x)`. `style()` is layout tokens only; visuals go in `class()`.
61
63
  - The full reference (stores, routing, theme, every primitive) is in [`skills/muten/SKILL.md`](skills/muten/SKILL.md).
@@ -6,7 +6,8 @@ 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 — **never** React/JSX/Vue/Svelte/HTML.
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).
10
11
  A page with no reactivity compiles to plain zero-runtime HTML; a reactive one ships ~1KB of signals.
11
12
 
12
13
  ## Mental model & golden rules
@@ -24,16 +25,21 @@ This is a normal **Vite** project, so the whole Vite/npm ecosystem for **styling
24
25
  `class()` emits raw class names, so any CSS framework (Tailwind, UnoCSS, Bootstrap CSS, your own CSS) works.
25
26
  - **Sass/SCSS** — supported out of the box if you scaffolded with SCSS (or add `sass`); use `src/styles.scss`.
26
27
  - **Any Vite plugin / PostCSS plugin** — add it to `vite.config.mjs` alongside `muten()`.
27
- - **Data / utility npm packages** — usable inside `.store` logic and inside `Custom` host components
28
- (date libs, fetch wrappers, zod, etc.).
28
+ - **Data / utility npm packages** — usable inside `.store` logic, inside `Custom` host components, and via
29
+ **`use` logic imports** (date libs, fetch wrappers, zod, etc.).
30
+ - **JS logic via `use … from "./lib.ts"` — YES.** Import named functions and call them in any expression
31
+ (`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.
29
34
  - **Host UI via the `Custom` primitive** — write vanilla JS in `src/components/<Name>.js` (charts,
30
35
  maps, a third-party widget) and mount it with `Custom`. See §Custom.
31
36
 
32
37
  ## 2. What you CANNOT do
33
- - **No React / Vue / Svelte component libraries as UI.** There is no React/Vue runtime — the UI is
34
- `.muten` compiled to vanilla DOM. You cannot drop in MUI, Chakra, Ant, shadcn, a React table, etc.
35
- If you truly need a JS widget, wrap it yourself in a `Custom` component (vanilla JS).
36
- - **No JSX / `.jsx` / `.vue` / hand-written DOM in pages.** No `className`, no hooks, no lifecycle.
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
43
  - **No arbitrary inline CSS via `style()`** — `style()` only takes the layout/typography tokens below.
38
44
  Visual styling (colors, borders, shadows) goes through `class("…")` + your CSS.
39
45
 
@@ -325,15 +331,45 @@ Custom Chart inputs(data: @sales) on(pointSelect: select)
325
331
  # → src/components/Chart.js exports a mount(el, { inputs, on }) that builds vanilla DOM.
326
332
  ```
327
333
 
328
- ## 14. Gotchas
329
- - It's NOT React: PascalCase primitives + `{ }` children; no JSX/hooks/`className`.
334
+ ## 14. `use` — JS logic functions & framework islands
335
+ Two escapes that pull in real JS/npm behind a typed border. Both reuse `use from`; the prefix decides which.
336
+
337
+ **Logic functions** — `use` named exports from a `.ts`/`.js` file and call them in any expression:
338
+ ```
339
+ use fmt, slug from "./lib/format.ts" # named exports ONLY (the .ts is a facade over any npm)
340
+ Text "{fmt(order.total)}" # called like any expression
341
+ Link "{slug(post.title)}" -> /blog/{post.id}
342
+ ```
343
+ Import zod/date-fns/nanoid/whatever *inside* `format.ts` and expose tidy named functions; Muten sees only the
344
+ names, so the oracle still checks your calls. Keep the border **synchronous** (no async functions).
345
+
346
+ **Islands** — mount a real **Svelte or React** component for an interactive widget or framework UI lib Muten
347
+ can't express (a date-picker, rich editor, a React charting component):
348
+ ```
349
+ use Counter from "svelte:./Counter.svelte" # `svelte:` / `react:` prefix = an ISLAND (not a logic fn)
350
+ use Likes from "react:./Likes.jsx"
351
+ Page {
352
+ Counter(start: @total, onChange: setTotal) # props ↓ (@state) + events ↑ (a muten action)
353
+ Likes(start: @total, onLike: setTotal) client:visible # lazy: hydrate when scrolled into view
354
+ }
355
+ ```
356
+ - `prop: @state` sends a value **down** (snapshot; a React island re-renders when the signal changes). `onX: action`
357
+ sends a callback that fires a **muten action** — that's how the island writes **back** to muten state.
358
+ - `client:visible` / `client:idle` = **lazy** hydration (load the island's JS only when visible / idle). No
359
+ directive = on load. Every island is code-split, so it never bloats the main bundle.
360
+ - **Install the framework's Vite plugin** (`@sveltejs/vite-plugin-svelte` or `@vitejs/plugin-react`) next to
361
+ `muten()` in `vite.config.mjs`. The component file is normal Svelte/React — it owns its own tooling; Muten
362
+ only validates the node + its args. This is how a **React/Svelte component lib** comes in: wrap it in an island.
363
+
364
+ ## 15. Gotchas
365
+ - It's NOT React: PascalCase primitives + `{ }` children; no JSX/hooks/`className` (those live in island files, §14).
330
366
  - No `main.js`/`<script>` — `app.muten` is the entry.
331
367
  - `style()` (layout tokens) ≠ `class()` (look). No colors/borders in `style()`.
332
368
  - `Image` without `alt` fails validation (`alt ""` for decorative).
333
369
  - Actions may only touch their declared `mutates`.
334
- - Want a library? If it's CSS → `class()`. If it's a JS widget → `Custom`. If it's React/Vue UInot possible.
370
+ - Want a library? CSS → `class()`. JS function `use` (§14). A widget → `Custom`. A React/Svelte componentan **island** (§14).
335
371
 
336
- ## 15. Minimal full app
372
+ ## 16. Minimal full app
337
373
  ```
338
374
  # src/app.muten
339
375
  routes { / -> home }
@@ -8,7 +8,7 @@
8
8
  "lint": "muten lint"
9
9
  },
10
10
  "dependencies": {
11
- "@muten/core": "^0.0.6"
11
+ "@muten/core": "^0.0.8"
12
12
  },
13
13
  "devDependencies": {
14
14
  "vite": "^8.0.16"
@@ -0,0 +1,4 @@
1
+ <svg width="157" height="157" viewBox="0 0 157 157" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M0 12C0 5.37258 5.37258 0 12 0H145C151.627 0 157 5.37258 157 12V145C157 151.627 151.627 157 145 157H12C5.37258 157 0 151.627 0 145V12Z" fill="#FF5E00"/>
3
+ <path d="M73.2841 144V91.6364H89.1364V101.25H89.7159C90.8068 98.0682 92.6477 95.5568 95.2386 93.7159C97.8295 91.875 100.92 90.9545 104.511 90.9545C108.148 90.9545 111.261 91.8864 113.852 93.75C116.443 95.6136 118.091 98.1136 118.795 101.25H119.341C120.318 98.1364 122.227 95.6477 125.068 93.7841C127.909 91.8977 131.261 90.9545 135.125 90.9545C140.08 90.9545 144.102 92.5455 147.193 95.7273C150.284 98.8864 151.83 103.227 151.83 108.75V144H135.159V112.568C135.159 109.955 134.489 107.966 133.148 106.602C131.807 105.216 130.068 104.523 127.932 104.523C125.636 104.523 123.83 105.273 122.511 106.773C121.216 108.25 120.568 110.239 120.568 112.739V144H104.545V112.398C104.545 109.966 103.886 108.045 102.568 106.636C101.25 105.227 99.5114 104.523 97.3523 104.523C95.8977 104.523 94.6136 104.875 93.5 105.58C92.3864 106.261 91.5114 107.239 90.875 108.511C90.2614 109.784 89.9545 111.284 89.9545 113.011V144H73.2841Z" fill="white"/>
4
+ </svg>
@@ -0,0 +1,19 @@
1
+ // Welcome-page code block (a `Custom` host component — the escape hatch for anything muten can't express).
2
+ // The snippet lives here in JS so it can contain quotes and { } that a .muten string can't. Delete with
3
+ // the welcome page. Used from home.muten as: Custom Snippet
4
+ const CODE = `screen home
5
+
6
+ state { count = 0 : number }
7
+ action inc mutates count { count.set(count + 1) }
8
+
9
+ Page style(padding.lg, gap.md) {
10
+ Title "Count: {count}"
11
+ Button "+1" -> inc
12
+ }`;
13
+
14
+ function mount(el) {
15
+ const pre = document.createElement('pre');
16
+ pre.className = 'snippet';
17
+ pre.textContent = CODE;
18
+ el.appendChild(pre);
19
+ }
@@ -1,9 +1,45 @@
1
- # A page = one .muten file; its folder name (home) is the route target from app.muten.
2
- # `Page` is the <main> landmark; lay it out with style() tokens, skin it with class("…").
1
+ # The starter welcome page. Skin with class(…) + src/styles.css. Delete it and build your own or just
2
+ # ask your AI assistant; it has the full muten reference in .claude/. The code sample is a Custom component
3
+ # (src/components/Snippet.js) because a .muten string can't hold the quotes/braces of muten source.
3
4
  screen home
4
5
 
5
- Page style(padding.xl, gap.md) {
6
- Title "Hello, Muten"
7
- Text "Edit src/pages/home/home.muten to build your first page."
8
- Text "Add routes in src/app.muten; define your token scale in theme.muten."
6
+ Page class("welcome") {
7
+ Stack class("wrap") {
8
+
9
+ Stack class("hero") {
10
+ Image "/muten.svg" alt "muten logo" class("logo")
11
+ Title "muten" class("brand")
12
+ Text "An AI-first frontend framework." class("tagline")
13
+ 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")
14
+ }
15
+
16
+ Stack class("stats") {
17
+ Stack class("stat") { Title "2.8 KB" h3 class("stat-n") Text "muten ships, gzipped" class("stat-l") }
18
+ Stack class("stat") { Title "5 to 21x" h3 class("stat-n") Text "less JS than React, Vue, Svelte" class("stat-l") }
19
+ Stack class("stat") { Title "0 KB" h3 class("stat-n") Text "on a static page" class("stat-l") }
20
+ }
21
+
22
+ Stack class("section") {
23
+ Title "A whole page, in muten" h2 class("h2")
24
+ Custom Snippet
25
+ Text "Reactivity is automatic — read a state and only that spot re-renders. No hooks, no effects to wire." class("note")
26
+ }
27
+
28
+ Stack class("section") {
29
+ Title "Where to go next" h2 class("h2")
30
+ Stack class("cards") {
31
+ Stack class("card") { Title "Edit this page" h3 class("card-title") Text "src/pages/home/home.muten" class("card-text") }
32
+ Stack class("card") { Title "Add a route" h3 class("card-title") Text "Map URLs to pages in src/app.muten" class("card-text") }
33
+ Stack class("card") { Title "Style it" h3 class("card-title") Text "class(…) plus your CSS in src/styles.css — Tailwind optional" class("card-text") }
34
+ Stack class("card") { Title "Need React or Svelte?" h3 class("card-title") Text "Drop a real component in as an island, lazy and code-split" class("card-text") }
35
+ }
36
+ }
37
+
38
+ Stack class("section") {
39
+ Title "Your AI already knows muten" h2 class("h2")
40
+ Text "The full language reference ships in this project under .claude — an AGENTS guide plus a Claude skill. Ask your assistant to build a page; it reads that instead of guessing." class("lead")
41
+ Text "More Claude skills: docs.claude.com — Claude Code, Skills." class("note")
42
+ }
43
+
44
+ }
9
45
  }