Add Zod runtime validation schemas for all UI components#193
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
📦 Bundle Size Report
Size Limits
|
There was a problem hiding this comment.
Pull request overview
This PR implements comprehensive Zod runtime validation schemas for 80+ ObjectUI components, following the @objectstack/spec UI specification format. The implementation adds validation for all component categories including layout, forms, data display, feedback, disclosure, overlay, navigation, and complex components.
Changes:
- Added Zod dependency (^3.22.4) to @object-ui/types package
- Created 10 Zod schema modules (2.3k LOC) covering all component types with recursive support via z.lazy()
- Exported schemas via new
@object-ui/types/zodpath for runtime validation - Included comprehensive README documentation and validation examples
Reviewed changes
Copilot reviewed 13 out of 14 changed files in this pull request and generated 12 comments.
Show a summary per file
| File | Description |
|---|---|
| pnpm-lock.yaml | Adds zod@3.25.76 to satisfy ^3.22.4 dependency |
| packages/types/package.json | Adds zod peer dependency and exports configuration for /zod path |
| packages/types/src/zod/base.zod.ts | Defines foundation BaseSchema with passthrough for extensions |
| packages/types/src/zod/layout.zod.ts | Implements 17 layout component schemas (Div, Flex, Grid, Card, Tabs, etc.) |
| packages/types/src/zod/form.zod.ts | Implements 17 form component schemas with validation rules and field conditions |
| packages/types/src/zod/data-display.zod.ts | Implements 13 data display schemas including recursive TreeNodeSchema |
| packages/types/src/zod/feedback.zod.ts | Implements 8 feedback component schemas (Loading, Progress, Toast, etc.) |
| packages/types/src/zod/disclosure.zod.ts | Implements 3 disclosure component schemas (Accordion, Collapsible, ToggleGroup) |
| packages/types/src/zod/overlay.zod.ts | Implements 10 overlay schemas with recursive MenuItemSchema |
| packages/types/src/zod/navigation.zod.ts | Implements 6 navigation schemas with recursive NavLinkSchema |
| packages/types/src/zod/complex.zod.ts | Implements 5 complex component schemas (Kanban, Calendar, FilterBuilder, etc.) |
| packages/types/src/zod/index.zod.ts | Central export file with AnyComponentSchema union and version info |
| packages/types/src/zod/README.md | Comprehensive documentation with usage examples and integration guides |
| packages/types/examples/zod-validation-example.ts | Example demonstrating validation for 6 component types |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
| onValueChange: z.function().optional().describe('Value change handler'), | ||
| variant: z.enum(['default', 'bordered', 'separated']).optional().describe('Accordion variant'), | ||
| }); | ||
|
|
||
| /** | ||
| * Collapsible Schema - Collapsible component | ||
| */ | ||
| export const CollapsibleSchema = BaseSchema.extend({ | ||
| type: z.literal('collapsible'), | ||
| trigger: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).describe('Trigger content'), | ||
| content: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).describe('Collapsible content'), | ||
| defaultOpen: z.boolean().optional().describe('Default open state'), | ||
| open: z.boolean().optional().describe('Controlled open state'), | ||
| disabled: z.boolean().optional().describe('Whether collapsible is disabled'), | ||
| onOpenChange: z.function().optional().describe('Open change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Toggle Group Item Schema | ||
| */ | ||
| export const ToggleGroupItemSchema = z.object({ | ||
| value: z.string().describe('Item value'), | ||
| label: z.string().describe('Item label'), | ||
| icon: z.string().optional().describe('Item icon'), | ||
| disabled: z.boolean().optional().describe('Whether item is disabled'), | ||
| }); | ||
|
|
||
| /** | ||
| * Toggle Group Schema - Toggle group component | ||
| */ | ||
| export const ToggleGroupSchema = BaseSchema.extend({ | ||
| type: z.literal('toggle-group'), | ||
| selectionType: z.enum(['single', 'multiple']).optional().describe('Selection type'), | ||
| variant: z.enum(['default', 'outline']).optional().describe('Toggle group variant'), | ||
| size: z.enum(['default', 'sm', 'lg']).optional().describe('Toggle group size'), | ||
| items: z.array(ToggleGroupItemSchema).optional().describe('Toggle group items'), | ||
| defaultValue: z.union([z.string(), z.array(z.string())]).optional().describe('Default value(s)'), | ||
| value: z.union([z.string(), z.array(z.string())]).optional().describe('Controlled value(s)'), | ||
| disabled: z.boolean().optional().describe('Whether toggle group is disabled'), | ||
| onValueChange: z.function().optional().describe('Value change handler'), |
There was a problem hiding this comment.
Using z.function() without type parameters for event handlers (onValueChange, onOpenChange) reduces type safety. These should specify exact signatures. For example, onValueChange should use z.function(z.tuple([z.union([z.string(), z.array(z.string())])]), z.void()) and onOpenChange should use z.function(z.tuple([z.boolean()]), z.void()).
| onCardMove: z.function().optional().describe('Card move handler'), | ||
| onCardClick: z.function().optional().describe('Card click handler'), | ||
| onColumnAdd: z.function().optional().describe('Column add handler'), | ||
| onCardAdd: z.function().optional().describe('Card add handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Calendar View Mode | ||
| */ | ||
| export const CalendarViewModeSchema = z.enum(['month', 'week', 'day', 'agenda']); | ||
|
|
||
| /** | ||
| * Calendar Event Schema | ||
| */ | ||
| export const CalendarEventSchema = z.object({ | ||
| id: z.string().describe('Event ID'), | ||
| title: z.string().describe('Event title'), | ||
| description: z.string().optional().describe('Event description'), | ||
| start: z.union([z.string(), z.date()]).describe('Event start time'), | ||
| end: z.union([z.string(), z.date()]).describe('Event end time'), | ||
| allDay: z.boolean().optional().describe('Whether event is all-day'), | ||
| color: z.string().optional().describe('Event color'), | ||
| data: z.any().optional().describe('Custom event data'), | ||
| }); | ||
|
|
||
| /** | ||
| * Calendar View Schema - Calendar component | ||
| */ | ||
| export const CalendarViewSchema = BaseSchema.extend({ | ||
| type: z.literal('calendar-view'), | ||
| events: z.array(CalendarEventSchema).describe('Calendar events'), | ||
| defaultView: CalendarViewModeSchema.optional().describe('Default view mode'), | ||
| view: CalendarViewModeSchema.optional().describe('Controlled view mode'), | ||
| defaultDate: z.union([z.string(), z.date()]).optional().describe('Default date'), | ||
| date: z.union([z.string(), z.date()]).optional().describe('Controlled date'), | ||
| views: z.array(CalendarViewModeSchema).optional().describe('Available views'), | ||
| editable: z.boolean().optional().describe('Whether events are editable'), | ||
| onEventClick: z.function().optional().describe('Event click handler'), | ||
| onEventCreate: z.function().optional().describe('Event create handler'), | ||
| onEventUpdate: z.function().optional().describe('Event update handler'), | ||
| onDateChange: z.function().optional().describe('Date change handler'), | ||
| onViewChange: z.function().optional().describe('View change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Filter Operator Enum | ||
| */ | ||
| export const FilterOperatorSchema = z.enum([ | ||
| 'equals', | ||
| 'not_equals', | ||
| 'contains', | ||
| 'not_contains', | ||
| 'starts_with', | ||
| 'ends_with', | ||
| 'greater_than', | ||
| 'greater_than_or_equal', | ||
| 'less_than', | ||
| 'less_than_or_equal', | ||
| 'in', | ||
| 'not_in', | ||
| 'is_null', | ||
| 'is_not_null', | ||
| ]); | ||
|
|
||
| /** | ||
| * Filter Condition Schema | ||
| */ | ||
| export const FilterConditionSchema: z.ZodType<any> = z.lazy(() => | ||
| z.object({ | ||
| field: z.string().describe('Field name'), | ||
| operator: FilterOperatorSchema.describe('Filter operator'), | ||
| value: z.any().optional().describe('Filter value'), | ||
| }) | ||
| ); | ||
|
|
||
| /** | ||
| * Filter Group Schema | ||
| */ | ||
| export const FilterGroupSchema: z.ZodType<any> = z.lazy(() => | ||
| z.object({ | ||
| operator: z.enum(['and', 'or']).describe('Group operator'), | ||
| conditions: z.array(z.union([FilterConditionSchema, FilterGroupSchema])).describe('Conditions or sub-groups'), | ||
| }) | ||
| ); | ||
|
|
||
| /** | ||
| * Filter Field Schema | ||
| */ | ||
| export const FilterFieldSchema = z.object({ | ||
| name: z.string().describe('Field name'), | ||
| label: z.string().describe('Field label'), | ||
| type: z.enum(['string', 'number', 'date', 'boolean', 'select']).describe('Field type'), | ||
| operators: z.array(FilterOperatorSchema).optional().describe('Available operators'), | ||
| options: z.array(z.object({ | ||
| label: z.string(), | ||
| value: z.any(), | ||
| })).optional().describe('Options for select type'), | ||
| }); | ||
|
|
||
| /** | ||
| * Filter Builder Schema - Filter builder component | ||
| */ | ||
| export const FilterBuilderSchema = BaseSchema.extend({ | ||
| type: z.literal('filter-builder'), | ||
| fields: z.array(FilterFieldSchema).describe('Available filter fields'), | ||
| defaultValue: z.union([FilterConditionSchema, FilterGroupSchema]).optional().describe('Default filter value'), | ||
| value: z.union([FilterConditionSchema, FilterGroupSchema]).optional().describe('Controlled filter value'), | ||
| onChange: z.function().optional().describe('Change handler'), | ||
| allowGroups: z.boolean().optional().describe('Allow grouped conditions'), | ||
| maxDepth: z.number().optional().describe('Maximum nesting depth'), | ||
| }); | ||
|
|
||
| /** | ||
| * Carousel Item Schema | ||
| */ | ||
| export const CarouselItemSchema = z.object({ | ||
| id: z.string().optional().describe('Item ID'), | ||
| content: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).describe('Item content'), | ||
| }); | ||
|
|
||
| /** | ||
| * Carousel Schema - Carousel component | ||
| */ | ||
| export const CarouselSchema = BaseSchema.extend({ | ||
| type: z.literal('carousel'), | ||
| items: z.array(CarouselItemSchema).describe('Carousel items'), | ||
| autoPlay: z.number().optional().describe('Auto-play interval (ms)'), | ||
| showArrows: z.boolean().optional().describe('Show navigation arrows'), | ||
| showDots: z.boolean().optional().describe('Show navigation dots'), | ||
| loop: z.boolean().optional().describe('Enable infinite loop'), | ||
| itemsPerView: z.number().optional().describe('Items per view'), | ||
| gap: z.number().optional().describe('Gap between items'), | ||
| onSlideChange: z.function().optional().describe('Slide change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Chat Message Schema | ||
| */ | ||
| export const ChatMessageSchema = z.object({ | ||
| id: z.string().describe('Message ID'), | ||
| role: z.enum(['user', 'assistant', 'system']).describe('Message role'), | ||
| content: z.string().describe('Message content'), | ||
| timestamp: z.union([z.string(), z.date()]).optional().describe('Message timestamp'), | ||
| metadata: z.record(z.any()).optional().describe('Custom metadata'), | ||
| }); | ||
|
|
||
| /** | ||
| * Chatbot Schema - Chatbot component | ||
| */ | ||
| export const ChatbotSchema = BaseSchema.extend({ | ||
| type: z.literal('chatbot'), | ||
| messages: z.array(ChatMessageSchema).describe('Chat messages'), | ||
| placeholder: z.string().optional().describe('Input placeholder'), | ||
| loading: z.boolean().optional().describe('Whether chat is loading'), | ||
| onSendMessage: z.function().optional().describe('Send message handler'), |
There was a problem hiding this comment.
Using z.function() without type parameters for all event handlers in this file reduces type safety. Following ObjectUI's "Type Safety over Magic" principle, these should specify exact signatures. For example, onCardMove, onEventClick, onChange, onSlideChange, and onSendMessage should all have their argument and return types explicitly defined.
| 'bottom-right', | ||
| ]).optional().describe('Toast position'), | ||
| action: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Action button'), | ||
| onDismiss: z.function().optional().describe('Dismiss handler'), |
There was a problem hiding this comment.
The onDismiss handler uses z.function() without type parameters. This should use z.function(z.tuple([]), z.void()) for proper type safety.
| NavigationMenuItemSchema, | ||
| NavigationMenuSchema, | ||
| ButtonGroupButtonSchema, | ||
| ButtonGroupSchema, |
There was a problem hiding this comment.
The BreadcrumbSchema and BreadcrumbItemSchema are defined in navigation.zod.ts but are not exported from the main index.zod.ts file. This means users cannot import these schemas using import { BreadcrumbSchema } from '@object-ui/types/zod'. Add these exports to the Navigation Components section.
| ButtonGroupSchema, | |
| ButtonGroupSchema, | |
| BreadcrumbItemSchema, | |
| BreadcrumbSchema, |
| - `AlertSchema`, `BadgeSchema`, `AvatarSchema` | ||
| - `ListSchema`, `TableSchema`, `DataTableSchema` | ||
| - `MarkdownSchema`, `TreeViewSchema`, `ChartSchema` | ||
| - `TimelineSchema`, `BreadcrumbSchema` |
There was a problem hiding this comment.
The README lists BreadcrumbSchema in both the Data Display Components section (line 131) and the Navigation Components section (line 149). Based on the actual implementation in navigation.zod.ts and the TypeScript types in navigation.ts, BreadcrumbSchema should only be listed under Navigation Components. Remove it from the Data Display section.
| - `TimelineSchema`, `BreadcrumbSchema` | |
| - `TimelineSchema` |
| open: z.boolean().optional().describe('Controlled open state'), | ||
| footer: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Dialog footer'), | ||
| modal: z.boolean().optional().describe('Whether dialog is modal'), | ||
| onOpenChange: z.function().optional().describe('Open change handler'), |
There was a problem hiding this comment.
Using z.function() without type parameters allows any function signature, which provides no type safety. According to ObjectUI's principle "Type Safety over Magic", this should specify the expected function signature. For example, onClick handlers should use z.function(z.tuple([]), z.union([z.void(), z.promise(z.void())])) like ButtonSchema does (line 110 in form.zod.ts), or onOpenChange should use z.function(z.tuple([z.boolean()]), z.void()) to specify it accepts a boolean argument.
| onConfirm: z.function().optional().describe('Confirm handler'), | ||
| onCancel: z.function().optional().describe('Cancel handler'), | ||
| onOpenChange: z.function().optional().describe('Open change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Sheet Schema - Sheet/side panel component | ||
| */ | ||
| export const SheetSchema = BaseSchema.extend({ | ||
| type: z.literal('sheet'), | ||
| title: z.string().optional().describe('Sheet title'), | ||
| description: z.string().optional().describe('Sheet description'), | ||
| content: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Sheet content'), | ||
| trigger: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Sheet trigger'), | ||
| defaultOpen: z.boolean().optional().describe('Default open state'), | ||
| open: z.boolean().optional().describe('Controlled open state'), | ||
| side: z.enum(['top', 'right', 'bottom', 'left']).optional().describe('Sheet position'), | ||
| footer: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Sheet footer'), | ||
| onOpenChange: z.function().optional().describe('Open change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Drawer Schema - Drawer component | ||
| */ | ||
| export const DrawerSchema = BaseSchema.extend({ | ||
| type: z.literal('drawer'), | ||
| title: z.string().optional().describe('Drawer title'), | ||
| description: z.string().optional().describe('Drawer description'), | ||
| content: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Drawer content'), | ||
| trigger: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Drawer trigger'), | ||
| defaultOpen: z.boolean().optional().describe('Default open state'), | ||
| open: z.boolean().optional().describe('Controlled open state'), | ||
| direction: z.enum(['top', 'right', 'bottom', 'left']).optional().describe('Drawer direction'), | ||
| onOpenChange: z.function().optional().describe('Open change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Popover Schema - Popover component | ||
| */ | ||
| export const PopoverSchema = BaseSchema.extend({ | ||
| type: z.literal('popover'), | ||
| content: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).describe('Popover content'), | ||
| trigger: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).describe('Popover trigger'), | ||
| defaultOpen: z.boolean().optional().describe('Default open state'), | ||
| open: z.boolean().optional().describe('Controlled open state'), | ||
| side: z.enum(['top', 'right', 'bottom', 'left']).optional().describe('Popover side'), | ||
| align: z.enum(['start', 'center', 'end']).optional().describe('Popover alignment'), | ||
| onOpenChange: z.function().optional().describe('Open change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Tooltip Schema - Tooltip component | ||
| */ | ||
| export const TooltipSchema = BaseSchema.extend({ | ||
| type: z.literal('tooltip'), | ||
| content: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).describe('Tooltip content'), | ||
| children: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).describe('Tooltip children'), | ||
| side: z.enum(['top', 'right', 'bottom', 'left']).optional().describe('Tooltip side'), | ||
| align: z.enum(['start', 'center', 'end']).optional().describe('Tooltip alignment'), | ||
| delayDuration: z.number().optional().describe('Delay before showing (ms)'), | ||
| }); | ||
|
|
||
| /** | ||
| * Hover Card Schema - Hover card component | ||
| */ | ||
| export const HoverCardSchema = BaseSchema.extend({ | ||
| type: z.literal('hover-card'), | ||
| content: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).describe('Hover card content'), | ||
| trigger: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).describe('Hover card trigger'), | ||
| defaultOpen: z.boolean().optional().describe('Default open state'), | ||
| open: z.boolean().optional().describe('Controlled open state'), | ||
| side: z.enum(['top', 'right', 'bottom', 'left']).optional().describe('Hover card side'), | ||
| openDelay: z.number().optional().describe('Delay before opening (ms)'), | ||
| closeDelay: z.number().optional().describe('Delay before closing (ms)'), | ||
| onOpenChange: z.function().optional().describe('Open change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Menu Item Schema | ||
| */ | ||
| export const MenuItemSchema: z.ZodType<any> = z.lazy(() => | ||
| z.object({ | ||
| label: z.string().describe('Menu item label'), | ||
| icon: z.string().optional().describe('Menu item icon'), | ||
| disabled: z.boolean().optional().describe('Whether item is disabled'), | ||
| onClick: z.function().optional().describe('Click handler'), | ||
| shortcut: z.string().optional().describe('Keyboard shortcut'), | ||
| children: z.array(MenuItemSchema).optional().describe('Submenu items'), | ||
| separator: z.boolean().optional().describe('Whether this is a separator'), | ||
| }) | ||
| ); | ||
|
|
||
| /** | ||
| * Dropdown Menu Schema - Dropdown menu component | ||
| */ | ||
| export const DropdownMenuSchema = BaseSchema.extend({ | ||
| type: z.literal('dropdown-menu'), | ||
| items: z.array(MenuItemSchema).describe('Menu items'), | ||
| trigger: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).describe('Menu trigger'), | ||
| defaultOpen: z.boolean().optional().describe('Default open state'), | ||
| open: z.boolean().optional().describe('Controlled open state'), | ||
| side: z.enum(['top', 'right', 'bottom', 'left']).optional().describe('Menu side'), | ||
| align: z.enum(['start', 'center', 'end']).optional().describe('Menu alignment'), | ||
| onOpenChange: z.function().optional().describe('Open change handler'), |
There was a problem hiding this comment.
Using z.function() without type parameters violates ObjectUI's "Type Safety over Magic" principle. Most function schemas in this file (lines 51-53, 69, 84, 98, 125, 136, 154) should specify their exact signatures. For example, onOpenChange handlers should use z.function(z.tuple([z.boolean()]), z.void()) and onClick handlers should use z.function(z.tuple([]), z.union([z.void(), z.promise(z.void())])) to match the pattern in ButtonSchema.
| /** | ||
| * ObjectUI | ||
| * Copyright (c) 2024-present ObjectStack Inc. | ||
| * | ||
| * This source code is licensed under the MIT license found in the | ||
| * LICENSE file in the root directory of this source tree. | ||
| */ | ||
|
|
||
| /** | ||
| * @object-ui/types/zod - Base Schema Zod Validators | ||
| * | ||
| * Zod validation schemas for base component types. | ||
| * These schemas follow the @objectstack/spec UI specification format. | ||
| * | ||
| * @module zod/base | ||
| * @packageDocumentation | ||
| */ | ||
|
|
||
| import { z } from 'zod'; | ||
|
|
||
| /** | ||
| * Schema Node - Can be a schema object or primitive value | ||
| */ | ||
| export const SchemaNodeSchema: z.ZodType<any> = z.lazy(() => | ||
| z.union([ | ||
| BaseSchemaCore, | ||
| z.string(), | ||
| z.number(), | ||
| z.boolean(), | ||
| z.null(), | ||
| z.undefined(), | ||
| ]) | ||
| ); | ||
|
|
||
| /** | ||
| * Base Schema - Core validation schema that all components extend | ||
| * | ||
| * This is the foundation for all UI component schemas in ObjectUI. | ||
| * Following @objectstack/spec UI specification format. | ||
| */ | ||
| const BaseSchemaCore = z.object({ | ||
| /** | ||
| * Component type identifier | ||
| */ | ||
| type: z.string().describe('Component type identifier'), | ||
|
|
||
| /** | ||
| * Unique identifier for the component | ||
| */ | ||
| id: z.string().optional().describe('Unique component identifier'), | ||
|
|
||
| /** | ||
| * Human-readable name | ||
| */ | ||
| name: z.string().optional().describe('Component name'), | ||
|
|
||
| /** | ||
| * Display label | ||
| */ | ||
| label: z.string().optional().describe('Display label'), | ||
|
|
||
| /** | ||
| * Description text | ||
| */ | ||
| description: z.string().optional().describe('Description text'), | ||
|
|
||
| /** | ||
| * Placeholder text | ||
| */ | ||
| placeholder: z.string().optional().describe('Placeholder text'), | ||
|
|
||
| /** | ||
| * Tailwind CSS classes | ||
| */ | ||
| className: z.string().optional().describe('Tailwind CSS classes'), | ||
|
|
||
| /** | ||
| * Inline styles | ||
| */ | ||
| style: z.record(z.union([z.string(), z.number()])).optional().describe('Inline CSS styles'), | ||
|
|
||
| /** | ||
| * Arbitrary data | ||
| */ | ||
| data: z.any().optional().describe('Custom data payload'), | ||
|
|
||
| /** | ||
| * Child components or content | ||
| */ | ||
| body: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Child components'), | ||
|
|
||
| /** | ||
| * Alternative children property | ||
| */ | ||
| children: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Child components (React-style)'), | ||
|
|
||
| /** | ||
| * Visibility control | ||
| */ | ||
| visible: z.boolean().optional().describe('Visibility control'), | ||
|
|
||
| /** | ||
| * Conditional visibility expression | ||
| */ | ||
| visibleOn: z.string().optional().describe('Expression for conditional visibility'), | ||
|
|
||
| /** | ||
| * Hidden control | ||
| */ | ||
| hidden: z.boolean().optional().describe('Hidden control'), | ||
|
|
||
| /** | ||
| * Conditional hidden expression | ||
| */ | ||
| hiddenOn: z.string().optional().describe('Expression for conditional hiding'), | ||
|
|
||
| /** | ||
| * Disabled state | ||
| */ | ||
| disabled: z.boolean().optional().describe('Disabled state'), | ||
|
|
||
| /** | ||
| * Conditional disabled expression | ||
| */ | ||
| disabledOn: z.string().optional().describe('Expression for conditional disabling'), | ||
|
|
||
| /** | ||
| * Test ID for automated testing | ||
| */ | ||
| testId: z.string().optional().describe('Test identifier'), | ||
|
|
||
| /** | ||
| * Accessibility label | ||
| */ | ||
| ariaLabel: z.string().optional().describe('Accessibility label'), | ||
| }).passthrough(); // Allow additional properties for type-specific extensions |
There was a problem hiding this comment.
According to the ObjectUI architecture, components should follow a "Protocol Agnostic" approach and support JSON-to-Shadcn rendering. However, function handlers (onClick, onChange, etc.) cannot be serialized to JSON, which creates a fundamental conflict with the server-driven UI JSON protocol described in the coding guidelines. Consider documenting that these function properties are only for TypeScript/runtime usage and not for JSON configurations, or implement an action system using string-based action definitions as suggested in Rule #3 of the coding guidelines (events as data, not functions).
| onChange: z.function().optional().describe('Change handler'), | ||
| min: z.number().optional().describe('Minimum value (for number type)'), | ||
| max: z.number().optional().describe('Maximum value (for number type)'), | ||
| step: z.number().optional().describe('Step value (for number type)'), | ||
| maxLength: z.number().optional().describe('Maximum length'), | ||
| pattern: z.string().optional().describe('Validation pattern'), | ||
| }); | ||
|
|
||
| /** | ||
| * Textarea Schema - Multi-line text input | ||
| */ | ||
| export const TextareaSchema = BaseSchema.extend({ | ||
| type: z.literal('textarea'), | ||
| name: z.string().optional().describe('Field name for form submission'), | ||
| label: z.string().optional().describe('Textarea label'), | ||
| placeholder: z.string().optional().describe('Placeholder text'), | ||
| defaultValue: z.string().optional().describe('Default value'), | ||
| value: z.string().optional().describe('Controlled value'), | ||
| rows: z.number().optional().describe('Number of visible rows'), | ||
| required: z.boolean().optional().describe('Whether field is required'), | ||
| disabled: z.boolean().optional().describe('Whether field is disabled'), | ||
| readOnly: z.boolean().optional().describe('Whether field is read-only'), | ||
| description: z.string().optional().describe('Help text'), | ||
| error: z.string().optional().describe('Error message'), | ||
| onChange: z.function().optional().describe('Change handler'), | ||
| maxLength: z.number().optional().describe('Maximum length'), | ||
| }); | ||
|
|
||
| /** | ||
| * Select Schema - Select/dropdown component | ||
| */ | ||
| export const SelectSchema = BaseSchema.extend({ | ||
| type: z.literal('select'), | ||
| name: z.string().optional().describe('Field name for form submission'), | ||
| label: z.string().optional().describe('Select label'), | ||
| placeholder: z.string().optional().describe('Placeholder text'), | ||
| defaultValue: z.union([z.string(), z.number()]).optional().describe('Default value'), | ||
| value: z.union([z.string(), z.number()]).optional().describe('Controlled value'), | ||
| options: z.array(SelectOptionSchema).describe('Select options'), | ||
| required: z.boolean().optional().describe('Whether field is required'), | ||
| disabled: z.boolean().optional().describe('Whether field is disabled'), | ||
| description: z.string().optional().describe('Help text'), | ||
| error: z.string().optional().describe('Error message'), | ||
| onChange: z.function().optional().describe('Change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Checkbox Schema - Checkbox component | ||
| */ | ||
| export const CheckboxSchema = BaseSchema.extend({ | ||
| type: z.literal('checkbox'), | ||
| name: z.string().optional().describe('Field name for form submission'), | ||
| label: z.string().optional().describe('Checkbox label'), | ||
| defaultChecked: z.boolean().optional().describe('Default checked state'), | ||
| checked: z.boolean().optional().describe('Controlled checked state'), | ||
| disabled: z.boolean().optional().describe('Whether checkbox is disabled'), | ||
| description: z.string().optional().describe('Help text'), | ||
| error: z.string().optional().describe('Error message'), | ||
| onChange: z.function().optional().describe('Change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Radio Group Schema - Radio button group | ||
| */ | ||
| export const RadioGroupSchema = BaseSchema.extend({ | ||
| type: z.literal('radio-group'), | ||
| name: z.string().optional().describe('Field name for form submission'), | ||
| label: z.string().optional().describe('Radio group label'), | ||
| defaultValue: z.union([z.string(), z.number()]).optional().describe('Default value'), | ||
| value: z.union([z.string(), z.number()]).optional().describe('Controlled value'), | ||
| options: z.array(RadioOptionSchema).describe('Radio options'), | ||
| orientation: z.enum(['horizontal', 'vertical']).optional().describe('Layout orientation'), | ||
| disabled: z.boolean().optional().describe('Whether radio group is disabled'), | ||
| description: z.string().optional().describe('Help text'), | ||
| error: z.string().optional().describe('Error message'), | ||
| onChange: z.function().optional().describe('Change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Switch Schema - Toggle switch component | ||
| */ | ||
| export const SwitchSchema = BaseSchema.extend({ | ||
| type: z.literal('switch'), | ||
| name: z.string().optional().describe('Field name for form submission'), | ||
| label: z.string().optional().describe('Switch label'), | ||
| defaultChecked: z.boolean().optional().describe('Default checked state'), | ||
| checked: z.boolean().optional().describe('Controlled checked state'), | ||
| disabled: z.boolean().optional().describe('Whether switch is disabled'), | ||
| description: z.string().optional().describe('Help text'), | ||
| onChange: z.function().optional().describe('Change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Toggle Schema - Toggle button component | ||
| */ | ||
| export const ToggleSchema = BaseSchema.extend({ | ||
| type: z.literal('toggle'), | ||
| label: z.string().optional().describe('Toggle label'), | ||
| defaultPressed: z.boolean().optional().describe('Default pressed state'), | ||
| pressed: z.boolean().optional().describe('Controlled pressed state'), | ||
| disabled: z.boolean().optional().describe('Whether toggle is disabled'), | ||
| variant: z.enum(['default', 'outline']).optional().describe('Toggle variant'), | ||
| size: z.enum(['default', 'sm', 'lg']).optional().describe('Toggle size'), | ||
| onChange: z.function().optional().describe('Change handler'), | ||
| children: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional(), | ||
| }); | ||
|
|
||
| /** | ||
| * Slider Schema - Range slider component | ||
| */ | ||
| export const SliderSchema = BaseSchema.extend({ | ||
| type: z.literal('slider'), | ||
| name: z.string().optional().describe('Field name for form submission'), | ||
| label: z.string().optional().describe('Slider label'), | ||
| defaultValue: z.union([z.number(), z.array(z.number())]).optional().describe('Default value(s)'), | ||
| value: z.union([z.number(), z.array(z.number())]).optional().describe('Controlled value(s)'), | ||
| min: z.number().optional().describe('Minimum value'), | ||
| max: z.number().optional().describe('Maximum value'), | ||
| step: z.number().optional().describe('Step value'), | ||
| disabled: z.boolean().optional().describe('Whether slider is disabled'), | ||
| description: z.string().optional().describe('Help text'), | ||
| onChange: z.function().optional().describe('Change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * File Upload Schema - File upload component | ||
| */ | ||
| export const FileUploadSchema = BaseSchema.extend({ | ||
| type: z.literal('file-upload'), | ||
| name: z.string().optional().describe('Field name for form submission'), | ||
| label: z.string().optional().describe('Upload label'), | ||
| accept: z.string().optional().describe('Accepted file types'), | ||
| multiple: z.boolean().optional().describe('Allow multiple files'), | ||
| maxSize: z.number().optional().describe('Maximum file size (bytes)'), | ||
| maxFiles: z.number().optional().describe('Maximum number of files'), | ||
| disabled: z.boolean().optional().describe('Whether upload is disabled'), | ||
| description: z.string().optional().describe('Help text'), | ||
| error: z.string().optional().describe('Error message'), | ||
| onChange: z.function().optional().describe('Change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Date Picker Schema - Date picker component | ||
| */ | ||
| export const DatePickerSchema = BaseSchema.extend({ | ||
| type: z.literal('date-picker'), | ||
| name: z.string().optional().describe('Field name for form submission'), | ||
| label: z.string().optional().describe('Date picker label'), | ||
| placeholder: z.string().optional().describe('Placeholder text'), | ||
| defaultValue: z.union([z.string(), z.date()]).optional().describe('Default value'), | ||
| value: z.union([z.string(), z.date()]).optional().describe('Controlled value'), | ||
| minDate: z.union([z.string(), z.date()]).optional().describe('Minimum date'), | ||
| maxDate: z.union([z.string(), z.date()]).optional().describe('Maximum date'), | ||
| format: z.string().optional().describe('Date format string'), | ||
| disabled: z.boolean().optional().describe('Whether date picker is disabled'), | ||
| description: z.string().optional().describe('Help text'), | ||
| error: z.string().optional().describe('Error message'), | ||
| onChange: z.function().optional().describe('Change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Calendar Schema - Calendar component | ||
| */ | ||
| export const CalendarSchema = BaseSchema.extend({ | ||
| type: z.literal('calendar'), | ||
| defaultValue: z.union([z.string(), z.date()]).optional().describe('Default value'), | ||
| value: z.union([z.string(), z.date()]).optional().describe('Controlled value'), | ||
| mode: z.enum(['single', 'multiple', 'range']).optional().describe('Selection mode'), | ||
| minDate: z.union([z.string(), z.date()]).optional().describe('Minimum date'), | ||
| maxDate: z.union([z.string(), z.date()]).optional().describe('Maximum date'), | ||
| disabled: z.boolean().optional().describe('Whether calendar is disabled'), | ||
| onChange: z.function().optional().describe('Change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Input OTP Schema - One-time password input | ||
| */ | ||
| export const InputOTPSchema = BaseSchema.extend({ | ||
| type: z.literal('input-otp'), | ||
| name: z.string().optional().describe('Field name for form submission'), | ||
| label: z.string().optional().describe('OTP input label'), | ||
| length: z.number().optional().describe('Number of OTP digits'), | ||
| defaultValue: z.string().optional().describe('Default value'), | ||
| value: z.string().optional().describe('Controlled value'), | ||
| disabled: z.boolean().optional().describe('Whether OTP input is disabled'), | ||
| description: z.string().optional().describe('Help text'), | ||
| error: z.string().optional().describe('Error message'), | ||
| onChange: z.function().optional().describe('Change handler'), | ||
| onComplete: z.function().optional().describe('Complete handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Combobox Schema - Searchable select component | ||
| */ | ||
| export const ComboboxSchema = BaseSchema.extend({ | ||
| type: z.literal('combobox'), | ||
| name: z.string().optional().describe('Field name for form submission'), | ||
| label: z.string().optional().describe('Combobox label'), | ||
| placeholder: z.string().optional().describe('Placeholder text'), | ||
| options: z.array(ComboboxOptionSchema).describe('Combobox options'), | ||
| defaultValue: z.string().optional().describe('Default value'), | ||
| value: z.string().optional().describe('Controlled value'), | ||
| disabled: z.boolean().optional().describe('Whether combobox is disabled'), | ||
| description: z.string().optional().describe('Help text'), | ||
| error: z.string().optional().describe('Error message'), | ||
| onChange: z.function().optional().describe('Change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Label Schema - Form label component | ||
| */ | ||
| export const LabelSchema = BaseSchema.extend({ | ||
| type: z.literal('label'), | ||
| text: z.string().optional().describe('Label text'), | ||
| label: z.string().optional().describe('Label text (alternative)'), | ||
| htmlFor: z.string().optional().describe('Associated input ID'), | ||
| }); | ||
|
|
||
| /** | ||
| * Command Schema - Command palette component | ||
| */ | ||
| export const CommandSchema = BaseSchema.extend({ | ||
| type: z.literal('command'), | ||
| placeholder: z.string().optional().describe('Search placeholder'), | ||
| emptyText: z.string().optional().describe('Empty state text'), | ||
| groups: z.array(CommandGroupSchema).describe('Command groups'), | ||
| onChange: z.function().optional().describe('Change handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Form Field Schema | ||
| */ | ||
| export const FormFieldSchema = z.object({ | ||
| id: z.string().optional().describe('Field ID'), | ||
| name: z.string().describe('Field name'), | ||
| label: z.string().optional().describe('Field label'), | ||
| description: z.string().optional().describe('Field description'), | ||
| type: z.string().describe('Field type'), | ||
| inputType: z.string().optional().describe('Input type'), | ||
| required: z.boolean().optional().describe('Required flag'), | ||
| disabled: z.boolean().optional().describe('Disabled flag'), | ||
| placeholder: z.string().optional().describe('Placeholder text'), | ||
| options: z.array(SelectOptionSchema).optional().describe('Options for select/radio'), | ||
| validation: ValidationRuleSchema.optional().describe('Validation rules'), | ||
| condition: FieldConditionSchema.optional().describe('Conditional display'), | ||
| colSpan: z.number().optional().describe('Column span in grid layout'), | ||
| }); | ||
|
|
||
| /** | ||
| * Form Schema - Complete form component | ||
| */ | ||
| export const FormSchema = BaseSchema.extend({ | ||
| type: z.literal('form'), | ||
| fields: z.array(FormFieldSchema).describe('Form fields'), | ||
| defaultValues: z.record(z.any()).optional().describe('Default form values'), | ||
| submitLabel: z.string().optional().describe('Submit button label'), | ||
| cancelLabel: z.string().optional().describe('Cancel button label'), | ||
| showCancel: z.boolean().optional().describe('Show cancel button'), | ||
| layout: z.enum(['vertical', 'horizontal', 'grid']).optional().describe('Form layout'), | ||
| columns: z.number().optional().describe('Number of columns (for grid layout)'), | ||
| validationMode: z.enum(['onSubmit', 'onChange', 'onBlur']).optional().describe('Validation mode'), | ||
| resetOnSubmit: z.boolean().optional().describe('Reset form on successful submit'), | ||
| disabled: z.boolean().optional().describe('Disable entire form'), | ||
| mode: z.enum(['create', 'edit', 'view']).optional().describe('Form mode'), | ||
| actions: z.array(z.any()).optional().describe('Custom actions'), | ||
| onSubmit: z.function().optional().describe('Submit handler'), | ||
| onChange: z.function().optional().describe('Change handler'), | ||
| onCancel: z.function().optional().describe('Cancel handler'), |
There was a problem hiding this comment.
Using z.function() without type parameters in all these event handlers (onChange, onSubmit, onCancel) reduces type safety. Following the pattern in ButtonSchema (line 110), these should specify exact signatures. For example, onChange should use z.function(z.tuple([z.any()]), z.void()) and onSubmit should use z.function(z.tuple([z.any()]), z.union([z.void(), z.promise(z.void())])).
| onDismiss: z.function().optional().describe('Dismiss handler'), | ||
| children: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional(), | ||
| }); | ||
|
|
||
| /** | ||
| * Statistic Schema - Statistic display component | ||
| */ | ||
| export const StatisticSchema = BaseSchema.extend({ | ||
| type: z.literal('statistic'), | ||
| label: z.string().optional().describe('Statistic label'), | ||
| value: z.union([z.string(), z.number()]).describe('Statistic value'), | ||
| trend: z.enum(['up', 'down', 'neutral']).optional().describe('Trend indicator'), | ||
| description: z.string().optional().describe('Description text'), | ||
| icon: z.string().optional().describe('Statistic icon'), | ||
| }); | ||
|
|
||
| /** | ||
| * Badge Schema - Badge/tag component | ||
| */ | ||
| export const BadgeSchema = BaseSchema.extend({ | ||
| type: z.literal('badge'), | ||
| label: z.string().optional().describe('Badge label'), | ||
| variant: z.enum(['default', 'secondary', 'destructive', 'outline']).optional().describe('Badge variant'), | ||
| icon: z.string().optional().describe('Badge icon'), | ||
| children: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional(), | ||
| }); | ||
|
|
||
| /** | ||
| * Avatar Schema - Avatar/profile picture component | ||
| */ | ||
| export const AvatarSchema = BaseSchema.extend({ | ||
| type: z.literal('avatar'), | ||
| src: z.string().optional().describe('Image source URL'), | ||
| alt: z.string().optional().describe('Alt text'), | ||
| fallback: z.string().optional().describe('Fallback text/initials'), | ||
| size: z.enum(['sm', 'default', 'lg', 'xl']).optional().describe('Avatar size'), | ||
| shape: z.enum(['circle', 'square']).optional().describe('Avatar shape'), | ||
| }); | ||
|
|
||
| /** | ||
| * List Item Schema | ||
| */ | ||
| export const ListItemSchema = z.object({ | ||
| id: z.string().optional().describe('Item ID'), | ||
| label: z.string().optional().describe('Item label'), | ||
| description: z.string().optional().describe('Item description'), | ||
| icon: z.string().optional().describe('Item icon'), | ||
| avatar: z.string().optional().describe('Item avatar URL'), | ||
| disabled: z.boolean().optional().describe('Whether item is disabled'), | ||
| onClick: z.function().optional().describe('Click handler'), | ||
| content: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Custom content'), | ||
| }); | ||
|
|
||
| /** | ||
| * List Schema - List component | ||
| */ | ||
| export const ListSchema = BaseSchema.extend({ | ||
| type: z.literal('list'), | ||
| items: z.array(ListItemSchema).describe('List items'), | ||
| ordered: z.boolean().optional().describe('Whether list is ordered'), | ||
| dividers: z.boolean().optional().describe('Show dividers between items'), | ||
| dense: z.boolean().optional().describe('Dense spacing'), | ||
| }); | ||
|
|
||
| /** | ||
| * Table Column Schema | ||
| */ | ||
| export const TableColumnSchema = z.object({ | ||
| header: z.string().describe('Column header text'), | ||
| accessorKey: z.string().describe('Data accessor key'), | ||
| className: z.string().optional().describe('Column class name'), | ||
| cellClassName: z.string().optional().describe('Cell class name'), | ||
| width: z.union([z.string(), z.number()]).optional().describe('Column width'), | ||
| minWidth: z.union([z.string(), z.number()]).optional().describe('Minimum width'), | ||
| align: z.enum(['left', 'center', 'right']).optional().describe('Column alignment'), | ||
| fixed: z.enum(['left', 'right']).optional().describe('Fixed column position'), | ||
| type: z.string().optional().describe('Column type'), | ||
| sortable: z.boolean().optional().describe('Whether column is sortable'), | ||
| filterable: z.boolean().optional().describe('Whether column is filterable'), | ||
| resizable: z.boolean().optional().describe('Whether column is resizable'), | ||
| cell: z.function().optional().describe('Custom cell renderer'), | ||
| }); | ||
|
|
||
| /** | ||
| * Table Schema - Simple table component | ||
| */ | ||
| export const TableSchema = BaseSchema.extend({ | ||
| type: z.literal('table'), | ||
| caption: z.string().optional().describe('Table caption'), | ||
| columns: z.array(TableColumnSchema).describe('Table columns'), | ||
| data: z.array(z.any()).describe('Table data'), | ||
| footer: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Table footer'), | ||
| hoverable: z.boolean().optional().describe('Highlight rows on hover'), | ||
| striped: z.boolean().optional().describe('Striped rows'), | ||
| }); | ||
|
|
||
| /** | ||
| * Data Table Schema - Advanced data table with features | ||
| */ | ||
| export const DataTableSchema = BaseSchema.extend({ | ||
| type: z.literal('data-table'), | ||
| caption: z.string().optional().describe('Table caption'), | ||
| toolbar: z.union([SchemaNodeSchema, z.array(SchemaNodeSchema)]).optional().describe('Toolbar content'), | ||
| columns: z.array(TableColumnSchema).describe('Table columns'), | ||
| data: z.array(z.any()).describe('Table data'), | ||
| pagination: z.boolean().optional().describe('Enable pagination'), | ||
| pageSize: z.number().optional().describe('Default page size'), | ||
| searchable: z.boolean().optional().describe('Enable search'), | ||
| selectable: z.boolean().optional().describe('Enable row selection'), | ||
| sortable: z.boolean().optional().describe('Enable sorting'), | ||
| exportable: z.boolean().optional().describe('Enable data export'), | ||
| rowActions: z.array(z.any()).optional().describe('Row action buttons'), | ||
| resizableColumns: z.boolean().optional().describe('Allow column resizing'), | ||
| reorderableColumns: z.boolean().optional().describe('Allow column reordering'), | ||
| onRowEdit: z.function().optional().describe('Row edit handler'), | ||
| onRowDelete: z.function().optional().describe('Row delete handler'), | ||
| onSelectionChange: z.function().optional().describe('Selection change handler'), | ||
| onColumnsReorder: z.function().optional().describe('Column reorder handler'), | ||
| }); | ||
|
|
||
| /** | ||
| * Markdown Schema - Markdown content renderer | ||
| */ | ||
| export const MarkdownSchema = BaseSchema.extend({ | ||
| type: z.literal('markdown'), | ||
| content: z.string().describe('Markdown content'), | ||
| sanitize: z.boolean().optional().describe('Sanitize HTML'), | ||
| components: z.record(z.any()).optional().describe('Custom component overrides'), | ||
| }); | ||
|
|
||
| /** | ||
| * Tree Node Schema | ||
| */ | ||
| export const TreeNodeSchema: z.ZodType<any> = z.lazy(() => | ||
| z.object({ | ||
| id: z.string().describe('Node ID'), | ||
| label: z.string().describe('Node label'), | ||
| icon: z.string().optional().describe('Node icon'), | ||
| defaultExpanded: z.boolean().optional().describe('Default expanded state'), | ||
| selectable: z.boolean().optional().describe('Whether node is selectable'), | ||
| children: z.array(TreeNodeSchema).optional().describe('Child nodes'), | ||
| data: z.any().optional().describe('Custom node data'), | ||
| }) | ||
| ); | ||
|
|
||
| /** | ||
| * Tree View Schema - Tree/hierarchical view component | ||
| */ | ||
| export const TreeViewSchema = BaseSchema.extend({ | ||
| type: z.literal('tree-view'), | ||
| data: z.array(TreeNodeSchema).describe('Tree data'), | ||
| defaultExpandedIds: z.array(z.string()).optional().describe('Default expanded node IDs'), | ||
| defaultSelectedIds: z.array(z.string()).optional().describe('Default selected node IDs'), | ||
| expandedIds: z.array(z.string()).optional().describe('Controlled expanded node IDs'), | ||
| selectedIds: z.array(z.string()).optional().describe('Controlled selected node IDs'), | ||
| multiSelect: z.boolean().optional().describe('Allow multiple selection'), | ||
| showLines: z.boolean().optional().describe('Show connecting lines'), | ||
| onSelectChange: z.function().optional().describe('Selection change handler'), | ||
| onExpandChange: z.function().optional().describe('Expand change handler'), |
There was a problem hiding this comment.
Using z.function() without type parameters reduces type safety. All event handlers in this file (onDismiss, onClick, onRowEdit, onRowDelete, onSelectionChange, onColumnsReorder, onSelectChange, onExpandChange) should specify their exact signatures following ObjectUI's "Type Safety over Magic" principle. For example, onClick should use z.function(z.tuple([]), z.union([z.void(), z.promise(z.void())])).
Implements runtime validation schemas using Zod for all 80+ ObjectUI components, following @objectstack/spec UI specification format.
Implementation
Schema Coverage (10 files, 2.3k LOC)
Key Technical Decisions
z.lazy()for recursive schemas (TreeNode, MenuItem, NavLink) to handle circular references.passthrough()on BaseSchema to allow type-specific property extensionscurrentPageproperty from PaginationSchemaPackage Changes
zod@^3.22.4peer dependency to @object-ui/types@object-ui/types/zodpathUsage
Integration Points
zodResolverSee
packages/types/src/zod/README.mdfor complete documentation.Original prompt
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.