create-nowaki 0.10.0 → 0.12.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 +55 -10
- package/package.json +2 -2
- package/{template → templates/_shared}/AGENTS.md +19 -5
- package/{template → templates/_shared}/README.md +12 -0
- package/{template → templates/_shared}/package.json +2 -2
- package/templates/basics/islands/Counter.tsx +17 -0
- package/templates/basics/routes/index.tsx +72 -0
- package/templates/minimal/routes/index.tsx +42 -0
- package/template/islands/Counter.tsx +0 -12
- package/template/routes/index.tsx +0 -19
- /package/{template → templates/_shared}/CLAUDE.md +0 -0
- /package/{template → templates/_shared}/_gitignore +0 -0
- /package/{template → templates/_shared}/nowaki-env.d.ts +0 -0
- /package/{template → templates/_shared}/tsconfig.json +0 -0
package/index.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
// 対話ウィザード(TTY 時): プロジェクト名・パッケージマネージャ・git・依存インストール。
|
|
4
4
|
// 非対話(CI / `-y` / パイプ)では既定値で黙って生成する。
|
|
5
5
|
//
|
|
6
|
-
// フラグ: -y/--yes(質問を飛ばす),
|
|
7
|
-
// --no-git, --pm <npm|pnpm|yarn|bun>
|
|
6
|
+
// フラグ: -t/--template <basics|minimal>, -y/--yes(質問を飛ばす),
|
|
7
|
+
// --install(非対話でも入れる), --no-install, --no-git, --pm <npm|pnpm|yarn|bun>
|
|
8
8
|
|
|
9
9
|
import { cp, mkdir, readdir, readFile, writeFile, rename } from "node:fs/promises";
|
|
10
10
|
import path from "node:path";
|
|
@@ -27,19 +27,35 @@ const yellow = wrap("33");
|
|
|
27
27
|
|
|
28
28
|
// --- 引数 ---
|
|
29
29
|
const args = argv.slice(2);
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const
|
|
33
|
-
const i = args.
|
|
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
34
|
return i >= 0 ? args[i + 1] : null;
|
|
35
|
-
}
|
|
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");
|
|
36
47
|
const yes = flags.has("-y") || flags.has("--yes");
|
|
37
48
|
const noInstall = flags.has("--no-install");
|
|
38
49
|
const forceInstall = flags.has("--install");
|
|
39
50
|
const noGit = flags.has("--no-git");
|
|
40
51
|
const interactive = !!stdin.isTTY && !!stdout.isTTY && !yes;
|
|
41
52
|
|
|
42
|
-
const
|
|
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
|
+
];
|
|
43
59
|
|
|
44
60
|
// --- パッケージマネージャ検出(`npm create` の user-agent から)---
|
|
45
61
|
function detectPM() {
|
|
@@ -67,6 +83,20 @@ async function confirm(question, def = true) {
|
|
|
67
83
|
if (!a) return def;
|
|
68
84
|
return a === "y" || a === "yes";
|
|
69
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
|
+
}
|
|
70
100
|
|
|
71
101
|
// --- 質問 ---
|
|
72
102
|
let name = positionals[0] || (await ask("Project name?", interactive ? "my-app" : "nowaki-app"));
|
|
@@ -87,6 +117,17 @@ try {
|
|
|
87
117
|
// 無ければOK
|
|
88
118
|
}
|
|
89
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);
|
|
129
|
+
}
|
|
130
|
+
|
|
90
131
|
let pm = pmFlag || detectPM();
|
|
91
132
|
if (interactive && !pmFlag) pm = (await ask("Package manager?", pm)).trim() || pm;
|
|
92
133
|
if (!["npm", "pnpm", "yarn", "bun"].includes(pm)) pm = "npm";
|
|
@@ -103,8 +144,10 @@ const doInstall = noInstall
|
|
|
103
144
|
rl?.close();
|
|
104
145
|
|
|
105
146
|
// --- 生成 ---
|
|
147
|
+
// 共通設定(_shared)→ 選択テンプレートの順に重ねる。
|
|
106
148
|
await mkdir(dest, { recursive: true });
|
|
107
|
-
await cp(
|
|
149
|
+
await cp(path.join(templatesDir, "_shared"), dest, { recursive: true });
|
|
150
|
+
await cp(path.join(templatesDir, template), dest, { recursive: true });
|
|
108
151
|
|
|
109
152
|
// npm は公開時に .gitignore を除外するので、テンプレートでは _gitignore で持ち、ここで戻す。
|
|
110
153
|
try {
|
|
@@ -123,7 +166,9 @@ try {
|
|
|
123
166
|
// package.json が無ければ素通し
|
|
124
167
|
}
|
|
125
168
|
|
|
126
|
-
console.log(
|
|
169
|
+
console.log(
|
|
170
|
+
`\n ${green("✓")} created ${bold(name)} ${dim(`(${template} template · ${path.relative(process.cwd(), dest) || "."})`)}`,
|
|
171
|
+
);
|
|
127
172
|
|
|
128
173
|
// git init(任意)
|
|
129
174
|
if (doGit) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-nowaki",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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
|
-
"
|
|
27
|
+
"templates"
|
|
28
28
|
],
|
|
29
29
|
"engines": {
|
|
30
30
|
"node": ">=22"
|
|
@@ -12,6 +12,7 @@ npm run dev # nowaki dev — dev server with hot reload (http://localho
|
|
|
12
12
|
npm run build # nowaki build — production build (dist/client + dist/server)
|
|
13
13
|
npm run start # nowaki start — serve the production build
|
|
14
14
|
npm run prerender # nowaki prerender — static export
|
|
15
|
+
npx nowaki upgrade # update nowaki (CLI) + @nowaki-dev/runtime to latest
|
|
15
16
|
```
|
|
16
17
|
|
|
17
18
|
## Project layout
|
|
@@ -38,16 +39,23 @@ nowaki.config.mjs plugins (optional)
|
|
|
38
39
|
`onClick`/hooks in route or `components/` files.
|
|
39
40
|
3. **Use explicit file extensions in relative imports**:
|
|
40
41
|
`import Counter from "../islands/Counter.tsx"` (not `"../islands/Counter"`).
|
|
41
|
-
4. **Routes** are components with an optional server-only `loader` and `action
|
|
42
|
+
4. **Routes** are components with an optional server-only `loader` and `action`.
|
|
43
|
+
Optional types come from `@nowaki-dev/runtime` — `PageProps<typeof loader>`
|
|
44
|
+
infers the page's `data`, `LoaderContext` types `params`/cookies/headers:
|
|
42
45
|
```tsx
|
|
43
46
|
// routes/blog/[slug].tsx
|
|
44
|
-
|
|
45
|
-
export async
|
|
47
|
+
import type { LoaderContext, PageProps } from "@nowaki-dev/runtime";
|
|
48
|
+
export const loader = async ({ params }: LoaderContext) => ({ post: await db.post(params.slug) }); // server-only
|
|
49
|
+
export async function action(ctx: LoaderContext) { // runs on non-GET requests
|
|
46
50
|
const form = await ctx.formData();
|
|
47
51
|
return ctx.redirect("/blog"); // Post/Redirect/Get
|
|
48
52
|
}
|
|
49
|
-
export default function Post({ data }) { return <article><h1>{data.post.title}</h1></article>; }
|
|
53
|
+
export default function Post({ data }: PageProps<typeof loader>) { return <article><h1>{data.post.title}</h1></article>; }
|
|
50
54
|
```
|
|
55
|
+
A catch-all segment `routes/files/[...path].tsx` matches `/files/a/b/c`;
|
|
56
|
+
`params.path` is the array `["a","b","c"]`. Add `export const revalidate = 60`
|
|
57
|
+
(seconds) to a route for ISR: the HTML is cached and regenerated in the
|
|
58
|
+
background when stale (don't put per-user data on an ISR page).
|
|
51
59
|
5. **API routes** are `routes/api/*.ts` with per-method exports; return a value
|
|
52
60
|
(JSON-encoded) or a `Response`:
|
|
53
61
|
```ts
|
|
@@ -69,7 +77,7 @@ nowaki.config.mjs plugins (optional)
|
|
|
69
77
|
```ts
|
|
70
78
|
// actions/todos.ts
|
|
71
79
|
"use server";
|
|
72
|
-
import { getContext } from "@nowaki-dev/runtime
|
|
80
|
+
import { getContext } from "@nowaki-dev/runtime";
|
|
73
81
|
export async function addTodo(text: string) { /* runs on the server */ }
|
|
74
82
|
```
|
|
75
83
|
Call it from an island like a normal async function:
|
|
@@ -77,6 +85,12 @@ nowaki.config.mjs plugins (optional)
|
|
|
77
85
|
8. **Jetstream islands** (server-reactive, zero client JS) — give an island
|
|
78
86
|
`export const live = { state, on }`; buttons use `data-live="handler"` instead
|
|
79
87
|
of `onClick`. State lives on the server; the server pushes HTML patches.
|
|
88
|
+
9. **Loading / error UI** (client navigation) — `routes/loading.tsx` renders
|
|
89
|
+
while a navigation is in flight; `routes/error.tsx` (props `{ error, reset }`,
|
|
90
|
+
type `ErrorPageProps`) renders if it fails. Both nest by directory. In
|
|
91
|
+
`error.tsx`, mark the message element `data-nowaki-error` and the retry button
|
|
92
|
+
`data-nowaki-reset`. Visited pages are kept in a ~30s Router Cache, so
|
|
93
|
+
back/forward is instant. These are server components, not islands.
|
|
80
94
|
|
|
81
95
|
## Don't
|
|
82
96
|
|
|
@@ -15,6 +15,18 @@ Other scripts: `npm run build`, `npm run start`, `npm run prerender`.
|
|
|
15
15
|
Pass `--host` to expose dev on your LAN, `--open` to open the browser:
|
|
16
16
|
`npm run dev -- --host --open`.
|
|
17
17
|
|
|
18
|
+
## Update
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx nowaki upgrade # bumps nowaki (CLI) and @nowaki-dev/runtime to latest
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`nowaki upgrade` detects your package manager and updates both packages,
|
|
25
|
+
rewriting the `package.json` ranges. (A plain `npm update` won't cross a
|
|
26
|
+
`0.x` minor, so prefer `nowaki upgrade` — or install `@latest` by hand.)
|
|
27
|
+
Pin a version with `--to`, e.g. `npx nowaki upgrade --to 0.11.0`. Pre-1.0,
|
|
28
|
+
check the release notes before upgrading; minors can include breaking changes.
|
|
29
|
+
|
|
18
30
|
## Structure
|
|
19
31
|
|
|
20
32
|
```
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
"prerender": "nowaki prerender"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"@nowaki-dev/runtime": "^0.
|
|
12
|
+
"@nowaki-dev/runtime": "^0.10.0",
|
|
13
13
|
"preact": "^10.25.4",
|
|
14
14
|
"preact-render-to-string": "^6.5.13"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
17
|
-
"nowaki": "^0.
|
|
17
|
+
"nowaki": "^0.11.0"
|
|
18
18
|
}
|
|
19
19
|
}
|
|
@@ -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,72 @@
|
|
|
1
|
+
import type { PageProps } from "@nowaki-dev/runtime";
|
|
2
|
+
import Counter from "../islands/Counter.tsx";
|
|
3
|
+
|
|
4
|
+
const CSS = `
|
|
5
|
+
:root{--ink:#10151c;--muted:#56616f;--line:#e7e9ee;--bg:#fbfcfd;--cyan:#0e7c86;--cyan-soft:#e9f4f5}
|
|
6
|
+
*{box-sizing:border-box}
|
|
7
|
+
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%)}
|
|
8
|
+
.wrap{max-width:46rem;margin:0 auto;padding:clamp(3.5rem,8vw,7rem) 1.5rem}
|
|
9
|
+
.brand{display:inline-flex;align-items:baseline;gap:.5rem;font-weight:800;letter-spacing:-0.03em;font-size:1.15rem}
|
|
10
|
+
.brand small{font-weight:500;color:var(--muted);font-size:.8rem}
|
|
11
|
+
.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}
|
|
12
|
+
h1{font-size:clamp(2.1rem,1.3rem + 3vw,3rem);letter-spacing:-0.035em;line-height:1.05;margin:1rem 0 0}
|
|
13
|
+
.lead{color:var(--muted);font-size:1.15rem;margin:.9rem 0 0;max-width:34rem}
|
|
14
|
+
.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)}
|
|
15
|
+
.card__label{font-size:.72rem;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.06em}
|
|
16
|
+
.counter{margin-top:.9rem;display:inline-flex;align-items:center;gap:1rem}
|
|
17
|
+
.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}
|
|
18
|
+
.counter__btn:hover{border-color:var(--cyan);color:var(--cyan);background:var(--cyan-soft)}
|
|
19
|
+
.counter__value{font-size:1.5rem;font-variant-numeric:tabular-nums;min-width:2.6rem;text-align:center}
|
|
20
|
+
.note{margin:.9rem 0 0;color:var(--muted);font-size:.92rem}
|
|
21
|
+
.grid{margin-top:2.4rem;display:grid;gap:.8rem;grid-template-columns:1fr 1fr}
|
|
22
|
+
.tile{border:1px solid var(--line);border-radius:12px;padding:.85rem 1rem;background:#fff}
|
|
23
|
+
.tile code{font:.85rem ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--cyan)}
|
|
24
|
+
.tile span{display:block;color:var(--muted);font-size:.85rem;margin-top:.25rem}
|
|
25
|
+
.foot{margin-top:2.6rem;color:var(--muted);font-size:.92rem}
|
|
26
|
+
a{color:var(--cyan);text-decoration:none;font-weight:500}
|
|
27
|
+
a:hover{text-decoration:underline}
|
|
28
|
+
@media(max-width:520px){.grid{grid-template-columns:1fr}}
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export const title = "Nowaki app";
|
|
32
|
+
export const head = `<style>${CSS}</style>`;
|
|
33
|
+
|
|
34
|
+
export const loader = async () => ({ greeting: "Welcome to Nowaki" });
|
|
35
|
+
|
|
36
|
+
export default function Home({ data }: PageProps<typeof loader>) {
|
|
37
|
+
return (
|
|
38
|
+
<main class="wrap">
|
|
39
|
+
<div class="brand">
|
|
40
|
+
Nowaki <small>野分</small>
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
<span class="badge">basics template</span>
|
|
44
|
+
<h1>{data.greeting} 🌀</h1>
|
|
45
|
+
<p class="lead">
|
|
46
|
+
Full-stack, yet zero JavaScript by default. The counter below is the only{" "}
|
|
47
|
+
<strong>island</strong> that hydrates on the client.
|
|
48
|
+
</p>
|
|
49
|
+
|
|
50
|
+
<div class="card">
|
|
51
|
+
<div class="card__label">island · the only JS on this page</div>
|
|
52
|
+
<Counter start={0} />
|
|
53
|
+
<p class="note">Everything else is server-rendered HTML.</p>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="grid">
|
|
57
|
+
<div class="tile">
|
|
58
|
+
<code>routes/index.tsx</code>
|
|
59
|
+
<span>This page. Edit it to get started.</span>
|
|
60
|
+
</div>
|
|
61
|
+
<div class="tile">
|
|
62
|
+
<code>islands/Counter.tsx</code>
|
|
63
|
+
<span>The interactive island that hydrates.</span>
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<p class="foot">
|
|
68
|
+
Read the docs → <a href="https://nowaki.dev/docs">nowaki.dev/docs</a>
|
|
69
|
+
</p>
|
|
70
|
+
</main>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|