@webjskit/cli 0.4.2 → 0.5.0
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 +13 -2
- package/bin/webjs.js +31 -20
- package/lib/create.js +242 -17
- package/lib/saas-template.js +134 -23
- package/package.json +3 -2
- package/templates/.cursorrules +1 -1
- package/templates/.github/copilot-instructions.md +2 -2
- package/templates/.windsurfrules +3 -2
- package/templates/AGENTS.md +207 -6
- package/templates/CONVENTIONS.md +58 -0
package/README.md
CHANGED
|
@@ -36,13 +36,24 @@ webjs create <name> --template api # backend-only API app
|
|
|
36
36
|
webjs create <name> --template saas # auth + dashboard + Prisma User model
|
|
37
37
|
|
|
38
38
|
webjs dev # dev server with live reload
|
|
39
|
-
webjs start # production server
|
|
40
|
-
webjs build # production bundle
|
|
39
|
+
webjs start # production server (no build step — serves source directly)
|
|
41
40
|
webjs check # validate project conventions
|
|
42
41
|
webjs test # run server + browser tests
|
|
43
42
|
webjs db <prisma-subcommand> # prisma passthrough (saas template)
|
|
43
|
+
|
|
44
|
+
webjs ui init # initialise @webjskit/ui in this project
|
|
45
|
+
webjs ui add <names...> # copy components from the registry (https://ui.webjs.dev/registry/<name>.json)
|
|
46
|
+
webjs ui list # list every component available in the registry
|
|
44
47
|
```
|
|
45
48
|
|
|
49
|
+
`webjs ui` proxies to [`@webjskit/ui`](https://www.npmjs.com/package/@webjskit/ui),
|
|
50
|
+
an AI-first component library + CLI that copies sources into your project — class
|
|
51
|
+
helpers (`buttonClass`, `cardClass`, …) for the visual primitives and a small set
|
|
52
|
+
of stateful custom elements (`<ui-dialog>`, `<ui-tabs>`, `<ui-popover>`) where
|
|
53
|
+
state matters. The package is a hard dependency of `@webjskit/cli` — installing
|
|
54
|
+
the CLI gives you `webjs ui` automatically. See
|
|
55
|
+
[https://ui.webjs.dev](https://ui.webjs.dev) for the catalogue.
|
|
56
|
+
|
|
46
57
|
## Scaffolded templates
|
|
47
58
|
|
|
48
59
|
The scaffold seeds opinionated defaults so AI agents produce consistent code:
|
package/bin/webjs.js
CHANGED
|
@@ -13,9 +13,7 @@ const TEMPLATES = ['full-stack', 'api', 'saas'];
|
|
|
13
13
|
|
|
14
14
|
const USAGE = `webjs — commands:
|
|
15
15
|
webjs dev [--port 3000] Start dev server with live reload
|
|
16
|
-
webjs start [--port 3000] Start production server (serves source directly; no build
|
|
17
|
-
[--http2 --cert <path> --key <path>] Serve HTTP/2 over TLS (falls back to h1.1)
|
|
18
|
-
webjs build Optional: bundle client modules into a single file (advanced/perf opt-in)
|
|
16
|
+
webjs start [--port 3000] Start production server (serves source directly; no build step)
|
|
19
17
|
webjs test [--server|--browser] Run server + browser tests
|
|
20
18
|
webjs check Validate app against conventions
|
|
21
19
|
webjs create <name> [--template full-stack|api|saas] Scaffold a new webjs app
|
|
@@ -23,6 +21,9 @@ const USAGE = `webjs — commands:
|
|
|
23
21
|
webjs db generate Run \`prisma generate\`
|
|
24
22
|
webjs db migrate [name] Run \`prisma migrate dev\`
|
|
25
23
|
webjs db studio Run \`prisma studio\`
|
|
24
|
+
webjs ui <subcmd> AI-first component library CLI
|
|
25
|
+
(init / add / list / view / diff / info)
|
|
26
|
+
Requires @webjskit/ui installed in the project
|
|
26
27
|
webjs help Show this help`;
|
|
27
28
|
|
|
28
29
|
/** @param {string[]} args */
|
|
@@ -79,23 +80,7 @@ async function main() {
|
|
|
79
80
|
case 'start': {
|
|
80
81
|
const { startServer } = await import('@webjskit/server');
|
|
81
82
|
const port = Number(flag(rest, '--port', process.env.PORT || 3000));
|
|
82
|
-
|
|
83
|
-
const cert = flag(rest, '--cert');
|
|
84
|
-
const key = flag(rest, '--key');
|
|
85
|
-
await startServer({ appDir: process.cwd(), port, dev: false, http2, cert, key });
|
|
86
|
-
break;
|
|
87
|
-
}
|
|
88
|
-
case 'build': {
|
|
89
|
-
const { buildBundle } = await import('@webjskit/server');
|
|
90
|
-
const t = Date.now();
|
|
91
|
-
const result = await buildBundle({
|
|
92
|
-
appDir: process.cwd(),
|
|
93
|
-
minify: rest.includes('--no-minify') ? false : true,
|
|
94
|
-
sourcemap: rest.includes('--no-sourcemap') ? false : true,
|
|
95
|
-
});
|
|
96
|
-
if (result.bundleFile) {
|
|
97
|
-
console.log(`webjs: bundled ${result.entries.length} entries → ${result.bundleFile} (${Date.now() - t}ms)`);
|
|
98
|
-
}
|
|
83
|
+
await startServer({ appDir: process.cwd(), port, dev: false });
|
|
99
84
|
break;
|
|
100
85
|
}
|
|
101
86
|
case 'db': {
|
|
@@ -108,6 +93,32 @@ async function main() {
|
|
|
108
93
|
child.on('exit', (code) => process.exit(code ?? 0));
|
|
109
94
|
break;
|
|
110
95
|
}
|
|
96
|
+
case 'ui': {
|
|
97
|
+
// Delegate to @webjskit/ui. Bundled as a hard dependency of
|
|
98
|
+
// @webjskit/cli — `npm install -g @webjskit/cli` pulls it in
|
|
99
|
+
// automatically, so `webjs ui add button` works out of the box
|
|
100
|
+
// without an extra install in user projects.
|
|
101
|
+
const { createRequire } = await import('node:module');
|
|
102
|
+
const req = createRequire(import.meta.url);
|
|
103
|
+
let entry;
|
|
104
|
+
try {
|
|
105
|
+
entry = req.resolve('@webjskit/ui/bin/webjsui.js');
|
|
106
|
+
} catch {
|
|
107
|
+
// Fallback: try resolving from the user's cwd in case of weird
|
|
108
|
+
// workspace setups.
|
|
109
|
+
try {
|
|
110
|
+
const userReq = createRequire(join(process.cwd(), 'package.json'));
|
|
111
|
+
entry = userReq.resolve('@webjskit/ui/bin/webjsui.js');
|
|
112
|
+
} catch {
|
|
113
|
+
console.error('@webjskit/ui could not be resolved.');
|
|
114
|
+
console.error('Reinstall the CLI: npm install -g @webjskit/cli');
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
const child = spawn('node', [entry, ...rest], { stdio: 'inherit', cwd: process.cwd() });
|
|
119
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
111
122
|
case 'test': {
|
|
112
123
|
const cwd = process.cwd();
|
|
113
124
|
const { existsSync } = await import('node:fs');
|
package/lib/create.js
CHANGED
|
@@ -19,6 +19,122 @@ import { existsSync } from 'node:fs';
|
|
|
19
19
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
20
|
const TEMPLATES = resolve(__dirname, '..', 'templates');
|
|
21
21
|
|
|
22
|
+
// Root of the @webjskit/ui registry workspace. We read component sources
|
|
23
|
+
// directly from disk at create time so the scaffolded app boots ready for
|
|
24
|
+
// `webjs ui add` without an HTTP round-trip during scaffolding.
|
|
25
|
+
//
|
|
26
|
+
// Layout in the monorepo:
|
|
27
|
+
// packages/cli/lib/create.js ← __dirname
|
|
28
|
+
// packages/ui/packages/registry/components/*.ts
|
|
29
|
+
// packages/ui/packages/registry/lib/utils.ts
|
|
30
|
+
// packages/ui/packages/registry/themes/index.css
|
|
31
|
+
const UI_REGISTRY_ROOT = resolve(
|
|
32
|
+
__dirname, '..', '..', 'ui', 'packages', 'registry',
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Read a single @webjskit/ui registry component, rewrite its relative import
|
|
37
|
+
* of `../lib/utils.ts` so it resolves correctly when written to
|
|
38
|
+
* `components/ui/<name>.ts` in the scaffolded app (which puts utils at
|
|
39
|
+
* `lib/utils.ts`, i.e. two levels up), and return the rewritten source.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} name component name without `.ts` (e.g. 'button')
|
|
42
|
+
* @returns {Promise<string|null>} source or null if not found
|
|
43
|
+
*/
|
|
44
|
+
async function readUiComponent(name) {
|
|
45
|
+
const src = join(UI_REGISTRY_ROOT, 'components', `${name}.ts`);
|
|
46
|
+
if (!existsSync(src)) return null;
|
|
47
|
+
const raw = await readFile(src, 'utf8');
|
|
48
|
+
// The registry source lives at <registry>/components/<x>.ts and imports
|
|
49
|
+
// its sibling utils via '../lib/utils.ts'. Once copied to the user's
|
|
50
|
+
// project at components/ui/<x>.ts, the equivalent path is two-up:
|
|
51
|
+
// '../../lib/utils.ts'. Same rewrite for the unquoted form.
|
|
52
|
+
return raw
|
|
53
|
+
.replaceAll("'../lib/utils.ts'", "'../../lib/utils.ts'")
|
|
54
|
+
.replaceAll('"../lib/utils.ts"', '"../../lib/utils.ts"');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Copy a list of @webjskit/ui registry components into the scaffolded app
|
|
59
|
+
* under `components/ui/`. Silently skips any name that isn't in the registry.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} appDir destination app root
|
|
62
|
+
* @param {string[]} names list of component file basenames (without `.ts`)
|
|
63
|
+
*/
|
|
64
|
+
async function copyUiComponents(appDir, names) {
|
|
65
|
+
const uiDir = join(appDir, 'components', 'ui');
|
|
66
|
+
await mkdir(uiDir, { recursive: true });
|
|
67
|
+
for (const n of names) {
|
|
68
|
+
const content = await readUiComponent(n);
|
|
69
|
+
if (content == null) continue;
|
|
70
|
+
await writeFile(join(uiDir, `${n}.ts`), content);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Write `lib/utils.ts` (the `cn()` helper) and `components.json` so the
|
|
76
|
+
* scaffolded app is pre-initialised for `webjs ui add`. Reads `lib/utils.ts`
|
|
77
|
+
* verbatim from the registry source so we never drift.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} appDir
|
|
80
|
+
*/
|
|
81
|
+
async function writeUiBootstrap(appDir) {
|
|
82
|
+
// 1) lib/utils.ts — the cn() helper
|
|
83
|
+
const utilsSrc = join(UI_REGISTRY_ROOT, 'lib', 'utils.ts');
|
|
84
|
+
if (existsSync(utilsSrc)) {
|
|
85
|
+
const content = await readFile(utilsSrc, 'utf8');
|
|
86
|
+
await mkdir(join(appDir, 'lib'), { recursive: true });
|
|
87
|
+
await writeFile(join(appDir, 'lib', 'utils.ts'), content);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// 2) components.json — the same shape `webjsui init` writes for webjs
|
|
91
|
+
// projects (see packages/ui/src/utils/detect-project.js).
|
|
92
|
+
const componentsJson = {
|
|
93
|
+
$schema: 'https://ui.webjs.dev/schema.json',
|
|
94
|
+
style: 'default',
|
|
95
|
+
tailwind: {
|
|
96
|
+
css: 'app/globals.css',
|
|
97
|
+
baseColor: 'neutral',
|
|
98
|
+
cssVariables: true,
|
|
99
|
+
},
|
|
100
|
+
aliases: {
|
|
101
|
+
components: 'components',
|
|
102
|
+
utils: 'lib/utils',
|
|
103
|
+
ui: 'components/ui',
|
|
104
|
+
lib: 'lib',
|
|
105
|
+
},
|
|
106
|
+
iconLibrary: 'lucide',
|
|
107
|
+
};
|
|
108
|
+
await writeFile(
|
|
109
|
+
join(appDir, 'components.json'),
|
|
110
|
+
JSON.stringify(componentsJson, null, 2) + '\n',
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// 3) app/globals.css — copy the neutral theme verbatim. components.json
|
|
114
|
+
// references this path; future `webjs ui add` calls append to it.
|
|
115
|
+
const themeSrc = join(UI_REGISTRY_ROOT, 'themes', 'index.css');
|
|
116
|
+
if (existsSync(themeSrc)) {
|
|
117
|
+
const css = await readFile(themeSrc, 'utf8');
|
|
118
|
+
await mkdir(join(appDir, 'app'), { recursive: true });
|
|
119
|
+
await writeFile(join(appDir, 'app', 'globals.css'), css);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Read the shadcn theme CSS so we can inline it into the layout's
|
|
125
|
+
* `<style type="text/tailwindcss">` block. The Tailwind browser runtime
|
|
126
|
+
* picks up inline `<style type="text/tailwindcss">` content, so the theme
|
|
127
|
+
* tokens (`--color-primary`, `--color-card`, …) the registry components
|
|
128
|
+
* consume are available at runtime without a build step.
|
|
129
|
+
*
|
|
130
|
+
* @returns {Promise<string>} theme CSS source, or '' if registry missing
|
|
131
|
+
*/
|
|
132
|
+
async function readThemeCss() {
|
|
133
|
+
const src = join(UI_REGISTRY_ROOT, 'themes', 'index.css');
|
|
134
|
+
if (!existsSync(src)) return '';
|
|
135
|
+
return await readFile(src, 'utf8');
|
|
136
|
+
}
|
|
137
|
+
|
|
22
138
|
/**
|
|
23
139
|
* @param {string} name App directory name
|
|
24
140
|
* @param {string} cwd Current working directory
|
|
@@ -87,10 +203,14 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
87
203
|
'@web/test-runner': '^0.20.0',
|
|
88
204
|
'@web/test-runner-playwright': '^0.11.0',
|
|
89
205
|
'playwright': '^1.59.0',
|
|
90
|
-
// tsserver
|
|
91
|
-
//
|
|
92
|
-
|
|
206
|
+
// tsserver plugin for editor intelligence inside html`` templates.
|
|
207
|
+
// @webjskit/ts-plugin bundles ts-lit-plugin internally, so just one
|
|
208
|
+
// plugin entry is needed in tsconfig (see below).
|
|
93
209
|
'@webjskit/ts-plugin': 'latest',
|
|
210
|
+
// AI-first component library CLI — preinstalled so `webjs ui add button`
|
|
211
|
+
// works immediately after scaffold. Users can remove if they prefer
|
|
212
|
+
// to add it later.
|
|
213
|
+
'@webjskit/ui': 'latest',
|
|
94
214
|
},
|
|
95
215
|
}, null, 2) + '\n');
|
|
96
216
|
|
|
@@ -104,14 +224,16 @@ export async function scaffoldApp(name, cwd, opts = {}) {
|
|
|
104
224
|
noEmit: true,
|
|
105
225
|
allowImportingTsExtensions: true,
|
|
106
226
|
skipLibCheck: true,
|
|
107
|
-
// ts-
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
//
|
|
227
|
+
// @webjskit/ts-plugin gives the editor:
|
|
228
|
+
// • type-check + diagnostics inside html`` templates (via the
|
|
229
|
+
// ts-lit-plugin it bundles internally)
|
|
230
|
+
// • webjs-aware go-to-definition on custom-element tags
|
|
231
|
+
// • "Unknown tag/attribute" suppression for elements registered
|
|
232
|
+
// via Class.register('tag-name')
|
|
233
|
+
// • attribute auto-complete sourced from `static properties`
|
|
234
|
+
// • attribute-value type-check against `declare` annotations
|
|
235
|
+
// Editor-only — the framework runs without it.
|
|
113
236
|
plugins: [
|
|
114
|
-
{ name: 'ts-lit-plugin', strict: true },
|
|
115
237
|
{ name: '@webjskit/ts-plugin' },
|
|
116
238
|
],
|
|
117
239
|
},
|
|
@@ -255,8 +377,8 @@ export async function createUser(input: { name: string; email: string }) {
|
|
|
255
377
|
* /api/users — thin route wrapper over typed server actions.
|
|
256
378
|
* Business logic lives in modules/users/, not here.
|
|
257
379
|
*/
|
|
258
|
-
import { listUsers } from '
|
|
259
|
-
import { createUser } from '
|
|
380
|
+
import { listUsers } from '../../../modules/users/queries/list-users.server.ts';
|
|
381
|
+
import { createUser } from '../../../modules/users/actions/create-user.server.ts';
|
|
260
382
|
|
|
261
383
|
export async function GET() {
|
|
262
384
|
return Response.json(await listUsers());
|
|
@@ -320,9 +442,45 @@ export type ActionResult<T> =
|
|
|
320
442
|
await cp(uiSrc, join(utilsDir, 'ui.ts'));
|
|
321
443
|
}
|
|
322
444
|
|
|
445
|
+
// Pre-initialise @webjskit/ui so the scaffold boots ready for
|
|
446
|
+
// `webjs ui add <name>`: writes components.json + lib/utils.ts +
|
|
447
|
+
// app/globals.css (the shadcn theme).
|
|
448
|
+
await writeUiBootstrap(appDir);
|
|
449
|
+
|
|
450
|
+
// Copy the standard ui-* component kit the scaffold's example pages
|
|
451
|
+
// use. Sources are read from packages/ui/packages/registry/ in this
|
|
452
|
+
// monorepo. Users can `webjs ui add <name>` for anything else.
|
|
453
|
+
await copyUiComponents(appDir, [
|
|
454
|
+
'button', 'card', 'alert', 'badge', 'separator', 'label', 'input',
|
|
455
|
+
]);
|
|
456
|
+
|
|
457
|
+
// The shadcn theme tokens (`--color-primary`, `--color-card`, …) the
|
|
458
|
+
// ui-* components consume. We read the registry's themes/index.css at
|
|
459
|
+
// create time and inline it into the layout's
|
|
460
|
+
// `<style type="text/tailwindcss">` block so the Tailwind browser
|
|
461
|
+
// runtime picks it up. Same content also lives at app/globals.css for
|
|
462
|
+
// `webjsui` tooling.
|
|
463
|
+
const SHADCN_THEME = (await readThemeCss())
|
|
464
|
+
// Escape backticks + ${} so the CSS survives interpolation into the
|
|
465
|
+
// layout's template literal below.
|
|
466
|
+
.replace(/\\/g, '\\\\')
|
|
467
|
+
.replace(/`/g, '\\`')
|
|
468
|
+
.replace(/\$\{/g, '\\${');
|
|
469
|
+
|
|
323
470
|
await writeFile(join(appDir, 'app', 'layout.ts'), `import { html } from '@webjskit/core';
|
|
324
471
|
import '@webjskit/core/client-router';
|
|
325
472
|
import '../components/theme-toggle.ts';
|
|
473
|
+
// Webjs UI components are tiered:
|
|
474
|
+
// - Tier 1 (button, card, input, label, alert, badge, separator, etc.) are
|
|
475
|
+
// class-helper FUNCTIONS — no custom element to register. Each page
|
|
476
|
+
// imports the specific helpers it needs (e.g.
|
|
477
|
+
// \`import { buttonClass } from '../components/ui/button.ts'\`).
|
|
478
|
+
// - Tier 2 (dialog, popover, tooltip, tabs, accordion, etc.) ARE custom
|
|
479
|
+
// elements. Register them by side-effect-importing here once so they
|
|
480
|
+
// work transitively across every page:
|
|
481
|
+
// import '../components/ui/dialog.ts';
|
|
482
|
+
// The example app/page.ts below uses only Tier-1 helpers, so nothing
|
|
483
|
+
// extra needs to be registered. Add Tier-2 imports as you 'webjs ui add'.
|
|
326
484
|
|
|
327
485
|
/**
|
|
328
486
|
* Root layout — globals + chrome.
|
|
@@ -353,6 +511,16 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
353
511
|
})();
|
|
354
512
|
</script>
|
|
355
513
|
<script src="/public/tailwind-browser.js"></script>
|
|
514
|
+
<!--
|
|
515
|
+
Webjs UI theme — design tokens (--color-primary,
|
|
516
|
+
--color-card, --radius, etc.) the ui-* components consume.
|
|
517
|
+
The same content is also at app/globals.css; we inline it here so
|
|
518
|
+
the Tailwind browser runtime resolves the tokens without a build step.
|
|
519
|
+
Edit base palette via the :root / .dark blocks below.
|
|
520
|
+
-->
|
|
521
|
+
<style type="text/tailwindcss">
|
|
522
|
+
${SHADCN_THEME}
|
|
523
|
+
</style>
|
|
356
524
|
<style type="text/tailwindcss">
|
|
357
525
|
@theme {
|
|
358
526
|
--color-fg: var(--fg);
|
|
@@ -458,6 +626,17 @@ export default function RootLayout({ children }: { children: unknown }) {
|
|
|
458
626
|
|
|
459
627
|
await writeFile(join(appDir, 'app', 'page.ts'), `import { html } from '@webjskit/core';
|
|
460
628
|
import { rubric, displayH1, accentLink } from './_utils/ui.ts';
|
|
629
|
+
import { buttonClass } from '../components/ui/button.ts';
|
|
630
|
+
import { badgeClass } from '../components/ui/badge.ts';
|
|
631
|
+
import {
|
|
632
|
+
cardClass,
|
|
633
|
+
cardHeaderClass,
|
|
634
|
+
cardTitleClass,
|
|
635
|
+
cardDescriptionClass,
|
|
636
|
+
cardContentClass,
|
|
637
|
+
} from '../components/ui/card.ts';
|
|
638
|
+
import { alertClass, alertTitleClass, alertDescriptionClass } from '../components/ui/alert.ts';
|
|
639
|
+
import { separatorClass } from '../components/ui/separator.ts';
|
|
461
640
|
|
|
462
641
|
export const metadata = {
|
|
463
642
|
title: '${name} — built with webjs',
|
|
@@ -468,14 +647,42 @@ export default function Home() {
|
|
|
468
647
|
<section class="mb-18">
|
|
469
648
|
\${rubric('welcome')}
|
|
470
649
|
\${displayH1(html\`Hello from <span class="text-accent italic">${name}</span>.\`)}
|
|
471
|
-
<p class="text-lede leading-[1.5] text-fg-muted max-w-[56ch] m-0">
|
|
650
|
+
<p class="text-lede leading-[1.5] text-fg-muted max-w-[56ch] m-0 mb-6">
|
|
472
651
|
Edit <code class="font-mono text-[0.9em]">app/page.ts</code> to get started.
|
|
473
652
|
Run \${accentLink('#', 'webjs test')} to run tests and
|
|
474
653
|
\${accentLink('#', 'webjs check')} to validate conventions.
|
|
475
654
|
</p>
|
|
655
|
+
<div class="flex gap-3 items-center">
|
|
656
|
+
<button class=\${buttonClass()}>Get started</button>
|
|
657
|
+
<button class=\${buttonClass({ variant: 'outline' })}>View docs</button>
|
|
658
|
+
<span class=\${badgeClass({ variant: 'secondary' })}>v0.1</span>
|
|
659
|
+
</div>
|
|
476
660
|
</section>
|
|
477
661
|
|
|
478
|
-
<
|
|
662
|
+
<div class=\${cardClass()} style="margin-bottom: 3rem">
|
|
663
|
+
<div class=\${cardHeaderClass()}>
|
|
664
|
+
<h3 class=\${cardTitleClass()}>Web Components + Server Actions</h3>
|
|
665
|
+
<p class=\${cardDescriptionClass()}>
|
|
666
|
+
Drop a custom element anywhere. Call a server action like a local
|
|
667
|
+
function — webjs rewrites the import into a typed RPC stub.
|
|
668
|
+
</p>
|
|
669
|
+
</div>
|
|
670
|
+
<div class=\${cardContentClass()}>
|
|
671
|
+
<div class=\${alertClass()}>
|
|
672
|
+
<h5 class=\${alertTitleClass()}>AI-first component kit included</h5>
|
|
673
|
+
<div class=\${alertDescriptionClass()}>
|
|
674
|
+
button, card, alert, badge, separator, label, input are already
|
|
675
|
+
in <code class="font-mono text-[0.9em]">components/ui/</code> as
|
|
676
|
+
class-helper functions you call from a native element. Add more
|
|
677
|
+
with <code class="font-mono text-[0.9em]">webjs ui add <name></code>.
|
|
678
|
+
</div>
|
|
679
|
+
</div>
|
|
680
|
+
</div>
|
|
681
|
+
</div>
|
|
682
|
+
|
|
683
|
+
<div class=\${separatorClass()} style="margin: 2.5rem 0"></div>
|
|
684
|
+
|
|
685
|
+
<section class="mt-10">
|
|
479
686
|
<h2 class="font-serif text-[1.6rem] tracking-[-0.02em] font-bold m-0 mb-2">Light DOM + Tailwind</h2>
|
|
480
687
|
<p class="text-fg-muted text-sm m-0 mb-4">
|
|
481
688
|
Components render into light DOM by default. Tailwind utility classes
|
|
@@ -590,25 +797,43 @@ ThemeToggle.register('theme-toggle');
|
|
|
590
797
|
app/layout.ts, page.ts, login/, signup/
|
|
591
798
|
app/dashboard/{page,settings,middleware}.ts ← protected
|
|
592
799
|
app/api/auth/[...path]/route.ts ← auth API
|
|
800
|
+
app/globals.css ← @webjskit/ui theme tokens
|
|
801
|
+
components.json ← preconfigured for \`webjs ui add\`
|
|
802
|
+
components/ui/{button,card,alert,badge,separator,label,input,
|
|
803
|
+
dialog,form,field,switch,checkbox}.ts
|
|
804
|
+
components/theme-toggle.ts
|
|
593
805
|
modules/auth/{actions,queries,types.ts}
|
|
594
|
-
lib/{auth,prisma,password}.ts
|
|
806
|
+
lib/{auth,prisma,password,utils}.ts ← utils.ts is the cn() helper
|
|
595
807
|
prisma/schema.prisma ← User model
|
|
596
|
-
components/theme-toggle.ts
|
|
597
808
|
CONVENTIONS.md, AGENTS.md, CLAUDE.md
|
|
598
809
|
`);
|
|
599
810
|
} else {
|
|
600
811
|
console.log(` ${name}/
|
|
601
812
|
app/layout.ts, page.ts ← light DOM + Tailwind + @theme tokens
|
|
602
813
|
app/_utils/ui.ts ← JS helpers for repeated class bundles
|
|
603
|
-
|
|
814
|
+
app/globals.css ← @webjskit/ui theme tokens
|
|
815
|
+
components.json ← preconfigured for \`webjs ui add\`
|
|
816
|
+
components/ui/{button,card,alert,badge,separator,label,input}.ts
|
|
604
817
|
components/theme-toggle.ts ← light DOM web component
|
|
818
|
+
lib/utils.ts ← cn() helper for ui-* components
|
|
819
|
+
public/tailwind-browser.js ← Tailwind runtime
|
|
605
820
|
modules/
|
|
606
821
|
CONVENTIONS.md, AGENTS.md, CLAUDE.md
|
|
607
822
|
`);
|
|
608
823
|
}
|
|
824
|
+
// Post-scaffold guidance. The full-stack and saas templates ship with
|
|
825
|
+
// @webjskit/ui already initialised (components.json, lib/utils.ts, the
|
|
826
|
+
// standard kit under components/ui/), so the user only runs `webjs dev`.
|
|
827
|
+
// The api template has no UI; we only mention `webjs ui` in case the
|
|
828
|
+
// user later adds one.
|
|
829
|
+
const uiNote = isApi
|
|
830
|
+
? `# If you later add a UI to this API project:
|
|
831
|
+
# webjs ui init && webjs ui add button card dialog`
|
|
832
|
+
: `webjs ui add <name> # optional — add more ui-* components later`;
|
|
609
833
|
console.log(`Next steps:
|
|
610
834
|
cd ${name}
|
|
611
835
|
npm install${isSaas ? '\n npx prisma migrate dev --name init' : ''}
|
|
836
|
+
${uiNote}
|
|
612
837
|
webjs dev
|
|
613
838
|
|
|
614
839
|
AI-driven development (enforced for all AI agents):
|
package/lib/saas-template.js
CHANGED
|
@@ -3,13 +3,53 @@
|
|
|
3
3
|
* Extracted to avoid nested template literal escaping issues.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
7
|
-
import {
|
|
6
|
+
import { mkdir, writeFile, readFile } from 'node:fs/promises';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { join, resolve, dirname } from 'node:path';
|
|
9
|
+
import { fileURLToPath } from 'node:url';
|
|
10
|
+
|
|
11
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
const UI_REGISTRY_ROOT = resolve(
|
|
13
|
+
__dirname, '..', '..', 'ui', 'packages', 'registry',
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read a registry component and rewrite its `'../lib/utils.ts'` import for
|
|
18
|
+
* the scaffolded app's `components/ui/<name>.ts` layout (two-up to lib/).
|
|
19
|
+
* Mirrors the helper in `create.js` — kept private here to avoid coupling.
|
|
20
|
+
*/
|
|
21
|
+
async function readUiComponent(name) {
|
|
22
|
+
const src = join(UI_REGISTRY_ROOT, 'components', `${name}.ts`);
|
|
23
|
+
if (!existsSync(src)) return null;
|
|
24
|
+
const raw = await readFile(src, 'utf8');
|
|
25
|
+
return raw
|
|
26
|
+
.replaceAll("'../lib/utils.ts'", "'../../lib/utils.ts'")
|
|
27
|
+
.replaceAll('"../lib/utils.ts"', '"../../lib/utils.ts"');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Copy named registry components into `<appDir>/components/ui/`. */
|
|
31
|
+
async function copyUiComponents(appDir, names) {
|
|
32
|
+
const uiDir = join(appDir, 'components', 'ui');
|
|
33
|
+
await mkdir(uiDir, { recursive: true });
|
|
34
|
+
for (const n of names) {
|
|
35
|
+
const content = await readUiComponent(n);
|
|
36
|
+
if (content == null) continue;
|
|
37
|
+
await writeFile(join(uiDir, `${n}.ts`), content);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
8
40
|
|
|
9
41
|
/**
|
|
10
42
|
* @param {string} appDir
|
|
11
43
|
*/
|
|
12
44
|
export async function writeSaasFiles(appDir) {
|
|
45
|
+
// SaaS pages use auth forms — copy the extra ui-* components on top of
|
|
46
|
+
// the standard set the full-stack scaffold already wrote. Pre-importing
|
|
47
|
+
// them in login/signup/dashboard pages below means the dev server will
|
|
48
|
+
// SSR these elements with full styling on first paint.
|
|
49
|
+
// `form` and `field` are deferred to v2 (see packages/ui/AGENTS.md) —
|
|
50
|
+
// the saas auth pages use raw <form> + label/input class helpers instead.
|
|
51
|
+
await copyUiComponents(appDir, ['dialog', 'switch', 'checkbox']);
|
|
52
|
+
|
|
13
53
|
// lib/prisma.ts
|
|
14
54
|
await mkdir(join(appDir, 'lib'), { recursive: true });
|
|
15
55
|
await writeFile(join(appDir, 'lib', 'prisma.ts'), [
|
|
@@ -194,18 +234,39 @@ export async function writeSaasFiles(appDir) {
|
|
|
194
234
|
await mkdir(join(appDir, 'app', 'login'), { recursive: true });
|
|
195
235
|
await writeFile(join(appDir, 'app', 'login', 'page.ts'), [
|
|
196
236
|
"import { html } from '@webjskit/core';",
|
|
237
|
+
"import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass, cardFooterClass } from '../../components/ui/card.ts';",
|
|
238
|
+
"import { buttonClass } from '../../components/ui/button.ts';",
|
|
239
|
+
"import { inputClass } from '../../components/ui/input.ts';",
|
|
240
|
+
"import { labelClass } from '../../components/ui/label.ts';",
|
|
197
241
|
"",
|
|
198
242
|
"export const metadata = { title: 'Login' };",
|
|
199
243
|
"",
|
|
200
244
|
"export default function LoginPage() {",
|
|
201
245
|
" return html`",
|
|
202
|
-
" <
|
|
203
|
-
"
|
|
204
|
-
"
|
|
205
|
-
"
|
|
206
|
-
"
|
|
207
|
-
"
|
|
208
|
-
"
|
|
246
|
+
" <div class=\"max-w-sm mx-auto mt-12\">",
|
|
247
|
+
" <div class=${cardClass()}>",
|
|
248
|
+
" <div class=${cardHeaderClass()}>",
|
|
249
|
+
" <h3 class=${cardTitleClass()}>Sign in</h3>",
|
|
250
|
+
" <p class=${cardDescriptionClass()}>Welcome back — log in to continue.</p>",
|
|
251
|
+
" </div>",
|
|
252
|
+
" <div class=${cardContentClass()}>",
|
|
253
|
+
" <form method=\"POST\" action=\"/api/auth/callback/credentials\" class=\"flex flex-col gap-4\">",
|
|
254
|
+
" <div class=\"flex flex-col gap-1.5\">",
|
|
255
|
+
" <label class=${labelClass()} for=\"email\">Email</label>",
|
|
256
|
+
" <input class=${inputClass()} id=\"email\" name=\"email\" type=\"email\" required>",
|
|
257
|
+
" </div>",
|
|
258
|
+
" <div class=\"flex flex-col gap-1.5\">",
|
|
259
|
+
" <label class=${labelClass()} for=\"password\">Password</label>",
|
|
260
|
+
" <input class=${inputClass()} id=\"password\" name=\"password\" type=\"password\" required>",
|
|
261
|
+
" </div>",
|
|
262
|
+
" <button class=${buttonClass()} type=\"submit\">Sign in</button>",
|
|
263
|
+
" </form>",
|
|
264
|
+
" </div>",
|
|
265
|
+
" <div class=${cardFooterClass()}>",
|
|
266
|
+
" <p class=\"text-sm text-muted-foreground\">Don't have an account? <a href=\"/signup\" class=\"underline\">Sign up</a></p>",
|
|
267
|
+
" </div>",
|
|
268
|
+
" </div>",
|
|
269
|
+
" </div>",
|
|
209
270
|
" `;",
|
|
210
271
|
"}",
|
|
211
272
|
"",
|
|
@@ -215,19 +276,43 @@ export async function writeSaasFiles(appDir) {
|
|
|
215
276
|
await mkdir(join(appDir, 'app', 'signup'), { recursive: true });
|
|
216
277
|
await writeFile(join(appDir, 'app', 'signup', 'page.ts'), [
|
|
217
278
|
"import { html } from '@webjskit/core';",
|
|
279
|
+
"import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass, cardFooterClass } from '../../components/ui/card.ts';",
|
|
280
|
+
"import { buttonClass } from '../../components/ui/button.ts';",
|
|
281
|
+
"import { inputClass } from '../../components/ui/input.ts';",
|
|
282
|
+
"import { labelClass } from '../../components/ui/label.ts';",
|
|
218
283
|
"",
|
|
219
284
|
"export const metadata = { title: 'Sign up' };",
|
|
220
285
|
"",
|
|
221
286
|
"export default function SignupPage() {",
|
|
222
287
|
" return html`",
|
|
223
|
-
" <
|
|
224
|
-
"
|
|
225
|
-
"
|
|
226
|
-
"
|
|
227
|
-
"
|
|
228
|
-
"
|
|
229
|
-
"
|
|
230
|
-
"
|
|
288
|
+
" <div class=\"max-w-sm mx-auto mt-12\">",
|
|
289
|
+
" <div class=${cardClass()}>",
|
|
290
|
+
" <div class=${cardHeaderClass()}>",
|
|
291
|
+
" <h3 class=${cardTitleClass()}>Create an account</h3>",
|
|
292
|
+
" <p class=${cardDescriptionClass()}>Get started with your new workspace.</p>",
|
|
293
|
+
" </div>",
|
|
294
|
+
" <div class=${cardContentClass()}>",
|
|
295
|
+
" <form id=\"signup-form\" class=\"flex flex-col gap-4\">",
|
|
296
|
+
" <div class=\"flex flex-col gap-1.5\">",
|
|
297
|
+
" <label class=${labelClass()} for=\"name\">Name</label>",
|
|
298
|
+
" <input class=${inputClass()} id=\"name\" name=\"name\" type=\"text\" required>",
|
|
299
|
+
" </div>",
|
|
300
|
+
" <div class=\"flex flex-col gap-1.5\">",
|
|
301
|
+
" <label class=${labelClass()} for=\"email\">Email</label>",
|
|
302
|
+
" <input class=${inputClass()} id=\"email\" name=\"email\" type=\"email\" required>",
|
|
303
|
+
" </div>",
|
|
304
|
+
" <div class=\"flex flex-col gap-1.5\">",
|
|
305
|
+
" <label class=${labelClass()} for=\"password\">Password</label>",
|
|
306
|
+
" <input class=${inputClass()} id=\"password\" name=\"password\" type=\"password\" required>",
|
|
307
|
+
" </div>",
|
|
308
|
+
" <button class=${buttonClass()} type=\"submit\">Create account</button>",
|
|
309
|
+
" </form>",
|
|
310
|
+
" </div>",
|
|
311
|
+
" <div class=${cardFooterClass()}>",
|
|
312
|
+
" <p class=\"text-sm text-muted-foreground\">Already have an account? <a href=\"/login\" class=\"underline\">Log in</a></p>",
|
|
313
|
+
" </div>",
|
|
314
|
+
" </div>",
|
|
315
|
+
" </div>",
|
|
231
316
|
" `;",
|
|
232
317
|
"}",
|
|
233
318
|
"",
|
|
@@ -252,15 +337,28 @@ export async function writeSaasFiles(appDir) {
|
|
|
252
337
|
await writeFile(join(appDir, 'app', 'dashboard', 'page.ts'), [
|
|
253
338
|
"import { html } from '@webjskit/core';",
|
|
254
339
|
"import { currentUser } from '../../modules/auth/queries/current-user.server.ts';",
|
|
340
|
+
"import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass } from '../../components/ui/card.ts';",
|
|
341
|
+
"import { buttonClass } from '../../components/ui/button.ts';",
|
|
342
|
+
"import { badgeClass } from '../../components/ui/badge.ts';",
|
|
255
343
|
"",
|
|
256
344
|
"export const metadata = { title: 'Dashboard' };",
|
|
257
345
|
"",
|
|
258
346
|
"export default async function Dashboard() {",
|
|
259
347
|
" const user = await currentUser();",
|
|
260
348
|
" return html`",
|
|
261
|
-
" <
|
|
262
|
-
"
|
|
263
|
-
"
|
|
349
|
+
" <div class=\"flex items-center justify-between mb-6\">",
|
|
350
|
+
" <h1 class=\"text-2xl font-semibold\">Dashboard</h1>",
|
|
351
|
+
" <span class=${badgeClass({ variant: 'secondary' })}>Signed in</span>",
|
|
352
|
+
" </div>",
|
|
353
|
+
" <div class=${cardClass()}>",
|
|
354
|
+
" <div class=${cardHeaderClass()}>",
|
|
355
|
+
" <h3 class=${cardTitleClass()}>Welcome, ${`\\$\\{user?.name || user?.email\\}`}!</h3>",
|
|
356
|
+
" <p class=${cardDescriptionClass()}>You're authenticated. Replace this scaffold with your real app.</p>",
|
|
357
|
+
" </div>",
|
|
358
|
+
" <div class=${cardContentClass()}>",
|
|
359
|
+
" <a class=${buttonClass({ variant: 'outline' })} href=\"/dashboard/settings\">Settings</a>",
|
|
360
|
+
" </div>",
|
|
361
|
+
" </div>",
|
|
264
362
|
" `;",
|
|
265
363
|
"}",
|
|
266
364
|
"",
|
|
@@ -270,15 +368,28 @@ export async function writeSaasFiles(appDir) {
|
|
|
270
368
|
await writeFile(join(appDir, 'app', 'dashboard', 'settings', 'page.ts'), [
|
|
271
369
|
"import { html } from '@webjskit/core';",
|
|
272
370
|
"import { currentUser } from '../../../modules/auth/queries/current-user.server.ts';",
|
|
371
|
+
"import { cardClass, cardHeaderClass, cardTitleClass, cardDescriptionClass, cardContentClass } from '../../../components/ui/card.ts';",
|
|
273
372
|
"",
|
|
274
373
|
"export const metadata = { title: 'Settings' };",
|
|
275
374
|
"",
|
|
276
375
|
"export default async function Settings() {",
|
|
277
376
|
" const user = await currentUser();",
|
|
278
377
|
" return html`",
|
|
279
|
-
" <h1>Settings</h1>",
|
|
280
|
-
" <
|
|
281
|
-
"
|
|
378
|
+
" <h1 class=\"text-2xl font-semibold mb-6\">Settings</h1>",
|
|
379
|
+
" <div class=${cardClass()}>",
|
|
380
|
+
" <div class=${cardHeaderClass()}>",
|
|
381
|
+
" <h3 class=${cardTitleClass()}>Account</h3>",
|
|
382
|
+
" <p class=${cardDescriptionClass()}>Your basic profile information.</p>",
|
|
383
|
+
" </div>",
|
|
384
|
+
" <div class=${cardContentClass()}>",
|
|
385
|
+
" <dl class=\"grid grid-cols-[max-content_1fr] gap-x-6 gap-y-2 text-sm\">",
|
|
386
|
+
" <dt class=\"text-muted-foreground\">Email</dt>",
|
|
387
|
+
" <dd>${`\\$\\{user?.email\\}`}</dd>",
|
|
388
|
+
" <dt class=\"text-muted-foreground\">Name</dt>",
|
|
389
|
+
" <dd>${`\\$\\{user?.name || 'Not set'\\}`}</dd>",
|
|
390
|
+
" </dl>",
|
|
391
|
+
" </div>",
|
|
392
|
+
" </div>",
|
|
282
393
|
" `;",
|
|
283
394
|
"}",
|
|
284
395
|
"",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webjskit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "webjs CLI — dev, start, create, db",
|
|
6
6
|
"bin": {
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
"README.md"
|
|
14
14
|
],
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@webjskit/server": "0.
|
|
16
|
+
"@webjskit/server": "0.5.0",
|
|
17
|
+
"@webjskit/ui": "0.1.0"
|
|
17
18
|
},
|
|
18
19
|
"publishConfig": {
|
|
19
20
|
"access": "public"
|
package/templates/.cursorrules
CHANGED
|
@@ -80,5 +80,5 @@ Quality bar stays the same — just no blocking on questions.
|
|
|
80
80
|
- One function per server action file (*.server.ts)
|
|
81
81
|
- Components must call customElements.define('tag', Class)
|
|
82
82
|
- Never import @prisma/client or node:* from client components
|
|
83
|
-
-
|
|
83
|
+
- Directives are deliberately minimal: only `unsafeHTML`, `live`, and `repeat` ship. Lit's `classMap` / `styleMap` / `ref` / `when` / `choose` / `guard` are NOT exported — use plain template-literal expressions (`class=${cond ? 'a' : 'b'}`, `${cond ? a : b}`) and lifecycle hooks (`this.query('#el')` in `firstUpdated`) instead.
|
|
84
84
|
- See AGENTS.md for the complete directive decision guide
|
|
@@ -64,9 +64,9 @@ Every code change must include:
|
|
|
64
64
|
## Code patterns
|
|
65
65
|
|
|
66
66
|
- Tagged template: html`<div>${value}</div>` with css`...` for styles
|
|
67
|
-
- Components: extend WebComponent,
|
|
67
|
+
- Components: extend WebComponent, declare `static properties` (and `static styles` for shadow-DOM components), call `Class.register('tag-name')` at the bottom of the file. The tag name is the argument to `.register()`, not a static field.
|
|
68
68
|
- Server actions: *.server.ts files with one exported async function each
|
|
69
|
-
- Directives:
|
|
69
|
+
- Directives: webjs ships only `unsafeHTML`, `live`, and `repeat`. Lit's `classMap` / `styleMap` / `ref` / `when` / `choose` / `guard` are NOT exported — use plain template-literal expressions and lifecycle hooks instead.
|
|
70
70
|
- Context: import { createContext, ContextProvider, ContextConsumer } from '@webjskit/core/context'
|
|
71
71
|
- Task: import { Task, TaskStatus } from '@webjskit/core/task'
|
|
72
72
|
- Routing: file-based under app/ (page.ts, layout.ts, route.ts, middleware.ts)
|
package/templates/.windsurfrules
CHANGED
|
@@ -67,8 +67,9 @@ The user should never have to ask for tests or documentation.
|
|
|
67
67
|
## Framework specifics
|
|
68
68
|
|
|
69
69
|
- No build step: ES modules served directly
|
|
70
|
-
- Web components
|
|
70
|
+
- Web components render into light DOM by default (so Tailwind / global CSS apply directly). Opt in to shadow DOM per component with `static shadow = true` when you need scoped styles, slot projection, or third-party-embed isolation.
|
|
71
|
+
- Custom-element tag names are passed to `.register('tag-name')` — they are NOT a static field on the class.
|
|
71
72
|
- One function per server action file (*.server.ts)
|
|
72
|
-
- Use
|
|
73
|
+
- Directives are deliberately minimal: only `unsafeHTML`, `live`, and `repeat` ship. Use plain template-literal expressions (`class=${active ? 'btn active' : 'btn'}`, `style=${'color:' + color}`, `${cond ? a : b}`) and lifecycle hooks (`this.query('#el')` in `firstUpdated`) instead of Lit's `classMap` / `styleMap` / `ref` / `when` / `choose` / `guard`.
|
|
73
74
|
- Use Context for cross-component data, Task for async data in components
|
|
74
75
|
- Full API reference in AGENTS.md
|
package/templates/AGENTS.md
CHANGED
|
@@ -84,6 +84,141 @@ node_modules/@webjskit/
|
|
|
84
84
|
Reaching straight for the source is the fastest way to resolve "why
|
|
85
85
|
doesn't X work?" — no documentation guesswork, no stale blog posts.
|
|
86
86
|
|
|
87
|
+
## Editor TS plugin — `@webjskit/ts-plugin`
|
|
88
|
+
|
|
89
|
+
This scaffold's `tsconfig.json` lists a single tsserver plugin. It is
|
|
90
|
+
editor-only — not required for the framework to run.
|
|
91
|
+
|
|
92
|
+
```jsonc
|
|
93
|
+
// tsconfig.json (already wired by the scaffold)
|
|
94
|
+
"plugins": [
|
|
95
|
+
{ "name": "@webjskit/ts-plugin" }
|
|
96
|
+
]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`@webjskit/ts-plugin` bundles `ts-lit-plugin` internally (it's a runtime
|
|
100
|
+
dependency of the plugin) and loads it programmatically — so users
|
|
101
|
+
list one entry, not two. You get the full stack of template-literal
|
|
102
|
+
intelligence (type-checking, diagnostics, go-to-def inside
|
|
103
|
+
`` html`…` `` and `` css`…` `` templates) **plus** webjs-aware behaviour
|
|
104
|
+
layered on top:
|
|
105
|
+
|
|
106
|
+
- "Unknown tag/attribute" diagnostics are silenced for elements
|
|
107
|
+
registered via `Class.register('tag-name')`.
|
|
108
|
+
- Attribute auto-complete sourced from each component's
|
|
109
|
+
`static properties`.
|
|
110
|
+
- Attribute-value type-check against `declare propName: T` annotations.
|
|
111
|
+
|
|
112
|
+
See [docs.webjs.com → Editor setup](https://docs.webjs.com/docs/editor-setup)
|
|
113
|
+
for the full walkthrough.
|
|
114
|
+
|
|
115
|
+
## UI components — Webjs UI (preinstalled)
|
|
116
|
+
|
|
117
|
+
This scaffold ships with the standard Webjs UI component kit
|
|
118
|
+
**already installed at `components/ui/`**. The kit is **AI-first** and
|
|
119
|
+
splits into two tiers. Internalise the split — picking the wrong tier
|
|
120
|
+
produces broken markup.
|
|
121
|
+
|
|
122
|
+
### Tier 1 — class-helper functions (the majority)
|
|
123
|
+
|
|
124
|
+
Pure functions that return Tailwind class strings. You apply them to
|
|
125
|
+
**raw native HTML elements** that you write yourself. Examples:
|
|
126
|
+
`button`, `card`, `input`, `label`, `alert`, `badge`, `separator`,
|
|
127
|
+
`skeleton`, `kbd`, `table`, `breadcrumb`, `pagination`, `native-select`,
|
|
128
|
+
`avatar`, `checkbox`, `switch`, `radio-group`, `textarea`, `toggle`,
|
|
129
|
+
`aspect-ratio`.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import {
|
|
133
|
+
cardClass, cardHeaderClass, cardTitleClass,
|
|
134
|
+
cardContentClass, cardFooterClass,
|
|
135
|
+
} from '../../components/ui/card.ts';
|
|
136
|
+
import { inputClass } from '../../components/ui/input.ts';
|
|
137
|
+
import { labelClass } from '../../components/ui/label.ts';
|
|
138
|
+
import { buttonClass } from '../../components/ui/button.ts';
|
|
139
|
+
|
|
140
|
+
return html`
|
|
141
|
+
<div class=${cardClass()}>
|
|
142
|
+
<div class=${cardHeaderClass()}>
|
|
143
|
+
<h3 class=${cardTitleClass()}>Profile</h3>
|
|
144
|
+
</div>
|
|
145
|
+
<div class=${cardContentClass()}>
|
|
146
|
+
<label class=${labelClass()} for="name">Name</label>
|
|
147
|
+
<input class=${inputClass()} id="name" name="name">
|
|
148
|
+
</div>
|
|
149
|
+
<div class=${cardFooterClass()}>
|
|
150
|
+
<button class=${buttonClass()}>Save</button>
|
|
151
|
+
</div>
|
|
152
|
+
</div>
|
|
153
|
+
`;
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
Helpers with variants take an options object:
|
|
157
|
+
`buttonClass({ variant: 'outline', size: 'sm' })`.
|
|
158
|
+
|
|
159
|
+
### Tier 2 — stateful custom elements
|
|
160
|
+
|
|
161
|
+
For things the browser doesn't provide natively (focus traps, portaled
|
|
162
|
+
overlays, keyboard-navigated lists): `dialog`, `alert-dialog`, `popover`,
|
|
163
|
+
`tooltip`, `hover-card`, `tabs`, `accordion`, `collapsible`,
|
|
164
|
+
`dropdown-menu`, `progress`, `sonner`, `toggle-group`. These ARE custom
|
|
165
|
+
elements — import them once (typically in `app/layout.ts`) and use
|
|
166
|
+
`<ui-X>` tags:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// app/layout.ts (registers the custom elements for every page)
|
|
170
|
+
import '../components/ui/dialog.ts';
|
|
171
|
+
import '../components/ui/tabs.ts';
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
```ts
|
|
175
|
+
// app/some-page/page.ts (uses the registered elements)
|
|
176
|
+
import { buttonClass } from '../../components/ui/button.ts';
|
|
177
|
+
|
|
178
|
+
return html`
|
|
179
|
+
<ui-dialog>
|
|
180
|
+
<ui-dialog-trigger>
|
|
181
|
+
<button class=${buttonClass({ variant: 'outline' })}>Edit</button>
|
|
182
|
+
</ui-dialog-trigger>
|
|
183
|
+
<ui-dialog-content>
|
|
184
|
+
<h2>Edit profile</h2>
|
|
185
|
+
...
|
|
186
|
+
</ui-dialog-content>
|
|
187
|
+
</ui-dialog>
|
|
188
|
+
`;
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Adding more components
|
|
192
|
+
|
|
193
|
+
```sh
|
|
194
|
+
webjs ui add dialog dropdown-menu tabs progress
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Each `webjs ui add` call fetches the component source from
|
|
198
|
+
`https://ui.webjs.dev/registry/<name>.json`, copies it into
|
|
199
|
+
`components/ui/`, and installs any required npm deps. Run
|
|
200
|
+
`webjs ui list` to browse the catalogue or visit
|
|
201
|
+
[https://ui.webjs.dev](https://ui.webjs.dev).
|
|
202
|
+
|
|
203
|
+
### AI agents — picking the right tier
|
|
204
|
+
|
|
205
|
+
For forms, dashboards, settings pages, marketing layouts: **call the
|
|
206
|
+
Tier-1 class helpers on raw native elements**. You get accessibility,
|
|
207
|
+
visual consistency, and form submission semantics for free —
|
|
208
|
+
`<input class=${inputClass()}>` is a real `<input>`, with native
|
|
209
|
+
autofill, browser validation, and `<form>` submission unchanged.
|
|
210
|
+
|
|
211
|
+
For modals, dropdowns, tooltips, tab strips, accordions: use the
|
|
212
|
+
Tier-2 `<ui-X>` custom element tags after importing the corresponding
|
|
213
|
+
module.
|
|
214
|
+
|
|
215
|
+
The composition style is deliberately **not** shadcn's
|
|
216
|
+
component-everything React API. We use native elements + class helpers
|
|
217
|
+
for the visual stuff because hiding a `<button>` inside a `<Button>`
|
|
218
|
+
wrapper adds zero value and obscures the real element from inspection,
|
|
219
|
+
form submission, and screen readers. Custom elements are reserved for
|
|
220
|
+
behavior the browser can't deliver natively.
|
|
221
|
+
|
|
87
222
|
## File conventions
|
|
88
223
|
|
|
89
224
|
```
|
|
@@ -172,9 +307,8 @@ import { rateLimit, cache, createAuth, Credentials, Session } from '@webjskit/se
|
|
|
172
307
|
import { WebComponent, html, css } from '@webjskit/core';
|
|
173
308
|
|
|
174
309
|
export class Counter extends WebComponent {
|
|
175
|
-
static tag = 'my-counter'; // required, must contain a hyphen
|
|
176
310
|
static properties = { count: { type: Number } };
|
|
177
|
-
static styles = css`button { padding: 8px 12px; }`;
|
|
311
|
+
static styles = css`button { padding: 8px 12px; }`; // shadow-DOM only
|
|
178
312
|
// static shadow = true; // opt into shadow DOM (default: light DOM)
|
|
179
313
|
// static lazy = true; // download JS only when scrolled into view
|
|
180
314
|
|
|
@@ -208,22 +342,89 @@ type-safe RPC stub automatically.
|
|
|
208
342
|
|
|
209
343
|
## Metadata (per-page)
|
|
210
344
|
|
|
345
|
+
The `metadata` export is Next.js-compatible. Common fields shown below;
|
|
346
|
+
the full surface includes `title.template / .default / .absolute`,
|
|
347
|
+
`metadataBase`, `alternates: { canonical, languages, media, types }`,
|
|
348
|
+
`robots`, `keywords`, `authors`, `creator`, `publisher`, `verification`,
|
|
349
|
+
`icons`, `manifest`, `appleWebApp`, `formatDetection`, `itunes`, and
|
|
350
|
+
the typed `other: { '<meta-name>': value }` escape hatch.
|
|
351
|
+
|
|
211
352
|
```ts
|
|
212
353
|
export const metadata = {
|
|
213
354
|
title: 'My page',
|
|
355
|
+
// OR: title: { template: '%s — {{APP_NAME}}', default: '{{APP_NAME}}' }
|
|
214
356
|
description: 'A page in {{APP_NAME}}',
|
|
215
|
-
|
|
357
|
+
metadataBase: 'https://example.com', // base for relative URLs below
|
|
358
|
+
openGraph: { type: 'website', image: '/og.png' },
|
|
216
359
|
twitter: { card: 'summary_large_image' },
|
|
217
|
-
|
|
360
|
+
icons: { icon: '/favicon.svg', apple: '/apple.png' },
|
|
361
|
+
alternates: { canonical: '/post' }, // → <link rel="canonical">
|
|
362
|
+
robots: { index: true, follow: true },
|
|
363
|
+
cacheControl: 'public, max-age=60', // opt into caching (default: no-store)
|
|
218
364
|
};
|
|
219
365
|
```
|
|
220
366
|
|
|
221
367
|
Use `generateMetadata(ctx)` when you need request-scoped values (e.g.
|
|
222
|
-
absolute URLs from `ctx.url`)
|
|
368
|
+
absolute URLs from `ctx.url`):
|
|
369
|
+
|
|
370
|
+
```ts
|
|
371
|
+
export function generateMetadata(ctx: { url: string }) {
|
|
372
|
+
return { metadataBase: new URL(ctx.url).origin, title: 'Hello' };
|
|
373
|
+
}
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Viewport may be split into its own export (Next.js 14+ pattern):
|
|
377
|
+
|
|
378
|
+
```ts
|
|
379
|
+
export const viewport = {
|
|
380
|
+
width: 'device-width',
|
|
381
|
+
initialScale: 1,
|
|
382
|
+
themeColor: '#1c1613',
|
|
383
|
+
colorScheme: 'light dark',
|
|
384
|
+
};
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Document shell (`<html>` / `<head>` / `<body>`)
|
|
388
|
+
|
|
389
|
+
The framework owns the shell by default. The SSR pipeline auto-emits
|
|
390
|
+
`<!doctype html><html lang="en"><head>…</head><body>` around every
|
|
391
|
+
composition, and auto-hoists `<link>` / `<style>` / `<meta>` / `<script>`
|
|
392
|
+
tags returned anywhere in a layout/page into the real `<head>`. The
|
|
393
|
+
`metadata` export drives `<title>` and `<meta>` tags.
|
|
394
|
+
|
|
395
|
+
**Only `app/layout.ts` (the root layout)** may optionally write its
|
|
396
|
+
own `<!doctype><html><head>…</head><body>` shell to override `<html lang>`,
|
|
397
|
+
`<html dir>`, `<html data-*>`, `<body class>`, or add a custom
|
|
398
|
+
`<link rel="preconnect">` etc. When the root layout supplies a shell,
|
|
399
|
+
the framework respects it and splices its required tags into the
|
|
400
|
+
user's `<head>`.
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
// app/layout.ts — root, optionally owning the shell
|
|
404
|
+
export default function RootLayout({ children }) {
|
|
405
|
+
return html`
|
|
406
|
+
<!doctype html>
|
|
407
|
+
<html lang="es" data-theme="dark">
|
|
408
|
+
<head>
|
|
409
|
+
<link rel="preconnect" href="https://cdn.example.com">
|
|
410
|
+
</head>
|
|
411
|
+
<body class="min-h-screen bg-bg">
|
|
412
|
+
<main>${children}</main>
|
|
413
|
+
</body>
|
|
414
|
+
</html>
|
|
415
|
+
`;
|
|
416
|
+
}
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Non-root layouts** (`app/<segment>/layout.ts`) and **pages**
|
|
420
|
+
(`app/**/page.ts`) **must NOT** write `<!doctype>` / `<html>` / `<head>`
|
|
421
|
+
/ `<body>`. The framework auto-emits the wrapper around the whole
|
|
422
|
+
composition, so a nested shell ends up dropped by the HTML parser.
|
|
423
|
+
`webjs check` enforces this via the `shell-in-non-root-layout` rule.
|
|
223
424
|
|
|
224
425
|
## Invariants (do not violate)
|
|
225
426
|
|
|
226
|
-
1. Custom element tags must contain a hyphen.
|
|
427
|
+
1. Custom element tags must contain a hyphen. Pass the tag to `.register('tag-name')` at the bottom of the file. The tag is not a static field.
|
|
227
428
|
2. Never import `@prisma/client` or `node:*` from client-reachable files —
|
|
228
429
|
only from `.server.ts` modules or `lib/*.ts`.
|
|
229
430
|
3. Event / property / boolean holes in `` html`` `` are unquoted:
|
package/templates/CONVENTIONS.md
CHANGED
|
@@ -345,6 +345,64 @@ with puppeteer or playwright imports.
|
|
|
345
345
|
|
|
346
346
|
---
|
|
347
347
|
|
|
348
|
+
## UI components — prefer the Webjs UI kit over raw Tailwind
|
|
349
|
+
|
|
350
|
+
<!-- OVERRIDE -->
|
|
351
|
+
|
|
352
|
+
This scaffold ships with the Webjs UI kit preinstalled at `components/ui/`.
|
|
353
|
+
The kit splits into **two tiers** — picking the wrong tier produces
|
|
354
|
+
broken markup.
|
|
355
|
+
|
|
356
|
+
**Tier 1 — class helpers** (button, card, input, label, alert, badge,
|
|
357
|
+
separator, skeleton, table, etc.): pure functions that return Tailwind
|
|
358
|
+
class strings. Call them and spread onto a **raw native element**.
|
|
359
|
+
|
|
360
|
+
**Tier 2 — custom elements** (dialog, popover, tooltip, dropdown-menu,
|
|
361
|
+
tabs, accordion, collapsible, progress, etc.): real `<ui-X>` tags. Import
|
|
362
|
+
the module once (typically in `app/layout.ts`) and use the tag.
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
// Tier 1 — class helpers on native elements (use this for forms,
|
|
366
|
+
// dashboards, cards, layouts — anywhere the value is purely visual)
|
|
367
|
+
import { buttonClass } from '../components/ui/button.ts';
|
|
368
|
+
import { inputClass } from '../components/ui/input.ts';
|
|
369
|
+
return html`
|
|
370
|
+
<button class=${buttonClass({ size: 'lg' })}>Save</button>
|
|
371
|
+
<input class=${inputClass()} placeholder="Email">
|
|
372
|
+
`;
|
|
373
|
+
|
|
374
|
+
// Tier 2 — custom elements (modals, dropdowns, tab strips, tooltips —
|
|
375
|
+
// state the browser doesn't give you natively)
|
|
376
|
+
return html`
|
|
377
|
+
<ui-dialog>
|
|
378
|
+
<ui-dialog-trigger>
|
|
379
|
+
<button class=${buttonClass({ variant: 'outline' })}>Edit</button>
|
|
380
|
+
</ui-dialog-trigger>
|
|
381
|
+
<ui-dialog-content>…</ui-dialog-content>
|
|
382
|
+
</ui-dialog>
|
|
383
|
+
`;
|
|
384
|
+
|
|
385
|
+
// Avoid — hand-rolled Tailwind on every <button> loses visual
|
|
386
|
+
// consistency. Tier-1 helpers give you the same control with one import.
|
|
387
|
+
return html`
|
|
388
|
+
<button class="px-4 py-2 rounded-md bg-accent text-accent-fg">Save</button>
|
|
389
|
+
`;
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
Add more components with `webjs ui add <name>` (e.g. `webjs ui add dialog
|
|
393
|
+
tabs popover`). The catalogue lives at
|
|
394
|
+
[https://ui.webjs.dev](https://ui.webjs.dev).
|
|
395
|
+
|
|
396
|
+
**Hand-rolled Tailwind is still appropriate for:**
|
|
397
|
+
- One-off marketing pages, hero sections, landing CTAs.
|
|
398
|
+
- Anywhere the visual design intentionally diverges from the kit baseline.
|
|
399
|
+
- Layout primitives (`<div class="grid grid-cols-3 gap-4">`).
|
|
400
|
+
|
|
401
|
+
The convention: any visual element with a Tier-1 helper uses the helper.
|
|
402
|
+
Any stateful behavior with a Tier-2 element uses the element.
|
|
403
|
+
|
|
404
|
+
---
|
|
405
|
+
|
|
348
406
|
## Components
|
|
349
407
|
|
|
350
408
|
<!-- OVERRIDE -->
|