create-muten 0.0.8 → 0.0.10

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.
Files changed (31) hide show
  1. package/README.md +27 -19
  2. package/index.js +101 -38
  3. package/package.json +2 -2
  4. package/template/package.json +1 -1
  5. package/template/public/muten.svg +4 -0
  6. package/template/src/components/Snippet.js +19 -0
  7. package/template/src/pages/home/home.muten +42 -6
  8. package/template-tauri/muten-logo.png +0 -0
  9. package/template-tauri/src-tauri/Cargo.toml +15 -0
  10. package/template-tauri/src-tauri/build.rs +3 -0
  11. package/template-tauri/src-tauri/capabilities/default.json +7 -0
  12. package/template-tauri/src-tauri/icons/128x128.png +0 -0
  13. package/template-tauri/src-tauri/icons/128x128@2x.png +0 -0
  14. package/template-tauri/src-tauri/icons/32x32.png +0 -0
  15. package/template-tauri/src-tauri/icons/64x64.png +0 -0
  16. package/template-tauri/src-tauri/icons/Square107x107Logo.png +0 -0
  17. package/template-tauri/src-tauri/icons/Square142x142Logo.png +0 -0
  18. package/template-tauri/src-tauri/icons/Square150x150Logo.png +0 -0
  19. package/template-tauri/src-tauri/icons/Square284x284Logo.png +0 -0
  20. package/template-tauri/src-tauri/icons/Square30x30Logo.png +0 -0
  21. package/template-tauri/src-tauri/icons/Square310x310Logo.png +0 -0
  22. package/template-tauri/src-tauri/icons/Square44x44Logo.png +0 -0
  23. package/template-tauri/src-tauri/icons/Square71x71Logo.png +0 -0
  24. package/template-tauri/src-tauri/icons/Square89x89Logo.png +0 -0
  25. package/template-tauri/src-tauri/icons/StoreLogo.png +0 -0
  26. package/template-tauri/src-tauri/icons/icon.icns +0 -0
  27. package/template-tauri/src-tauri/icons/icon.ico +0 -0
  28. package/template-tauri/src-tauri/icons/icon.png +0 -0
  29. package/template-tauri/src-tauri/src/lib.rs +6 -0
  30. package/template-tauri/src-tauri/src/main.rs +6 -0
  31. package/template-tauri/src-tauri/tauri.conf.json +29 -0
package/README.md CHANGED
@@ -60,35 +60,42 @@ 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` |
65
- | **Add Tailwind CSS?** | `Y` / `n` (CSS only) | `n` |
66
- | **Add DaisyUI?** | `Y` / `n` (needs Tailwind) | `n` |
67
- | **Framework islands?** | `none` / `svelte` / `react` / `both` | `none` |
63
+ | **Template** | `muten` / `muten + React` / `muten + Svelte` | `muten` |
64
+ | **Styling** | `CSS` / `SCSS` / `Tailwind CSS` / `DaisyUI` (brings Tailwind) | `CSS` |
65
+ | **Add Vercel deploy config?** | `Y` / `n` | `n` |
66
+ | **Desktop app (Tauri)?** | `Y` / `n` | `n` |
68
67
  | **Package manager** | `npm` / `pnpm` / `yarn` / `bun` | the one that launched it |
69
68
  | **Install deps and start dev now?** | `Y` / `n` | `Y` |
70
69
 
71
- Tailwind is an optional add-on **on top of** CSS (the look layer; you still style via `class("…")`) it
72
- wires `@tailwindcss/vite` + an `@import "tailwindcss"` and notes the setup in the app's `.claude/` guide.
70
+ **Styling is one explicit choice** each is opt-in, nothing is bundled by default: `CSS` (plain) or `SCSS`
71
+ ship no framework; `Tailwind CSS` adds `@tailwindcss/vite` + `@import "tailwindcss"`; `DaisyUI` adds its
72
+ component classes on top (and brings Tailwind). You always style via `class("…")`.
73
73
 
74
- ## Templates
74
+ **Targets are independent opt-ins** — web, desktop, both, or neither, from the same `.muten` source:
75
+ - **Vercel** writes a `vercel.json` so muten's real-path routes don't 404 on a hard refresh (SPA fallback to `index.html`).
76
+ - **Tauri** adds `src-tauri/` (a native desktop app — ships the OS webview, *not* a browser) + a `tauri` script:
77
+ `npm run tauri dev` / `tauri build`. Needs the [Rust toolchain](https://rustup.rs) installed (not auto-installed).
78
+
79
+ ## Templates (flavors)
80
+
81
+ Every flavor scaffolds the **same** welcome page and the same `.muten` workflow — the only difference is
82
+ whether a framework's island plugin is pre-wired:
75
83
 
76
84
  | Template | What you get |
77
85
  |---|---|
78
- | **basic** | one page — the minimal starter |
79
- | **routing** | a persistent shell (navbar + footer) + multiple real-path routes + a static `about` page |
80
- | **full** | routing + a `.store` + an `api` block + a source-backed products page + Tailwind — a real data app |
86
+ | **muten** | pure muten — the AI-first DSL, zero framework runtime |
87
+ | **muten + React** | same, plus `@vitejs/plugin-react` + React, so you can drop in a **React island** (shadcn/Radix, any React lib) |
88
+ | **muten + Svelte** | same, plus `@sveltejs/vite-plugin-svelte` + Svelte, for **Svelte islands** (a lighter runtime) |
81
89
 
82
- When **Tailwind or DaisyUI** is selected, `theme.muten` is centralized to **match Tailwind's scale** (so
90
+ An *island* is a real framework component used as a node `use X from "react:./X.jsx"` →
91
+ `X(value: @s, onChange: act) client:visible` (props ↓ + events ↑, lazy + code-split). Default to `.muten`;
92
+ reach for an island only for a widget muten can't express.
93
+
94
+ When **Tailwind or DaisyUI** is added, `theme.muten` is centralized to **match Tailwind's scale** (so
83
95
  `style()` tokens and Tailwind utilities share one scale, e.g. `style(gap.md)` == `gap-4`); plain CSS/SCSS
84
96
  keeps the default scale. **DaisyUI** adds component classes (`btn`, `card`, `modal`) usable in `class("…")` —
85
97
  pure classes, no React; behavior is Muten state + `on()`.
86
98
 
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.
91
-
92
99
  If you accept the last prompt it runs `<pm> install` followed by `<pm> run dev` — your app is live in a
93
100
  single step. Choosing SCSS also adds `sass` and switches the stylesheet to `.scss` automatically.
94
101
 
@@ -105,11 +112,12 @@ create-muten my-app --css --no-install # just scaffold, decide later
105
112
  | Flag | Effect |
106
113
  |---|---|
107
114
  | `<name>` | the project folder (positional argument) |
108
- | `--template <basic\|routing\|full>` | which starter (default: `basic`; `full` implies Tailwind) |
115
+ | `--template <muten\|react\|svelte>` | flavor (default: `muten`); `--react` / `--svelte` are shortcuts |
109
116
  | `--css` / `--scss` | pick the stylesheet (default: `css`) |
110
117
  | `--tailwind` | add Tailwind CSS v4 on top of CSS (forces `--css`) |
111
118
  | `--daisyui` | add DaisyUI component classes (implies `--tailwind`) |
112
- | `--svelte` / `--react` | wire the Svelte / React island plugin (drop in framework components, e.g. shadcn) |
119
+ | `--vercel` | add `vercel.json` (SPA fallback so real-path routes work on Vercel) |
120
+ | `--tauri` | add `src-tauri/` — a native desktop app (needs the Rust toolchain) |
113
121
  | `--pm <npm\|pnpm\|yarn\|bun>` | package manager to use (default: detected) |
114
122
  | `--no-install` | scaffold only — don't install or start the dev server |
115
123
  | `--help` | print usage and exit |
package/index.js CHANGED
@@ -15,8 +15,8 @@ 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 TAURI_TEMPLATE = join(SELF, 'template-tauri'); // src-tauri/ overlay, copied only when --tauri
19
+ const TEMPLATES = ['muten', 'react', 'svelte']; // the "template" IS the flavor: pure muten, or muten + a framework for islands
20
20
  const PKG = JSON.parse(readFileSync(join(SELF, 'package.json'), 'utf8'));
21
21
  const PMS = ['npm', 'pnpm', 'yarn', 'bun'];
22
22
 
@@ -37,6 +37,31 @@ 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
+ // Starter welcome page styles (used by the scaffolded home.muten). Self-contained plain CSS — looks good
41
+ // with or without Tailwind; delete it (and the page) when you build your own.
42
+ const WELCOME_CSS = `
43
+ /* — starter welcome page (src/pages/home/home.muten) — delete when you build your own — */
44
+ .welcome { background: #fafafa; color: #18181b; padding: 64px 24px; }
45
+ .wrap { max-width: 720px; margin: 0 auto; display: flex; flex-direction: column; gap: 52px; }
46
+ .hero { text-align: center; }
47
+ .logo { width: 64px; height: 64px; border-radius: 16px; margin: 0 auto; box-shadow: 0 6px 20px rgba(255,94,0,.28); }
48
+ .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; }
49
+ .tagline { font-size: 18px; font-weight: 600; color: #27272a; margin-top: 10px; }
50
+ .lead { max-width: 580px; margin: 14px auto 0; color: #52525b; font-size: 16px; line-height: 1.65; }
51
+ .stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
52
+ .stat { border: 1px solid #e4e4e7; border-radius: 14px; padding: 22px 16px; text-align: center; background: #fff; }
53
+ .stat-n { font-size: 26px; font-weight: 800; letter-spacing: -.02em; color: #ff5e00; }
54
+ .stat-l { color: #71717a; font-size: 12px; line-height: 1.45; margin-top: 6px; }
55
+ .section { display: flex; flex-direction: column; gap: 14px; }
56
+ .h2 { font-size: 22px; font-weight: 700; letter-spacing: -.02em; }
57
+ .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; }
58
+ .note { color: #71717a; font-size: 13px; line-height: 1.55; }
59
+ .cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }
60
+ .card { border: 1px solid #e4e4e7; border-radius: 14px; padding: 18px; background: #fff; }
61
+ .card-title { font-size: 14px; font-weight: 600; margin-bottom: 5px; }
62
+ .card-text { color: #71717a; font-size: 13px; line-height: 1.5; }
63
+ @media (max-width: 560px) { .stats, .cards { grid-template-columns: 1fr; } }
64
+ `;
40
65
  // vite.config composed from the chosen options — muten always; svelte/react add island plugins; tailwind last.
41
66
  const viteConfig = ({ tailwind, svelte, react }) => {
42
67
  const imports = [`import muten from '@muten/core/vite-plugin-muten.js';`];
@@ -89,26 +114,52 @@ DaisyUI adds **component classes** on top of Tailwind — use them in \`class("
89
114
  \`@plugin "daisyui";\` is already in \`src/styles.css\`. Interactive behavior (toggle a modal/dropdown) you build
90
115
  with Muten: \`state\` + \`class(active when isOpen)\` + \`on(click: …)\`.
91
116
  `;
117
+ // Tauri = the SAME web build wrapped in a native OS-webview window (no browser bundled). Desktop target.
118
+ const TAURI_NOTE = (pm) => `
119
+ ## Desktop app (Tauri)
120
+ This app also ships as a native desktop app via Tauri (\`src-tauri/\`). The SAME \`.muten\` frontend runs in
121
+ an OS-webview window — build the UI exactly like the web app (routing works as-is: the webview runs the SPA,
122
+ no server, no URL bar, no fallback needed).
123
+ - \`${pm} run tauri dev\` — run the desktop app (hot-reloads the frontend).
124
+ - \`${pm} run tauri build\` — native installer in \`src-tauri/target/release/bundle/\`.
125
+ - Needs the **Rust toolchain** on the machine (https://rustup.rs) — Tauri compiles a small native shell. Not auto-installed.
126
+ - Custom icon: \`${pm} run tauri icon path/to/logo.png\` regenerates \`src-tauri/icons/\`.
127
+ `;
128
+
129
+ // Deploy on Vercel: muten routes are real paths (History API), so an unmatched path must fall back to
130
+ // index.html (else a hard refresh of /about 404s). Static assets are served first; only routes rewrite.
131
+ const VERCEL_JSON = `{
132
+ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
133
+ }
134
+ `;
92
135
 
93
136
  // Which PM launched us? npm/pnpm/yarn/bun set npm_config_user_agent — the idiomatic default.
94
137
  const detectPM = () => { const ua = process.env.npm_config_user_agent || ''; return PMS.find((p) => ua.startsWith(p + '/')) || 'npm'; };
95
138
  const validName = (n) => /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(n);
96
139
  const keep = (v) => { if (isCancel(v)) { cancel('Cancelled.'); process.exit(0); } return v; };
97
140
 
141
+ // The muten mark + wordmark, in the brand orange (#FF5E00) — shown before the prompts. Truecolor ANSI
142
+ // (modern terminals); degrades to plain text where unsupported.
143
+ const logo = () => {
144
+ 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';
145
+ console.log(`\n ${tile} M ${r} ${b}${o}muten${r}`);
146
+ console.log(` ${d} the AI-first frontend framework${r}\n`);
147
+ };
148
+
98
149
  async function main() {
99
150
  const argv = process.argv.slice(2);
100
151
  const has = (f) => argv.includes(f);
101
152
  const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; };
102
153
  if (has('-v') || has('--version')) { console.log(PKG.version); 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; }
154
+ if (has('-h') || has('--help')) { console.log('Usage: create-muten [name] [--template muten|react|svelte] [--css|--scss] [--tailwind] [--daisyui] [--vercel] [--tauri] [--pm npm|pnpm|yarn|bun] [--no-install]'); return; }
104
155
 
105
156
  let name = argv.filter((a, i) => !a.startsWith('-') && argv[i - 1] !== '--pm' && argv[i - 1] !== '--template')[0];
106
- let template = val('--template') || (has('--full') ? 'full' : has('--routing') ? 'routing' : has('--basic') ? 'basic' : undefined);
157
+ let template = val('--template') || (has('--react') ? 'react' : has('--svelte') ? 'svelte' : has('--muten') ? 'muten' : undefined); // flavor: muten | react | svelte
107
158
  let style = has('--scss') ? 'scss' : has('--css') ? 'css' : undefined; // the base stylesheet
108
159
  let tailwind = has('--tailwind') ? true : undefined; // optional add-on (CSS only)
109
160
  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)
161
+ let vercel = has('--vercel') ? true : undefined; // a vercel.json with the SPA fallback rewrite
162
+ let tauri = has('--tauri') ? true : undefined; // src-tauri/ native desktop app
112
163
  let pm = val('--pm');
113
164
  let install = has('--no-install') ? false : undefined;
114
165
  if (name && !validName(name)) { console.error(`Invalid name: "${name}" (letters, digits, . _ -)`); process.exit(1); }
@@ -118,53 +169,51 @@ async function main() {
118
169
 
119
170
  // Styled prompts only with a real TTY (piped/CI input would hang); otherwise use flags + defaults.
120
171
  if (process.stdin.isTTY) {
121
- intro(color.bgCyan(color.black(' create-muten ')) + color.dim(' the AI-first frontend framework'));
172
+ logo();
173
+ intro(color.dim(`create-muten v${PKG.version}`));
122
174
  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 }));
123
175
  if (!template) template = keep(await select({ message: 'Template', options: [
124
- { value: 'basic', label: 'Basic', hint: 'one page' },
125
- { value: 'routing', label: 'Routing', hint: 'shell + multiple pages' },
126
- { value: 'full', label: 'Routing + store + API + Tailwind', hint: 'a real data app' },
127
- ] }));
128
- if (template === 'full') tailwind = true; // the full template implies Tailwind
129
- // Tailwind is an add-on on top of CSS (Tailwind v4 is CSS-native; Sass isn't recommended).
130
- if (!style && !tailwind) style = keep(await select({ message: 'Stylesheet', options: [
131
- { value: 'css', label: 'CSS', hint: 'plain, zero deps' },
132
- { value: 'scss', label: 'SCSS', hint: 'adds sass' },
176
+ { value: 'muten', label: 'muten', hint: 'pure — the AI-first DSL, zero framework runtime' },
177
+ { value: 'react', label: 'muten + React', hint: 'React islands: shadcn, Radix, any React lib' },
178
+ { value: 'svelte', label: 'muten + Svelte', hint: 'Svelte islands: a lighter runtime' },
133
179
  ] }));
134
- if (tailwind === undefined) tailwind = style === 'css' ? keep(await confirm({ message: 'Add Tailwind CSS? (utility classes via class("…"))', initialValue: false })) : false;
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' },
180
+ if (!style && tailwind === undefined) { // ONE explicit styling choice each is opt-in, "CSS" = nothing extra
181
+ const styling = keep(await select({ message: 'Styling', options: [
182
+ { value: 'css', label: 'CSS', hint: 'plain no framework, zero deps' },
183
+ { value: 'scss', label: 'SCSS', hint: 'adds sass' },
184
+ { value: 'tailwind', label: 'Tailwind CSS', hint: 'utility classes on top of CSS' },
185
+ { value: 'daisyui', label: 'DaisyUI', hint: 'component classes (btn, card, modal) — brings Tailwind with it' },
142
186
  ] }));
143
- svelte = islands === 'svelte' || islands === 'both';
144
- react = islands === 'react' || islands === 'both';
187
+ style = styling === 'scss' ? 'scss' : 'css';
188
+ tailwind = styling === 'tailwind' || styling === 'daisyui';
189
+ daisyui = styling === 'daisyui';
145
190
  }
191
+ if (vercel === undefined) vercel = keep(await confirm({ message: 'Add Vercel deploy config? (vercel.json — fixes real-path routing on Vercel)', initialValue: false }));
192
+ if (tauri === undefined) tauri = keep(await confirm({ message: 'Desktop app? (Tauri — native window, ships the OS webview, needs Rust)', initialValue: false }));
146
193
  }
147
194
  name = name || 'muten-app';
148
- template = template || 'basic';
149
- if (template === 'full') tailwind = true; // full implies Tailwind
150
- if (daisyui) tailwind = true; // DaisyUI is a Tailwind plugin
195
+ template = template || 'muten';
151
196
  style = style || 'css';
197
+ if (daisyui) tailwind = true; // DaisyUI is a Tailwind plugin
152
198
  if (tailwind === undefined) tailwind = false;
153
199
  if (daisyui === undefined) daisyui = false;
154
- if (svelte === undefined) svelte = false;
155
- if (react === undefined) react = false;
156
- if (tailwind) style = 'css'; // Tailwind implies a CSS base (not SCSS)
200
+ if (vercel === undefined) vercel = false;
201
+ if (tauri === undefined) tauri = false;
202
+ if (tailwind) style = 'css'; // Tailwind v4 is CSS-native (not SCSS)
203
+ const svelte = template === 'svelte'; // the flavor IS the islands choice
204
+ const react = template === 'react';
157
205
  pm = pm || dpm;
158
206
  if (install === undefined) install = false;
159
207
 
160
208
  const target = resolve(name);
161
209
  if (existsSync(target)) { (process.stdin.isTTY ? cancel : console.error)(`"${name}" already exists.`); process.exit(1); }
162
210
 
163
- // scaffold from ./template (the basic base) + layer the chosen variant's overlay + the stylesheet/add-ons
211
+ // EVERY flavor scaffolds the SAME base template (identical welcome page); react/svelte only add the
212
+ // island plugin + deps below, and tailwind/daisyui only swap the stylesheet.
164
213
  cpSync(TEMPLATE, target, { recursive: true });
165
- if (template !== 'basic') cpSync(join(OVERLAYS, template), target, { recursive: true }); // routing / full overlay
166
214
  const ignore = join(target, '_gitignore');
167
215
  if (existsSync(ignore)) renameSync(ignore, join(target, '.gitignore'));
216
+ if (vercel) writeFileSync(join(target, 'vercel.json'), VERCEL_JSON); // SPA fallback so real-path routes work on Vercel
168
217
 
169
218
  const pkgPath = join(target, 'package.json');
170
219
  const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
@@ -174,23 +223,37 @@ async function main() {
174
223
  const appendAgents = (text) => { const f = join(target, '.claude', 'AGENTS.md'); if (existsSync(f)) writeFileSync(f, readFileSync(f, 'utf8') + text); };
175
224
 
176
225
  if (tailwind) {
177
- writeFileSync(join(target, 'src', 'styles.css'), tailwindStyles(daisyui));
226
+ writeFileSync(join(target, 'src', 'styles.css'), tailwindStyles(daisyui) + WELCOME_CSS);
178
227
  writeFileSync(join(target, 'theme.muten'), TAILWIND_THEME); // scale centralized to Tailwind's
179
228
  addDev({ tailwindcss: '^4.0.0', '@tailwindcss/vite': '^4.0.0' });
180
229
  if (daisyui) addDev({ daisyui: '^5.0.0' });
181
230
  appendAgents(TAILWIND_NOTE + (daisyui ? DAISY_NOTE : '')); // tell the AI what styling is available
182
231
  } else {
183
- writeFileSync(join(target, 'src', style === 'scss' ? 'styles.scss' : 'styles.css'), RESET);
232
+ writeFileSync(join(target, 'src', style === 'scss' ? 'styles.scss' : 'styles.css'), RESET + WELCOME_CSS);
184
233
  }
185
234
  if (style === 'scss') addDev({ sass: '^1.101.0' });
186
235
  if (svelte) { addDep({ svelte: '^5.0.0' }); addDev({ '@sveltejs/vite-plugin-svelte': '^7.0.0' }); }
187
236
  if (react) { addDep({ react: '^19.0.0', 'react-dom': '^19.0.0' }); addDev({ '@vitejs/plugin-react': '^6.0.0' }); }
188
237
  if (svelte || react) appendAgents(ISLANDS_NOTE({ svelte, react }));
238
+ if (tauri) { // native desktop wrapper around the same web build (dist)
239
+ cpSync(join(TAURI_TEMPLATE, 'src-tauri'), join(target, 'src-tauri'), { recursive: true });
240
+ writeFileSync(join(target, 'src-tauri', '.gitignore'), '/target\n/gen/schemas\n'); // npm strips real .gitignore from packages
241
+ const confPath = join(target, 'src-tauri', 'tauri.conf.json');
242
+ const conf = JSON.parse(readFileSync(confPath, 'utf8'));
243
+ conf.productName = name;
244
+ conf.identifier = `com.muten.${name.replace(/[^a-z0-9]/gi, '').toLowerCase() || 'app'}`; // reverse-DNS, no dashes/underscores
245
+ conf.app.windows[0].title = name;
246
+ conf.build.beforeDevCommand = `${pm} run dev`;
247
+ conf.build.beforeBuildCommand = `${pm} run build`;
248
+ writeFileSync(confPath, JSON.stringify(conf, null, 2) + '\n');
249
+ addDev({ '@tauri-apps/cli': '^2.0.0' });
250
+ pkg.scripts = { ...pkg.scripts, tauri: 'tauri' };
251
+ appendAgents(TAURI_NOTE(pm));
252
+ }
189
253
  writeFileSync(join(target, 'vite.config.mjs'), viteConfig({ tailwind, svelte, react })); // composed: muten + chosen plugins
190
254
  writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
191
255
 
192
- const islandDesc = [svelte && 'Svelte', react && 'React'].filter(Boolean).join('+');
193
- const desc = `${template}, ${style}${tailwind ? ' + Tailwind' : ''}${daisyui ? ' + DaisyUI' : ''}${islandDesc ? ' + ' + islandDesc + ' islands' : ''}`;
256
+ const desc = `${template}, ${style}${tailwind ? ' + Tailwind' : ''}${daisyui ? ' + DaisyUI' : ''}${vercel ? ' + Vercel' : ''}${tauri ? ' + Tauri' : ''}`;
194
257
  if (!install) {
195
258
  if (process.stdin.isTTY) { note(`cd ${name}\n${pm} install\n${pm} run dev`, 'Next steps'); outro(color.green(`Created ${name} (${desc})`)); }
196
259
  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.8",
3
+ "version": "0.0.10",
4
4
  "description": "Scaffold a new Muten app.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -19,5 +19,5 @@
19
19
  "picocolors": "^1.0.1"
20
20
  },
21
21
  "keywords": ["muten", "create-muten", "scaffold", "starter", "cli", "frontend"],
22
- "files": ["index.js", "template", "overlays"]
22
+ "files": ["index.js", "template", "template-tauri", "overlays"]
23
23
  }
@@ -8,7 +8,7 @@
8
8
  "lint": "muten lint"
9
9
  },
10
10
  "dependencies": {
11
- "@muten/core": "^0.0.7"
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
  }
Binary file
@@ -0,0 +1,15 @@
1
+ [package]
2
+ name = "muten-app"
3
+ version = "0.1.0"
4
+ description = "A muten desktop app"
5
+ edition = "2021"
6
+
7
+ [lib]
8
+ name = "app_lib"
9
+ crate-type = ["staticlib", "cdylib", "rlib"]
10
+
11
+ [build-dependencies]
12
+ tauri-build = { version = "2", features = [] }
13
+
14
+ [dependencies]
15
+ tauri = { version = "2", features = [] }
@@ -0,0 +1,3 @@
1
+ fn main() {
2
+ tauri_build::build()
3
+ }
@@ -0,0 +1,7 @@
1
+ {
2
+ "$schema": "../gen/schemas/desktop-schema.json",
3
+ "identifier": "default",
4
+ "description": "enables the default permissions",
5
+ "windows": ["main"],
6
+ "permissions": ["core:default"]
7
+ }
@@ -0,0 +1,6 @@
1
+ #[cfg_attr(mobile, tauri::mobile_entry_point)]
2
+ pub fn run() {
3
+ tauri::Builder::default()
4
+ .run(tauri::generate_context!())
5
+ .expect("error while running tauri application");
6
+ }
@@ -0,0 +1,6 @@
1
+ // Prevents an extra console window on Windows in release, do NOT remove!!
2
+ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
3
+
4
+ fn main() {
5
+ app_lib::run()
6
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$schema": "https://schema.tauri.app/config/2",
3
+ "productName": "muten-app",
4
+ "version": "0.1.0",
5
+ "identifier": "com.muten.app",
6
+ "build": {
7
+ "frontendDist": "../dist",
8
+ "devUrl": "http://localhost:5173",
9
+ "beforeDevCommand": "npm run dev",
10
+ "beforeBuildCommand": "npm run build"
11
+ },
12
+ "app": {
13
+ "windows": [
14
+ { "title": "muten-app", "width": 1000, "height": 700 }
15
+ ],
16
+ "security": { "csp": null }
17
+ },
18
+ "bundle": {
19
+ "active": true,
20
+ "targets": "all",
21
+ "icon": [
22
+ "icons/32x32.png",
23
+ "icons/128x128.png",
24
+ "icons/128x128@2x.png",
25
+ "icons/icon.icns",
26
+ "icons/icon.ico"
27
+ ]
28
+ }
29
+ }