@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.
- package/lib/create.js +109 -141
- 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 +28 -4
- package/templates/.agents/skills/webjs/references/components.md +82 -2
- package/templates/.agents/skills/webjs/references/data-and-actions.md +19 -2
- 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/styling.md +82 -2
- package/templates/AGENTS.md +12 -5
- package/templates/gallery/app/apple-icon.ts +2 -5
- package/templates/gallery/app/examples/layout.ts +7 -2
- 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 +2 -1
- package/templates/gallery/app/features/auth/dashboard/page.ts +4 -2
- package/templates/gallery/app/features/auth/dashboard/settings/page.ts +2 -1
- package/templates/gallery/app/features/auth/login/middleware.ts +15 -0
- package/templates/gallery/app/features/auth/login/page.ts +7 -4
- package/templates/gallery/app/features/auth/page.ts +6 -5
- package/templates/gallery/app/features/auth/signup/middleware.ts +11 -0
- package/templates/gallery/app/features/auth/signup/page.ts +7 -4
- 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 +16 -8
- package/templates/gallery/app/features/layout.ts +60 -5
- 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 +5 -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 +4 -3
- package/templates/gallery/app/features/streaming/page.ts +4 -3
- package/templates/gallery/app/features/suspense/page.ts +4 -3
- package/templates/gallery/app/features/view-transitions/page.ts +6 -3
- package/templates/gallery/app/features/view-transitions/second/page.ts +4 -2
- 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/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/components/greeter.ts +7 -4
- package/templates/gallery/modules/stream/components/stream-demo.ts +10 -5
- package/templates/gallery/modules/streaming/components/token-stream.ts +10 -3
- package/templates/gallery/modules/suspense/components/slow-fact.ts +2 -1
- package/templates/gallery/modules/todo/components/todo-app.ts +8 -4
- package/templates/gallery/modules/websockets/components/ws-echo.ts +5 -3
- package/templates/public/favicon.svg +10 -3
- package/templates/scripts/clear-gallery.mjs +126 -19
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { html } from '@webjsdev/core';
|
|
2
|
+
import { cardClass } from '#components/ui/card.ts';
|
|
3
|
+
import { inputClass } from '#components/ui/input.ts';
|
|
4
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
2
5
|
|
|
3
6
|
export const metadata = { title: 'Log in' };
|
|
4
7
|
|
|
5
|
-
const inputCls =
|
|
8
|
+
const inputCls = inputClass();
|
|
6
9
|
|
|
7
10
|
// A failed sign-in 302s back here with ?error=... (createAuth is configured with
|
|
8
11
|
// pages.error: '/features/auth/login' in modules/auth/auth.server.ts). Map the
|
|
@@ -21,7 +24,7 @@ export default function LoginPage({ searchParams }: { searchParams: { error?: st
|
|
|
21
24
|
<h1 class="text-h2 font-bold mb-2">Sign in</h1>
|
|
22
25
|
<p class="text-muted-foreground mb-5">Welcome back: log in to continue.</p>
|
|
23
26
|
${error ? html`<p role="alert" class="mb-4 text-sm text-destructive">${error}</p>` : ''}
|
|
24
|
-
<form method="POST" action="/api/auth/signin/credentials" class="grid gap-4 p-5
|
|
27
|
+
<form method="POST" action="/api/auth/signin/credentials" class="${cardClass()} grid gap-4 p-5">
|
|
25
28
|
<!-- createAuth reads redirectTo from the posted form and 302s there after a successful signin. -->
|
|
26
29
|
<input type="hidden" name="redirectTo" value="/features/auth/dashboard">
|
|
27
30
|
<div class="grid gap-1.5">
|
|
@@ -32,9 +35,9 @@ export default function LoginPage({ searchParams }: { searchParams: { error?: st
|
|
|
32
35
|
<label for="password" class="text-[13px] font-medium text-muted-foreground">Password</label>
|
|
33
36
|
<input id="password" name="password" type="password" required class=${inputCls} />
|
|
34
37
|
</div>
|
|
35
|
-
<button type="submit" class="justify-self-start
|
|
38
|
+
<button type="submit" class="${buttonClass()} justify-self-start">Sign in</button>
|
|
36
39
|
</form>
|
|
37
|
-
<p class="text-sm text-muted-foreground mt-4">Don't have an account? <a href="/features/auth/signup" class="text-primary">Sign up</a></p>
|
|
40
|
+
<p class="text-sm text-muted-foreground mt-4">Don't have an account? <a href="/features/auth/signup" class="text-primary underline underline-offset-2">Sign up</a></p>
|
|
38
41
|
</div>
|
|
39
42
|
`;
|
|
40
43
|
}
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
// line is the real query during SSR and a safe RPC stub on the client.
|
|
11
11
|
import { html } from '@webjsdev/core';
|
|
12
12
|
import type { Metadata } from '@webjsdev/core';
|
|
13
|
+
import { pageHeading, lede } from '#lib/utils/ui.ts';
|
|
13
14
|
import { currentUser } from '#modules/auth/queries/current-user.server.ts';
|
|
14
15
|
|
|
15
16
|
export const metadata: Metadata = { title: 'Auth (login + protected route) | features' };
|
|
@@ -17,16 +18,16 @@ export const metadata: Metadata = { title: 'Auth (login + protected route) | fea
|
|
|
17
18
|
export default async function AuthExample() {
|
|
18
19
|
const user = await currentUser();
|
|
19
20
|
return html`
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
${pageHeading('Auth')}
|
|
22
|
+
${lede(html`Password login on <code>createAuth</code>, a signed session cookie, and a protected <code>/features/auth/dashboard</code> that redirects anonymous visitors to login.`)}
|
|
22
23
|
|
|
23
24
|
${user
|
|
24
25
|
? html`
|
|
25
26
|
<p class="mb-4">Signed in as <strong>${user.name || user.email}</strong>.</p>
|
|
26
|
-
<p><a class="text-primary" href="/features/auth/dashboard">Open the protected dashboard</a></p>`
|
|
27
|
+
<p><a class="text-primary underline underline-offset-2" href="/features/auth/dashboard">Open the protected dashboard</a></p>`
|
|
27
28
|
: html`
|
|
28
|
-
<p class="mb-4">You are signed out. <a class="text-primary" href="/features/auth/dashboard">Visiting the dashboard</a> bounces you to login.</p>
|
|
29
|
-
<p class="flex gap-4"><a class="text-primary" href="/features/auth/login">Log in</a><a class="text-primary" href="/features/auth/signup">Create an account</a></p>`}
|
|
29
|
+
<p class="mb-4">You are signed out. <a class="text-primary underline underline-offset-2" href="/features/auth/dashboard">Visiting the dashboard</a> bounces you to login.</p>
|
|
30
|
+
<p class="flex gap-4"><a class="text-primary underline underline-offset-2" href="/features/auth/login">Log in</a><a class="text-primary underline underline-offset-2" href="/features/auth/signup">Create an account</a></p>`}
|
|
30
31
|
|
|
31
32
|
<p class="text-muted-foreground text-sm mt-6">The gate is <code class="font-mono">app/features/auth/dashboard/middleware.ts</code> calling <code class="font-mono">auth(req)</code>; the login form posts to the <code class="font-mono">app/api/auth/[...path]</code> handler; OAuth (GitHub / Google) activates once you set the matching env vars.</p>
|
|
32
33
|
`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { auth } from '#modules/auth/auth.server.ts';
|
|
2
|
+
|
|
3
|
+
// Like the login middleware: an already-signed-in visitor has no reason to see
|
|
4
|
+
// the signup form, so send them to the dashboard before the page renders.
|
|
5
|
+
export default async function redirectIfSignedIn(req: Request, next: () => Promise<Response>) {
|
|
6
|
+
const session = await auth(req);
|
|
7
|
+
if (session?.user) {
|
|
8
|
+
return new Response(null, { status: 302, headers: { location: '/features/auth/dashboard' } });
|
|
9
|
+
}
|
|
10
|
+
return next();
|
|
11
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { html } from '@webjsdev/core';
|
|
2
|
+
import { cardClass } from '#components/ui/card.ts';
|
|
3
|
+
import { inputClass } from '#components/ui/input.ts';
|
|
4
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
2
5
|
import { signup } from '#modules/auth/actions/signup.server.ts';
|
|
3
6
|
|
|
4
7
|
export const metadata = { title: 'Sign up' };
|
|
5
8
|
|
|
6
|
-
const inputCls =
|
|
9
|
+
const inputCls = inputClass();
|
|
7
10
|
|
|
8
11
|
// Page server action: handles the POST from the form below. With JS disabled this
|
|
9
12
|
// is a plain <form> round-trip; with JS the client router swaps the 422 re-render
|
|
@@ -34,7 +37,7 @@ export default function SignupPage({ actionData }: { actionData?: { fieldErrors?
|
|
|
34
37
|
<div class="max-w-[420px] mx-auto">
|
|
35
38
|
<h1 class="text-h2 font-bold mb-2">Create an account</h1>
|
|
36
39
|
<p class="text-muted-foreground mb-5">Get started with your new workspace.</p>
|
|
37
|
-
<form method="POST" class="grid gap-4 p-5
|
|
40
|
+
<form method="POST" class="${cardClass()} grid gap-4 p-5">
|
|
38
41
|
<div class="grid gap-1.5">
|
|
39
42
|
<label for="name" class="text-[13px] font-medium text-muted-foreground">Name</label>
|
|
40
43
|
<input id="name" name="name" type="text" value=${values.name || ''} required class=${inputCls} placeholder="Ada Lovelace" />
|
|
@@ -50,9 +53,9 @@ export default function SignupPage({ actionData }: { actionData?: { fieldErrors?
|
|
|
50
53
|
<input id="password" name="password" type="password" minlength="8" required class=${inputCls} />
|
|
51
54
|
${errors.password ? html`<p class="m-0 text-[12.5px] text-destructive">${errors.password}</p>` : ''}
|
|
52
55
|
</div>
|
|
53
|
-
<button type="submit" class="justify-self-start
|
|
56
|
+
<button type="submit" class="${buttonClass()} justify-self-start">Create account</button>
|
|
54
57
|
</form>
|
|
55
|
-
<p class="text-sm text-muted-foreground mt-4">Already have an account? <a href="/features/auth/login" class="text-primary">Log in</a></p>
|
|
58
|
+
<p class="text-sm text-muted-foreground mt-4">Already have an account? <a href="/features/auth/login" class="text-primary underline underline-offset-2">Log in</a></p>
|
|
56
59
|
</div>
|
|
57
60
|
`;
|
|
58
61
|
}
|
|
@@ -3,14 +3,15 @@
|
|
|
3
3
|
// rendered scoped to this boundary, so outer layouts stay alive. The default
|
|
4
4
|
// export receives { error, ...ctx }; in production only error.message is sent.
|
|
5
5
|
import { html } from '@webjsdev/core';
|
|
6
|
+
import { pageHeading, lede } from '#lib/utils/ui.ts';
|
|
6
7
|
|
|
7
8
|
export default function BoundariesError({ error }: { error: Error }) {
|
|
8
9
|
return html`
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
${pageHeading('Something went wrong')}
|
|
11
|
+
${lede(html`
|
|
11
12
|
This segment's <code class="font-mono">error.ts</code> boundary caught a
|
|
12
13
|
render error: <code class="font-mono">${error?.message ?? 'unknown'}</code>.
|
|
13
|
-
|
|
14
|
-
<p><a class="text-primary" href="/features/boundaries">Back to boundaries</a></p>
|
|
14
|
+
`)}
|
|
15
|
+
<p><a class="text-primary underline underline-offset-2" href="/features/boundaries">Back to boundaries</a></p>
|
|
15
16
|
`;
|
|
16
17
|
}
|
|
@@ -4,15 +4,16 @@
|
|
|
4
4
|
// forbidden.ts higher up the tree. Keep the message actionable for an
|
|
5
5
|
// authenticated user who lacks permission.
|
|
6
6
|
import { html } from '@webjsdev/core';
|
|
7
|
+
import { pageHeading, lede } from '#lib/utils/ui.ts';
|
|
7
8
|
|
|
8
9
|
export default function Forbidden() {
|
|
9
10
|
return html`
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
${pageHeading('403 Forbidden')}
|
|
12
|
+
${lede(html`
|
|
12
13
|
You are signed in but do not have permission to view this page. This is the
|
|
13
14
|
nearest <code class="font-mono">forbidden.ts</code> boundary, rendered
|
|
14
15
|
because the page threw <code class="font-mono">forbidden()</code>.
|
|
15
|
-
|
|
16
|
-
<p><a class="text-primary" href="/features/boundaries">Back to boundaries</a></p>
|
|
16
|
+
`)}
|
|
17
|
+
<p><a class="text-primary underline underline-offset-2" href="/features/boundaries">Back to boundaries</a></p>
|
|
17
18
|
`;
|
|
18
19
|
}
|
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
// /features/boundaries/ (try /features/boundaries/does-not-exist). Nearest wins,
|
|
4
4
|
// so this beats the root not-found for anything in this segment.
|
|
5
5
|
import { html } from '@webjsdev/core';
|
|
6
|
+
import { pageHeading, lede } from '#lib/utils/ui.ts';
|
|
6
7
|
|
|
7
8
|
export default function BoundariesNotFound() {
|
|
8
9
|
return html`
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
${pageHeading('404: Not here')}
|
|
11
|
+
${lede(html`
|
|
11
12
|
This segment's <code class="font-mono">not-found.ts</code> boundary rendered,
|
|
12
13
|
because a page threw <code class="font-mono">notFound()</code> or the URL
|
|
13
14
|
matched nothing under this segment.
|
|
14
|
-
|
|
15
|
-
<p><a class="text-primary" href="/features/boundaries">Back to boundaries</a></p>
|
|
15
|
+
`)}
|
|
16
|
+
<p><a class="text-primary underline underline-offset-2" href="/features/boundaries">Back to boundaries</a></p>
|
|
16
17
|
`;
|
|
17
18
|
}
|
|
@@ -16,47 +16,48 @@
|
|
|
16
16
|
// /features/boundaries/private throws unauthorized() -> private/unauthorized.ts
|
|
17
17
|
import { html } from '@webjsdev/core';
|
|
18
18
|
import type { Metadata } from '@webjsdev/core';
|
|
19
|
+
import { pageHeading, lede } from '#lib/utils/ui.ts';
|
|
19
20
|
|
|
20
21
|
export const metadata: Metadata = { title: 'Boundaries (throws + boundary files) | features' };
|
|
21
22
|
|
|
22
23
|
export default function BoundariesExample() {
|
|
23
24
|
return html`
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
${pageHeading('Boundaries')}
|
|
26
|
+
${lede(html`
|
|
26
27
|
Throw a control-flow function from a page (or a page
|
|
27
28
|
<code class="font-mono">action</code>) to short-circuit the render. The
|
|
28
29
|
framework catches it and renders the nearest matching boundary file in the
|
|
29
30
|
route's chain, innermost wins.
|
|
30
|
-
|
|
31
|
+
`)}
|
|
31
32
|
<ul class="list-disc pl-5 mb-4">
|
|
32
33
|
<li>
|
|
33
|
-
<a class="text-primary" href="/features/boundaries/gated">/features/boundaries/gated</a>
|
|
34
|
+
<a class="text-primary underline underline-offset-2" href="/features/boundaries/gated">/features/boundaries/gated</a>
|
|
34
35
|
throws <code class="font-mono">forbidden()</code>, caught by
|
|
35
36
|
<code class="font-mono">gated/forbidden.ts</code> (403).
|
|
36
37
|
</li>
|
|
37
38
|
<li>
|
|
38
|
-
<a class="text-primary" href="/features/boundaries/private">/features/boundaries/private</a>
|
|
39
|
+
<a class="text-primary underline underline-offset-2" href="/features/boundaries/private">/features/boundaries/private</a>
|
|
39
40
|
throws <code class="font-mono">unauthorized()</code>, caught by
|
|
40
41
|
<code class="font-mono">private/unauthorized.ts</code> (401).
|
|
41
42
|
</li>
|
|
42
43
|
<li>
|
|
43
|
-
<a class="text-primary" href="/features/boundaries/crash">/features/boundaries/crash</a>
|
|
44
|
+
<a class="text-primary underline underline-offset-2" href="/features/boundaries/crash">/features/boundaries/crash</a>
|
|
44
45
|
throws a render error, caught by this segment's
|
|
45
46
|
<code class="font-mono">error.ts</code> (500).
|
|
46
47
|
</li>
|
|
47
48
|
<li>
|
|
48
|
-
<a class="text-primary" href="/features/boundaries/does-not-exist">/features/boundaries/does-not-exist</a>
|
|
49
|
+
<a class="text-primary underline underline-offset-2" href="/features/boundaries/does-not-exist">/features/boundaries/does-not-exist</a>
|
|
49
50
|
matches nothing, caught by the nearest
|
|
50
51
|
<code class="font-mono">not-found.ts</code> (404).
|
|
51
52
|
</li>
|
|
52
53
|
</ul>
|
|
53
|
-
<p class="text-muted-foreground text-sm
|
|
54
|
+
<p class="text-muted-foreground text-sm">
|
|
54
55
|
<code class="font-mono">forbidden()</code> is for an authenticated user who
|
|
55
56
|
lacks permission (403); <code class="font-mono">unauthorized()</code> is for
|
|
56
57
|
a request that is not authenticated at all (401). Both import from
|
|
57
58
|
<code class="font-mono">@webjsdev/core</code> and are thrown, never returned.
|
|
58
59
|
</p>
|
|
59
|
-
<p class="text-muted-foreground text-sm
|
|
60
|
+
<p class="text-muted-foreground text-sm">
|
|
60
61
|
Same throw model as <code class="font-mono">notFound()</code> (renders the
|
|
61
62
|
nearest <code class="font-mono">not-found.ts</code>) and
|
|
62
63
|
<code class="font-mono">redirect(url)</code> (sends an HTTP 3xx). Inside a
|
|
@@ -71,6 +72,6 @@ export default function BoundariesExample() {
|
|
|
71
72
|
and <code class="font-mono">app/global-not-found.ts</code> (a 404 for a URL
|
|
72
73
|
that matches nothing anywhere).
|
|
73
74
|
</p>
|
|
74
|
-
<p class="mt-3"><a class="text-primary" href="/">Back to the gallery</a></p>
|
|
75
|
+
<p class="mt-3"><a class="text-primary underline underline-offset-2" href="/">Back to the gallery</a></p>
|
|
75
76
|
`;
|
|
76
77
|
}
|
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
// in place of the page that threw. Nearest wins: this one (inside private/) beats
|
|
4
4
|
// any unauthorized.ts higher up the tree. A real one usually links to sign-in.
|
|
5
5
|
import { html } from '@webjsdev/core';
|
|
6
|
+
import { pageHeading, lede } from '#lib/utils/ui.ts';
|
|
6
7
|
|
|
7
8
|
export default function Unauthorized() {
|
|
8
9
|
return html`
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
${pageHeading('401 Unauthorized')}
|
|
11
|
+
${lede(html`
|
|
11
12
|
You need to sign in to view this page. This is the nearest
|
|
12
13
|
<code class="font-mono">unauthorized.ts</code> boundary, rendered because the
|
|
13
14
|
page threw <code class="font-mono">unauthorized()</code>.
|
|
14
|
-
|
|
15
|
-
<p><a class="text-primary" href="/features/boundaries">Back to boundaries</a></p>
|
|
15
|
+
`)}
|
|
16
|
+
<p><a class="text-primary underline underline-offset-2" href="/features/boundaries">Back to boundaries</a></p>
|
|
16
17
|
`;
|
|
17
18
|
}
|
|
@@ -5,19 +5,20 @@
|
|
|
5
5
|
// Open this page in two browser tabs and send: both see every message.
|
|
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/broadcast/components/broadcast-feed.ts';
|
|
9
10
|
|
|
10
11
|
export const metadata: Metadata = { title: 'Broadcast (fan-out to all clients) | features' };
|
|
11
12
|
|
|
12
13
|
export default function BroadcastExample() {
|
|
13
14
|
return html`
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
${pageHeading('Broadcast')}
|
|
16
|
+
${lede(html`
|
|
16
17
|
Every message is fanned out to all connected clients via
|
|
17
18
|
<code class="font-mono">broadcast()</code>. Open this page in a second tab
|
|
18
19
|
and watch messages appear in both. Single-instance by default; wire Redis
|
|
19
20
|
to scale across processes.
|
|
20
|
-
|
|
21
|
+
`)}
|
|
21
22
|
<broadcast-feed></broadcast-feed>
|
|
22
23
|
`;
|
|
23
24
|
}
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// use HTTP Cache-Control + ETag (conditional GET).
|
|
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/caching/components/cache-buster.ts';
|
|
11
12
|
|
|
12
13
|
export const metadata: Metadata = { title: 'Caching (revalidate) | features' };
|
|
@@ -19,11 +20,11 @@ export default function CachingExample() {
|
|
|
19
20
|
// seconds, so this value is frozen until the window elapses.
|
|
20
21
|
const renderedAt = new Date().toLocaleTimeString('en-US', { hour12: false });
|
|
21
22
|
return html`
|
|
22
|
-
|
|
23
|
-
|
|
23
|
+
${pageHeading('Caching')}
|
|
24
|
+
${lede(html`
|
|
24
25
|
This page sets <code>export const revalidate = 10</code>, so its
|
|
25
26
|
server-rendered HTML is cached per URL for ten seconds.
|
|
26
|
-
|
|
27
|
+
`)}
|
|
27
28
|
<p class="mb-4">
|
|
28
29
|
Rendered at
|
|
29
30
|
<code class="font-mono text-primary">${renderedAt}</code>.
|
|
@@ -35,7 +36,7 @@ export default function CachingExample() {
|
|
|
35
36
|
<code>revalidateTag</code>, or a GET action's
|
|
36
37
|
<code>export const cache</code>.
|
|
37
38
|
</p>
|
|
38
|
-
<p class="text-muted-foreground text-sm
|
|
39
|
+
<p class="text-muted-foreground text-sm">
|
|
39
40
|
A mutation evicts the cache on demand. Click below (it calls
|
|
40
41
|
<code class="font-mono">revalidatePath('/features/caching')</code>), then refresh:
|
|
41
42
|
the timestamp updates immediately, even inside the 10s window, because the
|
|
@@ -7,24 +7,26 @@
|
|
|
7
7
|
// JS off, every link is a normal full-page navigation.
|
|
8
8
|
import { html } from '@webjsdev/core';
|
|
9
9
|
import type { Metadata } from '@webjsdev/core';
|
|
10
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
11
|
+
import { pageHeading, lede } from '#lib/utils/ui.ts';
|
|
10
12
|
import '#modules/client-router/components/router-controls.ts';
|
|
11
13
|
|
|
12
14
|
export const metadata: Metadata = { title: 'Client router (soft nav) | features' };
|
|
13
15
|
|
|
14
16
|
export default function ClientRouterExample() {
|
|
15
17
|
return html`
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
${pageHeading('Client router')}
|
|
19
|
+
${lede(html`
|
|
18
20
|
Navigate to the second page and back. With JS on it is a soft swap (no full
|
|
19
21
|
reload, scroll restored); open the network tab to see only a fragment
|
|
20
22
|
fetched, prefetched on hover. With JS off the same links do full-page
|
|
21
23
|
navigations. Nothing was imported to get this.
|
|
22
|
-
|
|
24
|
+
`)}
|
|
23
25
|
<div class="flex gap-3 items-center">
|
|
24
|
-
<a href="/features/client-router/second" class="
|
|
26
|
+
<a href="/features/client-router/second" class="${buttonClass()} no-underline">Go to page two</a>
|
|
25
27
|
<a href="/" class="text-muted-foreground no-underline font-medium text-sm hover:text-foreground transition-colors">Home</a>
|
|
26
28
|
</div>
|
|
27
|
-
<p class="text-muted-foreground text-sm mt-6
|
|
29
|
+
<p class="text-muted-foreground text-sm mt-6">Or drive it from JS with <code class="font-mono">navigate()</code> / <code class="font-mono">revalidate()</code>:</p>
|
|
28
30
|
<router-controls></router-controls>
|
|
29
31
|
<p class="text-muted-foreground text-sm mt-6">
|
|
30
32
|
Opt out app-wide with <code class="font-mono">{ "webjs": { "clientRouter": false } }</code>,
|
|
@@ -3,17 +3,18 @@
|
|
|
3
3
|
// its scroll position from the client-router snapshot cache.
|
|
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: 'Client router: page two | features' };
|
|
8
9
|
|
|
9
10
|
export default function ClientRouterSecond() {
|
|
10
11
|
return html`
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
${pageHeading('Page two')}
|
|
13
|
+
${lede(html`
|
|
13
14
|
You arrived here without a full reload. Press the browser Back button (or
|
|
14
15
|
the link below): the previous page and its scroll position are restored
|
|
15
16
|
from the snapshot cache.
|
|
16
|
-
|
|
17
|
+
`)}
|
|
17
18
|
<a href="/features/client-router" class="text-primary no-underline font-medium">← Back to page one</a>
|
|
18
19
|
`;
|
|
19
20
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
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/components/components/counter-card.ts';
|
|
4
5
|
import '#modules/components/components/reactive-meter.ts';
|
|
5
6
|
import '#modules/components/components/theme-context.ts';
|
|
@@ -9,8 +10,8 @@ export const metadata: Metadata = { title: 'Components (signals + slots) | featu
|
|
|
9
10
|
|
|
10
11
|
export default function ComponentsExample() {
|
|
11
12
|
return html`
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
${pageHeading('Components')}
|
|
14
|
+
${lede(html`The WebComponent factory, a reactive prop, an instance signal, and a slot.`)}
|
|
14
15
|
<counter-card label="Taps"><strong>A slotted title</strong></counter-card>
|
|
15
16
|
<p class="text-muted-foreground mt-6 mb-2">Shadow DOM (scoped <code class="font-mono">css</code>) plus the rest of the signals API (<code class="font-mono">computed</code>, <code class="font-mono">effect</code>, <code class="font-mono">batch</code>):</p>
|
|
16
17
|
<reactive-meter></reactive-meter>
|
|
@@ -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/directives/components/directive-demo.ts';
|
|
4
5
|
|
|
5
6
|
export const metadata: Metadata = { title: 'Directives (repeat + watch) | features' };
|
|
6
7
|
|
|
7
8
|
export default function DirectivesExample() {
|
|
8
9
|
return html`
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
${pageHeading('Directives')}
|
|
11
|
+
${lede(html`The lit-html directive set: <code>repeat</code> keys a reordering list so nodes are reused, and <code>watch(signal)</code> swaps one node without a full re-render.`)}
|
|
11
12
|
<directive-demo></directive-demo>
|
|
12
13
|
`;
|
|
13
14
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// an app-root env.ts (a schema or a validator fn) that fails fast.
|
|
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: 'Env vars (public vs server) | features' };
|
|
11
12
|
|
|
@@ -16,11 +17,11 @@ export default function EnvExample() {
|
|
|
16
17
|
// add WEBJS_PUBLIC_APP_NAME=... to .env, which demonstrates the default.
|
|
17
18
|
const publicName = process.env.WEBJS_PUBLIC_APP_NAME || '(unset, add WEBJS_PUBLIC_APP_NAME to .env)';
|
|
18
19
|
return html`
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
${pageHeading('Environment variables')}
|
|
21
|
+
${lede(html`
|
|
21
22
|
Read on the server during SSR. Only <code>WEBJS_PUBLIC_</code>-prefixed
|
|
22
23
|
names are exposed to the browser; the rest stay server-side.
|
|
23
|
-
|
|
24
|
+
`)}
|
|
24
25
|
<ul class="list-disc pl-5 mb-4 space-y-1">
|
|
25
26
|
<li><code class="font-mono text-sm">NODE_ENV</code> = <span class="text-primary">${nodeEnv}</span> <span class="text-muted-foreground text-sm">(defined both sides)</span></li>
|
|
26
27
|
<li><code class="font-mono text-sm">WEBJS_PUBLIC_APP_NAME</code> = <span class="text-primary">${publicName}</span></li>
|
|
@@ -5,6 +5,9 @@
|
|
|
5
5
|
// streams the file back through file/[key]/route.ts. Works with JS off; the
|
|
6
6
|
// client router applies the same flow in place with JS on.
|
|
7
7
|
import { html } from '@webjsdev/core';
|
|
8
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
9
|
+
import { cardClass } from '#components/ui/card.ts';
|
|
10
|
+
import { pageHeading, lede } from '#lib/utils/ui.ts';
|
|
8
11
|
import type { Metadata } from '@webjsdev/core';
|
|
9
12
|
import { storeUpload } from '#modules/file-storage/actions/store-upload.server.ts';
|
|
10
13
|
|
|
@@ -33,25 +36,25 @@ export default function FileStorageExample({
|
|
|
33
36
|
const name = (searchParams.name || '').trim();
|
|
34
37
|
const size = (searchParams.size || '').trim();
|
|
35
38
|
return html`
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
${pageHeading('File storage')}
|
|
40
|
+
${lede(html`
|
|
38
41
|
Upload a file: the bytes stream into the FileStore (a local
|
|
39
42
|
<code class="font-mono">.webjs/uploads</code> directory by default,
|
|
40
43
|
gitignored). Swap the backend for S3/R2 with one
|
|
41
44
|
<code class="font-mono">setFileStore()</code> call, no call-site change.
|
|
42
|
-
|
|
45
|
+
`)}
|
|
43
46
|
<form method="post" enctype="multipart/form-data" class="flex flex-wrap gap-3 items-center mb-4">
|
|
44
47
|
<input type="file" name="file" required aria-label="Choose a file to upload"
|
|
45
48
|
class="text-sm text-muted-foreground file:mr-3 file:px-3.5 file:py-2 file:rounded-xl file:border-0 file:bg-card file:border file:border-border file:text-foreground file:text-sm file:cursor-pointer" />
|
|
46
49
|
<button type="submit"
|
|
47
|
-
class
|
|
50
|
+
class=${buttonClass()}>Upload</button>
|
|
48
51
|
</form>
|
|
49
52
|
${actionData?.error
|
|
50
53
|
? html`<p class="text-destructive text-sm mb-4">${actionData.error}</p>`
|
|
51
54
|
: ''}
|
|
52
55
|
${key
|
|
53
56
|
? html`
|
|
54
|
-
<div class="px-4 py-3
|
|
57
|
+
<div class="${cardClass('rounded-xl')} px-4 py-3 text-sm">
|
|
55
58
|
Stored <span class="text-foreground font-medium">${name}</span>
|
|
56
59
|
<span class="text-muted-foreground">(${size} bytes)</span>
|
|
57
60
|
<a class="text-primary no-underline ml-2" href="/features/file-storage/file/${key}">download</a>
|
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
// On failure the framework re-renders at 422 with the result; on success it
|
|
7
7
|
// does a 303 Post-Redirect-Get, so we redirect to ?sent=1 to show a confirmation.
|
|
8
8
|
import { html } from '@webjsdev/core';
|
|
9
|
+
import { cardClass } from '#components/ui/card.ts';
|
|
10
|
+
import { inputClass } from '#components/ui/input.ts';
|
|
11
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
12
|
+
import { pageHeading } from '#lib/utils/ui.ts';
|
|
9
13
|
import type { Metadata } from '@webjsdev/core';
|
|
10
14
|
|
|
11
15
|
export const metadata: Metadata = { title: 'Forms (no-JS PE) | features' };
|
|
@@ -25,18 +29,18 @@ const field = (label: string, name: string, input: unknown, error?: string) => h
|
|
|
25
29
|
</div>
|
|
26
30
|
`;
|
|
27
31
|
|
|
28
|
-
const inputCls =
|
|
32
|
+
const inputCls = inputClass();
|
|
29
33
|
|
|
30
34
|
export default function FormsFeature({ searchParams, actionData }: { searchParams: Record<string, string | undefined>; actionData?: Result }) {
|
|
31
35
|
if (searchParams.sent) {
|
|
32
36
|
return html`
|
|
33
|
-
|
|
34
|
-
<div class="max-w-[460px] grid gap-3 p-6
|
|
37
|
+
${pageHeading('Forms')}
|
|
38
|
+
<div class="${cardClass()} max-w-[460px] grid gap-3 p-6 text-center">
|
|
35
39
|
<span class="mx-auto grid place-items-center w-12 h-12 rounded-2xl bg-primary/15 text-primary">
|
|
36
40
|
<svg viewBox="0 0 24 24" class="w-6 h-6 stroke-current fill-none" style="stroke-width:2.4;stroke-linecap:round;stroke-linejoin:round"><path d="m5 13 4 4L19 7"/></svg>
|
|
37
41
|
</span>
|
|
38
42
|
<p class="m-0 text-lg font-semibold text-foreground">Message sent</p>
|
|
39
|
-
<p class="m-0 text-sm text-muted-foreground">Thanks, we got it. <a class="text-primary" href="/features/forms">Send another</a>.</p>
|
|
43
|
+
<p class="m-0 text-sm text-muted-foreground">Thanks, we got it. <a class="text-primary underline underline-offset-2" href="/features/forms">Send another</a>.</p>
|
|
40
44
|
</div>
|
|
41
45
|
`;
|
|
42
46
|
}
|
|
@@ -45,11 +49,11 @@ export default function FormsFeature({ searchParams, actionData }: { searchParam
|
|
|
45
49
|
return html`
|
|
46
50
|
<h1 class="text-h2 font-bold mb-2">Forms</h1>
|
|
47
51
|
<p class="text-muted-foreground mb-5 max-w-[460px]">A real <code><form></code> posting to this page's <code>action</code>. It works with JS off; validation errors come back on <code>actionData</code>.</p>
|
|
48
|
-
<form method="post" action="" class="max-w-[460px] grid gap-4 p-5
|
|
52
|
+
<form method="post" action="" class="${cardClass()} max-w-[460px] grid gap-4 p-5">
|
|
49
53
|
${field('Name', 'name', html`<input id="name" name="name" value=${v.name ?? ''} class=${inputCls} placeholder="Ada Lovelace" />`, errs.name)}
|
|
50
54
|
${field('Email', 'email', html`<input id="email" name="email" type="email" value=${v.email ?? ''} class=${inputCls} placeholder="ada@example.com" />`, errs.email)}
|
|
51
55
|
${field('Message', 'message', html`<textarea id="message" name="message" rows="3" class=${inputCls} placeholder="Say hello...">${v.message ?? ''}</textarea>`, errs.message)}
|
|
52
|
-
<button type="submit" class="justify-self-start
|
|
56
|
+
<button type="submit" class="${buttonClass()} justify-self-start">Send message</button>
|
|
53
57
|
</form>
|
|
54
58
|
`;
|
|
55
59
|
}
|
|
@@ -20,6 +20,10 @@
|
|
|
20
20
|
// requirement. The frame element itself upgrades because the root layout ships a
|
|
21
21
|
// component (the theme toggle), so @webjsdev/core and the router load app-wide.
|
|
22
22
|
import { html } from '@webjsdev/core';
|
|
23
|
+
import { cardClass } from '#components/ui/card.ts';
|
|
24
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
25
|
+
import { cn } from '#lib/utils/cn.ts';
|
|
26
|
+
import { pageHeading, lede } from '#lib/utils/ui.ts';
|
|
23
27
|
import type { Metadata } from '@webjsdev/core';
|
|
24
28
|
import { filterTasks, normalizeStatus, type Status } from '#modules/frames/utils/tasks.ts';
|
|
25
29
|
|
|
@@ -30,10 +34,14 @@ export const metadata: Metadata = { title: 'Frames (webjs-frame partial swap) |
|
|
|
30
34
|
// link OUTSIDE the frame would drive it from anywhere via data-webjs-frame="tasks".
|
|
31
35
|
function filterTab(current: Status, status: Status, label: string) {
|
|
32
36
|
const active = current === status;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
// A segmented control: keep ONE variant (constant weight, border, padding) so
|
|
38
|
+
// toggling the active tab never reflows the row, overriding only the active
|
|
39
|
+
// colors with cn().
|
|
40
|
+
const cls = cn(
|
|
41
|
+
buttonClass({ variant: 'secondary', size: 'sm' }),
|
|
42
|
+
'no-underline',
|
|
43
|
+
active && 'bg-primary text-primary-foreground border-primary hover:border-primary',
|
|
44
|
+
);
|
|
37
45
|
return html`<a href="/features/frames?status=${status}" class=${cls}>${label}</a>`;
|
|
38
46
|
}
|
|
39
47
|
|
|
@@ -41,16 +49,16 @@ export default function FramesExample({ searchParams }: { searchParams: Record<s
|
|
|
41
49
|
const status = normalizeStatus(searchParams?.status);
|
|
42
50
|
const tasks = filterTasks(status);
|
|
43
51
|
return html`
|
|
44
|
-
|
|
45
|
-
|
|
52
|
+
${pageHeading('Frames')}
|
|
53
|
+
${lede(html`
|
|
46
54
|
Filter the list. With JS on, only the framed region swaps (the response is
|
|
47
55
|
just the frame's subtree, not the whole page) and the heading above never
|
|
48
56
|
re-renders. With JS off, the same links do full-page navigations. It is one
|
|
49
57
|
region refreshing independently of a navigation, which neither a page nor a
|
|
50
58
|
layout can express: a layout wraps routes with shared chrome and only
|
|
51
59
|
re-renders when you navigate, while a frame updates one region in place.
|
|
52
|
-
|
|
53
|
-
<webjs-frame id="tasks" class="block p-4
|
|
60
|
+
`)}
|
|
61
|
+
<webjs-frame id="tasks" class="${cardClass()} block p-4">
|
|
54
62
|
<div class="flex gap-2 mb-4">
|
|
55
63
|
${filterTab(status, 'all', 'All')}
|
|
56
64
|
${filterTab(status, 'active', 'Active')}
|