@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
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The gallery's demo index, ONE source of truth for both the home cards and the
|
|
3
|
+
* left sidebar (so they can never drift). A browser-safe data module (no server
|
|
4
|
+
* imports, no client globals): the home flattens the groups into its card grid,
|
|
5
|
+
* and <gallery-nav> renders them grouped. gallery:clear removes this module.
|
|
6
|
+
*/
|
|
7
|
+
export interface NavItem { href: string; title: string; blurb: string; }
|
|
8
|
+
export interface NavGroup { label: string; items: NavItem[]; }
|
|
9
|
+
|
|
10
|
+
export const FEATURE_GROUPS: NavGroup[] = [
|
|
11
|
+
{
|
|
12
|
+
label: 'Routing',
|
|
13
|
+
items: [
|
|
14
|
+
{ href: '/features/routing', title: 'Routing', blurb: 'A static route plus a dynamic [id] segment that reads params. The file-based router in miniature.' },
|
|
15
|
+
{ href: '/features/boundaries', title: 'Boundaries', blurb: 'The control-flow throws (forbidden / unauthorized / notFound) and the nearest boundary file that catches each.' },
|
|
16
|
+
{ href: '/features/metadata', title: 'Metadata', blurb: 'Static metadata plus generateMetadata(ctx), which reads the request to compute the title and Open Graph tags.' },
|
|
17
|
+
],
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: 'Components',
|
|
21
|
+
items: [
|
|
22
|
+
{ href: '/features/components', title: 'Components', blurb: 'The WebComponent factory, reactive props, instance signals, and slot projection in light DOM.' },
|
|
23
|
+
{ href: '/features/directives', title: 'Directives', blurb: 'The lit-html directive set: repeat for keyed lists, watch(signal) for a fine-grained node swap.' },
|
|
24
|
+
{ href: '/features/async-render', title: 'Async render', blurb: 'A component that awaits server data in async render(), so the resolved value is in the first paint.' },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: 'Data & actions',
|
|
29
|
+
items: [
|
|
30
|
+
{ href: '/features/server-actions', title: 'Server actions', blurb: 'A use-server RPC action next to a server-only .server.ts utility, and why the boundary matters.' },
|
|
31
|
+
{ href: '/features/route-handler', title: 'Route handlers', blurb: 'A server-only route.ts HTTP endpoint returning JSON, the WebJs equivalent of a Next route handler.' },
|
|
32
|
+
{ href: '/features/forms', title: 'Forms', blurb: 'A no-JS progressive-enhancement form posting to the page action, with server-side validation errors.' },
|
|
33
|
+
{ href: '/features/optimistic-ui', title: 'Optimistic UI', blurb: 'The imperative optimistic(signal, value, action) flip: instant update, automatic rollback on failure.' },
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
label: 'Client & streaming',
|
|
38
|
+
items: [
|
|
39
|
+
{ href: '/features/client-router', title: 'Client router', blurb: 'Automatic soft navigation: fragment-only fetches, hover prefetch, scroll restore, and graceful no-JS fallback.' },
|
|
40
|
+
{ href: '/features/view-transitions', title: 'View transitions', blurb: 'The opt-in view-transition meta cross-fades a soft navigation, with a data-webjs-permanent element persisted across the swap.' },
|
|
41
|
+
{ href: '/features/streaming', title: 'Streaming actions', blurb: 'A use-server action that returns an async generator, streamed to the call site token by token with for await.' },
|
|
42
|
+
{ href: '/features/stream', title: 'Stream updates', blurb: 'The <webjs-stream> element: renderStream() applies surgical append / replace / remove DOM updates by target id, no region redraw.' },
|
|
43
|
+
{ href: '/features/suspense', title: 'Suspense boundary', blurb: 'The <webjs-suspense> element: a first-paint fallback for a SLOW component, with the resolved content streamed in.' },
|
|
44
|
+
{ href: '/features/frames', title: 'Frames', blurb: 'A webjs-frame region that swaps a filtered sub-list in place from a link, shipping zero component JS, with a no-JS full-nav fallback.' },
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
label: 'Real-time',
|
|
49
|
+
items: [
|
|
50
|
+
{ href: '/features/websockets', title: 'WebSockets', blurb: 'A WS(ws, req) route endpoint plus the connectWS() client, echoing messages over a live socket.' },
|
|
51
|
+
{ href: '/features/broadcast', title: 'Broadcast', blurb: 'Fan a message out to every connected client on a WebSocket path, so all open tabs stay in sync.' },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
label: 'Auth & sessions',
|
|
56
|
+
items: [
|
|
57
|
+
{ href: '/features/auth', title: 'Auth', blurb: 'Password login on createAuth, a signed session cookie, and a real protected route that redirects anonymous visitors to login.' },
|
|
58
|
+
{ href: '/features/sessions', title: 'Sessions', blurb: 'A signed-cookie session applied by a segment middleware, read and written per visitor with getSession() in a route.' },
|
|
59
|
+
],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: 'Built-ins',
|
|
63
|
+
items: [
|
|
64
|
+
{ href: '/features/caching', title: 'Caching', blurb: 'export const revalidate caches the page HTML per URL, with the safety rule for when a shared cache is allowed.' },
|
|
65
|
+
{ href: '/features/env', title: 'Env vars', blurb: 'The server-only vs WEBJS_PUBLIC_ boundary, read during SSR so secrets never reach the browser.' },
|
|
66
|
+
{ href: '/features/rate-limit', title: 'Rate limiting', blurb: 'The rateLimit() middleware scoped to one endpoint, returning a 429 with Retry-After past the window.' },
|
|
67
|
+
{ href: '/features/file-storage', title: 'File storage', blurb: 'A no-JS multipart upload streamed into the FileStore, then served back through a streaming route.' },
|
|
68
|
+
{ href: '/features/service-worker', title: 'Service worker', blurb: 'The opt-in offline enhancement, registered from a browser-only lifecycle hook (never a page or layout).' },
|
|
69
|
+
],
|
|
70
|
+
},
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
/** The whole example apps (composed features), shown after the single-feature demos. */
|
|
74
|
+
export const EXAMPLES: NavItem[] = [
|
|
75
|
+
{ href: '/examples/todo', title: 'Optimistic todo', blurb: 'A whole app composing several features: the declarative optimistic() list API, progressive-enhancement forms, accessible labels, the modules split, and SQLite.' },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/** Flattened single-feature list (for the home card grid). */
|
|
79
|
+
export const FEATURES: NavItem[] = FEATURE_GROUPS.flatMap((g) => g.items);
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
// see the declarative optimistic(host, { source, update }) + .add() form in the
|
|
10
10
|
// /examples/todo app.
|
|
11
11
|
import { WebComponent, signal, optimistic, html } from '@webjsdev/core';
|
|
12
|
+
import { cn } from '#lib/utils/cn.ts';
|
|
13
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
12
14
|
import { likePost } from '../actions/like-post.server.ts';
|
|
13
15
|
|
|
14
16
|
export class LikeButton extends WebComponent {
|
|
@@ -23,9 +25,25 @@ export class LikeButton extends WebComponent {
|
|
|
23
25
|
|
|
24
26
|
render() {
|
|
25
27
|
const liked = this.liked.get();
|
|
28
|
+
// Build ON the design system, do not re-implement it. The like toggle IS the
|
|
29
|
+
// button variants: unliked reads as `secondary` (card surface), liked as
|
|
30
|
+
// `default` (primary fill). So compose buttonClass() for the base + colors +
|
|
31
|
+
// focus ring + cursor, then override only the genuinely bespoke bits with cn()
|
|
32
|
+
// (which tailwind-merges): a pill radius over the helper's rounded-xl, and a
|
|
33
|
+
// border on the liked state. The border is GEOMETRY, not decoration: the
|
|
34
|
+
// `secondary` variant carries a 1px border and `default` carries none, so under
|
|
35
|
+
// box-sizing border-box an auto-width inline-flex button would change its outer
|
|
36
|
+
// size by ~1px each toggle (a small label jitter). Re-adding `border
|
|
37
|
+
// border-primary` keeps a 1px border in both states so the pill stays put. On
|
|
38
|
+
// the primary fill that border color matches the fill, so it reads as invisible,
|
|
39
|
+
// doing only the no-jitter job. Only the heart SVG stays fully custom.
|
|
26
40
|
return html`
|
|
27
41
|
<button @click=${() => this.toggle()} aria-pressed=${liked ? 'true' : 'false'}
|
|
28
|
-
class
|
|
42
|
+
class=${cn(
|
|
43
|
+
buttonClass({ variant: liked ? 'default' : 'secondary' }),
|
|
44
|
+
'rounded-full',
|
|
45
|
+
liked && 'border border-primary',
|
|
46
|
+
)}>
|
|
29
47
|
<svg viewBox="0 0 24 24" class="w-4 h-4 ${liked ? 'fill-current stroke-none' : 'fill-none stroke-current'}" style="stroke-width:2;stroke-linecap:round;stroke-linejoin:round"><path d="M20.8 4.6a5.5 5.5 0 0 0-7.8 0L12 5.7l-1-1.1a5.5 5.5 0 0 0-7.8 7.8l1.1 1L12 21l7.7-7.6 1.1-1a5.5 5.5 0 0 0 0-7.8Z"/></svg>
|
|
30
48
|
${liked ? 'Liked' : 'Like'}
|
|
31
49
|
</button>
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// import and call instead). The button is the interactivity signal, so this
|
|
6
6
|
// component ships and hydrates.
|
|
7
7
|
import { WebComponent, signal, html } from '@webjsdev/core';
|
|
8
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
8
9
|
|
|
9
10
|
interface Probe { n: number; status: number; remaining: string }
|
|
10
11
|
|
|
@@ -32,7 +33,7 @@ export class RateProbe extends WebComponent {
|
|
|
32
33
|
return html`
|
|
33
34
|
<div class="grid gap-4 max-w-[420px]">
|
|
34
35
|
<button @click=${() => this.ping()}
|
|
35
|
-
class="
|
|
36
|
+
class="${buttonClass({ size: 'sm' })} w-fit">Ping the endpoint</button>
|
|
36
37
|
<ul class="grid gap-1.5 list-none m-0 p-0">
|
|
37
38
|
${this.log.get().map((p) => html`
|
|
38
39
|
<li class="flex items-center justify-between px-3 py-2 rounded-xl bg-card border border-border text-sm">
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// (it runs in the browser), so it lives in a component; with JS off this button
|
|
7
7
|
// is inert and the page still reads.
|
|
8
8
|
import { WebComponent, signal, html, richFetch } from '@webjsdev/core';
|
|
9
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
9
10
|
|
|
10
11
|
interface RichPayload { at: Date; ip: string; requestId: string; cookieCount: number }
|
|
11
12
|
|
|
@@ -22,7 +23,7 @@ export class RichData extends WebComponent {
|
|
|
22
23
|
return html`
|
|
23
24
|
<div class="flex items-center gap-3 text-[15px]">
|
|
24
25
|
<button @click=${() => this.load()}
|
|
25
|
-
class
|
|
26
|
+
class=${buttonClass({ size: 'sm' })}>richFetch() the route</button>
|
|
26
27
|
<span class="text-muted-foreground">${this.line.get()}</span>
|
|
27
28
|
</div>
|
|
28
29
|
`;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
// Calls the 'use server' action via a normal import (the RPC stub). Never
|
|
2
2
|
// hand-write fetch(); importing the action IS the API.
|
|
3
3
|
import { WebComponent, signal, html } from '@webjsdev/core';
|
|
4
|
+
import { cardClass } from '#components/ui/card.ts';
|
|
5
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
6
|
+
import { bareInputClass } from '#components/ui/input.ts';
|
|
4
7
|
import { greet } from '../actions/greet.server.ts';
|
|
5
8
|
|
|
6
9
|
export class Greeter extends WebComponent {
|
|
@@ -21,13 +24,13 @@ export class Greeter extends WebComponent {
|
|
|
21
24
|
return html`
|
|
22
25
|
<div class="grid gap-3 max-w-[420px]">
|
|
23
26
|
<form @submit=${(e: SubmitEvent) => this.run(e)}
|
|
24
|
-
class="flex items-center gap-2 p-2 pl-4
|
|
27
|
+
class="${cardClass()} flex items-center gap-2 p-2 pl-4">
|
|
25
28
|
<input name="name" placeholder="your name" autocomplete="off"
|
|
26
|
-
class
|
|
29
|
+
class=${bareInputClass()} />
|
|
27
30
|
<button type="submit"
|
|
28
|
-
class="shrink-0
|
|
31
|
+
class="${buttonClass()} shrink-0">Greet</button>
|
|
29
32
|
</form>
|
|
30
|
-
<p class="m-0 text-sm text-muted-foreground">The action is gated by requireAuth. <a class="text-primary" href="/features/auth/login">Sign in</a> to greet; signed out returns a real 401.</p>
|
|
33
|
+
<p class="m-0 text-sm text-muted-foreground">The action is gated by requireAuth. <a class="text-primary underline underline-offset-2" href="/features/auth/login">Sign in</a> to greet; signed out returns a real 401.</p>
|
|
31
34
|
${this.msg.get() ? html`<p class="m-0 font-semibold text-foreground">${this.msg.get()}</p>` : ''}
|
|
32
35
|
</div>
|
|
33
36
|
`;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// That is the whole point: out-of-band updates that a signal re-render or a frame
|
|
13
13
|
// region-swap would clobber.
|
|
14
14
|
import { WebComponent, html, renderStream } from '@webjsdev/core';
|
|
15
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
15
16
|
|
|
16
17
|
// Build a <webjs-stream> payload string. It is a plain string (NOT an html``
|
|
17
18
|
// template), so interpolating the row markup here is fine. `remove` needs no
|
|
@@ -30,11 +31,15 @@ export class StreamDemo extends WebComponent {
|
|
|
30
31
|
// component and blow away the streamed-in rows.
|
|
31
32
|
#n = 2;
|
|
32
33
|
|
|
33
|
-
append()
|
|
34
|
+
// NOTE the method names: NOT append() / prepend(). Those are native
|
|
35
|
+
// ParentNode methods, and WebJs instruments them on every light-DOM host for
|
|
36
|
+
// the slot API (#1021), so a component method of the same name is shadowed and
|
|
37
|
+
// never runs. Name your handlers something else (see muscle-memory-gotchas).
|
|
38
|
+
appendRow() {
|
|
34
39
|
this.#n++;
|
|
35
40
|
renderStream(streamPayload('append', 'stream-list', row(`row-${this.#n}`, `Row ${this.#n} (appended)`)));
|
|
36
41
|
}
|
|
37
|
-
|
|
42
|
+
prependRow() {
|
|
38
43
|
this.#n++;
|
|
39
44
|
renderStream(streamPayload('prepend', 'stream-list', row(`row-${this.#n}`, `Row ${this.#n} (prepended)`)));
|
|
40
45
|
}
|
|
@@ -53,12 +58,12 @@ export class StreamDemo extends WebComponent {
|
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
render() {
|
|
56
|
-
const btn =
|
|
61
|
+
const btn = buttonClass({ variant: 'secondary', size: 'xs' });
|
|
57
62
|
return html`
|
|
58
63
|
<div class="grid gap-4 max-w-[460px]">
|
|
59
64
|
<div class="flex flex-wrap gap-2">
|
|
60
|
-
<button class=${btn} @click=${() => this.
|
|
61
|
-
<button class=${btn} @click=${() => this.
|
|
65
|
+
<button class=${btn} @click=${() => this.appendRow()}>Append</button>
|
|
66
|
+
<button class=${btn} @click=${() => this.prependRow()}>Prepend</button>
|
|
62
67
|
<button class=${btn} @click=${() => this.replaceFirst()}>Replace Row 1</button>
|
|
63
68
|
<button class=${btn} @click=${() => this.removeSecond()}>Remove Row 2</button>
|
|
64
69
|
<button class=${btn} @click=${() => this.reset()}>Reset</button>
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// the button is inert and the empty output renders (streaming is inherently a
|
|
7
7
|
// JS behaviour), so nothing here breaks the no-JS first paint.
|
|
8
8
|
import { WebComponent, signal, html } from '@webjsdev/core';
|
|
9
|
+
import { cardClass } from '#components/ui/card.ts';
|
|
10
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
9
11
|
import { streamTokens } from '../actions/stream-tokens.server.ts';
|
|
10
12
|
|
|
11
13
|
export class TokenStream extends WebComponent {
|
|
@@ -29,16 +31,21 @@ export class TokenStream extends WebComponent {
|
|
|
29
31
|
|
|
30
32
|
render() {
|
|
31
33
|
const busy = this.busy.get();
|
|
34
|
+
const output = this.output.get();
|
|
35
|
+
// Only reserve the output area once streaming starts (or has produced text),
|
|
36
|
+
// so an idle demo has no empty box below the button.
|
|
32
37
|
return html`
|
|
33
|
-
<div class="
|
|
38
|
+
<div class="${cardClass()} p-5">
|
|
34
39
|
<button
|
|
35
40
|
@click=${() => this.run()}
|
|
36
41
|
?disabled=${busy}
|
|
37
|
-
class
|
|
42
|
+
class=${buttonClass()}
|
|
38
43
|
>
|
|
39
44
|
${busy ? 'streaming…' : 'Stream tokens'}
|
|
40
45
|
</button>
|
|
41
|
-
|
|
46
|
+
${busy || output
|
|
47
|
+
? html`<pre class="mt-4 whitespace-pre-wrap font-mono text-sm text-foreground">${output}</pre>`
|
|
48
|
+
: ''}
|
|
42
49
|
</div>
|
|
43
50
|
`;
|
|
44
51
|
}
|
|
@@ -7,11 +7,12 @@
|
|
|
7
7
|
// navigation too). Same component either way; the boundary is what changes the
|
|
8
8
|
// SSR behaviour from block to stream.
|
|
9
9
|
import { WebComponent, html } from '@webjsdev/core';
|
|
10
|
+
import { cardClass } from '#components/ui/card.ts';
|
|
10
11
|
|
|
11
12
|
export class SlowFact extends WebComponent {
|
|
12
13
|
async render() {
|
|
13
14
|
await new Promise((r) => setTimeout(r, 900));
|
|
14
|
-
return html`<p class="
|
|
15
|
+
return html`<p class="${cardClass()} p-5 text-foreground">
|
|
15
16
|
The answer, after a slow lookup, is <strong>42</strong>.
|
|
16
17
|
</p>`;
|
|
17
18
|
}
|
|
@@ -10,6 +10,10 @@
|
|
|
10
10
|
// one coherent theme. Prefer these tokens (and opacity modifiers like
|
|
11
11
|
// bg-primary/90) over ad-hoc colors.
|
|
12
12
|
import { WebComponent, prop, optimistic, html } from '@webjsdev/core';
|
|
13
|
+
import { cardClass } from '#components/ui/card.ts';
|
|
14
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
15
|
+
import { bareInputClass } from '#components/ui/input.ts';
|
|
16
|
+
import { cn } from '#lib/utils/cn.ts';
|
|
13
17
|
import { createTodo } from '../actions/create-todo.server.ts';
|
|
14
18
|
import { toggleTodo } from '../actions/toggle-todo.server.ts';
|
|
15
19
|
import { deleteTodo } from '../actions/delete-todo.server.ts';
|
|
@@ -94,12 +98,12 @@ export class TodoApp extends WebComponent({
|
|
|
94
98
|
<!-- Add: a real <form> so it works with JS off (posts to the page action);
|
|
95
99
|
with JS, @submit intercepts and runs the optimistic path. -->
|
|
96
100
|
<form method="post" action="" @submit=${(e: SubmitEvent) => this.add(e)}
|
|
97
|
-
class="flex items-center gap-2 p-2 pl-4
|
|
101
|
+
class="${cardClass()} flex items-center gap-2 p-2 pl-4 shadow-[0_1px_0_0_color-mix(in_oklch,var(--foreground)_5%,transparent)]">
|
|
98
102
|
<input type="hidden" name="intent" value="create" />
|
|
99
103
|
<input name="title" required maxlength="280" autocomplete="off" placeholder="What needs doing?"
|
|
100
|
-
class
|
|
104
|
+
class=${bareInputClass()} />
|
|
101
105
|
<button type="submit"
|
|
102
|
-
class="shrink-0
|
|
106
|
+
class="${buttonClass()} shrink-0">Add</button>
|
|
103
107
|
</form>
|
|
104
108
|
|
|
105
109
|
<ul class="list-none m-0 p-0 grid gap-2">
|
|
@@ -124,7 +128,7 @@ export class TodoApp extends WebComponent({
|
|
|
124
128
|
<!-- Delete: a proper icon button, revealed on row hover / focus. -->
|
|
125
129
|
<button type="submit" name="intent" value="delete" aria-label="Delete task"
|
|
126
130
|
@click=${(e: Event) => this.removeTodo(e, todo)}
|
|
127
|
-
class
|
|
131
|
+
class=${cn(buttonClass({ variant: 'destructive', size: 'none' }), 'w-7 h-7 rounded-lg opacity-0 group-hover:opacity-100 focus:opacity-100')}>
|
|
128
132
|
<svg viewBox="0 0 24 24" class="w-4 h-4 stroke-current fill-none" style="stroke-width:2;stroke-linecap:round;stroke-linejoin:round"><path d="M18 6 6 18M6 6l12 12"/></svg>
|
|
129
133
|
</button>
|
|
130
134
|
</form>
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// component renders its disconnected state, so with JS off the page still reads
|
|
6
6
|
// (a live socket has no no-JS equivalent, which is the honest fallback here).
|
|
7
7
|
import { WebComponent, signal, html, connectWS, renderStream } from '@webjsdev/core';
|
|
8
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
9
|
+
import { inputClass } from '#components/ui/input.ts';
|
|
8
10
|
|
|
9
11
|
export class WsEcho extends WebComponent {
|
|
10
12
|
private connected = signal(false);
|
|
@@ -59,9 +61,9 @@ export class WsEcho extends WebComponent {
|
|
|
59
61
|
</div>
|
|
60
62
|
<form @submit=${(e: SubmitEvent) => this.send(e)} class="flex gap-2">
|
|
61
63
|
<input name="msg" autocomplete="off" placeholder="Say something"
|
|
62
|
-
class
|
|
64
|
+
class=${inputClass('flex-1 min-w-0')} />
|
|
63
65
|
<button type="submit"
|
|
64
|
-
class
|
|
66
|
+
class=${buttonClass({ size: 'sm' })}>Send</button>
|
|
65
67
|
</form>
|
|
66
68
|
<ul class="grid gap-1.5 list-none m-0 p-0">
|
|
67
69
|
${this.lines.get().map((line) => html`
|
|
@@ -70,7 +72,7 @@ export class WsEcho extends WebComponent {
|
|
|
70
72
|
</ul>
|
|
71
73
|
<div class="grid gap-2 border-t border-border pt-4">
|
|
72
74
|
<button @click=${() => this.applyStreamUpdate()}
|
|
73
|
-
class="
|
|
75
|
+
class="${buttonClass({ variant: 'secondary', size: 'sm' })} w-fit">renderStream() an element-level update</button>
|
|
74
76
|
<ul id="ws-stream-log" class="grid gap-1.5 list-none m-0 p-0"></ul>
|
|
75
77
|
</div>
|
|
76
78
|
</div>
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" width="32" height="32" role="img" aria-label="WebJs">
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
<!-- Matches the gallery navbar brand mark (a rounded square with a
|
|
3
|
+
foreground -> muted-foreground gradient), using the DARK-theme token
|
|
4
|
+
values (#dee2e6 -> #94989c) so it reads on a dark browser tab bar. -->
|
|
5
|
+
<defs>
|
|
6
|
+
<linearGradient id="wj" x1="0" y1="0" x2="1" y2="1">
|
|
7
|
+
<stop offset="0" stop-color="#dee2e6"/>
|
|
8
|
+
<stop offset="1" stop-color="#94989c"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
</defs>
|
|
11
|
+
<rect width="32" height="32" rx="10" fill="url(#wj)"/>
|
|
5
12
|
</svg>
|
|
@@ -9,18 +9,38 @@
|
|
|
9
9
|
// same patterns and SURVIVES this reset, so nothing is lost), then run this
|
|
10
10
|
// once to shed the gallery, then grow the app in place.
|
|
11
11
|
//
|
|
12
|
-
// It
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
// (
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
|
|
12
|
+
// It strips the app down to a TRULY BAREBONES blank slate: it removes the
|
|
13
|
+
// gallery routes + modules + demo metadata routes, the gallery's example design
|
|
14
|
+
// system (components/ui/), the example theme-toggle component, the example test
|
|
15
|
+
// suite (test/hello/), and every empty leftover dir, then resets app/page.ts to
|
|
16
|
+
// a minimal home AND app/layout.ts to a blank-slate base (the gallery's navbar,
|
|
17
|
+
// theme toggle, Google Fonts, AND design-token palette dropped: the reset layout
|
|
18
|
+
// carries NO design system, just the OS light/dark system colours, so the agent
|
|
19
|
+
// builds the palette as its own, guided by the skill), and drops the demo `todos`
|
|
20
|
+
// table plus the auth card's `passwordHash` column from the schema. It KEEPS only
|
|
21
|
+
// the buildable base: the agent skill (.agents/skills/webjs/), the reset root
|
|
22
|
+
// layout (the Tailwind wiring + a system-colour base, no tokens), the database
|
|
23
|
+
// wiring, the example `users` table, and `lib/utils/cn.ts` (the `webjs ui add`
|
|
24
|
+
// prerequisite).
|
|
25
|
+
//
|
|
26
|
+
// Why strip so much: the demos, the design system, and the example component /
|
|
27
|
+
// tests are all EXAMPLES to learn FROM, not a base to inherit. Leaving them
|
|
28
|
+
// nudges the implementing agent to lean on the scaffold's choices instead of
|
|
29
|
+
// building the app's own. The DURABLE knowledge is the pattern itself, which
|
|
30
|
+
// lives in the skill (.agents/skills/webjs/, re-read every session): learn it
|
|
31
|
+
// from the gallery here, then after this reset build your own (run
|
|
32
|
+
// `webjs ui add <name>` for UI primitives, cn.ts is kept for it) themed to your
|
|
33
|
+
// app. It is a one-time reset: if the gallery is already gone (no app/features/)
|
|
34
|
+
// it does nothing, so a rerun never clobbers an app you built.
|
|
35
|
+
import { rmSync, rmdirSync, existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
|
|
20
36
|
import { join } from 'node:path';
|
|
21
37
|
|
|
22
38
|
const root = process.cwd();
|
|
23
39
|
const rm = (p) => { if (existsSync(join(root, p))) { rmSync(join(root, p), { recursive: true, force: true }); return true; } return false; };
|
|
40
|
+
// Remove a directory ONLY if it exists and is empty (used to clean the empty
|
|
41
|
+
// parents a partial gallery removal leaves behind, e.g. app/api after its lone
|
|
42
|
+
// auth handler is gone). Never touches a dir that still holds files.
|
|
43
|
+
const pruneEmpty = (p) => { const abs = join(root, p); if (existsSync(abs) && readdirSync(abs).length === 0) { rmdirSync(abs); return true; } return false; };
|
|
24
44
|
|
|
25
45
|
// Guard: the gallery is identified by app/features/. If it is absent, the gallery
|
|
26
46
|
// was already cleared (or this is not a gallery scaffold), so exit before any
|
|
@@ -40,13 +60,26 @@ const galleryPaths = [
|
|
|
40
60
|
'app/icon.ts', 'app/apple-icon.ts', 'app/manifest.ts', 'app/opengraph-image.ts',
|
|
41
61
|
'app/twitter-image.ts', 'app/robots.ts', 'app/sitemap.ts',
|
|
42
62
|
'app/global-error.ts', 'app/global-not-found.ts',
|
|
63
|
+
// The gallery's EXAMPLE design system. Removed so the reset app is a blank
|
|
64
|
+
// slate: the agent builds its own components/ui/ (run `webjs ui add`, cn.ts is
|
|
65
|
+
// kept), learning the pattern from the skill, not inheriting the gallery's.
|
|
66
|
+
'components/ui',
|
|
67
|
+
// Example scaffold artifacts (invariant 2): the theme-toggle component and the
|
|
68
|
+
// example test suite are things to learn from, not the agent's app. Removed
|
|
69
|
+
// for a true blank slate; the root layout is reset to a minimal base below
|
|
70
|
+
// (which drops its toggle wiring). The agent writes its own tests + components.
|
|
71
|
+
'components/theme-toggle.ts', 'test/hello',
|
|
72
|
+
// The gallery's markup-chunk helpers. Their only users (the feature pages +
|
|
73
|
+
// the features/examples layouts) are removed above, so drop the example file
|
|
74
|
+
// too; cn.ts + dom.ts (real infrastructure) stay under lib/utils/.
|
|
75
|
+
'lib/utils/ui.ts',
|
|
43
76
|
];
|
|
44
77
|
// 2) The gallery's feature modules (by name). `auth` is the auth card's server
|
|
45
78
|
// modules (createAuth config, password hashing, signup action, current-user
|
|
46
79
|
// query), pruned with the rest of the card.
|
|
47
80
|
const galleryModules = [
|
|
48
81
|
'async-render', 'auth', 'broadcast', 'caching', 'client-router', 'components',
|
|
49
|
-
'directives', 'file-storage', 'frames', 'optimistic-ui', 'rate-limit',
|
|
82
|
+
'directives', 'file-storage', 'frames', 'gallery', 'optimistic-ui', 'rate-limit',
|
|
50
83
|
'route-handler', 'server-actions', 'sessions', 'stream', 'streaming', 'suspense',
|
|
51
84
|
'todo', 'websockets',
|
|
52
85
|
].map((m) => `modules/${m}`);
|
|
@@ -71,17 +104,51 @@ if (existsSync(schemaPath)) {
|
|
|
71
104
|
writeFileSync(schemaPath, s);
|
|
72
105
|
}
|
|
73
106
|
|
|
74
|
-
// 5)
|
|
107
|
+
// 5) Reset the root layout to a blank-slate base. The gallery layout ships a
|
|
108
|
+
// navbar, a theme toggle, Google Fonts, and a full design-token palette; a blank
|
|
109
|
+
// slate should inherit NONE of that, so OVERWRITE it with a minimal layout that
|
|
110
|
+
// carries no design system: the Tailwind stylesheet + OS light/dark system
|
|
111
|
+
// colours (Canvas / CanvasText) + a bare <main>. The agent then builds the
|
|
112
|
+
// palette (CSS tokens + @theme + a light-dark() theme) as its own, guided by
|
|
113
|
+
// .agents/skills/webjs/references/styling.md.
|
|
114
|
+
// GUARD: overwrite only while the layout still carries the gallery's own navbar
|
|
115
|
+
// brand. A hand-customised layout (the brand removed / replaced) is the user's
|
|
116
|
+
// work, so it is KEPT and only the dangling references to the removed
|
|
117
|
+
// theme-toggle file are stripped surgically.
|
|
118
|
+
const layoutPath = join(root, 'app/layout.ts');
|
|
119
|
+
if (existsSync(layoutPath)) {
|
|
120
|
+
const current = readFileSync(layoutPath, 'utf8');
|
|
121
|
+
if (current.includes('WebJs Gallery')) {
|
|
122
|
+
writeFileSync(layoutPath, MINIMAL_LAYOUT());
|
|
123
|
+
} else {
|
|
124
|
+
console.log('app/layout.ts looks customised; keeping it (only the theme-toggle wiring is stripped).');
|
|
125
|
+
writeFileSync(
|
|
126
|
+
layoutPath,
|
|
127
|
+
current
|
|
128
|
+
.replace(/^import '#components\/theme-toggle\.ts';\n/m, '')
|
|
129
|
+
.replace(/^\s*<theme-toggle><\/theme-toggle>\n?/m, ''),
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 6) Drop generated migrations + the dev database so the next db:generate is
|
|
75
135
|
// clean against the reset schema (safe: the scaffold has no real data yet).
|
|
76
136
|
rm('db/migrations');
|
|
77
137
|
for (const f of ['db/dev.db', 'db/dev.db-shm', 'db/dev.db-wal']) rm(f);
|
|
78
138
|
|
|
79
|
-
|
|
139
|
+
// 7) Prune the empty leftover dirs a partial removal left behind, so the reset
|
|
140
|
+
// tree is a clean blank slate: app/api (held only the gallery auth handler),
|
|
141
|
+
// test/unit + test/e2e (empty base placeholders), and test/ itself once its
|
|
142
|
+
// example suites are gone. modules/ + components/ are kept as empty build
|
|
143
|
+
// targets (the reset home + `webjs ui add` land there). Order matters: prune the
|
|
144
|
+
// children before test/ so it reads as empty.
|
|
145
|
+
for (const d of ['app/api', 'test/unit', 'test/e2e', 'test']) if (pruneEmpty(d)) removed++;
|
|
146
|
+
|
|
147
|
+
console.log(`Gallery cleared (${removed} paths removed). The agent skill and your database wiring are kept. Build your own design system: run \`npx webjsdev ui add <name>\` and theme it (see .agents/skills/webjs/references/styling.md).`);
|
|
80
148
|
console.log('Next: regenerate the database (db:generate then db:migrate), then start the dev server and build your app in app/ and modules/.');
|
|
81
149
|
|
|
82
150
|
function MINIMAL_PAGE() {
|
|
83
151
|
return `import { html } from '@webjsdev/core';
|
|
84
|
-
import '#components/theme-toggle.ts';
|
|
85
152
|
|
|
86
153
|
export const metadata = {
|
|
87
154
|
title: 'Home',
|
|
@@ -89,19 +156,59 @@ export const metadata = {
|
|
|
89
156
|
|
|
90
157
|
export default function Home() {
|
|
91
158
|
return html\`
|
|
92
|
-
<div class="fixed top-4 right-4 z-10"><theme-toggle></theme-toggle></div>
|
|
93
159
|
<div class="max-w-2xl mx-auto px-6 py-24 flex flex-col items-center text-center gap-6">
|
|
94
160
|
<h1 class="text-4xl font-bold tracking-tight m-0">Your app</h1>
|
|
95
|
-
<p class="text-base
|
|
96
|
-
The gallery is cleared. This is <code class="text-[0.9em]
|
|
97
|
-
app from here. The guide is <code class="text-[0.9em]
|
|
161
|
+
<p class="text-base leading-relaxed m-0 opacity-70">
|
|
162
|
+
The gallery is cleared. This is <code class="text-[0.9em]">app/page.ts</code>. Build your
|
|
163
|
+
app from here. The guide is <code class="text-[0.9em]">.agents/skills/webjs/SKILL.md</code>.
|
|
98
164
|
</p>
|
|
99
|
-
<nav class="flex items-center gap-5 text-sm
|
|
100
|
-
<a href="https://docs.webjs.dev" class="hover:
|
|
101
|
-
<a href="https://github.com/webjsdev/webjs" class="hover:
|
|
165
|
+
<nav class="flex items-center gap-5 text-sm opacity-70">
|
|
166
|
+
<a href="https://docs.webjs.dev" target="_blank" rel="noopener" class="hover:opacity-100 transition-opacity no-underline">Docs</a>
|
|
167
|
+
<a href="https://github.com/webjsdev/webjs" target="_blank" rel="noopener" class="hover:opacity-100 transition-opacity no-underline">GitHub</a>
|
|
102
168
|
</nav>
|
|
103
169
|
</div>
|
|
104
170
|
\`;
|
|
105
171
|
}
|
|
106
172
|
`;
|
|
107
173
|
}
|
|
174
|
+
|
|
175
|
+
function MINIMAL_LAYOUT() {
|
|
176
|
+
return `import { html } from '@webjsdev/core';
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Root layout: the ONLY file that writes the document shell. It links the
|
|
180
|
+
* Tailwind stylesheet and renders \${children} in a bare container. This is a
|
|
181
|
+
* BLANK SLATE with no design system: it uses the OS light/dark system colours
|
|
182
|
+
* (Canvas / CanvasText) so it reads fine immediately, and it is where YOU build
|
|
183
|
+
* the app's look. The recommended setup, CSS custom-property tokens mapped into
|
|
184
|
+
* Tailwind via @theme (so bg-background / text-foreground work), a DRY
|
|
185
|
+
* light-dark() palette, a header/nav, and the ui class helpers, is taught in
|
|
186
|
+
* .agents/skills/webjs/references/styling.md. Run \`npx webjsdev ui add <name>\`
|
|
187
|
+
* to pull primitives, then theme them here.
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
// Favicon via metadata.icons so the framework emits the <link> into <head> (a
|
|
191
|
+
// hand-written <link> in the template body is ignored by browsers).
|
|
192
|
+
export const metadata = { icons: '/public/favicon.svg' };
|
|
193
|
+
|
|
194
|
+
export default function RootLayout({ children }: { children: unknown }) {
|
|
195
|
+
return html\`
|
|
196
|
+
<meta name="color-scheme" content="light dark">
|
|
197
|
+
<link rel="stylesheet" href="/public/tailwind.css">
|
|
198
|
+
<style>
|
|
199
|
+
html, body { margin: 0; }
|
|
200
|
+
body {
|
|
201
|
+
background: Canvas;
|
|
202
|
+
color: CanvasText;
|
|
203
|
+
font: 15px/1.6 ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
|
|
204
|
+
-webkit-font-smoothing: antialiased;
|
|
205
|
+
-moz-osx-font-smoothing: grayscale;
|
|
206
|
+
}
|
|
207
|
+
</style>
|
|
208
|
+
<main class="min-h-dvh max-w-3xl mx-auto px-6 py-10">
|
|
209
|
+
\${children}
|
|
210
|
+
</main>
|
|
211
|
+
\`;
|
|
212
|
+
}
|
|
213
|
+
`;
|
|
214
|
+
}
|