@webjsdev/cli 0.10.38 → 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/bin/webjs.js +116 -3
- package/lib/clear-placeholders.js +98 -0
- package/lib/create.js +156 -131
- package/lib/db-hints.js +34 -0
- package/lib/design-bar.js +67 -0
- package/lib/doctor.js +122 -4
- 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 +41 -24
- package/templates/CONVENTIONS.md +60 -29
- package/templates/LAYOUT-REFERENCE.md +96 -0
- 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,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).
|
|
470
|
-
//
|
|
471
|
-
//
|
|
472
|
-
// `--watch`
|
|
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: {
|
|
475
|
-
|
|
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
|
-
//
|
|
1002
|
-
//
|
|
1003
|
-
//
|
|
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
|
|
1055
|
-
//
|
|
1056
|
-
//
|
|
1057
|
-
//
|
|
1058
|
-
//
|
|
1059
|
-
//
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
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 +
|
|
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
|
-
*
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
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
|
-
//
|
|
1136
|
-
//
|
|
1137
|
-
//
|
|
1138
|
-
//
|
|
1139
|
-
// the
|
|
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
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
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
|
|
1174
|
-
added later with webjs ui add <name> inherits it automatically.
|
|
1175
|
-
|
|
1176
|
-
|
|
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
|
|
1182
|
-
--color-x: var(--x) line in the @theme inline block
|
|
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
|
-
|
|
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
|
-
|
|
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>
|
|
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
|
|
1410
|
-
example app, all small, idiomatic, and heavily commented. Browse
|
|
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/
|
|
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)
|
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
|
+
}
|