@webjsdev/cli 0.10.30 → 0.10.31
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 +1 -1
- package/lib/api-gallery.js +229 -0
- package/lib/create.js +451 -158
- package/lib/saas-template.js +39 -15
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +67 -1
- package/templates/.claude/hooks/check-server-imports.mjs +86 -0
- package/templates/.claude/hooks/check-server-imports.sh +26 -0
- package/templates/.claude/settings.json +9 -0
- package/templates/.cursorrules +40 -1
- package/templates/.github/copilot-instructions.md +40 -1
- package/templates/AGENTS.md +155 -9
- package/templates/CONVENTIONS.md +149 -10
- package/templates/gallery/app/examples/todo/page.ts +34 -0
- package/templates/gallery/app/features/async-render/page.ts +14 -0
- package/templates/gallery/app/features/broadcast/feed/route.ts +19 -0
- package/templates/gallery/app/features/broadcast/page.ts +24 -0
- package/templates/gallery/app/features/caching/page.ts +39 -0
- package/templates/gallery/app/features/client-router/page.ts +34 -0
- package/templates/gallery/app/features/client-router/second/page.ts +20 -0
- package/templates/gallery/app/features/components/page.ts +14 -0
- package/templates/gallery/app/features/directives/page.ts +14 -0
- package/templates/gallery/app/features/env/page.ts +36 -0
- package/templates/gallery/app/features/file-storage/file/[key]/route.ts +19 -0
- package/templates/gallery/app/features/file-storage/page.ts +62 -0
- package/templates/gallery/app/features/forms/page.ts +72 -0
- package/templates/gallery/app/features/metadata/page.ts +55 -0
- package/templates/gallery/app/features/optimistic-ui/page.ts +14 -0
- package/templates/gallery/app/features/rate-limit/page.ts +29 -0
- package/templates/gallery/app/features/rate-limit/ping/middleware.ts +8 -0
- package/templates/gallery/app/features/rate-limit/ping/route.ts +7 -0
- package/templates/gallery/app/features/route-handler/data/route.ts +8 -0
- package/templates/gallery/app/features/route-handler/page.ts +13 -0
- package/templates/gallery/app/features/routing/[id]/page.ts +25 -0
- package/templates/gallery/app/features/routing/page.ts +47 -0
- package/templates/gallery/app/features/server-actions/page.ts +14 -0
- package/templates/gallery/app/features/service-worker/page.ts +35 -0
- package/templates/gallery/app/features/websockets/echo/route.ts +19 -0
- package/templates/gallery/app/features/websockets/page.ts +25 -0
- package/templates/gallery/modules/async-render/components/server-clock.ts +26 -0
- package/templates/gallery/modules/async-render/queries/server-greeting.server.ts +9 -0
- package/templates/gallery/modules/broadcast/components/broadcast-feed.ts +61 -0
- package/templates/gallery/modules/components/components/counter-card.ts +35 -0
- package/templates/gallery/modules/directives/components/directive-demo.ts +53 -0
- package/templates/gallery/modules/file-storage/actions/store-upload.server.ts +18 -0
- package/templates/gallery/modules/optimistic-ui/actions/like-post.server.ts +9 -0
- package/templates/gallery/modules/optimistic-ui/components/like-button.ts +35 -0
- package/templates/gallery/modules/rate-limit/components/rate-probe.ts +49 -0
- package/templates/gallery/modules/server-actions/actions/greet.server.ts +12 -0
- package/templates/gallery/modules/server-actions/components/greeter.ts +30 -0
- package/templates/gallery/modules/server-actions/utils/format.server.ts +8 -0
- package/templates/gallery/modules/todo/actions/create-todo.server.ts +16 -0
- package/templates/gallery/modules/todo/actions/delete-todo.server.ts +15 -0
- package/templates/gallery/modules/todo/actions/toggle-todo.server.ts +21 -0
- package/templates/gallery/modules/todo/components/todo-app.ts +140 -0
- package/templates/gallery/modules/todo/queries/list-todos.server.ts +19 -0
- package/templates/gallery/modules/todo/types.ts +12 -0
- package/templates/gallery/modules/websockets/components/ws-echo.ts +62 -0
- package/templates/lib/utils/ui.ts +4 -4
package/lib/create.js
CHANGED
|
@@ -101,7 +101,10 @@ async function readUiComponent(name) {
|
|
|
101
101
|
// it to the scaffolded app's aliased path (cn lives at lib/utils/cn.ts).
|
|
102
102
|
return raw
|
|
103
103
|
.replaceAll("'../lib/utils.ts'", "'#lib/utils/cn.ts'")
|
|
104
|
-
.replaceAll('"../lib/utils.ts"', '"#lib/utils/cn.ts"')
|
|
104
|
+
.replaceAll('"../lib/utils.ts"', '"#lib/utils/cn.ts"')
|
|
105
|
+
// onBeforeCache lives in its own client-only module so cn() stays pure (#819).
|
|
106
|
+
.replaceAll("'../lib/dom.ts'", "'#lib/utils/dom.ts'")
|
|
107
|
+
.replaceAll('"../lib/dom.ts"', '"#lib/utils/dom.ts"');
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
/**
|
|
@@ -131,6 +134,28 @@ async function copyUiComponents(appDir, names) {
|
|
|
131
134
|
}
|
|
132
135
|
}
|
|
133
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Copy the example gallery (idiomatic, densely-commented working examples) into
|
|
139
|
+
* the scaffolded app. Merges `templates/gallery/{app,modules}` over the app so
|
|
140
|
+
* single-feature demos land under `app/features/<name>/`, whole example apps
|
|
141
|
+
* under `app/examples/<name>/`, and their logic under `modules/<name>/`, the
|
|
142
|
+
* app-thin + modules-logic split webjs prescribes.
|
|
143
|
+
*
|
|
144
|
+
* Ships verbatim (no `{{APP_NAME}}` substitution): the examples are self-
|
|
145
|
+
* contained and reference only `@webjsdev/*`, drizzle, `#db/*`, and each other.
|
|
146
|
+
* The scaffold's own `app/page.ts` / `app/layout.ts` are written AFTER this and
|
|
147
|
+
* the gallery ships neither, so there is no clobber. `cp` merges into existing
|
|
148
|
+
* `app/` and `modules/` dirs rather than replacing them.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} appDir
|
|
151
|
+
*/
|
|
152
|
+
async function copyGallery(appDir) {
|
|
153
|
+
const galleryDir = join(TEMPLATES, 'gallery');
|
|
154
|
+
for (const sub of ['app', 'modules']) {
|
|
155
|
+
await cp(join(galleryDir, sub), join(appDir, sub), { recursive: true });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
134
159
|
/**
|
|
135
160
|
* Write `lib/utils/cn.ts` (the `cn()` helper) and `components.json` so the
|
|
136
161
|
* scaffolded app is pre-initialised for `webjs ui add`. Reads the registry's
|
|
@@ -144,21 +169,31 @@ async function writeUiBootstrap(appDir) {
|
|
|
144
169
|
// Caller (scaffoldApp) has already invoked assertUiRegistryAvailable(),
|
|
145
170
|
// so the source files below are guaranteed to exist.
|
|
146
171
|
|
|
147
|
-
// 1) lib/utils/cn.ts: the cn() helper
|
|
172
|
+
// 1) lib/utils/cn.ts: the cn() helper (pure; safe to import into a page).
|
|
148
173
|
const utilsContent = await readFile(
|
|
149
174
|
join(UI_REGISTRY_ROOT, 'lib', 'utils.ts'), 'utf8',
|
|
150
175
|
);
|
|
151
176
|
await mkdir(join(appDir, 'lib', 'utils'), { recursive: true });
|
|
152
177
|
await writeFile(join(appDir, 'lib', 'utils', 'cn.ts'), utilsContent);
|
|
153
178
|
|
|
179
|
+
// 1b) lib/utils/dom.ts: the client-only DOM helper (onBeforeCache). Split out
|
|
180
|
+
// of cn.ts so importing cn() does not pin a page to the browser (#819). Any
|
|
181
|
+
// `webjs ui add` component that uses onBeforeCache imports it from here.
|
|
182
|
+
const domContent = await readFile(
|
|
183
|
+
join(UI_REGISTRY_ROOT, 'lib', 'dom.ts'), 'utf8',
|
|
184
|
+
);
|
|
185
|
+
await writeFile(join(appDir, 'lib', 'utils', 'dom.ts'), domContent);
|
|
186
|
+
|
|
154
187
|
// 2) components.json: the same shape `webjsui init` writes for webjs
|
|
155
188
|
// projects (see packages/ui/src/utils/detect-project.js). The utils alias
|
|
156
189
|
// is lib/utils/cn so get-config.js's `+ '.ts'` resolves to lib/utils/cn.ts.
|
|
190
|
+
// The theme CSS lives at styles/globals.css, NOT app/globals.css: app/ is
|
|
191
|
+
// routing-only, so a non-routing stylesheet does not belong there.
|
|
157
192
|
const componentsJson = {
|
|
158
193
|
$schema: 'https://ui.webjs.dev/schema.json',
|
|
159
194
|
style: 'default',
|
|
160
195
|
tailwind: {
|
|
161
|
-
css: '
|
|
196
|
+
css: 'styles/globals.css',
|
|
162
197
|
baseColor: 'neutral',
|
|
163
198
|
cssVariables: true,
|
|
164
199
|
},
|
|
@@ -175,17 +210,20 @@ async function writeUiBootstrap(appDir) {
|
|
|
175
210
|
JSON.stringify(componentsJson, null, 2) + '\n',
|
|
176
211
|
);
|
|
177
212
|
|
|
178
|
-
// 3)
|
|
179
|
-
// references this path, and future `webjs ui add` calls append to it.
|
|
213
|
+
// 3) styles/globals.css: copy the neutral theme verbatim. components.json
|
|
214
|
+
// references this path, and future `webjs ui add` calls append to it. It
|
|
215
|
+
// lives OUTSIDE app/ because app/ is routing-only; the layout inlines the
|
|
216
|
+
// same tokens into a <style type="text/tailwindcss"> block so the browser
|
|
217
|
+
// Tailwind runtime resolves them with no build step.
|
|
180
218
|
const css = await readFile(
|
|
181
219
|
join(UI_REGISTRY_ROOT, 'themes', 'index.css'), 'utf8',
|
|
182
220
|
);
|
|
183
|
-
await mkdir(join(appDir, '
|
|
184
|
-
await writeFile(join(appDir, '
|
|
221
|
+
await mkdir(join(appDir, 'styles'), { recursive: true });
|
|
222
|
+
await writeFile(join(appDir, 'styles', 'globals.css'), css);
|
|
185
223
|
}
|
|
186
224
|
|
|
187
225
|
/**
|
|
188
|
-
* Read the
|
|
226
|
+
* Read the @webjsdev/ui theme CSS so we can inline it into the layout's
|
|
189
227
|
* `<style type="text/tailwindcss">` block. The Tailwind browser runtime
|
|
190
228
|
* picks up inline `<style type="text/tailwindcss">` content, so the theme
|
|
191
229
|
* tokens (`--color-primary`, `--color-card`, …) the registry components
|
|
@@ -200,7 +238,7 @@ async function readThemeCss() {
|
|
|
200
238
|
|
|
201
239
|
/**
|
|
202
240
|
* Fail loudly when the @webjsdev/ui registry is not on disk. The scaffold
|
|
203
|
-
* reads component sources, the cn() helper, and the
|
|
241
|
+
* reads component sources, the cn() helper, and the @webjsdev/ui theme from
|
|
204
242
|
* UI_REGISTRY_ROOT and weaves them into a generated app/page.ts that
|
|
205
243
|
* imports `components/ui/button.ts`. If the registry is missing, the
|
|
206
244
|
* generated app boots to ERR_MODULE_NOT_FOUND on first request, which is
|
|
@@ -236,6 +274,11 @@ function assertUiRegistryAvailable() {
|
|
|
236
274
|
*/
|
|
237
275
|
export async function scaffoldApp(name, cwd, opts = {}) {
|
|
238
276
|
const template = opts.template || 'full-stack';
|
|
277
|
+
// A human-friendly display title for the example home page. The npm `name`
|
|
278
|
+
// stays the raw slug (lowercase, hyphenated), but showing a hyphenated slug as
|
|
279
|
+
// a hero title looks unpolished, so title-case it for display ("my-app" ->
|
|
280
|
+
// "My App"). Replace this with your real brand anyway.
|
|
281
|
+
const displayName = name.replace(/[-_]+/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
239
282
|
// `install` is opt-in at the library level (so tests + programmatic
|
|
240
283
|
// callers get a side-effect-free scaffold by default). The CLI entry
|
|
241
284
|
// points (`webjs create` and `npx create-webjs-app`) explicitly set
|
|
@@ -251,6 +294,13 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
251
294
|
}
|
|
252
295
|
const isApi = template === 'api';
|
|
253
296
|
const isSaas = template === 'saas';
|
|
297
|
+
// The example gallery ships in every UI scaffold (full-stack AND saas). The
|
|
298
|
+
// copyGallery gate below is !isApi, since only the api template has no UI. saas
|
|
299
|
+
// overwrites db/schema.server.ts with its own schema (which includes the
|
|
300
|
+
// gallery's todos table) and renders the gallery below its auth landing.
|
|
301
|
+
// isFullStack distinguishes the plain full-stack app from saas for the parts
|
|
302
|
+
// that differ (its own home page and the create.js-written schema).
|
|
303
|
+
const isFullStack = !isApi && !isSaas;
|
|
254
304
|
|
|
255
305
|
// Database dialect (#563): sqlite (default) or postgres. Drizzle is the ORM;
|
|
256
306
|
// the schema/queries/actions are identical across dialects, only db/columns
|
|
@@ -381,9 +431,10 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
381
431
|
// tsserver plugin can't provide); tsserver dedupes by name, so loading
|
|
382
432
|
// it both ways is a no-op. Standalone, no Lit dependency. Editor-only.
|
|
383
433
|
'@webjsdev/intellisense': 'latest',
|
|
384
|
-
// NOTE: @webjsdev/ui is intentionally NOT pinned. The UI kit
|
|
385
|
-
// shadcn-
|
|
386
|
-
// into components/ui/ (they import
|
|
434
|
+
// NOTE: @webjsdev/ui is intentionally NOT pinned. The UI kit uses a
|
|
435
|
+
// copy-in model (shadcn-compatible conventions): `webjs ui add <name>`
|
|
436
|
+
// copies component source into components/ui/ (they import
|
|
437
|
+
// @webjsdev/core, not the kit), and the
|
|
387
438
|
// CLI resolves @webjsdev/ui from its own install.
|
|
388
439
|
},
|
|
389
440
|
// Dev + start task orchestration (#550). `webjs dev` / `webjs start` read
|
|
@@ -478,6 +529,8 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
478
529
|
'.claude/hooks/guard-branch-context.sh',
|
|
479
530
|
'.claude/hooks/nudge-uncommitted.sh',
|
|
480
531
|
'.claude/hooks/require-tests-with-src.sh',
|
|
532
|
+
'.claude/hooks/check-server-imports.sh',
|
|
533
|
+
'.claude/hooks/check-server-imports.mjs',
|
|
481
534
|
// Gemini CLI config + hooks
|
|
482
535
|
'.gemini/settings.json',
|
|
483
536
|
'.gemini/hooks/nudge-uncommitted.sh',
|
|
@@ -622,7 +675,7 @@ export const index = (...cols: PgColumn[]) =>
|
|
|
622
675
|
|
|
623
676
|
// Example schema (dialect-agnostic). Replace the User model with your own.
|
|
624
677
|
await writeFile(join(appDir, 'db', 'schema.server.ts'), `import { defineRelations } from 'drizzle-orm';
|
|
625
|
-
import { table, pk, text, createdAt } from './columns.server.ts';
|
|
678
|
+
import { table, pk, ${isFullStack ? 'uuidPk, ' : ''}text, ${isFullStack ? 'bool, ' : ''}createdAt } from './columns.server.ts';
|
|
626
679
|
|
|
627
680
|
// Example model. Feel free to delete or extend.
|
|
628
681
|
export const users = table('users', {
|
|
@@ -631,10 +684,19 @@ export const users = table('users', {
|
|
|
631
684
|
name: text(),
|
|
632
685
|
createdAt: createdAt(),
|
|
633
686
|
});
|
|
634
|
-
|
|
687
|
+
${isFullStack ? `
|
|
688
|
+
// Backs the example-gallery /examples/todo route (modules/todo). Delete it with
|
|
689
|
+
// the gallery when you prune the examples you do not use.
|
|
690
|
+
export const todos = table('todos', {
|
|
691
|
+
id: uuidPk(),
|
|
692
|
+
title: text().notNull(),
|
|
693
|
+
completed: bool().notNull().default(false),
|
|
694
|
+
createdAt: createdAt(),
|
|
695
|
+
});
|
|
696
|
+
` : ''}
|
|
635
697
|
// Relations live here (one defineRelations for the whole schema). Empty
|
|
636
698
|
// for now; add per-model relations as your schema grows.
|
|
637
|
-
export const relations = defineRelations({ users }, () => ({}));
|
|
699
|
+
export const relations = defineRelations({ users${isFullStack ? ', todos' : ''} }, () => ({}));
|
|
638
700
|
|
|
639
701
|
// Derived types, never hand-written.
|
|
640
702
|
export type User = typeof users.$inferSelect;
|
|
@@ -783,6 +845,28 @@ export default cors({
|
|
|
783
845
|
await writeFile(join(appDir, 'app', 'api', 'health', 'route.ts'), `export async function GET() {
|
|
784
846
|
return Response.json({ status: 'ok', timestamp: Date.now() });
|
|
785
847
|
}
|
|
848
|
+
`);
|
|
849
|
+
// Root API index. The api template has no UI, so \`/\` is a route handler,
|
|
850
|
+
// not a page: it lists the available endpoints instead of returning a bare
|
|
851
|
+
// 404 (friendlier than an empty root for an API-only app).
|
|
852
|
+
await writeFile(join(appDir, 'app', 'route.ts'), `export async function GET(request: Request) {
|
|
853
|
+
const base = new URL(request.url).origin;
|
|
854
|
+
return Response.json({
|
|
855
|
+
name: '${name}',
|
|
856
|
+
endpoints: {
|
|
857
|
+
health: \`\${base}/api/health\`,
|
|
858
|
+
users: \`\${base}/api/users\`,
|
|
859
|
+
},
|
|
860
|
+
// The backend-features showcase (delete app/api/features + its modules to prune).
|
|
861
|
+
features: {
|
|
862
|
+
validate: \`\${base}/api/features/validate\`,
|
|
863
|
+
'rate-limit': \`\${base}/api/features/rate-limit\`,
|
|
864
|
+
stream: \`\${base}/api/features/stream\`,
|
|
865
|
+
files: \`\${base}/api/features/files\`,
|
|
866
|
+
ws: \`\${base.replace(/^http/, 'ws')}/api/features/ws\`,
|
|
867
|
+
},
|
|
868
|
+
});
|
|
869
|
+
}
|
|
786
870
|
`);
|
|
787
871
|
await mkdir(join(appDir, 'modules', 'users', 'actions'), { recursive: true });
|
|
788
872
|
await mkdir(join(appDir, 'modules', 'users', 'queries'), { recursive: true });
|
|
@@ -865,6 +949,13 @@ export type ActionResult<T> =
|
|
|
865
949
|
| { success: true; data: T }
|
|
866
950
|
| { success: false; error: string; status: number };
|
|
867
951
|
`);
|
|
952
|
+
|
|
953
|
+
// The api backend-features showcase: endpoints under app/api/features/**
|
|
954
|
+
// demonstrating the server-side surface (route() adapter + validation, rate
|
|
955
|
+
// limiting, streaming, file storage, WebSockets + broadcast) plus env
|
|
956
|
+
// validation. The api counterpart of the UI gallery. Prune what you skip.
|
|
957
|
+
const { writeApiGallery } = await import('./api-gallery.js');
|
|
958
|
+
await writeApiGallery(appDir);
|
|
868
959
|
}
|
|
869
960
|
|
|
870
961
|
if (!isApi) {
|
|
@@ -905,7 +996,7 @@ export type ActionResult<T> =
|
|
|
905
996
|
|
|
906
997
|
// Pre-initialise @webjsdev/ui so the scaffold boots ready for
|
|
907
998
|
// `webjs ui add <name>`: writes components.json + lib/utils/cn.ts +
|
|
908
|
-
//
|
|
999
|
+
// styles/globals.css (the @webjsdev/ui theme).
|
|
909
1000
|
await writeUiBootstrap(appDir);
|
|
910
1001
|
|
|
911
1002
|
// Copy the standard ui-* component kit the scaffold's example pages
|
|
@@ -915,13 +1006,21 @@ export type ActionResult<T> =
|
|
|
915
1006
|
'button', 'card', 'alert', 'badge', 'separator', 'label', 'input',
|
|
916
1007
|
]);
|
|
917
1008
|
|
|
918
|
-
// The
|
|
1009
|
+
// The gallery: idiomatic, densely-commented single-feature demos under
|
|
1010
|
+
// app/features/ plus one whole example app under app/examples/, with logic
|
|
1011
|
+
// in modules/, linked from the home page. Shipped in the default full-stack
|
|
1012
|
+
// scaffold so an agent gains context by browsing real code; prune
|
|
1013
|
+
// per-feature (delete the route + its module) for what the app does not
|
|
1014
|
+
// use. See CONVENTIONS.md "prune what the app does not use".
|
|
1015
|
+
if (!isApi) await copyGallery(appDir);
|
|
1016
|
+
|
|
1017
|
+
// The @webjsdev/ui theme tokens (`--color-primary`, `--color-card`, …) the
|
|
919
1018
|
// ui-* components consume. We read the registry's themes/index.css at
|
|
920
1019
|
// create time and inline it into the layout's
|
|
921
1020
|
// `<style type="text/tailwindcss">` block so the Tailwind browser
|
|
922
|
-
// runtime picks it up. Same content also lives at
|
|
1021
|
+
// runtime picks it up. Same content also lives at styles/globals.css for
|
|
923
1022
|
// `webjsui` tooling.
|
|
924
|
-
const
|
|
1023
|
+
const UI_THEME = (await readThemeCss())
|
|
925
1024
|
// Escape backticks + ${} so the CSS survives interpolation into the
|
|
926
1025
|
// layout's template literal below.
|
|
927
1026
|
.replace(/\\/g, '\\\\')
|
|
@@ -948,7 +1047,7 @@ import '#components/theme-toggle.ts';
|
|
|
948
1047
|
*
|
|
949
1048
|
* Light DOM + Tailwind by default. Design tokens live in :root and are
|
|
950
1049
|
* mapped into the Tailwind palette via @theme, so classes like
|
|
951
|
-
* text-
|
|
1050
|
+
* text-foreground, bg-card, font-serif, duration-fast, text-display all work.
|
|
952
1051
|
*
|
|
953
1052
|
* Nav + footer links repeat the same class bundle, so they're extracted
|
|
954
1053
|
* into small JS helpers below. Each helper runs at SSR time inside
|
|
@@ -956,7 +1055,7 @@ import '#components/theme-toggle.ts';
|
|
|
956
1055
|
*/
|
|
957
1056
|
|
|
958
1057
|
const navLink = (href: string, label: string) => html\`
|
|
959
|
-
<a href=\${href} class="text-
|
|
1058
|
+
<a href=\${href} class="text-muted-foreground no-underline font-medium text-[13px] leading-none tracking-[0.005em] transition-colors duration-fast hover:text-foreground">\${label}</a>
|
|
960
1059
|
\`;
|
|
961
1060
|
|
|
962
1061
|
export default function RootLayout({ children }: { children: unknown }) {
|
|
@@ -976,7 +1075,7 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
976
1075
|
var el = document.documentElement;
|
|
977
1076
|
if (t === 'light' || t === 'dark') el.dataset.theme = t;
|
|
978
1077
|
else delete el.dataset.theme;
|
|
979
|
-
// Keep
|
|
1078
|
+
// Keep the .dark class the @webjsdev/ui kit uses in sync with the effective theme so the
|
|
980
1079
|
// copied ui-* components (button, card, etc.) follow light/dark too.
|
|
981
1080
|
// Dark is the default unless the OS prefers light or 'light' is set.
|
|
982
1081
|
var dark = t === 'dark' || (t !== 'light' && !mq.matches);
|
|
@@ -1011,27 +1110,124 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1011
1110
|
<!--
|
|
1012
1111
|
Webjs UI theme. Design tokens (--color-primary,
|
|
1013
1112
|
--color-card, --radius, etc.) the ui-* components consume.
|
|
1014
|
-
The same content is also at
|
|
1113
|
+
The same content is also at styles/globals.css. We inline it here so
|
|
1015
1114
|
the Tailwind browser runtime resolves the tokens without a build step.
|
|
1016
1115
|
Edit base palette via the :root / .dark blocks below.
|
|
1017
1116
|
-->
|
|
1018
1117
|
<style type="text/tailwindcss">
|
|
1019
|
-
${
|
|
1118
|
+
${UI_THEME}
|
|
1020
1119
|
</style>
|
|
1021
1120
|
<style type="text/tailwindcss">
|
|
1022
|
-
@theme
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1121
|
+
/* ONE theme, canonical shadcn-style tokens. The @webjsdev/ui theme above
|
|
1122
|
+
provides the token STRUCTURE and the @theme inline mappings that generate
|
|
1123
|
+
bg-background, text-foreground, bg-card, bg-primary, bg-accent,
|
|
1124
|
+
text-muted-foreground, border-border, ring-ring, and the rest. Here we
|
|
1125
|
+
set those tokens' VALUES to this app's brand palette, so the ui-*
|
|
1126
|
+
components AND the example chrome read ONE source of truth. Any component
|
|
1127
|
+
added later with webjs ui add <name> inherits it automatically. This
|
|
1128
|
+
block is emitted after the ui theme, so these values win on every Tailwind
|
|
1129
|
+
recompile. Dark-first, with light via the theme toggle (data-theme) or the
|
|
1130
|
+
OS. Follows the shadcn model: --primary is the BRAND color (orange, used
|
|
1131
|
+
for primary buttons, links, and emphasis), while --accent stays a NEUTRAL
|
|
1132
|
+
hover tint so the ui kit's outline/ghost/dropdown hover states keep proper
|
|
1133
|
+
contrast. Use bg-primary / text-primary for brand, bg-accent for hovers.
|
|
1134
|
+
Add a new design token the canonical way, a --x variable below plus a
|
|
1135
|
+
--color-x: var(--x) line in the @theme inline block, then use it as bg-x /
|
|
1136
|
+
text-x. Reach for opacity modifiers
|
|
1137
|
+
(bg-primary/10, hover:bg-primary/90, text-muted-foreground/70) before
|
|
1138
|
+
inventing a new token. */
|
|
1139
|
+
:root {
|
|
1140
|
+
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1141
|
+
--font-serif: ui-serif, 'Iowan Old Style', Palatino, Georgia, serif;
|
|
1142
|
+
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1143
|
+
--header-h: 56px;
|
|
1144
|
+
/* A translucent brand tint, derived from --primary so it tracks
|
|
1145
|
+
light/dark automatically. Used for the logo glow and focus ring. */
|
|
1146
|
+
--primary-tint: color-mix(in oklch, var(--primary) 20%, transparent);
|
|
1147
|
+
}
|
|
1148
|
+
/* dark (the default, and the explicit .dark the toggle sets) */
|
|
1149
|
+
:root, .dark {
|
|
1150
|
+
color-scheme: dark;
|
|
1151
|
+
--background: oklch(0.14 0.01 55);
|
|
1152
|
+
--foreground: oklch(0.96 0.015 60);
|
|
1153
|
+
--card: oklch(0.18 0.01 55);
|
|
1154
|
+
--card-foreground: oklch(0.96 0.015 60);
|
|
1155
|
+
--popover: oklch(0.18 0.01 55);
|
|
1156
|
+
--popover-foreground: oklch(0.96 0.015 60);
|
|
1157
|
+
/* --primary is the orange BRAND color (shadcn model: primary = brand). */
|
|
1158
|
+
--primary: oklch(0.7 0.16 52);
|
|
1159
|
+
--primary-foreground: oklch(0.17 0.02 52);
|
|
1160
|
+
--secondary: oklch(0.22 0.01 55);
|
|
1161
|
+
--secondary-foreground: oklch(0.96 0.015 60);
|
|
1162
|
+
--muted: oklch(0.16 0.01 55);
|
|
1163
|
+
--muted-foreground: oklch(0.72 0.02 60);
|
|
1164
|
+
/* --accent stays a NEUTRAL hover tint (shadcn model), so the ui kit's
|
|
1165
|
+
outline/ghost/dropdown hover states keep proper contrast. */
|
|
1166
|
+
--accent: oklch(0.27 0.008 55);
|
|
1167
|
+
--accent-foreground: oklch(0.96 0.015 60);
|
|
1168
|
+
--border: oklch(0.26 0.012 55 / 0.9);
|
|
1169
|
+
--border-strong: oklch(0.38 0.012 55 / 0.9);
|
|
1170
|
+
--input: oklch(0.26 0.012 55 / 0.9);
|
|
1171
|
+
--ring: oklch(0.7 0.16 52);
|
|
1172
|
+
--logo-from: oklch(0.8 0.16 58);
|
|
1173
|
+
--logo-to: oklch(0.62 0.18 44);
|
|
1174
|
+
}
|
|
1175
|
+
/* light (explicit via the toggle) */
|
|
1176
|
+
:root[data-theme='light'] {
|
|
1177
|
+
color-scheme: light;
|
|
1178
|
+
--background: oklch(0.985 0.008 80);
|
|
1179
|
+
--foreground: oklch(0.18 0.015 60);
|
|
1180
|
+
--card: oklch(1 0 0);
|
|
1181
|
+
--card-foreground: oklch(0.18 0.015 60);
|
|
1182
|
+
--popover: oklch(1 0 0);
|
|
1183
|
+
--popover-foreground: oklch(0.18 0.015 60);
|
|
1184
|
+
--primary: oklch(0.54 0.16 52);
|
|
1185
|
+
--primary-foreground: oklch(1 0 0);
|
|
1186
|
+
--secondary: oklch(0.96 0.008 80);
|
|
1187
|
+
--secondary-foreground: oklch(0.18 0.015 60);
|
|
1188
|
+
--muted: oklch(0.96 0.008 80);
|
|
1189
|
+
--muted-foreground: oklch(0.42 0.02 65);
|
|
1190
|
+
--accent: oklch(0.955 0.006 80);
|
|
1191
|
+
--accent-foreground: oklch(0.18 0.015 60);
|
|
1192
|
+
--border: oklch(0.88 0.01 75 / 0.95);
|
|
1193
|
+
--border-strong: oklch(0.78 0.01 75 / 0.95);
|
|
1194
|
+
--input: oklch(0.88 0.01 75 / 0.95);
|
|
1195
|
+
--ring: oklch(0.54 0.16 52);
|
|
1196
|
+
--logo-from: oklch(0.63 0.17 50);
|
|
1197
|
+
--logo-to: oklch(0.44 0.11 52);
|
|
1198
|
+
}
|
|
1199
|
+
/* light (OS preference, when the user has made no explicit choice) */
|
|
1200
|
+
@media (prefers-color-scheme: light) {
|
|
1201
|
+
:root:not(.dark):not([data-theme='dark']) {
|
|
1202
|
+
color-scheme: light;
|
|
1203
|
+
--background: oklch(0.985 0.008 80);
|
|
1204
|
+
--foreground: oklch(0.18 0.015 60);
|
|
1205
|
+
--card: oklch(1 0 0);
|
|
1206
|
+
--card-foreground: oklch(0.18 0.015 60);
|
|
1207
|
+
--popover: oklch(1 0 0);
|
|
1208
|
+
--popover-foreground: oklch(0.18 0.015 60);
|
|
1209
|
+
--primary: oklch(0.54 0.16 52);
|
|
1210
|
+
--primary-foreground: oklch(1 0 0);
|
|
1211
|
+
--secondary: oklch(0.96 0.008 80);
|
|
1212
|
+
--secondary-foreground: oklch(0.18 0.015 60);
|
|
1213
|
+
--muted: oklch(0.96 0.008 80);
|
|
1214
|
+
--muted-foreground: oklch(0.42 0.02 65);
|
|
1215
|
+
--accent: oklch(0.955 0.006 80);
|
|
1216
|
+
--accent-foreground: oklch(0.18 0.015 60);
|
|
1217
|
+
--border: oklch(0.88 0.01 75 / 0.95);
|
|
1218
|
+
--border-strong: oklch(0.78 0.01 75 / 0.95);
|
|
1219
|
+
--input: oklch(0.88 0.01 75 / 0.95);
|
|
1220
|
+
--ring: oklch(0.54 0.16 52);
|
|
1221
|
+
--logo-from: oklch(0.63 0.17 50);
|
|
1222
|
+
--logo-to: oklch(0.44 0.11 52);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
@theme inline {
|
|
1226
|
+
/* Only tokens the @webjsdev/ui theme does not already map live here. It
|
|
1227
|
+
already maps --color-background/foreground/card/primary/secondary/
|
|
1228
|
+
muted/accent/border/input/ring/destructive. */
|
|
1030
1229
|
--color-border-strong: var(--border-strong);
|
|
1031
|
-
--color-
|
|
1032
|
-
--color-accent-hover: var(--accent-hover);
|
|
1033
|
-
--color-accent-fg: var(--accent-fg);
|
|
1034
|
-
--color-accent-tint: var(--accent-tint);
|
|
1230
|
+
--color-primary-tint: var(--primary-tint);
|
|
1035
1231
|
--font-sans: var(--font-sans);
|
|
1036
1232
|
--font-serif: var(--font-serif);
|
|
1037
1233
|
--font-mono: var(--font-mono);
|
|
@@ -1044,71 +1240,21 @@ ${SHADCN_THEME}
|
|
|
1044
1240
|
}
|
|
1045
1241
|
</style>
|
|
1046
1242
|
<style>
|
|
1047
|
-
|
|
1048
|
-
color-scheme: light dark;
|
|
1049
|
-
/* ---------- dark (default) ---------- */
|
|
1050
|
-
--fg: oklch(0.96 0.015 60);
|
|
1051
|
-
--fg-muted: oklch(0.72 0.02 60);
|
|
1052
|
-
--fg-subtle: oklch(0.55 0.02 60);
|
|
1053
|
-
--bg: oklch(0.14 0.01 55);
|
|
1054
|
-
--bg-elev: oklch(0.18 0.01 55);
|
|
1055
|
-
--bg-subtle: oklch(0.16 0.01 55);
|
|
1056
|
-
--border: oklch(0.26 0.012 55 / 0.9);
|
|
1057
|
-
--border-strong: oklch(0.38 0.012 55 / 0.9);
|
|
1058
|
-
--accent: oklch(0.78 0.14 55);
|
|
1059
|
-
--accent-hover: oklch(0.85 0.14 55);
|
|
1060
|
-
--accent-fg: oklch(0.15 0.01 55);
|
|
1061
|
-
--accent-tint: oklch(0.78 0.14 55 / 0.14);
|
|
1062
|
-
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
1063
|
-
--font-serif: ui-serif, 'Iowan Old Style', Palatino, Georgia, serif;
|
|
1064
|
-
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1065
|
-
}
|
|
1066
|
-
:root[data-theme='light'] {
|
|
1067
|
-
--fg: oklch(0.18 0.015 60);
|
|
1068
|
-
--fg-muted: oklch(0.42 0.02 65);
|
|
1069
|
-
--fg-subtle: oklch(0.62 0.015 70);
|
|
1070
|
-
--bg: oklch(0.985 0.008 80);
|
|
1071
|
-
--bg-elev: oklch(1 0 0);
|
|
1072
|
-
--bg-subtle: oklch(0.96 0.008 80);
|
|
1073
|
-
--border: oklch(0.88 0.01 75 / 0.95);
|
|
1074
|
-
--border-strong: oklch(0.78 0.01 75 / 0.95);
|
|
1075
|
-
--accent: oklch(0.58 0.15 55);
|
|
1076
|
-
--accent-hover: oklch(0.5 0.15 55);
|
|
1077
|
-
--accent-fg: oklch(1 0 0);
|
|
1078
|
-
--accent-tint: oklch(0.58 0.15 55 / 0.1);
|
|
1079
|
-
}
|
|
1080
|
-
@media (prefers-color-scheme: light) {
|
|
1081
|
-
:root:not([data-theme='dark']) {
|
|
1082
|
-
--fg: oklch(0.18 0.015 60);
|
|
1083
|
-
--fg-muted: oklch(0.42 0.02 65);
|
|
1084
|
-
--fg-subtle: oklch(0.62 0.015 70);
|
|
1085
|
-
--bg: oklch(0.985 0.008 80);
|
|
1086
|
-
--bg-elev: oklch(1 0 0);
|
|
1087
|
-
--bg-subtle: oklch(0.96 0.008 80);
|
|
1088
|
-
--border: oklch(0.88 0.01 75 / 0.95);
|
|
1089
|
-
--border-strong: oklch(0.78 0.01 75 / 0.95);
|
|
1090
|
-
--accent: oklch(0.58 0.15 55);
|
|
1091
|
-
--accent-hover: oklch(0.5 0.15 55);
|
|
1092
|
-
--accent-fg: oklch(1 0 0);
|
|
1093
|
-
--accent-tint: oklch(0.58 0.15 55 / 0.1);
|
|
1094
|
-
}
|
|
1095
|
-
}
|
|
1096
|
-
/* Body + pseudo-elements utility classes can't reach. */
|
|
1243
|
+
/* Base styles utility classes can't reach. */
|
|
1097
1244
|
html, body { margin: 0; }
|
|
1098
|
-
:root { --header-h: 56px; } /* fixed-header offset, kept exact by the script above */
|
|
1099
1245
|
body {
|
|
1100
1246
|
padding-top: var(--header-h);
|
|
1101
|
-
background: var(--
|
|
1102
|
-
color: var(--
|
|
1247
|
+
background: var(--background);
|
|
1248
|
+
color: var(--foreground);
|
|
1103
1249
|
font: 16px/1.65 var(--font-sans);
|
|
1104
1250
|
-webkit-font-smoothing: antialiased;
|
|
1105
1251
|
}
|
|
1106
|
-
::selection { background: var(--
|
|
1252
|
+
::selection { background: color-mix(in oklch, var(--primary) 22%, transparent); color: var(--foreground); }
|
|
1107
1253
|
</style>
|
|
1108
1254
|
|
|
1109
|
-
<header class="fixed inset-x-0 top-0 z-20 flex items-center gap-6 px-4 sm:px-6 py-3 border-b border-border bg-[color-mix(in_oklch,var(--
|
|
1110
|
-
<a href="/" class="mr-auto inline-flex items-center gap-2 no-underline text-
|
|
1111
|
-
<span>${
|
|
1255
|
+
<header class="fixed inset-x-0 top-0 z-20 flex items-center gap-6 px-4 sm:px-6 py-3 border-b border-border bg-[color-mix(in_oklch,var(--background)_75%,transparent)] backdrop-blur-[18px]">
|
|
1256
|
+
<a href="/" class="mr-auto inline-flex items-center gap-2 no-underline text-foreground font-semibold text-[15px] leading-none tracking-tight">
|
|
1257
|
+
<span>${displayName}</span>
|
|
1112
1258
|
</a>
|
|
1113
1259
|
<nav class="flex gap-4 items-center">
|
|
1114
1260
|
<!-- Example nav. Replace with the real navigation for your app. -->
|
|
@@ -1124,13 +1270,30 @@ ${SHADCN_THEME}
|
|
|
1124
1270
|
drop the cap and mx-auto for an edge-to-edge layout. A wide layout left
|
|
1125
1271
|
inside the 760px reading column overflows into a horizontal scrollbar.
|
|
1126
1272
|
-->
|
|
1127
|
-
<
|
|
1128
|
-
|
|
1129
|
-
|
|
1273
|
+
<div class="flex flex-col min-h-[calc(100dvh-var(--header-h))]">
|
|
1274
|
+
<main class="flex-1 w-full max-w-[760px] mx-auto px-4 sm:px-6 pt-[72px] pb-12">
|
|
1275
|
+
\${children}
|
|
1276
|
+
</main>
|
|
1277
|
+
<!-- "Built with webjs" attribution. Keep it or replace it with your own
|
|
1278
|
+
footer; the gradient mark uses the --logo-from/--logo-to tokens. -->
|
|
1279
|
+
<footer class="border-t border-border">
|
|
1280
|
+
<div class="max-w-[760px] mx-auto px-4 sm:px-6 py-6 flex items-center justify-center">
|
|
1281
|
+
<a href="https://webjs.dev" class="inline-flex items-center gap-2 no-underline text-sm text-muted-foreground hover:text-foreground transition-colors">
|
|
1282
|
+
<span>Built with</span>
|
|
1283
|
+
<span class="w-[18px] h-[18px] rounded-[6px] bg-gradient-to-br from-[var(--logo-from)] to-[var(--logo-to)] shadow-[0_2px_10px_var(--primary-tint)]"></span>
|
|
1284
|
+
<span class="font-semibold text-foreground">webjs</span>
|
|
1285
|
+
</a>
|
|
1286
|
+
</div>
|
|
1287
|
+
</footer>
|
|
1288
|
+
</div>
|
|
1130
1289
|
\`;
|
|
1131
1290
|
}
|
|
1132
1291
|
`);
|
|
1133
1292
|
|
|
1293
|
+
// The gallery-index home (links every feature demo + the example app) ships
|
|
1294
|
+
// only in the full-stack scaffold, which is the only one that ships the
|
|
1295
|
+
// gallery. saas gets its own landing below.
|
|
1296
|
+
if (isFullStack) {
|
|
1134
1297
|
await writeFile(join(appDir, 'app', 'page.ts'), `// webjs-scaffold-placeholder. This is the example homepage. Replace it with your app's real page, then delete this line. webjs check fails while the marker remains.
|
|
1135
1298
|
import { html } from '@webjsdev/core';
|
|
1136
1299
|
import { rubric, displayH1, accentLink } from '#lib/utils/ui.ts';
|
|
@@ -1141,69 +1304,189 @@ import {
|
|
|
1141
1304
|
cardHeaderClass,
|
|
1142
1305
|
cardTitleClass,
|
|
1143
1306
|
cardDescriptionClass,
|
|
1144
|
-
cardContentClass,
|
|
1145
1307
|
} from '#components/ui/card.ts';
|
|
1146
|
-
import { alertClass, alertTitleClass, alertDescriptionClass } from '#components/ui/alert.ts';
|
|
1147
|
-
import { separatorClass } from '#components/ui/separator.ts';
|
|
1148
1308
|
|
|
1149
1309
|
export const metadata = {
|
|
1150
|
-
title: '${
|
|
1310
|
+
title: '${displayName}: built with webjs',
|
|
1151
1311
|
};
|
|
1152
1312
|
|
|
1313
|
+
// Two kinds of reference the scaffold ships. FEATURES are single-concept demos
|
|
1314
|
+
// (one webjs feature each, under app/features/, logic in modules/). EXAMPLES are
|
|
1315
|
+
// whole apps that compose several features (under app/examples/). Prune what you
|
|
1316
|
+
// do not use (delete the route AND its modules/<name>), then reshape this page.
|
|
1317
|
+
const features = [
|
|
1318
|
+
{ href: '/features/routing', title: 'Routing', blurb: 'A static route plus a dynamic [id] segment that reads params. The file-based router in miniature.' },
|
|
1319
|
+
{ href: '/features/components', title: 'Components', blurb: 'The WebComponent factory, reactive props, instance signals, and slot projection in light DOM.' },
|
|
1320
|
+
{ 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.' },
|
|
1321
|
+
{ href: '/features/optimistic-ui', title: 'Optimistic UI', blurb: 'The imperative optimistic(signal, value, action) flip: instant update, automatic rollback on failure.' },
|
|
1322
|
+
{ 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.' },
|
|
1323
|
+
{ href: '/features/directives', title: 'Directives', blurb: 'The lit-html directive set: repeat for keyed lists, watch(signal) for a fine-grained node swap.' },
|
|
1324
|
+
{ 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.' },
|
|
1325
|
+
{ href: '/features/forms', title: 'Forms', blurb: 'A no-JS progressive-enhancement form posting to the page action, with server-side validation errors.' },
|
|
1326
|
+
{ href: '/features/metadata', title: 'Metadata', blurb: 'Static metadata plus generateMetadata(ctx), which reads the request to compute the title and Open Graph tags.' },
|
|
1327
|
+
{ 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.' },
|
|
1328
|
+
{ href: '/features/env', title: 'Env vars', blurb: 'The server-only vs WEBJS_PUBLIC_ boundary, read during SSR so secrets never reach the browser.' },
|
|
1329
|
+
{ href: '/features/client-router', title: 'Client router', blurb: 'Automatic soft navigation: fragment-only fetches, hover prefetch, scroll restore, and graceful no-JS fallback.' },
|
|
1330
|
+
{ 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).' },
|
|
1331
|
+
{ href: '/features/websockets', title: 'WebSockets', blurb: 'A WS(ws, req) route endpoint plus the connectWS() client, echoing messages over a live socket.' },
|
|
1332
|
+
{ 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.' },
|
|
1333
|
+
{ href: '/features/rate-limit', title: 'Rate limiting', blurb: 'The rateLimit() middleware scoped to one endpoint, returning a 429 with Retry-After past the window.' },
|
|
1334
|
+
{ href: '/features/file-storage', title: 'File storage', blurb: 'A no-JS multipart upload streamed into the FileStore, then served back through a streaming route.' },
|
|
1335
|
+
];
|
|
1336
|
+
const examples = [
|
|
1337
|
+
{ 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.' },
|
|
1338
|
+
];
|
|
1339
|
+
|
|
1340
|
+
const galleryCard = (item: { href: string; title: string; blurb: string }) => html\`
|
|
1341
|
+
<a href=\${item.href} class="block no-underline">
|
|
1342
|
+
<div class="\${cardClass()} h-full transition-colors hover:border-border-strong">
|
|
1343
|
+
<div class=\${cardHeaderClass()}>
|
|
1344
|
+
<h3 class=\${cardTitleClass()}>\${item.title}</h3>
|
|
1345
|
+
<p class=\${cardDescriptionClass()}>\${item.blurb}</p>
|
|
1346
|
+
</div>
|
|
1347
|
+
</div>
|
|
1348
|
+
</a>
|
|
1349
|
+
\`;
|
|
1350
|
+
|
|
1153
1351
|
export default function Home() {
|
|
1154
1352
|
return html\`
|
|
1155
|
-
<section class="mb-
|
|
1353
|
+
<section class="mb-14">
|
|
1156
1354
|
\${rubric('welcome')}
|
|
1157
|
-
\${displayH1(html\`Hello from <span class="text-
|
|
1158
|
-
<p class="text-lede leading-[1.5] text-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1355
|
+
\${displayH1(html\`Hello from <span class="text-primary italic">${displayName}</span>.\`)}
|
|
1356
|
+
<p class="text-lede leading-[1.5] text-muted-foreground max-w-[56ch] m-0 mb-6">
|
|
1357
|
+
This scaffold ships a gallery below: single-feature demos and one whole
|
|
1358
|
+
example app, all small, idiomatic, and heavily commented. Browse them for
|
|
1359
|
+
context, then replace this page with your own. See
|
|
1360
|
+
\${accentLink('https://docs.webjs.dev', 'the docs')} for the full reference.
|
|
1162
1361
|
</p>
|
|
1163
1362
|
<div class="flex gap-3 items-center">
|
|
1164
|
-
<
|
|
1165
|
-
<button class=\${buttonClass({ variant: 'outline' })}>View docs</button>
|
|
1363
|
+
<a href="/examples/todo" class=\${buttonClass()}>Open the todo app</a>
|
|
1166
1364
|
<span class=\${badgeClass({ variant: 'secondary' })}>v0.1</span>
|
|
1167
1365
|
</div>
|
|
1168
1366
|
</section>
|
|
1169
1367
|
|
|
1170
|
-
<
|
|
1171
|
-
<
|
|
1172
|
-
|
|
1173
|
-
<
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1368
|
+
<section class="mb-12">
|
|
1369
|
+
<h2 class="font-serif text-[1.6rem] tracking-[-0.02em] font-bold m-0 mb-1">Features</h2>
|
|
1370
|
+
<p class="text-muted-foreground text-sm m-0 mb-5">
|
|
1371
|
+
One webjs concept each, under <code class="font-mono text-[0.9em]">app/features/</code>
|
|
1372
|
+
with logic in <code class="font-mono text-[0.9em]">modules/</code>. Delete the ones you do not need.
|
|
1373
|
+
</p>
|
|
1374
|
+
<div class="grid gap-4 sm:grid-cols-2">
|
|
1375
|
+
\${features.map(galleryCard)}
|
|
1177
1376
|
</div>
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1377
|
+
</section>
|
|
1378
|
+
|
|
1379
|
+
<section>
|
|
1380
|
+
<h2 class="font-serif text-[1.6rem] tracking-[-0.02em] font-bold m-0 mb-1">Example apps</h2>
|
|
1381
|
+
<p class="text-muted-foreground text-sm m-0 mb-5">
|
|
1382
|
+
Whole apps that compose several features, under <code class="font-mono text-[0.9em]">app/examples/</code>.
|
|
1383
|
+
</p>
|
|
1384
|
+
<div class="grid gap-4 sm:grid-cols-2">
|
|
1385
|
+
\${examples.map(galleryCard)}
|
|
1386
|
+
</div>
|
|
1387
|
+
</section>
|
|
1388
|
+
\`;
|
|
1389
|
+
}
|
|
1390
|
+
`);
|
|
1391
|
+
} else {
|
|
1392
|
+
// saas home: the auth landing (hero + login/signup/dashboard) stays the
|
|
1393
|
+
// headline, and the webjs feature gallery sits BELOW it, so a saas app is
|
|
1394
|
+
// also a learning surface. Keep the `features` list in sync with the
|
|
1395
|
+
// full-stack home above (both ship the same gallery).
|
|
1396
|
+
await writeFile(join(appDir, 'app', 'page.ts'), `// webjs-scaffold-placeholder. This is the example homepage. Replace it with your app's real landing page, then delete this line. webjs check fails while the marker remains.
|
|
1397
|
+
import { html } from '@webjsdev/core';
|
|
1398
|
+
import { rubric, displayH1, accentLink } from '#lib/utils/ui.ts';
|
|
1399
|
+
import {
|
|
1400
|
+
cardClass,
|
|
1401
|
+
cardHeaderClass,
|
|
1402
|
+
cardTitleClass,
|
|
1403
|
+
cardDescriptionClass,
|
|
1404
|
+
} from '#components/ui/card.ts';
|
|
1405
|
+
|
|
1406
|
+
export const metadata = {
|
|
1407
|
+
title: '${displayName}: built with webjs',
|
|
1408
|
+
};
|
|
1409
|
+
|
|
1410
|
+
// The webjs feature gallery, shown below the auth landing. FEATURES are
|
|
1411
|
+
// single-concept demos (under app/features/, logic in modules/); EXAMPLES are
|
|
1412
|
+
// whole apps (under app/examples/). Prune what you do not use (delete the route
|
|
1413
|
+
// AND its modules/<name>). Keep this list in sync with the full-stack home.
|
|
1414
|
+
const features = [
|
|
1415
|
+
{ href: '/features/routing', title: 'Routing', blurb: 'A static route plus a dynamic [id] segment that reads params. The file-based router in miniature.' },
|
|
1416
|
+
{ href: '/features/components', title: 'Components', blurb: 'The WebComponent factory, reactive props, instance signals, and slot projection in light DOM.' },
|
|
1417
|
+
{ 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.' },
|
|
1418
|
+
{ href: '/features/optimistic-ui', title: 'Optimistic UI', blurb: 'The imperative optimistic(signal, value, action) flip: instant update, automatic rollback on failure.' },
|
|
1419
|
+
{ 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.' },
|
|
1420
|
+
{ href: '/features/directives', title: 'Directives', blurb: 'The lit-html directive set: repeat for keyed lists, watch(signal) for a fine-grained node swap.' },
|
|
1421
|
+
{ 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.' },
|
|
1422
|
+
{ href: '/features/forms', title: 'Forms', blurb: 'A no-JS progressive-enhancement form posting to the page action, with server-side validation errors.' },
|
|
1423
|
+
{ href: '/features/metadata', title: 'Metadata', blurb: 'Static metadata plus generateMetadata(ctx), which reads the request to compute the title and Open Graph tags.' },
|
|
1424
|
+
{ 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.' },
|
|
1425
|
+
{ href: '/features/env', title: 'Env vars', blurb: 'The server-only vs WEBJS_PUBLIC_ boundary, read during SSR so secrets never reach the browser.' },
|
|
1426
|
+
{ href: '/features/client-router', title: 'Client router', blurb: 'Automatic soft navigation: fragment-only fetches, hover prefetch, scroll restore, and graceful no-JS fallback.' },
|
|
1427
|
+
{ 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).' },
|
|
1428
|
+
{ href: '/features/websockets', title: 'WebSockets', blurb: 'A WS(ws, req) route endpoint plus the connectWS() client, echoing messages over a live socket.' },
|
|
1429
|
+
{ 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.' },
|
|
1430
|
+
{ href: '/features/rate-limit', title: 'Rate limiting', blurb: 'The rateLimit() middleware scoped to one endpoint, returning a 429 with Retry-After past the window.' },
|
|
1431
|
+
{ href: '/features/file-storage', title: 'File storage', blurb: 'A no-JS multipart upload streamed into the FileStore, then served back through a streaming route.' },
|
|
1432
|
+
];
|
|
1433
|
+
const examples = [
|
|
1434
|
+
{ 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.' },
|
|
1435
|
+
];
|
|
1436
|
+
|
|
1437
|
+
const galleryCard = (item: { href: string; title: string; blurb: string }) => html\`
|
|
1438
|
+
<a href=\${item.href} class="block no-underline">
|
|
1439
|
+
<div class="\${cardClass()} h-full transition-colors hover:border-border-strong">
|
|
1440
|
+
<div class=\${cardHeaderClass()}>
|
|
1441
|
+
<h3 class=\${cardTitleClass()}>\${item.title}</h3>
|
|
1442
|
+
<p class=\${cardDescriptionClass()}>\${item.blurb}</p>
|
|
1188
1443
|
</div>
|
|
1189
1444
|
</div>
|
|
1445
|
+
</a>
|
|
1446
|
+
\`;
|
|
1190
1447
|
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
<section class="
|
|
1194
|
-
|
|
1195
|
-
<
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
assignedNodes / slotchange API.
|
|
1448
|
+
export default function Home() {
|
|
1449
|
+
return html\`
|
|
1450
|
+
<section class="mb-14">
|
|
1451
|
+
\${rubric('welcome')}
|
|
1452
|
+
\${displayH1(html\`Hello from <span class="text-primary italic">${displayName}</span>.\`)}
|
|
1453
|
+
<p class="text-lede leading-[1.5] text-muted-foreground max-w-[56ch] m-0 mb-6">
|
|
1454
|
+
The saas starter: email and password auth, a protected dashboard, and a
|
|
1455
|
+
User model, all wired up. Replace this hero with your product's landing;
|
|
1456
|
+
the webjs feature gallery below is reference, prune what you do not need.
|
|
1457
|
+
See \${accentLink('https://docs.webjs.dev', 'the docs')} for the full reference.
|
|
1202
1458
|
</p>
|
|
1459
|
+
<div class="flex gap-3 items-center">
|
|
1460
|
+
<a href="/login" class="inline-flex items-center px-4 py-2 rounded-xl bg-primary text-primary-foreground font-semibold text-sm no-underline transition-all hover:bg-primary/90 active:scale-[0.97]">Log in</a>
|
|
1461
|
+
<a href="/signup" class="text-primary no-underline font-medium text-sm">Create an account</a>
|
|
1462
|
+
<a href="/dashboard" class="text-muted-foreground no-underline font-medium text-sm hover:text-foreground transition-colors">Dashboard</a>
|
|
1463
|
+
</div>
|
|
1464
|
+
</section>
|
|
1465
|
+
|
|
1466
|
+
<section class="mb-12">
|
|
1467
|
+
<h2 class="font-serif text-[1.6rem] tracking-[-0.02em] font-bold m-0 mb-1">Features</h2>
|
|
1468
|
+
<p class="text-muted-foreground text-sm m-0 mb-5">
|
|
1469
|
+
One webjs concept each, under <code class="font-mono text-[0.9em]">app/features/</code>
|
|
1470
|
+
with logic in <code class="font-mono text-[0.9em]">modules/</code>. Delete the ones you do not need.
|
|
1471
|
+
</p>
|
|
1472
|
+
<div class="grid gap-4 sm:grid-cols-2">
|
|
1473
|
+
\${features.map(galleryCard)}
|
|
1474
|
+
</div>
|
|
1475
|
+
</section>
|
|
1476
|
+
|
|
1477
|
+
<section>
|
|
1478
|
+
<h2 class="font-serif text-[1.6rem] tracking-[-0.02em] font-bold m-0 mb-1">Example apps</h2>
|
|
1479
|
+
<p class="text-muted-foreground text-sm m-0 mb-5">
|
|
1480
|
+
Whole apps that compose several features, under <code class="font-mono text-[0.9em]">app/examples/</code>.
|
|
1481
|
+
</p>
|
|
1482
|
+
<div class="grid gap-4 sm:grid-cols-2">
|
|
1483
|
+
\${examples.map(galleryCard)}
|
|
1484
|
+
</div>
|
|
1203
1485
|
</section>
|
|
1204
1486
|
\`;
|
|
1205
1487
|
}
|
|
1206
1488
|
`);
|
|
1489
|
+
}
|
|
1207
1490
|
|
|
1208
1491
|
// AGENTS.md is copied via the `templateFiles` loop above, from
|
|
1209
1492
|
// `packages/cli/templates/AGENTS.md` with `{{APP_NAME}}` substitution.
|
|
@@ -1246,7 +1529,7 @@ export class ThemeToggle extends WebComponent {
|
|
|
1246
1529
|
const el = document.documentElement;
|
|
1247
1530
|
if (next === 'system') delete el.dataset.theme;
|
|
1248
1531
|
else el.dataset.theme = next;
|
|
1249
|
-
// Keep
|
|
1532
|
+
// Keep the .dark class the @webjsdev/ui kit uses in sync so the ui-* components follow the theme.
|
|
1250
1533
|
const dark = next === 'dark'
|
|
1251
1534
|
|| (next === 'system' && !window.matchMedia('(prefers-color-scheme: light)').matches);
|
|
1252
1535
|
el.classList.toggle('dark', dark);
|
|
@@ -1258,7 +1541,7 @@ export class ThemeToggle extends WebComponent {
|
|
|
1258
1541
|
const icon = t === 'light' ? ICONS.sun : t === 'dark' ? ICONS.moon : ICONS.system;
|
|
1259
1542
|
return html\`
|
|
1260
1543
|
<button
|
|
1261
|
-
class="inline-flex items-center justify-center w-9 h-9 p-0 border border-border rounded-full bg-
|
|
1544
|
+
class="inline-flex items-center justify-center w-9 h-9 p-0 border border-border rounded-full bg-card text-muted-foreground cursor-pointer transition-all duration-150 hover:text-foreground hover:border-border-strong active:scale-[0.94] focus-visible:outline-none focus-visible:border-primary focus-visible:ring-[3px] focus-visible:ring-primary-tint"
|
|
1262
1545
|
@click=\${() => this.cycle()}
|
|
1263
1546
|
aria-label="Cycle theme (currently \${label})"
|
|
1264
1547
|
title="Theme: \${label.toLowerCase()}"
|
|
@@ -1308,7 +1591,7 @@ ThemeToggle.register('theme-toggle');
|
|
|
1308
1591
|
app/layout.ts, page.ts, login/, signup/
|
|
1309
1592
|
app/dashboard/{page,settings,middleware}.ts ← protected
|
|
1310
1593
|
app/api/auth/[...path]/route.ts ← auth API
|
|
1311
|
-
|
|
1594
|
+
styles/globals.css ← @webjsdev/ui theme tokens
|
|
1312
1595
|
components.json ← preconfigured for \`webjs ui add\`
|
|
1313
1596
|
components/ui/{button,card,alert,badge,separator,label,input,
|
|
1314
1597
|
dialog,form,field,switch,checkbox}.ts
|
|
@@ -1321,15 +1604,21 @@ ThemeToggle.register('theme-toggle');
|
|
|
1321
1604
|
`);
|
|
1322
1605
|
} else {
|
|
1323
1606
|
console.log(` ${name}/
|
|
1324
|
-
app/layout.ts, page.ts ←
|
|
1325
|
-
app/
|
|
1607
|
+
app/layout.ts, page.ts ← home links to the gallery
|
|
1608
|
+
app/features/{routing,components,server-actions,optimistic-ui,
|
|
1609
|
+
async-render,directives,route-handler}/
|
|
1610
|
+
← single-feature demos
|
|
1611
|
+
app/examples/todo/ ← one whole example app (composes features)
|
|
1612
|
+
styles/globals.css ← @webjsdev/ui theme tokens
|
|
1326
1613
|
components.json ← preconfigured for \`webjs ui add\`
|
|
1327
1614
|
components/ui/{button,card,alert,badge,separator,label,input}.ts
|
|
1328
1615
|
components/theme-toggle.ts ← light DOM web component
|
|
1329
1616
|
lib/utils/cn.ts ← cn() helper for ui-* components
|
|
1330
1617
|
lib/utils/ui.ts ← Tailwind class-bundle helpers
|
|
1331
1618
|
public/tailwind-browser.js ← Tailwind runtime
|
|
1332
|
-
modules/
|
|
1619
|
+
modules/{components,server-actions,optimistic-ui,async-render,
|
|
1620
|
+
directives,todo}/ ← feature + example logic (prune what you skip)
|
|
1621
|
+
db/{schema,columns,connection}.server.ts ← Drizzle (User + Todo)
|
|
1333
1622
|
CONVENTIONS.md, AGENTS.md, CLAUDE.md
|
|
1334
1623
|
`);
|
|
1335
1624
|
}
|
|
@@ -1346,14 +1635,17 @@ ThemeToggle.register('theme-toggle');
|
|
|
1346
1635
|
✓ Convention validation via \`webjs check\`
|
|
1347
1636
|
|
|
1348
1637
|
For AI agents, read this before editing scaffolded files:
|
|
1349
|
-
• The example layout
|
|
1350
|
-
|
|
1351
|
-
the
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1638
|
+
• The example layout, home page, the gallery under app/features/ and
|
|
1639
|
+
app/examples/ (+ their modules) and schema are REFERENCE ONLY. Replace
|
|
1640
|
+
them with the app the user asked for, and prune the routes you do not
|
|
1641
|
+
use (delete the app/features/<name> or app/examples/<name> route AND
|
|
1642
|
+
its modules/<name>). Don't ship the scaffold's examples as the final
|
|
1643
|
+
product.
|
|
1644
|
+
• This fresh app intentionally FAILS \`webjs check\` with
|
|
1645
|
+
no-scaffold-placeholder violations on the example content (the home
|
|
1646
|
+
page, the layout, and each gallery route). That is the signal to
|
|
1647
|
+
replace or prune each. Delete the marker comment line as you do, and
|
|
1648
|
+
the check goes green.
|
|
1357
1649
|
• Use Drizzle + SQLite for app data. It's already wired up. Define
|
|
1358
1650
|
real models in db/schema.server.ts, then run \`webjs db generate\`
|
|
1359
1651
|
and \`webjs db migrate\`. NEVER store app data in JSON files,
|
|
@@ -1363,7 +1655,7 @@ For AI agents, read this before editing scaffolded files:
|
|
|
1363
1655
|
the closest scaffold and adapt it.
|
|
1364
1656
|
• Read AGENTS.md + CONVENTIONS.md in the new project before writing
|
|
1365
1657
|
any code. They are the contract.
|
|
1366
|
-
• Need more detail? Full hosted docs are at https://docs.webjs.
|
|
1658
|
+
• Need more detail? Full hosted docs are at https://docs.webjs.dev
|
|
1367
1659
|
(every API, directive, recipe, and deployment guide).
|
|
1368
1660
|
`);
|
|
1369
1661
|
|
|
@@ -1391,13 +1683,14 @@ For AI agents, read this before editing scaffolded files:
|
|
|
1391
1683
|
// templates ship with @webjsdev/ui already initialised; the api
|
|
1392
1684
|
// template has no UI but may add one later.
|
|
1393
1685
|
const installSegment = installed ? '' : `${pm} install && `;
|
|
1394
|
-
//
|
|
1395
|
-
//
|
|
1396
|
-
//
|
|
1397
|
-
//
|
|
1398
|
-
//
|
|
1399
|
-
//
|
|
1400
|
-
|
|
1686
|
+
// Some examples query the db on their first request, so a migration must be
|
|
1687
|
+
// authored first: `db:generate` writes it and the `webjs.dev.before` migrate
|
|
1688
|
+
// applies it on `run dev` (Drizzle splits Prisma's `migrate dev` into
|
|
1689
|
+
// generate-then-migrate). The saas example queries users (auth); the
|
|
1690
|
+
// full-stack scaffold ships the gallery's /examples/todo route (queries todos).
|
|
1691
|
+
// The api template has no such first-request query, so it boots with just
|
|
1692
|
+
// `run dev`; once you add a db route, `db:generate` then `run dev` is the loop.
|
|
1693
|
+
const dbSegment = isApi ? '' : `${pm} run db:generate && `;
|
|
1401
1694
|
const runCommand = `cd ${name} && ${installSegment}${dbSegment}${pm} run dev`;
|
|
1402
1695
|
// Postgres needs a reachable DATABASE_URL before any migrate (sqlite uses a
|
|
1403
1696
|
// local file with no .env). Point it at a running database; `dev` / `start`
|