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.
- package/README.md +27 -19
- package/index.js +101 -38
- package/package.json +2 -2
- package/template/package.json +1 -1
- package/template/public/muten.svg +4 -0
- package/template/src/components/Snippet.js +19 -0
- package/template/src/pages/home/home.muten +42 -6
- package/template-tauri/muten-logo.png +0 -0
- package/template-tauri/src-tauri/Cargo.toml +15 -0
- package/template-tauri/src-tauri/build.rs +3 -0
- package/template-tauri/src-tauri/capabilities/default.json +7 -0
- package/template-tauri/src-tauri/icons/128x128.png +0 -0
- package/template-tauri/src-tauri/icons/128x128@2x.png +0 -0
- package/template-tauri/src-tauri/icons/32x32.png +0 -0
- package/template-tauri/src-tauri/icons/64x64.png +0 -0
- package/template-tauri/src-tauri/icons/Square107x107Logo.png +0 -0
- package/template-tauri/src-tauri/icons/Square142x142Logo.png +0 -0
- package/template-tauri/src-tauri/icons/Square150x150Logo.png +0 -0
- package/template-tauri/src-tauri/icons/Square284x284Logo.png +0 -0
- package/template-tauri/src-tauri/icons/Square30x30Logo.png +0 -0
- package/template-tauri/src-tauri/icons/Square310x310Logo.png +0 -0
- package/template-tauri/src-tauri/icons/Square44x44Logo.png +0 -0
- package/template-tauri/src-tauri/icons/Square71x71Logo.png +0 -0
- package/template-tauri/src-tauri/icons/Square89x89Logo.png +0 -0
- package/template-tauri/src-tauri/icons/StoreLogo.png +0 -0
- package/template-tauri/src-tauri/icons/icon.icns +0 -0
- package/template-tauri/src-tauri/icons/icon.ico +0 -0
- package/template-tauri/src-tauri/icons/icon.png +0 -0
- package/template-tauri/src-tauri/src/lib.rs +6 -0
- package/template-tauri/src-tauri/src/main.rs +6 -0
- 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** | `
|
|
64
|
-
| **
|
|
65
|
-
| **Add
|
|
66
|
-
| **
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
-
| **
|
|
79
|
-
| **
|
|
80
|
-
| **
|
|
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
|
-
|
|
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 <
|
|
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
|
-
| `--
|
|
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
|
|
19
|
-
const TEMPLATES = ['
|
|
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
|
|
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('--
|
|
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
|
|
111
|
-
let
|
|
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
|
-
|
|
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: '
|
|
125
|
-
{ value: '
|
|
126
|
-
{ value: '
|
|
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)
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
{ value: '
|
|
139
|
-
{ value: '
|
|
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
|
-
|
|
144
|
-
|
|
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 || '
|
|
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 (
|
|
155
|
-
if (
|
|
156
|
-
if (tailwind) style = 'css';
|
|
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
|
-
//
|
|
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
|
|
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.
|
|
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
|
}
|
package/template/package.json
CHANGED
|
@@ -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
|
-
#
|
|
2
|
-
#
|
|
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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 = [] }
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
}
|