create-muten 0.0.1 → 0.0.3

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
@@ -1,14 +1,123 @@
1
1
  # create-muten
2
2
 
3
- Scaffold a new [Muten](https://www.npmjs.com/package/muten) app.
3
+ The official scaffolder for **[Muten](https://www.npmjs.com/package/@muten/core)** — an AI-first
4
+ frontend framework. One command bootstraps a complete, ready-to-run Muten app, so you never copy
5
+ boilerplate by hand.
4
6
 
5
7
  ```sh
6
- npm create muten@latest # or: npx create-muten
8
+ npm create muten@latest
7
9
  ```
8
10
 
9
- It prompts for the **project name**, the **stylesheet** (CSS/SCSS) and the **package manager**
10
- (default = the one you invoked it with), then scaffolds and — unless you decline — runs
11
- `<pm> install` + `<pm> run dev`. CI flags: `--css|--scss`, `--pm <npm|pnpm|yarn|bun>`, `--no-install`.
11
+ ## Why this exists
12
12
 
13
- The engine (`@muten/core`) is a separate package the app installs as a dependency — the same split as
14
- `create-vue` ↔ `vue`. It's a Node CLI (not a shell script), so it behaves the same on Windows and macOS.
13
+ Muten ships as **two** packages — the same split as `vue` ↔ `create-vue`:
14
+
15
+ - **[`@muten/core`](https://www.npmjs.com/package/@muten/core)** — the engine (compiler + runtime +
16
+ Vite plugin). Your app installs it as a normal dependency and it stays up to date on its own.
17
+ - **`create-muten`** (this package) — a tiny, **zero-dependency** CLI whose only job is to generate a
18
+ new project already wired to the engine: `index.html`, the Vite config, a `theme.muten`, a first
19
+ page and the right `package.json`.
20
+
21
+ You run `create-muten` **once** to scaffold; after that you just work inside your project.
22
+
23
+ ## Quick start
24
+
25
+ Every package manager has a `create` shortcut. `create-muten` detects which one you used and makes it
26
+ the default for the rest of the prompts:
27
+
28
+ ```sh
29
+ npm create muten@latest # npm
30
+ pnpm create muten # pnpm
31
+ yarn create muten # yarn
32
+ bun create muten # bun
33
+ ```
34
+
35
+ Prefer no install step? Run it on demand with **npx**, optionally naming the folder:
36
+
37
+ ```sh
38
+ npx create-muten my-app
39
+ ```
40
+
41
+ Or install it **globally** and reuse the command anywhere:
42
+
43
+ ```sh
44
+ npm i -g create-muten
45
+ create-muten my-app
46
+ ```
47
+
48
+ Then start the app:
49
+
50
+ ```sh
51
+ cd my-app
52
+ npm install # only if you skipped the auto-install
53
+ npm run dev
54
+ ```
55
+
56
+ ## What it asks
57
+
58
+ In an interactive terminal it prompts for a few things (defaults in parentheses):
59
+
60
+ | Prompt | Options | Default |
61
+ |---|---|---|
62
+ | **Project name** | any valid folder name | `muten-app` |
63
+ | **Stylesheet** | `css` / `scss` | `css` |
64
+ | **Package manager** | `npm` / `pnpm` / `yarn` / `bun` | the one that launched it |
65
+ | **Install deps and start dev now?** | `Y` / `n` | `Y` |
66
+
67
+ If you accept the last prompt it runs `<pm> install` followed by `<pm> run dev` — your app is live in a
68
+ single step. Choosing SCSS also adds `sass` and switches the stylesheet to `.scss` automatically.
69
+
70
+ ## Non-interactive (CI / scripts)
71
+
72
+ Pass the answers as arguments and it skips every prompt (this is also what runs when there is no TTY,
73
+ e.g. in CI):
74
+
75
+ ```sh
76
+ create-muten my-app --scss --pm pnpm # full control
77
+ create-muten my-app --css --no-install # just scaffold, decide later
78
+ ```
79
+
80
+ | Flag | Effect |
81
+ |---|---|
82
+ | `<name>` | the project folder (positional argument) |
83
+ | `--css` / `--scss` | pick the stylesheet (default: `css`) |
84
+ | `--pm <npm\|pnpm\|yarn\|bun>` | package manager to use (default: detected) |
85
+ | `--no-install` | scaffold only — don't install or start the dev server |
86
+ | `--help` | print usage and exit |
87
+ | `--version` | print the version and exit |
88
+
89
+ ## What you get
90
+
91
+ A minimal, conventional Muten app:
92
+
93
+ ```
94
+ my-app/
95
+ ├─ index.html # entry — loads /src/app.muten through the Vite plugin
96
+ ├─ vite.config.mjs # the @muten/core Vite plugin (dev server, HMR, routing)
97
+ ├─ theme.muten # your design tokens: spacing, fonts, weights, breakpoints
98
+ ├─ package.json # depends on @muten/core + vite
99
+ └─ src/
100
+ ├─ app.muten # the ROOT: routes (+ an optional persistent shell)
101
+ ├─ styles.css # your look (.scss if you chose SCSS)
102
+ └─ pages/
103
+ └─ home/home.muten # a page — the folder name is its route
104
+ ```
105
+
106
+ There is **no hand-written `main.js`**: the Vite plugin compiles `src/app.muten` into the app's entry,
107
+ so the whole app is `.muten` from the first line.
108
+
109
+ ## Requirements
110
+
111
+ - **Node.js 18+**
112
+ - One of: **npm**, **pnpm**, **yarn**, **bun**
113
+
114
+ ## Cross-platform
115
+
116
+ `create-muten` is a Node CLI (not a shell script), so the **exact same command** works on **Windows,
117
+ macOS and Linux** — npm generates the right launcher on each OS.
118
+
119
+ ## Links
120
+
121
+ - Engine — [`@muten/core`](https://www.npmjs.com/package/@muten/core)
122
+ - Source — [github.com/karttofer/create-muten](https://github.com/karttofer/create-muten)
123
+ - License — MIT
package/index.js CHANGED
@@ -1,86 +1,86 @@
1
1
  #!/usr/bin/env node
2
- // create-muten — scaffold a new Muten app. Zero runtime deps (Node built-ins only).
2
+ // create-muten — scaffold a new Muten app, with modern interactive prompts (@clack/prompts).
3
3
  //
4
- // npm create muten@latest [name] (when published to npm)
5
- // npx github:karttofer/create-muten [name] (from GitHub, not yet public)
6
- // create-muten [name] [--css|--scss] [--pm npm|pnpm|yarn|bun] [--no-install]
4
+ // npm create muten@latest [name] (or: npx create-muten)
5
+ // create-muten [name] [--css|--scss|--tailwind] [--pm npm|pnpm|yarn|bun] [--no-install]
7
6
  //
8
- // Interactive in a TTY (prompts for name, stylesheet, package manager); flags / non-TTY make it
9
- // scriptable. The package manager defaults to the one that invoked us. Then it scaffolds and,
10
- // unless --no-install, runs `<pm> install` + `<pm> run dev`.
7
+ // Interactive in a TTY (styled prompts: name, styling, package manager); flags / non-TTY make it
8
+ // scriptable. It scaffolds and unless declined runs `<pm> install` + `<pm> run dev`.
11
9
  import { cpSync, existsSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
12
10
  import { join, dirname, resolve } from 'node:path';
13
11
  import { fileURLToPath } from 'node:url';
14
- import { createInterface } from 'node:readline/promises';
15
12
  import { spawnSync } from 'node:child_process';
13
+ import { intro, outro, text, select, confirm, isCancel, cancel, note } from '@clack/prompts';
14
+ import color from 'picocolors';
16
15
 
17
16
  const SELF = dirname(fileURLToPath(import.meta.url));
18
17
  const TEMPLATE = join(SELF, 'template');
19
18
  const PKG = JSON.parse(readFileSync(join(SELF, 'package.json'), 'utf8'));
20
19
  const PMS = ['npm', 'pnpm', 'yarn', 'bun'];
21
-
22
- const BANNER = [
23
- '',
24
- ' _',
25
- ' _ __ ___ _ _| |_ ___ _ __',
26
- " | '_ ` _ \\| | | | __/ _ \\ '_ \\",
27
- ' | | | | | | |_| | || __/ | | |',
28
- ' |_| |_| |_|\\__,_|\\__\\___|_| |_|',
29
- '',
30
- ' AI-first frontend framework',
31
- '',
32
- ].join('\n');
33
-
34
- // Which PM launched us? npm/pnpm/yarn/bun all set npm_config_user_agent (e.g. "pnpm/8.6 ...").
35
- // Detecting it is what create-vue/vite do — the idiomatic default, no guessing.
36
- const detectPM = () => {
37
- const ua = process.env.npm_config_user_agent || '';
38
- return PMS.find((p) => ua.startsWith(p + '/')) || 'npm';
20
+ const STYLES = ['css', 'scss', 'tailwind'];
21
+
22
+ // the starter reset — written by the CLI so the template stays pure .muten (no default styles file).
23
+ const RESET = `/* Your look. Muten ships STRUCTURE + LAYOUT (style() tokens); the LOOK lives here, via class("…"). */
24
+ * { box-sizing: border-box; }
25
+ body { margin: 0; font: 15px/1.55 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; color: #111; }
26
+ h1, h2, h3, h4, h5, h6, p { margin: 0; }
27
+ h1 { font-size: 32px; font-weight: 700; letter-spacing: -.02em; }
28
+ .stack { display: flex; flex-direction: column; }
29
+ img { max-width: 100%; display: block; }
30
+ a { color: inherit; text-decoration: none; }
31
+ `;
32
+ // Tailwind v4: one @import + the @tailwindcss/vite plugin (no config file). Preflight does the reset;
33
+ // .stack is a Muten layout primitive Tailwind doesn't know about, so it stays.
34
+ const TAILWIND_STYLES = `@import "tailwindcss";
35
+
36
+ /* Muten layout primitive Tailwind doesn't define */
37
+ .stack { display: flex; flex-direction: column; }
38
+ `;
39
+ const TAILWIND_VITE = `import muten from '@muten/core/vite-plugin-muten.js';
40
+ import tailwindcss from '@tailwindcss/vite';
41
+
42
+ export default {
43
+ plugins: [muten(), tailwindcss()],
39
44
  };
40
-
41
- // Safe folder name: starts alphanumeric, then [A-Za-z0-9._-]. Blocks path traversal / odd input.
45
+ `;
46
+ const TAILWIND_NOTE = `
47
+ ## Styling: Tailwind CSS v4 (installed)
48
+ This app has Tailwind. Write the LOOK with \`class("…")\` using Tailwind utilities, e.g.
49
+ \`class("flex gap-4 rounded-lg bg-zinc-900 text-white")\`. \`style()\` still owns Muten's layout/
50
+ typography tokens — don't put Tailwind classes in \`style()\`, and don't put layout in \`class()\`.
51
+ `;
52
+
53
+ // Which PM launched us? npm/pnpm/yarn/bun set npm_config_user_agent — the idiomatic default.
54
+ const detectPM = () => { const ua = process.env.npm_config_user_agent || ''; return PMS.find((p) => ua.startsWith(p + '/')) || 'npm'; };
42
55
  const validName = (n) => /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(n);
43
- const die = (msg) => { console.error(msg); process.exit(1); };
56
+ const keep = (v) => { if (isCancel(v)) { cancel('Cancelled.'); process.exit(0); } return v; };
44
57
 
45
58
  async function main() {
46
- // args: one positional name + optional flags (so the CLI is also scriptable / CI-friendly)
47
59
  const argv = process.argv.slice(2);
48
60
  const has = (f) => argv.includes(f);
49
61
  const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; };
50
62
  if (has('-v') || has('--version')) { console.log(PKG.version); return; }
51
- console.log(BANNER);
52
- if (has('-h') || has('--help')) { console.log(' Usage: create-muten [name] [--css|--scss] [--pm npm|pnpm|yarn|bun] [--no-install]\n'); return; }
63
+ if (has('-h') || has('--help')) { console.log('Usage: create-muten [name] [--css|--scss|--tailwind] [--pm npm|pnpm|yarn|bun] [--no-install]'); return; }
53
64
 
54
65
  let name = argv.filter((a, i) => !a.startsWith('-') && argv[i - 1] !== '--pm')[0];
55
- let style = has('--scss') ? 'scss' : has('--css') ? 'css' : undefined;
66
+ let style = has('--tailwind') ? 'tailwind' : has('--scss') ? 'scss' : has('--css') ? 'css' : undefined;
56
67
  let pm = val('--pm');
57
68
  let install = has('--no-install') ? false : undefined;
58
-
59
- if (name && !validName(name)) die(`Invalid name: "${name}" (letters, digits, . _ -)`);
60
- if (pm && !PMS.includes(pm)) die(`Unknown package manager: "${pm}" (${PMS.join(', ')})`);
61
-
69
+ if (name && !validName(name)) { console.error(`Invalid name: "${name}" (letters, digits, . _ -)`); process.exit(1); }
70
+ if (pm && !PMS.includes(pm)) { console.error(`Unknown package manager: "${pm}" (${PMS.join(', ')})`); process.exit(1); }
62
71
  const dpm = detectPM();
63
72
 
64
- // Prompt only with a real TTY piped/CI input drops lines through readline, so there we use
65
- // flags + defaults instead of hanging on a prompt.
73
+ // Styled prompts only with a real TTY (piped/CI input would hang); otherwise use flags + defaults.
66
74
  if (process.stdin.isTTY) {
67
- const rl = createInterface({ input: process.stdin, output: process.stdout });
68
- const ask = async (q, def) => (await rl.question(q)).trim() || def;
69
- const pick = async (q, opts, def) => {
70
- for (;;) {
71
- const v = (await ask(q, def)).toLowerCase();
72
- if (opts.includes(v)) return v;
73
- console.log(` Choose: ${opts.join(', ')}.`);
74
- }
75
- };
76
- while (!name) {
77
- const a = await ask('Project name: (muten-app) ', 'muten-app');
78
- if (validName(a)) name = a; else console.log(' Letters, digits, . _ - (start alphanumeric).');
79
- }
80
- if (!style) style = await pick('Stylesheet? [css/scss] (css) ', ['css', 'scss'], 'css');
81
- if (!pm) pm = await pick(`Package manager? [${PMS.join('/')}] (${dpm}) `, PMS, dpm);
82
- if (install === undefined) install = (await ask('Install deps and start the dev server now? [Y/n] ', 'y')).toLowerCase() !== 'n';
83
- rl.close();
75
+ intro(color.bgCyan(color.black(' create-muten ')) + color.dim(' the AI-first frontend framework'));
76
+ 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 }));
77
+ if (!style) style = keep(await select({ message: 'Styling', options: [
78
+ { value: 'css', label: 'Plain CSS', hint: 'zero deps' },
79
+ { value: 'scss', label: 'SCSS', hint: 'adds sass' },
80
+ { value: 'tailwind', label: 'Tailwind CSS', hint: 'utility classes via class("…") fastest to style' },
81
+ ] }));
82
+ if (!pm) pm = keep(await select({ message: 'Package manager', initialValue: dpm, options: PMS.map((p) => ({ value: p, label: p })) }));
83
+ if (install === undefined) install = keep(await confirm({ message: 'Install dependencies and start the dev server now?' }));
84
84
  }
85
85
  name = name || 'muten-app';
86
86
  style = style || 'css';
@@ -88,24 +88,37 @@ async function main() {
88
88
  if (install === undefined) install = false;
89
89
 
90
90
  const target = resolve(name);
91
- if (existsSync(target)) die(`"${name}" already exists.`);
91
+ if (existsSync(target)) { (process.stdin.isTTY ? cancel : console.error)(`"${name}" already exists.`); process.exit(1); }
92
92
 
93
- // scaffold from ./template
93
+ // scaffold from ./template (pure .muten) + apply the styling choice
94
94
  cpSync(TEMPLATE, target, { recursive: true });
95
- const ignore = join(target, '_gitignore'); // npm strips a real .gitignore on publish
95
+ const ignore = join(target, '_gitignore');
96
96
  if (existsSync(ignore)) renameSync(ignore, join(target, '.gitignore'));
97
- if (style === 'scss') renameSync(join(target, 'src', 'styles.css'), join(target, 'src', 'styles.scss')); // plugin auto-detects
98
97
 
99
98
  const pkgPath = join(target, 'package.json');
100
99
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
101
100
  pkg.name = name;
102
- if (style === 'scss') pkg.devDependencies = { ...(pkg.devDependencies || {}), sass: '^1.101.0' };
101
+ const addDev = (deps) => { pkg.devDependencies = { ...(pkg.devDependencies || {}), ...deps }; };
102
+
103
+ if (style === 'tailwind') {
104
+ writeFileSync(join(target, 'src', 'styles.css'), TAILWIND_STYLES);
105
+ writeFileSync(join(target, 'vite.config.mjs'), TAILWIND_VITE);
106
+ addDev({ tailwindcss: '^4.0.0', '@tailwindcss/vite': '^4.0.0' });
107
+ const agents = join(target, '.claude', 'AGENTS.md'); // tell the AI Tailwind is available
108
+ if (existsSync(agents)) writeFileSync(agents, readFileSync(agents, 'utf8') + TAILWIND_NOTE);
109
+ } else {
110
+ writeFileSync(join(target, 'src', style === 'scss' ? 'styles.scss' : 'styles.css'), RESET);
111
+ if (style === 'scss') addDev({ sass: '^1.101.0' });
112
+ }
103
113
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
104
114
 
105
- console.log(`\n Created ${name} (${style}, ${pm})\n`);
106
-
107
- if (!install) { console.log(` cd ${name}\n ${pm} install\n ${pm} run dev\n`); return; }
115
+ if (!install) {
116
+ if (process.stdin.isTTY) { note(`cd ${name}\n${pm} install\n${pm} run dev`, 'Next steps'); outro(color.green(`Created ${name} (${style})`)); }
117
+ else console.log(`\n Created ${name} (${style}, ${pm})\n cd ${name} && ${pm} install && ${pm} run dev\n`);
118
+ return;
119
+ }
108
120
 
121
+ if (process.stdin.isTTY) outro(color.green(`Created ${name} (${style}) — installing with ${pm}…`));
109
122
  // PMs are .cmd shims on Windows → spawn needs shell:true to find them.
110
123
  const run = (a) => spawnSync(pm, a, { cwd: target, stdio: 'inherit', shell: process.platform === 'win32' });
111
124
  if (run(['install']).status === 0) run(['run', 'dev']);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-muten",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Scaffold a new Muten app.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,6 +14,10 @@
14
14
  "bin": {
15
15
  "create-muten": "index.js"
16
16
  },
17
+ "dependencies": {
18
+ "@clack/prompts": "^0.7.0",
19
+ "picocolors": "^1.0.1"
20
+ },
17
21
  "keywords": ["muten", "create-muten", "scaffold", "starter", "cli", "frontend"],
18
22
  "files": ["index.js", "template"]
19
23
  }
@@ -0,0 +1,59 @@
1
+ # Working in a Muten app — guide for AI agents
2
+
3
+ This project uses **Muten**, an AI-first frontend framework. The UI is written in **`.muten` files**
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.
7
+
8
+ > Full language reference (every primitive, prop, token, pattern): the **`muten` skill** at
9
+ > [`skills/muten/SKILL.md`](skills/muten/SKILL.md).
10
+
11
+ ## Golden rules
12
+ - UI → `.muten` files. App-global state → `.store` files. Both compile via the `@muten/core` Vite plugin.
13
+ - **No `main.js`.** `src/app.muten` IS the entry (loaded by `index.html`). Never add a JS entry/bootstrap.
14
+ - Primitives are **PascalCase** (`Stack`, `Text`, `Button`); control flow is lowercase (`when`, `each`).
15
+ - **`style(...)`** = layout/typography tokens (Muten builds STRUCTURE). **`class("...")`** = your look
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}"`.
18
+
19
+ ## File map
20
+ ```
21
+ src/
22
+ app.muten ROOT — routes { /url -> page } (+ optional shell { … slot … })
23
+ pages/<route>/<route>.muten a page; the folder name IS the route
24
+ parts/<name>.muten reusable component (composition, inlined at build)
25
+ components/<Name>.js escape hatch (host JS) used via the `Custom` primitive
26
+ theme.muten design tokens: space, font, weight, breakpoints
27
+ src/styles.css your look (.scss if you picked SCSS)
28
+ ```
29
+
30
+ ## A page looks like this
31
+ ```
32
+ screen home
33
+
34
+ Page style(padding.lg, gap.md) {
35
+ Title "Hello"
36
+ Text "Body copy with reactive state: {user.name}"
37
+ Button "Save" -> save
38
+ }
39
+ ```
40
+
41
+ ## Cheat-sheet
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`.
45
+ - **Control:** `when <expr> { … }`, `each <list> as item { … }`.
46
+ - **State:** `state { q = "" : text users = query listUsers : list<User> }` — query states expose `.loading/.error/.data`.
47
+ - **Actions:** `action add mutates users <- item { users.push(item) }` — ops: `push/set/reset/remove`; branch with `if/else`.
48
+ - **Tokens:** `gap.md padding.lg cols.3 text.lg row center between` — responsive prefix: `md:cols.2`.
49
+
50
+ ## Dependencies & limits
51
+ - **CSS / Tailwind / SCSS: YES** — it's a Vite app; install them and use `class("…")` + your CSS.
52
+ - **React / Vue / Svelte UI libraries: NO** — the UI is `.muten` (vanilla DOM, no JS framework runtime).
53
+ Need a JS widget? Wrap it in a `Custom` host component (`src/components/<Name>.js`).
54
+ - Routing is **hash-based with static paths** (no `:id` params yet). Shell has no local state → use a
55
+ `.store`. No `toggle` op → `set(not x)`. `style()` is layout tokens only; visuals go in `class()`.
56
+ - The full reference (stores, routing, theme, every primitive) is in [`skills/muten/SKILL.md`](skills/muten/SKILL.md).
57
+
58
+ ## Commands
59
+ `npm run dev` (dev server + HMR) · `npm run build` (production) · `npm run lint` (validate `.muten`).
@@ -0,0 +1,249 @@
1
+ ---
2
+ name: muten
3
+ description: Read and write Muten — the AI-first frontend DSL this app is built in (.muten and .store files), NOT React/Vue/HTML. Use whenever creating or editing any .muten or .store file, app.muten routes, theme.muten, parts, components, stores, or deciding what can be installed. No model is trained on Muten, so consult this before writing any Muten code or adding a dependency. Assume the human will NOT read the code — you do everything.
4
+ ---
5
+
6
+ # Muten — complete language reference
7
+
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.
10
+ A page with no reactivity compiles to plain zero-runtime HTML; a reactive one ships ~1KB of signals.
11
+
12
+ ## Mental model & golden rules
13
+ - **UI** → `.muten` files (pages, parts, the app root, the theme). **App-global state** → `.store` files.
14
+ - **`src/app.muten` is the entry.** `index.html` loads it; the plugin boots it. **Never create `main.js`** or a `<script>` bootstrap.
15
+ - Primitives are **PascalCase** (`Stack`, `Text`); keywords/control flow are **lowercase** (`when`, `each`, `state`).
16
+ - `style(...)` = layout/typography **tokens** (Muten builds STRUCTURE). `class("...")` = **look** (your CSS / Tailwind). Muten ships no skin.
17
+ - `@name` = a state reference. `{expr}` = interpolation inside any string: `Text "Hi, {user.name}"`.
18
+ - Each page has **one root node**. Reactivity is automatic: reading a state in interpolation / `when` / `each` re-renders just that spot.
19
+
20
+ ## 1. What you CAN install / use
21
+ This is a normal **Vite** project, so the whole Vite/npm ecosystem for **styling, build, and data** works:
22
+ - **Tailwind CSS — YES.** Install it (`tailwindcss`, `postcss`, `autoprefixer`), add the config + the
23
+ `@tailwind` directives to `src/styles.css`, and use utilities via `class("flex gap-4 rounded")`.
24
+ `class()` emits raw class names, so any CSS framework (Tailwind, UnoCSS, Bootstrap CSS, your own CSS) works.
25
+ - **Sass/SCSS** — supported out of the box if you scaffolded with SCSS (or add `sass`); use `src/styles.scss`.
26
+ - **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.).
29
+ - **Host UI via the `Custom` primitive** — write vanilla JS in `src/components/<Name>.js` (charts,
30
+ maps, a third-party widget) and mount it with `Custom`. See §Custom.
31
+
32
+ ## 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.
37
+ - **No arbitrary inline CSS via `style()`** — `style()` only takes the layout/typography tokens below.
38
+ Visual styling (colors, borders, shadows) goes through `class("…")` + your CSS.
39
+
40
+ ## 3. Limitations (current)
41
+ - **Routing is hash-based** (`#/path`) and **paths are static** — there are **no route params** yet
42
+ (no `/product/:id`). Model per-item views with state + `when`, or a query param read in host JS.
43
+ - **Shell has no local state** — put shell/cross-page state in a `.store` (see the mobile-menu pattern).
44
+ - **No `toggle` op** — flip a bool with `set(not x)`.
45
+ - **Forms**: `Form` (auto-generated from an entity) and `SearchField` (single text input) are the
46
+ built-ins; richer custom inputs need a `Custom` component for now.
47
+ - **Pages are single-root** (one top node per page).
48
+
49
+ ## 4. Files
50
+ ```
51
+ src/app.muten routes (+ optional shell) — the ROOT; read it first
52
+ src/pages/<route>/<route>.muten one page; the folder name IS the route
53
+ src/parts/<name>.muten reusable component (inlined at build time)
54
+ src/components/<Name>.js host-JS escape hatch, mounted via Custom
55
+ src/<domain>.store app-global state slice (domain = file name)
56
+ theme.muten token scale (space/font/weight/leading/breakpoints)
57
+ src/styles.css reset + look (or styles.scss)
58
+ index.html / vite.config.mjs wired to @muten/core; don't hand-edit the boot
59
+ ```
60
+
61
+ ## 5. Declarations
62
+ ```
63
+ screen <name> # page identity (first line of a page)
64
+
65
+ entity User { # data shape + validation (implicit `id uuid`)
66
+ name text required # constraints: required | min:N | max:N
67
+ email email required
68
+ role admin | member # `a | b | c` = enum
69
+ }
70
+
71
+ state { # page-LOCAL reactive state
72
+ q = "" : text
73
+ users = query listUsers : list<User> # query → async; exposes @users.loading/.error/.data
74
+ }
75
+
76
+ const TAX = 0.21 # compile-time immutable scalar (inlined, never reactive)
77
+
78
+ action add mutates users <- item { # mutation; `mutates` lists what it may change (enforced)
79
+ users.push(item) # ops: push | set | reset | remove
80
+ if item.vip { rating.set(5) } else { rating.set(1) } # if/else = the only branching in actions
81
+ }
82
+
83
+ mock { listUsers: [ { name: "Ana", role: admin } ] } # mock data for a query
84
+ sources { listUsers: { url: "https://api…", at: "results" } } # real data source for a query
85
+ ```
86
+
87
+ ## 6. Primitives
88
+ A bare string is the node's main prop. `{ }` = children. Lay out with `style()`, skin with `class()`.
89
+
90
+ | Primitive | Use | Example |
91
+ |---|---|---|
92
+ | `Stack` | vertical stack (flex column) | `Stack style(gap.md) { … }` |
93
+ | `Page` | page root `<main>` (one per route) | `Page style(padding.lg) { … }` |
94
+ | `Header`/`Nav`/`Sidebar`/`Footer` | landmarks | `Header style(row, between, center) { … }` |
95
+ | `Text` | paragraph, interpolates | `Text "Hi, {user.name}"` |
96
+ | `Title` | heading; level keyword | `Title "Dashboard" h2` |
97
+ | `Span` | inline text | `Span "{cart.total}"` |
98
+ | `Image` | `<img>`, **alt required** | `Image "{p.image}" alt "{p.title}"` |
99
+ | `Link` | client-side nav | `Link "Catalog" -> /catalog` |
100
+ | `Button` | runs an action | `Button "Save" -> save(draft)` |
101
+ | `SearchField` | text input bound to state | `SearchField bind @q "Search…"` |
102
+ | `Form` | auto-form from an entity draft | `Form bind @draft submit create "Save"` |
103
+ | `DataTable` | reactive table over a list/query | `DataTable @users columns(name, email)` |
104
+ | `RowAction` | a button inside each table row | `RowAction "Delete" -> remove(row.id)` |
105
+ | `slot` | outlet inside `shell` | `slot` |
106
+ | `Custom` | host-JS escape hatch | `Custom Chart inputs(data: @sales) on(pick: select)` |
107
+
108
+ Horizontal layout = a region with `style(row)` (there is no `Row` primitive). Clickable card =
109
+ `Button { … }` or `Link "" -> /x { … }` with children instead of a label.
110
+
111
+ Modifiers (after a primitive): `style(tokens)` · `class("css")` · `bind @state` · `submit action` ·
112
+ `where(clauses)` · `columns(a, b)` · `alt "…"` · `inputs(k: v)` · `on(event: action)`.
113
+
114
+ ## 7. Theme — how it works
115
+ `theme.muten` supplies the **scale** (values); the engine owns only the **vocabulary** (token names).
116
+ ```
117
+ theme {
118
+ space { xs "4px" sm "8px" md "16px" lg "24px" xl "32px" }
119
+ font { sm "13px" md "15px" lg "20px" xl "28px" }
120
+ weight { medium "500" bold "700" }
121
+ leading { tight "1.2" normal "1.5" }
122
+ breakpoints { sm "640px" md "768px" lg "1024px" }
123
+ }
124
+ ```
125
+ A token like `gap.md` resolves to `gap: 16px` via `space.md`; `text.lg` → `font.lg`; `md:cols.2` uses
126
+ `breakpoints.md`. **No CSS/reset goes in `theme.muten`** — the reset and the look live in `src/styles.css`.
127
+
128
+ ### Style tokens (`style(...)`)
129
+ ```
130
+ row column wrap grid grow center between
131
+ gap.sm|md|lg padding.md|lg padding.x.md padding.y.md margin.md
132
+ cols.2|3|auto rows.2
133
+ text.sm|md|lg|xl weight.medium|bold leading.normal italic bold
134
+ align.left|center|right justify.center|between items.center|start
135
+ width.full height.full
136
+ ```
137
+ Responsive: prefix any token with a breakpoint → `md:cols.2`, `lg:cols.4` (`sm/md/lg/xl`).
138
+
139
+ ## 8. State, actions & reactivity
140
+ - `state` cells are signals; reading them in interpolation / `when` / `each` auto-updates that spot.
141
+ - `query` state is async → render with `when @x.loading { … }`, then use `@x.data`.
142
+ - Mutate **only** through `action`s, and only the state in `mutates` (the linter enforces it):
143
+ - `list.push(x)` (append; auto-fills uuid fields) · `s.set(v)` · `s.reset()` · `list.remove(x => x.id == id)`
144
+ - There is no `toggle`: `flag.set(not flag)`.
145
+ - Control flow in the tree: `when <expr> { … }` (mount/unmount), `each <list> as item { … }` (item is a scope var).
146
+ - Expressions: `== != < > <= >=`, `and or not`, `contains` (case-insensitive substring / list membership),
147
+ `+ - * /`, ternary `c ? a : b`, parentheses, refs (`user.name`, `cart.total`, `$item.x`).
148
+
149
+ ## 9. Stores — app-global state
150
+ A `.store` file = state shared across pages, **no prop drilling**. The file name is the domain.
151
+ ```
152
+ # src/ui.store → referenced everywhere as ui.<member>
153
+ state { menuOpen = false : bool }
154
+ get isOpen = menuOpen # derived/memoized value (read as ui.isOpen)
155
+ action toggleMenu mutates menuOpen <- x { menuOpen.set(not menuOpen) }
156
+ effect { /* runs whenever the store state it reads changes */ }
157
+ ```
158
+ Use it from any page/shell by name: `when ui.menuOpen { … }`, `Button "☰" -> ui.toggleMenu`. The Vite
159
+ plugin auto-detects every `.store` file. `get` = memoized; `effect` = reactive side-effect (Angular-style).
160
+
161
+ ## 10. Routing — how it works
162
+ `src/app.muten` maps URLs to pages. It's a **hash router** (URLs look like `#/about`); the **first
163
+ route is the default**. The folder under `src/pages/` must match the page name.
164
+ ```
165
+ routes {
166
+ / -> home # src/pages/home/home.muten
167
+ /about -> about # static page → compiles to zero-runtime HTML
168
+ /cart -> cart guard auth.loggedIn else /login # guard: a store boolean; redirect if false
169
+ /login -> login guard not auth.loggedIn else / # guest-only page
170
+ }
171
+ ```
172
+ Guards read a **store boolean**; when it flips (login/logout) the active route re-renders automatically.
173
+ Navigate with `Link "x" -> /path` (client-side, no reload). **No path params** (`:id`) yet.
174
+
175
+ ### Shell (persistent chrome)
176
+ Wrap routes in a `shell { … slot … }` for a nav/footer around every page. `slot` is where the active
177
+ page mounts. The shell has **no local state** → use a store for things like a mobile menu:
178
+ ```
179
+ shell {
180
+ Header style(row, between, center) class("nav") {
181
+ Link "Home" -> /
182
+ Button "☰" -> ui.toggleMenu class("burger")
183
+ }
184
+ when ui.menuOpen { Stack class("mobile-menu") { Link "About" -> /about } }
185
+ slot
186
+ Footer { Span "© 2026" }
187
+ }
188
+ routes { / -> home }
189
+ ```
190
+
191
+ ## 11. Entities, forms & validation
192
+ `entity` defines a shape + constraints. `Form bind @draft submit create` auto-renders one input per
193
+ field and validates on submit (per-field `.field-error`), blocking the action if invalid.
194
+ ```
195
+ entity Task { title text required notes text done bool }
196
+ state { draft = {} : Task tasks = [] : list<Task> }
197
+ action create mutates tasks, draft <- t { tasks.push(draft) draft.reset() }
198
+ # in the page: Form bind @draft submit create "Add task"
199
+ ```
200
+
201
+ ## 12. Parts — reusable composition
202
+ `part` = a reusable fragment, **inlined at build** (not a runtime component). Pass OBJECTS (`$x.field`)
203
+ and ACTION callbacks (`-> $onPick(...)`).
204
+ ```
205
+ # src/parts/feature.muten
206
+ part Feature(item: Feature, onPick: action) {
207
+ Stack style(column, gap.sm) class("card") {
208
+ Title "{$item.title}" h3
209
+ Text "{$item.body}"
210
+ Button "Choose" -> $onPick($item.id)
211
+ }
212
+ }
213
+ # use it: Feature(item: f, onPick: select)
214
+ ```
215
+
216
+ ## 13. Custom — the host-JS escape hatch
217
+ For anything Muten can't express (a chart, a 3rd-party widget), write vanilla JS in
218
+ `src/components/<Name>.js` and mount it with `Custom`. It receives `inputs` (values/state) and wires
219
+ DOM events to your actions via `on`. This is the ONLY way to use non-Muten UI code.
220
+ ```
221
+ Custom Chart inputs(data: @sales) on(pointSelect: select)
222
+ # → src/components/Chart.js exports a mount(el, { inputs, on }) that builds vanilla DOM.
223
+ ```
224
+
225
+ ## 14. Gotchas
226
+ - It's NOT React: PascalCase primitives + `{ }` children; no JSX/hooks/`className`.
227
+ - No `main.js`/`<script>` — `app.muten` is the entry.
228
+ - `style()` (layout tokens) ≠ `class()` (look). No colors/borders in `style()`.
229
+ - `Image` without `alt` fails validation (`alt ""` for decorative).
230
+ - Actions may only touch their declared `mutates`.
231
+ - Want a library? If it's CSS → `class()`. If it's a JS widget → `Custom`. If it's React/Vue UI → not possible.
232
+
233
+ ## 15. Minimal full app
234
+ ```
235
+ # src/app.muten
236
+ routes { / -> home }
237
+
238
+ # src/pages/home/home.muten
239
+ screen home
240
+ state { name = "" : text }
241
+ action greet mutates name <- v { name.set(v) }
242
+
243
+ Page style(padding.lg, gap.md) {
244
+ Title "Hello"
245
+ SearchField bind @name "Your name"
246
+ when name { Text "Hi, {name}!" }
247
+ }
248
+ ```
249
+ Validate anytime: `npm run lint`.
@@ -8,7 +8,7 @@
8
8
  "lint": "muten lint"
9
9
  },
10
10
  "dependencies": {
11
- "@muten/core": "^0.0.1",
11
+ "@muten/core": "^0.0.3",
12
12
  "vite": "^8.0.16"
13
13
  }
14
14
  }
@@ -4,5 +4,6 @@ theme {
4
4
  space { xs "4px" sm "8px" md "16px" lg "24px" xl "32px" }
5
5
  font { sm "13px" md "15px" lg "20px" xl "28px" }
6
6
  weight { medium "500" bold "700" }
7
+ leading { tight "1.2" normal "1.5" loose "1.8" }
7
8
  breakpoints { sm "640px" md "768px" lg "1024px" }
8
9
  }
@@ -1,9 +0,0 @@
1
- /* Your look. Muten ships STRUCTURE (semantic tags) + LAYOUT (style tokens); the LOOK lives here,
2
- referenced from .muten via class("…"). This base reset is all a fresh app needs to start. */
3
- * { box-sizing: border-box; }
4
- body { margin: 0; font: 15px/1.55 system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif; color: #111; }
5
- h1, h2, h3, h4, h5, h6, p { margin: 0; }
6
- h1 { font-size: 32px; font-weight: 700; letter-spacing: -.02em; }
7
- .stack { display: flex; flex-direction: column; } /* the class Stack and column layouts emit */
8
- img { max-width: 100%; display: block; }
9
- a { color: inherit; text-decoration: none; }