@webjsdev/cli 0.10.45 → 0.10.46

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.
Files changed (90) hide show
  1. package/lib/create.js +109 -141
  2. package/package.json +1 -1
  3. package/templates/.agents/rules/workflow.md +7 -3
  4. package/templates/.agents/skills/webjs/SKILL.md +4 -2
  5. package/templates/.agents/skills/webjs/references/auth-and-sessions.md +78 -16
  6. package/templates/.agents/skills/webjs/references/built-ins.md +16 -2
  7. package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +28 -4
  8. package/templates/.agents/skills/webjs/references/components.md +82 -2
  9. package/templates/.agents/skills/webjs/references/data-and-actions.md +19 -2
  10. package/templates/.agents/skills/webjs/references/optimistic-ui.md +18 -0
  11. package/templates/.agents/skills/webjs/references/routing-and-pages.md +25 -2
  12. package/templates/.agents/skills/webjs/references/styling.md +82 -2
  13. package/templates/AGENTS.md +12 -5
  14. package/templates/gallery/app/apple-icon.ts +2 -5
  15. package/templates/gallery/app/examples/layout.ts +7 -2
  16. package/templates/gallery/app/examples/todo/page.ts +2 -1
  17. package/templates/gallery/app/features/async-render/page.ts +3 -2
  18. package/templates/gallery/app/features/auth/dashboard/layout.ts +2 -1
  19. package/templates/gallery/app/features/auth/dashboard/page.ts +4 -2
  20. package/templates/gallery/app/features/auth/dashboard/settings/page.ts +2 -1
  21. package/templates/gallery/app/features/auth/login/middleware.ts +15 -0
  22. package/templates/gallery/app/features/auth/login/page.ts +7 -4
  23. package/templates/gallery/app/features/auth/page.ts +6 -5
  24. package/templates/gallery/app/features/auth/signup/middleware.ts +11 -0
  25. package/templates/gallery/app/features/auth/signup/page.ts +7 -4
  26. package/templates/gallery/app/features/boundaries/error.ts +5 -4
  27. package/templates/gallery/app/features/boundaries/gated/forbidden.ts +5 -4
  28. package/templates/gallery/app/features/boundaries/not-found.ts +5 -4
  29. package/templates/gallery/app/features/boundaries/page.ts +11 -10
  30. package/templates/gallery/app/features/boundaries/private/unauthorized.ts +5 -4
  31. package/templates/gallery/app/features/broadcast/page.ts +4 -3
  32. package/templates/gallery/app/features/caching/page.ts +5 -4
  33. package/templates/gallery/app/features/client-router/page.ts +7 -5
  34. package/templates/gallery/app/features/client-router/second/page.ts +4 -3
  35. package/templates/gallery/app/features/components/page.ts +3 -2
  36. package/templates/gallery/app/features/directives/page.ts +3 -2
  37. package/templates/gallery/app/features/env/page.ts +4 -3
  38. package/templates/gallery/app/features/file-storage/page.ts +8 -5
  39. package/templates/gallery/app/features/forms/page.ts +10 -6
  40. package/templates/gallery/app/features/frames/page.ts +16 -8
  41. package/templates/gallery/app/features/layout.ts +60 -5
  42. package/templates/gallery/app/features/metadata/page.ts +7 -6
  43. package/templates/gallery/app/features/optimistic-ui/page.ts +3 -2
  44. package/templates/gallery/app/features/rate-limit/page.ts +6 -5
  45. package/templates/gallery/app/features/route-handler/page.ts +4 -3
  46. package/templates/gallery/app/features/routing/[id]/page.ts +7 -6
  47. package/templates/gallery/app/features/routing/page.ts +10 -9
  48. package/templates/gallery/app/features/server-actions/page.ts +5 -4
  49. package/templates/gallery/app/features/service-worker/page.ts +4 -3
  50. package/templates/gallery/app/features/sessions/page.ts +5 -4
  51. package/templates/gallery/app/features/stream/page.ts +4 -3
  52. package/templates/gallery/app/features/streaming/page.ts +4 -3
  53. package/templates/gallery/app/features/suspense/page.ts +4 -3
  54. package/templates/gallery/app/features/view-transitions/page.ts +6 -3
  55. package/templates/gallery/app/features/view-transitions/second/page.ts +4 -2
  56. package/templates/gallery/app/features/websockets/page.ts +4 -3
  57. package/templates/gallery/app/global-error.ts +2 -5
  58. package/templates/gallery/app/global-not-found.ts +4 -6
  59. package/templates/gallery/app/icon.ts +2 -5
  60. package/templates/gallery/app/manifest.ts +1 -4
  61. package/templates/gallery/app/opengraph-image.ts +3 -6
  62. package/templates/gallery/app/robots.ts +0 -3
  63. package/templates/gallery/app/sitemap.ts +0 -3
  64. package/templates/gallery/app/twitter-image.ts +3 -6
  65. package/templates/gallery/components/ui/badge.ts +41 -0
  66. package/templates/gallery/components/ui/button.ts +86 -0
  67. package/templates/gallery/components/ui/card.ts +36 -0
  68. package/templates/gallery/components/ui/input.ts +50 -0
  69. package/templates/gallery/lib/utils/ui.ts +31 -0
  70. package/templates/gallery/modules/broadcast/components/broadcast-feed.ts +4 -2
  71. package/templates/gallery/modules/caching/components/cache-buster.ts +2 -1
  72. package/templates/gallery/modules/client-router/components/router-controls.ts +4 -3
  73. package/templates/gallery/modules/components/components/counter-card.ts +4 -2
  74. package/templates/gallery/modules/components/components/reactive-meter.ts +9 -1
  75. package/templates/gallery/modules/components/components/task-loader.ts +3 -2
  76. package/templates/gallery/modules/components/components/theme-context.ts +5 -3
  77. package/templates/gallery/modules/directives/components/directive-demo.ts +17 -10
  78. package/templates/gallery/modules/gallery/components/gallery-nav.ts +54 -0
  79. package/templates/gallery/modules/gallery/nav.ts +79 -0
  80. package/templates/gallery/modules/optimistic-ui/components/like-button.ts +19 -1
  81. package/templates/gallery/modules/rate-limit/components/rate-probe.ts +2 -1
  82. package/templates/gallery/modules/route-handler/components/rich-data.ts +2 -1
  83. package/templates/gallery/modules/server-actions/components/greeter.ts +7 -4
  84. package/templates/gallery/modules/stream/components/stream-demo.ts +10 -5
  85. package/templates/gallery/modules/streaming/components/token-stream.ts +10 -3
  86. package/templates/gallery/modules/suspense/components/slow-fact.ts +2 -1
  87. package/templates/gallery/modules/todo/components/todo-app.ts +8 -4
  88. package/templates/gallery/modules/websockets/components/ws-echo.ts +5 -3
  89. package/templates/public/favicon.svg +10 -3
  90. package/templates/scripts/clear-gallery.mjs +126 -19
@@ -1,12 +1,67 @@
1
1
  import { html } from '@webjsdev/core';
2
+ import { backLink } from '#lib/utils/ui.ts';
3
+ import '#modules/gallery/components/gallery-nav.ts';
2
4
 
3
- // Shared layout for every gallery feature demo under /features/*. It adds a slim
4
- // "back to the gallery" link above each demo so a card is never a dead end.
5
+ // A subtle, hover-revealed scrollbar for the sidebar (macOS overlay feel): the
6
+ // track + thumb are transparent while idle (no bar), and fade in on hover /
7
+ // keyboard focus, then fade out again. A layout may interpolate CSS into a
8
+ // <style> (it never hydrates, unlike a component), so this is legitimate here.
9
+ const SIDENAV_CSS = `
10
+ .gallery-sidenav { scrollbar-width: thin; scrollbar-color: transparent transparent; transition: scrollbar-color .2s ease; }
11
+ .gallery-sidenav:hover, .gallery-sidenav:focus-within { scrollbar-color: color-mix(in oklch, var(--foreground) 22%, transparent) transparent; }
12
+ .gallery-sidenav::-webkit-scrollbar { width: 8px; }
13
+ .gallery-sidenav::-webkit-scrollbar-track { background: transparent; }
14
+ .gallery-sidenav::-webkit-scrollbar-thumb { background-color: transparent; border-radius: 999px; border: 2px solid transparent; background-clip: padding-box; transition: background-color .2s ease; }
15
+ .gallery-sidenav:hover::-webkit-scrollbar-thumb, .gallery-sidenav:focus-within::-webkit-scrollbar-thumb { background-color: color-mix(in oklch, var(--foreground) 22%, transparent); }
16
+ .gallery-sidenav::-webkit-scrollbar-thumb:hover { background-color: color-mix(in oklch, var(--foreground) 40%, transparent); }
17
+ `;
18
+
19
+ // Section rhythm: ONE uniform gap between every top-level block of a demo
20
+ // (heading, lede, paragraph, component, list, anything), so spacing stays
21
+ // consistent with NO per-element margins. This is the single place that controls
22
+ // demo spacing. A flex column with `gap` does it WITHOUT touching each child's
23
+ // own display: a `grid`/`flex` child (like the forms card) keeps its layout, a
24
+ // shadow-DOM host (inline by default) is blockified as a flex item so it honours
25
+ // the gap, a display:contents <webjs-suspense> is replaced by its children (which
26
+ // become the flex items and get the gap), and a display:none streaming
27
+ // <script>/<template> is not an item at all (no phantom gap).
28
+ const STACK_CSS = `
29
+ .demo-stack {
30
+ --section-gap: 1.5rem;
31
+ display: flex;
32
+ flex-direction: column;
33
+ gap: var(--section-gap);
34
+ }
35
+ /* Zero any BLOCK-axis margins so ONLY the flex gap spaces sections. Block
36
+ axis only: this style is unlayered so it beats Tailwind's layered
37
+ utilities, and a full margin: 0 would also kill mx-auto (an auth card's
38
+ horizontal centering). */
39
+ .demo-stack > * { margin-block: 0; }
40
+ `;
41
+
42
+ // Shared layout for every gallery feature demo under /features/*. A docs-style
43
+ // two-column shell: a grouped sidebar (the <gallery-nav> component, which tracks
44
+ // the active demo across soft navigation) plus the demo content. On mobile the
45
+ // sidebar is hidden, so a slim back link keeps a demo from being a dead end.
5
46
  // Nested layouts (like the auth dashboard's sub-nav) render inside ${children}.
6
47
  // A non-root layout, so it never writes the document shell (the framework does).
7
- export default function FeaturesLayout({ children }: { children: unknown }) {
48
+ export default function FeaturesLayout({ children, url }: { children: unknown; url: URL | string }) {
49
+ const path = typeof url === 'string' ? new URL(url, 'http://x').pathname : url.pathname;
8
50
  return html`
9
- <a href="/" class="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors no-underline mb-6">&larr; Gallery</a>
10
- ${children}
51
+ <style>${SIDENAV_CSS}${STACK_CSS}</style>
52
+ <div class="lg:hidden mb-6">${backLink('/', html`&larr; Gallery`)}</div>
53
+ <div class="grid lg:grid-cols-[190px_1fr] gap-8 lg:gap-12">
54
+ <!-- The sidebar caps at the viewport minus the root chrome (the h-14 navbar
55
+ = 3.5rem plus the root <main>'s py-8 = 4rem, so 7.5rem) and scrolls
56
+ internally, so a short demo never forces a page scrollbar. -->
57
+ <aside class="hidden lg:flex lg:flex-col sticky top-6 self-start max-h-[calc(100dvh-7.5rem)] text-sm">
58
+ <!-- Pinned header: stays put while the demo list below scrolls. -->
59
+ <a href="/" class="shrink-0 block px-3 py-1.5 mb-4 rounded-lg no-underline text-muted-foreground hover:text-foreground transition-colors">&larr; Gallery</a>
60
+ <div class="gallery-sidenav min-h-0 overflow-y-auto overflow-x-hidden -mr-2 pr-2">
61
+ <gallery-nav current=${path}></gallery-nav>
62
+ </div>
63
+ </aside>
64
+ <div class="min-w-0 demo-stack">${children}</div>
65
+ </div>
11
66
  `;
12
67
  }
@@ -6,6 +6,7 @@
6
6
  // (sitemap.ts, robots.ts, opengraph-image.ts, ...) live at the app root.
7
7
  import { html } from '@webjsdev/core';
8
8
  import type { Metadata } from '@webjsdev/core';
9
+ import { pageHeading, lede } from '#lib/utils/ui.ts';
9
10
 
10
11
  // generateMetadata wins over a static `metadata` export when both exist. Here
11
12
  // it derives the title from a ?topic= query param to show the dynamic form;
@@ -31,20 +32,20 @@ export default function MetadataExample({
31
32
  }) {
32
33
  const topic = (searchParams.topic || '').trim();
33
34
  return html`
34
- <h1 class="text-h2 font-bold mb-4">Metadata</h1>
35
- <p class="text-muted-foreground mb-4">
35
+ ${pageHeading('Metadata')}
36
+ ${lede(html`
36
37
  <code>generateMetadata(ctx)</code> runs on the server and can read the
37
38
  request, so the <code>&lt;title&gt;</code> is computed per URL. View source
38
39
  to see the tag this page produced.
39
- </p>
40
+ `)}
40
41
  <p class="mb-4">
41
42
  Current title source:
42
43
  <code class="font-mono text-sm">${topic ? '?topic=' + topic : '(default, no ?topic=)'}</code>
43
44
  </p>
44
45
  <ul class="list-disc pl-5 mb-4">
45
- <li><a class="text-primary" href="/features/metadata?topic=webjs">?topic=webjs</a></li>
46
- <li><a class="text-primary" href="/features/metadata?topic=Routing">?topic=Routing</a></li>
47
- <li><a class="text-primary" href="/features/metadata">clear the param</a></li>
46
+ <li><a class="text-primary underline underline-offset-2" href="/features/metadata?topic=webjs">?topic=webjs</a></li>
47
+ <li><a class="text-primary underline underline-offset-2" href="/features/metadata?topic=Routing">?topic=Routing</a></li>
48
+ <li><a class="text-primary underline underline-offset-2" href="/features/metadata">clear the param</a></li>
48
49
  </ul>
49
50
  <p class="text-muted-foreground text-sm">
50
51
  Site-wide metadata (sitemap, robots, Open Graph images) lives in metadata
@@ -1,13 +1,14 @@
1
1
  import { html } from '@webjsdev/core';
2
2
  import type { Metadata } from '@webjsdev/core';
3
+ import { pageHeading, lede } from '#lib/utils/ui.ts';
3
4
  import '#modules/optimistic-ui/components/like-button.ts';
4
5
 
5
6
  export const metadata: Metadata = { title: 'Optimistic UI (imperative flip) | features' };
6
7
 
7
8
  export default function OptimisticUiFeature() {
8
9
  return html`
9
- <h1 class="text-h2 font-bold mb-4">Optimistic UI</h1>
10
- <p class="text-muted-foreground mb-4">The imperative <code>optimistic(signal, value, action)</code> form: the UI flips instantly and rolls back if the action fails. For the declarative list form (add / remove with rollback) in a full app, see <a class="text-primary" href="/examples/todo">/examples/todo</a>.</p>
10
+ ${pageHeading('Optimistic UI')}
11
+ ${lede(html`The imperative <code>optimistic(signal, value, action)</code> form: the UI flips instantly and rolls back if the action fails. For the declarative list form (add / remove with rollback) in a full app, see <a class="text-primary underline underline-offset-2" href="/examples/todo">/examples/todo</a>.`)}
11
12
  <like-button></like-button>
12
13
  `;
13
14
  }
@@ -5,23 +5,24 @@
5
5
  // default; point the store at Redis to share the window across instances.
6
6
  import { html } from '@webjsdev/core';
7
7
  import type { Metadata } from '@webjsdev/core';
8
+ import { pageHeading, lede } from '#lib/utils/ui.ts';
8
9
  import '#modules/rate-limit/components/rate-probe.ts';
9
10
 
10
11
  export const metadata: Metadata = { title: 'Rate limiting (rateLimit middleware) | features' };
11
12
 
12
13
  export default function RateLimitExample() {
13
14
  return html`
14
- <h1 class="text-h2 font-bold mb-4">Rate limiting</h1>
15
- <p class="text-muted-foreground mb-4">
15
+ ${pageHeading('Rate limiting')}
16
+ ${lede(html`
16
17
  The <code class="font-mono">/ping</code> endpoint is limited to five
17
18
  requests per ten seconds by a scoped <code class="font-mono">middleware.ts</code>.
18
19
  Ping past the limit to get a <code class="font-mono">429</code> with a
19
20
  <code class="font-mono">Retry-After</code> header.
20
- </p>
21
+ `)}
21
22
  <rate-probe></rate-probe>
22
- <p class="text-muted-foreground text-sm mt-4">
23
+ <p class="text-muted-foreground text-sm">
23
24
  With JavaScript off, hit
24
- <a class="text-primary" href="/features/rate-limit/ping" data-no-router>/features/rate-limit/ping</a>
25
+ <a class="text-primary underline underline-offset-2" href="/features/rate-limit/ping" data-no-router>/features/rate-limit/ping</a>
25
26
  directly (refresh past five times in ten seconds).
26
27
  </p>
27
28
  `;
@@ -1,14 +1,15 @@
1
1
  import { html } from '@webjsdev/core';
2
2
  import type { Metadata } from '@webjsdev/core';
3
+ import { pageHeading, lede } from '#lib/utils/ui.ts';
3
4
  import '#modules/route-handler/components/rich-data.ts';
4
5
 
5
6
  export const metadata: Metadata = { title: 'Route handlers (route.ts) | features' };
6
7
 
7
8
  export default function RouteHandlerExample() {
8
9
  return html`
9
- <h1 class="text-h2 font-bold mb-4">Route handlers</h1>
10
- <p class="text-muted-foreground mb-4">A <code>route.ts</code> is a server-only HTTP endpoint (named <code>GET</code>/<code>POST</code>/... exports), the WebJs equivalent of a Next route handler. It never ships to the client.</p>
11
- <p>GET <a class="text-primary" href="/features/route-handler/data" data-no-router>/features/route-handler/data</a> returns rich JSON via <code class="font-mono">json()</code>. It carries <code class="font-mono">data-no-router</code> so the client router does not try to soft-navigate to it: a <code>route.ts</code> is not a page, so the browser loads its JSON directly.</p>
10
+ ${pageHeading('Route handlers')}
11
+ ${lede(html`A <code>route.ts</code> is a server-only HTTP endpoint (named <code>GET</code>/<code>POST</code>/... exports), the WebJs equivalent of a Next route handler. It never ships to the client.`)}
12
+ <p>GET <a class="text-primary underline underline-offset-2" href="/features/route-handler/data" data-no-router>/features/route-handler/data</a> returns rich JSON via <code class="font-mono">json()</code>. It carries <code class="font-mono">data-no-router</code> so the client router does not try to soft-navigate to it: a <code>route.ts</code> is not a page, so the browser loads its JSON directly.</p>
12
13
  <p class="text-muted-foreground mt-6 mb-2">A client component reading it with <code class="font-mono">richFetch</code>, so <code class="font-mono">at</code> comes back as a real <code class="font-mono">Date</code>:</p>
13
14
  <rich-data></rich-data>
14
15
  `;
@@ -14,6 +14,7 @@
14
14
  // route literal that does not exist and it is a compile error.
15
15
  import { html, notFound } from '@webjsdev/core';
16
16
  import type { PageProps } from '@webjsdev/core';
17
+ import { pageHeading } from '#lib/utils/ui.ts';
17
18
 
18
19
  export default async function RoutingParam({ params }: PageProps<'/features/routing/[id]'>) {
19
20
  // The Next-style await also works; `params.id` sync would be identical.
@@ -23,23 +24,23 @@ export default async function RoutingParam({ params }: PageProps<'/features/rout
23
24
  // you throw this after a DB lookup returns nothing. Try /features/routing/missing.
24
25
  if (id === 'missing') notFound();
25
26
  return html`
26
- <h1 class="text-h2 font-bold mb-4">Route param</h1>
27
+ ${pageHeading('Route param')}
27
28
  <p>The <code>[id]</code> segment is: <strong>${id}</strong></p>
28
- <p class="text-muted-foreground text-sm mt-3">
29
+ <p class="text-muted-foreground text-sm">
29
30
  Typed with <code class="font-mono">PageProps&lt;'/features/routing/[id]'&gt;</code>,
30
31
  so <code class="font-mono">params.id</code> is a checked
31
32
  <code class="font-mono">string</code> from the generated route union.
32
33
  <code class="font-mono">params</code> is awaitable too:
33
34
  <code class="font-mono">const { id } = await params</code> works, same value.
34
35
  </p>
35
- <p class="text-muted-foreground text-sm mt-3">
36
- Throwing wins over rendering: <a class="text-primary" href="/features/routing/missing">/features/routing/missing</a>
36
+ <p class="text-muted-foreground text-sm">
37
+ Throwing wins over rendering: <a class="text-primary underline underline-offset-2" href="/features/routing/missing">/features/routing/missing</a>
37
38
  throws <code class="font-mono">notFound()</code> and renders the nearest
38
39
  not-found boundary at 404. See the
39
- <a class="text-primary" href="/features/boundaries">Boundaries</a> demo for
40
+ <a class="text-primary underline underline-offset-2" href="/features/boundaries">Boundaries</a> demo for
40
41
  <code class="font-mono">forbidden()</code> and
41
42
  <code class="font-mono">unauthorized()</code>.
42
43
  </p>
43
- <p class="mt-3"><a class="text-primary" href="/features/routing">Back</a></p>
44
+ <p class="mt-3"><a class="text-primary underline underline-offset-2" href="/features/routing">Back</a></p>
44
45
  `;
45
46
  }
@@ -3,19 +3,20 @@
3
3
  // `params`. See app/features/routing/[id]/page.ts.
4
4
  import { html } from '@webjsdev/core';
5
5
  import type { Metadata } from '@webjsdev/core';
6
+ import { pageHeading, lede } from '#lib/utils/ui.ts';
6
7
 
7
8
  export const metadata: Metadata = { title: 'Routing (dynamic params) | features' };
8
9
 
9
10
  export default function RoutingExample() {
10
11
  return html`
11
- <h1 class="text-h2 font-bold mb-4">Routing</h1>
12
- <p class="text-muted-foreground mb-4">A folder is a URL segment; a <code>[id]</code> folder is a dynamic param.</p>
12
+ ${pageHeading('Routing')}
13
+ ${lede(html`A folder is a URL segment; a <code>[id]</code> folder is a dynamic param.`)}
13
14
  <ul class="list-disc pl-5 mb-4">
14
- <li><a class="text-primary" href="/features/routing/42">/features/routing/42</a></li>
15
- <li><a class="text-primary" href="/features/routing/hello">/features/routing/hello</a></li>
16
- <li><a class="text-primary" href="/features/routing/legacy">/features/routing/legacy</a> throws <code class="font-mono">redirect()</code> back here</li>
15
+ <li><a class="text-primary underline underline-offset-2" href="/features/routing/42">/features/routing/42</a></li>
16
+ <li><a class="text-primary underline underline-offset-2" href="/features/routing/hello">/features/routing/hello</a></li>
17
+ <li><a class="text-primary underline underline-offset-2" href="/features/routing/legacy">/features/routing/legacy</a> throws <code class="font-mono">redirect()</code> back here</li>
17
18
  </ul>
18
- <p class="text-muted-foreground text-sm mb-2">
19
+ <p class="text-muted-foreground text-sm">
19
20
  Routes are type-safe: <code class="font-mono">webjs types</code> (run by
20
21
  <code class="font-mono">webjs dev</code>) generates a
21
22
  <code class="font-mono">Route</code> union, and the
@@ -23,7 +24,7 @@ export default function RoutingExample() {
23
24
  <code class="font-mono">PageProps&lt;'/features/routing/[id]'&gt;</code>
24
25
  so <code class="font-mono">params</code> is checked against the real routes.
25
26
  </p>
26
- <p class="text-muted-foreground text-sm mb-2">
27
+ <p class="text-muted-foreground text-sm">
27
28
  Programmatic navigation is checked too:
28
29
  <code class="font-mono">navigate(url)</code> takes that
29
30
  <code class="font-mono">Route</code> union, so
@@ -43,13 +44,13 @@ export default function RoutingExample() {
43
44
  <code class="font-mono">redirect()</code> on the server, call
44
45
  <code class="font-mono">navigate()</code> on the client.
45
46
  </p>
46
- <p class="text-muted-foreground text-sm mt-3">
47
+ <p class="text-muted-foreground text-sm">
47
48
  A page can also THROW to short-circuit rendering:
48
49
  <code class="font-mono">notFound()</code> (404),
49
50
  <code class="font-mono">forbidden()</code> (403), and
50
51
  <code class="font-mono">unauthorized()</code> (401), each rendering the
51
52
  nearest matching boundary file. See the
52
- <a class="text-primary" href="/features/boundaries">Boundaries</a> demo.
53
+ <a class="text-primary underline underline-offset-2" href="/features/boundaries">Boundaries</a> demo.
53
54
  </p>
54
55
  `;
55
56
  }
@@ -1,23 +1,24 @@
1
1
  import { html } from '@webjsdev/core';
2
2
  import type { Metadata } from '@webjsdev/core';
3
+ import { pageHeading, lede } from '#lib/utils/ui.ts';
3
4
  import '#modules/server-actions/components/greeter.ts';
4
5
 
5
6
  export const metadata: Metadata = { title: 'Server actions (.server vs use server) | features' };
6
7
 
7
8
  export default function ServerActionsExample() {
8
9
  return html`
9
- <h1 class="text-h2 font-bold mb-4">Server actions</h1>
10
- <p class="text-muted-foreground mb-4">A 'use server' action is RPC-callable from the client; a plain .server.ts is a server-only utility you never import into a component.</p>
10
+ ${pageHeading('Server actions')}
11
+ ${lede(html`A 'use server' action is RPC-callable from the client; a plain .server.ts is a server-only utility you never import into a component.`)}
11
12
  <p class="text-muted-foreground mb-4">
12
13
  This action also declares <code class="font-mono">export const middleware</code>: a
13
14
  chain that runs around it on every boundary. The auth middleware reads the
14
- real signed session (from the <a class="text-primary" href="/features/auth">auth card</a>) and
15
+ real signed session (from the <a class="text-primary underline underline-offset-2" href="/features/auth">auth card</a>) and
15
16
  sets the caller on the request context (read back with <code class="font-mono">actionContext()</code>),
16
17
  or 401s before the action runs. The action threads
17
18
  <code class="font-mono">actionSignal()</code>, the request AbortSignal, through
18
19
  its work so a client disconnect or a superseded render stops it early.
19
20
  </p>
20
- <p class="text-muted-foreground mb-4 text-sm">Signed out, the greeter returns a real 401. <a class="text-primary" href="/features/auth/login">Sign in</a> first to see it succeed. (This card depends on the auth card; prune both together.)</p>
21
+ <p class="text-muted-foreground mb-4 text-sm">Signed out, the greeter returns a real 401. <a class="text-primary underline underline-offset-2" href="/features/auth/login">Sign in</a> first to see it succeed. (This card depends on the auth card; prune both together.)</p>
21
22
  <server-greeter></server-greeter>
22
23
  `;
23
24
  }
@@ -6,17 +6,18 @@
6
6
  // agent-docs/service-worker.md for the full recipe.
7
7
  import { html } from '@webjsdev/core';
8
8
  import type { Metadata } from '@webjsdev/core';
9
+ import { pageHeading, lede } from '#lib/utils/ui.ts';
9
10
 
10
11
  export const metadata: Metadata = { title: 'Service worker (opt-in) | features' };
11
12
 
12
13
  export default function ServiceWorkerExample() {
13
14
  return html`
14
- <h1 class="text-h2 font-bold mb-4">Service worker</h1>
15
- <p class="text-muted-foreground mb-4">
15
+ ${pageHeading('Service worker')}
16
+ ${lede(html`
16
17
  Opt-in offline/caching enhancement. Nothing is registered until you add
17
18
  <code class="font-mono">public/sw.js</code> and register it from a
18
19
  browser-only lifecycle hook.
19
- </p>
20
+ `)}
20
21
  <p class="mb-2 text-sm font-medium">Register inside a component (never in a page or layout):</p>
21
22
  <pre class="bg-card border border-border rounded-xl p-4 overflow-x-auto text-sm font-mono mb-4"><code>connectedCallback() {
22
23
  super.connectedCallback();
@@ -5,14 +5,15 @@
5
5
  // browser. See modules/sessions/session-config.server.ts.
6
6
  import { html } from '@webjsdev/core';
7
7
  import type { Metadata } from '@webjsdev/core';
8
+ import { pageHeading, lede } from '#lib/utils/ui.ts';
8
9
 
9
10
  export const metadata: Metadata = { title: 'Sessions (cookie + store) | features' };
10
11
 
11
12
  export default function SessionsExample() {
12
13
  return html`
13
- <h1 class="text-h2 font-bold mb-4">Sessions</h1>
14
- <p class="text-muted-foreground mb-4">A per-segment <code>middleware.ts</code> applies <code>session()</code>; a <code>route.ts</code> reads it with <code>getSession(req)</code>.</p>
15
- <p>GET <a class="text-primary" href="/features/sessions/count" data-no-router>/features/sessions/count</a> increments a per-visitor counter kept in the signed session cookie. Reload it and the count climbs; open it in a private window and it starts over. (<code class="font-mono">data-no-router</code> opts the link out of the client router, since a <code>route.ts</code> returns JSON, not a page.)</p>
16
- <p class="text-muted-foreground text-sm mt-3">Swap the storage from <code class="font-mono">cookieSession()</code> to <code class="font-mono">storeSession()</code> to hold larger sessions in the active store (Redis in production).</p>
14
+ ${pageHeading('Sessions')}
15
+ ${lede(html`A per-segment <code>middleware.ts</code> applies <code>session()</code>; a <code>route.ts</code> reads it with <code>getSession(req)</code>.`)}
16
+ <p>GET <a class="text-primary underline underline-offset-2" href="/features/sessions/count" data-no-router>/features/sessions/count</a> increments a per-visitor counter kept in the signed session cookie. Reload it and the count climbs; open it in a private window and it starts over. (<code class="font-mono">data-no-router</code> opts the link out of the client router, since a <code>route.ts</code> returns JSON, not a page.)</p>
17
+ <p class="text-muted-foreground text-sm">Swap the storage from <code class="font-mono">cookieSession()</code> to <code class="font-mono">storeSession()</code> to hold larger sessions in the active store (Redis in production).</p>
17
18
  `;
18
19
  }
@@ -18,20 +18,21 @@
18
18
  // message applied with renderStream() from a WS handler, see the WebSockets card).
19
19
  import { html } from '@webjsdev/core';
20
20
  import type { Metadata } from '@webjsdev/core';
21
+ import { pageHeading, lede } from '#lib/utils/ui.ts';
21
22
  import '#modules/stream/components/stream-demo.ts';
22
23
 
23
24
  export const metadata: Metadata = { title: 'Stream updates (webjs-stream) | features' };
24
25
 
25
26
  export default function StreamExample() {
26
27
  return html`
27
- <h1 class="text-h2 font-bold mb-4">Stream updates</h1>
28
- <p class="text-muted-foreground mb-4">
28
+ ${pageHeading('Stream updates')}
29
+ ${lede(html`
29
30
  <code class="font-mono">renderStream()</code> applies a
30
31
  <code class="font-mono">&lt;webjs-stream action="..." target="..."&gt;</code>
31
32
  payload: a surgical, element-level DOM update by id. Each button below mutates
32
33
  the live list in place, and the component never re-renders (contrast a frame,
33
34
  which swaps a whole region, and the client router, which swaps a whole page).
34
- </p>
35
+ `)}
35
36
  <stream-demo></stream-demo>
36
37
  <p class="text-muted-foreground text-sm mt-6">
37
38
  The same grammar arrives over HTTP (a content-negotiated
@@ -7,20 +7,21 @@
7
7
  // the typed action mechanism, no hand-written fetch.
8
8
  import { html } from '@webjsdev/core';
9
9
  import type { Metadata } from '@webjsdev/core';
10
+ import { pageHeading, lede } from '#lib/utils/ui.ts';
10
11
  import '#modules/streaming/components/token-stream.ts';
11
12
 
12
13
  export const metadata: Metadata = { title: 'Streaming actions (for await) | features' };
13
14
 
14
15
  export default function StreamingExample() {
15
16
  return html`
16
- <h1 class="text-h2 font-bold mb-4">Streaming actions</h1>
17
- <p class="text-muted-foreground mb-4">
17
+ ${pageHeading('Streaming actions')}
18
+ ${lede(html`
18
19
  A <code class="font-mono">'use server'</code> action that returns an
19
20
  <code class="font-mono">async function*</code> streams each
20
21
  <code class="font-mono">yield</code> over the single RPC response. The call
21
22
  site consumes it with <code class="font-mono">for await (const chunk of await streamTokens())</code>,
22
23
  so tokens render as they arrive instead of waiting for the whole result.
23
- </p>
24
+ `)}
24
25
  <p class="text-muted-foreground mb-6 text-sm">
25
26
  Detection is on the return value (no config export), and a streamed result
26
27
  is never cached, ETagged, or seeded. The source generator is cancelled if
@@ -8,19 +8,20 @@
8
8
  // for fast data); reach for <webjs-suspense> when the data is genuinely slow.
9
9
  import { html } from '@webjsdev/core';
10
10
  import type { Metadata } from '@webjsdev/core';
11
+ import { pageHeading, lede } from '#lib/utils/ui.ts';
11
12
  import '#modules/suspense/components/slow-fact.ts';
12
13
 
13
14
  export const metadata: Metadata = { title: 'Suspense boundary (<webjs-suspense>) | features' };
14
15
 
15
16
  export default function SuspenseExample() {
16
17
  return html`
17
- <h1 class="text-h2 font-bold mb-4">Suspense boundary</h1>
18
- <p class="text-muted-foreground mb-4">
18
+ ${pageHeading('Suspense boundary')}
19
+ ${lede(html`
19
20
  The slow component below is wrapped in
20
21
  <code class="font-mono">&lt;webjs-suspense&gt;</code>. Its fallback shows on
21
22
  the first byte and the resolved content streams in when the slow await
22
23
  settles. Reload to see the fallback, then the fact stream in.
23
- </p>
24
+ `)}
24
25
  <p class="text-muted-foreground mb-6 text-sm">
25
26
  <code class="font-mono">.fallback</code> is a property hole (unquoted, per
26
27
  invariant 4). Contrast with the
@@ -8,6 +8,9 @@
8
8
  // across the swap: type into it, navigate, and your text survives the transition.
9
9
  import { html } from '@webjsdev/core';
10
10
  import type { Metadata } from '@webjsdev/core';
11
+ import { buttonClass } from '#components/ui/button.ts';
12
+ import { inputClass } from '#components/ui/input.ts';
13
+ import { pageHeading } from '#lib/utils/ui.ts';
11
14
 
12
15
  export const metadata: Metadata = {
13
16
  title: 'View transitions (soft-nav cross-fade) | features',
@@ -16,7 +19,7 @@ export const metadata: Metadata = {
16
19
 
17
20
  export default function ViewTransitionsExample() {
18
21
  return html`
19
- <h1 class="text-h2 font-bold mb-4">View transitions</h1>
22
+ ${pageHeading('View transitions')}
20
23
  <div class="rounded-2xl bg-primary/10 border border-primary/30 p-6 mb-6">
21
24
  <p class="text-foreground m-0">Page one. Navigate to page two: with the
22
25
  <code class="font-mono">&lt;meta name="view-transition"&gt;</code> opt-in,
@@ -26,10 +29,10 @@ export default function ViewTransitionsExample() {
26
29
  <span class="text-muted-foreground text-sm">Type here, then navigate. This input is
27
30
  <code class="font-mono">data-webjs-permanent</code>, so its value survives the swap:</span>
28
31
  <input id="vt-note" data-webjs-permanent type="text" placeholder="type something…"
29
- class="mt-2 block w-full max-w-sm rounded-xl border border-border bg-card px-4 py-2 text-foreground" />
32
+ class=${inputClass('mt-2 block max-w-sm')} />
30
33
  </label>
31
34
  <div class="flex gap-3 items-center">
32
- <a href="/features/view-transitions/second" class="inline-flex items-center px-4 py-2 rounded-xl bg-primary text-primary-foreground font-semibold text-sm no-underline transition-all hover:bg-primary/90 active:scale-[0.97]">Go to page two</a>
35
+ <a href="/features/view-transitions/second" class="${buttonClass()} no-underline">Go to page two</a>
33
36
  <a href="/" class="text-muted-foreground no-underline font-medium text-sm hover:text-foreground transition-colors">Home</a>
34
37
  </div>
35
38
  <p class="text-muted-foreground text-sm mt-6">
@@ -4,6 +4,8 @@
4
4
  // across the swap: the text you typed on page one is still here.
5
5
  import { html } from '@webjsdev/core';
6
6
  import type { Metadata } from '@webjsdev/core';
7
+ import { inputClass } from '#components/ui/input.ts';
8
+ import { pageHeading } from '#lib/utils/ui.ts';
7
9
 
8
10
  export const metadata: Metadata = {
9
11
  title: 'View transitions: page two | features',
@@ -12,7 +14,7 @@ export const metadata: Metadata = {
12
14
 
13
15
  export default function ViewTransitionsSecond() {
14
16
  return html`
15
- <h1 class="text-h2 font-bold mb-4">Page two</h1>
17
+ ${pageHeading('Page two')}
16
18
  <div class="rounded-2xl bg-foreground/5 border border-border p-6 mb-6">
17
19
  <p class="text-foreground m-0">You arrived with a cross-fade, no full
18
20
  reload. The input below is the same node from page one, regrafted across
@@ -21,7 +23,7 @@ export default function ViewTransitionsSecond() {
21
23
  <label class="block mb-6">
22
24
  <span class="text-muted-foreground text-sm">Its value persisted across the transition:</span>
23
25
  <input id="vt-note" data-webjs-permanent type="text" placeholder="type something…"
24
- class="mt-2 block w-full max-w-sm rounded-xl border border-border bg-card px-4 py-2 text-foreground" />
26
+ class=${inputClass('mt-2 block max-w-sm')} />
25
27
  </label>
26
28
  <a href="/features/view-transitions" class="text-primary no-underline font-medium">&larr; Back to page one</a>
27
29
  `;
@@ -6,19 +6,20 @@
6
6
  // it does not collide with this page).
7
7
  import { html } from '@webjsdev/core';
8
8
  import type { Metadata } from '@webjsdev/core';
9
+ import { pageHeading, lede } from '#lib/utils/ui.ts';
9
10
  import '#modules/websockets/components/ws-echo.ts';
10
11
 
11
12
  export const metadata: Metadata = { title: 'WebSockets (connectWS + WS) | features' };
12
13
 
13
14
  export default function WebSocketsExample() {
14
15
  return html`
15
- <h1 class="text-h2 font-bold mb-4">WebSockets</h1>
16
- <p class="text-muted-foreground mb-4">
16
+ ${pageHeading('WebSockets')}
17
+ ${lede(html`
17
18
  A <code class="font-mono">WS(ws, req)</code> export in
18
19
  <code class="font-mono">route.ts</code> is the server endpoint;
19
20
  <code class="font-mono">connectWS()</code> (auto-reconnect, JSON
20
21
  encode/decode, queued sends) is the client. This echoes each message back.
21
- </p>
22
+ `)}
22
23
  <ws-echo></ws-echo>
23
24
  `;
24
25
  }
@@ -1,6 +1,3 @@
1
- // file), then delete this marker line. webjs check fails while the marker
2
- // remains.
3
- //
4
1
  // app/global-error.ts is the ROOT-ONLY, app-wide catch-all error boundary. It
5
2
  // fires only after every nested error.ts boundary is exhausted, which includes
6
3
  // a failure in the root layout itself. Because a root-layout failure is exactly
@@ -29,9 +26,9 @@ export default function GlobalError({ error }: { error: Error }) {
29
26
  <meta name="viewport" content="width=device-width, initial-scale=1" />
30
27
  <title>Something went wrong</title>
31
28
  <style nonce="${cspNonce()}">
32
- body { font: 16px/1.6 system-ui, sans-serif; margin: 0; display: grid; place-items: center; min-height: 100vh; background: #1c1613; color: #f5f0eb; }
29
+ body { font: 16px/1.6 system-ui, sans-serif; margin: 0; display: grid; place-items: center; min-height: 100vh; background: #1e2226; color: #dee2e6; }
33
30
  main { max-width: 32rem; padding: 2rem; text-align: center; }
34
- a { color: #ff8a3d; }
31
+ a { color: #94989c; }
35
32
  </style>
36
33
  </head>
37
34
  <body>
@@ -1,6 +1,3 @@
1
- // file), then delete this marker line. webjs check fails while the marker
2
- // remains.
3
- //
4
1
  // app/global-not-found.ts is the ROOT-ONLY 404 for a URL that matches nothing
5
2
  // anywhere, used when no nested not-found.ts applies. Unlike global-error.ts it
6
3
  // renders only a BODY fragment: the framework wraps it in the document shell
@@ -8,14 +5,15 @@
8
5
  // here. Use a nested <segment>/not-found.ts for a section-specific 404 (nearest
9
6
  // wins); this file is the app-wide fallback.
10
7
  import { html } from '@webjsdev/core';
8
+ import { buttonClass } from '#components/ui/button.ts';
11
9
 
12
10
  export default function GlobalNotFound() {
13
11
  return html`
14
12
  <main class="mx-auto max-w-[40rem] px-6 py-24 text-center">
15
- <p class="text-sm font-semibold uppercase tracking-wide text-orange-500">404</p>
13
+ <p class="text-sm font-semibold uppercase tracking-wide text-primary">404</p>
16
14
  <h1 class="mt-2 text-3xl font-bold">Page not found</h1>
17
- <p class="mt-4 text-neutral-500">We could not find the page you were looking for.</p>
18
- <a href="/" class="mt-8 inline-block rounded-md bg-neutral-900 px-4 py-2 text-white transition-colors hover:bg-neutral-700 dark:bg-white dark:text-neutral-900 dark:hover:bg-neutral-200">Back to home</a>
15
+ <p class="mt-4 text-muted-foreground">We could not find the page you were looking for.</p>
16
+ <a href="/" class="${buttonClass()} mt-8 inline-block no-underline">Back to home</a>
19
17
  </main>
20
18
  `;
21
19
  }
@@ -1,6 +1,3 @@
1
- // (delete this file), then delete this marker line. webjs check fails while the
2
- // marker remains.
3
- //
4
1
  // app/icon.ts serves /icon (the dynamic favicon). The default export is a
5
2
  // (possibly async) server function; returning a Response lets you set the exact
6
3
  // content type, so an inline SVG needs no asset file. For a favicon that never
@@ -9,8 +6,8 @@
9
6
  // mark must be computed at request time.
10
7
  export default function Icon() {
11
8
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
12
- <rect width="32" height="32" rx="7" fill="#1c1613"/>
13
- <text x="16" y="22" font-family="system-ui, sans-serif" font-size="18" font-weight="700" fill="#ff8a3d" text-anchor="middle">w</text>
9
+ <rect width="32" height="32" rx="7" fill="#1e2226"/>
10
+ <text x="16" y="22" font-family="system-ui, sans-serif" font-size="18" font-weight="700" fill="#94989c" text-anchor="middle">w</text>
14
11
  </svg>`;
15
12
  return new Response(svg, {
16
13
  headers: { 'content-type': 'image/svg+xml', 'cache-control': 'public, max-age=3600' },
@@ -1,6 +1,3 @@
1
- // (delete this file), then delete this marker line. webjs check fails while the
2
- // marker remains.
3
- //
4
1
  // app/manifest.ts serves /manifest.json (the web app manifest). The default
5
2
  // export returns an object, serialized to JSON. Adapt the name, colors, and
6
3
  // icons to your app; pair it with the opt-in service worker for an installable
@@ -13,7 +10,7 @@ export default function Manifest() {
13
10
  start_url: '/',
14
11
  display: 'standalone',
15
12
  background_color: '#ffffff',
16
- theme_color: '#1c1613',
13
+ theme_color: '#1e2226',
17
14
  icons: [
18
15
  { src: '/favicon.svg', sizes: 'any', type: 'image/svg+xml' },
19
16
  ],
@@ -1,6 +1,3 @@
1
- // (delete this file), then delete this marker line. webjs check fails while the
2
- // marker remains.
3
- //
4
1
  // app/opengraph-image.ts serves /opengraph-image (the preview card social
5
2
  // platforms show when the site is shared). The Open Graph spec wants 1200x630.
6
3
  // Returning a Response with an inline SVG keeps this buildless; for per-page
@@ -9,9 +6,9 @@
9
6
  // `openGraph: { images: ['/opengraph-image'] }`.
10
7
  export default function OpengraphImage() {
11
8
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
12
- <rect width="1200" height="630" fill="#1c1613"/>
13
- <text x="80" y="330" font-family="system-ui, sans-serif" font-size="88" font-weight="700" fill="#f5f0eb">My App</text>
14
- <text x="80" y="410" font-family="system-ui, sans-serif" font-size="36" fill="#ff8a3d">Build on the platform, not against it</text>
9
+ <rect width="1200" height="630" fill="#1e2226"/>
10
+ <text x="80" y="330" font-family="system-ui, sans-serif" font-size="88" font-weight="700" fill="#dee2e6">My App</text>
11
+ <text x="80" y="410" font-family="system-ui, sans-serif" font-size="36" fill="#94989c">Build on the platform, not against it</text>
15
12
  </svg>`;
16
13
  return new Response(svg, {
17
14
  headers: { 'content-type': 'image/svg+xml', 'cache-control': 'public, max-age=3600' },