@bytesbrains/weblocks 0.2.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.
Files changed (109) hide show
  1. package/AGENT.md +214 -0
  2. package/CATALOG.md +387 -0
  3. package/CHANGELOG.md +63 -0
  4. package/LICENSE +21 -0
  5. package/README.md +294 -0
  6. package/catalog.json +1850 -0
  7. package/lib/blocks/about.d.ts +2 -0
  8. package/lib/blocks/about.js +50 -0
  9. package/lib/blocks/accordion.d.ts +2 -0
  10. package/lib/blocks/accordion.js +51 -0
  11. package/lib/blocks/announcementBar.d.ts +2 -0
  12. package/lib/blocks/announcementBar.js +56 -0
  13. package/lib/blocks/appShell.d.ts +2 -0
  14. package/lib/blocks/appShell.js +54 -0
  15. package/lib/blocks/auth.d.ts +2 -0
  16. package/lib/blocks/auth.js +82 -0
  17. package/lib/blocks/blogList.d.ts +2 -0
  18. package/lib/blocks/blogList.js +81 -0
  19. package/lib/blocks/blogPost.d.ts +2 -0
  20. package/lib/blocks/blogPost.js +60 -0
  21. package/lib/blocks/carousel.d.ts +2 -0
  22. package/lib/blocks/carousel.js +56 -0
  23. package/lib/blocks/contactDetails.d.ts +2 -0
  24. package/lib/blocks/contactDetails.js +50 -0
  25. package/lib/blocks/contactForm.d.ts +2 -0
  26. package/lib/blocks/contactForm.js +100 -0
  27. package/lib/blocks/cta.d.ts +2 -0
  28. package/lib/blocks/cta.js +40 -0
  29. package/lib/blocks/divider.d.ts +2 -0
  30. package/lib/blocks/divider.js +28 -0
  31. package/lib/blocks/faq.d.ts +2 -0
  32. package/lib/blocks/faq.js +52 -0
  33. package/lib/blocks/features.d.ts +2 -0
  34. package/lib/blocks/features.js +58 -0
  35. package/lib/blocks/feed.d.ts +2 -0
  36. package/lib/blocks/feed.js +81 -0
  37. package/lib/blocks/footer.d.ts +2 -0
  38. package/lib/blocks/footer.js +40 -0
  39. package/lib/blocks/gallery.d.ts +2 -0
  40. package/lib/blocks/gallery.js +64 -0
  41. package/lib/blocks/hero.d.ts +2 -0
  42. package/lib/blocks/hero.js +48 -0
  43. package/lib/blocks/heroApp.d.ts +2 -0
  44. package/lib/blocks/heroApp.js +65 -0
  45. package/lib/blocks/logos.d.ts +2 -0
  46. package/lib/blocks/logos.js +59 -0
  47. package/lib/blocks/map.d.ts +2 -0
  48. package/lib/blocks/map.js +43 -0
  49. package/lib/blocks/nav.d.ts +2 -0
  50. package/lib/blocks/nav.js +39 -0
  51. package/lib/blocks/newsletter.d.ts +2 -0
  52. package/lib/blocks/newsletter.js +56 -0
  53. package/lib/blocks/pricing.d.ts +2 -0
  54. package/lib/blocks/pricing.js +89 -0
  55. package/lib/blocks/prose.d.ts +5 -0
  56. package/lib/blocks/prose.js +46 -0
  57. package/lib/blocks/richText.d.ts +2 -0
  58. package/lib/blocks/richText.js +46 -0
  59. package/lib/blocks/services.d.ts +2 -0
  60. package/lib/blocks/services.js +65 -0
  61. package/lib/blocks/sidebar.d.ts +2 -0
  62. package/lib/blocks/sidebar.js +58 -0
  63. package/lib/blocks/socialLinks.d.ts +2 -0
  64. package/lib/blocks/socialLinks.js +53 -0
  65. package/lib/blocks/spacer.d.ts +2 -0
  66. package/lib/blocks/spacer.js +25 -0
  67. package/lib/blocks/split.d.ts +2 -0
  68. package/lib/blocks/split.js +67 -0
  69. package/lib/blocks/stats.d.ts +2 -0
  70. package/lib/blocks/stats.js +64 -0
  71. package/lib/blocks/steps.d.ts +2 -0
  72. package/lib/blocks/steps.js +63 -0
  73. package/lib/blocks/tabs.d.ts +2 -0
  74. package/lib/blocks/tabs.js +57 -0
  75. package/lib/blocks/team.d.ts +2 -0
  76. package/lib/blocks/team.js +94 -0
  77. package/lib/blocks/testimonials.d.ts +2 -0
  78. package/lib/blocks/testimonials.js +54 -0
  79. package/lib/blocks/timeline.d.ts +2 -0
  80. package/lib/blocks/timeline.js +53 -0
  81. package/lib/blocks/video.d.ts +2 -0
  82. package/lib/blocks/video.js +61 -0
  83. package/lib/catalog.d.ts +26 -0
  84. package/lib/catalog.js +62 -0
  85. package/lib/generate.d.ts +35 -0
  86. package/lib/generate.js +160 -0
  87. package/lib/index.d.ts +19 -0
  88. package/lib/index.js +11 -0
  89. package/lib/ops.d.ts +85 -0
  90. package/lib/ops.js +248 -0
  91. package/lib/presets.d.ts +16 -0
  92. package/lib/presets.js +50 -0
  93. package/lib/pwa.d.ts +30 -0
  94. package/lib/pwa.js +79 -0
  95. package/lib/registry.d.ts +53 -0
  96. package/lib/registry.js +81 -0
  97. package/lib/render.d.ts +8 -0
  98. package/lib/render.js +105 -0
  99. package/lib/runtime.d.ts +68 -0
  100. package/lib/runtime.js +46 -0
  101. package/lib/schema.d.ts +67 -0
  102. package/lib/schema.js +150 -0
  103. package/lib/tokens.d.ts +37 -0
  104. package/lib/tokens.js +136 -0
  105. package/lib/types.d.ts +108 -0
  106. package/lib/types.js +10 -0
  107. package/lib/validate.d.ts +10 -0
  108. package/lib/validate.js +48 -0
  109. package/package.json +65 -0
package/README.md ADDED
@@ -0,0 +1,294 @@
1
+ <div align="center">
2
+
3
+ # @bytesbrains/weblocks
4
+
5
+ **The block engine for AI-composable web apps.**
6
+
7
+ An AI composes a `SiteManifest` from a fixed catalog of typed blocks — its entire
8
+ API surface — and the engine validates it and renders one self-contained static
9
+ HTML document. Snap-together “Lego bricks” for web apps: safe by construction,
10
+ provider- and host-neutral, zero runtime dependencies.
11
+
12
+ [![npm](https://img.shields.io/npm/v/@bytesbrains/weblocks.svg)](https://www.npmjs.com/package/@bytesbrains/weblocks)
13
+ [![license: MIT](https://img.shields.io/npm/l/@bytesbrains/weblocks.svg)](./LICENSE)
14
+ [![node](https://img.shields.io/node/v/@bytesbrains/weblocks.svg)](https://nodejs.org)
15
+ [![types](https://img.shields.io/npm/types/@bytesbrains/weblocks.svg)](./lib/index.d.ts)
16
+ [![deps](https://img.shields.io/badge/runtime%20deps-0-brightgreen.svg)](./package.json)
17
+
18
+ </div>
19
+
20
+ ```
21
+ brief ──generateSite(callModel)──▶ SiteManifest ──validate──▶ renderSite ──▶ one static HTML document
22
+ message ──editSite(callModel)────▶ EditOp[] ─────apply────▶ new SiteManifest (version++)
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Why
28
+
29
+ Letting a model emit raw HTML/CSS/JS for a whole site is powerful and unsafe:
30
+ malformed output, injection, incoherent styling, and no way to change one thing
31
+ later without regenerating everything. **weblocks gives the AI a closed vocabulary
32
+ and a typed configuration contract** — a catalog of blocks. The model’s job
33
+ shrinks from *“write a correct website”* to *“pick and fill known bricks,”* which
34
+ is exactly what LLMs are reliable at, and the engine guarantees the result is
35
+ valid and coherent.
36
+
37
+ - 🔒 **Closed vocabulary** — a block exists only if it’s in the catalog; the AI
38
+ can never invent markup or emit raw HTML.
39
+ - 🧱 **Illegal states unrepresentable** — a bad edit is *rejected*, never applied.
40
+ - 🛡️ **Total renderer** — every field defaulted, all text escaped, all URLs
41
+ sanitized; it cannot throw, so a broken page is structurally impossible.
42
+ - 🔀 **Validity ⟂ model quality** — page validity comes from the schema +
43
+ renderer, not the model being right. Swap or downgrade models freely.
44
+ - 🎨 **Coherent theming** — one palette swap re-themes the whole app; automatic
45
+ light/dark; contrast-safe fills.
46
+ - 🌐 **Provider- & host-neutral** — you inject the model call; the engine bundles
47
+ no backend, provider, or host.
48
+
49
+ Zero runtime dependencies · pure TypeScript · ESM · Node ≥ 20.
50
+
51
+ ## Table of contents
52
+
53
+ - [Install](#install) · [Quickstart](#quickstart) · [Core concepts](#core-concepts)
54
+ - [The AI contract](#the-ai-contract) · [Block catalog](#block-catalog)
55
+ - [Editing](#editing) · [Theming](#theming) · [Powered blocks & runtime](#powered-blocks--runtime) · [PWA](#pwa)
56
+ - [API reference](#api-reference) · [Adding a block](#adding-a-block) · [Local development](#local-development)
57
+
58
+ ## Install
59
+
60
+ ```bash
61
+ npm install @bytesbrains/weblocks
62
+ ```
63
+
64
+ ## Quickstart
65
+
66
+ You supply a `callModel` function (any provider — you own the key). The engine
67
+ turns a brief into a validated manifest and static HTML.
68
+
69
+ ```ts
70
+ import { writeFileSync } from 'node:fs';
71
+ import { generateSite, renderSite } from '@bytesbrains/weblocks';
72
+
73
+ // Bring your own provider: (system, user) => model's text reply.
74
+ const callModel = async ({ system, user }) => {
75
+ const res = await fetch('https://api.your-provider.com/v1/chat/completions', {
76
+ method: 'POST',
77
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${process.env.API_KEY}` },
78
+ body: JSON.stringify({
79
+ model: 'your-model',
80
+ messages: [{ role: 'system', content: system }, { role: 'user', content: user }],
81
+ }),
82
+ });
83
+ return (await res.json()).choices[0].message.content;
84
+ };
85
+
86
+ const { ok, manifest, warnings } = await generateSite('a Lisbon bakery landing page', callModel);
87
+ if (ok) writeFileSync('index.html', renderSite(manifest)); // self-contained HTML
88
+ ```
89
+
90
+ `renderSite(manifest)` returns a complete `<!doctype html>…</html>` string with
91
+ the used blocks’ CSS inlined — no build step, no framework runtime.
92
+
93
+ > **Building an AI application?** Use the latest, most capable models — the
94
+ > catalog is designed to be fed as a function-calling / structured-output schema.
95
+ > See **[`AGENT.md`](./AGENT.md)** for a ready-to-use guide you can hand to a model.
96
+
97
+ ## Core concepts
98
+
99
+ A **`SiteManifest`** is the single source of truth the AI composes and edits — it
100
+ is never raw HTML:
101
+
102
+ ```ts
103
+ type SiteManifest = {
104
+ meta: { title: string; description: string; lang: string };
105
+ design: DesignTokens; // the shared baseplate (CSS custom properties)
106
+ blocks: Block[]; // ordered, typed page sections
107
+ version: number; // bumped per accepted edit → undo / history / diff
108
+ pwa?: PwaConfig; // opt-in installable PWA
109
+ seo?: SeoConfig; // opt-in <head> meta
110
+ };
111
+
112
+ type Block = { id: string; type: string; visible: boolean; config: object; overrides?: object };
113
+ ```
114
+
115
+ - **`design`** — CSS custom properties every block styles from, so one edit
116
+ restyles the whole app coherently.
117
+ - **`blocks`** — placed bricks; `type` comes from the closed catalog and `config`
118
+ is validated against that block’s schema before it is ever applied.
119
+
120
+ Two walls make a broken page impossible: **schema validation** (the strict gate an
121
+ edit op passes) and the **total renderer** (defaults + escaping, never throws).
122
+
123
+ ## The AI contract
124
+
125
+ The catalog is the *only* surface the model is told it may use. It ships with the
126
+ package in two forms, and is also generated at runtime:
127
+
128
+ | Form | What | Use |
129
+ |---|---|---|
130
+ | **`catalog.json`** | JSON Schema per block | Function-calling / structured output |
131
+ | **`CATALOG.md`** | Human-readable reference | Docs / review |
132
+ | `catalog()` | `BlockCatalogEntry[]` at runtime | Programmatic |
133
+ | `catalogPrompt()` | Compact string | Cheap system prompt |
134
+
135
+ ```ts
136
+ import { catalog, catalogPrompt } from '@bytesbrains/weblocks';
137
+ import catalogJson from '@bytesbrains/weblocks/catalog.json' with { type: 'json' };
138
+ ```
139
+
140
+ ## Block catalog
141
+
142
+ **37 typed blocks.** Full field reference in [`CATALOG.md`](./CATALOG.md).
143
+
144
+ | Group | Blocks |
145
+ |---|---|
146
+ | Chrome / app-shell | `nav` · `app-shell` · `sidebar` · `announcement-bar` · `footer` |
147
+ | Heroes | `hero` · `hero-app` |
148
+ | Content | `features` · `about` · `rich-text` · `split` · `steps` · `stats` · `services-catalogue` · `pricing` · `logos` · `team` |
149
+ | Media | `gallery` · `carousel` · `video` · `map` |
150
+ | Structured | `timeline` · `tabs` · `accordion` · `testimonials` · `faq` |
151
+ | Collections | `blog-list` · `blog-post` · `feed` |
152
+ | Dynamic (powered) | `contact-form` · `newsletter` · `auth` |
153
+ | Conversion / rhythm | `cta` · `social-links` · `contact-details` · `divider` · `spacer` |
154
+
155
+ `rich-text` and `blog-post` carry **typed** content nodes (headings, paragraphs,
156
+ quotes, lists) — a safe freeform-content escape hatch that is never raw HTML.
157
+
158
+ ## Editing
159
+
160
+ Editing is a set of validated verbs, not a regeneration. Drive them from natural
161
+ language (`editSite`) or emit them directly:
162
+
163
+ ```ts
164
+ import { applyOp, editSite } from '@bytesbrains/weblocks';
165
+
166
+ // Natural language → validated ops → new versioned manifest:
167
+ const { manifest: edited, applied } = await editSite(manifest, 'go dark and add a gallery', callModel);
168
+
169
+ // Or emit ops directly (what a chat/inspector produces):
170
+ applyOp(manifest, { op: 'updateBlock', id: 'hero-1', config: { headline: 'New' } });
171
+
172
+ // Array-item ops edit ONE item without rewriting the block:
173
+ applyOp(manifest, { op: 'addItem', id: 'features-1', field: 'items', item: { title: 'Fast' } });
174
+ applyOp(manifest, { op: 'updateItem', id: 'features-1', field: 'items', index: 0, patch: { text: 'Now faster' } });
175
+ ```
176
+
177
+ Every op is validated before it applies; a bad op is a no-op (with errors), and
178
+ `version` bumps on each accepted edit — so undo / history / diff come for free.
179
+
180
+ **Ops:** `addBlock` · `updateBlock` · `removeBlock` · `moveBlock` · `setVisible` ·
181
+ `addItem` · `updateItem` · `removeItem` · `moveItem` · `setDesignTokens` ·
182
+ `applyPreset` · `setOverrides` · `setMeta`.
183
+
184
+ ## Theming
185
+
186
+ ```ts
187
+ import { presetNames } from '@bytesbrains/weblocks';
188
+
189
+ applyOp(manifest, { op: 'applyPreset', name: 'midnight' }); // named token presets
190
+ applyOp(manifest, { op: 'setDesignTokens', patch: { radius: 'round' } }); // patch any tokens
191
+ applyOp(manifest, { op: 'setOverrides', id: 'cta-1', overrides: { primary: '#0af', radius: 'sharp' } }); // per-section
192
+ presetNames(); // ['sand', 'midnight', 'forest', 'mono', 'candy', 'ocean']
193
+ ```
194
+
195
+ - **Automatic light/dark** — set `design.mode = 'auto'` to follow the viewer’s OS
196
+ theme (supply an optional `design.darkPalette`), or `'light'` / `'dark'` for a
197
+ fixed one.
198
+ - **Contrast-safe fills** — derived `--on-primary` / `--on-accent` tokens keep
199
+ button text legible on *any* palette (no hardcoded colors).
200
+ - **Per-section overrides** — tint one block’s palette / radius / spacing without
201
+ breaking overall coherence.
202
+
203
+ ## Powered blocks & runtime
204
+
205
+ Blocks like `contact-form`, `newsletter`, and `auth` need a backend. The engine
206
+ bundles none: a powered block **declares** the capabilities it needs, and your
207
+ host wires them through a tiny adapter.
208
+
209
+ ```ts
210
+ import { renderSite, pathRuntime, runtimeNeeds } from '@bytesbrains/weblocks';
211
+
212
+ runtimeNeeds(manifest); // e.g. [{ type: 'contact-form', capabilities: ['contact-form.submit'] }]
213
+
214
+ // Map every capability to POST /api/<capability>/<blockId> in one line:
215
+ const html = renderSite(manifest, { runtime: pathRuntime('/api') });
216
+ ```
217
+
218
+ With no runtime, powered blocks render **inert-but-valid** (a disabled control + a
219
+ note), keeping `data-wl-*` hooks so a host can enhance them client-side. Captcha,
220
+ server-side validation, delivery, abuse limits, and identity are the host’s job.
221
+
222
+ ## PWA
223
+
224
+ Add a `pwa` field and the engine derives an installable app shell:
225
+
226
+ ```ts
227
+ import { renderSite, emitPwa } from '@bytesbrains/weblocks';
228
+
229
+ manifest.pwa = { name: 'My App', offline: true };
230
+ writeFileSync('index.html', renderSite(manifest)); // adds manifest + SW meta to <head>
231
+ for (const [file, body] of Object.entries(emitPwa(manifest) ?? {})) writeFileSync(file, body);
232
+ // → manifest.webmanifest + sw.js
233
+ ```
234
+
235
+ ## API reference
236
+
237
+ All exports are named; types are shipped (`lib/index.d.ts`).
238
+
239
+ | Area | Exports |
240
+ |---|---|
241
+ | **Compose / edit (AI)** | `generateSite` · `editSite` · `buildGenerationPrompt` · `buildEditPrompt` · `parseManifestResponse` · `parseOpsResponse` |
242
+ | **Render** | `renderSite` |
243
+ | **Edit ops** | `applyOp` · `applyOps` |
244
+ | **Validate** | `validateManifest` · `validateBlock` |
245
+ | **Catalog** | `catalog` · `catalogPrompt` |
246
+ | **Registry** | `REGISTRY` · `getSpec` · `blockTypes` · `needsIsland` |
247
+ | **Theming** | `DEFAULT_TOKENS` · `normalizeTokens` · `tokensToCss` · `sectionOverrideCss` · `readableOn` · `PRESETS` · `presetNames` · `getPreset` |
248
+ | **Runtime** | `NOOP_RUNTIME` · `pathRuntime` · `runtimeNeeds` |
249
+ | **PWA** | `buildWebManifest` · `buildWebManifestJson` · `buildServiceWorker` · `emitPwa` |
250
+ | **Schema utils** | `parse` · `escapeHtml` · `escapeAttr` · `sanitizeUrl` |
251
+
252
+ Core types: `SiteManifest` · `Block` · `DesignTokens` · `Palette` ·
253
+ `SectionOverrides` · `PwaConfig` · `SeoConfig` · `EditOp` · `BlockSpec` ·
254
+ `RuntimeAdapter` · `ModelCall`.
255
+
256
+ `ModelCall` is `(args: { system: string; user: string }) => Promise<string>` — the
257
+ one thing you inject, so the engine never depends on a provider.
258
+
259
+ ## Adding a block
260
+
261
+ Register a `BlockSpec` (`type` + `schema` + `css` + `render`, optionally `island`
262
+ / `runtime`) in `registry.ts`. It must clear the **block definition-of-done**: a
263
+ typed schema (no raw-HTML field), consumes shared tokens, renders totally
264
+ (defaults + escaping, never throws), valid regardless of neighbours. See
265
+ [`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md#adding-a-block-concretely).
266
+
267
+ ## Local development
268
+
269
+ ```bash
270
+ npm install # dev deps only (typescript, @types/node)
271
+ npm run build # tsc → lib/
272
+ npm test # block definition-of-done + engine invariants
273
+ npm run example # render a sample → example-output.html
274
+ npm run emit:catalog # regenerate catalog.json + CATALOG.md from code
275
+ ```
276
+
277
+ Drive it end-to-end with a real model (dev harness — provider is env, not code):
278
+
279
+ ```bash
280
+ PROVIDER=openai OPENAI_API_KEY=sk-… npm run ai -- generate "a Lisbon bakery landing page"
281
+ PROVIDER=openai OPENAI_API_KEY=sk-… npm run ai -- edit "make it dark, add a gallery"
282
+ ```
283
+
284
+ ## Documentation
285
+
286
+ - **[`AGENT.md`](./AGENT.md)** — how to use this package from an AI / agent.
287
+ - **[`VISION.md`](./VISION.md)** — principles and direction.
288
+ - **[`docs/ARCHITECTURE.md`](./docs/ARCHITECTURE.md)** — internals.
289
+ - **[`CATALOG.md`](./CATALOG.md)** — every block’s fields.
290
+ - **[`CHANGELOG.md`](./CHANGELOG.md)** · **[`CONTRIBUTING.md`](./CONTRIBUTING.md)** · **[`SECURITY.md`](./SECURITY.md)**
291
+
292
+ ## License
293
+
294
+ [MIT](./LICENSE) © bytesbrains