diff --git a/README.md b/README.md index d2b03ecc..6894efc4 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,8 @@ Generate dynamic, personalized UIs from prompts without sacrificing reliability. npm install @json-render/core @json-render/react # for React with pre-built shadcn/ui components npm install @json-render/shadcn +# for React with pre-built Ant Design components +npm install @json-render/antd # or for React Native npm install @json-render/core @json-render/react-native # or for video @@ -34,8 +36,9 @@ json-render is a **Generative UI** framework: AI generates interfaces from natur - **Guardrailed** - AI can only use components in your catalog - **Predictable** - JSON output matches your schema, every time - **Fast** - Stream and render progressively as the model responds -- **Cross-Platform** - React, Vue, Svelte, Solid (web), React Native (mobile) from the same catalog -- **Batteries Included** - 36 pre-built shadcn/ui components ready to use +- **Cross-Platform** - React, Vue, Svelte (web), React Native (mobile) from the same catalog +- **Batteries Included** - 36 pre-built shadcn/ui components and 50+ Ant Design components ready to use + ## Quick Start @@ -115,27 +118,25 @@ function Dashboard({ spec }) { ## Packages -| Package | Description | -| --------------------------- | ---------------------------------------------------------------------- | -| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities | -| `@json-render/react` | React renderer, contexts, hooks | -| `@json-render/vue` | Vue 3 renderer, composables, providers | -| `@json-render/svelte` | Svelte 5 renderer with runes-based reactivity | -| `@json-render/solid` | SolidJS renderer with fine-grained reactive contexts | -| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) | -| `@json-render/react-three-fiber` | React Three Fiber renderer for 3D scenes (19 built-in components) | -| `@json-render/react-native` | React Native renderer with standard mobile components | -| `@json-render/remotion` | Remotion video renderer, timeline schema | -| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs | -| `@json-render/react-email` | React Email renderer for HTML/plain-text emails from specs | -| `@json-render/image` | Image renderer for SVG/PNG output (OG images, social cards) via Satori | -| `@json-render/codegen` | Utilities for generating code from json-render UI trees | -| `@json-render/redux` | Redux / Redux Toolkit adapter for `StateStore` | -| `@json-render/zustand` | Zustand adapter for `StateStore` | -| `@json-render/jotai` | Jotai adapter for `StateStore` | -| `@json-render/xstate` | XState Store (atom) adapter for `StateStore` | -| `@json-render/mcp` | MCP Apps integration for Claude, ChatGPT, Cursor, VS Code | -| `@json-render/yaml` | YAML wire format with streaming parser, edit modes, AI SDK transform | +| Package | Description | +|---------|-------------| +| `@json-render/core` | Schemas, catalogs, AI prompts, dynamic props, SpecStream utilities | +| `@json-render/react` | React renderer, contexts, hooks | +| `@json-render/vue` | Vue 3 renderer, composables, providers | +| `@json-render/svelte` | Svelte 5 renderer with runes-based reactivity | +| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) | +| `@json-render/antd` | 50+ pre-built Ant Design components | +| `@json-render/react-native` | React Native renderer with standard mobile components | +| `@json-render/remotion` | Remotion video renderer, timeline schema | +| `@json-render/react-pdf` | React PDF renderer for generating PDF documents from specs | +| `@json-render/react-email` | React Email renderer for HTML/plain-text emails from specs | +| `@json-render/image` | Image renderer for SVG/PNG output (OG images, social cards) via Satori | +| `@json-render/codegen` | Utilities for generating code from json-render UI trees | +| `@json-render/redux` | Redux / Redux Toolkit adapter for `StateStore` | +| `@json-render/zustand` | Zustand adapter for `StateStore` | +| `@json-render/jotai` | Jotai adapter for `StateStore` | +| `@json-render/xstate` | XState Store (atom) adapter for `StateStore` | +| `@json-render/mcp` | MCP Apps integration for Claude, ChatGPT, Cursor, VS Code | ## Renderers @@ -257,6 +258,39 @@ const { registry } = defineRegistry(catalog, { ; ``` +### Ant Design (Web) + +```tsx +import { defineCatalog } from "@json-render/core"; +import { schema } from "@json-render/react/schema"; +import { defineRegistry, Renderer } from "@json-render/react"; +import { antdComponentDefinitions } from "@json-render/antd/catalog"; +import { antdComponents } from "@json-render/antd"; + +// Pick components from the 50+ standard definitions +const catalog = defineCatalog(schema, { + components: { + Card: antdComponentDefinitions.Card, + Stack: antdComponentDefinitions.Stack, + Heading: antdComponentDefinitions.Heading, + Button: antdComponentDefinitions.Button, + }, + actions: {}, +}); + +// Use matching implementations +const { registry } = defineRegistry(catalog, { + components: { + Card: antdComponents.Card, + Stack: antdComponents.Stack, + Heading: antdComponents.Heading, + Button: antdComponents.Button, + }, +}); + + +``` + ### React Native (Mobile) ```tsx diff --git a/packages/antd/CHANGELOG.md b/packages/antd/CHANGELOG.md new file mode 100644 index 00000000..8f555e12 --- /dev/null +++ b/packages/antd/CHANGELOG.md @@ -0,0 +1,36 @@ +# @json-render/antd + +## 0.11.0 + +### Minor Changes + +- Initial release of `@json-render/antd` package. + + Pre-built [Ant Design](https://ant.design/) component library for json-render. 50+ components ready to use with `defineCatalog` and `defineRegistry`. + + - `antdComponentDefinitions` — Zod-based catalog definitions for all components (server-safe, no React dependency via `@json-render/antd/catalog`) + - `antdComponents` — React implementations for all components + + ### Layout Components + - Card, Stack, Grid, Divider, Space + + ### Navigation Components + - Tabs, Collapse, Menu + + ### Overlay Components + - Modal, Drawer, Popover, Tooltip, Dropdown + + ### Data Display Components + - Table, Heading, Text, Paragraph, Image, Avatar, Badge, Tag, Alert, Progress, Skeleton, Spin, Empty, Statistic, Descriptions, Timeline, Carousel + + ### Form Components + - Input, TextArea, InputNumber, Select, Checkbox, CheckboxGroup, Radio, Switch, Slider, Rate, DatePicker, TimePicker, Upload, Transfer + + ### Action Components + - Button, ButtonGroup, Link, Pagination, Segmented, Steps, Result + +### Patch Changes + +- Updated dependencies + - @json-render/core@0.11.0 + - @json-render/react@0.11.0 diff --git a/packages/antd/README.md b/packages/antd/README.md new file mode 100644 index 00000000..3e71e612 --- /dev/null +++ b/packages/antd/README.md @@ -0,0 +1,245 @@ +# @json-render/antd + +Pre-built [Ant Design](https://ant.design/) components for json-render. Drop-in catalog definitions and React implementations for 70+ components built on Ant Design. + +## Installation + +```bash +npm install @json-render/antd @json-render/core @json-render/react antd zod +``` + +## Quick Start + +### 1. Create a Catalog + +Import standard definitions from `@json-render/antd/catalog` and pass them to `defineCatalog`: + +```typescript +import { defineCatalog } from "@json-render/core"; +import { schema } from "@json-render/react/schema"; +import { antdComponentDefinitions } from "@json-render/antd/catalog"; + +const catalog = defineCatalog(schema, { + components: { + // Pick the components you need + Card: antdComponentDefinitions.Card, + Stack: antdComponentDefinitions.Stack, + Heading: antdComponentDefinitions.Heading, + Button: antdComponentDefinitions.Button, + Input: antdComponentDefinitions.Input, + }, + actions: {}, +}); +``` + +> **Note:** State actions (`setState`, `pushState`, `removeState`) are built into the React schema and handled automatically by `ActionProvider`. You don't need to declare them in your catalog. + +### 2. Create a Registry + +Import standard implementations from `@json-render/antd` and pass them to `defineRegistry`: + +```typescript +import { defineRegistry } from "@json-render/react"; +import { antdComponents } from "@json-render/antd"; + +const { registry } = defineRegistry(catalog, { + components: { + Card: antdComponents.Card, + Stack: antdComponents.Stack, + Heading: antdComponents.Heading, + Button: antdComponents.Button, + Input: antdComponents.Input, + }, +}); +``` + +### 3. Render + +```tsx +import { Renderer } from "@json-render/react"; + +function App({ spec }) { + return ; +} +``` + +## Extending with Custom Components + +Pick standard components as a base and add your own alongside them: + +```typescript +import { z } from "zod"; + +// Catalog +const catalog = defineCatalog(schema, { + components: { + // Standard + Card: antdComponentDefinitions.Card, + Stack: antdComponentDefinitions.Stack, + Button: antdComponentDefinitions.Button, + + // Custom + Metric: { + props: z.object({ + label: z.string(), + value: z.string(), + trend: z.enum(["up", "down", "neutral"]).nullable(), + }), + description: "KPI metric display", + }, + }, + actions: {}, +}); + +// Registry +const { registry } = defineRegistry(catalog, { + components: { + // Standard + Card: antdComponents.Card, + Stack: antdComponents.Stack, + Button: antdComponents.Button, + + // Custom + Metric: ({ props }) => ( +
+ {props.label} + {props.value} +
+ ), + }, +}); +``` + +## Standard Components + +### Layout + +| Component | Description | +|-----------|-------------| +| `Card` | Container card with optional title and description | +| `Flex` | Flex layout container with gap, alignment, justify | +| `Stack` | Stack layout container (alias for Flex) | +| `Row` | Grid row (24-column system) | +| `Col` | Grid column with span, offset | +| `Masonry` | Masonry layout with responsive columns | +| `Layout` | Antd layout container | +| `LayoutHeader` | Layout header | +| `LayoutContent` | Layout main content area | +| `LayoutFooter` | Layout footer | +| `LayoutSider` | Layout sidebar | +| `Divider` | Visual separator line | +| `Space` | Spacing component | + +### Navigation + +| Component | Description | +|-----------|-------------| +| `Tabs` | Tabbed navigation | +| `Collapse` | Collapsible accordion sections | +| `Menu` | Navigation menu | +| `Affix` | Pin content to fixed position when scrolling | +| `Anchor` | Anchor navigation for page sections | +| `Breadcrumb` | Breadcrumb navigation path | +| `BackTop` | Back to top button | + +### Overlay + +| Component | Description | +|-----------|-------------| +| `Modal` | Modal dialog | +| `Drawer` | Drawer panel | +| `Popover` | Click-triggered popover | +| `Tooltip` | Hover tooltip | +| `Dropdown` | Dropdown menu | + +### Data Display + +| Component | Description | +|-----------|-------------| +| `Table` | Data table with columns and rows | +| `Heading` | Heading text | +| `Text` | Text content | +| `Paragraph` | Paragraph text | +| `Image` | Image display | +| `Avatar` | User avatar with fallback | +| `Badge` | Status badge | +| `Tag` | Tag component | +| `Alert` | Alert banner | +| `Progress` | Progress bar | +| `Skeleton` | Loading placeholder | +| `Spin` | Loading spinner | +| `Empty` | Empty state | +| `Statistic` | Statistic display | +| `Descriptions` | Description list | +| `Timeline` | Timeline display | +| `Carousel` | Horizontally scrollable carousel | +| `Calendar` | Calendar for date display/selection | +| `List` | List component with pagination and grid | +| `Tree` | Tree structure display and selection | +| `QRCode` | QRCode generator | + +### Data Entry + +| Component | Description | +|-----------|-------------| +| `Input` | Text input with label, validation, and `validateOn` timing | +| `TextArea` | Multi-line text input with validation | +| `InputNumber` | Number input | +| `Select` | Dropdown select with validation | +| `Checkbox` | Checkbox input with validation | +| `CheckboxGroup` | Group of checkboxes | +| `Radio` | Radio button group with validation | +| `Switch` | Toggle switch with validation | +| `Slider` | Range slider | +| `Rate` | Star rating | +| `DatePicker` | Date picker | +| `TimePicker` | Time picker | +| `Upload` | File upload | +| `Transfer` | Transfer shuttle | +| `AutoComplete` | Input with suggestions | +| `Cascader` | Cascader selection for hierarchical data | +| `ColorPicker` | Color picker | +| `Mentions` | Mentions input for @-tagging | +| `TreeSelect` | Tree select dropdown | + +### Action + +| Component | Description | +|-----------|-------------| +| `Button` | Clickable button with variants | +| `Link` | Anchor link | +| `ButtonGroup` | Group of buttons | +| `Pagination` | Page navigation | +| `Segmented` | Segmented control | +| `Steps` | Steps component | +| `Result` | Result page | + +## Built-in Actions + +State actions (`setState`, `pushState`, `removeState`, `validateForm`) are built into the `@json-render/react` schema and handled automatically by `ActionProvider`. They are included in prompts without needing to be declared in your catalog. + +| Action | Description | +|--------|-------------| +| `setState` | Set a value at a state path | +| `pushState` | Push a value onto an array in state | +| `removeState` | Remove an item from an array in state | +| `validateForm` | Validate all fields and write result to state | + +### Validation Timing (`validateOn`) + +All form components support the `validateOn` prop to control when validation runs: + +| Value | Description | Default For | +|-------|-------------|-------------| +| `"change"` | Validate on every input change | Select, Checkbox, Radio, Switch | +| `"blur"` | Validate when field loses focus | Input, Textarea | +| `"submit"` | Validate only on form submission | — | + +## Exports + +| Entry Point | Exports | +|-------------|---------| +| `@json-render/antd` | `antdComponents` | +| `@json-render/antd/catalog` | `antdComponentDefinitions` | + +The `/catalog` entry point contains only Zod schemas (no React dependency), so it can be used in server-side code for prompt generation. diff --git a/packages/antd/package.json b/packages/antd/package.json new file mode 100644 index 00000000..3093ed6a --- /dev/null +++ b/packages/antd/package.json @@ -0,0 +1,74 @@ +{ + "name": "@json-render/antd", + "version": "0.11.0", + "license": "Apache-2.0", + "description": "Ant Design component library for @json-render/core. JSON becomes beautiful Ant Design React components.", + "keywords": [ + "json", + "ui", + "react", + "antd", + "ant-design", + "ai", + "generative-ui", + "llm", + "renderer", + "streaming", + "components" + ], + "repository": { + "type": "git", + "url": "git+https://github.com/vercel-labs/json-render.git", + "directory": "packages/antd" + }, + "homepage": "https://github.com/vercel-labs/json-render#readme", + "bugs": { + "url": "https://github.com/vercel-labs/json-render/issues" + }, + "publishConfig": { + "access": "public" + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + }, + "./catalog": { + "types": "./dist/catalog.d.ts", + "import": "./dist/catalog.mjs", + "require": "./dist/catalog.js" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "check-types": "tsc --noEmit", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@json-render/core": "workspace:*", + "@json-render/react": "workspace:*" + }, + "devDependencies": { + "@internal/typescript-config": "workspace:*", + "@types/react": "19.2.3", + "antd": "^6.3.1", + "tsup": "^8.0.2", + "dayjs": "^1.11.11", + "typescript": "^5.4.5", + "zod": "^4.3.6" + }, + "peerDependencies": { + "react": "^19.0.0", + "react-dom": "^19.0.0", + "antd": "^6.0.0", + "zod": "^4.0.0" + } +} diff --git a/packages/antd/src/catalog.ts b/packages/antd/src/catalog.ts new file mode 100644 index 00000000..82013c41 --- /dev/null +++ b/packages/antd/src/catalog.ts @@ -0,0 +1,1400 @@ +import { z } from "zod"; + +// ============================================================================= +// Shared validation schemas used across form components +// ============================================================================= + +const validationCheckSchema = z + .array( + z.object({ + type: z.string(), + message: z.string(), + args: z.record(z.string(), z.unknown()).optional(), + }), + ) + .nullable(); + +const validateOnSchema = z.enum(["change", "blur", "submit"]).nullable(); + +// ============================================================================= +// Ant Design v6 Component Definitions +// ============================================================================= + +/** + * Ant Design v6 component definitions for json-render catalogs. + * + * These can be used directly or extended with custom components. + * All components are built using Ant Design v6 components. + * + * Note: Component APIs follow Antd v6 specifications: + * - Use `variant` instead of deprecated `bordered` where applicable + * - Use `orientation` instead of deprecated `direction` where applicable + * - Use `titlePlacement` for Divider text alignment + * - Use `expandIconPlacement` instead of deprecated `expandIconPosition` + */ +export const antdComponentDefinitions = { + // ========================================================================== + // Layout Components + // ========================================================================== + + Layout: { + props: z.object({ + hasSider: z.boolean().nullable(), + }), + slots: ["default"], + description: + "Antd layout container. Compose with LayoutHeader / LayoutSider / LayoutContent / LayoutFooter inside.", + }, + + LayoutHeader: { + props: z.object({}), + slots: ["default"], + description: "Antd layout header. Use inside Layout only.", + }, + + LayoutContent: { + props: z.object({}), + slots: ["default"], + description: "Antd layout main content area. Use inside Layout only.", + }, + + LayoutFooter: { + props: z.object({}), + slots: ["default"], + description: "Antd layout footer. Use inside Layout only.", + }, + + LayoutSider: { + props: z.object({ + width: z.union([z.number(), z.string()]).nullable(), + collapsible: z.boolean().nullable(), + collapsed: z.boolean().nullable(), + defaultCollapsed: z.boolean().nullable(), + collapsedWidth: z.union([z.number(), z.string()]).nullable(), + reverseArrow: z.boolean().nullable(), + breakpoint: z.enum(["xs", "sm", "md", "lg", "xl", "xxl"]).nullable(), + theme: z.enum(["light", "dark"]).nullable(), + }), + slots: ["default"], + events: ["collapse"], + description: "Antd layout sidebar. Use inside Layout only.", + }, + + Card: { + props: z.object({ + title: z.string().nullable(), + extra: z.string().nullable(), + variant: z.enum(["outlined", "borderless"]).nullable(), + hoverable: z.boolean().nullable(), + loading: z.boolean().nullable(), + size: z.enum(["default", "small"]).nullable(), + cover: z.string().nullable(), + actions: z.array(z.string()).nullable(), + }), + slots: ["default", "extra", "cover", "actions"], + description: + "Container card for content sections. Use slots.default for card body, slots.extra for header extra content, slots.cover for cover image, slots.actions for action buttons.", + example: { title: "Overview", variant: "outlined" }, + }, + + Flex: { + props: z.object({ + vertical: z.boolean().nullable(), + wrap: z.boolean().nullable(), + justify: z.string().nullable(), + align: z.string().nullable(), + gap: z.union([z.string(), z.number()]).nullable(), + flex: z.string().nullable(), + }), + slots: ["default"], + description: + "Flex layout container. justify/align accept CSS values (e.g. 'center', 'space-between'). gap accepts 'small'/'middle'/'large' or a number.", + example: { vertical: true, gap: "middle" }, + }, + + Stack: { + props: z.object({ + direction: z.enum(["vertical", "horizontal"]).nullable(), + wrap: z.boolean().nullable(), + justify: z + .enum([ + "start", + "end", + "center", + "space-around", + "space-between", + "space-evenly", + ]) + .nullable(), + align: z + .enum(["start", "center", "end", "baseline", "stretch"]) + .nullable(), + gap: z + .union([z.enum(["small", "middle", "large"]), z.number()]) + .nullable(), + }), + slots: ["default"], + description: + "Stack layout container based on Flex. Defaults to vertical direction.", + example: { direction: "vertical", gap: "middle" }, + }, + + Grid: { + props: z.object({ + columns: z.number().nullable(), + gap: z.enum(["sm", "md", "lg"]).nullable(), + }), + slots: ["default"], + description: "Grid layout (1-6 columns). Antd v6.", + example: { columns: 3, gap: "md" }, + }, + + Row: { + props: z.object({ + gutter: z + .union([z.number(), z.tuple([z.number(), z.number()])]) + .nullable(), + align: z.enum(["top", "middle", "bottom", "stretch"]).nullable(), + justify: z + .enum([ + "start", + "end", + "center", + "space-around", + "space-between", + "space-evenly", + ]) + .nullable(), + wrap: z.boolean().nullable(), + }), + slots: ["default"], + description: + "Antd grid row. Use Col children inside. gutter: horizontal spacing (or [horizontal, vertical]).", + example: { gutter: 16 }, + }, + + Col: { + props: z.object({ + span: z.number().nullable(), + offset: z.number().nullable(), + order: z.number().nullable(), + push: z.number().nullable(), + pull: z.number().nullable(), + flex: z.union([z.number(), z.string()]).nullable(), + }), + slots: ["default"], + description: + "Antd grid column (span 1-24). Use inside Row. offset shifts the col right.", + example: { span: 12 }, + }, + + Masonry: { + props: z.object({ + columns: z + .union([z.number(), z.record(z.string(), z.number())]) + .nullable(), + gutter: z + .union([z.number(), z.tuple([z.number(), z.number()])]) + .nullable(), + }), + slots: ["default"], + description: + "Masonry layout. Children are automatically distributed across columns. columns can be number or responsive object like { xs: 1, sm: 2, md: 3 }.", + example: { columns: 3, gutter: [16, 16] }, + }, + + Divider: { + props: z.object({ + orientation: z.enum(["horizontal", "vertical"]).nullable(), + vertical: z.boolean().nullable(), + titlePlacement: z + .enum(["left", "center", "right", "start", "end"]) + .nullable(), + dashed: z.boolean().nullable(), + variant: z.enum(["solid", "dashed", "dotted"]).nullable(), + plain: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + text: z.string().nullable(), + }), + description: "A divider line that separates content.", + }, + + Space: { + props: z.object({ + orientation: z.enum(["horizontal", "vertical"]).nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + wrap: z.boolean().nullable(), + align: z.enum(["start", "center", "end", "baseline"]).nullable(), + }), + slots: ["default"], + description: "Set components spacing with Ant Design Space component.", + }, + + // ========================================================================== + // Navigation Components + // ========================================================================== + + Affix: { + props: z.object({ + offsetBottom: z.number().nullable(), + offsetTop: z.number().nullable(), + target: z.string().nullable(), + }), + slots: ["default"], + description: + "Affix component. Pins children to a fixed position when scrolling.", + }, + + Anchor: { + props: z.object({ + items: z.array( + z.object({ + key: z.string(), + title: z.string(), + href: z.string().nullable(), + }), + ), + affix: z.boolean().nullable(), + bounds: z.number().nullable(), + offsetTop: z.number().nullable(), + targetOffset: z.number().nullable(), + }), + events: ["change", "click"], + description: "Anchor navigation for page sections.", + }, + + Breadcrumb: { + props: z.object({ + items: z.array( + z.object({ + title: z.string(), + href: z.string().nullable(), + }), + ), + separator: z.string().nullable(), + }), + description: "Breadcrumb navigation path.", + }, + + BackTop: { + props: z.object({ + visibilityHeight: z.number().nullable(), + target: z.string().nullable(), + duration: z.number().nullable(), + }), + slots: ["default"], + description: + "Back to top button. Deprecated in antd v6, use FloatButton.BackTop instead.", + }, + + Tabs: { + props: z.object({ + tabs: z.array( + z.object({ + label: z.string(), + value: z.string(), + }), + ), + defaultValue: z.string().nullable(), + value: z.string().nullable(), + position: z.enum(["top", "bottom", "left", "right"]).nullable(), + type: z.enum(["line", "card"]).nullable(), + centered: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + tabBarGutter: z.number().nullable(), + destroyInactiveTabPane: z.boolean().nullable(), + }), + slots: ["tabs"], + events: ["change"], + description: + "Tab navigation. Use slots.tabs for tab content items. Use { $bindState } on value for active tab binding.", + }, + + Collapse: { + props: z.object({ + items: z.array( + z.object({ + title: z.string(), + }), + ), + accordion: z.boolean().nullable(), + bordered: z.boolean().nullable(), + ghost: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + expandIconPlacement: z.enum(["start", "end"]).nullable(), + collapsible: z.enum(["header", "icon", "disabled"]).nullable(), + defaultActiveKey: z.union([z.string(), z.array(z.string())]).nullable(), + activeKey: z.union([z.string(), z.array(z.string())]).nullable(), + }), + slots: ["items"], + description: + "Collapsible sections. Each item has a title. Use slots.items for panel content.", + }, + + Menu: { + props: z.object({ + items: z.array( + z.object({ + label: z.string(), + key: z.string(), + icon: z.string().nullable(), + }), + ), + mode: z.enum(["horizontal", "vertical", "inline"]).nullable(), + selectedKey: z.string().nullable(), + theme: z.enum(["light", "dark"]).nullable(), + defaultSelectedKeys: z.array(z.string()).nullable(), + inlineCollapsed: z.boolean().nullable(), + multiple: z.boolean().nullable(), + }), + events: ["select"], + description: "Navigation menu with items.", + }, + + // ========================================================================== + // Overlay Components + // ========================================================================== + + Modal: { + props: z.object({ + title: z.string(), + description: z.string().nullable(), + openPath: z.string(), + width: z.number().nullable(), + footer: z.boolean().nullable(), + centered: z.boolean().nullable(), + closable: z.boolean().nullable(), + maskClosable: z.boolean().nullable(), + okText: z.string().nullable(), + cancelText: z.string().nullable(), + confirmLoading: z.boolean().nullable(), + destroyOnClose: z.boolean().nullable(), + keyboard: z.boolean().nullable(), + loading: z.boolean().nullable(), + }), + slots: ["default"], + events: ["ok", "cancel"], + description: + "Modal dialog. Set openPath to a boolean state path. Use setState to toggle.", + }, + + Drawer: { + props: z.object({ + title: z.string(), + description: z.string().nullable(), + openPath: z.string(), + placement: z.enum(["top", "bottom", "left", "right"]).nullable(), + width: z.union([z.number(), z.string()]).nullable(), + height: z.union([z.number(), z.string()]).nullable(), + closable: z.boolean().nullable(), + maskClosable: z.boolean().nullable(), + destroyOnClose: z.boolean().nullable(), + keyboard: z.boolean().nullable(), + loading: z.boolean().nullable(), + size: z.enum(["default", "large"]).nullable(), + }), + slots: ["default"], + events: ["close"], + description: "Side drawer panel. Set openPath to a boolean state path.", + }, + + Popover: { + props: z.object({ + trigger: z.string(), + title: z.string().nullable(), + content: z.string(), + placement: z + .enum([ + "top", + "topLeft", + "topRight", + "bottom", + "bottomLeft", + "bottomRight", + "left", + "leftTop", + "leftBottom", + "right", + "rightTop", + "rightBottom", + ]) + .nullable(), + triggerType: z + .enum(["hover", "focus", "click", "contextMenu"]) + .nullable(), + arrow: z.boolean().nullable(), + open: z.boolean().nullable(), + defaultOpen: z.boolean().nullable(), + }), + description: "Popover that appears on click of trigger.", + }, + + Tooltip: { + props: z.object({ + content: z.string(), + text: z.string(), + placement: z + .enum([ + "top", + "topLeft", + "topRight", + "bottom", + "bottomLeft", + "bottomRight", + "left", + "leftTop", + "leftBottom", + "right", + "rightTop", + "rightBottom", + ]) + .nullable(), + triggerType: z + .enum(["hover", "focus", "click", "contextMenu"]) + .nullable(), + arrow: z.boolean().nullable(), + color: z.string().nullable(), + open: z.boolean().nullable(), + defaultOpen: z.boolean().nullable(), + }), + description: "Hover tooltip. Shows content on hover over text.", + }, + + Dropdown: { + props: z.object({ + items: z.array( + z.object({ + label: z.string(), + key: z.string(), + icon: z.string().nullable(), + danger: z.boolean().nullable(), + disabled: z.boolean().nullable(), + divider: z.boolean().nullable(), + }), + ), + trigger: z.enum(["hover", "click", "contextMenu"]).nullable(), + placement: z + .enum([ + "topLeft", + "topCenter", + "topRight", + "bottomLeft", + "bottomCenter", + "bottomRight", + ]) + .nullable(), + arrow: z.boolean().nullable(), + disabled: z.boolean().nullable(), + open: z.boolean().nullable(), + defaultOpen: z.boolean().nullable(), + }), + slots: ["default"], + events: ["select", "openChange", "visibleChange"], + description: "Dropdown menu. Use children as trigger element.", + }, + + // ========================================================================== + // Data Display Components + // ========================================================================== + + Table: { + props: z.object({ + columns: z.array(z.string()), + rows: z.array(z.array(z.string())), + caption: z.string().nullable(), + bordered: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + loading: z.boolean().nullable(), + pagination: z + .union([ + z.boolean(), + z.object({ + pageSize: z.number().nullable(), + current: z.number().nullable(), + total: z.number().nullable(), + showSizeChanger: z.boolean().nullable(), + showQuickJumper: z.boolean().nullable(), + simple: z.boolean().nullable(), + hideOnSinglePage: z.boolean().nullable(), + }), + ]) + .nullable(), + scroll: z + .object({ + x: z.union([z.number(), z.string()]).nullable(), + y: z.union([z.number(), z.string()]).nullable(), + }) + .nullable(), + showHeader: z.boolean().nullable(), + rowKey: z.string().nullable(), + sticky: z.boolean().nullable(), + }), + description: + "Data table. columns: header labels. rows: 2D array of cell strings.", + example: { + columns: ["Name", "Role"], + rows: [ + ["Alice", "Admin"], + ["Bob", "User"], + ], + }, + }, + + Heading: { + props: z.object({ + text: z.string(), + level: z.enum(["h1", "h2", "h3", "h4", "h5"]).nullable(), + }), + description: "Heading text (h1-h5)", + example: { text: "Welcome", level: "h1" }, + }, + + Text: { + props: z.object({ + text: z.string(), + type: z.enum(["secondary", "success", "warning", "danger"]).nullable(), + code: z.boolean().nullable(), + copyable: z.boolean().nullable(), + delete: z.boolean().nullable(), + mark: z.boolean().nullable(), + underline: z.boolean().nullable(), + strong: z.boolean().nullable(), + italic: z.boolean().nullable(), + }), + description: "Text with various styles", + example: { text: "Hello, world!", strong: true }, + }, + + Paragraph: { + props: z.object({ + text: z.string(), + ellipsis: z.boolean().nullable(), + rows: z.number().nullable(), + }), + description: "Paragraph text with optional ellipsis", + }, + + Image: { + props: z.object({ + src: z.string(), + alt: z.string(), + width: z.union([z.number(), z.string()]).nullable(), + height: z.union([z.number(), z.string()]).nullable(), + preview: z.boolean().nullable(), + fallback: z.string().nullable(), + }), + description: "Image component with preview support.", + }, + + Avatar: { + props: z.object({ + src: z.string().nullable(), + name: z.string(), + size: z + .union([z.number(), z.enum(["small", "default", "large"])]) + .nullable(), + shape: z.enum(["circle", "square"]).nullable(), + icon: z.string().nullable(), + alt: z.string().nullable(), + gap: z.number().nullable(), + }), + description: "User avatar with fallback initials", + example: { name: "Jane Doe", size: "default" }, + }, + + Badge: { + props: z.object({ + count: z.union([z.number(), z.string()]).nullable(), + dot: z.boolean().nullable(), + color: z.string().nullable(), + status: z + .enum(["success", "processing", "default", "error", "warning"]) + .nullable(), + text: z.string().nullable(), + size: z.enum(["default", "small"]).nullable(), + overflowCount: z.number().nullable(), + showZero: z.boolean().nullable(), + title: z.string().nullable(), + offset: z + .tuple([ + z.union([z.number(), z.string()]), + z.union([z.number(), z.string()]), + ]) + .nullable(), + }), + slots: ["default"], + description: "Badge component for status or count display", + example: { count: 5 }, + }, + + Tag: { + props: z.object({ + text: z.string(), + color: z.string().nullable(), + closable: z.boolean().nullable(), + bordered: z.boolean().nullable(), + icon: z.string().nullable(), + }), + events: ["close"], + description: "Tag for categorizing or marking.", + example: { text: "Active", color: "green" }, + }, + + Alert: { + props: z.object({ + title: z.string(), + description: z.string().nullable(), + type: z.enum(["info", "success", "warning", "error"]).nullable(), + closable: z.boolean().nullable(), + showIcon: z.boolean().nullable(), + banner: z.boolean().nullable(), + }), + events: ["close"], + description: "Alert banner", + example: { + title: "Note", + description: "Your changes have been saved.", + type: "success", + }, + }, + + Progress: { + props: z.object({ + value: z.number(), + max: z.number().nullable(), + label: z.string().nullable(), + status: z.enum(["success", "exception", "normal", "active"]).nullable(), + type: z.enum(["line", "circle", "dashboard"]).nullable(), + showInfo: z.boolean().nullable(), + strokeColor: z.string().nullable(), + size: z.union([z.enum(["small", "default"]), z.number()]).nullable(), + steps: z.number().nullable(), + }), + description: "Progress bar (value 0-100)", + example: { value: 65, max: 100, label: "Upload progress" }, + }, + + Skeleton: { + props: z.object({ + loading: z.boolean().nullable(), + active: z.boolean().nullable(), + rows: z.number().nullable(), + avatar: z.boolean().nullable(), + title: z.boolean().nullable(), + round: z.boolean().nullable(), + }), + slots: ["default"], + description: "Loading placeholder skeleton", + }, + + Spin: { + props: z.object({ + size: z.enum(["small", "default", "large"]).nullable(), + label: z.string().nullable(), + spinning: z.boolean().nullable(), + delay: z.number().nullable(), + fullscreen: z.boolean().nullable(), + }), + slots: ["default"], + description: "Loading spinner indicator", + }, + + Empty: { + props: z.object({ + description: z.string().nullable(), + }), + description: "Empty state placeholder", + }, + + Statistic: { + props: z.object({ + title: z.string(), + value: z.union([z.number(), z.string()]), + prefix: z.string().nullable(), + suffix: z.string().nullable(), + precision: z.number().nullable(), + loading: z.boolean().nullable(), + groupSeparator: z.string().nullable(), + decimalSeparator: z.string().nullable(), + }), + description: "Display statistic value with title", + }, + + Descriptions: { + props: z.object({ + title: z.string().nullable(), + items: z.array( + z.object({ + label: z.string(), + value: z.string(), + span: z.number().nullable(), + }), + ), + bordered: z.boolean().nullable(), + column: z.number().nullable(), + colon: z.boolean().nullable(), + layout: z.enum(["horizontal", "vertical"]).nullable(), + size: z.enum(["default", "middle", "small"]).nullable(), + }), + description: "Display read-only data in key-value pairs", + }, + + Timeline: { + props: z.object({ + items: z.array( + z.object({ + color: z.string().nullable(), + }), + ), + mode: z.enum(["left", "alternate", "right"]).nullable(), + reverse: z.boolean().nullable(), + }), + slots: ["items"], + description: + "Vertical timeline display. Use slots.items for each node's content.", + }, + + Carousel: { + props: z.object({ + autoplay: z.boolean().nullable(), + dots: z.boolean().nullable(), + effect: z.enum(["scrollx", "fade"]).nullable(), + autoplaySpeed: z.number().nullable(), + speed: z.number().nullable(), + infinite: z.boolean().nullable(), + arrows: z.boolean().nullable(), + dotPosition: z.enum(["top", "bottom", "left", "right"]).nullable(), + }), + slots: ["default"], + description: + "Horizontally scrollable carousel. Use slots for slide content.", + }, + + Calendar: { + props: z.object({ + value: z.string().nullable(), + mode: z.enum(["month", "year"]).nullable(), + fullscreen: z.boolean().nullable(), + }), + events: ["change", "select"], + description: "Calendar component for date display and selection.", + }, + + List: { + props: z.object({ + dataSource: z.array(z.any()).nullable(), + bordered: z.boolean().nullable(), + loading: z.boolean().nullable(), + size: z.enum(["small", "default", "large"]).nullable(), + split: z.boolean().nullable(), + grid: z + .object({ + gutter: z.number().nullable(), + column: z.number().nullable(), + }) + .nullable(), + pagination: z + .union([ + z.boolean(), + z.object({ + pageSize: z.number().nullable(), + total: z.number().nullable(), + }), + ]) + .nullable(), + }), + slots: ["default"], + events: ["change"], + description: "List component. Use slots.default for list items.", + }, + + Tree: { + props: z.object({ + treeData: z.array( + z.object({ + key: z.string(), + title: z.string(), + children: z.array(z.any()).nullable(), + }), + ), + checkable: z.boolean().nullable(), + checkedKeys: z.array(z.string()).nullable(), + expandedKeys: z.array(z.string()).nullable(), + selectedKeys: z.array(z.string()).nullable(), + defaultExpandAll: z.boolean().nullable(), + showLine: z.boolean().nullable(), + multiple: z.boolean().nullable(), + }), + events: ["check", "expand", "select"], + description: "Tree structure display and selection.", + }, + + QRCode: { + props: z.object({ + value: z.string(), + size: z.number().nullable(), + color: z.string().nullable(), + bgColor: z.string().nullable(), + bordered: z.boolean().nullable(), + status: z.enum(["active", "expired", "loading"]).nullable(), + }), + description: "QRCode generator component.", + }, + + // ========================================================================== + // Form Input Components + // ========================================================================== + + Input: { + props: z.object({ + label: z.string(), + name: z.string(), + type: z.enum(["text", "email", "password", "number"]).nullable(), + placeholder: z.string().nullable(), + value: z.string().nullable(), + prefix: z.string().nullable(), + suffix: z.string().nullable(), + allowClear: z.boolean().nullable(), + showCount: z.boolean().nullable(), + maxLength: z.number().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + variant: z.enum(["outlined", "borderless", "filled"]).nullable(), + readOnly: z.boolean().nullable(), + addonBefore: z.string().nullable(), + addonAfter: z.string().nullable(), + disabled: z.boolean().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + status: z.enum(["error", "warning"]).nullable(), + }), + events: ["submit", "focus", "blur", "change"], + description: + "Text input field. Use { $bindState } on value for two-way binding. Use checks for validation.", + example: { + label: "Email", + name: "email", + type: "email", + placeholder: "you@example.com", + }, + }, + + TextArea: { + props: z.object({ + label: z.string(), + name: z.string(), + placeholder: z.string().nullable(), + rows: z.number().nullable(), + value: z.string().nullable(), + allowClear: z.boolean().nullable(), + showCount: z.boolean().nullable(), + maxLength: z.number().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + variant: z.enum(["outlined", "borderless", "filled"]).nullable(), + readOnly: z.boolean().nullable(), + autoSize: z + .union([ + z.boolean(), + z.object({ minRows: z.number(), maxRows: z.number() }), + ]) + .nullable(), + disabled: z.boolean().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + description: + "Multi-line text input. Use { $bindState } on value for binding.", + }, + + InputNumber: { + props: z.object({ + label: z.string(), + name: z.string(), + placeholder: z.string().nullable(), + value: z.number().nullable(), + min: z.number().nullable(), + max: z.number().nullable(), + step: z.number().nullable(), + precision: z.number().nullable(), + prefix: z.string().nullable(), + suffix: z.string().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + variant: z.enum(["outlined", "borderless", "filled"]).nullable(), + disabled: z.boolean().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + events: ["change"], + description: "Number input with controls.", + }, + + Select: { + props: z.object({ + label: z.string(), + name: z.string(), + options: z.array( + z.union([ + z.string(), + z.object({ label: z.string(), value: z.string() }), + ]), + ), + placeholder: z.string().nullable(), + value: z.string().nullable(), + mode: z.enum(["multiple", "tags"]).nullable(), + allowClear: z.boolean().nullable(), + showSearch: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + variant: z.enum(["outlined", "borderless", "filled"]).nullable(), + disabled: z.boolean().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + events: ["change"], + description: + "Dropdown select input. Use { $bindState } on value for binding.", + }, + + Checkbox: { + props: z.object({ + label: z.string(), + name: z.string(), + checked: z.boolean().nullable(), + indeterminate: z.boolean().nullable(), + disabled: z.boolean().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + events: ["change"], + description: "Checkbox input. Use { $bindState } on checked for binding.", + }, + + CheckboxGroup: { + props: z.object({ + label: z.string(), + name: z.string(), + options: z.array( + z.union([ + z.string(), + z.object({ label: z.string(), value: z.string() }), + ]), + ), + value: z.array(z.string()).nullable(), + disabled: z.boolean().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + events: ["change"], + description: "Group of checkboxes.", + }, + + Radio: { + props: z.object({ + label: z.string(), + name: z.string(), + options: z.array( + z.union([ + z.string(), + z.object({ label: z.string(), value: z.string() }), + ]), + ), + value: z.string().nullable(), + optionType: z.enum(["default", "button"]).nullable(), + disabled: z.boolean().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + events: ["change"], + description: "Radio button group. Use { $bindState } on value for binding.", + }, + + Switch: { + props: z.object({ + label: z.string(), + name: z.string(), + checked: z.boolean().nullable(), + checkedChildren: z.string().nullable(), + unCheckedChildren: z.string().nullable(), + disabled: z.boolean().nullable(), + checks: validationCheckSchema, + validateOn: validateOnSchema, + }), + events: ["change"], + description: "Toggle switch. Use { $bindState } on checked for binding.", + }, + + Slider: { + props: z.object({ + label: z.string().nullable(), + name: z.string().nullable(), + min: z.number().nullable(), + max: z.number().nullable(), + step: z.number().nullable(), + value: z.union([z.number(), z.array(z.number())]).nullable(), + range: z.boolean().nullable(), + disabled: z.boolean().nullable(), + marks: z + .record( + z.string(), + z.union([ + z.string(), + z.object({ style: z.any(), label: z.string() }), + ]), + ) + .nullable(), + }), + events: ["change"], + description: "Range slider input. Use { $bindState } on value for binding.", + }, + + Rate: { + props: z.object({ + label: z.string().nullable(), + name: z.string().nullable(), + count: z.number().nullable(), + value: z.number().nullable(), + allowHalf: z.boolean().nullable(), + allowClear: z.boolean().nullable(), + disabled: z.boolean().nullable(), + }), + events: ["change"], + description: "Star rating component.", + }, + + DatePicker: { + props: z.object({ + label: z.string(), + name: z.string(), + placeholder: z.string().nullable(), + value: z.string().nullable(), + format: z.string().nullable(), + picker: z.enum(["date", "week", "month", "quarter", "year"]).nullable(), + showTime: z.boolean().nullable(), + disabled: z.boolean().nullable(), + }), + events: ["change"], + description: "Date picker input.", + }, + + TimePicker: { + props: z.object({ + label: z.string(), + name: z.string(), + placeholder: z.string().nullable(), + value: z.string().nullable(), + format: z.string().nullable(), + disabled: z.boolean().nullable(), + }), + events: ["change"], + description: "Time picker input.", + }, + + Upload: { + props: z.object({ + label: z.string(), + name: z.string(), + accept: z.string().nullable(), + multiple: z.boolean().nullable(), + maxCount: z.number().nullable(), + listType: z.enum(["text", "picture", "picture-card"]).nullable(), + buttonText: z.string().nullable(), + disabled: z.boolean().nullable(), + }), + events: ["change"], + description: "File upload component.", + }, + + Transfer: { + props: z.object({ + label: z.string(), + dataSource: z.array( + z.object({ + key: z.string(), + title: z.string(), + description: z.string().nullable(), + }), + ), + targetKeys: z.array(z.string()).nullable(), + titles: z.array(z.string()).nullable(), + disabled: z.boolean().nullable(), + }), + events: ["change"], + description: "Transfer items between two columns.", + }, + + AutoComplete: { + props: z.object({ + label: z.string(), + name: z.string(), + options: z.array( + z.union([ + z.string(), + z.object({ label: z.string(), value: z.string() }), + ]), + ), + placeholder: z.string().nullable(), + value: z.string().nullable(), + allowClear: z.boolean().nullable(), + disabled: z.boolean().nullable(), + status: z.enum(["error", "warning"]).nullable(), + }), + events: ["change", "select"], + description: "AutoComplete input with suggestions.", + }, + + Cascader: { + props: z.object({ + label: z.string(), + name: z.string(), + options: z.array( + z.object({ + label: z.string(), + value: z.string(), + children: z.array(z.any()).nullable(), + }), + ), + placeholder: z.string().nullable(), + value: z.array(z.string()).nullable(), + allowClear: z.boolean().nullable(), + showSearch: z.boolean().nullable(), + disabled: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + }), + events: ["change"], + description: "Cascader selection for hierarchical data.", + }, + + ColorPicker: { + props: z.object({ + label: z.string(), + name: z.string(), + value: z.string().nullable(), + showText: z.boolean().nullable(), + disabled: z.boolean().nullable(), + allowClear: z.boolean().nullable(), + format: z.enum(["hex", "rgb", "hsl"]).nullable(), + }), + events: ["change"], + description: "Color picker component.", + }, + + Mentions: { + props: z.object({ + label: z.string(), + name: z.string(), + options: z.array( + z.object({ + value: z.string(), + label: z.string(), + }), + ), + placeholder: z.string().nullable(), + value: z.string().nullable(), + autoSize: z + .union([ + z.boolean(), + z.object({ minRows: z.number(), maxRows: z.number() }), + ]) + .nullable(), + disabled: z.boolean().nullable(), + }), + events: ["change"], + description: "Mentions input for @-tagging.", + }, + + TreeSelect: { + props: z.object({ + label: z.string(), + name: z.string(), + treeData: z.array( + z.object({ + key: z.string(), + title: z.string(), + value: z.string(), + children: z.array(z.any()).nullable(), + }), + ), + placeholder: z.string().nullable(), + value: z.string().nullable(), + allowClear: z.boolean().nullable(), + showSearch: z.boolean().nullable(), + multiple: z.boolean().nullable(), + disabled: z.boolean().nullable(), + treeCheckable: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + }), + events: ["change"], + description: "Tree select dropdown component.", + }, + + // ========================================================================== + // Action Components + // ========================================================================== + + Button: { + props: z.object({ + label: z.string(), + type: z.enum(["primary", "default", "dashed", "text", "link"]).nullable(), + danger: z.boolean().nullable(), + disabled: z.boolean().nullable(), + loading: z.boolean().nullable(), + icon: z.string().nullable(), + block: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + ghost: z.boolean().nullable(), + shape: z.enum(["default", "circle", "round"]).nullable(), + href: z.string().nullable(), + target: z.enum(["_blank", "_self", "_parent", "_top"]).nullable(), + htmlType: z.enum(["button", "submit", "reset"]).nullable(), + }), + events: ["press"], + description: "Clickable button. Bind on.press for handler.", + example: { label: "Submit", type: "primary" }, + }, + + ButtonGroup: { + props: z.object({ + buttons: z.array( + z.object({ + label: z.string(), + value: z.string(), + type: z + .enum(["primary", "default", "dashed", "text", "link"]) + .nullable(), + }), + ), + selected: z.string().nullable(), + }), + events: ["change"], + description: + "Segmented button group. Use { $bindState } on selected for selected value.", + }, + + Link: { + props: z.object({ + label: z.string(), + href: z.string(), + target: z.enum(["_blank", "_self", "_parent", "_top"]).nullable(), + disabled: z.boolean().nullable(), + }), + events: ["press"], + description: "Anchor link. Bind on.press for click handler.", + }, + + Pagination: { + props: z.object({ + total: z.number(), + pageSize: z.number().nullable(), + current: z.number().nullable(), + showSizeChanger: z.boolean().nullable(), + showQuickJumper: z.boolean().nullable(), + simple: z.boolean().nullable(), + disabled: z.boolean().nullable(), + size: z.enum(["default", "small"]).nullable(), + hideOnSinglePage: z.boolean().nullable(), + pageSizeOptions: z.array(z.number()).nullable(), + align: z.enum(["start", "center", "end"]).nullable(), + }), + events: ["change"], + description: + "Page navigation. Use { $bindState } on current for current page number.", + }, + + Segmented: { + props: z.object({ + options: z.array( + z.union([ + z.string(), + z.object({ + label: z.string(), + value: z.string(), + icon: z.string().nullable(), + }), + ]), + ), + value: z.string().nullable(), + block: z.boolean().nullable(), + disabled: z.boolean().nullable(), + size: z.enum(["small", "middle", "large"]).nullable(), + }), + events: ["change"], + description: "Segmented control for toggling between options.", + }, + + Steps: { + props: z.object({ + items: z.array( + z.object({ + title: z.string(), + description: z.string().nullable(), + subTitle: z.string().nullable(), + icon: z.string().nullable(), + disabled: z.boolean().nullable(), + status: z.enum(["wait", "process", "finish", "error"]).nullable(), + }), + ), + current: z.number().nullable(), + direction: z.enum(["horizontal", "vertical"]).nullable(), + status: z.enum(["wait", "process", "finish", "error"]).nullable(), + size: z.enum(["default", "small"]).nullable(), + type: z.enum(["default", "navigation", "inline"]).nullable(), + initial: z.number().nullable(), + labelPlacement: z.enum(["horizontal", "vertical"]).nullable(), + percent: z.number().nullable(), + }), + events: ["change"], + description: "Step navigation bar.", + }, + + Result: { + props: z.object({ + status: z + .enum(["success", "error", "info", "warning", "404", "403", "500"]) + .nullable(), + title: z.string(), + subTitle: z.string().nullable(), + }), + slots: ["default"], + description: "Result page for success/error states.", + }, +}; + +// ============================================================================= +// Types +// ============================================================================= + +/** + * Type for a component definition + */ +export type ComponentDefinition = { + props: z.ZodType; + slots?: string[]; + events?: string[]; + description: string; + example?: Record; +}; + +/** + * Infer the props type for an antd component by name. + * Derives the TypeScript type directly from the Zod schema, + * so component implementations stay in sync with catalog definitions. + * + * @example + * ```ts + * type CardProps = AntdProps<"Card">; + * // { title: string | null; description: string | null; ... } + * ``` + */ +export type AntdProps = + z.output<(typeof antdComponentDefinitions)[K]["props"]>; + +/** + * Bindings configuration for state binding paths. + * Used for two-way data binding with state management. + */ +export type BindingsConfig = { + [key: string]: string | undefined; +}; + +/** + * Event emit function type + */ +export type EmitFunction = (eventName: string) => void; diff --git a/packages/antd/src/components.tsx b/packages/antd/src/components.tsx new file mode 100644 index 00000000..8c33acbc --- /dev/null +++ b/packages/antd/src/components.tsx @@ -0,0 +1,2121 @@ +"use client"; + +import { Children, useState } from "react"; +import dayjs from "dayjs"; +import { + useBoundProp, + useStateBinding, + useFieldValidation, + type BaseComponentProps, +} from "@json-render/react"; +import type { ReactNode } from "react"; + +/** BaseComponentProps extended with slots for components that use slotted content. */ +type SlottedComponentProps

= BaseComponentProps

& { + slots?: Record; +}; + +import { + Card, + Space, + Divider, + Row, + Col, + Masonry, + Layout, + Tabs, + Collapse, + Menu, + Modal, + Drawer, + Popover, + Tooltip, + Dropdown, + Table, + Typography, + Image, + Avatar, + Badge, + Tag, + Alert, + Progress, + Skeleton, + Spin, + Empty, + Statistic, + Descriptions, + Timeline, + Carousel, + Calendar, + List, + Tree, + QRCode, + Input, + InputNumber, + Select, + Checkbox, + Radio, + Switch, + Slider, + Rate, + DatePicker, + TimePicker, + Upload, + Transfer, + AutoComplete, + Cascader, + ColorPicker, + Mentions, + TreeSelect, + Button, + Pagination, + Segmented, + Steps, + Result, + Flex, + Form, + Affix, + Anchor, + Breadcrumb, + BackTop, +} from "antd"; +import type { UploadFile } from "antd/es/upload/interface"; +import type { AntdProps } from "./catalog"; + +const { + Title, + Text: AntText, + Paragraph: AntParagraph, + Link: AntLink, +} = Typography; + +// ============================================================================= +// Standard Component Implementations +// ============================================================================= + +/** + * Ant Design component implementations for json-render. + * + * Pass to `defineRegistry()` from `@json-render/react` to create a + * component registry for rendering JSON specs with Ant Design components. + * + * @example + * ```ts + * import { defineRegistry } from "@json-render/react"; + * import { antdComponents } from "@json-render/antd"; + * + * const { registry } = defineRegistry(catalog, { + * components: { + * Card: antdComponents.Card, + * Button: antdComponents.Button, + * }, + * }); + * ``` + */ +export const antdComponents = { + // ── Layout ──────────────────────────────────────────────────────────── + + Layout: ({ props, children }: BaseComponentProps>) => { + return {children}; + }, + + LayoutHeader: ({ + children, + }: BaseComponentProps>) => { + return {children}; + }, + + LayoutContent: ({ + children, + }: BaseComponentProps>) => { + return {children}; + }, + + LayoutFooter: ({ + children, + }: BaseComponentProps>) => { + return {children}; + }, + + LayoutSider: ({ + props, + children, + emit, + }: BaseComponentProps>) => { + return ( + emit("collapse")} + > + {children} + + ); + }, + + Card: ({ + props, + children, + slots, + }: SlottedComponentProps>) => { + const extraSlot = slots?.extra?.[0]; + const coverSlot = slots?.cover?.[0]; + const actionsSlots = slots?.actions ?? []; + + return ( + {props.extra} : undefined) + } + variant={props.variant ?? "outlined"} + hoverable={props.hoverable ?? false} + loading={props.loading ?? false} + size={props.size ?? "default"} + cover={ + coverSlot ?? + (props.cover ? cover : undefined) + } + actions={ + actionsSlots.length > 0 + ? actionsSlots + : props.actions + ? props.actions.map((action, idx) => ( + {action} + )) + : undefined + } + > + {children} + + ); + }, + + Flex: ({ props, children }: BaseComponentProps>) => { + return ( + + {children} + + ); + }, + + Stack: ({ props, children }: BaseComponentProps>) => { + return ( + + {children} + + ); + }, + + Grid: ({ props, children }: BaseComponentProps>) => { + const columns = props.columns ?? 3; + const span = Math.floor(24 / Math.min(Math.max(columns, 1), 6)); + const gapMap = { sm: 8, md: 16, lg: 24 } as const; + const gutter = props.gap ? gapMap[props.gap] : 16; + const childArray = Children.toArray(children); + return ( + + {childArray.map((child, i) => ( + + {child} + + ))} + + ); + }, + + Row: ({ props, children }: BaseComponentProps>) => { + return ( + + {children} + + ); + }, + + Col: ({ props, children }: BaseComponentProps>) => { + return ( + + {children} + + ); + }, + + Masonry: ({ props, children }: BaseComponentProps>) => { + const childArray = Array.isArray(children) + ? children + : children + ? [children] + : []; + const items = childArray.map((child, index) => ({ + key: index, + children: child, + data: {}, + })); + + return ( + + ); + }, + + Divider: ({ props }: BaseComponentProps>) => { + return ( + + {props.text} + + ); + }, + + Space: ({ props, children }: BaseComponentProps>) => { + const sizeMap: Record = { + small: 8, + middle: 16, + large: 24, + }; + + return ( + + {children} + + ); + }, + + // ── Navigation ───────────────────────────────────────────────────────── + + Tabs: ({ + props, + slots, + bindings, + emit, + }: SlottedComponentProps>) => { + const tabs = props.tabs ?? []; + const tabContents = slots?.tabs ?? []; + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState( + props.defaultValue ?? tabs[0]?.value ?? "", + ); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? tabs[0]?.value ?? "") : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + + const items = tabs.map((tab, index) => ({ + key: tab.value, + label: tab.label, + children: tabContents[index] ?? null, + })); + + return ( + { + setValue(key); + emit("change"); + }} + tabPosition={props.position ?? "top"} + type={props.type ?? "line"} + centered={props.centered ?? false} + size={props.size ?? undefined} + tabBarGutter={props.tabBarGutter ?? undefined} + destroyInactiveTabPane={props.destroyInactiveTabPane ?? false} + items={items} + /> + ); + }, + + Collapse: ({ + props, + slots, + }: SlottedComponentProps>) => { + const items = props.items ?? []; + const itemContents = slots?.items ?? []; + + const collapseItems = items.map((item, idx) => ({ + key: String(idx), + label: item.title, + children: itemContents[idx] ?? null, + })); + + return ( + + ); + }, + + Menu: ({ props, emit }: BaseComponentProps>) => { + const items = props.items ?? []; + + const menuItems = items.map((item) => ({ + key: item.key, + label: item.label, + icon: item.icon ? : undefined, + })); + + return ( +

emit("select")} + /> + ); + }, + + Affix: ({ props, children }: BaseComponentProps>) => { + return ( + + {children} + + ); + }, + + Anchor: ({ props, emit }: BaseComponentProps>) => { + const items = (props.items ?? []).map((item) => ({ + key: item.key, + title: item.title, + href: item.href ?? `#${item.key}`, + })); + + return ( + emit("change")} + onClick={(e, link) => emit("click")} + /> + ); + }, + + Breadcrumb: ({ props }: BaseComponentProps>) => { + const items = (props.items ?? []).map((item) => ({ + title: item.title, + href: item.href ?? undefined, + })); + + return ; + }, + + BackTop: ({ props, children }: BaseComponentProps>) => { + return ( + + {children} + + ); + }, + + // ── Overlay ──────────────────────────────────────────────────────────── + + Modal: ({ + props, + children, + emit, + }: BaseComponentProps>) => { + const [open, setOpen] = useStateBinding(props.openPath ?? ""); + + return ( + { + emit("ok"); + setOpen(false); + }} + onCancel={() => { + emit("cancel"); + setOpen(false); + }} + width={props.width ?? 520} + footer={props.footer === false ? null : undefined} + centered={props.centered ?? false} + closable={props.closable ?? true} + maskClosable={props.maskClosable ?? true} + okText={props.okText ?? undefined} + cancelText={props.cancelText ?? undefined} + confirmLoading={props.confirmLoading ?? false} + destroyOnClose={props.destroyOnClose ?? false} + keyboard={props.keyboard ?? true} + loading={props.loading ?? false} + > + {props.description && {props.description}} + {children} + + ); + }, + + Drawer: ({ + props, + children, + emit, + }: BaseComponentProps>) => { + const [open, setOpen] = useStateBinding(props.openPath ?? ""); + + return ( + { + emit("close"); + setOpen(false); + }} + placement={props.placement ?? "right"} + width={props.width ?? 378} + height={props.height ?? undefined} + closable={props.closable ?? true} + maskClosable={props.maskClosable ?? true} + destroyOnClose={props.destroyOnClose ?? false} + keyboard={props.keyboard ?? true} + loading={props.loading ?? false} + size={props.size ?? undefined} + > + {props.description && {props.description}} + {children} + + ); + }, + + Popover: ({ props }: BaseComponentProps>) => { + return ( + + {props.trigger} + + ); + }, + + Tooltip: ({ props }: BaseComponentProps>) => { + return ( + + {props.text} + + ); + }, + + Dropdown: ({ + props, + children, + emit, + bindings, + }: SlottedComponentProps>) => { + const items = props.items ?? []; + + const menuItems = items + .filter((item) => !item.divider) + .map((item) => ({ + key: item.key, + label: item.label, + icon: item.icon ? : undefined, + danger: item.danger ?? false, + disabled: item.disabled ?? false, + })); + + const [boundOpen, setBoundOpen] = useBoundProp( + props.open as boolean | undefined, + bindings?.open, + ); + const isControlled = + bindings?.open !== undefined || props.open !== undefined; + const [localOpen, setLocalOpen] = useState(props.defaultOpen ?? false); + const open = isControlled ? (boundOpen ?? false) : localOpen; + const setOpen = isControlled ? setBoundOpen : setLocalOpen; + + return ( + { + emit("select"); + }, + }} + trigger={props.trigger ? [props.trigger] : undefined} + placement={props.placement ?? undefined} + arrow={props.arrow ?? false} + disabled={props.disabled ?? false} + open={open} + onOpenChange={(visible) => { + setOpen(visible); + emit("openChange"); + emit("visibleChange"); + }} + > + {children} + + ); + }, + + // ── Data Display ─────────────────────────────────────────────────────── + + Table: ({ props }: BaseComponentProps>) => { + const columns = props.columns ?? []; + const rows = props.rows ?? []; + + const dataSource = rows.map((row, idx) => ({ + key: String(idx), + ...row.reduce( + (acc, cell, colIdx) => { + acc[`col${colIdx}`] = cell; + return acc; + }, + {} as Record, + ), + })); + + const tableColumns = columns.map((col, idx) => ({ + title: col, + dataIndex: `col${idx}`, + key: `col${idx}`, + })); + + const paginationConfig = + props.pagination === false + ? false + : props.pagination === true || props.pagination == null + ? false + : { + pageSize: props.pagination.pageSize ?? undefined, + current: props.pagination.current ?? undefined, + total: props.pagination.total ?? undefined, + showSizeChanger: props.pagination.showSizeChanger ?? false, + showQuickJumper: props.pagination.showQuickJumper ?? false, + simple: props.pagination.simple ?? false, + hideOnSinglePage: props.pagination.hideOnSinglePage ?? false, + }; + + return ( + {props.caption} + : undefined + } + pagination={paginationConfig} + scroll={ + props.scroll + ? { x: props.scroll.x ?? undefined, y: props.scroll.y ?? undefined } + : undefined + } + showHeader={props.showHeader ?? true} + rowKey={props.rowKey ?? "key"} + sticky={props.sticky ?? false} + /> + ); + }, + + Heading: ({ props }: BaseComponentProps>) => { + const levelMap: Record = { + h1: 1, + h2: 2, + h3: 3, + h4: 4, + h5: 5, + }; + + return ( + {props.text} + ); + }, + + Text: ({ props }: BaseComponentProps>) => { + return ( + + {props.text} + + ); + }, + + Paragraph: ({ props }: BaseComponentProps>) => { + return ( + + {props.text} + + ); + }, + + Image: ({ props }: BaseComponentProps>) => { + return ( + + ); + }, + + Avatar: ({ props }: BaseComponentProps>) => { + const name = props.name || "?"; + + return ( + : undefined} + alt={props.alt ?? undefined} + gap={props.gap ?? undefined} + > + {!props.src && name.charAt(0).toUpperCase()} + + ); + }, + + Badge: ({ props, children }: BaseComponentProps>) => { + const hasChildren = !!children; + + if (!hasChildren) { + return ( + + ); + } + + return ( + + {children} + + ); + }, + + Tag: ({ props, emit }: BaseComponentProps>) => { + return ( + : undefined} + onClose={(e) => { + e.preventDefault(); + emit("close"); + }} + > + {props.text} + + ); + }, + + Alert: ({ props, emit }: BaseComponentProps>) => { + return ( + emit("close")} + /> + ); + }, + + Progress: ({ props }: BaseComponentProps>) => { + const max = props.max ?? 100; + const value = Math.min(max, Math.max(0, props.value ?? 0)); + const percent = Math.round((value / max) * 100); + + return ( + props.label ?? `${percent}%`} + /> + ); + }, + + Skeleton: ({ + props, + children, + }: BaseComponentProps>) => { + return ( + + {children} + + ); + }, + + Spin: ({ props, children }: BaseComponentProps>) => { + return ( + + {children} + + ); + }, + + Empty: ({ props }: BaseComponentProps>) => { + return ; + }, + + Statistic: ({ props }: BaseComponentProps>) => { + return ( + + ); + }, + + Descriptions: ({ props }: BaseComponentProps>) => { + const items = props.items ?? []; + + return ( + ({ + key: item.label, + label: item.label, + children: item.value, + span: item.span ?? undefined, + }))} + /> + ); + }, + + Timeline: ({ + props, + slots, + }: SlottedComponentProps>) => { + const items = props.items ?? []; + const itemContents = slots?.items ?? []; + + return ( + ({ + color: item.color ?? undefined, + children: itemContents[idx] ?? null, + }))} + /> + ); + }, + + Carousel: ({ + props, + children, + }: BaseComponentProps>) => { + return ( + + {children} + + ); + }, + + Calendar: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(null); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? null) : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + + const dayjsValue = value ? dayjs(value) : undefined; + + return ( + { + setValue(date.format("YYYY-MM-DD")); + emit("change"); + }} + onSelect={(date) => { + setValue(date.format("YYYY-MM-DD")); + emit("select"); + }} + /> + ); + }, + + List: ({ props, children }: BaseComponentProps>) => { + const pagination = + props.pagination === false + ? false + : props.pagination === true + ? { pageSize: 10 } + : props.pagination + ? { + pageSize: props.pagination.pageSize ?? 10, + total: props.pagination.total ?? undefined, + } + : false; + + const grid = props.grid + ? { + gutter: props.grid.gutter ?? undefined, + column: props.grid.column ?? undefined, + } + : undefined; + + return ( + { + const childArray = Array.isArray(children) + ? children + : children + ? [children] + : []; + return {childArray[index] ?? null}; + }} + /> + ); + }, + + Tree: ({ props, bindings, emit }: BaseComponentProps>) => { + const [boundCheckedKeys, setBoundCheckedKeys] = useBoundProp( + props.checkedKeys as string[] | undefined, + bindings?.checkedKeys, + ); + const [localCheckedKeys, setLocalCheckedKeys] = useState([]); + const isBoundChecked = !!bindings?.checkedKeys; + const checkedKeys = isBoundChecked + ? (boundCheckedKeys ?? []) + : localCheckedKeys; + const setCheckedKeys = isBoundChecked + ? setBoundCheckedKeys + : setLocalCheckedKeys; + + // Transform treeData to handle null children -> undefined + const transformTreeData = (data: unknown[]): unknown[] => { + return data.map((item: any) => ({ + key: item.key, + title: item.title, + ...(item.children + ? { children: transformTreeData(item.children) } + : {}), + })); + }; + + return ( + { + setCheckedKeys(checked as string[]); + emit("check"); + }} + onExpand={(expandedKeys) => emit("expand")} + onSelect={(selectedKeys) => emit("select")} + /> + ); + }, + + QRCode: ({ props }: BaseComponentProps>) => { + return ( + + ); + }, + + // ── Form Inputs ──────────────────────────────────────────────────────── + + Input: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(""); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? "") : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + const validateOn = props.validateOn ?? "blur"; + + const hasValidation = !!(bindings?.value && props.checks?.length); + const { errors, validate } = useFieldValidation( + bindings?.value ?? "", + hasValidation ? { checks: props.checks ?? [], validateOn } : undefined, + ); + + return ( + 0 ? "error" : (props.status ?? undefined) + } + help={errors[0]} + > + : undefined} + suffix={props.suffix ? : undefined} + allowClear={props.allowClear ?? false} + showCount={props.showCount ?? false} + maxLength={props.maxLength ?? undefined} + size={props.size ?? undefined} + variant={props.variant ?? undefined} + readOnly={props.readOnly ?? false} + addonBefore={props.addonBefore ?? undefined} + addonAfter={props.addonAfter ?? undefined} + disabled={props.disabled ?? false} + status={props.status ?? (errors.length > 0 ? "error" : undefined)} + onChange={(e) => { + setValue(e.target.value); + if (hasValidation && validateOn === "change") validate(); + emit("change"); + }} + onFocus={() => emit("focus")} + onBlur={() => { + if (hasValidation && validateOn === "blur") validate(); + emit("blur"); + }} + onPressEnter={() => emit("submit")} + /> + + ); + }, + + TextArea: ({ + props, + bindings, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(""); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? "") : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + const validateOn = props.validateOn ?? "blur"; + + const hasValidation = !!(bindings?.value && props.checks?.length); + const { errors, validate } = useFieldValidation( + bindings?.value ?? "", + hasValidation ? { checks: props.checks ?? [], validateOn } : undefined, + ); + + return ( + 0 ? "error" : undefined} + help={errors[0]} + > + 0 ? "error" : undefined} + onChange={(e) => { + setValue(e.target.value); + if (hasValidation && validateOn === "change") validate(); + }} + onBlur={() => { + if (hasValidation && validateOn === "blur") validate(); + }} + /> + + ); + }, + + InputNumber: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as number | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(null); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? null) : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + + const validateOn = props.validateOn ?? "change"; + const hasValidation = !!(bindings?.value && props.checks?.length); + const { errors, validate } = useFieldValidation( + bindings?.value ?? "", + hasValidation ? { checks: props.checks ?? [], validateOn } : undefined, + ); + + return ( + 0 ? "error" : undefined} + help={errors[0]} + > + : undefined} + addonAfter={ + props.suffix ? : undefined + } + status={errors.length > 0 ? "error" : undefined} + onChange={(val) => { + setValue(val as number); + if (hasValidation && validateOn === "change") validate(); + emit("change"); + }} + /> + + ); + }, + + Select: ({ + props, + bindings, + emit, + }: BaseComponentProps>) => { + const [boundValue, setBoundValue] = useBoundProp( + props.value as string | undefined, + bindings?.value, + ); + const [localValue, setLocalValue] = useState(""); + const isBound = !!bindings?.value; + const value = isBound ? (boundValue ?? "") : localValue; + const setValue = isBound ? setBoundValue : setLocalValue; + const validateOn = props.validateOn ?? "change"; + + const hasValidation = !!(bindings?.value && props.checks?.length); + const { errors, validate } = useFieldValidation( + bindings?.value ?? "", + hasValidation ? { checks: props.checks ?? [], validateOn } : undefined, + ); + + const options = (props.options ?? []).map((opt) => + typeof opt === "string" ? { label: opt, value: opt } : opt, + ); + + return ( + 0 ? "error" : undefined} + help={errors[0]} + > +