@object-ui/components 2.0.0 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/.turbo/turbo-build.log +12 -12
  2. package/CHANGELOG.md +28 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.js +19610 -19344
  5. package/dist/index.umd.cjs +29 -29
  6. package/dist/src/custom/index.d.ts +2 -0
  7. package/dist/src/custom/view-skeleton.d.ts +37 -0
  8. package/dist/src/custom/view-states.d.ts +33 -0
  9. package/package.json +17 -17
  10. package/src/__tests__/__snapshots__/snapshot-critical.test.tsx.snap +811 -0
  11. package/src/__tests__/__snapshots__/snapshot.test.tsx.snap +327 -0
  12. package/src/__tests__/accessibility.test.tsx +137 -0
  13. package/src/__tests__/api-consistency.test.tsx +596 -0
  14. package/src/__tests__/color-contrast.test.tsx +212 -0
  15. package/src/__tests__/edge-cases.test.tsx +285 -0
  16. package/src/__tests__/snapshot-critical.test.tsx +317 -0
  17. package/src/__tests__/snapshot.test.tsx +205 -0
  18. package/src/__tests__/wcag-audit.test.tsx +493 -0
  19. package/src/custom/index.ts +2 -0
  20. package/src/custom/view-skeleton.tsx +243 -0
  21. package/src/custom/view-states.tsx +153 -0
  22. package/src/renderers/complex/data-table.tsx +28 -13
  23. package/src/renderers/complex/resizable.tsx +20 -17
  24. package/src/renderers/data-display/list.tsx +1 -1
  25. package/src/renderers/data-display/table.tsx +1 -1
  26. package/src/renderers/data-display/tree-view.tsx +2 -1
  27. package/src/renderers/form/form.tsx +10 -6
  28. package/src/renderers/layout/aspect-ratio.tsx +1 -1
  29. package/src/stories-json/Accessibility.mdx +297 -0
  30. package/src/stories-json/EdgeCases.stories.tsx +160 -0
  31. package/src/stories-json/GettingStarted.mdx +89 -0
  32. package/src/stories-json/Introduction.mdx +127 -0
  33. package/src/ui/slider.tsx +6 -2
  34. package/src/stories/Introduction.mdx +0 -61
@@ -0,0 +1,243 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import * as React from "react"
10
+ import { cn } from "../lib/utils"
11
+ import { Skeleton } from "../ui/skeleton"
12
+
13
+ // ---------------------------------------------------------------------------
14
+ // Shared types
15
+ // ---------------------------------------------------------------------------
16
+
17
+ export interface ViewSkeletonProps extends React.ComponentProps<"div"> {
18
+ /** Number of rows/items to render */
19
+ rows?: number
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // GridSkeleton – table rows with header and column cells
24
+ // ---------------------------------------------------------------------------
25
+
26
+ export interface GridSkeletonProps extends ViewSkeletonProps {
27
+ /** Number of columns to render */
28
+ columns?: number
29
+ }
30
+
31
+ function GridSkeleton({
32
+ rows = 5,
33
+ columns = 4,
34
+ className,
35
+ ...props
36
+ }: GridSkeletonProps) {
37
+ return (
38
+ <div
39
+ data-slot="grid-skeleton"
40
+ className={cn("w-full space-y-2", className)}
41
+ {...props}
42
+ >
43
+ {/* Header row */}
44
+ <div className="flex gap-4 px-4 py-2">
45
+ {Array.from({ length: columns }).map((_, col) => (
46
+ <Skeleton key={col} className="h-4 flex-1 rounded" />
47
+ ))}
48
+ </div>
49
+
50
+ {/* Data rows */}
51
+ {Array.from({ length: rows }).map((_, row) => (
52
+ <div key={row} className="flex gap-4 rounded-md border px-4 py-3">
53
+ {Array.from({ length: columns }).map((_, col) => (
54
+ <Skeleton
55
+ key={col}
56
+ className={cn("h-4 flex-1 rounded", col === 0 && "max-w-[40%]")}
57
+ />
58
+ ))}
59
+ </div>
60
+ ))}
61
+ </div>
62
+ )
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // KanbanSkeleton – columns with placeholder cards
67
+ // ---------------------------------------------------------------------------
68
+
69
+ export interface KanbanSkeletonProps extends ViewSkeletonProps {
70
+ /** Number of kanban columns to render */
71
+ columns?: number
72
+ /** Number of cards per column */
73
+ cardsPerColumn?: number
74
+ }
75
+
76
+ function KanbanSkeleton({
77
+ columns = 3,
78
+ cardsPerColumn = 3,
79
+ className,
80
+ ...props
81
+ }: KanbanSkeletonProps) {
82
+ return (
83
+ <div
84
+ data-slot="kanban-skeleton"
85
+ className={cn("flex gap-4 overflow-x-auto", className)}
86
+ {...props}
87
+ >
88
+ {Array.from({ length: columns }).map((_, col) => (
89
+ <div
90
+ key={col}
91
+ className="flex w-72 shrink-0 flex-col gap-3 rounded-lg border bg-muted/30 p-3"
92
+ >
93
+ {/* Column header */}
94
+ <Skeleton className="h-5 w-24 rounded" />
95
+
96
+ {/* Cards */}
97
+ {Array.from({ length: cardsPerColumn }).map((_, card) => (
98
+ <div key={card} className="space-y-2 rounded-md border bg-background p-3">
99
+ <Skeleton className="h-4 w-3/4 rounded" />
100
+ <Skeleton className="h-3 w-1/2 rounded" />
101
+ </div>
102
+ ))}
103
+ </div>
104
+ ))}
105
+ </div>
106
+ )
107
+ }
108
+
109
+ // ---------------------------------------------------------------------------
110
+ // FormSkeleton – labeled form fields
111
+ // ---------------------------------------------------------------------------
112
+
113
+ function FormSkeleton({
114
+ rows = 4,
115
+ className,
116
+ ...props
117
+ }: ViewSkeletonProps) {
118
+ return (
119
+ <div
120
+ data-slot="form-skeleton"
121
+ className={cn("w-full max-w-lg space-y-6", className)}
122
+ {...props}
123
+ >
124
+ {Array.from({ length: rows }).map((_, i) => (
125
+ <div key={i} className="space-y-2">
126
+ {/* Label */}
127
+ <Skeleton className="h-4 w-28 rounded" />
128
+ {/* Input */}
129
+ <Skeleton className="h-9 w-full rounded-md" />
130
+ </div>
131
+ ))}
132
+
133
+ {/* Submit button */}
134
+ <Skeleton className="h-9 w-24 rounded-md" />
135
+ </div>
136
+ )
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // ListSkeleton – stacked list items
141
+ // ---------------------------------------------------------------------------
142
+
143
+ function ListSkeleton({
144
+ rows = 5,
145
+ className,
146
+ ...props
147
+ }: ViewSkeletonProps) {
148
+ return (
149
+ <div
150
+ data-slot="list-skeleton"
151
+ className={cn("w-full space-y-3", className)}
152
+ {...props}
153
+ >
154
+ {Array.from({ length: rows }).map((_, i) => (
155
+ <div
156
+ key={i}
157
+ className="flex items-center gap-3 rounded-md border px-4 py-3"
158
+ >
159
+ <Skeleton className="size-8 shrink-0 rounded-full" />
160
+ <div className="flex-1 space-y-2">
161
+ <Skeleton className="h-4 w-3/5 rounded" />
162
+ <Skeleton className="h-3 w-2/5 rounded" />
163
+ </div>
164
+ </div>
165
+ ))}
166
+ </div>
167
+ )
168
+ }
169
+
170
+ // ---------------------------------------------------------------------------
171
+ // ChartSkeleton – chart area placeholder
172
+ // ---------------------------------------------------------------------------
173
+
174
+ function ChartSkeleton({
175
+ className,
176
+ ...props
177
+ }: Omit<ViewSkeletonProps, "rows">) {
178
+ return (
179
+ <div
180
+ data-slot="chart-skeleton"
181
+ className={cn("w-full space-y-4", className)}
182
+ {...props}
183
+ >
184
+ {/* Chart title */}
185
+ <Skeleton className="h-5 w-40 rounded" />
186
+
187
+ {/* Chart area with bar placeholders */}
188
+ <div className="flex h-48 items-end gap-2 rounded-md border p-4">
189
+ {(["h-2/5", "h-3/5", "h-1/3", "h-4/5", "h-1/2", "h-3/4", "h-2/5"] as const).map((heightClass, i) => (
190
+ <Skeleton
191
+ key={i}
192
+ className={cn("flex-1 rounded-t", heightClass)}
193
+ />
194
+ ))}
195
+ </div>
196
+
197
+ {/* Legend */}
198
+ <div className="flex gap-4">
199
+ <Skeleton className="h-3 w-16 rounded" />
200
+ <Skeleton className="h-3 w-16 rounded" />
201
+ <Skeleton className="h-3 w-16 rounded" />
202
+ </div>
203
+ </div>
204
+ )
205
+ }
206
+
207
+ // ---------------------------------------------------------------------------
208
+ // ViewSkeleton – convenience wrapper that dispatches by variant
209
+ // ---------------------------------------------------------------------------
210
+
211
+ export type ViewSkeletonVariant = "grid" | "kanban" | "form" | "list" | "chart"
212
+
213
+ export interface ViewSkeletonDispatchProps extends ViewSkeletonProps {
214
+ variant: ViewSkeletonVariant
215
+ /** Number of columns (grid / kanban) */
216
+ columns?: number
217
+ /** Cards per column (kanban only) */
218
+ cardsPerColumn?: number
219
+ }
220
+
221
+ function ViewSkeleton({ variant, ...props }: ViewSkeletonDispatchProps) {
222
+ switch (variant) {
223
+ case "grid":
224
+ return <GridSkeleton {...props} />
225
+ case "kanban":
226
+ return <KanbanSkeleton {...props} />
227
+ case "form":
228
+ return <FormSkeleton {...props} />
229
+ case "list":
230
+ return <ListSkeleton {...props} />
231
+ case "chart":
232
+ return <ChartSkeleton {...props} />
233
+ }
234
+ }
235
+
236
+ export {
237
+ ViewSkeleton,
238
+ GridSkeleton,
239
+ KanbanSkeleton,
240
+ FormSkeleton,
241
+ ListSkeleton,
242
+ ChartSkeleton,
243
+ }
@@ -0,0 +1,153 @@
1
+ /**
2
+ * ObjectUI
3
+ * Copyright (c) 2024-present ObjectStack Inc.
4
+ *
5
+ * This source code is licensed under the MIT license found in the
6
+ * LICENSE file in the root directory of this source tree.
7
+ */
8
+
9
+ import * as React from "react"
10
+ import { Loader2, InboxIcon, AlertCircle } from "lucide-react"
11
+
12
+ import { cn } from "../lib/utils"
13
+ import { Button } from "../ui/button"
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // DataLoadingState
17
+ // ---------------------------------------------------------------------------
18
+
19
+ interface DataLoadingStateProps extends React.ComponentProps<"div"> {
20
+ /** Message displayed below the spinner */
21
+ message?: string
22
+ }
23
+
24
+ function DataLoadingState({
25
+ className,
26
+ message = "Loading…",
27
+ ...props
28
+ }: DataLoadingStateProps) {
29
+ return (
30
+ <div
31
+ role="status"
32
+ aria-label={message}
33
+ data-slot="data-loading-state"
34
+ className={cn(
35
+ "flex flex-col items-center justify-center gap-3 p-6 text-center",
36
+ className
37
+ )}
38
+ {...props}
39
+ >
40
+ <Loader2 className="size-6 animate-spin text-muted-foreground" />
41
+ {message && (
42
+ <p className="text-sm text-muted-foreground">{message}</p>
43
+ )}
44
+ </div>
45
+ )
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // DataEmptyState
50
+ // ---------------------------------------------------------------------------
51
+
52
+ interface DataEmptyStateProps extends React.ComponentProps<"div"> {
53
+ /** Icon rendered above the title */
54
+ icon?: React.ReactNode
55
+ title?: string
56
+ description?: string
57
+ /** Optional action rendered below the description */
58
+ action?: React.ReactNode
59
+ }
60
+
61
+ function DataEmptyState({
62
+ className,
63
+ icon,
64
+ title = "No data",
65
+ description,
66
+ action,
67
+ children,
68
+ ...props
69
+ }: DataEmptyStateProps) {
70
+ return (
71
+ <div
72
+ data-slot="data-empty-state"
73
+ className={cn(
74
+ "flex flex-col items-center justify-center gap-3 p-6 text-center",
75
+ className
76
+ )}
77
+ {...props}
78
+ >
79
+ <div className="flex size-10 items-center justify-center rounded-lg bg-muted">
80
+ {icon ?? <InboxIcon className="size-5 text-muted-foreground" />}
81
+ </div>
82
+ {title && (
83
+ <h3 className="text-sm font-medium">{title}</h3>
84
+ )}
85
+ {description && (
86
+ <p className="max-w-sm text-sm text-muted-foreground">{description}</p>
87
+ )}
88
+ {action}
89
+ {children}
90
+ </div>
91
+ )
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // DataErrorState
96
+ // ---------------------------------------------------------------------------
97
+
98
+ interface DataErrorStateProps extends React.ComponentProps<"div"> {
99
+ title?: string
100
+ /** Error message or description */
101
+ message?: string
102
+ /** Callback invoked when the retry button is clicked */
103
+ onRetry?: () => void
104
+ /** Label for the retry button */
105
+ retryLabel?: string
106
+ }
107
+
108
+ function DataErrorState({
109
+ className,
110
+ title = "Something went wrong",
111
+ message,
112
+ onRetry,
113
+ retryLabel = "Retry",
114
+ children,
115
+ ...props
116
+ }: DataErrorStateProps) {
117
+ return (
118
+ <div
119
+ role="alert"
120
+ data-slot="data-error-state"
121
+ className={cn(
122
+ "flex flex-col items-center justify-center gap-3 p-6 text-center",
123
+ className
124
+ )}
125
+ {...props}
126
+ >
127
+ <div className="flex size-10 items-center justify-center rounded-lg bg-destructive/10">
128
+ <AlertCircle className="size-5 text-destructive" />
129
+ </div>
130
+ {title && (
131
+ <h3 className="text-sm font-medium">{title}</h3>
132
+ )}
133
+ {message && (
134
+ <p className="max-w-sm text-sm text-muted-foreground">{message}</p>
135
+ )}
136
+ {onRetry && (
137
+ <Button variant="outline" size="sm" onClick={onRetry}>
138
+ {retryLabel}
139
+ </Button>
140
+ )}
141
+ {children}
142
+ </div>
143
+ )
144
+ }
145
+
146
+ export {
147
+ DataLoadingState,
148
+ DataEmptyState,
149
+ DataErrorState,
150
+ type DataLoadingStateProps,
151
+ type DataEmptyStateProps,
152
+ type DataErrorStateProps,
153
+ }
@@ -100,6 +100,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
100
100
  resizableColumns = true,
101
101
  reorderableColumns = true,
102
102
  editable = false,
103
+ rowClassName,
103
104
  className,
104
105
  } = schema;
105
106
 
@@ -509,13 +510,13 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
509
510
  const showToolbar = searchable || exportable || (selectable && selectedRowIds.size > 0) || hasPendingChanges;
510
511
 
511
512
  return (
512
- <div className={`flex flex-col h-full gap-4 ${className || ''}`}>
513
+ <div className={`flex flex-col h-full gap-2 sm:gap-4 ${className || ''}`}>
513
514
  {/* Toolbar */}
514
515
  {showToolbar && (
515
- <div className="flex items-center justify-between gap-4 flex-none">
516
+ <div className="flex flex-col sm:flex-row items-stretch sm:items-center justify-between gap-2 sm:gap-4 flex-none">
516
517
  <div className="flex items-center gap-2 flex-1">
517
518
  {searchable && (
518
- <div className="relative max-w-sm flex-1">
519
+ <div className="relative w-full sm:max-w-sm flex-1">
519
520
  <Search className="absolute left-2 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
520
521
  <Input
521
522
  placeholder="Search..."
@@ -530,7 +531,7 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
530
531
  )}
531
532
  </div>
532
533
 
533
- <div className="flex items-center gap-2">
534
+ <div className="flex flex-wrap items-center gap-2">
534
535
  {hasPendingChanges && (
535
536
  <>
536
537
  <div className="text-sm text-muted-foreground">
@@ -578,8 +579,8 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
578
579
  </div>
579
580
  )}
580
581
 
581
- {/* Table */}
582
- <div className="rounded-md border flex-1 min-h-0 overflow-auto relative bg-background">
582
+ {/* Table - horizontal scroll indicator via inset shadow on mobile */}
583
+ <div className="rounded-md border flex-1 min-h-0 overflow-auto relative bg-background [-webkit-overflow-scrolling:touch] shadow-[inset_-8px_0_8px_-8px_rgba(0,0,0,0.08)]">
583
584
  <Table>
584
585
  {caption && <TableCaption>{caption}</TableCaption>}
585
586
  <TableHeader className="sticky top-0 bg-background z-10 shadow-sm">
@@ -600,7 +601,15 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
600
601
  return (
601
602
  <TableHead
602
603
  key={col.accessorKey}
603
- className={`${col.className || ''} ${sortable && col.sortable !== false ? 'cursor-pointer select-none' : ''} ${isDragging ? 'opacity-50' : ''} ${isDragOver ? 'border-l-2 border-primary' : ''} relative group bg-background`}
604
+ className={cn(
605
+ col.className,
606
+ sortable && col.sortable !== false && 'cursor-pointer select-none',
607
+ isDragging && 'opacity-50',
608
+ isDragOver && 'border-l-2 border-primary',
609
+ col.align === 'right' && 'text-right',
610
+ col.align === 'center' && 'text-center',
611
+ 'relative group bg-background'
612
+ )}
604
613
  style={{
605
614
  width: columnWidth,
606
615
  minWidth: columnWidth
@@ -612,7 +621,10 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
612
621
  onDragEnd={handleColumnDragEnd}
613
622
  onClick={() => sortable && col.sortable !== false && handleSort(col.accessorKey)}
614
623
  >
615
- <div className="flex items-center justify-between">
624
+ <div className={cn(
625
+ "flex items-center",
626
+ col.align === 'right' ? 'justify-end' : 'justify-between'
627
+ )}>
616
628
  <div className="flex items-center gap-1">
617
629
  {reorderableColumns && (
618
630
  <GripVertical className="h-4 w-4 opacity-0 group-hover:opacity-50 cursor-grab active:cursor-grabbing flex-shrink-0" />
@@ -665,7 +677,8 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
665
677
  data-state={isSelected ? 'selected' : undefined}
666
678
  className={cn(
667
679
  schema.onRowClick && "cursor-pointer",
668
- rowHasChanges && "bg-amber-50 dark:bg-amber-950/20"
680
+ rowHasChanges && "bg-amber-50 dark:bg-amber-950/20",
681
+ rowClassName && rowClassName(row, rowIndex)
669
682
  )}
670
683
  onClick={(e) => {
671
684
  if (schema.onRowClick && !e.defaultPrevented) {
@@ -699,6 +712,8 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
699
712
  key={colIndex}
700
713
  className={cn(
701
714
  col.cellClassName,
715
+ col.align === 'right' && 'text-right',
716
+ col.align === 'center' && 'text-center',
702
717
  isEditable && !isEditing && "cursor-text hover:bg-muted/50",
703
718
  hasPendingChange && "font-semibold text-amber-700 dark:text-amber-400"
704
719
  )}
@@ -789,9 +804,9 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
789
804
 
790
805
  {/* Pagination */}
791
806
  {pagination && sortedData.length > 0 && (
792
- <div className="flex items-center justify-between">
807
+ <div className="flex flex-col sm:flex-row items-center justify-between gap-2">
793
808
  <div className="flex items-center gap-2">
794
- <span className="text-sm text-muted-foreground">Rows per page:</span>
809
+ <span className="text-xs sm:text-sm text-muted-foreground">Rows per page:</span>
795
810
  <Select
796
811
  value={pageSize.toString()}
797
812
  onValueChange={(value) => {
@@ -813,8 +828,8 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => {
813
828
  </div>
814
829
 
815
830
  <div className="flex items-center gap-2">
816
- <span className="text-sm text-muted-foreground">
817
- Page {currentPage} of {totalPages} ({sortedData.length} total)
831
+ <span className="text-xs sm:text-sm text-muted-foreground">
832
+ Page {currentPage} of {totalPages} <span className="hidden sm:inline">({sortedData.length} total)</span>
818
833
  </span>
819
834
  <div className="flex items-center gap-1">
820
835
  <Button
@@ -17,23 +17,26 @@ import {
17
17
  import { renderChildren } from '../../lib/utils';
18
18
 
19
19
  ComponentRegistry.register('resizable',
20
- ({ schema, className, ...props }: { schema: ResizableSchema; className?: string; [key: string]: any }) => (
21
- <ResizablePanelGroup
22
- orientation={(schema.direction || 'horizontal') as "horizontal" | "vertical"}
23
- className={className}
24
- {...props}
25
- style={{ minHeight: schema.minHeight || '200px' }}
26
- >
27
- {schema.panels?.map((panel: any, index: number) => (
28
- <React.Fragment key={index}>
29
- <ResizablePanel defaultSize={panel.defaultSize} minSize={panel.minSize} maxSize={panel.maxSize}>
30
- {renderChildren(panel.content)}
31
- </ResizablePanel>
32
- {index < schema.panels.length - 1 && <ResizableHandle withHandle={schema.withHandle} />}
33
- </React.Fragment>
34
- ))}
35
- </ResizablePanelGroup>
36
- ),
20
+ ({ schema, className, ...props }: { schema: ResizableSchema; className?: string; [key: string]: any }) => {
21
+ const panels = Array.isArray(schema.panels) ? schema.panels : [];
22
+ return (
23
+ <ResizablePanelGroup
24
+ orientation={(schema.direction || 'horizontal') as "horizontal" | "vertical"}
25
+ className={className}
26
+ {...props}
27
+ style={{ minHeight: schema.minHeight || '200px' }}
28
+ >
29
+ {panels.map((panel: any, index: number) => (
30
+ <React.Fragment key={index}>
31
+ <ResizablePanel defaultSize={panel.defaultSize} minSize={panel.minSize} maxSize={panel.maxSize}>
32
+ {renderChildren(panel.content)}
33
+ </ResizablePanel>
34
+ {index < panels.length - 1 && <ResizableHandle withHandle={schema.withHandle} />}
35
+ </React.Fragment>
36
+ ))}
37
+ </ResizablePanelGroup>
38
+ );
39
+ },
37
40
  {
38
41
  namespace: 'ui',
39
42
  label: 'Resizable Panel Group',
@@ -15,7 +15,7 @@ ComponentRegistry.register('list',
15
15
  ({ schema, className, ...props }: { schema: ListSchema; className?: string; [key: string]: any }) => {
16
16
  // Support data binding
17
17
  const boundData = useDataScope(schema.bind);
18
- const items = boundData || schema.items || [];
18
+ const items = Array.isArray(boundData) ? boundData : Array.isArray(schema.items) ? schema.items : [];
19
19
 
20
20
  // We use 'ol' or 'ul' based on ordered prop
21
21
  const ListTag = schema.ordered ? 'ol' : 'ul';
@@ -23,7 +23,7 @@ export const SimpleTableRenderer = ({ schema, className }: any) => {
23
23
  // Try to get data from binding first, then fall back to inline data
24
24
  const boundData = useDataScope(schema.bind);
25
25
  const data = boundData || schema.data || schema.props?.data || [];
26
- const columns = schema.columns || schema.props?.columns || [];
26
+ const columns = Array.isArray(schema.columns) ? schema.columns : Array.isArray(schema.props?.columns) ? schema.props.columns : [];
27
27
 
28
28
  // If we have data but it's not an array, show error.
29
29
  // If data is undefined, we might just be loading or empty.
@@ -102,7 +102,8 @@ ComponentRegistry.register('tree-view',
102
102
 
103
103
  // Support data binding
104
104
  const boundData = useDataScope(schema.bind);
105
- const nodes = boundData || schema.nodes || schema.data || [];
105
+ const rawNodes = boundData || schema.nodes || schema.data || [];
106
+ const nodes = Array.isArray(rawNodes) ? rawNodes : [];
106
107
 
107
108
  return (
108
109
  <div className={cn(
@@ -292,7 +292,7 @@ ComponentRegistry.register('form',
292
292
  render={({ field: formField }) => (
293
293
  <FormItem className={colSpanClass || undefined}>
294
294
  {label && (
295
- <FormLabel>
295
+ <FormLabel className="text-xs sm:text-sm">
296
296
  {label}
297
297
  {required && (
298
298
  <span className="text-destructive ml-1" aria-label="required">
@@ -329,13 +329,14 @@ ComponentRegistry.register('form',
329
329
 
330
330
  {/* Form Actions */}
331
331
  {(schema.showActions !== false) && (
332
- <div className={`flex gap-2 ${layout === 'horizontal' ? 'justify-end' : 'justify-start'} mt-6`}>
332
+ <div className={`flex flex-col sm:flex-row gap-2 ${layout === 'horizontal' ? 'sm:justify-end' : 'sm:justify-start'} mt-6`}>
333
333
  {showCancel && (
334
334
  <Button
335
335
  type="button"
336
336
  variant="outline"
337
337
  onClick={handleCancel}
338
338
  disabled={isSubmitting || disabled}
339
+ className="w-full sm:w-auto"
339
340
  >
340
341
  {cancelLabel}
341
342
  </Button>
@@ -344,6 +345,7 @@ ComponentRegistry.register('form',
344
345
  <Button
345
346
  type="submit"
346
347
  disabled={isSubmitting || disabled}
348
+ className="w-full sm:w-auto"
347
349
  >
348
350
  {isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
349
351
  {submitLabel}
@@ -460,12 +462,12 @@ function renderFieldComponent(type: string, props: RenderFieldProps) {
460
462
  if (inputType === 'file') {
461
463
  // File inputs cannot be controlled with value prop
462
464
  const { value, ...fileProps } = fieldProps;
463
- return <Input type="file" placeholder={placeholder} {...fileProps} />;
465
+ return <Input type="file" placeholder={placeholder} className="min-h-[44px] sm:min-h-0" {...fileProps} />;
464
466
  }
465
- return <Input type={inputType || 'text'} placeholder={placeholder} {...fieldProps} value={fieldProps.value ?? ''} />;
467
+ return <Input type={inputType || 'text'} placeholder={placeholder} className="min-h-[44px] sm:min-h-0" {...fieldProps} value={fieldProps.value ?? ''} />;
466
468
 
467
469
  case 'textarea':
468
- return <Textarea placeholder={placeholder} {...fieldProps} value={fieldProps.value ?? ''} />;
470
+ return <Textarea placeholder={placeholder} className="min-h-[44px] sm:min-h-0" {...fieldProps} value={fieldProps.value ?? ''} />;
469
471
 
470
472
  case 'checkbox': {
471
473
  // For checkbox, we need to handle the value differently
@@ -474,6 +476,7 @@ function renderFieldComponent(type: string, props: RenderFieldProps) {
474
476
  <Checkbox
475
477
  checked={value}
476
478
  onCheckedChange={onChange}
479
+ className="min-h-[44px] min-w-[44px] sm:min-h-0 sm:min-w-0"
477
480
  {...checkboxProps}
478
481
  />
479
482
  );
@@ -486,6 +489,7 @@ function renderFieldComponent(type: string, props: RenderFieldProps) {
486
489
  <Switch
487
490
  checked={value}
488
491
  onCheckedChange={onChange}
492
+ className="min-h-[44px] min-w-[44px] sm:min-h-0 sm:min-w-0"
489
493
  {...switchProps}
490
494
  />
491
495
  );
@@ -502,7 +506,7 @@ function renderFieldComponent(type: string, props: RenderFieldProps) {
502
506
 
503
507
  return (
504
508
  <Select value={selectValue} onValueChange={selectOnChange} {...selectProps}>
505
- <SelectTrigger>
509
+ <SelectTrigger className="min-h-[44px] sm:min-h-0">
506
510
  <SelectValue placeholder={placeholder ?? 'Select an option'} />
507
511
  </SelectTrigger>
508
512
  <SelectContent>
@@ -28,7 +28,7 @@ ComponentRegistry.register('aspect-ratio',
28
28
  {...{ 'data-obj-id': dataObjId, 'data-obj-type': dataObjType, style }}
29
29
  >
30
30
  {schema.image ? (
31
- <img src={schema.image} alt={schema.alt || ''} className="rounded-md object-cover w-full h-full" />
31
+ <img src={schema.image} alt={schema.alt || ''} loading="lazy" className="rounded-md object-cover w-full h-full" />
32
32
  ) : (
33
33
  renderChildren(schema.children || schema.body)
34
34
  )}