@webjsdev/cli 0.10.37 → 0.10.39

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,16 @@ 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 / parallel 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`;
356
+ const cssWatchCmd = `${twBin} -i ./public/input.css -o ./public/tailwind.css --watch`;
347
357
  const appDir = join(cwd, name);
348
358
  if (existsSync(appDir)) {
349
359
  console.error(`Error: directory '${name}' already exists.`);
@@ -399,6 +409,13 @@ export async function scaffoldApp(name, cwd, opts = {}) {
399
409
  // / doctor) stay plain `webjs ...`: they spawn node tooling (`node --test`,
400
410
  // drizzle-kit, tsc) and forcing `--bun` there buys nothing (and `webjs
401
411
  // test` shells `node --test`, which a `bun --test` would not be).
412
+ // Compile Tailwind from public/input.css to a STATIC public/tailwind.css
413
+ // that app/layout.ts links, so the app is fully styled with JavaScript
414
+ // DISABLED (a real stylesheet, not an in-browser compile). Runs inside the
415
+ // dev and start tasks via the `before` hooks below. Runtime-aware: a Bun
416
+ // app runs the compiler under Bun (its image has no Node), a Node app runs
417
+ // it directly.
418
+ ...(isApi ? {} : { 'css:build': cssBuildCmd }),
402
419
  dev: isBun ? 'bun --bun webjs dev' : 'webjs dev',
403
420
  start: isBun ? 'bun --bun webjs start' : 'webjs start',
404
421
  test: 'webjs test',
@@ -447,6 +464,10 @@ export async function scaffoldApp(name, cwd, opts = {}) {
447
464
  // assertNoA11yViolations() test helper from @webjsdev/core/testing.
448
465
  // Test-only: dynamically imported, never shipped to the app runtime.
449
466
  'axe-core': '^4.10.0',
467
+ // The Tailwind v4 CLI that css:build runs to compile public/input.css into
468
+ // the static public/tailwind.css the layout links. UI templates only (the
469
+ // api template has no CSS). Build tooling, never shipped to the runtime.
470
+ ...(isApi ? {} : { '@tailwindcss/cli': '^4.1.0' }),
450
471
  // tsserver plugin, wired into tsconfig below. Gives the language
451
472
  // INTELLIGENCE (go-to-def, completions, diagnostics, hover inside html``
452
473
  // templates) in any tsserver editor with NO editor plugin installed,
@@ -466,13 +487,19 @@ export async function scaffoldApp(name, cwd, opts = {}) {
466
487
  // `before` and run it in-process, so `npm run dev` / `start` (thin aliases
467
488
  // above) behave identically. Both apply pending migrations via `webjs db
468
489
  // 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`.
490
+ // generated migration is applied without a manual step (#725). For a UI
491
+ // template it ALSO compiles Tailwind in `before` so a freshly cloned app is
492
+ // styled on the very first boot with no manual step, and runs the Tailwind
493
+ // `--watch` under `parallel` for live recompiles in dev. The compile command
494
+ // is the runtime-aware `cssBuildCmd` (a Bun app runs it under Bun, since its
495
+ // image has no npm or Node), NOT `npm run css:build`. The api template has no
496
+ // CSS, so it gets neither.
473
497
  webjs: {
474
- dev: { before: ['webjs db migrate'] },
475
- start: { before: ['webjs db migrate'] },
498
+ dev: {
499
+ before: isApi ? ['webjs db migrate'] : ['webjs db migrate', cssBuildCmd],
500
+ ...(isApi ? {} : { parallel: [cssWatchCmd] }),
501
+ },
502
+ start: { before: isApi ? ['webjs db migrate'] : ['webjs db migrate', cssBuildCmd] },
476
503
  },
477
504
  }, null, 2) + '\n');
478
505
 
@@ -534,6 +561,11 @@ export async function scaffoldApp(name, cwd, opts = {}) {
534
561
  'AGENTS.md',
535
562
  'CONVENTIONS.md',
536
563
  'CLAUDE.md',
564
+ // A worked layout (header/nav/theme-toggle/reading-column/footer) the agent
565
+ // reads to learn the patterns, since app/layout.ts ships as a minimal shell
566
+ // so the app designs its own chrome. Shipped for every template (harmless
567
+ // for api, which has no app/layout.ts).
568
+ 'LAYOUT-REFERENCE.md',
537
569
  // Starter tests under the new feature-folder layout.
538
570
  'test/hello/hello.test.ts',
539
571
  'test/hello/browser/hello.test.js',
@@ -563,6 +595,14 @@ export async function scaffoldApp(name, cwd, opts = {}) {
563
595
  '.claude/hooks/require-tests-with-src.sh',
564
596
  '.claude/hooks/check-server-imports.sh',
565
597
  '.claude/hooks/check-server-imports.mjs',
598
+ // Render-and-look enforcement for UI work: a UserPromptSubmit router that
599
+ // points UI-building prompts at the webjs-design-review skill, a Stop-hook
600
+ // backstop that nudges a render-and-look before finishing UI changes, and
601
+ // the skill they route to. A design/layout defect has no failing test, so
602
+ // this vision-in-the-loop is the only thing that catches it.
603
+ '.claude/hooks/route-skills.sh',
604
+ '.claude/hooks/design-review-before-stop.sh',
605
+ '.claude/skills/webjs-design-review/SKILL.md',
566
606
  // Gemini CLI config + hooks
567
607
  '.gemini/settings.json',
568
608
  '.gemini/hooks/nudge-uncommitted.sh',
@@ -638,7 +678,7 @@ export async function scaffoldApp(name, cwd, opts = {}) {
638
678
 
639
679
  // Make hook scripts executable
640
680
  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']) {
681
+ for (const hook of ['block-prose-punctuation.sh', 'guard-branch-context.sh', 'nudge-uncommitted.sh', 'commit-before-stop.sh', 'cleanup-merged-worktree.sh', 'require-tests-with-src.sh', 'route-skills.sh', 'design-review-before-stop.sh']) {
642
682
  const hookPath = join(appDir, '.claude', 'hooks', hook);
643
683
  if (existsSync(hookPath)) await chmod(hookPath, 0o755);
644
684
  }
@@ -675,6 +715,9 @@ export const table = sqliteTableCreator((name) => name, 'snake_case');
675
715
  export const pk = () => integer().primaryKey({ autoIncrement: true });
676
716
  export const uuidPk = () => text().primaryKey().$defaultFn(() => crypto.randomUUID());
677
717
  export const uuid = () => text();
718
+ // Structured value (array / object) stored as JSON. Type it with json<T>() so
719
+ // the column is narrowed on read/write instead of \`unknown\`.
720
+ export const json = <T>() => text({ mode: 'json' }).$type<T>();
678
721
  export const bool = () => integer({ mode: 'boolean' });
679
722
  export const timestamp = () => integer({ mode: 'timestamp_ms' });
680
723
  export const createdAt = () => timestamp().notNull().defaultNow();
@@ -686,7 +729,7 @@ export const index = (...cols: SQLiteColumn[]) =>
686
729
  _index(getTableName((cols[0] as unknown as { table: Table }).table) + '_' + cols.map((c) => c.name).join('_') + '_idx').on(...(cols as [SQLiteColumn, ...SQLiteColumn[]]));
687
730
  `;
688
731
 
689
- const columnsPg = `import { pgTableCreator, serial, uuid as pgUuid, integer, text, real, boolean, timestamp as pgTimestamp, index as _index } from 'drizzle-orm/pg-core';
732
+ 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
733
  import type { PgColumn } from 'drizzle-orm/pg-core';
691
734
  import { getTableName, type Table } from 'drizzle-orm';
692
735
 
@@ -697,6 +740,9 @@ export const table = pgTableCreator((name) => name, 'snake_case');
697
740
  export const pk = () => serial().primaryKey();
698
741
  export const uuidPk = () => pgUuid().primaryKey().defaultRandom();
699
742
  export const uuid = () => pgUuid();
743
+ // Structured value (array / object) stored as JSON. Type it with json<T>() so
744
+ // the column is narrowed on read/write instead of \`unknown\`.
745
+ export const json = <T>() => jsonb().$type<T>();
700
746
  export const bool = () => boolean();
701
747
  export const timestamp = () => pgTimestamp({ withTimezone: true });
702
748
  export const createdAt = () => timestamp().notNull().defaultNow();
@@ -710,13 +756,16 @@ export const index = (...cols: PgColumn[]) =>
710
756
 
711
757
  // Example schema (dialect-agnostic). Replace the User model with your own.
712
758
  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';
759
+ import { table, pk, ${isFullStack ? 'uuidPk, ' : ''}text, ${isFullStack ? 'bool, ' : ''}json, createdAt } from './columns.server.ts';
714
760
 
715
761
  // Example model. Feel free to delete or extend.
716
762
  export const users = table('users', {
717
763
  id: pk(),
718
764
  email: text().notNull().unique(),
719
765
  name: text(),
766
+ // JSON column: a structured value persisted as JSON, typed via json<T>().
767
+ // Same helper works on SQLite and Postgres. Delete if you do not need it.
768
+ settings: json<{ theme?: string }>(),
720
769
  createdAt: createdAt(),
721
770
  });
722
771
  ${isFullStack ? `
@@ -998,15 +1047,13 @@ export type ActionResult<T> =
998
1047
  if (!isApi) {
999
1048
  // Full-stack and SaaS templates: layout + page + theme toggle + Tailwind
1000
1049
 
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.
1050
+ // The Tailwind stylesheet is compiled from public/input.css (written below)
1051
+ // to a STATIC public/tailwind.css by css:build, and lib/utils/ui.ts helpers
1052
+ // are copied below, so the app boots with the exact blog example
1053
+ // architecture: light DOM + a real Tailwind stylesheet (styled with JS off)
1054
+ // + JS helpers.
1004
1055
  const publicDir = join(appDir, 'public');
1005
1056
  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
1057
  // Progressive-enhancement service worker (#271): ship the opt-in offline
1011
1058
  // primitive (the worker + its offline fallback) into the UI scaffolds
1012
1059
  // (full-stack / saas; this block is api-excluded since api has no UI).
@@ -1051,21 +1098,46 @@ export type ActionResult<T> =
1051
1098
  // use. See CONVENTIONS.md "prune what the app does not use".
1052
1099
  if (!isApi) await copyGallery(appDir);
1053
1100
 
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';
1101
+ // The @webjsdev/ui theme (`--color-primary`, `--color-card`, the @theme maps,
1102
+ // @custom-variant, @keyframes) plus the app @theme mappings are compiled from
1103
+ // public/input.css into the STATIC public/tailwind.css that app/layout.ts
1104
+ // links, so the app is fully styled with JavaScript disabled. Write input.css:
1105
+ // `@import "tailwindcss"`, source globs, the ui theme (read from the registry
1106
+ // themes/index.css), then the app @theme inline block. The token VALUES live
1107
+ // on :root in app/layout.ts (plain CSS, so they resolve with JS off) and the
1108
+ // @theme inline maps here reference them by var(), the same split the blog
1109
+ // uses. Same theme also lives at styles/globals.css for `webjsui` tooling.
1110
+ const uiThemeRaw = await readThemeCss();
1111
+ await writeFile(join(publicDir, 'input.css'), `@import "tailwindcss";
1112
+
1113
+ /* Scan app sources so their utility classes make it into the compiled bundle.
1114
+ Tailwind v4 auto-scans the project too; these @source lines are explicit. */
1115
+ @source "../app/**/*.{ts,js}";
1116
+ @source "../components/**/*.{ts,js}";
1117
+ @source "../modules/**/*.{ts,js}";
1118
+ @source "../lib/**/*.{ts,js}";
1119
+
1120
+ ${uiThemeRaw}
1121
+
1122
+ /* App @theme mappings. The token VALUES live on :root in app/layout.ts (plain
1123
+ CSS custom properties, so they resolve with JavaScript disabled); these
1124
+ @theme inline maps turn them into utilities (bg-primary, text-display, ...). */
1125
+ @theme inline {
1126
+ --color-border-strong: var(--border-strong);
1127
+ --color-primary-tint: var(--primary-tint);
1128
+ --font-sans: var(--font-sans);
1129
+ --font-serif: var(--font-serif);
1130
+ --font-mono: var(--font-mono);
1131
+ --text-display: clamp(2.6rem, 1.6rem + 3.2vw, 4.25rem);
1132
+ --text-h1: clamp(2rem, 1.5rem + 1.6vw, 2.85rem);
1133
+ --text-h2: clamp(1.35rem, 1.15rem + 0.7vw, 1.7rem);
1134
+ --text-lede: clamp(1.05rem, 0.95rem + 0.3vw, 1.2rem);
1135
+ --duration-fast: 140ms;
1136
+ --duration-slow: 380ms;
1137
+ }
1138
+ `);
1139
+
1140
+ await writeFile(join(appDir, 'app', 'layout.ts'), `import { html, cspNonce } from '@webjsdev/core';
1069
1141
  import '#components/theme-toggle.ts';
1070
1142
  // Webjs UI components are tiered:
1071
1143
  // - Tier 1 (button, card, input, label, alert, badge, separator, etc.) are
@@ -1080,21 +1152,18 @@ import '#components/theme-toggle.ts';
1080
1152
  // extra needs to be registered. Add Tier-2 imports as you 'webjs ui add'.
1081
1153
 
1082
1154
  /**
1083
- * Root layout: globals + chrome.
1155
+ * Root layout: globals + a minimal shell.
1084
1156
  *
1085
1157
  * Light DOM + Tailwind by default. Design tokens live in :root and are
1086
1158
  * mapped into the Tailwind palette via @theme, so classes like
1087
1159
  * text-foreground, bg-card, font-serif, duration-fast, text-display all work.
1088
1160
  *
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.
1161
+ * This shell is deliberately MINIMAL: it wires the theme, tokens, and Tailwind,
1162
+ * then renders \${children} in a bare container with no chrome, so you design the
1163
+ * app's own layout. LAYOUT-REFERENCE.md (project root) is a complete worked
1164
+ * layout (header, nav, theme toggle, reading column, footer) to learn from.
1092
1165
  */
1093
1166
 
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
1167
  export default function RootLayout({ children }: { children: unknown }) {
1099
1168
  // Read the in-flight request's CSP nonce so the theme-detection
1100
1169
  // inline script below passes strict CSP (script-src 'nonce-...').
@@ -1111,7 +1180,8 @@ export default function RootLayout({ children }: { children: unknown }) {
1111
1180
  // Delete this IIFE, delete the dark and light style blocks below, and set
1112
1181
  // your palette once on the root selector. That removes the wiring so it
1113
1182
  // cannot fight your own colours (it will not override a plain root
1114
- // palette). The header-measure IIFE that follows is unrelated, keep it.
1183
+ // palette). The header-measure IIFE that follows is unrelated, keep it
1184
+ // (it is dormant until you add a fixed header, per LAYOUT-REFERENCE.md).
1115
1185
  (function(){
1116
1186
  try {
1117
1187
  var mq = window.matchMedia('(prefers-color-scheme: light)');
@@ -1132,11 +1202,13 @@ export default function RootLayout({ children }: { children: unknown }) {
1132
1202
  } catch (_) {}
1133
1203
  })();
1134
1204
  // ===== 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.
1205
+ // Header-measure script: DORMANT until you add a fixed header (the minimal
1206
+ // shell has none). When you add one (see LAYOUT-REFERENCE.md), make it
1207
+ // position:fixed (NOT sticky: a sticky header flickers on iOS WebKit during
1208
+ // a client-router nav). fixed leaves normal flow, so --header-h reserves its
1209
+ // height for the content below; this measures the real (responsive) height.
1210
+ // With no header it is a no-op (querySelector returns null) and --header-h
1211
+ // stays 0, so there is no phantom gap.
1140
1212
  (function(){
1141
1213
  function measure(){
1142
1214
  try {
@@ -1153,34 +1225,30 @@ export default function RootLayout({ children }: { children: unknown }) {
1153
1225
  else measure();
1154
1226
  })();
1155
1227
  </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
1228
+ <!-- Tailwind: a STATIC stylesheet compiled from public/input.css to
1229
+ public/tailwind.css by css:build / css:watch (run automatically by the
1230
+ dev and start tasks). A real stylesheet, so the app is fully styled with
1231
+ JavaScript DISABLED (no in-browser compile). -->
1232
+ <link rel="stylesheet" href="/public/tailwind.css">
1233
+ <style>
1234
+ /* ONE theme, canonical shadcn-style tokens. These are the token VALUES as
1235
+ plain CSS custom properties, so the palette resolves with JavaScript
1236
+ DISABLED and needs no build. public/input.css holds the token STRUCTURE
1237
+ and the @theme inline mappings that generate
1170
1238
  bg-background, text-foreground, bg-card, bg-primary, bg-accent,
1171
1239
  text-muted-foreground, border-border, ring-ring, and the rest. Here we
1172
1240
  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
1241
+ components AND your own chrome read ONE source of truth. Any component
1242
+ added later with webjs ui add <name> inherits it automatically. The
1243
+ stylesheet is linked before this block, so these VALUES win over the ui
1244
+ theme defaults. Dark-first, with light via the theme toggle (data-theme) or the
1177
1245
  OS. Follows the shadcn model: --primary is the BRAND color (orange, used
1178
1246
  for primary buttons, links, and emphasis), while --accent stays a NEUTRAL
1179
1247
  hover tint so the ui kit's outline/ghost/dropdown hover states keep proper
1180
1248
  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
1249
+ Add a new design token the canonical way, a --x VALUE below plus a
1250
+ --color-x: var(--x) line in the @theme inline block in public/input.css,
1251
+ then use it as bg-x / text-x. Reach for opacity modifiers
1184
1252
  (bg-primary/10, hover:bg-primary/90, border-border/60) before inventing a
1185
1253
  new token, but keep body text at full opacity so it stays above the AA
1186
1254
  contrast floor (a faded text-muted-foreground/70 measured 3.83:1). */
@@ -1188,11 +1256,16 @@ ${UI_THEME}
1188
1256
  --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1189
1257
  --font-serif: ui-serif, 'Iowan Old Style', Palatino, Georgia, serif;
1190
1258
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
1191
- --header-h: 56px;
1259
+ /* 0 by default because the minimal shell has no fixed header. The
1260
+ header-measure script below overrides it to the real height the moment
1261
+ you add a fixed header element (see LAYOUT-REFERENCE.md), so body
1262
+ padding tracks it automatically. */
1263
+ --header-h: 0px;
1192
1264
  /* A translucent brand tint, derived from --primary so it tracks
1193
1265
  light/dark automatically. Used for the logo glow and focus ring. */
1194
1266
  --primary-tint: color-mix(in oklch, var(--primary) 20%, transparent);
1195
1267
  }
1268
+ /* webjs-scaffold-placeholder. These are the scaffold's STARTER brand colors (the shadcn-style orange); they look finished on purpose. Own the palette: set the token VALUES below (dark AND light blocks) to colors that fit what THIS app IS, keeping the token NAMES. A recolor chosen for the app beats keeping the starter orange. Then delete this marker line (or run webjs check --clear-placeholders to keep the starter palette deliberately). webjs check fails while the marker remains. */
1196
1269
  /* dark (the default, and the explicit .dark the toggle sets) */
1197
1270
  :root, .dark {
1198
1271
  color-scheme: dark;
@@ -1270,22 +1343,6 @@ ${UI_THEME}
1270
1343
  --logo-to: oklch(0.44 0.11 52);
1271
1344
  }
1272
1345
  }
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
1346
  </style>
1290
1347
  <style>
1291
1348
  /* Base styles utility classes can't reach. */
@@ -1300,42 +1357,10 @@ ${UI_THEME}
1300
1357
  ::selection { background: color-mix(in oklch, var(--primary) 22%, transparent); color: var(--foreground); }
1301
1358
  </style>
1302
1359
 
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>
1360
+ <!-- webjs-scaffold-placeholder. MINIMAL SHELL, on purpose. Everything above (the theme apparatus, the design tokens, the linked Tailwind stylesheet) is infrastructure to keep. Below, \${children} drops into a bare full-height container with NO chrome: design THIS app's layout from scratch. Decide from what the app IS whether it needs a header, a nav, a footer, a sidebar, a centered reading column, or a full-bleed canvas, and build that here. A COMPLETE reference layout (fixed header, brand, nav, theme toggle, reading column, footer) ships at LAYOUT-REFERENCE.md in the project root: read it to learn the patterns, then write your own. Delete this line once your layout is designed. webjs check fails while the marker remains. -->
1361
+ <main class="min-h-dvh px-4 sm:px-6 py-8">
1362
+ \${children}
1363
+ </main>
1339
1364
  \`;
1340
1365
  }
1341
1366
  `);
@@ -1406,9 +1431,9 @@ export default function Home() {
1406
1431
  \${rubric('welcome')}
1407
1432
  \${displayH1(html\`Hello from <span class="text-primary italic">${displayName}</span>.\`)}
1408
1433
  <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
1434
+ This WebJs scaffold ships a gallery below: single-feature demos and one
1435
+ whole example app, all small, idiomatic, and heavily commented. Browse
1436
+ them for context, then replace this page with your own. See
1412
1437
  \${accentLink('https://docs.webjs.dev', 'the docs')} for the full reference.
1413
1438
  </p>
1414
1439
  <div class="flex gap-3 items-center">
@@ -1669,7 +1694,7 @@ ThemeToggle.register('theme-toggle');
1669
1694
  components/theme-toggle.ts ← light DOM web component
1670
1695
  lib/utils/cn.ts ← cn() helper for ui-* components
1671
1696
  lib/utils/ui.ts ← Tailwind class-bundle helpers
1672
- public/tailwind-browser.js ← Tailwind runtime
1697
+ public/input.css ← Tailwind entry (compiled to public/tailwind.css)
1673
1698
  modules/{components,server-actions,optimistic-ui,async-render,
1674
1699
  directives,todo}/ ← feature + example logic (prune what you skip)
1675
1700
  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
+ }