@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.
Files changed (71) hide show
  1. package/bin/webjs.js +4 -46
  2. package/lib/app-tasks.js +13 -7
  3. package/lib/create.js +306 -487
  4. package/lib/doctor.js +73 -26
  5. package/package.json +5 -1
  6. package/templates/.agents/rules/workflow.md +61 -271
  7. package/templates/.agents/skills/webjs/SKILL.md +226 -0
  8. package/templates/.agents/skills/webjs/references/auth-and-sessions.md +220 -0
  9. package/templates/.agents/skills/webjs/references/built-ins.md +200 -0
  10. package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +204 -0
  11. package/templates/.agents/skills/webjs/references/components.md +167 -0
  12. package/templates/.agents/skills/webjs/references/data-and-actions.md +187 -0
  13. package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +170 -0
  14. package/templates/.agents/skills/webjs/references/optimistic-ui.md +128 -0
  15. package/templates/.agents/skills/webjs/references/routing-and-pages.md +158 -0
  16. package/templates/.agents/skills/webjs/references/runtime.md +80 -0
  17. package/templates/.agents/skills/webjs/references/service-worker.md +78 -0
  18. package/templates/.agents/skills/webjs/references/styling.md +123 -0
  19. package/templates/.agents/skills/webjs/references/testing.md +125 -0
  20. package/templates/.agents/skills/webjs/references/typescript.md +148 -0
  21. package/templates/.claude/hooks/check-server-imports.mjs +1 -1
  22. package/templates/.claude/hooks/require-tests-with-src.sh +1 -1
  23. package/templates/.claude/settings.json +0 -14
  24. package/templates/.cursorrules +21 -189
  25. package/templates/.github/copilot-instructions.md +7 -185
  26. package/templates/.github/pull_request_template.md +1 -1
  27. package/templates/AGENTS.md +59 -1484
  28. package/templates/CLAUDE.md +0 -1
  29. package/templates/CONVENTIONS.md +32 -1383
  30. package/templates/GEMINI.md +11 -0
  31. package/templates/gallery/app/apple-icon.ts +0 -1
  32. package/templates/gallery/app/examples/todo/page.ts +0 -1
  33. package/templates/gallery/app/features/async-render/page.ts +0 -1
  34. package/templates/gallery/app/features/boundaries/page.ts +0 -1
  35. package/templates/gallery/app/features/broadcast/page.ts +0 -1
  36. package/templates/gallery/app/features/caching/page.ts +0 -1
  37. package/templates/gallery/app/features/client-router/page.ts +0 -1
  38. package/templates/gallery/app/features/client-router/second/page.ts +0 -1
  39. package/templates/gallery/app/features/components/page.ts +0 -1
  40. package/templates/gallery/app/features/directives/page.ts +0 -1
  41. package/templates/gallery/app/features/env/page.ts +0 -1
  42. package/templates/gallery/app/features/file-storage/page.ts +0 -1
  43. package/templates/gallery/app/features/forms/page.ts +0 -1
  44. package/templates/gallery/app/features/metadata/page.ts +0 -1
  45. package/templates/gallery/app/features/optimistic-ui/page.ts +0 -1
  46. package/templates/gallery/app/features/rate-limit/page.ts +0 -1
  47. package/templates/gallery/app/features/route-handler/page.ts +0 -1
  48. package/templates/gallery/app/features/routing/page.ts +0 -1
  49. package/templates/gallery/app/features/server-actions/page.ts +0 -1
  50. package/templates/gallery/app/features/service-worker/page.ts +0 -1
  51. package/templates/gallery/app/features/sessions/page.ts +0 -1
  52. package/templates/gallery/app/features/websockets/page.ts +0 -1
  53. package/templates/gallery/app/global-error.ts +0 -1
  54. package/templates/gallery/app/global-not-found.ts +0 -1
  55. package/templates/gallery/app/icon.ts +0 -1
  56. package/templates/gallery/app/manifest.ts +0 -1
  57. package/templates/gallery/app/opengraph-image.ts +0 -1
  58. package/templates/gallery/app/robots.ts +0 -1
  59. package/templates/gallery/app/sitemap.ts +0 -1
  60. package/templates/gallery/app/twitter-image.ts +0 -1
  61. package/templates/gallery/modules/async-render/components/server-clock.ts +7 -6
  62. package/templates/public/favicon.svg +5 -0
  63. package/templates/public/sw.js +1 -1
  64. package/templates/scripts/clear-gallery.mjs +95 -0
  65. package/lib/clear-placeholders.js +0 -98
  66. package/lib/design-bar.js +0 -67
  67. package/templates/.claude/hooks/design-review-before-stop.sh +0 -36
  68. package/templates/.claude/hooks/route-skills.sh +0 -35
  69. package/templates/.claude/skills/webjs-design-review/SKILL.md +0 -84
  70. package/templates/LAYOUT-REFERENCE.md +0 -96
  71. 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 / parallel steps get node_modules/.bin on PATH
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, and runs the Tailwind
493
- // `--watch` under `parallel` for live recompiles in dev. The compile command
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 ? {} : { parallel: [cssWatchCmd] }),
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
- // A worked layout (header/nav/theme-toggle/reading-column/footer) the agent
565
- // reads to learn the patterns, since app/layout.ts ships as a minimal shell
566
- // so the app designs its own chrome. Shipped for every template (harmless
567
- // for api, which has no app/layout.ts).
568
- 'LAYOUT-REFERENCE.md',
569
- // Starter tests under the new feature-folder layout.
570
- 'test/hello/hello.test.ts',
571
- 'test/hello/browser/hello.test.js',
572
- 'test/hello/e2e/hello.test.ts',
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
- // Render-and-look enforcement for UI work: a UserPromptSubmit router that
599
- // points UI-building prompts at the webjs-design-review skill, a Stop-hook
600
- // backstop that nudges a render-and-look before finishing UI changes, and
601
- // the skill they route to. A design/layout defect has no failing test, so
602
- // this vision-in-the-loop is the only thing that catches it.
603
- '.claude/hooks/route-skills.sh',
604
- '.claude/hooks/design-review-before-stop.sh',
605
- '.claude/skills/webjs-design-review/SKILL.md',
606
- // Gemini CLI config + hooks
607
- '.gemini/settings.json',
608
- '.gemini/hooks/nudge-uncommitted.sh',
609
- // Cursor config + hooks
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 is the test gate (the pre-commit hook only blocks main). Runs
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. `docker compose up --build` runs
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', '.github/copilot-instructions.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
- // Make hook scripts executable
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', 'route-skills.sh', 'design-review-before-stop.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
- // demonstrating the server-side surface (route() adapter + validation, rate
1041
- // limiting, streaming, file storage, WebSockets + broadcast) plus env
1042
- // validation. The api counterpart of the UI gallery. Prune what you skip.
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 agent-docs/service-worker.md);
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
- const utilsDir = join(appDir, 'lib', 'utils');
1068
- await mkdir(utilsDir, { recursive: true });
1069
- const uiSrc = join(TEMPLATES, 'lib', 'utils', 'ui.ts');
1070
- if (existsSync(uiSrc)) {
1071
- await cp(uiSrc, join(utilsDir, 'ui.ts'));
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
- // Copy the standard ui-* component kit the scaffold's example pages
1087
- // use. Sources are read from packages/ui/packages/registry/ in this
1088
- // monorepo. Users can `webjs ui add <name>` for anything else.
1089
- await copyUiComponents(appDir, [
1090
- 'button', 'card', 'alert', 'badge', 'separator', 'label', 'input',
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: globals + a minimal shell.
1156
- *
1157
- * Light DOM + Tailwind by default. Design tokens live in :root and are
1158
- * mapped into the Tailwind palette via @theme, so classes like
1159
- * text-foreground, bg-card, font-serif, duration-fast, text-display all work.
1160
- *
1161
- * This shell is deliberately MINIMAL: it wires the theme, tokens, and Tailwind,
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
- // inline script below passes strict CSP (script-src 'nonce-...').
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
- // ===== OPTIONAL: light/dark theme apparatus (remove as one unit) =====
1176
- // This IIFE reads the saved or OS theme and toggles the data-theme
1177
- // attribute plus the dark class the ui kit reads, so the token VALUES in
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
- // ===== end optional theme apparatus =====
1205
- // Header-measure script: DORMANT until you add a fixed header (the minimal
1206
- // shell has none). When you add one (see LAYOUT-REFERENCE.md), make it
1207
- // position:fixed (NOT sticky: a sticky header flickers on iOS WebKit during
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 / css:watch (run automatically by the
1230
- dev and start tasks). A real stylesheet, so the app is fully styled with
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
- /* ONE theme, canonical shadcn-style tokens. These are the token VALUES as
1235
- plain CSS custom properties, so the palette resolves with JavaScript
1236
- DISABLED and needs no build. public/input.css holds the token STRUCTURE
1237
- and the @theme inline mappings that generate
1238
- bg-background, text-foreground, bg-card, bg-primary, bg-accent,
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: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
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
- /* 0 by default because the minimal shell has no fixed header. The
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 brand tint, derived from --primary so it tracks
1265
- light/dark automatically. Used for the logo glow and focus ring. */
1266
- --primary-tint: color-mix(in oklch, var(--primary) 20%, transparent);
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: oklch(0.14 0.01 55);
1273
- --foreground: oklch(0.96 0.015 60);
1274
- --card: oklch(0.18 0.01 55);
1275
- --card-foreground: oklch(0.96 0.015 60);
1276
- --popover: oklch(0.18 0.01 55);
1277
- --popover-foreground: oklch(0.96 0.015 60);
1278
- /* --primary is the orange BRAND color (shadcn model: primary = brand). */
1279
- --primary: oklch(0.7 0.16 52);
1280
- --primary-foreground: oklch(0.17 0.02 52);
1281
- --secondary: oklch(0.22 0.01 55);
1282
- --secondary-foreground: oklch(0.96 0.015 60);
1283
- --muted: oklch(0.16 0.01 55);
1284
- --muted-foreground: oklch(0.72 0.02 60);
1285
- /* --accent stays a NEUTRAL hover tint (shadcn model), so the ui kit's
1286
- outline/ghost/dropdown hover states keep proper contrast. */
1287
- --accent: oklch(0.27 0.008 55);
1288
- --accent-foreground: oklch(0.96 0.015 60);
1289
- --border: oklch(0.26 0.012 55 / 0.9);
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: oklch(0.985 0.008 80);
1300
- --foreground: oklch(0.18 0.015 60);
1301
- --card: oklch(1 0 0);
1302
- --card-foreground: oklch(0.18 0.015 60);
1303
- --popover: oklch(1 0 0);
1304
- --popover-foreground: oklch(0.18 0.015 60);
1305
- --primary: oklch(0.54 0.16 52);
1306
- --primary-foreground: oklch(1 0 0);
1307
- --secondary: oklch(0.96 0.008 80);
1308
- --secondary-foreground: oklch(0.18 0.015 60);
1309
- --muted: oklch(0.96 0.008 80);
1310
- --muted-foreground: oklch(0.42 0.02 65);
1311
- --accent: oklch(0.955 0.006 80);
1312
- --accent-foreground: oklch(0.18 0.015 60);
1313
- --border: oklch(0.88 0.01 75 / 0.95);
1314
- --border-strong: oklch(0.78 0.01 75 / 0.95);
1315
- --input: oklch(0.88 0.01 75 / 0.95);
1316
- --ring: oklch(0.54 0.16 52);
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: oklch(0.985 0.008 80);
1325
- --foreground: oklch(0.18 0.015 60);
1326
- --card: oklch(1 0 0);
1327
- --card-foreground: oklch(0.18 0.015 60);
1328
- --popover: oklch(1 0 0);
1329
- --popover-foreground: oklch(0.18 0.015 60);
1330
- --primary: oklch(0.54 0.16 52);
1331
- --primary-foreground: oklch(1 0 0);
1332
- --secondary: oklch(0.96 0.008 80);
1333
- --secondary-foreground: oklch(0.18 0.015 60);
1334
- --muted: oklch(0.96 0.008 80);
1335
- --muted-foreground: oklch(0.42 0.02 65);
1336
- --accent: oklch(0.955 0.006 80);
1337
- --accent-foreground: oklch(0.18 0.015 60);
1338
- --border: oklch(0.88 0.01 75 / 0.95);
1339
- --border-strong: oklch(0.78 0.01 75 / 0.95);
1340
- --input: oklch(0.88 0.01 75 / 0.95);
1341
- --ring: oklch(0.54 0.16 52);
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: 16px/1.65 var(--font-sans);
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
- // The gallery-index home (links every feature demo + the example app) ships
1369
- // only in the full-stack scaffold, which is the only one that ships the
1370
- // gallery. saas gets its own landing below.
1371
- if (isFullStack) {
1372
- 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.
1373
- import { html } from '@webjsdev/core';
1374
- import { rubric, displayH1, accentLink } from '#lib/utils/ui.ts';
1375
- import { buttonClass } from '#components/ui/button.ts';
1376
- import { badgeClass } from '#components/ui/badge.ts';
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}: built with webjs',
1328
+ title: '${displayName}',
1386
1329
  };
1387
1330
 
1388
- // Two kinds of reference the scaffold ships. FEATURES are single-concept demos
1389
- // (one WebJs feature each, under app/features/, logic in modules/). EXAMPLES are
1390
- // whole apps that compose several features (under app/examples/). Prune what you
1391
- // do not use (delete the route AND its modules/<name>), then reshape this page.
1392
- const features = [
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 webjs equivalent of a Next route handler.' },
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 examples = [
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
- <section class="mb-14">
1431
- \${rubric('welcome')}
1432
- \${displayH1(html\`Hello from <span class="text-primary italic">${displayName}</span>.\`)}
1433
- <p class="text-lede leading-[1.5] text-muted-foreground max-w-[56ch] m-0 mb-6">
1434
- This WebJs scaffold ships a gallery below: single-feature demos and one
1435
- whole example app, all small, idiomatic, and heavily commented. Browse
1436
- them for context, then replace this page with your own. See
1437
- \${accentLink('https://docs.webjs.dev', 'the docs')} for the full reference.
1438
- </p>
1439
- <div class="flex gap-3 items-center">
1440
- <a href="/examples/todo" class=\${buttonClass()}>Open the todo app</a>
1441
- <span class=\${badgeClass({ variant: 'secondary' })}>v0.1</span>
1442
- </div>
1443
- </section>
1444
-
1445
- <section class="mb-12">
1446
- <h2 class="font-serif text-[1.6rem] tracking-[-0.02em] font-bold m-0 mb-1">Features</h2>
1447
- <p class="text-muted-foreground text-sm m-0 mb-5">
1448
- One webjs concept each, under <code class="font-mono text-[0.9em]">app/features/</code>
1449
- with logic in <code class="font-mono text-[0.9em]">modules/</code>. Delete the ones you do not need.
1450
- </p>
1451
- <div class="grid gap-4 sm:grid-cols-2">
1452
- \${features.map(galleryCard)}
1453
- </div>
1454
- </section>
1455
-
1456
- <section>
1457
- <h2 class="font-serif text-[1.6rem] tracking-[-0.02em] font-bold m-0 mb-1">Example apps</h2>
1458
- <p class="text-muted-foreground text-sm m-0 mb-5">
1459
- Whole apps that compose several features, under <code class="font-mono text-[0.9em]">app/examples/</code>.
1460
- </p>
1461
- <div class="grid gap-4 sm:grid-cols-2">
1462
- \${examples.map(galleryCard)}
1463
- </div>
1464
- </section>
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">&rarr;</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">&rarr;</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 &middot; 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
- // The WebJs feature gallery, shown below the auth landing. FEATURES are
1488
- // single-concept demos (under app/features/, logic in modules/); EXAMPLES are
1489
- // whole apps (under app/examples/). Prune what you do not use (delete the route
1490
- // AND its modules/<name>). Keep this list in sync with the full-stack home.
1491
- const features = [
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
- // Tell git to use .hooks/ as the hooks directory (tracked in the repo)
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
- app/api/users/route.ts ← thin wrapper over server actions
1665
- modules/users/{actions,queries,types.ts}
1666
- CONVENTIONS.md, AGENTS.md, CLAUDE.md
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.ts, page.ts, login/, signup/
1531
+ app/{layout,page}.ts, login/, signup/
1671
1532
  app/dashboard/{page,settings,middleware}.ts ← protected
1672
- app/api/auth/[...path]/route.ts ← auth API
1673
- styles/globals.css ← @webjsdev/ui theme tokens
1674
- components.json ← preconfigured for \`webjs ui add\`
1675
- components/ui/{button,card,alert,badge,separator,label,input,
1676
- dialog,form,field,switch,checkbox}.ts
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.ts, page.ts ← home links to the gallery
1687
- app/features/{routing,components,server-actions,optimistic-ui,
1688
- async-render,directives,route-handler}/
1689
- ← single-feature demos
1690
- app/examples/todo/ ← one whole example app (composes features)
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
- // AI-agent guidance comes first so it scrolls past in the terminal
1705
- // (long reading material, not actionable). The actionable run
1706
- // command lands LAST in the output so it is the final thing on
1707
- // screen after the install completes.
1708
- console.log(`AI-driven development (enforced for all AI agents):
1709
- ✓ Tests auto-generated with every feature
1710
- ✓ Docs auto-updated with every change
1711
- ✓ Git merges/pushes to main require approval
1712
- ✓ Commits are automatic, small, and meaningful
1713
- ✓ No AI attribution in commit messages
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