@webjsdev/cli 0.10.44 → 0.10.46
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/README.md +2 -3
- package/bin/webjs.js +18 -18
- package/lib/api-gallery.js +1 -1
- package/lib/create.js +154 -243
- package/package.json +1 -1
- package/templates/.agents/rules/workflow.md +7 -3
- package/templates/.agents/skills/webjs/SKILL.md +4 -2
- package/templates/.agents/skills/webjs/references/auth-and-sessions.md +78 -16
- package/templates/.agents/skills/webjs/references/built-ins.md +16 -2
- package/templates/.agents/skills/webjs/references/client-router-and-streaming.md +38 -6
- package/templates/.agents/skills/webjs/references/components.md +101 -6
- package/templates/.agents/skills/webjs/references/data-and-actions.md +19 -2
- package/templates/.agents/skills/webjs/references/muscle-memory-gotchas.md +5 -1
- package/templates/.agents/skills/webjs/references/optimistic-ui.md +18 -0
- package/templates/.agents/skills/webjs/references/routing-and-pages.md +25 -2
- package/templates/.agents/skills/webjs/references/runtime.md +1 -1
- package/templates/.agents/skills/webjs/references/service-worker.md +1 -1
- package/templates/.agents/skills/webjs/references/styling.md +82 -2
- package/templates/AGENTS.md +12 -5
- package/templates/gallery/app/api/auth/[...path]/route.ts +7 -0
- package/templates/gallery/app/apple-icon.ts +2 -5
- package/templates/gallery/app/examples/layout.ts +16 -0
- package/templates/gallery/app/examples/todo/page.ts +2 -1
- package/templates/gallery/app/features/async-render/page.ts +3 -2
- package/templates/gallery/app/features/auth/dashboard/layout.ts +21 -0
- package/templates/gallery/app/features/auth/dashboard/middleware.ts +14 -0
- package/templates/gallery/app/features/auth/dashboard/page.ts +20 -0
- package/templates/gallery/app/features/auth/dashboard/settings/page.ts +22 -0
- package/templates/gallery/app/features/auth/login/middleware.ts +15 -0
- package/templates/gallery/app/features/auth/login/page.ts +43 -0
- package/templates/gallery/app/features/auth/page.ts +34 -0
- package/templates/gallery/app/features/auth/signup/middleware.ts +11 -0
- package/templates/gallery/app/features/auth/signup/page.ts +61 -0
- package/templates/gallery/app/features/boundaries/error.ts +5 -4
- package/templates/gallery/app/features/boundaries/gated/forbidden.ts +5 -4
- package/templates/gallery/app/features/boundaries/not-found.ts +5 -4
- package/templates/gallery/app/features/boundaries/page.ts +11 -10
- package/templates/gallery/app/features/boundaries/private/unauthorized.ts +5 -4
- package/templates/gallery/app/features/broadcast/page.ts +4 -3
- package/templates/gallery/app/features/caching/page.ts +5 -4
- package/templates/gallery/app/features/client-router/page.ts +7 -5
- package/templates/gallery/app/features/client-router/second/page.ts +4 -3
- package/templates/gallery/app/features/components/page.ts +3 -2
- package/templates/gallery/app/features/directives/page.ts +3 -2
- package/templates/gallery/app/features/env/page.ts +4 -3
- package/templates/gallery/app/features/file-storage/page.ts +8 -5
- package/templates/gallery/app/features/forms/page.ts +10 -6
- package/templates/gallery/app/features/frames/page.ts +26 -10
- package/templates/gallery/app/features/layout.ts +67 -0
- package/templates/gallery/app/features/metadata/page.ts +7 -6
- package/templates/gallery/app/features/optimistic-ui/page.ts +3 -2
- package/templates/gallery/app/features/rate-limit/page.ts +6 -5
- package/templates/gallery/app/features/route-handler/page.ts +4 -3
- package/templates/gallery/app/features/routing/[id]/page.ts +7 -6
- package/templates/gallery/app/features/routing/page.ts +10 -9
- package/templates/gallery/app/features/server-actions/page.ts +7 -4
- package/templates/gallery/app/features/service-worker/page.ts +4 -3
- package/templates/gallery/app/features/sessions/page.ts +5 -4
- package/templates/gallery/app/features/stream/page.ts +46 -0
- package/templates/gallery/app/features/streaming/page.ts +32 -0
- package/templates/gallery/app/features/suspense/page.ts +35 -0
- package/templates/gallery/app/features/view-transitions/page.ts +44 -0
- package/templates/gallery/app/features/view-transitions/second/page.ts +30 -0
- package/templates/gallery/app/features/websockets/page.ts +4 -3
- package/templates/gallery/app/global-error.ts +2 -5
- package/templates/gallery/app/global-not-found.ts +4 -6
- package/templates/gallery/app/icon.ts +2 -5
- package/templates/gallery/app/manifest.ts +1 -4
- package/templates/gallery/app/opengraph-image.ts +3 -6
- package/templates/gallery/app/robots.ts +0 -3
- package/templates/gallery/app/sitemap.ts +0 -3
- package/templates/gallery/app/twitter-image.ts +3 -6
- package/templates/gallery/components/ui/badge.ts +41 -0
- package/templates/gallery/components/ui/button.ts +86 -0
- package/templates/gallery/components/ui/card.ts +36 -0
- package/templates/gallery/components/ui/input.ts +50 -0
- package/templates/gallery/lib/utils/ui.ts +31 -0
- package/templates/gallery/modules/auth/actions/signup.server.ts +19 -0
- package/templates/gallery/modules/auth/auth.server.ts +53 -0
- package/templates/gallery/modules/auth/password.server.ts +20 -0
- package/templates/gallery/modules/auth/queries/current-user.server.ts +12 -0
- package/templates/gallery/modules/auth/types.ts +9 -0
- package/templates/gallery/modules/broadcast/components/broadcast-feed.ts +4 -2
- package/templates/gallery/modules/caching/components/cache-buster.ts +2 -1
- package/templates/gallery/modules/client-router/components/router-controls.ts +4 -3
- package/templates/gallery/modules/components/components/counter-card.ts +4 -2
- package/templates/gallery/modules/components/components/reactive-meter.ts +9 -1
- package/templates/gallery/modules/components/components/task-loader.ts +3 -2
- package/templates/gallery/modules/components/components/theme-context.ts +5 -3
- package/templates/gallery/modules/directives/components/directive-demo.ts +17 -10
- package/templates/gallery/modules/gallery/components/gallery-nav.ts +54 -0
- package/templates/gallery/modules/gallery/nav.ts +79 -0
- package/templates/gallery/modules/optimistic-ui/components/like-button.ts +19 -1
- package/templates/gallery/modules/rate-limit/components/rate-probe.ts +2 -1
- package/templates/gallery/modules/route-handler/components/rich-data.ts +2 -1
- package/templates/gallery/modules/server-actions/actions/greet.server.ts +2 -2
- package/templates/gallery/modules/server-actions/actions/greet.test.ts +39 -36
- package/templates/gallery/modules/server-actions/components/greeter.ts +11 -11
- package/templates/gallery/modules/server-actions/middleware/require-auth.server.ts +14 -9
- package/templates/gallery/modules/stream/components/stream-demo.ts +81 -0
- package/templates/gallery/modules/streaming/actions/stream-tokens.server.ts +17 -0
- package/templates/gallery/modules/streaming/components/token-stream.ts +53 -0
- package/templates/gallery/modules/suspense/components/slow-fact.ts +20 -0
- package/templates/gallery/modules/todo/components/todo-app.ts +8 -4
- package/templates/gallery/modules/websockets/components/ws-echo.ts +5 -3
- package/templates/gallery/test/auth/auth.test.ts +81 -0
- package/templates/public/favicon.svg +10 -3
- package/templates/scripts/clear-api-gallery.mjs +55 -0
- package/templates/scripts/clear-gallery.mjs +143 -24
- package/lib/lean-copy.js +0 -43
- package/lib/saas-template.js +0 -568
package/lib/create.js
CHANGED
|
@@ -18,7 +18,6 @@ import { existsSync } from 'node:fs';
|
|
|
18
18
|
import { createRequire } from 'node:module';
|
|
19
19
|
import { spawnSync } from 'node:child_process';
|
|
20
20
|
import { bunifyProse, bunifyDockerfile, bunifyCompose, bunifyCi } from './runtime-rewrite.js';
|
|
21
|
-
import { leanComponentSource } from './lean-copy.js';
|
|
22
21
|
|
|
23
22
|
/**
|
|
24
23
|
* Detect which package manager invoked us. Reads `npm_config_user_agent`,
|
|
@@ -106,60 +105,6 @@ function resolveUiRegistryRoot() {
|
|
|
106
105
|
}
|
|
107
106
|
const UI_REGISTRY_ROOT = resolveUiRegistryRoot();
|
|
108
107
|
|
|
109
|
-
/**
|
|
110
|
-
* Read a single @webjsdev/ui registry component, rewrite its relative import
|
|
111
|
-
* of `../lib/utils.ts` to the scaffolded app's aliased path so it resolves
|
|
112
|
-
* when written to `components/ui/<name>.ts`. The scaffold puts cn() at
|
|
113
|
-
* `lib/utils/cn.ts` (folder-grouped with other browser-safe helpers), so the
|
|
114
|
-
* alias form is `#lib/utils/cn.ts` (#555/#556).
|
|
115
|
-
*
|
|
116
|
-
* @param {string} name component name without `.ts` (e.g. 'button')
|
|
117
|
-
* @returns {Promise<string>} source with import rewritten
|
|
118
|
-
*/
|
|
119
|
-
async function readUiComponent(name) {
|
|
120
|
-
const src = join(UI_REGISTRY_ROOT, 'components', `${name}.ts`);
|
|
121
|
-
const raw = await readFile(src, 'utf8');
|
|
122
|
-
// The registry component imports cn() via a relative `../lib/utils.ts`; rewrite
|
|
123
|
-
// it to the scaffolded app's aliased path (cn lives at lib/utils/cn.ts).
|
|
124
|
-
const rewritten = raw
|
|
125
|
-
.replaceAll("'../lib/utils.ts'", "'#lib/utils/cn.ts'")
|
|
126
|
-
.replaceAll('"../lib/utils.ts"', '"#lib/utils/cn.ts"')
|
|
127
|
-
// onBeforeCache lives in its own client-only module so cn() stays pure (#819).
|
|
128
|
-
.replaceAll("'../lib/dom.ts'", "'#lib/utils/dom.ts'")
|
|
129
|
-
.replaceAll('"../lib/dom.ts"', '"#lib/utils/dom.ts"');
|
|
130
|
-
// Strip the worked @example from a Tier-1 helper (same as `webjs ui add`), so
|
|
131
|
-
// the scaffolded component is lean and the example is served on demand. The
|
|
132
|
-
// shared helper is used by the saas-template copier too, so they cannot drift.
|
|
133
|
-
return leanComponentSource(rewritten, name);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
/**
|
|
137
|
-
* Copy a list of @webjsdev/ui registry components into the scaffolded app
|
|
138
|
-
* under `components/ui/`. Throws if any name is missing from the registry,
|
|
139
|
-
* since the scaffold's generated pages import these by name and a missing
|
|
140
|
-
* file would produce ERR_MODULE_NOT_FOUND at first request. Caller must
|
|
141
|
-
* have already invoked assertUiRegistryAvailable().
|
|
142
|
-
*
|
|
143
|
-
* @param {string} appDir destination app root
|
|
144
|
-
* @param {string[]} names list of component file basenames (without `.ts`)
|
|
145
|
-
*/
|
|
146
|
-
async function copyUiComponents(appDir, names) {
|
|
147
|
-
const uiDir = join(appDir, 'components', 'ui');
|
|
148
|
-
await mkdir(uiDir, { recursive: true });
|
|
149
|
-
for (const n of names) {
|
|
150
|
-
const src = join(UI_REGISTRY_ROOT, 'components', `${n}.ts`);
|
|
151
|
-
if (!existsSync(src)) {
|
|
152
|
-
throw new Error(
|
|
153
|
-
`@webjsdev/ui registry is missing component '${n}.ts' at ${src}. ` +
|
|
154
|
-
`The scaffold's example pages import this component by name. ` +
|
|
155
|
-
`Either the registry was published incompletely or the scaffold's ` +
|
|
156
|
-
`component list is out of sync with the registry.`,
|
|
157
|
-
);
|
|
158
|
-
}
|
|
159
|
-
await writeFile(join(uiDir, `${n}.ts`), await readUiComponent(n));
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
108
|
/**
|
|
164
109
|
* Copy the example gallery (idiomatic, densely-commented working examples) into
|
|
165
110
|
* the scaffolded app. Merges `templates/gallery/{app,modules}` over the app so
|
|
@@ -177,7 +122,18 @@ async function copyUiComponents(appDir, names) {
|
|
|
177
122
|
*/
|
|
178
123
|
async function copyGallery(appDir) {
|
|
179
124
|
const galleryDir = join(TEMPLATES, 'gallery');
|
|
180
|
-
|
|
125
|
+
// `test` carries the auth card's real request-pipeline test (test/auth); it
|
|
126
|
+
// ships with the gallery and is pruned by gallery:clear alongside the card.
|
|
127
|
+
// `components` carries the gallery's EXAMPLE design system (components/ui/ class
|
|
128
|
+
// helpers) the demos import. It ships here so a fresh app's demos work, but it
|
|
129
|
+
// is an example to learn from, so gallery:clear REMOVES it (the agent then runs
|
|
130
|
+
// `webjs ui add` and themes its own components/ui/); only cn.ts is kept as the
|
|
131
|
+
// `webjs ui add` prerequisite. It merges into the app's components/ alongside
|
|
132
|
+
// the separately-written theme toggle (also removed by gallery:clear).
|
|
133
|
+
// `lib` merges the gallery's markup-chunk helpers (lib/utils/ui.ts) alongside
|
|
134
|
+
// the ui bootstrap's cn.ts/dom.ts (written earlier); gallery:clear removes just
|
|
135
|
+
// ui.ts. cp is recursive-merge, so the pre-written lib/utils/ files are kept.
|
|
136
|
+
for (const sub of ['app', 'modules', 'test', 'components', 'lib']) {
|
|
181
137
|
await cp(join(galleryDir, sub), join(appDir, sub), { recursive: true });
|
|
182
138
|
}
|
|
183
139
|
}
|
|
@@ -312,21 +268,19 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
312
268
|
const shouldInstall = opts.install === true;
|
|
313
269
|
// Defence in depth. The CLI already validates this, but library
|
|
314
270
|
// callers (tests, programmatic use) might pass anything.
|
|
315
|
-
const VALID_TEMPLATES = ['full-stack', 'api'
|
|
271
|
+
const VALID_TEMPLATES = ['full-stack', 'api'];
|
|
316
272
|
if (!VALID_TEMPLATES.includes(template)) {
|
|
317
273
|
throw new Error(
|
|
318
274
|
`Unknown template '${template}'. Only ${VALID_TEMPLATES.join(' / ')} exist.`,
|
|
319
275
|
);
|
|
320
276
|
}
|
|
321
277
|
const isApi = template === 'api';
|
|
322
|
-
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
|
|
328
|
-
// that differ (its own home page and the create.js-written schema).
|
|
329
|
-
const isFullStack = !isApi && !isSaas;
|
|
278
|
+
// The example gallery ships in the one UI template (not api, which has no UI),
|
|
279
|
+
// so the copyGallery gate below is !isApi. Auth is one of the gallery cards
|
|
280
|
+
// (app/features/auth + modules/auth), so a UI app ships a real, prunable auth
|
|
281
|
+
// baseline. `isFullStack` names the UI template for the parts that differ from
|
|
282
|
+
// api (the todos table + the passwordHash column the auth card needs).
|
|
283
|
+
const isFullStack = !isApi;
|
|
330
284
|
|
|
331
285
|
// Database dialect (#563): sqlite (default) or postgres. Drizzle is the ORM;
|
|
332
286
|
// the schema/queries/actions are identical across dialects, only db/columns
|
|
@@ -420,8 +374,13 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
420
374
|
// app runs the compiler under Bun (its image has no Node), a Node app runs
|
|
421
375
|
// it directly.
|
|
422
376
|
...(isApi ? {} : { 'css:build': cssBuildCmd }),
|
|
423
|
-
// Shed the demo gallery to a clean, buildable
|
|
424
|
-
|
|
377
|
+
// Shed the demo gallery / backend-features showcase to a clean, buildable
|
|
378
|
+
// base. The UI template runs clear-gallery.mjs; the api template runs
|
|
379
|
+
// clear-api-gallery.mjs (its showcase is app/api/features, not app/features).
|
|
380
|
+
'gallery:clear': (() => {
|
|
381
|
+
const script = isApi ? 'clear-api-gallery.mjs' : 'clear-gallery.mjs';
|
|
382
|
+
return isBun ? `bun scripts/${script}` : `node scripts/${script}`;
|
|
383
|
+
})(),
|
|
425
384
|
dev: isBun ? 'bun --bun webjs dev' : 'webjs dev',
|
|
426
385
|
start: isBun ? 'bun --bun webjs start' : 'webjs start',
|
|
427
386
|
test: 'webjs test',
|
|
@@ -763,7 +722,10 @@ export const users = table('users', {
|
|
|
763
722
|
name: text(),
|
|
764
723
|
// JSON column: a structured value persisted as JSON, typed via json<T>().
|
|
765
724
|
// Same helper works on SQLite and Postgres. Delete if you do not need it.
|
|
766
|
-
settings: json<{ theme?: string }>()
|
|
725
|
+
settings: json<{ theme?: string }>(),${isFullStack ? `
|
|
726
|
+
// The auth gallery card (app/features/auth) signs credentials against this.
|
|
727
|
+
// gallery:clear removes this column with the rest of the auth surface.
|
|
728
|
+
passwordHash: text(),` : ''}
|
|
767
729
|
createdAt: createdAt(),
|
|
768
730
|
});
|
|
769
731
|
${isFullStack ? `
|
|
@@ -1040,10 +1002,18 @@ export type ActionResult<T> =
|
|
|
1040
1002
|
// counterpart of the UI gallery. Prune what you skip.
|
|
1041
1003
|
const { writeApiGallery } = await import('./api-gallery.js');
|
|
1042
1004
|
await writeApiGallery(appDir);
|
|
1005
|
+
|
|
1006
|
+
// The showcase-reset script (wired as `gallery:clear` for the api template).
|
|
1007
|
+
// It sheds app/api/features + its modules back to the health + users base.
|
|
1008
|
+
const apiClearSrc = join(TEMPLATES, 'scripts', 'clear-api-gallery.mjs');
|
|
1009
|
+
if (existsSync(apiClearSrc)) {
|
|
1010
|
+
await mkdir(join(appDir, 'scripts'), { recursive: true });
|
|
1011
|
+
await cp(apiClearSrc, join(appDir, 'scripts', 'clear-api-gallery.mjs'));
|
|
1012
|
+
}
|
|
1043
1013
|
}
|
|
1044
1014
|
|
|
1045
1015
|
if (!isApi) {
|
|
1046
|
-
//
|
|
1016
|
+
// The UI template: layout + page + theme toggle + Tailwind
|
|
1047
1017
|
|
|
1048
1018
|
// The Tailwind stylesheet is compiled from public/input.css (written below)
|
|
1049
1019
|
// to a STATIC public/tailwind.css by css:build, and lib/utils/ui.ts helpers
|
|
@@ -1053,8 +1023,8 @@ export type ActionResult<T> =
|
|
|
1053
1023
|
const publicDir = join(appDir, 'public');
|
|
1054
1024
|
await mkdir(publicDir, { recursive: true });
|
|
1055
1025
|
// Progressive-enhancement service worker (#271): ship the opt-in offline
|
|
1056
|
-
// primitive (the worker + its offline fallback) into the UI
|
|
1057
|
-
// (
|
|
1026
|
+
// primitive (the worker + its offline fallback) into the UI scaffold
|
|
1027
|
+
// (this block is api-excluded since api has no UI).
|
|
1058
1028
|
// Dormant until the app registers it (see the skill's references/service-worker.md);
|
|
1059
1029
|
// it never changes the JS-disabled baseline.
|
|
1060
1030
|
for (const swFile of ['sw.js', 'offline.html']) {
|
|
@@ -1087,13 +1057,9 @@ export type ActionResult<T> =
|
|
|
1087
1057
|
// styles/globals.css (the @webjsdev/ui theme).
|
|
1088
1058
|
await writeUiBootstrap(appDir);
|
|
1089
1059
|
|
|
1090
|
-
// The
|
|
1091
|
-
//
|
|
1092
|
-
|
|
1093
|
-
await copyUiComponents(appDir, [
|
|
1094
|
-
'button', 'card', 'alert', 'badge', 'separator', 'label', 'input',
|
|
1095
|
-
]);
|
|
1096
|
-
}
|
|
1060
|
+
// The gallery cards style with plain Tailwind (the app is pre-initialised for
|
|
1061
|
+
// `webjs ui add <name>` via writeUiBootstrap above, but ships no components
|
|
1062
|
+
// until you add them on demand).
|
|
1097
1063
|
|
|
1098
1064
|
// The @webjsdev/ui theme (`--color-primary`, `--color-card`, the @theme maps,
|
|
1099
1065
|
// @custom-variant, @keyframes) plus the app @theme mappings are compiled from
|
|
@@ -1136,10 +1102,10 @@ ${uiThemeRaw}
|
|
|
1136
1102
|
|
|
1137
1103
|
// The gallery: idiomatic, densely-commented single-feature demos under
|
|
1138
1104
|
// app/features/ plus one whole example app under app/examples/, with logic
|
|
1139
|
-
// in modules/, all linked from the home page below. Shipped in
|
|
1140
|
-
// scaffold
|
|
1141
|
-
//
|
|
1142
|
-
// the
|
|
1105
|
+
// in modules/, all linked from the home page below. Shipped in the UI
|
|
1106
|
+
// scaffold so an agent gains context by browsing real working code; prune
|
|
1107
|
+
// per-feature (delete the route + its module) for what the app does not use,
|
|
1108
|
+
// or shed the whole gallery at once with `gallery:clear`.
|
|
1143
1109
|
await copyGallery(appDir);
|
|
1144
1110
|
|
|
1145
1111
|
await writeFile(join(appDir, 'app', 'layout.ts'), `import { html, cspNonce } from '@webjsdev/core';
|
|
@@ -1154,26 +1120,34 @@ import '#components/theme-toggle.ts';
|
|
|
1154
1120
|
* mapped into Tailwind via @theme in public/input.css, so bg-background,
|
|
1155
1121
|
* text-foreground, bg-card, bg-primary, and border-border all work.
|
|
1156
1122
|
*/
|
|
1123
|
+
|
|
1124
|
+
// Declare the favicon via metadata.icons (NOT a hand-written <link> in the
|
|
1125
|
+
// template): the framework emits metadata links into <head>, whereas a <link>
|
|
1126
|
+
// written in the layout body stays in <body>, where browsers ignore it. The SVG
|
|
1127
|
+
// lives at public/favicon.svg and serves at /public/favicon.svg.
|
|
1128
|
+
export const metadata = { icons: '/public/favicon.svg' };
|
|
1129
|
+
|
|
1157
1130
|
export default function RootLayout({ children }: { children: unknown }) {
|
|
1158
1131
|
// Read the in-flight request's CSP nonce so the theme-detection inline script
|
|
1159
1132
|
// passes strict CSP. Returns '' when no CSP nonce is set.
|
|
1160
1133
|
const nonce = cspNonce();
|
|
1161
1134
|
return html\`
|
|
1162
1135
|
<script nonce="\${nonce}">
|
|
1163
|
-
// Light/dark theme:
|
|
1164
|
-
//
|
|
1165
|
-
//
|
|
1136
|
+
// Light/dark theme: apply the saved choice before paint (no flash). The
|
|
1137
|
+
// tokens follow color-scheme, which [data-theme] forces and otherwise
|
|
1138
|
+
// follows the OS, so an unset choice needs NO inline work here. The .dark
|
|
1139
|
+
// class is synced only for @webjsdev/ui components (they key off .dark).
|
|
1140
|
+
// Delete this block and the [data-theme] rules below for a single-theme app.
|
|
1166
1141
|
(function(){
|
|
1167
1142
|
try {
|
|
1168
|
-
var mq = window.matchMedia('(prefers-color-scheme:
|
|
1143
|
+
var mq = window.matchMedia('(prefers-color-scheme: dark)');
|
|
1169
1144
|
function apply(){
|
|
1170
1145
|
var t = null;
|
|
1171
1146
|
try { t = localStorage.getItem('webjs_theme'); } catch (_) {}
|
|
1172
1147
|
var el = document.documentElement;
|
|
1173
1148
|
if (t === 'light' || t === 'dark') el.dataset.theme = t;
|
|
1174
1149
|
else delete el.dataset.theme;
|
|
1175
|
-
|
|
1176
|
-
el.classList.toggle('dark', dark);
|
|
1150
|
+
el.classList.toggle('dark', t === 'dark' || (t !== 'light' && mq.matches));
|
|
1177
1151
|
}
|
|
1178
1152
|
apply();
|
|
1179
1153
|
mq.addEventListener('change', apply);
|
|
@@ -1187,7 +1161,9 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1187
1161
|
function measure(){
|
|
1188
1162
|
try {
|
|
1189
1163
|
var hdr = document.querySelector('header');
|
|
1190
|
-
|
|
1164
|
+
// Only a FIXED header leaves normal flow and needs its height
|
|
1165
|
+
// reserved; a normal in-flow header (the gallery's navbar) does not.
|
|
1166
|
+
if (!hdr || getComputedStyle(hdr).position !== 'fixed') return;
|
|
1191
1167
|
var apply = function(){
|
|
1192
1168
|
document.documentElement.style.setProperty('--header-h', hdr.offsetHeight + 'px');
|
|
1193
1169
|
};
|
|
@@ -1200,7 +1176,6 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1200
1176
|
})();
|
|
1201
1177
|
</script>
|
|
1202
1178
|
<meta name="color-scheme" content="light dark">
|
|
1203
|
-
<link rel="icon" href="/public/favicon.svg" type="image/svg+xml">
|
|
1204
1179
|
<!-- JetBrains Mono for body/UI (its monospaced, developer-console feel) and
|
|
1205
1180
|
Bricolage Grotesque for the display wordmark. Swap these for your own
|
|
1206
1181
|
fonts (and update --font-sans / --font-display below). -->
|
|
@@ -1215,89 +1190,59 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1215
1190
|
|
|
1216
1191
|
<link rel="stylesheet" href="/public/tailwind.css">
|
|
1217
1192
|
<style>
|
|
1218
|
-
/* Design tokens
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1193
|
+
/* Design tokens: ONE definition per colour via light-dark(LIGHT, DARK), so
|
|
1194
|
+
a palette change lands in a single place (DRY). The token NAMES are
|
|
1195
|
+
infrastructure (public/input.css maps them into Tailwind via @theme); the
|
|
1196
|
+
VALUES are a cool neutral-grey palette with a monospaced type system.
|
|
1197
|
+
color-scheme decides which side of each light-dark() applies: the default
|
|
1198
|
+
'light dark' follows the OS, and the toggle FORCES one via [data-theme]
|
|
1199
|
+
below. The light theme is a crisp WHITE page with near-black text, a
|
|
1200
|
+
readable muted grey, and visible borders (a washed-out light theme comes
|
|
1201
|
+
from a grey page + too-light muted text + faint borders). For a
|
|
1202
|
+
single-theme app, delete the [data-theme] rules and give each token a
|
|
1203
|
+
single colour instead of light-dark().
|
|
1204
|
+
EDGE CASES: light-dark() is COLOUR-only. A colour needed in just one
|
|
1205
|
+
theme sets the unused side to a no-op, e.g. light-dark(#fff, transparent).
|
|
1206
|
+
A DERIVED token that references a light-dark() one (like --primary-tint
|
|
1207
|
+
below) tracks both themes for free. A NON-colour token that must differ
|
|
1208
|
+
per theme (a shadow's geometry, a gradient, a size, an image) cannot use
|
|
1209
|
+
light-dark(); give it a :root[data-theme='dark'] override plus an
|
|
1210
|
+
@media (prefers-color-scheme: dark) { :root:not([data-theme]) { ... } }
|
|
1211
|
+
rule for the OS default. */
|
|
1223
1212
|
:root {
|
|
1224
1213
|
--font-sans: 'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
|
1225
1214
|
--font-serif: ui-serif, 'Iowan Old Style', Palatino, Georgia, serif;
|
|
1226
1215
|
--font-mono: 'JetBrains Mono', ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace;
|
|
1227
1216
|
--font-display: 'Bricolage Grotesque', 'JetBrains Mono', ui-sans-serif, system-ui, sans-serif;
|
|
1228
1217
|
--header-h: 0px;
|
|
1218
|
+
|
|
1219
|
+
color-scheme: light dark; /* default: follow the OS */
|
|
1220
|
+
--background: light-dark(#ffffff, #1e2226);
|
|
1221
|
+
--foreground: light-dark(#191c20, #dee2e6);
|
|
1222
|
+
--card: light-dark(#f7f8fa, #313539);
|
|
1223
|
+
--card-foreground: light-dark(#191c20, #dee2e6);
|
|
1224
|
+
--popover: light-dark(#ffffff, #313539);
|
|
1225
|
+
--popover-foreground: light-dark(#191c20, #dee2e6);
|
|
1226
|
+
--primary: light-dark(#1e2226, #dee2e6);
|
|
1227
|
+
--primary-foreground: light-dark(#ffffff, #1e2226);
|
|
1228
|
+
--secondary: light-dark(#eef0f2, #363a3e);
|
|
1229
|
+
--secondary-foreground: light-dark(#191c20, #dee2e6);
|
|
1230
|
+
--muted: light-dark(#eef0f2, #313539);
|
|
1231
|
+
--muted-foreground: light-dark(#565c64, #94989c);
|
|
1232
|
+
--accent: light-dark(#e9ebef, #363a3e);
|
|
1233
|
+
--accent-foreground: light-dark(#191c20, #f7fbff);
|
|
1234
|
+
--border: light-dark(#e2e5e9, #3d434b);
|
|
1235
|
+
--border-strong: light-dark(#ccd1d7, #454b51);
|
|
1236
|
+
--input: light-dark(#e2e5e9, #34393e);
|
|
1237
|
+
--ring: light-dark(#8b9198, #6b7075);
|
|
1229
1238
|
/* A translucent tint of the primary, tracked automatically across
|
|
1230
1239
|
light/dark. Used for focus rings (ring-primary-tint). */
|
|
1231
1240
|
--primary-tint: color-mix(in srgb, var(--primary) 22%, transparent);
|
|
1232
1241
|
}
|
|
1233
|
-
/*
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
--foreground: #dee2e6;
|
|
1238
|
-
--card: #313539;
|
|
1239
|
-
--card-foreground: #dee2e6;
|
|
1240
|
-
--popover: #313539;
|
|
1241
|
-
--popover-foreground: #dee2e6;
|
|
1242
|
-
--primary: #dee2e6;
|
|
1243
|
-
--primary-foreground: #1e2226;
|
|
1244
|
-
--secondary: #363a3e;
|
|
1245
|
-
--secondary-foreground: #dee2e6;
|
|
1246
|
-
--muted: #313539;
|
|
1247
|
-
--muted-foreground: #94989c;
|
|
1248
|
-
--accent: #363a3e;
|
|
1249
|
-
--accent-foreground: #f7fbff;
|
|
1250
|
-
--border: #34393e;
|
|
1251
|
-
--border-strong: #454b51;
|
|
1252
|
-
--input: #34393e;
|
|
1253
|
-
--ring: #6b7075;
|
|
1254
|
-
}
|
|
1255
|
-
/* light (explicit via the toggle) */
|
|
1256
|
-
:root[data-theme='light'] {
|
|
1257
|
-
color-scheme: light;
|
|
1258
|
-
--background: #dee2e6;
|
|
1259
|
-
--foreground: #313539;
|
|
1260
|
-
--card: #f0f4f7;
|
|
1261
|
-
--card-foreground: #313539;
|
|
1262
|
-
--popover: #f0f4f7;
|
|
1263
|
-
--popover-foreground: #313539;
|
|
1264
|
-
--primary: #313539;
|
|
1265
|
-
--primary-foreground: #f7fbff;
|
|
1266
|
-
--secondary: #f7fbff;
|
|
1267
|
-
--secondary-foreground: #313539;
|
|
1268
|
-
--muted: #eaeef1;
|
|
1269
|
-
--muted-foreground: #767b80;
|
|
1270
|
-
--accent: #f7fbff;
|
|
1271
|
-
--accent-foreground: #313539;
|
|
1272
|
-
--border: #c9d0d6;
|
|
1273
|
-
--border-strong: #b3bbc2;
|
|
1274
|
-
--input: #c9d0d6;
|
|
1275
|
-
--ring: #9aa0a5;
|
|
1276
|
-
}
|
|
1277
|
-
/* light (OS preference, when the user has made no explicit choice) */
|
|
1278
|
-
@media (prefers-color-scheme: light) {
|
|
1279
|
-
:root:not(.dark):not([data-theme='dark']) {
|
|
1280
|
-
color-scheme: light;
|
|
1281
|
-
--background: #dee2e6;
|
|
1282
|
-
--foreground: #313539;
|
|
1283
|
-
--card: #f0f4f7;
|
|
1284
|
-
--card-foreground: #313539;
|
|
1285
|
-
--popover: #f0f4f7;
|
|
1286
|
-
--popover-foreground: #313539;
|
|
1287
|
-
--primary: #313539;
|
|
1288
|
-
--primary-foreground: #f7fbff;
|
|
1289
|
-
--secondary: #f7fbff;
|
|
1290
|
-
--secondary-foreground: #313539;
|
|
1291
|
-
--muted: #eaeef1;
|
|
1292
|
-
--muted-foreground: #767b80;
|
|
1293
|
-
--accent: #f7fbff;
|
|
1294
|
-
--accent-foreground: #313539;
|
|
1295
|
-
--border: #c9d0d6;
|
|
1296
|
-
--border-strong: #b3bbc2;
|
|
1297
|
-
--input: #c9d0d6;
|
|
1298
|
-
--ring: #9aa0a5;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1242
|
+
/* The toggle writes data-theme to FORCE a scheme; with neither attribute
|
|
1243
|
+
the default 'color-scheme: light dark' above follows the OS. */
|
|
1244
|
+
:root[data-theme='light'] { color-scheme: light; }
|
|
1245
|
+
:root[data-theme='dark'] { color-scheme: dark; }
|
|
1301
1246
|
</style>
|
|
1302
1247
|
<style>
|
|
1303
1248
|
/* Base styles utility classes can't reach. */
|
|
@@ -1311,7 +1256,24 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1311
1256
|
-moz-osx-font-smoothing: grayscale;
|
|
1312
1257
|
}
|
|
1313
1258
|
</style>
|
|
1314
|
-
|
|
1259
|
+
<!-- Top navbar, on every page: brand on the left, links + theme toggle on
|
|
1260
|
+
the right. It floats (no separator) and is in normal flow, so it just
|
|
1261
|
+
scrolls with the page; make it a fixed header only if you want it pinned
|
|
1262
|
+
(position: fixed, never sticky, which flickers on iOS during a nav). -->
|
|
1263
|
+
<header class="max-w-5xl mx-auto px-4 sm:px-6 h-14 flex items-center justify-between gap-4">
|
|
1264
|
+
<a href="/" class="inline-flex items-center gap-2 no-underline text-foreground font-bold tracking-tight" style="font-family: var(--font-display)">
|
|
1265
|
+
<span class="w-[22px] h-[22px] rounded-[7px] bg-gradient-to-br from-foreground to-muted-foreground" aria-hidden="true"></span>
|
|
1266
|
+
WebJs Gallery
|
|
1267
|
+
</a>
|
|
1268
|
+
<nav class="flex items-center gap-4 text-sm" aria-label="Primary">
|
|
1269
|
+
<a href="https://docs.webjs.dev" target="_blank" rel="noopener" class="hidden sm:inline text-muted-foreground hover:text-foreground no-underline transition-colors">Docs</a>
|
|
1270
|
+
<a href="https://github.com/webjsdev/webjs" target="_blank" rel="noopener" class="hidden sm:inline text-muted-foreground hover:text-foreground no-underline transition-colors">GitHub</a>
|
|
1271
|
+
<theme-toggle></theme-toggle>
|
|
1272
|
+
</nav>
|
|
1273
|
+
</header>
|
|
1274
|
+
<!-- Fill the viewport minus the h-14 (3.5rem) navbar, so a short page has no
|
|
1275
|
+
spurious scrollbar (min-h-dvh alone would overflow by the navbar height). -->
|
|
1276
|
+
<main class="min-h-[calc(100dvh-3.5rem)] max-w-5xl mx-auto px-4 sm:px-6 py-8">
|
|
1315
1277
|
\${children}
|
|
1316
1278
|
</main>
|
|
1317
1279
|
\`;
|
|
@@ -1322,75 +1284,36 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
1322
1284
|
// demo and the example app, and a footer with the docs + source links. Treat it
|
|
1323
1285
|
// as a starting point: prune the demos you do not use (delete the
|
|
1324
1286
|
// app/features/<x> route AND its modules/<x>), then reshape this page into the
|
|
1325
|
-
// app's real landing page.
|
|
1326
|
-
// spliced under the tagline.
|
|
1327
|
-
const homeAuthLinks = isSaas
|
|
1328
|
-
? '\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>'
|
|
1329
|
-
: '';
|
|
1287
|
+
// app's real landing page.
|
|
1330
1288
|
await writeFile(join(appDir, 'app', 'page.ts'), `import { html } from '@webjsdev/core';
|
|
1289
|
+
import { cardClass } from '#components/ui/card.ts';
|
|
1290
|
+
import { badgeClass } from '#components/ui/badge.ts';
|
|
1291
|
+
// The demo index is defined once in modules/gallery/nav.ts (the same source the
|
|
1292
|
+
// left sidebar reads), so the home cards and the sidebar can never drift.
|
|
1293
|
+
import { FEATURES, EXAMPLES } from '#modules/gallery/nav.ts';
|
|
1331
1294
|
|
|
1332
1295
|
export const metadata = {
|
|
1333
1296
|
title: '${displayName}',
|
|
1334
1297
|
};
|
|
1335
1298
|
|
|
1336
|
-
// The gallery this page links. FEATURES are single-concept demos (one WebJs
|
|
1337
|
-
// concept each, under app/features/, logic in modules/). EXAMPLES are whole apps
|
|
1338
|
-
// composing several features (under app/examples/). Prune what you do not use
|
|
1339
|
-
// (delete the route AND its modules/<name>), then reshape this page.
|
|
1340
|
-
const FEATURES = [
|
|
1341
|
-
{ href: '/features/routing', title: 'Routing', blurb: 'A static route plus a dynamic [id] segment that reads params. The file-based router in miniature.' },
|
|
1342
|
-
{ href: '/features/boundaries', title: 'Boundaries', blurb: 'The control-flow throws (forbidden / unauthorized / notFound) and the nearest boundary file that catches each.' },
|
|
1343
|
-
{ href: '/features/components', title: 'Components', blurb: 'The WebComponent factory, reactive props, instance signals, and slot projection in light DOM.' },
|
|
1344
|
-
{ 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.' },
|
|
1345
|
-
{ href: '/features/optimistic-ui', title: 'Optimistic UI', blurb: 'The imperative optimistic(signal, value, action) flip: instant update, automatic rollback on failure.' },
|
|
1346
|
-
{ 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.' },
|
|
1347
|
-
{ href: '/features/directives', title: 'Directives', blurb: 'The lit-html directive set: repeat for keyed lists, watch(signal) for a fine-grained node swap.' },
|
|
1348
|
-
{ 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.' },
|
|
1349
|
-
{ href: '/features/forms', title: 'Forms', blurb: 'A no-JS progressive-enhancement form posting to the page action, with server-side validation errors.' },
|
|
1350
|
-
{ href: '/features/metadata', title: 'Metadata', blurb: 'Static metadata plus generateMetadata(ctx), which reads the request to compute the title and Open Graph tags.' },
|
|
1351
|
-
{ 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.' },
|
|
1352
|
-
{ href: '/features/env', title: 'Env vars', blurb: 'The server-only vs WEBJS_PUBLIC_ boundary, read during SSR so secrets never reach the browser.' },
|
|
1353
|
-
{ href: '/features/client-router', title: 'Client router', blurb: 'Automatic soft navigation: fragment-only fetches, hover prefetch, scroll restore, and graceful no-JS fallback.' },
|
|
1354
|
-
{ href: '/features/frames', title: 'Frames', blurb: 'A webjs-frame region that swaps a filtered sub-list in place from a link, shipping zero component JS, with a no-JS full-nav fallback.' },
|
|
1355
|
-
{ 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).' },
|
|
1356
|
-
{ href: '/features/websockets', title: 'WebSockets', blurb: 'A WS(ws, req) route endpoint plus the connectWS() client, echoing messages over a live socket.' },
|
|
1357
|
-
{ 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.' },
|
|
1358
|
-
{ href: '/features/rate-limit', title: 'Rate limiting', blurb: 'The rateLimit() middleware scoped to one endpoint, returning a 429 with Retry-After past the window.' },
|
|
1359
|
-
{ href: '/features/file-storage', title: 'File storage', blurb: 'A no-JS multipart upload streamed into the FileStore, then served back through a streaming route.' },
|
|
1360
|
-
{ 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.' },
|
|
1361
|
-
];
|
|
1362
|
-
const EXAMPLES = [
|
|
1363
|
-
{ 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.' },
|
|
1364
|
-
];
|
|
1365
|
-
|
|
1366
1299
|
export default function Home() {
|
|
1367
1300
|
return html\`
|
|
1368
|
-
<div class="
|
|
1369
|
-
|
|
1370
|
-
<div class="max-w-5xl mx-auto px-6 py-16 flex flex-col items-center gap-16">
|
|
1371
|
-
<!-- Masthead -->
|
|
1301
|
+
<div class="py-8 flex flex-col items-center gap-16">
|
|
1302
|
+
<!-- Hero -->
|
|
1372
1303
|
<section class="flex flex-col items-center text-center gap-5">
|
|
1373
|
-
<
|
|
1374
|
-
|
|
1375
|
-
WebJs Gallery
|
|
1304
|
+
<h1 class="text-5xl sm:text-6xl font-bold 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); letter-spacing: -0.02em;">
|
|
1305
|
+
Explore the gallery
|
|
1376
1306
|
</h1>
|
|
1377
1307
|
<p class="text-base sm:text-lg text-muted-foreground max-w-lg leading-relaxed m-0">
|
|
1378
|
-
|
|
1379
|
-
</p
|
|
1308
|
+
Each demo isolates a single WebJs capability in real, runnable code. Read the ones you need, then build your app on the same patterns.
|
|
1309
|
+
</p>
|
|
1380
1310
|
</section>
|
|
1381
1311
|
|
|
1382
1312
|
<!-- Gallery: every feature demo + the example app -->
|
|
1383
1313
|
<section class="w-full flex flex-col gap-6">
|
|
1384
|
-
<div class="flex flex-col items-center gap-2 text-center">
|
|
1385
|
-
<h2 class="text-xs font-semibold uppercase tracking-[0.16em] text-muted-foreground m-0">Explore the gallery</h2>
|
|
1386
|
-
<p class="text-sm text-muted-foreground max-w-lg leading-relaxed m-0">
|
|
1387
|
-
One WebJs concept per demo under <code class="text-[0.9em] text-foreground">app/features/</code>, with logic
|
|
1388
|
-
in <code class="text-[0.9em] text-foreground">modules/</code>.
|
|
1389
|
-
</p>
|
|
1390
|
-
</div>
|
|
1391
1314
|
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
|
|
1392
1315
|
\${FEATURES.map(f => html\`
|
|
1393
|
-
<a href="\${f.href}" class
|
|
1316
|
+
<a href="\${f.href}" class=\${cardClass('group flex flex-col gap-1.5 rounded-xl p-4 no-underline transition-colors hover:border-border-strong hover:bg-accent')}>
|
|
1394
1317
|
<span class="flex items-center justify-between gap-2">
|
|
1395
1318
|
<span class="text-sm font-medium text-foreground">\${f.title}</span>
|
|
1396
1319
|
<span class="text-muted-foreground transition-transform group-hover:translate-x-0.5" aria-hidden="true">→</span>
|
|
@@ -1400,9 +1323,9 @@ export default function Home() {
|
|
|
1400
1323
|
\`)}
|
|
1401
1324
|
</div>
|
|
1402
1325
|
\${EXAMPLES.map(e => html\`
|
|
1403
|
-
<a href="\${e.href}" class
|
|
1326
|
+
<a href="\${e.href}" class=\${cardClass('group flex flex-col gap-2 rounded-xl p-5 no-underline transition-colors hover:border-border-strong hover:bg-accent')}>
|
|
1404
1327
|
<span class="flex items-center gap-2.5">
|
|
1405
|
-
<span class
|
|
1328
|
+
<span class=\${badgeClass({ variant: 'outline' })}>Example app</span>
|
|
1406
1329
|
<span class="text-sm font-medium text-foreground">\${e.title}</span>
|
|
1407
1330
|
<span class="ml-auto text-muted-foreground transition-transform group-hover:translate-x-0.5" aria-hidden="true">→</span>
|
|
1408
1331
|
</span>
|
|
@@ -1414,8 +1337,8 @@ export default function Home() {
|
|
|
1414
1337
|
<!-- Footer: docs + source -->
|
|
1415
1338
|
<footer class="flex flex-col items-center gap-3">
|
|
1416
1339
|
<nav class="flex items-center gap-6 text-sm text-muted-foreground" aria-label="WebJs links">
|
|
1417
|
-
<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>
|
|
1418
|
-
<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>
|
|
1340
|
+
<a href="https://docs.webjs.dev" target="_blank" rel="noopener" class="inline-flex items-center gap-2 hover:text-foreground transition-colors no-underline">\${iconBook()}<span>Docs</span></a>
|
|
1341
|
+
<a href="https://github.com/webjsdev/webjs" target="_blank" rel="noopener" class="inline-flex items-center gap-2 hover:text-foreground transition-colors no-underline">\${iconGithub()}<span>GitHub</span></a>
|
|
1419
1342
|
</nav>
|
|
1420
1343
|
<p class="text-[0.7rem] uppercase tracking-[0.15em] text-muted-foreground m-0 text-center">
|
|
1421
1344
|
Built with WebJs · MIT License
|
|
@@ -1439,6 +1362,8 @@ function iconGithub() {
|
|
|
1439
1362
|
// --- Theme toggle component ---
|
|
1440
1363
|
|
|
1441
1364
|
await writeFile(join(appDir, 'components', 'theme-toggle.ts'), `import { WebComponent, html, signal } from '@webjsdev/core';
|
|
1365
|
+
import { cn } from '#lib/utils/cn.ts';
|
|
1366
|
+
import { buttonClass } from '#components/ui/button.ts';
|
|
1442
1367
|
|
|
1443
1368
|
type Theme = 'system' | 'light' | 'dark';
|
|
1444
1369
|
|
|
@@ -1474,9 +1399,10 @@ export class ThemeToggle extends WebComponent {
|
|
|
1474
1399
|
const el = document.documentElement;
|
|
1475
1400
|
if (next === 'system') delete el.dataset.theme;
|
|
1476
1401
|
else el.dataset.theme = next;
|
|
1477
|
-
//
|
|
1402
|
+
// Our own tokens follow color-scheme via data-theme (set above). Keep the
|
|
1403
|
+
// .dark class in sync only for @webjsdev/ui components, which key off it.
|
|
1478
1404
|
const dark = next === 'dark'
|
|
1479
|
-
|| (next === 'system' &&
|
|
1405
|
+
|| (next === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
1480
1406
|
el.classList.toggle('dark', dark);
|
|
1481
1407
|
}
|
|
1482
1408
|
|
|
@@ -1486,7 +1412,7 @@ export class ThemeToggle extends WebComponent {
|
|
|
1486
1412
|
const icon = t === 'light' ? ICONS.sun : t === 'dark' ? ICONS.moon : ICONS.system;
|
|
1487
1413
|
return html\`
|
|
1488
1414
|
<button
|
|
1489
|
-
class
|
|
1415
|
+
class=\${cn(buttonClass({ variant: 'secondary', size: 'none' }), 'w-9 h-9 rounded-full text-muted-foreground duration-150 hover:text-foreground active:scale-[0.94]')}
|
|
1490
1416
|
@click=\${() => this.cycle()}
|
|
1491
1417
|
aria-label="Cycle theme (currently \${label})"
|
|
1492
1418
|
title="Theme: \${label.toLowerCase()}"
|
|
@@ -1505,12 +1431,6 @@ ThemeToggle.register('theme-toggle');
|
|
|
1505
1431
|
`);
|
|
1506
1432
|
} // end if (!isApi)
|
|
1507
1433
|
|
|
1508
|
-
// --- SaaS template extras: auth, dashboard, drizzle User model ---
|
|
1509
|
-
if (isSaas) {
|
|
1510
|
-
const { writeSaasFiles } = await import('./saas-template.js');
|
|
1511
|
-
await writeSaasFiles(appDir, { runtime });
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
1434
|
// AGENTS.md is already in place via the shared `templateFiles` loop
|
|
1515
1435
|
// earlier in this function, so no framework-root fallback needed.
|
|
1516
1436
|
|
|
@@ -1531,20 +1451,11 @@ ThemeToggle.register('theme-toggle');
|
|
|
1531
1451
|
modules/users/{actions,queries,types.ts} ← routes over server actions
|
|
1532
1452
|
db/{schema,columns,connection}.server.ts ← Drizzle (User model)
|
|
1533
1453
|
${guide}
|
|
1534
|
-
`);
|
|
1535
|
-
} else if (isSaas) {
|
|
1536
|
-
console.log(` ${name}/
|
|
1537
|
-
app/{layout,page}.ts, login/, signup/
|
|
1538
|
-
app/dashboard/{page,settings,middleware}.ts ← protected
|
|
1539
|
-
app/api/auth/[...path]/route.ts ← auth API
|
|
1540
|
-
components/ui/*, components/theme-toggle.ts
|
|
1541
|
-
modules/auth/*, lib/{auth,password}.server.ts
|
|
1542
|
-
db/{schema,columns,connection}.server.ts ← Drizzle (User model)
|
|
1543
|
-
${guide}
|
|
1544
1454
|
`);
|
|
1545
1455
|
} else {
|
|
1546
1456
|
console.log(` ${name}/
|
|
1547
|
-
app/{layout,page}.ts ←
|
|
1457
|
+
app/{layout,page}.ts ← gallery home; gallery:clear grows in place
|
|
1458
|
+
app/features/*, modules/* ← browsable demos (incl. auth: login + protected route)
|
|
1548
1459
|
components/theme-toggle.ts
|
|
1549
1460
|
public/input.css ← Tailwind entry (compiles to public/tailwind.css)
|
|
1550
1461
|
db/{schema,columns,connection}.server.ts ← Drizzle
|
|
@@ -1593,9 +1504,9 @@ ThemeToggle.register('theme-toggle');
|
|
|
1593
1504
|
// Next-steps banner prints LAST so the actionable command is the
|
|
1594
1505
|
// final thing on screen, never buried above the AI-agent guidance.
|
|
1595
1506
|
// Single copy-paste line so the user can move from "scaffold done"
|
|
1596
|
-
// to "dev server up" in one command. The full-stack
|
|
1597
|
-
//
|
|
1598
|
-
//
|
|
1507
|
+
// to "dev server up" in one command. The full-stack template ships
|
|
1508
|
+
// with @webjsdev/ui already initialised; the api template has no UI
|
|
1509
|
+
// but may add one later.
|
|
1599
1510
|
const installSegment = installed ? '' : `${pm} install && `;
|
|
1600
1511
|
// The shipped schema is applied on the first `run dev` (webjs.*.before runs
|
|
1601
1512
|
// `db migrate`), but only if a migration FILE exists. When we installed, we
|