diff --git a/README.md b/README.md index df3d2d2a..c8f8a1b7 100644 --- a/README.md +++ b/README.md @@ -7,237 +7,151 @@ ![Coverage](https://img.shields.io/badge/coverage-86.6%25-green) -A monorepo of production-quality TypeScript libraries. Written with zero runtime dependencies, strict types, and a strong focus on correctness — every package ships with unit tests, full TypeScript type coverage, and automated CI on every pull request. +Cleverbrush is a schema-first TypeScript framework monorepo. It provides the +building blocks for contract-driven web applications: validation, object +mapping, React forms, typed HTTP clients, server endpoint contracts, OpenAPI, +dependency injection, environment parsing, persistence helpers, logging, and +OpenTelemetry. -The flagship package is **`@cleverbrush/schema`** — a schema validation library that is faster than Zod in 14 out of 15 benchmarks (up to 204× faster on invalid input), 3× smaller than Zod v4 in bundle size, and compatible with 50+ ecosystem tools via [Standard Schema v1](https://standardschema.dev/). - ---- +`@cleverbrush/schema` is the foundation. A single schema definition can drive +runtime validation, TypeScript inference, property descriptors, forms, mappers, +JSON Schema, API contracts, and Standard Schema integrations. ## Packages | Package | Description | | --- | --- | -| [`@cleverbrush/schema`](./libs/schema) | Schema definition, type inference, and runtime validation. [Standard Schema v1](https://standardschema.dev/) compatible — works with tRPC, TanStack Form, React Hook Form, T3 Env, Hono, and 50+ other tools. Wraps external schemas (Zod, Valibot, ArkType) via `extern()` | -| [`@cleverbrush/mapper`](./libs/mapper) | Schema-driven object mapping with compile-time completeness checking and type-safe property selectors | -| [`@cleverbrush/react-form`](./libs/react-form) | Headless, schema-driven form system for React — type-safe field binding, auto-field rendering, UI-agnostic | -| [`@cleverbrush/schema-json`](./libs/schema-json) | Bidirectional JSON Schema conversion: `toJsonSchema()` + `fromJsonSchema()` with full type inference | -| [`@cleverbrush/async`](./libs/async) | Async utilities: `Collector`, `debounce`, `throttle`, `retry` | -| [`@cleverbrush/deep`](./libs/deep) | Deep operations on objects: deep equality, deep merge, flattening, hashing | -| [`@cleverbrush/scheduler`](./libs/scheduler) | Cron-like job scheduler for Node.js using worker threads | -| [`@cleverbrush/client`](./libs/client) | Typed HTTP client for `@cleverbrush/server` API contracts — Proxy-based, zero codegen, full type inference. Optional React + TanStack Query integration via `/react` subpath | -| [`@cleverbrush/otel`](./libs/otel) | OpenTelemetry instrumentation — traces for HTTP, SQL, and outbound client calls; OTLP log sink with trace correlation; DI integration | -| [`@cleverbrush/knex-clickhouse`](./libs/knex-clickhouse) | Knex query builder dialect for ClickHouse | - ---- - -## Why @cleverbrush/schema? - -If you have used Zod, Yup, or Joi, the fluent API will feel immediately familiar — with several important differences. - -### One schema, four capabilities - -``` -@cleverbrush/schema - │ - ├── TypeScript inference (InferType) - ├── Runtime validation (.validate() / .validateAsync()) - ├── @cleverbrush/mapper (type-safe object mapping) - ├── @cleverbrush/react-form (auto-generated, schema-driven forms) - └── @cleverbrush/schema-json (bidirectional JSON Schema) -``` - -Define a schema once and get all four capabilities for free — no duplication between types, validators, mappers, and form configs. - -### Quick example +| [`@cleverbrush/schema`](./libs/schema) | Immutable fluent schemas with runtime validation, type inference, property descriptors, extensions, and Standard Schema v1 support. | +| [`@cleverbrush/server`](./libs/server) | Schema-first HTTP endpoint contracts, validation, authorization, dependency injection, and RFC 9457 errors. | +| [`@cleverbrush/client`](./libs/client) | Type-safe HTTP client for `@cleverbrush/server` contracts, with optional batching, retries, dedupe, cache tags, offline queue, and React integration. | +| [`@cleverbrush/server-openapi`](./libs/server-openapi) | OpenAPI 3.x generation from server endpoint metadata. | +| [`@cleverbrush/mapper`](./libs/mapper) | Schema-driven object mapping with compile-time completeness checks and type-safe property selectors. | +| [`@cleverbrush/react-form`](./libs/react-form) | Headless React form primitives powered by schema property descriptors. | +| [`@cleverbrush/schema-json`](./libs/schema-json) | JSON Schema generation and JSON Schema to Cleverbrush schema conversion. | +| [`@cleverbrush/di`](./libs/di) | Small dependency-injection container used by server and application code. | +| [`@cleverbrush/auth`](./libs/auth) | Principal and authorization utility types. | +| [`@cleverbrush/env`](./libs/env) | Environment-variable parsing and validation with schema builders. | +| [`@cleverbrush/orm`](./libs/orm) | Knex-backed ORM layer with typed entity maps and query helpers. | +| [`@cleverbrush/orm-cli`](./libs/orm-cli) | CLI tooling for ORM migrations. | +| [`@cleverbrush/knex-schema`](./libs/knex-schema) | Knex schema helpers that connect database names to schema metadata. | +| [`@cleverbrush/knex-clickhouse`](./libs/knex-clickhouse) | ClickHouse dialect support for Knex. | +| [`@cleverbrush/log`](./libs/log) | Structured logging pipeline, sinks, batching, redaction, and context helpers. | +| [`@cleverbrush/otel`](./libs/otel) | OpenTelemetry setup and instrumentation helpers for apps and clients. | +| [`@cleverbrush/async`](./libs/async) | Async utilities including collector, debounce, throttle, and retry. | +| [`@cleverbrush/deep`](./libs/deep) | Deep equality, deep extension, flattening, and object utilities. | +| [`@cleverbrush/scheduler`](./libs/scheduler) | Cron-like job scheduler with schema-validated job configuration. | + +## How The Pieces Fit ```ts -import { object, string, number, InferType } from '@cleverbrush/schema'; +import { object, string, number, type InferType } from '@cleverbrush/schema'; +import { endpoint } from '@cleverbrush/server/contract'; const UserSchema = object({ - name: string().nonempty().minLength(2), - email: string().email(), - age: number().min(18).optional(), + id: number().int().min(1), + email: string().email(), + displayName: string().minLength(2) }); -// TypeScript type — inferred automatically, no duplication type User = InferType; -// Runtime validation -const result = UserSchema.validate({ name: 'Alice', email: 'alice@example.com' }); -if (result.valid) { - console.log(result.object); // typed as User -} else { - const nameErrors = result.getErrorsFor((p) => p.name); - console.log(nameErrors.errors); // ['Name must be at least 2 characters'] -} - -// Standard Schema interop — pass directly to tRPC, TanStack Form, T3 Env, … -const validator = UserSchema['~standard']; +const GetUserEndpoint = endpoint + .get('/api/users/:id') + .params(object({ id: number().int().min(1) })) + .responses({ 200: UserSchema }); ``` -### Performance vs Zod - -Benchmarked with [Vitest bench](https://vitest.dev/guide/features.html#benchmarking) against Zod v4 on the same machine: - -| Benchmark | @cleverbrush/schema | Zod | Ratio | -| --- | --- | --- | --- | -| Array 100 objects — valid | 35,228 ops/s | 13,277 ops/s | **2.65× faster** | -| Array 100 objects — invalid | 899,329 ops/s | 4,396 ops/s | **204× faster** | -| Complex order — valid | 198,988 ops/s | 136,090 ops/s | **1.46× faster** | -| Complex order — invalid | 884,706 ops/s | 26,106 ops/s | **33.9× faster** | -| Flat object — valid | 1,001,194 ops/s | 840,725 ops/s | **1.19× faster** | -| Flat object — invalid | 2,653,630 ops/s | 176,222 ops/s | **15.1× faster** | -| Nested object — valid | 690,556 ops/s | 368,893 ops/s | **1.87× faster** | -| Nested object — invalid | 2,739,319 ops/s | 87,245 ops/s | **31.4× faster** | -| String — valid | 5,348,564 ops/s | 3,533,945 ops/s | **1.51× faster** | -| String — invalid | 5,749,087 ops/s | 482,961 ops/s | **11.9× faster** | -| Number — valid | 7,911,266 ops/s | 4,806,511 ops/s | **1.65× faster** | -| Number — invalid | 5,387,475 ops/s | 637,513 ops/s | **8.45× faster** | -| Union first branch | 1,925,508 ops/s | 1,529,547 ops/s | **1.26× faster** | -| Union last branch | 676,107 ops/s | 732,682 ops/s | 0.92× | -| Union no match — invalid | 5,873,118 ops/s | 385,453 ops/s | **15.2× faster** | - -**14 out of 15 benchmarks.** The early-exit optimization on invalid data produces especially large gains — up to 204× — because type errors are caught at the first failing field without evaluating the rest. - -Run the benchmarks yourself: -```bash -npm run bench +That same schema can be reused across the stack: + +- Runtime validation through `.validate()` and `.validateAsync()`. +- Type inference through `InferType`. +- API input and response contracts in `@cleverbrush/server`. +- Typed clients through `@cleverbrush/client`. +- OpenAPI documents through `@cleverbrush/server-openapi`. +- Type-safe object mapping through `@cleverbrush/mapper`. +- React form fields through `@cleverbrush/react-form`. +- JSON Schema interop through `@cleverbrush/schema-json`. +- Standard Schema compatible integrations such as TanStack Form and T3 Env. + +## Repository Layout + +```text +libs/ publishable @cleverbrush/* packages +demos/ demo applications and e2e setup +websites/ docs, schema site, playground, and shared website UI +scripts/ build, release, docs, and website helper scripts ``` -### Bundle size vs competitors - -| Bundle | Gzipped | Notes | -| --- | --- | --- | -| `@cleverbrush/schema` (full) | **14 KB** | All builders + built-in extensions | -| `@cleverbrush/schema/string` | **3.8 KB** | Sub-path import, one builder only | -| `@cleverbrush/schema/object` | **5.8 KB** | Sub-path import, one builder only | -| Zod v3 (full) | 14.4 KB | For reference | -| Zod v4 (full) | **41 KB** | **3× larger than @cleverbrush/schema** | - -Sub-path exports (`@cleverbrush/schema/string`, `/number`, `/object`, `/array`, `/core`) enable fine-grained tree-shaking for bundle-critical applications. - -### Competitive feature comparison - -| | @cleverbrush/schema | Zod | Yup | Joi | -| --- | --- | --- | --- | --- | -| TypeScript type inference | ✓ | ✓ | ~ | ✗ | -| [Standard Schema v1](https://standardschema.dev/) | ✓ | ✓ | ✗ | ✗ | -| **PropertyDescriptors** (runtime introspection) | ✓ | ✗ | ✗ | ✗ | -| **Type-safe extension system** | ✓ | ✗ | ✗ | ✗ | -| **Built-in object mapper** | ✓ | ✗ | ✗ | ✗ | -| **Built-in form generation** | ✓ | ✗ | ✗ | ✗ | -| Bidirectional JSON Schema | ✓ | ~ (output only) | ✗ | ✗ | -| **External schema interop** (`extern()`) | ✓ | ✗ | ✗ | ✗ | -| JSDoc comment preservation | ✓ | ✗ | ✗ | ✗ | -| Immutable fluent API | ✓ | ✓ | ✗ | ✗ | -| Zero runtime dependencies | ✓ | ✓ | ✗ | ✗ | -| Bundle size (full, gzipped) | **14 KB** | 41 KB (v4) | ~19 KB | ~26 KB | - -**PropertyDescriptors** are the architectural differentiator. Every schema emits a structured descriptor tree at runtime — not just a black-box validator function. This is what enables the mapper to provide type-safe property selectors, react-form to auto-generate fields, and schema-json to produce accurate JSON Schema output. No other popular validation library exposes this level of runtime metadata. - ---- - -## Code Quality - -Every pull request must pass all of the following gates before merging — enforced by the CI pipeline: - -| Gate | Tool | What it checks | -| --- | --- | --- | -| **Linting** | [Biome](https://biomejs.dev/) | Code style, formatting, and static analysis across all packages and the website | -| **Type checking** | TypeScript (strict mode) | Strict null-checks, no implicit `any`, full type coverage | -| **Unit tests** | [Vitest](https://vitest.dev/) | Runtime behaviour + type-level tests (`expectTypeOf`) — coverage spans all builders, extensions, edge cases, and error paths | -| **Build** | [tsup](https://tsup.egoist.dev/) + [Turbo](https://turbo.build/) | ESM output compiles cleanly with no TypeScript errors | -| **Benchmarks** | Vitest bench | Performance regressions are visible before merge | - -Run everything locally before opening a PR: - -```bash -npm run lint # Biome static analysis -npm run build # compile all packages -npm test # unit tests + type checks -npm run bench # performance benchmarks -``` - ---- +The repository uses npm workspaces, Turborepo, TypeScript, Biome, Vitest, and +ES modules. ## Development -This project uses [npm workspaces](https://docs.npmjs.com/cli/using-npm/workspaces) and [Turborepo](https://turbo.build/) for incremental builds. All library source is under `libs/`. - -### Setup - -```bash -npm install -``` - -### Build +Use Node.js 20 or newer. Node.js 22 is recommended. ```bash +npm ci +npm run lint npm run build +npm run test ``` -### Test +Useful targeted commands: ```bash -npm test +npx vitest --run libs/schema +npm run typecheck:schema-site +npm run typecheck:docs-site +npm run build:schema-site +npm run build:docs-site ``` -### Documentation - -API docs are generated by [TypeDoc](https://typedoc.org/) and published at https://docs.cleverbrush.com/. +The demo app can be started with: ```bash -npm run docs +npm run dev:demo ``` -Each library has its own `README.md` with usage examples and full API reference. - ---- - -## Release - -This project uses [Changesets](https://github.com/changesets/changesets) for versioning and publishing. All packages are versioned together. - -1. **Add a changeset** after making changes: +This starts the todo backend, frontend, and local database stack used by the +demo workflow. - ```bash - npm run changeset - ``` +## Documentation - Follow the prompts to describe the change. A changeset file is created in `.changeset/`. +- Framework docs: https://docs.cleverbrush.com +- Schema docs and playground: https://schema.cleverbrush.com +- Standard Schema: https://standardschema.dev -2. **Version packages** when ready to release: +Each package also has local source, tests, and exports under `libs/`. - ```bash - npm run version - ``` +## Quality Gates - This bumps `package.json` versions and updates `CHANGELOG.md` files. +Every change should leave these commands passing: -3. **Publish** to npm: - - ```bash - npm run release - ``` - - For a beta release: - - ```bash - npm run publish:beta - ``` +```bash +npm run lint +npm run build +npm run test +``` ---- +Website changes should also run the relevant site typecheck or build command. +Published package behavior changes require a changeset. -## Contributing +## Release -Contributions are welcome. See [CONTRIBUTING.md](./CONTRIBUTING.md) for full guidelines. +This repository uses Changesets and fixed-version package releases. -The short version: make sure your changes include tests, pass linting (`npm run lint`), and don't break existing tests (`npm test`). If you add or change behaviour, update the relevant JSDoc comments — that's all the documentation update that's usually needed. +```bash +npm run changeset +npm run version +npm run release +``` -Extensions are the easiest place to start contributing — each one is a self-contained file with tests. Look for issues labelled **good first issue**. +For beta releases: ---- +```bash +npm run publish:beta +``` ## License -[BSD-3-Clause](./LICENSE) +BSD-3-Clause. See [LICENSE](./LICENSE). diff --git a/package-lock.json b/package-lock.json index 5918a68a..5382d67f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "cleverbrush-framework", - "version": "3.1.0", + "version": "4.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "cleverbrush-framework", - "version": "3.1.0", + "version": "4.2.0", "license": "BSD 3-Clause", "workspaces": [ "./libs/*", @@ -149,7 +149,7 @@ }, "libs/async": { "name": "@cleverbrush/async", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "devDependencies": { "@types/node": "^25.4.0" @@ -167,10 +167,10 @@ }, "libs/auth": { "name": "@cleverbrush/auth", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "dependencies": { - "@cleverbrush/schema": "^4.0.0" + "@cleverbrush/schema": "^4.3.2" } }, "libs/benchmarks": { @@ -185,11 +185,11 @@ }, "libs/client": { "name": "@cleverbrush/client", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "dependencies": { - "@cleverbrush/schema": "^4.0.0", - "@cleverbrush/server": "^4.0.0" + "@cleverbrush/schema": "^4.3.2", + "@cleverbrush/server": "^4.3.2" }, "devDependencies": { "@tanstack/react-query": "^5.75.0", @@ -213,23 +213,23 @@ }, "libs/deep": { "name": "@cleverbrush/deep", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause" }, "libs/di": { "name": "@cleverbrush/di", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "dependencies": { - "@cleverbrush/schema": "^4.0.0" + "@cleverbrush/schema": "^4.3.2" } }, "libs/env": { "name": "@cleverbrush/env", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "dependencies": { - "@cleverbrush/deep": "^4.0.0" + "@cleverbrush/deep": "^4.3.2" }, "devDependencies": { "@types/node": "^25.4.0" @@ -250,11 +250,11 @@ }, "libs/knex-clickhouse": { "name": "@cleverbrush/knex-clickhouse", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "dependencies": { - "@cleverbrush/async": "^4.0.0", - "@cleverbrush/deep": "^4.0.0", + "@cleverbrush/async": "^4.3.2", + "@cleverbrush/deep": "^4.3.2", "@clickhouse/client": "^1.18.2" }, "peerDependencies": { @@ -263,10 +263,10 @@ }, "libs/knex-schema": { "name": "@cleverbrush/knex-schema", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "dependencies": { - "@cleverbrush/schema": "^4.0.0" + "@cleverbrush/schema": "^4.3.2" }, "peerDependencies": { "knex": ">=3.1.0" @@ -274,11 +274,11 @@ }, "libs/log": { "name": "@cleverbrush/log", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "dependencies": { - "@cleverbrush/async": "^4.0.0", - "@cleverbrush/schema": "^4.0.0" + "@cleverbrush/async": "^4.3.2", + "@cleverbrush/schema": "^4.3.2" }, "devDependencies": { "@types/node": "^25.4.0" @@ -312,19 +312,19 @@ }, "libs/mapper": { "name": "@cleverbrush/mapper", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "dependencies": { - "@cleverbrush/schema": "^4.0.0" + "@cleverbrush/schema": "^4.3.2" } }, "libs/orm": { "name": "@cleverbrush/orm", - "version": "1.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "dependencies": { - "@cleverbrush/knex-schema": "^4.0.0", - "@cleverbrush/schema": "^4.0.0" + "@cleverbrush/knex-schema": "^4.3.2", + "@cleverbrush/schema": "^4.3.2" }, "peerDependencies": { "knex": ">=3.1.0" @@ -332,7 +332,7 @@ }, "libs/orm-cli": { "name": "@cleverbrush/orm-cli", - "version": "1.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "dependencies": { "@cleverbrush/knex-schema": "*", @@ -347,7 +347,7 @@ }, "libs/otel": { "name": "@cleverbrush/otel", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "dependencies": { "@opentelemetry/api": "^1.9.0", @@ -419,10 +419,10 @@ }, "libs/react-form": { "name": "@cleverbrush/react-form", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "dependencies": { - "@cleverbrush/schema": "^4.0.0" + "@cleverbrush/schema": "^4.3.2" }, "devDependencies": { "@types/react": "^19.0.0", @@ -434,10 +434,10 @@ }, "libs/scheduler": { "name": "@cleverbrush/scheduler", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "dependencies": { - "@cleverbrush/schema": "^4.0.0" + "@cleverbrush/schema": "^4.3.2" }, "devDependencies": { "@types/node": "^25.4.0" @@ -455,10 +455,10 @@ }, "libs/schema": { "name": "@cleverbrush/schema", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "devDependencies": { - "@cleverbrush/deep": "^4.0.0" + "@cleverbrush/deep": "^4.3.2" }, "peerDependencies": { "@standard-schema/spec": "^1.1.0" @@ -466,7 +466,7 @@ }, "libs/schema-json": { "name": "@cleverbrush/schema-json", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "peerDependencies": { "@cleverbrush/schema": "^4.0.0", @@ -475,12 +475,12 @@ }, "libs/server": { "name": "@cleverbrush/server", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "dependencies": { - "@cleverbrush/auth": "^4.0.0", - "@cleverbrush/di": "^4.0.0", - "@cleverbrush/schema": "^4.0.0", + "@cleverbrush/auth": "^4.3.2", + "@cleverbrush/di": "^4.3.2", + "@cleverbrush/schema": "^4.3.2", "@fastify/busboy": "^3.2.0", "ws": "^8.20.0" }, @@ -501,7 +501,7 @@ }, "libs/server-openapi": { "name": "@cleverbrush/server-openapi", - "version": "4.0.0", + "version": "4.3.2", "license": "BSD 3-Clause", "peerDependencies": { "@cleverbrush/auth": "^4.0.0", @@ -8434,6 +8434,16 @@ "dev": true, "license": "MIT" }, + "node_modules/performative-ui": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/performative-ui/-/performative-ui-0.3.0.tgz", + "integrity": "sha512-hUY2Jk4D2iu143Kmd8G/wdY4B1sQC/1RBYDwaJr9/BhayW4404icSU5zJc2cTDNaKpgLmvCNvYfbWuIO50U40A==", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + } + }, "node_modules/pg": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", @@ -11129,6 +11139,7 @@ "dependencies": { "@cleverbrush/website-shared": "file:../shared", "next": "^16.2.1", + "performative-ui": "^0.3.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, @@ -11165,6 +11176,7 @@ "@t3-oss/env-nextjs": "^0.13.11", "@tanstack/react-form": "^1.28.6", "next": "^16.2.1", + "performative-ui": "^0.3.0", "react": "^19.2.4", "react-dom": "^19.2.4" }, @@ -11197,6 +11209,7 @@ "version": "0.0.0", "dependencies": { "next": "^16.2.1", + "performative-ui": "^0.3.0", "react": "^19.1.0" } } diff --git a/websites/docs/app/comparisons/page.tsx b/websites/docs/app/comparisons/page.tsx index 68f6017f..efcea117 100644 --- a/websites/docs/app/comparisons/page.tsx +++ b/websites/docs/app/comparisons/page.tsx @@ -1,5 +1,6 @@ /** biome-ignore-all lint/security/noDangerouslySetInnerHtml: needed for code examples */ +import { PerformativeBeforeAfter } from '@cleverbrush/website-shared/components/Performative'; import { highlightTS } from '@cleverbrush/website-shared/lib/highlight'; import { docsMetadata } from '../site'; @@ -18,6 +19,20 @@ export default function ComparisonsPage() {

+ + {/* ── Overview Table ──────────────────────────────── */}

Feature matrix

diff --git a/websites/docs/app/demo/page.tsx b/websites/docs/app/demo/page.tsx index c49ae37a..67654009 100644 --- a/websites/docs/app/demo/page.tsx +++ b/websites/docs/app/demo/page.tsx @@ -1,3 +1,4 @@ +import { PerformativeMetricStrip } from '@cleverbrush/website-shared/components/Performative'; import Link from 'next/link'; import { docsMetadata } from '../site'; @@ -20,6 +21,15 @@ export default function DemoPage() {

+ + {/* ── Docker Compose setup ─────────────────────────── */}

Running the demo

diff --git a/websites/docs/app/examples/page.tsx b/websites/docs/app/examples/page.tsx index 09a1b6ac..d6307c42 100644 --- a/websites/docs/app/examples/page.tsx +++ b/websites/docs/app/examples/page.tsx @@ -1,5 +1,6 @@ /** biome-ignore-all lint/security/noDangerouslySetInnerHtml: needed for code examples */ +import { PerformativeGlassGrid } from '@cleverbrush/website-shared/components/Performative'; import { highlightTS } from '@cleverbrush/website-shared/lib/highlight'; import { docsMetadata } from '../site'; @@ -18,6 +19,32 @@ export default function ExamplesPage() {

+ + {/* ── Overview ────────────────────────────────────── */}

Todo app

diff --git a/websites/docs/app/getting-started/page.tsx b/websites/docs/app/getting-started/page.tsx index 510b2c52..ac28e3a6 100644 --- a/websites/docs/app/getting-started/page.tsx +++ b/websites/docs/app/getting-started/page.tsx @@ -1,6 +1,7 @@ /** biome-ignore-all lint/security/noDangerouslySetInnerHtml: needed for code examples */ import { InstallBanner } from '@cleverbrush/website-shared/components/InstallBanner'; +import { PerformativeCodeStage } from '@cleverbrush/website-shared/components/Performative'; import { highlightTS } from '@cleverbrush/website-shared/lib/highlight'; import Link from 'next/link'; import { docsMetadata } from '../site'; @@ -19,6 +20,25 @@ export default function GettingStartedPage() {

+ + {/* ── Step 0: Install ─────────────────────────────── */} {/* ── Hero ─────────────────────────────────────────────── */} -
-

- Schema-first full-stack TypeScript framework -

-

- One schema. -
- Full stack. -

-

- Define your data shapes once with{' '} - - @cleverbrush/schema - - . Get type-safe servers, auto-typed clients, OpenAPI docs, - dependency injection, auth, and React forms — all from that - single definition. Zero duplication. Zero drift. -

-
- - Get Started - - - - Why Cleverbrush? - - - - GitHub - -
-
- Contract-first - Zero codegen - OpenAPI 3.1 - Built-in auth & DI - Client resilience - React integration + @cleverbrush/schema + + . Get type-safe servers, auto-typed clients, OpenAPI + docs, dependency injection, auth, and React forms from + that single definition. + + } + actions={[ + { + href: '/getting-started', + label: 'Get started', + variant: 'glow' + }, + { + href: '/why', + label: 'Why Cleverbrush?', + variant: 'wave' + }, + { + href: 'https://github.com/cleverbrush/framework', + label: 'GitHub', + external: true, + variant: 'ghost' + } + ]} + metrics={[ + { target: 18, label: 'workspace packages' }, + { value: '0', label: 'client codegen steps' }, + { value: '3.1', label: 'OpenAPI target' }, + { value: '1', label: 'shared schema contract' } + ]} + badges={[ + 'Contract-first REST', + 'Built-in auth and DI', + 'Typed resilient client', + 'Schema-driven React forms' + ]} + code={{ + filename: 'contract.ts', + code: `import { defineApi, endpoint } from '@cleverbrush/server/contract'; +import { object, string, array } from '@cleverbrush/schema'; + +const User = object({ + id: string().uuid(), + email: string().email() +}); + +export const api = defineApi({ + users: { + list: endpoint.get('/api/users').returns(array(User)), + create: endpoint.post('/api/users').body(User).returns(User) + } +});` + }} + /> +
+
+
diff --git a/websites/docs/app/why/page.tsx b/websites/docs/app/why/page.tsx index 1eda81bf..8782a76d 100644 --- a/websites/docs/app/why/page.tsx +++ b/websites/docs/app/why/page.tsx @@ -1,5 +1,6 @@ /** biome-ignore-all lint/security/noDangerouslySetInnerHtml: needed for code examples */ +import { PerformativeBeforeAfter } from '@cleverbrush/website-shared/components/Performative'; import { highlightTS } from '@cleverbrush/website-shared/lib/highlight'; import Link from 'next/link'; import { docsMetadata } from '../site'; @@ -18,6 +19,20 @@ export default function WhyPage() {

+ + {/* ── The Problem ─────────────────────────────────── */}

The problem: types everywhere, in sync nowhere

diff --git a/websites/docs/package.json b/websites/docs/package.json index ab648246..d9f05403 100644 --- a/websites/docs/package.json +++ b/websites/docs/package.json @@ -10,6 +10,7 @@ "dependencies": { "@cleverbrush/website-shared": "file:../shared", "next": "^16.2.1", + "performative-ui": "^0.3.0", "react": "^19.1.0", "react-dom": "^19.1.0" }, diff --git a/websites/schema/app/layout.tsx b/websites/schema/app/layout.tsx index b7b32fa7..41cb74e5 100644 --- a/websites/schema/app/layout.tsx +++ b/websites/schema/app/layout.tsx @@ -1,4 +1,5 @@ import type { Metadata } from 'next'; +import 'performative-ui/styles.css'; import '@cleverbrush/website-shared/styles/globals.css'; import { ConsentManager } from '@cleverbrush/website-shared/components/ConsentManager'; import type { FooterSection } from '@cleverbrush/website-shared/components/Footer'; diff --git a/websites/schema/app/mapper/page.tsx b/websites/schema/app/mapper/page.tsx index 98b31038..3637b31f 100644 --- a/websites/schema/app/mapper/page.tsx +++ b/websites/schema/app/mapper/page.tsx @@ -1,4 +1,5 @@ import { InstallBanner } from '@cleverbrush/website-shared/components/InstallBanner'; +import { PerformativeBeforeAfter } from '@cleverbrush/website-shared/components/Performative'; import { highlightTS } from '@cleverbrush/website-shared/lib/highlight'; import { schemaMetadata } from '../site'; @@ -16,6 +17,20 @@ export default function MapperPage() {

+ + {/* ── Installation ─────────────────────────────────── */} void; } +const monacoVsPath = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs'; + +let monacoPrepared = false; + export function PlaygroundEditor({ code, onChange, onMount }: Props) { const editorRef = useRef(null); const monacoRef = useRef(null); - const initDone = useRef(false); + const [loadState, setLoadState] = useState<'loading' | 'ready' | 'error'>( + 'loading' + ); + const [loadError, setLoadError] = useState(null); + + useEffect(() => { + let cancelled = false; + + import('@monaco-editor/react') + .then(({ loader }) => { + loader.config({ + paths: { + vs: monacoVsPath + } + }); + return loader.init(); + }) + .then(monaco => { + if (cancelled) return; + prepareMonaco(monaco as MonacoInstance); + setLoadState('ready'); + }) + .catch(error => { + const message = describeMonacoError(error); + console.error('Monaco initialization error:', message); + if (cancelled) return; + setLoadError(message); + setLoadState('error'); + }); + + return () => { + cancelled = true; + }; + }, []); const handleBeforeMount = useCallback((monaco: unknown) => { - defineTheme(monaco as MonacoInstance); + prepareMonaco(monaco as MonacoInstance); }, []); const handleMount = useCallback( (editor: unknown, monaco: unknown) => { editorRef.current = editor; monacoRef.current = monaco; - - if (!initDone.current) { - initDone.current = true; - configureMonaco(monaco as MonacoInstance); - } + prepareMonaco(monaco as MonacoInstance); onMount?.(editor, monaco); }, @@ -47,6 +80,24 @@ export function PlaygroundEditor({ code, onChange, onMount }: Props) { [onChange] ); + if (loadState === 'error') { + return ( +
+
+ Editor failed to load: {loadError ?? 'unknown error'} +
+
+ ); + } + + if (loadState === 'loading') { + return ( +
+
Loading editor...
+
+ ); + } + return (
+ + {/* ── Installation ─────────────────────────────────── */} -

- The cornerstone of type-safe TypeScript -

-

- One schema. -
- Types, validation, forms. -

-

- @cleverbrush/schema is an immutable, composable - schema library that infers your TypeScript types at compile time - and validates your data at runtime — with zero dependencies. It - lays the foundation for a rich ecosystem — much like{' '} - - Zod - {' '} - has shown is possible. -

-
- - Explore the Schema Library - - - - - Try in Playground - - - - GitHub - -
-
- Zero runtime dependencies - Compile-time type inference - Immutable & composable - BSD-3 Licensed - - {smallGzip} min (full {fullGzip}) gzipped - - Standard Schema compatible - 98% test coverage - Faster than Zod in most tests -
-
+ <> + + @cleverbrush/schema infers TypeScript types + at compile time, validates untrusted data at runtime, + and exposes typed descriptors that power forms, mappers, + OpenAPI, and your own tooling. + + } + actions={[ + { + href: '/docs', + label: 'Explore the schema library', + variant: 'glow' + }, + { + href: '/playground', + label: 'Try the playground', + variant: 'wave' + }, + { + href: 'https://github.com/cleverbrush/framework', + label: 'GitHub', + external: true, + variant: 'ghost' + } + ]} + metrics={[ + { value: smallGzip, label: 'minimal gzipped build' }, + { value: fullGzip, label: 'full gzipped build' }, + { value: '0', label: 'runtime dependencies' }, + { target: 98, suffix: '%', label: 'line coverage' } + ]} + badges={[ + 'Immutable builders', + 'Standard Schema compatible', + 'Typed field selectors', + 'BSD-3 licensed' + ]} + code={{ + filename: 'schema.ts', + code: `import { object, string, number } from '@cleverbrush/schema'; + +const User = object({ + name: string().minLength(2), + email: string().email(), + age: number().min(0).max(150) +}); + +const result = User.validate(input); + +if (!result.valid) { + result.getErrorsFor(u => u.email); +}` + }} + /> +
+
+ + +
+
+ ); } diff --git a/websites/schema/app/showcases/page.tsx b/websites/schema/app/showcases/page.tsx index 4eed8306..17204158 100644 --- a/websites/schema/app/showcases/page.tsx +++ b/websites/schema/app/showcases/page.tsx @@ -1,9 +1,25 @@ -import Link from 'next/link'; +import { PerformativeGlassGrid } from '@cleverbrush/website-shared/components/Performative'; import { schemaMetadata } from '../site'; export const metadata = schemaMetadata('/showcases'); -const SHOWCASES = [ +interface Showcase { + href: string; + title: string; + description: string; + badge: string; + external?: boolean; +} + +const SHOWCASES: Showcase[] = [ + { + href: 'https://xpenser.cleverbrush.com', + title: 'xpenser', + description: + 'Self-hostable personal finance tracker and real Cleverbrush reference app using schemas, contracts, server handlers, typed clients, React forms, OpenAPI, observability, Telegram, and MCP.', + badge: 'App', + external: true + }, { href: '/showcases/tanstack-form', title: 'TanStack Form', @@ -41,48 +57,16 @@ export default function ShowcasesPage() {

-
- {SHOWCASES.map(s => ( - -
-

{s.title}

- - {s.badge} - -
-

- {s.description} -

- - ))} -
+ ({ + title: s.title, + body: s.description, + icon: s.badge, + href: s.href, + external: s.external, + linkLabel: 'Open showcase' + }))} + /> ); diff --git a/websites/schema/app/showcases/t3-env/page.tsx b/websites/schema/app/showcases/t3-env/page.tsx index 7e37c1af..72893537 100644 --- a/websites/schema/app/showcases/t3-env/page.tsx +++ b/websites/schema/app/showcases/t3-env/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { boolean, number, string } from '@cleverbrush/schema'; +import { PerformativeBeforeAfter } from '@cleverbrush/website-shared/components/Performative'; import { highlightTS } from '@cleverbrush/website-shared/lib/highlight'; import { useState } from 'react'; @@ -241,6 +242,20 @@ export default function T3EnvPage() {

+ + {/* ── How it works ─────────────────────────── */}

How it works

diff --git a/websites/schema/app/showcases/tanstack-form/page.tsx b/websites/schema/app/showcases/tanstack-form/page.tsx index 205e5677..0b385e8f 100644 --- a/websites/schema/app/showcases/tanstack-form/page.tsx +++ b/websites/schema/app/showcases/tanstack-form/page.tsx @@ -1,6 +1,7 @@ 'use client'; import { boolean, number, string } from '@cleverbrush/schema'; +import { PerformativeBeforeAfter } from '@cleverbrush/website-shared/components/Performative'; import { highlightTS } from '@cleverbrush/website-shared/lib/highlight'; import { useForm } from '@tanstack/react-form'; import { useState } from 'react'; @@ -381,6 +382,20 @@ export default function TanStackFormPage() {

+ + {/* ── How it works ─────────────────────────────── */}

How it works

diff --git a/websites/schema/next.config.ts b/websites/schema/next.config.ts index e72f37ee..5eb2eae8 100644 --- a/websites/schema/next.config.ts +++ b/websites/schema/next.config.ts @@ -4,11 +4,11 @@ const isDev = process.env.NODE_ENV === 'development'; const cspHeader = [ "default-src 'self'", - `script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ''} https://www.googletagmanager.com https://www.google-analytics.com`, - "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", + `script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ''} https://www.googletagmanager.com https://www.google-analytics.com https://cdn.jsdelivr.net`, + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com https://cdn.jsdelivr.net", "img-src 'self' data: blob:", - "font-src 'self' https://fonts.gstatic.com data:", - "connect-src 'self' https://www.google-analytics.com", + "font-src 'self' https://fonts.gstatic.com https://cdn.jsdelivr.net data:", + "connect-src 'self' https://www.google-analytics.com https://cdn.jsdelivr.net", 'frame-src https://www.googletagmanager.com', "worker-src 'self' blob:", "object-src 'none'", diff --git a/websites/schema/package.json b/websites/schema/package.json index 9c5d4312..691964b6 100644 --- a/websites/schema/package.json +++ b/websites/schema/package.json @@ -19,6 +19,7 @@ "@t3-oss/env-nextjs": "^0.13.11", "@tanstack/react-form": "^1.28.6", "next": "^16.2.1", + "performative-ui": "^0.3.0", "react": "^19.2.4", "react-dom": "^19.2.4" }, diff --git a/websites/shared/components/Performative.tsx b/websites/shared/components/Performative.tsx new file mode 100644 index 00000000..175da04e --- /dev/null +++ b/websites/shared/components/Performative.tsx @@ -0,0 +1,611 @@ +'use client'; + +import Link from 'next/link'; +import { useEffect, useMemo, useState } from 'react'; +import type { ReactNode } from 'react'; +import { + AsciiHero, + BeforeAfter, + Button, + CommunityBadge, + EyebrowPill, + FloatingSparkles, + GlassCard, + GradientText, + LogoMarquee, + MockIDE, + NodeGraphBackground, + StatCounter, + WordRoll +} from 'performative-ui'; +import type { ButtonVariant, IdeToken } from 'performative-ui'; + +export interface PerformativeAction { + href: string; + label: string; + external?: boolean; + variant?: ButtonVariant; +} + +export interface PerformativeMetric { + label: string; + value?: string; + target?: number; + prefix?: string; + suffix?: string; + decimals?: number; +} + +export interface PerformativeCodeSample { + filename: string; + code: string; + thinkingLabel?: ReactNode | false; +} + +export interface PerformativeHeroProps { + eyebrow: string; + headline: string; + rotatingWords: string[]; + body: ReactNode; + actions: PerformativeAction[]; + metrics?: PerformativeMetric[]; + badges?: string[]; + code?: PerformativeCodeSample; + wordDirection?: 'up' | 'down'; +} + +export interface PerformativeFeature { + title: string; + body: ReactNode; + icon?: ReactNode; + href?: string; + external?: boolean; + linkLabel?: string; +} + +export interface PerformativeProofItem { + title: ReactNode; + subtitle: ReactNode; + href: string; + icon?: ReactNode; +} + +export interface PerformativeMarqueeItem { + label: string; + tone?: 'serif' | 'mono' | 'strong'; +} + +const keywordPattern = + /\b(import|from|const|type|export|return|await|async|if|else|new)\b/g; + +const HERO_CODE_CHAR_MS: [number, number] = [1, 8]; +const CODE_STAGE_CHAR_MS: [number, number] = [1, 6]; + +function tokenizeLine(line: string): IdeToken[] { + const tokens: IdeToken[] = []; + let cursor = 0; + const matches = Array.from(line.matchAll(keywordPattern)); + + for (const match of matches) { + const index = match.index ?? 0; + if (index > cursor) { + tokens.push(...tokenizePlain(line.slice(cursor, index))); + } + tokens.push({ c: match[0], cls: 'key' }); + cursor = index + match[0].length; + } + + if (cursor < line.length) { + tokens.push(...tokenizePlain(line.slice(cursor))); + } + + tokens.push({ c: '\n' }); + return tokens; +} + +function tokenizePlain(source: string): IdeToken[] { + const tokens: IdeToken[] = []; + const pattern = + /('[^']*'|"[^"]*"|`[^`]*`|\b\d+(?:\.\d+)?\b|\/\/.*$)/g; + let cursor = 0; + const matches = Array.from(source.matchAll(pattern)); + + for (const match of matches) { + const index = match.index ?? 0; + if (index > cursor) { + tokens.push({ c: source.slice(cursor, index) }); + } + const value = match[0]; + const cls = value.startsWith('//') + ? 'com' + : /^\d/.test(value) + ? 'num' + : 'str'; + tokens.push({ c: value, cls }); + cursor = index + value.length; + } + + if (cursor < source.length) { + tokens.push({ c: source.slice(cursor) }); + } + + return tokens; +} + +function tokenizeCode(code: string): IdeToken[] { + return code.split('\n').flatMap(tokenizeLine); +} + +function useMounted() { + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setMounted(true); + }, []); + + return mounted; +} + +function ClientOnlySparkles() { + const mounted = useMounted(); + + if (!mounted) { + return