create-muten 0.0.9 → 0.0.11
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 +13 -5
- package/index.js +57 -10
- package/package.json +2 -2
- package/template/src/pages/home/home.muten +1 -8
- 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/overlays/full/src/app.muten +0 -21
- package/overlays/full/src/cart.store +0 -9
- package/overlays/full/src/pages/products/products.muten +0 -26
- package/overlays/routing/src/app.muten +0 -19
- package/overlays/routing/src/pages/about/about.muten +0 -15
- package/template/src/components/Snippet.js +0 -19
package/README.md
CHANGED
|
@@ -61,14 +61,20 @@ In an interactive terminal it prompts for a few things (defaults in parentheses)
|
|
|
61
61
|
|---|---|---|
|
|
62
62
|
| **Project name** | any valid folder name | `muten-app` |
|
|
63
63
|
| **Template** | `muten` / `muten + React` / `muten + Svelte` | `muten` |
|
|
64
|
-
| **Styling** | `
|
|
65
|
-
| **Add
|
|
66
|
-
| **
|
|
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` |
|
|
67
67
|
| **Package manager** | `npm` / `pnpm` / `yarn` / `bun` | the one that launched it |
|
|
68
68
|
| **Install deps and start dev now?** | `Y` / `n` | `Y` |
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
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
|
+
|
|
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).
|
|
72
78
|
|
|
73
79
|
## Templates (flavors)
|
|
74
80
|
|
|
@@ -110,6 +116,8 @@ create-muten my-app --css --no-install # just scaffold, decide later
|
|
|
110
116
|
| `--css` / `--scss` | pick the stylesheet (default: `css`) |
|
|
111
117
|
| `--tailwind` | add Tailwind CSS v4 on top of CSS (forces `--css`) |
|
|
112
118
|
| `--daisyui` | add DaisyUI component classes (implies `--tailwind`) |
|
|
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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// create-muten — scaffold a new Muten app, with modern interactive prompts (@clack/prompts).
|
|
3
3
|
//
|
|
4
4
|
// npm create muten@latest [name] (or: npx create-muten)
|
|
5
|
-
// create-muten [name] [--template
|
|
5
|
+
// create-muten [name] [--template muten|react|svelte] [--css|--scss] [--tailwind] [--daisyui] [--vercel] [--tauri] [--pm npm|pnpm|yarn|bun] [--no-install]
|
|
6
6
|
//
|
|
7
7
|
// Stylesheet (CSS or SCSS) is the base; Tailwind is an optional add-on ON TOP of CSS (it's a styling
|
|
8
8
|
// library, not a stylesheet replacement). Interactive in a TTY; flags / non-TTY make it scriptable.
|
|
@@ -15,6 +15,7 @@ import color from 'picocolors';
|
|
|
15
15
|
|
|
16
16
|
const SELF = dirname(fileURLToPath(import.meta.url));
|
|
17
17
|
const TEMPLATE = join(SELF, 'template');
|
|
18
|
+
const TAURI_TEMPLATE = join(SELF, 'template-tauri'); // src-tauri/ overlay, copied only when --tauri
|
|
18
19
|
const TEMPLATES = ['muten', 'react', 'svelte']; // the "template" IS the flavor: pure muten, or muten + a framework for islands
|
|
19
20
|
const PKG = JSON.parse(readFileSync(join(SELF, 'package.json'), 'utf8'));
|
|
20
21
|
const PMS = ['npm', 'pnpm', 'yarn', 'bun'];
|
|
@@ -53,7 +54,6 @@ const WELCOME_CSS = `
|
|
|
53
54
|
.stat-l { color: #71717a; font-size: 12px; line-height: 1.45; margin-top: 6px; }
|
|
54
55
|
.section { display: flex; flex-direction: column; gap: 14px; }
|
|
55
56
|
.h2 { font-size: 22px; font-weight: 700; letter-spacing: -.02em; }
|
|
56
|
-
.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; }
|
|
57
57
|
.note { color: #71717a; font-size: 13px; line-height: 1.55; }
|
|
58
58
|
.cards { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }
|
|
59
59
|
.card { border: 1px solid #e4e4e7; border-radius: 14px; padding: 18px; background: #fff; }
|
|
@@ -113,6 +113,25 @@ DaisyUI adds **component classes** on top of Tailwind — use them in \`class("
|
|
|
113
113
|
\`@plugin "daisyui";\` is already in \`src/styles.css\`. Interactive behavior (toggle a modal/dropdown) you build
|
|
114
114
|
with Muten: \`state\` + \`class(active when isOpen)\` + \`on(click: …)\`.
|
|
115
115
|
`;
|
|
116
|
+
// Tauri = the SAME web build wrapped in a native OS-webview window (no browser bundled). Desktop target.
|
|
117
|
+
const TAURI_NOTE = (pm) => `
|
|
118
|
+
## Desktop app (Tauri)
|
|
119
|
+
This app also ships as a native desktop app via Tauri (\`src-tauri/\`). The SAME \`.muten\` frontend runs in
|
|
120
|
+
an OS-webview window — build the UI exactly like the web app (routing works as-is: the webview runs the SPA,
|
|
121
|
+
no server, no URL bar, no fallback needed).
|
|
122
|
+
- \`${pm} run tauri:dev\` — run the desktop app (opens the native window; hot-reloads the frontend).
|
|
123
|
+
- \`${pm} run tauri:build\` — standalone native installer in \`src-tauri/target/release/bundle/\` (frontend embedded, no server).
|
|
124
|
+
- (\`${pm} run tauri\` alone does nothing — the Tauri CLI needs a subcommand like \`dev\`/\`build\`.)
|
|
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
|
+
`;
|
|
116
135
|
|
|
117
136
|
// Which PM launched us? npm/pnpm/yarn/bun set npm_config_user_agent — the idiomatic default.
|
|
118
137
|
const detectPM = () => { const ua = process.env.npm_config_user_agent || ''; return PMS.find((p) => ua.startsWith(p + '/')) || 'npm'; };
|
|
@@ -132,13 +151,15 @@ async function main() {
|
|
|
132
151
|
const has = (f) => argv.includes(f);
|
|
133
152
|
const val = (f) => { const i = argv.indexOf(f); return i >= 0 ? argv[i + 1] : undefined; };
|
|
134
153
|
if (has('-v') || has('--version')) { console.log(PKG.version); return; }
|
|
135
|
-
if (has('-h') || has('--help')) { console.log('Usage: create-muten [name] [--template muten|react|svelte] [--css|--scss] [--tailwind] [--daisyui] [--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; }
|
|
136
155
|
|
|
137
156
|
let name = argv.filter((a, i) => !a.startsWith('-') && argv[i - 1] !== '--pm' && argv[i - 1] !== '--template')[0];
|
|
138
157
|
let template = val('--template') || (has('--react') ? 'react' : has('--svelte') ? 'svelte' : has('--muten') ? 'muten' : undefined); // flavor: muten | react | svelte
|
|
139
158
|
let style = has('--scss') ? 'scss' : has('--css') ? 'css' : undefined; // the base stylesheet
|
|
140
159
|
let tailwind = has('--tailwind') ? true : undefined; // optional add-on (CSS only)
|
|
141
160
|
let daisyui = has('--daisyui') ? true : undefined; // component classes on Tailwind
|
|
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
|
|
142
163
|
let pm = val('--pm');
|
|
143
164
|
let install = has('--no-install') ? false : undefined;
|
|
144
165
|
if (name && !validName(name)) { console.error(`Invalid name: "${name}" (letters, digits, . _ -)`); process.exit(1); }
|
|
@@ -156,12 +177,19 @@ async function main() {
|
|
|
156
177
|
{ value: 'react', label: 'muten + React', hint: 'React islands: shadcn, Radix, any React lib' },
|
|
157
178
|
{ value: 'svelte', label: 'muten + Svelte', hint: 'Svelte islands: a lighter runtime' },
|
|
158
179
|
] }));
|
|
159
|
-
if (!style
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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' },
|
|
186
|
+
] }));
|
|
187
|
+
style = styling === 'scss' ? 'scss' : 'css';
|
|
188
|
+
tailwind = styling === 'tailwind' || styling === 'daisyui';
|
|
189
|
+
daisyui = styling === 'daisyui';
|
|
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 }));
|
|
165
193
|
}
|
|
166
194
|
name = name || 'muten-app';
|
|
167
195
|
template = template || 'muten';
|
|
@@ -169,6 +197,8 @@ async function main() {
|
|
|
169
197
|
if (daisyui) tailwind = true; // DaisyUI is a Tailwind plugin
|
|
170
198
|
if (tailwind === undefined) tailwind = false;
|
|
171
199
|
if (daisyui === undefined) daisyui = false;
|
|
200
|
+
if (vercel === undefined) vercel = false;
|
|
201
|
+
if (tauri === undefined) tauri = false;
|
|
172
202
|
if (tailwind) style = 'css'; // Tailwind v4 is CSS-native (not SCSS)
|
|
173
203
|
const svelte = template === 'svelte'; // the flavor IS the islands choice
|
|
174
204
|
const react = template === 'react';
|
|
@@ -183,6 +213,7 @@ async function main() {
|
|
|
183
213
|
cpSync(TEMPLATE, target, { recursive: true });
|
|
184
214
|
const ignore = join(target, '_gitignore');
|
|
185
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
|
|
186
217
|
|
|
187
218
|
const pkgPath = join(target, 'package.json');
|
|
188
219
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
@@ -204,10 +235,26 @@ async function main() {
|
|
|
204
235
|
if (svelte) { addDep({ svelte: '^5.0.0' }); addDev({ '@sveltejs/vite-plugin-svelte': '^7.0.0' }); }
|
|
205
236
|
if (react) { addDep({ react: '^19.0.0', 'react-dom': '^19.0.0' }); addDev({ '@vitejs/plugin-react': '^6.0.0' }); }
|
|
206
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
|
+
// `tauri` alone errors (it needs a subcommand) → ship explicit run scripts so `tauri dev`/`build` are obvious.
|
|
251
|
+
pkg.scripts = { ...pkg.scripts, tauri: 'tauri', 'tauri:dev': 'tauri dev', 'tauri:build': 'tauri build' };
|
|
252
|
+
appendAgents(TAURI_NOTE(pm));
|
|
253
|
+
}
|
|
207
254
|
writeFileSync(join(target, 'vite.config.mjs'), viteConfig({ tailwind, svelte, react })); // composed: muten + chosen plugins
|
|
208
255
|
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
209
256
|
|
|
210
|
-
const desc = `${template}, ${style}${tailwind ? ' + Tailwind' : ''}${daisyui ? ' + DaisyUI' : ''}`;
|
|
257
|
+
const desc = `${template}, ${style}${tailwind ? ' + Tailwind' : ''}${daisyui ? ' + DaisyUI' : ''}${vercel ? ' + Vercel' : ''}${tauri ? ' + Tauri' : ''}`;
|
|
211
258
|
if (!install) {
|
|
212
259
|
if (process.stdin.isTTY) { note(`cd ${name}\n${pm} install\n${pm} run dev`, 'Next steps'); outro(color.green(`Created ${name} (${desc})`)); }
|
|
213
260
|
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.11",
|
|
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", "
|
|
22
|
+
"files": ["index.js", "template", "template-tauri"]
|
|
23
23
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
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/.
|
|
3
|
-
# (src/components/Snippet.js) because a .muten string can't hold the quotes/braces of muten source.
|
|
2
|
+
# ask your AI assistant; it has the full muten reference in .claude/. Pure muten — no JS, no escape hatch.
|
|
4
3
|
screen home
|
|
5
4
|
|
|
6
5
|
Page class("welcome") {
|
|
@@ -19,12 +18,6 @@ Page class("welcome") {
|
|
|
19
18
|
Stack class("stat") { Title "0 KB" h3 class("stat-n") Text "on a static page" class("stat-l") }
|
|
20
19
|
}
|
|
21
20
|
|
|
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
21
|
Stack class("section") {
|
|
29
22
|
Title "Where to go next" h2 class("h2")
|
|
30
23
|
Stack class("cards") {
|
|
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
|
+
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# The app ROOT. `api` is the backend config (one place: base URL + default headers) — every `sources`
|
|
2
|
-
# inherits it. The shell's navbar reads the cart store (global) and shows a live count.
|
|
3
|
-
api {
|
|
4
|
-
base: "https://fakestoreapi.com"
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
shell {
|
|
8
|
-
Header style(row, between, center) class("p-4 shadow bg-white") {
|
|
9
|
-
Link "Shop" -> / class("text-xl font-bold")
|
|
10
|
-
Nav "Main" style(row, gap.md, center) {
|
|
11
|
-
Link "Products" -> /products
|
|
12
|
-
Span "🛒 {cart.count}" class("font-medium")
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
slot
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
routes {
|
|
19
|
-
/ -> home
|
|
20
|
-
/products -> products
|
|
21
|
-
}
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
# An app-global store (any `*.store` under src/). State is shared across pages + the shell, no imports.
|
|
2
|
-
# `get` is a derived value; `action`s are the only way to mutate.
|
|
3
|
-
state { items = [] : list<text> }
|
|
4
|
-
|
|
5
|
-
get count = items.length
|
|
6
|
-
|
|
7
|
-
action add mutates items <- id {
|
|
8
|
-
items.push(id)
|
|
9
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
# A real data page: `query` state backed by a `sources` URL (relative → joined to the app's `api.base`).
|
|
2
|
-
# `muten build` fetches it at build and bakes the rows into the HTML (SSG); the runtime then takes over.
|
|
3
|
-
# Clicking "Add" calls the cart store action — the navbar count updates reactively.
|
|
4
|
-
screen products
|
|
5
|
-
|
|
6
|
-
meta { title "Products" }
|
|
7
|
-
|
|
8
|
-
entity Product { title text price text }
|
|
9
|
-
|
|
10
|
-
state { products = query products : list<Product> }
|
|
11
|
-
|
|
12
|
-
sources { products: "/products" }
|
|
13
|
-
|
|
14
|
-
Page style(padding.lg, gap.md) {
|
|
15
|
-
Title "Products" class("text-2xl font-bold")
|
|
16
|
-
when products.loading { Text "Loading…" class("opacity-60") }
|
|
17
|
-
each products as p {
|
|
18
|
-
Stack style(gap.sm) class("p-4 rounded-lg shadow bg-white") {
|
|
19
|
-
Text "{p.title}" class("font-semibold")
|
|
20
|
-
Stack style(row, between, center) {
|
|
21
|
-
Span "$ {p.price}" class("text-lg")
|
|
22
|
-
Button "Add to cart" -> cart.add(p.id) class("px-3 py-1 rounded bg-black text-white")
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# The app ROOT. A persistent `shell { … slot … }` wraps every page (navbar here); `slot` is where the
|
|
2
|
-
# active page mounts. `routes` maps a real-path URL to a page (the folder under src/pages/).
|
|
3
|
-
shell {
|
|
4
|
-
Header style(row, between, center) class("nav") {
|
|
5
|
-
Link "Home" -> /
|
|
6
|
-
Nav "Main" {
|
|
7
|
-
Link "About" -> /about
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
slot
|
|
11
|
-
Footer style(padding.md) {
|
|
12
|
-
Text "Built with Muten"
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
routes {
|
|
17
|
-
/ -> home
|
|
18
|
-
/about -> about
|
|
19
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# A second page. `meta { … }` sets the <head> title/description (og:* auto-derived) — applied on
|
|
2
|
-
# navigation and baked into the static HTML at build. This page has no reactivity, so `muten build`
|
|
3
|
-
# pre-renders it to zero-JS HTML (crawlable).
|
|
4
|
-
screen about
|
|
5
|
-
|
|
6
|
-
meta {
|
|
7
|
-
title "About"
|
|
8
|
-
description "About this Muten app."
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
Page style(padding.lg, gap.md) {
|
|
12
|
-
Title "About"
|
|
13
|
-
Text "Static pages compile to zero-JS HTML at build — crawlable and instant."
|
|
14
|
-
Link "← Home" -> /
|
|
15
|
-
}
|
|
@@ -1,19 +0,0 @@
|
|
|
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
|
-
}
|