Skip to content

feat(blocks-ui): default widget registry for JSON UI documents - #36

Merged
pyramation merged 1 commit into
mainfrom
feat/blocks-ui-registry
Aug 22, 2026
Merged

feat(blocks-ui): default widget registry for JSON UI documents#36
pyramation merged 1 commit into
mainfrom
feat/blocks-ui-registry

Conversation

@pyramation

Copy link
Copy Markdown
Contributor

Summary

blocks-renderer ships the walker, bindings and registry contract but no components, so a generated UIDocument renders nothing until a host hand-writes an adapter for every node type. This adds @constructive-io/blocks-ui: the default nodeType -> component map wired to @constructive-io/ui, plus a live docs page where a JSON Schema becomes a working form with no hand-written UI.

<DocumentRenderer document={schemaToDocument(schema)} registry={defaultBlockRegistry} />

It is a separate package rather than an export of @constructive-io/ui so the design system never depends on blocks-renderer/blocks-schema (primitives shouldn't know about documents), and so a host can layer or replace it:

const registry = composeRegistry(defaultBlockRegistry, { Select: MyCombobox });

What's in the registry

  • Widgets: Input, Textarea, Select, RadioGroup, Checkbox, Switch, NumberInput, DatePicker, DateTimePicker, TimePicker, PhoneInput, FileUpload, and CodeEditor/MarkdownEditor/JsonEditor on a monospace-textarea fallback (Ace stays opt-in via a host override).
  • Containers: Page, Form, Section, Grid, GridColumn, Tabs, Tab. Blocks: Button, ActionBar, Markdown, StatCard.
  • Deliberately unregistered: DataTable, DetailPanel, RelationList, Chart, AgentChat. They need a query/agent runtime, so they fall through to the renderer's unknown-node handling until a host registers them. widgetRegistry / containerRegistry / blockRegistry are exported separately for hosts that want a subset.

Adapters own no form state — it all lives in renderer context, so validation and onSubmit keep working with a mixed registry:

const field = useNodeField(props);            // useBlockField + a stable id + disabled/required/placeholder
<FieldShell props={props} id={field.id} error={field.error}></FieldShell>

NumberInput and FileUpload preserve absence as null rather than ''/NaN; FileUpload stores the selected filename only, since bytes belong to a host storage adapter.

Packaging

Follows the newer tsup packages (command-palette, sheets): scoped name, exports map, publishes from the package root. It is not on the makage publish-from-dist layout, so the naked-name assertions in check-packed-packages.ts don't apply to it; docs/RELEASING.md records that and the fact its peers publish first. Wired into build:packages, pack-local.ts, and the packed-consumer check, which now installs the tarball and asserts a generated document renders real inputs with no unknown-node fallback. Version is 0.1.0 and nothing is published — Lerna bumps it on the next manual release.

Verified locally: pnpm check, pnpm pack:check, pnpm build:pages (new /blocks/documents route prerenders).

Link to Devin session: https://app.devin.ai/sessions/027937d092794c92a31c6ee49c513f59
Requested by: @pyramation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@tenki-reviewer

tenki-reviewer Bot commented Aug 22, 2026

Copy link
Copy Markdown

Review complete. 🟡 1 medium

💬 Inline comments (1)

🧹 Nitpicks (2) — 🟢 2 low
  • 🟢 Application nav count off by one after adding JSON documents (site-sidebar.tsx:250) — The 'Application' group badge undercounts by one: the count adds +2 for the group's static links (site-sidebar.tsx:250), but the group now holds three — Feature packs, Console Kit, and the newly added JSON documents (site-sidebar.tsx:297-301).
  • 🟢 NumberInput stores NaN for partial input (widgets.tsx:118) — In NumberInputBlock (widgets.tsx:118), onChange converts the raw input with Number(event.target.value) and only special-cases the empty string.

The change adds a new packages/blocks-ui React package providing default widget implementations for blocks-renderer, wires it into the build/pack verification scripts and root manifests, and adds a JSON documents showcase page with sidebar/topbar navigation, sitemap, and llms.txt route updates plus docs.

Files Change
packages/blocks-ui/* New default widget registry package: widgets, containers, field, registry, tests, and build config
apps/blocks/src/app/blocks/documents/*, llms.txt/*, sitemap.ts, site components New JSON documents page, nav links, and route/sitemap/llms.txt wiring
scripts/pack-local.ts, scripts/check-packed-packages.ts, package.json Packaging scripts extended to cover the new blocks-ui package
README.md, docs/RELEASING.md, packages/blocks-ui/README.md, LICENSE Documentation for the new package and release process

The sweep high-recall pass did not publish its findings; the review relies on the lens, domain, and holistic passes, all of which completed and were adjudicated.

Reviewed commit: 6dacca3

@tenki-reviewer tenki-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR introduces the new @constructive-io/blocks-ui default widget registry package, adds a JSON documents page with nav/sitemap/llms.txt wiring, and updates packaging scripts and docs.

Key findings

Comment on lines +227 to +236
const local = raw.length > 16 ? raw.slice(0, 16) : raw;

return (
<FieldShell props={props} id={field.id} error={field.error}>
<Input
id={field.id}
name={field.name}
type="datetime-local"
value={local}
onChange={(event) => field.setValue(event.target.value)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 bug · medium

DateTimePicker write-back drops the timezone

DateTimePickerBlock slices a zoned ISO-8601 value to YYYY-MM-DDTHH:mm for the datetime-local input (widgets.tsx:227), then onChange stores event.target.value verbatim (widgets.tsx:236), persisting the trimmed, zone-less local string. The doc comment says the value is 'trimmed for display only', but the edit path overwrites the document value with a zone-less, seconds-less form, so any user edit silently strips the timezone (and seconds) from the persisted value, changing the instant it represents for downstream consumers.

📋 Prompt for AI Agents

In packages/blocks-ui/src/widgets.tsx, DateTimePickerBlock (lines 224-242): the onChange at line 236 stores event.target.value verbatim, which is a zone-less, seconds-less YYYY-MM-DDTHH:mm string, contradicting the comment that the document/Postgres value is ISO-8601 with a zone and only trimmed for display. Change the onChange to preserve the timezone and seconds when writing back (e.g. parse the edited local datetime and serialize to ISO-8601 with the local offset, or re-append the original value's zone suffix) so the persisted form-state value keeps its zone instead of being replaced by the raw datetime-local string.

@pyramation
pyramation merged commit 9fe093f into main Aug 22, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant