@webjsdev/cli 0.10.44 → 0.10.46

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 (111) hide show
  1. package/README.md +2 -3
  2. package/bin/webjs.js +18 -18
  3. package/lib/api-gallery.js +1 -1
  4. package/lib/create.js +154 -243
  5. package/package.json +1 -1
  6. package/templates/.agents/rules/workflow.md +7 -3
  7. package/templates/.agents/skills/webjs/SKILL.md +4 -2
  8. package/templates/.agents/skills/webjs/references/auth-and-sessions.md +78 -16
  9. package/templates/.agents/skills/webjs/references/built-ins.md +16 -2
  10. package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +38 -6
  11. package/templates/.agents/skills/webjs/references/components.md +101 -6
  12. package/templates/.agents/skills/webjs/references/data-and-actions.md +19 -2
  13. package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +5 -1
  14. package/templates/.agents/skills/webjs/references/optimistic-ui.md +18 -0
  15. package/templates/.agents/skills/webjs/references/routing-and-pages.md +25 -2
  16. package/templates/.agents/skills/webjs/references/runtime.md +1 -1
  17. package/templates/.agents/skills/webjs/references/service-worker.md +1 -1
  18. package/templates/.agents/skills/webjs/references/styling.md +82 -2
  19. package/templates/AGENTS.md +12 -5
  20. package/templates/gallery/app/api/auth/[...path]/route.ts +7 -0
  21. package/templates/gallery/app/apple-icon.ts +2 -5
  22. package/templates/gallery/app/examples/layout.ts +16 -0
  23. package/templates/gallery/app/examples/todo/page.ts +2 -1
  24. package/templates/gallery/app/features/async-render/page.ts +3 -2
  25. package/templates/gallery/app/features/auth/dashboard/layout.ts +21 -0
  26. package/templates/gallery/app/features/auth/dashboard/middleware.ts +14 -0
  27. package/templates/gallery/app/features/auth/dashboard/page.ts +20 -0
  28. package/templates/gallery/app/features/auth/dashboard/settings/page.ts +22 -0
  29. package/templates/gallery/app/features/auth/login/middleware.ts +15 -0
  30. package/templates/gallery/app/features/auth/login/page.ts +43 -0
  31. package/templates/gallery/app/features/auth/page.ts +34 -0
  32. package/templates/gallery/app/features/auth/signup/middleware.ts +11 -0
  33. package/templates/gallery/app/features/auth/signup/page.ts +61 -0
  34. package/templates/gallery/app/features/boundaries/error.ts +5 -4
  35. package/templates/gallery/app/features/boundaries/gated/forbidden.ts +5 -4
  36. package/templates/gallery/app/features/boundaries/not-found.ts +5 -4
  37. package/templates/gallery/app/features/boundaries/page.ts +11 -10
  38. package/templates/gallery/app/features/boundaries/private/unauthorized.ts +5 -4
  39. package/templates/gallery/app/features/broadcast/page.ts +4 -3
  40. package/templates/gallery/app/features/caching/page.ts +5 -4
  41. package/templates/gallery/app/features/client-router/page.ts +7 -5
  42. package/templates/gallery/app/features/client-router/second/page.ts +4 -3
  43. package/templates/gallery/app/features/components/page.ts +3 -2
  44. package/templates/gallery/app/features/directives/page.ts +3 -2
  45. package/templates/gallery/app/features/env/page.ts +4 -3
  46. package/templates/gallery/app/features/file-storage/page.ts +8 -5
  47. package/templates/gallery/app/features/forms/page.ts +10 -6
  48. package/templates/gallery/app/features/frames/page.ts +26 -10
  49. package/templates/gallery/app/features/layout.ts +67 -0
  50. package/templates/gallery/app/features/metadata/page.ts +7 -6
  51. package/templates/gallery/app/features/optimistic-ui/page.ts +3 -2
  52. package/templates/gallery/app/features/rate-limit/page.ts +6 -5
  53. package/templates/gallery/app/features/route-handler/page.ts +4 -3
  54. package/templates/gallery/app/features/routing/[id]/page.ts +7 -6
  55. package/templates/gallery/app/features/routing/page.ts +10 -9
  56. package/templates/gallery/app/features/server-actions/page.ts +7 -4
  57. package/templates/gallery/app/features/service-worker/page.ts +4 -3
  58. package/templates/gallery/app/features/sessions/page.ts +5 -4
  59. package/templates/gallery/app/features/stream/page.ts +46 -0
  60. package/templates/gallery/app/features/streaming/page.ts +32 -0
  61. package/templates/gallery/app/features/suspense/page.ts +35 -0
  62. package/templates/gallery/app/features/view-transitions/page.ts +44 -0
  63. package/templates/gallery/app/features/view-transitions/second/page.ts +30 -0
  64. package/templates/gallery/app/features/websockets/page.ts +4 -3
  65. package/templates/gallery/app/global-error.ts +2 -5
  66. package/templates/gallery/app/global-not-found.ts +4 -6
  67. package/templates/gallery/app/icon.ts +2 -5
  68. package/templates/gallery/app/manifest.ts +1 -4
  69. package/templates/gallery/app/opengraph-image.ts +3 -6
  70. package/templates/gallery/app/robots.ts +0 -3
  71. package/templates/gallery/app/sitemap.ts +0 -3
  72. package/templates/gallery/app/twitter-image.ts +3 -6
  73. package/templates/gallery/components/ui/badge.ts +41 -0
  74. package/templates/gallery/components/ui/button.ts +86 -0
  75. package/templates/gallery/components/ui/card.ts +36 -0
  76. package/templates/gallery/components/ui/input.ts +50 -0
  77. package/templates/gallery/lib/utils/ui.ts +31 -0
  78. package/templates/gallery/modules/auth/actions/signup.server.ts +19 -0
  79. package/templates/gallery/modules/auth/auth.server.ts +53 -0
  80. package/templates/gallery/modules/auth/password.server.ts +20 -0
  81. package/templates/gallery/modules/auth/queries/current-user.server.ts +12 -0
  82. package/templates/gallery/modules/auth/types.ts +9 -0
  83. package/templates/gallery/modules/broadcast/components/broadcast-feed.ts +4 -2
  84. package/templates/gallery/modules/caching/components/cache-buster.ts +2 -1
  85. package/templates/gallery/modules/client-router/components/router-controls.ts +4 -3
  86. package/templates/gallery/modules/components/components/counter-card.ts +4 -2
  87. package/templates/gallery/modules/components/components/reactive-meter.ts +9 -1
  88. package/templates/gallery/modules/components/components/task-loader.ts +3 -2
  89. package/templates/gallery/modules/components/components/theme-context.ts +5 -3
  90. package/templates/gallery/modules/directives/components/directive-demo.ts +17 -10
  91. package/templates/gallery/modules/gallery/components/gallery-nav.ts +54 -0
  92. package/templates/gallery/modules/gallery/nav.ts +79 -0
  93. package/templates/gallery/modules/optimistic-ui/components/like-button.ts +19 -1
  94. package/templates/gallery/modules/rate-limit/components/rate-probe.ts +2 -1
  95. package/templates/gallery/modules/route-handler/components/rich-data.ts +2 -1
  96. package/templates/gallery/modules/server-actions/actions/greet.server.ts +2 -2
  97. package/templates/gallery/modules/server-actions/actions/greet.test.ts +39 -36
  98. package/templates/gallery/modules/server-actions/components/greeter.ts +11 -11
  99. package/templates/gallery/modules/server-actions/middleware/require-auth.server.ts +14 -9
  100. package/templates/gallery/modules/stream/components/stream-demo.ts +81 -0
  101. package/templates/gallery/modules/streaming/actions/stream-tokens.server.ts +17 -0
  102. package/templates/gallery/modules/streaming/components/token-stream.ts +53 -0
  103. package/templates/gallery/modules/suspense/components/slow-fact.ts +20 -0
  104. package/templates/gallery/modules/todo/components/todo-app.ts +8 -4
  105. package/templates/gallery/modules/websockets/components/ws-echo.ts +5 -3
  106. package/templates/gallery/test/auth/auth.test.ts +81 -0
  107. package/templates/public/favicon.svg +10 -3
  108. package/templates/scripts/clear-api-gallery.mjs +55 -0
  109. package/templates/scripts/clear-gallery.mjs +143 -24
  110. package/lib/lean-copy.js +0 -43
  111. package/lib/saas-template.js +0 -568
package/lib/create.js CHANGED
@@ -18,7 +18,6 @@ import { existsSync } from 'node:fs';
18
18
  import { createRequire } from 'node:module';
19
19
  import { spawnSync } from 'node:child_process';
20
20
  import { bunifyProse, bunifyDockerfile, bunifyCompose, bunifyCi } from './runtime-rewrite.js';
21
- import { leanComponentSource } from './lean-copy.js';
22
21
 
23
22
  /**
24
23
  * Detect which package manager invoked us. Reads `npm_config_user_agent`,
@@ -106,60 +105,6 @@ function resolveUiRegistryRoot() {
106
105
  }
107
106
  const UI_REGISTRY_ROOT = resolveUiRegistryRoot();
108
107
 
109
- /**
110
- * Read a single @webjsdev/ui registry component, rewrite its relative import
111
- * of `../lib/utils.ts` to the scaffolded app's aliased path so it resolves
112
- * when written to `components/ui/<name>.ts`. The scaffold puts cn() at
113
- * `lib/utils/cn.ts` (folder-grouped with other browser-safe helpers), so the
114
- * alias form is `#lib/utils/cn.ts` (#555/#556).
115
- *
116
- * @param {string} name component name without `.ts` (e.g. 'button')
117
- * @returns {Promise<string>} source with import rewritten
118
- */
119
- async function readUiComponent(name) {
120
- const src = join(UI_REGISTRY_ROOT, 'components', `${name}.ts`);
121
- const raw = await readFile(src, 'utf8');
122
- // The registry component imports cn() via a relative `../lib/utils.ts`; rewrite
123
- // it to the scaffolded app's aliased path (cn lives at lib/utils/cn.ts).
124
- const rewritten = raw
125
- .replaceAll("'../lib/utils.ts'", "'#lib/utils/cn.ts'")
126
- .replaceAll('"../lib/utils.ts"', '"#lib/utils/cn.ts"')
127
- // onBeforeCache lives in its own client-only module so cn() stays pure (#819).
128
- .replaceAll("'../lib/dom.ts'", "'#lib/utils/dom.ts'")
129
- .replaceAll('"../lib/dom.ts"', '"#lib/utils/dom.ts"');
130
- // Strip the worked @example from a Tier-1 helper (same as `webjs ui add`), so
131
- // the scaffolded component is lean and the example is served on demand. The
132
- // shared helper is used by the saas-template copier too, so they cannot drift.
133
- return leanComponentSource(rewritten, name);
134
- }
135
-
136
- /**
137
- * Copy a list of @webjsdev/ui registry components into the scaffolded app
138
- * under `components/ui/`. Throws if any name is missing from the registry,
139
- * since the scaffold's generated pages import these by name and a missing
140
- * file would produce ERR_MODULE_NOT_FOUND at first request. Caller must
141
- * have already invoked assertUiRegistryAvailable().
142
- *
143
- * @param {string} appDir destination app root
144
- * @param {string[]} names list of component file basenames (without `.ts`)
145
- */
146
- async function copyUiComponents(appDir, names) {
147
- const uiDir = join(appDir, 'components', 'ui');
148
- await mkdir(uiDir, { recursive: true });
149
- for (const n of names) {
150
- const src = join(UI_REGISTRY_ROOT, 'components', `${n}.ts`);
151
- if (!existsSync(src)) {
152
- throw new Error(
153
- `@webjsdev/ui registry is missing component '${n}.ts' at ${src}. ` +
154
- `The scaffold's example pages import this component by name. ` +
155
- `Either the registry was published incompletely or the scaffold's ` +
156
- `component list is out of sync with the registry.`,
157
- );
158
- }
159
- await writeFile(join(uiDir, `${n}.ts`), await readUiComponent(n));
160
- }
161
- }
162
-
163
108
  /**
164
109
  * Copy the example gallery (idiomatic, densely-commented working examples) into
165
110
  * the scaffolded app. Merges `templates/gallery/{app,modules}` over the app so
@@ -177,7 +122,18 @@ async function copyUiComponents(appDir, names) {
177
122
  */
178
123
  async function copyGallery(appDir) {
179
124
  const galleryDir = join(TEMPLATES, 'gallery');
180
- for (const sub of ['app', 'modules']) {
125
+ // `test` carries the auth card's real request-pipeline test (test/auth); it
126
+ // ships with the gallery and is pruned by gallery:clear alongside the card.
127
+ // `components` carries the gallery's EXAMPLE design system (components/ui/ class
128
+ // helpers) the demos import. It ships here so a fresh app's demos work, but it
129
+ // is an example to learn from, so gallery:clear REMOVES it (the agent then runs
130
+ // `webjs ui add` and themes its own components/ui/); only cn.ts is kept as the
131
+ // `webjs ui add` prerequisite. It merges into the app's components/ alongside
132
+ // the separately-written theme toggle (also removed by gallery:clear).
133
+ // `lib` merges the gallery's markup-chunk helpers (lib/utils/ui.ts) alongside
134
+ // the ui bootstrap's cn.ts/dom.ts (written earlier); gallery:clear removes just
135
+ // ui.ts. cp is recursive-merge, so the pre-written lib/utils/ files are kept.
136
+ for (const sub of ['app', 'modules', 'test', 'components', 'lib']) {
181
137
  await cp(join(galleryDir, sub), join(appDir, sub), { recursive: true });
182
138
  }
183
139
  }
@@ -312,21 +268,19 @@ export async function scaffoldApp(name, cwd, opts = {}) {
312
268
  const shouldInstall = opts.install === true;
313
269
  // Defence in depth. The CLI already validates this, but library
314
270
  // callers (tests, programmatic use) might pass anything.
315
- const VALID_TEMPLATES = ['full-stack', 'api', 'saas'];
271
+ const VALID_TEMPLATES = ['full-stack', 'api'];
316
272
  if (!VALID_TEMPLATES.includes(template)) {
317
273
  throw new Error(
318
274
  `Unknown template '${template}'. Only ${VALID_TEMPLATES.join(' / ')} exist.`,
319
275
  );
320
276
  }
321
277
  const isApi = template === 'api';
322
- const isSaas = template === 'saas';
323
- // The example gallery ships in every UI scaffold (full-stack AND saas). The
324
- // copyGallery gate below is !isApi, since only the api template has no UI. saas
325
- // overwrites db/schema.server.ts with its own schema (which includes the
326
- // gallery's todos table) and renders the gallery below its auth landing.
327
- // isFullStack distinguishes the plain full-stack app from saas for the parts
328
- // that differ (its own home page and the create.js-written schema).
329
- const isFullStack = !isApi && !isSaas;
278
+ // The example gallery ships in the one UI template (not api, which has no UI),
279
+ // so the copyGallery gate below is !isApi. Auth is one of the gallery cards
280
+ // (app/features/auth + modules/auth), so a UI app ships a real, prunable auth
281
+ // baseline. `isFullStack` names the UI template for the parts that differ from
282
+ // api (the todos table + the passwordHash column the auth card needs).
283
+ const isFullStack = !isApi;
330
284
 
331
285
  // Database dialect (#563): sqlite (default) or postgres. Drizzle is the ORM;
332
286
  // the schema/queries/actions are identical across dialects, only db/columns
@@ -420,8 +374,13 @@ export async function scaffoldApp(name, cwd, opts = {}) {
420
374
  // app runs the compiler under Bun (its image has no Node), a Node app runs
421
375
  // it directly.
422
376
  ...(isApi ? {} : { 'css:build': cssBuildCmd }),
423
- // Shed the demo gallery to a clean, buildable base (scripts/clear-gallery.mjs).
424
- ...(isApi ? {} : { 'gallery:clear': isBun ? 'bun scripts/clear-gallery.mjs' : 'node scripts/clear-gallery.mjs' }),
377
+ // Shed the demo gallery / backend-features showcase to a clean, buildable
378
+ // base. The UI template runs clear-gallery.mjs; the api template runs
379
+ // clear-api-gallery.mjs (its showcase is app/api/features, not app/features).
380
+ 'gallery:clear': (() => {
381
+ const script = isApi ? 'clear-api-gallery.mjs' : 'clear-gallery.mjs';
382
+ return isBun ? `bun scripts/${script}` : `node scripts/${script}`;
383
+ })(),
425
384
  dev: isBun ? 'bun --bun webjs dev' : 'webjs dev',
426
385
  start: isBun ? 'bun --bun webjs start' : 'webjs start',
427
386
  test: 'webjs test',
@@ -763,7 +722,10 @@ export const users = table('users', {
763
722
  name: text(),
764
723
  // JSON column: a structured value persisted as JSON, typed via json<T>().
765
724
  // Same helper works on SQLite and Postgres. Delete if you do not need it.
766
- settings: json<{ theme?: string }>(),
725
+ settings: json<{ theme?: string }>(),${isFullStack ? `
726
+ // The auth gallery card (app/features/auth) signs credentials against this.
727
+ // gallery:clear removes this column with the rest of the auth surface.
728
+ passwordHash: text(),` : ''}
767
729
  createdAt: createdAt(),
768
730
  });
769
731
  ${isFullStack ? `
@@ -1040,10 +1002,18 @@ export type ActionResult<T> =
1040
1002
  // counterpart of the UI gallery. Prune what you skip.
1041
1003
  const { writeApiGallery } = await import('./api-gallery.js');
1042
1004
  await writeApiGallery(appDir);
1005
+
1006
+ // The showcase-reset script (wired as `gallery:clear` for the api template).
1007
+ // It sheds app/api/features + its modules back to the health + users base.
1008
+ const apiClearSrc = join(TEMPLATES, 'scripts', 'clear-api-gallery.mjs');
1009
+ if (existsSync(apiClearSrc)) {
1010
+ await mkdir(join(appDir, 'scripts'), { recursive: true });
1011
+ await cp(apiClearSrc, join(appDir, 'scripts', 'clear-api-gallery.mjs'));
1012
+ }
1043
1013
  }
1044
1014
 
1045
1015
  if (!isApi) {
1046
- // Full-stack and SaaS templates: layout + page + theme toggle + Tailwind
1016
+ // The UI template: layout + page + theme toggle + Tailwind
1047
1017
 
1048
1018
  // The Tailwind stylesheet is compiled from public/input.css (written below)
1049
1019
  // to a STATIC public/tailwind.css by css:build, and lib/utils/ui.ts helpers
@@ -1053,8 +1023,8 @@ export type ActionResult<T> =
1053
1023
  const publicDir = join(appDir, 'public');
1054
1024
  await mkdir(publicDir, { recursive: true });
1055
1025
  // Progressive-enhancement service worker (#271): ship the opt-in offline
1056
- // primitive (the worker + its offline fallback) into the UI scaffolds
1057
- // (full-stack / saas; this block is api-excluded since api has no UI).
1026
+ // primitive (the worker + its offline fallback) into the UI scaffold
1027
+ // (this block is api-excluded since api has no UI).
1058
1028
  // Dormant until the app registers it (see the skill's references/service-worker.md);
1059
1029
  // it never changes the JS-disabled baseline.
1060
1030
  for (const swFile of ['sw.js', 'offline.html']) {
@@ -1087,13 +1057,9 @@ export type ActionResult<T> =
1087
1057
  // styles/globals.css (the @webjsdev/ui theme).
1088
1058
  await writeUiBootstrap(appDir);
1089
1059
 
1090
- // The saas auth pages import a few ui-* primitives. A full-stack app adds
1091
- // any component on demand with `webjs ui add <name>`.
1092
- if (isSaas) {
1093
- await copyUiComponents(appDir, [
1094
- 'button', 'card', 'alert', 'badge', 'separator', 'label', 'input',
1095
- ]);
1096
- }
1060
+ // The gallery cards style with plain Tailwind (the app is pre-initialised for
1061
+ // `webjs ui add <name>` via writeUiBootstrap above, but ships no components
1062
+ // until you add them on demand).
1097
1063
 
1098
1064
  // The @webjsdev/ui theme (`--color-primary`, `--color-card`, the @theme maps,
1099
1065
  // @custom-variant, @keyframes) plus the app @theme mappings are compiled from
@@ -1136,10 +1102,10 @@ ${uiThemeRaw}
1136
1102
 
1137
1103
  // The gallery: idiomatic, densely-commented single-feature demos under
1138
1104
  // app/features/ plus one whole example app under app/examples/, with logic
1139
- // in modules/, all linked from the home page below. Shipped in every UI
1140
- // scaffold (full-stack AND saas) so an agent gains context by browsing real
1141
- // working code; prune per-feature (delete the route + its module) for what
1142
- // the app does not use.
1105
+ // in modules/, all linked from the home page below. Shipped in the UI
1106
+ // scaffold so an agent gains context by browsing real working code; prune
1107
+ // per-feature (delete the route + its module) for what the app does not use,
1108
+ // or shed the whole gallery at once with `gallery:clear`.
1143
1109
  await copyGallery(appDir);
1144
1110
 
1145
1111
  await writeFile(join(appDir, 'app', 'layout.ts'), `import { html, cspNonce } from '@webjsdev/core';
@@ -1154,26 +1120,34 @@ import '#components/theme-toggle.ts';
1154
1120
  * mapped into Tailwind via @theme in public/input.css, so bg-background,
1155
1121
  * text-foreground, bg-card, bg-primary, and border-border all work.
1156
1122
  */
1123
+
1124
+ // Declare the favicon via metadata.icons (NOT a hand-written <link> in the
1125
+ // template): the framework emits metadata links into <head>, whereas a <link>
1126
+ // written in the layout body stays in <body>, where browsers ignore it. The SVG
1127
+ // lives at public/favicon.svg and serves at /public/favicon.svg.
1128
+ export const metadata = { icons: '/public/favicon.svg' };
1129
+
1157
1130
  export default function RootLayout({ children }: { children: unknown }) {
1158
1131
  // Read the in-flight request's CSP nonce so the theme-detection inline script
1159
1132
  // passes strict CSP. Returns '' when no CSP nonce is set.
1160
1133
  const nonce = cspNonce();
1161
1134
  return html\`
1162
1135
  <script nonce="\${nonce}">
1163
- // Light/dark theme: read the saved or OS choice and set data-theme plus the
1164
- // .dark class the tokens key off. Delete this block (and the light blocks
1165
- // below) for a single-theme app.
1136
+ // Light/dark theme: apply the saved choice before paint (no flash). The
1137
+ // tokens follow color-scheme, which [data-theme] forces and otherwise
1138
+ // follows the OS, so an unset choice needs NO inline work here. The .dark
1139
+ // class is synced only for @webjsdev/ui components (they key off .dark).
1140
+ // Delete this block and the [data-theme] rules below for a single-theme app.
1166
1141
  (function(){
1167
1142
  try {
1168
- var mq = window.matchMedia('(prefers-color-scheme: light)');
1143
+ var mq = window.matchMedia('(prefers-color-scheme: dark)');
1169
1144
  function apply(){
1170
1145
  var t = null;
1171
1146
  try { t = localStorage.getItem('webjs_theme'); } catch (_) {}
1172
1147
  var el = document.documentElement;
1173
1148
  if (t === 'light' || t === 'dark') el.dataset.theme = t;
1174
1149
  else delete el.dataset.theme;
1175
- var dark = t === 'dark' || (t !== 'light' && !mq.matches);
1176
- el.classList.toggle('dark', dark);
1150
+ el.classList.toggle('dark', t === 'dark' || (t !== 'light' && mq.matches));
1177
1151
  }
1178
1152
  apply();
1179
1153
  mq.addEventListener('change', apply);
@@ -1187,7 +1161,9 @@ export default function RootLayout({ children }: { children: unknown }) {
1187
1161
  function measure(){
1188
1162
  try {
1189
1163
  var hdr = document.querySelector('header');
1190
- if (!hdr) return;
1164
+ // Only a FIXED header leaves normal flow and needs its height
1165
+ // reserved; a normal in-flow header (the gallery's navbar) does not.
1166
+ if (!hdr || getComputedStyle(hdr).position !== 'fixed') return;
1191
1167
  var apply = function(){
1192
1168
  document.documentElement.style.setProperty('--header-h', hdr.offsetHeight + 'px');
1193
1169
  };
@@ -1200,7 +1176,6 @@ export default function RootLayout({ children }: { children: unknown }) {
1200
1176
  })();
1201
1177
  </script>
1202
1178
  <meta name="color-scheme" content="light dark">
1203
- <link rel="icon" href="/public/favicon.svg" type="image/svg+xml">
1204
1179
  <!-- JetBrains Mono for body/UI (its monospaced, developer-console feel) and
1205
1180
  Bricolage Grotesque for the display wordmark. Swap these for your own
1206
1181
  fonts (and update --font-sans / --font-display below). -->
@@ -1215,89 +1190,59 @@ export default function RootLayout({ children }: { children: unknown }) {
1215
1190
 
1216
1191
  <link rel="stylesheet" href="/public/tailwind.css">
1217
1192
  <style>
1218
- /* Design tokens. The token NAMES are infrastructure (public/input.css maps
1219
- them into Tailwind via @theme). The VALUES are a cool neutral-grey palette
1220
- with a monospaced type system: change them here to give the app its own
1221
- look. bg-background / text-foreground / bg-card / bg-primary / border-border
1222
- all resolve from these. */
1193
+ /* Design tokens: ONE definition per colour via light-dark(LIGHT, DARK), so
1194
+ a palette change lands in a single place (DRY). The token NAMES are
1195
+ infrastructure (public/input.css maps them into Tailwind via @theme); the
1196
+ VALUES are a cool neutral-grey palette with a monospaced type system.
1197
+ color-scheme decides which side of each light-dark() applies: the default
1198
+ 'light dark' follows the OS, and the toggle FORCES one via [data-theme]
1199
+ below. The light theme is a crisp WHITE page with near-black text, a
1200
+ readable muted grey, and visible borders (a washed-out light theme comes
1201
+ from a grey page + too-light muted text + faint borders). For a
1202
+ single-theme app, delete the [data-theme] rules and give each token a
1203
+ single colour instead of light-dark().
1204
+ EDGE CASES: light-dark() is COLOUR-only. A colour needed in just one
1205
+ theme sets the unused side to a no-op, e.g. light-dark(#fff, transparent).
1206
+ A DERIVED token that references a light-dark() one (like --primary-tint
1207
+ below) tracks both themes for free. A NON-colour token that must differ
1208
+ per theme (a shadow's geometry, a gradient, a size, an image) cannot use
1209
+ light-dark(); give it a :root[data-theme='dark'] override plus an
1210
+ @media (prefers-color-scheme: dark) { :root:not([data-theme]) { ... } }
1211
+ rule for the OS default. */
1223
1212
  :root {
1224
1213
  --font-sans: 'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
1225
1214
  --font-serif: ui-serif, 'Iowan Old Style', Palatino, Georgia, serif;
1226
1215
  --font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
1227
1216
  --font-display: 'Bricolage Grotesque', 'JetBrains Mono', ui-sans-serif, system-ui, sans-serif;
1228
1217
  --header-h: 0px;
1218
+
1219
+ color-scheme: light dark; /* default: follow the OS */
1220
+ --background: light-dark(#ffffff, #1e2226);
1221
+ --foreground: light-dark(#191c20, #dee2e6);
1222
+ --card: light-dark(#f7f8fa, #313539);
1223
+ --card-foreground: light-dark(#191c20, #dee2e6);
1224
+ --popover: light-dark(#ffffff, #313539);
1225
+ --popover-foreground: light-dark(#191c20, #dee2e6);
1226
+ --primary: light-dark(#1e2226, #dee2e6);
1227
+ --primary-foreground: light-dark(#ffffff, #1e2226);
1228
+ --secondary: light-dark(#eef0f2, #363a3e);
1229
+ --secondary-foreground: light-dark(#191c20, #dee2e6);
1230
+ --muted: light-dark(#eef0f2, #313539);
1231
+ --muted-foreground: light-dark(#565c64, #94989c);
1232
+ --accent: light-dark(#e9ebef, #363a3e);
1233
+ --accent-foreground: light-dark(#191c20, #f7fbff);
1234
+ --border: light-dark(#e2e5e9, #3d434b);
1235
+ --border-strong: light-dark(#ccd1d7, #454b51);
1236
+ --input: light-dark(#e2e5e9, #34393e);
1237
+ --ring: light-dark(#8b9198, #6b7075);
1229
1238
  /* A translucent tint of the primary, tracked automatically across
1230
1239
  light/dark. Used for focus rings (ring-primary-tint). */
1231
1240
  --primary-tint: color-mix(in srgb, var(--primary) 22%, transparent);
1232
1241
  }
1233
- /* dark (the default, and the explicit .dark the toggle sets) */
1234
- :root, .dark {
1235
- color-scheme: dark;
1236
- --background: #1e2226;
1237
- --foreground: #dee2e6;
1238
- --card: #313539;
1239
- --card-foreground: #dee2e6;
1240
- --popover: #313539;
1241
- --popover-foreground: #dee2e6;
1242
- --primary: #dee2e6;
1243
- --primary-foreground: #1e2226;
1244
- --secondary: #363a3e;
1245
- --secondary-foreground: #dee2e6;
1246
- --muted: #313539;
1247
- --muted-foreground: #94989c;
1248
- --accent: #363a3e;
1249
- --accent-foreground: #f7fbff;
1250
- --border: #34393e;
1251
- --border-strong: #454b51;
1252
- --input: #34393e;
1253
- --ring: #6b7075;
1254
- }
1255
- /* light (explicit via the toggle) */
1256
- :root[data-theme='light'] {
1257
- color-scheme: light;
1258
- --background: #dee2e6;
1259
- --foreground: #313539;
1260
- --card: #f0f4f7;
1261
- --card-foreground: #313539;
1262
- --popover: #f0f4f7;
1263
- --popover-foreground: #313539;
1264
- --primary: #313539;
1265
- --primary-foreground: #f7fbff;
1266
- --secondary: #f7fbff;
1267
- --secondary-foreground: #313539;
1268
- --muted: #eaeef1;
1269
- --muted-foreground: #767b80;
1270
- --accent: #f7fbff;
1271
- --accent-foreground: #313539;
1272
- --border: #c9d0d6;
1273
- --border-strong: #b3bbc2;
1274
- --input: #c9d0d6;
1275
- --ring: #9aa0a5;
1276
- }
1277
- /* light (OS preference, when the user has made no explicit choice) */
1278
- @media (prefers-color-scheme: light) {
1279
- :root:not(.dark):not([data-theme='dark']) {
1280
- color-scheme: light;
1281
- --background: #dee2e6;
1282
- --foreground: #313539;
1283
- --card: #f0f4f7;
1284
- --card-foreground: #313539;
1285
- --popover: #f0f4f7;
1286
- --popover-foreground: #313539;
1287
- --primary: #313539;
1288
- --primary-foreground: #f7fbff;
1289
- --secondary: #f7fbff;
1290
- --secondary-foreground: #313539;
1291
- --muted: #eaeef1;
1292
- --muted-foreground: #767b80;
1293
- --accent: #f7fbff;
1294
- --accent-foreground: #313539;
1295
- --border: #c9d0d6;
1296
- --border-strong: #b3bbc2;
1297
- --input: #c9d0d6;
1298
- --ring: #9aa0a5;
1299
- }
1300
- }
1242
+ /* The toggle writes data-theme to FORCE a scheme; with neither attribute
1243
+ the default 'color-scheme: light dark' above follows the OS. */
1244
+ :root[data-theme='light'] { color-scheme: light; }
1245
+ :root[data-theme='dark'] { color-scheme: dark; }
1301
1246
  </style>
1302
1247
  <style>
1303
1248
  /* Base styles utility classes can't reach. */
@@ -1311,7 +1256,24 @@ export default function RootLayout({ children }: { children: unknown }) {
1311
1256
  -moz-osx-font-smoothing: grayscale;
1312
1257
  }
1313
1258
  </style>
1314
- <main class="min-h-dvh px-4 sm:px-6 py-8">
1259
+ <!-- Top navbar, on every page: brand on the left, links + theme toggle on
1260
+ the right. It floats (no separator) and is in normal flow, so it just
1261
+ scrolls with the page; make it a fixed header only if you want it pinned
1262
+ (position: fixed, never sticky, which flickers on iOS during a nav). -->
1263
+ <header class="max-w-5xl mx-auto px-4 sm:px-6 h-14 flex items-center justify-between gap-4">
1264
+ <a href="/" class="inline-flex items-center gap-2 no-underline text-foreground font-bold tracking-tight" style="font-family: var(--font-display)">
1265
+ <span class="w-[22px] h-[22px] rounded-[7px] bg-gradient-to-br from-foreground to-muted-foreground" aria-hidden="true"></span>
1266
+ WebJs Gallery
1267
+ </a>
1268
+ <nav class="flex items-center gap-4 text-sm" aria-label="Primary">
1269
+ <a href="https://docs.webjs.dev" target="_blank" rel="noopener" class="hidden sm:inline text-muted-foreground hover:text-foreground no-underline transition-colors">Docs</a>
1270
+ <a href="https://github.com/webjsdev/webjs" target="_blank" rel="noopener" class="hidden sm:inline text-muted-foreground hover:text-foreground no-underline transition-colors">GitHub</a>
1271
+ <theme-toggle></theme-toggle>
1272
+ </nav>
1273
+ </header>
1274
+ <!-- Fill the viewport minus the h-14 (3.5rem) navbar, so a short page has no
1275
+ spurious scrollbar (min-h-dvh alone would overflow by the navbar height). -->
1276
+ <main class="min-h-[calc(100dvh-3.5rem)] max-w-5xl mx-auto px-4 sm:px-6 py-8">
1315
1277
  \${children}
1316
1278
  </main>
1317
1279
  \`;
@@ -1322,75 +1284,36 @@ export default function RootLayout({ children }: { children: unknown }) {
1322
1284
  // demo and the example app, and a footer with the docs + source links. Treat it
1323
1285
  // as a starting point: prune the demos you do not use (delete the
1324
1286
  // app/features/<x> route AND its modules/<x>), then reshape this page into the
1325
- // app's real landing page. For the saas template a login/signup CTA row is
1326
- // spliced under the tagline.
1327
- const homeAuthLinks = isSaas
1328
- ? '\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>'
1329
- : '';
1287
+ // app's real landing page.
1330
1288
  await writeFile(join(appDir, 'app', 'page.ts'), `import { html } from '@webjsdev/core';
1289
+ import { cardClass } from '#components/ui/card.ts';
1290
+ import { badgeClass } from '#components/ui/badge.ts';
1291
+ // The demo index is defined once in modules/gallery/nav.ts (the same source the
1292
+ // left sidebar reads), so the home cards and the sidebar can never drift.
1293
+ import { FEATURES, EXAMPLES } from '#modules/gallery/nav.ts';
1331
1294
 
1332
1295
  export const metadata = {
1333
1296
  title: '${displayName}',
1334
1297
  };
1335
1298
 
1336
- // The gallery this page links. FEATURES are single-concept demos (one WebJs
1337
- // concept each, under app/features/, logic in modules/). EXAMPLES are whole apps
1338
- // composing several features (under app/examples/). Prune what you do not use
1339
- // (delete the route AND its modules/<name>), then reshape this page.
1340
- const FEATURES = [
1341
- { href: '/features/routing', title: 'Routing', blurb: 'A static route plus a dynamic [id] segment that reads params. The file-based router in miniature.' },
1342
- { href: '/features/boundaries', title: 'Boundaries', blurb: 'The control-flow throws (forbidden / unauthorized / notFound) and the nearest boundary file that catches each.' },
1343
- { href: '/features/components', title: 'Components', blurb: 'The WebComponent factory, reactive props, instance signals, and slot projection in light DOM.' },
1344
- { 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.' },
1345
- { href: '/features/optimistic-ui', title: 'Optimistic UI', blurb: 'The imperative optimistic(signal, value, action) flip: instant update, automatic rollback on failure.' },
1346
- { 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.' },
1347
- { href: '/features/directives', title: 'Directives', blurb: 'The lit-html directive set: repeat for keyed lists, watch(signal) for a fine-grained node swap.' },
1348
- { 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.' },
1349
- { href: '/features/forms', title: 'Forms', blurb: 'A no-JS progressive-enhancement form posting to the page action, with server-side validation errors.' },
1350
- { href: '/features/metadata', title: 'Metadata', blurb: 'Static metadata plus generateMetadata(ctx), which reads the request to compute the title and Open Graph tags.' },
1351
- { 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.' },
1352
- { href: '/features/env', title: 'Env vars', blurb: 'The server-only vs WEBJS_PUBLIC_ boundary, read during SSR so secrets never reach the browser.' },
1353
- { href: '/features/client-router', title: 'Client router', blurb: 'Automatic soft navigation: fragment-only fetches, hover prefetch, scroll restore, and graceful no-JS fallback.' },
1354
- { href: '/features/frames', title: 'Frames', blurb: 'A webjs-frame region that swaps a filtered sub-list in place from a link, shipping zero component JS, with a no-JS full-nav fallback.' },
1355
- { 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).' },
1356
- { href: '/features/websockets', title: 'WebSockets', blurb: 'A WS(ws, req) route endpoint plus the connectWS() client, echoing messages over a live socket.' },
1357
- { 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.' },
1358
- { href: '/features/rate-limit', title: 'Rate limiting', blurb: 'The rateLimit() middleware scoped to one endpoint, returning a 429 with Retry-After past the window.' },
1359
- { href: '/features/file-storage', title: 'File storage', blurb: 'A no-JS multipart upload streamed into the FileStore, then served back through a streaming route.' },
1360
- { 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.' },
1361
- ];
1362
- const EXAMPLES = [
1363
- { 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.' },
1364
- ];
1365
-
1366
1299
  export default function Home() {
1367
1300
  return html\`
1368
- <div class="fixed top-4 right-4 z-10"><theme-toggle></theme-toggle></div>
1369
-
1370
- <div class="max-w-5xl mx-auto px-6 py-16 flex flex-col items-center gap-16">
1371
- <!-- Masthead -->
1301
+ <div class="py-8 flex flex-col items-center gap-16">
1302
+ <!-- Hero -->
1372
1303
  <section class="flex flex-col items-center text-center gap-5">
1373
- <p class="text-xs font-semibold uppercase tracking-[0.22em] text-muted-foreground m-0">Welcome to</p>
1374
- <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;">
1375
- WebJs Gallery
1304
+ <h1 class="text-5xl sm:text-6xl font-bold 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); letter-spacing: -0.02em;">
1305
+ Explore the gallery
1376
1306
  </h1>
1377
1307
  <p class="text-base sm:text-lg text-muted-foreground max-w-lg leading-relaxed m-0">
1378
- AI-first and web-components-first. Server-rendered, progressively enhanced, and buildless.
1379
- </p>${homeAuthLinks}
1308
+ Each demo isolates a single WebJs capability in real, runnable code. Read the ones you need, then build your app on the same patterns.
1309
+ </p>
1380
1310
  </section>
1381
1311
 
1382
1312
  <!-- Gallery: every feature demo + the example app -->
1383
1313
  <section class="w-full flex flex-col gap-6">
1384
- <div class="flex flex-col items-center gap-2 text-center">
1385
- <h2 class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground m-0">Explore the gallery</h2>
1386
- <p class="text-sm text-muted-foreground max-w-lg leading-relaxed m-0">
1387
- One WebJs concept per demo under <code class="text-[0.9em] text-foreground">app/features/</code>, with logic
1388
- in <code class="text-[0.9em] text-foreground">modules/</code>.
1389
- </p>
1390
- </div>
1391
1314
  <div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
1392
1315
  \${FEATURES.map(f => html\`
1393
- <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">
1316
+ <a href="\${f.href}" class=\${cardClass('group flex flex-col gap-1.5 rounded-xl p-4 no-underline transition-colors hover:border-border-strong hover:bg-accent')}>
1394
1317
  <span class="flex items-center justify-between gap-2">
1395
1318
  <span class="text-sm font-medium text-foreground">\${f.title}</span>
1396
1319
  <span class="text-muted-foreground transition-transform group-hover:translate-x-0.5" aria-hidden="true">&rarr;</span>
@@ -1400,9 +1323,9 @@ export default function Home() {
1400
1323
  \`)}
1401
1324
  </div>
1402
1325
  \${EXAMPLES.map(e => html\`
1403
- <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">
1326
+ <a href="\${e.href}" class=\${cardClass('group flex flex-col gap-2 rounded-xl p-5 no-underline transition-colors hover:border-border-strong hover:bg-accent')}>
1404
1327
  <span class="flex items-center gap-2.5">
1405
- <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>
1328
+ <span class=\${badgeClass({ variant: 'outline' })}>Example app</span>
1406
1329
  <span class="text-sm font-medium text-foreground">\${e.title}</span>
1407
1330
  <span class="ml-auto text-muted-foreground transition-transform group-hover:translate-x-0.5" aria-hidden="true">&rarr;</span>
1408
1331
  </span>
@@ -1414,8 +1337,8 @@ export default function Home() {
1414
1337
  <!-- Footer: docs + source -->
1415
1338
  <footer class="flex flex-col items-center gap-3">
1416
1339
  <nav class="flex items-center gap-6 text-sm text-muted-foreground" aria-label="WebJs links">
1417
- <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>
1418
- <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>
1340
+ <a href="https://docs.webjs.dev" target="_blank" rel="noopener" class="inline-flex items-center gap-2 hover:text-foreground transition-colors no-underline">\${iconBook()}<span>Docs</span></a>
1341
+ <a href="https://github.com/webjsdev/webjs" target="_blank" rel="noopener" class="inline-flex items-center gap-2 hover:text-foreground transition-colors no-underline">\${iconGithub()}<span>GitHub</span></a>
1419
1342
  </nav>
1420
1343
  <p class="text-[0.7rem] uppercase tracking-[0.15em] text-muted-foreground m-0 text-center">
1421
1344
  Built with WebJs &middot; MIT License
@@ -1439,6 +1362,8 @@ function iconGithub() {
1439
1362
  // --- Theme toggle component ---
1440
1363
 
1441
1364
  await writeFile(join(appDir, 'components', 'theme-toggle.ts'), `import { WebComponent, html, signal } from '@webjsdev/core';
1365
+ import { cn } from '#lib/utils/cn.ts';
1366
+ import { buttonClass } from '#components/ui/button.ts';
1442
1367
 
1443
1368
  type Theme = 'system' | 'light' | 'dark';
1444
1369
 
@@ -1474,9 +1399,10 @@ export class ThemeToggle extends WebComponent {
1474
1399
  const el = document.documentElement;
1475
1400
  if (next === 'system') delete el.dataset.theme;
1476
1401
  else el.dataset.theme = next;
1477
- // Keep the .dark class the @webjsdev/ui kit uses in sync so the ui-* components follow the theme.
1402
+ // Our own tokens follow color-scheme via data-theme (set above). Keep the
1403
+ // .dark class in sync only for @webjsdev/ui components, which key off it.
1478
1404
  const dark = next === 'dark'
1479
- || (next === 'system' && !window.matchMedia('(prefers-color-scheme: light)').matches);
1405
+ || (next === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
1480
1406
  el.classList.toggle('dark', dark);
1481
1407
  }
1482
1408
 
@@ -1486,7 +1412,7 @@ export class ThemeToggle extends WebComponent {
1486
1412
  const icon = t === 'light' ? ICONS.sun : t === 'dark' ? ICONS.moon : ICONS.system;
1487
1413
  return html\`
1488
1414
  <button
1489
- class="inline-flex items-center justify-center w-9 h-9 p-0 border border-border rounded-full bg-card text-muted-foreground cursor-pointer transition-all duration-150 hover:text-foreground hover:border-border-strong active:scale-[0.94] focus-visible:outline-none focus-visible:border-primary focus-visible:ring-[3px] focus-visible:ring-primary-tint"
1415
+ class=\${cn(buttonClass({ variant: 'secondary', size: 'none' }), 'w-9 h-9 rounded-full text-muted-foreground duration-150 hover:text-foreground active:scale-[0.94]')}
1490
1416
  @click=\${() => this.cycle()}
1491
1417
  aria-label="Cycle theme (currently \${label})"
1492
1418
  title="Theme: \${label.toLowerCase()}"
@@ -1505,12 +1431,6 @@ ThemeToggle.register('theme-toggle');
1505
1431
  `);
1506
1432
  } // end if (!isApi)
1507
1433
 
1508
- // --- SaaS template extras: auth, dashboard, drizzle User model ---
1509
- if (isSaas) {
1510
- const { writeSaasFiles } = await import('./saas-template.js');
1511
- await writeSaasFiles(appDir, { runtime });
1512
- }
1513
-
1514
1434
  // AGENTS.md is already in place via the shared `templateFiles` loop
1515
1435
  // earlier in this function, so no framework-root fallback needed.
1516
1436
 
@@ -1531,20 +1451,11 @@ ThemeToggle.register('theme-toggle');
1531
1451
  modules/users/{actions,queries,types.ts} ← routes over server actions
1532
1452
  db/{schema,columns,connection}.server.ts ← Drizzle (User model)
1533
1453
  ${guide}
1534
- `);
1535
- } else if (isSaas) {
1536
- console.log(` ${name}/
1537
- app/{layout,page}.ts, login/, signup/
1538
- app/dashboard/{page,settings,middleware}.ts ← protected
1539
- app/api/auth/[...path]/route.ts ← auth API
1540
- components/ui/*, components/theme-toggle.ts
1541
- modules/auth/*, lib/{auth,password}.server.ts
1542
- db/{schema,columns,connection}.server.ts ← Drizzle (User model)
1543
- ${guide}
1544
1454
  `);
1545
1455
  } else {
1546
1456
  console.log(` ${name}/
1547
- app/{layout,page}.ts ← a minimal home to grow in place
1457
+ app/{layout,page}.ts ← gallery home; gallery:clear grows in place
1458
+ app/features/*, modules/* ← browsable demos (incl. auth: login + protected route)
1548
1459
  components/theme-toggle.ts
1549
1460
  public/input.css ← Tailwind entry (compiles to public/tailwind.css)
1550
1461
  db/{schema,columns,connection}.server.ts ← Drizzle
@@ -1593,9 +1504,9 @@ ThemeToggle.register('theme-toggle');
1593
1504
  // Next-steps banner prints LAST so the actionable command is the
1594
1505
  // final thing on screen, never buried above the AI-agent guidance.
1595
1506
  // Single copy-paste line so the user can move from "scaffold done"
1596
- // to "dev server up" in one command. The full-stack and saas
1597
- // templates ship with @webjsdev/ui already initialised; the api
1598
- // template has no UI but may add one later.
1507
+ // to "dev server up" in one command. The full-stack template ships
1508
+ // with @webjsdev/ui already initialised; the api template has no UI
1509
+ // but may add one later.
1599
1510
  const installSegment = installed ? '' : `${pm} install && `;
1600
1511
  // The shipped schema is applied on the first `run dev` (webjs.*.before runs
1601
1512
  // `db migrate`), but only if a migration FILE exists. When we installed, we
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webjsdev/cli",
3
- "version": "0.10.44",
3
+ "version": "0.10.46",
4
4
  "type": "module",
5
5
  "description": "webjs CLI - dev, start, create, db",
6
6
  "bin": {