create-shibumi 0.2.9 → 0.3.2

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.
@@ -1,422 +0,0 @@
1
- /* shibumi.css — canonical shared layer for shibumistack.dev, shibumi-server, shibumi-forms.
2
- Source of truth: shibumistack.dev repo public/shibumi.css. Do not edit vendored copies. */
3
-
4
- /* shared.css: common styles for all pages */
5
-
6
- :root {
7
- color-scheme: light dark;
8
- /* palette copied from shibumi-server site/styles.css */
9
- --paper: light-dark(#f5f0e4, #1b130f);
10
- --ink: light-dark(#272016, #eee5d7);
11
- --muted: light-dark(#746b5d, #aaa092);
12
- --faint: light-dark(rgba(39, 32, 22, 0.055), rgba(238, 229, 215, 0.07));
13
- --line: light-dark(rgba(39, 32, 22, 0.14), rgba(238, 229, 215, 0.16));
14
- --orange: light-dark(#e95f19, #ff8648);
15
- --orange-soft: light-dark(#f8dfcd, #3c2117);
16
- --green: light-dark(#26723e, #75d58a);
17
- --danger: light-dark(#a13b2c, #e36c59);
18
- --code: light-dark(#201c16, #120e0b);
19
- --code-ink: #e9e2d5;
20
- /* aliases so existing components pick up the server palette */
21
- --bg: var(--paper);
22
- --text: var(--ink);
23
- --quiet: var(--faint);
24
- --accent: var(--orange);
25
- --accent-soft: var(--orange-soft);
26
- --accent-hover: light-dark(#d15515, #ff9a66);
27
- --success: var(--green);
28
- --success-text: var(--green);
29
- --success-ink: #17331f;
30
- --terminal-success: light-dark(#3d8f50, #78e88f);
31
- --terminal-active: light-dark(#16839a, #62dff0);
32
- --terminal-info: light-dark(#397fc0, #74b6ff);
33
- --max: 72.5rem;
34
- --font-sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
35
- --font-serif: ui-serif, Georgia, "Iowan Old Style", "Apple Garamond", Cambria, serif;
36
- --font-mono: ui-monospace, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
37
- --text-xs: 0.75rem; /* 12px tiny */
38
- --text-sm: 0.875rem; /* 14px small */
39
- --text-md: 1rem; /* 16px normal / UI */
40
- --text-base: 1.125rem; /* 18px body */
41
- --text-lg: 1.3125rem; /* 21px */
42
- --text-xl: 1.563rem; /* 25px */
43
- --text-2xl: 1.953rem;
44
- }
45
-
46
- [data-theme="dark"] { color-scheme: dark; }
47
- [data-theme="light"] { color-scheme: light; }
48
-
49
- *, *::before, *::after { box-sizing: border-box; }
50
- html { min-height: 100%; }
51
-
52
- body {
53
- margin: 0;
54
- min-height: 100svh;
55
- font-family: var(--font-sans);
56
- font-size: var(--text-base);
57
- font-weight: 400;
58
- line-height: 1.65;
59
- color: var(--text);
60
- background: var(--bg);
61
- }
62
-
63
- /* Kozo paper texture, light theme only; dark stays flat ink. The image lives
64
- next to this stylesheet in every consumer, hence the relative URL. */
65
- body {
66
- background-image: url("kozo.webp");
67
- background-size: cover;
68
- background-position: center;
69
- background-attachment: fixed;
70
- background-repeat: no-repeat;
71
- }
72
- @media (prefers-color-scheme: dark) {
73
- body { background-image: none; }
74
- }
75
- [data-theme="light"] body { background-image: url("kozo.webp"); }
76
- [data-theme="dark"] body { background-image: none; }
77
-
78
- h1, h2, h3 {
79
- font-family: var(--font-serif);
80
- }
81
-
82
- a { color: inherit; }
83
-
84
- main a:not(.btn):not(.button) {
85
- text-decoration: underline;
86
- text-decoration-color: color-mix(in srgb, var(--accent) 30%, transparent);
87
- text-underline-offset: 0.18em;
88
- transition: text-decoration-color 160ms ease;
89
- }
90
-
91
- main a:not(.btn):not(.button):hover,
92
- main a:not(.btn):not(.button):focus-visible {
93
- text-decoration-color: var(--accent);
94
- }
95
-
96
- .shell {
97
- width: min(var(--max), calc(100% - 2rem));
98
- margin: 0 auto;
99
- padding: 5.5rem 0 0;
100
- position: relative;
101
- }
102
-
103
- /* ---- header / nav ---- */
104
-
105
- .shell > header,
106
- .site-header {
107
- position: fixed;
108
- top: 0;
109
- left: 50%;
110
- z-index: 20;
111
- width: min(var(--max), calc(100% - 2rem));
112
- display: flex;
113
- justify-content: space-between;
114
- align-items: center;
115
- gap: 1rem;
116
- padding: 1rem 0;
117
- transform: translateX(-50%);
118
- }
119
-
120
- .shell > header::before,
121
- .site-header::before {
122
- content: "";
123
- position: fixed;
124
- top: 0;
125
- left: 50%;
126
- z-index: -1;
127
- width: 100vw;
128
- height: 100%;
129
- transform: translateX(-50%);
130
- background: light-dark(rgb(245 240 228 / 0%), rgb(27 19 15 / 72%));
131
- backdrop-filter: blur(1.25rem);
132
- -webkit-backdrop-filter: blur(1.25rem);
133
- }
134
-
135
- .mark {
136
- display: inline-flex;
137
- align-items: center;
138
- gap: 0.75rem;
139
- color: var(--muted);
140
- text-decoration: none;
141
- font-family: var(--font-sans);
142
- font-size: 1.125rem;
143
- font-weight: 400;
144
- letter-spacing: 0.03rem;
145
- }
146
-
147
- .mark-tld {
148
- color: light-dark(rgba(92, 88, 82, 0.5), rgba(189, 183, 173, 0.62));
149
- font-weight: 300;
150
- }
151
-
152
- .mark img,
153
- img.mark-logo {
154
- width: 2.125rem;
155
- height: 2.125rem;
156
- border-radius: 0.5rem;
157
- }
158
-
159
- [data-theme="dark"] .mark .logo-light,
160
- [data-theme="light"] .mark .logo-dark { display: none; }
161
-
162
- main, .page { min-width: 0; }
163
-
164
- .eyebrow {
165
- margin: 0 0 1rem;
166
- color: var(--accent);
167
- font: 300 var(--text-md)/1.2 var(--font-mono);
168
- letter-spacing: 0.14em;
169
- text-transform: uppercase;
170
- }
171
-
172
- nav {
173
- display: flex;
174
- align-items: center;
175
- gap: 1rem;
176
- font-family: var(--font-sans);
177
- font-size: var(--text-md);
178
- color: var(--muted);
179
- }
180
-
181
- nav a {
182
- text-decoration: none;
183
- border-bottom: 1px solid transparent;
184
- }
185
-
186
- nav a:hover,
187
- nav a[aria-current="page"] {
188
- border-bottom-color: var(--accent);
189
- color: var(--text);
190
- }
191
-
192
- .nav-links {
193
- display: flex;
194
- align-items: center;
195
- gap: 1rem;
196
- }
197
-
198
- .nav-actions {
199
- display: flex;
200
- align-items: center;
201
- gap: 1rem;
202
- }
203
-
204
- .nav-icons {
205
- display: flex;
206
- align-items: center;
207
- gap: 0.75rem;
208
- }
209
-
210
- .github-link {
211
- display: inline-flex;
212
- align-items: center;
213
- border-bottom: 0;
214
- color: var(--muted);
215
- }
216
-
217
- .github-link svg {
218
- width: 1.05rem;
219
- height: 1.05rem;
220
- fill: currentColor;
221
- }
222
-
223
- .github-link:hover {
224
- color: var(--text);
225
- border-bottom: 0;
226
- }
227
-
228
- .stack-menu { position: relative; }
229
-
230
- .stack-menu summary {
231
- cursor: pointer;
232
- list-style: none;
233
- transition: color 160ms ease;
234
- }
235
-
236
- .stack-menu summary::-webkit-details-marker { display: none; }
237
- .stack-menu summary:hover,
238
- .stack-menu summary:focus-visible,
239
- .stack-menu[open] summary,
240
- .stack-menu:has(a[aria-current="page"]) summary { color: var(--text); }
241
-
242
- .stack-menu summary:focus-visible {
243
- border-radius: 0.2rem;
244
- outline: 2px solid var(--accent);
245
- outline-offset: 4px;
246
- }
247
-
248
- .stack-popover {
249
- position: absolute;
250
- top: calc(100% + 0.75rem);
251
- right: 0;
252
- z-index: 30;
253
- width: 14rem;
254
- padding: 0.45rem;
255
- border-radius: 0.5rem;
256
- background: light-dark(rgb(255 250 240 / 78%), rgb(48 35 29 / 72%));
257
- -webkit-backdrop-filter: blur(0.5rem);
258
- backdrop-filter: blur(0.5rem);
259
- box-shadow: 0 0.8rem 2.5rem light-dark(rgb(20 14 9 / 18%), rgb(0 0 0 / 58%));
260
- }
261
-
262
- .stack-popover a {
263
- display: block;
264
- padding: 0.65rem 0.75rem;
265
- border: 0;
266
- border-radius: 0.3rem;
267
- color: var(--muted);
268
- }
269
-
270
- .stack-popover a:hover,
271
- .stack-popover a:focus-visible {
272
- border: 0;
273
- outline: none;
274
- background: var(--quiet);
275
- color: var(--text);
276
- }
277
-
278
- .stack-popover a[aria-current="page"] { color: var(--accent); }
279
-
280
- /* first menu in nav (About) opens aligned to its left edge */
281
- .nav-links > .stack-menu:first-child .stack-popover { left: 0; right: auto; }
282
- .stack-popover span,
283
- .stack-popover small { display: block; }
284
- .stack-popover span { color: inherit; font-weight: 700; }
285
- .stack-popover small { margin-top: 0.1rem; color: var(--muted); font-size: var(--text-xs); }
286
-
287
- /* ---- theme toggle ---- */
288
-
289
- .theme-toggle {
290
- appearance: none;
291
- display: inline-grid;
292
- place-items: center;
293
- width: 1.7rem;
294
- height: 1.7rem;
295
- border: 0;
296
- background: transparent;
297
- color: var(--muted);
298
- cursor: pointer;
299
- padding: 0;
300
- }
301
-
302
- .theme-toggle:hover { color: var(--text); }
303
-
304
- .theme-toggle svg {
305
- width: 1.1rem;
306
- height: 1.1rem;
307
- fill: none;
308
- stroke: currentColor;
309
- stroke-width: 1.8;
310
- }
311
-
312
- [data-theme="dark"] .theme-toggle .icon-sun,
313
- [data-theme="light"] .theme-toggle .icon-moon { display: none; }
314
- [data-theme="dark"] .theme-toggle .icon-moon,
315
- [data-theme="light"] .theme-toggle .icon-sun { display: block; }
316
- /* alias var names used by server/forms stylesheets */
317
- :root {
318
- --sans: var(--font-sans);
319
- --serif: var(--font-serif);
320
- --mono: var(--font-mono);
321
- --fs-xs: var(--text-xs);
322
- --fs-sm: var(--text-sm);
323
- --fs-md: var(--text-md);
324
- --fs-base: var(--text-base);
325
- --fs-lg: var(--text-lg);
326
- }
327
-
328
- /* ---- footer ---- */
329
-
330
- .site-footer {
331
- margin-top: clamp(4rem, 8vw, 6rem);
332
- padding: 1.5rem 0;
333
- border-top: 1px solid color-mix(in srgb, var(--line) 50%, transparent);
334
- font-family: var(--font-sans);
335
- font-size: var(--text-sm);
336
- color: var(--muted);
337
- }
338
-
339
- .footer-line {
340
- display: flex;
341
- align-items: center;
342
- justify-content: space-between;
343
- gap: 1rem;
344
- }
345
-
346
- .footer-line nav {
347
- display: flex;
348
- align-items: center;
349
- gap: 1.25rem;
350
- font: inherit;
351
- }
352
-
353
- .site-footer .stack-popover {
354
- top: auto;
355
- right: auto;
356
- bottom: calc(100% + 0.75rem);
357
- left: 0;
358
- }
359
-
360
- .footer-meta {
361
- display: flex;
362
- align-items: center;
363
- gap: 0.3rem;
364
- margin: 0.75rem 0 0;
365
- font-size: var(--text-xs);
366
- opacity: 0.75;
367
- }
368
-
369
- .site-footer a {
370
- text-decoration: none;
371
- color: var(--muted);
372
- }
373
-
374
- .site-footer a:hover {
375
- color: var(--accent);
376
- }
377
-
378
- .footer-heart {
379
- width: 0.875rem;
380
- height: 0.875rem;
381
- fill: currentColor;
382
- color: var(--danger);
383
- opacity: 0.5;
384
- transition: opacity 150ms;
385
- }
386
-
387
- .footer-meta:hover .footer-heart {
388
- opacity: 1;
389
- animation: heart-pulse 1s ease-in-out infinite;
390
- }
391
-
392
- @keyframes heart-pulse {
393
- 0%, 100% { transform: scale(1); }
394
- 50% { transform: scale(1.3); }
395
- }
396
-
397
- .sr-only {
398
- position: absolute;
399
- width: 1px;
400
- height: 1px;
401
- margin: -1px;
402
- padding: 0;
403
- overflow: hidden;
404
- clip: rect(0 0 0 0);
405
- white-space: nowrap;
406
- border: 0;
407
- }
408
-
409
-
410
- /* green pulse on the box around a just-used copy button */
411
- @keyframes copy-glow {
412
- 0%, 100% { box-shadow: 0 0 0 0 transparent; }
413
- 35% {
414
- box-shadow:
415
- 0 0 0 3px color-mix(in srgb, var(--success) 30%, transparent),
416
- 0 0 1.5rem color-mix(in srgb, var(--success) 40%, transparent);
417
- }
418
- }
419
-
420
- :has(> .copied), :has(> .is-copied) {
421
- animation: copy-glow 1.2s ease-in-out;
422
- }
@@ -1,108 +0,0 @@
1
- import { Hono } from "hono";
2
- import { HTTPException } from "hono/http-exception";
3
- import { serveStatic } from "hono/bun";
4
- import { z } from "zod";
5
-
6
- // The exact response header set; test/app.test.ts asserts every entry on
7
- // success, error, API, and static responses. Change both together.
8
- export const SECURITY_HEADERS: Record<string, string> = {
9
- "Content-Security-Policy":
10
- "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'",
11
- "X-Content-Type-Options": "nosniff",
12
- "X-Frame-Options": "DENY",
13
- "Referrer-Policy": "strict-origin-when-cross-origin",
14
- "Permissions-Policy": "camera=(), geolocation=(), microphone=()",
15
- "Cross-Origin-Opener-Policy": "same-origin",
16
- "Cross-Origin-Resource-Policy": "same-origin",
17
- };
18
-
19
- function applyHeaders(res: Response): Response {
20
- for (const [name, value] of Object.entries(SECURITY_HEADERS)) {
21
- res.headers.set(name, value);
22
- }
23
- return res;
24
- }
25
-
26
- // Factory so tests can build an instance and add probe routes before the
27
- // router freezes on first dispatch.
28
- export function createApp(): Hono {
29
- const app = new Hono();
30
-
31
- app.use("*", async (c, next) => {
32
- await next();
33
- applyHeaders(c.res);
34
- });
35
-
36
- // Thrown errors bypass the middleware above (the exception unwinds past
37
- // its post-next line), so the error handler applies the same set.
38
- // Deliberate HTTP errors (framework middleware like CSRF throws
39
- // HTTPException) keep their status; everything else is a logged 500.
40
- app.onError((err, c) => {
41
- if (err instanceof HTTPException) return applyHeaders(err.getResponse());
42
- console.error(err);
43
- return applyHeaders(c.text("Internal error", 500));
44
- });
45
-
46
- app.get("/healthz", (c) => c.json({ ok: true }));
47
-
48
- const helloQuery = z.object({
49
- name: z.string().min(1).max(100).optional(),
50
- });
51
-
52
- app.get("/api/hello", (c) => {
53
- const parsed = helloQuery.safeParse(c.req.query());
54
- if (!parsed.success) {
55
- return c.json({ error: "Invalid query" }, 400);
56
- }
57
- return c.json({ hello: parsed.data.name ?? "world" });
58
- });
59
-
60
- const page = `<!doctype html>
61
- <html lang="en">
62
- <head>
63
- <meta charset="utf-8" />
64
- <meta name="viewport" content="width=device-width, initial-scale=1" />
65
- <title>Web app · shibumi</title>
66
- <link rel="icon" type="image/svg+xml" href="/public/favicon.svg" />
67
- <meta name="generator" content="create-shibumi (shibumistack.dev)" />
68
- <link rel="stylesheet" href="/public/vendor/shibumi.css" />
69
- <link rel="stylesheet" href="/public/style.css" />
70
- <!-- app.js must load before Alpine so the alpine:init listener exists
71
- when Alpine starts; both defer, so document order is execution order. -->
72
- <script src="/public/app.js" defer></script>
73
- <script src="/public/vendor/alpine-csp-3.16.2.min.js" defer></script>
74
- </head>
75
- <body>
76
- <main class="scaffold">
77
- <p class="masthead"><span class="masthead-mark">渋み</span> shibumi web</p>
78
- <h1>Web app</h1>
79
- <p class="lede">Hono serves the routes, Zod validates every input, and Alpine runs the client behavior. Deploy to your own server with one command.</p>
80
- <section class="demo" x-data="counter" aria-label="Alpine counter demo">
81
- <button x-on:click="inc" type="button">Count</button>
82
- <output x-text="count">0</output>
83
- <span class="demo-hint">client state without a build step</span>
84
- </section>
85
- <ul class="endpoints">
86
- <li><a href="/api/hello?name=you"><code>GET /api/hello</code><span>query validated with Zod</span></a></li>
87
- <li><a href="/healthz"><code>GET /healthz</code><span>the check every deploy waits for</span></a></li>
88
- </ul>
89
- <footer class="colophon">
90
- <p>This page lives in <code>src/app.ts</code>. House rules for coding agents are in <code>agents.md</code>. Add features with <code>bun shi add email</code>.</p>
91
- <p class="colophon-links"><a href="https://shibumistack.dev/docs" rel="noreferrer">shibumi docs</a> · <a href="https://server.shibumistack.dev/docs" rel="noreferrer">server docs</a> · <a href="https://shibumistack.dev/docs/cli/extensions" rel="noreferrer">extensions</a></p>
92
- </footer>
93
- </main>
94
- </body>
95
- </html>
96
- `;
97
-
98
- app.get("/", (c) => c.html(page));
99
-
100
- // GET-only: static files must not answer mutation verbs.
101
- app.get("/public/*", serveStatic({ root: "./" }));
102
-
103
- app.notFound((c) => c.text("Not found", 404));
104
-
105
- return app;
106
- }
107
-
108
- export const app = createApp();
@@ -1,21 +0,0 @@
1
- import { z } from "zod";
2
-
3
- // Validate the environment at the boundary; the app never reads process.env
4
- // directly. Add new variables here so misconfiguration fails at startup.
5
- const schema = z.object({
6
- PORT: z.coerce.number().int().min(1).max(65535).default(3000),
7
- });
8
-
9
- export type Env = z.infer<typeof schema>;
10
-
11
- export function loadEnv(source: Record<string, string | undefined> = process.env): Env {
12
- const parsed = schema.safeParse(source);
13
- if (!parsed.success) {
14
- console.error("Invalid environment:");
15
- for (const issue of parsed.error.issues) {
16
- console.error(` ${issue.path.join(".")}: ${issue.message}`);
17
- }
18
- process.exit(1);
19
- }
20
- return parsed.data;
21
- }
@@ -1,23 +0,0 @@
1
- import { app } from "./app";
2
- import { loadEnv } from "./env";
3
-
4
- const env = loadEnv();
5
-
6
- const server = Bun.serve({
7
- port: env.PORT,
8
- // Cap request bodies so an unauthenticated client cannot exhaust memory
9
- // before a route (or its rate limiter) runs. Raise it for large uploads.
10
- maxRequestBodySize: 1024 * 1024,
11
- fetch: app.fetch,
12
- });
13
-
14
- console.log(`Listening on http://localhost:${server.port}`);
15
-
16
- // Graceful shutdown: stop accepting connections, let in-flight requests
17
- // finish, then exit. The container runtime sends SIGTERM on replacement.
18
- async function shutdown(code: number): Promise<void> {
19
- await server.stop();
20
- process.exit(code);
21
- }
22
- process.on("SIGTERM", () => void shutdown(0));
23
- process.on("SIGINT", () => void shutdown(0));