@webjsdev/cli 0.10.40 → 0.10.41
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 +4 -46
- package/lib/create.js +282 -479
- package/lib/doctor.js +1 -38
- package/package.json +5 -1
- package/templates/.agents/rules/workflow.md +61 -271
- package/templates/.agents/skills/webjs/SKILL.md +226 -0
- package/templates/.agents/skills/webjs/references/auth-and-sessions.md +220 -0
- package/templates/.agents/skills/webjs/references/built-ins.md +200 -0
- package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +204 -0
- package/templates/.agents/skills/webjs/references/components.md +167 -0
- package/templates/.agents/skills/webjs/references/data-and-actions.md +187 -0
- package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +170 -0
- package/templates/.agents/skills/webjs/references/optimistic-ui.md +128 -0
- package/templates/.agents/skills/webjs/references/routing-and-pages.md +158 -0
- package/templates/.agents/skills/webjs/references/runtime.md +80 -0
- package/templates/.agents/skills/webjs/references/service-worker.md +78 -0
- package/templates/.agents/skills/webjs/references/styling.md +123 -0
- package/templates/.agents/skills/webjs/references/testing.md +125 -0
- package/templates/.agents/skills/webjs/references/typescript.md +148 -0
- package/templates/.claude/hooks/check-server-imports.mjs +1 -1
- package/templates/.claude/hooks/require-tests-with-src.sh +1 -1
- package/templates/.claude/settings.json +0 -14
- package/templates/.cursorrules +21 -189
- package/templates/.github/copilot-instructions.md +7 -185
- package/templates/.github/pull_request_template.md +1 -1
- package/templates/AGENTS.md +59 -1494
- package/templates/CLAUDE.md +0 -1
- package/templates/CONVENTIONS.md +32 -1383
- package/templates/GEMINI.md +11 -0
- package/templates/gallery/app/apple-icon.ts +0 -1
- package/templates/gallery/app/examples/todo/page.ts +0 -1
- package/templates/gallery/app/features/async-render/page.ts +0 -1
- package/templates/gallery/app/features/boundaries/page.ts +0 -1
- package/templates/gallery/app/features/broadcast/page.ts +0 -1
- package/templates/gallery/app/features/caching/page.ts +0 -1
- package/templates/gallery/app/features/client-router/page.ts +0 -1
- package/templates/gallery/app/features/client-router/second/page.ts +0 -1
- package/templates/gallery/app/features/components/page.ts +0 -1
- package/templates/gallery/app/features/directives/page.ts +0 -1
- package/templates/gallery/app/features/env/page.ts +0 -1
- package/templates/gallery/app/features/file-storage/page.ts +0 -1
- package/templates/gallery/app/features/forms/page.ts +0 -1
- package/templates/gallery/app/features/metadata/page.ts +0 -1
- package/templates/gallery/app/features/optimistic-ui/page.ts +0 -1
- package/templates/gallery/app/features/rate-limit/page.ts +0 -1
- package/templates/gallery/app/features/route-handler/page.ts +0 -1
- package/templates/gallery/app/features/routing/page.ts +0 -1
- package/templates/gallery/app/features/server-actions/page.ts +0 -1
- package/templates/gallery/app/features/service-worker/page.ts +0 -1
- package/templates/gallery/app/features/sessions/page.ts +0 -1
- package/templates/gallery/app/features/websockets/page.ts +0 -1
- package/templates/gallery/app/global-error.ts +0 -1
- package/templates/gallery/app/global-not-found.ts +0 -1
- package/templates/gallery/app/icon.ts +0 -1
- package/templates/gallery/app/manifest.ts +0 -1
- package/templates/gallery/app/opengraph-image.ts +0 -1
- package/templates/gallery/app/robots.ts +0 -1
- package/templates/gallery/app/sitemap.ts +0 -1
- package/templates/gallery/app/twitter-image.ts +0 -1
- package/templates/public/favicon.svg +5 -0
- package/templates/public/sw.js +1 -1
- package/templates/scripts/clear-gallery.mjs +95 -0
- package/lib/clear-placeholders.js +0 -98
- package/lib/design-bar.js +0 -67
- package/templates/.claude/hooks/design-review-before-stop.sh +0 -36
- package/templates/.claude/hooks/route-skills.sh +0 -35
- package/templates/.claude/skills/webjs-design-review/SKILL.md +0 -84
- package/templates/LAYOUT-REFERENCE.md +0 -96
- package/templates/lib/utils/ui.ts +0 -83
package/lib/create.js
CHANGED
|
@@ -415,6 +415,8 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
415
415
|
// app runs the compiler under Bun (its image has no Node), a Node app runs
|
|
416
416
|
// it directly.
|
|
417
417
|
...(isApi ? {} : { 'css:build': cssBuildCmd }),
|
|
418
|
+
// Shed the demo gallery to a clean, buildable base (scripts/clear-gallery.mjs).
|
|
419
|
+
...(isApi ? {} : { 'gallery:clear': isBun ? 'bun scripts/clear-gallery.mjs' : 'node scripts/clear-gallery.mjs' }),
|
|
418
420
|
dev: isBun ? 'bun --bun webjs dev' : 'webjs dev',
|
|
419
421
|
start: isBun ? 'bun --bun webjs start' : 'webjs start',
|
|
420
422
|
test: 'webjs test',
|
|
@@ -573,36 +575,31 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
573
575
|
// --- Templates (AGENTS.md, CONVENTIONS.md, CLAUDE.md, test files, Claude hooks) ---
|
|
574
576
|
|
|
575
577
|
const templateFiles = [
|
|
578
|
+
// Single cross-agent source: a thin AGENTS.md points at the skill; the
|
|
579
|
+
// .agents/rules workflow rules and the Claude enforcement hooks back it up.
|
|
576
580
|
'AGENTS.md',
|
|
577
581
|
'CONVENTIONS.md',
|
|
582
|
+
'.agents/rules/workflow.md',
|
|
583
|
+
// Per-agent files. Content is single-source (AGENTS.md + the skill); these
|
|
584
|
+
// are thin bridges plus each tool's own config and commit-nudge hook. Claude
|
|
585
|
+
// Code (CLAUDE.md @-imports AGENTS.md), Gemini CLI (GEMINI.md), Copilot in VS
|
|
586
|
+
// Code (copilot-instructions.md). Cursor / opencode / Antigravity read
|
|
587
|
+
// AGENTS.md natively; Cursor also gets a .cursorrules bridge, and each of
|
|
588
|
+
// Cursor / Gemini / opencode ships a "commit often" nudge hook.
|
|
578
589
|
'CLAUDE.md',
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
'
|
|
584
|
-
|
|
585
|
-
'
|
|
586
|
-
'
|
|
587
|
-
|
|
588
|
-
'web-test-runner.config.js',
|
|
589
|
-
// Optional boot-time APM hook (setOnError). Delete if unused.
|
|
590
|
-
'instrumentation.ts',
|
|
591
|
-
// Environment variables
|
|
592
|
-
'.env.example',
|
|
593
|
-
// Project-level gitignore (node_modules, .webjs, .env, OS junk).
|
|
594
|
-
// Shipped as `gitignore` (no dot) and renamed to `.gitignore` on copy:
|
|
595
|
-
// npm STRIPS a `.gitignore` from a published tarball, so a dotfile name
|
|
596
|
-
// would arrive missing and the app would ship without a `.env` ignore
|
|
597
|
-
// (dogfood #845). The SQLite dev.db rule is appended programmatically
|
|
598
|
-
// below so it only appears for the sqlite dialect.
|
|
599
|
-
'gitignore',
|
|
600
|
-
// Git hooks (blocks commits on main)
|
|
601
|
-
'.hooks/pre-commit',
|
|
602
|
-
// Claude Code config + hooks
|
|
590
|
+
'GEMINI.md',
|
|
591
|
+
'.github/copilot-instructions.md',
|
|
592
|
+
'.cursorrules',
|
|
593
|
+
'.cursor/hooks.json',
|
|
594
|
+
'.cursor/hooks/nudge-uncommitted.sh',
|
|
595
|
+
'.gemini/settings.json',
|
|
596
|
+
'.gemini/hooks/nudge-uncommitted.sh',
|
|
597
|
+
'.opencode/plugins/nudge-uncommitted.ts',
|
|
598
|
+
// Claude Code config + the protective enforcement hooks (no design ceremony).
|
|
603
599
|
'.claude.json',
|
|
604
600
|
'.claude/settings.json',
|
|
605
601
|
'.claude/hooks/block-prose-punctuation.sh',
|
|
602
|
+
'.claude/hooks/block-raw-htmlelement.sh',
|
|
606
603
|
'.claude/hooks/guard-branch-context.sh',
|
|
607
604
|
'.claude/hooks/nudge-uncommitted.sh',
|
|
608
605
|
'.claude/hooks/commit-before-stop.sh',
|
|
@@ -610,43 +607,24 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
610
607
|
'.claude/hooks/require-tests-with-src.sh',
|
|
611
608
|
'.claude/hooks/check-server-imports.sh',
|
|
612
609
|
'.claude/hooks/check-server-imports.mjs',
|
|
613
|
-
//
|
|
614
|
-
|
|
615
|
-
//
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
'
|
|
619
|
-
'
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
'.
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
'.cursor/hooks.json',
|
|
626
|
-
'.cursor/hooks/nudge-uncommitted.sh',
|
|
627
|
-
// OpenCode plugins (loaded as TS by Bun at runtime)
|
|
628
|
-
'.opencode/plugins/nudge-uncommitted.ts',
|
|
629
|
-
// Antigravity workspace rules (Google's documented convention is
|
|
630
|
-
// `.agents/rules/*.md`, lowercase, per the Codelab
|
|
631
|
-
// "Build Autonomous Developer Pipelines using agents.md and skills.md
|
|
632
|
-
// in Antigravity"). Replaced the legacy `.windsurfrules` ship when
|
|
633
|
-
// Windsurf was acquired by Google.
|
|
634
|
-
'.agents/rules/workflow.md',
|
|
635
|
-
// Cross-agent config files
|
|
636
|
-
'.cursorrules',
|
|
637
|
-
'.github/copilot-instructions.md',
|
|
610
|
+
// Git pre-commit hook (blocks commits directly to main).
|
|
611
|
+
'.hooks/pre-commit',
|
|
612
|
+
// Starter tests under the feature-folder layout.
|
|
613
|
+
'test/hello/hello.test.ts',
|
|
614
|
+
'test/hello/browser/hello.test.js',
|
|
615
|
+
'test/hello/e2e/hello.test.ts',
|
|
616
|
+
'web-test-runner.config.js',
|
|
617
|
+
// Optional boot-time APM hook (setOnError). Delete if unused.
|
|
618
|
+
'instrumentation.ts',
|
|
619
|
+
'.env.example',
|
|
620
|
+
// Shipped without a dot (npm strips a published .gitignore) and renamed on copy.
|
|
621
|
+
'gitignore',
|
|
638
622
|
'.github/pull_request_template.md',
|
|
639
|
-
// CI
|
|
640
|
-
// webjs check + the unit / browser / e2e layers on every PR and push
|
|
641
|
-
// to main, mirroring the WebJs framework's own CI.
|
|
623
|
+
// CI runs webjs check + the test layers on every PR and push to main.
|
|
642
624
|
'.github/workflows/ci.yml',
|
|
643
625
|
'.editorconfig',
|
|
644
|
-
// VS Code: associate the published webjs-config JSON Schema with the
|
|
645
|
-
// package.json `webjs` block, so an unknown / typo'd key (#259) is
|
|
646
|
-
// flagged natively in the editor instead of silently dropped.
|
|
647
626
|
'.vscode/settings.json',
|
|
648
|
-
// Production / deploy scaffolding.
|
|
649
|
-
// the app locally with the same Dockerfile production builds from.
|
|
627
|
+
// Production / deploy scaffolding.
|
|
650
628
|
'Dockerfile',
|
|
651
629
|
'compose.yaml',
|
|
652
630
|
'.dockerignore',
|
|
@@ -658,12 +636,8 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
658
636
|
// rewrites; the three infra files get their file-specific transform. On Node,
|
|
659
637
|
// every file is copied byte-identical (the map is empty).
|
|
660
638
|
const PROSE_REWRITE = new Set([
|
|
661
|
-
'AGENTS.md', 'CONVENTIONS.md', '.cursorrules',
|
|
662
|
-
'.agents/rules/workflow.md',
|
|
663
|
-
// The starter tests carry header comments with run commands (`npx wtr`,
|
|
664
|
-
// `npm i -D puppeteer-core`); bun-ify those too so a bun app's test files
|
|
665
|
-
// do not tell the user to run npm/npx (#541 review). The transform only
|
|
666
|
-
// touches npm/npx command tokens, so the test code itself is unaffected.
|
|
639
|
+
'AGENTS.md', 'CLAUDE.md', 'CONVENTIONS.md', '.cursorrules',
|
|
640
|
+
'.agents/rules/workflow.md',
|
|
667
641
|
'test/hello/browser/hello.test.js', 'test/hello/e2e/hello.test.ts',
|
|
668
642
|
]);
|
|
669
643
|
// compose.yaml builds from the (pure oven/bun) Dockerfile and inherits its
|
|
@@ -691,21 +665,25 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
691
665
|
}
|
|
692
666
|
}
|
|
693
667
|
|
|
694
|
-
//
|
|
668
|
+
// The agent skill is the one cross-agent source (AGENTS.md points to it). It
|
|
669
|
+
// lives ONCE, canonically, at the repo-root `.agents/skills/webjs/`. A
|
|
670
|
+
// published CLI bundles it under `templates/` at prepack (see
|
|
671
|
+
// scripts/sync-scaffold-skill.mjs, wired into this package's prepack), so copy
|
|
672
|
+
// that bundle when present; in the monorepo the bundle is gitignored, so fall
|
|
673
|
+
// back to the repo-root canonical.
|
|
674
|
+
const bundledSkill = join(TEMPLATES, '.agents', 'skills', 'webjs');
|
|
675
|
+
const repoRootSkill = resolve(__dirname, '..', '..', '..', '.agents', 'skills', 'webjs');
|
|
676
|
+
const skillSrc = existsSync(bundledSkill) ? bundledSkill : repoRootSkill;
|
|
677
|
+
if (existsSync(skillSrc)) {
|
|
678
|
+
await cp(skillSrc, join(appDir, '.agents', 'skills', 'webjs'), { recursive: true });
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Make the Claude enforcement hooks + the git pre-commit executable.
|
|
695
682
|
const { chmod } = await import('node:fs/promises');
|
|
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', '
|
|
683
|
+
for (const hook of ['block-prose-punctuation.sh', 'block-raw-htmlelement.sh', 'guard-branch-context.sh', 'nudge-uncommitted.sh', 'commit-before-stop.sh', 'cleanup-merged-worktree.sh', 'require-tests-with-src.sh', 'check-server-imports.sh']) {
|
|
697
684
|
const hookPath = join(appDir, '.claude', 'hooks', hook);
|
|
698
685
|
if (existsSync(hookPath)) await chmod(hookPath, 0o755);
|
|
699
686
|
}
|
|
700
|
-
for (const hook of ['nudge-uncommitted.sh']) {
|
|
701
|
-
const hookPath = join(appDir, '.gemini', 'hooks', hook);
|
|
702
|
-
if (existsSync(hookPath)) await chmod(hookPath, 0o755);
|
|
703
|
-
}
|
|
704
|
-
for (const hook of ['nudge-uncommitted.sh']) {
|
|
705
|
-
const hookPath = join(appDir, '.cursor', 'hooks', hook);
|
|
706
|
-
if (existsSync(hookPath)) await chmod(hookPath, 0o755);
|
|
707
|
-
}
|
|
708
|
-
// Make git pre-commit hook executable
|
|
709
687
|
const preCommitPath = join(appDir, '.hooks', 'pre-commit');
|
|
710
688
|
if (existsSync(preCommitPath)) await chmod(preCommitPath, 0o755);
|
|
711
689
|
|
|
@@ -1052,9 +1030,9 @@ export type ActionResult<T> =
|
|
|
1052
1030
|
`);
|
|
1053
1031
|
|
|
1054
1032
|
// The api backend-features showcase: endpoints under app/api/features/**
|
|
1055
|
-
//
|
|
1056
|
-
//
|
|
1057
|
-
//
|
|
1033
|
+
// (the route() adapter + validation, rate limiting, streaming, file storage,
|
|
1034
|
+
// WebSockets + broadcast) that the root api index above links. The api
|
|
1035
|
+
// counterpart of the UI gallery. Prune what you skip.
|
|
1058
1036
|
const { writeApiGallery } = await import('./api-gallery.js');
|
|
1059
1037
|
await writeApiGallery(appDir);
|
|
1060
1038
|
}
|
|
@@ -1072,18 +1050,24 @@ export type ActionResult<T> =
|
|
|
1072
1050
|
// Progressive-enhancement service worker (#271): ship the opt-in offline
|
|
1073
1051
|
// primitive (the worker + its offline fallback) into the UI scaffolds
|
|
1074
1052
|
// (full-stack / saas; this block is api-excluded since api has no UI).
|
|
1075
|
-
// Dormant until the app registers it (see
|
|
1053
|
+
// Dormant until the app registers it (see the skill's references/service-worker.md);
|
|
1076
1054
|
// it never changes the JS-disabled baseline.
|
|
1077
1055
|
for (const swFile of ['sw.js', 'offline.html']) {
|
|
1078
1056
|
const swSrc = join(TEMPLATES, 'public', swFile);
|
|
1079
1057
|
if (existsSync(swSrc)) await cp(swSrc, join(publicDir, swFile));
|
|
1080
1058
|
}
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1059
|
+
// A base SVG favicon (the root layout links it). It ships with the app, not
|
|
1060
|
+
// the gallery, so it survives `npm run gallery:clear`.
|
|
1061
|
+
const faviconSrc = join(TEMPLATES, 'public', 'favicon.svg');
|
|
1062
|
+
if (existsSync(faviconSrc)) await cp(faviconSrc, join(publicDir, 'favicon.svg'));
|
|
1063
|
+
|
|
1064
|
+
// The gallery-reset script (wired as `gallery:clear`). Only UI templates have
|
|
1065
|
+
// a gallery, so it ships here (NOT in the flat templateFiles list, which would
|
|
1066
|
+
// copy it into the api app where it has no gallery and would clobber app/).
|
|
1067
|
+
const clearScriptSrc = join(TEMPLATES, 'scripts', 'clear-gallery.mjs');
|
|
1068
|
+
if (existsSync(clearScriptSrc)) {
|
|
1069
|
+
await mkdir(join(appDir, 'scripts'), { recursive: true });
|
|
1070
|
+
await cp(clearScriptSrc, join(appDir, 'scripts', 'clear-gallery.mjs'));
|
|
1087
1071
|
}
|
|
1088
1072
|
|
|
1089
1073
|
// Fail loudly if the @webjsdev/ui registry sources aren't on disk.
|
|
@@ -1098,20 +1082,13 @@ export type ActionResult<T> =
|
|
|
1098
1082
|
// styles/globals.css (the @webjsdev/ui theme).
|
|
1099
1083
|
await writeUiBootstrap(appDir);
|
|
1100
1084
|
|
|
1101
|
-
//
|
|
1102
|
-
//
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
// The gallery: idiomatic, densely-commented single-feature demos under
|
|
1109
|
-
// app/features/ plus one whole example app under app/examples/, with logic
|
|
1110
|
-
// in modules/, linked from the home page. Shipped in the default full-stack
|
|
1111
|
-
// scaffold so an agent gains context by browsing real code; prune
|
|
1112
|
-
// per-feature (delete the route + its module) for what the app does not
|
|
1113
|
-
// use. See CONVENTIONS.md "prune what the app does not use".
|
|
1114
|
-
if (!isApi) await copyGallery(appDir);
|
|
1085
|
+
// The saas auth pages import a few ui-* primitives. A full-stack app adds
|
|
1086
|
+
// any component on demand with `webjs ui add <name>`.
|
|
1087
|
+
if (isSaas) {
|
|
1088
|
+
await copyUiComponents(appDir, [
|
|
1089
|
+
'button', 'card', 'alert', 'badge', 'separator', 'label', 'input',
|
|
1090
|
+
]);
|
|
1091
|
+
}
|
|
1115
1092
|
|
|
1116
1093
|
// The @webjsdev/ui theme (`--color-primary`, `--color-card`, the @theme maps,
|
|
1117
1094
|
// @custom-variant, @keyframes) plus the app @theme mappings are compiled from
|
|
@@ -1152,51 +1129,35 @@ ${uiThemeRaw}
|
|
|
1152
1129
|
}
|
|
1153
1130
|
`);
|
|
1154
1131
|
|
|
1132
|
+
// The gallery: idiomatic, densely-commented single-feature demos under
|
|
1133
|
+
// app/features/ plus one whole example app under app/examples/, with logic
|
|
1134
|
+
// in modules/, all linked from the home page below. Shipped in every UI
|
|
1135
|
+
// scaffold (full-stack AND saas) so an agent gains context by browsing real
|
|
1136
|
+
// working code; prune per-feature (delete the route + its module) for what
|
|
1137
|
+
// the app does not use.
|
|
1138
|
+
await copyGallery(appDir);
|
|
1139
|
+
|
|
1155
1140
|
await writeFile(join(appDir, 'app', 'layout.ts'), `import { html, cspNonce } from '@webjsdev/core';
|
|
1156
1141
|
import '#components/theme-toggle.ts';
|
|
1157
|
-
// Webjs UI components are tiered:
|
|
1158
|
-
// - Tier 1 (button, card, input, label, alert, badge, separator, etc.) are
|
|
1159
|
-
// class-helper FUNCTIONS, with no custom element to register. Each page
|
|
1160
|
-
// imports the specific helpers it needs (e.g.
|
|
1161
|
-
// \`import { buttonClass } from '#components/ui/button.ts'\`).
|
|
1162
|
-
// - Tier 2 (dialog, popover, tooltip, tabs, accordion, etc.) ARE custom
|
|
1163
|
-
// elements. Register them by side-effect-importing here once so they
|
|
1164
|
-
// work transitively across every page:
|
|
1165
|
-
// import '#components/ui/dialog.ts';
|
|
1166
|
-
// The example app/page.ts below uses only Tier-1 helpers, so nothing
|
|
1167
|
-
// extra needs to be registered. Add Tier-2 imports as you 'webjs ui add'.
|
|
1168
1142
|
|
|
1169
1143
|
/**
|
|
1170
|
-
* Root layout:
|
|
1171
|
-
*
|
|
1172
|
-
*
|
|
1173
|
-
*
|
|
1174
|
-
*
|
|
1175
|
-
*
|
|
1176
|
-
*
|
|
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.
|
|
1144
|
+
* Root layout: the ONLY file that writes the document shell. It wires a neutral
|
|
1145
|
+
* design-token palette, the light/dark theme, and the Tailwind stylesheet, then
|
|
1146
|
+
* renders \${children} in a bare container. Grow it in place: add a header, nav,
|
|
1147
|
+
* footer, or reading column here as your app needs them. Design tokens live as
|
|
1148
|
+
* plain CSS custom properties (they resolve with JavaScript disabled) and are
|
|
1149
|
+
* mapped into Tailwind via @theme in public/input.css, so bg-background,
|
|
1150
|
+
* text-foreground, bg-card, bg-primary, and border-border all work.
|
|
1180
1151
|
*/
|
|
1181
|
-
|
|
1182
1152
|
export default function RootLayout({ children }: { children: unknown }) {
|
|
1183
|
-
// Read the in-flight request's CSP nonce so the theme-detection
|
|
1184
|
-
//
|
|
1185
|
-
// Returns '' when no CSP nonce is set, in which case the attribute
|
|
1186
|
-
// is empty and the browser ignores it.
|
|
1153
|
+
// Read the in-flight request's CSP nonce so the theme-detection inline script
|
|
1154
|
+
// passes strict CSP. Returns '' when no CSP nonce is set.
|
|
1187
1155
|
const nonce = cspNonce();
|
|
1188
1156
|
return html\`
|
|
1189
1157
|
<script nonce="\${nonce}">
|
|
1190
|
-
//
|
|
1191
|
-
//
|
|
1192
|
-
//
|
|
1193
|
-
// the root, dark, and data-theme style blocks below switch. It is what
|
|
1194
|
-
// makes the app theme-aware. Building a SINGLE-theme app of your own?
|
|
1195
|
-
// Delete this IIFE, delete the dark and light style blocks below, and set
|
|
1196
|
-
// your palette once on the root selector. That removes the wiring so it
|
|
1197
|
-
// cannot fight your own colours (it will not override a plain root
|
|
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).
|
|
1158
|
+
// Light/dark theme: read the saved or OS choice and set data-theme plus the
|
|
1159
|
+
// .dark class the tokens key off. Delete this block (and the light blocks
|
|
1160
|
+
// below) for a single-theme app.
|
|
1200
1161
|
(function(){
|
|
1201
1162
|
try {
|
|
1202
1163
|
var mq = window.matchMedia('(prefers-color-scheme: light)');
|
|
@@ -1206,9 +1167,6 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1206
1167
|
var el = document.documentElement;
|
|
1207
1168
|
if (t === 'light' || t === 'dark') el.dataset.theme = t;
|
|
1208
1169
|
else delete el.dataset.theme;
|
|
1209
|
-
// Keep the .dark class the @webjsdev/ui kit uses in sync with the effective theme so the
|
|
1210
|
-
// copied ui-* components (button, card, etc.) follow light/dark too.
|
|
1211
|
-
// Dark is the default unless the OS prefers light or 'light' is set.
|
|
1212
1170
|
var dark = t === 'dark' || (t !== 'light' && !mq.matches);
|
|
1213
1171
|
el.classList.toggle('dark', dark);
|
|
1214
1172
|
}
|
|
@@ -1216,14 +1174,10 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1216
1174
|
mq.addEventListener('change', apply);
|
|
1217
1175
|
} catch (_) {}
|
|
1218
1176
|
})();
|
|
1219
|
-
//
|
|
1220
|
-
//
|
|
1221
|
-
//
|
|
1222
|
-
//
|
|
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.
|
|
1177
|
+
// Header-measure: dormant until you add a fixed header. A fixed header
|
|
1178
|
+
// (use position:fixed, NOT sticky, which flickers on iOS WebKit during a
|
|
1179
|
+
// client-router nav) leaves normal flow, so --header-h reserves its height
|
|
1180
|
+
// for the content below. No header means a no-op and --header-h stays 0.
|
|
1227
1181
|
(function(){
|
|
1228
1182
|
function measure(){
|
|
1229
1183
|
try {
|
|
@@ -1240,123 +1194,103 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1240
1194
|
else measure();
|
|
1241
1195
|
})();
|
|
1242
1196
|
</script>
|
|
1197
|
+
<meta name="color-scheme" content="light dark">
|
|
1198
|
+
<link rel="icon" href="/public/favicon.svg" type="image/svg+xml">
|
|
1199
|
+
<!-- JetBrains Mono for body/UI (its monospaced, developer-console feel) and
|
|
1200
|
+
Bricolage Grotesque for the display wordmark. Swap these for your own
|
|
1201
|
+
fonts (and update --font-sans / --font-display below). -->
|
|
1202
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
1203
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin="anonymous">
|
|
1204
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400..800&family=JetBrains+Mono:wght@400;500;700&display=swap">
|
|
1243
1205
|
<!-- Tailwind: a STATIC stylesheet compiled from public/input.css to
|
|
1244
1206
|
public/tailwind.css by css:build (run automatically by the dev and start
|
|
1245
1207
|
tasks; in dev it is also recompiled on request when a source changes, so
|
|
1246
1208
|
it never goes stale). A real stylesheet, so the app is fully styled with
|
|
1247
1209
|
JavaScript DISABLED (no in-browser compile). -->
|
|
1210
|
+
|
|
1248
1211
|
<link rel="stylesheet" href="/public/tailwind.css">
|
|
1249
1212
|
<style>
|
|
1250
|
-
/*
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
text-muted-foreground, border-border, ring-ring, and the rest. Here we
|
|
1256
|
-
set those tokens' VALUES to this app's brand palette, so the ui-*
|
|
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
|
|
1261
|
-
OS. Follows the shadcn model: --primary is the BRAND color (orange, used
|
|
1262
|
-
for primary buttons, links, and emphasis), while --accent stays a NEUTRAL
|
|
1263
|
-
hover tint so the ui kit's outline/ghost/dropdown hover states keep proper
|
|
1264
|
-
contrast. Use bg-primary / text-primary for brand, bg-accent for hovers.
|
|
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
|
|
1268
|
-
(bg-primary/10, hover:bg-primary/90, border-border/60) before inventing a
|
|
1269
|
-
new token, but keep body text at full opacity so it stays above the AA
|
|
1270
|
-
contrast floor (a faded text-muted-foreground/70 measured 3.83:1). */
|
|
1213
|
+
/* Design tokens. The token NAMES are infrastructure (public/input.css maps
|
|
1214
|
+
them into Tailwind via @theme). The VALUES are a cool neutral-grey palette
|
|
1215
|
+
with a monospaced type system: change them here to give the app its own
|
|
1216
|
+
look. bg-background / text-foreground / bg-card / bg-primary / border-border
|
|
1217
|
+
all resolve from these. */
|
|
1271
1218
|
:root {
|
|
1272
|
-
--font-sans: -
|
|
1219
|
+
--font-sans: 'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
|
1273
1220
|
--font-serif: ui-serif, 'Iowan Old Style', Palatino, Georgia, serif;
|
|
1274
|
-
--font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1275
|
-
|
|
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. */
|
|
1221
|
+
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
|
1222
|
+
--font-display: 'Bricolage Grotesque', 'JetBrains Mono', ui-sans-serif, system-ui, sans-serif;
|
|
1279
1223
|
--header-h: 0px;
|
|
1280
|
-
/* A translucent
|
|
1281
|
-
light/dark
|
|
1282
|
-
--primary-tint: color-mix(in
|
|
1224
|
+
/* A translucent tint of the primary, tracked automatically across
|
|
1225
|
+
light/dark. Used for focus rings (ring-primary-tint). */
|
|
1226
|
+
--primary-tint: color-mix(in srgb, var(--primary) 22%, transparent);
|
|
1283
1227
|
}
|
|
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. */
|
|
1285
1228
|
/* dark (the default, and the explicit .dark the toggle sets) */
|
|
1286
1229
|
:root, .dark {
|
|
1287
1230
|
color-scheme: dark;
|
|
1288
|
-
--background:
|
|
1289
|
-
--foreground:
|
|
1290
|
-
--card:
|
|
1291
|
-
--card-foreground:
|
|
1292
|
-
--popover:
|
|
1293
|
-
--popover-foreground:
|
|
1294
|
-
|
|
1295
|
-
--primary:
|
|
1296
|
-
--
|
|
1297
|
-
--secondary:
|
|
1298
|
-
--
|
|
1299
|
-
--muted:
|
|
1300
|
-
--
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
--
|
|
1304
|
-
--
|
|
1305
|
-
--
|
|
1306
|
-
--border-strong: oklch(0.38 0.012 55 / 0.9);
|
|
1307
|
-
--input: oklch(0.26 0.012 55 / 0.9);
|
|
1308
|
-
--ring: oklch(0.7 0.16 52);
|
|
1309
|
-
--logo-from: oklch(0.8 0.16 58);
|
|
1310
|
-
--logo-to: oklch(0.62 0.18 44);
|
|
1231
|
+
--background: #1e2226;
|
|
1232
|
+
--foreground: #dee2e6;
|
|
1233
|
+
--card: #313539;
|
|
1234
|
+
--card-foreground: #dee2e6;
|
|
1235
|
+
--popover: #313539;
|
|
1236
|
+
--popover-foreground: #dee2e6;
|
|
1237
|
+
--primary: #dee2e6;
|
|
1238
|
+
--primary-foreground: #1e2226;
|
|
1239
|
+
--secondary: #363a3e;
|
|
1240
|
+
--secondary-foreground: #dee2e6;
|
|
1241
|
+
--muted: #313539;
|
|
1242
|
+
--muted-foreground: #94989c;
|
|
1243
|
+
--accent: #363a3e;
|
|
1244
|
+
--accent-foreground: #f7fbff;
|
|
1245
|
+
--border: #34393e;
|
|
1246
|
+
--border-strong: #454b51;
|
|
1247
|
+
--input: #34393e;
|
|
1248
|
+
--ring: #6b7075;
|
|
1311
1249
|
}
|
|
1312
1250
|
/* light (explicit via the toggle) */
|
|
1313
1251
|
:root[data-theme='light'] {
|
|
1314
1252
|
color-scheme: light;
|
|
1315
|
-
--background:
|
|
1316
|
-
--foreground:
|
|
1317
|
-
--card:
|
|
1318
|
-
--card-foreground:
|
|
1319
|
-
--popover:
|
|
1320
|
-
--popover-foreground:
|
|
1321
|
-
--primary:
|
|
1322
|
-
--primary-foreground:
|
|
1323
|
-
--secondary:
|
|
1324
|
-
--secondary-foreground:
|
|
1325
|
-
--muted:
|
|
1326
|
-
--muted-foreground:
|
|
1327
|
-
--accent:
|
|
1328
|
-
--accent-foreground:
|
|
1329
|
-
--border:
|
|
1330
|
-
--border-strong:
|
|
1331
|
-
--input:
|
|
1332
|
-
--ring:
|
|
1333
|
-
--logo-from: oklch(0.63 0.17 50);
|
|
1334
|
-
--logo-to: oklch(0.44 0.11 52);
|
|
1253
|
+
--background: #dee2e6;
|
|
1254
|
+
--foreground: #313539;
|
|
1255
|
+
--card: #f0f4f7;
|
|
1256
|
+
--card-foreground: #313539;
|
|
1257
|
+
--popover: #f0f4f7;
|
|
1258
|
+
--popover-foreground: #313539;
|
|
1259
|
+
--primary: #313539;
|
|
1260
|
+
--primary-foreground: #f7fbff;
|
|
1261
|
+
--secondary: #f7fbff;
|
|
1262
|
+
--secondary-foreground: #313539;
|
|
1263
|
+
--muted: #eaeef1;
|
|
1264
|
+
--muted-foreground: #767b80;
|
|
1265
|
+
--accent: #f7fbff;
|
|
1266
|
+
--accent-foreground: #313539;
|
|
1267
|
+
--border: #c9d0d6;
|
|
1268
|
+
--border-strong: #b3bbc2;
|
|
1269
|
+
--input: #c9d0d6;
|
|
1270
|
+
--ring: #9aa0a5;
|
|
1335
1271
|
}
|
|
1336
1272
|
/* light (OS preference, when the user has made no explicit choice) */
|
|
1337
1273
|
@media (prefers-color-scheme: light) {
|
|
1338
1274
|
:root:not(.dark):not([data-theme='dark']) {
|
|
1339
1275
|
color-scheme: light;
|
|
1340
|
-
--background:
|
|
1341
|
-
--foreground:
|
|
1342
|
-
--card:
|
|
1343
|
-
--card-foreground:
|
|
1344
|
-
--popover:
|
|
1345
|
-
--popover-foreground:
|
|
1346
|
-
--primary:
|
|
1347
|
-
--primary-foreground:
|
|
1348
|
-
--secondary:
|
|
1349
|
-
--secondary-foreground:
|
|
1350
|
-
--muted:
|
|
1351
|
-
--muted-foreground:
|
|
1352
|
-
--accent:
|
|
1353
|
-
--accent-foreground:
|
|
1354
|
-
--border:
|
|
1355
|
-
--border-strong:
|
|
1356
|
-
--input:
|
|
1357
|
-
--ring:
|
|
1358
|
-
--logo-from: oklch(0.63 0.17 50);
|
|
1359
|
-
--logo-to: oklch(0.44 0.11 52);
|
|
1276
|
+
--background: #dee2e6;
|
|
1277
|
+
--foreground: #313539;
|
|
1278
|
+
--card: #f0f4f7;
|
|
1279
|
+
--card-foreground: #313539;
|
|
1280
|
+
--popover: #f0f4f7;
|
|
1281
|
+
--popover-foreground: #313539;
|
|
1282
|
+
--primary: #313539;
|
|
1283
|
+
--primary-foreground: #f7fbff;
|
|
1284
|
+
--secondary: #f7fbff;
|
|
1285
|
+
--secondary-foreground: #313539;
|
|
1286
|
+
--muted: #eaeef1;
|
|
1287
|
+
--muted-foreground: #767b80;
|
|
1288
|
+
--accent: #f7fbff;
|
|
1289
|
+
--accent-foreground: #313539;
|
|
1290
|
+
--border: #c9d0d6;
|
|
1291
|
+
--border-strong: #b3bbc2;
|
|
1292
|
+
--input: #c9d0d6;
|
|
1293
|
+
--ring: #9aa0a5;
|
|
1360
1294
|
}
|
|
1361
1295
|
}
|
|
1362
1296
|
</style>
|
|
@@ -1367,13 +1301,11 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1367
1301
|
padding-top: var(--header-h);
|
|
1368
1302
|
background: var(--background);
|
|
1369
1303
|
color: var(--foreground);
|
|
1370
|
-
font:
|
|
1304
|
+
font: 15px/1.6 var(--font-sans);
|
|
1371
1305
|
-webkit-font-smoothing: antialiased;
|
|
1306
|
+
-moz-osx-font-smoothing: grayscale;
|
|
1372
1307
|
}
|
|
1373
|
-
::selection { background: color-mix(in oklch, var(--primary) 22%, transparent); color: var(--foreground); }
|
|
1374
1308
|
</style>
|
|
1375
|
-
|
|
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
1309
|
<main class="min-h-dvh px-4 sm:px-6 py-8">
|
|
1378
1310
|
\${children}
|
|
1379
1311
|
</main>
|
|
@@ -1381,31 +1313,26 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1381
1313
|
}
|
|
1382
1314
|
`);
|
|
1383
1315
|
|
|
1384
|
-
//
|
|
1385
|
-
//
|
|
1386
|
-
//
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
import {
|
|
1394
|
-
cardClass,
|
|
1395
|
-
cardHeaderClass,
|
|
1396
|
-
cardTitleClass,
|
|
1397
|
-
cardDescriptionClass,
|
|
1398
|
-
} from '#components/ui/card.ts';
|
|
1316
|
+
// Home page: a gallery index. A masthead, then a grid that links every feature
|
|
1317
|
+
// demo and the example app, and a footer with the docs + source links. Treat it
|
|
1318
|
+
// as a starting point: prune the demos you do not use (delete the
|
|
1319
|
+
// app/features/<x> route AND its modules/<x>), then reshape this page into the
|
|
1320
|
+
// app's real landing page. For the saas template a login/signup CTA row is
|
|
1321
|
+
// spliced under the tagline.
|
|
1322
|
+
const homeAuthLinks = isSaas
|
|
1323
|
+
? '\n <div class="flex flex-wrap gap-3 items-center justify-center mt-2"><a href="/login" class="inline-flex items-center px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm font-medium no-underline hover:opacity-90">Log in</a><a href="/signup" class="inline-flex items-center px-4 py-2 rounded-lg border border-border text-foreground text-sm font-medium no-underline hover:bg-accent">Create an account</a></div>'
|
|
1324
|
+
: '';
|
|
1325
|
+
await writeFile(join(appDir, 'app', 'page.ts'), `import { html } from '@webjsdev/core';
|
|
1399
1326
|
|
|
1400
1327
|
export const metadata = {
|
|
1401
|
-
title: '${displayName}
|
|
1328
|
+
title: '${displayName}',
|
|
1402
1329
|
};
|
|
1403
1330
|
|
|
1404
|
-
//
|
|
1405
|
-
//
|
|
1406
|
-
//
|
|
1407
|
-
//
|
|
1408
|
-
const
|
|
1331
|
+
// The gallery this page links. FEATURES are single-concept demos (one WebJs
|
|
1332
|
+
// concept each, under app/features/, logic in modules/). EXAMPLES are whole apps
|
|
1333
|
+
// composing several features (under app/examples/). Prune what you do not use
|
|
1334
|
+
// (delete the route AND its modules/<name>), then reshape this page.
|
|
1335
|
+
const FEATURES = [
|
|
1409
1336
|
{ href: '/features/routing', title: 'Routing', blurb: 'A static route plus a dynamic [id] segment that reads params. The file-based router in miniature.' },
|
|
1410
1337
|
{ href: '/features/boundaries', title: 'Boundaries', blurb: 'The control-flow throws (forbidden / unauthorized / notFound) and the nearest boundary file that catches each.' },
|
|
1411
1338
|
{ href: '/features/components', title: 'Components', blurb: 'The WebComponent factory, reactive props, instance signals, and slot projection in light DOM.' },
|
|
@@ -1413,7 +1340,7 @@ const features = [
|
|
|
1413
1340
|
{ href: '/features/optimistic-ui', title: 'Optimistic UI', blurb: 'The imperative optimistic(signal, value, action) flip: instant update, automatic rollback on failure.' },
|
|
1414
1341
|
{ href: '/features/async-render', title: 'Async render', blurb: 'A component that awaits server data in async render(), so the resolved value is in the first paint.' },
|
|
1415
1342
|
{ href: '/features/directives', title: 'Directives', blurb: 'The lit-html directive set: repeat for keyed lists, watch(signal) for a fine-grained node swap.' },
|
|
1416
|
-
{ href: '/features/route-handler', title: 'Route handlers', blurb: 'A server-only route.ts HTTP endpoint returning JSON, the
|
|
1343
|
+
{ href: '/features/route-handler', title: 'Route handlers', blurb: 'A server-only route.ts HTTP endpoint returning JSON, the WebJs equivalent of a Next route handler.' },
|
|
1417
1344
|
{ href: '/features/forms', title: 'Forms', blurb: 'A no-JS progressive-enhancement form posting to the page action, with server-side validation errors.' },
|
|
1418
1345
|
{ href: '/features/metadata', title: 'Metadata', blurb: 'Static metadata plus generateMetadata(ctx), which reads the request to compute the title and Open Graph tags.' },
|
|
1419
1346
|
{ href: '/features/caching', title: 'Caching', blurb: 'export const revalidate caches the page HTML per URL, with the safety rule for when a shared cache is allowed.' },
|
|
@@ -1426,162 +1353,79 @@ const features = [
|
|
|
1426
1353
|
{ href: '/features/file-storage', title: 'File storage', blurb: 'A no-JS multipart upload streamed into the FileStore, then served back through a streaming route.' },
|
|
1427
1354
|
{ href: '/features/sessions', title: 'Sessions', blurb: 'A signed-cookie session applied by a segment middleware, read and written per visitor with getSession() in a route.' },
|
|
1428
1355
|
];
|
|
1429
|
-
const
|
|
1356
|
+
const EXAMPLES = [
|
|
1430
1357
|
{ href: '/examples/todo', title: 'Optimistic todo', blurb: 'A whole app composing several features: the declarative optimistic() list API, progressive-enhancement forms, accessible labels, the modules split, and SQLite.' },
|
|
1431
1358
|
];
|
|
1432
1359
|
|
|
1433
|
-
const galleryCard = (item: { href: string; title: string; blurb: string }) => html\`
|
|
1434
|
-
<a href=\${item.href} class="block no-underline">
|
|
1435
|
-
<div class="\${cardClass()} h-full transition-colors hover:border-border-strong">
|
|
1436
|
-
<div class=\${cardHeaderClass()}>
|
|
1437
|
-
<h3 class=\${cardTitleClass()}>\${item.title}</h3>
|
|
1438
|
-
<p class=\${cardDescriptionClass()}>\${item.blurb}</p>
|
|
1439
|
-
</div>
|
|
1440
|
-
</div>
|
|
1441
|
-
</a>
|
|
1442
|
-
\`;
|
|
1443
|
-
|
|
1444
1360
|
export default function Home() {
|
|
1445
1361
|
return html\`
|
|
1446
|
-
<
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
</
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1362
|
+
<div class="fixed top-4 right-4 z-10"><theme-toggle></theme-toggle></div>
|
|
1363
|
+
|
|
1364
|
+
<div class="max-w-5xl mx-auto px-6 py-16 flex flex-col items-center gap-16">
|
|
1365
|
+
<!-- Masthead -->
|
|
1366
|
+
<section class="flex flex-col items-center text-center gap-5">
|
|
1367
|
+
<p class="text-xs font-semibold uppercase tracking-[0.22em] text-muted-foreground m-0">Welcome to</p>
|
|
1368
|
+
<h1 class="text-6xl sm:text-7xl font-bold uppercase tracking-tight leading-none m-0 break-words bg-gradient-to-b from-foreground to-muted-foreground bg-clip-text text-transparent" style="font-family: var(--font-display); word-spacing: 0.08em; letter-spacing: -0.02em;">
|
|
1369
|
+
WebJs Gallery
|
|
1370
|
+
</h1>
|
|
1371
|
+
<p class="text-base sm:text-lg text-muted-foreground max-w-lg leading-relaxed m-0">
|
|
1372
|
+
AI-first and web-components-first. Server-rendered, progressively enhanced, and buildless.
|
|
1373
|
+
</p>${homeAuthLinks}
|
|
1374
|
+
</section>
|
|
1375
|
+
|
|
1376
|
+
<!-- Gallery: every feature demo + the example app -->
|
|
1377
|
+
<section class="w-full flex flex-col gap-6">
|
|
1378
|
+
<div class="flex flex-col items-center gap-2 text-center">
|
|
1379
|
+
<h2 class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground m-0">Explore the gallery</h2>
|
|
1380
|
+
<p class="text-sm text-muted-foreground max-w-lg leading-relaxed m-0">
|
|
1381
|
+
One WebJs concept per demo under <code class="text-[0.9em] text-foreground">app/features/</code>, with logic
|
|
1382
|
+
in <code class="text-[0.9em] text-foreground">modules/</code>.
|
|
1383
|
+
</p>
|
|
1384
|
+
</div>
|
|
1385
|
+
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
1386
|
+
\${FEATURES.map(f => html\`
|
|
1387
|
+
<a href="\${f.href}" class="group flex flex-col gap-1.5 rounded-xl border border-border bg-card p-4 no-underline transition-colors hover:border-border-strong hover:bg-accent">
|
|
1388
|
+
<span class="flex items-center justify-between gap-2">
|
|
1389
|
+
<span class="text-sm font-medium text-foreground">\${f.title}</span>
|
|
1390
|
+
<span class="text-muted-foreground transition-transform group-hover:translate-x-0.5" aria-hidden="true">→</span>
|
|
1391
|
+
</span>
|
|
1392
|
+
<span class="text-xs leading-relaxed text-muted-foreground">\${f.blurb}</span>
|
|
1393
|
+
</a>
|
|
1394
|
+
\`)}
|
|
1395
|
+
</div>
|
|
1396
|
+
\${EXAMPLES.map(e => html\`
|
|
1397
|
+
<a href="\${e.href}" class="group flex flex-col gap-2 rounded-xl border border-border bg-card p-5 no-underline transition-colors hover:border-border-strong hover:bg-accent">
|
|
1398
|
+
<span class="flex items-center gap-2.5">
|
|
1399
|
+
<span class="text-[0.6rem] font-semibold uppercase tracking-wider text-muted-foreground rounded border border-border px-1.5 py-0.5">Example app</span>
|
|
1400
|
+
<span class="text-sm font-medium text-foreground">\${e.title}</span>
|
|
1401
|
+
<span class="ml-auto text-muted-foreground transition-transform group-hover:translate-x-0.5" aria-hidden="true">→</span>
|
|
1402
|
+
</span>
|
|
1403
|
+
<span class="text-xs leading-relaxed text-muted-foreground">\${e.blurb}</span>
|
|
1404
|
+
</a>
|
|
1405
|
+
\`)}
|
|
1406
|
+
</section>
|
|
1407
|
+
|
|
1408
|
+
<!-- Footer: docs + source -->
|
|
1409
|
+
<footer class="flex flex-col items-center gap-3">
|
|
1410
|
+
<nav class="flex items-center gap-6 text-sm text-muted-foreground" aria-label="WebJs links">
|
|
1411
|
+
<a href="https://docs.webjs.dev" class="inline-flex items-center gap-2 hover:text-foreground transition-colors no-underline">\${iconBook()}<span>Docs</span></a>
|
|
1412
|
+
<a href="https://github.com/webjsdev/webjs" class="inline-flex items-center gap-2 hover:text-foreground transition-colors no-underline">\${iconGithub()}<span>GitHub</span></a>
|
|
1413
|
+
</nav>
|
|
1414
|
+
<p class="text-[0.7rem] uppercase tracking-[0.15em] text-muted-foreground m-0 text-center">
|
|
1415
|
+
Built with WebJs · MIT License
|
|
1416
|
+
</p>
|
|
1417
|
+
</footer>
|
|
1418
|
+
</div>
|
|
1481
1419
|
\`;
|
|
1482
1420
|
}
|
|
1483
|
-
`);
|
|
1484
|
-
} else {
|
|
1485
|
-
// saas home: the auth landing (hero + login/signup/dashboard) stays the
|
|
1486
|
-
// headline, and the WebJs feature gallery sits BELOW it, so a saas app is
|
|
1487
|
-
// also a learning surface. Keep the `features` list in sync with the
|
|
1488
|
-
// full-stack home above (both ship the same gallery).
|
|
1489
|
-
await writeFile(join(appDir, 'app', 'page.ts'), `// webjs-scaffold-placeholder. This is the example homepage. Replace it with your app's real landing page, then delete this line. webjs check fails while the marker remains.
|
|
1490
|
-
import { html } from '@webjsdev/core';
|
|
1491
|
-
import { rubric, displayH1, accentLink } from '#lib/utils/ui.ts';
|
|
1492
|
-
import {
|
|
1493
|
-
cardClass,
|
|
1494
|
-
cardHeaderClass,
|
|
1495
|
-
cardTitleClass,
|
|
1496
|
-
cardDescriptionClass,
|
|
1497
|
-
} from '#components/ui/card.ts';
|
|
1498
|
-
|
|
1499
|
-
export const metadata = {
|
|
1500
|
-
title: '${displayName}: built with webjs',
|
|
1501
|
-
};
|
|
1502
1421
|
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
{ href: '/features/routing', title: 'Routing', blurb: 'A static route plus a dynamic [id] segment that reads params. The file-based router in miniature.' },
|
|
1509
|
-
{ href: '/features/boundaries', title: 'Boundaries', blurb: 'The control-flow throws (forbidden / unauthorized / notFound) and the nearest boundary file that catches each.' },
|
|
1510
|
-
{ href: '/features/components', title: 'Components', blurb: 'The WebComponent factory, reactive props, instance signals, and slot projection in light DOM.' },
|
|
1511
|
-
{ href: '/features/server-actions', title: 'Server actions', blurb: 'A use-server RPC action next to a server-only .server.ts utility, and why the boundary matters.' },
|
|
1512
|
-
{ href: '/features/optimistic-ui', title: 'Optimistic UI', blurb: 'The imperative optimistic(signal, value, action) flip: instant update, automatic rollback on failure.' },
|
|
1513
|
-
{ href: '/features/async-render', title: 'Async render', blurb: 'A component that awaits server data in async render(), so the resolved value is in the first paint.' },
|
|
1514
|
-
{ href: '/features/directives', title: 'Directives', blurb: 'The lit-html directive set: repeat for keyed lists, watch(signal) for a fine-grained node swap.' },
|
|
1515
|
-
{ href: '/features/route-handler', title: 'Route handlers', blurb: 'A server-only route.ts HTTP endpoint returning JSON, the webjs equivalent of a Next route handler.' },
|
|
1516
|
-
{ href: '/features/forms', title: 'Forms', blurb: 'A no-JS progressive-enhancement form posting to the page action, with server-side validation errors.' },
|
|
1517
|
-
{ href: '/features/metadata', title: 'Metadata', blurb: 'Static metadata plus generateMetadata(ctx), which reads the request to compute the title and Open Graph tags.' },
|
|
1518
|
-
{ href: '/features/caching', title: 'Caching', blurb: 'export const revalidate caches the page HTML per URL, with the safety rule for when a shared cache is allowed.' },
|
|
1519
|
-
{ href: '/features/env', title: 'Env vars', blurb: 'The server-only vs WEBJS_PUBLIC_ boundary, read during SSR so secrets never reach the browser.' },
|
|
1520
|
-
{ href: '/features/client-router', title: 'Client router', blurb: 'Automatic soft navigation: fragment-only fetches, hover prefetch, scroll restore, and graceful no-JS fallback.' },
|
|
1521
|
-
{ href: '/features/service-worker', title: 'Service worker', blurb: 'The opt-in offline enhancement, registered from a browser-only lifecycle hook (never a page or layout).' },
|
|
1522
|
-
{ href: '/features/websockets', title: 'WebSockets', blurb: 'A WS(ws, req) route endpoint plus the connectWS() client, echoing messages over a live socket.' },
|
|
1523
|
-
{ href: '/features/broadcast', title: 'Broadcast', blurb: 'Fan a message out to every connected client on a WebSocket path, so all open tabs stay in sync.' },
|
|
1524
|
-
{ href: '/features/rate-limit', title: 'Rate limiting', blurb: 'The rateLimit() middleware scoped to one endpoint, returning a 429 with Retry-After past the window.' },
|
|
1525
|
-
{ href: '/features/file-storage', title: 'File storage', blurb: 'A no-JS multipart upload streamed into the FileStore, then served back through a streaming route.' },
|
|
1526
|
-
{ href: '/features/sessions', title: 'Sessions', blurb: 'A signed-cookie session applied by a segment middleware, read and written per visitor with getSession() in a route.' },
|
|
1527
|
-
];
|
|
1528
|
-
const examples = [
|
|
1529
|
-
{ href: '/examples/todo', title: 'Optimistic todo', blurb: 'A whole app composing several features: the declarative optimistic() list API, progressive-enhancement forms, accessible labels, the modules split, and SQLite.' },
|
|
1530
|
-
];
|
|
1531
|
-
|
|
1532
|
-
const galleryCard = (item: { href: string; title: string; blurb: string }) => html\`
|
|
1533
|
-
<a href=\${item.href} class="block no-underline">
|
|
1534
|
-
<div class="\${cardClass()} h-full transition-colors hover:border-border-strong">
|
|
1535
|
-
<div class=\${cardHeaderClass()}>
|
|
1536
|
-
<h3 class=\${cardTitleClass()}>\${item.title}</h3>
|
|
1537
|
-
<p class=\${cardDescriptionClass()}>\${item.blurb}</p>
|
|
1538
|
-
</div>
|
|
1539
|
-
</div>
|
|
1540
|
-
</a>
|
|
1541
|
-
\`;
|
|
1542
|
-
|
|
1543
|
-
export default function Home() {
|
|
1544
|
-
return html\`
|
|
1545
|
-
<section class="mb-14">
|
|
1546
|
-
\${rubric('welcome')}
|
|
1547
|
-
\${displayH1(html\`Hello from <span class="text-primary italic">${displayName}</span>.\`)}
|
|
1548
|
-
<p class="text-lede leading-[1.5] text-muted-foreground max-w-[56ch] m-0 mb-6">
|
|
1549
|
-
The saas starter: email and password auth, a protected dashboard, and a
|
|
1550
|
-
User model, all wired up. Replace this hero with your product's landing;
|
|
1551
|
-
the webjs feature gallery below is reference, prune what you do not need.
|
|
1552
|
-
See \${accentLink('https://docs.webjs.dev', 'the docs')} for the full reference.
|
|
1553
|
-
</p>
|
|
1554
|
-
<div class="flex gap-3 items-center">
|
|
1555
|
-
<a href="/login" class="inline-flex items-center px-4 py-2 rounded-xl bg-primary text-primary-foreground font-semibold text-sm no-underline transition-all hover:bg-primary/90 active:scale-[0.97]">Log in</a>
|
|
1556
|
-
<a href="/signup" class="text-primary no-underline font-medium text-sm">Create an account</a>
|
|
1557
|
-
<a href="/dashboard" class="text-muted-foreground no-underline font-medium text-sm hover:text-foreground transition-colors">Dashboard</a>
|
|
1558
|
-
</div>
|
|
1559
|
-
</section>
|
|
1560
|
-
|
|
1561
|
-
<section class="mb-12">
|
|
1562
|
-
<h2 class="font-serif text-[1.6rem] tracking-[-0.02em] font-bold m-0 mb-1">Features</h2>
|
|
1563
|
-
<p class="text-muted-foreground text-sm m-0 mb-5">
|
|
1564
|
-
One webjs concept each, under <code class="font-mono text-[0.9em]">app/features/</code>
|
|
1565
|
-
with logic in <code class="font-mono text-[0.9em]">modules/</code>. Delete the ones you do not need.
|
|
1566
|
-
</p>
|
|
1567
|
-
<div class="grid gap-4 sm:grid-cols-2">
|
|
1568
|
-
\${features.map(galleryCard)}
|
|
1569
|
-
</div>
|
|
1570
|
-
</section>
|
|
1571
|
-
|
|
1572
|
-
<section>
|
|
1573
|
-
<h2 class="font-serif text-[1.6rem] tracking-[-0.02em] font-bold m-0 mb-1">Example apps</h2>
|
|
1574
|
-
<p class="text-muted-foreground text-sm m-0 mb-5">
|
|
1575
|
-
Whole apps that compose several features, under <code class="font-mono text-[0.9em]">app/examples/</code>.
|
|
1576
|
-
</p>
|
|
1577
|
-
<div class="grid gap-4 sm:grid-cols-2">
|
|
1578
|
-
\${examples.map(galleryCard)}
|
|
1579
|
-
</div>
|
|
1580
|
-
</section>
|
|
1581
|
-
\`;
|
|
1422
|
+
function iconBook() {
|
|
1423
|
+
return html\`<svg class="w-4 h-4 stroke-current fill-none" style="stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round" viewBox="0 0 24 24"><path d="M4 5a2 2 0 0 1 2-2h13v16H6a2 2 0 0 0-2 2z"/><path d="M4 19a2 2 0 0 1 2-2h13"/></svg>\`;
|
|
1424
|
+
}
|
|
1425
|
+
function iconGithub() {
|
|
1426
|
+
return html\`<svg class="w-4 h-4 fill-current" viewBox="0 0 24 24"><path d="M12 2a10 10 0 0 0-3.16 19.49c.5.09.68-.22.68-.48v-1.7c-2.78.6-3.37-1.34-3.37-1.34-.45-1.16-1.11-1.47-1.11-1.47-.9-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.9 1.53 2.34 1.09 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02a9.5 9.5 0 0 1 5 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.59 1.03 2.68 0 3.84-2.34 4.69-4.57 4.94.36.31.68.92.68 1.85v2.74c0 .27.18.58.69.48A10 10 0 0 0 12 2Z"/></svg>\`;
|
|
1582
1427
|
}
|
|
1583
1428
|
`);
|
|
1584
|
-
}
|
|
1585
1429
|
|
|
1586
1430
|
// AGENTS.md is copied via the `templateFiles` loop above, from
|
|
1587
1431
|
// `packages/cli/templates/AGENTS.md` with `{{APP_NAME}}` substitution.
|
|
@@ -1668,90 +1512,49 @@ ThemeToggle.register('theme-toggle');
|
|
|
1668
1512
|
const { execSync } = await import('node:child_process');
|
|
1669
1513
|
try {
|
|
1670
1514
|
execSync('git init', { cwd: appDir, stdio: 'pipe' });
|
|
1671
|
-
//
|
|
1515
|
+
// Use the tracked .hooks/ dir (the pre-commit blocks commits to main).
|
|
1672
1516
|
execSync('git config core.hooksPath .hooks', { cwd: appDir, stdio: 'pipe' });
|
|
1673
1517
|
} catch { /* git not available: skip */ }
|
|
1674
1518
|
|
|
1675
1519
|
// --- Print success ---
|
|
1676
1520
|
|
|
1521
|
+
const guide = 'AGENTS.md, .agents/skills/webjs/ ← the agent guide';
|
|
1677
1522
|
if (isApi) {
|
|
1678
1523
|
console.log(` ${name}/
|
|
1679
|
-
app/api/health/route.ts
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1524
|
+
app/api/{health,users}/route.ts
|
|
1525
|
+
modules/users/{actions,queries,types.ts} ← routes over server actions
|
|
1526
|
+
db/{schema,columns,connection}.server.ts ← Drizzle (User model)
|
|
1527
|
+
${guide}
|
|
1683
1528
|
`);
|
|
1684
1529
|
} else if (isSaas) {
|
|
1685
1530
|
console.log(` ${name}/
|
|
1686
|
-
app/layout
|
|
1531
|
+
app/{layout,page}.ts, login/, signup/
|
|
1687
1532
|
app/dashboard/{page,settings,middleware}.ts ← protected
|
|
1688
|
-
app/api/auth/[...path]/route.ts
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
components/theme-toggle.ts
|
|
1694
|
-
modules/auth/{actions,queries,types.ts}
|
|
1695
|
-
lib/{auth,password}.server.ts
|
|
1696
|
-
lib/utils/cn.ts ← cn() helper for ui-* components
|
|
1697
|
-
db/{schema,columns,connection}.server.ts ← Drizzle (User model)
|
|
1698
|
-
CONVENTIONS.md, AGENTS.md, CLAUDE.md
|
|
1533
|
+
app/api/auth/[...path]/route.ts ← auth API
|
|
1534
|
+
components/ui/*, components/theme-toggle.ts
|
|
1535
|
+
modules/auth/*, lib/{auth,password}.server.ts
|
|
1536
|
+
db/{schema,columns,connection}.server.ts ← Drizzle (User model)
|
|
1537
|
+
${guide}
|
|
1699
1538
|
`);
|
|
1700
1539
|
} else {
|
|
1701
1540
|
console.log(` ${name}/
|
|
1702
|
-
app/layout
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
styles/globals.css ← @webjsdev/ui theme tokens
|
|
1708
|
-
components.json ← preconfigured for \`webjs ui add\`
|
|
1709
|
-
components/ui/{button,card,alert,badge,separator,label,input}.ts
|
|
1710
|
-
components/theme-toggle.ts ← light DOM web component
|
|
1711
|
-
lib/utils/cn.ts ← cn() helper for ui-* components
|
|
1712
|
-
lib/utils/ui.ts ← Tailwind class-bundle helpers
|
|
1713
|
-
public/input.css ← Tailwind entry (compiled to public/tailwind.css)
|
|
1714
|
-
modules/{components,server-actions,optimistic-ui,async-render,
|
|
1715
|
-
directives,todo}/ ← feature + example logic (prune what you skip)
|
|
1716
|
-
db/{schema,columns,connection}.server.ts ← Drizzle (User + Todo)
|
|
1717
|
-
CONVENTIONS.md, AGENTS.md, CLAUDE.md
|
|
1541
|
+
app/{layout,page}.ts ← a minimal home to grow in place
|
|
1542
|
+
components/theme-toggle.ts
|
|
1543
|
+
public/input.css ← Tailwind entry (compiles to public/tailwind.css)
|
|
1544
|
+
db/{schema,columns,connection}.server.ts ← Drizzle
|
|
1545
|
+
${guide}
|
|
1718
1546
|
`);
|
|
1719
1547
|
}
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
✓ Convention validation via \`webjs check\`
|
|
1731
|
-
|
|
1732
|
-
For AI agents, read this before editing scaffolded files:
|
|
1733
|
-
• The example layout, home page, the gallery under app/features/ and
|
|
1734
|
-
app/examples/ (+ their modules) and schema are REFERENCE ONLY. Replace
|
|
1735
|
-
them with the app the user asked for, and prune the routes you do not
|
|
1736
|
-
use (delete the app/features/<name> or app/examples/<name> route AND
|
|
1737
|
-
its modules/<name>). Don't ship the scaffold's examples as the final
|
|
1738
|
-
product.
|
|
1739
|
-
• This fresh app intentionally FAILS \`webjs check\` with
|
|
1740
|
-
no-scaffold-placeholder violations on the example content (the home
|
|
1741
|
-
page, the layout, and each gallery route). That is the signal to
|
|
1742
|
-
replace or prune each. Delete the marker comment line as you do, and
|
|
1743
|
-
the check goes green.
|
|
1744
|
-
• Use Drizzle + SQLite for app data. It's already wired up. Define
|
|
1745
|
-
real models in db/schema.server.ts, then run \`webjs db generate\`
|
|
1746
|
-
and \`webjs db migrate\`. NEVER store app data in JSON files,
|
|
1747
|
-
in-memory arrays, or localStorage as a substitute for the database.
|
|
1748
|
-
• Only three scaffolds exist: full-stack (default), api, saas. Don't
|
|
1749
|
-
invent template names. If you need a different kind of app, pick
|
|
1750
|
-
the closest scaffold and adapt it.
|
|
1751
|
-
• Read AGENTS.md + CONVENTIONS.md in the new project before writing
|
|
1752
|
-
any code. They are the contract.
|
|
1753
|
-
• Need more detail? Full hosted docs are at https://docs.webjs.dev
|
|
1754
|
-
(every API, directive, recipe, and deployment guide).
|
|
1548
|
+
console.log(`For AI agents, read this before editing:
|
|
1549
|
+
• Read AGENTS.md, then .agents/skills/webjs/SKILL.md. The skill is the guide
|
|
1550
|
+
to building a WebJs app and routes to focused references on demand.
|
|
1551
|
+
• This scaffold is a minimal starting point, not a demo to prune. Grow the app
|
|
1552
|
+
in place: add routes under app/, components under components/, and features
|
|
1553
|
+
under modules/<feature>/, and keep server-only code behind .server.ts.
|
|
1554
|
+
• Use the wired-up database (Drizzle): define real models in
|
|
1555
|
+
db/schema.server.ts, then run 'npm run db:generate' and 'npm run db:migrate'.
|
|
1556
|
+
Never store app data in JSON files, in-memory arrays, or localStorage.
|
|
1557
|
+
• Full hosted docs are at https://docs.webjs.dev.
|
|
1755
1558
|
`);
|
|
1756
1559
|
|
|
1757
1560
|
// Auto-install (default). Detect the package manager from the env so
|