create-nowaki 0.9.1 → 0.11.0

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/index.js CHANGED
@@ -1,31 +1,202 @@
1
1
  #!/usr/bin/env node
2
- import { cp, mkdir, readdir } from "node:fs/promises";
2
+ // `npm create nowaki` のスキャフォールダ。依存ゼロ(Node 組み込みのみ)。
3
+ // 対話ウィザード(TTY 時): プロジェクト名・パッケージマネージャ・git・依存インストール。
4
+ // 非対話(CI / `-y` / パイプ)では既定値で黙って生成する。
5
+ //
6
+ // フラグ: -t/--template <basics|minimal>, -y/--yes(質問を飛ばす),
7
+ // --install(非対話でも入れる), --no-install, --no-git, --pm <npm|pnpm|yarn|bun>
8
+
9
+ import { cp, mkdir, readdir, readFile, writeFile, rename } from "node:fs/promises";
3
10
  import path from "node:path";
4
11
  import { fileURLToPath } from "node:url";
12
+ import { createInterface } from "node:readline/promises";
13
+ import { execFileSync } from "node:child_process";
14
+ import process from "node:process";
15
+
16
+ const { stdin, stdout, env, argv, platform } = process;
17
+
18
+ // --- 色(NO_COLOR と非 TTY を尊重)---
19
+ const useColor = !!stdout.isTTY && !env.NO_COLOR;
20
+ const wrap = (code) => (s) => (useColor ? `\x1b[${code}m${s}\x1b[0m` : `${s}`);
21
+ const cyan = wrap("36");
22
+ const dim = wrap("2");
23
+ const bold = wrap("1");
24
+ const green = wrap("32");
25
+ const red = wrap("31");
26
+ const yellow = wrap("33");
27
+
28
+ // --- 引数 ---
29
+ const args = argv.slice(2);
30
+ // 値をとるフラグ。これらの直後の引数は positional ではない。
31
+ const VALUE_FLAGS = new Set(["--pm", "--template", "-t"]);
32
+ const flagAt = (...names) => {
33
+ const i = args.findIndex((a) => names.includes(a));
34
+ return i >= 0 ? args[i + 1] : null;
35
+ };
36
+ const flags = new Set(args.filter((a) => a.startsWith("-")));
37
+ const positionals = [];
38
+ for (let i = 0; i < args.length; i++) {
39
+ if (args[i].startsWith("-")) {
40
+ if (VALUE_FLAGS.has(args[i])) i++; // フラグの値を飛ばす
41
+ continue;
42
+ }
43
+ positionals.push(args[i]);
44
+ }
45
+ const pmFlag = flagAt("--pm");
46
+ const templateFlag = flagAt("--template", "-t");
47
+ const yes = flags.has("-y") || flags.has("--yes");
48
+ const noInstall = flags.has("--no-install");
49
+ const forceInstall = flags.has("--install");
50
+ const noGit = flags.has("--no-git");
51
+ const interactive = !!stdin.isTTY && !!stdout.isTTY && !yes;
52
+
53
+ const templatesDir = path.join(path.dirname(fileURLToPath(import.meta.url)), "templates");
54
+ // 利用可能なテンプレート(templates/<name>、_shared は共通設定)。
55
+ const TEMPLATES = [
56
+ { value: "basics", hint: "starter with an interactive island" },
57
+ { value: "minimal", hint: "a single page, zero islands" },
58
+ ];
59
+
60
+ // --- パッケージマネージャ検出(`npm create` の user-agent から)---
61
+ function detectPM() {
62
+ const ua = env.npm_config_user_agent || "";
63
+ if (ua.startsWith("pnpm")) return "pnpm";
64
+ if (ua.startsWith("yarn")) return "yarn";
65
+ if (ua.startsWith("bun")) return "bun";
66
+ return "npm";
67
+ }
5
68
 
6
- const name = process.argv[2] ?? "nowaki-app";
69
+ // --- バナー ---
70
+ console.log();
71
+ console.log(` ${cyan(bold("Nowaki"))} ${dim("野分")} ${dim("· create a new app")}`);
72
+ console.log();
73
+
74
+ const rl = interactive ? createInterface({ input: stdin, output: stdout }) : null;
75
+ async function ask(question, def) {
76
+ if (!rl) return def;
77
+ const a = (await rl.question(` ${question} ${dim(`(${def})`)} `)).trim();
78
+ return a || def;
79
+ }
80
+ async function confirm(question, def = true) {
81
+ if (!rl) return def;
82
+ const a = (await rl.question(` ${question} ${dim(def ? "(Y/n)" : "(y/N)")} `)).trim().toLowerCase();
83
+ if (!a) return def;
84
+ return a === "y" || a === "yes";
85
+ }
86
+ // 番号 or 名前で選ぶ簡易メニュー(依存なし)。
87
+ async function select(question, options, def) {
88
+ if (!rl) return def;
89
+ console.log(` ${question}`);
90
+ options.forEach((o, i) => {
91
+ const d = o.value === def ? dim(" (default)") : "";
92
+ console.log(` ${dim(`${i + 1})`)} ${bold(o.value)}${d} ${dim(o.hint ?? "")}`);
93
+ });
94
+ const a = (await rl.question(` ${dim(`(${def})`)} › `)).trim().toLowerCase();
95
+ if (!a) return def;
96
+ const byNum = options[Number.parseInt(a, 10) - 1];
97
+ if (byNum) return byNum.value;
98
+ return options.some((o) => o.value === a) ? a : def;
99
+ }
100
+
101
+ // --- 質問 ---
102
+ let name = positionals[0] || (await ask("Project name?", interactive ? "my-app" : "nowaki-app"));
103
+ name = name.trim() || "nowaki-app";
104
+ // package.json の name は valid な npm 名に正規化(ディレクトリ名は入力のまま)
105
+ const pkgName = name.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+/, "") || "nowaki-app";
7
106
  const dest = path.resolve(process.cwd(), name);
8
- const templateDir = path.join(
9
- path.dirname(fileURLToPath(import.meta.url)),
10
- "template",
11
- );
12
107
 
13
- // 既存の空でないディレクトリへの上書きは防ぐ
108
+ // 既存の空でないディレクトリは中止
14
109
  try {
15
110
  const existing = await readdir(dest);
16
111
  if (existing.length > 0) {
17
- console.error(`✗ ディレクトリ "${name}" は空ではありません。中止します。`);
112
+ console.error(`\n ${red("✗")} "${name}" already exists and is not empty. Aborting.\n`);
113
+ rl?.close();
18
114
  process.exit(1);
19
115
  }
20
116
  } catch {
21
- // 存在しなければOK
117
+ // 無ければOK
118
+ }
119
+
120
+ // テンプレート選択(-t/--template or 対話メニュー、既定 basics)。
121
+ let template = templateFlag || (interactive ? await select("Template?", TEMPLATES, "basics") : "basics");
122
+ template = String(template).toLowerCase();
123
+ if (!TEMPLATES.some((t) => t.value === template)) {
124
+ console.error(
125
+ `\n ${red("✗")} unknown template "${template}" (choose: ${TEMPLATES.map((t) => t.value).join(", ")}).\n`,
126
+ );
127
+ rl?.close();
128
+ process.exit(1);
22
129
  }
23
130
 
131
+ let pm = pmFlag || detectPM();
132
+ if (interactive && !pmFlag) pm = (await ask("Package manager?", pm)).trim() || pm;
133
+ if (!["npm", "pnpm", "yarn", "bun"].includes(pm)) pm = "npm";
134
+ const doGit = noGit ? false : await confirm("Initialize a git repository?", false);
135
+ // 依存インストール: 対話なら確認(既定 Yes)。非対話(-y / CI / パイプ)は自動実行しない
136
+ //(驚き防止)。非対話で入れたいときは --install。--no-install は常にスキップ。
137
+ const doInstall = noInstall
138
+ ? false
139
+ : forceInstall
140
+ ? true
141
+ : interactive
142
+ ? await confirm(`Install dependencies with ${pm}?`, true)
143
+ : false;
144
+ rl?.close();
145
+
146
+ // --- 生成 ---
147
+ // 共通設定(_shared)→ 選択テンプレートの順に重ねる。
24
148
  await mkdir(dest, { recursive: true });
25
- await cp(templateDir, dest, { recursive: true });
149
+ await cp(path.join(templatesDir, "_shared"), dest, { recursive: true });
150
+ await cp(path.join(templatesDir, template), dest, { recursive: true });
151
+
152
+ // npm は公開時に .gitignore を除外するので、テンプレートでは _gitignore で持ち、ここで戻す。
153
+ try {
154
+ await rename(path.join(dest, "_gitignore"), path.join(dest, ".gitignore"));
155
+ } catch {
156
+ // _gitignore が無ければ素通し
157
+ }
158
+
159
+ // package.json の name を設定
160
+ try {
161
+ const pkgPath = path.join(dest, "package.json");
162
+ const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
163
+ pkg.name = pkgName;
164
+ await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
165
+ } catch {
166
+ // package.json が無ければ素通し
167
+ }
168
+
169
+ console.log(
170
+ `\n ${green("✓")} created ${bold(name)} ${dim(`(${template} template · ${path.relative(process.cwd(), dest) || "."})`)}`,
171
+ );
172
+
173
+ // git init(任意)
174
+ if (doGit) {
175
+ try {
176
+ execFileSync("git", ["init", "-q"], { cwd: dest, stdio: "ignore" });
177
+ console.log(` ${green("✓")} initialized a git repository`);
178
+ } catch {
179
+ console.log(` ${yellow("!")} skipped git init (git not found)`);
180
+ }
181
+ }
182
+
183
+ // 依存インストール(任意)
184
+ let installed = false;
185
+ if (doInstall) {
186
+ console.log(`\n ${dim(`installing dependencies with ${pm}…`)}\n`);
187
+ try {
188
+ execFileSync(pm, ["install"], { cwd: dest, stdio: "inherit", shell: platform === "win32" });
189
+ installed = true;
190
+ console.log(`\n ${green("✓")} dependencies installed`);
191
+ } catch {
192
+ console.log(`\n ${red("✗")} \`${pm} install\` failed — run it yourself after \`cd ${name}\`.`);
193
+ }
194
+ }
26
195
 
27
- console.log(`\n🌀 Nowaki アプリを作成しました: ${name}\n`);
28
- console.log("次のステップ:");
29
- console.log(` cd ${name}`);
30
- console.log(" pnpm install # nowaki CLI とランタイムを取得 (Rust 不要)");
31
- console.log(" pnpm dev # → http://127.0.0.1:3000\n");
196
+ // --- 次のステップ ---
197
+ const runDev = pm === "npm" || pm === "bun" ? `${pm} run dev` : `${pm} dev`;
198
+ console.log(`\n ${bold("Next steps:")}`);
199
+ console.log(` cd ${name}`);
200
+ if (!installed) console.log(` ${pm} install`);
201
+ console.log(` ${cyan(runDev)} ${dim("# → http://localhost:3000")}`);
202
+ console.log(`\n ${dim("Docs:")} ${cyan("https://nowaki.dev/docs")}\n`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-nowaki",
3
- "version": "0.9.1",
3
+ "version": "0.11.0",
4
4
  "description": "Scaffold a new Nowaki app",
5
5
  "license": "MIT",
6
6
  "author": "VorEdge <dev@voredge.com>",
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "files": [
26
26
  "index.js",
27
- "template"
27
+ "templates"
28
28
  ],
29
29
  "engines": {
30
30
  "node": ">=22"
@@ -0,0 +1,36 @@
1
+ # Nowaki app
2
+
3
+ A full-stack app built with [Nowaki](https://nowaki.dev) — full-stack like
4
+ Next.js, islands like Astro, on a Rust toolchain. Pages render to HTML; only the
5
+ components under `islands/` ship JavaScript and hydrate.
6
+
7
+ ## Develop
8
+
9
+ ```bash
10
+ npm install
11
+ npm run dev # http://localhost:3000
12
+ ```
13
+
14
+ Other scripts: `npm run build`, `npm run start`, `npm run prerender`.
15
+ Pass `--host` to expose dev on your LAN, `--open` to open the browser:
16
+ `npm run dev -- --host --open`.
17
+
18
+ ## Structure
19
+
20
+ ```
21
+ routes/ pages + API (file-based). _layout.tsx, _middleware.ts, [slug].tsx, api/*.ts
22
+ islands/ interactive components — the only things that ship JS
23
+ components/ shared server components (no client JS)
24
+ lib/ shared server code
25
+ actions/ "use server" RPC modules (optional)
26
+ ```
27
+
28
+ ## Notes
29
+
30
+ - Write **Preact** — import hooks from `preact/hooks`. (`react` is aliased to
31
+ `preact/compat`, so many React libraries work.)
32
+ - Use **explicit file extensions** in relative imports: `../islands/Counter.tsx`.
33
+ - A route's `loader` runs only on the server; `action` handles non-GET requests.
34
+ - `AGENTS.md` documents the conventions for AI coding agents.
35
+
36
+ Docs: <https://nowaki.dev/docs>
@@ -0,0 +1,12 @@
1
+ node_modules/
2
+ dist/
3
+ .cache/
4
+
5
+ # env (keep secrets out of git; only PUBLIC_* reaches the client)
6
+ .env
7
+ .env.local
8
+ .env.*.local
9
+
10
+ # logs / os
11
+ *.log
12
+ .DS_Store
@@ -0,0 +1,42 @@
1
+ // Nowaki アンビエント型。CSS / アセット / 仮想モジュールの import をエディタが理解できるように。
2
+ // 編集不要(フレームワークの import 規約を型に伝えるだけ)。
3
+
4
+ declare module "*.module.css" {
5
+ const classes: Record<string, string>;
6
+ export default classes;
7
+ }
8
+ declare module "*.css" {
9
+ const css: string;
10
+ export default css;
11
+ }
12
+
13
+ // アセット import は配信 URL(文字列)になる。
14
+ declare module "*.svg" { const url: string; export default url; }
15
+ declare module "*.png" { const url: string; export default url; }
16
+ declare module "*.jpg" { const url: string; export default url; }
17
+ declare module "*.jpeg" { const url: string; export default url; }
18
+ declare module "*.gif" { const url: string; export default url; }
19
+ declare module "*.webp" { const url: string; export default url; }
20
+ declare module "*.avif" { const url: string; export default url; }
21
+ declare module "*.ico" { const url: string; export default url; }
22
+ declare module "*.woff" { const url: string; export default url; }
23
+ declare module "*.woff2" { const url: string; export default url; }
24
+ declare module "*.ttf" { const url: string; export default url; }
25
+ declare module "*.otf" { const url: string; export default url; }
26
+ declare module "*.mp4" { const url: string; export default url; }
27
+ declare module "*.webm" { const url: string; export default url; }
28
+ declare module "*.mp3" { const url: string; export default url; }
29
+ declare module "*.wav" { const url: string; export default url; }
30
+ declare module "*.pdf" { const url: string; export default url; }
31
+
32
+ // プラグインの仮想モジュール(nowaki.config の resolveId/load)。任意の export を許す。
33
+ declare module "virtual:*";
34
+
35
+ // import.meta.env(PUBLIC_* と MODE がビルド時に inline される)。
36
+ interface ImportMetaEnv {
37
+ readonly MODE: string;
38
+ readonly [key: `PUBLIC_${string}`]: string | undefined;
39
+ }
40
+ interface ImportMeta {
41
+ readonly env: ImportMetaEnv;
42
+ }
@@ -14,6 +14,6 @@
14
14
  "preact-render-to-string": "^6.5.13"
15
15
  },
16
16
  "devDependencies": {
17
- "nowaki": "^0.9.0"
17
+ "nowaki": "^0.10.0"
18
18
  }
19
19
  }
@@ -0,0 +1,33 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ESNext",
4
+ "module": "ESNext",
5
+ "moduleResolution": "bundler",
6
+ "allowImportingTsExtensions": true,
7
+ "verbatimModuleSyntax": false,
8
+ "noEmit": true,
9
+ "isolatedModules": true,
10
+ "esModuleInterop": true,
11
+ "resolveJsonModule": true,
12
+ "skipLibCheck": true,
13
+ "strict": true,
14
+ "lib": ["ESNext", "DOM", "DOM.Iterable"],
15
+ "jsx": "react-jsx",
16
+ "jsxImportSource": "preact",
17
+ "types": [],
18
+ "paths": {
19
+ "react": ["./node_modules/preact/compat"],
20
+ "react-dom": ["./node_modules/preact/compat"],
21
+ "react/jsx-runtime": ["./node_modules/preact/jsx-runtime"]
22
+ }
23
+ },
24
+ "include": [
25
+ "nowaki-env.d.ts",
26
+ "nowaki.config.*",
27
+ "routes",
28
+ "islands",
29
+ "components",
30
+ "lib",
31
+ "actions"
32
+ ]
33
+ }
@@ -0,0 +1,17 @@
1
+ import { useState } from "preact/hooks";
2
+
3
+ // クライアントでハイドレートする唯一のコンポーネント(島)。スタイルは routes/index.tsx の <style> に。
4
+ export default function Counter({ start = 0 }: { start?: number }) {
5
+ const [count, setCount] = useState(start);
6
+ return (
7
+ <div class="counter">
8
+ <button class="counter__btn" type="button" aria-label="decrement" onClick={() => setCount((c) => c - 1)}>
9
+
10
+ </button>
11
+ <strong class="counter__value">{count}</strong>
12
+ <button class="counter__btn" type="button" aria-label="increment" onClick={() => setCount((c) => c + 1)}>
13
+ +
14
+ </button>
15
+ </div>
16
+ );
17
+ }
@@ -0,0 +1,73 @@
1
+ import Counter from "../islands/Counter.tsx";
2
+
3
+ const CSS = `
4
+ :root{--ink:#10151c;--muted:#56616f;--line:#e7e9ee;--bg:#fbfcfd;--cyan:#0e7c86;--cyan-soft:#e9f4f5}
5
+ *{box-sizing:border-box}
6
+ body{margin:0;color:var(--ink);background:var(--bg);font:16px/1.65 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-font-smoothing:antialiased;background-image:radial-gradient(70% 55% at 50% -8%, #eaf5f6 0%, transparent 70%)}
7
+ .wrap{max-width:46rem;margin:0 auto;padding:clamp(3.5rem,8vw,7rem) 1.5rem}
8
+ .brand{display:inline-flex;align-items:baseline;gap:.5rem;font-weight:800;letter-spacing:-0.03em;font-size:1.15rem}
9
+ .brand small{font-weight:500;color:var(--muted);font-size:.8rem}
10
+ .badge{display:inline-block;margin-top:2.2rem;font-size:.72rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--cyan);background:var(--cyan-soft);border:1px solid #cfe7e9;border-radius:999px;padding:.25rem .7rem}
11
+ h1{font-size:clamp(2.1rem,1.3rem + 3vw,3rem);letter-spacing:-0.035em;line-height:1.05;margin:1rem 0 0}
12
+ .lead{color:var(--muted);font-size:1.15rem;margin:.9rem 0 0;max-width:34rem}
13
+ .card{margin-top:2.2rem;border:1px solid var(--line);border-radius:16px;background:#fff;padding:1.4rem 1.6rem;box-shadow:0 1px 2px rgba(16,21,28,.04),0 14px 34px -20px rgba(16,21,28,.22)}
14
+ .card__label{font-size:.72rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}
15
+ .counter{margin-top:.9rem;display:inline-flex;align-items:center;gap:1rem}
16
+ .counter__btn{width:2.4rem;height:2.4rem;border-radius:10px;border:1px solid var(--line);background:#fff;font-size:1.3rem;line-height:1;cursor:pointer;color:var(--ink);transition:border-color .15s,color .15s,background .15s}
17
+ .counter__btn:hover{border-color:var(--cyan);color:var(--cyan);background:var(--cyan-soft)}
18
+ .counter__value{font-size:1.5rem;font-variant-numeric:tabular-nums;min-width:2.6rem;text-align:center}
19
+ .note{margin:.9rem 0 0;color:var(--muted);font-size:.92rem}
20
+ .grid{margin-top:2.4rem;display:grid;gap:.8rem;grid-template-columns:1fr 1fr}
21
+ .tile{border:1px solid var(--line);border-radius:12px;padding:.85rem 1rem;background:#fff}
22
+ .tile code{font:.85rem ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--cyan)}
23
+ .tile span{display:block;color:var(--muted);font-size:.85rem;margin-top:.25rem}
24
+ .foot{margin-top:2.6rem;color:var(--muted);font-size:.92rem}
25
+ a{color:var(--cyan);text-decoration:none;font-weight:500}
26
+ a:hover{text-decoration:underline}
27
+ @media(max-width:520px){.grid{grid-template-columns:1fr}}
28
+ `;
29
+
30
+ export const title = "Nowaki app";
31
+ export const head = `<style>${CSS}</style>`;
32
+
33
+ export const loader = async () => ({ greeting: "Welcome to Nowaki" });
34
+
35
+ type Data = Awaited<ReturnType<typeof loader>>;
36
+
37
+ export default function Home({ data }: { data: Data }) {
38
+ return (
39
+ <main class="wrap">
40
+ <div class="brand">
41
+ Nowaki <small>野分</small>
42
+ </div>
43
+
44
+ <span class="badge">basics template</span>
45
+ <h1>{data.greeting} 🌀</h1>
46
+ <p class="lead">
47
+ Full-stack, yet zero JavaScript by default. The counter below is the only{" "}
48
+ <strong>island</strong> that hydrates on the client.
49
+ </p>
50
+
51
+ <div class="card">
52
+ <div class="card__label">island · the only JS on this page</div>
53
+ <Counter start={0} />
54
+ <p class="note">Everything else is server-rendered HTML.</p>
55
+ </div>
56
+
57
+ <div class="grid">
58
+ <div class="tile">
59
+ <code>routes/index.tsx</code>
60
+ <span>This page. Edit it to get started.</span>
61
+ </div>
62
+ <div class="tile">
63
+ <code>islands/Counter.tsx</code>
64
+ <span>The interactive island that hydrates.</span>
65
+ </div>
66
+ </div>
67
+
68
+ <p class="foot">
69
+ Read the docs → <a href="https://nowaki.dev/docs">nowaki.dev/docs</a>
70
+ </p>
71
+ </main>
72
+ );
73
+ }
@@ -0,0 +1,42 @@
1
+ const CSS = `
2
+ :root{--ink:#10151c;--muted:#56616f;--line:#e7e9ee;--bg:#fbfcfd;--cyan:#0e7c86;--cyan-soft:#e9f4f5}
3
+ *{box-sizing:border-box}
4
+ body{margin:0;color:var(--ink);background:var(--bg);font:16px/1.65 ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;-webkit-font-smoothing:antialiased;background-image:radial-gradient(70% 55% at 50% -8%, #eaf5f6 0%, transparent 70%)}
5
+ .wrap{max-width:42rem;margin:0 auto;padding:clamp(4rem,10vw,8rem) 1.5rem}
6
+ .brand{display:inline-flex;align-items:baseline;gap:.5rem;font-weight:800;letter-spacing:-0.03em;font-size:1.15rem}
7
+ .brand small{font-weight:500;color:var(--muted);font-size:.8rem}
8
+ .badge{display:inline-block;margin-top:2.2rem;font-size:.72rem;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--cyan);background:var(--cyan-soft);border:1px solid #cfe7e9;border-radius:999px;padding:.25rem .7rem}
9
+ h1{font-size:clamp(2.1rem,1.3rem + 3vw,3rem);letter-spacing:-0.035em;line-height:1.05;margin:1rem 0 0}
10
+ .lead{color:var(--muted);font-size:1.15rem;margin:.9rem 0 0;max-width:32rem}
11
+ p code{font:.9em ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--cyan);background:#eef2f3;padding:.1em .4em;border-radius:5px}
12
+ .foot{margin-top:2.6rem;color:var(--muted);font-size:.92rem}
13
+ a{color:var(--cyan);text-decoration:none;font-weight:500}
14
+ a:hover{text-decoration:underline}
15
+ `;
16
+
17
+ export const title = "Nowaki";
18
+ export const head = `<style>${CSS}</style>`;
19
+
20
+ export default function Home() {
21
+ return (
22
+ <main class="wrap">
23
+ <div class="brand">
24
+ Nowaki <small>野分</small>
25
+ </div>
26
+
27
+ <span class="badge">minimal template</span>
28
+ <h1>Hello, Nowaki 🌀</h1>
29
+ <p class="lead">
30
+ This page is server-rendered HTML and ships <strong>zero JavaScript</strong>.
31
+ </p>
32
+ <p style="color:var(--muted);margin-top:1rem">
33
+ Edit <code>routes/index.tsx</code> to get started. Add interactive components under{" "}
34
+ <code>islands/</code> when you need them.
35
+ </p>
36
+
37
+ <p class="foot">
38
+ Read the docs → <a href="https://nowaki.dev/docs">nowaki.dev/docs</a>
39
+ </p>
40
+ </main>
41
+ );
42
+ }
@@ -1,12 +0,0 @@
1
- import { useState } from "preact/hooks";
2
-
3
- export default function Counter({ start = 0 }: { start?: number }) {
4
- const [count, setCount] = useState(start);
5
- return (
6
- <div style="border:1px solid #ccc;padding:1rem;border-radius:8px;display:inline-flex;align-items:center;gap:1rem">
7
- <button onClick={() => setCount((c) => c - 1)}>-</button>
8
- <strong>{count}</strong>
9
- <button onClick={() => setCount((c) => c + 1)}>+</button>
10
- </div>
11
- );
12
- }
@@ -1,19 +0,0 @@
1
- import Counter from "../islands/Counter.tsx";
2
-
3
- export const title = "Nowaki App";
4
-
5
- export const loader = async () => {
6
- return { message: "Nowaki へようこそ 🌀" };
7
- };
8
-
9
- type Data = Awaited<ReturnType<typeof loader>>;
10
-
11
- export default function Home({ data }: { data: Data }) {
12
- return (
13
- <main style="font-family:sans-serif;max-width:640px;margin:4rem auto">
14
- <h1>{data.message}</h1>
15
- <p>このカウンターだけがクライアントでハイドレートされる島です。</p>
16
- <Counter start={0} />
17
- </main>
18
- );
19
- }
File without changes
File without changes