@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
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// (delete this file), then delete this marker line. webjs check fails while the
|
|
2
|
-
// marker remains.
|
|
3
|
-
//
|
|
4
1
|
// app/icon.ts serves /icon (the dynamic favicon). The default export is a
|
|
5
2
|
// (possibly async) server function; returning a Response lets you set the exact
|
|
6
3
|
// content type, so an inline SVG needs no asset file. For a favicon that never
|
|
@@ -9,8 +6,8 @@
|
|
|
9
6
|
// mark must be computed at request time.
|
|
10
7
|
export default function Icon() {
|
|
11
8
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
|
12
|
-
<rect width="32" height="32" rx="7" fill="#
|
|
13
|
-
<text x="16" y="22" font-family="system-ui, sans-serif" font-size="18" font-weight="700" fill="#
|
|
9
|
+
<rect width="32" height="32" rx="7" fill="#1e2226"/>
|
|
10
|
+
<text x="16" y="22" font-family="system-ui, sans-serif" font-size="18" font-weight="700" fill="#94989c" text-anchor="middle">w</text>
|
|
14
11
|
</svg>`;
|
|
15
12
|
return new Response(svg, {
|
|
16
13
|
headers: { 'content-type': 'image/svg+xml', 'cache-control': 'public, max-age=3600' },
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// (delete this file), then delete this marker line. webjs check fails while the
|
|
2
|
-
// marker remains.
|
|
3
|
-
//
|
|
4
1
|
// app/manifest.ts serves /manifest.json (the web app manifest). The default
|
|
5
2
|
// export returns an object, serialized to JSON. Adapt the name, colors, and
|
|
6
3
|
// icons to your app; pair it with the opt-in service worker for an installable
|
|
@@ -13,7 +10,7 @@ export default function Manifest() {
|
|
|
13
10
|
start_url: '/',
|
|
14
11
|
display: 'standalone',
|
|
15
12
|
background_color: '#ffffff',
|
|
16
|
-
theme_color: '#
|
|
13
|
+
theme_color: '#1e2226',
|
|
17
14
|
icons: [
|
|
18
15
|
{ src: '/favicon.svg', sizes: 'any', type: 'image/svg+xml' },
|
|
19
16
|
],
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// (delete this file), then delete this marker line. webjs check fails while the
|
|
2
|
-
// marker remains.
|
|
3
|
-
//
|
|
4
1
|
// app/opengraph-image.ts serves /opengraph-image (the preview card social
|
|
5
2
|
// platforms show when the site is shared). The Open Graph spec wants 1200x630.
|
|
6
3
|
// Returning a Response with an inline SVG keeps this buildless; for per-page
|
|
@@ -9,9 +6,9 @@
|
|
|
9
6
|
// `openGraph: { images: ['/opengraph-image'] }`.
|
|
10
7
|
export default function OpengraphImage() {
|
|
11
8
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
|
12
|
-
<rect width="1200" height="630" fill="#
|
|
13
|
-
<text x="80" y="330" font-family="system-ui, sans-serif" font-size="88" font-weight="700" fill="#
|
|
14
|
-
<text x="80" y="410" font-family="system-ui, sans-serif" font-size="36" fill="#
|
|
9
|
+
<rect width="1200" height="630" fill="#1e2226"/>
|
|
10
|
+
<text x="80" y="330" font-family="system-ui, sans-serif" font-size="88" font-weight="700" fill="#dee2e6">My App</text>
|
|
11
|
+
<text x="80" y="410" font-family="system-ui, sans-serif" font-size="36" fill="#94989c">Build on the platform, not against it</text>
|
|
15
12
|
</svg>`;
|
|
16
13
|
return new Response(svg, {
|
|
17
14
|
headers: { 'content-type': 'image/svg+xml', 'cache-control': 'public, max-age=3600' },
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// (delete this file), then delete this marker line. webjs check fails while the
|
|
2
|
-
// marker remains.
|
|
3
|
-
//
|
|
4
1
|
// app/robots.ts serves /robots.txt. The default export returns a string (served
|
|
5
2
|
// as text/plain) or an object. This example allows all crawlers and points them
|
|
6
3
|
// at the sitemap. Tighten the rules (Disallow paths) for a real app.
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// routes, or prune it (delete this file), then delete this marker line. webjs
|
|
2
|
-
// check fails while the marker remains.
|
|
3
|
-
//
|
|
4
1
|
// app/sitemap.ts serves /sitemap.xml. The default export is a (possibly async)
|
|
5
2
|
// server function; `sitemap(entries)` from @webjsdev/server serializes a
|
|
6
3
|
// spec-valid XML sitemap. In a real app, build the entries from your content
|
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
// (delete this file), then delete this marker line. webjs check fails while the
|
|
2
|
-
// marker remains.
|
|
3
|
-
//
|
|
4
1
|
// app/twitter-image.ts serves /twitter-image (the card image shown when the
|
|
5
2
|
// site is shared on Twitter/X). Its own route so the Twitter card can differ
|
|
6
3
|
// from the Open Graph image (opengraph-image.ts); when they are identical, drop
|
|
@@ -8,9 +5,9 @@
|
|
|
8
5
|
// roughly 1200x630. Reference it via metadata `twitter: { images: [...] }`.
|
|
9
6
|
export default function TwitterImage() {
|
|
10
7
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="630" viewBox="0 0 1200 630">
|
|
11
|
-
<rect width="1200" height="630" fill="#
|
|
12
|
-
<text x="80" y="330" font-family="system-ui, sans-serif" font-size="88" font-weight="700" fill="#
|
|
13
|
-
<text x="80" y="410" font-family="system-ui, sans-serif" font-size="36" fill="#
|
|
8
|
+
<rect width="1200" height="630" fill="#1e2226"/>
|
|
9
|
+
<text x="80" y="330" font-family="system-ui, sans-serif" font-size="88" font-weight="700" fill="#dee2e6">My App</text>
|
|
10
|
+
<text x="80" y="410" font-family="system-ui, sans-serif" font-size="36" fill="#94989c">Build on the platform, not against it</text>
|
|
14
11
|
</svg>`;
|
|
15
12
|
return new Response(svg, {
|
|
16
13
|
headers: { 'content-type': 'image/svg+xml', 'cache-control': 'public, max-age=3600' },
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* badgeClass: the gallery's badge/pill design token, built on @webjsdev/ui.
|
|
3
|
+
*
|
|
4
|
+
* A `@webjsdev/ui` tier-1 CLASS HELPER (like buttonClass): it returns a Tailwind
|
|
5
|
+
* class string you spread onto a NATIVE inline element, almost always a `<span>`
|
|
6
|
+
* (a static label) or an `<a>` (a linked tag). Native markup means no wrapper
|
|
7
|
+
* component and correct semantics for free:
|
|
8
|
+
*
|
|
9
|
+
* <span class=${badgeClass()}>Signed in</span>
|
|
10
|
+
* <span class=${badgeClass({ variant: 'outline' })}>Example app</span>
|
|
11
|
+
*
|
|
12
|
+
* OWN-AND-THEME: this is `@webjsdev/ui`'s badge THEMED to this scaffold's two
|
|
13
|
+
* real uses, NOT the fuller shadcn badge (solid primary, secondary, destructive,
|
|
14
|
+
* ghost, link). Keep only the variants you use, styled to your look. `default`
|
|
15
|
+
* is a soft primary pill (a status chip); `outline` is a quiet muted tag.
|
|
16
|
+
*
|
|
17
|
+
* A11y: a static badge is a plain non-focusable `<span>`. Make it an `<a>` only
|
|
18
|
+
* when it links somewhere.
|
|
19
|
+
*
|
|
20
|
+
* Design tokens used (app/layout.ts): --color-primary, --color-border,
|
|
21
|
+
* --color-muted-foreground.
|
|
22
|
+
*/
|
|
23
|
+
import { cn } from '#lib/utils/cn.ts';
|
|
24
|
+
|
|
25
|
+
// Shared by every variant: an inline pill that hugs its content and never wraps.
|
|
26
|
+
const BASE = 'inline-flex w-fit shrink-0 items-center whitespace-nowrap';
|
|
27
|
+
|
|
28
|
+
const VARIANTS = {
|
|
29
|
+
// A soft, tinted primary chip. Used for a status ("Signed in").
|
|
30
|
+
default: 'rounded-full bg-primary/15 text-primary px-2.5 py-1 text-xs font-medium',
|
|
31
|
+
// A tiny, quiet outline tag. Used to label a kind of thing ("Example app").
|
|
32
|
+
outline:
|
|
33
|
+
'rounded border border-border text-muted-foreground px-1.5 py-0.5 text-[0.6rem] font-semibold uppercase tracking-wider',
|
|
34
|
+
} as const;
|
|
35
|
+
|
|
36
|
+
export type BadgeVariant = keyof typeof VARIANTS;
|
|
37
|
+
|
|
38
|
+
/** Compose the Tailwind classes for a badge/pill. Append your own layout as needed. */
|
|
39
|
+
export function badgeClass(opts: { variant?: BadgeVariant } = {}): string {
|
|
40
|
+
return cn(BASE, VARIANTS[opts.variant ?? 'default']);
|
|
41
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* buttonClass: the gallery's button design token, built on @webjsdev/ui.
|
|
3
|
+
*
|
|
4
|
+
* This is a `@webjsdev/ui` tier-1 CLASS HELPER (the shadcn model): instead of a
|
|
5
|
+
* `<ui-button>` wrapper element, it returns a Tailwind class string you spread
|
|
6
|
+
* onto a NATIVE `<button>` (or `<a>` for a link-styled button). Native markup
|
|
7
|
+
* means form submission, focus, keyboard activation, and screen-reader
|
|
8
|
+
* semantics all work with no extra wiring:
|
|
9
|
+
*
|
|
10
|
+
* <button class=${buttonClass()} @click=${...}>Save</button>
|
|
11
|
+
* <button class=${buttonClass({ variant: 'secondary', size: 'sm' })}>Cancel</button>
|
|
12
|
+
* <a class=${buttonClass({ variant: 'link', size: 'none' })} href="/x">More</a>
|
|
13
|
+
*
|
|
14
|
+
* WHY a helper and not a component: the class helper adds NO indirection, so a
|
|
15
|
+
* demo's real markup (the `@click`, the `?disabled`, the concept it teaches)
|
|
16
|
+
* stays fully visible, while every button in the gallery shares one source of
|
|
17
|
+
* truth for its look. That is also why the #1057-style "one button forgot
|
|
18
|
+
* cursor-pointer" gap cannot happen: `cursor-pointer` lives on BASE.
|
|
19
|
+
*
|
|
20
|
+
* OWN-AND-THEME your copy: this file is `webjs ui add button` output THEMED to
|
|
21
|
+
* this scaffold's look (rounded-xl, the primary / card / muted-link styles the
|
|
22
|
+
* gallery uses). That is the intended `@webjsdev/ui` workflow: you own the
|
|
23
|
+
* component file and tune its class values to your brand. Run
|
|
24
|
+
* `webjs ui add button` (or edit here) to change the button system app-wide.
|
|
25
|
+
*
|
|
26
|
+
* A11y: an icon-only button has no visible text, so give it an `aria-label`.
|
|
27
|
+
* Native `<button>` focus + keyboard activation are already correct.
|
|
28
|
+
*
|
|
29
|
+
* Design tokens used (defined in app/layout.ts): --color-primary,
|
|
30
|
+
* --color-primary-foreground, --color-card, --color-border, --color-foreground,
|
|
31
|
+
* --color-muted-foreground, --color-accent, --color-ring.
|
|
32
|
+
*/
|
|
33
|
+
import { cn } from '#lib/utils/cn.ts';
|
|
34
|
+
|
|
35
|
+
// BASE: shared by every variant + size, so a fix here (e.g. the focus ring, or
|
|
36
|
+
// cursor-pointer) applies to the whole button system at once.
|
|
37
|
+
// Focus is handled ONCE by the design system's global :focus-visible ring (the
|
|
38
|
+
// ui theme's @layer base in public/input.css), so buttons need no per-element
|
|
39
|
+
// focus style here.
|
|
40
|
+
const BASE =
|
|
41
|
+
'inline-flex shrink-0 cursor-pointer items-center justify-center gap-2 whitespace-nowrap text-sm transition-all disabled:pointer-events-none disabled:opacity-60';
|
|
42
|
+
|
|
43
|
+
// VARIANTS carry COLOR + weight + hover (what the button IS). Add a variant here
|
|
44
|
+
// and it is instantly available to every demo.
|
|
45
|
+
const VARIANTS = {
|
|
46
|
+
// The filled call-to-action (Send, Upload, Stream tokens, Greet).
|
|
47
|
+
default: 'bg-primary text-primary-foreground font-semibold hover:bg-primary/90 active:scale-[0.97]',
|
|
48
|
+
// The quieter card-surfaced action (Reset, Read once, secondary controls).
|
|
49
|
+
secondary: 'bg-card border border-border text-foreground font-medium hover:border-border-strong',
|
|
50
|
+
// Transparent until hovered (a toolbar / menu action, e.g. Log out).
|
|
51
|
+
ghost: 'bg-transparent text-foreground font-medium hover:bg-accent',
|
|
52
|
+
// A button that reads as an inline text link (navigate, toggle, reveal).
|
|
53
|
+
link: 'text-muted-foreground font-medium underline decoration-dotted underline-offset-4 hover:text-foreground',
|
|
54
|
+
// A subtle destructive action (a delete icon / a remove control): transparent
|
|
55
|
+
// and muted at rest, turning red on hover. NOT a loud solid-red button; theme
|
|
56
|
+
// it to a filled `bg-destructive` variant if you want a prominent confirm.
|
|
57
|
+
destructive: 'bg-transparent text-muted-foreground font-medium hover:text-destructive hover:bg-destructive/10',
|
|
58
|
+
} as const;
|
|
59
|
+
|
|
60
|
+
// SIZES carry SHAPE (padding + radius). `none` is for the link variant, which
|
|
61
|
+
// is inline text and takes no box padding.
|
|
62
|
+
const SIZES = {
|
|
63
|
+
default: 'px-4 py-2 rounded-xl',
|
|
64
|
+
sm: 'px-3.5 py-1.5 rounded-xl',
|
|
65
|
+
xs: 'px-3 py-1 rounded-lg',
|
|
66
|
+
none: '',
|
|
67
|
+
} as const;
|
|
68
|
+
|
|
69
|
+
export type ButtonVariant = keyof typeof VARIANTS;
|
|
70
|
+
export type ButtonSize = keyof typeof SIZES;
|
|
71
|
+
|
|
72
|
+
export interface ButtonClassOptions {
|
|
73
|
+
variant?: ButtonVariant;
|
|
74
|
+
size?: ButtonSize;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Compose the Tailwind classes for a button. Both options are optional
|
|
79
|
+
* (`buttonClass()` is the primary call-to-action). `cn` de-duplicates, so you
|
|
80
|
+
* can append your own layout classes: `class="${buttonClass()} w-full"`.
|
|
81
|
+
*/
|
|
82
|
+
export function buttonClass(opts: ButtonClassOptions = {}): string {
|
|
83
|
+
const variant = opts.variant ?? 'default';
|
|
84
|
+
const size = opts.size ?? 'default';
|
|
85
|
+
return cn(BASE, VARIANTS[variant], SIZES[size]);
|
|
86
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cardClass: the gallery's panel-surface design token, built on @webjsdev/ui.
|
|
3
|
+
*
|
|
4
|
+
* A `@webjsdev/ui` tier-1 CLASS HELPER for the "card" surface every demo panel
|
|
5
|
+
* sits on. It owns only the SURFACE (radius, border, background), NOT the
|
|
6
|
+
* padding or inner layout, because those genuinely differ per panel (a form
|
|
7
|
+
* grid, a centered card, a tight toolbar). So you compose:
|
|
8
|
+
*
|
|
9
|
+
* <div class="${cardClass()} p-5 grid gap-4">...</div>
|
|
10
|
+
* <div class="${cardClass()} p-2 flex items-center gap-2">...</div>
|
|
11
|
+
*
|
|
12
|
+
* WHY split surface from layout: the surface is the thing that MUST be identical
|
|
13
|
+
* everywhere (so all panels read as one system), while padding and layout are a
|
|
14
|
+
* per-panel decision. Centralizing only the surface gives consistency without
|
|
15
|
+
* flattening intentional layout differences. Contrast the fuller shadcn Card
|
|
16
|
+
* (header / title / content / footer subparts) that `webjs ui add card` ships:
|
|
17
|
+
* this scaffold's panels are simple containers, so it keeps only what it uses.
|
|
18
|
+
*
|
|
19
|
+
* OWN-AND-THEME: this is `@webjsdev/ui`'s card THEMED to this scaffold (a
|
|
20
|
+
* `rounded-2xl` panel on `bg-card` with a hairline border). Change the surface
|
|
21
|
+
* here and every panel updates at once. When you build a real app on
|
|
22
|
+
* `@webjsdev/ui`, do the same: pull the primitive, keep the parts you use, and
|
|
23
|
+
* theme it to your brand.
|
|
24
|
+
*/
|
|
25
|
+
import { cn } from '#lib/utils/cn.ts';
|
|
26
|
+
|
|
27
|
+
// The shared panel surface. Padding + inner layout stay on the call site.
|
|
28
|
+
const SURFACE = 'rounded-2xl border border-border bg-card';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Compose the Tailwind classes for a card/panel surface. Append the panel's own
|
|
32
|
+
* padding + layout: `class="${cardClass()} p-5 grid gap-4 max-w-[460px]"`.
|
|
33
|
+
*/
|
|
34
|
+
export function cardClass(extra?: string): string {
|
|
35
|
+
return cn(SURFACE, extra);
|
|
36
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* inputClass: the gallery's text-field design token, built on @webjsdev/ui.
|
|
3
|
+
*
|
|
4
|
+
* Like buttonClass, this is a `@webjsdev/ui` tier-1 CLASS HELPER: it returns a
|
|
5
|
+
* Tailwind class string you spread onto a NATIVE `<input>` or `<textarea>`, so
|
|
6
|
+
* the browser's built-in form behaviour, validation, and accessibility all work
|
|
7
|
+
* with nothing extra:
|
|
8
|
+
*
|
|
9
|
+
* <input class=${inputClass()} name="email" type="email" />
|
|
10
|
+
* <textarea class=${inputClass()} name="message" rows="3"></textarea>
|
|
11
|
+
*
|
|
12
|
+
* The same helper styles inputs AND textareas here, because this app wants them
|
|
13
|
+
* to look identical. That is the point of a design token: one place decides how
|
|
14
|
+
* every field looks, so they cannot drift apart. If you later want a distinct
|
|
15
|
+
* textarea look, add a `textareaClass` alongside this and theme it separately
|
|
16
|
+
* (that is exactly how `webjs ui add textarea` would give you one).
|
|
17
|
+
*
|
|
18
|
+
* OWN-AND-THEME: this is `@webjsdev/ui`'s input, THEMED to this scaffold's look
|
|
19
|
+
* (a filled `bg-background` field, `rounded-xl`, a primary focus border). You
|
|
20
|
+
* own this file: change the class values here and every field in the app
|
|
21
|
+
* updates at once. This is the intended workflow when building on `@webjsdev/ui`
|
|
22
|
+
* for a real app: pull the primitive, then make it yours.
|
|
23
|
+
*/
|
|
24
|
+
import { cn } from '#lib/utils/cn.ts';
|
|
25
|
+
|
|
26
|
+
// One surface for every text field. `focus:border-primary` is the whole focus
|
|
27
|
+
// affordance (no ring), to match this app's quiet, token-driven chrome.
|
|
28
|
+
const BASE =
|
|
29
|
+
'w-full bg-background border border-border rounded-xl px-3 py-2 text-[15px] text-foreground outline-none transition-colors focus:border-primary placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-60';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Compose the Tailwind classes for a text input or textarea. Append your own
|
|
33
|
+
* layout classes as needed: `class="${inputClass()} max-w-sm"`.
|
|
34
|
+
*/
|
|
35
|
+
export function inputClass(extra?: string): string {
|
|
36
|
+
return cn(BASE, extra);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// A BORDERLESS field for composing INSIDE a card/pill: a `cardClass()` form with a
|
|
40
|
+
// button (a search-bar). The card is the bordered container, so the field is
|
|
41
|
+
// seamless (transparent, no border), and it flexes to fill the row. Distinct from
|
|
42
|
+
// inputClass (a standalone bordered field); a bordered input inside a bordered
|
|
43
|
+
// card would be a box-in-a-box.
|
|
44
|
+
const BARE =
|
|
45
|
+
'flex-1 min-w-0 bg-transparent border-0 outline-none py-1.5 text-[15px] text-foreground placeholder:text-muted-foreground';
|
|
46
|
+
|
|
47
|
+
/** Compose the classes for a borderless text field composed inside a card pill. */
|
|
48
|
+
export function bareInputClass(extra?: string): string {
|
|
49
|
+
return cn(BARE, extra);
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ui.ts: repeated MARKUP-chunk helpers (the `lib/utils/ui.ts` pattern).
|
|
3
|
+
*
|
|
4
|
+
* A design system has two kinds of helper. A repeated PRIMITIVE with variants
|
|
5
|
+
* (button, input, card, badge) is a CLASS helper in `components/ui/` returning a
|
|
6
|
+
* class string. A repeated markup CHUNK (a heading, a lede, a back link) is an
|
|
7
|
+
* `html`-fragment helper here in `lib/utils/ui.ts`: it returns SSR-time markup,
|
|
8
|
+
* so the output is byte-identical to writing the tags inline, with no client
|
|
9
|
+
* runtime. Use these for chunks that repeat across pages; keep genuinely one-off
|
|
10
|
+
* or trivial prose (a lone `<code>`, a single text link) inline for readability.
|
|
11
|
+
*
|
|
12
|
+
* OWN-AND-THEME: these fragments are THIS gallery's shared bits. Change a class
|
|
13
|
+
* here and every page updates at once. Build your own for your app.
|
|
14
|
+
*/
|
|
15
|
+
import { html } from '@webjsdev/core';
|
|
16
|
+
|
|
17
|
+
/** A page's `<h1>` heading (the standard demo/page title). */
|
|
18
|
+
export function pageHeading(title: unknown) {
|
|
19
|
+
return html`<h1 class="text-h2 font-bold mb-4">${title}</h1>`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The muted intro paragraph under a heading. Pass a string OR an `html` fragment
|
|
23
|
+
* (a lede often wraps an inline `<code>`). */
|
|
24
|
+
export function lede(content: unknown) {
|
|
25
|
+
return html`<p class="text-muted-foreground mb-4">${content}</p>`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A slim "back" link (used above each gallery demo to return to the index). */
|
|
29
|
+
export function backLink(href: string, label: unknown) {
|
|
30
|
+
return html`<a href=${href} class="inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground transition-colors no-underline mb-6">${label}</a>`;
|
|
31
|
+
}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// closes in disconnectedCallback. At SSR it renders the disconnected state, so
|
|
5
5
|
// the page still reads with JS off (a live feed has no no-JS equivalent).
|
|
6
6
|
import { WebComponent, signal, html, connectWS } from '@webjsdev/core';
|
|
7
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
8
|
+
import { inputClass } from '#components/ui/input.ts';
|
|
7
9
|
|
|
8
10
|
export class BroadcastFeed extends WebComponent {
|
|
9
11
|
private connected = signal(false);
|
|
@@ -45,9 +47,9 @@ export class BroadcastFeed extends WebComponent {
|
|
|
45
47
|
</div>
|
|
46
48
|
<form @submit=${(e: SubmitEvent) => this.send(e)} class="flex gap-2">
|
|
47
49
|
<input name="msg" autocomplete="off" placeholder="Message everyone"
|
|
48
|
-
class
|
|
50
|
+
class=${inputClass('flex-1 min-w-0')} />
|
|
49
51
|
<button type="submit"
|
|
50
|
-
class
|
|
52
|
+
class=${buttonClass({ size: 'sm' })}>Send</button>
|
|
51
53
|
</form>
|
|
52
54
|
<ul class="grid gap-1.5 list-none m-0 p-0">
|
|
53
55
|
${this.lines.get().map((line) => html`
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// button is inert (cache eviction there happens as a side effect of the write
|
|
4
4
|
// action that changed the data).
|
|
5
5
|
import { WebComponent, signal, html } from '@webjsdev/core';
|
|
6
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
6
7
|
import { bustCaches } from '#modules/caching/actions/bust-caches.server.ts';
|
|
7
8
|
|
|
8
9
|
export class CacheBuster extends WebComponent {
|
|
@@ -21,7 +22,7 @@ export class CacheBuster extends WebComponent {
|
|
|
21
22
|
return html`
|
|
22
23
|
<div class="flex items-center gap-3 text-[15px]">
|
|
23
24
|
<button @click=${() => this.bust()}
|
|
24
|
-
class
|
|
25
|
+
class=${buttonClass({ variant: 'secondary', size: 'sm' })}>revalidate this page</button>
|
|
25
26
|
<span class="text-muted-foreground">${this.status.get()}</span>
|
|
26
27
|
</div>
|
|
27
28
|
`;
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// home; a page/layout never hydrates. With JS off the plain link still works
|
|
13
13
|
// (progressive enhancement), while the buttons are inert.
|
|
14
14
|
import { WebComponent, html, signal, navigate, revalidate, disableClientRouter, enableClientRouter } from '@webjsdev/core';
|
|
15
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
15
16
|
|
|
16
17
|
export class RouterControls extends WebComponent {
|
|
17
18
|
// Instance signal mirroring whether soft navigation is currently on.
|
|
@@ -30,13 +31,13 @@ export class RouterControls extends WebComponent {
|
|
|
30
31
|
<div class="flex flex-wrap gap-3 items-center">
|
|
31
32
|
<button
|
|
32
33
|
@click=${() => navigate('/features/client-router/second')}
|
|
33
|
-
class
|
|
34
|
+
class=${buttonClass({ variant: 'secondary' })}>navigate() to page two</button>
|
|
34
35
|
<button
|
|
35
36
|
@click=${() => revalidate()}
|
|
36
|
-
class
|
|
37
|
+
class=${buttonClass({ variant: 'link', size: 'none' })}>revalidate() the snapshot cache</button>
|
|
37
38
|
<button
|
|
38
39
|
@click=${() => this.toggleRouter()}
|
|
39
|
-
class
|
|
40
|
+
class=${buttonClass({ variant: 'link', size: 'none' })}>${soft ? 'disableClientRouter()' : 'enableClientRouter()'} (soft nav: ${soft ? 'on' : 'off'})</button>
|
|
40
41
|
</div>
|
|
41
42
|
<p class="text-sm text-muted-foreground">
|
|
42
43
|
Plain link:
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// component authoring shape. Styling is crafted Tailwind on the app's design
|
|
5
5
|
// tokens so it stays visually coherent with the rest of the app.
|
|
6
6
|
import { WebComponent, prop, signal, html } from '@webjsdev/core';
|
|
7
|
+
import { cardClass } from '#components/ui/card.ts';
|
|
8
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
7
9
|
|
|
8
10
|
export class CounterCard extends WebComponent({
|
|
9
11
|
// A reactive prop rides an HTML attribute / SSR hydration. Declared ONLY here
|
|
@@ -20,14 +22,14 @@ export class CounterCard extends WebComponent({
|
|
|
20
22
|
|
|
21
23
|
render() {
|
|
22
24
|
return html`
|
|
23
|
-
<div class="grid gap-4 p-5
|
|
25
|
+
<div class="${cardClass()} grid gap-4 p-5 max-w-[320px]">
|
|
24
26
|
<slot></slot>
|
|
25
27
|
<div class="flex items-baseline gap-2">
|
|
26
28
|
<span class="text-[2.5rem] font-bold tabular-nums leading-none text-foreground">${this.count.get()}</span>
|
|
27
29
|
<span class="text-sm text-muted-foreground">${this.label}</span>
|
|
28
30
|
</div>
|
|
29
31
|
<button @click=${() => this.count.set(this.count.get() + 1)}
|
|
30
|
-
class="w-fit
|
|
32
|
+
class="${buttonClass()} w-fit">Increment</button>
|
|
31
33
|
</div>
|
|
32
34
|
`;
|
|
33
35
|
}
|
|
@@ -10,9 +10,17 @@ export class ReactiveMeter extends WebComponent {
|
|
|
10
10
|
static styles = css`
|
|
11
11
|
.row { display: flex; align-items: center; gap: 8px; }
|
|
12
12
|
button {
|
|
13
|
-
padding: 6px 12px; border-radius: 10px; border: 1px solid
|
|
13
|
+
padding: 6px 12px; border-radius: 10px; border: 1px solid var(--color-border);
|
|
14
14
|
background: transparent; color: inherit; cursor: pointer; font: inherit;
|
|
15
15
|
}
|
|
16
|
+
/* Tailwind's focus styles do NOT cross the shadow boundary, so a shadow-DOM
|
|
17
|
+
component styles its own focus, else the browser's default outline (thick,
|
|
18
|
+
shown on window-refocus) appears. Match the design system's global ring
|
|
19
|
+
EXACTLY: the --ring colour at 50% (the same as outline-ring/50). */
|
|
20
|
+
button:focus-visible {
|
|
21
|
+
outline: 2px solid color-mix(in oklab, var(--color-ring) 50%, transparent);
|
|
22
|
+
outline-offset: 2px;
|
|
23
|
+
}
|
|
16
24
|
.val { font-variant-numeric: tabular-nums; font-weight: 600; }
|
|
17
25
|
.muted { opacity: 0.6; font-size: 13px; }
|
|
18
26
|
`;
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// `async render()` instead, which blocks SSR so the data is server-rendered;
|
|
7
7
|
// a Task shows its PENDING state at SSR, so the value is not in the first paint.
|
|
8
8
|
import { WebComponent, html } from '@webjsdev/core';
|
|
9
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
9
10
|
import { Task, TaskStatus } from '@webjsdev/core/task';
|
|
10
11
|
|
|
11
12
|
export class TaskLoader extends WebComponent {
|
|
@@ -33,13 +34,13 @@ export class TaskLoader extends WebComponent {
|
|
|
33
34
|
const t = this.task;
|
|
34
35
|
const body =
|
|
35
36
|
t.status === TaskStatus.PENDING ? html`<span class="text-muted-foreground">loading…</span>`
|
|
36
|
-
: t.status === TaskStatus.ERROR ? html`<span class="text-
|
|
37
|
+
: t.status === TaskStatus.ERROR ? html`<span class="text-destructive">error: ${String((t.error as Error)?.message ?? t.error)}</span>`
|
|
37
38
|
: t.status === TaskStatus.COMPLETE ? html`<span class="text-foreground">${t.value}</span>`
|
|
38
39
|
: html`<span class="text-muted-foreground">idle</span>`;
|
|
39
40
|
return html`
|
|
40
41
|
<div class="flex items-center gap-3 text-[15px]">
|
|
41
42
|
<button @click=${() => this.reload()}
|
|
42
|
-
class
|
|
43
|
+
class=${buttonClass({ variant: 'secondary', size: 'sm' })}>reload</button>
|
|
43
44
|
${body}
|
|
44
45
|
</div>
|
|
45
46
|
`;
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
// controllers automate. All light DOM, so the provider's <slot> projects its
|
|
8
8
|
// children normally.
|
|
9
9
|
import { WebComponent, html, signal } from '@webjsdev/core';
|
|
10
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
11
|
+
import { cardClass } from '#components/ui/card.ts';
|
|
10
12
|
import { createContext, ContextProvider, ContextConsumer, ContextRequestEvent } from '@webjsdev/core/context';
|
|
11
13
|
|
|
12
14
|
type Theme = 'light' | 'dark';
|
|
@@ -26,9 +28,9 @@ export class ThemeProvider extends WebComponent {
|
|
|
26
28
|
|
|
27
29
|
render() {
|
|
28
30
|
return html`
|
|
29
|
-
<div class
|
|
31
|
+
<div class=${cardClass('grid gap-3 p-3 max-w-[420px]')}>
|
|
30
32
|
<button @click=${() => this.toggle()}
|
|
31
|
-
class="
|
|
33
|
+
class="${buttonClass({ size: 'sm' })} w-fit">
|
|
32
34
|
provider theme: ${this.provider.value} (toggle)
|
|
33
35
|
</button>
|
|
34
36
|
<slot></slot>
|
|
@@ -58,7 +60,7 @@ export class ThemeConsumer extends WebComponent {
|
|
|
58
60
|
<div class="flex items-center gap-3 text-[15px] text-foreground">
|
|
59
61
|
<span>child sees: <strong>${theme}</strong></span>
|
|
60
62
|
<button @click=${() => this.readOnce()}
|
|
61
|
-
class
|
|
63
|
+
class=${buttonClass({ variant: 'secondary', size: 'xs' })}>read once</button>
|
|
62
64
|
<span class="text-muted-foreground text-sm">last one-shot: ${this.lastRead.get()}</span>
|
|
63
65
|
</div>
|
|
64
66
|
`;
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
// HTML), and `asyncAppend` / `asyncReplace` (stream values from an async
|
|
13
13
|
// iterable, appending each or replacing with the latest).
|
|
14
14
|
import { WebComponent, signal, html } from '@webjsdev/core';
|
|
15
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
16
|
+
import { cn } from '#lib/utils/cn.ts';
|
|
17
|
+
import { inputClass } from '#components/ui/input.ts';
|
|
15
18
|
import { repeat, watch, live, until, keyed, unsafeHTML, ref, createRef, guard, cache, templateContent, asyncAppend, asyncReplace } from '@webjsdev/core/directives';
|
|
16
19
|
|
|
17
20
|
interface Item { id: number; label: string }
|
|
@@ -107,9 +110,9 @@ export class DirectiveDemo extends WebComponent {
|
|
|
107
110
|
<div class="grid gap-4">
|
|
108
111
|
<div class="flex gap-2">
|
|
109
112
|
<button @click=${() => this.reverse()}
|
|
110
|
-
class
|
|
113
|
+
class=${buttonClass({ size: 'sm' })}>Reverse</button>
|
|
111
114
|
<button @click=${() => this.add()}
|
|
112
|
-
class
|
|
115
|
+
class=${buttonClass({ variant: 'secondary', size: 'sm' })}>Add</button>
|
|
113
116
|
</div>
|
|
114
117
|
<!-- repeat keyed by item.id: reversing reuses the existing <li> nodes. -->
|
|
115
118
|
<ul class="grid gap-2 list-none m-0 p-0">
|
|
@@ -121,7 +124,7 @@ export class DirectiveDemo extends WebComponent {
|
|
|
121
124
|
</ul>
|
|
122
125
|
<!-- watch(signal) swaps only this node when ticks changes. -->
|
|
123
126
|
<button @click=${() => this.ticks.set(this.ticks.get() + 1)}
|
|
124
|
-
class="
|
|
127
|
+
class="${buttonClass({ variant: 'link', size: 'none' })} w-fit">ticks: ${watch(this.ticks)}</button>
|
|
125
128
|
</div>
|
|
126
129
|
|
|
127
130
|
<div class="grid gap-3 border-t border-border pt-4">
|
|
@@ -132,18 +135,18 @@ export class DirectiveDemo extends WebComponent {
|
|
|
132
135
|
<input
|
|
133
136
|
${ref(this.inputRef)}
|
|
134
137
|
aria-label="Editable text for the ref focus demo"
|
|
135
|
-
class
|
|
138
|
+
class=${inputClass('flex-1 min-w-0')}
|
|
136
139
|
.value=${live(this.text.get())}
|
|
137
140
|
@input=${(e: Event) => this.text.set((e.target as HTMLInputElement).value)}>
|
|
138
141
|
<button @click=${() => this.focusInput()}
|
|
139
|
-
class
|
|
142
|
+
class=${buttonClass({ variant: 'secondary', size: 'sm' })}>Focus</button>
|
|
140
143
|
</div>
|
|
141
144
|
<!-- until shows the fallback until the promise resolves. -->
|
|
142
145
|
<p class="text-sm text-muted-foreground">async: ${until(this.asyncValue, 'loading...')}</p>
|
|
143
146
|
<!-- keyed discards and rebuilds this subtree when the key changes;
|
|
144
147
|
unsafeHTML injects trusted, author-written HTML (NEVER user input). -->
|
|
145
148
|
<button @click=${() => this.variant.set(variant + 1)}
|
|
146
|
-
class="
|
|
149
|
+
class="${buttonClass({ variant: 'link', size: 'none' })} w-fit">rekey</button>
|
|
147
150
|
${keyed(variant, html`<div class="text-[15px] text-foreground">${unsafeHTML('<em>fresh subtree</em>')} #${variant}</div>`)}
|
|
148
151
|
</div>
|
|
149
152
|
|
|
@@ -152,16 +155,20 @@ export class DirectiveDemo extends WebComponent {
|
|
|
152
155
|
guard counter re-renders it; the OTHER counter (ticks above) does
|
|
153
156
|
not, so the guarded value stays put. -->
|
|
154
157
|
<button @click=${() => this.guardBumps.set(this.guardBumps.get() + 1)}
|
|
155
|
-
class="
|
|
158
|
+
class="${buttonClass({ variant: 'link', size: 'none' })} w-fit">bump guard dep</button>
|
|
156
159
|
<p class="text-sm text-foreground">guarded: ${guard([this.guardBumps.get()], () => html`computed at bump #${this.guardBumps.get()}`)}</p>
|
|
157
160
|
|
|
158
161
|
<!-- cache(value) keeps the inactive tab's DOM alive while you toggle,
|
|
159
162
|
so switching back is instant and preserves any element state. -->
|
|
163
|
+
<!-- A tab toggle must keep a CONSTANT size between states, or the row
|
|
164
|
+
reflows and the content below dances. So both tabs use ONE variant
|
|
165
|
+
(same weight, border, padding) and cn() overrides only the active
|
|
166
|
+
COLORS, never the box. -->
|
|
160
167
|
<div class="flex gap-2">
|
|
161
168
|
<button @click=${() => this.tab.set('a')}
|
|
162
|
-
class
|
|
169
|
+
class=${cn(buttonClass({ variant: 'secondary', size: 'xs' }), this.tab.get() === 'a' && 'bg-primary text-primary-foreground border-primary hover:border-primary')}>Tab A</button>
|
|
163
170
|
<button @click=${() => this.tab.set('b')}
|
|
164
|
-
class
|
|
171
|
+
class=${cn(buttonClass({ variant: 'secondary', size: 'xs' }), this.tab.get() === 'b' && 'bg-primary text-primary-foreground border-primary hover:border-primary')}>Tab B</button>
|
|
165
172
|
</div>
|
|
166
173
|
<div class="text-[15px] text-foreground">${cache(
|
|
167
174
|
this.tab.get() === 'a'
|
|
@@ -181,7 +188,7 @@ export class DirectiveDemo extends WebComponent {
|
|
|
181
188
|
you can watch them replay. The run counter forces the re-render. -->
|
|
182
189
|
<div class="flex items-center gap-3">
|
|
183
190
|
<button @click=${() => this.restartStreams()}
|
|
184
|
-
class="
|
|
191
|
+
class="${buttonClass({ variant: 'secondary', size: 'sm' })} w-fit">restart streams</button>
|
|
185
192
|
<span class="text-sm text-muted-foreground">run #${this.streamRun.get()}</span>
|
|
186
193
|
</div>
|
|
187
194
|
<div class="text-sm text-muted-foreground">log (asyncAppend, one row per value):</div>
|