@webjsdev/cli 0.10.45 → 0.10.47
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/bin/webjs.js +15 -12
- 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/skills/webjs/references/ui-kit.md +8 -3
- package/templates/AGENTS.md +13 -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
|
@@ -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')}
|
|
@@ -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
|
-
//
|
|
4
|
-
//
|
|
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
|
-
<
|
|
10
|
-
|
|
51
|
+
<style>${SIDENAV_CSS}${STACK_CSS}</style>
|
|
52
|
+
<div class="lg:hidden mb-6">${backLink('/', html`← 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">← 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
|
-
|
|
35
|
-
|
|
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><title></code> is computed per URL. View source
|
|
38
39
|
to see the tag this page produced.
|
|
39
|
-
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
-
|
|
21
|
+
`)}
|
|
21
22
|
<rate-probe></rate-probe>
|
|
22
|
-
<p class="text-muted-foreground text-sm
|
|
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
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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,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
|
-
|
|
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
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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
28
|
-
|
|
28
|
+
${pageHeading('Stream updates')}
|
|
29
|
+
${lede(html`
|
|
29
30
|
<code class="font-mono">renderStream()</code> applies a
|
|
30
31
|
<code class="font-mono"><webjs-stream action="..." target="..."></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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
18
|
+
${pageHeading('Suspense boundary')}
|
|
19
|
+
${lede(html`
|
|
19
20
|
The slow component below is wrapped in
|
|
20
21
|
<code class="font-mono"><webjs-suspense></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
|
-
|
|
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
|
-
|
|
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"><meta name="view-transition"></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
|
|
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="
|
|
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
|
-
|
|
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
|
|
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">← 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
|
-
|
|
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
|
}
|