create-muten 0.0.7 → 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 +25 -1
- package/index.js +47 -11
- package/package.json +1 -1
- package/template/.claude/AGENTS.md +6 -4
- package/template/.claude/skills/muten/SKILL.md +47 -11
- package/template/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
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
|
|
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
|
@@ -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
|
|
6
|
-
|
|
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
|
|
58
|
-
|
|
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 — **
|
|
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
|
|
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
|
-
- **
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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.
|
|
329
|
-
|
|
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?
|
|
370
|
+
- Want a library? CSS → `class()`. JS function → `use` (§14). A widget → `Custom`. A React/Svelte component → an **island** (§14).
|
|
335
371
|
|
|
336
|
-
##
|
|
372
|
+
## 16. Minimal full app
|
|
337
373
|
```
|
|
338
374
|
# src/app.muten
|
|
339
375
|
routes { / -> home }
|