@object-ui/components 0.5.0 → 3.0.0
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 -25
- package/CHANGELOG.md +32 -0
- package/dist/index.css +1 -1
- package/dist/index.js +23987 -22576
- package/dist/index.umd.cjs +30 -30
- package/dist/src/custom/action-param-dialog.d.ts +21 -0
- package/dist/src/custom/index.d.ts +4 -0
- package/dist/src/custom/navigation-overlay.d.ts +50 -0
- package/dist/src/custom/view-skeleton.d.ts +37 -0
- package/dist/src/custom/view-states.d.ts +33 -0
- package/dist/src/index.d.ts +1 -0
- package/dist/src/renderers/action/action-button.d.ts +11 -0
- package/dist/src/renderers/action/action-group.d.ts +25 -0
- package/dist/src/renderers/action/action-icon.d.ts +10 -0
- package/dist/src/renderers/action/action-menu.d.ts +19 -0
- package/dist/src/renderers/action/index.d.ts +0 -0
- package/dist/src/renderers/action/resolve-icon.d.ts +6 -0
- package/package.json +20 -19
- package/src/__tests__/PageRendererRegions.test.tsx +664 -55
- 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__/compliance.test.tsx +72 -0
- package/src/__tests__/edge-cases.test.tsx +285 -0
- package/src/__tests__/navigation-overlay.test.tsx +273 -0
- package/src/__tests__/snapshot-critical.test.tsx +317 -0
- package/src/__tests__/snapshot.test.tsx +205 -0
- package/src/__tests__/view-compliance.test.tsx +153 -0
- package/src/__tests__/wcag-audit.test.tsx +493 -0
- package/src/custom/action-param-dialog.tsx +264 -0
- package/src/custom/index.ts +4 -0
- package/src/custom/navigation-overlay.tsx +296 -0
- package/src/custom/view-skeleton.tsx +243 -0
- package/src/custom/view-states.tsx +153 -0
- package/src/index.ts +1 -0
- package/src/renderers/action/action-button.tsx +147 -0
- package/src/renderers/action/action-group.tsx +270 -0
- package/src/renderers/action/action-icon.tsx +150 -0
- package/src/renderers/action/action-menu.tsx +203 -0
- package/src/renderers/action/index.ts +18 -0
- package/src/renderers/action/resolve-icon.ts +35 -0
- package/src/renderers/complex/__tests__/data-table-batch-editing.test.tsx +275 -0
- package/src/renderers/complex/__tests__/data-table-cell-renderer.test.tsx +120 -0
- package/src/renderers/complex/__tests__/data-table-editing.test.tsx +221 -0
- package/src/renderers/complex/data-table.tsx +269 -33
- 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 +33 -10
- package/src/renderers/index.ts +1 -0
- package/src/renderers/layout/aspect-ratio.tsx +1 -1
- package/src/renderers/layout/page.tsx +416 -52
- package/src/renderers/navigation/sidebar.tsx +6 -0
- package/src/renderers/placeholders.tsx +2 -2
- package/src/stories/MockedData.stories.tsx +87 -37
- 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/stories-json/accordion.stories.tsx +1 -1
- package/src/stories-json/aggrid.stories.tsx +1 -1
- package/src/stories-json/alert.stories.tsx +1 -1
- package/src/stories-json/aspect-ratio.stories.tsx +1 -1
- package/src/stories-json/avatar.stories.tsx +1 -1
- package/src/stories-json/badge.stories.tsx +1 -1
- package/src/stories-json/breadcrumb.stories.tsx +1 -1
- package/src/stories-json/button-group.stories.tsx +1 -1
- package/src/stories-json/button.stories.tsx +1 -1
- package/src/stories-json/calendar.stories.tsx +1 -1
- package/src/stories-json/card.stories.tsx +1 -1
- package/src/stories-json/carousel.stories.tsx +1 -1
- package/src/stories-json/charts.stories.tsx +1 -1
- package/src/stories-json/chatbot.stories.tsx +1 -1
- package/src/stories-json/code-editor.stories.tsx +1 -1
- package/src/stories-json/collapsible.stories.tsx +1 -1
- package/src/stories-json/controls.stories.tsx +1 -1
- package/src/stories-json/crm-live-data.stories.tsx +154 -0
- package/src/stories-json/data-table.stories.tsx +80 -4
- package/src/stories-json/data_display_extras.stories.tsx +1 -1
- package/src/stories-json/date-picker.stories.tsx +1 -1
- package/src/stories-json/detail-view.stories.tsx +1 -1
- package/src/stories-json/dialog.stories.tsx +1 -1
- package/src/stories-json/feedback_extras.stories.tsx +1 -1
- package/src/stories-json/feedback_others.stories.tsx +1 -1
- package/src/stories-json/form-variants.stories.tsx +210 -0
- package/src/stories-json/form_advanced.stories.tsx +1 -1
- package/src/stories-json/form_extras.stories.tsx +1 -1
- package/src/stories-json/grid.stories.tsx +1 -1
- package/src/stories-json/icon.stories.tsx +1 -1
- package/src/stories-json/input.stories.tsx +1 -1
- package/src/stories-json/kanban.stories.tsx +1 -1
- package/src/stories-json/layout_extended.stories.tsx +1 -1
- package/src/stories-json/layout_flex.stories.tsx +1 -1
- package/src/stories-json/list-view.stories.tsx +1 -1
- package/src/stories-json/markdown.stories.tsx +1 -1
- package/src/stories-json/menus.stories.tsx +1 -1
- package/src/stories-json/metric-card.stories.tsx +1 -1
- package/src/stories-json/navigation-menu.stories.tsx +1 -1
- package/src/stories-json/object-aggrid-advanced.stories.tsx +389 -0
- package/src/stories-json/object-aggrid.stories.tsx +1 -1
- package/src/stories-json/object-form.stories.tsx +1 -1
- package/src/stories-json/object-gantt.stories.tsx +1 -1
- package/src/stories-json/object-grid.stories.tsx +159 -1
- package/src/stories-json/object-map.stories.tsx +1 -1
- package/src/stories-json/object-view.stories.tsx +1 -1
- package/src/stories-json/overlay_extras.stories.tsx +1 -1
- package/src/stories-json/overlay_others.stories.tsx +1 -1
- package/src/stories-json/resizable.stories.tsx +1 -1
- package/src/stories-json/select.stories.tsx +1 -1
- package/src/stories-json/separator.stories.tsx +1 -1
- package/src/stories-json/statistic.stories.tsx +1 -1
- package/src/stories-json/tabs.stories.tsx +1 -1
- package/src/stories-json/timeline.stories.tsx +1 -1
- package/src/stories-json/typography.stories.tsx +1 -1
- package/src/ui/slider.tsx +6 -2
- package/src/stories/Introduction.mdx +0 -34
|
@@ -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
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -14,6 +14,7 @@ import './renderers';
|
|
|
14
14
|
// Export utils
|
|
15
15
|
export { cn } from './lib/utils';
|
|
16
16
|
export { renderChildren } from './lib/utils';
|
|
17
|
+
export { cva } from 'class-variance-authority';
|
|
17
18
|
|
|
18
19
|
// Export placeholder registration
|
|
19
20
|
export { registerPlaceholders } from './renderers/placeholders';
|
|
@@ -0,0 +1,147 @@
|
|
|
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
|
+
/**
|
|
10
|
+
* action:button — Smart action button driven by ActionSchema.
|
|
11
|
+
*
|
|
12
|
+
* Renders a Shadcn Button wired to the ActionRunner. Supports:
|
|
13
|
+
* - All 5 spec action types (script, url, modal, flow, api)
|
|
14
|
+
* - Conditional visibility & enabled state
|
|
15
|
+
* - Loading indicator during async execution
|
|
16
|
+
* - Icon rendering via Lucide
|
|
17
|
+
* - Variant / size / className overrides from schema
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import React, { forwardRef, useCallback, useState } from 'react';
|
|
21
|
+
import { ComponentRegistry } from '@object-ui/core';
|
|
22
|
+
import type { ActionSchema } from '@object-ui/types';
|
|
23
|
+
import { useAction } from '@object-ui/react';
|
|
24
|
+
import { useCondition } from '@object-ui/react';
|
|
25
|
+
import { Button } from '../../ui';
|
|
26
|
+
import { cn } from '../../lib/utils';
|
|
27
|
+
import { Loader2 } from 'lucide-react';
|
|
28
|
+
import { resolveIcon } from './resolve-icon';
|
|
29
|
+
|
|
30
|
+
export interface ActionButtonProps {
|
|
31
|
+
schema: ActionSchema & { type: string; className?: string };
|
|
32
|
+
className?: string;
|
|
33
|
+
/** Override context for this specific action */
|
|
34
|
+
context?: Record<string, any>;
|
|
35
|
+
[key: string]: any;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const ActionButtonRenderer = forwardRef<HTMLButtonElement, ActionButtonProps>(
|
|
39
|
+
({ schema, className, context: localContext, ...props }, ref) => {
|
|
40
|
+
const {
|
|
41
|
+
'data-obj-id': dataObjId,
|
|
42
|
+
'data-obj-type': dataObjType,
|
|
43
|
+
style,
|
|
44
|
+
...rest
|
|
45
|
+
} = props;
|
|
46
|
+
|
|
47
|
+
const { execute } = useAction();
|
|
48
|
+
const [loading, setLoading] = useState(false);
|
|
49
|
+
|
|
50
|
+
// Evaluate visibility and enabled conditions
|
|
51
|
+
const isVisible = useCondition(schema.visible ? `\${${schema.visible}}` : undefined);
|
|
52
|
+
const isEnabled = useCondition(schema.enabled ? `\${${schema.enabled}}` : undefined);
|
|
53
|
+
|
|
54
|
+
// Resolve icon
|
|
55
|
+
const Icon = resolveIcon(schema.icon);
|
|
56
|
+
|
|
57
|
+
// Map schema variant to Shadcn button variant
|
|
58
|
+
const variant = schema.variant === 'primary' ? 'default' : (schema.variant || 'default');
|
|
59
|
+
const size = schema.size === 'md' ? 'default' : (schema.size || 'default');
|
|
60
|
+
|
|
61
|
+
const handleClick = useCallback(async () => {
|
|
62
|
+
if (loading) return;
|
|
63
|
+
setLoading(true);
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await execute({
|
|
67
|
+
type: schema.type,
|
|
68
|
+
name: schema.name,
|
|
69
|
+
target: schema.target,
|
|
70
|
+
execute: schema.execute,
|
|
71
|
+
endpoint: schema.endpoint,
|
|
72
|
+
method: schema.method,
|
|
73
|
+
params: schema.params as Record<string, any> | undefined,
|
|
74
|
+
confirmText: schema.confirmText,
|
|
75
|
+
successMessage: schema.successMessage,
|
|
76
|
+
errorMessage: schema.errorMessage,
|
|
77
|
+
refreshAfter: schema.refreshAfter,
|
|
78
|
+
toast: schema.toast,
|
|
79
|
+
...localContext,
|
|
80
|
+
});
|
|
81
|
+
} finally {
|
|
82
|
+
setLoading(false);
|
|
83
|
+
}
|
|
84
|
+
}, [schema, execute, loading, localContext]);
|
|
85
|
+
|
|
86
|
+
if (schema.visible && !isVisible) return null;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<Button
|
|
90
|
+
ref={ref}
|
|
91
|
+
type="button"
|
|
92
|
+
variant={variant as any}
|
|
93
|
+
size={size as any}
|
|
94
|
+
className={cn(schema.className, className)}
|
|
95
|
+
disabled={(schema.enabled ? !isEnabled : false) || loading}
|
|
96
|
+
onClick={handleClick}
|
|
97
|
+
{...rest}
|
|
98
|
+
{...{ 'data-obj-id': dataObjId, 'data-obj-type': dataObjType, style }}
|
|
99
|
+
>
|
|
100
|
+
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
101
|
+
{!loading && Icon && <Icon className={cn('h-4 w-4', schema.label && 'mr-2')} />}
|
|
102
|
+
{schema.label}
|
|
103
|
+
</Button>
|
|
104
|
+
);
|
|
105
|
+
},
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
ActionButtonRenderer.displayName = 'ActionButtonRenderer';
|
|
109
|
+
|
|
110
|
+
ComponentRegistry.register('action:button', ActionButtonRenderer, {
|
|
111
|
+
namespace: 'action',
|
|
112
|
+
label: 'Action Button',
|
|
113
|
+
inputs: [
|
|
114
|
+
{ name: 'name', type: 'string', label: 'Action Name' },
|
|
115
|
+
{ name: 'label', type: 'string', label: 'Label', defaultValue: 'Action' },
|
|
116
|
+
{ name: 'icon', type: 'string', label: 'Icon' },
|
|
117
|
+
{
|
|
118
|
+
name: 'type',
|
|
119
|
+
type: 'enum',
|
|
120
|
+
label: 'Action Type',
|
|
121
|
+
enum: ['script', 'url', 'modal', 'flow', 'api'],
|
|
122
|
+
defaultValue: 'script',
|
|
123
|
+
},
|
|
124
|
+
{ name: 'target', type: 'string', label: 'Target' },
|
|
125
|
+
{
|
|
126
|
+
name: 'variant',
|
|
127
|
+
type: 'enum',
|
|
128
|
+
label: 'Variant',
|
|
129
|
+
enum: ['default', 'primary', 'secondary', 'destructive', 'outline', 'ghost'],
|
|
130
|
+
defaultValue: 'default',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
name: 'size',
|
|
134
|
+
type: 'enum',
|
|
135
|
+
label: 'Size',
|
|
136
|
+
enum: ['sm', 'md', 'lg'],
|
|
137
|
+
defaultValue: 'md',
|
|
138
|
+
},
|
|
139
|
+
{ name: 'className', type: 'string', label: 'CSS Class', advanced: true },
|
|
140
|
+
],
|
|
141
|
+
defaultProps: {
|
|
142
|
+
label: 'Action',
|
|
143
|
+
type: 'script',
|
|
144
|
+
variant: 'default',
|
|
145
|
+
size: 'md',
|
|
146
|
+
},
|
|
147
|
+
});
|