create-muten 0.0.6 → 0.0.8

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
@@ -64,6 +64,7 @@ In an interactive terminal it prompts for a few things (defaults in parentheses)
64
64
  | **Stylesheet** | `css` / `scss` | `css` |
65
65
  | **Add Tailwind CSS?** | `Y` / `n` (CSS only) | `n` |
66
66
  | **Add DaisyUI?** | `Y` / `n` (needs Tailwind) | `n` |
67
+ | **Framework islands?** | `none` / `svelte` / `react` / `both` | `none` |
67
68
  | **Package manager** | `npm` / `pnpm` / `yarn` / `bun` | the one that launched it |
68
69
  | **Install deps and start dev now?** | `Y` / `n` | `Y` |
69
70
 
@@ -81,7 +82,12 @@ wires `@tailwindcss/vite` + an `@import "tailwindcss"` and notes the setup in th
81
82
  When **Tailwind or DaisyUI** is selected, `theme.muten` is centralized to **match Tailwind's scale** (so
82
83
  `style()` tokens and Tailwind utilities share one scale, e.g. `style(gap.md)` == `gap-4`); plain CSS/SCSS
83
84
  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()`).
85
+ pure classes, no React; behavior is Muten state + `on()`.
86
+
87
+ **Framework islands** (`svelte` / `react` / `both`) wire `@sveltejs/vite-plugin-svelte` / `@vitejs/plugin-react`
88
+ into `vite.config.mjs` so you can drop a real Svelte/React component (incl. React libs like **shadcn/Radix**)
89
+ into a page as an *island* — `use X from "react:./X.jsx"` → `X(value: @s, onChange: act) client:visible`
90
+ (props ↓ + events ↑, lazy + code-split). Default to `.muten`; reach for an island only for the hard widget.
85
91
 
86
92
  If you accept the last prompt it runs `<pm> install` followed by `<pm> run dev` — your app is live in a
87
93
  single step. Choosing SCSS also adds `sass` and switches the stylesheet to `.scss` automatically.
@@ -103,6 +109,7 @@ create-muten my-app --css --no-install # just scaffold, decide later
103
109
  | `--css` / `--scss` | pick the stylesheet (default: `css`) |
104
110
  | `--tailwind` | add Tailwind CSS v4 on top of CSS (forces `--css`) |
105
111
  | `--daisyui` | add DaisyUI component classes (implies `--tailwind`) |
112
+ | `--svelte` / `--react` | wire the Svelte / React island plugin (drop in framework components, e.g. shadcn) |
106
113
  | `--pm <npm\|pnpm\|yarn\|bun>` | package manager to use (default: detected) |
107
114
  | `--no-install` | scaffold only — don't install or start the dev server |
108
115
  | `--help` | print usage and exit |
@@ -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.
@@ -37,13 +37,30 @@ const tailwindStyles = (daisyui) => `@import "tailwindcss";${daisyui ? '\n@plugi
37
37
  /* Muten layout primitive Tailwind doesn't define */
38
38
  .stack { display: flex; flex-direction: column; }
39
39
  `;
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()],
40
+ // vite.config composed from the chosen options — muten always; svelte/react add island plugins; tailwind last.
41
+ const viteConfig = ({ tailwind, svelte, react }) => {
42
+ const imports = [`import muten from '@muten/core/vite-plugin-muten.js';`];
43
+ const plugins = ['muten()'];
44
+ if (svelte) { imports.push(`import { svelte } from '@sveltejs/vite-plugin-svelte';`); plugins.push('svelte()'); }
45
+ if (react) { imports.push(`import react from '@vitejs/plugin-react';`); plugins.push('react()'); }
46
+ if (tailwind) { imports.push(`import tailwindcss from '@tailwindcss/vite';`); plugins.push('tailwindcss()'); }
47
+ return `${imports.join('\n')}\n\nexport default {\n plugins: [${plugins.join(', ')}],\n};\n`;
45
48
  };
49
+ // Tell the AI the island plugin is wired so it can drop in a real Svelte/React component (incl. shadcn/Radix).
50
+ const ISLANDS_NOTE = ({ svelte, react }) => {
51
+ const techs = [svelte && 'Svelte', react && 'React'].filter(Boolean).join(' + ');
52
+ const ex = react
53
+ ? `use Widget from "react:./Widget.jsx"` + ' → ' + `Widget(value: @sel, onChange: pick) client:visible`
54
+ : `use Widget from "svelte:./Widget.svelte"` + ' → ' + `Widget(value: @sel, onChange: pick) client:visible`;
55
+ return `
56
+ ## Framework islands (${techs} — wired)
57
+ The ${techs} Vite plugin is installed. For a genuinely-interactive widget Muten can't express (date-picker,
58
+ combobox, command palette, rich editor — including React/Svelte libs like shadcn/Radix), write the component
59
+ in its own \`.${react ? 'jsx' : 'svelte'}\` file and mount it as a node: \`${ex}\`. props ↓ (\`@state\`) + events ↑
60
+ (an \`onX: action\` calls a Muten action), lazy + code-split. Default to \`.muten\` for the UI; reach for an
61
+ island only for the foreign piece. Full details: SKILL §14.
46
62
  `;
63
+ };
47
64
  // When Tailwind/DaisyUI is chosen, the Muten token scale is centralized to MATCH Tailwind's defaults, so
48
65
  // style() tokens and Tailwind utilities share one scale (e.g. style(gap.md) == gap-4 == 1rem). Plain
49
66
  // css/scss keeps the default theme.muten. (DaisyUI builds on Tailwind's scale; its colors come via @plugin.)
@@ -83,13 +100,15 @@ async function main() {
83
100
  const has = (f) => argv.includes(f);
84
101
  const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; };
85
102
  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; }
103
+ if (has('-h') || has('--help')) { console.log('Usage: create-muten [name] [--template basic|routing|full] [--css|--scss] [--tailwind] [--daisyui] [--svelte] [--react] [--pm npm|pnpm|yarn|bun] [--no-install]'); return; }
87
104
 
88
105
  let name = argv.filter((a, i) => !a.startsWith('-') && argv[i - 1] !== '--pm' && argv[i - 1] !== '--template')[0];
89
106
  let template = val('--template') || (has('--full') ? 'full' : has('--routing') ? 'routing' : has('--basic') ? 'basic' : undefined);
90
107
  let style = has('--scss') ? 'scss' : has('--css') ? 'css' : undefined; // the base stylesheet
91
108
  let tailwind = has('--tailwind') ? true : undefined; // optional add-on (CSS only)
92
109
  let daisyui = has('--daisyui') ? true : undefined; // component classes on Tailwind
110
+ let svelte = has('--svelte') ? true : undefined; // island: Svelte components
111
+ let react = has('--react') ? true : undefined; // island: React components (shadcn/Radix)
93
112
  let pm = val('--pm');
94
113
  let install = has('--no-install') ? false : undefined;
95
114
  if (name && !validName(name)) { console.error(`Invalid name: "${name}" (letters, digits, . _ -)`); process.exit(1); }
@@ -114,6 +133,16 @@ async function main() {
114
133
  ] }));
115
134
  if (tailwind === undefined) tailwind = style === 'css' ? keep(await confirm({ message: 'Add Tailwind CSS? (utility classes via class("…"))', initialValue: false })) : false;
116
135
  if (tailwind && daisyui === undefined) daisyui = keep(await confirm({ message: 'Add DaisyUI? (component classes: btn, card, modal…)', initialValue: template === 'full' }));
136
+ if (svelte === undefined && react === undefined) {
137
+ const islands = keep(await select({ message: 'Framework islands? (drop in Svelte/React components for hard widgets)', options: [
138
+ { value: 'none', label: 'None', hint: 'pure Muten' },
139
+ { value: 'svelte', label: 'Svelte', hint: 'lighter to embed' },
140
+ { value: 'react', label: 'React', hint: 'shadcn / Radix ecosystem' },
141
+ { value: 'both', label: 'Both' },
142
+ ] }));
143
+ svelte = islands === 'svelte' || islands === 'both';
144
+ react = islands === 'react' || islands === 'both';
145
+ }
117
146
  }
118
147
  name = name || 'muten-app';
119
148
  template = template || 'basic';
@@ -122,6 +151,8 @@ async function main() {
122
151
  style = style || 'css';
123
152
  if (tailwind === undefined) tailwind = false;
124
153
  if (daisyui === undefined) daisyui = false;
154
+ if (svelte === undefined) svelte = false;
155
+ if (react === undefined) react = false;
125
156
  if (tailwind) style = 'css'; // Tailwind implies a CSS base (not SCSS)
126
157
  pm = pm || dpm;
127
158
  if (install === undefined) install = false;
@@ -139,22 +170,27 @@ async function main() {
139
170
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
140
171
  pkg.name = name;
141
172
  const addDev = (deps) => { pkg.devDependencies = { ...(pkg.devDependencies || {}), ...deps }; };
173
+ const addDep = (deps) => { pkg.dependencies = { ...(pkg.dependencies || {}), ...deps }; };
174
+ const appendAgents = (text) => { const f = join(target, '.claude', 'AGENTS.md'); if (existsSync(f)) writeFileSync(f, readFileSync(f, 'utf8') + text); };
142
175
 
143
176
  if (tailwind) {
144
177
  writeFileSync(join(target, 'src', 'styles.css'), tailwindStyles(daisyui));
145
- writeFileSync(join(target, 'vite.config.mjs'), TAILWIND_VITE);
146
178
  writeFileSync(join(target, 'theme.muten'), TAILWIND_THEME); // scale centralized to Tailwind's
147
179
  addDev({ tailwindcss: '^4.0.0', '@tailwindcss/vite': '^4.0.0' });
148
180
  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 : ''));
181
+ appendAgents(TAILWIND_NOTE + (daisyui ? DAISY_NOTE : '')); // tell the AI what styling is available
151
182
  } else {
152
183
  writeFileSync(join(target, 'src', style === 'scss' ? 'styles.scss' : 'styles.css'), RESET);
153
184
  }
154
185
  if (style === 'scss') addDev({ sass: '^1.101.0' });
186
+ if (svelte) { addDep({ svelte: '^5.0.0' }); addDev({ '@sveltejs/vite-plugin-svelte': '^7.0.0' }); }
187
+ if (react) { addDep({ react: '^19.0.0', 'react-dom': '^19.0.0' }); addDev({ '@vitejs/plugin-react': '^6.0.0' }); }
188
+ if (svelte || react) appendAgents(ISLANDS_NOTE({ svelte, react }));
189
+ writeFileSync(join(target, 'vite.config.mjs'), viteConfig({ tailwind, svelte, react })); // composed: muten + chosen plugins
155
190
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
156
191
 
157
- const desc = `${template}, ${style}${tailwind ? ' + Tailwind' : ''}${daisyui ? ' + DaisyUI' : ''}`;
192
+ const islandDesc = [svelte && 'Svelte', react && 'React'].filter(Boolean).join('+');
193
+ const desc = `${template}, ${style}${tailwind ? ' + Tailwind' : ''}${daisyui ? ' + DaisyUI' : ''}${islandDesc ? ' + ' + islandDesc + ' islands' : ''}`;
158
194
  if (!install) {
159
195
  if (process.stdin.isTTY) { note(`cd ${name}\n${pm} install\n${pm} run dev`, 'Next steps'); outro(color.green(`Created ${name} (${desc})`)); }
160
196
  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.6",
3
+ "version": "0.0.8",
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.5"
11
+ "@muten/core": "^0.0.7"
12
12
  },
13
13
  "devDependencies": {
14
14
  "vite": "^8.0.16"