@webjsdev/cli 0.10.44 → 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.
- package/README.md +2 -3
- package/bin/webjs.js +18 -18
- package/lib/api-gallery.js +1 -1
- package/lib/create.js +154 -243
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +7 -3
- package/templates/.agents/skills/webjs/SKILL.md +4 -2
- package/templates/.agents/skills/webjs/references/auth-and-sessions.md +78 -16
- package/templates/.agents/skills/webjs/references/built-ins.md +16 -2
- package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +38 -6
- package/templates/.agents/skills/webjs/references/components.md +101 -6
- package/templates/.agents/skills/webjs/references/data-and-actions.md +19 -2
- package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +5 -1
- package/templates/.agents/skills/webjs/references/optimistic-ui.md +18 -0
- package/templates/.agents/skills/webjs/references/routing-and-pages.md +25 -2
- package/templates/.agents/skills/webjs/references/runtime.md +1 -1
- package/templates/.agents/skills/webjs/references/service-worker.md +1 -1
- package/templates/.agents/skills/webjs/references/styling.md +82 -2
- package/templates/AGENTS.md +12 -5
- package/templates/gallery/app/api/auth/[...path]/route.ts +7 -0
- package/templates/gallery/app/apple-icon.ts +2 -5
- package/templates/gallery/app/examples/layout.ts +16 -0
- package/templates/gallery/app/examples/todo/page.ts +2 -1
- package/templates/gallery/app/features/async-render/page.ts +3 -2
- package/templates/gallery/app/features/auth/dashboard/layout.ts +21 -0
- package/templates/gallery/app/features/auth/dashboard/middleware.ts +14 -0
- package/templates/gallery/app/features/auth/dashboard/page.ts +20 -0
- package/templates/gallery/app/features/auth/dashboard/settings/page.ts +22 -0
- package/templates/gallery/app/features/auth/login/middleware.ts +15 -0
- package/templates/gallery/app/features/auth/login/page.ts +43 -0
- package/templates/gallery/app/features/auth/page.ts +34 -0
- package/templates/gallery/app/features/auth/signup/middleware.ts +11 -0
- package/templates/gallery/app/features/auth/signup/page.ts +61 -0
- package/templates/gallery/app/features/boundaries/error.ts +5 -4
- package/templates/gallery/app/features/boundaries/gated/forbidden.ts +5 -4
- package/templates/gallery/app/features/boundaries/not-found.ts +5 -4
- package/templates/gallery/app/features/boundaries/page.ts +11 -10
- package/templates/gallery/app/features/boundaries/private/unauthorized.ts +5 -4
- package/templates/gallery/app/features/broadcast/page.ts +4 -3
- package/templates/gallery/app/features/caching/page.ts +5 -4
- package/templates/gallery/app/features/client-router/page.ts +7 -5
- package/templates/gallery/app/features/client-router/second/page.ts +4 -3
- package/templates/gallery/app/features/components/page.ts +3 -2
- package/templates/gallery/app/features/directives/page.ts +3 -2
- package/templates/gallery/app/features/env/page.ts +4 -3
- package/templates/gallery/app/features/file-storage/page.ts +8 -5
- package/templates/gallery/app/features/forms/page.ts +10 -6
- package/templates/gallery/app/features/frames/page.ts +26 -10
- package/templates/gallery/app/features/layout.ts +67 -0
- package/templates/gallery/app/features/metadata/page.ts +7 -6
- package/templates/gallery/app/features/optimistic-ui/page.ts +3 -2
- package/templates/gallery/app/features/rate-limit/page.ts +6 -5
- package/templates/gallery/app/features/route-handler/page.ts +4 -3
- package/templates/gallery/app/features/routing/[id]/page.ts +7 -6
- package/templates/gallery/app/features/routing/page.ts +10 -9
- package/templates/gallery/app/features/server-actions/page.ts +7 -4
- package/templates/gallery/app/features/service-worker/page.ts +4 -3
- package/templates/gallery/app/features/sessions/page.ts +5 -4
- package/templates/gallery/app/features/stream/page.ts +46 -0
- package/templates/gallery/app/features/streaming/page.ts +32 -0
- package/templates/gallery/app/features/suspense/page.ts +35 -0
- package/templates/gallery/app/features/view-transitions/page.ts +44 -0
- package/templates/gallery/app/features/view-transitions/second/page.ts +30 -0
- package/templates/gallery/app/features/websockets/page.ts +4 -3
- package/templates/gallery/app/global-error.ts +2 -5
- package/templates/gallery/app/global-not-found.ts +4 -6
- package/templates/gallery/app/icon.ts +2 -5
- package/templates/gallery/app/manifest.ts +1 -4
- package/templates/gallery/app/opengraph-image.ts +3 -6
- package/templates/gallery/app/robots.ts +0 -3
- package/templates/gallery/app/sitemap.ts +0 -3
- package/templates/gallery/app/twitter-image.ts +3 -6
- package/templates/gallery/components/ui/badge.ts +41 -0
- package/templates/gallery/components/ui/button.ts +86 -0
- package/templates/gallery/components/ui/card.ts +36 -0
- package/templates/gallery/components/ui/input.ts +50 -0
- package/templates/gallery/lib/utils/ui.ts +31 -0
- package/templates/gallery/modules/auth/actions/signup.server.ts +19 -0
- package/templates/gallery/modules/auth/auth.server.ts +53 -0
- package/templates/gallery/modules/auth/password.server.ts +20 -0
- package/templates/gallery/modules/auth/queries/current-user.server.ts +12 -0
- package/templates/gallery/modules/auth/types.ts +9 -0
- package/templates/gallery/modules/broadcast/components/broadcast-feed.ts +4 -2
- package/templates/gallery/modules/caching/components/cache-buster.ts +2 -1
- package/templates/gallery/modules/client-router/components/router-controls.ts +4 -3
- package/templates/gallery/modules/components/components/counter-card.ts +4 -2
- package/templates/gallery/modules/components/components/reactive-meter.ts +9 -1
- package/templates/gallery/modules/components/components/task-loader.ts +3 -2
- package/templates/gallery/modules/components/components/theme-context.ts +5 -3
- package/templates/gallery/modules/directives/components/directive-demo.ts +17 -10
- package/templates/gallery/modules/gallery/components/gallery-nav.ts +54 -0
- package/templates/gallery/modules/gallery/nav.ts +79 -0
- package/templates/gallery/modules/optimistic-ui/components/like-button.ts +19 -1
- package/templates/gallery/modules/rate-limit/components/rate-probe.ts +2 -1
- package/templates/gallery/modules/route-handler/components/rich-data.ts +2 -1
- package/templates/gallery/modules/server-actions/actions/greet.server.ts +2 -2
- package/templates/gallery/modules/server-actions/actions/greet.test.ts +39 -36
- package/templates/gallery/modules/server-actions/components/greeter.ts +11 -11
- package/templates/gallery/modules/server-actions/middleware/require-auth.server.ts +14 -9
- package/templates/gallery/modules/stream/components/stream-demo.ts +81 -0
- package/templates/gallery/modules/streaming/actions/stream-tokens.server.ts +17 -0
- package/templates/gallery/modules/streaming/components/token-stream.ts +53 -0
- package/templates/gallery/modules/suspense/components/slow-fact.ts +20 -0
- package/templates/gallery/modules/todo/components/todo-app.ts +8 -4
- package/templates/gallery/modules/websockets/components/ws-echo.ts +5 -3
- package/templates/gallery/test/auth/auth.test.ts +81 -0
- package/templates/public/favicon.svg +10 -3
- package/templates/scripts/clear-api-gallery.mjs +55 -0
- package/templates/scripts/clear-gallery.mjs +143 -24
- package/lib/lean-copy.js +0 -43
- package/lib/saas-template.js +0 -568
|
@@ -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
|
-
|
|
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
|
|
29
|
+
<p class="text-muted-foreground text-sm">
|
|
29
30
|
Typed with <code class="font-mono">PageProps<'/features/routing/[id]'></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
|
|
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
|
-
|
|
12
|
-
|
|
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
|
|
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<'/features/routing/[id]'></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
|
|
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
|
|
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,21 +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
|
-
|
|
10
|
-
|
|
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
|
-
chain that runs around it on every boundary. The auth middleware
|
|
14
|
-
|
|
14
|
+
chain that runs around it on every boundary. The auth middleware reads the
|
|
15
|
+
real signed session (from the <a class="text-primary underline underline-offset-2" href="/features/auth">auth card</a>) and
|
|
16
|
+
sets the caller on the request context (read back with <code class="font-mono">actionContext()</code>),
|
|
15
17
|
or 401s before the action runs. The action threads
|
|
16
18
|
<code class="font-mono">actionSignal()</code>, the request AbortSignal, through
|
|
17
19
|
its work so a client disconnect or a superseded render stops it early.
|
|
18
20
|
</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>
|
|
19
22
|
<server-greeter></server-greeter>
|
|
20
23
|
`;
|
|
21
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
|
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
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Stream updates: the <webjs-stream> element + renderStream() (#248). It is the
|
|
2
|
+
// element-level DOM-update grammar (Turbo Streams parity): a self-applying
|
|
3
|
+
// element that clones its <template> and runs ONE native DOM method (append /
|
|
4
|
+
// prepend / before / after / replace / update / remove) against a target id, then
|
|
5
|
+
// removes itself. See modules/stream/components/stream-demo.ts.
|
|
6
|
+
//
|
|
7
|
+
// How it differs from the neighbours:
|
|
8
|
+
// - The client router swaps a whole page (a navigation to a different URL).
|
|
9
|
+
// - A <webjs-frame> refreshes one URL-addressable region in place.
|
|
10
|
+
// - <webjs-stream> is finer still: it mutates individual ELEMENTS by id (add a
|
|
11
|
+
// row, remove a row, replace one node), with no region redraw and no
|
|
12
|
+
// component re-render. Reach for it when a signal re-render or a frame swap
|
|
13
|
+
// would be too coarse (a chat append, an optimistic row removal, a toast).
|
|
14
|
+
//
|
|
15
|
+
// Three delivery paths share this one applier: the client renderStream() below;
|
|
16
|
+
// a content-negotiated <form> response the router applies surgically (JS off
|
|
17
|
+
// degrades to a normal round-trip); and a live channel (a broadcast() / connectWS
|
|
18
|
+
// message applied with renderStream() from a WS handler, see the WebSockets card).
|
|
19
|
+
import { html } from '@webjsdev/core';
|
|
20
|
+
import type { Metadata } from '@webjsdev/core';
|
|
21
|
+
import { pageHeading, lede } from '#lib/utils/ui.ts';
|
|
22
|
+
import '#modules/stream/components/stream-demo.ts';
|
|
23
|
+
|
|
24
|
+
export const metadata: Metadata = { title: 'Stream updates (webjs-stream) | features' };
|
|
25
|
+
|
|
26
|
+
export default function StreamExample() {
|
|
27
|
+
return html`
|
|
28
|
+
${pageHeading('Stream updates')}
|
|
29
|
+
${lede(html`
|
|
30
|
+
<code class="font-mono">renderStream()</code> applies a
|
|
31
|
+
<code class="font-mono"><webjs-stream action="..." target="..."></code>
|
|
32
|
+
payload: a surgical, element-level DOM update by id. Each button below mutates
|
|
33
|
+
the live list in place, and the component never re-renders (contrast a frame,
|
|
34
|
+
which swaps a whole region, and the client router, which swaps a whole page).
|
|
35
|
+
`)}
|
|
36
|
+
<stream-demo></stream-demo>
|
|
37
|
+
<p class="text-muted-foreground text-sm mt-6">
|
|
38
|
+
The same grammar arrives over HTTP (a content-negotiated
|
|
39
|
+
<code class="font-mono"><form></code> response the router applies
|
|
40
|
+
surgically, degrading to a full round-trip with JS off) and over a live
|
|
41
|
+
channel (a <code class="font-mono">broadcast()</code> message applied with
|
|
42
|
+
<code class="font-mono">renderStream()</code> from a
|
|
43
|
+
<code class="font-mono">connectWS</code> handler, see the WebSockets card).
|
|
44
|
+
</p>
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Streaming server-action results (#489). A `'use server'` action that returns
|
|
2
|
+
// an async generator streams its chunks over the one RPC response; the call
|
|
3
|
+
// site consumes them with `for await`, rendering each as it arrives. This is
|
|
4
|
+
// the token-by-token shape (LLM output, a log tail, a DB cursor) over the
|
|
5
|
+
// normal action call site, back-pressured and cancelled on client disconnect.
|
|
6
|
+
// Contrast with a route.ts that hands back a raw HTTP ReadableStream: this rides
|
|
7
|
+
// the typed action mechanism, no hand-written fetch.
|
|
8
|
+
import { html } from '@webjsdev/core';
|
|
9
|
+
import type { Metadata } from '@webjsdev/core';
|
|
10
|
+
import { pageHeading, lede } from '#lib/utils/ui.ts';
|
|
11
|
+
import '#modules/streaming/components/token-stream.ts';
|
|
12
|
+
|
|
13
|
+
export const metadata: Metadata = { title: 'Streaming actions (for await) | features' };
|
|
14
|
+
|
|
15
|
+
export default function StreamingExample() {
|
|
16
|
+
return html`
|
|
17
|
+
${pageHeading('Streaming actions')}
|
|
18
|
+
${lede(html`
|
|
19
|
+
A <code class="font-mono">'use server'</code> action that returns an
|
|
20
|
+
<code class="font-mono">async function*</code> streams each
|
|
21
|
+
<code class="font-mono">yield</code> over the single RPC response. The call
|
|
22
|
+
site consumes it with <code class="font-mono">for await (const chunk of await streamTokens())</code>,
|
|
23
|
+
so tokens render as they arrive instead of waiting for the whole result.
|
|
24
|
+
`)}
|
|
25
|
+
<p class="text-muted-foreground mb-6 text-sm">
|
|
26
|
+
Detection is on the return value (no config export), and a streamed result
|
|
27
|
+
is never cached, ETagged, or seeded. The source generator is cancelled if
|
|
28
|
+
the client navigates away mid-stream.
|
|
29
|
+
</p>
|
|
30
|
+
<token-stream></token-stream>
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Component-level <webjs-suspense> streaming (#471). Page-level Suspense (the
|
|
2
|
+
// async-render demo) streams a region of the PAGE; <webjs-suspense> is the
|
|
3
|
+
// element that wraps one or more COMPONENTS, flushes its .fallback on the first
|
|
4
|
+
// byte, and streams the resolved content in. It is the only way to show a
|
|
5
|
+
// first-paint fallback for a SLOW component, and the deliberate choice when
|
|
6
|
+
// blocking the first byte on that component's data would hurt. Plain async
|
|
7
|
+
// render() blocks SSR so its data is in the first paint with NO fallback (right
|
|
8
|
+
// for fast data); reach for <webjs-suspense> when the data is genuinely slow.
|
|
9
|
+
import { html } from '@webjsdev/core';
|
|
10
|
+
import type { Metadata } from '@webjsdev/core';
|
|
11
|
+
import { pageHeading, lede } from '#lib/utils/ui.ts';
|
|
12
|
+
import '#modules/suspense/components/slow-fact.ts';
|
|
13
|
+
|
|
14
|
+
export const metadata: Metadata = { title: 'Suspense boundary (<webjs-suspense>) | features' };
|
|
15
|
+
|
|
16
|
+
export default function SuspenseExample() {
|
|
17
|
+
return html`
|
|
18
|
+
${pageHeading('Suspense boundary')}
|
|
19
|
+
${lede(html`
|
|
20
|
+
The slow component below is wrapped in
|
|
21
|
+
<code class="font-mono"><webjs-suspense></code>. Its fallback shows on
|
|
22
|
+
the first byte and the resolved content streams in when the slow await
|
|
23
|
+
settles. Reload to see the fallback, then the fact stream in.
|
|
24
|
+
`)}
|
|
25
|
+
<p class="text-muted-foreground mb-6 text-sm">
|
|
26
|
+
<code class="font-mono">.fallback</code> is a property hole (unquoted, per
|
|
27
|
+
invariant 4). Contrast with the
|
|
28
|
+
<a href="/features/async-render" class="text-primary no-underline font-medium">async render</a>
|
|
29
|
+
demo, where SSR blocks so the data is in the first paint with no fallback.
|
|
30
|
+
</p>
|
|
31
|
+
<webjs-suspense .fallback=${html`<p class="rounded-2xl border border-dashed border-border p-5 text-muted-foreground">loading the fact…</p>`}>
|
|
32
|
+
<slow-fact></slow-fact>
|
|
33
|
+
</webjs-suspense>
|
|
34
|
+
`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// View Transitions: opt in with a <meta name="view-transition" content="same-origin">
|
|
2
|
+
// (declared here via metadata.other, so it scopes to this page, not the whole
|
|
3
|
+
// app). When present, the client router wraps its DOM swap in the native View
|
|
4
|
+
// Transitions API, so a soft navigation cross-fades instead of snapping. Where
|
|
5
|
+
// startViewTransition is unavailable the swap runs synchronously with no flash
|
|
6
|
+
// and no throw, so it degrades cleanly. The <input> below is marked
|
|
7
|
+
// data-webjs-permanent with an id, so the router carries the SAME live node
|
|
8
|
+
// across the swap: type into it, navigate, and your text survives the transition.
|
|
9
|
+
import { html } from '@webjsdev/core';
|
|
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';
|
|
14
|
+
|
|
15
|
+
export const metadata: Metadata = {
|
|
16
|
+
title: 'View transitions (soft-nav cross-fade) | features',
|
|
17
|
+
other: { 'view-transition': 'same-origin' },
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default function ViewTransitionsExample() {
|
|
21
|
+
return html`
|
|
22
|
+
${pageHeading('View transitions')}
|
|
23
|
+
<div class="rounded-2xl bg-primary/10 border border-primary/30 p-6 mb-6">
|
|
24
|
+
<p class="text-foreground m-0">Page one. Navigate to page two: with the
|
|
25
|
+
<code class="font-mono"><meta name="view-transition"></code> opt-in,
|
|
26
|
+
the swap cross-fades.</p>
|
|
27
|
+
</div>
|
|
28
|
+
<label class="block mb-6">
|
|
29
|
+
<span class="text-muted-foreground text-sm">Type here, then navigate. This input is
|
|
30
|
+
<code class="font-mono">data-webjs-permanent</code>, so its value survives the swap:</span>
|
|
31
|
+
<input id="vt-note" data-webjs-permanent type="text" placeholder="type something…"
|
|
32
|
+
class=${inputClass('mt-2 block max-w-sm')} />
|
|
33
|
+
</label>
|
|
34
|
+
<div class="flex gap-3 items-center">
|
|
35
|
+
<a href="/features/view-transitions/second" class="${buttonClass()} no-underline">Go to page two</a>
|
|
36
|
+
<a href="/" class="text-muted-foreground no-underline font-medium text-sm hover:text-foreground transition-colors">Home</a>
|
|
37
|
+
</div>
|
|
38
|
+
<p class="text-muted-foreground text-sm mt-6">
|
|
39
|
+
Opt in app-wide instead by putting the meta on the root layout. Keep the
|
|
40
|
+
transition simple: toggling root attributes mid-nav can cause a one-frame
|
|
41
|
+
repaint flash on iOS WebKit.
|
|
42
|
+
</p>
|
|
43
|
+
`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The soft-navigation target. It carries the same view-transition opt-in so the
|
|
2
|
+
// cross-fade wraps the swap in BOTH directions, and the same
|
|
3
|
+
// data-webjs-permanent input (same id) so the router regrafts the one live node
|
|
4
|
+
// across the swap: the text you typed on page one is still here.
|
|
5
|
+
import { html } from '@webjsdev/core';
|
|
6
|
+
import type { Metadata } from '@webjsdev/core';
|
|
7
|
+
import { inputClass } from '#components/ui/input.ts';
|
|
8
|
+
import { pageHeading } from '#lib/utils/ui.ts';
|
|
9
|
+
|
|
10
|
+
export const metadata: Metadata = {
|
|
11
|
+
title: 'View transitions: page two | features',
|
|
12
|
+
other: { 'view-transition': 'same-origin' },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default function ViewTransitionsSecond() {
|
|
16
|
+
return html`
|
|
17
|
+
${pageHeading('Page two')}
|
|
18
|
+
<div class="rounded-2xl bg-foreground/5 border border-border p-6 mb-6">
|
|
19
|
+
<p class="text-foreground m-0">You arrived with a cross-fade, no full
|
|
20
|
+
reload. The input below is the same node from page one, regrafted across
|
|
21
|
+
the swap.</p>
|
|
22
|
+
</div>
|
|
23
|
+
<label class="block mb-6">
|
|
24
|
+
<span class="text-muted-foreground text-sm">Its value persisted across the transition:</span>
|
|
25
|
+
<input id="vt-note" data-webjs-permanent type="text" placeholder="type something…"
|
|
26
|
+
class=${inputClass('mt-2 block max-w-sm')} />
|
|
27
|
+
</label>
|
|
28
|
+
<a href="/features/view-transitions" class="text-primary no-underline font-medium">← Back to page one</a>
|
|
29
|
+
`;
|
|
30
|
+
}
|
|
@@ -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
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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: #
|
|
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: #
|
|
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-
|
|
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-
|
|
18
|
-
<a href="/" class="mt-8 inline-block
|
|
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="#
|
|
13
|
-
<text x="16" y="22" font-family="system-ui, sans-serif" font-size="18" font-weight="700" fill="#
|
|
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: '#
|
|
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="#
|
|
13
|
-
<text x="80" y="330" font-family="system-ui, sans-serif" font-size="88" font-weight="700" fill="#
|
|
14
|
-
<text x="80" y="410" font-family="system-ui, sans-serif" font-size="36" fill="#
|
|
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' },
|
|
@@ -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/robots.ts serves /robots.txt. The default export returns a string (served
|
|
5
2
|
// as text/plain) or an object. This example allows all crawlers and points them
|
|
6
3
|
// at the sitemap. Tighten the rules (Disallow paths) for a real app.
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// routes, or prune it (delete this file), then delete this marker line. webjs
|
|
2
|
-
// check fails while the marker remains.
|
|
3
|
-
//
|
|
4
1
|
// app/sitemap.ts serves /sitemap.xml. The default export is a (possibly async)
|
|
5
2
|
// server function; `sitemap(entries)` from @webjsdev/server serializes a
|
|
6
3
|
// spec-valid XML sitemap. In a real app, build the entries from your content
|
|
@@ -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/twitter-image.ts serves /twitter-image (the card image shown when the
|
|
5
2
|
// site is shared on Twitter/X). Its own route so the Twitter card can differ
|
|
6
3
|
// from the Open Graph image (opengraph-image.ts); when they are identical, drop
|
|
@@ -8,9 +5,9 @@
|
|
|
8
5
|
// roughly 1200x630. Reference it via metadata `twitter: { images: [...] }`.
|
|
9
6
|
export default function TwitterImage() {
|
|
10
7
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
|
11
|
-
<rect width="1200" height="630" fill="#
|
|
12
|
-
<text x="80" y="330" font-family="system-ui, sans-serif" font-size="88" font-weight="700" fill="#
|
|
13
|
-
<text x="80" y="410" font-family="system-ui, sans-serif" font-size="36" fill="#
|
|
8
|
+
<rect width="1200" height="630" fill="#1e2226"/>
|
|
9
|
+
<text x="80" y="330" font-family="system-ui, sans-serif" font-size="88" font-weight="700" fill="#dee2e6">My App</text>
|
|
10
|
+
<text x="80" y="410" font-family="system-ui, sans-serif" font-size="36" fill="#94989c">Build on the platform, not against it</text>
|
|
14
11
|
</svg>`;
|
|
15
12
|
return new Response(svg, {
|
|
16
13
|
headers: { 'content-type': 'image/svg+xml', 'cache-control': 'public, max-age=3600' },
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* badgeClass: the gallery's badge/pill design token, built on @webjsdev/ui.
|
|
3
|
+
*
|
|
4
|
+
* A `@webjsdev/ui` tier-1 CLASS HELPER (like buttonClass): it returns a Tailwind
|
|
5
|
+
* class string you spread onto a NATIVE inline element, almost always a `<span>`
|
|
6
|
+
* (a static label) or an `<a>` (a linked tag). Native markup means no wrapper
|
|
7
|
+
* component and correct semantics for free:
|
|
8
|
+
*
|
|
9
|
+
* <span class=${badgeClass()}>Signed in</span>
|
|
10
|
+
* <span class=${badgeClass({ variant: 'outline' })}>Example app</span>
|
|
11
|
+
*
|
|
12
|
+
* OWN-AND-THEME: this is `@webjsdev/ui`'s badge THEMED to this scaffold's two
|
|
13
|
+
* real uses, NOT the fuller shadcn badge (solid primary, secondary, destructive,
|
|
14
|
+
* ghost, link). Keep only the variants you use, styled to your look. `default`
|
|
15
|
+
* is a soft primary pill (a status chip); `outline` is a quiet muted tag.
|
|
16
|
+
*
|
|
17
|
+
* A11y: a static badge is a plain non-focusable `<span>`. Make it an `<a>` only
|
|
18
|
+
* when it links somewhere.
|
|
19
|
+
*
|
|
20
|
+
* Design tokens used (app/layout.ts): --color-primary, --color-border,
|
|
21
|
+
* --color-muted-foreground.
|
|
22
|
+
*/
|
|
23
|
+
import { cn } from '#lib/utils/cn.ts';
|
|
24
|
+
|
|
25
|
+
// Shared by every variant: an inline pill that hugs its content and never wraps.
|
|
26
|
+
const BASE = 'inline-flex w-fit shrink-0 items-center whitespace-nowrap';
|
|
27
|
+
|
|
28
|
+
const VARIANTS = {
|
|
29
|
+
// A soft, tinted primary chip. Used for a status ("Signed in").
|
|
30
|
+
default: 'rounded-full bg-primary/15 text-primary px-2.5 py-1 text-xs font-medium',
|
|
31
|
+
// A tiny, quiet outline tag. Used to label a kind of thing ("Example app").
|
|
32
|
+
outline:
|
|
33
|
+
'rounded border border-border text-muted-foreground px-1.5 py-0.5 text-[0.6rem] font-semibold uppercase tracking-wider',
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export type BadgeVariant = keyof typeof VARIANTS;
|
|
37
|
+
|
|
38
|
+
/** Compose the Tailwind classes for a badge/pill. Append your own layout as needed. */
|
|
39
|
+
export function badgeClass(opts: { variant?: BadgeVariant } = {}): string {
|
|
40
|
+
return cn(BASE, VARIANTS[opts.variant ?? 'default']);
|
|
41
|
+
}
|