@webjsdev/cli 0.10.39 → 0.10.41
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 +4 -46
- package/lib/app-tasks.js +13 -7
- package/lib/create.js +306 -487
- package/lib/doctor.js +73 -26
- package/package.json +5 -1
- package/templates/.agents/rules/workflow.md +61 -271
- package/templates/.agents/skills/webjs/SKILL.md +226 -0
- package/templates/.agents/skills/webjs/references/auth-and-sessions.md +220 -0
- package/templates/.agents/skills/webjs/references/built-ins.md +200 -0
- package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +204 -0
- package/templates/.agents/skills/webjs/references/components.md +167 -0
- package/templates/.agents/skills/webjs/references/data-and-actions.md +187 -0
- package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +170 -0
- package/templates/.agents/skills/webjs/references/optimistic-ui.md +128 -0
- package/templates/.agents/skills/webjs/references/routing-and-pages.md +158 -0
- package/templates/.agents/skills/webjs/references/runtime.md +80 -0
- package/templates/.agents/skills/webjs/references/service-worker.md +78 -0
- package/templates/.agents/skills/webjs/references/styling.md +123 -0
- package/templates/.agents/skills/webjs/references/testing.md +125 -0
- package/templates/.agents/skills/webjs/references/typescript.md +148 -0
- package/templates/.claude/hooks/check-server-imports.mjs +1 -1
- package/templates/.claude/hooks/require-tests-with-src.sh +1 -1
- package/templates/.claude/settings.json +0 -14
- package/templates/.cursorrules +21 -189
- package/templates/.github/copilot-instructions.md +7 -185
- package/templates/.github/pull_request_template.md +1 -1
- package/templates/AGENTS.md +59 -1484
- package/templates/CLAUDE.md +0 -1
- package/templates/CONVENTIONS.md +32 -1383
- package/templates/GEMINI.md +11 -0
- package/templates/gallery/app/apple-icon.ts +0 -1
- package/templates/gallery/app/examples/todo/page.ts +0 -1
- package/templates/gallery/app/features/async-render/page.ts +0 -1
- package/templates/gallery/app/features/boundaries/page.ts +0 -1
- package/templates/gallery/app/features/broadcast/page.ts +0 -1
- package/templates/gallery/app/features/caching/page.ts +0 -1
- package/templates/gallery/app/features/client-router/page.ts +0 -1
- package/templates/gallery/app/features/client-router/second/page.ts +0 -1
- package/templates/gallery/app/features/components/page.ts +0 -1
- package/templates/gallery/app/features/directives/page.ts +0 -1
- package/templates/gallery/app/features/env/page.ts +0 -1
- package/templates/gallery/app/features/file-storage/page.ts +0 -1
- package/templates/gallery/app/features/forms/page.ts +0 -1
- package/templates/gallery/app/features/metadata/page.ts +0 -1
- package/templates/gallery/app/features/optimistic-ui/page.ts +0 -1
- package/templates/gallery/app/features/rate-limit/page.ts +0 -1
- package/templates/gallery/app/features/route-handler/page.ts +0 -1
- package/templates/gallery/app/features/routing/page.ts +0 -1
- package/templates/gallery/app/features/server-actions/page.ts +0 -1
- package/templates/gallery/app/features/service-worker/page.ts +0 -1
- package/templates/gallery/app/features/sessions/page.ts +0 -1
- package/templates/gallery/app/features/websockets/page.ts +0 -1
- package/templates/gallery/app/global-error.ts +0 -1
- package/templates/gallery/app/global-not-found.ts +0 -1
- package/templates/gallery/app/icon.ts +0 -1
- package/templates/gallery/app/manifest.ts +0 -1
- package/templates/gallery/app/opengraph-image.ts +0 -1
- package/templates/gallery/app/robots.ts +0 -1
- package/templates/gallery/app/sitemap.ts +0 -1
- package/templates/gallery/app/twitter-image.ts +0 -1
- package/templates/gallery/modules/async-render/components/server-clock.ts +7 -6
- package/templates/public/favicon.svg +5 -0
- package/templates/public/sw.js +1 -1
- package/templates/scripts/clear-gallery.mjs +95 -0
- package/lib/clear-placeholders.js +0 -98
- package/lib/design-bar.js +0 -67
- package/templates/.claude/hooks/design-review-before-stop.sh +0 -36
- package/templates/.claude/hooks/route-skills.sh +0 -35
- package/templates/.claude/skills/webjs-design-review/SKILL.md +0 -84
- package/templates/LAYOUT-REFERENCE.md +0 -96
- package/templates/lib/utils/ui.ts +0 -83
package/lib/create.js
CHANGED
|
@@ -348,12 +348,11 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
348
348
|
// (not the browser runtime), so it renders styled with JavaScript off. The
|
|
349
349
|
// compiler is a node-shebang CLI, so a Bun app (whose Dockerfile is a node-less
|
|
350
350
|
// `oven/bun:1` image, #595) must run it under Bun via `bun --bun`; a Node app
|
|
351
|
-
// runs it directly (the before /
|
|
351
|
+
// runs it directly (the before / regenerate steps get node_modules/.bin on PATH
|
|
352
352
|
// via envWithLocalBin). Deliberately NOT `npm run css:build` in the hooks: the
|
|
353
353
|
// Bun image has no npm, so that step would exit 127 and abort the boot.
|
|
354
354
|
const twBin = isBun ? 'bun --bun tailwindcss' : 'tailwindcss';
|
|
355
355
|
const cssBuildCmd = `${twBin} -i ./public/input.css -o ./public/tailwind.css --minify`;
|
|
356
|
-
const cssWatchCmd = `${twBin} -i ./public/input.css -o ./public/tailwind.css --watch`;
|
|
357
356
|
const appDir = join(cwd, name);
|
|
358
357
|
if (existsSync(appDir)) {
|
|
359
358
|
console.error(`Error: directory '${name}' already exists.`);
|
|
@@ -416,6 +415,8 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
416
415
|
// app runs the compiler under Bun (its image has no Node), a Node app runs
|
|
417
416
|
// it directly.
|
|
418
417
|
...(isApi ? {} : { 'css:build': cssBuildCmd }),
|
|
418
|
+
// Shed the demo gallery to a clean, buildable base (scripts/clear-gallery.mjs).
|
|
419
|
+
...(isApi ? {} : { 'gallery:clear': isBun ? 'bun scripts/clear-gallery.mjs' : 'node scripts/clear-gallery.mjs' }),
|
|
419
420
|
dev: isBun ? 'bun --bun webjs dev' : 'webjs dev',
|
|
420
421
|
start: isBun ? 'bun --bun webjs start' : 'webjs start',
|
|
421
422
|
test: 'webjs test',
|
|
@@ -489,15 +490,31 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
489
490
|
// migrate` (idempotent, a no-op when the db is current), so a freshly
|
|
490
491
|
// generated migration is applied without a manual step (#725). For a UI
|
|
491
492
|
// template it ALSO compiles Tailwind in `before` so a freshly cloned app is
|
|
492
|
-
// styled on the very first boot with no manual step
|
|
493
|
-
//
|
|
494
|
-
// is the runtime-aware `cssBuildCmd` (a Bun app runs it under Bun, since its
|
|
493
|
+
// styled on the very first boot with no manual step. The compile command is
|
|
494
|
+
// the runtime-aware `cssBuildCmd` (a Bun app runs it under Bun, since its
|
|
495
495
|
// image has no npm or Node), NOT `npm run css:build`. The api template has no
|
|
496
496
|
// CSS, so it gets neither.
|
|
497
|
+
//
|
|
498
|
+
// In dev the static public/tailwind.css is kept fresh by `dev.regenerate`
|
|
499
|
+
// (#967), NOT a background `tailwindcss --watch`. A watch that dies mid-
|
|
500
|
+
// session or never starts serves stale/missing CSS with no error (a newly
|
|
501
|
+
// added utility class has no backing rule, so the app renders unstyled
|
|
502
|
+
// locally while prod is fine). `regenerate` instead recompiles ON REQUEST
|
|
503
|
+
// when the output is older than a source (or missing): the framework rebuilds
|
|
504
|
+
// it before serving `/public/tailwind.css`, so there is no watch process to
|
|
505
|
+
// die and no staleness window. Same `cssBuildCmd` as prod, so dev and prod
|
|
506
|
+
// resolve classes identically (nothing to diverge). `inputs` mirrors the
|
|
507
|
+
// input.css @source globs (the dirs Tailwind scans for classes).
|
|
497
508
|
webjs: {
|
|
498
509
|
dev: {
|
|
499
510
|
before: isApi ? ['webjs db migrate'] : ['webjs db migrate', cssBuildCmd],
|
|
500
|
-
...(isApi ? {} : {
|
|
511
|
+
...(isApi ? {} : {
|
|
512
|
+
regenerate: [{
|
|
513
|
+
output: 'public/tailwind.css',
|
|
514
|
+
command: cssBuildCmd,
|
|
515
|
+
inputs: ['app', 'components', 'modules', 'lib', 'public/input.css'],
|
|
516
|
+
}],
|
|
517
|
+
}),
|
|
501
518
|
},
|
|
502
519
|
start: { before: isApi ? ['webjs db migrate'] : ['webjs db migrate', cssBuildCmd] },
|
|
503
520
|
},
|
|
@@ -558,36 +575,31 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
558
575
|
// --- Templates (AGENTS.md, CONVENTIONS.md, CLAUDE.md, test files, Claude hooks) ---
|
|
559
576
|
|
|
560
577
|
const templateFiles = [
|
|
578
|
+
// Single cross-agent source: a thin AGENTS.md points at the skill; the
|
|
579
|
+
// .agents/rules workflow rules and the Claude enforcement hooks back it up.
|
|
561
580
|
'AGENTS.md',
|
|
562
581
|
'CONVENTIONS.md',
|
|
582
|
+
'.agents/rules/workflow.md',
|
|
583
|
+
// Per-agent files. Content is single-source (AGENTS.md + the skill); these
|
|
584
|
+
// are thin bridges plus each tool's own config and commit-nudge hook. Claude
|
|
585
|
+
// Code (CLAUDE.md @-imports AGENTS.md), Gemini CLI (GEMINI.md), Copilot in VS
|
|
586
|
+
// Code (copilot-instructions.md). Cursor / opencode / Antigravity read
|
|
587
|
+
// AGENTS.md natively; Cursor also gets a .cursorrules bridge, and each of
|
|
588
|
+
// Cursor / Gemini / opencode ships a "commit often" nudge hook.
|
|
563
589
|
'CLAUDE.md',
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
'
|
|
569
|
-
|
|
570
|
-
'
|
|
571
|
-
'
|
|
572
|
-
|
|
573
|
-
'web-test-runner.config.js',
|
|
574
|
-
// Optional boot-time APM hook (setOnError). Delete if unused.
|
|
575
|
-
'instrumentation.ts',
|
|
576
|
-
// Environment variables
|
|
577
|
-
'.env.example',
|
|
578
|
-
// Project-level gitignore (node_modules, .webjs, .env, OS junk).
|
|
579
|
-
// Shipped as `gitignore` (no dot) and renamed to `.gitignore` on copy:
|
|
580
|
-
// npm STRIPS a `.gitignore` from a published tarball, so a dotfile name
|
|
581
|
-
// would arrive missing and the app would ship without a `.env` ignore
|
|
582
|
-
// (dogfood #845). The SQLite dev.db rule is appended programmatically
|
|
583
|
-
// below so it only appears for the sqlite dialect.
|
|
584
|
-
'gitignore',
|
|
585
|
-
// Git hooks (blocks commits on main)
|
|
586
|
-
'.hooks/pre-commit',
|
|
587
|
-
// Claude Code config + hooks
|
|
590
|
+
'GEMINI.md',
|
|
591
|
+
'.github/copilot-instructions.md',
|
|
592
|
+
'.cursorrules',
|
|
593
|
+
'.cursor/hooks.json',
|
|
594
|
+
'.cursor/hooks/nudge-uncommitted.sh',
|
|
595
|
+
'.gemini/settings.json',
|
|
596
|
+
'.gemini/hooks/nudge-uncommitted.sh',
|
|
597
|
+
'.opencode/plugins/nudge-uncommitted.ts',
|
|
598
|
+
// Claude Code config + the protective enforcement hooks (no design ceremony).
|
|
588
599
|
'.claude.json',
|
|
589
600
|
'.claude/settings.json',
|
|
590
601
|
'.claude/hooks/block-prose-punctuation.sh',
|
|
602
|
+
'.claude/hooks/block-raw-htmlelement.sh',
|
|
591
603
|
'.claude/hooks/guard-branch-context.sh',
|
|
592
604
|
'.claude/hooks/nudge-uncommitted.sh',
|
|
593
605
|
'.claude/hooks/commit-before-stop.sh',
|
|
@@ -595,43 +607,24 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
595
607
|
'.claude/hooks/require-tests-with-src.sh',
|
|
596
608
|
'.claude/hooks/check-server-imports.sh',
|
|
597
609
|
'.claude/hooks/check-server-imports.mjs',
|
|
598
|
-
//
|
|
599
|
-
|
|
600
|
-
//
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
'
|
|
604
|
-
'
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
'.
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
'.cursor/hooks.json',
|
|
611
|
-
'.cursor/hooks/nudge-uncommitted.sh',
|
|
612
|
-
// OpenCode plugins (loaded as TS by Bun at runtime)
|
|
613
|
-
'.opencode/plugins/nudge-uncommitted.ts',
|
|
614
|
-
// Antigravity workspace rules (Google's documented convention is
|
|
615
|
-
// `.agents/rules/*.md`, lowercase, per the Codelab
|
|
616
|
-
// "Build Autonomous Developer Pipelines using agents.md and skills.md
|
|
617
|
-
// in Antigravity"). Replaced the legacy `.windsurfrules` ship when
|
|
618
|
-
// Windsurf was acquired by Google.
|
|
619
|
-
'.agents/rules/workflow.md',
|
|
620
|
-
// Cross-agent config files
|
|
621
|
-
'.cursorrules',
|
|
622
|
-
'.github/copilot-instructions.md',
|
|
610
|
+
// Git pre-commit hook (blocks commits directly to main).
|
|
611
|
+
'.hooks/pre-commit',
|
|
612
|
+
// Starter tests under the feature-folder layout.
|
|
613
|
+
'test/hello/hello.test.ts',
|
|
614
|
+
'test/hello/browser/hello.test.js',
|
|
615
|
+
'test/hello/e2e/hello.test.ts',
|
|
616
|
+
'web-test-runner.config.js',
|
|
617
|
+
// Optional boot-time APM hook (setOnError). Delete if unused.
|
|
618
|
+
'instrumentation.ts',
|
|
619
|
+
'.env.example',
|
|
620
|
+
// Shipped without a dot (npm strips a published .gitignore) and renamed on copy.
|
|
621
|
+
'gitignore',
|
|
623
622
|
'.github/pull_request_template.md',
|
|
624
|
-
// CI
|
|
625
|
-
// webjs check + the unit / browser / e2e layers on every PR and push
|
|
626
|
-
// to main, mirroring the WebJs framework's own CI.
|
|
623
|
+
// CI runs webjs check + the test layers on every PR and push to main.
|
|
627
624
|
'.github/workflows/ci.yml',
|
|
628
625
|
'.editorconfig',
|
|
629
|
-
// VS Code: associate the published webjs-config JSON Schema with the
|
|
630
|
-
// package.json `webjs` block, so an unknown / typo'd key (#259) is
|
|
631
|
-
// flagged natively in the editor instead of silently dropped.
|
|
632
626
|
'.vscode/settings.json',
|
|
633
|
-
// Production / deploy scaffolding.
|
|
634
|
-
// the app locally with the same Dockerfile production builds from.
|
|
627
|
+
// Production / deploy scaffolding.
|
|
635
628
|
'Dockerfile',
|
|
636
629
|
'compose.yaml',
|
|
637
630
|
'.dockerignore',
|
|
@@ -643,12 +636,8 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
643
636
|
// rewrites; the three infra files get their file-specific transform. On Node,
|
|
644
637
|
// every file is copied byte-identical (the map is empty).
|
|
645
638
|
const PROSE_REWRITE = new Set([
|
|
646
|
-
'AGENTS.md', 'CONVENTIONS.md', '.cursorrules',
|
|
647
|
-
'.agents/rules/workflow.md',
|
|
648
|
-
// The starter tests carry header comments with run commands (`npx wtr`,
|
|
649
|
-
// `npm i -D puppeteer-core`); bun-ify those too so a bun app's test files
|
|
650
|
-
// do not tell the user to run npm/npx (#541 review). The transform only
|
|
651
|
-
// touches npm/npx command tokens, so the test code itself is unaffected.
|
|
639
|
+
'AGENTS.md', 'CLAUDE.md', 'CONVENTIONS.md', '.cursorrules',
|
|
640
|
+
'.agents/rules/workflow.md',
|
|
652
641
|
'test/hello/browser/hello.test.js', 'test/hello/e2e/hello.test.ts',
|
|
653
642
|
]);
|
|
654
643
|
// compose.yaml builds from the (pure oven/bun) Dockerfile and inherits its
|
|
@@ -676,21 +665,25 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
676
665
|
}
|
|
677
666
|
}
|
|
678
667
|
|
|
679
|
-
//
|
|
668
|
+
// The agent skill is the one cross-agent source (AGENTS.md points to it). It
|
|
669
|
+
// lives ONCE, canonically, at the repo-root `.agents/skills/webjs/`. A
|
|
670
|
+
// published CLI bundles it under `templates/` at prepack (see
|
|
671
|
+
// scripts/sync-scaffold-skill.mjs, wired into this package's prepack), so copy
|
|
672
|
+
// that bundle when present; in the monorepo the bundle is gitignored, so fall
|
|
673
|
+
// back to the repo-root canonical.
|
|
674
|
+
const bundledSkill = join(TEMPLATES, '.agents', 'skills', 'webjs');
|
|
675
|
+
const repoRootSkill = resolve(__dirname, '..', '..', '..', '.agents', 'skills', 'webjs');
|
|
676
|
+
const skillSrc = existsSync(bundledSkill) ? bundledSkill : repoRootSkill;
|
|
677
|
+
if (existsSync(skillSrc)) {
|
|
678
|
+
await cp(skillSrc, join(appDir, '.agents', 'skills', 'webjs'), { recursive: true });
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Make the Claude enforcement hooks + the git pre-commit executable.
|
|
680
682
|
const { chmod } = await import('node:fs/promises');
|
|
681
|
-
for (const hook of ['block-prose-punctuation.sh', 'guard-branch-context.sh', 'nudge-uncommitted.sh', 'commit-before-stop.sh', 'cleanup-merged-worktree.sh', 'require-tests-with-src.sh', '
|
|
683
|
+
for (const hook of ['block-prose-punctuation.sh', 'block-raw-htmlelement.sh', 'guard-branch-context.sh', 'nudge-uncommitted.sh', 'commit-before-stop.sh', 'cleanup-merged-worktree.sh', 'require-tests-with-src.sh', 'check-server-imports.sh']) {
|
|
682
684
|
const hookPath = join(appDir, '.claude', 'hooks', hook);
|
|
683
685
|
if (existsSync(hookPath)) await chmod(hookPath, 0o755);
|
|
684
686
|
}
|
|
685
|
-
for (const hook of ['nudge-uncommitted.sh']) {
|
|
686
|
-
const hookPath = join(appDir, '.gemini', 'hooks', hook);
|
|
687
|
-
if (existsSync(hookPath)) await chmod(hookPath, 0o755);
|
|
688
|
-
}
|
|
689
|
-
for (const hook of ['nudge-uncommitted.sh']) {
|
|
690
|
-
const hookPath = join(appDir, '.cursor', 'hooks', hook);
|
|
691
|
-
if (existsSync(hookPath)) await chmod(hookPath, 0o755);
|
|
692
|
-
}
|
|
693
|
-
// Make git pre-commit hook executable
|
|
694
687
|
const preCommitPath = join(appDir, '.hooks', 'pre-commit');
|
|
695
688
|
if (existsSync(preCommitPath)) await chmod(preCommitPath, 0o755);
|
|
696
689
|
|
|
@@ -1037,9 +1030,9 @@ export type ActionResult<T> =
|
|
|
1037
1030
|
`);
|
|
1038
1031
|
|
|
1039
1032
|
// The api backend-features showcase: endpoints under app/api/features/**
|
|
1040
|
-
//
|
|
1041
|
-
//
|
|
1042
|
-
//
|
|
1033
|
+
// (the route() adapter + validation, rate limiting, streaming, file storage,
|
|
1034
|
+
// WebSockets + broadcast) that the root api index above links. The api
|
|
1035
|
+
// counterpart of the UI gallery. Prune what you skip.
|
|
1043
1036
|
const { writeApiGallery } = await import('./api-gallery.js');
|
|
1044
1037
|
await writeApiGallery(appDir);
|
|
1045
1038
|
}
|
|
@@ -1057,18 +1050,24 @@ export type ActionResult<T> =
|
|
|
1057
1050
|
// Progressive-enhancement service worker (#271): ship the opt-in offline
|
|
1058
1051
|
// primitive (the worker + its offline fallback) into the UI scaffolds
|
|
1059
1052
|
// (full-stack / saas; this block is api-excluded since api has no UI).
|
|
1060
|
-
// Dormant until the app registers it (see
|
|
1053
|
+
// Dormant until the app registers it (see the skill's references/service-worker.md);
|
|
1061
1054
|
// it never changes the JS-disabled baseline.
|
|
1062
1055
|
for (const swFile of ['sw.js', 'offline.html']) {
|
|
1063
1056
|
const swSrc = join(TEMPLATES, 'public', swFile);
|
|
1064
1057
|
if (existsSync(swSrc)) await cp(swSrc, join(publicDir, swFile));
|
|
1065
1058
|
}
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1059
|
+
// A base SVG favicon (the root layout links it). It ships with the app, not
|
|
1060
|
+
// the gallery, so it survives `npm run gallery:clear`.
|
|
1061
|
+
const faviconSrc = join(TEMPLATES, 'public', 'favicon.svg');
|
|
1062
|
+
if (existsSync(faviconSrc)) await cp(faviconSrc, join(publicDir, 'favicon.svg'));
|
|
1063
|
+
|
|
1064
|
+
// The gallery-reset script (wired as `gallery:clear`). Only UI templates have
|
|
1065
|
+
// a gallery, so it ships here (NOT in the flat templateFiles list, which would
|
|
1066
|
+
// copy it into the api app where it has no gallery and would clobber app/).
|
|
1067
|
+
const clearScriptSrc = join(TEMPLATES, 'scripts', 'clear-gallery.mjs');
|
|
1068
|
+
if (existsSync(clearScriptSrc)) {
|
|
1069
|
+
await mkdir(join(appDir, 'scripts'), { recursive: true });
|
|
1070
|
+
await cp(clearScriptSrc, join(appDir, 'scripts', 'clear-gallery.mjs'));
|
|
1072
1071
|
}
|
|
1073
1072
|
|
|
1074
1073
|
// Fail loudly if the @webjsdev/ui registry sources aren't on disk.
|
|
@@ -1083,20 +1082,13 @@ export type ActionResult<T> =
|
|
|
1083
1082
|
// styles/globals.css (the @webjsdev/ui theme).
|
|
1084
1083
|
await writeUiBootstrap(appDir);
|
|
1085
1084
|
|
|
1086
|
-
//
|
|
1087
|
-
//
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
// The gallery: idiomatic, densely-commented single-feature demos under
|
|
1094
|
-
// app/features/ plus one whole example app under app/examples/, with logic
|
|
1095
|
-
// in modules/, linked from the home page. Shipped in the default full-stack
|
|
1096
|
-
// scaffold so an agent gains context by browsing real code; prune
|
|
1097
|
-
// per-feature (delete the route + its module) for what the app does not
|
|
1098
|
-
// use. See CONVENTIONS.md "prune what the app does not use".
|
|
1099
|
-
if (!isApi) await copyGallery(appDir);
|
|
1085
|
+
// The saas auth pages import a few ui-* primitives. A full-stack app adds
|
|
1086
|
+
// any component on demand with `webjs ui add <name>`.
|
|
1087
|
+
if (isSaas) {
|
|
1088
|
+
await copyUiComponents(appDir, [
|
|
1089
|
+
'button', 'card', 'alert', 'badge', 'separator', 'label', 'input',
|
|
1090
|
+
]);
|
|
1091
|
+
}
|
|
1100
1092
|
|
|
1101
1093
|
// The @webjsdev/ui theme (`--color-primary`, `--color-card`, the @theme maps,
|
|
1102
1094
|
// @custom-variant, @keyframes) plus the app @theme mappings are compiled from
|
|
@@ -1137,51 +1129,35 @@ ${uiThemeRaw}
|
|
|
1137
1129
|
}
|
|
1138
1130
|
`);
|
|
1139
1131
|
|
|
1132
|
+
// The gallery: idiomatic, densely-commented single-feature demos under
|
|
1133
|
+
// app/features/ plus one whole example app under app/examples/, with logic
|
|
1134
|
+
// in modules/, all linked from the home page below. Shipped in every UI
|
|
1135
|
+
// scaffold (full-stack AND saas) so an agent gains context by browsing real
|
|
1136
|
+
// working code; prune per-feature (delete the route + its module) for what
|
|
1137
|
+
// the app does not use.
|
|
1138
|
+
await copyGallery(appDir);
|
|
1139
|
+
|
|
1140
1140
|
await writeFile(join(appDir, 'app', 'layout.ts'), `import { html, cspNonce } from '@webjsdev/core';
|
|
1141
1141
|
import '#components/theme-toggle.ts';
|
|
1142
|
-
// Webjs UI components are tiered:
|
|
1143
|
-
// - Tier 1 (button, card, input, label, alert, badge, separator, etc.) are
|
|
1144
|
-
// class-helper FUNCTIONS, with no custom element to register. Each page
|
|
1145
|
-
// imports the specific helpers it needs (e.g.
|
|
1146
|
-
// \`import { buttonClass } from '#components/ui/button.ts'\`).
|
|
1147
|
-
// - Tier 2 (dialog, popover, tooltip, tabs, accordion, etc.) ARE custom
|
|
1148
|
-
// elements. Register them by side-effect-importing here once so they
|
|
1149
|
-
// work transitively across every page:
|
|
1150
|
-
// import '#components/ui/dialog.ts';
|
|
1151
|
-
// The example app/page.ts below uses only Tier-1 helpers, so nothing
|
|
1152
|
-
// extra needs to be registered. Add Tier-2 imports as you 'webjs ui add'.
|
|
1153
1142
|
|
|
1154
1143
|
/**
|
|
1155
|
-
* Root layout:
|
|
1156
|
-
*
|
|
1157
|
-
*
|
|
1158
|
-
*
|
|
1159
|
-
*
|
|
1160
|
-
*
|
|
1161
|
-
*
|
|
1162
|
-
* then renders \${children} in a bare container with no chrome, so you design the
|
|
1163
|
-
* app's own layout. LAYOUT-REFERENCE.md (project root) is a complete worked
|
|
1164
|
-
* layout (header, nav, theme toggle, reading column, footer) to learn from.
|
|
1144
|
+
* Root layout: the ONLY file that writes the document shell. It wires a neutral
|
|
1145
|
+
* design-token palette, the light/dark theme, and the Tailwind stylesheet, then
|
|
1146
|
+
* renders \${children} in a bare container. Grow it in place: add a header, nav,
|
|
1147
|
+
* footer, or reading column here as your app needs them. Design tokens live as
|
|
1148
|
+
* plain CSS custom properties (they resolve with JavaScript disabled) and are
|
|
1149
|
+
* mapped into Tailwind via @theme in public/input.css, so bg-background,
|
|
1150
|
+
* text-foreground, bg-card, bg-primary, and border-border all work.
|
|
1165
1151
|
*/
|
|
1166
|
-
|
|
1167
1152
|
export default function RootLayout({ children }: { children: unknown }) {
|
|
1168
|
-
// Read the in-flight request's CSP nonce so the theme-detection
|
|
1169
|
-
//
|
|
1170
|
-
// Returns '' when no CSP nonce is set, in which case the attribute
|
|
1171
|
-
// is empty and the browser ignores it.
|
|
1153
|
+
// Read the in-flight request's CSP nonce so the theme-detection inline script
|
|
1154
|
+
// passes strict CSP. Returns '' when no CSP nonce is set.
|
|
1172
1155
|
const nonce = cspNonce();
|
|
1173
1156
|
return html\`
|
|
1174
1157
|
<script nonce="\${nonce}">
|
|
1175
|
-
//
|
|
1176
|
-
//
|
|
1177
|
-
//
|
|
1178
|
-
// the root, dark, and data-theme style blocks below switch. It is what
|
|
1179
|
-
// makes the app theme-aware. Building a SINGLE-theme app of your own?
|
|
1180
|
-
// Delete this IIFE, delete the dark and light style blocks below, and set
|
|
1181
|
-
// your palette once on the root selector. That removes the wiring so it
|
|
1182
|
-
// cannot fight your own colours (it will not override a plain root
|
|
1183
|
-
// palette). The header-measure IIFE that follows is unrelated, keep it
|
|
1184
|
-
// (it is dormant until you add a fixed header, per LAYOUT-REFERENCE.md).
|
|
1158
|
+
// Light/dark theme: read the saved or OS choice and set data-theme plus the
|
|
1159
|
+
// .dark class the tokens key off. Delete this block (and the light blocks
|
|
1160
|
+
// below) for a single-theme app.
|
|
1185
1161
|
(function(){
|
|
1186
1162
|
try {
|
|
1187
1163
|
var mq = window.matchMedia('(prefers-color-scheme: light)');
|
|
@@ -1191,9 +1167,6 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1191
1167
|
var el = document.documentElement;
|
|
1192
1168
|
if (t === 'light' || t === 'dark') el.dataset.theme = t;
|
|
1193
1169
|
else delete el.dataset.theme;
|
|
1194
|
-
// Keep the .dark class the @webjsdev/ui kit uses in sync with the effective theme so the
|
|
1195
|
-
// copied ui-* components (button, card, etc.) follow light/dark too.
|
|
1196
|
-
// Dark is the default unless the OS prefers light or 'light' is set.
|
|
1197
1170
|
var dark = t === 'dark' || (t !== 'light' && !mq.matches);
|
|
1198
1171
|
el.classList.toggle('dark', dark);
|
|
1199
1172
|
}
|
|
@@ -1201,14 +1174,10 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1201
1174
|
mq.addEventListener('change', apply);
|
|
1202
1175
|
} catch (_) {}
|
|
1203
1176
|
})();
|
|
1204
|
-
//
|
|
1205
|
-
//
|
|
1206
|
-
//
|
|
1207
|
-
//
|
|
1208
|
-
// a client-router nav). fixed leaves normal flow, so --header-h reserves its
|
|
1209
|
-
// height for the content below; this measures the real (responsive) height.
|
|
1210
|
-
// With no header it is a no-op (querySelector returns null) and --header-h
|
|
1211
|
-
// stays 0, so there is no phantom gap.
|
|
1177
|
+
// Header-measure: dormant until you add a fixed header. A fixed header
|
|
1178
|
+
// (use position:fixed, NOT sticky, which flickers on iOS WebKit during a
|
|
1179
|
+
// client-router nav) leaves normal flow, so --header-h reserves its height
|
|
1180
|
+
// for the content below. No header means a no-op and --header-h stays 0.
|
|
1212
1181
|
(function(){
|
|
1213
1182
|
function measure(){
|
|
1214
1183
|
try {
|
|
@@ -1225,122 +1194,103 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1225
1194
|
else measure();
|
|
1226
1195
|
})();
|
|
1227
1196
|
</script>
|
|
1197
|
+
<meta name="color-scheme" content="light dark">
|
|
1198
|
+
<link rel="icon" href="/public/favicon.svg" type="image/svg+xml">
|
|
1199
|
+
<!-- JetBrains Mono for body/UI (its monospaced, developer-console feel) and
|
|
1200
|
+
Bricolage Grotesque for the display wordmark. Swap these for your own
|
|
1201
|
+
fonts (and update --font-sans / --font-display below). -->
|
|
1202
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1203
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous">
|
|
1204
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400..800&family=JetBrains+Mono:wght@400;500;700&display=swap">
|
|
1228
1205
|
<!-- Tailwind: a STATIC stylesheet compiled from public/input.css to
|
|
1229
|
-
public/tailwind.css by css:build
|
|
1230
|
-
|
|
1206
|
+
public/tailwind.css by css:build (run automatically by the dev and start
|
|
1207
|
+
tasks; in dev it is also recompiled on request when a source changes, so
|
|
1208
|
+
it never goes stale). A real stylesheet, so the app is fully styled with
|
|
1231
1209
|
JavaScript DISABLED (no in-browser compile). -->
|
|
1210
|
+
|
|
1232
1211
|
<link rel="stylesheet" href="/public/tailwind.css">
|
|
1233
1212
|
<style>
|
|
1234
|
-
/*
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
text-muted-foreground, border-border, ring-ring, and the rest. Here we
|
|
1240
|
-
set those tokens' VALUES to this app's brand palette, so the ui-*
|
|
1241
|
-
components AND your own chrome read ONE source of truth. Any component
|
|
1242
|
-
added later with webjs ui add <name> inherits it automatically. The
|
|
1243
|
-
stylesheet is linked before this block, so these VALUES win over the ui
|
|
1244
|
-
theme defaults. Dark-first, with light via the theme toggle (data-theme) or the
|
|
1245
|
-
OS. Follows the shadcn model: --primary is the BRAND color (orange, used
|
|
1246
|
-
for primary buttons, links, and emphasis), while --accent stays a NEUTRAL
|
|
1247
|
-
hover tint so the ui kit's outline/ghost/dropdown hover states keep proper
|
|
1248
|
-
contrast. Use bg-primary / text-primary for brand, bg-accent for hovers.
|
|
1249
|
-
Add a new design token the canonical way, a --x VALUE below plus a
|
|
1250
|
-
--color-x: var(--x) line in the @theme inline block in public/input.css,
|
|
1251
|
-
then use it as bg-x / text-x. Reach for opacity modifiers
|
|
1252
|
-
(bg-primary/10, hover:bg-primary/90, border-border/60) before inventing a
|
|
1253
|
-
new token, but keep body text at full opacity so it stays above the AA
|
|
1254
|
-
contrast floor (a faded text-muted-foreground/70 measured 3.83:1). */
|
|
1213
|
+
/* Design tokens. The token NAMES are infrastructure (public/input.css maps
|
|
1214
|
+
them into Tailwind via @theme). The VALUES are a cool neutral-grey palette
|
|
1215
|
+
with a monospaced type system: change them here to give the app its own
|
|
1216
|
+
look. bg-background / text-foreground / bg-card / bg-primary / border-border
|
|
1217
|
+
all resolve from these. */
|
|
1255
1218
|
:root {
|
|
1256
|
-
--font-sans: -
|
|
1219
|
+
--font-sans: 'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
|
1257
1220
|
--font-serif: ui-serif, 'Iowan Old Style', Palatino, Georgia, serif;
|
|
1258
|
-
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1259
|
-
|
|
1260
|
-
header-measure script below overrides it to the real height the moment
|
|
1261
|
-
you add a fixed header element (see LAYOUT-REFERENCE.md), so body
|
|
1262
|
-
padding tracks it automatically. */
|
|
1221
|
+
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
|
1222
|
+
--font-display: 'Bricolage Grotesque', 'JetBrains Mono', ui-sans-serif, system-ui, sans-serif;
|
|
1263
1223
|
--header-h: 0px;
|
|
1264
|
-
/* A translucent
|
|
1265
|
-
light/dark
|
|
1266
|
-
--primary-tint: color-mix(in
|
|
1224
|
+
/* A translucent tint of the primary, tracked automatically across
|
|
1225
|
+
light/dark. Used for focus rings (ring-primary-tint). */
|
|
1226
|
+
--primary-tint: color-mix(in srgb, var(--primary) 22%, transparent);
|
|
1267
1227
|
}
|
|
1268
|
-
/* webjs-scaffold-placeholder. These are the scaffold's STARTER brand colors (the shadcn-style orange); they look finished on purpose. Own the palette: set the token VALUES below (dark AND light blocks) to colors that fit what THIS app IS, keeping the token NAMES. A recolor chosen for the app beats keeping the starter orange. Then delete this marker line (or run webjs check --clear-placeholders to keep the starter palette deliberately). webjs check fails while the marker remains. */
|
|
1269
1228
|
/* dark (the default, and the explicit .dark the toggle sets) */
|
|
1270
1229
|
:root, .dark {
|
|
1271
1230
|
color-scheme: dark;
|
|
1272
|
-
--background:
|
|
1273
|
-
--foreground:
|
|
1274
|
-
--card:
|
|
1275
|
-
--card-foreground:
|
|
1276
|
-
--popover:
|
|
1277
|
-
--popover-foreground:
|
|
1278
|
-
|
|
1279
|
-
--primary:
|
|
1280
|
-
--
|
|
1281
|
-
--secondary:
|
|
1282
|
-
--
|
|
1283
|
-
--muted:
|
|
1284
|
-
--
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
--
|
|
1288
|
-
--
|
|
1289
|
-
--
|
|
1290
|
-
--border-strong: oklch(0.38 0.012 55 / 0.9);
|
|
1291
|
-
--input: oklch(0.26 0.012 55 / 0.9);
|
|
1292
|
-
--ring: oklch(0.7 0.16 52);
|
|
1293
|
-
--logo-from: oklch(0.8 0.16 58);
|
|
1294
|
-
--logo-to: oklch(0.62 0.18 44);
|
|
1231
|
+
--background: #1e2226;
|
|
1232
|
+
--foreground: #dee2e6;
|
|
1233
|
+
--card: #313539;
|
|
1234
|
+
--card-foreground: #dee2e6;
|
|
1235
|
+
--popover: #313539;
|
|
1236
|
+
--popover-foreground: #dee2e6;
|
|
1237
|
+
--primary: #dee2e6;
|
|
1238
|
+
--primary-foreground: #1e2226;
|
|
1239
|
+
--secondary: #363a3e;
|
|
1240
|
+
--secondary-foreground: #dee2e6;
|
|
1241
|
+
--muted: #313539;
|
|
1242
|
+
--muted-foreground: #94989c;
|
|
1243
|
+
--accent: #363a3e;
|
|
1244
|
+
--accent-foreground: #f7fbff;
|
|
1245
|
+
--border: #34393e;
|
|
1246
|
+
--border-strong: #454b51;
|
|
1247
|
+
--input: #34393e;
|
|
1248
|
+
--ring: #6b7075;
|
|
1295
1249
|
}
|
|
1296
1250
|
/* light (explicit via the toggle) */
|
|
1297
1251
|
:root[data-theme='light'] {
|
|
1298
1252
|
color-scheme: light;
|
|
1299
|
-
--background:
|
|
1300
|
-
--foreground:
|
|
1301
|
-
--card:
|
|
1302
|
-
--card-foreground:
|
|
1303
|
-
--popover:
|
|
1304
|
-
--popover-foreground:
|
|
1305
|
-
--primary:
|
|
1306
|
-
--primary-foreground:
|
|
1307
|
-
--secondary:
|
|
1308
|
-
--secondary-foreground:
|
|
1309
|
-
--muted:
|
|
1310
|
-
--muted-foreground:
|
|
1311
|
-
--accent:
|
|
1312
|
-
--accent-foreground:
|
|
1313
|
-
--border:
|
|
1314
|
-
--border-strong:
|
|
1315
|
-
--input:
|
|
1316
|
-
--ring:
|
|
1317
|
-
--logo-from: oklch(0.63 0.17 50);
|
|
1318
|
-
--logo-to: oklch(0.44 0.11 52);
|
|
1253
|
+
--background: #dee2e6;
|
|
1254
|
+
--foreground: #313539;
|
|
1255
|
+
--card: #f0f4f7;
|
|
1256
|
+
--card-foreground: #313539;
|
|
1257
|
+
--popover: #f0f4f7;
|
|
1258
|
+
--popover-foreground: #313539;
|
|
1259
|
+
--primary: #313539;
|
|
1260
|
+
--primary-foreground: #f7fbff;
|
|
1261
|
+
--secondary: #f7fbff;
|
|
1262
|
+
--secondary-foreground: #313539;
|
|
1263
|
+
--muted: #eaeef1;
|
|
1264
|
+
--muted-foreground: #767b80;
|
|
1265
|
+
--accent: #f7fbff;
|
|
1266
|
+
--accent-foreground: #313539;
|
|
1267
|
+
--border: #c9d0d6;
|
|
1268
|
+
--border-strong: #b3bbc2;
|
|
1269
|
+
--input: #c9d0d6;
|
|
1270
|
+
--ring: #9aa0a5;
|
|
1319
1271
|
}
|
|
1320
1272
|
/* light (OS preference, when the user has made no explicit choice) */
|
|
1321
1273
|
@media (prefers-color-scheme: light) {
|
|
1322
1274
|
:root:not(.dark):not([data-theme='dark']) {
|
|
1323
1275
|
color-scheme: light;
|
|
1324
|
-
--background:
|
|
1325
|
-
--foreground:
|
|
1326
|
-
--card:
|
|
1327
|
-
--card-foreground:
|
|
1328
|
-
--popover:
|
|
1329
|
-
--popover-foreground:
|
|
1330
|
-
--primary:
|
|
1331
|
-
--primary-foreground:
|
|
1332
|
-
--secondary:
|
|
1333
|
-
--secondary-foreground:
|
|
1334
|
-
--muted:
|
|
1335
|
-
--muted-foreground:
|
|
1336
|
-
--accent:
|
|
1337
|
-
--accent-foreground:
|
|
1338
|
-
--border:
|
|
1339
|
-
--border-strong:
|
|
1340
|
-
--input:
|
|
1341
|
-
--ring:
|
|
1342
|
-
--logo-from: oklch(0.63 0.17 50);
|
|
1343
|
-
--logo-to: oklch(0.44 0.11 52);
|
|
1276
|
+
--background: #dee2e6;
|
|
1277
|
+
--foreground: #313539;
|
|
1278
|
+
--card: #f0f4f7;
|
|
1279
|
+
--card-foreground: #313539;
|
|
1280
|
+
--popover: #f0f4f7;
|
|
1281
|
+
--popover-foreground: #313539;
|
|
1282
|
+
--primary: #313539;
|
|
1283
|
+
--primary-foreground: #f7fbff;
|
|
1284
|
+
--secondary: #f7fbff;
|
|
1285
|
+
--secondary-foreground: #313539;
|
|
1286
|
+
--muted: #eaeef1;
|
|
1287
|
+
--muted-foreground: #767b80;
|
|
1288
|
+
--accent: #f7fbff;
|
|
1289
|
+
--accent-foreground: #313539;
|
|
1290
|
+
--border: #c9d0d6;
|
|
1291
|
+
--border-strong: #b3bbc2;
|
|
1292
|
+
--input: #c9d0d6;
|
|
1293
|
+
--ring: #9aa0a5;
|
|
1344
1294
|
}
|
|
1345
1295
|
}
|
|
1346
1296
|
</style>
|
|
@@ -1351,13 +1301,11 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1351
1301
|
padding-top: var(--header-h);
|
|
1352
1302
|
background: var(--background);
|
|
1353
1303
|
color: var(--foreground);
|
|
1354
|
-
font:
|
|
1304
|
+
font: 15px/1.6 var(--font-sans);
|
|
1355
1305
|
-webkit-font-smoothing: antialiased;
|
|
1306
|
+
-moz-osx-font-smoothing: grayscale;
|
|
1356
1307
|
}
|
|
1357
|
-
::selection { background: color-mix(in oklch, var(--primary) 22%, transparent); color: var(--foreground); }
|
|
1358
1308
|
</style>
|
|
1359
|
-
|
|
1360
|
-
<!-- webjs-scaffold-placeholder. MINIMAL SHELL, on purpose. Everything above (the theme apparatus, the design tokens, the linked Tailwind stylesheet) is infrastructure to keep. Below, \${children} drops into a bare full-height container with NO chrome: design THIS app's layout from scratch. Decide from what the app IS whether it needs a header, a nav, a footer, a sidebar, a centered reading column, or a full-bleed canvas, and build that here. A COMPLETE reference layout (fixed header, brand, nav, theme toggle, reading column, footer) ships at LAYOUT-REFERENCE.md in the project root: read it to learn the patterns, then write your own. Delete this line once your layout is designed. webjs check fails while the marker remains. -->
|
|
1361
1309
|
<main class="min-h-dvh px-4 sm:px-6 py-8">
|
|
1362
1310
|
\${children}
|
|
1363
1311
|
</main>
|
|
@@ -1365,31 +1313,26 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1365
1313
|
}
|
|
1366
1314
|
`);
|
|
1367
1315
|
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
1370
|
-
//
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
import {
|
|
1378
|
-
cardClass,
|
|
1379
|
-
cardHeaderClass,
|
|
1380
|
-
cardTitleClass,
|
|
1381
|
-
cardDescriptionClass,
|
|
1382
|
-
} from '#components/ui/card.ts';
|
|
1316
|
+
// Home page: a gallery index. A masthead, then a grid that links every feature
|
|
1317
|
+
// demo and the example app, and a footer with the docs + source links. Treat it
|
|
1318
|
+
// as a starting point: prune the demos you do not use (delete the
|
|
1319
|
+
// app/features/<x> route AND its modules/<x>), then reshape this page into the
|
|
1320
|
+
// app's real landing page. For the saas template a login/signup CTA row is
|
|
1321
|
+
// spliced under the tagline.
|
|
1322
|
+
const homeAuthLinks = isSaas
|
|
1323
|
+
? '\n <div class="flex flex-wrap gap-3 items-center justify-center mt-2"><a href="/login" class="inline-flex items-center px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium no-underline hover:opacity-90">Log in</a><a href="/signup" class="inline-flex items-center px-4 py-2 rounded-lg border border-border text-foreground text-sm font-medium no-underline hover:bg-accent">Create an account</a></div>'
|
|
1324
|
+
: '';
|
|
1325
|
+
await writeFile(join(appDir, 'app', 'page.ts'), `import { html } from '@webjsdev/core';
|
|
1383
1326
|
|
|
1384
1327
|
export const metadata = {
|
|
1385
|
-
title: '${displayName}
|
|
1328
|
+
title: '${displayName}',
|
|
1386
1329
|
};
|
|
1387
1330
|
|
|
1388
|
-
//
|
|
1389
|
-
//
|
|
1390
|
-
//
|
|
1391
|
-
//
|
|
1392
|
-
const
|
|
1331
|
+
// The gallery this page links. FEATURES are single-concept demos (one WebJs
|
|
1332
|
+
// concept each, under app/features/, logic in modules/). EXAMPLES are whole apps
|
|
1333
|
+
// composing several features (under app/examples/). Prune what you do not use
|
|
1334
|
+
// (delete the route AND its modules/<name>), then reshape this page.
|
|
1335
|
+
const FEATURES = [
|
|
1393
1336
|
{ href: '/features/routing', title: 'Routing', blurb: 'A static route plus a dynamic [id] segment that reads params. The file-based router in miniature.' },
|
|
1394
1337
|
{ href: '/features/boundaries', title: 'Boundaries', blurb: 'The control-flow throws (forbidden / unauthorized / notFound) and the nearest boundary file that catches each.' },
|
|
1395
1338
|
{ href: '/features/components', title: 'Components', blurb: 'The WebComponent factory, reactive props, instance signals, and slot projection in light DOM.' },
|
|
@@ -1397,7 +1340,7 @@ const features = [
|
|
|
1397
1340
|
{ href: '/features/optimistic-ui', title: 'Optimistic UI', blurb: 'The imperative optimistic(signal, value, action) flip: instant update, automatic rollback on failure.' },
|
|
1398
1341
|
{ 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.' },
|
|
1399
1342
|
{ href: '/features/directives', title: 'Directives', blurb: 'The lit-html directive set: repeat for keyed lists, watch(signal) for a fine-grained node swap.' },
|
|
1400
|
-
{ href: '/features/route-handler', title: 'Route handlers', blurb: 'A server-only route.ts HTTP endpoint returning JSON, the
|
|
1343
|
+
{ 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.' },
|
|
1401
1344
|
{ href: '/features/forms', title: 'Forms', blurb: 'A no-JS progressive-enhancement form posting to the page action, with server-side validation errors.' },
|
|
1402
1345
|
{ href: '/features/metadata', title: 'Metadata', blurb: 'Static metadata plus generateMetadata(ctx), which reads the request to compute the title and Open Graph tags.' },
|
|
1403
1346
|
{ 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.' },
|
|
@@ -1410,162 +1353,79 @@ const features = [
|
|
|
1410
1353
|
{ href: '/features/file-storage', title: 'File storage', blurb: 'A no-JS multipart upload streamed into the FileStore, then served back through a streaming route.' },
|
|
1411
1354
|
{ href: '/features/sessions', title: 'Sessions', blurb: 'A signed-cookie session applied by a segment middleware, read and written per visitor with getSession() in a route.' },
|
|
1412
1355
|
];
|
|
1413
|
-
const
|
|
1356
|
+
const EXAMPLES = [
|
|
1414
1357
|
{ 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.' },
|
|
1415
1358
|
];
|
|
1416
1359
|
|
|
1417
|
-
const galleryCard = (item: { href: string; title: string; blurb: string }) => html\`
|
|
1418
|
-
<a href=\${item.href} class="block no-underline">
|
|
1419
|
-
<div class="\${cardClass()} h-full transition-colors hover:border-border-strong">
|
|
1420
|
-
<div class=\${cardHeaderClass()}>
|
|
1421
|
-
<h3 class=\${cardTitleClass()}>\${item.title}</h3>
|
|
1422
|
-
<p class=\${cardDescriptionClass()}>\${item.blurb}</p>
|
|
1423
|
-
</div>
|
|
1424
|
-
</div>
|
|
1425
|
-
</a>
|
|
1426
|
-
\`;
|
|
1427
|
-
|
|
1428
1360
|
export default function Home() {
|
|
1429
1361
|
return html\`
|
|
1430
|
-
<
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
</
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1362
|
+
<div class="fixed top-4 right-4 z-10"><theme-toggle></theme-toggle></div>
|
|
1363
|
+
|
|
1364
|
+
<div class="max-w-5xl mx-auto px-6 py-16 flex flex-col items-center gap-16">
|
|
1365
|
+
<!-- Masthead -->
|
|
1366
|
+
<section class="flex flex-col items-center text-center gap-5">
|
|
1367
|
+
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-muted-foreground m-0">Welcome to</p>
|
|
1368
|
+
<h1 class="text-6xl sm:text-7xl font-bold uppercase tracking-tight leading-none m-0 break-words bg-gradient-to-b from-foreground to-muted-foreground bg-clip-text text-transparent" style="font-family: var(--font-display); word-spacing: 0.08em; letter-spacing: -0.02em;">
|
|
1369
|
+
WebJs Gallery
|
|
1370
|
+
</h1>
|
|
1371
|
+
<p class="text-base sm:text-lg text-muted-foreground max-w-lg leading-relaxed m-0">
|
|
1372
|
+
AI-first and web-components-first. Server-rendered, progressively enhanced, and buildless.
|
|
1373
|
+
</p>${homeAuthLinks}
|
|
1374
|
+
</section>
|
|
1375
|
+
|
|
1376
|
+
<!-- Gallery: every feature demo + the example app -->
|
|
1377
|
+
<section class="w-full flex flex-col gap-6">
|
|
1378
|
+
<div class="flex flex-col items-center gap-2 text-center">
|
|
1379
|
+
<h2 class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground m-0">Explore the gallery</h2>
|
|
1380
|
+
<p class="text-sm text-muted-foreground max-w-lg leading-relaxed m-0">
|
|
1381
|
+
One WebJs concept per demo under <code class="text-[0.9em] text-foreground">app/features/</code>, with logic
|
|
1382
|
+
in <code class="text-[0.9em] text-foreground">modules/</code>.
|
|
1383
|
+
</p>
|
|
1384
|
+
</div>
|
|
1385
|
+
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
1386
|
+
\${FEATURES.map(f => html\`
|
|
1387
|
+
<a href="\${f.href}" class="group flex flex-col gap-1.5 rounded-xl border border-border bg-card p-4 no-underline transition-colors hover:border-border-strong hover:bg-accent">
|
|
1388
|
+
<span class="flex items-center justify-between gap-2">
|
|
1389
|
+
<span class="text-sm font-medium text-foreground">\${f.title}</span>
|
|
1390
|
+
<span class="text-muted-foreground transition-transform group-hover:translate-x-0.5" aria-hidden="true">→</span>
|
|
1391
|
+
</span>
|
|
1392
|
+
<span class="text-xs leading-relaxed text-muted-foreground">\${f.blurb}</span>
|
|
1393
|
+
</a>
|
|
1394
|
+
\`)}
|
|
1395
|
+
</div>
|
|
1396
|
+
\${EXAMPLES.map(e => html\`
|
|
1397
|
+
<a href="\${e.href}" class="group flex flex-col gap-2 rounded-xl border border-border bg-card p-5 no-underline transition-colors hover:border-border-strong hover:bg-accent">
|
|
1398
|
+
<span class="flex items-center gap-2.5">
|
|
1399
|
+
<span class="text-[0.6rem] font-semibold uppercase tracking-wider text-muted-foreground rounded border border-border px-1.5 py-0.5">Example app</span>
|
|
1400
|
+
<span class="text-sm font-medium text-foreground">\${e.title}</span>
|
|
1401
|
+
<span class="ml-auto text-muted-foreground transition-transform group-hover:translate-x-0.5" aria-hidden="true">→</span>
|
|
1402
|
+
</span>
|
|
1403
|
+
<span class="text-xs leading-relaxed text-muted-foreground">\${e.blurb}</span>
|
|
1404
|
+
</a>
|
|
1405
|
+
\`)}
|
|
1406
|
+
</section>
|
|
1407
|
+
|
|
1408
|
+
<!-- Footer: docs + source -->
|
|
1409
|
+
<footer class="flex flex-col items-center gap-3">
|
|
1410
|
+
<nav class="flex items-center gap-6 text-sm text-muted-foreground" aria-label="WebJs links">
|
|
1411
|
+
<a href="https://docs.webjs.dev" class="inline-flex items-center gap-2 hover:text-foreground transition-colors no-underline">\${iconBook()}<span>Docs</span></a>
|
|
1412
|
+
<a href="https://github.com/webjsdev/webjs" class="inline-flex items-center gap-2 hover:text-foreground transition-colors no-underline">\${iconGithub()}<span>GitHub</span></a>
|
|
1413
|
+
</nav>
|
|
1414
|
+
<p class="text-[0.7rem] uppercase tracking-[0.15em] text-muted-foreground m-0 text-center">
|
|
1415
|
+
Built with WebJs · MIT License
|
|
1416
|
+
</p>
|
|
1417
|
+
</footer>
|
|
1418
|
+
</div>
|
|
1465
1419
|
\`;
|
|
1466
1420
|
}
|
|
1467
|
-
`);
|
|
1468
|
-
} else {
|
|
1469
|
-
// saas home: the auth landing (hero + login/signup/dashboard) stays the
|
|
1470
|
-
// headline, and the WebJs feature gallery sits BELOW it, so a saas app is
|
|
1471
|
-
// also a learning surface. Keep the `features` list in sync with the
|
|
1472
|
-
// full-stack home above (both ship the same gallery).
|
|
1473
|
-
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.
|
|
1474
|
-
import { html } from '@webjsdev/core';
|
|
1475
|
-
import { rubric, displayH1, accentLink } from '#lib/utils/ui.ts';
|
|
1476
|
-
import {
|
|
1477
|
-
cardClass,
|
|
1478
|
-
cardHeaderClass,
|
|
1479
|
-
cardTitleClass,
|
|
1480
|
-
cardDescriptionClass,
|
|
1481
|
-
} from '#components/ui/card.ts';
|
|
1482
|
-
|
|
1483
|
-
export const metadata = {
|
|
1484
|
-
title: '${displayName}: built with webjs',
|
|
1485
|
-
};
|
|
1486
1421
|
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
{ href: '/features/routing', title: 'Routing', blurb: 'A static route plus a dynamic [id] segment that reads params. The file-based router in miniature.' },
|
|
1493
|
-
{ href: '/features/boundaries', title: 'Boundaries', blurb: 'The control-flow throws (forbidden / unauthorized / notFound) and the nearest boundary file that catches each.' },
|
|
1494
|
-
{ href: '/features/components', title: 'Components', blurb: 'The WebComponent factory, reactive props, instance signals, and slot projection in light DOM.' },
|
|
1495
|
-
{ 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.' },
|
|
1496
|
-
{ href: '/features/optimistic-ui', title: 'Optimistic UI', blurb: 'The imperative optimistic(signal, value, action) flip: instant update, automatic rollback on failure.' },
|
|
1497
|
-
{ 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.' },
|
|
1498
|
-
{ href: '/features/directives', title: 'Directives', blurb: 'The lit-html directive set: repeat for keyed lists, watch(signal) for a fine-grained node swap.' },
|
|
1499
|
-
{ 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.' },
|
|
1500
|
-
{ href: '/features/forms', title: 'Forms', blurb: 'A no-JS progressive-enhancement form posting to the page action, with server-side validation errors.' },
|
|
1501
|
-
{ href: '/features/metadata', title: 'Metadata', blurb: 'Static metadata plus generateMetadata(ctx), which reads the request to compute the title and Open Graph tags.' },
|
|
1502
|
-
{ 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.' },
|
|
1503
|
-
{ href: '/features/env', title: 'Env vars', blurb: 'The server-only vs WEBJS_PUBLIC_ boundary, read during SSR so secrets never reach the browser.' },
|
|
1504
|
-
{ href: '/features/client-router', title: 'Client router', blurb: 'Automatic soft navigation: fragment-only fetches, hover prefetch, scroll restore, and graceful no-JS fallback.' },
|
|
1505
|
-
{ 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).' },
|
|
1506
|
-
{ href: '/features/websockets', title: 'WebSockets', blurb: 'A WS(ws, req) route endpoint plus the connectWS() client, echoing messages over a live socket.' },
|
|
1507
|
-
{ 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.' },
|
|
1508
|
-
{ href: '/features/rate-limit', title: 'Rate limiting', blurb: 'The rateLimit() middleware scoped to one endpoint, returning a 429 with Retry-After past the window.' },
|
|
1509
|
-
{ href: '/features/file-storage', title: 'File storage', blurb: 'A no-JS multipart upload streamed into the FileStore, then served back through a streaming route.' },
|
|
1510
|
-
{ href: '/features/sessions', title: 'Sessions', blurb: 'A signed-cookie session applied by a segment middleware, read and written per visitor with getSession() in a route.' },
|
|
1511
|
-
];
|
|
1512
|
-
const examples = [
|
|
1513
|
-
{ 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.' },
|
|
1514
|
-
];
|
|
1515
|
-
|
|
1516
|
-
const galleryCard = (item: { href: string; title: string; blurb: string }) => html\`
|
|
1517
|
-
<a href=\${item.href} class="block no-underline">
|
|
1518
|
-
<div class="\${cardClass()} h-full transition-colors hover:border-border-strong">
|
|
1519
|
-
<div class=\${cardHeaderClass()}>
|
|
1520
|
-
<h3 class=\${cardTitleClass()}>\${item.title}</h3>
|
|
1521
|
-
<p class=\${cardDescriptionClass()}>\${item.blurb}</p>
|
|
1522
|
-
</div>
|
|
1523
|
-
</div>
|
|
1524
|
-
</a>
|
|
1525
|
-
\`;
|
|
1526
|
-
|
|
1527
|
-
export default function Home() {
|
|
1528
|
-
return html\`
|
|
1529
|
-
<section class="mb-14">
|
|
1530
|
-
\${rubric('welcome')}
|
|
1531
|
-
\${displayH1(html\`Hello from <span class="text-primary italic">${displayName}</span>.\`)}
|
|
1532
|
-
<p class="text-lede leading-[1.5] text-muted-foreground max-w-[56ch] m-0 mb-6">
|
|
1533
|
-
The saas starter: email and password auth, a protected dashboard, and a
|
|
1534
|
-
User model, all wired up. Replace this hero with your product's landing;
|
|
1535
|
-
the webjs feature gallery below is reference, prune what you do not need.
|
|
1536
|
-
See \${accentLink('https://docs.webjs.dev', 'the docs')} for the full reference.
|
|
1537
|
-
</p>
|
|
1538
|
-
<div class="flex gap-3 items-center">
|
|
1539
|
-
<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>
|
|
1540
|
-
<a href="/signup" class="text-primary no-underline font-medium text-sm">Create an account</a>
|
|
1541
|
-
<a href="/dashboard" class="text-muted-foreground no-underline font-medium text-sm hover:text-foreground transition-colors">Dashboard</a>
|
|
1542
|
-
</div>
|
|
1543
|
-
</section>
|
|
1544
|
-
|
|
1545
|
-
<section class="mb-12">
|
|
1546
|
-
<h2 class="font-serif text-[1.6rem] tracking-[-0.02em] font-bold m-0 mb-1">Features</h2>
|
|
1547
|
-
<p class="text-muted-foreground text-sm m-0 mb-5">
|
|
1548
|
-
One webjs concept each, under <code class="font-mono text-[0.9em]">app/features/</code>
|
|
1549
|
-
with logic in <code class="font-mono text-[0.9em]">modules/</code>. Delete the ones you do not need.
|
|
1550
|
-
</p>
|
|
1551
|
-
<div class="grid gap-4 sm:grid-cols-2">
|
|
1552
|
-
\${features.map(galleryCard)}
|
|
1553
|
-
</div>
|
|
1554
|
-
</section>
|
|
1555
|
-
|
|
1556
|
-
<section>
|
|
1557
|
-
<h2 class="font-serif text-[1.6rem] tracking-[-0.02em] font-bold m-0 mb-1">Example apps</h2>
|
|
1558
|
-
<p class="text-muted-foreground text-sm m-0 mb-5">
|
|
1559
|
-
Whole apps that compose several features, under <code class="font-mono text-[0.9em]">app/examples/</code>.
|
|
1560
|
-
</p>
|
|
1561
|
-
<div class="grid gap-4 sm:grid-cols-2">
|
|
1562
|
-
\${examples.map(galleryCard)}
|
|
1563
|
-
</div>
|
|
1564
|
-
</section>
|
|
1565
|
-
\`;
|
|
1422
|
+
function iconBook() {
|
|
1423
|
+
return html\`<svg class="w-4 h-4 stroke-current fill-none" style="stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round" viewBox="0 0 24 24"><path d="M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2z"/><path d="M4 19a2 2 0 0 1 2-2h13"/></svg>\`;
|
|
1424
|
+
}
|
|
1425
|
+
function iconGithub() {
|
|
1426
|
+
return html\`<svg class="w-4 h-4 fill-current" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 0-3.16 19.49c.5.09.68-.22.68-.48v-1.7c-2.78.6-3.37-1.34-3.37-1.34-.45-1.16-1.11-1.47-1.11-1.47-.9-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.53 2.34 1.09 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02a9.5 9.5 0 0 1 5 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.59 1.03 2.68 0 3.84-2.34 4.69-4.57 4.94.36.31.68.92.68 1.85v2.74c0 .27.18.58.69.48A10 10 0 0 0 12 2Z"/></svg>\`;
|
|
1566
1427
|
}
|
|
1567
1428
|
`);
|
|
1568
|
-
}
|
|
1569
1429
|
|
|
1570
1430
|
// AGENTS.md is copied via the `templateFiles` loop above, from
|
|
1571
1431
|
// `packages/cli/templates/AGENTS.md` with `{{APP_NAME}}` substitution.
|
|
@@ -1652,90 +1512,49 @@ ThemeToggle.register('theme-toggle');
|
|
|
1652
1512
|
const { execSync } = await import('node:child_process');
|
|
1653
1513
|
try {
|
|
1654
1514
|
execSync('git init', { cwd: appDir, stdio: 'pipe' });
|
|
1655
|
-
//
|
|
1515
|
+
// Use the tracked .hooks/ dir (the pre-commit blocks commits to main).
|
|
1656
1516
|
execSync('git config core.hooksPath .hooks', { cwd: appDir, stdio: 'pipe' });
|
|
1657
1517
|
} catch { /* git not available: skip */ }
|
|
1658
1518
|
|
|
1659
1519
|
// --- Print success ---
|
|
1660
1520
|
|
|
1521
|
+
const guide = 'AGENTS.md, .agents/skills/webjs/ ← the agent guide';
|
|
1661
1522
|
if (isApi) {
|
|
1662
1523
|
console.log(` ${name}/
|
|
1663
|
-
app/api/health/route.ts
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1524
|
+
app/api/{health,users}/route.ts
|
|
1525
|
+
modules/users/{actions,queries,types.ts} ← routes over server actions
|
|
1526
|
+
db/{schema,columns,connection}.server.ts ← Drizzle (User model)
|
|
1527
|
+
${guide}
|
|
1667
1528
|
`);
|
|
1668
1529
|
} else if (isSaas) {
|
|
1669
1530
|
console.log(` ${name}/
|
|
1670
|
-
app/layout
|
|
1531
|
+
app/{layout,page}.ts, login/, signup/
|
|
1671
1532
|
app/dashboard/{page,settings,middleware}.ts ← protected
|
|
1672
|
-
app/api/auth/[...path]/route.ts
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
components/theme-toggle.ts
|
|
1678
|
-
modules/auth/{actions,queries,types.ts}
|
|
1679
|
-
lib/{auth,password}.server.ts
|
|
1680
|
-
lib/utils/cn.ts ← cn() helper for ui-* components
|
|
1681
|
-
db/{schema,columns,connection}.server.ts ← Drizzle (User model)
|
|
1682
|
-
CONVENTIONS.md, AGENTS.md, CLAUDE.md
|
|
1533
|
+
app/api/auth/[...path]/route.ts ← auth API
|
|
1534
|
+
components/ui/*, components/theme-toggle.ts
|
|
1535
|
+
modules/auth/*, lib/{auth,password}.server.ts
|
|
1536
|
+
db/{schema,columns,connection}.server.ts ← Drizzle (User model)
|
|
1537
|
+
${guide}
|
|
1683
1538
|
`);
|
|
1684
1539
|
} else {
|
|
1685
1540
|
console.log(` ${name}/
|
|
1686
|
-
app/layout
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
styles/globals.css ← @webjsdev/ui theme tokens
|
|
1692
|
-
components.json ← preconfigured for \`webjs ui add\`
|
|
1693
|
-
components/ui/{button,card,alert,badge,separator,label,input}.ts
|
|
1694
|
-
components/theme-toggle.ts ← light DOM web component
|
|
1695
|
-
lib/utils/cn.ts ← cn() helper for ui-* components
|
|
1696
|
-
lib/utils/ui.ts ← Tailwind class-bundle helpers
|
|
1697
|
-
public/input.css ← Tailwind entry (compiled to public/tailwind.css)
|
|
1698
|
-
modules/{components,server-actions,optimistic-ui,async-render,
|
|
1699
|
-
directives,todo}/ ← feature + example logic (prune what you skip)
|
|
1700
|
-
db/{schema,columns,connection}.server.ts ← Drizzle (User + Todo)
|
|
1701
|
-
CONVENTIONS.md, AGENTS.md, CLAUDE.md
|
|
1541
|
+
app/{layout,page}.ts ← a minimal home to grow in place
|
|
1542
|
+
components/theme-toggle.ts
|
|
1543
|
+
public/input.css ← Tailwind entry (compiles to public/tailwind.css)
|
|
1544
|
+
db/{schema,columns,connection}.server.ts ← Drizzle
|
|
1545
|
+
${guide}
|
|
1702
1546
|
`);
|
|
1703
1547
|
}
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
✓ Convention validation via \`webjs check\`
|
|
1715
|
-
|
|
1716
|
-
For AI agents, read this before editing scaffolded files:
|
|
1717
|
-
• The example layout, home page, the gallery under app/features/ and
|
|
1718
|
-
app/examples/ (+ their modules) and schema are REFERENCE ONLY. Replace
|
|
1719
|
-
them with the app the user asked for, and prune the routes you do not
|
|
1720
|
-
use (delete the app/features/<name> or app/examples/<name> route AND
|
|
1721
|
-
its modules/<name>). Don't ship the scaffold's examples as the final
|
|
1722
|
-
product.
|
|
1723
|
-
• This fresh app intentionally FAILS \`webjs check\` with
|
|
1724
|
-
no-scaffold-placeholder violations on the example content (the home
|
|
1725
|
-
page, the layout, and each gallery route). That is the signal to
|
|
1726
|
-
replace or prune each. Delete the marker comment line as you do, and
|
|
1727
|
-
the check goes green.
|
|
1728
|
-
• Use Drizzle + SQLite for app data. It's already wired up. Define
|
|
1729
|
-
real models in db/schema.server.ts, then run \`webjs db generate\`
|
|
1730
|
-
and \`webjs db migrate\`. NEVER store app data in JSON files,
|
|
1731
|
-
in-memory arrays, or localStorage as a substitute for the database.
|
|
1732
|
-
• Only three scaffolds exist: full-stack (default), api, saas. Don't
|
|
1733
|
-
invent template names. If you need a different kind of app, pick
|
|
1734
|
-
the closest scaffold and adapt it.
|
|
1735
|
-
• Read AGENTS.md + CONVENTIONS.md in the new project before writing
|
|
1736
|
-
any code. They are the contract.
|
|
1737
|
-
• Need more detail? Full hosted docs are at https://docs.webjs.dev
|
|
1738
|
-
(every API, directive, recipe, and deployment guide).
|
|
1548
|
+
console.log(`For AI agents, read this before editing:
|
|
1549
|
+
• Read AGENTS.md, then .agents/skills/webjs/SKILL.md. The skill is the guide
|
|
1550
|
+
to building a WebJs app and routes to focused references on demand.
|
|
1551
|
+
• This scaffold is a minimal starting point, not a demo to prune. Grow the app
|
|
1552
|
+
in place: add routes under app/, components under components/, and features
|
|
1553
|
+
under modules/<feature>/, and keep server-only code behind .server.ts.
|
|
1554
|
+
• Use the wired-up database (Drizzle): define real models in
|
|
1555
|
+
db/schema.server.ts, then run 'npm run db:generate' and 'npm run db:migrate'.
|
|
1556
|
+
Never store app data in JSON files, in-memory arrays, or localStorage.
|
|
1557
|
+
• Full hosted docs are at https://docs.webjs.dev.
|
|
1739
1558
|
`);
|
|
1740
1559
|
|
|
1741
1560
|
// Auto-install (default). Detect the package manager from the env so
|