@webjsdev/cli 0.10.38 → 0.10.40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/create.js CHANGED
@@ -233,9 +233,9 @@ async function writeUiBootstrap(appDir) {
233
233
 
234
234
  // 3) styles/globals.css: copy the neutral theme verbatim. components.json
235
235
  // references this path, and future `webjs ui add` calls append to it. It
236
- // lives OUTSIDE app/ because app/ is routing-only; the layout inlines the
237
- // same tokens into a <style type="text/tailwindcss"> block so the browser
238
- // Tailwind runtime resolves them with no build step.
236
+ // lives OUTSIDE app/ because app/ is routing-only. The same @theme maps are
237
+ // written to public/input.css and compiled to the static public/tailwind.css
238
+ // the layout links (so the app is styled with JavaScript off).
239
239
  const css = await readFile(
240
240
  join(UI_REGISTRY_ROOT, 'themes', 'index.css'), 'utf8',
241
241
  );
@@ -244,11 +244,11 @@ async function writeUiBootstrap(appDir) {
244
244
  }
245
245
 
246
246
  /**
247
- * Read the @webjsdev/ui theme CSS so we can inline it into the layout's
248
- * `<style type="text/tailwindcss">` block. The Tailwind browser runtime
249
- * picks up inline `<style type="text/tailwindcss">` content, so the theme
250
- * tokens (`--color-primary`, `--color-card`, …) the registry components
251
- * consume are available at runtime without a build step.
247
+ * Read the @webjsdev/ui theme CSS so we can write it into public/input.css,
248
+ * which css:build compiles into the static public/tailwind.css the layout
249
+ * links. The theme tokens (`--color-primary`, `--color-card`, …) the registry
250
+ * components consume become real utility classes in the compiled stylesheet,
251
+ * so the app is styled with JavaScript off.
252
252
  *
253
253
  * @returns {Promise<string>} theme CSS source
254
254
  */
@@ -344,6 +344,15 @@ export async function scaffoldApp(name, cwd, opts = {}) {
344
344
  throw new Error(`Unknown --runtime '${runtime}'. Only ${VALID_RUNTIMES.join(' / ')} are supported.`);
345
345
  }
346
346
  const isBun = runtime === 'bun';
347
+ // Tailwind compile commands (#947). A UI scaffold compiles a STATIC stylesheet
348
+ // (not the browser runtime), so it renders styled with JavaScript off. The
349
+ // compiler is a node-shebang CLI, so a Bun app (whose Dockerfile is a node-less
350
+ // `oven/bun:1` image, #595) must run it under Bun via `bun --bun`; a Node app
351
+ // runs it directly (the before / regenerate steps get node_modules/.bin on PATH
352
+ // via envWithLocalBin). Deliberately NOT `npm run css:build` in the hooks: the
353
+ // Bun image has no npm, so that step would exit 127 and abort the boot.
354
+ const twBin = isBun ? 'bun --bun tailwindcss' : 'tailwindcss';
355
+ const cssBuildCmd = `${twBin} -i ./public/input.css -o ./public/tailwind.css --minify`;
347
356
  const appDir = join(cwd, name);
348
357
  if (existsSync(appDir)) {
349
358
  console.error(`Error: directory '${name}' already exists.`);
@@ -399,6 +408,13 @@ export async function scaffoldApp(name, cwd, opts = {}) {
399
408
  // / doctor) stay plain `webjs ...`: they spawn node tooling (`node --test`,
400
409
  // drizzle-kit, tsc) and forcing `--bun` there buys nothing (and `webjs
401
410
  // test` shells `node --test`, which a `bun --test` would not be).
411
+ // Compile Tailwind from public/input.css to a STATIC public/tailwind.css
412
+ // that app/layout.ts links, so the app is fully styled with JavaScript
413
+ // DISABLED (a real stylesheet, not an in-browser compile). Runs inside the
414
+ // dev and start tasks via the `before` hooks below. Runtime-aware: a Bun
415
+ // app runs the compiler under Bun (its image has no Node), a Node app runs
416
+ // it directly.
417
+ ...(isApi ? {} : { 'css:build': cssBuildCmd }),
402
418
  dev: isBun ? 'bun --bun webjs dev' : 'webjs dev',
403
419
  start: isBun ? 'bun --bun webjs start' : 'webjs start',
404
420
  test: 'webjs test',
@@ -447,6 +463,10 @@ export async function scaffoldApp(name, cwd, opts = {}) {
447
463
  // assertNoA11yViolations() test helper from @webjsdev/core/testing.
448
464
  // Test-only: dynamically imported, never shipped to the app runtime.
449
465
  'axe-core': '^4.10.0',
466
+ // The Tailwind v4 CLI that css:build runs to compile public/input.css into
467
+ // the static public/tailwind.css the layout links. UI templates only (the
468
+ // api template has no CSS). Build tooling, never shipped to the runtime.
469
+ ...(isApi ? {} : { '@tailwindcss/cli': '^4.1.0' }),
450
470
  // tsserver plugin, wired into tsconfig below. Gives the language
451
471
  // INTELLIGENCE (go-to-def, completions, diagnostics, hover inside html``
452
472
  // templates) in any tsserver editor with NO editor plugin installed,
@@ -466,13 +486,35 @@ export async function scaffoldApp(name, cwd, opts = {}) {
466
486
  // `before` and run it in-process, so `npm run dev` / `start` (thin aliases
467
487
  // above) behave identically. Both apply pending migrations via `webjs db
468
488
  // migrate` (idempotent, a no-op when the db is current), so a freshly
469
- // generated migration is applied without a manual step (#725). The scaffold
470
- // uses the Tailwind browser runtime (no CSS build step), so there is no dev
471
- // `parallel` watcher here; an app that adds the Tailwind CLI puts its
472
- // `--watch` command under `webjs.dev.parallel`.
489
+ // generated migration is applied without a manual step (#725). For a UI
490
+ // template it ALSO compiles Tailwind in `before` so a freshly cloned app is
491
+ // styled on the very first boot with no manual step. The compile command is
492
+ // the runtime-aware `cssBuildCmd` (a Bun app runs it under Bun, since its
493
+ // image has no npm or Node), NOT `npm run css:build`. The api template has no
494
+ // CSS, so it gets neither.
495
+ //
496
+ // In dev the static public/tailwind.css is kept fresh by `dev.regenerate`
497
+ // (#967), NOT a background `tailwindcss --watch`. A watch that dies mid-
498
+ // session or never starts serves stale/missing CSS with no error (a newly
499
+ // added utility class has no backing rule, so the app renders unstyled
500
+ // locally while prod is fine). `regenerate` instead recompiles ON REQUEST
501
+ // when the output is older than a source (or missing): the framework rebuilds
502
+ // it before serving `/public/tailwind.css`, so there is no watch process to
503
+ // die and no staleness window. Same `cssBuildCmd` as prod, so dev and prod
504
+ // resolve classes identically (nothing to diverge). `inputs` mirrors the
505
+ // input.css @source globs (the dirs Tailwind scans for classes).
473
506
  webjs: {
474
- dev: { before: ['webjs db migrate'] },
475
- start: { before: ['webjs db migrate'] },
507
+ dev: {
508
+ before: isApi ? ['webjs db migrate'] : ['webjs db migrate', cssBuildCmd],
509
+ ...(isApi ? {} : {
510
+ regenerate: [{
511
+ output: 'public/tailwind.css',
512
+ command: cssBuildCmd,
513
+ inputs: ['app', 'components', 'modules', 'lib', 'public/input.css'],
514
+ }],
515
+ }),
516
+ },
517
+ start: { before: isApi ? ['webjs db migrate'] : ['webjs db migrate', cssBuildCmd] },
476
518
  },
477
519
  }, null, 2) + '\n');
478
520
 
@@ -534,6 +576,11 @@ export async function scaffoldApp(name, cwd, opts = {}) {
534
576
  'AGENTS.md',
535
577
  'CONVENTIONS.md',
536
578
  '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',
537
584
  // Starter tests under the new feature-folder layout.
538
585
  'test/hello/hello.test.ts',
539
586
  'test/hello/browser/hello.test.js',
@@ -563,6 +610,14 @@ export async function scaffoldApp(name, cwd, opts = {}) {
563
610
  '.claude/hooks/require-tests-with-src.sh',
564
611
  '.claude/hooks/check-server-imports.sh',
565
612
  '.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',
566
621
  // Gemini CLI config + hooks
567
622
  '.gemini/settings.json',
568
623
  '.gemini/hooks/nudge-uncommitted.sh',
@@ -638,7 +693,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
638
693
 
639
694
  // Make hook scripts executable
640
695
  const { chmod } = await import('node:fs/promises');
641
- 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']) {
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']) {
642
697
  const hookPath = join(appDir, '.claude', 'hooks', hook);
643
698
  if (existsSync(hookPath)) await chmod(hookPath, 0o755);
644
699
  }
@@ -675,6 +730,9 @@ export const table = sqliteTableCreator((name) => name, 'snake_case');
675
730
  export const pk = () => integer().primaryKey({ autoIncrement: true });
676
731
  export const uuidPk = () => text().primaryKey().$defaultFn(() => crypto.randomUUID());
677
732
  export const uuid = () => text();
733
+ // Structured value (array / object) stored as JSON. Type it with json<T>() so
734
+ // the column is narrowed on read/write instead of \`unknown\`.
735
+ export const json = <T>() => text({ mode: 'json' }).$type<T>();
678
736
  export const bool = () => integer({ mode: 'boolean' });
679
737
  export const timestamp = () => integer({ mode: 'timestamp_ms' });
680
738
  export const createdAt = () => timestamp().notNull().defaultNow();
@@ -686,7 +744,7 @@ export const index = (...cols: SQLiteColumn[]) =>
686
744
  _index(getTableName((cols[0] as unknown as { table: Table }).table) + '_' + cols.map((c) => c.name).join('_') + '_idx').on(...(cols as [SQLiteColumn, ...SQLiteColumn[]]));
687
745
  `;
688
746
 
689
- const columnsPg = `import { pgTableCreator, serial, uuid as pgUuid, integer, text, real, boolean, timestamp as pgTimestamp, index as _index } from 'drizzle-orm/pg-core';
747
+ const columnsPg = `import { pgTableCreator, serial, uuid as pgUuid, integer, text, real, boolean, jsonb, timestamp as pgTimestamp, index as _index } from 'drizzle-orm/pg-core';
690
748
  import type { PgColumn } from 'drizzle-orm/pg-core';
691
749
  import { getTableName, type Table } from 'drizzle-orm';
692
750
 
@@ -697,6 +755,9 @@ export const table = pgTableCreator((name) => name, 'snake_case');
697
755
  export const pk = () => serial().primaryKey();
698
756
  export const uuidPk = () => pgUuid().primaryKey().defaultRandom();
699
757
  export const uuid = () => pgUuid();
758
+ // Structured value (array / object) stored as JSON. Type it with json<T>() so
759
+ // the column is narrowed on read/write instead of \`unknown\`.
760
+ export const json = <T>() => jsonb().$type<T>();
700
761
  export const bool = () => boolean();
701
762
  export const timestamp = () => pgTimestamp({ withTimezone: true });
702
763
  export const createdAt = () => timestamp().notNull().defaultNow();
@@ -710,13 +771,16 @@ export const index = (...cols: PgColumn[]) =>
710
771
 
711
772
  // Example schema (dialect-agnostic). Replace the User model with your own.
712
773
  await writeFile(join(appDir, 'db', 'schema.server.ts'), `import { defineRelations } from 'drizzle-orm';
713
- import { table, pk, ${isFullStack ? 'uuidPk, ' : ''}text, ${isFullStack ? 'bool, ' : ''}createdAt } from './columns.server.ts';
774
+ import { table, pk, ${isFullStack ? 'uuidPk, ' : ''}text, ${isFullStack ? 'bool, ' : ''}json, createdAt } from './columns.server.ts';
714
775
 
715
776
  // Example model. Feel free to delete or extend.
716
777
  export const users = table('users', {
717
778
  id: pk(),
718
779
  email: text().notNull().unique(),
719
780
  name: text(),
781
+ // JSON column: a structured value persisted as JSON, typed via json<T>().
782
+ // Same helper works on SQLite and Postgres. Delete if you do not need it.
783
+ settings: json<{ theme?: string }>(),
720
784
  createdAt: createdAt(),
721
785
  });
722
786
  ${isFullStack ? `
@@ -998,15 +1062,13 @@ export type ActionResult<T> =
998
1062
  if (!isApi) {
999
1063
  // Full-stack and SaaS templates: layout + page + theme toggle + Tailwind
1000
1064
 
1001
- // Copy the Tailwind browser runtime + lib/utils/ui.ts helpers from
1002
- // the scaffold templates directory so the app boots with the exact
1003
- // blog example architecture: light DOM + Tailwind + JS helpers.
1065
+ // The Tailwind stylesheet is compiled from public/input.css (written below)
1066
+ // to a STATIC public/tailwind.css by css:build, and lib/utils/ui.ts helpers
1067
+ // are copied below, so the app boots with the exact blog example
1068
+ // architecture: light DOM + a real Tailwind stylesheet (styled with JS off)
1069
+ // + JS helpers.
1004
1070
  const publicDir = join(appDir, 'public');
1005
1071
  await mkdir(publicDir, { recursive: true });
1006
- const tailwindSrc = join(TEMPLATES, 'public', 'tailwind-browser.js');
1007
- if (existsSync(tailwindSrc)) {
1008
- await cp(tailwindSrc, join(publicDir, 'tailwind-browser.js'));
1009
- }
1010
1072
  // Progressive-enhancement service worker (#271): ship the opt-in offline
1011
1073
  // primitive (the worker + its offline fallback) into the UI scaffolds
1012
1074
  // (full-stack / saas; this block is api-excluded since api has no UI).
@@ -1051,21 +1113,46 @@ export type ActionResult<T> =
1051
1113
  // use. See CONVENTIONS.md "prune what the app does not use".
1052
1114
  if (!isApi) await copyGallery(appDir);
1053
1115
 
1054
- // The @webjsdev/ui theme tokens (`--color-primary`, `--color-card`, …) the
1055
- // ui-* components consume. We read the registry's themes/index.css at
1056
- // create time and inline it into the layout's
1057
- // `<style type="text/tailwindcss">` block so the Tailwind browser
1058
- // runtime picks it up. Same content also lives at styles/globals.css for
1059
- // `webjsui` tooling.
1060
- const UI_THEME = (await readThemeCss())
1061
- // Escape backticks + ${} so the CSS survives interpolation into the
1062
- // layout's template literal below.
1063
- .replace(/\\/g, '\\\\')
1064
- .replace(/`/g, '\\`')
1065
- .replace(/\$\{/g, '\\${');
1066
-
1067
- await writeFile(join(appDir, 'app', 'layout.ts'), `// webjs-scaffold-placeholder. This is the example app chrome (brand, nav, content-width container). Adapt it to your app, then delete this line. webjs check fails while the marker remains.
1068
- import { html, cspNonce } from '@webjsdev/core';
1116
+ // The @webjsdev/ui theme (`--color-primary`, `--color-card`, the @theme maps,
1117
+ // @custom-variant, @keyframes) plus the app @theme mappings are compiled from
1118
+ // public/input.css into the STATIC public/tailwind.css that app/layout.ts
1119
+ // links, so the app is fully styled with JavaScript disabled. Write input.css:
1120
+ // `@import "tailwindcss"`, source globs, the ui theme (read from the registry
1121
+ // themes/index.css), then the app @theme inline block. The token VALUES live
1122
+ // on :root in app/layout.ts (plain CSS, so they resolve with JS off) and the
1123
+ // @theme inline maps here reference them by var(), the same split the blog
1124
+ // uses. Same theme also lives at styles/globals.css for `webjsui` tooling.
1125
+ const uiThemeRaw = await readThemeCss();
1126
+ await writeFile(join(publicDir, 'input.css'), `@import "tailwindcss";
1127
+
1128
+ /* Scan app sources so their utility classes make it into the compiled bundle.
1129
+ Tailwind v4 auto-scans the project too; these @source lines are explicit. */
1130
+ @source "../app/**/*.{ts,js}";
1131
+ @source "../components/**/*.{ts,js}";
1132
+ @source "../modules/**/*.{ts,js}";
1133
+ @source "../lib/**/*.{ts,js}";
1134
+
1135
+ ${uiThemeRaw}
1136
+
1137
+ /* App @theme mappings. The token VALUES live on :root in app/layout.ts (plain
1138
+ CSS custom properties, so they resolve with JavaScript disabled); these
1139
+ @theme inline maps turn them into utilities (bg-primary, text-display, ...). */
1140
+ @theme inline {
1141
+ --color-border-strong: var(--border-strong);
1142
+ --color-primary-tint: var(--primary-tint);
1143
+ --font-sans: var(--font-sans);
1144
+ --font-serif: var(--font-serif);
1145
+ --font-mono: var(--font-mono);
1146
+ --text-display: clamp(2.6rem, 1.6rem + 3.2vw, 4.25rem);
1147
+ --text-h1: clamp(2rem, 1.5rem + 1.6vw, 2.85rem);
1148
+ --text-h2: clamp(1.35rem, 1.15rem + 0.7vw, 1.7rem);
1149
+ --text-lede: clamp(1.05rem, 0.95rem + 0.3vw, 1.2rem);
1150
+ --duration-fast: 140ms;
1151
+ --duration-slow: 380ms;
1152
+ }
1153
+ `);
1154
+
1155
+ await writeFile(join(appDir, 'app', 'layout.ts'), `import { html, cspNonce } from '@webjsdev/core';
1069
1156
  import '#components/theme-toggle.ts';
1070
1157
  // Webjs UI components are tiered:
1071
1158
  // - Tier 1 (button, card, input, label, alert, badge, separator, etc.) are
@@ -1080,21 +1167,18 @@ import '#components/theme-toggle.ts';
1080
1167
  // extra needs to be registered. Add Tier-2 imports as you 'webjs ui add'.
1081
1168
 
1082
1169
  /**
1083
- * Root layout: globals + chrome.
1170
+ * Root layout: globals + a minimal shell.
1084
1171
  *
1085
1172
  * Light DOM + Tailwind by default. Design tokens live in :root and are
1086
1173
  * mapped into the Tailwind palette via @theme, so classes like
1087
1174
  * text-foreground, bg-card, font-serif, duration-fast, text-display all work.
1088
1175
  *
1089
- * Nav + footer links repeat the same class bundle, so they're extracted
1090
- * into small JS helpers below. Each helper runs at SSR time inside
1091
- * html\\\`\\\`, producing static HTML in the response with no client runtime.
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.
1092
1180
  */
1093
1181
 
1094
- const navLink = (href: string, label: string) => html\`
1095
- <a href=\${href} class="text-muted-foreground no-underline font-medium text-[13px] leading-none tracking-[0.005em] transition-colors duration-fast hover:text-foreground">\${label}</a>
1096
- \`;
1097
-
1098
1182
  export default function RootLayout({ children }: { children: unknown }) {
1099
1183
  // Read the in-flight request's CSP nonce so the theme-detection
1100
1184
  // inline script below passes strict CSP (script-src 'nonce-...').
@@ -1111,7 +1195,8 @@ export default function RootLayout({ children }: { children: unknown }) {
1111
1195
  // Delete this IIFE, delete the dark and light style blocks below, and set
1112
1196
  // your palette once on the root selector. That removes the wiring so it
1113
1197
  // cannot fight your own colours (it will not override a plain root
1114
- // palette). The header-measure IIFE that follows is unrelated, keep it.
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).
1115
1200
  (function(){
1116
1201
  try {
1117
1202
  var mq = window.matchMedia('(prefers-color-scheme: light)');
@@ -1132,11 +1217,13 @@ export default function RootLayout({ children }: { children: unknown }) {
1132
1217
  } catch (_) {}
1133
1218
  })();
1134
1219
  // ===== end optional theme apparatus =====
1135
- // The header is position:fixed (not sticky): a sticky header flickers on
1136
- // iOS WebKit during a client-router nav. fixed leaves normal flow, so
1137
- // --header-h reserves its height for the content below. Measured here so
1138
- // it tracks the real (responsive) height; degrades fine with no JS via
1139
- // the :root default.
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.
1140
1227
  (function(){
1141
1228
  function measure(){
1142
1229
  try {
@@ -1153,34 +1240,31 @@ export default function RootLayout({ children }: { children: unknown }) {
1153
1240
  else measure();
1154
1241
  })();
1155
1242
  </script>
1156
- <script src="/public/tailwind-browser.js"></script>
1157
- <!--
1158
- Webjs UI theme. Design tokens (--color-primary,
1159
- --color-card, --radius, etc.) the ui-* components consume.
1160
- The same content is also at styles/globals.css. We inline it here so
1161
- the Tailwind browser runtime resolves the tokens without a build step.
1162
- Edit base palette via the :root / .dark blocks below.
1163
- -->
1164
- <style type="text/tailwindcss">
1165
- ${UI_THEME}
1166
- </style>
1167
- <style type="text/tailwindcss">
1168
- /* ONE theme, canonical shadcn-style tokens. The @webjsdev/ui theme above
1169
- provides the token STRUCTURE and the @theme inline mappings that generate
1243
+ <!-- Tailwind: a STATIC stylesheet compiled from public/input.css to
1244
+ public/tailwind.css by css:build (run automatically by the dev and start
1245
+ tasks; in dev it is also recompiled on request when a source changes, so
1246
+ it never goes stale). A real stylesheet, so the app is fully styled with
1247
+ JavaScript DISABLED (no in-browser compile). -->
1248
+ <link rel="stylesheet" href="/public/tailwind.css">
1249
+ <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
1170
1254
  bg-background, text-foreground, bg-card, bg-primary, bg-accent,
1171
1255
  text-muted-foreground, border-border, ring-ring, and the rest. Here we
1172
1256
  set those tokens' VALUES to this app's brand palette, so the ui-*
1173
- components AND the example chrome read ONE source of truth. Any component
1174
- added later with webjs ui add <name> inherits it automatically. This
1175
- block is emitted after the ui theme, so these values win on every Tailwind
1176
- recompile. Dark-first, with light via the theme toggle (data-theme) or the
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
1177
1261
  OS. Follows the shadcn model: --primary is the BRAND color (orange, used
1178
1262
  for primary buttons, links, and emphasis), while --accent stays a NEUTRAL
1179
1263
  hover tint so the ui kit's outline/ghost/dropdown hover states keep proper
1180
1264
  contrast. Use bg-primary / text-primary for brand, bg-accent for hovers.
1181
- Add a new design token the canonical way, a --x variable below plus a
1182
- --color-x: var(--x) line in the @theme inline block, then use it as bg-x /
1183
- text-x. Reach for opacity modifiers
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
1184
1268
  (bg-primary/10, hover:bg-primary/90, border-border/60) before inventing a
1185
1269
  new token, but keep body text at full opacity so it stays above the AA
1186
1270
  contrast floor (a faded text-muted-foreground/70 measured 3.83:1). */
@@ -1188,11 +1272,16 @@ ${UI_THEME}
1188
1272
  --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1189
1273
  --font-serif: ui-serif, 'Iowan Old Style', Palatino, Georgia, serif;
1190
1274
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
1191
- --header-h: 56px;
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. */
1279
+ --header-h: 0px;
1192
1280
  /* A translucent brand tint, derived from --primary so it tracks
1193
1281
  light/dark automatically. Used for the logo glow and focus ring. */
1194
1282
  --primary-tint: color-mix(in oklch, var(--primary) 20%, transparent);
1195
1283
  }
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. */
1196
1285
  /* dark (the default, and the explicit .dark the toggle sets) */
1197
1286
  :root, .dark {
1198
1287
  color-scheme: dark;
@@ -1270,22 +1359,6 @@ ${UI_THEME}
1270
1359
  --logo-to: oklch(0.44 0.11 52);
1271
1360
  }
1272
1361
  }
1273
- @theme inline {
1274
- /* Only tokens the @webjsdev/ui theme does not already map live here. It
1275
- already maps --color-background/foreground/card/primary/secondary/
1276
- muted/accent/border/input/ring/destructive. */
1277
- --color-border-strong: var(--border-strong);
1278
- --color-primary-tint: var(--primary-tint);
1279
- --font-sans: var(--font-sans);
1280
- --font-serif: var(--font-serif);
1281
- --font-mono: var(--font-mono);
1282
- --text-display: clamp(2.6rem, 1.6rem + 3.2vw, 4.25rem);
1283
- --text-h1: clamp(2rem, 1.5rem + 1.6vw, 2.85rem);
1284
- --text-h2: clamp(1.35rem, 1.15rem + 0.7vw, 1.7rem);
1285
- --text-lede: clamp(1.05rem, 0.95rem + 0.3vw, 1.2rem);
1286
- --duration-fast: 140ms;
1287
- --duration-slow: 380ms;
1288
- }
1289
1362
  </style>
1290
1363
  <style>
1291
1364
  /* Base styles utility classes can't reach. */
@@ -1300,42 +1373,10 @@ ${UI_THEME}
1300
1373
  ::selection { background: color-mix(in oklch, var(--primary) 22%, transparent); color: var(--foreground); }
1301
1374
  </style>
1302
1375
 
1303
- <header class="fixed inset-x-0 top-0 z-20 flex items-center gap-6 px-4 sm:px-6 py-3 border-b border-border bg-[color-mix(in_oklch,var(--background)_75%,transparent)] backdrop-blur-[18px]">
1304
- <a href="/" class="mr-auto inline-flex items-center gap-2 no-underline text-foreground font-semibold text-[15px] leading-none tracking-tight">
1305
- <span>${displayName}</span>
1306
- </a>
1307
- <nav class="flex gap-4 items-center">
1308
- <!-- Example nav. Replace with the real navigation for your app. -->
1309
- \${navLink('/', 'Home')}
1310
- <theme-toggle></theme-toggle>
1311
- </nav>
1312
- </header>
1313
-
1314
- <!--
1315
- Content shell. The max-w-[760px] cap is a comfortable READING width,
1316
- right for prose, forms, and marketing. For a full-bleed app, dashboard,
1317
- or board, REPLACE it: widen the cap (for example max-w-[1400px]) or
1318
- drop the cap and mx-auto for an edge-to-edge layout. A wide layout left
1319
- inside the 760px reading column overflows into a horizontal scrollbar.
1320
- -->
1321
- <div class="flex flex-col min-h-[calc(100dvh-var(--header-h))]">
1322
- <main class="flex-1 w-full max-w-[760px] mx-auto px-4 sm:px-6 pt-[72px] pb-12">
1323
- \${children}
1324
- </main>
1325
- <!-- webjs-scaffold-placeholder. This "Built with webjs" footer is SCAFFOLD
1326
- branding, not your app's. REMOVE it, or replace it with your own
1327
- footer, before shipping a delivered app. Delete this line once done.
1328
- webjs check fails while the marker remains. -->
1329
- <footer class="border-t border-border">
1330
- <div class="max-w-[760px] mx-auto px-4 sm:px-6 py-6 flex items-center justify-center">
1331
- <a href="https://webjs.dev" class="inline-flex items-center gap-2 no-underline text-sm text-muted-foreground hover:text-foreground transition-colors">
1332
- <span>Built with</span>
1333
- <span class="w-[18px] h-[18px] rounded-[6px] bg-gradient-to-br from-[var(--logo-from)] to-[var(--logo-to)] shadow-[0_2px_10px_var(--primary-tint)]"></span>
1334
- <span class="font-semibold text-foreground">webjs</span>
1335
- </a>
1336
- </div>
1337
- </footer>
1338
- </div>
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
+ <main class="min-h-dvh px-4 sm:px-6 py-8">
1378
+ \${children}
1379
+ </main>
1339
1380
  \`;
1340
1381
  }
1341
1382
  `);
@@ -1406,9 +1447,9 @@ export default function Home() {
1406
1447
  \${rubric('welcome')}
1407
1448
  \${displayH1(html\`Hello from <span class="text-primary italic">${displayName}</span>.\`)}
1408
1449
  <p class="text-lede leading-[1.5] text-muted-foreground max-w-[56ch] m-0 mb-6">
1409
- This scaffold ships a gallery below: single-feature demos and one whole
1410
- example app, all small, idiomatic, and heavily commented. Browse them for
1411
- context, then replace this page with your own. See
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
1412
1453
  \${accentLink('https://docs.webjs.dev', 'the docs')} for the full reference.
1413
1454
  </p>
1414
1455
  <div class="flex gap-3 items-center">
@@ -1669,7 +1710,7 @@ ThemeToggle.register('theme-toggle');
1669
1710
  components/theme-toggle.ts ← light DOM web component
1670
1711
  lib/utils/cn.ts ← cn() helper for ui-* components
1671
1712
  lib/utils/ui.ts ← Tailwind class-bundle helpers
1672
- public/tailwind-browser.js ← Tailwind runtime
1713
+ public/input.css ← Tailwind entry (compiled to public/tailwind.css)
1673
1714
  modules/{components,server-actions,optimistic-ui,async-render,
1674
1715
  directives,todo}/ ← feature + example logic (prune what you skip)
1675
1716
  db/{schema,columns,connection}.server.ts ← Drizzle (User + Todo)
@@ -0,0 +1,34 @@
1
+ // Actionable hints for `webjs db` failure modes that drizzle-kit surfaces
2
+ // opaquely. Kept as pure functions so the dispatch path in bin/webjs.js stays a
3
+ // thin spawn and the messages are unit-testable without a child process.
4
+
5
+ // drizzle-kit prints this on stderr when a rename prompt has no TTY to answer.
6
+ // It is the reliable signal: this drizzle-kit version EXITS 0 on that failure,
7
+ // so the exit code cannot be used, and keying on stderr also means an unrelated
8
+ // generate failure (a schema type error, a missing config) does NOT misfire.
9
+ const TTY_PROMPT = /require a tty|interactive prompt/i;
10
+
11
+ /**
12
+ * `webjs db generate` off a non-interactive stdin dead-ends when a table is
13
+ * renamed or swapped: drizzle-kit asks "is <newTable> a rename of <oldTable>?"
14
+ * and, with no TTY to answer, prints "Interactive prompts require a TTY" to
15
+ * stderr (and exits 0). Returns the escape-hatch hint only when the captured
16
+ * stderr carries that signature, so it fires on exactly that case and stays
17
+ * silent on success, an interactive run, another subcommand, or an unrelated
18
+ * generate error.
19
+ *
20
+ * @param {string} sub the `webjs db` subcommand (generate|migrate|push|studio)
21
+ * @param {boolean|undefined} isTTY process.stdin.isTTY
22
+ * @param {string|undefined} stderr drizzle-kit's captured stderr
23
+ * @returns {string|null}
24
+ */
25
+ export function dbGenerateTtyHint(sub, isTTY, stderr) {
26
+ if (sub !== 'generate' || isTTY) return null;
27
+ if (!TTY_PROMPT.test(stderr || '')) return null;
28
+ return (
29
+ '\nwebjs db generate needs an interactive terminal to resolve a table rename.\n' +
30
+ 'Run it in a real terminal to answer the prompt, or, if the dev database has no\n' +
31
+ 'data yet, delete the db/migrations/<initial> folder and re-run to author a clean\n' +
32
+ 'create-table migration.'
33
+ );
34
+ }
@@ -0,0 +1,67 @@
1
+ // The design bar the scaffold sets (AGENTS.md / CONVENTIONS.md item 6): a
2
+ // delivered UI app must have its OWN design, not the scaffold's. The scaffold is
3
+ // a teaching artifact for how to USE the framework, never a starting design.
4
+ // This lives in one place so the `--clear-placeholders` reminder and the
5
+ // `webjs doctor` advisory speak with one voice (the clear command strips the
6
+ // layout marker that carried this reminder just-in-time, so it is re-surfaced
7
+ // there, and doctor catches an app that kept the shell anyway).
8
+
9
+ import { existsSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+
12
+ /**
13
+ * True when the app has a root layout, i.e. it is a UI app the design bar
14
+ * applies to. The `api` template ships no `app/layout`, so the reminder /
15
+ * advisory stay quiet there.
16
+ * @param {string} appDir
17
+ * @returns {boolean}
18
+ */
19
+ export function hasUiLayout(appDir) {
20
+ return ['ts', 'js', 'mts', 'mjs'].some((e) => existsSync(join(appDir, 'app', `layout.${e}`)));
21
+ }
22
+
23
+ export const DESIGN_REMINDER =
24
+ '\nDesign: the scaffold is a TEACHING artifact, not a starting design.\n' +
25
+ 'A delivered UI app must have its OWN design chosen from what the app IS:\n' +
26
+ 'layout, palette, typography, icons, spacing, and chrome.\n' +
27
+ '- Layout: app/layout.ts ships as a MINIMAL shell (no header / nav / footer /\n' +
28
+ ' reading column). Design your own from what the app IS. LAYOUT-REFERENCE.md\n' +
29
+ ' shows the mechanics; do not reproduce its example header verbatim.\n' +
30
+ '- Palette: the design-token NAMES (--background, --primary, --card, ...) are\n' +
31
+ ' infrastructure to keep, but their COLOR VALUES are yours. Set a distinctive\n' +
32
+ ' palette that fits the app; keeping the starter orange is not a redesign.\n' +
33
+ '- Verify by USING it: render the app and play through every state, and confirm\n' +
34
+ ' nothing resizes or shifts as it fills (even, stable cells). A glance at the\n' +
35
+ ' empty first paint is not enough; the layout bugs show up mid-interaction.\n' +
36
+ 'See AGENTS.md / CONVENTIONS.md item 6.';
37
+
38
+ // Distinctive strings that indicate an app kept scaffold-specific chrome or the
39
+ // unmodified starter palette, rather than designing its own. Counting them is an
40
+ // objective proxy for "did not own the design" without judging taste. NOTE: the
41
+ // theme apparatus (`--header-h`, the `theme-toggle` import) is KEEP-infrastructure
42
+ // the minimal shell ships in every app, so it is NOT a tell (it fired on every
43
+ // finished app and made the advisory nag forever). The tells that remain are
44
+ // genuine "reproduced the scaffold" signals: the exact 760px reading column, the
45
+ // scaffold's own attribution footer, and the two exact default palette VALUES (a
46
+ // verbatim match means the palette was never changed; a recolor does not match).
47
+ const SHELL_TELLS = [
48
+ { key: 'reading-column (max-w-[760px])', re: /max-w-\[760px\]/ },
49
+ // Specific to the scaffold's own attribution (its footer links webjs.dev and
50
+ // says "Built with webjs"). A bare "Built with ..." is a common bespoke footer,
51
+ // so it is NOT a tell on its own.
52
+ { key: 'attribution footer', re: /webjs\.dev|Built with webjs/ },
53
+ { key: 'default scaffold primary color', re: /--primary:\s*oklch\(0\.7\s+0\.16\s+52\)/ },
54
+ { key: 'default scaffold card color', re: /--card:\s*oklch\(0\.18\s+0\.01\s+55\)/ },
55
+ ];
56
+
57
+ /**
58
+ * The scaffold-shell tells present in a root-layout source string. Two or more
59
+ * is a strong signal the app kept the scaffold chrome instead of designing its
60
+ * own. Returns the human-readable keys that matched.
61
+ * @param {string} layoutSrc
62
+ * @returns {string[]}
63
+ */
64
+ export function scaffoldShellTells(layoutSrc) {
65
+ if (typeof layoutSrc !== 'string' || !layoutSrc) return [];
66
+ return SHELL_TELLS.filter((t) => t.re.test(layoutSrc)).map((t) => t.key);
67
+ }