@webjsdev/cli 0.10.40 → 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 (69) hide show
  1. package/bin/webjs.js +4 -46
  2. package/lib/create.js +282 -479
  3. package/lib/doctor.js +1 -38
  4. package/package.json +5 -1
  5. package/templates/.agents/rules/workflow.md +61 -271
  6. package/templates/.agents/skills/webjs/SKILL.md +226 -0
  7. package/templates/.agents/skills/webjs/references/auth-and-sessions.md +220 -0
  8. package/templates/.agents/skills/webjs/references/built-ins.md +200 -0
  9. package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +204 -0
  10. package/templates/.agents/skills/webjs/references/components.md +167 -0
  11. package/templates/.agents/skills/webjs/references/data-and-actions.md +187 -0
  12. package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +170 -0
  13. package/templates/.agents/skills/webjs/references/optimistic-ui.md +128 -0
  14. package/templates/.agents/skills/webjs/references/routing-and-pages.md +158 -0
  15. package/templates/.agents/skills/webjs/references/runtime.md +80 -0
  16. package/templates/.agents/skills/webjs/references/service-worker.md +78 -0
  17. package/templates/.agents/skills/webjs/references/styling.md +123 -0
  18. package/templates/.agents/skills/webjs/references/testing.md +125 -0
  19. package/templates/.agents/skills/webjs/references/typescript.md +148 -0
  20. package/templates/.claude/hooks/check-server-imports.mjs +1 -1
  21. package/templates/.claude/hooks/require-tests-with-src.sh +1 -1
  22. package/templates/.claude/settings.json +0 -14
  23. package/templates/.cursorrules +21 -189
  24. package/templates/.github/copilot-instructions.md +7 -185
  25. package/templates/.github/pull_request_template.md +1 -1
  26. package/templates/AGENTS.md +59 -1494
  27. package/templates/CLAUDE.md +0 -1
  28. package/templates/CONVENTIONS.md +32 -1383
  29. package/templates/GEMINI.md +11 -0
  30. package/templates/gallery/app/apple-icon.ts +0 -1
  31. package/templates/gallery/app/examples/todo/page.ts +0 -1
  32. package/templates/gallery/app/features/async-render/page.ts +0 -1
  33. package/templates/gallery/app/features/boundaries/page.ts +0 -1
  34. package/templates/gallery/app/features/broadcast/page.ts +0 -1
  35. package/templates/gallery/app/features/caching/page.ts +0 -1
  36. package/templates/gallery/app/features/client-router/page.ts +0 -1
  37. package/templates/gallery/app/features/client-router/second/page.ts +0 -1
  38. package/templates/gallery/app/features/components/page.ts +0 -1
  39. package/templates/gallery/app/features/directives/page.ts +0 -1
  40. package/templates/gallery/app/features/env/page.ts +0 -1
  41. package/templates/gallery/app/features/file-storage/page.ts +0 -1
  42. package/templates/gallery/app/features/forms/page.ts +0 -1
  43. package/templates/gallery/app/features/metadata/page.ts +0 -1
  44. package/templates/gallery/app/features/optimistic-ui/page.ts +0 -1
  45. package/templates/gallery/app/features/rate-limit/page.ts +0 -1
  46. package/templates/gallery/app/features/route-handler/page.ts +0 -1
  47. package/templates/gallery/app/features/routing/page.ts +0 -1
  48. package/templates/gallery/app/features/server-actions/page.ts +0 -1
  49. package/templates/gallery/app/features/service-worker/page.ts +0 -1
  50. package/templates/gallery/app/features/sessions/page.ts +0 -1
  51. package/templates/gallery/app/features/websockets/page.ts +0 -1
  52. package/templates/gallery/app/global-error.ts +0 -1
  53. package/templates/gallery/app/global-not-found.ts +0 -1
  54. package/templates/gallery/app/icon.ts +0 -1
  55. package/templates/gallery/app/manifest.ts +0 -1
  56. package/templates/gallery/app/opengraph-image.ts +0 -1
  57. package/templates/gallery/app/robots.ts +0 -1
  58. package/templates/gallery/app/sitemap.ts +0 -1
  59. package/templates/gallery/app/twitter-image.ts +0 -1
  60. package/templates/public/favicon.svg +5 -0
  61. package/templates/public/sw.js +1 -1
  62. package/templates/scripts/clear-gallery.mjs +95 -0
  63. package/lib/clear-placeholders.js +0 -98
  64. package/lib/design-bar.js +0 -67
  65. package/templates/.claude/hooks/design-review-before-stop.sh +0 -36
  66. package/templates/.claude/hooks/route-skills.sh +0 -35
  67. package/templates/.claude/skills/webjs-design-review/SKILL.md +0 -84
  68. package/templates/LAYOUT-REFERENCE.md +0 -96
  69. package/templates/lib/utils/ui.ts +0 -83
package/lib/create.js CHANGED
@@ -415,6 +415,8 @@ export async function scaffoldApp(name, cwd, opts = {}) {
415
415
  // app runs the compiler under Bun (its image has no Node), a Node app runs
416
416
  // it directly.
417
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' }),
418
420
  dev: isBun ? 'bun --bun webjs dev' : 'webjs dev',
419
421
  start: isBun ? 'bun --bun webjs start' : 'webjs start',
420
422
  test: 'webjs test',
@@ -573,36 +575,31 @@ export async function scaffoldApp(name, cwd, opts = {}) {
573
575
  // --- Templates (AGENTS.md, CONVENTIONS.md, CLAUDE.md, test files, Claude hooks) ---
574
576
 
575
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.
576
580
  'AGENTS.md',
577
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.
578
589
  'CLAUDE.md',
579
- // A worked layout (header/nav/theme-toggle/reading-column/footer) the agent
580
- // reads to learn the patterns, since app/layout.ts ships as a minimal shell
581
- // so the app designs its own chrome. Shipped for every template (harmless
582
- // for api, which has no app/layout.ts).
583
- 'LAYOUT-REFERENCE.md',
584
- // Starter tests under the new feature-folder layout.
585
- 'test/hello/hello.test.ts',
586
- 'test/hello/browser/hello.test.js',
587
- 'test/hello/e2e/hello.test.ts',
588
- 'web-test-runner.config.js',
589
- // Optional boot-time APM hook (setOnError). Delete if unused.
590
- 'instrumentation.ts',
591
- // Environment variables
592
- '.env.example',
593
- // Project-level gitignore (node_modules, .webjs, .env, OS junk).
594
- // Shipped as `gitignore` (no dot) and renamed to `.gitignore` on copy:
595
- // npm STRIPS a `.gitignore` from a published tarball, so a dotfile name
596
- // would arrive missing and the app would ship without a `.env` ignore
597
- // (dogfood #845). The SQLite dev.db rule is appended programmatically
598
- // below so it only appears for the sqlite dialect.
599
- 'gitignore',
600
- // Git hooks (blocks commits on main)
601
- '.hooks/pre-commit',
602
- // 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).
603
599
  '.claude.json',
604
600
  '.claude/settings.json',
605
601
  '.claude/hooks/block-prose-punctuation.sh',
602
+ '.claude/hooks/block-raw-htmlelement.sh',
606
603
  '.claude/hooks/guard-branch-context.sh',
607
604
  '.claude/hooks/nudge-uncommitted.sh',
608
605
  '.claude/hooks/commit-before-stop.sh',
@@ -610,43 +607,24 @@ export async function scaffoldApp(name, cwd, opts = {}) {
610
607
  '.claude/hooks/require-tests-with-src.sh',
611
608
  '.claude/hooks/check-server-imports.sh',
612
609
  '.claude/hooks/check-server-imports.mjs',
613
- // Render-and-look enforcement for UI work: a UserPromptSubmit router that
614
- // points UI-building prompts at the webjs-design-review skill, a Stop-hook
615
- // backstop that nudges a render-and-look before finishing UI changes, and
616
- // the skill they route to. A design/layout defect has no failing test, so
617
- // this vision-in-the-loop is the only thing that catches it.
618
- '.claude/hooks/route-skills.sh',
619
- '.claude/hooks/design-review-before-stop.sh',
620
- '.claude/skills/webjs-design-review/SKILL.md',
621
- // Gemini CLI config + hooks
622
- '.gemini/settings.json',
623
- '.gemini/hooks/nudge-uncommitted.sh',
624
- // Cursor config + hooks
625
- '.cursor/hooks.json',
626
- '.cursor/hooks/nudge-uncommitted.sh',
627
- // OpenCode plugins (loaded as TS by Bun at runtime)
628
- '.opencode/plugins/nudge-uncommitted.ts',
629
- // Antigravity workspace rules (Google's documented convention is
630
- // `.agents/rules/*.md`, lowercase, per the Codelab
631
- // "Build Autonomous Developer Pipelines using agents.md and skills.md
632
- // in Antigravity"). Replaced the legacy `.windsurfrules` ship when
633
- // Windsurf was acquired by Google.
634
- '.agents/rules/workflow.md',
635
- // Cross-agent config files
636
- '.cursorrules',
637
- '.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',
638
622
  '.github/pull_request_template.md',
639
- // CI is the test gate (the pre-commit hook only blocks main). Runs
640
- // webjs check + the unit / browser / e2e layers on every PR and push
641
- // to main, mirroring the WebJs framework's own CI.
623
+ // CI runs webjs check + the test layers on every PR and push to main.
642
624
  '.github/workflows/ci.yml',
643
625
  '.editorconfig',
644
- // VS Code: associate the published webjs-config JSON Schema with the
645
- // package.json `webjs` block, so an unknown / typo'd key (#259) is
646
- // flagged natively in the editor instead of silently dropped.
647
626
  '.vscode/settings.json',
648
- // Production / deploy scaffolding. `docker compose up --build` runs
649
- // the app locally with the same Dockerfile production builds from.
627
+ // Production / deploy scaffolding.
650
628
  'Dockerfile',
651
629
  'compose.yaml',
652
630
  '.dockerignore',
@@ -658,12 +636,8 @@ export async function scaffoldApp(name, cwd, opts = {}) {
658
636
  // rewrites; the three infra files get their file-specific transform. On Node,
659
637
  // every file is copied byte-identical (the map is empty).
660
638
  const PROSE_REWRITE = new Set([
661
- 'AGENTS.md', 'CONVENTIONS.md', '.cursorrules',
662
- '.agents/rules/workflow.md', '.github/copilot-instructions.md',
663
- // The starter tests carry header comments with run commands (`npx wtr`,
664
- // `npm i -D puppeteer-core`); bun-ify those too so a bun app's test files
665
- // do not tell the user to run npm/npx (#541 review). The transform only
666
- // 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',
667
641
  'test/hello/browser/hello.test.js', 'test/hello/e2e/hello.test.ts',
668
642
  ]);
669
643
  // compose.yaml builds from the (pure oven/bun) Dockerfile and inherits its
@@ -691,21 +665,25 @@ export async function scaffoldApp(name, cwd, opts = {}) {
691
665
  }
692
666
  }
693
667
 
694
- // 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.
695
682
  const { chmod } = await import('node:fs/promises');
696
- 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']) {
697
684
  const hookPath = join(appDir, '.claude', 'hooks', hook);
698
685
  if (existsSync(hookPath)) await chmod(hookPath, 0o755);
699
686
  }
700
- for (const hook of ['nudge-uncommitted.sh']) {
701
- const hookPath = join(appDir, '.gemini', 'hooks', hook);
702
- if (existsSync(hookPath)) await chmod(hookPath, 0o755);
703
- }
704
- for (const hook of ['nudge-uncommitted.sh']) {
705
- const hookPath = join(appDir, '.cursor', 'hooks', hook);
706
- if (existsSync(hookPath)) await chmod(hookPath, 0o755);
707
- }
708
- // Make git pre-commit hook executable
709
687
  const preCommitPath = join(appDir, '.hooks', 'pre-commit');
710
688
  if (existsSync(preCommitPath)) await chmod(preCommitPath, 0o755);
711
689
 
@@ -1052,9 +1030,9 @@ export type ActionResult<T> =
1052
1030
  `);
1053
1031
 
1054
1032
  // The api backend-features showcase: endpoints under app/api/features/**
1055
- // demonstrating the server-side surface (route() adapter + validation, rate
1056
- // limiting, streaming, file storage, WebSockets + broadcast) plus env
1057
- // 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.
1058
1036
  const { writeApiGallery } = await import('./api-gallery.js');
1059
1037
  await writeApiGallery(appDir);
1060
1038
  }
@@ -1072,18 +1050,24 @@ export type ActionResult<T> =
1072
1050
  // Progressive-enhancement service worker (#271): ship the opt-in offline
1073
1051
  // primitive (the worker + its offline fallback) into the UI scaffolds
1074
1052
  // (full-stack / saas; this block is api-excluded since api has no UI).
1075
- // 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);
1076
1054
  // it never changes the JS-disabled baseline.
1077
1055
  for (const swFile of ['sw.js', 'offline.html']) {
1078
1056
  const swSrc = join(TEMPLATES, 'public', swFile);
1079
1057
  if (existsSync(swSrc)) await cp(swSrc, join(publicDir, swFile));
1080
1058
  }
1081
-
1082
- const utilsDir = join(appDir, 'lib', 'utils');
1083
- await mkdir(utilsDir, { recursive: true });
1084
- const uiSrc = join(TEMPLATES, 'lib', 'utils', 'ui.ts');
1085
- if (existsSync(uiSrc)) {
1086
- 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'));
1087
1071
  }
1088
1072
 
1089
1073
  // Fail loudly if the @webjsdev/ui registry sources aren't on disk.
@@ -1098,20 +1082,13 @@ export type ActionResult<T> =
1098
1082
  // styles/globals.css (the @webjsdev/ui theme).
1099
1083
  await writeUiBootstrap(appDir);
1100
1084
 
1101
- // Copy the standard ui-* component kit the scaffold's example pages
1102
- // use. Sources are read from packages/ui/packages/registry/ in this
1103
- // monorepo. Users can `webjs ui add <name>` for anything else.
1104
- await copyUiComponents(appDir, [
1105
- 'button', 'card', 'alert', 'badge', 'separator', 'label', 'input',
1106
- ]);
1107
-
1108
- // The gallery: idiomatic, densely-commented single-feature demos under
1109
- // app/features/ plus one whole example app under app/examples/, with logic
1110
- // in modules/, linked from the home page. Shipped in the default full-stack
1111
- // scaffold so an agent gains context by browsing real code; prune
1112
- // per-feature (delete the route + its module) for what the app does not
1113
- // use. See CONVENTIONS.md "prune what the app does not use".
1114
- 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
+ }
1115
1092
 
1116
1093
  // The @webjsdev/ui theme (`--color-primary`, `--color-card`, the @theme maps,
1117
1094
  // @custom-variant, @keyframes) plus the app @theme mappings are compiled from
@@ -1152,51 +1129,35 @@ ${uiThemeRaw}
1152
1129
  }
1153
1130
  `);
1154
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
+
1155
1140
  await writeFile(join(appDir, 'app', 'layout.ts'), `import { html, cspNonce } from '@webjsdev/core';
1156
1141
  import '#components/theme-toggle.ts';
1157
- // Webjs UI components are tiered:
1158
- // - Tier 1 (button, card, input, label, alert, badge, separator, etc.) are
1159
- // class-helper FUNCTIONS, with no custom element to register. Each page
1160
- // imports the specific helpers it needs (e.g.
1161
- // \`import { buttonClass } from '#components/ui/button.ts'\`).
1162
- // - Tier 2 (dialog, popover, tooltip, tabs, accordion, etc.) ARE custom
1163
- // elements. Register them by side-effect-importing here once so they
1164
- // work transitively across every page:
1165
- // import '#components/ui/dialog.ts';
1166
- // The example app/page.ts below uses only Tier-1 helpers, so nothing
1167
- // extra needs to be registered. Add Tier-2 imports as you 'webjs ui add'.
1168
1142
 
1169
1143
  /**
1170
- * Root layout: globals + a minimal shell.
1171
- *
1172
- * Light DOM + Tailwind by default. Design tokens live in :root and are
1173
- * mapped into the Tailwind palette via @theme, so classes like
1174
- * text-foreground, bg-card, font-serif, duration-fast, text-display all work.
1175
- *
1176
- * This shell is deliberately MINIMAL: it wires the theme, tokens, and Tailwind,
1177
- * then renders \${children} in a bare container with no chrome, so you design the
1178
- * app's own layout. LAYOUT-REFERENCE.md (project root) is a complete worked
1179
- * 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.
1180
1151
  */
1181
-
1182
1152
  export default function RootLayout({ children }: { children: unknown }) {
1183
- // Read the in-flight request's CSP nonce so the theme-detection
1184
- // inline script below passes strict CSP (script-src 'nonce-...').
1185
- // Returns '' when no CSP nonce is set, in which case the attribute
1186
- // 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.
1187
1155
  const nonce = cspNonce();
1188
1156
  return html\`
1189
1157
  <script nonce="\${nonce}">
1190
- // ===== OPTIONAL: light/dark theme apparatus (remove as one unit) =====
1191
- // This IIFE reads the saved or OS theme and toggles the data-theme
1192
- // attribute plus the dark class the ui kit reads, so the token VALUES in
1193
- // the root, dark, and data-theme style blocks below switch. It is what
1194
- // makes the app theme-aware. Building a SINGLE-theme app of your own?
1195
- // Delete this IIFE, delete the dark and light style blocks below, and set
1196
- // your palette once on the root selector. That removes the wiring so it
1197
- // cannot fight your own colours (it will not override a plain root
1198
- // palette). The header-measure IIFE that follows is unrelated, keep it
1199
- // (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.
1200
1161
  (function(){
1201
1162
  try {
1202
1163
  var mq = window.matchMedia('(prefers-color-scheme: light)');
@@ -1206,9 +1167,6 @@ export default function RootLayout({ children }: { children: unknown }) {
1206
1167
  var el = document.documentElement;
1207
1168
  if (t === 'light' || t === 'dark') el.dataset.theme = t;
1208
1169
  else delete el.dataset.theme;
1209
- // Keep the .dark class the @webjsdev/ui kit uses in sync with the effective theme so the
1210
- // copied ui-* components (button, card, etc.) follow light/dark too.
1211
- // Dark is the default unless the OS prefers light or 'light' is set.
1212
1170
  var dark = t === 'dark' || (t !== 'light' && !mq.matches);
1213
1171
  el.classList.toggle('dark', dark);
1214
1172
  }
@@ -1216,14 +1174,10 @@ export default function RootLayout({ children }: { children: unknown }) {
1216
1174
  mq.addEventListener('change', apply);
1217
1175
  } catch (_) {}
1218
1176
  })();
1219
- // ===== end optional theme apparatus =====
1220
- // Header-measure script: DORMANT until you add a fixed header (the minimal
1221
- // shell has none). When you add one (see LAYOUT-REFERENCE.md), make it
1222
- // position:fixed (NOT sticky: a sticky header flickers on iOS WebKit during
1223
- // a client-router nav). fixed leaves normal flow, so --header-h reserves its
1224
- // height for the content below; this measures the real (responsive) height.
1225
- // With no header it is a no-op (querySelector returns null) and --header-h
1226
- // 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.
1227
1181
  (function(){
1228
1182
  function measure(){
1229
1183
  try {
@@ -1240,123 +1194,103 @@ export default function RootLayout({ children }: { children: unknown }) {
1240
1194
  else measure();
1241
1195
  })();
1242
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">
1243
1205
  <!-- Tailwind: a STATIC stylesheet compiled from public/input.css to
1244
1206
  public/tailwind.css by css:build (run automatically by the dev and start
1245
1207
  tasks; in dev it is also recompiled on request when a source changes, so
1246
1208
  it never goes stale). A real stylesheet, so the app is fully styled with
1247
1209
  JavaScript DISABLED (no in-browser compile). -->
1210
+
1248
1211
  <link rel="stylesheet" href="/public/tailwind.css">
1249
1212
  <style>
1250
- /* ONE theme, canonical shadcn-style tokens. These are the token VALUES as
1251
- plain CSS custom properties, so the palette resolves with JavaScript
1252
- DISABLED and needs no build. public/input.css holds the token STRUCTURE
1253
- and the @theme inline mappings that generate
1254
- bg-background, text-foreground, bg-card, bg-primary, bg-accent,
1255
- text-muted-foreground, border-border, ring-ring, and the rest. Here we
1256
- set those tokens' VALUES to this app's brand palette, so the ui-*
1257
- components AND your own chrome read ONE source of truth. Any component
1258
- added later with webjs ui add <name> inherits it automatically. The
1259
- stylesheet is linked before this block, so these VALUES win over the ui
1260
- theme defaults. Dark-first, with light via the theme toggle (data-theme) or the
1261
- OS. Follows the shadcn model: --primary is the BRAND color (orange, used
1262
- for primary buttons, links, and emphasis), while --accent stays a NEUTRAL
1263
- hover tint so the ui kit's outline/ghost/dropdown hover states keep proper
1264
- contrast. Use bg-primary / text-primary for brand, bg-accent for hovers.
1265
- Add a new design token the canonical way, a --x VALUE below plus a
1266
- --color-x: var(--x) line in the @theme inline block in public/input.css,
1267
- then use it as bg-x / text-x. Reach for opacity modifiers
1268
- (bg-primary/10, hover:bg-primary/90, border-border/60) before inventing a
1269
- new token, but keep body text at full opacity so it stays above the AA
1270
- 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. */
1271
1218
  :root {
1272
- --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1219
+ --font-sans: 'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
1273
1220
  --font-serif: ui-serif, 'Iowan Old Style', Palatino, Georgia, serif;
1274
- --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
1275
- /* 0 by default because the minimal shell has no fixed header. The
1276
- header-measure script below overrides it to the real height the moment
1277
- you add a fixed header element (see LAYOUT-REFERENCE.md), so body
1278
- 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;
1279
1223
  --header-h: 0px;
1280
- /* A translucent brand tint, derived from --primary so it tracks
1281
- light/dark automatically. Used for the logo glow and focus ring. */
1282
- --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);
1283
1227
  }
1284
- /* 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. */
1285
1228
  /* dark (the default, and the explicit .dark the toggle sets) */
1286
1229
  :root, .dark {
1287
1230
  color-scheme: dark;
1288
- --background: oklch(0.14 0.01 55);
1289
- --foreground: oklch(0.96 0.015 60);
1290
- --card: oklch(0.18 0.01 55);
1291
- --card-foreground: oklch(0.96 0.015 60);
1292
- --popover: oklch(0.18 0.01 55);
1293
- --popover-foreground: oklch(0.96 0.015 60);
1294
- /* --primary is the orange BRAND color (shadcn model: primary = brand). */
1295
- --primary: oklch(0.7 0.16 52);
1296
- --primary-foreground: oklch(0.17 0.02 52);
1297
- --secondary: oklch(0.22 0.01 55);
1298
- --secondary-foreground: oklch(0.96 0.015 60);
1299
- --muted: oklch(0.16 0.01 55);
1300
- --muted-foreground: oklch(0.72 0.02 60);
1301
- /* --accent stays a NEUTRAL hover tint (shadcn model), so the ui kit's
1302
- outline/ghost/dropdown hover states keep proper contrast. */
1303
- --accent: oklch(0.27 0.008 55);
1304
- --accent-foreground: oklch(0.96 0.015 60);
1305
- --border: oklch(0.26 0.012 55 / 0.9);
1306
- --border-strong: oklch(0.38 0.012 55 / 0.9);
1307
- --input: oklch(0.26 0.012 55 / 0.9);
1308
- --ring: oklch(0.7 0.16 52);
1309
- --logo-from: oklch(0.8 0.16 58);
1310
- --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;
1311
1249
  }
1312
1250
  /* light (explicit via the toggle) */
1313
1251
  :root[data-theme='light'] {
1314
1252
  color-scheme: light;
1315
- --background: oklch(0.985 0.008 80);
1316
- --foreground: oklch(0.18 0.015 60);
1317
- --card: oklch(1 0 0);
1318
- --card-foreground: oklch(0.18 0.015 60);
1319
- --popover: oklch(1 0 0);
1320
- --popover-foreground: oklch(0.18 0.015 60);
1321
- --primary: oklch(0.54 0.16 52);
1322
- --primary-foreground: oklch(1 0 0);
1323
- --secondary: oklch(0.96 0.008 80);
1324
- --secondary-foreground: oklch(0.18 0.015 60);
1325
- --muted: oklch(0.96 0.008 80);
1326
- --muted-foreground: oklch(0.42 0.02 65);
1327
- --accent: oklch(0.955 0.006 80);
1328
- --accent-foreground: oklch(0.18 0.015 60);
1329
- --border: oklch(0.88 0.01 75 / 0.95);
1330
- --border-strong: oklch(0.78 0.01 75 / 0.95);
1331
- --input: oklch(0.88 0.01 75 / 0.95);
1332
- --ring: oklch(0.54 0.16 52);
1333
- --logo-from: oklch(0.63 0.17 50);
1334
- --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;
1335
1271
  }
1336
1272
  /* light (OS preference, when the user has made no explicit choice) */
1337
1273
  @media (prefers-color-scheme: light) {
1338
1274
  :root:not(.dark):not([data-theme='dark']) {
1339
1275
  color-scheme: light;
1340
- --background: oklch(0.985 0.008 80);
1341
- --foreground: oklch(0.18 0.015 60);
1342
- --card: oklch(1 0 0);
1343
- --card-foreground: oklch(0.18 0.015 60);
1344
- --popover: oklch(1 0 0);
1345
- --popover-foreground: oklch(0.18 0.015 60);
1346
- --primary: oklch(0.54 0.16 52);
1347
- --primary-foreground: oklch(1 0 0);
1348
- --secondary: oklch(0.96 0.008 80);
1349
- --secondary-foreground: oklch(0.18 0.015 60);
1350
- --muted: oklch(0.96 0.008 80);
1351
- --muted-foreground: oklch(0.42 0.02 65);
1352
- --accent: oklch(0.955 0.006 80);
1353
- --accent-foreground: oklch(0.18 0.015 60);
1354
- --border: oklch(0.88 0.01 75 / 0.95);
1355
- --border-strong: oklch(0.78 0.01 75 / 0.95);
1356
- --input: oklch(0.88 0.01 75 / 0.95);
1357
- --ring: oklch(0.54 0.16 52);
1358
- --logo-from: oklch(0.63 0.17 50);
1359
- --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;
1360
1294
  }
1361
1295
  }
1362
1296
  </style>
@@ -1367,13 +1301,11 @@ export default function RootLayout({ children }: { children: unknown }) {
1367
1301
  padding-top: var(--header-h);
1368
1302
  background: var(--background);
1369
1303
  color: var(--foreground);
1370
- font: 16px/1.65 var(--font-sans);
1304
+ font: 15px/1.6 var(--font-sans);
1371
1305
  -webkit-font-smoothing: antialiased;
1306
+ -moz-osx-font-smoothing: grayscale;
1372
1307
  }
1373
- ::selection { background: color-mix(in oklch, var(--primary) 22%, transparent); color: var(--foreground); }
1374
1308
  </style>
1375
-
1376
- <!-- 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. -->
1377
1309
  <main class="min-h-dvh px-4 sm:px-6 py-8">
1378
1310
  \${children}
1379
1311
  </main>
@@ -1381,31 +1313,26 @@ export default function RootLayout({ children }: { children: unknown }) {
1381
1313
  }
1382
1314
  `);
1383
1315
 
1384
- // The gallery-index home (links every feature demo + the example app) ships
1385
- // only in the full-stack scaffold, which is the only one that ships the
1386
- // gallery. saas gets its own landing below.
1387
- if (isFullStack) {
1388
- 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.
1389
- import { html } from '@webjsdev/core';
1390
- import { rubric, displayH1, accentLink } from '#lib/utils/ui.ts';
1391
- import { buttonClass } from '#components/ui/button.ts';
1392
- import { badgeClass } from '#components/ui/badge.ts';
1393
- import {
1394
- cardClass,
1395
- cardHeaderClass,
1396
- cardTitleClass,
1397
- cardDescriptionClass,
1398
- } 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';
1399
1326
 
1400
1327
  export const metadata = {
1401
- title: '${displayName}: built with webjs',
1328
+ title: '${displayName}',
1402
1329
  };
1403
1330
 
1404
- // Two kinds of reference the scaffold ships. FEATURES are single-concept demos
1405
- // (one WebJs feature each, under app/features/, logic in modules/). EXAMPLES are
1406
- // whole apps that compose several features (under app/examples/). Prune what you
1407
- // do not use (delete the route AND its modules/<name>), then reshape this page.
1408
- 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 = [
1409
1336
  { href: '/features/routing', title: 'Routing', blurb: 'A static route plus a dynamic [id] segment that reads params. The file-based router in miniature.' },
1410
1337
  { href: '/features/boundaries', title: 'Boundaries', blurb: 'The control-flow throws (forbidden / unauthorized / notFound) and the nearest boundary file that catches each.' },
1411
1338
  { href: '/features/components', title: 'Components', blurb: 'The WebComponent factory, reactive props, instance signals, and slot projection in light DOM.' },
@@ -1413,7 +1340,7 @@ const features = [
1413
1340
  { href: '/features/optimistic-ui', title: 'Optimistic UI', blurb: 'The imperative optimistic(signal, value, action) flip: instant update, automatic rollback on failure.' },
1414
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.' },
1415
1342
  { href: '/features/directives', title: 'Directives', blurb: 'The lit-html directive set: repeat for keyed lists, watch(signal) for a fine-grained node swap.' },
1416
- { 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.' },
1417
1344
  { href: '/features/forms', title: 'Forms', blurb: 'A no-JS progressive-enhancement form posting to the page action, with server-side validation errors.' },
1418
1345
  { href: '/features/metadata', title: 'Metadata', blurb: 'Static metadata plus generateMetadata(ctx), which reads the request to compute the title and Open Graph tags.' },
1419
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.' },
@@ -1426,162 +1353,79 @@ const features = [
1426
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.' },
1427
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.' },
1428
1355
  ];
1429
- const examples = [
1356
+ const EXAMPLES = [
1430
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.' },
1431
1358
  ];
1432
1359
 
1433
- const galleryCard = (item: { href: string; title: string; blurb: string }) => html\`
1434
- <a href=\${item.href} class="block no-underline">
1435
- <div class="\${cardClass()} h-full transition-colors hover:border-border-strong">
1436
- <div class=\${cardHeaderClass()}>
1437
- <h3 class=\${cardTitleClass()}>\${item.title}</h3>
1438
- <p class=\${cardDescriptionClass()}>\${item.blurb}</p>
1439
- </div>
1440
- </div>
1441
- </a>
1442
- \`;
1443
-
1444
1360
  export default function Home() {
1445
1361
  return html\`
1446
- <section class="mb-14">
1447
- \${rubric('welcome')}
1448
- \${displayH1(html\`Hello from <span class="text-primary italic">${displayName}</span>.\`)}
1449
- <p class="text-lede leading-[1.5] text-muted-foreground max-w-[56ch] m-0 mb-6">
1450
- This WebJs scaffold ships a gallery below: single-feature demos and one
1451
- whole example app, all small, idiomatic, and heavily commented. Browse
1452
- them for context, then replace this page with your own. See
1453
- \${accentLink('https://docs.webjs.dev', 'the docs')} for the full reference.
1454
- </p>
1455
- <div class="flex gap-3 items-center">
1456
- <a href="/examples/todo" class=\${buttonClass()}>Open the todo app</a>
1457
- <span class=\${badgeClass({ variant: 'secondary' })}>v0.1</span>
1458
- </div>
1459
- </section>
1460
-
1461
- <section class="mb-12">
1462
- <h2 class="font-serif text-[1.6rem] tracking-[-0.02em] font-bold m-0 mb-1">Features</h2>
1463
- <p class="text-muted-foreground text-sm m-0 mb-5">
1464
- One webjs concept each, under <code class="font-mono text-[0.9em]">app/features/</code>
1465
- with logic in <code class="font-mono text-[0.9em]">modules/</code>. Delete the ones you do not need.
1466
- </p>
1467
- <div class="grid gap-4 sm:grid-cols-2">
1468
- \${features.map(galleryCard)}
1469
- </div>
1470
- </section>
1471
-
1472
- <section>
1473
- <h2 class="font-serif text-[1.6rem] tracking-[-0.02em] font-bold m-0 mb-1">Example apps</h2>
1474
- <p class="text-muted-foreground text-sm m-0 mb-5">
1475
- Whole apps that compose several features, under <code class="font-mono text-[0.9em]">app/examples/</code>.
1476
- </p>
1477
- <div class="grid gap-4 sm:grid-cols-2">
1478
- \${examples.map(galleryCard)}
1479
- </div>
1480
- </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>
1481
1419
  \`;
1482
1420
  }
1483
- `);
1484
- } else {
1485
- // saas home: the auth landing (hero + login/signup/dashboard) stays the
1486
- // headline, and the WebJs feature gallery sits BELOW it, so a saas app is
1487
- // also a learning surface. Keep the `features` list in sync with the
1488
- // full-stack home above (both ship the same gallery).
1489
- 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.
1490
- import { html } from '@webjsdev/core';
1491
- import { rubric, displayH1, accentLink } from '#lib/utils/ui.ts';
1492
- import {
1493
- cardClass,
1494
- cardHeaderClass,
1495
- cardTitleClass,
1496
- cardDescriptionClass,
1497
- } from '#components/ui/card.ts';
1498
-
1499
- export const metadata = {
1500
- title: '${displayName}: built with webjs',
1501
- };
1502
1421
 
1503
- // The WebJs feature gallery, shown below the auth landing. FEATURES are
1504
- // single-concept demos (under app/features/, logic in modules/); EXAMPLES are
1505
- // whole apps (under app/examples/). Prune what you do not use (delete the route
1506
- // AND its modules/<name>). Keep this list in sync with the full-stack home.
1507
- const features = [
1508
- { href: '/features/routing', title: 'Routing', blurb: 'A static route plus a dynamic [id] segment that reads params. The file-based router in miniature.' },
1509
- { href: '/features/boundaries', title: 'Boundaries', blurb: 'The control-flow throws (forbidden / unauthorized / notFound) and the nearest boundary file that catches each.' },
1510
- { href: '/features/components', title: 'Components', blurb: 'The WebComponent factory, reactive props, instance signals, and slot projection in light DOM.' },
1511
- { 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.' },
1512
- { href: '/features/optimistic-ui', title: 'Optimistic UI', blurb: 'The imperative optimistic(signal, value, action) flip: instant update, automatic rollback on failure.' },
1513
- { 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.' },
1514
- { href: '/features/directives', title: 'Directives', blurb: 'The lit-html directive set: repeat for keyed lists, watch(signal) for a fine-grained node swap.' },
1515
- { 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.' },
1516
- { href: '/features/forms', title: 'Forms', blurb: 'A no-JS progressive-enhancement form posting to the page action, with server-side validation errors.' },
1517
- { href: '/features/metadata', title: 'Metadata', blurb: 'Static metadata plus generateMetadata(ctx), which reads the request to compute the title and Open Graph tags.' },
1518
- { 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.' },
1519
- { href: '/features/env', title: 'Env vars', blurb: 'The server-only vs WEBJS_PUBLIC_ boundary, read during SSR so secrets never reach the browser.' },
1520
- { href: '/features/client-router', title: 'Client router', blurb: 'Automatic soft navigation: fragment-only fetches, hover prefetch, scroll restore, and graceful no-JS fallback.' },
1521
- { 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).' },
1522
- { href: '/features/websockets', title: 'WebSockets', blurb: 'A WS(ws, req) route endpoint plus the connectWS() client, echoing messages over a live socket.' },
1523
- { 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.' },
1524
- { href: '/features/rate-limit', title: 'Rate limiting', blurb: 'The rateLimit() middleware scoped to one endpoint, returning a 429 with Retry-After past the window.' },
1525
- { href: '/features/file-storage', title: 'File storage', blurb: 'A no-JS multipart upload streamed into the FileStore, then served back through a streaming route.' },
1526
- { 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.' },
1527
- ];
1528
- const examples = [
1529
- { 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.' },
1530
- ];
1531
-
1532
- const galleryCard = (item: { href: string; title: string; blurb: string }) => html\`
1533
- <a href=\${item.href} class="block no-underline">
1534
- <div class="\${cardClass()} h-full transition-colors hover:border-border-strong">
1535
- <div class=\${cardHeaderClass()}>
1536
- <h3 class=\${cardTitleClass()}>\${item.title}</h3>
1537
- <p class=\${cardDescriptionClass()}>\${item.blurb}</p>
1538
- </div>
1539
- </div>
1540
- </a>
1541
- \`;
1542
-
1543
- export default function Home() {
1544
- return html\`
1545
- <section class="mb-14">
1546
- \${rubric('welcome')}
1547
- \${displayH1(html\`Hello from <span class="text-primary italic">${displayName}</span>.\`)}
1548
- <p class="text-lede leading-[1.5] text-muted-foreground max-w-[56ch] m-0 mb-6">
1549
- The saas starter: email and password auth, a protected dashboard, and a
1550
- User model, all wired up. Replace this hero with your product's landing;
1551
- the webjs feature gallery below is reference, prune what you do not need.
1552
- See \${accentLink('https://docs.webjs.dev', 'the docs')} for the full reference.
1553
- </p>
1554
- <div class="flex gap-3 items-center">
1555
- <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>
1556
- <a href="/signup" class="text-primary no-underline font-medium text-sm">Create an account</a>
1557
- <a href="/dashboard" class="text-muted-foreground no-underline font-medium text-sm hover:text-foreground transition-colors">Dashboard</a>
1558
- </div>
1559
- </section>
1560
-
1561
- <section class="mb-12">
1562
- <h2 class="font-serif text-[1.6rem] tracking-[-0.02em] font-bold m-0 mb-1">Features</h2>
1563
- <p class="text-muted-foreground text-sm m-0 mb-5">
1564
- One webjs concept each, under <code class="font-mono text-[0.9em]">app/features/</code>
1565
- with logic in <code class="font-mono text-[0.9em]">modules/</code>. Delete the ones you do not need.
1566
- </p>
1567
- <div class="grid gap-4 sm:grid-cols-2">
1568
- \${features.map(galleryCard)}
1569
- </div>
1570
- </section>
1571
-
1572
- <section>
1573
- <h2 class="font-serif text-[1.6rem] tracking-[-0.02em] font-bold m-0 mb-1">Example apps</h2>
1574
- <p class="text-muted-foreground text-sm m-0 mb-5">
1575
- Whole apps that compose several features, under <code class="font-mono text-[0.9em]">app/examples/</code>.
1576
- </p>
1577
- <div class="grid gap-4 sm:grid-cols-2">
1578
- \${examples.map(galleryCard)}
1579
- </div>
1580
- </section>
1581
- \`;
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>\`;
1582
1427
  }
1583
1428
  `);
1584
- }
1585
1429
 
1586
1430
  // AGENTS.md is copied via the `templateFiles` loop above, from
1587
1431
  // `packages/cli/templates/AGENTS.md` with `{{APP_NAME}}` substitution.
@@ -1668,90 +1512,49 @@ ThemeToggle.register('theme-toggle');
1668
1512
  const { execSync } = await import('node:child_process');
1669
1513
  try {
1670
1514
  execSync('git init', { cwd: appDir, stdio: 'pipe' });
1671
- // 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).
1672
1516
  execSync('git config core.hooksPath .hooks', { cwd: appDir, stdio: 'pipe' });
1673
1517
  } catch { /* git not available: skip */ }
1674
1518
 
1675
1519
  // --- Print success ---
1676
1520
 
1521
+ const guide = 'AGENTS.md, .agents/skills/webjs/ ← the agent guide';
1677
1522
  if (isApi) {
1678
1523
  console.log(` ${name}/
1679
- app/api/health/route.ts
1680
- app/api/users/route.ts ← thin wrapper over server actions
1681
- modules/users/{actions,queries,types.ts}
1682
- 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}
1683
1528
  `);
1684
1529
  } else if (isSaas) {
1685
1530
  console.log(` ${name}/
1686
- app/layout.ts, page.ts, login/, signup/
1531
+ app/{layout,page}.ts, login/, signup/
1687
1532
  app/dashboard/{page,settings,middleware}.ts ← protected
1688
- app/api/auth/[...path]/route.ts ← auth API
1689
- styles/globals.css ← @webjsdev/ui theme tokens
1690
- components.json ← preconfigured for \`webjs ui add\`
1691
- components/ui/{button,card,alert,badge,separator,label,input,
1692
- dialog,form,field,switch,checkbox}.ts
1693
- components/theme-toggle.ts
1694
- modules/auth/{actions,queries,types.ts}
1695
- lib/{auth,password}.server.ts
1696
- lib/utils/cn.ts ← cn() helper for ui-* components
1697
- db/{schema,columns,connection}.server.ts ← Drizzle (User model)
1698
- 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}
1699
1538
  `);
1700
1539
  } else {
1701
1540
  console.log(` ${name}/
1702
- app/layout.ts, page.ts ← home links to the gallery
1703
- app/features/{routing,components,server-actions,optimistic-ui,
1704
- async-render,directives,route-handler}/
1705
- ← single-feature demos
1706
- app/examples/todo/ ← one whole example app (composes features)
1707
- styles/globals.css ← @webjsdev/ui theme tokens
1708
- components.json ← preconfigured for \`webjs ui add\`
1709
- components/ui/{button,card,alert,badge,separator,label,input}.ts
1710
- components/theme-toggle.ts ← light DOM web component
1711
- lib/utils/cn.ts ← cn() helper for ui-* components
1712
- lib/utils/ui.ts ← Tailwind class-bundle helpers
1713
- public/input.css ← Tailwind entry (compiled to public/tailwind.css)
1714
- modules/{components,server-actions,optimistic-ui,async-render,
1715
- directives,todo}/ ← feature + example logic (prune what you skip)
1716
- db/{schema,columns,connection}.server.ts ← Drizzle (User + Todo)
1717
- 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}
1718
1546
  `);
1719
1547
  }
1720
- // AI-agent guidance comes first so it scrolls past in the terminal
1721
- // (long reading material, not actionable). The actionable run
1722
- // command lands LAST in the output so it is the final thing on
1723
- // screen after the install completes.
1724
- console.log(`AI-driven development (enforced for all AI agents):
1725
- ✓ Tests auto-generated with every feature
1726
- ✓ Docs auto-updated with every change
1727
- ✓ Git merges/pushes to main require approval
1728
- ✓ Commits are automatic, small, and meaningful
1729
- ✓ No AI attribution in commit messages
1730
- ✓ Convention validation via \`webjs check\`
1731
-
1732
- For AI agents, read this before editing scaffolded files:
1733
- • The example layout, home page, the gallery under app/features/ and
1734
- app/examples/ (+ their modules) and schema are REFERENCE ONLY. Replace
1735
- them with the app the user asked for, and prune the routes you do not
1736
- use (delete the app/features/<name> or app/examples/<name> route AND
1737
- its modules/<name>). Don't ship the scaffold's examples as the final
1738
- product.
1739
- • This fresh app intentionally FAILS \`webjs check\` with
1740
- no-scaffold-placeholder violations on the example content (the home
1741
- page, the layout, and each gallery route). That is the signal to
1742
- replace or prune each. Delete the marker comment line as you do, and
1743
- the check goes green.
1744
- • Use Drizzle + SQLite for app data. It's already wired up. Define
1745
- real models in db/schema.server.ts, then run \`webjs db generate\`
1746
- and \`webjs db migrate\`. NEVER store app data in JSON files,
1747
- in-memory arrays, or localStorage as a substitute for the database.
1748
- • Only three scaffolds exist: full-stack (default), api, saas. Don't
1749
- invent template names. If you need a different kind of app, pick
1750
- the closest scaffold and adapt it.
1751
- • Read AGENTS.md + CONVENTIONS.md in the new project before writing
1752
- any code. They are the contract.
1753
- • Need more detail? Full hosted docs are at https://docs.webjs.dev
1754
- (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.
1755
1558
  `);
1756
1559
 
1757
1560
  // Auto-install (default). Detect the package manager from the env so