@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/bin/webjs.js +116 -3
- package/lib/app-tasks.js +13 -7
- package/lib/clear-placeholders.js +98 -0
- package/lib/create.js +172 -131
- package/lib/db-hints.js +34 -0
- package/lib/design-bar.js +67 -0
- package/lib/doctor.js +208 -6
- package/lib/runtime-rewrite.js +4 -3
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +33 -15
- package/templates/.claude/hooks/design-review-before-stop.sh +36 -0
- package/templates/.claude/hooks/route-skills.sh +35 -0
- package/templates/.claude/settings.json +14 -0
- package/templates/.claude/skills/webjs-design-review/SKILL.md +84 -0
- package/templates/.cursorrules +33 -15
- package/templates/.github/copilot-instructions.md +33 -15
- package/templates/AGENTS.md +61 -34
- package/templates/CONVENTIONS.md +60 -29
- package/templates/LAYOUT-REFERENCE.md +96 -0
- package/templates/gallery/modules/async-render/components/server-clock.ts +7 -6
- package/templates/public/tailwind-browser.js +0 -947
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
|
|
237
|
-
//
|
|
238
|
-
//
|
|
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
|
|
248
|
-
*
|
|
249
|
-
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
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).
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
//
|
|
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: {
|
|
475
|
-
|
|
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
|
-
//
|
|
1002
|
-
//
|
|
1003
|
-
//
|
|
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
|
|
1055
|
-
//
|
|
1056
|
-
//
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
1059
|
-
//
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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 +
|
|
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
|
-
*
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
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
|
-
//
|
|
1136
|
-
//
|
|
1137
|
-
//
|
|
1138
|
-
//
|
|
1139
|
-
// the
|
|
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
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
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
|
|
1174
|
-
added later with webjs ui add <name> inherits it automatically.
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
|
1182
|
-
--color-x: var(--x) line in the @theme inline block
|
|
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
|
-
|
|
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
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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
|
|
1410
|
-
example app, all small, idiomatic, and heavily commented. Browse
|
|
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/
|
|
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)
|
package/lib/db-hints.js
ADDED
|
@@ -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
|
+
}
|