@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.
- package/.turbo/turbo-build.log +12 -12
- package/CHANGELOG.md +28 -0
- package/dist/index.css +1 -1
- package/dist/index.js +19610 -19344
- package/dist/index.umd.cjs +29 -29
- package/dist/src/custom/index.d.ts +2 -0
- package/dist/src/custom/view-skeleton.d.ts +37 -0
- package/dist/src/custom/view-states.d.ts +33 -0
- package/package.json +17 -17
- package/src/__tests__/__snapshots__/snapshot-critical.test.tsx.snap +811 -0
- package/src/__tests__/__snapshots__/snapshot.test.tsx.snap +327 -0
- package/src/__tests__/accessibility.test.tsx +137 -0
- package/src/__tests__/api-consistency.test.tsx +596 -0
- package/src/__tests__/color-contrast.test.tsx +212 -0
- package/src/__tests__/edge-cases.test.tsx +285 -0
- package/src/__tests__/snapshot-critical.test.tsx +317 -0
- package/src/__tests__/snapshot.test.tsx +205 -0
- package/src/__tests__/wcag-audit.test.tsx +493 -0
- package/src/custom/index.ts +2 -0
- package/src/custom/view-skeleton.tsx +243 -0
- package/src/custom/view-states.tsx +153 -0
- package/src/renderers/complex/data-table.tsx +28 -13
- package/src/renderers/complex/resizable.tsx +20 -17
- package/src/renderers/data-display/list.tsx +1 -1
- package/src/renderers/data-display/table.tsx +1 -1
- package/src/renderers/data-display/tree-view.tsx +2 -1
- package/src/renderers/form/form.tsx +10 -6
- package/src/renderers/layout/aspect-ratio.tsx +1 -1
- package/src/stories-json/Accessibility.mdx +297 -0
- package/src/stories-json/EdgeCases.stories.tsx +160 -0
- package/src/stories-json/GettingStarted.mdx +89 -0
- package/src/stories-json/Introduction.mdx +127 -0
- package/src/ui/slider.tsx +6 -2
- 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={
|
|
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=
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
)}
|