Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ component documentation, and published React packages.
- `packages/json-schema-to-blocks` — the `json-schema-to-blocks` lowering of JSON Schema into those documents.
- `packages/meta-to-blocks` — the `meta-to-blocks` lowering of database metadata into generated form, list, and detail documents.
- `packages/flow-to-blocks` — the `flow-to-blocks` evaluation of a flow graph into a computed document.
- `packages/blocks-ui` — the `@constructive-io/blocks-ui` default registry: Constructive document node types wired to `@constructive-io/ui` components.

The documentation site is published at
<https://constructive-io.github.io/blocks/>. Registry JSON is served from
Expand Down
4 changes: 4 additions & 0 deletions apps/blocks/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@ai-sdk/openai-compatible": "^3.0.9",
"@ai-sdk/react": "^4.0.27",
"@base-ui/react": "^1.0.0",
"@constructive-io/blocks-ui": "workspace:*",
"@constructive-io/command-palette": "workspace:*",
"@constructive-io/data": "workspace:*",
"@constructive-io/graphql-types": "^3.4.3",
Expand All @@ -42,10 +43,13 @@
"@tanstack/react-form": "^1.27.7",
"@tanstack/react-query": "^5.90.16",
"ai": "^7.0.26",
"blocks-renderer": "workspace:*",
"blocks-schema": "workspace:*",
"clsx": "^2.1.1",
"dompurify": "^3.3.1",
"gql-ast": "^3.3.3",
"graphql": "16.13.0",
"json-schema-to-blocks": "workspace:*",
"lucide-react": "^0.525.0",
"marked": "^16.4.2",
"motion": "^12.40.0",
Expand Down
109 changes: 109 additions & 0 deletions apps/blocks/src/app/blocks/documents/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import type { Metadata } from 'next';

import { CodeBlock } from '@/components/docs/code-block';
import { DocSection } from '@/components/docs/doc-section';
import { DocumentFormDemo } from '@/components/documents-showcase/document-form-demo';
import { OG_IMAGE, withBase } from '@/lib/site';

const TITLE = 'JSON documents';
const DESCRIPTION =
'Render a declarative JSON UI document with the default widget registry: JSON Schema, database metadata, or an agent tool produces the document, and no page hand-writes the form.';

const INSTALL = `pnpm add blocks-schema blocks-renderer json-schema-to-blocks @constructive-io/blocks-ui`;

const USAGE = `'use client';

import { DocumentRenderer } from 'blocks-renderer';
import { defaultBlockRegistry } from '@constructive-io/blocks-ui';
import { schemaToDocument } from 'json-schema-to-blocks';

const document = schemaToDocument({
type: 'object',
required: ['title'],
properties: {
title: { type: 'string', maxLength: 120 },
status: { type: 'string', enum: ['draft', 'review', 'published'] },
featured: { type: 'boolean' }
}
});

export function PostForm() {
return (
<DocumentRenderer
document={document}
registry={defaultBlockRegistry}
onSubmit={(values) => save(values)}
/>
);
}`;

const OVERRIDE = `import { composeRegistry } from 'blocks-renderer';
import { defaultBlockRegistry } from '@constructive-io/blocks-ui';

// Layer over the default registry one node type at a time.
const registry = composeRegistry(defaultBlockRegistry, {
Select: MyCombobox,
DataTable: MyDataTable
});`;

export const metadata: Metadata = {
title: TITLE,
description: DESCRIPTION,
alternates: { canonical: withBase('/blocks/documents') },
openGraph: {
title: TITLE,
description: DESCRIPTION,
url: withBase('/blocks/documents'),
images: [OG_IMAGE],
},
};

export default function DocumentsPage() {
return (
<div className="registry-page">
<header className="mb-8 max-w-2xl">
<p className="registry-eyebrow">Documents</p>
<h1 className="mt-2 text-[22px] font-semibold tracking-tight sm:text-[1.75rem]">
{TITLE}
</h1>
<p className="mt-2 text-pretty text-sm leading-7 text-muted-foreground sm:text-[15px]">
{DESCRIPTION}
</p>
</header>

<DocSection id="installation" title="Installation">
<CodeBlock label="terminal">
{INSTALL}
</CodeBlock>
</DocSection>

<DocSection
description="A document is data: an envelope plus a node tree. The renderer walks the tree and asks a registry which component renders each node type, so the same document works in an admin screen, a human-in-the-loop task, or an agent-generated page."
id="usage"
title="Basic usage"
>
<CodeBlock label="post-form.tsx" language="tsx">
{USAGE}
</CodeBlock>
</DocSection>

<DocSection
description="The form below is generated from the JSON Schema on the left of the source: labels, widget selection, constraints, and validation all come from the document."
id="examples"
title="Live example"
>
<DocumentFormDemo />
</DocSection>

<DocSection
description="The registry is a plain node type to component map, so replace any subset without forking it. Data-bound nodes such as DataTable and AgentChat are deliberately unregistered: they need a query runtime, so the host supplies them."
id="composition"
title="Replacing components"
>
<CodeBlock label="registry.ts" language="tsx">
{OVERRIDE}
</CodeBlock>
</DocSection>
</div>
);
}
6 changes: 6 additions & 0 deletions apps/blocks/src/app/blocks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ const APPLICATION_CATALOG = [
description:
'A full-page console driven by injected endpoints, session state, adapters, and versioned _meta.',
},
{
href: '/blocks/documents',
title: 'JSON documents',
description:
'Declarative JSON UI documents rendered by the default widget registry, generated from JSON Schema or database metadata.',
},
{
href: '/blocks/ai',
title: 'AI',
Expand Down
1 change: 1 addition & 0 deletions apps/blocks/src/app/llms.txt/route.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ describe('llms.txt', () => {
'/blocks/billing/',
'/blocks/features/',
'/blocks/console-kit/',
'/blocks/documents/',
]) {
expect(source).toContain(
`https://constructive-io.github.io/blocks${route}`,
Expand Down
6 changes: 6 additions & 0 deletions apps/blocks/src/app/llms.txt/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ const surfaceLinks: readonly SurfaceLink[] = [
description:
'Composable tenant-console roots, modules, presets, endpoint contracts, and integration diagnostics.',
},
{
title: 'JSON documents',
path: '/blocks/documents/',
description:
'Declarative JSON UI documents plus the default widget registry that renders them, generated from JSON Schema or database metadata.',
},
{
title: 'Primitives and bundles',
path: '/blocks/',
Expand Down
2 changes: 2 additions & 0 deletions apps/blocks/src/app/sitemap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
...SOURCE_BLOCKS.map(({ name }) => `/blocks/${name}`),
...APPLICATION_BLOCKS.map(({ name }) => `/blocks/${name}`),
'/blocks/console-kit',
'/blocks/documents',
...BASE_PRIMITIVES.map(({ name }) => `/blocks/ui/${name}`),
'/blocks/billing',
...BILLING_BLOCKS.map(({ name }) => `/blocks/billing/${name}`),
Expand All @@ -42,6 +43,7 @@ export default function sitemap(): MetadataRoute.Sitemap {
path === '/blocks/ai' ||
SOURCE_BLOCKS.some(({ name }) => path === `/blocks/${name}`) ||
path === '/blocks/console-kit' ||
path === '/blocks/documents' ||
path === '/blocks/billing'
? 0.9
: 0.7,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use client';

import { defaultBlockRegistry } from '@constructive-io/blocks-ui';
import { Card, CardContent } from '@constructive-io/ui';
import { DocumentRenderer } from 'blocks-renderer';
import type { JSONSchema } from 'json-schema-to-blocks';
import { schemaToDocument } from 'json-schema-to-blocks';
import { useMemo, useState } from 'react';

/**
* The JSON Schema is the only input: `json-schema-to-blocks` lowers it to a
* document and the default registry renders it, so this page hand-writes no UI
* for any of the fields below.
*/
const POST_SCHEMA: JSONSchema = {
$id: 'post',
type: 'object',
title: 'Publish a post',
required: ['title', 'status'],
properties: {
title: { type: 'string', title: 'Title', maxLength: 120 },
slug: { type: 'string', title: 'Slug', pattern: '^[a-z0-9-]+$' },
summary: { type: 'string', title: 'Summary', maxLength: 400 },
status: {
type: 'string',
title: 'Status',
enum: ['draft', 'in_review', 'published', 'archived'],
},
reading_time: {
type: 'integer',
title: 'Reading time (minutes)',
minimum: 1,
maximum: 120,
},
publish_at: { type: 'string', title: 'Publish at', format: 'date-time' },
featured: { type: 'boolean', title: 'Featured' },
},
};

export function DocumentFormDemo() {
const document = useMemo(
() => schemaToDocument(POST_SCHEMA),
[],
);
const [submitted, setSubmitted] = useState<Record<string, unknown> | null>(
null,
);

return (
<div className="grid gap-6 lg:grid-cols-2">
<Card>
<CardContent className="pt-6">
<DocumentRenderer
document={document}
registry={defaultBlockRegistry}
onSubmit={setSubmitted}
/>
</CardContent>
</Card>
<div className="flex flex-col gap-4">
<div>
<p className="mb-2 text-sm font-medium">Generated document</p>
<pre className="max-h-80 overflow-auto rounded-lg border bg-muted/40 p-4 text-xs leading-5">
{JSON.stringify(document, null, 2)}
</pre>
</div>
<div>
<p className="mb-2 text-sm font-medium">Submitted values</p>
<pre className="max-h-60 overflow-auto rounded-lg border bg-muted/40 p-4 text-xs leading-5">
{submitted
? JSON.stringify(submitted, null, 2)
: 'Submit the form. Validation comes from the document constraints, not from this page.'}
</pre>
</div>
</div>
</div>
);
}
5 changes: 5 additions & 0 deletions apps/blocks/src/components/site/site-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,11 @@ export const SiteSidebar = forwardRef<HTMLElement, SiteSidebarProps>(function Si
Console Kit
</NavLink>
</li>
<li>
<NavLink href="/blocks/documents" active={pathname === '/blocks/documents'} onNavigate={onNavigate}>
JSON documents
</NavLink>
</li>
</ul>
</div>

Expand Down
1 change: 1 addition & 0 deletions apps/blocks/src/components/site/site-topbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ function crumbFor(path: string): string {
return pack ? `${pack.title} feature pack` : 'Feature packs';
}
if (p === '/blocks/console-kit') return 'Console Kit';
if (p === '/blocks/documents') return 'JSON documents';
if (p === '/blocks/command-palette') return 'Command Palette';
if (p === '/blocks/ai') return 'AI';
if (p.startsWith('/blocks/ai/')) {
Expand Down
5 changes: 5 additions & 0 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ so their entry points are root-level files and consumers get deep imports
layout in an isolated consumer, including packed dependents resolving the packed
schema.

`@constructive-io/blocks-ui` is a React package built with `tsup`, so it keeps
the scoped-package layout (`dist` plus an exports map) rather than publishing
from `dist`. It peer-depends on `@constructive-io/ui`, `blocks-renderer`, and
`blocks-schema`, so publish those first.

## Verify the exact publish inputs

From the validated tag checkout:
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"scripts": {
"build": "lerna run build",
"build:packages": "pnpm --filter @constructive-io/ui build && pnpm --filter @constructive-io/data build && pnpm --filter @constructive-io/command-palette build && pnpm --filter @constructive-io/sheets build && pnpm --filter @constructive-io/schema-builder build && pnpm --filter json-renderer build && pnpm --filter blocks-schema build && pnpm --filter blocks-renderer build && pnpm --filter json-schema-to-blocks build && pnpm --filter meta-to-blocks build && pnpm --filter flow-to-blocks build",
"build:packages": "pnpm --filter @constructive-io/ui build && pnpm --filter @constructive-io/data build && pnpm --filter @constructive-io/command-palette build && pnpm --filter @constructive-io/sheets build && pnpm --filter @constructive-io/schema-builder build && pnpm --filter json-renderer build && pnpm --filter blocks-schema build && pnpm --filter blocks-renderer build && pnpm --filter json-schema-to-blocks build && pnpm --filter meta-to-blocks build && pnpm --filter flow-to-blocks build && pnpm --filter @constructive-io/blocks-ui build",
"build:registry": "pnpm --filter @constructive-io/registry build && pnpm check:console-kit-inspector",
"build:pages": "pnpm build:packages && pnpm build:registry && pnpm --filter blocks build:pages && pnpm pages:artifact",
"build:storybook": "pnpm --filter @constructive-io/ui build-sb",
Expand Down
21 changes: 21 additions & 0 deletions packages/blocks-ui/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Constructive

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading
Loading